Skip to content

Commit 8d06c8a

Browse files
committed
refactor: support composite PKs in auto-DDL generator
Add compositePrimaryKey field to TableSchema so columnDefsToDDL can emit table-level PRIMARY KEY constraints. This eliminates all special-casing for pagination_cursors: CUSTOM_DDL_TABLES, PAGINATION_CURSORS_DDL, repairPaginationCursorsTable, and the hand-written DDL in test files. Net -67 lines of code removed.
1 parent 700d764 commit 8d06c8a

File tree

3 files changed

+22
-89
lines changed

3 files changed

+22
-89
lines changed

src/lib/db/schema.ts

Lines changed: 22 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ export type ColumnDef = {
3232

3333
export type TableSchema = {
3434
columns: Record<string, ColumnDef>;
35+
/**
36+
* Composite primary key columns. When set, the DDL generator emits a
37+
* table-level `PRIMARY KEY (col1, col2, ...)` constraint instead of
38+
* per-column `PRIMARY KEY` attributes. Individual columns listed here
39+
* should NOT also set `primaryKey: true`.
40+
*/
41+
compositePrimaryKey?: string[];
3542
};
3643

3744
/**
@@ -144,6 +151,7 @@ export const TABLE_SCHEMAS: Record<string, TableSchema> = {
144151
cursor: { type: "TEXT", notNull: true },
145152
expires_at: { type: "INTEGER", notNull: true },
146153
},
154+
compositePrimaryKey: ["command_key", "context"],
147155
},
148156
metadata: {
149157
columns: {
@@ -206,7 +214,8 @@ export const TABLE_SCHEMAS: Record<string, TableSchema> = {
206214
/** Generate CREATE TABLE DDL from column definitions */
207215
function columnDefsToDDL(
208216
tableName: string,
209-
columns: [string, ColumnDef][]
217+
columns: [string, ColumnDef][],
218+
compositePrimaryKey?: string[]
210219
): string {
211220
const columnDefs = columns.map(([name, col]) => {
212221
const parts = [name, col.type];
@@ -225,6 +234,10 @@ function columnDefsToDDL(
225234
return parts.join(" ");
226235
});
227236

237+
if (compositePrimaryKey && compositePrimaryKey.length > 0) {
238+
columnDefs.push(`PRIMARY KEY (${compositePrimaryKey.join(", ")})`);
239+
}
240+
228241
return `CREATE TABLE IF NOT EXISTS ${tableName} (\n ${columnDefs.join(",\n ")}\n )`;
229242
}
230243

@@ -233,7 +246,11 @@ export function generateTableDDL(
233246
tableName: string,
234247
schema: TableSchema
235248
): string {
236-
return columnDefsToDDL(tableName, Object.entries(schema.columns));
249+
return columnDefsToDDL(
250+
tableName,
251+
Object.entries(schema.columns),
252+
schema.compositePrimaryKey
253+
);
237254
}
238255

239256
/**
@@ -258,7 +275,7 @@ export function generatePreMigrationTableDDL(tableName: string): string {
258275
);
259276
}
260277

261-
return columnDefsToDDL(tableName, baseColumns);
278+
return columnDefsToDDL(tableName, baseColumns, schema.compositePrimaryKey);
262279
}
263280

264281
/** Generated DDL statements for all tables (used for repair and init) */
@@ -365,38 +382,8 @@ export type RepairResult = {
365382
failed: string[];
366383
};
367384

368-
/**
369-
* Tables that require hand-written DDL instead of auto-generation from TABLE_SCHEMAS.
370-
*
371-
* The auto-generation via `columnDefsToDDL` only supports single-column primary keys
372-
* (via `primaryKey: true` on a column). Tables with composite primary keys (like
373-
* `pagination_cursors` with `PRIMARY KEY (command_key, context)`) need custom DDL
374-
* because SQLite requires composite PKs as a table-level constraint, not a column attribute.
375-
*/
376-
const CUSTOM_DDL_TABLES = new Set(["pagination_cursors"]);
377-
378-
function repairPaginationCursorsTable(
379-
db: Database,
380-
result: RepairResult
381-
): void {
382-
if (tableExists(db, "pagination_cursors")) {
383-
return;
384-
}
385-
try {
386-
db.exec(PAGINATION_CURSORS_DDL);
387-
result.fixed.push("Created table pagination_cursors");
388-
} catch (e) {
389-
const msg = e instanceof Error ? e.message : String(e);
390-
result.failed.push(`Failed to create table pagination_cursors: ${msg}`);
391-
}
392-
}
393-
394385
function repairMissingTables(db: Database, result: RepairResult): void {
395386
for (const [tableName, ddl] of Object.entries(EXPECTED_TABLES)) {
396-
// Skip tables that need custom DDL
397-
if (CUSTOM_DDL_TABLES.has(tableName)) {
398-
continue;
399-
}
400387
if (tableExists(db, tableName)) {
401388
continue;
402389
}
@@ -408,9 +395,6 @@ function repairMissingTables(db: Database, result: RepairResult): void {
408395
result.failed.push(`Failed to create table ${tableName}: ${msg}`);
409396
}
410397
}
411-
412-
// Handle tables with custom DDL
413-
repairPaginationCursorsTable(db, result);
414398
}
415399

416400
function repairMissingColumns(db: Database, result: RepairResult): void {
@@ -549,32 +533,10 @@ export function tryRepairAndRetry<T>(
549533
return { attempted: false };
550534
}
551535

552-
/**
553-
* Custom DDL for pagination_cursors table with composite primary key.
554-
* Uses (command_key, context) so different contexts (e.g., different orgs)
555-
* can each store their own cursor independently.
556-
*/
557-
const PAGINATION_CURSORS_DDL = `
558-
CREATE TABLE IF NOT EXISTS pagination_cursors (
559-
command_key TEXT NOT NULL,
560-
context TEXT NOT NULL,
561-
cursor TEXT NOT NULL,
562-
expires_at INTEGER NOT NULL,
563-
PRIMARY KEY (command_key, context)
564-
)
565-
`;
566-
567536
export function initSchema(db: Database): void {
568-
// Generate combined DDL from all table schemas (except those with custom DDL)
569-
const ddlStatements = Object.entries(EXPECTED_TABLES)
570-
.filter(([name]) => !CUSTOM_DDL_TABLES.has(name))
571-
.map(([, ddl]) => ddl)
572-
.join(";\n\n");
537+
const ddlStatements = Object.values(EXPECTED_TABLES).join(";\n\n");
573538
db.exec(ddlStatements);
574539

575-
// Add tables with composite primary keys
576-
db.exec(PAGINATION_CURSORS_DDL);
577-
578540
const versionRow = db
579541
.query("SELECT version FROM schema_version LIMIT 1")
580542
.get() as { version: number } | null;
@@ -632,9 +594,8 @@ export function runMigrations(db: Database): void {
632594
}
633595

634596
// Migration 4 -> 5: Add pagination_cursors table for --cursor last support
635-
// Uses custom DDL for composite primary key (command_key, context)
636597
if (currentVersion < 5) {
637-
db.exec(PAGINATION_CURSORS_DDL);
598+
db.exec(EXPECTED_TABLES.pagination_cursors as string);
638599
}
639600

640601
if (currentVersion < CURRENT_SCHEMA_VERSION) {

test/commands/cli/fix.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ import {
1717
initSchema,
1818
} from "../../../src/lib/db/schema.js";
1919

20-
/** Hand-written DDL for pagination_cursors with composite PK (matches production) */
21-
const PAGINATION_CURSORS_DDL = `CREATE TABLE IF NOT EXISTS pagination_cursors (
22-
command_key TEXT NOT NULL, context TEXT NOT NULL, cursor TEXT NOT NULL,
23-
expires_at INTEGER NOT NULL, PRIMARY KEY (command_key, context)
24-
)`;
25-
2620
/**
2721
* Generate DDL for creating a database with pre-migration tables.
2822
* This simulates a database that was created before certain migrations ran.
@@ -33,15 +27,12 @@ function createPreMigrationDatabase(db: Database): void {
3327
const statements: string[] = [];
3428

3529
for (const tableName of Object.keys(EXPECTED_TABLES)) {
36-
// pagination_cursors needs custom DDL with composite PK
37-
if (tableName === "pagination_cursors") continue;
3830
if (preMigrationTables.includes(tableName)) {
3931
statements.push(generatePreMigrationTableDDL(tableName));
4032
} else {
4133
statements.push(EXPECTED_TABLES[tableName] as string);
4234
}
4335
}
44-
statements.push(PAGINATION_CURSORS_DDL);
4536

4637
db.exec(statements.join(";\n"));
4738
db.query("INSERT INTO schema_version (version) VALUES (4)").run();
@@ -62,13 +53,8 @@ function createDatabaseWithMissingTables(
6253

6354
for (const tableName of Object.keys(EXPECTED_TABLES)) {
6455
if (missingTables.includes(tableName)) continue;
65-
// pagination_cursors needs custom DDL with composite PK
66-
if (tableName === "pagination_cursors") continue;
6756
statements.push(EXPECTED_TABLES[tableName] as string);
6857
}
69-
if (!missingTables.includes("pagination_cursors")) {
70-
statements.push(PAGINATION_CURSORS_DDL);
71-
}
7258

7359
db.exec(statements.join(";\n"));
7460
db.query("INSERT INTO schema_version (version) VALUES (4)").run();

test/lib/db/schema.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ import {
2222
tableExists,
2323
} from "../../../src/lib/db/schema.js";
2424

25-
/** Hand-written DDL for pagination_cursors with composite PK (matches production) */
26-
const PAGINATION_CURSORS_DDL = `CREATE TABLE IF NOT EXISTS pagination_cursors (
27-
command_key TEXT NOT NULL, context TEXT NOT NULL, cursor TEXT NOT NULL,
28-
expires_at INTEGER NOT NULL, PRIMARY KEY (command_key, context)
29-
)`;
30-
3125
/**
3226
* Create a database with all tables but some missing (for testing repair).
3327
*/
@@ -38,13 +32,8 @@ function createDatabaseWithMissingTables(
3832
const statements: string[] = [];
3933
for (const tableName of Object.keys(EXPECTED_TABLES)) {
4034
if (missingTables.includes(tableName)) continue;
41-
// pagination_cursors needs custom DDL with composite PK
42-
if (tableName === "pagination_cursors") continue;
4335
statements.push(EXPECTED_TABLES[tableName] as string);
4436
}
45-
if (!missingTables.includes("pagination_cursors")) {
46-
statements.push(PAGINATION_CURSORS_DDL);
47-
}
4837
db.exec(statements.join(";\n"));
4938
db.query("INSERT INTO schema_version (version) VALUES (?)").run(
5039
CURRENT_SCHEMA_VERSION
@@ -61,15 +50,12 @@ function createPreMigrationDatabase(
6150
): void {
6251
const statements: string[] = [];
6352
for (const tableName of Object.keys(EXPECTED_TABLES)) {
64-
// pagination_cursors needs custom DDL with composite PK
65-
if (tableName === "pagination_cursors") continue;
6653
if (preMigrationTables.includes(tableName)) {
6754
statements.push(generatePreMigrationTableDDL(tableName));
6855
} else {
6956
statements.push(EXPECTED_TABLES[tableName] as string);
7057
}
7158
}
72-
statements.push(PAGINATION_CURSORS_DDL);
7359
db.exec(statements.join(";\n"));
7460
db.query("INSERT INTO schema_version (version) VALUES (?)").run(
7561
CURRENT_SCHEMA_VERSION

0 commit comments

Comments
 (0)