@happyvertical/smrt-core 0.51.10 → 0.51.14
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.
- package/agents/change-feed.md +82 -0
- package/agents/generators.md +3 -1
- package/agents/schema-paths.md +23 -6
- package/dist/change-feed-sensitivity.d.ts +155 -0
- package/dist/change-feed-sensitivity.d.ts.map +1 -0
- package/dist/change-feed-sensitivity.js +139 -0
- package/dist/change-feed-sensitivity.js.map +1 -0
- package/dist/change-feed.d.ts +37 -10
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +128 -12
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts.map +1 -1
- package/dist/change-signals.js +3 -0
- package/dist/change-signals.js.map +1 -1
- package/dist/class.d.ts +1 -1
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +4 -2
- package/dist/class.js.map +1 -1
- package/dist/decorators/index.d.ts +19 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +64 -5
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +274 -37
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -2
- package/dist/migrations/sqlite-rebuild.d.ts +41 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -1
- package/dist/migrations/sqlite-rebuild.js +68 -13
- package/dist/migrations/sqlite-rebuild.js.map +1 -1
- package/dist/migrations.js +2 -2
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/assert-valid-pattern.js +1 -1
- 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
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/ast.js +1 -1
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/ast.js.map +1 -1
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/brace-expressions.js +1 -1
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/escape.js +1 -1
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/escape.js.map +1 -1
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/index.js +1 -1
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/index.js.map +1 -1
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/unescape.js +1 -1
- package/dist/node_modules/.pnpm/{minimatch@10.2.5 → minimatch@10.2.6}/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
- package/dist/registry/class-registration.d.ts +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +50 -4
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/db-cache-key.d.ts +47 -0
- package/dist/registry/db-cache-key.d.ts.map +1 -0
- package/dist/registry/db-cache-key.js +201 -0
- package/dist/registry/db-cache-key.js.map +1 -0
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +6 -1
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +43 -0
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +17 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +20 -8
- package/dist/registry.js.map +1 -1
- package/dist/scanner/types.d.ts +2 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/backfill.d.ts +16 -0
- package/dist/schema/backfill.d.ts.map +1 -0
- package/dist/schema/backfill.js +26 -0
- package/dist/schema/backfill.js.map +1 -0
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +3 -0
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/types.d.ts +20 -1
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +21 -21
- package/dist/vite-plugin/dev-plane-route.js +1 -1
- package/dist/vite-plugin/dev-plane-route.js.map +1 -1
- package/dist/vite-plugin/index.js +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +54 -7
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts +7 -0
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +50 -3
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- 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
|
|
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
|
|
660
|
-
*
|
|
661
|
-
*
|
|
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":"AAAA;;;;;GAKG;AAiCH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/migrations/differ.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiCH,OAAO,KAAK,EAEV,gBAAgB,EAEhB,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACV,WAAW,EACZ,MAAM,oBAAoB,CAAC;AAoB5B,OAAO,KAAK,EAAE,iBAAiB,EAAsB,MAAM,YAAY,CAAC;AAIxE;;;GAGG;AACH,eAAO,MAAM,yBAAyB,uBAAuB,CAAC;AA4B9D;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;;;;;OAYG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,iDAAiD;IACjD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACxD;AAqBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,MAAM,CAER;AAgCD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAIlE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAQtE;AASD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAsCrE;AAiBD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,UAAU,EAAE,WAAW,GACtB;IACD,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;IACnE,GAAG,EAAE,MAAM,CAAC;CACb,CAyFA;AAED;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,WAAW,CAAoC;IACvD;;;;;OAKG;IACH;;;OAGG;IACH,OAAO,CAAC,YAAY,CAGhB;IACJ,0EAA0E;IAC1E,OAAO,CAAC,aAAa,CAAkC;IAEvD,OAAO,CAAC,WAAW,CAGf;IACJ,uEAAuE;IACvE,OAAO,CAAC,eAAe,CAAoC;IAE3D;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,sBAAsB,CAA4C;IAE1E;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,6BAA6B,CAAO;gBAEhD,EAAE,EAAE,iBAAiB,EAAE,OAAO,GAAE,WAAgB;IAsB5D;;OAEG;IACG,OAAO,CACX,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAChD,OAAO,CAAC,UAAU,CAAC;IAqGtB;;OAEG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,gBAAgB,EAC1B,eAAe,GAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAM,GACrD,OAAO,CAAC,YAAY,EAAE,CAAC;IAkL1B;;;;OAIG;YACW,aAAa;IAW3B;;;;;;OAMG;YACW,wBAAwB;IA+FtC;;;;OAIG;YACW,cAAc;IAsC5B;;;;;;;;OAQG;YACW,kBAAkB;IAQhC;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAgD9B;;;;OAIG;YACW,2BAA2B;YAqC3B,kBAAkB;YAuPlB,oBAAoB;YA2BpB,0BAA0B;IAwDxC;;;;;;;;;;;;;;OAcG;YACW,cAAc;IAub5B;;;;;;;OAOG;YACW,uBAAuB;IAcrC;;;;;;;;;;;;;;;;;;OAkBG;YACW,kCAAkC;IA8NhD;;;;;;;;;;;;;;;;;;OAkBG;YACW,2BAA2B;IA+OzC;;;;;;;;;;;;;;OAcG;YACW,uCAAuC;IAarD;;;;OAIG;YACW,yCAAyC;IAmBvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;YACW,oBAAoB;IAmGlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACH,OAAO,CAAC,yBAAyB;IAyFjC;;;;;;;;OAQG;IACH,OAAO,CAAC,kCAAkC;IA6B1C;;;;;;;;;;;;;;;;;;;;;OAqBG;YACW,wBAAwB;IAwPtC;;;;;;;;OAQG;IACH,OAAO,CAAC,sBAAsB;IAgE9B;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IA2D5B;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,gBAAgB;IAWxB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,qBAAqB;IAe7B;;;;;OAKG;YACW,YAAY;IAe1B;;;;OAIG;YACW,cAAc;IAkB5B,OAAO,CAAC,0BAA0B;IAkBlC,OAAO,CAAC,gCAAgC;IAexC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwCG;IACH,OAAO,CAAC,cAAc;IAyPtB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAO,CAAC,iBAAiB;IAezB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,sBAAsB;IAI9B;;;;;;;;;;;;;;;;;;;OAmBG;YACW,oBAAoB;IAmDlC;;OAEG;YACW,iBAAiB;IAoB/B;;OAEG;IACH,OAAO,CAAC,aAAa;IAiErB;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,uBAAuB;IA8D/B;;;;;;;OAOG;IACH,OAAO,CAAC,sBAAsB;IAmJ9B;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB;;;;OAIG;IACH,OAAO,CAAC,mCAAmC;IAgB3C;;;;OAIG;IACH,OAAO,CAAC,qCAAqC;IAU7C,OAAO,CAAC,YAAY;IAIpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;YACW,wBAAwB;IAsKtC;;;;;;;OAOG;YACW,kBAAkB;IAqBhC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,cAAc;YAWR,aAAa;IAuC3B;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IAmBnC;;;;;;OAMG;YACW,oBAAoB;IA2ClC;;;;;;OAMG;IACH,4BAA4B,CAC1B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,gBAAgB,EACxB,MAAM,CAAC,EAAE,MAAM,GACd,YAAY;IA+Bf;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAI7B;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,mBAAmB;IAc3B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,oBAAoB;CAG7B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,iBAAiB,EACrB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACjD,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,UAAU,CAAC,CAGrB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAI9D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,CAIzD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,EAAE,CAU5E"}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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`,
|
|
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:
|
|
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
|
|
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
|
|
2107
|
-
*
|
|
2108
|
-
*
|
|
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
|
-
|
|
2237
|
+
`${quotedTable}${quotedCol}`;
|
|
2131
2238
|
let enforceNotNull = false;
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
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(
|
|
2158
|
-
|
|
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
|