ramblings on PHP, SQL, the web, politics, ultimate frisbee and what else is on in my life
back

Musings on ordered lists inside RDBMS (part II)

So I took Roland's comment and tried to get it integrated into my code. For the tab management it worked well, but for portlet management it was a lot harder. Actually I only have a partial implementation finished. What's missing is the necessary logic to be able to move a portlet in the same tab from one column to another (there are 3 columns a portlet can be in for each tab). The thing that was most important to me was cleaning up the pruning operation. This took some trickery (aka hackery). I think its a better implementation but it does make me a bit nervous. Of course its all still very MySQL only.

Anyways so here goes the final query for moving a tab:

UPDATE user_tabs ut
    JOIN user_tabs ut2
        ON (ut.user_id = ut2.user_id
            AND ut2.id = :id
            AND ut2.pos != :pos
            AND ut.pos >= LEAST(ut2.pos, :pos)
            AND ut.pos <= GREATEST(ut2.pos, :pos)
        )
    SET ut.pos = CASE
        WHEN ut.id = :id THEN :pos
        WHEN ut2.pos > :pos THEN ut.pos + 1
        WHEN ut2.pos < :pos THEN ut.pos - 1
    END
    WHERE ut.user_id = :user_id;

Like I said the pruning operation took some hackery. What I do now is essentially redoing the entire positions for the tabs and all portlets for the given user. If a portlet is marked as to be removed, I update the position to NULL. This way I can easily remove the processed removed tabs and portlets afterwards. The tricky part was in updating the positions for tabs. I update all portlets in the order of the tabs. Of course whenever I start processing the next tab, I need to reinitialize the position counters for the columns. Since I am not aware of any way to do this directly, I created a fake condition that actually just ensures that the counters are reinitialized if the tab_id has changed since the last processed row.

Also note that I have to do two separate UPDATE's, because MySQL does not support JOIN's in UPDATE's together with an ORDER BY. Since I cannot work without the ORDER BY, I had to split things in two queries. This also means that I need to SELECT the tab_id's for the current user, so that I can filter on them in the UPDATE of the portlets, where I do not have a column with the user_id.

$tab_ids = SELECT id FROM user_tabs WHERE user_id = :user_id;

SET @pos := -1;
UPDATE user_tabs
    SET pos =
    (
    CASE
        WHEN is_removed = 1 THEN NULL
        ELSE (@pos := @pos + 1)
        END
    )
    WHERE id IN ($tab_ids) ORDER BY id, pos;

SET @tab_id := 0;
UPDATE user_tab_portlets
    SET pos =
    (
    CASE

        WHEN (tab_id = @tab_id OR ((@tab_id := tab_id) > 0 AND (@poscolleft := -1) >= -1 AND (@poscolmiddle := -1) >= -1 AND (@poscolright := -1) >= -1)) THEN

            CASE
                WHEN is_removed = 1 THEN NULL
                WHEN col = :colleft THEN (@poscolleft := @poscolleft + 1)
                WHEN col = :colmiddle THEN (@poscolmiddle := @poscolmiddle + 1)
                ELSE (@poscolright := @poscolright + 1)
                END

    END
    )
    WHERE tab_id IN ($tab_ids) ORDER BY tab_id, pos;

DELETE ut, utp
    FROM user_tabs ut LEFT JOIN user_tab_portlets utp
        ON (ut.id = utp.tab_id)
    WHERE (ut.pos IS NULL OR (utp.id AND utp.pos IS NULL)) AND ut.user_id = :user_id;

Thanks again Roland for your feedback. Lets see if I find time to also rewrite the other queries.