@@ -32,6 +32,13 @@ export type ColumnDef = {
3232
3333export 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 */
207215function 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-
394385function 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
416400function 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-
567536export 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 ) {
0 commit comments