This is not so easy to do in SQL, because it depends on the order whose SQL is not suitable.
The query is rather cumbersome, so I will give it in full first, and then a breakdown showing how it develops.
SELECT @rownum: =@rownum +1 AS id, t.user_id, type, date, urls FROM (SELECT MIN(ID) AS original_id, user_id, type, date, GROUP_CONCAT(url) urls FROM (SELECT i1.*, IF(i1.type='image', IFNULL((SELECT MIN(i2.ID)-1 FROM Items i2 WHERE i2.ID>i1.ID AND (i2.type!=i1.type OR i1.user_id!=i2.user_id OR i1.date!=i2.date)), (SELECT MAX(id) FROM Items)), i1.ID) AS lastRow, IF (i1.type='image', IFNULL(SELECT MAX(i3.ID)+1 FROM Items i3 WHERE i3.ID<=i1.ID AND (i3.type!=i1.type OR i1.user_id!=i3.user_id OR i1.date!=i3.date)), (SELECT MIN(id) FROM Items)), i1.ID) AS firstRow) AS groupItems GROUP BY user_id, type, date, firstRow, lastRow) t, (SELECT @rownum:=0) r ORDER BY t.original_id;
The query uses a correlated subquery to find the start and end identifiers of each image group. A group boundary is an element that is not the same type, user, or date.
SELECT i1.ID, IF(i1.type='image', IFNULL((SELECT MIN(i2.ID)-1 FROM Items i2 WHERE i2.ID>i1.ID AND (i2.type!=i1.type OR i1.user_id!=i2.user_id OR i1.date!=i2.date)), (SELECT MAX(id) FROM Items)), i1.ID) AS lastRow, IF (i1.type='image', IFNULL(SELECT MAX(i3.ID)+1 FROM Items i3 WHERE i3.ID<=i1.ID AND (i3.type!=i1.type OR i1.user_id!=i3.user_id OR i1.date!=i3.date)), (SELECT MIN(id) FROM Items)), i1.ID) AS firstRow
For each element, the firstRow / lastRow columns provide the beginning and end of the group. Then we can use GROUP_CONCAT to concatenate all the URLs. To maintain order, MIN (id) is printed, giving the first identifier for each group.
SELECT MIN(id) AS original_id, user_id, type, date, GROUP_CONCAT(url) urls FROM (SELECT i1.*, IF(i1.type='image', IFNULL((SELECT MIN(i2.ID)-1 FROM Items i2 WHERE i2.ID>i1.ID AND (i2.type!=i1.type OR i1.user_id!=i2.user_id OR i1.date!=i2.date)), (SELECT MAX(id) FROM Items)), i1.ID) AS lastRow, IF (i1.type='image', IFNULL(SELECT MAX(i3.ID)+1 FROM Items i3 WHERE i3.ID<=i1.ID AND (i3.type!=i1.type OR i1.user_id!=i3.user_id OR i1.date!=i3.date)), (SELECT MIN(id) FROM Items)), i1.ID) AS firstRow) AS groupItems GROUP BY user_id, type, date, firstRow, lastRow
Finally, to get consecutive identifiers for the new table, use the variable to calculate the rank:
SELECT @rownum: =@rownum +1 AS id, user_id, type, date, urls FROM (SELECT MIN(ID) AS original_id, user_id, type, date, GROUP_CONCAT(url) urls FROM (SELECT i1.*, IF(i1.type='image', IFNULL((SELECT MIN(i2.ID)-1 FROM Items i2 WHERE i2.ID>i1.ID AND (i2.type!=i1.type OR i1.user_id!=i2.user_id OR i1.date!=i2.date)), (SELECT MAX(id) FROM Items)), i1.ID) AS lastRow, IF (i1.type='image', IFNULL(SELECT MAX(i3.ID)+1 FROM Items i3 WHERE i3.ID<=i1.ID AND (i3.type!=i1.type OR i1.user_id!=i3.user_id OR i1.date!=i3.date)), (SELECT MIN(id) FROM Items)), i1.ID) AS firstRow) AS groupItems GROUP BY user_id, type, date, firstRow, lastRow) t, (SELECT @rownum:=0) r ORDER BY t.original_id;
SQL is best suited for working with unordered datasets, rather than sequences, as here. If you can do this in presentation code, or perhaps better at your application level, I believe it will be faster and more flexible. A manual coded solution will find the beginning and end of each group in one pass through the data. I doubt that the SQL query will execute as efficiently as this.