ReviewOS

also looking at this

stacks/bun-query-builder

fix(sqlite): keep the foreign key when a diff adds a column

#1155
Merged glennmichael123 wants to merge fix/1154-sqlite-add-column-references into main
3 files +209 -15
packages/bun-query-builder/src/drivers/sqlite.tsmodified+43-15
Changes to packages/bun-query-builder/src/drivers/sqlite.ts
@@ -147,6 +147,21 @@ export class SQLiteDriver implements DialectDriver {
147147 return ''
148148 }
149149
150 /**
151 * `ALTER TABLE … ADD COLUMN`, with the column's FK inline.
152 *
153 * The FK used to be dropped here: `addForeignKey` returns '' on SQLite (no
154 * `ADD CONSTRAINT`), and this rendered only the type and constraints, so a
155 * `belongsTo` added to an EXISTING table produced a plain column and no
156 * foreign key at all — silently, on a migration that reported success. A
157 * fresh CREATE TABLE inlined it correctly the whole time. See #1154.
158 *
159 * SQLite accepts `ADD COLUMN … REFERENCES …` as long as the new column's
160 * default is NULL. A non-NULL default is refused once foreign keys are on and
161 * the table has rows (`Cannot add a REFERENCES column with non-NULL default
162 * value`), so the diff engine sends that combination through `rebuildTable`
163 * instead — see `addedColumnNeedsRebuild`.
164 */
150165 addColumn(tableName: string, column: ColumnPlan): string {
151166 const typeSql = this.getColumnType(column)
152167 const parts: string[] = [this.quoteIdentifier(column.name), typeSql]
@@ -160,6 +175,8 @@ export class SQLiteDriver implements DialectDriver {
160175 parts.push(defaultValue)
161176 }
162177
178 parts.push(...this.referencesClause(column))
179
163180 return `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${parts.join(' ')};`
164181 }
165182
@@ -335,22 +352,33 @@ export class SQLiteDriver implements DialectDriver {
335352 parts.push(defaultValue)
336353 }
337354
338 // Inline FK — for SQLite this is the ONLY path that works, since
339 // SQLite doesn't support `ALTER TABLE ADD CONSTRAINT`. The
340 // orchestrator (`generateSql` / `generateDiffSql` in migrations.ts)
341 // skips its post-CREATE `addForeignKey` pass when emitting CREATE
342 // TABLE so we don't duplicate the FK on dialects that accept both
343 // forms. Enforcement still requires `PRAGMA foreign_keys = ON` on
344 // the SQLite connection (off by default — set this in the
345 // consumer's connection bootstrap).
346 if (column.references) {
347 parts.push(`REFERENCES ${this.quoteIdentifier(column.references.table)}(${this.quoteIdentifier(column.references.column)})`)
348 if (column.references.onDelete)
349 parts.push(`ON DELETE ${column.references.onDelete.toUpperCase()}`)
350 if (column.references.onUpdate)
351 parts.push(`ON UPDATE ${column.references.onUpdate.toUpperCase()}`)
352 }
355 parts.push(...this.referencesClause(column))
353356
354357 return parts.join(' ')
355358 }
359
360 /**
361 * The inline `REFERENCES` clause, or nothing when the column declares no FK.
362 *
363 * For SQLite this is the ONLY path that works, since SQLite doesn't support
364 * `ALTER TABLE ADD CONSTRAINT`. The orchestrator (`generateSql` /
365 * `generateDiffSql` in migrations.ts) skips its post-CREATE `addForeignKey`
366 * pass when emitting CREATE TABLE so we don't duplicate the FK on dialects
367 * that accept both forms. Enforcement still requires `PRAGMA foreign_keys =
368 * ON` on the SQLite connection (off by default — set this in the consumer's
369 * connection bootstrap).
370 *
371 * Shared with `addColumn`: a column added to an existing table carries its FK
372 * too, and only the CREATE TABLE path used to render it. See #1154.
373 */
374 private referencesClause(column: ColumnPlan): string[] {
375 if (!column.references)
376 return []
377 const parts = [`REFERENCES ${this.quoteIdentifier(column.references.table)}(${this.quoteIdentifier(column.references.column)})`]
378 if (column.references.onDelete)
379 parts.push(`ON DELETE ${column.references.onDelete.toUpperCase()}`)
380 if (column.references.onUpdate)
381 parts.push(`ON UPDATE ${column.references.onUpdate.toUpperCase()}`)
382 return parts
383 }
356384}