@happyvertical/smrt-core 0.43.6 → 0.43.8
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/schema-paths.md +12 -4
- 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 +2 -1
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +63 -11
- package/dist/migrations/differ.js.map +1 -1
- package/dist/schema/foreign-key-ddl.d.ts +10 -1
- package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
- package/dist/schema/foreign-key-ddl.js +28 -10
- package/dist/schema/foreign-key-ddl.js.map +1 -1
- package/dist/schema/schema-manager.d.ts +8 -1
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +55 -4
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/package.json +10 -10
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DatabaseEngine } from './ddl/types.js';
|
|
2
2
|
import { ForeignKeyDefinition, SchemaDefinition } from './types.js';
|
|
3
|
+
export type ForeignKeyUuidCastSide = 'child' | 'parent' | 'both' | 'none';
|
|
3
4
|
export declare function foreignKeyConstraintName(tableName: string, foreignKey: ForeignKeyDefinition): string;
|
|
4
5
|
export declare function renderForeignKeyConstraint(tableName: string, foreignKey: ForeignKeyDefinition): string;
|
|
5
6
|
/** Render a PostgreSQL DROP CONSTRAINT statement for a catalog-owned FK. */
|
|
@@ -9,9 +10,17 @@ export declare function renderForeignKeyConstraintValidate(tableName: string, co
|
|
|
9
10
|
/** Render a PostgreSQL constraint comment used to identify framework ownership. */
|
|
10
11
|
export declare function renderForeignKeyConstraintComment(tableName: string, constraintName: string, comment: string): string;
|
|
11
12
|
export declare function renderForeignKeyOrphanDetector(tableName: string, foreignKey: ForeignKeyDefinition, options?: {
|
|
13
|
+
engine?: DatabaseEngine;
|
|
12
14
|
limitOne?: boolean;
|
|
15
|
+
uuidComparison?: boolean;
|
|
16
|
+
uuidCastSide?: ForeignKeyUuidCastSide;
|
|
17
|
+
}): string;
|
|
18
|
+
export declare function renderForeignKeyOrphanRepair(tableName: string, foreignKey: ForeignKeyDefinition, options?: {
|
|
19
|
+
engine?: DatabaseEngine;
|
|
20
|
+
uuidComparison?: boolean;
|
|
21
|
+
uuidCastSide?: ForeignKeyUuidCastSide;
|
|
22
|
+
nullable?: boolean;
|
|
13
23
|
}): string;
|
|
14
|
-
export declare function renderForeignKeyOrphanRepair(tableName: string, foreignKey: ForeignKeyDefinition): string;
|
|
15
24
|
export declare function schemaForeignKeys(schema: Pick<SchemaDefinition, 'columns'> & Partial<Pick<SchemaDefinition, 'foreignKeys'>>): ForeignKeyDefinition[];
|
|
16
25
|
/** Stable identity for one physical relationship, independent of allocation. */
|
|
17
26
|
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,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;
|
|
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;AA2CD,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"}
|
|
@@ -34,17 +34,35 @@ function renderForeignKeyConstraintComment(tableName, constraintName, comment) {
|
|
|
34
34
|
return `COMMENT ON CONSTRAINT ${quoteIdentifier(constraintName)} ON ${quoteIdentifier(tableName)} IS ${quoteStringLiteral(comment)};`;
|
|
35
35
|
}
|
|
36
36
|
function renderForeignKeyOrphanDetector(tableName, foreignKey, options = {}) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const parentTable = quoteIdentifier(foreignKey.referencesTable);
|
|
40
|
-
const parentColumn = quoteIdentifier(foreignKey.referencesColumn);
|
|
41
|
-
return `SELECT ${childTable}.${childColumn} AS orphan_key FROM ${childTable} LEFT JOIN ${parentTable} ON ${parentTable}.${parentColumn} = ${childTable}.${childColumn} WHERE ${childTable}.${childColumn} IS NOT NULL AND ${parentTable}.${parentColumn} IS NULL` + (options.limitOne ? " LIMIT 1" : "");
|
|
37
|
+
const parts = foreignKeyOrphanParts(tableName, foreignKey, options.engine, options.uuidComparison, options.uuidCastSide);
|
|
38
|
+
return `SELECT ${parts.childColumn} AS orphan_key FROM ${parts.childTable} LEFT JOIN ${parts.parentTable} ON ${parts.joinPredicate} WHERE ${parts.childColumn} IS NOT NULL AND ${parts.parentColumn} IS NULL` + (options.limitOne ? " LIMIT 1" : "");
|
|
42
39
|
}
|
|
43
|
-
function renderForeignKeyOrphanRepair(tableName, foreignKey) {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
return `
|
|
40
|
+
function renderForeignKeyOrphanRepair(tableName, foreignKey, options = {}) {
|
|
41
|
+
const parts = foreignKeyOrphanParts(tableName, foreignKey, options.engine, options.uuidComparison, options.uuidCastSide);
|
|
42
|
+
const orphanPredicate = `${parts.childColumn} IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${parts.parentTable} WHERE ${parts.joinPredicate})`;
|
|
43
|
+
if (options.nullable === false) return `-- Manual repair required: ${quoteIdentifier(tableName)}.${quoteIdentifier(foreignKey.column)} is NOT NULL. Reassign each orphaned reference to a valid ${quoteIdentifier(foreignKey.referencesTable)}.${quoteIdentifier(foreignKey.referencesColumn)} value, or deliberately delete rows only after preserving any required child data.`;
|
|
44
|
+
return `UPDATE ${quoteIdentifier(tableName)} AS ${parts.childAlias} SET ${quoteIdentifier(foreignKey.column)} = NULL WHERE ${orphanPredicate}`;
|
|
45
|
+
}
|
|
46
|
+
var FOREIGN_KEY_CHILD_ALIAS = "smrt_fk_child";
|
|
47
|
+
var FOREIGN_KEY_PARENT_ALIAS = "smrt_fk_parent";
|
|
48
|
+
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
|
+
function foreignKeyOrphanParts(tableName, foreignKey, engine = "postgres", uuidComparison = false, uuidCastSide) {
|
|
50
|
+
const childAlias = quoteIdentifier(FOREIGN_KEY_CHILD_ALIAS);
|
|
51
|
+
const parentAlias = quoteIdentifier(FOREIGN_KEY_PARENT_ALIAS);
|
|
52
|
+
const childColumn = `${childAlias}.${quoteIdentifier(foreignKey.column)}`;
|
|
53
|
+
const parentColumn = `${parentAlias}.${quoteIdentifier(foreignKey.referencesColumn)}`;
|
|
54
|
+
const guardedUuidValue = (column) => `CASE WHEN ${column}::text ~* '${CANONICAL_UUID_PATTERN}' THEN ${column}::uuid ELSE NULL END`;
|
|
55
|
+
const effectiveUuidCastSide = engine === "postgres" && uuidComparison ? uuidCastSide ?? "child" : void 0;
|
|
56
|
+
const childValue = effectiveUuidCastSide === "child" || effectiveUuidCastSide === "both" ? guardedUuidValue(childColumn) : childColumn;
|
|
57
|
+
const parentValue = effectiveUuidCastSide === "parent" || effectiveUuidCastSide === "both" ? guardedUuidValue(parentColumn) : parentColumn;
|
|
58
|
+
return {
|
|
59
|
+
childAlias,
|
|
60
|
+
childColumn,
|
|
61
|
+
childTable: `${quoteIdentifier(tableName)} AS ${childAlias}`,
|
|
62
|
+
joinPredicate: `${parentValue} = ${childValue}`,
|
|
63
|
+
parentColumn,
|
|
64
|
+
parentTable: `${quoteIdentifier(foreignKey.referencesTable)} AS ${parentAlias}`
|
|
65
|
+
};
|
|
48
66
|
}
|
|
49
67
|
function schemaForeignKeys(schema) {
|
|
50
68
|
if (schema.foreignKeys && schema.foreignKeys.length > 0) return schema.foreignKeys;
|
|
@@ -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 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: { limitOne?: boolean } = {},\n): string {\n const childTable = quoteIdentifier(tableName);\n const childColumn = quoteIdentifier(foreignKey.column);\n const parentTable = quoteIdentifier(foreignKey.referencesTable);\n const parentColumn = quoteIdentifier(foreignKey.referencesColumn);\n return (\n `SELECT ${childTable}.${childColumn} AS orphan_key FROM ${childTable} ` +\n `LEFT JOIN ${parentTable} ON ${parentTable}.${parentColumn} = ${childTable}.${childColumn} ` +\n `WHERE ${childTable}.${childColumn} IS NOT NULL AND ${parentTable}.${parentColumn} IS NULL` +\n (options.limitOne ? ' LIMIT 1' : '')\n );\n}\n\nexport function renderForeignKeyOrphanRepair(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n): string {\n const childTable = quoteIdentifier(tableName);\n const childColumn = quoteIdentifier(foreignKey.column);\n const parentTable = quoteIdentifier(foreignKey.referencesTable);\n const parentColumn = quoteIdentifier(foreignKey.referencesColumn);\n return (\n `DELETE FROM ${childTable} WHERE ${childColumn} IS NOT NULL AND NOT EXISTS (` +\n `SELECT 1 FROM ${parentTable} WHERE ${parentTable}.${parentColumn} = ${childTable}.${childColumn})`\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';\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,5 +1,6 @@
|
|
|
1
1
|
import { DatabaseInterface } from '@happyvertical/sql';
|
|
2
2
|
import { DatabaseEngine, EngineSpecificDDL } from './ddl/index.js';
|
|
3
|
+
import { ForeignKeyUuidCastSide } from './foreign-key-ddl.js';
|
|
3
4
|
import { ForeignKeyDefinition, SchemaDefinition } from './types.js';
|
|
4
5
|
/**
|
|
5
6
|
* Schema Manager Options
|
|
@@ -73,7 +74,13 @@ export declare class SchemaManager {
|
|
|
73
74
|
* before an absent constraint is added as NOT VALID and then explicitly
|
|
74
75
|
* validated. Existing NOT VALID constraints follow the same preflight.
|
|
75
76
|
*/
|
|
76
|
-
ensurePostgresForeignKey(tableName: string, foreignKey: ForeignKeyDefinition
|
|
77
|
+
ensurePostgresForeignKey(tableName: string, foreignKey: ForeignKeyDefinition, options?: {
|
|
78
|
+
nullable?: boolean;
|
|
79
|
+
uuidComparison?: boolean;
|
|
80
|
+
uuidCastSide?: ForeignKeyUuidCastSide;
|
|
81
|
+
}): Promise<void>;
|
|
82
|
+
private resolvePostgresForeignKeyProbeOptions;
|
|
83
|
+
private normalizeForeignKeyProbeType;
|
|
77
84
|
/**
|
|
78
85
|
* Generate DDL without executing (useful for debugging/preview)
|
|
79
86
|
*
|
|
@@ -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;
|
|
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;AAiBzE;;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;YAsDF,qCAAqC;IAkCnD,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"}
|
|
@@ -171,8 +171,16 @@ var SchemaManager = class {
|
|
|
171
171
|
*/
|
|
172
172
|
async ensureTables(schemas) {
|
|
173
173
|
const plan = planForeignKeyCreation(schemas, this.engine);
|
|
174
|
+
const schemasByTable = new Map(schemas.map((schema) => [schema.tableName, schema]));
|
|
174
175
|
for (const schema of plan.schemas) await this.ensureTable(schema);
|
|
175
|
-
for (const { table, foreignKey } of plan.deferredConstraints)
|
|
176
|
+
for (const { table, foreignKey } of plan.deferredConstraints) {
|
|
177
|
+
const source = schemasByTable.get(table);
|
|
178
|
+
const target = schemasByTable.get(foreignKey.referencesTable);
|
|
179
|
+
await this.ensurePostgresForeignKey(table, foreignKey, {
|
|
180
|
+
nullable: source?.columns[foreignKey.column]?.notNull !== true,
|
|
181
|
+
uuidComparison: source?.columns[foreignKey.column]?.type === "UUID" && target?.columns[foreignKey.referencesColumn]?.type === "UUID"
|
|
182
|
+
});
|
|
183
|
+
}
|
|
176
184
|
}
|
|
177
185
|
/**
|
|
178
186
|
* Safely add or validate one PostgreSQL foreign key.
|
|
@@ -181,7 +189,7 @@ var SchemaManager = class {
|
|
|
181
189
|
* before an absent constraint is added as NOT VALID and then explicitly
|
|
182
190
|
* validated. Existing NOT VALID constraints follow the same preflight.
|
|
183
191
|
*/
|
|
184
|
-
async ensurePostgresForeignKey(tableName, foreignKey) {
|
|
192
|
+
async ensurePostgresForeignKey(tableName, foreignKey, options = {}) {
|
|
185
193
|
const constraintName = foreignKeyConstraintName(tableName, foreignKey);
|
|
186
194
|
let existing;
|
|
187
195
|
try {
|
|
@@ -191,17 +199,60 @@ var SchemaManager = class {
|
|
|
191
199
|
}
|
|
192
200
|
const existingRows = extractRows(existing);
|
|
193
201
|
if (existingRows[0]?.convalidated === true) return;
|
|
194
|
-
const
|
|
202
|
+
const probeOptions = await this.resolvePostgresForeignKeyProbeOptions(tableName, foreignKey, options);
|
|
203
|
+
const detector = renderForeignKeyOrphanDetector(tableName, foreignKey, {
|
|
204
|
+
engine: "postgres",
|
|
205
|
+
limitOne: true,
|
|
206
|
+
uuidComparison: probeOptions.uuidComparison,
|
|
207
|
+
uuidCastSide: probeOptions.uuidCastSide
|
|
208
|
+
});
|
|
195
209
|
let orphanResult;
|
|
196
210
|
try {
|
|
197
211
|
orphanResult = await this.db.query(detector);
|
|
198
212
|
} catch (error) {
|
|
199
213
|
throw new Error(`[SchemaManager] Cannot probe ${tableName}.${foreignKey.column} for orphan rows; refusing to add ${constraintName}. Run the detector manually: ${detector}`, { cause: error });
|
|
200
214
|
}
|
|
201
|
-
if (extractRows(orphanResult).length > 0) throw new Error(`[SchemaManager] Cannot add ${constraintName}: existing rows do not match ${foreignKey.referencesTable}.${foreignKey.referencesColumn}. Repair them, then retry. Suggested repair: ${renderForeignKeyOrphanRepair(tableName, foreignKey
|
|
215
|
+
if (extractRows(orphanResult).length > 0) throw new Error(`[SchemaManager] Cannot add ${constraintName}: existing rows do not match ${foreignKey.referencesTable}.${foreignKey.referencesColumn}. Repair them, then retry. Suggested repair: ${renderForeignKeyOrphanRepair(tableName, foreignKey, {
|
|
216
|
+
engine: "postgres",
|
|
217
|
+
nullable: probeOptions.nullable,
|
|
218
|
+
uuidComparison: probeOptions.uuidComparison,
|
|
219
|
+
uuidCastSide: probeOptions.uuidCastSide
|
|
220
|
+
})}`);
|
|
202
221
|
if (existingRows.length === 0) await this.db.query(`ALTER TABLE ${this.quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`);
|
|
203
222
|
await this.db.query(`ALTER TABLE ${this.quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${this.quoteIdentifier(constraintName)}`);
|
|
204
223
|
}
|
|
224
|
+
async resolvePostgresForeignKeyProbeOptions(tableName, foreignKey, options) {
|
|
225
|
+
if (!options.uuidComparison || options.uuidCastSide) return options;
|
|
226
|
+
const childSchema = await this.db.getTableSchema?.(tableName);
|
|
227
|
+
const parentSchema = foreignKey.referencesTable === tableName ? childSchema : await this.db.getTableSchema?.(foreignKey.referencesTable);
|
|
228
|
+
const childType = childSchema?.columns[foreignKey.column]?.type;
|
|
229
|
+
const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;
|
|
230
|
+
if (!childType || !parentType) return options;
|
|
231
|
+
const childTypeNormalized = this.normalizeForeignKeyProbeType(childType);
|
|
232
|
+
const parentTypeNormalized = this.normalizeForeignKeyProbeType(parentType);
|
|
233
|
+
if (childTypeNormalized === parentTypeNormalized) return {
|
|
234
|
+
...options,
|
|
235
|
+
uuidCastSide: "none"
|
|
236
|
+
};
|
|
237
|
+
if (childTypeNormalized === "UUID") return {
|
|
238
|
+
...options,
|
|
239
|
+
uuidCastSide: "parent"
|
|
240
|
+
};
|
|
241
|
+
if (parentTypeNormalized === "UUID") return {
|
|
242
|
+
...options,
|
|
243
|
+
uuidCastSide: "child"
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
...options,
|
|
247
|
+
uuidCastSide: "both"
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
normalizeForeignKeyProbeType(type) {
|
|
251
|
+
const upper = type.toUpperCase().trim();
|
|
252
|
+
if (/^UUID$/i.test(upper)) return "UUID";
|
|
253
|
+
if (/^(TEXT|CLOB|STRING|VARCHAR|CHAR)/i.test(upper)) return "TEXT";
|
|
254
|
+
return upper;
|
|
255
|
+
}
|
|
205
256
|
/**
|
|
206
257
|
* Generate DDL without executing (useful for debugging/preview)
|
|
207
258
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-manager.js","names":[],"sources":["../../src/schema/schema-manager.ts"],"sourcesContent":["/**\n * Schema Manager - Direct DDL Execution for SMRT\n *\n * SMRT handles ALL database maintenance directly.\n * SDK SQL remains a pure query/CRUD layer.\n *\n * This module replaces the use of SDK's syncSchema() with direct DDL execution\n * using engine-specific DDL strategies.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n type DatabaseEngine,\n detectEngine,\n type EngineSpecificDDL,\n getDDLStrategy,\n} from './ddl/index.js';\nimport {\n foreignKeyConstraintName,\n renderForeignKeyConstraint,\n renderForeignKeyOrphanDetector,\n renderForeignKeyOrphanRepair,\n} from './foreign-key-ddl.js';\nimport { planForeignKeyCreation } from './foreign-key-planner.js';\nimport type { ForeignKeyDefinition, SchemaDefinition } from './types.js';\n\n/**\n * Normalize a `db.query` result into an array of rows.\n *\n * Adapters return either a bare row array or a `{ rows }` envelope. This\n * mirrors the prior inline `Array.isArray(result) ? result : result?.rows ?? []`\n * narrowing while keeping the row shape typed for callers.\n */\nfunction extractRows<Row>(result: unknown): Row[] {\n if (Array.isArray(result)) {\n return result as Row[];\n }\n const rows = (result as { rows?: unknown } | null | undefined)?.rows;\n return Array.isArray(rows) ? (rows as Row[]) : [];\n}\n\n/**\n * Schema Manager Options\n */\nexport interface SchemaManagerOptions {\n /** Force engine type (overrides auto-detection) */\n engine?: DatabaseEngine;\n /** Whether to skip triggers (useful for DuckDB-backed JSON) */\n skipTriggers?: boolean;\n /** Log DDL statements before execution */\n debug?: boolean;\n}\n\n/**\n * Schema Manager\n *\n * Handles direct DDL execution for database schema management.\n * Replaces SDK's syncSchema() with SMRT-controlled schema creation.\n */\nexport class SchemaManager {\n private db: DatabaseInterface;\n private engine: DatabaseEngine;\n private options: SchemaManagerOptions;\n // Per-instance logger: the `debug` option must actually emit debug traces, so\n // the level follows it (a fixed level: 'info' would filter every debug call).\n private logger: ReturnType<typeof createLogger>;\n\n constructor(db: DatabaseInterface, options: SchemaManagerOptions = {}) {\n this.db = db;\n this.options = options;\n this.logger = createLogger({ level: options.debug ? 'debug' : 'info' });\n\n // Detect or use provided engine\n if (options.engine) {\n this.engine = options.engine;\n } else if ((db as { exportTable?: unknown }).exportTable) {\n // JSON adapter detected (has unique exportTable method)\n // JSON adapter uses DuckDB internally, but native UUID values do not\n // round-trip through the SDK row objects as canonical UUID strings.\n this.engine = 'json';\n } else {\n this.engine = detectEngine(db.url);\n }\n }\n\n /**\n * Ensure a table exists with the correct schema\n *\n * If table doesn't exist, creates it with DDL from the engine-specific strategy.\n * If table exists but is missing columns, adds them via ALTER TABLE ADD COLUMN.\n *\n * @param schema - The schema definition\n */\n async ensureTable(schema: SchemaDefinition): Promise<void> {\n const { tableName } = schema;\n\n // Check if table already exists\n const exists = await this.db.tableExists(tableName);\n if (exists) {\n // Table exists — check for missing columns and add them\n // This handles the case where a table was created with a partial schema\n // (e.g., base SmrtObject fields only) and needs additional columns\n await this.addMissingColumns(tableName, schema);\n\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Table \"${tableName}\" already exists, checked for missing columns`,\n );\n }\n return;\n }\n\n // Generate DDL using engine-specific strategy\n const strategy = getDDLStrategy(this.engine);\n const ddl: EngineSpecificDDL = {\n createTable: strategy.generateCreateTable(schema),\n indexes: strategy.generateIndexes(schema),\n triggers: strategy.generateTriggers(schema),\n };\n\n // Prefer adapter's syncSchema if available (handles adapter-specific transformations)\n // This is critical for JSON adapter which needs to convert UNIQUE indexes to\n // inline constraints for DuckDB compatibility\n // Adapters that support direct DDL sync expose an optional `syncSchema`\n // method beyond the base `DatabaseInterface`; narrow to it here so both the\n // capability check and the call preserve `this.db` as the receiver.\n const dbWithSync = this.db as DatabaseInterface & {\n syncSchema?: (schema: string) => Promise<void>;\n };\n if (dbWithSync.syncSchema) {\n // Combine DDL into single schema string for adapter's syncSchema\n const fullSchema = [\n ddl.createTable,\n ...ddl.indexes,\n ...(this.options.skipTriggers ? [] : ddl.triggers),\n ].join(';\\n');\n\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Using adapter syncSchema for \"${tableName}\"`,\n { fullSchema },\n );\n }\n\n try {\n await dbWithSync.syncSchema(fullSchema);\n\n // FIX #735: Verify table was actually created\n // Some adapters (notably PostgreSQL) may silently fail to create tables\n // when syncSchema returns without error but doesn't execute the DDL\n const createdSuccessfully = await this.db.tableExists(tableName);\n if (createdSuccessfully) {\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Table \"${tableName}\" created via adapter syncSchema`,\n );\n }\n return;\n }\n\n // Table wasn't created - fall through to direct DDL execution\n this.logger.warn(\n `[SchemaManager] syncSchema returned but table \"${tableName}\" doesn't exist, falling back to direct DDL`,\n );\n } catch (syncError) {\n // syncSchema failed - fall through to direct DDL execution\n this.logger.warn(\n `[SchemaManager] syncSchema failed for \"${tableName}\", falling back to direct DDL`,\n { error: syncError },\n );\n }\n }\n\n // Fallback: Execute DDL directly (for adapters without syncSchema)\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Creating table \"${tableName}\" for ${this.engine} (direct DDL)`,\n { createTable: ddl.createTable },\n );\n }\n\n await this.db.query(ddl.createTable);\n\n // Execute indexes\n for (const indexSQL of ddl.indexes) {\n if (this.options.debug) {\n this.logger.debug(`[SchemaManager] Creating index`, { indexSQL });\n }\n await this.db.query(indexSQL);\n }\n\n // Execute triggers (unless skipped)\n if (!this.options.skipTriggers) {\n for (const triggerSQL of ddl.triggers) {\n if (this.options.debug) {\n this.logger.debug(`[SchemaManager] Creating trigger`, { triggerSQL });\n }\n await this.db.query(triggerSQL);\n }\n }\n\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Table \"${tableName}\" created successfully`,\n );\n }\n }\n\n /**\n * Get existing column names from a database table\n *\n * Uses engine-appropriate introspection:\n * - SQLite/DuckDB: PRAGMA table_info\n * - PostgreSQL: pg_catalog.pg_attribute for the exact search-path relation\n *\n * @param tableName - Name of the table to inspect\n * @returns Set of existing column names with catalog identifier case preserved\n */\n private async getExistingColumns(tableName: string): Promise<Set<string>> {\n const columns = new Set<string>();\n\n try {\n if (this.engine === 'postgres') {\n const result = await this.db.query(\n `SELECT attribute.attname AS column_name\n FROM pg_attribute AS attribute\n WHERE attribute.attrelid = to_regclass(format('%I', $1::text))\n AND attribute.attnum > 0\n AND NOT attribute.attisdropped`,\n tableName,\n );\n const rows = extractRows<{ column_name: string }>(result);\n for (const row of rows) {\n columns.add(row.column_name);\n }\n } else {\n // SQLite and DuckDB use PRAGMA\n const result = await this.db.query(\n `PRAGMA table_info(${this.quoteIdentifier(tableName)})`,\n );\n const rows = extractRows<{ name: string }>(result);\n for (const row of rows) {\n columns.add(row.name);\n }\n }\n } catch (error) {\n // If introspection fails, return empty set (no columns will be added)\n if (this.options.debug) {\n this.logger.warn(\n `[SchemaManager] Failed to introspect columns for \"${tableName}\"`,\n { error },\n );\n }\n }\n\n return columns;\n }\n\n /**\n * Add missing columns to an existing table via ALTER TABLE ADD COLUMN\n *\n * Compares the schema's expected columns against the table's actual columns\n * and adds any that are missing. This handles the race condition where a table\n * is created with base fields by one code path, then a later code path needs\n * additional columns from a more complete schema.\n *\n * SQLite limitation: ALTER TABLE ADD COLUMN cannot add NOT NULL columns\n * without a DEFAULT value. We relax NOT NULL for such columns.\n *\n * @param tableName - Name of the existing table\n * @param schema - The full schema definition with expected columns\n */\n private async addMissingColumns(\n tableName: string,\n schema: SchemaDefinition,\n ): Promise<void> {\n if (!schema.columns || Object.keys(schema.columns).length === 0) {\n return;\n }\n\n const existingColumns = await this.getExistingColumns(tableName);\n if (existingColumns.size === 0) {\n // Introspection failed or returned no columns — skip to avoid errors\n return;\n }\n\n const strategy = getDDLStrategy(this.engine);\n let addedCount = 0;\n\n for (const [colName, colDef] of Object.entries(schema.columns)) {\n if (existingColumns.has(colName)) {\n continue; // Column already exists\n }\n\n // SQLite limitation: ALTER TABLE ADD COLUMN cannot have NOT NULL\n // without a DEFAULT value. Relax NOT NULL for such columns.\n const safeDef = { ...colDef };\n if (\n safeDef.notNull &&\n safeDef.defaultValue === undefined &&\n !safeDef.primaryKey\n ) {\n safeDef.notNull = false;\n }\n\n // Cannot add PRIMARY KEY columns via ALTER TABLE\n if (safeDef.primaryKey) {\n continue;\n }\n\n const colSQL = strategy.generateColumnDefinition(colName, safeDef);\n const alterSQL = `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${colSQL}`;\n\n try {\n await this.db.query(alterSQL);\n addedCount++;\n\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Added column \"${colName}\" to \"${tableName}\"`,\n );\n }\n } catch (error) {\n // Column might already exist (race condition) or other issue\n // Log and continue — don't fail the entire operation\n if (this.options.debug) {\n this.logger.warn(\n `[SchemaManager] Failed to add column \"${colName}\" to \"${tableName}\"`,\n { error },\n );\n }\n }\n }\n\n if (addedCount > 0 && this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Added ${addedCount} missing column(s) to \"${tableName}\"`,\n );\n }\n }\n\n private quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n }\n\n /**\n * Create multiple tables with dependency ordering\n *\n * @param schemas - Array of schema definitions\n */\n async ensureTables(schemas: SchemaDefinition[]): Promise<void> {\n const plan = planForeignKeyCreation(schemas, this.engine);\n\n for (const schema of plan.schemas) {\n await this.ensureTable(schema);\n }\n for (const { table, foreignKey } of plan.deferredConstraints) {\n await this.ensurePostgresForeignKey(table, foreignKey);\n }\n }\n\n /**\n * Safely add or validate one PostgreSQL foreign key.\n *\n * Existing rows are probed with the canonical exact-column orphan detector\n * before an absent constraint is added as NOT VALID and then explicitly\n * validated. Existing NOT VALID constraints follow the same preflight.\n */\n async ensurePostgresForeignKey(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n ): Promise<void> {\n const constraintName = foreignKeyConstraintName(tableName, foreignKey);\n let existing: unknown;\n try {\n existing = await this.db.query(\n `SELECT constraint_row.convalidated FROM pg_constraint AS constraint_row JOIN pg_class AS table_row ON table_row.oid = constraint_row.conrelid JOIN pg_namespace AS namespace_row ON namespace_row.oid = table_row.relnamespace WHERE table_row.relname = $1 AND namespace_row.nspname = current_schema() AND constraint_row.conname = $2 AND constraint_row.contype = 'f' LIMIT 1`,\n [tableName, constraintName],\n );\n } catch (error) {\n throw new Error(\n `[SchemaManager] Cannot verify deferred foreign key ${constraintName}; refusing an unsafe duplicate/add attempt: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n const existingRows = extractRows<{ convalidated?: boolean }>(existing);\n if (existingRows[0]?.convalidated === true) return;\n\n const detector = renderForeignKeyOrphanDetector(tableName, foreignKey, {\n limitOne: true,\n });\n let orphanResult: unknown;\n try {\n orphanResult = await this.db.query(detector);\n } catch (error) {\n throw new Error(\n `[SchemaManager] Cannot probe ${tableName}.${foreignKey.column} for orphan rows; refusing to add ${constraintName}. Run the detector manually: ${detector}`,\n { cause: error },\n );\n }\n if (extractRows(orphanResult).length > 0) {\n throw new Error(\n `[SchemaManager] Cannot add ${constraintName}: existing rows do not match ${foreignKey.referencesTable}.${foreignKey.referencesColumn}. Repair them, then retry. Suggested repair: ${renderForeignKeyOrphanRepair(tableName, foreignKey)}`,\n );\n }\n\n if (existingRows.length === 0) {\n await this.db.query(\n `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`,\n );\n }\n await this.db.query(\n `ALTER TABLE ${this.quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${this.quoteIdentifier(constraintName)}`,\n );\n }\n\n /**\n * Generate DDL without executing (useful for debugging/preview)\n *\n * @param schema - The schema definition\n * @returns Engine-specific DDL\n */\n generateDDL(schema: SchemaDefinition): EngineSpecificDDL {\n const strategy = getDDLStrategy(this.engine);\n return {\n createTable: strategy.generateCreateTable(schema),\n indexes: strategy.generateIndexes(schema),\n triggers: strategy.generateTriggers(schema),\n };\n }\n\n /**\n * Get the detected/configured database engine\n */\n getEngine(): DatabaseEngine {\n return this.engine;\n }\n}\n\n/**\n * Create a SchemaManager instance\n *\n * @param db - Database interface\n * @param options - Optional configuration\n * @returns SchemaManager instance\n */\nexport function createSchemaManager(\n db: DatabaseInterface,\n options?: SchemaManagerOptions,\n): SchemaManager {\n return new SchemaManager(db, options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-manager.js","names":[],"sources":["../../src/schema/schema-manager.ts"],"sourcesContent":["/**\n * Schema Manager - Direct DDL Execution for SMRT\n *\n * SMRT handles ALL database maintenance directly.\n * SDK SQL remains a pure query/CRUD layer.\n *\n * This module replaces the use of SDK's syncSchema() with direct DDL execution\n * using engine-specific DDL strategies.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n type DatabaseEngine,\n detectEngine,\n type EngineSpecificDDL,\n getDDLStrategy,\n} from './ddl/index.js';\nimport {\n type ForeignKeyUuidCastSide,\n foreignKeyConstraintName,\n renderForeignKeyConstraint,\n renderForeignKeyOrphanDetector,\n renderForeignKeyOrphanRepair,\n} from './foreign-key-ddl.js';\nimport { planForeignKeyCreation } from './foreign-key-planner.js';\nimport type { ForeignKeyDefinition, SchemaDefinition } from './types.js';\n\n/**\n * Normalize a `db.query` result into an array of rows.\n *\n * Adapters return either a bare row array or a `{ rows }` envelope. This\n * mirrors the prior inline `Array.isArray(result) ? result : result?.rows ?? []`\n * narrowing while keeping the row shape typed for callers.\n */\nfunction extractRows<Row>(result: unknown): Row[] {\n if (Array.isArray(result)) {\n return result as Row[];\n }\n const rows = (result as { rows?: unknown } | null | undefined)?.rows;\n return Array.isArray(rows) ? (rows as Row[]) : [];\n}\n\n/**\n * Schema Manager Options\n */\nexport interface SchemaManagerOptions {\n /** Force engine type (overrides auto-detection) */\n engine?: DatabaseEngine;\n /** Whether to skip triggers (useful for DuckDB-backed JSON) */\n skipTriggers?: boolean;\n /** Log DDL statements before execution */\n debug?: boolean;\n}\n\n/**\n * Schema Manager\n *\n * Handles direct DDL execution for database schema management.\n * Replaces SDK's syncSchema() with SMRT-controlled schema creation.\n */\nexport class SchemaManager {\n private db: DatabaseInterface;\n private engine: DatabaseEngine;\n private options: SchemaManagerOptions;\n // Per-instance logger: the `debug` option must actually emit debug traces, so\n // the level follows it (a fixed level: 'info' would filter every debug call).\n private logger: ReturnType<typeof createLogger>;\n\n constructor(db: DatabaseInterface, options: SchemaManagerOptions = {}) {\n this.db = db;\n this.options = options;\n this.logger = createLogger({ level: options.debug ? 'debug' : 'info' });\n\n // Detect or use provided engine\n if (options.engine) {\n this.engine = options.engine;\n } else if ((db as { exportTable?: unknown }).exportTable) {\n // JSON adapter detected (has unique exportTable method)\n // JSON adapter uses DuckDB internally, but native UUID values do not\n // round-trip through the SDK row objects as canonical UUID strings.\n this.engine = 'json';\n } else {\n this.engine = detectEngine(db.url);\n }\n }\n\n /**\n * Ensure a table exists with the correct schema\n *\n * If table doesn't exist, creates it with DDL from the engine-specific strategy.\n * If table exists but is missing columns, adds them via ALTER TABLE ADD COLUMN.\n *\n * @param schema - The schema definition\n */\n async ensureTable(schema: SchemaDefinition): Promise<void> {\n const { tableName } = schema;\n\n // Check if table already exists\n const exists = await this.db.tableExists(tableName);\n if (exists) {\n // Table exists — check for missing columns and add them\n // This handles the case where a table was created with a partial schema\n // (e.g., base SmrtObject fields only) and needs additional columns\n await this.addMissingColumns(tableName, schema);\n\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Table \"${tableName}\" already exists, checked for missing columns`,\n );\n }\n return;\n }\n\n // Generate DDL using engine-specific strategy\n const strategy = getDDLStrategy(this.engine);\n const ddl: EngineSpecificDDL = {\n createTable: strategy.generateCreateTable(schema),\n indexes: strategy.generateIndexes(schema),\n triggers: strategy.generateTriggers(schema),\n };\n\n // Prefer adapter's syncSchema if available (handles adapter-specific transformations)\n // This is critical for JSON adapter which needs to convert UNIQUE indexes to\n // inline constraints for DuckDB compatibility\n // Adapters that support direct DDL sync expose an optional `syncSchema`\n // method beyond the base `DatabaseInterface`; narrow to it here so both the\n // capability check and the call preserve `this.db` as the receiver.\n const dbWithSync = this.db as DatabaseInterface & {\n syncSchema?: (schema: string) => Promise<void>;\n };\n if (dbWithSync.syncSchema) {\n // Combine DDL into single schema string for adapter's syncSchema\n const fullSchema = [\n ddl.createTable,\n ...ddl.indexes,\n ...(this.options.skipTriggers ? [] : ddl.triggers),\n ].join(';\\n');\n\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Using adapter syncSchema for \"${tableName}\"`,\n { fullSchema },\n );\n }\n\n try {\n await dbWithSync.syncSchema(fullSchema);\n\n // FIX #735: Verify table was actually created\n // Some adapters (notably PostgreSQL) may silently fail to create tables\n // when syncSchema returns without error but doesn't execute the DDL\n const createdSuccessfully = await this.db.tableExists(tableName);\n if (createdSuccessfully) {\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Table \"${tableName}\" created via adapter syncSchema`,\n );\n }\n return;\n }\n\n // Table wasn't created - fall through to direct DDL execution\n this.logger.warn(\n `[SchemaManager] syncSchema returned but table \"${tableName}\" doesn't exist, falling back to direct DDL`,\n );\n } catch (syncError) {\n // syncSchema failed - fall through to direct DDL execution\n this.logger.warn(\n `[SchemaManager] syncSchema failed for \"${tableName}\", falling back to direct DDL`,\n { error: syncError },\n );\n }\n }\n\n // Fallback: Execute DDL directly (for adapters without syncSchema)\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Creating table \"${tableName}\" for ${this.engine} (direct DDL)`,\n { createTable: ddl.createTable },\n );\n }\n\n await this.db.query(ddl.createTable);\n\n // Execute indexes\n for (const indexSQL of ddl.indexes) {\n if (this.options.debug) {\n this.logger.debug(`[SchemaManager] Creating index`, { indexSQL });\n }\n await this.db.query(indexSQL);\n }\n\n // Execute triggers (unless skipped)\n if (!this.options.skipTriggers) {\n for (const triggerSQL of ddl.triggers) {\n if (this.options.debug) {\n this.logger.debug(`[SchemaManager] Creating trigger`, { triggerSQL });\n }\n await this.db.query(triggerSQL);\n }\n }\n\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Table \"${tableName}\" created successfully`,\n );\n }\n }\n\n /**\n * Get existing column names from a database table\n *\n * Uses engine-appropriate introspection:\n * - SQLite/DuckDB: PRAGMA table_info\n * - PostgreSQL: pg_catalog.pg_attribute for the exact search-path relation\n *\n * @param tableName - Name of the table to inspect\n * @returns Set of existing column names with catalog identifier case preserved\n */\n private async getExistingColumns(tableName: string): Promise<Set<string>> {\n const columns = new Set<string>();\n\n try {\n if (this.engine === 'postgres') {\n const result = await this.db.query(\n `SELECT attribute.attname AS column_name\n FROM pg_attribute AS attribute\n WHERE attribute.attrelid = to_regclass(format('%I', $1::text))\n AND attribute.attnum > 0\n AND NOT attribute.attisdropped`,\n tableName,\n );\n const rows = extractRows<{ column_name: string }>(result);\n for (const row of rows) {\n columns.add(row.column_name);\n }\n } else {\n // SQLite and DuckDB use PRAGMA\n const result = await this.db.query(\n `PRAGMA table_info(${this.quoteIdentifier(tableName)})`,\n );\n const rows = extractRows<{ name: string }>(result);\n for (const row of rows) {\n columns.add(row.name);\n }\n }\n } catch (error) {\n // If introspection fails, return empty set (no columns will be added)\n if (this.options.debug) {\n this.logger.warn(\n `[SchemaManager] Failed to introspect columns for \"${tableName}\"`,\n { error },\n );\n }\n }\n\n return columns;\n }\n\n /**\n * Add missing columns to an existing table via ALTER TABLE ADD COLUMN\n *\n * Compares the schema's expected columns against the table's actual columns\n * and adds any that are missing. This handles the race condition where a table\n * is created with base fields by one code path, then a later code path needs\n * additional columns from a more complete schema.\n *\n * SQLite limitation: ALTER TABLE ADD COLUMN cannot add NOT NULL columns\n * without a DEFAULT value. We relax NOT NULL for such columns.\n *\n * @param tableName - Name of the existing table\n * @param schema - The full schema definition with expected columns\n */\n private async addMissingColumns(\n tableName: string,\n schema: SchemaDefinition,\n ): Promise<void> {\n if (!schema.columns || Object.keys(schema.columns).length === 0) {\n return;\n }\n\n const existingColumns = await this.getExistingColumns(tableName);\n if (existingColumns.size === 0) {\n // Introspection failed or returned no columns — skip to avoid errors\n return;\n }\n\n const strategy = getDDLStrategy(this.engine);\n let addedCount = 0;\n\n for (const [colName, colDef] of Object.entries(schema.columns)) {\n if (existingColumns.has(colName)) {\n continue; // Column already exists\n }\n\n // SQLite limitation: ALTER TABLE ADD COLUMN cannot have NOT NULL\n // without a DEFAULT value. Relax NOT NULL for such columns.\n const safeDef = { ...colDef };\n if (\n safeDef.notNull &&\n safeDef.defaultValue === undefined &&\n !safeDef.primaryKey\n ) {\n safeDef.notNull = false;\n }\n\n // Cannot add PRIMARY KEY columns via ALTER TABLE\n if (safeDef.primaryKey) {\n continue;\n }\n\n const colSQL = strategy.generateColumnDefinition(colName, safeDef);\n const alterSQL = `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${colSQL}`;\n\n try {\n await this.db.query(alterSQL);\n addedCount++;\n\n if (this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Added column \"${colName}\" to \"${tableName}\"`,\n );\n }\n } catch (error) {\n // Column might already exist (race condition) or other issue\n // Log and continue — don't fail the entire operation\n if (this.options.debug) {\n this.logger.warn(\n `[SchemaManager] Failed to add column \"${colName}\" to \"${tableName}\"`,\n { error },\n );\n }\n }\n }\n\n if (addedCount > 0 && this.options.debug) {\n this.logger.debug(\n `[SchemaManager] Added ${addedCount} missing column(s) to \"${tableName}\"`,\n );\n }\n }\n\n private quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n }\n\n /**\n * Create multiple tables with dependency ordering\n *\n * @param schemas - Array of schema definitions\n */\n async ensureTables(schemas: SchemaDefinition[]): Promise<void> {\n const plan = planForeignKeyCreation(schemas, this.engine);\n const schemasByTable = new Map(\n schemas.map((schema) => [schema.tableName, schema]),\n );\n\n for (const schema of plan.schemas) {\n await this.ensureTable(schema);\n }\n for (const { table, foreignKey } of plan.deferredConstraints) {\n const source = schemasByTable.get(table);\n const target = schemasByTable.get(foreignKey.referencesTable);\n await this.ensurePostgresForeignKey(table, foreignKey, {\n nullable: source?.columns[foreignKey.column]?.notNull !== true,\n uuidComparison:\n source?.columns[foreignKey.column]?.type === 'UUID' &&\n target?.columns[foreignKey.referencesColumn]?.type === 'UUID',\n });\n }\n }\n\n /**\n * Safely add or validate one PostgreSQL foreign key.\n *\n * Existing rows are probed with the canonical exact-column orphan detector\n * before an absent constraint is added as NOT VALID and then explicitly\n * validated. Existing NOT VALID constraints follow the same preflight.\n */\n async ensurePostgresForeignKey(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n nullable?: boolean;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n } = {},\n ): Promise<void> {\n const constraintName = foreignKeyConstraintName(tableName, foreignKey);\n let existing: unknown;\n try {\n existing = await this.db.query(\n `SELECT constraint_row.convalidated FROM pg_constraint AS constraint_row JOIN pg_class AS table_row ON table_row.oid = constraint_row.conrelid JOIN pg_namespace AS namespace_row ON namespace_row.oid = table_row.relnamespace WHERE table_row.relname = $1 AND namespace_row.nspname = current_schema() AND constraint_row.conname = $2 AND constraint_row.contype = 'f' LIMIT 1`,\n [tableName, constraintName],\n );\n } catch (error) {\n throw new Error(\n `[SchemaManager] Cannot verify deferred foreign key ${constraintName}; refusing an unsafe duplicate/add attempt: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n const existingRows = extractRows<{ convalidated?: boolean }>(existing);\n if (existingRows[0]?.convalidated === true) return;\n\n const probeOptions = await this.resolvePostgresForeignKeyProbeOptions(\n tableName,\n foreignKey,\n options,\n );\n\n const detector = renderForeignKeyOrphanDetector(tableName, foreignKey, {\n engine: 'postgres',\n limitOne: true,\n uuidComparison: probeOptions.uuidComparison,\n uuidCastSide: probeOptions.uuidCastSide,\n });\n let orphanResult: unknown;\n try {\n orphanResult = await this.db.query(detector);\n } catch (error) {\n throw new Error(\n `[SchemaManager] Cannot probe ${tableName}.${foreignKey.column} for orphan rows; refusing to add ${constraintName}. Run the detector manually: ${detector}`,\n { cause: error },\n );\n }\n if (extractRows(orphanResult).length > 0) {\n throw new Error(\n `[SchemaManager] Cannot add ${constraintName}: existing rows do not match ${foreignKey.referencesTable}.${foreignKey.referencesColumn}. Repair them, then retry. Suggested repair: ${renderForeignKeyOrphanRepair(tableName, foreignKey, { engine: 'postgres', nullable: probeOptions.nullable, uuidComparison: probeOptions.uuidComparison, uuidCastSide: probeOptions.uuidCastSide })}`,\n );\n }\n\n if (existingRows.length === 0) {\n await this.db.query(\n `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`,\n );\n }\n await this.db.query(\n `ALTER TABLE ${this.quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${this.quoteIdentifier(constraintName)}`,\n );\n }\n\n private async resolvePostgresForeignKeyProbeOptions(\n tableName: string,\n foreignKey: ForeignKeyDefinition,\n options: {\n nullable?: boolean;\n uuidComparison?: boolean;\n uuidCastSide?: ForeignKeyUuidCastSide;\n },\n ): Promise<typeof options> {\n if (!options.uuidComparison || options.uuidCastSide) return options;\n\n const childSchema = await this.db.getTableSchema?.(tableName);\n const parentSchema =\n foreignKey.referencesTable === tableName\n ? childSchema\n : await this.db.getTableSchema?.(foreignKey.referencesTable);\n const childType = childSchema?.columns[foreignKey.column]?.type;\n const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;\n if (!childType || !parentType) return options;\n\n const childTypeNormalized = this.normalizeForeignKeyProbeType(childType);\n const parentTypeNormalized = this.normalizeForeignKeyProbeType(parentType);\n if (childTypeNormalized === parentTypeNormalized) {\n return { ...options, uuidCastSide: 'none' };\n }\n if (childTypeNormalized === 'UUID') {\n return { ...options, uuidCastSide: 'parent' };\n }\n if (parentTypeNormalized === 'UUID') {\n return { ...options, uuidCastSide: 'child' };\n }\n return { ...options, uuidCastSide: 'both' };\n }\n\n private normalizeForeignKeyProbeType(type: string): string {\n const upper = type.toUpperCase().trim();\n if (/^UUID$/i.test(upper)) return 'UUID';\n if (/^(TEXT|CLOB|STRING|VARCHAR|CHAR)/i.test(upper)) return 'TEXT';\n return upper;\n }\n\n /**\n * Generate DDL without executing (useful for debugging/preview)\n *\n * @param schema - The schema definition\n * @returns Engine-specific DDL\n */\n generateDDL(schema: SchemaDefinition): EngineSpecificDDL {\n const strategy = getDDLStrategy(this.engine);\n return {\n createTable: strategy.generateCreateTable(schema),\n indexes: strategy.generateIndexes(schema),\n triggers: strategy.generateTriggers(schema),\n };\n }\n\n /**\n * Get the detected/configured database engine\n */\n getEngine(): DatabaseEngine {\n return this.engine;\n }\n}\n\n/**\n * Create a SchemaManager instance\n *\n * @param db - Database interface\n * @param options - Optional configuration\n * @returns SchemaManager instance\n */\nexport function createSchemaManager(\n db: DatabaseInterface,\n options?: SchemaManagerOptions,\n): SchemaManager {\n return new SchemaManager(db, options);\n}\n"],"mappings":"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"}
|