feat(04-07): migrate list_items.rank to COLLATE utf8mb4_bin (LIST-03)

- Add varcharBin customType helper emitting varchar(255) COLLATE utf8mb4_bin
- Replace listItems.rank varchar with varcharBin to carry explicit binary collation
- Generate migration 0002_yielding_mattie_franklin.sql: ALTER TABLE list_items
  MODIFY COLUMN rank varchar(255) COLLATE utf8mb4_bin NOT NULL (additive, no DROP)
- Apply migration via db:migrate (never db:push, per project memory constraint)
- utf8mb4_bin ensures uppercase-prefixed ranks (e.g. Zz) sort before lowercase
  ranks (e.g. a0) in DB ORDER BY, matching JS string order — closes LIST-03 gap
This commit is contained in:
Lucas Berger
2026-06-09 14:21:42 -04:00
parent ece663d1df
commit 9b860617c5
2 changed files with 14 additions and 1 deletions
@@ -0,0 +1 @@
ALTER TABLE `list_items` MODIFY COLUMN `rank` varchar(255) COLLATE utf8mb4_bin NOT NULL;
+13 -1
View File
@@ -10,8 +10,20 @@ import {
boolean,
index,
unique,
customType,
} from 'drizzle-orm/mysql-core'
// Custom varchar type with explicit binary collation.
// Drizzle 0.45.x does not expose a first-class collation option on varchar,
// so we use customType to emit `varchar(255) COLLATE utf8mb4_bin`.
// utf8mb4_bin is required for fractional-indexing rank keys: uppercase-prefixed
// ranks (e.g. 'Zz') must sort BEFORE lowercase ranks (e.g. 'a0') — matching JS
// string order — so that drag-to-top persists across a DB ORDER BY rank.
const varcharBin = (name: string) =>
customType<{ data: string; driverData: string }>({
dataType: () => 'varchar(255) COLLATE utf8mb4_bin',
})(name)
// ── Phase 4: List tables ───────────────────────────────────────────────────
// Imported by test/setup.ts for afterEach cleanup — keep exports consistent.
@@ -228,7 +240,7 @@ export const listItems = mysqlTable(
.references(() => lists.id, { onDelete: 'cascade' }),
text: varchar('text', { length: 500 }).notNull(),
checked: boolean('checked').default(false).notNull(),
rank: varchar('rank', { length: 255 }).notNull(), // fractional-indexing string (D-13)
rank: varcharBin('rank').notNull(), // fractional-indexing string (D-13) — COLLATE utf8mb4_bin
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
},