From 9b860617c5d94f5c501dde7dc8f68a81b2dbb9f7 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 14:21:42 -0400 Subject: [PATCH] feat(04-07): migrate list_items.rank to COLLATE utf8mb4_bin (LIST-03) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../migrations/0002_yielding_mattie_franklin.sql | 1 + apps/api/src/db/schema.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/db/migrations/0002_yielding_mattie_franklin.sql diff --git a/apps/api/src/db/migrations/0002_yielding_mattie_franklin.sql b/apps/api/src/db/migrations/0002_yielding_mattie_franklin.sql new file mode 100644 index 0000000..2c1aa71 --- /dev/null +++ b/apps/api/src/db/migrations/0002_yielding_mattie_franklin.sql @@ -0,0 +1 @@ +ALTER TABLE `list_items` MODIFY COLUMN `rank` varchar(255) COLLATE utf8mb4_bin NOT NULL; \ No newline at end of file diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index ea7d8d9..6ebd148 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -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(), },