@happyvertical/smrt-core 0.47.2 → 0.49.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 +12 -0
- package/agents/object-runtime.md +7 -0
- package/agents/schema-paths.md +62 -0
- package/agents/system-diagnostics.md +6 -1
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +9 -6
- package/dist/cascade.js.map +1 -1
- package/dist/collection.d.ts +0 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +35 -45
- package/dist/collection.js.map +1 -1
- package/dist/decorators/compatibility.d.ts +1 -1
- package/dist/decorators/compatibility.d.ts.map +1 -1
- package/dist/decorators/compatibility.js +2 -2
- package/dist/decorators/compatibility.js.map +1 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +33 -24
- package/dist/decorators/index.js.map +1 -1
- package/dist/interceptors.d.ts +3 -1
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +3 -2
- package/dist/interceptors.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 +10 -4
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +173 -5
- package/dist/migrations/differ.js.map +1 -1
- package/dist/object.d.ts +1 -5
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +28 -37
- package/dist/object.js.map +1 -1
- package/dist/registry/class-registration.d.ts +2 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +47 -16
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/name-resolver.d.ts.map +1 -1
- package/dist/registry/name-resolver.js +3 -3
- package/dist/registry/name-resolver.js.map +1 -1
- package/dist/registry/relationship-graph.d.ts +2 -0
- package/dist/registry/relationship-graph.d.ts.map +1 -1
- package/dist/registry/relationship-graph.js +60 -5
- package/dist/registry/relationship-graph.js.map +1 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +4 -1
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/shared-state.d.ts +18 -0
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js +25 -1
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/registry/types.d.ts +8 -0
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +32 -5
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +120 -12
- package/dist/registry.js.map +1 -1
- package/dist/relationship-loader.d.ts +6 -0
- package/dist/relationship-loader.d.ts.map +1 -0
- package/dist/relationship-loader.js +35 -0
- package/dist/relationship-loader.js.map +1 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +6 -3
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/schema/foreign-key-ddl.d.ts +8 -0
- package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
- package/dist/schema/foreign-key-ddl.js +12 -1
- package/dist/schema/foreign-key-ddl.js.map +1 -1
- package/dist/schema/foreign-key-orphan-report.d.ts.map +1 -1
- package/dist/schema/foreign-key-orphan-report.js +18 -1
- package/dist/schema/foreign-key-orphan-report.js.map +1 -1
- package/dist/schema/generator.d.ts +2 -0
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +16 -9
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -2
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +74 -0
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/schema/text-cast-probe.d.ts +66 -0
- package/dist/schema/text-cast-probe.d.ts.map +1 -0
- package/dist/schema/text-cast-probe.js +142 -0
- package/dist/schema/text-cast-probe.js.map +1 -0
- package/dist/schema/types.d.ts +28 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +2 -1
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +2 -2
- package/dist/smrt-knowledge.json +11 -11
- package/dist/system/diagnostics.d.ts +22 -0
- package/dist/system/diagnostics.d.ts.map +1 -1
- package/dist/system/diagnostics.js +88 -31
- package/dist/system/diagnostics.js.map +1 -1
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -1
- package/dist/utils.js.map +1 -1
- package/dist/vite-plugin/dev-plane-route.d.ts +5 -0
- package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -0
- package/dist/vite-plugin/dev-plane-route.js +91 -0
- package/dist/vite-plugin/dev-plane-route.js.map +1 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +2 -0
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/package.json +4 -4
|
@@ -3,6 +3,14 @@ import { ForeignKeyDefinition, SchemaDefinition } from './types.js';
|
|
|
3
3
|
export type ForeignKeyUuidCastSide = 'child' | 'parent' | 'both' | 'none';
|
|
4
4
|
export declare function foreignKeyConstraintName(tableName: string, foreignKey: ForeignKeyDefinition): string;
|
|
5
5
|
export declare function renderForeignKeyConstraint(tableName: string, foreignKey: ForeignKeyDefinition): string;
|
|
6
|
+
/**
|
|
7
|
+
* Render the two-statement `ADD ... NOT VALID` + `VALIDATE CONSTRAINT`
|
|
8
|
+
* sequence PostgreSQL uses to add a foreign key without holding a full-table
|
|
9
|
+
* lock during validation. Shared by the differ's normal FK-add planning and
|
|
10
|
+
* `db:migrate --null-orphans` (#2748), which reconstructs this same pair
|
|
11
|
+
* after nulling out orphan references the differ refused to plan around.
|
|
12
|
+
*/
|
|
13
|
+
export declare function renderForeignKeyAddStatements(tableName: string, foreignKey: ForeignKeyDefinition): string[];
|
|
6
14
|
/** Render a PostgreSQL DROP CONSTRAINT statement for a catalog-owned FK. */
|
|
7
15
|
export declare function renderForeignKeyConstraintDrop(tableName: string, constraintName: string): string;
|
|
8
16
|
/** Render a PostgreSQL VALIDATE CONSTRAINT statement. */
|
|
@@ -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;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;CAClC,GACL,MAAM,CAmBR;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;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,kCAAkC,QAKqD,CAAC;AAsCrG,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"}
|
|
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;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,oBAAoB,GAC/B,MAAM,EAAE,CAMV;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;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,sBAAsB,CAAC;CAClC,GACL,MAAM,CAmBR;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;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,kCAAkC,QAKqD,CAAC;AAsCrG,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"}
|
|
@@ -21,6 +21,17 @@ function renderForeignKeyConstraint(tableName, foreignKey) {
|
|
|
21
21
|
if (onUpdate) parts.push(`ON UPDATE ${onUpdate}`);
|
|
22
22
|
return parts.join(" ");
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Render the two-statement `ADD ... NOT VALID` + `VALIDATE CONSTRAINT`
|
|
26
|
+
* sequence PostgreSQL uses to add a foreign key without holding a full-table
|
|
27
|
+
* lock during validation. Shared by the differ's normal FK-add planning and
|
|
28
|
+
* `db:migrate --null-orphans` (#2748), which reconstructs this same pair
|
|
29
|
+
* after nulling out orphan references the differ refused to plan around.
|
|
30
|
+
*/
|
|
31
|
+
function renderForeignKeyAddStatements(tableName, foreignKey) {
|
|
32
|
+
const constraintName = foreignKeyConstraintName(tableName, foreignKey);
|
|
33
|
+
return [`ALTER TABLE ${quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`, `ALTER TABLE ${quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${quoteIdentifier(constraintName)}`];
|
|
34
|
+
}
|
|
24
35
|
/** Render a PostgreSQL DROP CONSTRAINT statement for a catalog-owned FK. */
|
|
25
36
|
function renderForeignKeyConstraintDrop(tableName, constraintName) {
|
|
26
37
|
return `ALTER TABLE ${quoteIdentifier(tableName)} DROP CONSTRAINT ${quoteIdentifier(constraintName)};`;
|
|
@@ -129,6 +140,6 @@ function schemaDependenciesForEngine(schema, engine) {
|
|
|
129
140
|
return Array.from(/* @__PURE__ */ new Set([...(schema.dependencies ?? []).filter((dependency) => !disabledTargets.has(dependency) || enabledTargets.has(dependency)), ...enabledTargets]));
|
|
130
141
|
}
|
|
131
142
|
//#endregion
|
|
132
|
-
export { CANONICAL_UUID_PATTERN, CANONICAL_UUID_SQLITE_GLOB_PATTERN, foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyConstraintComment, renderForeignKeyConstraintDrop, renderForeignKeyConstraintValidate, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaDependenciesForEngine, schemaForeignKeys, schemaForeignKeysForEngine };
|
|
143
|
+
export { CANONICAL_UUID_PATTERN, CANONICAL_UUID_SQLITE_GLOB_PATTERN, foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyAddStatements, renderForeignKeyConstraint, renderForeignKeyConstraintComment, renderForeignKeyConstraintDrop, renderForeignKeyConstraintValidate, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaDependenciesForEngine, schemaForeignKeys, schemaForeignKeysForEngine };
|
|
133
144
|
|
|
134
145
|
//# 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 /**\n * Aggregate the same predicate as `COUNT(*)` instead of listing orphan\n * keys. Used by the read-only per-foreign-key orphan count report\n * (#2753) so the report shares the exact FROM/JOIN/WHERE clause the\n * migration gate probes with, rather than a second hand-written copy.\n */\n countOnly?: 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 const fromClause =\n `${parts.childTable} ` +\n `LEFT JOIN ${parts.parentTable} ON ${parts.joinPredicate} ` +\n `WHERE ${parts.childColumn} IS NOT NULL AND ${parts.parentColumn} IS NULL`;\n if (options.countOnly) {\n return `SELECT COUNT(*) AS orphan_count FROM ${fromClause}`;\n }\n return (\n `SELECT ${parts.childColumn} AS orphan_key FROM ${fromClause}` +\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\n/**\n * The same canonical UUID shape as {@link CANONICAL_UUID_PATTERN}, expressed\n * as a SQLite `GLOB` pattern instead of a regex — SQLite has no built-in\n * regex operator, so a shape probe there cannot push a regex predicate down\n * to the server. `GLOB` is case-sensitive, so callers must `LOWER()` the\n * value first; combine with a `LENGTH(...) = 36` check (`GLOB` alone allows\n * a shorter/longer value to still match a middle substring) so this can run\n * as a whole-table server-side aggregate instead of fetching every row into\n * JS to test in a loop (#2767 review).\n */\nexport const CANONICAL_UUID_SQLITE_GLOB_PATTERN =\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-' +\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-' +\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-' +\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-' +\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]';\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/**\n * Render the two-statement `ADD ... NOT VALID` + `VALIDATE CONSTRAINT`\n * sequence PostgreSQL uses to add a foreign key without holding a full-table\n * lock during validation. Shared by the differ's normal FK-add planning and\n * `db:migrate --null-orphans` (#2748), which reconstructs this same pair\n * after nulling out orphan references the differ refused to plan around.\n */\nexport function renderForeignKeyAddStatements(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string[] {\n const constraintName = foreignKeyConstraintName(tableName, foreignKey);\n return [\n `ALTER TABLE ${quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`,\n `ALTER TABLE ${quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${quoteIdentifier(constraintName)}`,\n ];\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 /**\n * Aggregate the same predicate as `COUNT(*)` instead of listing orphan\n * keys. Used by the read-only per-foreign-key orphan count report\n * (#2753) so the report shares the exact FROM/JOIN/WHERE clause the\n * migration gate probes with, rather than a second hand-written copy.\n */\n countOnly?: 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 const fromClause =\n `${parts.childTable} ` +\n `LEFT JOIN ${parts.parentTable} ON ${parts.joinPredicate} ` +\n `WHERE ${parts.childColumn} IS NOT NULL AND ${parts.parentColumn} IS NULL`;\n if (options.countOnly) {\n return `SELECT COUNT(*) AS orphan_count FROM ${fromClause}`;\n }\n return (\n `SELECT ${parts.childColumn} AS orphan_key FROM ${fromClause}` +\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\n/**\n * The same canonical UUID shape as {@link CANONICAL_UUID_PATTERN}, expressed\n * as a SQLite `GLOB` pattern instead of a regex — SQLite has no built-in\n * regex operator, so a shape probe there cannot push a regex predicate down\n * to the server. `GLOB` is case-sensitive, so callers must `LOWER()` the\n * value first; combine with a `LENGTH(...) = 36` check (`GLOB` alone allows\n * a shorter/longer value to still match a middle substring) so this can run\n * as a whole-table server-side aggregate instead of fetching every row into\n * JS to test in a loop (#2767 review).\n */\nexport const CANONICAL_UUID_SQLITE_GLOB_PATTERN =\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-' +\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-' +\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-' +\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-' +\n '[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]';\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":";;;;AAQA,SAAgB,yBACd,WACA,YACQ;CACR,OAAO,kBACL,GAAG,UAAU,GAAG,WAAW,OAAO,GAAG,WAAW,gBAAgB,GAAG,WAAW,iBAAiB,MACjG;AACF;AAEA,SAAgB,2BACd,WACA,YACQ;CACR,MAAM,UAAU,OAAgB,WAAgC;EAC9D,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAChC,OAAO,wBACL,OACA,GAAG,UAAU,GAAG,WAAW,OAAO,MAAM,QAC1C;CACF;CACA,MAAM,WAAW,OAAO,WAAW,UAAU,QAAQ;CACrD,MAAM,WAAW,OAAO,WAAW,UAAU,QAAQ;CAErD,MAAM,QAAQ;EACZ,cAAc,gBAFH,yBAAyB,WAAW,UAEjB,CAAI;EAClC,gBAAgB,gBAAgB,WAAW,MAAM,EAAE;EACnD,cAAc,gBAAgB,WAAW,eAAe,EAAE,IAAI,gBAAgB,WAAW,gBAAgB,EAAE;CAC7G;CACA,IAAI,UAAU,MAAM,KAAK,aAAa,UAAU;CAChD,IAAI,UAAU,MAAM,KAAK,aAAa,UAAU;CAChD,OAAO,MAAM,KAAK,GAAG;AACvB;;;;;;;;AASA,SAAgB,8BACd,WACA,YACU;CACV,MAAM,iBAAiB,yBAAyB,WAAW,UAAU;CACrE,OAAO,CACL,eAAe,gBAAgB,SAAS,EAAE,OAAO,2BAA2B,WAAW,UAAU,EAAE,aACnG,eAAe,gBAAgB,SAAS,EAAE,uBAAuB,gBAAgB,cAAc,GACjG;AACF;;AAGA,SAAgB,+BACd,WACA,gBACQ;CACR,OAAO,eAAe,gBAAgB,SAAS,EAAE,mBAAmB,gBAAgB,cAAc,EAAE;AACtG;;AAGA,SAAgB,mCACd,WACA,gBACQ;CACR,OAAO,eAAe,gBAAgB,SAAS,EAAE,uBAAuB,gBAAgB,cAAc,EAAE;AAC1G;;AAGA,SAAgB,kCACd,WACA,gBACA,SACQ;CACR,OAAO,yBAAyB,gBAAgB,cAAc,EAAE,MAAM,gBAAgB,SAAS,EAAE,MAAM,mBAAmB,OAAO,EAAE;AACrI;AAEA,SAAgB,+BACd,WACA,YACA,UAYI,CAAC,GACG;CACR,MAAM,QAAQ,sBACZ,WACA,YACA,QAAQ,QACR,QAAQ,gBACR,QAAQ,YACV;CACA,MAAM,aACJ,GAAG,MAAM,WAAW,aACP,MAAM,YAAY,MAAM,MAAM,cAAc,SAChD,MAAM,YAAY,mBAAmB,MAAM,aAAa;CACnE,IAAI,QAAQ,WACV,OAAO,wCAAwC;CAEjD,OACE,UAAU,MAAM,YAAY,sBAAsB,gBACjD,QAAQ,WAAW,aAAa;AAErC;AAEA,SAAgB,6BACd,WACA,YACA,UAKI,CAAC,GACG;CACR,MAAM,QAAQ,sBACZ,WACA,YACA,QAAQ,QACR,QAAQ,gBACR,QAAQ,YACV;CACA,MAAM,kBACJ,GAAG,MAAM,YAAY,6CACJ,MAAM,YAAY,SAAS,MAAM,cAAc;CAClE,IAAI,QAAQ,aAAa,OACvB,OACE,8BAA8B,gBAAgB,SAAS,EAAE,GAAG,gBAAgB,WAAW,MAAM,EAAE,4DAChD,gBAAgB,WAAW,eAAe,EAAE,GAAG,gBAAgB,WAAW,gBAAgB,EAAE;CAI/I,OACE,UAAU,gBAAgB,SAAS,EAAE,MAAM,MAAM,WAAW,OACrD,gBAAgB,WAAW,MAAM,EAAE,gBACjC;AAEb;AAEA,IAAM,0BAA0B;AAChC,IAAM,2BAA2B;;;;;;;AAOjC,IAAa,yBACX;;;;;;;;;;;AAYF,IAAa,qCACX;AAMF,SAAS,sBACP,WACA,YACA,SAAyB,YACzB,iBAAiB,OACjB,cACA;CACA,MAAM,aAAa,gBAAgB,uBAAuB;CAC1D,MAAM,cAAc,gBAAgB,wBAAwB;CAC5D,MAAM,cAAc,GAAG,WAAW,GAAG,gBAAgB,WAAW,MAAM;CACtE,MAAM,eAAe,GAAG,YAAY,GAAG,gBAAgB,WAAW,gBAAgB;CAClF,MAAM,oBAAoB,WACxB,aAAa,OAAO,aAAa,uBAAuB,SAAS,OAAO;CAC1E,MAAM,wBACJ,WAAW,cAAc,iBACpB,gBAAgB,UACjB,KAAA;CACN,MAAM,aACJ,0BAA0B,WAAW,0BAA0B,SAC3D,iBAAiB,WAAW,IAC5B;CACN,MAAM,cACJ,0BAA0B,YAAY,0BAA0B,SAC5D,iBAAiB,YAAY,IAC7B;CAEN,OAAO;EACL;EACA;EACA,YAAY,GAAG,gBAAgB,SAAS,EAAE,MAAM;EAChD,eAAe,GAAG,YAAY,KAAK;EACnC;EACA,aAAa,GAAG,gBAAgB,WAAW,eAAe,EAAE,MAAM;CACpE;AACF;AAEA,SAAgB,kBACd,QAEwB;CACxB,IAAI,OAAO,eAAe,OAAO,YAAY,SAAS,GACpD,OAAO,OAAO;CAEhB,OAAO,OAAO,QAAQ,OAAO,OAAO,CAAC,CAAC,SAAS,CAAC,QAAQ,gBAAgB;EACtE,IAAI,CAAC,WAAW,YAAY,OAAO,CAAC;EACpC,OAAO,CACL;GACE;GACA,iBAAiB,WAAW,WAAW;GACvC,kBAAkB,WAAW,WAAW;GACxC,UAAU,WAAW,WAAW;GAChC,UAAU,WAAW,WAAW;GAChC,GAAI,WAAW,WAAW,UACtB,EAAE,SAAS,WAAW,WAAW,QAAQ,IACzC,CAAC;EACP,CACF;CACF,CAAC;AACH;;AAGA,SAAgB,0BACd,YAIQ;CACR,OAAO,KAAK,UAAU;EACpB,WAAW;EACX,WAAW;EACX,WAAW;CACb,CAAC;AACH;;AAGA,SAAgB,2BACd,QAEA,QACwB;CACxB,OAAO,kBAAkB,MAAM,CAAC,CAAC,QAAQ,eAAe;EACtD,IAAI,WAAW,YAAY,KAAA,GAAW,OAAO;EAE7C,MAAM,+BAAe,IAAI,IAAoB;GAC3C;GACA;GACA;GACA;EACF,CAAC;EACD,IACE,WAAW,QAAQ,WAAW,KAC9B,WAAW,QAAQ,MAChB,mBAAmB,CAAC,aAAa,IAAI,cAAc,CACtD,GAEA,MAAM,IAAI,MACR,QAAQ,OAAO,gBAAgB,WAAW,OAAO,+GACnD;EAGF,OAAO,WAAW,QAAQ,SAAS,MAAM;CAC3C,CAAC;AACH;;AAGA,SAAgB,4BACd,QAEA,QACU;CACV,MAAM,cAAc,kBAAkB,MAAM;CAC5C,MAAM,qBAAqB,2BAA2B,QAAQ,MAAM;CACpE,MAAM,cAAc,IAAI,IACtB,mBAAmB,IAAI,yBAAyB,CAClD;CACA,MAAM,iBAAiB,IAAI,IACzB,mBAAmB,KAAK,eAAe,WAAW,eAAe,CACnE;CACA,MAAM,kBAAkB,IAAI,IAC1B,YACG,QACE,eAAe,CAAC,YAAY,IAAI,0BAA0B,UAAU,CAAC,CACxE,CAAC,CACA,KAAK,eAAe,WAAW,eAAe,CACnD;CAEA,OAAO,MAAM,qBACX,IAAI,IAAI,CACN,IAAI,OAAO,gBAAgB,CAAC,EAAA,CAAG,QAC5B,eACC,CAAC,gBAAgB,IAAI,UAAU,KAAK,eAAe,IAAI,UAAU,CACrE,GACA,GAAG,cACL,CAAC,CACH;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foreign-key-orphan-report.d.ts","sourceRoot":"","sources":["../../src/schema/foreign-key-orphan-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAMrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,IAAI,EAAE,eAAe,GAAG,cAAc,CAAC;CACxC;AAED,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,cAAc,CAAC;IACvB;;;;OAIG;IACH,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAChC,oFAAoF;IACpF,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,oCAAoC;IACnD,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,iBAAiB,EACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,OAAO,GAAE,oCAAyC,GACjD,OAAO,CAAC,2BAA2B,CAAC,
|
|
1
|
+
{"version":3,"file":"foreign-key-orphan-report.d.ts","sourceRoot":"","sources":["../../src/schema/foreign-key-orphan-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAMrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,IAAI,EAAE,eAAe,GAAG,cAAc,CAAC;CACxC;AAED,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,cAAc,CAAC;IACvB;;;;OAIG;IACH,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAChC,oFAAoF;IACpF,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,oCAAoC;IACnD,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,iBAAiB,EACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,OAAO,GAAE,oCAAyC,GACjD,OAAO,CAAC,2BAA2B,CAAC,CAuItC"}
|
|
@@ -13,6 +13,20 @@ import { detectEngine } from "./ddl/index.js";
|
|
|
13
13
|
async function collectForeignKeyOrphanCounts(db, schemas, options = {}) {
|
|
14
14
|
const engine = resolveEngine(db, options.engineHint);
|
|
15
15
|
const liveTables = await listLiveTables(db, engine);
|
|
16
|
+
const fetchLiveSchema = async (tableName) => {
|
|
17
|
+
try {
|
|
18
|
+
return await db.getTableSchema?.(tableName);
|
|
19
|
+
} catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const liveSchemaCache = /* @__PURE__ */ new Map();
|
|
24
|
+
const getLiveSchema = async (tableName) => {
|
|
25
|
+
if (liveSchemaCache.has(tableName)) return liveSchemaCache.get(tableName);
|
|
26
|
+
const liveSchema = await fetchLiveSchema(tableName);
|
|
27
|
+
liveSchemaCache.set(tableName, liveSchema);
|
|
28
|
+
return liveSchema;
|
|
29
|
+
};
|
|
16
30
|
const counts = [];
|
|
17
31
|
const skipped = [];
|
|
18
32
|
for (const [childTable, schema] of Object.entries(schemas)) {
|
|
@@ -42,7 +56,10 @@ async function collectForeignKeyOrphanCounts(db, schemas, options = {}) {
|
|
|
42
56
|
continue;
|
|
43
57
|
}
|
|
44
58
|
const childColumnDefinition = schema.columns[foreignKey.column];
|
|
45
|
-
const
|
|
59
|
+
const liveChildSchema = await getLiveSchema(childTable);
|
|
60
|
+
const manifestNullable = childColumnDefinition?.notNull !== true;
|
|
61
|
+
const liveNotNull = liveChildSchema?.columns[foreignKey.column]?.notNull === true;
|
|
62
|
+
const nullable = manifestNullable && !liveNotNull;
|
|
46
63
|
const parentColumnDefinition = schemas[parentTable]?.columns[foreignKey.referencesColumn];
|
|
47
64
|
const { uuidComparison, uuidCastSide } = await resolveUuidCastSide(db, {
|
|
48
65
|
engine,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foreign-key-orphan-report.js","names":[],"sources":["../../src/schema/foreign-key-orphan-report.ts"],"sourcesContent":["/**\n * Per-foreign-key orphan count report (#2753)\n *\n * `foreign-key-ddl.ts` already builds the orphan probe SQL and the suggested\n * repair the migration differ uses to gate `ADD CONSTRAINT` — deliberately\n * refusing to repair anything, since only the relationship's owner knows\n * whether an orphan should be reassigned, nulled, or deleted. What is\n * missing is the counting: `renderForeignKeyOrphanDetector({ limitOne: true })`\n * makes the probe a gate, not a report.\n *\n * This module runs the same probe as a `COUNT(*)` aggregate (via\n * `renderForeignKeyOrphanDetector({ countOnly: true })`, so the exact\n * FROM/JOIN/WHERE predicate is never duplicated) for every manifest-declared\n * foreign key, and reports child/parent table and column, the orphan count,\n * and whether the child column is nullable — so the two dispositions the\n * repair-SQL builder already distinguishes (manual repair vs. null-out) are\n * visible up front.\n *\n * Strictly read-only and diagnostic: it never repairs, and a relationship\n * whose child or parent table does not exist live is skipped and reported\n * separately rather than failing the whole report.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { detectEngine } from './ddl/index.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport {\n type ForeignKeyUuidCastSide,\n renderForeignKeyOrphanDetector,\n schemaForeignKeysForEngine,\n} from './foreign-key-ddl.js';\nimport type { SchemaDefinition } from './types.js';\n\n/** One manifest foreign key's live orphan count. */\nexport interface ForeignKeyOrphanCount {\n childTable: string;\n childColumn: string;\n parentTable: string;\n parentColumn: string;\n /** Number of live child rows whose reference does not resolve. */\n orphanCount: number;\n /** Whether the child column allows NULL (a null-out repair is possible). */\n nullable: boolean;\n}\n\n/** A manifest foreign key the report could not probe, and why. */\nexport interface ForeignKeyOrphanSkipped {\n childTable: string;\n childColumn: string;\n parentTable: string;\n parentColumn: string;\n reason: string;\n /**\n * `missing_table` is benign and expected (a manifest table not yet\n * migrated, or filtered out of this database). `probe_failed` means the\n * `COUNT(*)` query itself errored — a real signal (permissions, a\n * malformed live column, a genuine SQL failure) that a caller should\n * surface distinctly rather than silently treat as \"no orphans found\".\n */\n kind: 'missing_table' | 'probe_failed';\n}\n\n/** Aggregate result of one orphan-count run. */\nexport interface ForeignKeyOrphanCountReport {\n engine: DatabaseEngine;\n /**\n * Every probed foreign key with a live orphan count, sorted by\n * `orphanCount` descending so the largest problems surface first.\n * Includes relationships with zero orphans.\n */\n counts: ForeignKeyOrphanCount[];\n /** Foreign keys skipped because a table did not exist live, or the probe failed. */\n skipped: ForeignKeyOrphanSkipped[];\n}\n\nexport interface CollectForeignKeyOrphanCountsOptions {\n /** Explicit engine hint for adapters whose URL is empty or ambiguous. */\n engineHint?: string;\n}\n\n/**\n * Probe every manifest-declared foreign key for live orphan rows and report\n * a count per relationship. Read-only: never repairs, never fails the whole\n * report for one bad relationship.\n *\n * @param db Live database connection.\n * @param schemas Manifest schemas, keyed by table name — typically\n * `ObjectRegistry.getAllSchemasAsDefinitions()`.\n */\nexport async function collectForeignKeyOrphanCounts(\n db: DatabaseInterface,\n schemas: Record<string, SchemaDefinition>,\n options: CollectForeignKeyOrphanCountsOptions = {},\n): Promise<ForeignKeyOrphanCountReport> {\n const engine = resolveEngine(db, options.engineHint);\n const liveTables = await listLiveTables(db, engine);\n\n const counts: ForeignKeyOrphanCount[] = [];\n const skipped: ForeignKeyOrphanSkipped[] = [];\n\n for (const [childTable, schema] of Object.entries(schemas)) {\n const foreignKeys = schemaForeignKeysForEngine(schema, engine);\n\n for (const foreignKey of foreignKeys) {\n const parentTable = foreignKey.referencesTable;\n const base = {\n childTable,\n childColumn: foreignKey.column,\n parentTable,\n parentColumn: foreignKey.referencesColumn,\n };\n\n if (!liveTables.has(childTable)) {\n skipped.push({\n ...base,\n kind: 'missing_table',\n reason: `Child table \\`${childTable}\\` does not exist in the live database.`,\n });\n continue;\n }\n if (!liveTables.has(parentTable)) {\n skipped.push({\n ...base,\n kind: 'missing_table',\n reason: `Parent table \\`${parentTable}\\` does not exist in the live database.`,\n });\n continue;\n }\n\n const childColumnDefinition = schema.columns[foreignKey.column];\n const nullable = childColumnDefinition?.notNull !== true;\n const parentColumnDefinition =\n schemas[parentTable]?.columns[foreignKey.referencesColumn];\n const declaredUuidComparison =\n childColumnDefinition?.type === 'UUID' &&\n (parentColumnDefinition === undefined ||\n parentColumnDefinition.type === 'UUID');\n\n // The manifest declaring UUID on both sides is not proof the live\n // columns are actually native uuid yet — #2608 tolerates a legacy\n // component that is still `text` on every side until it converges. Mirror\n // the migration gate's live-type cast-side selection\n // (`SchemaComparer.getForeignKeyOrphanOptions` in migrations/differ.ts)\n // instead of guessing from the manifest alone, or a legacy text/text or\n // text/uuid relationship either errors (`operator does not exist: text\n // = uuid`) or silently mismatches instead of being counted.\n const { uuidComparison, uuidCastSide } = await resolveUuidCastSide(db, {\n engine,\n declaredUuidComparison,\n childTable,\n childColumn: foreignKey.column,\n parentTable,\n parentColumn: foreignKey.referencesColumn,\n });\n\n const sql = renderForeignKeyOrphanDetector(childTable, foreignKey, {\n engine,\n countOnly: true,\n uuidComparison,\n uuidCastSide,\n });\n\n try {\n const result = await db.query(sql);\n const rows = normalizeQueryRows(result);\n const raw = rows[0]?.orphan_count;\n const orphanCount = Number(raw);\n counts.push({\n ...base,\n orphanCount: Number.isFinite(orphanCount) ? orphanCount : 0,\n nullable,\n });\n } catch (error) {\n skipped.push({\n ...base,\n kind: 'probe_failed',\n reason: `Could not probe for orphan rows: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n }\n }\n\n counts.sort((a, b) => b.orphanCount - a.orphanCount);\n\n return { engine, counts, skipped };\n}\n\n/**\n * Resolve whether to guard-cast the probe's join predicate, and which side,\n * from the LIVE column types rather than the manifest's declared types —\n * mirroring `SchemaComparer.getForeignKeyOrphanOptions()` in\n * `migrations/differ.ts` so the diagnostic and the migration gate treat the\n * same relationship the same way. Only PostgreSQL casts at all\n * (`foreignKeyOrphanParts()` ignores `uuidComparison` on every other engine).\n */\nasync function resolveUuidCastSide(\n db: DatabaseInterface,\n options: {\n engine: DatabaseEngine;\n declaredUuidComparison: boolean;\n childTable: string;\n childColumn: string;\n parentTable: string;\n parentColumn: string;\n },\n): Promise<{ uuidComparison: boolean; uuidCastSide?: ForeignKeyUuidCastSide }> {\n const {\n engine,\n declaredUuidComparison,\n childTable,\n childColumn,\n parentTable,\n parentColumn,\n } = options;\n if (!declaredUuidComparison || engine !== 'postgres') {\n return { uuidComparison: false };\n }\n if (typeof db.getTableSchema !== 'function') {\n // No live-type introspection available on this adapter; fall back to\n // the manifest-declared signal with the builder's own default cast side\n // ('child') rather than refusing to count anything.\n return { uuidComparison: true };\n }\n\n const childType = await readLiveColumnType(db, childTable, childColumn);\n const parentType = await readLiveColumnType(db, parentTable, parentColumn);\n if (!childType || !parentType) {\n return { uuidComparison: true };\n }\n\n const childIsUuid = isUuidType(childType);\n const parentIsUuid = isUuidType(parentType);\n if (childIsUuid === parentIsUuid) {\n // Both sides already agree (native uuid/uuid, or the #2608-tolerated\n // legacy text/text component) — compare directly, no guarded cast.\n return { uuidComparison: false };\n }\n return {\n uuidComparison: true,\n uuidCastSide: childIsUuid ? 'parent' : 'child',\n };\n}\n\nfunction isUuidType(type: string): boolean {\n return type.toUpperCase().includes('UUID');\n}\n\nasync function readLiveColumnType(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n): Promise<string | undefined> {\n try {\n const schema = await db.getTableSchema?.(tableName);\n const type = schema?.columns?.[columnName]?.type;\n return type === undefined || type === null ? undefined : String(type);\n } catch {\n // Introspection failure here is not the probe failing — the caller\n // falls back to the manifest-declared signal, and the COUNT(*) probe\n // itself still runs and can report its own failure.\n return undefined;\n }\n}\n\nfunction resolveEngine(\n db: DatabaseInterface,\n engineHint?: string,\n): DatabaseEngine {\n if (typeof (db as { exportTable?: unknown }).exportTable === 'function') {\n return 'json';\n }\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n return detectEngine(db.url || dbWithConfig.config?.url || '', engineHint);\n}\n\n/**\n * Normalize a `db.query()` result to a row array. Adapters disagree on the\n * envelope: most return `{ rows }`, but some (per the same normalization in\n * `migrations/differ.ts`'s `getExistingTables()`) return a bare array. A\n * caller that only checks `.rows` reads every row as absent on the latter,\n * which for `listLiveTables()` means every relationship looks like it has a\n * `missing_table` — a silent false negative, not a thrown error.\n */\nfunction normalizeQueryRows(result: unknown): Record<string, unknown>[] {\n return (\n Array.isArray(result)\n ? result\n : ((result as { rows?: unknown[] })?.rows ?? [])\n ) as Record<string, unknown>[];\n}\n\nasync function listLiveTables(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n): Promise<Set<string>> {\n const sql =\n engine === 'postgres'\n ? `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`\n : `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;\n\n const result = await db.query(sql);\n const rows = normalizeQueryRows(result) as {\n name?: string;\n table_name?: string;\n }[];\n return new Set(\n rows.map((row) => row.name || row.table_name || '').filter(Boolean),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"foreign-key-orphan-report.js","names":[],"sources":["../../src/schema/foreign-key-orphan-report.ts"],"sourcesContent":["/**\n * Per-foreign-key orphan count report (#2753)\n *\n * `foreign-key-ddl.ts` already builds the orphan probe SQL and the suggested\n * repair the migration differ uses to gate `ADD CONSTRAINT` — deliberately\n * refusing to repair anything, since only the relationship's owner knows\n * whether an orphan should be reassigned, nulled, or deleted. What is\n * missing is the counting: `renderForeignKeyOrphanDetector({ limitOne: true })`\n * makes the probe a gate, not a report.\n *\n * This module runs the same probe as a `COUNT(*)` aggregate (via\n * `renderForeignKeyOrphanDetector({ countOnly: true })`, so the exact\n * FROM/JOIN/WHERE predicate is never duplicated) for every manifest-declared\n * foreign key, and reports child/parent table and column, the orphan count,\n * and whether the child column is nullable — so the two dispositions the\n * repair-SQL builder already distinguishes (manual repair vs. null-out) are\n * visible up front.\n *\n * Strictly read-only and diagnostic: it never repairs, and a relationship\n * whose child or parent table does not exist live is skipped and reported\n * separately rather than failing the whole report.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { detectEngine } from './ddl/index.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport {\n type ForeignKeyUuidCastSide,\n renderForeignKeyOrphanDetector,\n schemaForeignKeysForEngine,\n} from './foreign-key-ddl.js';\nimport type { SchemaDefinition } from './types.js';\n\n/** One manifest foreign key's live orphan count. */\nexport interface ForeignKeyOrphanCount {\n childTable: string;\n childColumn: string;\n parentTable: string;\n parentColumn: string;\n /** Number of live child rows whose reference does not resolve. */\n orphanCount: number;\n /** Whether the child column allows NULL (a null-out repair is possible). */\n nullable: boolean;\n}\n\n/** A manifest foreign key the report could not probe, and why. */\nexport interface ForeignKeyOrphanSkipped {\n childTable: string;\n childColumn: string;\n parentTable: string;\n parentColumn: string;\n reason: string;\n /**\n * `missing_table` is benign and expected (a manifest table not yet\n * migrated, or filtered out of this database). `probe_failed` means the\n * `COUNT(*)` query itself errored — a real signal (permissions, a\n * malformed live column, a genuine SQL failure) that a caller should\n * surface distinctly rather than silently treat as \"no orphans found\".\n */\n kind: 'missing_table' | 'probe_failed';\n}\n\n/** Aggregate result of one orphan-count run. */\nexport interface ForeignKeyOrphanCountReport {\n engine: DatabaseEngine;\n /**\n * Every probed foreign key with a live orphan count, sorted by\n * `orphanCount` descending so the largest problems surface first.\n * Includes relationships with zero orphans.\n */\n counts: ForeignKeyOrphanCount[];\n /** Foreign keys skipped because a table did not exist live, or the probe failed. */\n skipped: ForeignKeyOrphanSkipped[];\n}\n\nexport interface CollectForeignKeyOrphanCountsOptions {\n /** Explicit engine hint for adapters whose URL is empty or ambiguous. */\n engineHint?: string;\n}\n\n/**\n * Probe every manifest-declared foreign key for live orphan rows and report\n * a count per relationship. Read-only: never repairs, never fails the whole\n * report for one bad relationship.\n *\n * @param db Live database connection.\n * @param schemas Manifest schemas, keyed by table name — typically\n * `ObjectRegistry.getAllSchemasAsDefinitions()`.\n */\nexport async function collectForeignKeyOrphanCounts(\n db: DatabaseInterface,\n schemas: Record<string, SchemaDefinition>,\n options: CollectForeignKeyOrphanCountsOptions = {},\n): Promise<ForeignKeyOrphanCountReport> {\n const engine = resolveEngine(db, options.engineHint);\n const liveTables = await listLiveTables(db, engine);\n // Cache per table: several foreign keys can share a child table, and\n // `db.getTableSchema` is a live introspection round-trip. Both shipped\n // adapters throw on a failed schema lookup rather than returning `null`\n // (review finding, #2748: an unguarded call here aborted the whole\n // report, one bad child table taking down every other relationship's\n // count) — swallow the same way `readLiveColumnType()` already does\n // below: this introspection failing is not the orphan probe itself\n // failing, so the relationship still gets counted (falling back to\n // manifest-only nullability) or lands in `skipped` via the probe's own\n // `catch`.\n const fetchLiveSchema = async (tableName: string) => {\n try {\n return await db.getTableSchema?.(tableName);\n } catch {\n return undefined;\n }\n };\n const liveSchemaCache = new Map<\n string,\n Awaited<ReturnType<typeof fetchLiveSchema>>\n >();\n const getLiveSchema = async (tableName: string) => {\n if (liveSchemaCache.has(tableName)) {\n return liveSchemaCache.get(tableName);\n }\n const liveSchema = await fetchLiveSchema(tableName);\n liveSchemaCache.set(tableName, liveSchema);\n return liveSchema;\n };\n\n const counts: ForeignKeyOrphanCount[] = [];\n const skipped: ForeignKeyOrphanSkipped[] = [];\n\n for (const [childTable, schema] of Object.entries(schemas)) {\n const foreignKeys = schemaForeignKeysForEngine(schema, engine);\n\n for (const foreignKey of foreignKeys) {\n const parentTable = foreignKey.referencesTable;\n const base = {\n childTable,\n childColumn: foreignKey.column,\n parentTable,\n parentColumn: foreignKey.referencesColumn,\n };\n\n if (!liveTables.has(childTable)) {\n skipped.push({\n ...base,\n kind: 'missing_table',\n reason: `Child table \\`${childTable}\\` does not exist in the live database.`,\n });\n continue;\n }\n if (!liveTables.has(parentTable)) {\n skipped.push({\n ...base,\n kind: 'missing_table',\n reason: `Parent table \\`${parentTable}\\` does not exist in the live database.`,\n });\n continue;\n }\n\n const childColumnDefinition = schema.columns[foreignKey.column];\n // Nullable only when BOTH the manifest and the live column agree\n // (review finding, #2748): a manifest relaxed to nullable while the\n // live column is still physically NOT NULL (relaxation pending, not\n // yet applied) is exactly the drift `db:migrate --null-orphans`\n // refuses to null out — this report must say the same thing, or an\n // operator reading `db:orphans`'s \"null-out possible\" summary and\n // then running `--null-orphans` hits an unconditional refusal for a\n // relationship this report told them was nullable. Mirrors\n // `SchemaComparer.getForeignKeyOrphanOptions()` in\n // `migrations/differ.ts`.\n const liveChildSchema = await getLiveSchema(childTable);\n const manifestNullable = childColumnDefinition?.notNull !== true;\n const liveNotNull =\n liveChildSchema?.columns[foreignKey.column]?.notNull === true;\n const nullable = manifestNullable && !liveNotNull;\n const parentColumnDefinition =\n schemas[parentTable]?.columns[foreignKey.referencesColumn];\n const declaredUuidComparison =\n childColumnDefinition?.type === 'UUID' &&\n (parentColumnDefinition === undefined ||\n parentColumnDefinition.type === 'UUID');\n\n // The manifest declaring UUID on both sides is not proof the live\n // columns are actually native uuid yet — #2608 tolerates a legacy\n // component that is still `text` on every side until it converges. Mirror\n // the migration gate's live-type cast-side selection\n // (`SchemaComparer.getForeignKeyOrphanOptions` in migrations/differ.ts)\n // instead of guessing from the manifest alone, or a legacy text/text or\n // text/uuid relationship either errors (`operator does not exist: text\n // = uuid`) or silently mismatches instead of being counted.\n const { uuidComparison, uuidCastSide } = await resolveUuidCastSide(db, {\n engine,\n declaredUuidComparison,\n childTable,\n childColumn: foreignKey.column,\n parentTable,\n parentColumn: foreignKey.referencesColumn,\n });\n\n const sql = renderForeignKeyOrphanDetector(childTable, foreignKey, {\n engine,\n countOnly: true,\n uuidComparison,\n uuidCastSide,\n });\n\n try {\n const result = await db.query(sql);\n const rows = normalizeQueryRows(result);\n const raw = rows[0]?.orphan_count;\n const orphanCount = Number(raw);\n counts.push({\n ...base,\n orphanCount: Number.isFinite(orphanCount) ? orphanCount : 0,\n nullable,\n });\n } catch (error) {\n skipped.push({\n ...base,\n kind: 'probe_failed',\n reason: `Could not probe for orphan rows: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n }\n }\n\n counts.sort((a, b) => b.orphanCount - a.orphanCount);\n\n return { engine, counts, skipped };\n}\n\n/**\n * Resolve whether to guard-cast the probe's join predicate, and which side,\n * from the LIVE column types rather than the manifest's declared types —\n * mirroring `SchemaComparer.getForeignKeyOrphanOptions()` in\n * `migrations/differ.ts` so the diagnostic and the migration gate treat the\n * same relationship the same way. Only PostgreSQL casts at all\n * (`foreignKeyOrphanParts()` ignores `uuidComparison` on every other engine).\n */\nasync function resolveUuidCastSide(\n db: DatabaseInterface,\n options: {\n engine: DatabaseEngine;\n declaredUuidComparison: boolean;\n childTable: string;\n childColumn: string;\n parentTable: string;\n parentColumn: string;\n },\n): Promise<{ uuidComparison: boolean; uuidCastSide?: ForeignKeyUuidCastSide }> {\n const {\n engine,\n declaredUuidComparison,\n childTable,\n childColumn,\n parentTable,\n parentColumn,\n } = options;\n if (!declaredUuidComparison || engine !== 'postgres') {\n return { uuidComparison: false };\n }\n if (typeof db.getTableSchema !== 'function') {\n // No live-type introspection available on this adapter; fall back to\n // the manifest-declared signal with the builder's own default cast side\n // ('child') rather than refusing to count anything.\n return { uuidComparison: true };\n }\n\n const childType = await readLiveColumnType(db, childTable, childColumn);\n const parentType = await readLiveColumnType(db, parentTable, parentColumn);\n if (!childType || !parentType) {\n return { uuidComparison: true };\n }\n\n const childIsUuid = isUuidType(childType);\n const parentIsUuid = isUuidType(parentType);\n if (childIsUuid === parentIsUuid) {\n // Both sides already agree (native uuid/uuid, or the #2608-tolerated\n // legacy text/text component) — compare directly, no guarded cast.\n return { uuidComparison: false };\n }\n return {\n uuidComparison: true,\n uuidCastSide: childIsUuid ? 'parent' : 'child',\n };\n}\n\nfunction isUuidType(type: string): boolean {\n return type.toUpperCase().includes('UUID');\n}\n\nasync function readLiveColumnType(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n): Promise<string | undefined> {\n try {\n const schema = await db.getTableSchema?.(tableName);\n const type = schema?.columns?.[columnName]?.type;\n return type === undefined || type === null ? undefined : String(type);\n } catch {\n // Introspection failure here is not the probe failing — the caller\n // falls back to the manifest-declared signal, and the COUNT(*) probe\n // itself still runs and can report its own failure.\n return undefined;\n }\n}\n\nfunction resolveEngine(\n db: DatabaseInterface,\n engineHint?: string,\n): DatabaseEngine {\n if (typeof (db as { exportTable?: unknown }).exportTable === 'function') {\n return 'json';\n }\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n return detectEngine(db.url || dbWithConfig.config?.url || '', engineHint);\n}\n\n/**\n * Normalize a `db.query()` result to a row array. Adapters disagree on the\n * envelope: most return `{ rows }`, but some (per the same normalization in\n * `migrations/differ.ts`'s `getExistingTables()`) return a bare array. A\n * caller that only checks `.rows` reads every row as absent on the latter,\n * which for `listLiveTables()` means every relationship looks like it has a\n * `missing_table` — a silent false negative, not a thrown error.\n */\nfunction normalizeQueryRows(result: unknown): Record<string, unknown>[] {\n return (\n Array.isArray(result)\n ? result\n : ((result as { rows?: unknown[] })?.rows ?? [])\n ) as Record<string, unknown>[];\n}\n\nasync function listLiveTables(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n): Promise<Set<string>> {\n const sql =\n engine === 'postgres'\n ? `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`\n : `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;\n\n const result = await db.query(sql);\n const rows = normalizeQueryRows(result) as {\n name?: string;\n table_name?: string;\n }[];\n return new Set(\n rows.map((row) => row.name || row.table_name || '').filter(Boolean),\n );\n}\n"],"mappings":"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"}
|
|
@@ -52,6 +52,7 @@ type SchemaGeneratorConfig = {
|
|
|
52
52
|
*/
|
|
53
53
|
indexes?: DeclaredIndexDefinition[];
|
|
54
54
|
registry?: {
|
|
55
|
+
resolveRelationshipTarget?(className: string, fieldName: string): string | null | undefined;
|
|
55
56
|
getConfig?(className: string): {
|
|
56
57
|
idType?: 'uuid' | 'text';
|
|
57
58
|
};
|
|
@@ -74,6 +75,7 @@ export declare class SchemaGenerator {
|
|
|
74
75
|
private getRegistryTargetIdColumnType;
|
|
75
76
|
private reconcileRegistryForeignKeyColumnTypes;
|
|
76
77
|
private applyRegistryForeignKeys;
|
|
78
|
+
private resolveRegistryForeignKeyTarget;
|
|
77
79
|
/**
|
|
78
80
|
* Whether an existing index already leads with `leadingColumns`, in order.
|
|
79
81
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/schema/generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,eAAe,EACf,SAAS,EAGT,cAAc,EACd,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAU7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAWrD,OAAO,KAAK,EAEV,uBAAuB,EAGvB,gBAAgB,EAEjB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;GAUG;AACH,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AASD,KAAK,qBAAqB,GAAG;IAC3B;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,uBAAuB,EAAE,CAAC;IACpC,QAAQ,CAAC,EAAE;QACT,SAAS,CAAC,CAAC,SAAS,EAAE,MAAM,GAAG;YAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAC;QAC5D,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QAChD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;QACrE,SAAS,CAAC,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QAC1D,UAAU,CAAC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;QAC9C,YAAY,CAAC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KACtD,CAAC;CACH,CAAC;AAEF,qBAAa,eAAe;IAC1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,yBAAyB;IAejC,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,6BAA6B;IA4BrC,OAAO,CAAC,sCAAsC;
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/schema/generator.ts"],"names":[],"mappings":"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"}
|
package/dist/schema/generator.js
CHANGED
|
@@ -49,7 +49,7 @@ var SchemaGenerator = class {
|
|
|
49
49
|
}
|
|
50
50
|
getRegistryTargetIdColumnType(relatedName, registry) {
|
|
51
51
|
if (!relatedName || !registry?.getConfig) return;
|
|
52
|
-
const targetName = relatedName
|
|
52
|
+
const targetName = relatedName;
|
|
53
53
|
const targetNames = [targetName];
|
|
54
54
|
const stiBase = registry.getSTIBase?.(targetName);
|
|
55
55
|
if (stiBase && !targetNames.includes(stiBase)) targetNames.push(stiBase);
|
|
@@ -59,11 +59,11 @@ var SchemaGenerator = class {
|
|
|
59
59
|
if (idType === "uuid") return "UUID";
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
reconcileRegistryForeignKeyColumnTypes(columns, fields, registry) {
|
|
62
|
+
reconcileRegistryForeignKeyColumnTypes(className, columns, fields, registry) {
|
|
63
63
|
if (!registry?.getConfig) return;
|
|
64
64
|
for (const [fieldName, field] of fields.entries()) {
|
|
65
65
|
if (field.type !== "foreignKey" || field._meta?.sqlType) continue;
|
|
66
|
-
const targetIdType = this.getRegistryTargetIdColumnType(
|
|
66
|
+
const targetIdType = this.getRegistryTargetIdColumnType(this.resolveRegistryForeignKeyTarget(className, fieldName, field, registry), registry);
|
|
67
67
|
if (!targetIdType) continue;
|
|
68
68
|
const columnName = this.toSnakeCase(fieldName);
|
|
69
69
|
if (columns[columnName]) columns[columnName] = {
|
|
@@ -72,7 +72,7 @@ var SchemaGenerator = class {
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
applyRegistryForeignKeys(columns, fields, conflictColumns, registry) {
|
|
75
|
+
applyRegistryForeignKeys(className, columns, fields, conflictColumns, registry) {
|
|
76
76
|
const conflictSet = new Set(conflictColumns);
|
|
77
77
|
for (const [fieldName, field] of fields.entries()) {
|
|
78
78
|
if (field.type !== "foreignKey" || !field.related) continue;
|
|
@@ -88,7 +88,9 @@ var SchemaGenerator = class {
|
|
|
88
88
|
column.foreignKey = void 0;
|
|
89
89
|
continue;
|
|
90
90
|
}
|
|
91
|
-
const [
|
|
91
|
+
const [, declaredTargetColumn] = field.related.split(".");
|
|
92
|
+
const targetName = this.resolveRegistryForeignKeyTarget(className, fieldName, field, registry);
|
|
93
|
+
if (!targetName) continue;
|
|
92
94
|
const targetBase = registry?.getSTIBase?.(targetName) || targetName;
|
|
93
95
|
const registeredTargetTable = registry?.getTableName?.(targetBase);
|
|
94
96
|
if (registry && !registeredTargetTable) {
|
|
@@ -111,6 +113,11 @@ var SchemaGenerator = class {
|
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
115
|
}
|
|
116
|
+
resolveRegistryForeignKeyTarget(className, fieldName, field, registry) {
|
|
117
|
+
const target = registry?.resolveRelationshipTarget?.(className, fieldName);
|
|
118
|
+
if (target === null) throw new Error(`Cannot resolve foreign key ${className}.${fieldName}: target constructor is unregistered or the target name is ambiguous`);
|
|
119
|
+
return target ?? field.related?.split(".")[0];
|
|
120
|
+
}
|
|
114
121
|
/**
|
|
115
122
|
* Whether an existing index already leads with `leadingColumns`, in order.
|
|
116
123
|
*
|
|
@@ -641,9 +648,9 @@ var SchemaGenerator = class {
|
|
|
641
648
|
defaultValue: "current_timestamp",
|
|
642
649
|
description: "Last update timestamp"
|
|
643
650
|
};
|
|
644
|
-
this.reconcileRegistryForeignKeyColumnTypes(columns, fields, config?.registry);
|
|
651
|
+
this.reconcileRegistryForeignKeyColumnTypes(className, columns, fields, config?.registry);
|
|
645
652
|
const resolvedConflict = this.resolveConflictTarget("cti", columns, config);
|
|
646
|
-
this.applyRegistryForeignKeys(columns, fields, resolvedConflict.conflictColumns, config?.registry);
|
|
653
|
+
this.applyRegistryForeignKeys(className, columns, fields, resolvedConflict.conflictColumns, config?.registry);
|
|
647
654
|
const indexes = [];
|
|
648
655
|
if (!hasCustomPK) {
|
|
649
656
|
const { conflictColumns, tenantColumn } = resolvedConflict;
|
|
@@ -833,8 +840,8 @@ var SchemaGenerator = class {
|
|
|
833
840
|
const resolvedConflict = this.resolveConflictTarget("sti", columns, config);
|
|
834
841
|
for (const className of allClassNames) {
|
|
835
842
|
const classFields = await ObjectRegistry.getAllFields(className);
|
|
836
|
-
this.reconcileRegistryForeignKeyColumnTypes(columns, classFields, ObjectRegistry);
|
|
837
|
-
this.applyRegistryForeignKeys(columns, classFields, resolvedConflict.conflictColumns, ObjectRegistry);
|
|
843
|
+
this.reconcileRegistryForeignKeyColumnTypes(className, columns, classFields, ObjectRegistry);
|
|
844
|
+
this.applyRegistryForeignKeys(className, columns, classFields, resolvedConflict.conflictColumns, ObjectRegistry);
|
|
838
845
|
}
|
|
839
846
|
const indexes = [];
|
|
840
847
|
this.emitStiConflictIndex(indexes, columns, tableName, config);
|