Fractional indexing for kanban ordering#41
Open
hobbescodes wants to merge 7 commits intomasterfrom
Open
Conversation
Replaces integer column_index / index columns on task, column, project, and project_column with text-based fractional-indexing keys so reordering becomes a single-row update instead of N sibling rewrites. Three-migration sequence: - 0051: add nullable text companion columns alongside existing int cols - 0052: backfill via PL/pgSQL helper producing length-3 base-62 keys, partitioned per parent (column_id, project_id, project_column_id, organization_id), ordered by existing int + created_at + id - 0053: cutover with ALTER COLUMN SET DATA TYPE text USING <companion>, drops the companions, adds the new (project_column_id, column_index) composite index for project board ordering PostGraphile auto-derives GraphQL types from column types; columnIndex and index fields flip from Int to String on the next graphql:generate. The COLUMN_INDEX_ASC / INDEX_ASC orderBy enums keep working because lex sort on text matches base-62 ascending order.
Replaces hardcoded numeric indices in DefaultColumnsPlugin and the IDP webhook with generateNKeysBetween calls so newly seeded columns and project columns get valid fractional keys at insert time. Registers fractional-indexing in the EXPORTABLE module map so graphile-export can serialize generateNKeysBetween into the generated executable schema. Regenerates schema.graphql and schema.executable.ts: ordering fields flip from Int to String, IntFilter inputs become StringFilter, numeric aggregates (sum, avg, stddev, variance, having-int) drop off the text columns, and COLUMN_INDEX_ASC / INDEX_ASC orderBy enums survive (lex sort on text matches base-62 ascending).
Postgres default DB collation is en_US.UTF-8, which sorts uppercase Z after lowercase a (e.g. 'a0' < 'Zz'). The fractional-indexing library produces keys assuming byte-order lex comparison, where 'Zz' < 'a0'. Without this fix, dragging an item to the top of a column persists a correct key (Zz) but the server returns rows in the wrong order on refetch, so the moved item snaps back to the bottom. Switching the four ordering columns to COLLATE "C" makes ORDER BY use byte order. Postgres auto-rebuilds dependent composite indexes with the new collation. Drizzle does not track column collation in snapshots, so no schema-file change is required.
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Replaces the integer
column_index/indexcolumns ontask,column,project, andproject_columnwith text-based fractional-indexing keys, so reordering an item is a single-row update from its destination's two neighbors instead of N sibling rewrites.PostGraphile auto-derives the GraphQL types from the column types, so
columnIndex/indexfields flip fromInttoString. TheCOLUMN_INDEX_ASC/INDEX_ASCorderBy enums keep working because lex sort on text matches base-62 ascending order.Migration sequence (single PR, all three apply via
bun db:migrate):0051_cooing_vulture(auto): add nullable text companion columns alongside the existing int cols.0052_backfill_fractional_indices(custom SQL): PL/pgSQL helper produces length-3 base-62 keys (headerc), partitioned per parent (column_id,project_id,project_column_id,organization_id), ordered by existing int +created_at+id.0053_graceful_captain_midlands(custom SQL): cutover viaALTER COLUMN ... SET DATA TYPE text USING <companion>, drops the companions, adds the new(project_column_id, column_index)composite index.0054_collate_c_for_fractional_indices(custom SQL): forcesCOLLATE \"C\"on the four columns so Postgres ORDER BY uses byte order, matching the library's assumption ('Zz' < 'a0'). Without this,en_US.UTF-8collation putsZzaftera0and dragging an item to the top of a column persists a correct key but the server returns rows in the wrong order on refetch.Insert-time key generation:
DefaultColumnsPlugin: replaces hardcodedindex: 0..4withgenerateNKeysBetween(null, null, 5)so seeded columns get valid fractional keys.handleOrganizationCreated): same treatment for default project columns.generateGraphqlSchema.ts: registersfractional-indexingin theEXPORTABLEmodule map sographile-exportcan serializegenerateNKeysBetweeninto the executable schema.Coordinated deploy required: the GraphQL schema flips
Int→Stringafter this PR lands. The runa-app PR (omnidotdev/runa-app#TBD) consumes the new types. Deploy both services together; in-flight reorder mutations from older app instances will fail with type errors during the window between API restart and App restart.Test Steps
bun db:checkclean before and after each migrationbun db:migrateapplies 0051 → 0052 → 0053 → 0054 in order, regenerates GraphQL schema, all four columns aretext NOT NULLwithCOLLATE \"C\"(\\d task,\\d \"column\",\\d project,\\d project_column)SELECT proname FROM pg_proc WHERE proname = 'fi_seed_key'returns 0 rows after migration'Zz'::text COLLATE \"C\" < 'a0'::text COLLATE \"C\"returnstbun db:generatereports "No schema changes" (snapshot matches DB)bun run check && bun run build && bun testall passsrc/generated/graphql/schema.graphqlshowscolumnIndex: String!/index: String!,IntFilterbecomesStringFilter, numeric aggregates removed for the four columnsa0,a1, …)