@happyvertical/smrt-core 0.43.10 → 0.44.0
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/generators.md +70 -0
- package/agents/schema-paths.md +99 -0
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +1 -1
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/knowledge.d.ts +17 -1
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +33 -3
- package/dist/knowledge.js.map +1 -1
- 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 +49 -0
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +220 -4
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/orchestrate.d.ts +23 -0
- package/dist/migrations/orchestrate.d.ts.map +1 -1
- package/dist/migrations/orchestrate.js +12 -3
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/registry/types.d.ts +8 -3
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/foreign-key-ddl.d.ts +7 -0
- package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
- package/dist/schema/foreign-key-ddl.js +7 -1
- package/dist/schema/foreign-key-ddl.js.map +1 -1
- package/dist/schema/schema-manager.d.ts +15 -0
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +52 -9
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/schema/types.d.ts +10 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/uuid-convergence.d.ts +142 -0
- package/dist/schema/uuid-convergence.d.ts.map +1 -0
- package/dist/schema/uuid-convergence.js +0 -0
- package/dist/schema/uuid-convergence.js.map +1 -0
- package/dist/smrt-knowledge.json +7 -7
- package/dist/utils/scanner-module.d.ts +44 -0
- package/dist/utils/scanner-module.d.ts.map +1 -1
- package/dist/vite-plugin/index.d.ts +14 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +54 -2
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin.js +3 -2
- package/package.json +4 -4
|
@@ -155,4 +155,27 @@ export declare function getPendingSchemaStatements(db: DatabaseInterface, option
|
|
|
155
155
|
* field, the result would look indistinguishable from "already in sync."
|
|
156
156
|
*/
|
|
157
157
|
export declare function migrateSmrtSchemas(options: MigrateSmrtSchemasOptions): Promise<MigrateSmrtSchemasResult>;
|
|
158
|
+
/**
|
|
159
|
+
* Materialize the diff into the engine-correct SQL statements.
|
|
160
|
+
*
|
|
161
|
+
* For new tables we delegate to the engine's `DDLStrategy` so column types
|
|
162
|
+
* (`REAL` → `DOUBLE PRECISION` on Postgres, `JSON` → `JSONB`, etc.),
|
|
163
|
+
* partial-index `WHERE` clauses, and trigger syntax all match what the
|
|
164
|
+
* SchemaComparer would expect on subsequent runs. Emitting the raw
|
|
165
|
+
* abstract `SchemaDefinition.ddl` here would cause immediate type-drift
|
|
166
|
+
* migrations on Postgres/DuckDB.
|
|
167
|
+
*
|
|
168
|
+
* Column/index/trigger changes for existing tables continue to come from
|
|
169
|
+
* `getSQLFromDiff`, which is already engine-aware via the differ's per-
|
|
170
|
+
* change `sqlStatements`.
|
|
171
|
+
*
|
|
172
|
+
* Exported for statement-ordering tests; it is not part of the package's
|
|
173
|
+
* public entry point (`migrations/index.ts` re-exports names explicitly).
|
|
174
|
+
*
|
|
175
|
+
* @param diff - Schema diff to materialize.
|
|
176
|
+
* @param db - Database whose URL selects the DDL strategy.
|
|
177
|
+
* @param engineHint - Explicit engine when `db.url` is empty or ambiguous.
|
|
178
|
+
* @returns Ordered, executable DDL statements.
|
|
179
|
+
*/
|
|
180
|
+
export declare function collectStatementsFromDiff(diff: Awaited<ReturnType<typeof generateSchemaDiff>>, db: DatabaseInterface, engineHint?: DatabaseEngine): string[];
|
|
158
181
|
//# sourceMappingURL=orchestrate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrate.d.ts","sourceRoot":"","sources":["../../src/migrations/orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAOrB,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,iBAAiB,CAAC;IACtB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;OAGG;IACH,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;OAOG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACnD,GACL,OAAO,CAAC,6BAA6B,CAAC,CAqBxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA0DnC"}
|
|
1
|
+
{"version":3,"file":"orchestrate.d.ts","sourceRoot":"","sources":["../../src/migrations/orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAOrB,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,iBAAiB,CAAC;IACtB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;OAGG;IACH,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;OAOG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACnD,GACL,OAAO,CAAC,6BAA6B,CAAC,CAqBxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA0DnC;AAcD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,EACpD,EAAE,EAAE,iBAAiB,EACrB,UAAU,CAAC,EAAE,cAAc,GAC1B,MAAM,EAAE,CAwCV"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { detectEngine, getDDLStrategy } from "../schema/ddl/index.js";
|
|
2
2
|
import { ObjectRegistry } from "../registry.js";
|
|
3
3
|
import { planForeignKeyCreation } from "../schema/foreign-key-planner.js";
|
|
4
|
-
import { generateSchemaDiff,
|
|
4
|
+
import { generateSchemaDiff, getSQLFromChanges, hasActionableChanges } from "./differ.js";
|
|
5
5
|
import { createMigrationDefinition, generateMigrationTimestamp } from "./generator.js";
|
|
6
6
|
import { MigrationTracker } from "./tracker.js";
|
|
7
7
|
//#region src/migrations/orchestrate.ts
|
|
@@ -135,18 +135,27 @@ function resolveDatabaseUrl(db) {
|
|
|
135
135
|
* Column/index/trigger changes for existing tables continue to come from
|
|
136
136
|
* `getSQLFromDiff`, which is already engine-aware via the differ's per-
|
|
137
137
|
* change `sqlStatements`.
|
|
138
|
+
*
|
|
139
|
+
* Exported for statement-ordering tests; it is not part of the package's
|
|
140
|
+
* public entry point (`migrations/index.ts` re-exports names explicitly).
|
|
141
|
+
*
|
|
142
|
+
* @param diff - Schema diff to materialize.
|
|
143
|
+
* @param db - Database whose URL selects the DDL strategy.
|
|
144
|
+
* @param engineHint - Explicit engine when `db.url` is empty or ambiguous.
|
|
145
|
+
* @returns Ordered, executable DDL statements.
|
|
138
146
|
*/
|
|
139
147
|
function collectStatementsFromDiff(diff, db, engineHint) {
|
|
140
148
|
const strategy = getDDLStrategy(detectEngine(resolveDatabaseUrl(db), engineHint));
|
|
141
149
|
const statements = [];
|
|
142
150
|
const tablePlan = planForeignKeyCreation(diff.added_tables, strategy.engine);
|
|
151
|
+
statements.push(...getSQLFromChanges(diff.changes.filter((change) => change.phase === "pre_foreign_key")));
|
|
143
152
|
for (const schema of tablePlan.schemas) {
|
|
144
153
|
statements.push(strategy.generateCreateTable(schema));
|
|
145
154
|
statements.push(...strategy.generateIndexes(schema));
|
|
146
155
|
statements.push(...strategy.generateTriggers(schema));
|
|
147
156
|
}
|
|
148
157
|
statements.push(...tablePlan.deferredStatements);
|
|
149
|
-
statements.push(...
|
|
158
|
+
statements.push(...getSQLFromChanges(diff.changes.filter((change) => change.phase !== "pre_foreign_key")));
|
|
150
159
|
return statements.filter((statement) => {
|
|
151
160
|
const trimmed = statement.trim();
|
|
152
161
|
if (trimmed.length === 0) return false;
|
|
@@ -216,6 +225,6 @@ function collectUnactionableChanges(diff) {
|
|
|
216
225
|
return unactionable;
|
|
217
226
|
}
|
|
218
227
|
//#endregion
|
|
219
|
-
export { getPendingSchemaStatements, migrateSmrtSchemas };
|
|
228
|
+
export { collectStatementsFromDiff, getPendingSchemaStatements, migrateSmrtSchemas };
|
|
220
229
|
|
|
221
230
|
//# sourceMappingURL=orchestrate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrate.js","names":[],"sources":["../../src/migrations/orchestrate.ts"],"sourcesContent":["/**\n * High-level \"make my SMRT app's schema match its database\" orchestration.\n *\n * Wraps `ObjectRegistry.getAllSchemasAsDefinitions()` → `generateSchemaDiff`\n * → `MigrationTracker.applyAll`. Apps that just want to run pending\n * schema changes call `migrateSmrtSchemas({ db, packageName, version })`\n * and don't have to assemble the migration definition by hand.\n *\n * The migration name is timestamped, so re-running on an up-to-date\n * database is a no-op (the diff is empty, no statements are generated,\n * `applied: false` is returned).\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ObjectRegistry } from '../registry.js';\nimport { detectEngine, getDDLStrategy } from '../schema/ddl/index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport { planForeignKeyCreation } from '../schema/foreign-key-planner.js';\nimport type { MigrationResult, SchemaChange } from '../schema/types.js';\nimport {\n generateSchemaDiff,\n getSQLFromDiff,\n hasActionableChanges,\n} from './differ.js';\nimport {\n createMigrationDefinition,\n generateMigrationTimestamp,\n} from './generator.js';\nimport { MigrationTracker } from './tracker.js';\n\nexport interface MigrateSmrtSchemasOptions {\n db: DatabaseInterface;\n /** Stored on the migration record so the audit log shows which app applied it. */\n packageName: string;\n /**\n * Stored on the migration record. Typically the app's package.json\n * version. Optional because most apps don't have a runtime-readable\n * version handy and would hardcode a stale string if forced to supply\n * one — `'unknown'` is honest. Pass an explicit value when you can\n * (e.g. `version: process.env.APP_VERSION`).\n */\n version?: string;\n /** Override the description on the synthetic migration definition. */\n description?: string;\n /** Override the migration name (default: `<timestamp>_smrt_schema_sync`). */\n name?: string;\n /** Forwarded to MigrationTracker — defaults to true for pg-safe runs. */\n postgresSafe?: boolean;\n /** Forwarded to MigrationTracker — defaults to true. */\n reconcile?: boolean;\n /** Forwarded to MigrationTracker — defaults to true for `CREATE INDEX CONCURRENTLY`. */\n useConcurrentIndexes?: boolean;\n /**\n * Forwarded to MigrationTracker (Postgres lock timeout, milliseconds).\n * Lets apps that customize tracker timeouts directly preserve their\n * tuning when going through the orchestrator. Falls back to the\n * tracker's default (30000ms) when omitted.\n */\n lockTimeout?: number;\n /**\n * Forwarded to MigrationTracker (Postgres statement timeout, ms). Falls\n * back to the tracker's default (60000ms) when omitted.\n */\n statementTimeout?: number;\n /**\n * Explicit engine hint forwarded to `detectEngine`, `SchemaComparer`, and\n * `MigrationTracker`. Useful when `db.url` doesn't unambiguously identify\n * the engine (e.g. the JSON adapter, which uses DuckDB internally and may\n * not be recognizable by URL alone). All three layers honor the same hint\n * so the generated DDL, drift comparison, and execution path stay\n * consistent — without it, an empty-URL connection could produce\n * Postgres-flavored DDL but run through the SQLite tracker path.\n *\n * Typed as `DatabaseEngine` so unsupported values (e.g. `'mysql'`) fail at\n * compile time rather than silently falling back to `'sqlite'` at runtime.\n */\n engineHint?: DatabaseEngine;\n /**\n * Confirm that historical PostgreSQL Date writers used UTC wall times before\n * reinterpreting legacy TIMESTAMP/TEXT/JSON columns as TIMESTAMPTZ.\n */\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n}\n\nexport interface MigrateSmrtSchemasResult {\n applied: boolean;\n results: MigrationResult[];\n statements: string[];\n schemaCount: number;\n /**\n * Schema changes the orchestrator detected but cannot apply automatically\n * (incompatible type mismatches, SQLite type upgrades that require table\n * recreation, etc.). Empty when the schema is fully reconciled. Inspect\n * this to surface a \"manual migration required\" warning in CLI output.\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Lets a caller write\n * `if (result.hasManualDrift) ...` without iterating the array first,\n * and makes the case visible to anyone scanning the result shape (the\n * full array is easy to gloss over when `applied: false`).\n */\n hasManualDrift: boolean;\n}\n\nexport interface PendingSchemaStatementsResult {\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>;\n statements: string[];\n schemaCount: number;\n hasChanges: boolean;\n /**\n * Schema changes the differ detected but that have no executable SQL\n * (incompatible `type_mismatch` entries, or `type_upgrade` entries whose\n * only generated SQL is an advisory comment because the engine cannot\n * upgrade the column in place — e.g. SQLite type widening). Surfacing\n * these lets status commands distinguish \"no drift\" from \"drift the\n * orchestrator can't fix on its own.\"\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Mirrors the\n * `MigrateSmrtSchemasResult.hasManualDrift` shortcut.\n */\n hasManualDrift: boolean;\n}\n\n/**\n * Compute the SQL statements needed to bring `db` in sync with the\n * currently-registered SMRT object schemas. Pure inspection — no writes.\n *\n * Useful for status commands (\"how far behind is the schema?\") and as the\n * inner step of `migrateSmrtSchemas`.\n *\n * **Note on the returned `statements`:** these are the engine-correct DDL\n * statements as the differ + DDL strategy produce them, *prior to* any\n * Postgres-specific rewrites the `MigrationTracker` applies at execution\n * time (e.g. rewriting `CREATE INDEX` → `CREATE INDEX CONCURRENTLY` and\n * moving CONCURRENTLY statements outside the surrounding transaction).\n * The list is suitable for preview/status/\"what would change?\" use cases.\n * For the exact byte-for-byte SQL the tracker ran, inspect the\n * `MigrationResult`s from `migrateSmrtSchemas` or the tracker's own logs.\n */\nexport async function getPendingSchemaStatements(\n db: DatabaseInterface,\n options: {\n engineHint?: DatabaseEngine;\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n } = {},\n): Promise<PendingSchemaStatementsResult> {\n const schemas = ObjectRegistry.getAllSchemasAsDefinitions();\n // Forward engineHint into the diff itself so the SchemaComparer's\n // existing-table SQL (ALTER/index drift) uses the same DDL strategy\n // we use for newly-added tables — otherwise the two halves of the\n // statement list can land on different engines (split-brain) when\n // `db.url` is empty or ambiguous.\n const diff = await generateSchemaDiff(db, schemas, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n });\n const statements = collectStatementsFromDiff(diff, db, options.engineHint);\n const unactionableChanges = collectUnactionableChanges(diff);\n return {\n diff,\n schemaCount: Object.keys(schemas).length,\n statements,\n hasChanges: hasActionableChanges(diff),\n unactionableChanges,\n hasManualDrift: unactionableChanges.length > 0,\n };\n}\n\n/**\n * Apply any pending schema changes from `ObjectRegistry` to `db`.\n *\n * Returns `applied: false` (with no error) if the database is already in\n * sync. Throws on the first migration failure with the underlying error\n * preserved (callers should let it propagate to the CLI).\n *\n * **Note on the returned `statements`:** mirrors `getPendingSchemaStatements`\n * — the list is the planned DDL, prior to Postgres-specific tracker rewrites\n * (CONCURRENTLY, transaction reordering). The actual executed SQL is\n * tracked in the returned `results`.\n *\n * **Note on `applied` semantics under `reconcile: true` (the default):**\n * the tracker may re-execute a migration that is already in `completed`\n * state when the checksum matches — that counts as `applied: true` in the\n * returned `MigrationResult`. So `result.applied === true` means \"the\n * tracker ran the migration\", not specifically \"the database changed for\n * the first time.\" Pass `reconcile: false` if you need to distinguish a\n * fresh apply from a reconcile-replay over an existing completed record.\n *\n * **Note on repeated failures:** the migration name defaults to a fresh\n * timestamp on every call, so a sequence of failed runs leaves one\n * `failed` row in `_smrt_schema_migrations` per attempt. Pass an explicit\n * `name` if you want to overwrite the same record across retries.\n *\n * **Note on `unactionableChanges`:** the differ can detect schema drift\n * the migration step can't auto-resolve — incompatible type mismatches,\n * SQLite type widening that needs table recreation, etc. Those changes\n * appear in `result.unactionableChanges` and are NOT applied (the\n * statements list is filtered to executable DDL only). When the diff\n * contains *only* unactionable changes, the function returns\n * `applied: false` with a populated `unactionableChanges` so callers can\n * surface a \"manual migration required\" signal. Without inspecting that\n * field, the result would look indistinguishable from \"already in sync.\"\n */\nexport async function migrateSmrtSchemas(\n options: MigrateSmrtSchemasOptions,\n): Promise<MigrateSmrtSchemasResult> {\n const pending = await getPendingSchemaStatements(options.db, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n });\n if (!pending.hasChanges || pending.statements.length === 0) {\n return {\n applied: false,\n results: [],\n statements: [],\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n }\n\n const tracker = new MigrationTracker({\n db: options.db,\n engineHint: options.engineHint,\n lockTimeout: options.lockTimeout,\n statementTimeout: options.statementTimeout,\n useConcurrentIndexes: options.useConcurrentIndexes ?? true,\n });\n const migration = createMigrationDefinition(\n options.name ?? `${generateMigrationTimestamp()}_smrt_schema_sync`,\n pending.statements,\n [],\n {\n description: options.description ?? 'Synchronize SMRT object schemas',\n packageName: options.packageName,\n version: options.version ?? 'unknown',\n },\n );\n\n const results = await tracker.applyAll([migration], {\n postgresSafe: options.postgresSafe ?? true,\n reconcile: options.reconcile ?? true,\n });\n\n const failed = results.find((result) => !result.success);\n if (failed) {\n throw failed.error instanceof Error\n ? failed.error\n : new Error(String(failed.error ?? `Migration ${failed.name} failed`));\n }\n\n const applied = results.some(\n (result) => result.applied !== false && result.skipped !== true,\n );\n\n return {\n applied,\n results,\n statements: pending.statements,\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n}\n\n/** Mirror of the `getDatabaseUrl` helper in class.ts — some adapters\n * (JSON, certain in-memory wrappers) leave `db.url` undefined and expose\n * the URL on `db.config?.url` instead. Detecting the engine from a raw\n * `db.url` would throw or silently fall through to the sqlite default.\n */\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n return db.url || dbWithConfig.config?.url || '';\n}\n\n/**\n * Materialize the diff into the engine-correct SQL statements.\n *\n * For new tables we delegate to the engine's `DDLStrategy` so column types\n * (`REAL` → `DOUBLE PRECISION` on Postgres, `JSON` → `JSONB`, etc.),\n * partial-index `WHERE` clauses, and trigger syntax all match what the\n * SchemaComparer would expect on subsequent runs. Emitting the raw\n * abstract `SchemaDefinition.ddl` here would cause immediate type-drift\n * migrations on Postgres/DuckDB.\n *\n * Column/index/trigger changes for existing tables continue to come from\n * `getSQLFromDiff`, which is already engine-aware via the differ's per-\n * change `sqlStatements`.\n */\nfunction collectStatementsFromDiff(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n db: DatabaseInterface,\n engineHint?: DatabaseEngine,\n): string[] {\n const strategy = getDDLStrategy(\n detectEngine(resolveDatabaseUrl(db), engineHint),\n );\n const statements: string[] = [];\n const tablePlan = planForeignKeyCreation(diff.added_tables, strategy.engine);\n for (const schema of tablePlan.schemas) {\n statements.push(strategy.generateCreateTable(schema));\n statements.push(...strategy.generateIndexes(schema));\n statements.push(...strategy.generateTriggers(schema));\n }\n statements.push(...tablePlan.deferredStatements);\n statements.push(...getSQLFromDiff(diff));\n // Drop empty and comment-only entries. SQLite type-widening upgrades\n // surface as `-- SQLite: Type upgrade for X requires table recreation`\n // — passing those through to the tracker records a successful migration\n // without actually fixing the column, so the same drift re-appears on\n // every subsequent run. Those changes are surfaced separately via\n // `unactionableChanges` so callers can prompt for manual remediation.\n return statements.filter((statement) => {\n const trimmed = statement.trim();\n if (trimmed.length === 0) return false;\n return !isCommentOnlySql(trimmed);\n });\n}\n\n/**\n * True if `sql` contains no executable DDL — only line comments (`--`)\n * and/or block comments (`/* … */`). Used to discard advisory comments\n * the differ emits when a column upgrade can't be performed in place\n * (e.g. SQLite type widening). Defensive: strips block comments first so\n * a future differ that emits `/* type_upgrade: no-op */` doesn't slip\n * through.\n */\nfunction isCommentOnlySql(sql: string): boolean {\n // Strip /* ... */ block comments (non-greedy, spans newlines).\n const withoutBlockComments = sql.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n const lines = withoutBlockComments\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n if (lines.length === 0) return sql.trim().length > 0;\n return lines.every((line) => line.startsWith('--'));\n}\n\n/**\n * True if `sql` is an advisory comment that says *nothing needs to happen* —\n * as opposed to one that says a human has to act.\n *\n * The differ emits both kinds. \"This SQLite column already stores JSON as\n * TEXT\" and \"#2370: this column is rebuilt together with its sibling\" are\n * no-ops; \"requires table recreation\" / \"blocked: …\" are real manual drift.\n * Treating the first kind as drift makes a fully auto-repairable migration\n * report `hasManualDrift: true`.\n *\n * Keep in sync with `classifyTypeUpgradeSql`'s `noop` arm in\n * `@happyvertical/smrt-cli` (`src/commands/db-migrate-actions.ts`), which\n * applies the same rule on the CLI side.\n */\nfunction isNoOpAdvisorySql(sql: string): boolean {\n const trimmed = sql.trim();\n return (\n /no change needed/i.test(trimmed) || /already stores .* as /i.test(trimmed)\n );\n}\n\n/**\n * Surface changes the differ produced but that the migrator cannot apply\n * automatically — `type_mismatch` entries (the differ explicitly gives up on\n * these), `type_upgrade`/`alter_column` entries whose generated SQL is\n * advisory-comment-only (a SQLite rebuild that would be unsafe, an in-place\n * constraint change SQLite cannot perform), and warning-level report-only\n * advisories (an orphan NOT NULL column that will break inserts, a required\n * column that could not be enforced, a stale unique constraint, a relaxation\n * the caller has not opted into). Info-level advisories (a harmless orphan\n * column, a stale default) and comments that mean \"already handled\" (the\n * no-op advisories a table rebuild leaves on covered siblings, #2370) are\n * deliberately excluded so `hasManualDrift` keeps meaning \"something needs\n * an operator\". Callers use this to distinguish \"schema is in sync\" from\n * \"schema is drifted but we can't fix it from here\".\n */\nfunction collectUnactionableChanges(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n): SchemaChange[] {\n const unactionable: SchemaChange[] = [];\n for (const change of diff.changes) {\n if (change.type === 'type_mismatch') {\n unactionable.push(change);\n continue;\n }\n const statements = change.sqlStatements ?? (change.sql ? [change.sql] : []);\n if (statements.length === 0) {\n if (change.advisory?.severity === 'warning') {\n unactionable.push(change);\n }\n continue;\n }\n if (\n statements.every((stmt) => isCommentOnlySql(stmt.trim())) &&\n !statements.every((stmt) => isNoOpAdvisorySql(stmt))\n ) {\n unactionable.push(change);\n }\n }\n return unactionable;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"orchestrate.js","names":[],"sources":["../../src/migrations/orchestrate.ts"],"sourcesContent":["/**\n * High-level \"make my SMRT app's schema match its database\" orchestration.\n *\n * Wraps `ObjectRegistry.getAllSchemasAsDefinitions()` → `generateSchemaDiff`\n * → `MigrationTracker.applyAll`. Apps that just want to run pending\n * schema changes call `migrateSmrtSchemas({ db, packageName, version })`\n * and don't have to assemble the migration definition by hand.\n *\n * The migration name is timestamped, so re-running on an up-to-date\n * database is a no-op (the diff is empty, no statements are generated,\n * `applied: false` is returned).\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ObjectRegistry } from '../registry.js';\nimport { detectEngine, getDDLStrategy } from '../schema/ddl/index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport { planForeignKeyCreation } from '../schema/foreign-key-planner.js';\nimport type { MigrationResult, SchemaChange } from '../schema/types.js';\nimport {\n generateSchemaDiff,\n getSQLFromChanges,\n hasActionableChanges,\n} from './differ.js';\nimport {\n createMigrationDefinition,\n generateMigrationTimestamp,\n} from './generator.js';\nimport { MigrationTracker } from './tracker.js';\n\nexport interface MigrateSmrtSchemasOptions {\n db: DatabaseInterface;\n /** Stored on the migration record so the audit log shows which app applied it. */\n packageName: string;\n /**\n * Stored on the migration record. Typically the app's package.json\n * version. Optional because most apps don't have a runtime-readable\n * version handy and would hardcode a stale string if forced to supply\n * one — `'unknown'` is honest. Pass an explicit value when you can\n * (e.g. `version: process.env.APP_VERSION`).\n */\n version?: string;\n /** Override the description on the synthetic migration definition. */\n description?: string;\n /** Override the migration name (default: `<timestamp>_smrt_schema_sync`). */\n name?: string;\n /** Forwarded to MigrationTracker — defaults to true for pg-safe runs. */\n postgresSafe?: boolean;\n /** Forwarded to MigrationTracker — defaults to true. */\n reconcile?: boolean;\n /** Forwarded to MigrationTracker — defaults to true for `CREATE INDEX CONCURRENTLY`. */\n useConcurrentIndexes?: boolean;\n /**\n * Forwarded to MigrationTracker (Postgres lock timeout, milliseconds).\n * Lets apps that customize tracker timeouts directly preserve their\n * tuning when going through the orchestrator. Falls back to the\n * tracker's default (30000ms) when omitted.\n */\n lockTimeout?: number;\n /**\n * Forwarded to MigrationTracker (Postgres statement timeout, ms). Falls\n * back to the tracker's default (60000ms) when omitted.\n */\n statementTimeout?: number;\n /**\n * Explicit engine hint forwarded to `detectEngine`, `SchemaComparer`, and\n * `MigrationTracker`. Useful when `db.url` doesn't unambiguously identify\n * the engine (e.g. the JSON adapter, which uses DuckDB internally and may\n * not be recognizable by URL alone). All three layers honor the same hint\n * so the generated DDL, drift comparison, and execution path stay\n * consistent — without it, an empty-URL connection could produce\n * Postgres-flavored DDL but run through the SQLite tracker path.\n *\n * Typed as `DatabaseEngine` so unsupported values (e.g. `'mysql'`) fail at\n * compile time rather than silently falling back to `'sqlite'` at runtime.\n */\n engineHint?: DatabaseEngine;\n /**\n * Confirm that historical PostgreSQL Date writers used UTC wall times before\n * reinterpreting legacy TIMESTAMP/TEXT/JSON columns as TIMESTAMPTZ.\n */\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n}\n\nexport interface MigrateSmrtSchemasResult {\n applied: boolean;\n results: MigrationResult[];\n statements: string[];\n schemaCount: number;\n /**\n * Schema changes the orchestrator detected but cannot apply automatically\n * (incompatible type mismatches, SQLite type upgrades that require table\n * recreation, etc.). Empty when the schema is fully reconciled. Inspect\n * this to surface a \"manual migration required\" warning in CLI output.\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Lets a caller write\n * `if (result.hasManualDrift) ...` without iterating the array first,\n * and makes the case visible to anyone scanning the result shape (the\n * full array is easy to gloss over when `applied: false`).\n */\n hasManualDrift: boolean;\n}\n\nexport interface PendingSchemaStatementsResult {\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>;\n statements: string[];\n schemaCount: number;\n hasChanges: boolean;\n /**\n * Schema changes the differ detected but that have no executable SQL\n * (incompatible `type_mismatch` entries, or `type_upgrade` entries whose\n * only generated SQL is an advisory comment because the engine cannot\n * upgrade the column in place — e.g. SQLite type widening). Surfacing\n * these lets status commands distinguish \"no drift\" from \"drift the\n * orchestrator can't fix on its own.\"\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Mirrors the\n * `MigrateSmrtSchemasResult.hasManualDrift` shortcut.\n */\n hasManualDrift: boolean;\n}\n\n/**\n * Compute the SQL statements needed to bring `db` in sync with the\n * currently-registered SMRT object schemas. Pure inspection — no writes.\n *\n * Useful for status commands (\"how far behind is the schema?\") and as the\n * inner step of `migrateSmrtSchemas`.\n *\n * **Note on the returned `statements`:** these are the engine-correct DDL\n * statements as the differ + DDL strategy produce them, *prior to* any\n * Postgres-specific rewrites the `MigrationTracker` applies at execution\n * time (e.g. rewriting `CREATE INDEX` → `CREATE INDEX CONCURRENTLY` and\n * moving CONCURRENTLY statements outside the surrounding transaction).\n * The list is suitable for preview/status/\"what would change?\" use cases.\n * For the exact byte-for-byte SQL the tracker ran, inspect the\n * `MigrationResult`s from `migrateSmrtSchemas` or the tracker's own logs.\n */\nexport async function getPendingSchemaStatements(\n db: DatabaseInterface,\n options: {\n engineHint?: DatabaseEngine;\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n } = {},\n): Promise<PendingSchemaStatementsResult> {\n const schemas = ObjectRegistry.getAllSchemasAsDefinitions();\n // Forward engineHint into the diff itself so the SchemaComparer's\n // existing-table SQL (ALTER/index drift) uses the same DDL strategy\n // we use for newly-added tables — otherwise the two halves of the\n // statement list can land on different engines (split-brain) when\n // `db.url` is empty or ambiguous.\n const diff = await generateSchemaDiff(db, schemas, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n });\n const statements = collectStatementsFromDiff(diff, db, options.engineHint);\n const unactionableChanges = collectUnactionableChanges(diff);\n return {\n diff,\n schemaCount: Object.keys(schemas).length,\n statements,\n hasChanges: hasActionableChanges(diff),\n unactionableChanges,\n hasManualDrift: unactionableChanges.length > 0,\n };\n}\n\n/**\n * Apply any pending schema changes from `ObjectRegistry` to `db`.\n *\n * Returns `applied: false` (with no error) if the database is already in\n * sync. Throws on the first migration failure with the underlying error\n * preserved (callers should let it propagate to the CLI).\n *\n * **Note on the returned `statements`:** mirrors `getPendingSchemaStatements`\n * — the list is the planned DDL, prior to Postgres-specific tracker rewrites\n * (CONCURRENTLY, transaction reordering). The actual executed SQL is\n * tracked in the returned `results`.\n *\n * **Note on `applied` semantics under `reconcile: true` (the default):**\n * the tracker may re-execute a migration that is already in `completed`\n * state when the checksum matches — that counts as `applied: true` in the\n * returned `MigrationResult`. So `result.applied === true` means \"the\n * tracker ran the migration\", not specifically \"the database changed for\n * the first time.\" Pass `reconcile: false` if you need to distinguish a\n * fresh apply from a reconcile-replay over an existing completed record.\n *\n * **Note on repeated failures:** the migration name defaults to a fresh\n * timestamp on every call, so a sequence of failed runs leaves one\n * `failed` row in `_smrt_schema_migrations` per attempt. Pass an explicit\n * `name` if you want to overwrite the same record across retries.\n *\n * **Note on `unactionableChanges`:** the differ can detect schema drift\n * the migration step can't auto-resolve — incompatible type mismatches,\n * SQLite type widening that needs table recreation, etc. Those changes\n * appear in `result.unactionableChanges` and are NOT applied (the\n * statements list is filtered to executable DDL only). When the diff\n * contains *only* unactionable changes, the function returns\n * `applied: false` with a populated `unactionableChanges` so callers can\n * surface a \"manual migration required\" signal. Without inspecting that\n * field, the result would look indistinguishable from \"already in sync.\"\n */\nexport async function migrateSmrtSchemas(\n options: MigrateSmrtSchemasOptions,\n): Promise<MigrateSmrtSchemasResult> {\n const pending = await getPendingSchemaStatements(options.db, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n });\n if (!pending.hasChanges || pending.statements.length === 0) {\n return {\n applied: false,\n results: [],\n statements: [],\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n }\n\n const tracker = new MigrationTracker({\n db: options.db,\n engineHint: options.engineHint,\n lockTimeout: options.lockTimeout,\n statementTimeout: options.statementTimeout,\n useConcurrentIndexes: options.useConcurrentIndexes ?? true,\n });\n const migration = createMigrationDefinition(\n options.name ?? `${generateMigrationTimestamp()}_smrt_schema_sync`,\n pending.statements,\n [],\n {\n description: options.description ?? 'Synchronize SMRT object schemas',\n packageName: options.packageName,\n version: options.version ?? 'unknown',\n },\n );\n\n const results = await tracker.applyAll([migration], {\n postgresSafe: options.postgresSafe ?? true,\n reconcile: options.reconcile ?? true,\n });\n\n const failed = results.find((result) => !result.success);\n if (failed) {\n throw failed.error instanceof Error\n ? failed.error\n : new Error(String(failed.error ?? `Migration ${failed.name} failed`));\n }\n\n const applied = results.some(\n (result) => result.applied !== false && result.skipped !== true,\n );\n\n return {\n applied,\n results,\n statements: pending.statements,\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n}\n\n/** Mirror of the `getDatabaseUrl` helper in class.ts — some adapters\n * (JSON, certain in-memory wrappers) leave `db.url` undefined and expose\n * the URL on `db.config?.url` instead. Detecting the engine from a raw\n * `db.url` would throw or silently fall through to the sqlite default.\n */\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n return db.url || dbWithConfig.config?.url || '';\n}\n\n/**\n * Materialize the diff into the engine-correct SQL statements.\n *\n * For new tables we delegate to the engine's `DDLStrategy` so column types\n * (`REAL` → `DOUBLE PRECISION` on Postgres, `JSON` → `JSONB`, etc.),\n * partial-index `WHERE` clauses, and trigger syntax all match what the\n * SchemaComparer would expect on subsequent runs. Emitting the raw\n * abstract `SchemaDefinition.ddl` here would cause immediate type-drift\n * migrations on Postgres/DuckDB.\n *\n * Column/index/trigger changes for existing tables continue to come from\n * `getSQLFromDiff`, which is already engine-aware via the differ's per-\n * change `sqlStatements`.\n *\n * Exported for statement-ordering tests; it is not part of the package's\n * public entry point (`migrations/index.ts` re-exports names explicitly).\n *\n * @param diff - Schema diff to materialize.\n * @param db - Database whose URL selects the DDL strategy.\n * @param engineHint - Explicit engine when `db.url` is empty or ambiguous.\n * @returns Ordered, executable DDL statements.\n */\nexport function collectStatementsFromDiff(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n db: DatabaseInterface,\n engineHint?: DatabaseEngine,\n): string[] {\n const strategy = getDDLStrategy(\n detectEngine(resolveDatabaseUrl(db), engineHint),\n );\n const statements: string[] = [];\n const tablePlan = planForeignKeyCreation(diff.added_tables, strategy.engine);\n // #2608: the pre-R11 uuid convergence has to precede *every* foreign-key\n // statement. `planForeignKeyCreation` only defers the constraints inside a\n // mutual cycle, so an acyclic new child table keeps its FK **inline in\n // CREATE TABLE** — a fresh `uuid` child pointing at a legacy `text` parent\n // would fail with SQLSTATE 42804 before the parent could be converted.\n // Conversions only ever rewrite columns that already exist, so they are\n // safe to run ahead of every CREATE TABLE in this batch.\n statements.push(\n ...getSQLFromChanges(\n diff.changes.filter((change) => change.phase === 'pre_foreign_key'),\n ),\n );\n for (const schema of tablePlan.schemas) {\n statements.push(strategy.generateCreateTable(schema));\n statements.push(...strategy.generateIndexes(schema));\n statements.push(...strategy.generateTriggers(schema));\n }\n statements.push(...tablePlan.deferredStatements);\n statements.push(\n ...getSQLFromChanges(\n diff.changes.filter((change) => change.phase !== 'pre_foreign_key'),\n ),\n );\n // Drop empty and comment-only entries. SQLite type-widening upgrades\n // surface as `-- SQLite: Type upgrade for X requires table recreation`\n // — passing those through to the tracker records a successful migration\n // without actually fixing the column, so the same drift re-appears on\n // every subsequent run. Those changes are surfaced separately via\n // `unactionableChanges` so callers can prompt for manual remediation.\n return statements.filter((statement) => {\n const trimmed = statement.trim();\n if (trimmed.length === 0) return false;\n return !isCommentOnlySql(trimmed);\n });\n}\n\n/**\n * True if `sql` contains no executable DDL — only line comments (`--`)\n * and/or block comments (`/* … */`). Used to discard advisory comments\n * the differ emits when a column upgrade can't be performed in place\n * (e.g. SQLite type widening). Defensive: strips block comments first so\n * a future differ that emits `/* type_upgrade: no-op */` doesn't slip\n * through.\n */\nfunction isCommentOnlySql(sql: string): boolean {\n // Strip /* ... */ block comments (non-greedy, spans newlines).\n const withoutBlockComments = sql.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n const lines = withoutBlockComments\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n if (lines.length === 0) return sql.trim().length > 0;\n return lines.every((line) => line.startsWith('--'));\n}\n\n/**\n * True if `sql` is an advisory comment that says *nothing needs to happen* —\n * as opposed to one that says a human has to act.\n *\n * The differ emits both kinds. \"This SQLite column already stores JSON as\n * TEXT\" and \"#2370: this column is rebuilt together with its sibling\" are\n * no-ops; \"requires table recreation\" / \"blocked: …\" are real manual drift.\n * Treating the first kind as drift makes a fully auto-repairable migration\n * report `hasManualDrift: true`.\n *\n * Keep in sync with `classifyTypeUpgradeSql`'s `noop` arm in\n * `@happyvertical/smrt-cli` (`src/commands/db-migrate-actions.ts`), which\n * applies the same rule on the CLI side.\n */\nfunction isNoOpAdvisorySql(sql: string): boolean {\n const trimmed = sql.trim();\n return (\n /no change needed/i.test(trimmed) || /already stores .* as /i.test(trimmed)\n );\n}\n\n/**\n * Surface changes the differ produced but that the migrator cannot apply\n * automatically — `type_mismatch` entries (the differ explicitly gives up on\n * these), `type_upgrade`/`alter_column` entries whose generated SQL is\n * advisory-comment-only (a SQLite rebuild that would be unsafe, an in-place\n * constraint change SQLite cannot perform), and warning-level report-only\n * advisories (an orphan NOT NULL column that will break inserts, a required\n * column that could not be enforced, a stale unique constraint, a relaxation\n * the caller has not opted into). Info-level advisories (a harmless orphan\n * column, a stale default) and comments that mean \"already handled\" (the\n * no-op advisories a table rebuild leaves on covered siblings, #2370) are\n * deliberately excluded so `hasManualDrift` keeps meaning \"something needs\n * an operator\". Callers use this to distinguish \"schema is in sync\" from\n * \"schema is drifted but we can't fix it from here\".\n */\nfunction collectUnactionableChanges(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n): SchemaChange[] {\n const unactionable: SchemaChange[] = [];\n for (const change of diff.changes) {\n if (change.type === 'type_mismatch') {\n unactionable.push(change);\n continue;\n }\n const statements = change.sqlStatements ?? (change.sql ? [change.sql] : []);\n if (statements.length === 0) {\n if (change.advisory?.severity === 'warning') {\n unactionable.push(change);\n }\n continue;\n }\n if (\n statements.every((stmt) => isCommentOnlySql(stmt.trim())) &&\n !statements.every((stmt) => isNoOpAdvisorySql(stmt))\n ) {\n unactionable.push(change);\n }\n }\n return unactionable;\n}\n"],"mappings":"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"}
|
package/dist/registry/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DomainKnowledgeConfig } from '@happyvertical/smrt-types';
|
|
1
|
+
import { CapabilityEffect, DomainKnowledgeConfig } from '@happyvertical/smrt-types';
|
|
2
2
|
import { SmrtCollection } from '../collection';
|
|
3
3
|
import { CollectionCacheConfig } from '../collection-cache';
|
|
4
4
|
import { FieldOptions } from '../decorators/index.js';
|
|
@@ -11,8 +11,13 @@ import { DeclaredIndexDefinition, SchemaDefinition } from '../schema/types.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export type SmrtObjectConstructor = new (...args: any[]) => SmrtObject;
|
|
13
13
|
export type ApiHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Browser/agent-visible effect classification for a generated API action.
|
|
16
|
+
* Aliases the shared `smrt-types` contract (#2587) — see
|
|
17
|
+
* `CapabilityEffect` there for the fail-closed defaulting rule this
|
|
18
|
+
* classification is bound by.
|
|
19
|
+
*/
|
|
20
|
+
export type ToolEffect = CapabilityEffect;
|
|
16
21
|
export interface ApiSerializerReference {
|
|
17
22
|
/**
|
|
18
23
|
* Module specifier to import the serializer from.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,6 +21,13 @@ export declare function renderForeignKeyOrphanRepair(tableName: string, foreignK
|
|
|
21
21
|
uuidCastSide?: ForeignKeyUuidCastSide;
|
|
22
22
|
nullable?: boolean;
|
|
23
23
|
}): string;
|
|
24
|
+
/**
|
|
25
|
+
* Canonical UUID text shape used by every PostgreSQL guard that has to decide
|
|
26
|
+
* whether a `text` column can be reinterpreted as `uuid`. Exported so the
|
|
27
|
+
* orphan probe, the FK provisioning guard, and the uuid convergence planner
|
|
28
|
+
* all test the same shape (#2608).
|
|
29
|
+
*/
|
|
30
|
+
export declare const CANONICAL_UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
|
|
24
31
|
export declare function schemaForeignKeys(schema: Pick<SchemaDefinition, 'columns'> & Partial<Pick<SchemaDefinition, 'foreignKeys'>>): ForeignKeyDefinition[];
|
|
25
32
|
/** Stable identity for one physical relationship, independent of allocation. */
|
|
26
33
|
export declare function foreignKeyRelationshipKey(foreignKey: Pick<ForeignKeyDefinition, 'column' | 'referencesColumn' | 'referencesTable'>): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foreign-key-ddl.d.ts","sourceRoot":"","sources":["../../src/schema/foreign-key-ddl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,GAC/B,MAAM,CAIR;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,GAC/B,MAAM,CAmBR;AAED,4EAA4E;AAC5E,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED,yDAAyD;AACzD,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED,mFAAmF;AACnF,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAED,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;CAClC,GACL,MAAM,CAcR;AAED,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACf,GACL,MAAM,CAuBR;
|
|
1
|
+
{"version":3,"file":"foreign-key-ddl.d.ts","sourceRoot":"","sources":["../../src/schema/foreign-key-ddl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,GAC/B,MAAM,CAIR;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,GAC/B,MAAM,CAmBR;AAED,4EAA4E;AAC5E,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED,yDAAyD;AACzD,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,CAER;AAED,mFAAmF;AACnF,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAED,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;CAClC,GACL,MAAM,CAcR;AAED,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACf,GACL,MAAM,CAuBR;AAID;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,mEAC+B,CAAC;AAsCnE,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC,GAC/C,oBAAoB,EAAE,CAmBxB;AAED,gFAAgF;AAChF,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,IAAI,CACd,oBAAoB,EACpB,QAAQ,GAAG,kBAAkB,GAAG,iBAAiB,CAClD,GACA,MAAM,CAMR;AAED,yEAAyE;AACzE,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC,EAChD,MAAM,EAAE,cAAc,GACrB,oBAAoB,EAAE,CAuBxB;AAED,+EAA+E;AAC/E,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,cAAc,GAAG,aAAa,CAAC,CAAC,EACjE,MAAM,EAAE,cAAc,GACrB,MAAM,EAAE,CA0BV"}
|
|
@@ -45,6 +45,12 @@ function renderForeignKeyOrphanRepair(tableName, foreignKey, options = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
var FOREIGN_KEY_CHILD_ALIAS = "smrt_fk_child";
|
|
47
47
|
var FOREIGN_KEY_PARENT_ALIAS = "smrt_fk_parent";
|
|
48
|
+
/**
|
|
49
|
+
* Canonical UUID text shape used by every PostgreSQL guard that has to decide
|
|
50
|
+
* whether a `text` column can be reinterpreted as `uuid`. Exported so the
|
|
51
|
+
* orphan probe, the FK provisioning guard, and the uuid convergence planner
|
|
52
|
+
* all test the same shape (#2608).
|
|
53
|
+
*/
|
|
48
54
|
var CANONICAL_UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
|
|
49
55
|
function foreignKeyOrphanParts(tableName, foreignKey, engine = "postgres", uuidComparison = false, uuidCastSide) {
|
|
50
56
|
const childAlias = quoteIdentifier(FOREIGN_KEY_CHILD_ALIAS);
|
|
@@ -110,6 +116,6 @@ function schemaDependenciesForEngine(schema, engine) {
|
|
|
110
116
|
return Array.from(/* @__PURE__ */ new Set([...(schema.dependencies ?? []).filter((dependency) => !disabledTargets.has(dependency) || enabledTargets.has(dependency)), ...enabledTargets]));
|
|
111
117
|
}
|
|
112
118
|
//#endregion
|
|
113
|
-
export { foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyConstraintComment, renderForeignKeyConstraintDrop, renderForeignKeyConstraintValidate, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaDependenciesForEngine, schemaForeignKeys, schemaForeignKeysForEngine };
|
|
119
|
+
export { CANONICAL_UUID_PATTERN, foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyConstraintComment, renderForeignKeyConstraintDrop, renderForeignKeyConstraintValidate, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaDependenciesForEngine, schemaForeignKeys, schemaForeignKeysForEngine };
|
|
114
120
|
|
|
115
121
|
//# sourceMappingURL=foreign-key-ddl.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foreign-key-ddl.js","names":[],"sources":["../../src/schema/foreign-key-ddl.ts"],"sourcesContent":["import type { DatabaseEngine } from './ddl/types.js';\nimport { requireForeignKeyAction } from './foreign-key-policy.js';\nimport { shortenIdentifier } from './index-utils.js';\nimport { quoteIdentifier, quoteStringLiteral } from './sql-identifiers.js';\nimport type { ForeignKeyDefinition, SchemaDefinition } from './types.js';\n\nexport type ForeignKeyUuidCastSide = 'child' | 'parent' | 'both' | 'none';\n\nexport function foreignKeyConstraintName(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string {\n return shortenIdentifier(\n `${tableName}_${foreignKey.column}_${foreignKey.referencesTable}_${foreignKey.referencesColumn}_fkey`,\n );\n}\n\nexport function renderForeignKeyConstraint(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string {\n const action = (value: unknown, clause: 'DELETE' | 'UPDATE') => {\n if (value === undefined) return undefined;\n return requireForeignKeyAction(\n value,\n `${tableName}.${foreignKey.column} ON ${clause}`,\n );\n };\n const onDelete = action(foreignKey.onDelete, 'DELETE');\n const onUpdate = action(foreignKey.onUpdate, 'UPDATE');\n const name = foreignKeyConstraintName(tableName, foreignKey);\n const parts = [\n `CONSTRAINT ${quoteIdentifier(name)}`,\n `FOREIGN KEY (${quoteIdentifier(foreignKey.column)})`,\n `REFERENCES ${quoteIdentifier(foreignKey.referencesTable)} (${quoteIdentifier(foreignKey.referencesColumn)})`,\n ];\n if (onDelete) parts.push(`ON DELETE ${onDelete}`);\n if (onUpdate) parts.push(`ON UPDATE ${onUpdate}`);\n return parts.join(' ');\n}\n\n/** Render a PostgreSQL DROP CONSTRAINT statement for a catalog-owned FK. */\nexport function renderForeignKeyConstraintDrop(\n tableName: string,\n constraintName: string,\n): string {\n return `ALTER TABLE ${quoteIdentifier(tableName)} DROP CONSTRAINT ${quoteIdentifier(constraintName)};`;\n}\n\n/** Render a PostgreSQL VALIDATE CONSTRAINT statement. */\nexport function renderForeignKeyConstraintValidate(\n tableName: string,\n constraintName: string,\n): string {\n return `ALTER TABLE ${quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${quoteIdentifier(constraintName)};`;\n}\n\n/** Render a PostgreSQL constraint comment used to identify framework ownership. */\nexport function renderForeignKeyConstraintComment(\n tableName: string,\n constraintName: string,\n comment: string,\n): string {\n return `COMMENT ON CONSTRAINT ${quoteIdentifier(constraintName)} ON ${quoteIdentifier(tableName)} IS ${quoteStringLiteral(comment)};`;\n}\n\nexport function renderForeignKeyOrphanDetector(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n engine?: DatabaseEngine;\n limitOne?: boolean;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n } = {},\n): string {\n const parts = foreignKeyOrphanParts(\n tableName,\n foreignKey,\n options.engine,\n options.uuidComparison,\n options.uuidCastSide,\n );\n return (\n `SELECT ${parts.childColumn} AS orphan_key FROM ${parts.childTable} ` +\n `LEFT JOIN ${parts.parentTable} ON ${parts.joinPredicate} ` +\n `WHERE ${parts.childColumn} IS NOT NULL AND ${parts.parentColumn} IS NULL` +\n (options.limitOne ? ' LIMIT 1' : '')\n );\n}\n\nexport function renderForeignKeyOrphanRepair(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n engine?: DatabaseEngine;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n nullable?: boolean;\n } = {},\n): string {\n const parts = foreignKeyOrphanParts(\n tableName,\n foreignKey,\n options.engine,\n options.uuidComparison,\n options.uuidCastSide,\n );\n const orphanPredicate =\n `${parts.childColumn} IS NOT NULL AND NOT EXISTS (` +\n `SELECT 1 FROM ${parts.parentTable} WHERE ${parts.joinPredicate})`;\n if (options.nullable === false) {\n return (\n `-- Manual repair required: ${quoteIdentifier(tableName)}.${quoteIdentifier(foreignKey.column)} is NOT NULL. ` +\n `Reassign each orphaned reference to a valid ${quoteIdentifier(foreignKey.referencesTable)}.${quoteIdentifier(foreignKey.referencesColumn)} value, ` +\n 'or deliberately delete rows only after preserving any required child data.'\n );\n }\n return (\n `UPDATE ${quoteIdentifier(tableName)} AS ${parts.childAlias} ` +\n `SET ${quoteIdentifier(foreignKey.column)} = NULL ` +\n `WHERE ${orphanPredicate}`\n );\n}\n\nconst FOREIGN_KEY_CHILD_ALIAS = 'smrt_fk_child';\nconst FOREIGN_KEY_PARENT_ALIAS = 'smrt_fk_parent';\nconst CANONICAL_UUID_PATTERN =\n '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$';\n\nfunction foreignKeyOrphanParts(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n engine: DatabaseEngine = 'postgres',\n uuidComparison = false,\n uuidCastSide?: ForeignKeyUuidCastSide,\n) {\n const childAlias = quoteIdentifier(FOREIGN_KEY_CHILD_ALIAS);\n const parentAlias = quoteIdentifier(FOREIGN_KEY_PARENT_ALIAS);\n const childColumn = `${childAlias}.${quoteIdentifier(foreignKey.column)}`;\n const parentColumn = `${parentAlias}.${quoteIdentifier(foreignKey.referencesColumn)}`;\n const guardedUuidValue = (column: string) =>\n `CASE WHEN ${column}::text ~* '${CANONICAL_UUID_PATTERN}' THEN ${column}::uuid ELSE NULL END`;\n const effectiveUuidCastSide =\n engine === 'postgres' && uuidComparison\n ? (uuidCastSide ?? 'child')\n : undefined;\n const childValue =\n effectiveUuidCastSide === 'child' || effectiveUuidCastSide === 'both'\n ? guardedUuidValue(childColumn)\n : childColumn;\n const parentValue =\n effectiveUuidCastSide === 'parent' || effectiveUuidCastSide === 'both'\n ? guardedUuidValue(parentColumn)\n : parentColumn;\n\n return {\n childAlias,\n childColumn,\n childTable: `${quoteIdentifier(tableName)} AS ${childAlias}`,\n joinPredicate: `${parentValue} = ${childValue}`,\n parentColumn,\n parentTable: `${quoteIdentifier(foreignKey.referencesTable)} AS ${parentAlias}`,\n };\n}\n\nexport function schemaForeignKeys(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'foreignKeys'>>,\n): ForeignKeyDefinition[] {\n if (schema.foreignKeys && schema.foreignKeys.length > 0) {\n return schema.foreignKeys;\n }\n return Object.entries(schema.columns).flatMap(([column, definition]) => {\n if (!definition.foreignKey) return [];\n return [\n {\n column,\n referencesTable: definition.foreignKey.table,\n referencesColumn: definition.foreignKey.column,\n onDelete: definition.foreignKey.onDelete,\n onUpdate: definition.foreignKey.onUpdate,\n ...(definition.foreignKey.engines\n ? { engines: definition.foreignKey.engines }\n : {}),\n },\n ];\n });\n}\n\n/** Stable identity for one physical relationship, independent of allocation. */\nexport function foreignKeyRelationshipKey(\n foreignKey: Pick<\n ForeignKeyDefinition,\n 'column' | 'referencesColumn' | 'referencesTable'\n >,\n): string {\n return JSON.stringify([\n foreignKey.column,\n foreignKey.referencesTable,\n foreignKey.referencesColumn,\n ]);\n}\n\n/** Return only physical constraints explicitly enabled for an engine. */\nexport function schemaForeignKeysForEngine(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'foreignKeys'>>,\n engine: DatabaseEngine,\n): ForeignKeyDefinition[] {\n return schemaForeignKeys(schema).filter((foreignKey) => {\n if (foreignKey.engines === undefined) return true;\n\n const validEngines = new Set<DatabaseEngine>([\n 'postgres',\n 'sqlite',\n 'duckdb',\n 'json',\n ]);\n if (\n foreignKey.engines.length === 0 ||\n foreignKey.engines.some(\n (declaredEngine) => !validEngines.has(declaredEngine),\n )\n ) {\n throw new Error(\n `[DDL:${engine}] Foreign key ${foreignKey.column} declares an invalid physical constraint engine allowlist. Use one or more of: postgres, sqlite, duckdb, json.`,\n );\n }\n\n return foreignKey.engines.includes(engine);\n });\n}\n\n/** Return declared schema dependencies after excluding engine-disabled FKs. */\nexport function schemaDependenciesForEngine(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'dependencies' | 'foreignKeys'>>,\n engine: DatabaseEngine,\n): string[] {\n const foreignKeys = schemaForeignKeys(schema);\n const enabledForeignKeys = schemaForeignKeysForEngine(schema, engine);\n const enabledKeys = new Set(\n enabledForeignKeys.map(foreignKeyRelationshipKey),\n );\n const enabledTargets = new Set(\n enabledForeignKeys.map((foreignKey) => foreignKey.referencesTable),\n );\n const disabledTargets = new Set(\n foreignKeys\n .filter(\n (foreignKey) => !enabledKeys.has(foreignKeyRelationshipKey(foreignKey)),\n )\n .map((foreignKey) => foreignKey.referencesTable),\n );\n\n return Array.from(\n new Set([\n ...(schema.dependencies ?? []).filter(\n (dependency) =>\n !disabledTargets.has(dependency) || enabledTargets.has(dependency),\n ),\n ...enabledTargets,\n ]),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"foreign-key-ddl.js","names":[],"sources":["../../src/schema/foreign-key-ddl.ts"],"sourcesContent":["import type { DatabaseEngine } from './ddl/types.js';\nimport { requireForeignKeyAction } from './foreign-key-policy.js';\nimport { shortenIdentifier } from './index-utils.js';\nimport { quoteIdentifier, quoteStringLiteral } from './sql-identifiers.js';\nimport type { ForeignKeyDefinition, SchemaDefinition } from './types.js';\n\nexport type ForeignKeyUuidCastSide = 'child' | 'parent' | 'both' | 'none';\n\nexport function foreignKeyConstraintName(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string {\n return shortenIdentifier(\n `${tableName}_${foreignKey.column}_${foreignKey.referencesTable}_${foreignKey.referencesColumn}_fkey`,\n );\n}\n\nexport function renderForeignKeyConstraint(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string {\n const action = (value: unknown, clause: 'DELETE' | 'UPDATE') => {\n if (value === undefined) return undefined;\n return requireForeignKeyAction(\n value,\n `${tableName}.${foreignKey.column} ON ${clause}`,\n );\n };\n const onDelete = action(foreignKey.onDelete, 'DELETE');\n const onUpdate = action(foreignKey.onUpdate, 'UPDATE');\n const name = foreignKeyConstraintName(tableName, foreignKey);\n const parts = [\n `CONSTRAINT ${quoteIdentifier(name)}`,\n `FOREIGN KEY (${quoteIdentifier(foreignKey.column)})`,\n `REFERENCES ${quoteIdentifier(foreignKey.referencesTable)} (${quoteIdentifier(foreignKey.referencesColumn)})`,\n ];\n if (onDelete) parts.push(`ON DELETE ${onDelete}`);\n if (onUpdate) parts.push(`ON UPDATE ${onUpdate}`);\n return parts.join(' ');\n}\n\n/** Render a PostgreSQL DROP CONSTRAINT statement for a catalog-owned FK. */\nexport function renderForeignKeyConstraintDrop(\n tableName: string,\n constraintName: string,\n): string {\n return `ALTER TABLE ${quoteIdentifier(tableName)} DROP CONSTRAINT ${quoteIdentifier(constraintName)};`;\n}\n\n/** Render a PostgreSQL VALIDATE CONSTRAINT statement. */\nexport function renderForeignKeyConstraintValidate(\n tableName: string,\n constraintName: string,\n): string {\n return `ALTER TABLE ${quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${quoteIdentifier(constraintName)};`;\n}\n\n/** Render a PostgreSQL constraint comment used to identify framework ownership. */\nexport function renderForeignKeyConstraintComment(\n tableName: string,\n constraintName: string,\n comment: string,\n): string {\n return `COMMENT ON CONSTRAINT ${quoteIdentifier(constraintName)} ON ${quoteIdentifier(tableName)} IS ${quoteStringLiteral(comment)};`;\n}\n\nexport function renderForeignKeyOrphanDetector(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n engine?: DatabaseEngine;\n limitOne?: boolean;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n } = {},\n): string {\n const parts = foreignKeyOrphanParts(\n tableName,\n foreignKey,\n options.engine,\n options.uuidComparison,\n options.uuidCastSide,\n );\n return (\n `SELECT ${parts.childColumn} AS orphan_key FROM ${parts.childTable} ` +\n `LEFT JOIN ${parts.parentTable} ON ${parts.joinPredicate} ` +\n `WHERE ${parts.childColumn} IS NOT NULL AND ${parts.parentColumn} IS NULL` +\n (options.limitOne ? ' LIMIT 1' : '')\n );\n}\n\nexport function renderForeignKeyOrphanRepair(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n engine?: DatabaseEngine;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n nullable?: boolean;\n } = {},\n): string {\n const parts = foreignKeyOrphanParts(\n tableName,\n foreignKey,\n options.engine,\n options.uuidComparison,\n options.uuidCastSide,\n );\n const orphanPredicate =\n `${parts.childColumn} IS NOT NULL AND NOT EXISTS (` +\n `SELECT 1 FROM ${parts.parentTable} WHERE ${parts.joinPredicate})`;\n if (options.nullable === false) {\n return (\n `-- Manual repair required: ${quoteIdentifier(tableName)}.${quoteIdentifier(foreignKey.column)} is NOT NULL. ` +\n `Reassign each orphaned reference to a valid ${quoteIdentifier(foreignKey.referencesTable)}.${quoteIdentifier(foreignKey.referencesColumn)} value, ` +\n 'or deliberately delete rows only after preserving any required child data.'\n );\n }\n return (\n `UPDATE ${quoteIdentifier(tableName)} AS ${parts.childAlias} ` +\n `SET ${quoteIdentifier(foreignKey.column)} = NULL ` +\n `WHERE ${orphanPredicate}`\n );\n}\n\nconst FOREIGN_KEY_CHILD_ALIAS = 'smrt_fk_child';\nconst FOREIGN_KEY_PARENT_ALIAS = 'smrt_fk_parent';\n/**\n * Canonical UUID text shape used by every PostgreSQL guard that has to decide\n * whether a `text` column can be reinterpreted as `uuid`. Exported so the\n * orphan probe, the FK provisioning guard, and the uuid convergence planner\n * all test the same shape (#2608).\n */\nexport const CANONICAL_UUID_PATTERN =\n '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$';\n\nfunction foreignKeyOrphanParts(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n engine: DatabaseEngine = 'postgres',\n uuidComparison = false,\n uuidCastSide?: ForeignKeyUuidCastSide,\n) {\n const childAlias = quoteIdentifier(FOREIGN_KEY_CHILD_ALIAS);\n const parentAlias = quoteIdentifier(FOREIGN_KEY_PARENT_ALIAS);\n const childColumn = `${childAlias}.${quoteIdentifier(foreignKey.column)}`;\n const parentColumn = `${parentAlias}.${quoteIdentifier(foreignKey.referencesColumn)}`;\n const guardedUuidValue = (column: string) =>\n `CASE WHEN ${column}::text ~* '${CANONICAL_UUID_PATTERN}' THEN ${column}::uuid ELSE NULL END`;\n const effectiveUuidCastSide =\n engine === 'postgres' && uuidComparison\n ? (uuidCastSide ?? 'child')\n : undefined;\n const childValue =\n effectiveUuidCastSide === 'child' || effectiveUuidCastSide === 'both'\n ? guardedUuidValue(childColumn)\n : childColumn;\n const parentValue =\n effectiveUuidCastSide === 'parent' || effectiveUuidCastSide === 'both'\n ? guardedUuidValue(parentColumn)\n : parentColumn;\n\n return {\n childAlias,\n childColumn,\n childTable: `${quoteIdentifier(tableName)} AS ${childAlias}`,\n joinPredicate: `${parentValue} = ${childValue}`,\n parentColumn,\n parentTable: `${quoteIdentifier(foreignKey.referencesTable)} AS ${parentAlias}`,\n };\n}\n\nexport function schemaForeignKeys(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'foreignKeys'>>,\n): ForeignKeyDefinition[] {\n if (schema.foreignKeys && schema.foreignKeys.length > 0) {\n return schema.foreignKeys;\n }\n return Object.entries(schema.columns).flatMap(([column, definition]) => {\n if (!definition.foreignKey) return [];\n return [\n {\n column,\n referencesTable: definition.foreignKey.table,\n referencesColumn: definition.foreignKey.column,\n onDelete: definition.foreignKey.onDelete,\n onUpdate: definition.foreignKey.onUpdate,\n ...(definition.foreignKey.engines\n ? { engines: definition.foreignKey.engines }\n : {}),\n },\n ];\n });\n}\n\n/** Stable identity for one physical relationship, independent of allocation. */\nexport function foreignKeyRelationshipKey(\n foreignKey: Pick<\n ForeignKeyDefinition,\n 'column' | 'referencesColumn' | 'referencesTable'\n >,\n): string {\n return JSON.stringify([\n foreignKey.column,\n foreignKey.referencesTable,\n foreignKey.referencesColumn,\n ]);\n}\n\n/** Return only physical constraints explicitly enabled for an engine. */\nexport function schemaForeignKeysForEngine(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'foreignKeys'>>,\n engine: DatabaseEngine,\n): ForeignKeyDefinition[] {\n return schemaForeignKeys(schema).filter((foreignKey) => {\n if (foreignKey.engines === undefined) return true;\n\n const validEngines = new Set<DatabaseEngine>([\n 'postgres',\n 'sqlite',\n 'duckdb',\n 'json',\n ]);\n if (\n foreignKey.engines.length === 0 ||\n foreignKey.engines.some(\n (declaredEngine) => !validEngines.has(declaredEngine),\n )\n ) {\n throw new Error(\n `[DDL:${engine}] Foreign key ${foreignKey.column} declares an invalid physical constraint engine allowlist. Use one or more of: postgres, sqlite, duckdb, json.`,\n );\n }\n\n return foreignKey.engines.includes(engine);\n });\n}\n\n/** Return declared schema dependencies after excluding engine-disabled FKs. */\nexport function schemaDependenciesForEngine(\n schema: Pick<SchemaDefinition, 'columns'> &\n Partial<Pick<SchemaDefinition, 'dependencies' | 'foreignKeys'>>,\n engine: DatabaseEngine,\n): string[] {\n const foreignKeys = schemaForeignKeys(schema);\n const enabledForeignKeys = schemaForeignKeysForEngine(schema, engine);\n const enabledKeys = new Set(\n enabledForeignKeys.map(foreignKeyRelationshipKey),\n );\n const enabledTargets = new Set(\n enabledForeignKeys.map((foreignKey) => foreignKey.referencesTable),\n );\n const disabledTargets = new Set(\n foreignKeys\n .filter(\n (foreignKey) => !enabledKeys.has(foreignKeyRelationshipKey(foreignKey)),\n )\n .map((foreignKey) => foreignKey.referencesTable),\n );\n\n return Array.from(\n new Set([\n ...(schema.dependencies ?? []).filter(\n (dependency) =>\n !disabledTargets.has(dependency) || enabledTargets.has(dependency),\n ),\n ...enabledTargets,\n ]),\n );\n}\n"],"mappings":"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"}
|
|
@@ -79,6 +79,21 @@ export declare class SchemaManager {
|
|
|
79
79
|
uuidComparison?: boolean;
|
|
80
80
|
uuidCastSide?: ForeignKeyUuidCastSide;
|
|
81
81
|
}): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Read the live physical types of one child/parent column pair.
|
|
84
|
+
*
|
|
85
|
+
* Returns `undefined` when either side cannot be introspected (adapter
|
|
86
|
+
* without `getTableSchema`, table not created yet) — callers then keep the
|
|
87
|
+
* pre-#2608 behaviour rather than guessing at a mismatch.
|
|
88
|
+
*/
|
|
89
|
+
private resolveLiveForeignKeyTypes;
|
|
90
|
+
/**
|
|
91
|
+
* Classify which side of an orphan probe needs a guarded uuid cast.
|
|
92
|
+
*
|
|
93
|
+
* Only reachable with matching live types while the #2608 guard rejects
|
|
94
|
+
* every mismatch, but the classification stays correct for callers that
|
|
95
|
+
* reuse the helper with an unguarded pair.
|
|
96
|
+
*/
|
|
82
97
|
private resolvePostgresForeignKeyProbeOptions;
|
|
83
98
|
private normalizeForeignKeyProbeType;
|
|
84
99
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-manager.d.ts","sourceRoot":"","sources":["../../src/schema/schema-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,iBAAiB,EAEvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,sBAAsB,EAK5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"schema-manager.d.ts","sourceRoot":"","sources":["../../src/schema/schema-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,iBAAiB,EAEvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,sBAAsB,EAK5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAqBzE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,OAAO,CAAuB;IAGtC,OAAO,CAAC,MAAM,CAAkC;gBAEpC,EAAE,EAAE,iBAAiB,EAAE,OAAO,GAAE,oBAAyB;IAkBrE;;;;;;;OAOG;IACG,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmH1D;;;;;;;;;OASG;YACW,kBAAkB;IAwChC;;;;;;;;;;;;;OAaG;YACW,iBAAiB;IAqE/B,OAAO,CAAC,eAAe;IAIvB;;;;OAIG;IACG,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB9D;;;;;;OAMG;IACG,wBAAwB,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,EAChC,OAAO,GAAE;QACP,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;KAClC,GACL,OAAO,CAAC,IAAI,CAAC;IAuGhB;;;;;;OAMG;YACW,0BAA0B;IA4BxC;;;;;;OAMG;IACH,OAAO,CAAC,qCAAqC;IA0B7C,OAAO,CAAC,4BAA4B;IAOpC;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB;IASxD;;OAEG;IACH,SAAS,IAAI,cAAc;CAG5B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,iBAAiB,EACrB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,aAAa,CAEf"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { foreignKeyConstraintName, renderForeignKeyConstraint, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair } from "./foreign-key-ddl.js";
|
|
2
2
|
import { detectEngine, getDDLStrategy } from "./ddl/index.js";
|
|
3
3
|
import { planForeignKeyCreation } from "./foreign-key-planner.js";
|
|
4
|
+
import { describeForeignKeyTypeConflict, renderUuidConvergenceRepairHint } from "./uuid-convergence.js";
|
|
4
5
|
import { createLogger } from "@happyvertical/logger";
|
|
5
6
|
//#region src/schema/schema-manager.ts
|
|
6
7
|
/**
|
|
@@ -199,7 +200,27 @@ var SchemaManager = class {
|
|
|
199
200
|
}
|
|
200
201
|
const existingRows = extractRows(existing);
|
|
201
202
|
if (existingRows[0]?.convalidated === true) return;
|
|
202
|
-
const
|
|
203
|
+
const liveTypes = await this.resolveLiveForeignKeyTypes(tableName, foreignKey);
|
|
204
|
+
if (liveTypes && liveTypes.childNormalized !== liveTypes.parentNormalized) {
|
|
205
|
+
const conflict = describeForeignKeyTypeConflict({
|
|
206
|
+
childTable: tableName,
|
|
207
|
+
childColumn: foreignKey.column,
|
|
208
|
+
childType: liveTypes.childType,
|
|
209
|
+
parentTable: foreignKey.referencesTable,
|
|
210
|
+
parentColumn: foreignKey.referencesColumn,
|
|
211
|
+
parentType: liveTypes.parentType
|
|
212
|
+
});
|
|
213
|
+
const textNodes = options.uuidComparison ? [liveTypes.childNormalized === "TEXT" ? {
|
|
214
|
+
table: tableName,
|
|
215
|
+
column: foreignKey.column
|
|
216
|
+
} : void 0, liveTypes.parentNormalized === "TEXT" ? {
|
|
217
|
+
table: foreignKey.referencesTable,
|
|
218
|
+
column: foreignKey.referencesColumn
|
|
219
|
+
} : void 0].filter((node) => node !== void 0) : [];
|
|
220
|
+
const repair = textNodes.length > 0 ? ` Suggested repair: ${renderUuidConvergenceRepairHint(textNodes).join("; ")}` : ` Align ${tableName}.${foreignKey.column} (${liveTypes.childType}) and ${foreignKey.referencesTable}.${foreignKey.referencesColumn} (${liveTypes.parentType}) on one physical type deliberately, then retry.`;
|
|
221
|
+
throw new Error(`[SchemaManager] Cannot add ${constraintName}: incompatible column types. ${conflict}. Converge the columns first, then retry.${repair}`);
|
|
222
|
+
}
|
|
223
|
+
const probeOptions = this.resolvePostgresForeignKeyProbeOptions(options, liveTypes);
|
|
203
224
|
const detector = renderForeignKeyOrphanDetector(tableName, foreignKey, {
|
|
204
225
|
engine: "postgres",
|
|
205
226
|
limitOne: true,
|
|
@@ -221,24 +242,46 @@ var SchemaManager = class {
|
|
|
221
242
|
if (existingRows.length === 0) await this.db.query(`ALTER TABLE ${this.quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`);
|
|
222
243
|
await this.db.query(`ALTER TABLE ${this.quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${this.quoteIdentifier(constraintName)}`);
|
|
223
244
|
}
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Read the live physical types of one child/parent column pair.
|
|
247
|
+
*
|
|
248
|
+
* Returns `undefined` when either side cannot be introspected (adapter
|
|
249
|
+
* without `getTableSchema`, table not created yet) — callers then keep the
|
|
250
|
+
* pre-#2608 behaviour rather than guessing at a mismatch.
|
|
251
|
+
*/
|
|
252
|
+
async resolveLiveForeignKeyTypes(tableName, foreignKey) {
|
|
226
253
|
const childSchema = await this.db.getTableSchema?.(tableName);
|
|
227
254
|
const parentSchema = foreignKey.referencesTable === tableName ? childSchema : await this.db.getTableSchema?.(foreignKey.referencesTable);
|
|
228
255
|
const childType = childSchema?.columns[foreignKey.column]?.type;
|
|
229
256
|
const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;
|
|
230
|
-
if (!childType || !parentType) return
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
257
|
+
if (!childType || !parentType) return void 0;
|
|
258
|
+
return {
|
|
259
|
+
childType,
|
|
260
|
+
parentType,
|
|
261
|
+
childNormalized: this.normalizeForeignKeyProbeType(childType),
|
|
262
|
+
parentNormalized: this.normalizeForeignKeyProbeType(parentType)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Classify which side of an orphan probe needs a guarded uuid cast.
|
|
267
|
+
*
|
|
268
|
+
* Only reachable with matching live types while the #2608 guard rejects
|
|
269
|
+
* every mismatch, but the classification stays correct for callers that
|
|
270
|
+
* reuse the helper with an unguarded pair.
|
|
271
|
+
*/
|
|
272
|
+
resolvePostgresForeignKeyProbeOptions(options, liveTypes) {
|
|
273
|
+
if (!options.uuidComparison || options.uuidCastSide) return options;
|
|
274
|
+
if (!liveTypes) return options;
|
|
275
|
+
const { childNormalized, parentNormalized } = liveTypes;
|
|
276
|
+
if (childNormalized === parentNormalized) return {
|
|
234
277
|
...options,
|
|
235
278
|
uuidCastSide: "none"
|
|
236
279
|
};
|
|
237
|
-
if (
|
|
280
|
+
if (childNormalized === "UUID") return {
|
|
238
281
|
...options,
|
|
239
282
|
uuidCastSide: "parent"
|
|
240
283
|
};
|
|
241
|
-
if (
|
|
284
|
+
if (parentNormalized === "UUID") return {
|
|
242
285
|
...options,
|
|
243
286
|
uuidCastSide: "child"
|
|
244
287
|
};
|