@happyvertical/smrt-core 0.51.11 → 0.51.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/agents/schema-paths.md +23 -6
  2. package/dist/class.d.ts +1 -1
  3. package/dist/class.d.ts.map +1 -1
  4. package/dist/class.js +4 -2
  5. package/dist/class.js.map +1 -1
  6. package/dist/decorators/index.d.ts +19 -0
  7. package/dist/decorators/index.d.ts.map +1 -1
  8. package/dist/decorators/index.js.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/manifest/static-manifest.js +1 -1
  11. package/dist/manifest/static-manifest.js.map +1 -1
  12. package/dist/manifest/store.js +1 -1
  13. package/dist/manifest.json +1 -1
  14. package/dist/migrations/differ.d.ts +64 -5
  15. package/dist/migrations/differ.d.ts.map +1 -1
  16. package/dist/migrations/differ.js +274 -37
  17. package/dist/migrations/differ.js.map +1 -1
  18. package/dist/migrations/index.d.ts +1 -1
  19. package/dist/migrations/index.d.ts.map +1 -1
  20. package/dist/migrations/index.js +2 -2
  21. package/dist/migrations/sqlite-rebuild.d.ts +41 -0
  22. package/dist/migrations/sqlite-rebuild.d.ts.map +1 -1
  23. package/dist/migrations/sqlite-rebuild.js +68 -13
  24. package/dist/migrations/sqlite-rebuild.js.map +1 -1
  25. package/dist/migrations.js +2 -2
  26. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/assert-valid-pattern.js +1 -1
  27. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  28. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/ast.js +1 -1
  29. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/ast.js.map +1 -1
  30. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/brace-expressions.js +1 -1
  31. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  32. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/escape.js +1 -1
  33. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  34. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/index.js +1 -1
  35. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/index.js.map +1 -1
  36. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/unescape.js +1 -1
  37. package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  38. package/dist/registry/db-cache-key.d.ts +47 -0
  39. package/dist/registry/db-cache-key.d.ts.map +1 -0
  40. package/dist/registry/db-cache-key.js +201 -0
  41. package/dist/registry/db-cache-key.js.map +1 -0
  42. package/dist/registry/schema-builder.d.ts.map +1 -1
  43. package/dist/registry/schema-builder.js +6 -1
  44. package/dist/registry/schema-builder.js.map +1 -1
  45. package/dist/registry.d.ts +3 -0
  46. package/dist/registry.d.ts.map +1 -1
  47. package/dist/registry.js +5 -6
  48. package/dist/registry.js.map +1 -1
  49. package/dist/scanner/types.d.ts +2 -0
  50. package/dist/scanner/types.d.ts.map +1 -1
  51. package/dist/scanner/types.js.map +1 -1
  52. package/dist/schema/backfill.d.ts +16 -0
  53. package/dist/schema/backfill.d.ts.map +1 -0
  54. package/dist/schema/backfill.js +26 -0
  55. package/dist/schema/backfill.js.map +1 -0
  56. package/dist/schema/generator.d.ts.map +1 -1
  57. package/dist/schema/generator.js +3 -0
  58. package/dist/schema/generator.js.map +1 -1
  59. package/dist/schema/types.d.ts +12 -0
  60. package/dist/schema/types.d.ts.map +1 -1
  61. package/dist/smrt-knowledge.json +17 -17
  62. package/dist/vite-plugin/index.js +1 -1
  63. package/dist/vite-plugin/sync-apply-route.d.ts +7 -0
  64. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  65. package/dist/vite-plugin/sync-apply-route.js +50 -3
  66. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  67. package/package.json +23 -23
@@ -1,5 +1,10 @@
1
- import { SchemaChange, SchemaDefinition, SchemaDiff, SQLDataType } from '../schema/types.js';
1
+ import { ColumnDefinition, SchemaChange, SchemaDefinition, SchemaDiff, SQLDataType } from '../schema/types.js';
2
2
  import { DatabaseInterface } from './types.js';
3
+ /**
4
+ * `mismatch.actual` of the manual change reported when a required column
5
+ * could not be added to a populated table (#3008): the column is absent.
6
+ */
7
+ export declare const REQUIRED_COLUMN_NOT_ADDED = "(column not added)";
3
8
  /**
4
9
  * Options for schema comparison
5
10
  */
@@ -153,6 +158,13 @@ export declare class SchemaComparer {
153
158
  * re-reading `getTableSchema` per relationship would multiply catalog
154
159
  * round-trips on a wide schema.
155
160
  */
161
+ /**
162
+ * #3008: indexes planned earlier in this table's batch (DuckDB unique
163
+ * follow-ups), which a later `SET NOT NULL` must also drop and recreate.
164
+ */
165
+ private batchIndexes;
166
+ /** #3008: per-table single-column unique targets, set by compareTable. */
167
+ private uniqueTargets;
156
168
  private liveSchemas;
157
169
  /** Convergence plan for this run; `null` on non-PostgreSQL engines. */
158
170
  private uuidConvergence;
@@ -654,11 +666,15 @@ export declare class SchemaComparer {
654
666
  * - `NOT NULL` with a default: inline on SQLite/PostgreSQL (existing rows
655
667
  * receive the default); DuckDB adds with `DEFAULT` then a separate
656
668
  * `ALTER COLUMN ... SET NOT NULL`.
657
- * - `NOT NULL` without a default: enforced only when the table is empty
669
+ * - `NOT NULL` without a default: enforced inline when the table is empty
658
670
  * (inline on SQLite/PostgreSQL, `SET NOT NULL` step on DuckDB). On a
659
- * populated table there is nothing to backfill with, so the column is
660
- * added nullable and a manual `alter_column` (`set_not_null`) is reported
661
- * with the exact remediation instead of emitting DDL every engine rejects.
671
+ * populated table it needs a declared per-row `backfill` (#3008): add
672
+ * nullable, `UPDATE` from the backfill, then `SET NOT NULL` (SQLite: a
673
+ * table rebuild). Without one — or when the backfill yields NULL or does
674
+ * not evaluate — the column is NOT added and a manual `alter_column`
675
+ * (`mismatch.actual === REQUIRED_COLUMN_NOT_ADDED`) names it.
676
+ * - A backfill also wins over a declared default for existing rows; the
677
+ * default is then set after the fill, for future inserts only.
662
678
  * - `UNIQUE`: inline on PostgreSQL (a real constraint); SQLite/DuckDB get a
663
679
  * separate `CREATE UNIQUE INDEX <table>_<col>_key` (the PostgreSQL
664
680
  * constraint-index name, so the orphan sweep leaves it alone).
@@ -670,6 +686,49 @@ export declare class SchemaComparer {
670
686
  * orchestrator execute it as one unit.
671
687
  */
672
688
  private generateAddColumnChanges;
689
+ /**
690
+ * #3008: fill a column's NULLs from its declared per-row backfill, then
691
+ * enforce NOT NULL (PostgreSQL/DuckDB; SQLite goes through a table
692
+ * rebuild). {@link probeBackfill} has already checked, at plan time, that
693
+ * the expression yields a value for every row; should a row written after
694
+ * that still produce NULL, `SET NOT NULL` fails and the whole migration
695
+ * unit rolls back.
696
+ */
697
+ private backfillStatements;
698
+ /**
699
+ * #3008: evaluate a declared backfill against the live rows before any DDL
700
+ * is planned. Returns a reason the column cannot be backfilled — the
701
+ * expression yields NULL for some row, or does not evaluate at all — or
702
+ * `undefined` when every row that needs a value gets one. `existing` is
703
+ * true when the column already exists (the tighten path), so only its NULL
704
+ * rows count.
705
+ *
706
+ * Plan-time, so the refusal names the table and column in the report
707
+ * instead of surfacing later as a bare, redacted engine error.
708
+ */
709
+ private isUniqueTarget;
710
+ private probeBackfill;
711
+ /**
712
+ * #3008: the column definition a SQLite backfill rebuild appends — NOT
713
+ * NULL, plus the declared default (for future inserts) and CHECK.
714
+ */
715
+ private sqliteAddedColumnDefinition;
716
+ /**
717
+ * `ALTER COLUMN ... SET NOT NULL`, made executable on DuckDB. DuckDB
718
+ * refuses to alter a table any index depends on ("Cannot alter entry …
719
+ * because there are entries that depend on it"), so there the table's
720
+ * explicit indexes are dropped around the ALTER and recreated from their
721
+ * own DDL, inside the same migration unit.
722
+ */
723
+ private setNotNullStatements;
724
+ /**
725
+ * #3008: the manual change reported instead of an `add_column` when a
726
+ * required column without a default or backfill cannot be added to a
727
+ * populated table (or when a SQLite backfill rebuild is refused). The
728
+ * column is NOT added; `compareTable` also withholds the indexes and
729
+ * foreign keys that would target it.
730
+ */
731
+ requiredColumnNotAddedChange(tableName: string, colName: string, colDef: ColumnDefinition, reason?: string): SchemaChange;
673
732
  /**
674
733
  * Generate SQL for dropping a column
675
734
  */
@@ -1 +1 @@
1
- {"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/migrations/differ.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/migrations/differ.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,7 @@ import { columnsAllValuesUuidShapedBatch, columnsHaveNonEmptyValueBatch, nonEmpt
7
7
  import { maskSampleValue, probeCastSafety, renderJsonbColumnConversion, renderTimestamptzColumnConversion } from "../schema/text-cast-probe.js";
8
8
  import { describeForeignKeyTypeConflict, planUuidConvergence, renderUuidConvergenceRepairHint, renderUuidShapeProbe } from "../schema/uuid-convergence.js";
9
9
  import "../schema/utils.js";
10
- import { planSqliteTableRebuilds, sqliteRebuildPlaceholderSql } from "./sqlite-rebuild.js";
10
+ import { planSqliteTableRebuilds, sqliteBackfillPlaceholderSql, sqliteRebuildPlaceholderSql } from "./sqlite-rebuild.js";
11
11
  import { createLogger } from "@happyvertical/logger";
12
12
  //#region src/migrations/differ.ts
13
13
  /**
@@ -18,6 +18,11 @@ import { createLogger } from "@happyvertical/logger";
18
18
  */
19
19
  var logger = createLogger({ level: "info" });
20
20
  /**
21
+ * `mismatch.actual` of the manual change reported when a required column
22
+ * could not be added to a populated table (#3008): the column is absent.
23
+ */
24
+ var REQUIRED_COLUMN_NOT_ADDED = "(column not added)";
25
+ /**
21
26
  * Valid SQLDataType values for validation
22
27
  */
23
28
  var VALID_SQL_DATA_TYPES = /* @__PURE__ */ new Set([
@@ -313,6 +318,13 @@ var SchemaComparer = class SchemaComparer {
313
318
  * re-reading `getTableSchema` per relationship would multiply catalog
314
319
  * round-trips on a wide schema.
315
320
  */
321
+ /**
322
+ * #3008: indexes planned earlier in this table's batch (DuckDB unique
323
+ * follow-ups), which a later `SET NOT NULL` must also drop and recreate.
324
+ */
325
+ batchIndexes = /* @__PURE__ */ new Map();
326
+ /** #3008: per-table single-column unique targets, set by compareTable. */
327
+ uniqueTargets = /* @__PURE__ */ new Map();
316
328
  liveSchemas = /* @__PURE__ */ new Map();
317
329
  /** Convergence plan for this run; `null` on non-PostgreSQL engines. */
318
330
  uuidConvergence = null;
@@ -413,19 +425,70 @@ var SchemaComparer = class SchemaComparer {
413
425
  */
414
426
  async compareTable(tableName, manifest, manifestSchemas = {}) {
415
427
  const changes = [];
428
+ this.batchIndexes.delete(tableName);
429
+ this.uniqueTargets.set(tableName, new Set((manifest.indexes ?? []).filter((index) => {
430
+ if (!index.unique || index.columns.length !== 1) return false;
431
+ if (!index.where) return true;
432
+ return !this.supportsPartialIndexes() && !isStiSubtypeUniqueIndex(index);
433
+ }).map((index) => index.columns[0])));
416
434
  const dbSchema = await this.getLiveSchema(tableName);
417
435
  if (!dbSchema) return changes;
418
436
  const columnChanges = await this.compareColumns(tableName, manifest, dbSchema);
419
- changes.push(...this.engine === "sqlite" ? await planSqliteTableRebuilds({
437
+ const plannedColumnChanges = this.engine === "sqlite" ? await planSqliteTableRebuilds({
420
438
  db: this.db,
421
439
  tableName,
422
440
  changes: columnChanges,
423
- mapType: (type) => this.ddlStrategy.mapType(type)
424
- }) : columnChanges);
441
+ mapType: (type) => this.ddlStrategy.mapType(type),
442
+ addedColumnDefinition: (change) => this.sqliteAddedColumnDefinition(change),
443
+ addedColumnFollowUps: (change) => change.column?.unique && change.name ? [`CREATE UNIQUE INDEX ${this.quoteIdentifier(uniqueColumnIndexName(tableName, change.name))} ON ${this.quoteIdentifier(tableName)} (${this.quoteIdentifier(change.name)})`] : []
444
+ }) : columnChanges;
445
+ const notAdded = /* @__PURE__ */ new Set();
446
+ for (const change of plannedColumnChanges) {
447
+ if (change.type === "add_column" && change.name && change.column && !change.sqlStatements && change.sql?.trim().startsWith("--")) {
448
+ const reason = change.sql.replace(/^--\s*/, "");
449
+ changes.push(this.requiredColumnNotAddedChange(tableName, change.name, change.column, reason));
450
+ notAdded.add(change.name);
451
+ continue;
452
+ }
453
+ if (change.type === "alter_column" && change.name && change.mismatch?.actual === "(column not added)") notAdded.add(change.name);
454
+ changes.push(change);
455
+ }
425
456
  const dbIndexPredicates = await this.getDbIndexPredicates(tableName);
426
457
  const indexChanges = this.compareIndexes(tableName, manifest, dbSchema, dbIndexPredicates);
427
- changes.push(...indexChanges);
428
- changes.push(...await this.compareForeignKeys(tableName, manifest, dbSchema, manifestSchemas));
458
+ const foreignKeyChanges = await this.compareForeignKeys(tableName, manifest, dbSchema, manifestSchemas);
459
+ if (notAdded.size === 0) {
460
+ changes.push(...indexChanges, ...foreignKeyChanges);
461
+ return changes;
462
+ }
463
+ const withheld = /* @__PURE__ */ new Map();
464
+ const dependsOnMissing = (change) => {
465
+ if (change.type === "add_index") return change.index?.columns.find((column) => notAdded.has(column));
466
+ if (change.type === "add_foreign_key") {
467
+ const column = change.foreignKey?.column;
468
+ return column && notAdded.has(column) ? column : void 0;
469
+ }
470
+ };
471
+ const withheldIndexNames = new Set(indexChanges.filter((change) => dependsOnMissing(change)).map((change) => change.index?.name ?? change.name));
472
+ for (const change of [...indexChanges, ...foreignKeyChanges]) {
473
+ if (change.type === "drop_index" && withheldIndexNames.has(change.index?.name ?? change.name)) continue;
474
+ const missing = dependsOnMissing(change);
475
+ if (!missing) {
476
+ changes.push(change);
477
+ continue;
478
+ }
479
+ const label = change.type === "add_index" ? `index ${change.index?.name ?? change.name ?? "(unnamed)"}` : `foreign key on ${missing}`;
480
+ withheld.set(missing, [...withheld.get(missing) ?? [], label]);
481
+ }
482
+ for (const change of changes) {
483
+ const labels = change.name && change.mismatch?.actual === "(column not added)" ? withheld.get(change.name) : void 0;
484
+ if (!labels || !change.advisory) continue;
485
+ const note = ` Withheld until it exists: ${labels.join(", ")}.`;
486
+ change.advisory = {
487
+ ...change.advisory,
488
+ message: `${change.advisory.message}${note}`
489
+ };
490
+ change.sql = `${change.sql}${note}`;
491
+ }
429
492
  return changes;
430
493
  }
431
494
  /**
@@ -1500,7 +1563,38 @@ var SchemaComparer = class SchemaComparer {
1500
1563
  const quotedTable = this.quoteIdentifier(tableName);
1501
1564
  const quotedCol = this.quoteIdentifier(colName);
1502
1565
  const setNotNull = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET NOT NULL`;
1503
- if (!this.supportsAlterColumn()) changes.push(this.buildAlterColumnChange({
1566
+ const backfillNeeded = Boolean(colDef.backfill) && await this.columnHasNulls(tableName, colName);
1567
+ const unusableBackfill = backfillNeeded && colDef.backfill ? await this.probeBackfill(tableName, colName, colDef.backfill, true, this.isUniqueTarget(tableName, colName, colDef)) : void 0;
1568
+ if (backfillNeeded && unusableBackfill) changes.push({
1569
+ type: "alter_column",
1570
+ table: tableName,
1571
+ name: colName,
1572
+ column: colDef,
1573
+ alteration: "set_not_null",
1574
+ mismatch: {
1575
+ expected: "NOT NULL",
1576
+ actual: "NULL"
1577
+ },
1578
+ sql: `-- ${tableName}.${colName} is required but live rows hold NULL and ${unusableBackfill}`,
1579
+ advisory: {
1580
+ severity: "warning",
1581
+ message: `${tableName}.${colName} is required but live rows hold NULL and ${unusableBackfill}.`,
1582
+ suggestedSql: [`UPDATE ${quotedTable} SET ${quotedCol} = <value> WHERE ${quotedCol} IS NULL`, this.supportsAlterColumn() ? setNotNull : "-- SQLite: enforcing NOT NULL afterwards requires a table rebuild (#2370)"]
1583
+ }
1584
+ });
1585
+ else if (this.engine === "sqlite" && backfillNeeded) changes.push({
1586
+ type: "alter_column",
1587
+ table: tableName,
1588
+ name: colName,
1589
+ column: colDef,
1590
+ alteration: "set_not_null",
1591
+ mismatch: {
1592
+ expected: "NOT NULL",
1593
+ actual: "NULL"
1594
+ },
1595
+ sql: sqliteBackfillPlaceholderSql(colName)
1596
+ });
1597
+ else if (!this.supportsAlterColumn()) changes.push(this.buildAlterColumnChange({
1504
1598
  tableName,
1505
1599
  colName,
1506
1600
  colDef,
@@ -1509,6 +1603,15 @@ var SchemaComparer = class SchemaComparer {
1509
1603
  actual: "NULL",
1510
1604
  statements: null
1511
1605
  }));
1606
+ else if (backfillNeeded && colDef.backfill) changes.push(this.buildAlterColumnChange({
1607
+ tableName,
1608
+ colName,
1609
+ colDef,
1610
+ alteration: "set_not_null",
1611
+ expected: "NOT NULL",
1612
+ actual: "NULL",
1613
+ statements: await this.backfillStatements(tableName, colName, colDef.backfill)
1614
+ }));
1512
1615
  else if (hasManifestDefault && manifestDefaultSql !== void 0) changes.push(this.buildAlterColumnChange({
1513
1616
  tableName,
1514
1617
  colName,
@@ -1516,7 +1619,7 @@ var SchemaComparer = class SchemaComparer {
1516
1619
  alteration: "set_not_null",
1517
1620
  expected: "NOT NULL",
1518
1621
  actual: "NULL",
1519
- statements: [`UPDATE ${quotedTable} SET ${quotedCol} = ${manifestDefaultSql} WHERE ${quotedCol} IS NULL`, setNotNull]
1622
+ statements: [`UPDATE ${quotedTable} SET ${quotedCol} = ${manifestDefaultSql} WHERE ${quotedCol} IS NULL`, ...await this.setNotNullStatements(tableName, colName)]
1520
1623
  }));
1521
1624
  else if (await this.columnHasNulls(tableName, colName)) changes.push({
1522
1625
  type: "alter_column",
@@ -1542,7 +1645,7 @@ var SchemaComparer = class SchemaComparer {
1542
1645
  alteration: "set_not_null",
1543
1646
  expected: "NOT NULL",
1544
1647
  actual: "NULL",
1545
- statements: [setNotNull]
1648
+ statements: await this.setNotNullStatements(tableName, colName)
1546
1649
  }));
1547
1650
  } else if (!manifestNotNull && dbNotNull) changes.push(this.buildAlterColumnChange({
1548
1651
  tableName,
@@ -2101,11 +2204,15 @@ var SchemaComparer = class SchemaComparer {
2101
2204
  * - `NOT NULL` with a default: inline on SQLite/PostgreSQL (existing rows
2102
2205
  * receive the default); DuckDB adds with `DEFAULT` then a separate
2103
2206
  * `ALTER COLUMN ... SET NOT NULL`.
2104
- * - `NOT NULL` without a default: enforced only when the table is empty
2207
+ * - `NOT NULL` without a default: enforced inline when the table is empty
2105
2208
  * (inline on SQLite/PostgreSQL, `SET NOT NULL` step on DuckDB). On a
2106
- * populated table there is nothing to backfill with, so the column is
2107
- * added nullable and a manual `alter_column` (`set_not_null`) is reported
2108
- * with the exact remediation instead of emitting DDL every engine rejects.
2209
+ * populated table it needs a declared per-row `backfill` (#3008): add
2210
+ * nullable, `UPDATE` from the backfill, then `SET NOT NULL` (SQLite: a
2211
+ * table rebuild). Without one — or when the backfill yields NULL or does
2212
+ * not evaluate — the column is NOT added and a manual `alter_column`
2213
+ * (`mismatch.actual === REQUIRED_COLUMN_NOT_ADDED`) names it.
2214
+ * - A backfill also wins over a declared default for existing rows; the
2215
+ * default is then set after the fill, for future inserts only.
2109
2216
  * - `UNIQUE`: inline on PostgreSQL (a real constraint); SQLite/DuckDB get a
2110
2217
  * separate `CREATE UNIQUE INDEX <table>_<col>_key` (the PostgreSQL
2111
2218
  * constraint-index name, so the orphan sweep leaves it alone).
@@ -2127,35 +2234,44 @@ var SchemaComparer = class SchemaComparer {
2127
2234
  const hasDefault = colDef.defaultValue !== void 0 && colDef.defaultValue !== null;
2128
2235
  const formattedDefault = hasDefault ? this.ddlStrategy.formatDefaultValue(colDef.defaultValue, validatedType) : void 0;
2129
2236
  const inlineConstraintsAllowed = this.engine !== "duckdb" && this.engine !== "json";
2130
- const setNotNull = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET NOT NULL`;
2237
+ `${quotedTable}${quotedCol}`;
2131
2238
  let enforceNotNull = false;
2132
- if (colDef.notNull) if (hasDefault) enforceNotNull = true;
2133
- else if (!await this.tableHasRows(tableName)) enforceNotNull = true;
2134
- else extraChanges.push({
2135
- type: "alter_column",
2136
- table: tableName,
2137
- name: colName,
2138
- column: colDef,
2139
- alteration: "set_not_null",
2140
- mismatch: {
2141
- expected: "NOT NULL",
2142
- actual: "NULL"
2143
- },
2144
- sql: `-- ${tableName}.${colName} is required by the manifest but has no default to backfill existing rows; the column is added nullable. Backfill it, then run: ${setNotNull}`,
2145
- advisory: {
2146
- severity: "warning",
2147
- message: `${tableName}.${colName} is required by the manifest but has no default, and ${tableName} already holds rows — no engine can add it NOT NULL. It is added nullable; backfill it, then rerun db:migrate (or declare a default on the field).`,
2148
- suggestedSql: [`UPDATE ${quotedTable} SET ${quotedCol} = <value> WHERE ${quotedCol} IS NULL`, this.supportsAlterColumn() ? setNotNull : `-- SQLite: enforcing NOT NULL afterwards requires a table rebuild (#2370)`]
2149
- }
2150
- });
2239
+ const backfillFollowUps = [];
2240
+ let deferDefault = false;
2241
+ if (colDef.notNull) {
2242
+ const populated = colDef.backfill || !hasDefault ? await this.tableHasRows(tableName) : true;
2243
+ if (colDef.backfill && populated) {
2244
+ const unusable = await this.probeBackfill(tableName, colName, colDef.backfill, false, this.isUniqueTarget(tableName, colName, colDef));
2245
+ if (unusable) return [this.requiredColumnNotAddedChange(tableName, colName, colDef, unusable)];
2246
+ if (this.engine === "sqlite") return [{
2247
+ type: "add_column",
2248
+ table: tableName,
2249
+ name: colName,
2250
+ column: colDef,
2251
+ sql: sqliteBackfillPlaceholderSql(colName)
2252
+ }];
2253
+ deferDefault = formattedDefault !== void 0;
2254
+ backfillFollowUps.push(...await this.backfillStatements(tableName, colName, colDef.backfill, deferDefault ? [this.generateSetDefaultSQL(tableName, colName, formattedDefault, validatedType)] : []));
2255
+ } else if (hasDefault || !populated) enforceNotNull = true;
2256
+ else return [this.requiredColumnNotAddedChange(tableName, colName, colDef)];
2257
+ }
2151
2258
  if (enforceNotNull && inlineConstraintsAllowed) parts.push("NOT NULL");
2152
2259
  if (colDef.unique && this.engine === "postgres") parts.push("UNIQUE");
2153
- if (formattedDefault !== void 0) parts.push(`DEFAULT ${formattedDefault}`);
2260
+ if (formattedDefault !== void 0 && !deferDefault) parts.push(`DEFAULT ${formattedDefault}`);
2154
2261
  if (colDef.check) if (inlineConstraintsAllowed) parts.push(`CHECK (${colDef.check})`);
2155
2262
  else logger.warn(`[SchemaComparer] ${this.engine} rejects CHECK constraints in ADD COLUMN; ${tableName}.${colName} is added without CHECK (${colDef.check})`);
2156
2263
  const addColumn = `ALTER TABLE ${quotedTable} ADD COLUMN ${parts.join(" ")}`;
2157
- if (enforceNotNull && !inlineConstraintsAllowed) followUps.push(setNotNull);
2158
- if (colDef.unique && this.engine !== "postgres") followUps.push(`CREATE UNIQUE INDEX ${this.quoteIdentifier(uniqueColumnIndexName(tableName, colName))} ON ${quotedTable} (${quotedCol})`);
2264
+ if (enforceNotNull && !inlineConstraintsAllowed) followUps.push(...await this.setNotNullStatements(tableName, colName));
2265
+ followUps.push(...backfillFollowUps);
2266
+ if (colDef.unique && this.engine !== "postgres") {
2267
+ const indexName = uniqueColumnIndexName(tableName, colName);
2268
+ const createIndex = `CREATE UNIQUE INDEX ${this.quoteIdentifier(indexName)} ON ${quotedTable} (${quotedCol})`;
2269
+ followUps.push(createIndex);
2270
+ this.batchIndexes.set(tableName, [...this.batchIndexes.get(tableName) ?? [], {
2271
+ name: indexName,
2272
+ sql: createIndex
2273
+ }]);
2274
+ }
2159
2275
  const statements = [addColumn, ...followUps];
2160
2276
  return [{
2161
2277
  type: "add_column",
@@ -2167,6 +2283,127 @@ var SchemaComparer = class SchemaComparer {
2167
2283
  }, ...extraChanges];
2168
2284
  }
2169
2285
  /**
2286
+ * #3008: fill a column's NULLs from its declared per-row backfill, then
2287
+ * enforce NOT NULL (PostgreSQL/DuckDB; SQLite goes through a table
2288
+ * rebuild). {@link probeBackfill} has already checked, at plan time, that
2289
+ * the expression yields a value for every row; should a row written after
2290
+ * that still produce NULL, `SET NOT NULL` fails and the whole migration
2291
+ * unit rolls back.
2292
+ */
2293
+ async backfillStatements(tableName, colName, backfill, alterBeforeNotNull = []) {
2294
+ const quotedTable = this.quoteIdentifier(tableName);
2295
+ const quotedCol = this.quoteIdentifier(colName);
2296
+ const statements = [`UPDATE ${quotedTable} SET ${quotedCol} = (${backfill}) WHERE ${quotedCol} IS NULL`];
2297
+ statements.push(...await this.setNotNullStatements(tableName, colName, alterBeforeNotNull));
2298
+ return statements;
2299
+ }
2300
+ /**
2301
+ * #3008: evaluate a declared backfill against the live rows before any DDL
2302
+ * is planned. Returns a reason the column cannot be backfilled — the
2303
+ * expression yields NULL for some row, or does not evaluate at all — or
2304
+ * `undefined` when every row that needs a value gets one. `existing` is
2305
+ * true when the column already exists (the tighten path), so only its NULL
2306
+ * rows count.
2307
+ *
2308
+ * Plan-time, so the refusal names the table and column in the report
2309
+ * instead of surfacing later as a bare, redacted engine error.
2310
+ */
2311
+ isUniqueTarget(tableName, colName, colDef) {
2312
+ return Boolean(colDef.unique) || (this.uniqueTargets.get(tableName)?.has(colName) ?? false);
2313
+ }
2314
+ async probeBackfill(tableName, colName, backfill, existing, unique = false) {
2315
+ const quotedCol = this.quoteIdentifier(colName);
2316
+ const where = existing ? `${quotedCol} IS NULL AND (${backfill}) IS NULL` : `(${backfill}) IS NULL`;
2317
+ try {
2318
+ if (((await this.db.query(`SELECT 1 AS present FROM ${this.quoteIdentifier(tableName)} WHERE ${where} LIMIT 1`)).rows?.length ?? 0) > 0) return `its declared backfill (${backfill}) yields NULL for at least one existing row, so NOT NULL cannot be enforced; make the expression non-null for every row (e.g. wrap it in COALESCE)`;
2319
+ if (unique) {
2320
+ const value = existing ? `COALESCE(${quotedCol}, (${backfill}))` : `(${backfill})`;
2321
+ if (((await this.db.query(`SELECT ${value} AS v FROM ${this.quoteIdentifier(tableName)} GROUP BY ${value} HAVING COUNT(*) > 1 LIMIT 1`)).rows?.length ?? 0) > 0) return `its declared backfill (${backfill}) gives two or more existing rows the same value, but the column is unique; derive a per-row value (e.g. from id)`;
2322
+ }
2323
+ return;
2324
+ } catch (err) {
2325
+ logger.debug(`[SchemaComparer] Backfill probe failed for ${tableName}.${colName}`, { error: err instanceof Error ? err.message : String(err) });
2326
+ return `its declared backfill (${backfill}) does not evaluate on this database; check the expression's SQL and column names (snake_case)`;
2327
+ }
2328
+ }
2329
+ /**
2330
+ * #3008: the column definition a SQLite backfill rebuild appends — NOT
2331
+ * NULL, plus the declared default (for future inserts) and CHECK.
2332
+ */
2333
+ sqliteAddedColumnDefinition(change) {
2334
+ const column = change.column;
2335
+ const type = isValidSQLDataType(column.type) ? column.type : "TEXT";
2336
+ const parts = [
2337
+ this.quoteIdentifier(change.name),
2338
+ this.ddlStrategy.mapType(type),
2339
+ "NOT NULL"
2340
+ ];
2341
+ if (column.defaultValue !== void 0 && column.defaultValue !== null) parts.push(`DEFAULT ${this.ddlStrategy.formatDefaultValue(column.defaultValue, type)}`);
2342
+ if (column.check) parts.push(`CHECK (${column.check})`);
2343
+ return parts.join(" ");
2344
+ }
2345
+ /**
2346
+ * `ALTER COLUMN ... SET NOT NULL`, made executable on DuckDB. DuckDB
2347
+ * refuses to alter a table any index depends on ("Cannot alter entry …
2348
+ * because there are entries that depend on it"), so there the table's
2349
+ * explicit indexes are dropped around the ALTER and recreated from their
2350
+ * own DDL, inside the same migration unit.
2351
+ */
2352
+ async setNotNullStatements(tableName, colName, alterBefore = []) {
2353
+ const setNotNull = `ALTER TABLE ${this.quoteIdentifier(tableName)} ALTER COLUMN ${this.quoteIdentifier(colName)} SET NOT NULL`;
2354
+ if (this.engine !== "duckdb" && this.engine !== "json") return [...alterBefore, setNotNull];
2355
+ let indexes = [];
2356
+ try {
2357
+ indexes = (await this.db.query(`SELECT index_name, sql FROM duckdb_indexes() WHERE table_name = ${this.quoteLiteral(tableName)} AND sql IS NOT NULL`)).rows.flatMap((row) => typeof row.index_name === "string" && typeof row.sql === "string" ? [{
2358
+ name: row.index_name,
2359
+ sql: row.sql.replace(/;\s*$/, "")
2360
+ }] : []);
2361
+ } catch (err) {
2362
+ logger.debug(`[SchemaComparer] DuckDB index probe unavailable for ${tableName}`, { error: err instanceof Error ? err.message : String(err) });
2363
+ }
2364
+ for (const planned of this.batchIndexes.get(tableName) ?? []) if (!indexes.some((index) => index.name === planned.name)) indexes.push(planned);
2365
+ return [
2366
+ ...indexes.map((index) => `DROP INDEX ${this.quoteIdentifier(index.name)}`),
2367
+ ...alterBefore,
2368
+ setNotNull,
2369
+ ...indexes.map((index) => index.sql)
2370
+ ];
2371
+ }
2372
+ /**
2373
+ * #3008: the manual change reported instead of an `add_column` when a
2374
+ * required column without a default or backfill cannot be added to a
2375
+ * populated table (or when a SQLite backfill rebuild is refused). The
2376
+ * column is NOT added; `compareTable` also withholds the indexes and
2377
+ * foreign keys that would target it.
2378
+ */
2379
+ requiredColumnNotAddedChange(tableName, colName, colDef, reason) {
2380
+ const quotedTable = this.quoteIdentifier(tableName);
2381
+ const quotedCol = this.quoteIdentifier(colName);
2382
+ const why = reason ?? `it is required, has no default, and ${tableName} already holds rows`;
2383
+ const fix = reason ? "Fix that, then rerun db:migrate." : "Declare a per-row backfill on the field, e.g. @field({ required: true, backfill: \"'row:' || id\" }), or a default, then rerun db:migrate.";
2384
+ return {
2385
+ type: "alter_column",
2386
+ table: tableName,
2387
+ name: colName,
2388
+ column: colDef,
2389
+ alteration: "set_not_null",
2390
+ mismatch: {
2391
+ expected: "NOT NULL",
2392
+ actual: REQUIRED_COLUMN_NOT_ADDED
2393
+ },
2394
+ sql: `-- ${tableName}.${colName} was not added: ${why}. ${fix}`,
2395
+ advisory: {
2396
+ severity: "warning",
2397
+ message: `${tableName}.${colName} was not added: ${why}. ${fix}`,
2398
+ suggestedSql: [
2399
+ `ALTER TABLE ${quotedTable} ADD COLUMN ${quotedCol} ${this.ddlStrategy.mapType(isValidSQLDataType(colDef.type) ? colDef.type : "TEXT")}`,
2400
+ `UPDATE ${quotedTable} SET ${quotedCol} = <per-row value> WHERE ${quotedCol} IS NULL`,
2401
+ this.supportsAlterColumn() ? `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET NOT NULL` : "-- SQLite: enforcing NOT NULL afterwards requires a table rebuild (#2370)"
2402
+ ]
2403
+ }
2404
+ };
2405
+ }
2406
+ /**
2170
2407
  * Generate SQL for dropping a column
2171
2408
  */
2172
2409
  generateDropColumnSQL(tableName, colName) {
@@ -2257,6 +2494,6 @@ function getSQLFromChanges(changes) {
2257
2494
  return statements;
2258
2495
  }
2259
2496
  //#endregion
2260
- export { SchemaComparer, canonicalizeDefault, extractIndexPredicate, generateSchemaDiff, getSQLFromChanges, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, normalizeIndexPredicate, uniqueColumnIndexName };
2497
+ export { REQUIRED_COLUMN_NOT_ADDED, SchemaComparer, canonicalizeDefault, extractIndexPredicate, generateSchemaDiff, getSQLFromChanges, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, normalizeIndexPredicate, uniqueColumnIndexName };
2261
2498
 
2262
2499
  //# sourceMappingURL=differ.js.map