@happyvertical/smrt-core 0.42.6 → 0.43.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.md +39 -43
- package/agents/collection-reads.md +40 -0
- package/agents/data-query.md +21 -0
- package/agents/latest-related.md +35 -0
- package/agents/memory.md +16 -0
- package/agents/schema-paths.md +29 -15
- package/dist/browser.js +2 -2
- package/dist/cascade.d.ts +7 -6
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +19 -21
- package/dist/cascade.js.map +1 -1
- package/dist/class.d.ts +17 -0
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +30 -0
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts +164 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +345 -10
- package/dist/collection.js.map +1 -1
- package/dist/decorators/index.d.ts +33 -15
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +16 -11
- package/dist/decorators/index.js.map +1 -1
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +10 -2
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators/tool-schema.d.ts +14 -0
- package/dist/generators/tool-schema.d.ts.map +1 -1
- package/dist/generators/tool-schema.js +13 -1
- package/dist/generators/tool-schema.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/interceptors.d.ts +2 -1
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +53 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +62 -1
- package/dist/migrations/differ.d.ts +2 -0
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +75 -0
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +19 -2
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/orchestrate.d.ts.map +1 -1
- package/dist/migrations/orchestrate.js +4 -1
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/object.d.ts +4 -4
- package/dist/object.js +4 -4
- package/dist/object.js.map +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +9 -0
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +6 -0
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +78 -8
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry.d.ts +5 -7
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +6 -8
- package/dist/registry.js.map +1 -1
- package/dist/runtime/types.d.ts +2 -2
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +62 -18
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +5 -1
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -0
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.d.ts +1 -0
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +17 -1
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +1 -1
- package/dist/schema/foreign-key-ddl.d.ts +9 -0
- package/dist/schema/foreign-key-ddl.d.ts.map +1 -0
- package/dist/schema/foreign-key-ddl.js +53 -0
- package/dist/schema/foreign-key-ddl.js.map +1 -0
- package/dist/schema/foreign-key-planner.d.ts +18 -0
- package/dist/schema/foreign-key-planner.d.ts.map +1 -0
- package/dist/schema/foreign-key-planner.js +109 -0
- package/dist/schema/foreign-key-planner.js.map +1 -0
- package/dist/schema/foreign-key-policy.d.ts +21 -0
- package/dist/schema/foreign-key-policy.d.ts.map +1 -0
- package/dist/schema/foreign-key-policy.js +41 -0
- package/dist/schema/foreign-key-policy.js.map +1 -0
- package/dist/schema/generator.d.ts +3 -0
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +54 -10
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/manifest-schema.d.ts.map +1 -1
- package/dist/schema/manifest-schema.js +20 -3
- package/dist/schema/manifest-schema.js.map +1 -1
- package/dist/schema/schema-aggregator.d.ts +7 -1
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +54 -9
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/schema-manager.d.ts +1 -6
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +26 -30
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/schema/types.d.ts +8 -5
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +14 -1
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +63 -8
- package/dist/testing/database.d.ts +11 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +80 -9
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +12 -3
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +24 -3
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +3 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +24 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +4 -4
|
@@ -71,14 +71,22 @@ function manifestIndexesToDefinitions(indexes) {
|
|
|
71
71
|
* fall back to it when the manifest exposes no structured columns.
|
|
72
72
|
*/
|
|
73
73
|
function manifestSchemaToDefinition(schema) {
|
|
74
|
+
const columns = manifestColumnsToDefinitions(schema.columns);
|
|
75
|
+
const foreignKeys = Object.entries(columns).flatMap(([column, definition]) => definition.foreignKey ? [{
|
|
76
|
+
column,
|
|
77
|
+
referencesTable: definition.foreignKey.table,
|
|
78
|
+
referencesColumn: definition.foreignKey.column,
|
|
79
|
+
onDelete: definition.foreignKey.onDelete,
|
|
80
|
+
onUpdate: definition.foreignKey.onUpdate
|
|
81
|
+
}] : []);
|
|
74
82
|
return {
|
|
75
83
|
tableName: schema.tableName,
|
|
76
84
|
ddl: schema.ddl,
|
|
77
|
-
columns
|
|
85
|
+
columns,
|
|
78
86
|
indexes: manifestIndexesToDefinitions(schema.indexes),
|
|
79
87
|
triggers: [],
|
|
80
|
-
foreignKeys
|
|
81
|
-
dependencies:
|
|
88
|
+
foreignKeys,
|
|
89
|
+
dependencies: foreignKeys.map((foreignKey) => foreignKey.referencesTable).filter((dependency) => dependency !== schema.tableName),
|
|
82
90
|
version: schema.version ?? ""
|
|
83
91
|
};
|
|
84
92
|
}
|
|
@@ -93,6 +101,15 @@ function mergeSchemaDefinitionInto(target, incoming) {
|
|
|
93
101
|
indexNames.add(index.name);
|
|
94
102
|
target.indexes.push(index);
|
|
95
103
|
}
|
|
104
|
+
const foreignKeyKeys = new Set(target.foreignKeys.map((foreignKey) => `${foreignKey.column}\0${foreignKey.referencesTable}\0${foreignKey.referencesColumn}`));
|
|
105
|
+
for (const foreignKey of incoming.foreignKeys) {
|
|
106
|
+
const key = `${foreignKey.column}\0${foreignKey.referencesTable}\0${foreignKey.referencesColumn}`;
|
|
107
|
+
if (!foreignKeyKeys.has(key)) {
|
|
108
|
+
foreignKeyKeys.add(key);
|
|
109
|
+
target.foreignKeys.push(foreignKey);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
target.dependencies = Array.from(/* @__PURE__ */ new Set([...target.dependencies, ...incoming.dependencies])).sort();
|
|
96
113
|
}
|
|
97
114
|
/**
|
|
98
115
|
* Collect manifest schemas into physical tables.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-schema.js","names":[],"sources":["../../src/schema/manifest-schema.ts"],"sourcesContent":["/**\n * Manifest schema → structured `SchemaDefinition` helpers (#2358).\n *\n * A manifest's `schema.ddl` string is an engine-neutral CREATE TABLE preview:\n * it carries no indexes and no per-engine type mapping, so nothing that needs\n * an executable schema may treat it as authoritative. Every consumer that\n * reads raw manifest JSON — `SchemaAggregator`, `createIsolatedTestDbFromManifest`\n * in `@happyvertical/smrt-vitest`, third-party tooling — should collect the\n * structured `columns` / `indexes` through this module and render them with\n * `getDDLStrategy(engine)`, exactly as `db:migrate` does.\n *\n * The cached `ddl` string is used only when a manifest object exposes no\n * structured columns at all (hand-authored or pre-structured manifests); that\n * legacy path keeps working but is not engine-complete.\n *\n * Keep this module light — type-only imports plus the DDL strategies and the\n * logger — so it can be re-exported from lightweight entry points without\n * pulling the registry/collection module graph in by itself.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { getDDLStrategy } from './ddl/index.js';\nimport {\n findCreateTableBodyRange,\n getSQLDefinitionComparisonKey,\n getSQLDefinitionIdentifier,\n isSQLTableConstraintDefinition,\n materializeManifestDDLForEngine,\n tokenizeSQLDDLBody,\n} from './ddl/materialize-manifest.js';\nimport type { DatabaseEngine, EngineSpecificDDL } from './ddl/types.js';\nimport { quoteIdentifier } from './sql-identifiers.js';\nimport type {\n ColumnDefinition,\n IndexDefinition,\n SchemaDefinition,\n SQLDataType,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Column shape as it appears in raw manifest JSON. `ManifestColumnDefinition`\n * spells the default `default`; registry-side `ColumnDefinition` spells it\n * `defaultValue`. Hand-authored manifests use either.\n */\nexport interface ManifestColumnLike {\n type: string;\n primaryKey?: boolean;\n referenceKind?: ColumnDefinition['referenceKind'];\n notNull?: boolean;\n unique?: boolean;\n default?: unknown;\n defaultValue?: unknown;\n foreignKey?: ColumnDefinition['foreignKey'];\n check?: string;\n}\n\n/** Index shape as it appears in raw manifest JSON. */\nexport interface ManifestIndexLike {\n name: string;\n columns?: string[];\n unique?: boolean;\n where?: string;\n jsonPath?: IndexDefinition['jsonPath'];\n}\n\n/** The subset of a manifest object's `schema` this module reads. */\nexport interface ManifestSchemaLike {\n tableName: string;\n ddl?: string;\n columns?: Record<string, ManifestColumnLike>;\n indexes?: ManifestIndexLike[];\n version?: string;\n}\n\n/**\n * Convert one raw manifest column map into structured `ColumnDefinition`s.\n * Picks known fields only, so stray manifest keys never leak into DDL.\n */\nexport function manifestColumnsToDefinitions(\n columns: Record<string, ManifestColumnLike> | undefined,\n): Record<string, ColumnDefinition> {\n const result: Record<string, ColumnDefinition> = {};\n for (const [name, column] of Object.entries(columns || {})) {\n if (!column || typeof column.type !== 'string') continue;\n const definition: ColumnDefinition = {\n type: column.type as SQLDataType,\n };\n if (column.primaryKey !== undefined)\n definition.primaryKey = column.primaryKey;\n if (column.referenceKind !== undefined)\n definition.referenceKind = column.referenceKind;\n if (column.notNull !== undefined) definition.notNull = column.notNull;\n if (column.unique !== undefined) definition.unique = column.unique;\n const defaultValue =\n column.defaultValue !== undefined ? column.defaultValue : column.default;\n if (defaultValue !== undefined) definition.defaultValue = defaultValue;\n if (column.foreignKey) definition.foreignKey = { ...column.foreignKey };\n if (column.check) definition.check = column.check;\n result[name] = definition;\n }\n return result;\n}\n\n/**\n * Convert raw manifest indexes into structured `IndexDefinition`s, keeping\n * `where` (partial) and `jsonPath` (expression) targets — the two fields the\n * retired string-based renderers dropped.\n */\nexport function manifestIndexesToDefinitions(\n indexes: ManifestIndexLike[] | undefined,\n): IndexDefinition[] {\n const result: IndexDefinition[] = [];\n for (const index of indexes || []) {\n if (!index?.name) continue;\n const definition: IndexDefinition = {\n name: index.name,\n columns: Array.isArray(index.columns) ? [...index.columns] : [],\n };\n if (index.unique !== undefined) definition.unique = index.unique;\n if (index.where) definition.where = index.where;\n if (index.jsonPath?.column && index.jsonPath.path) {\n definition.jsonPath = { ...index.jsonPath };\n }\n result.push(definition);\n }\n return result;\n}\n\n/**\n * Convert one manifest object's `schema` into a `SchemaDefinition`.\n *\n * The cached `ddl` string is carried along (non-authoritative) so callers can\n * fall back to it when the manifest exposes no structured columns.\n */\nexport function manifestSchemaToDefinition(\n schema: ManifestSchemaLike,\n): SchemaDefinition {\n return {\n tableName: schema.tableName,\n ddl: schema.ddl,\n columns: manifestColumnsToDefinitions(schema.columns),\n indexes: manifestIndexesToDefinitions(schema.indexes),\n triggers: [],\n foreignKeys: [],\n dependencies: [],\n version: schema.version ?? '',\n };\n}\n\n/**\n * One physical table collected from one or more manifest objects (STI\n * subclasses share a table and each contributes columns and indexes).\n */\nexport interface CollectedManifestTable {\n tableName: string;\n /**\n * Structured union of every contributor: columns first-wins by name,\n * indexes deduplicated by name.\n */\n definition: SchemaDefinition;\n /**\n * `true` when every contributor exposed structured columns, i.e. the whole\n * table is rendered by a DDL strategy. `false` means at least one\n * contributor carried only a cached `ddl` string: the CREATE TABLE is then\n * the strategy render of the structured contributors (if any) merged with\n * the cached strings of the column-less ones. The decision is per table,\n * not per contributor.\n */\n structured: boolean;\n /**\n * The contributors' cached `ddl` strings merged column-wise (legacy path).\n * Empty when no contributor carried a `ddl`.\n */\n legacyDdl: string;\n /** `<package>:<ClassName>` labels of the contributors, in encounter order. */\n sources: string[];\n}\n\n/**\n * Deterministic STI merge of two structured definitions: existing columns\n * win, new columns are appended, indexes are deduplicated by name.\n */\nexport function mergeSchemaDefinitionInto(\n target: SchemaDefinition,\n incoming: SchemaDefinition,\n): void {\n for (const [name, column] of Object.entries(incoming.columns)) {\n if (!(name in target.columns)) {\n target.columns[name] = column;\n }\n }\n const indexNames = new Set(target.indexes.map((index) => index.name));\n for (const index of incoming.indexes) {\n if (!indexNames.has(index.name)) {\n indexNames.add(index.name);\n target.indexes.push(index);\n }\n }\n}\n\n/**\n * Collect manifest schemas into physical tables.\n *\n * Accepts the `schema` entries of manifest objects (any package, any manifest)\n * in encounter order and groups them by `tableName`, merging STI contributors\n * structurally. Objects without a `schema`/`tableName` are skipped by the\n * caller; objects with neither columns nor `ddl` are ignored here.\n */\nexport function collectManifestTables(\n entries: Iterable<{ schema: ManifestSchemaLike; source: string }>,\n): Map<string, CollectedManifestTable> {\n const tables = new Map<string, CollectedManifestTable>();\n // Structured contributors whose cached ddl declares table constraints. Only\n // a fully structured table drops them (a mixed table re-merges the string),\n // so the warning is decided after collection.\n const constraintCarriers = new Map<\n string,\n Array<{ source: string; constraints: string[] }>\n >();\n\n for (const { schema, source } of entries) {\n if (!schema?.tableName) continue;\n const definition = manifestSchemaToDefinition(schema);\n const hasColumns = Object.keys(definition.columns).length > 0;\n const ddl = typeof schema.ddl === 'string' ? schema.ddl : '';\n if (!hasColumns && !ddl.trim()) continue;\n\n if (hasColumns && ddl.trim()) {\n const constraints = cachedDdlTableConstraints(ddl);\n if (constraints.length > 0) {\n const carriers = constraintCarriers.get(schema.tableName) ?? [];\n carriers.push({ source, constraints });\n constraintCarriers.set(schema.tableName, carriers);\n }\n }\n\n const existing = tables.get(schema.tableName);\n if (!existing) {\n tables.set(schema.tableName, {\n tableName: schema.tableName,\n definition,\n structured: hasColumns,\n legacyDdl: ddl,\n sources: [source],\n });\n continue;\n }\n\n mergeSchemaDefinitionInto(existing.definition, definition);\n existing.structured = existing.structured && hasColumns;\n existing.legacyDdl = existing.legacyDdl\n ? ddl\n ? mergeManifestDDL(existing.legacyDdl, ddl)\n : existing.legacyDdl\n : ddl;\n existing.sources.push(source);\n }\n\n for (const [tableName, carriers] of constraintCarriers) {\n if (!tables.get(tableName)?.structured) continue;\n for (const { source, constraints } of carriers) {\n warnAboutDroppedTableConstraints(tableName, source, constraints);\n }\n }\n\n return tables;\n}\n\n/**\n * Render a collected table for one engine.\n *\n * Structured tables go through the engine strategy for the CREATE TABLE\n * (per-engine types, inline UNIQUE where the engine needs it) and for every\n * index (partial `WHERE`, JSON-path expressions, engine-specific syntax).\n * Tables with a column-less contributor keep that contributor's cached `ddl`\n * string, made minimally safe with `materializeManifestDDLForEngine`, merged\n * on top of the strategy render of any structured contributors — so a\n * structured STI base never loses its engine typing to a hand-authored\n * subclass, and the subclass's extra columns and table constraints survive.\n * Indexes always render through the strategy — the string never carried them.\n */\nexport function renderCollectedManifestTable(\n table: CollectedManifestTable,\n engine: DatabaseEngine,\n): EngineSpecificDDL {\n const strategy = getDDLStrategy(engine);\n const hasColumns = Object.keys(table.definition.columns).length > 0;\n let createTable: string;\n if (table.structured) {\n createTable = strategy.generateCreateTable(table.definition);\n } else if (hasColumns) {\n createTable = mergeManifestDDL(\n strategy.generateCreateTable(table.definition),\n materializeManifestDDLForEngine(table.legacyDdl, engine),\n );\n } else {\n createTable = materializeManifestDDLForEngine(table.legacyDdl, engine);\n }\n\n // Engines that need UNIQUE inline for ON CONFLICT (DuckDB, JSON) get it\n // from `generateCreateTable` on the structured path and skip those indexes\n // in `generateIndexes`. A cached string never carried them, so a table with\n // a column-less contributor would lose its uniqueness entirely; append the\n // missing constraints as table elements (deduplicated against any the\n // string already declares).\n if (!table.structured && strategy.requiresInlineUnique()) {\n const uniqueElements = table.definition.indexes\n .filter(\n (index) =>\n index.unique &&\n !index.where &&\n !index.jsonPath &&\n index.columns.length > 0,\n )\n .map(\n (index) =>\n `UNIQUE(${index.columns.map((column) => quoteIdentifier(column)).join(', ')})`,\n );\n // Hand-authored strings may spell the same constraint without quotes\n // (`UNIQUE(tenant_id)`); compare quote- and whitespace-insensitively so\n // the synthesized element is not added beside it.\n const declared = new Set(\n cachedDdlTableConstraints(createTable).map(normalizeTableElement),\n );\n const missing = uniqueElements.filter(\n (element) => !declared.has(normalizeTableElement(element)),\n );\n if (missing.length > 0) {\n createTable = mergeManifestDDL(\n createTable,\n `CREATE TABLE ${quoteIdentifier(table.tableName)} (\\n ${missing.join(',\\n ')}\\n)`,\n );\n }\n }\n\n return {\n createTable,\n indexes: strategy.generateIndexes(table.definition),\n triggers: strategy.generateTriggers(table.definition),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Legacy cached-DDL string merge (column-less manifests only)\n// ---------------------------------------------------------------------------\n\nfunction parseDefinitionsFromDDL(ddl: string): {\n columns: Map<string, string>;\n tableElements: string[];\n} {\n const columns = new Map<string, string>();\n const tableElements: string[] = [];\n const bodyRange = findCreateTableBodyRange(ddl);\n if (!bodyRange) return { columns, tableElements };\n const [bodyStart, bodyEnd] = bodyRange;\n\n const segments = tokenizeSQLDDLBody(ddl.slice(bodyStart + 1, bodyEnd));\n\n for (const segment of segments) {\n const identifier = getSQLDefinitionIdentifier(segment);\n if (!identifier) continue;\n if (isSQLTableConstraintDefinition(segment)) {\n tableElements.push(segment);\n continue;\n }\n\n columns.set(identifier.name, segment);\n }\n\n return { columns, tableElements };\n}\n\n/**\n * Table-level constraints (`UNIQUE(...)`, `CHECK (...)`, `CONSTRAINT ...`,\n * ...) declared in a cached CREATE TABLE string. Structured `columns` cannot\n * represent them, so a structured render drops them — exactly as `db:migrate`\n * does. Exposed so callers can warn instead of losing them silently.\n *\n * @internal\n */\nexport function cachedDdlTableConstraints(ddl: string): string[] {\n return parseDefinitionsFromDDL(ddl).tableElements;\n}\n\n/** Quote/whitespace/case-insensitive key for a table constraint element. */\nfunction normalizeTableElement(element: string): string {\n return element.replace(/\"/g, '').replace(/\\s+/g, '').toLowerCase();\n}\n\nconst warnedDroppedConstraints = new Set<string>();\n\n/**\n * Warn once per process per table+source when a structured contributor's\n * cached `ddl` declares table constraints that the structured render cannot\n * carry. The structured schema is authoritative: a constraint that lives only\n * in the string never reaches `db:migrate` either, so rendering it here would\n * recreate the test/production drift #2358 removes.\n */\nfunction warnAboutDroppedTableConstraints(\n tableName: string,\n source: string,\n constraints: string[],\n): void {\n const key = `${tableName}\\0${source}`;\n if (warnedDroppedConstraints.has(key)) return;\n warnedDroppedConstraints.add(key);\n logger.warn(\n `[manifest-schema] ${source}: cached ddl for table \"${tableName}\" declares ` +\n `${constraints.length} table constraint(s) that structured columns cannot ` +\n `carry (${constraints.join('; ')}). They are not rendered — the ` +\n `structured schema is authoritative (#2358). Declare uniqueness through ` +\n `indexes/conflictColumns or a column-level constraint instead.`,\n );\n}\n\n/**\n * Merge two cached CREATE TABLE strings for the same table (STI subclasses):\n * the union of columns, existing definitions winning, new columns inserted\n * before any trailing table constraints, missing table constraints appended.\n *\n * Only the legacy column-less manifest path uses this; structured manifests\n * merge through `mergeSchemaDefinitionInto`.\n */\nfunction mergeManifestDDL(existingDDL: string, newDDL: string): string {\n const existingDefinitions = parseDefinitionsFromDDL(existingDDL);\n const newDefinitions = parseDefinitionsFromDDL(newDDL);\n\n const missingCols: string[] = [];\n for (const [name, def] of newDefinitions.columns) {\n if (!existingDefinitions.columns.has(name)) {\n missingCols.push(def);\n }\n }\n\n const existingTableElements = new Set(\n existingDefinitions.tableElements.map(getSQLDefinitionComparisonKey),\n );\n const missingTableElements = newDefinitions.tableElements.filter(\n (definition) =>\n !existingTableElements.has(getSQLDefinitionComparisonKey(definition)),\n );\n\n if (missingCols.length === 0 && missingTableElements.length === 0) {\n return existingDDL;\n }\n\n const bodyRange = findCreateTableBodyRange(existingDDL);\n if (!bodyRange) return existingDDL;\n const [bodyStart, bodyEnd] = bodyRange;\n const prefix = existingDDL.slice(0, bodyStart + 1);\n const body = existingDDL.slice(bodyStart + 1, bodyEnd);\n const suffix = existingDDL.slice(bodyEnd);\n const segments = tokenizeSQLDDLBody(body);\n const firstConstraintIndex = segments.findIndex((segment) =>\n isSQLTableConstraintDefinition(segment),\n );\n const insertionIndex =\n firstConstraintIndex === -1 ? segments.length : firstConstraintIndex;\n const mergedSegments = [\n ...segments.slice(0, insertionIndex),\n ...missingCols,\n ...segments.slice(insertionIndex),\n ...missingTableElements,\n ];\n\n return `${prefix}\\n${mergedSegments.map((segment) => ` ${segment}`).join(',\\n')}\\n${suffix}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"manifest-schema.js","names":[],"sources":["../../src/schema/manifest-schema.ts"],"sourcesContent":["/**\n * Manifest schema → structured `SchemaDefinition` helpers (#2358).\n *\n * A manifest's `schema.ddl` string is an engine-neutral CREATE TABLE preview:\n * it carries no indexes and no per-engine type mapping, so nothing that needs\n * an executable schema may treat it as authoritative. Every consumer that\n * reads raw manifest JSON — `SchemaAggregator`, `createIsolatedTestDbFromManifest`\n * in `@happyvertical/smrt-vitest`, third-party tooling — should collect the\n * structured `columns` / `indexes` through this module and render them with\n * `getDDLStrategy(engine)`, exactly as `db:migrate` does.\n *\n * The cached `ddl` string is used only when a manifest object exposes no\n * structured columns at all (hand-authored or pre-structured manifests); that\n * legacy path keeps working but is not engine-complete.\n *\n * Keep this module light — type-only imports plus the DDL strategies and the\n * logger — so it can be re-exported from lightweight entry points without\n * pulling the registry/collection module graph in by itself.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { getDDLStrategy } from './ddl/index.js';\nimport {\n findCreateTableBodyRange,\n getSQLDefinitionComparisonKey,\n getSQLDefinitionIdentifier,\n isSQLTableConstraintDefinition,\n materializeManifestDDLForEngine,\n tokenizeSQLDDLBody,\n} from './ddl/materialize-manifest.js';\nimport type { DatabaseEngine, EngineSpecificDDL } from './ddl/types.js';\nimport { quoteIdentifier } from './sql-identifiers.js';\nimport type {\n ColumnDefinition,\n IndexDefinition,\n SchemaDefinition,\n SQLDataType,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Column shape as it appears in raw manifest JSON. `ManifestColumnDefinition`\n * spells the default `default`; registry-side `ColumnDefinition` spells it\n * `defaultValue`. Hand-authored manifests use either.\n */\nexport interface ManifestColumnLike {\n type: string;\n primaryKey?: boolean;\n referenceKind?: ColumnDefinition['referenceKind'];\n notNull?: boolean;\n unique?: boolean;\n default?: unknown;\n defaultValue?: unknown;\n foreignKey?: ColumnDefinition['foreignKey'];\n check?: string;\n}\n\n/** Index shape as it appears in raw manifest JSON. */\nexport interface ManifestIndexLike {\n name: string;\n columns?: string[];\n unique?: boolean;\n where?: string;\n jsonPath?: IndexDefinition['jsonPath'];\n}\n\n/** The subset of a manifest object's `schema` this module reads. */\nexport interface ManifestSchemaLike {\n tableName: string;\n ddl?: string;\n columns?: Record<string, ManifestColumnLike>;\n indexes?: ManifestIndexLike[];\n version?: string;\n}\n\n/**\n * Convert one raw manifest column map into structured `ColumnDefinition`s.\n * Picks known fields only, so stray manifest keys never leak into DDL.\n */\nexport function manifestColumnsToDefinitions(\n columns: Record<string, ManifestColumnLike> | undefined,\n): Record<string, ColumnDefinition> {\n const result: Record<string, ColumnDefinition> = {};\n for (const [name, column] of Object.entries(columns || {})) {\n if (!column || typeof column.type !== 'string') continue;\n const definition: ColumnDefinition = {\n type: column.type as SQLDataType,\n };\n if (column.primaryKey !== undefined)\n definition.primaryKey = column.primaryKey;\n if (column.referenceKind !== undefined)\n definition.referenceKind = column.referenceKind;\n if (column.notNull !== undefined) definition.notNull = column.notNull;\n if (column.unique !== undefined) definition.unique = column.unique;\n const defaultValue =\n column.defaultValue !== undefined ? column.defaultValue : column.default;\n if (defaultValue !== undefined) definition.defaultValue = defaultValue;\n if (column.foreignKey) definition.foreignKey = { ...column.foreignKey };\n if (column.check) definition.check = column.check;\n result[name] = definition;\n }\n return result;\n}\n\n/**\n * Convert raw manifest indexes into structured `IndexDefinition`s, keeping\n * `where` (partial) and `jsonPath` (expression) targets — the two fields the\n * retired string-based renderers dropped.\n */\nexport function manifestIndexesToDefinitions(\n indexes: ManifestIndexLike[] | undefined,\n): IndexDefinition[] {\n const result: IndexDefinition[] = [];\n for (const index of indexes || []) {\n if (!index?.name) continue;\n const definition: IndexDefinition = {\n name: index.name,\n columns: Array.isArray(index.columns) ? [...index.columns] : [],\n };\n if (index.unique !== undefined) definition.unique = index.unique;\n if (index.where) definition.where = index.where;\n if (index.jsonPath?.column && index.jsonPath.path) {\n definition.jsonPath = { ...index.jsonPath };\n }\n result.push(definition);\n }\n return result;\n}\n\n/**\n * Convert one manifest object's `schema` into a `SchemaDefinition`.\n *\n * The cached `ddl` string is carried along (non-authoritative) so callers can\n * fall back to it when the manifest exposes no structured columns.\n */\nexport function manifestSchemaToDefinition(\n schema: ManifestSchemaLike,\n): SchemaDefinition {\n const columns = manifestColumnsToDefinitions(schema.columns);\n const foreignKeys = Object.entries(columns).flatMap(([column, definition]) =>\n definition.foreignKey\n ? [\n {\n column,\n referencesTable: definition.foreignKey.table,\n referencesColumn: definition.foreignKey.column,\n onDelete: definition.foreignKey.onDelete,\n onUpdate: definition.foreignKey.onUpdate,\n },\n ]\n : [],\n );\n return {\n tableName: schema.tableName,\n ddl: schema.ddl,\n columns,\n indexes: manifestIndexesToDefinitions(schema.indexes),\n triggers: [],\n foreignKeys,\n dependencies: foreignKeys\n .map((foreignKey) => foreignKey.referencesTable)\n .filter((dependency) => dependency !== schema.tableName),\n version: schema.version ?? '',\n };\n}\n\n/**\n * One physical table collected from one or more manifest objects (STI\n * subclasses share a table and each contributes columns and indexes).\n */\nexport interface CollectedManifestTable {\n tableName: string;\n /**\n * Structured union of every contributor: columns first-wins by name,\n * indexes deduplicated by name.\n */\n definition: SchemaDefinition;\n /**\n * `true` when every contributor exposed structured columns, i.e. the whole\n * table is rendered by a DDL strategy. `false` means at least one\n * contributor carried only a cached `ddl` string: the CREATE TABLE is then\n * the strategy render of the structured contributors (if any) merged with\n * the cached strings of the column-less ones. The decision is per table,\n * not per contributor.\n */\n structured: boolean;\n /**\n * The contributors' cached `ddl` strings merged column-wise (legacy path).\n * Empty when no contributor carried a `ddl`.\n */\n legacyDdl: string;\n /** `<package>:<ClassName>` labels of the contributors, in encounter order. */\n sources: string[];\n}\n\n/**\n * Deterministic STI merge of two structured definitions: existing columns\n * win, new columns are appended, indexes are deduplicated by name.\n */\nexport function mergeSchemaDefinitionInto(\n target: SchemaDefinition,\n incoming: SchemaDefinition,\n): void {\n for (const [name, column] of Object.entries(incoming.columns)) {\n if (!(name in target.columns)) {\n target.columns[name] = column;\n }\n }\n const indexNames = new Set(target.indexes.map((index) => index.name));\n for (const index of incoming.indexes) {\n if (!indexNames.has(index.name)) {\n indexNames.add(index.name);\n target.indexes.push(index);\n }\n }\n const foreignKeyKeys = new Set(\n target.foreignKeys.map(\n (foreignKey) =>\n `${foreignKey.column}\\0${foreignKey.referencesTable}\\0${foreignKey.referencesColumn}`,\n ),\n );\n for (const foreignKey of incoming.foreignKeys) {\n const key = `${foreignKey.column}\\0${foreignKey.referencesTable}\\0${foreignKey.referencesColumn}`;\n if (!foreignKeyKeys.has(key)) {\n foreignKeyKeys.add(key);\n target.foreignKeys.push(foreignKey);\n }\n }\n target.dependencies = Array.from(\n new Set([...target.dependencies, ...incoming.dependencies]),\n ).sort();\n}\n\n/**\n * Collect manifest schemas into physical tables.\n *\n * Accepts the `schema` entries of manifest objects (any package, any manifest)\n * in encounter order and groups them by `tableName`, merging STI contributors\n * structurally. Objects without a `schema`/`tableName` are skipped by the\n * caller; objects with neither columns nor `ddl` are ignored here.\n */\nexport function collectManifestTables(\n entries: Iterable<{ schema: ManifestSchemaLike; source: string }>,\n): Map<string, CollectedManifestTable> {\n const tables = new Map<string, CollectedManifestTable>();\n // Structured contributors whose cached ddl declares table constraints. Only\n // a fully structured table drops them (a mixed table re-merges the string),\n // so the warning is decided after collection.\n const constraintCarriers = new Map<\n string,\n Array<{ source: string; constraints: string[] }>\n >();\n\n for (const { schema, source } of entries) {\n if (!schema?.tableName) continue;\n const definition = manifestSchemaToDefinition(schema);\n const hasColumns = Object.keys(definition.columns).length > 0;\n const ddl = typeof schema.ddl === 'string' ? schema.ddl : '';\n if (!hasColumns && !ddl.trim()) continue;\n\n if (hasColumns && ddl.trim()) {\n const constraints = cachedDdlTableConstraints(ddl);\n if (constraints.length > 0) {\n const carriers = constraintCarriers.get(schema.tableName) ?? [];\n carriers.push({ source, constraints });\n constraintCarriers.set(schema.tableName, carriers);\n }\n }\n\n const existing = tables.get(schema.tableName);\n if (!existing) {\n tables.set(schema.tableName, {\n tableName: schema.tableName,\n definition,\n structured: hasColumns,\n legacyDdl: ddl,\n sources: [source],\n });\n continue;\n }\n\n mergeSchemaDefinitionInto(existing.definition, definition);\n existing.structured = existing.structured && hasColumns;\n existing.legacyDdl = existing.legacyDdl\n ? ddl\n ? mergeManifestDDL(existing.legacyDdl, ddl)\n : existing.legacyDdl\n : ddl;\n existing.sources.push(source);\n }\n\n for (const [tableName, carriers] of constraintCarriers) {\n if (!tables.get(tableName)?.structured) continue;\n for (const { source, constraints } of carriers) {\n warnAboutDroppedTableConstraints(tableName, source, constraints);\n }\n }\n\n return tables;\n}\n\n/**\n * Render a collected table for one engine.\n *\n * Structured tables go through the engine strategy for the CREATE TABLE\n * (per-engine types, inline UNIQUE where the engine needs it) and for every\n * index (partial `WHERE`, JSON-path expressions, engine-specific syntax).\n * Tables with a column-less contributor keep that contributor's cached `ddl`\n * string, made minimally safe with `materializeManifestDDLForEngine`, merged\n * on top of the strategy render of any structured contributors — so a\n * structured STI base never loses its engine typing to a hand-authored\n * subclass, and the subclass's extra columns and table constraints survive.\n * Indexes always render through the strategy — the string never carried them.\n */\nexport function renderCollectedManifestTable(\n table: CollectedManifestTable,\n engine: DatabaseEngine,\n): EngineSpecificDDL {\n const strategy = getDDLStrategy(engine);\n const hasColumns = Object.keys(table.definition.columns).length > 0;\n let createTable: string;\n if (table.structured) {\n createTable = strategy.generateCreateTable(table.definition);\n } else if (hasColumns) {\n createTable = mergeManifestDDL(\n strategy.generateCreateTable(table.definition),\n materializeManifestDDLForEngine(table.legacyDdl, engine),\n );\n } else {\n createTable = materializeManifestDDLForEngine(table.legacyDdl, engine);\n }\n\n // Engines that need UNIQUE inline for ON CONFLICT (DuckDB, JSON) get it\n // from `generateCreateTable` on the structured path and skip those indexes\n // in `generateIndexes`. A cached string never carried them, so a table with\n // a column-less contributor would lose its uniqueness entirely; append the\n // missing constraints as table elements (deduplicated against any the\n // string already declares).\n if (!table.structured && strategy.requiresInlineUnique()) {\n const uniqueElements = table.definition.indexes\n .filter(\n (index) =>\n index.unique &&\n !index.where &&\n !index.jsonPath &&\n index.columns.length > 0,\n )\n .map(\n (index) =>\n `UNIQUE(${index.columns.map((column) => quoteIdentifier(column)).join(', ')})`,\n );\n // Hand-authored strings may spell the same constraint without quotes\n // (`UNIQUE(tenant_id)`); compare quote- and whitespace-insensitively so\n // the synthesized element is not added beside it.\n const declared = new Set(\n cachedDdlTableConstraints(createTable).map(normalizeTableElement),\n );\n const missing = uniqueElements.filter(\n (element) => !declared.has(normalizeTableElement(element)),\n );\n if (missing.length > 0) {\n createTable = mergeManifestDDL(\n createTable,\n `CREATE TABLE ${quoteIdentifier(table.tableName)} (\\n ${missing.join(',\\n ')}\\n)`,\n );\n }\n }\n\n return {\n createTable,\n indexes: strategy.generateIndexes(table.definition),\n triggers: strategy.generateTriggers(table.definition),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Legacy cached-DDL string merge (column-less manifests only)\n// ---------------------------------------------------------------------------\n\nfunction parseDefinitionsFromDDL(ddl: string): {\n columns: Map<string, string>;\n tableElements: string[];\n} {\n const columns = new Map<string, string>();\n const tableElements: string[] = [];\n const bodyRange = findCreateTableBodyRange(ddl);\n if (!bodyRange) return { columns, tableElements };\n const [bodyStart, bodyEnd] = bodyRange;\n\n const segments = tokenizeSQLDDLBody(ddl.slice(bodyStart + 1, bodyEnd));\n\n for (const segment of segments) {\n const identifier = getSQLDefinitionIdentifier(segment);\n if (!identifier) continue;\n if (isSQLTableConstraintDefinition(segment)) {\n tableElements.push(segment);\n continue;\n }\n\n columns.set(identifier.name, segment);\n }\n\n return { columns, tableElements };\n}\n\n/**\n * Table-level constraints (`UNIQUE(...)`, `CHECK (...)`, `CONSTRAINT ...`,\n * ...) declared in a cached CREATE TABLE string. Structured `columns` cannot\n * represent them, so a structured render drops them — exactly as `db:migrate`\n * does. Exposed so callers can warn instead of losing them silently.\n *\n * @internal\n */\nexport function cachedDdlTableConstraints(ddl: string): string[] {\n return parseDefinitionsFromDDL(ddl).tableElements;\n}\n\n/** Quote/whitespace/case-insensitive key for a table constraint element. */\nfunction normalizeTableElement(element: string): string {\n return element.replace(/\"/g, '').replace(/\\s+/g, '').toLowerCase();\n}\n\nconst warnedDroppedConstraints = new Set<string>();\n\n/**\n * Warn once per process per table+source when a structured contributor's\n * cached `ddl` declares table constraints that the structured render cannot\n * carry. The structured schema is authoritative: a constraint that lives only\n * in the string never reaches `db:migrate` either, so rendering it here would\n * recreate the test/production drift #2358 removes.\n */\nfunction warnAboutDroppedTableConstraints(\n tableName: string,\n source: string,\n constraints: string[],\n): void {\n const key = `${tableName}\\0${source}`;\n if (warnedDroppedConstraints.has(key)) return;\n warnedDroppedConstraints.add(key);\n logger.warn(\n `[manifest-schema] ${source}: cached ddl for table \"${tableName}\" declares ` +\n `${constraints.length} table constraint(s) that structured columns cannot ` +\n `carry (${constraints.join('; ')}). They are not rendered — the ` +\n `structured schema is authoritative (#2358). Declare uniqueness through ` +\n `indexes/conflictColumns or a column-level constraint instead.`,\n );\n}\n\n/**\n * Merge two cached CREATE TABLE strings for the same table (STI subclasses):\n * the union of columns, existing definitions winning, new columns inserted\n * before any trailing table constraints, missing table constraints appended.\n *\n * Only the legacy column-less manifest path uses this; structured manifests\n * merge through `mergeSchemaDefinitionInto`.\n */\nfunction mergeManifestDDL(existingDDL: string, newDDL: string): string {\n const existingDefinitions = parseDefinitionsFromDDL(existingDDL);\n const newDefinitions = parseDefinitionsFromDDL(newDDL);\n\n const missingCols: string[] = [];\n for (const [name, def] of newDefinitions.columns) {\n if (!existingDefinitions.columns.has(name)) {\n missingCols.push(def);\n }\n }\n\n const existingTableElements = new Set(\n existingDefinitions.tableElements.map(getSQLDefinitionComparisonKey),\n );\n const missingTableElements = newDefinitions.tableElements.filter(\n (definition) =>\n !existingTableElements.has(getSQLDefinitionComparisonKey(definition)),\n );\n\n if (missingCols.length === 0 && missingTableElements.length === 0) {\n return existingDDL;\n }\n\n const bodyRange = findCreateTableBodyRange(existingDDL);\n if (!bodyRange) return existingDDL;\n const [bodyStart, bodyEnd] = bodyRange;\n const prefix = existingDDL.slice(0, bodyStart + 1);\n const body = existingDDL.slice(bodyStart + 1, bodyEnd);\n const suffix = existingDDL.slice(bodyEnd);\n const segments = tokenizeSQLDDLBody(body);\n const firstConstraintIndex = segments.findIndex((segment) =>\n isSQLTableConstraintDefinition(segment),\n );\n const insertionIndex =\n firstConstraintIndex === -1 ? segments.length : firstConstraintIndex;\n const mergedSegments = [\n ...segments.slice(0, insertionIndex),\n ...missingCols,\n ...segments.slice(insertionIndex),\n ...missingTableElements,\n ];\n\n return `${prefix}\\n${mergedSegments.map((segment) => ` ${segment}`).join(',\\n')}\\n${suffix}`;\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SmartObjectManifest } from '../scanner/types.js';
|
|
2
|
-
import { ManifestColumnLike } from './manifest-schema.js';
|
|
2
|
+
import { CollectedManifestTable, ManifestColumnLike } from './manifest-schema.js';
|
|
3
3
|
/**
|
|
4
4
|
* Aggregated table information including DDL, indexes, and source tracking.
|
|
5
5
|
*/
|
|
@@ -28,6 +28,12 @@ export interface AggregatedTable {
|
|
|
28
28
|
* per column name, matching the rendered DDL.
|
|
29
29
|
*/
|
|
30
30
|
columns?: Record<string, ManifestColumnLike>;
|
|
31
|
+
/** Structured schema retained so combined SQL can use dependency planning. */
|
|
32
|
+
definition: CollectedManifestTable['definition'];
|
|
33
|
+
/** Whether every contributing manifest provided structured columns. */
|
|
34
|
+
structured: boolean;
|
|
35
|
+
/** Cached DDL retained only for legacy column-less contributors. */
|
|
36
|
+
legacyDdl: string;
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
33
39
|
* Result of schema aggregation across packages.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-aggregator.d.ts","sourceRoot":"","sources":["../../src/schema/schema-aggregator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"schema-aggregator.d.ts","sourceRoot":"","sources":["../../src/schema/schema-aggregator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG/D,OAAO,EACL,KAAK,sBAAsB,EAE3B,KAAK,kBAAkB,EAExB,MAAM,sBAAsB,CAAC;AA4B9B;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,iBAAiB;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,8EAA8E;IAC9E,UAAU,EAAE,sBAAsB,CAAC,YAAY,CAAC,CAAC;IACjD,uEAAuE;IACvE,UAAU,EAAE,OAAO,CAAC;IACpB,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACrC,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;IACZ,6CAA6C;IAC7C,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC5C,yBAAyB;IACzB,KAAK,EAAE;QACL,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;QACxB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAEpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEpC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE/B;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;OAGG;IACH,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAEhC,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAqBD;;;;;GAKG;AACH,qBAAa,gBAAgB;IAC3B;;;;;OAKG;IACH,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,iBAAiB;IAoE5D;;OAEG;IACH,OAAO,CAAC,YAAY;IAgDpB;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IAwDtB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA0B1B;;OAEG;IACH,OAAO,CAAC,WAAW;CA6FpB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadExternalManifestSync } from "../manifest/manifest-loader.js";
|
|
2
2
|
import { discoverSmrtPackages } from "../manifest/discover-smrt-packages.js";
|
|
3
|
+
import { planForeignKeyCreation } from "./foreign-key-planner.js";
|
|
3
4
|
import { collectManifestTables, renderCollectedManifestTable } from "./manifest-schema.js";
|
|
4
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
6
|
import { join } from "node:path";
|
|
@@ -37,6 +38,13 @@ import { join } from "node:path";
|
|
|
37
38
|
* console.log(result.manifests); // Loaded manifests by package
|
|
38
39
|
* ```
|
|
39
40
|
*/
|
|
41
|
+
function referencesFilteredTable(ddl, filteredTables) {
|
|
42
|
+
for (const tableName of filteredTables) {
|
|
43
|
+
const quoted = tableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
44
|
+
const sqlQuoted = tableName.replaceAll("\"", "\"\"").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45
|
+
if (new RegExp(`\\bREFERENCES\\s+(?:(?:"(?:[^"]|"")*"|[A-Za-z_][A-Za-z0-9_\$]*)\\s*\\.\\s*)*(?:"${sqlQuoted}"|${quoted})(?![A-Za-z0-9_$"])`, "i").test(ddl)) return tableName;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
40
48
|
var DEFAULT_MINIMAL_SKIP_PATTERNS = [
|
|
41
49
|
/^api_key/,
|
|
42
50
|
/^magic_link/,
|
|
@@ -80,14 +88,14 @@ var SchemaAggregator = class {
|
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
const tables = this.extractSchemas(Array.from(manifests.values()), options.dialect);
|
|
83
|
-
|
|
91
|
+
const filteredTables = options.minimal ? this.applyMinimalFilter(tables, options) : /* @__PURE__ */ new Set();
|
|
84
92
|
let stiTables = 0;
|
|
85
93
|
let totalObjects = 0;
|
|
86
94
|
for (const table of tables.values()) if (table.sources.length > 1) stiTables++;
|
|
87
95
|
for (const manifest of manifests.values()) totalObjects += Object.keys(manifest.objects).length;
|
|
88
96
|
return {
|
|
89
97
|
tables,
|
|
90
|
-
sql: this.generateSQL(tables, options),
|
|
98
|
+
sql: this.generateSQL(tables, options, filteredTables),
|
|
91
99
|
manifests,
|
|
92
100
|
stats: {
|
|
93
101
|
packagesLoaded: manifests.size,
|
|
@@ -153,7 +161,10 @@ var SchemaAggregator = class {
|
|
|
153
161
|
ddl: rendered.createTable,
|
|
154
162
|
indexes: rendered.indexes,
|
|
155
163
|
sources: table.sources,
|
|
156
|
-
columns: manifestColumns.get(tableName)
|
|
164
|
+
columns: manifestColumns.get(tableName),
|
|
165
|
+
definition: table.definition,
|
|
166
|
+
structured: table.structured,
|
|
167
|
+
legacyDdl: table.legacyDdl
|
|
157
168
|
});
|
|
158
169
|
}
|
|
159
170
|
return tables;
|
|
@@ -164,39 +175,73 @@ var SchemaAggregator = class {
|
|
|
164
175
|
applyMinimalFilter(tables, options) {
|
|
165
176
|
const skipPatterns = options.minimalSkipPatterns || DEFAULT_MINIMAL_SKIP_PATTERNS;
|
|
166
177
|
const skipTables = new Set(options.minimalSkipTables || []);
|
|
178
|
+
const removed = /* @__PURE__ */ new Set();
|
|
167
179
|
for (const tableName of Array.from(tables.keys())) {
|
|
168
180
|
if (skipTables.has(tableName)) {
|
|
169
181
|
tables.delete(tableName);
|
|
182
|
+
removed.add(tableName);
|
|
170
183
|
continue;
|
|
171
184
|
}
|
|
172
185
|
for (const pattern of skipPatterns) if (pattern.test(tableName)) {
|
|
173
186
|
tables.delete(tableName);
|
|
187
|
+
removed.add(tableName);
|
|
174
188
|
break;
|
|
175
189
|
}
|
|
176
190
|
}
|
|
191
|
+
return removed;
|
|
177
192
|
}
|
|
178
193
|
/**
|
|
179
194
|
* Generate combined SQL from aggregated tables.
|
|
180
195
|
*/
|
|
181
|
-
generateSQL(tables, options) {
|
|
196
|
+
generateSQL(tables, options, filteredTables) {
|
|
182
197
|
const lines = [
|
|
183
198
|
"-- Combined SMRT Database Schema",
|
|
184
199
|
`-- Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
185
200
|
""
|
|
186
201
|
];
|
|
187
202
|
if (options.dialect === "sqlite") lines.push("PRAGMA foreign_keys = ON;", "");
|
|
188
|
-
const
|
|
189
|
-
for (const
|
|
203
|
+
const engine = options.dialect === "sqlite" ? "sqlite" : "postgres";
|
|
204
|
+
if (filteredTables.size > 0) for (const table of tables.values()) {
|
|
205
|
+
const unsafeLegacyTarget = !table.structured && referencesFilteredTable(table.legacyDdl, filteredTables);
|
|
206
|
+
if (unsafeLegacyTarget) throw new Error(`[SchemaAggregator] Cannot safely generate minimal schema: legacy cached DDL for "${table.tableName}" references filtered table "${unsafeLegacyTarget}". Publish structured columns/foreignKeys for every contributor or keep the referenced table in the aggregate.`);
|
|
207
|
+
table.definition = {
|
|
208
|
+
...table.definition,
|
|
209
|
+
columns: Object.fromEntries(Object.entries(table.definition.columns).map(([name, column]) => [name, column.foreignKey && filteredTables.has(column.foreignKey.table) ? {
|
|
210
|
+
...column,
|
|
211
|
+
foreignKey: void 0
|
|
212
|
+
} : column])),
|
|
213
|
+
foreignKeys: table.definition.foreignKeys.filter((foreignKey) => !filteredTables.has(foreignKey.referencesTable)),
|
|
214
|
+
dependencies: table.definition.dependencies.filter((dependency) => !filteredTables.has(dependency))
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const plan = planForeignKeyCreation([...tables.values()].map((table) => table.definition), engine);
|
|
218
|
+
for (const definition of plan.schemas) {
|
|
219
|
+
const tableName = definition.tableName;
|
|
220
|
+
const table = tables.get(tableName);
|
|
221
|
+
table.definition = definition;
|
|
222
|
+
const rendered = renderCollectedManifestTable({
|
|
223
|
+
tableName,
|
|
224
|
+
definition,
|
|
225
|
+
structured: table.structured,
|
|
226
|
+
legacyDdl: table.legacyDdl,
|
|
227
|
+
sources: table.sources
|
|
228
|
+
}, engine);
|
|
229
|
+
table.ddl = rendered.createTable;
|
|
230
|
+
table.indexes = rendered.indexes;
|
|
190
231
|
lines.push(`-- Table: ${tableName}`);
|
|
191
232
|
if (table.sources.length > 1) lines.push(`-- STI table, sources: ${table.sources.join(", ")}`);
|
|
192
233
|
else lines.push(`-- Source: ${table.sources[0]}`);
|
|
193
|
-
lines.push(
|
|
234
|
+
lines.push(rendered.createTable);
|
|
194
235
|
lines.push("");
|
|
195
|
-
if (
|
|
196
|
-
for (const idx of
|
|
236
|
+
if (rendered.indexes.length > 0) {
|
|
237
|
+
for (const idx of rendered.indexes) lines.push(idx);
|
|
197
238
|
lines.push("");
|
|
198
239
|
}
|
|
199
240
|
}
|
|
241
|
+
if (plan.deferredStatements.length > 0) {
|
|
242
|
+
lines.push("-- Deferred foreign-key constraints");
|
|
243
|
+
lines.push(...plan.deferredStatements, "");
|
|
244
|
+
}
|
|
200
245
|
return lines.join("\n");
|
|
201
246
|
}
|
|
202
247
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-aggregator.js","names":[],"sources":["../../src/schema/schema-aggregator.ts"],"sourcesContent":["/**\n * Schema Aggregator\n *\n * Aggregates database schemas across multiple SMRT package manifests.\n * Handles STI table deduplication, index merging, and sorting for deterministic output.\n *\n * Tables and indexes are rendered from the manifests' structured `columns` /\n * `indexes` through the dialect's DDL strategy (`getDDLStrategy`), the same\n * renderer `smrt db:migrate` uses — never from the cached `schema.ddl` string,\n * which carries no indexes and no per-engine types (#2358). Objects that expose\n * no structured columns fall back to their cached `ddl` for the CREATE TABLE.\n *\n * This replaces the manual generate-schema.ts pattern used by downstream\n * projects like blindmanpress.com (Issue #1013).\n *\n * @example\n * ```typescript\n * import { SchemaAggregator } from '@happyvertical/smrt-core/schema';\n *\n * // Auto-discover from installed @happyvertical packages in node_modules\n * const aggregator = new SchemaAggregator();\n * const result = aggregator.aggregate();\n *\n * // Or with explicit packages\n * const result = aggregator.aggregate({\n * packages: ['@happyvertical/smrt-profiles', '@happyvertical/smrt-events'],\n * });\n *\n * console.log(result.sql); // Combined CREATE TABLE + INDEX SQL\n * console.log(result.tables); // Map of table info\n * console.log(result.manifests); // Loaded manifests by package\n * ```\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { discoverSmrtPackages } from '../manifest/discover-smrt-packages.js';\nimport { loadExternalManifestSync } from '../manifest/manifest-loader.js';\nimport type { SmartObjectManifest } from '../scanner/types.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport {\n collectManifestTables,\n type ManifestColumnLike,\n renderCollectedManifestTable,\n} from './manifest-schema.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Aggregated table information including DDL, indexes, and source tracking.\n */\nexport interface AggregatedTable {\n /** Table name */\n tableName: string;\n /**\n * CREATE TABLE statement rendered by the dialect's DDL strategy from the\n * merged structured columns of every contributing class (STI union). A\n * table with a column-less contributor (hand-authored manifest) has that\n * contributor's cached `ddl` merged on top; a table with no structured\n * columns at all is the merged cached `ddl`.\n */\n ddl: string;\n /**\n * CREATE INDEX statements rendered by the same strategy (partial `WHERE`\n * and JSON-path targets included), deduplicated by index name across\n * sources.\n */\n indexes: string[];\n /** Package:className sources that contribute to this table */\n sources: string[];\n /**\n * Manifest column metadata as published in `manifest.json` (`default`,\n * not `defaultValue`), merged across contributors — first contributor wins\n * per column name, matching the rendered DDL.\n */\n columns?: Record<string, ManifestColumnLike>;\n}\n\n/**\n * Result of schema aggregation across packages.\n */\nexport interface AggregationResult {\n /** Aggregated tables keyed by table name */\n tables: Map<string, AggregatedTable>;\n /** Combined SQL string (CREATE TABLE + CREATE INDEX statements) */\n sql: string;\n /** Loaded manifests keyed by package name */\n manifests: Map<string, SmartObjectManifest>;\n /** Summary statistics */\n stats: {\n packagesLoaded: number;\n packagesSkipped: number;\n totalObjects: number;\n uniqueTables: number;\n stiTables: number;\n };\n}\n\n/**\n * Options for schema aggregation.\n */\nexport interface AggregateOptions {\n /**\n * Explicit list of packages to scan. If omitted, auto-discovers\n * installed `@happyvertical/smrt-*` packages from `node_modules`\n * via `discoverSmrtPackages()`.\n */\n packages?: string[];\n\n /**\n * Additional local filesystem paths to check for manifests,\n * keyed by package name. Useful for development when local\n * packages have newer schemas than node_modules.\n */\n localPaths?: Record<string, string>;\n\n /**\n * Skip collection tables and infrastructure tables.\n * Useful for generating minimal schemas for lightweight deployments.\n */\n minimal?: boolean;\n\n /**\n * Table name patterns to skip in minimal mode.\n * Defaults to common infrastructure tables.\n */\n minimalSkipPatterns?: RegExp[];\n\n /**\n * Specific table names to skip in minimal mode.\n */\n minimalSkipTables?: string[];\n\n /**\n * SQL dialect for the output. Affects PRAGMA statements and syntax.\n * @default 'postgres'\n */\n dialect?: 'postgres' | 'sqlite';\n\n /** Log progress to console */\n verbose?: boolean;\n}\n\n// ============================================================================\n// Default minimal-mode skip lists\n// ============================================================================\n\nconst DEFAULT_MINIMAL_SKIP_PATTERNS: RegExp[] = [\n /^api_key/,\n /^magic_link/,\n /^nostr_/,\n /^audit_log/,\n /_metafield/,\n /_metadata/,\n /_relationship_term/,\n /_relationship_type/,\n];\n\n// ============================================================================\n// SchemaAggregator\n// ============================================================================\n\n/**\n * Aggregates database schemas from multiple SMRT package manifests.\n *\n * Handles the complete workflow of discovering packages, loading manifests,\n * extracting DDL, deduplicating STI tables, and producing combined SQL.\n */\nexport class SchemaAggregator {\n /**\n * Aggregate schemas from SMRT packages.\n *\n * @param options - Aggregation options\n * @returns Aggregated schemas with combined SQL\n */\n aggregate(options: AggregateOptions = {}): AggregationResult {\n const verbose = options.verbose ?? false;\n\n // 1. Discover packages\n const packages = options.packages || discoverSmrtPackages();\n\n if (verbose) {\n console.log(`[SchemaAggregator] Scanning ${packages.length} packages...`);\n }\n\n // 2. Load manifests\n const manifests = new Map<string, SmartObjectManifest>();\n let packagesSkipped = 0;\n\n for (const pkg of packages) {\n const manifest = this.loadManifest(pkg, options.localPaths);\n if (manifest) {\n manifests.set(pkg, manifest);\n if (verbose) {\n const objectCount = Object.keys(manifest.objects).length;\n console.log(` ✓ ${pkg} (${objectCount} objects)`);\n }\n } else {\n packagesSkipped++;\n if (verbose) {\n console.log(` [skip] ${pkg} - manifest not found`);\n }\n }\n }\n\n // 3. Extract and deduplicate schemas\n const tables = this.extractSchemas(\n Array.from(manifests.values()),\n options.dialect,\n );\n\n // 4. Apply minimal filtering if requested\n if (options.minimal) {\n this.applyMinimalFilter(tables, options);\n }\n\n // 5. Count STI tables and total objects\n let stiTables = 0;\n let totalObjects = 0;\n for (const table of tables.values()) {\n if (table.sources.length > 1) stiTables++;\n }\n for (const manifest of manifests.values()) {\n totalObjects += Object.keys(manifest.objects).length;\n }\n\n // 6. Generate combined SQL\n const sql = this.generateSQL(tables, options);\n\n return {\n tables,\n sql,\n manifests,\n stats: {\n packagesLoaded: manifests.size,\n packagesSkipped,\n totalObjects,\n uniqueTables: tables.size,\n stiTables,\n },\n };\n }\n\n /**\n * Load a manifest from local path or node_modules.\n */\n private loadManifest(\n packageName: string,\n localPaths?: Record<string, string>,\n ): SmartObjectManifest | null {\n // Try local development path first (may have newer schemas)\n if (localPaths?.[packageName]) {\n const localPath = localPaths[packageName];\n if (existsSync(localPath)) {\n try {\n const content = JSON.parse(\n readFileSync(localPath, 'utf-8'),\n ) as SmartObjectManifest;\n // Only use local if it has schemas\n const hasSchemas = Object.values(content.objects || {}).some(\n (obj) =>\n obj.schema?.tableName &&\n (Object.keys(obj.schema.columns || {}).length > 0 ||\n obj.schema.ddl),\n );\n if (hasSchemas) return content;\n } catch {\n // Fall through to node_modules\n }\n }\n }\n\n // Try loadExternalManifestSync (handles node_modules resolution)\n const manifest = loadExternalManifestSync(packageName);\n if (manifest) return manifest;\n\n // Fallback: try common node_modules paths relative to cwd\n const root = process.cwd();\n const paths = [\n join(root, 'node_modules', packageName, 'dist', 'manifest.json'),\n join(root, 'node_modules', packageName, 'manifest.json'),\n ];\n\n for (const manifestPath of paths) {\n if (existsSync(manifestPath)) {\n try {\n return JSON.parse(readFileSync(manifestPath, 'utf-8'));\n } catch {}\n }\n }\n\n return null;\n }\n\n /**\n * Extract schemas from manifests, deduplicating STI tables.\n *\n * STI tables (shared by multiple classes across packages) are merged\n * structurally — columns first-wins by name, indexes deduplicated by name —\n * and every table is rendered through the dialect's DDL strategy, the same\n * renderer `db:migrate` uses. The manifest's cached `schema.ddl` string is\n * consulted only for objects that expose no structured columns (#2358).\n */\n private extractSchemas(\n manifests: SmartObjectManifest[],\n dialect?: 'postgres' | 'sqlite',\n ): Map<string, AggregatedTable> {\n // Default mirrors `AggregateOptions.dialect` (`@default 'postgres'`) so\n // unspecified callers get the documented shape.\n const engine: DatabaseEngine = dialect === 'sqlite' ? 'sqlite' : 'postgres';\n\n const entries: Array<{\n schema: NonNullable<SmartObjectManifest['objects'][string]['schema']>;\n source: string;\n }> = [];\n // Published manifest column metadata, merged first-wins per table so\n // `AggregatedTable.columns` keeps its manifest shape for downstream\n // generate-schema scripts.\n const manifestColumns = new Map<\n string,\n Record<string, ManifestColumnLike>\n >();\n for (const manifest of manifests) {\n for (const [_key, object] of Object.entries(manifest.objects)) {\n const schema = object.schema;\n if (!schema?.tableName) continue;\n const className: string = object.className || _key;\n entries.push({\n schema,\n source: `${manifest.packageName || '?'}:${className}`,\n });\n if (schema.columns && Object.keys(schema.columns).length > 0) {\n const merged = manifestColumns.get(schema.tableName) ?? {};\n for (const [name, column] of Object.entries(schema.columns)) {\n if (!(name in merged)) merged[name] = column;\n }\n manifestColumns.set(schema.tableName, merged);\n }\n }\n }\n\n const tables = new Map<string, AggregatedTable>();\n for (const [tableName, table] of collectManifestTables(entries)) {\n const rendered = renderCollectedManifestTable(table, engine);\n tables.set(tableName, {\n tableName,\n ddl: rendered.createTable,\n indexes: rendered.indexes,\n sources: table.sources,\n columns: manifestColumns.get(tableName),\n });\n }\n\n return tables;\n }\n\n /**\n * Apply minimal filtering to remove infrastructure/collection tables.\n */\n private applyMinimalFilter(\n tables: Map<string, AggregatedTable>,\n options: AggregateOptions,\n ): void {\n const skipPatterns =\n options.minimalSkipPatterns || DEFAULT_MINIMAL_SKIP_PATTERNS;\n const skipTables = new Set(options.minimalSkipTables || []);\n\n for (const tableName of Array.from(tables.keys())) {\n if (skipTables.has(tableName)) {\n tables.delete(tableName);\n continue;\n }\n for (const pattern of skipPatterns) {\n if (pattern.test(tableName)) {\n tables.delete(tableName);\n break;\n }\n }\n }\n }\n\n /**\n * Generate combined SQL from aggregated tables.\n */\n private generateSQL(\n tables: Map<string, AggregatedTable>,\n options: AggregateOptions,\n ): string {\n const lines: string[] = [\n '-- Combined SMRT Database Schema',\n `-- Generated: ${new Date().toISOString()}`,\n '',\n ];\n\n if (options.dialect === 'sqlite') {\n lines.push('PRAGMA foreign_keys = ON;', '');\n }\n\n // Sort tables by name for deterministic output\n const sortedTables = Array.from(tables.entries()).sort((a, b) =>\n a[0].localeCompare(b[0]),\n );\n\n for (const [tableName, table] of sortedTables) {\n lines.push(`-- Table: ${tableName}`);\n if (table.sources.length > 1) {\n lines.push(`-- STI table, sources: ${table.sources.join(', ')}`);\n } else {\n lines.push(`-- Source: ${table.sources[0]}`);\n }\n lines.push(table.ddl);\n lines.push('');\n\n if (table.indexes.length > 0) {\n for (const idx of table.indexes) {\n lines.push(idx);\n }\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-aggregator.js","names":[],"sources":["../../src/schema/schema-aggregator.ts"],"sourcesContent":["/**\n * Schema Aggregator\n *\n * Aggregates database schemas across multiple SMRT package manifests.\n * Handles STI table deduplication, index merging, and sorting for deterministic output.\n *\n * Tables and indexes are rendered from the manifests' structured `columns` /\n * `indexes` through the dialect's DDL strategy (`getDDLStrategy`), the same\n * renderer `smrt db:migrate` uses — never from the cached `schema.ddl` string,\n * which carries no indexes and no per-engine types (#2358). Objects that expose\n * no structured columns fall back to their cached `ddl` for the CREATE TABLE.\n *\n * This replaces the manual generate-schema.ts pattern used by downstream\n * projects like blindmanpress.com (Issue #1013).\n *\n * @example\n * ```typescript\n * import { SchemaAggregator } from '@happyvertical/smrt-core/schema';\n *\n * // Auto-discover from installed @happyvertical packages in node_modules\n * const aggregator = new SchemaAggregator();\n * const result = aggregator.aggregate();\n *\n * // Or with explicit packages\n * const result = aggregator.aggregate({\n * packages: ['@happyvertical/smrt-profiles', '@happyvertical/smrt-events'],\n * });\n *\n * console.log(result.sql); // Combined CREATE TABLE + INDEX SQL\n * console.log(result.tables); // Map of table info\n * console.log(result.manifests); // Loaded manifests by package\n * ```\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { discoverSmrtPackages } from '../manifest/discover-smrt-packages.js';\nimport { loadExternalManifestSync } from '../manifest/manifest-loader.js';\nimport type { SmartObjectManifest } from '../scanner/types.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport { planForeignKeyCreation } from './foreign-key-planner.js';\nimport {\n type CollectedManifestTable,\n collectManifestTables,\n type ManifestColumnLike,\n renderCollectedManifestTable,\n} from './manifest-schema.js';\n\nfunction referencesFilteredTable(\n ddl: string,\n filteredTables: ReadonlySet<string>,\n): string | undefined {\n for (const tableName of filteredTables) {\n const quoted = tableName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const sqlQuoted = tableName\n .replaceAll('\"', '\"\"')\n .replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const qualifier = '(?:\"(?:[^\"]|\"\")*\"|[A-Za-z_][A-Za-z0-9_$]*)';\n if (\n new RegExp(\n `\\\\bREFERENCES\\\\s+(?:${qualifier}\\\\s*\\\\.\\\\s*)*(?:\"${sqlQuoted}\"|${quoted})(?![A-Za-z0-9_$\"])`,\n 'i',\n ).test(ddl)\n ) {\n return tableName;\n }\n }\n return undefined;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Aggregated table information including DDL, indexes, and source tracking.\n */\nexport interface AggregatedTable {\n /** Table name */\n tableName: string;\n /**\n * CREATE TABLE statement rendered by the dialect's DDL strategy from the\n * merged structured columns of every contributing class (STI union). A\n * table with a column-less contributor (hand-authored manifest) has that\n * contributor's cached `ddl` merged on top; a table with no structured\n * columns at all is the merged cached `ddl`.\n */\n ddl: string;\n /**\n * CREATE INDEX statements rendered by the same strategy (partial `WHERE`\n * and JSON-path targets included), deduplicated by index name across\n * sources.\n */\n indexes: string[];\n /** Package:className sources that contribute to this table */\n sources: string[];\n /**\n * Manifest column metadata as published in `manifest.json` (`default`,\n * not `defaultValue`), merged across contributors — first contributor wins\n * per column name, matching the rendered DDL.\n */\n columns?: Record<string, ManifestColumnLike>;\n /** Structured schema retained so combined SQL can use dependency planning. */\n definition: CollectedManifestTable['definition'];\n /** Whether every contributing manifest provided structured columns. */\n structured: boolean;\n /** Cached DDL retained only for legacy column-less contributors. */\n legacyDdl: string;\n}\n\n/**\n * Result of schema aggregation across packages.\n */\nexport interface AggregationResult {\n /** Aggregated tables keyed by table name */\n tables: Map<string, AggregatedTable>;\n /** Combined SQL string (CREATE TABLE + CREATE INDEX statements) */\n sql: string;\n /** Loaded manifests keyed by package name */\n manifests: Map<string, SmartObjectManifest>;\n /** Summary statistics */\n stats: {\n packagesLoaded: number;\n packagesSkipped: number;\n totalObjects: number;\n uniqueTables: number;\n stiTables: number;\n };\n}\n\n/**\n * Options for schema aggregation.\n */\nexport interface AggregateOptions {\n /**\n * Explicit list of packages to scan. If omitted, auto-discovers\n * installed `@happyvertical/smrt-*` packages from `node_modules`\n * via `discoverSmrtPackages()`.\n */\n packages?: string[];\n\n /**\n * Additional local filesystem paths to check for manifests,\n * keyed by package name. Useful for development when local\n * packages have newer schemas than node_modules.\n */\n localPaths?: Record<string, string>;\n\n /**\n * Skip collection tables and infrastructure tables.\n * Useful for generating minimal schemas for lightweight deployments.\n */\n minimal?: boolean;\n\n /**\n * Table name patterns to skip in minimal mode.\n * Defaults to common infrastructure tables.\n */\n minimalSkipPatterns?: RegExp[];\n\n /**\n * Specific table names to skip in minimal mode.\n */\n minimalSkipTables?: string[];\n\n /**\n * SQL dialect for the output. Affects PRAGMA statements and syntax.\n * @default 'postgres'\n */\n dialect?: 'postgres' | 'sqlite';\n\n /** Log progress to console */\n verbose?: boolean;\n}\n\n// ============================================================================\n// Default minimal-mode skip lists\n// ============================================================================\n\nconst DEFAULT_MINIMAL_SKIP_PATTERNS: RegExp[] = [\n /^api_key/,\n /^magic_link/,\n /^nostr_/,\n /^audit_log/,\n /_metafield/,\n /_metadata/,\n /_relationship_term/,\n /_relationship_type/,\n];\n\n// ============================================================================\n// SchemaAggregator\n// ============================================================================\n\n/**\n * Aggregates database schemas from multiple SMRT package manifests.\n *\n * Handles the complete workflow of discovering packages, loading manifests,\n * extracting DDL, deduplicating STI tables, and producing combined SQL.\n */\nexport class SchemaAggregator {\n /**\n * Aggregate schemas from SMRT packages.\n *\n * @param options - Aggregation options\n * @returns Aggregated schemas with combined SQL\n */\n aggregate(options: AggregateOptions = {}): AggregationResult {\n const verbose = options.verbose ?? false;\n\n // 1. Discover packages\n const packages = options.packages || discoverSmrtPackages();\n\n if (verbose) {\n console.log(`[SchemaAggregator] Scanning ${packages.length} packages...`);\n }\n\n // 2. Load manifests\n const manifests = new Map<string, SmartObjectManifest>();\n let packagesSkipped = 0;\n\n for (const pkg of packages) {\n const manifest = this.loadManifest(pkg, options.localPaths);\n if (manifest) {\n manifests.set(pkg, manifest);\n if (verbose) {\n const objectCount = Object.keys(manifest.objects).length;\n console.log(` ✓ ${pkg} (${objectCount} objects)`);\n }\n } else {\n packagesSkipped++;\n if (verbose) {\n console.log(` [skip] ${pkg} - manifest not found`);\n }\n }\n }\n\n // 3. Extract and deduplicate schemas\n const tables = this.extractSchemas(\n Array.from(manifests.values()),\n options.dialect,\n );\n\n // 4. Apply minimal filtering if requested\n const filteredTables = options.minimal\n ? this.applyMinimalFilter(tables, options)\n : new Set<string>();\n\n // 5. Count STI tables and total objects\n let stiTables = 0;\n let totalObjects = 0;\n for (const table of tables.values()) {\n if (table.sources.length > 1) stiTables++;\n }\n for (const manifest of manifests.values()) {\n totalObjects += Object.keys(manifest.objects).length;\n }\n\n // 6. Generate combined SQL\n const sql = this.generateSQL(tables, options, filteredTables);\n\n return {\n tables,\n sql,\n manifests,\n stats: {\n packagesLoaded: manifests.size,\n packagesSkipped,\n totalObjects,\n uniqueTables: tables.size,\n stiTables,\n },\n };\n }\n\n /**\n * Load a manifest from local path or node_modules.\n */\n private loadManifest(\n packageName: string,\n localPaths?: Record<string, string>,\n ): SmartObjectManifest | null {\n // Try local development path first (may have newer schemas)\n if (localPaths?.[packageName]) {\n const localPath = localPaths[packageName];\n if (existsSync(localPath)) {\n try {\n const content = JSON.parse(\n readFileSync(localPath, 'utf-8'),\n ) as SmartObjectManifest;\n // Only use local if it has schemas\n const hasSchemas = Object.values(content.objects || {}).some(\n (obj) =>\n obj.schema?.tableName &&\n (Object.keys(obj.schema.columns || {}).length > 0 ||\n obj.schema.ddl),\n );\n if (hasSchemas) return content;\n } catch {\n // Fall through to node_modules\n }\n }\n }\n\n // Try loadExternalManifestSync (handles node_modules resolution)\n const manifest = loadExternalManifestSync(packageName);\n if (manifest) return manifest;\n\n // Fallback: try common node_modules paths relative to cwd\n const root = process.cwd();\n const paths = [\n join(root, 'node_modules', packageName, 'dist', 'manifest.json'),\n join(root, 'node_modules', packageName, 'manifest.json'),\n ];\n\n for (const manifestPath of paths) {\n if (existsSync(manifestPath)) {\n try {\n return JSON.parse(readFileSync(manifestPath, 'utf-8'));\n } catch {}\n }\n }\n\n return null;\n }\n\n /**\n * Extract schemas from manifests, deduplicating STI tables.\n *\n * STI tables (shared by multiple classes across packages) are merged\n * structurally — columns first-wins by name, indexes deduplicated by name —\n * and every table is rendered through the dialect's DDL strategy, the same\n * renderer `db:migrate` uses. The manifest's cached `schema.ddl` string is\n * consulted only for objects that expose no structured columns (#2358).\n */\n private extractSchemas(\n manifests: SmartObjectManifest[],\n dialect?: 'postgres' | 'sqlite',\n ): Map<string, AggregatedTable> {\n // Default mirrors `AggregateOptions.dialect` (`@default 'postgres'`) so\n // unspecified callers get the documented shape.\n const engine: DatabaseEngine = dialect === 'sqlite' ? 'sqlite' : 'postgres';\n\n const entries: Array<{\n schema: NonNullable<SmartObjectManifest['objects'][string]['schema']>;\n source: string;\n }> = [];\n // Published manifest column metadata, merged first-wins per table so\n // `AggregatedTable.columns` keeps its manifest shape for downstream\n // generate-schema scripts.\n const manifestColumns = new Map<\n string,\n Record<string, ManifestColumnLike>\n >();\n for (const manifest of manifests) {\n for (const [_key, object] of Object.entries(manifest.objects)) {\n const schema = object.schema;\n if (!schema?.tableName) continue;\n const className: string = object.className || _key;\n entries.push({\n schema,\n source: `${manifest.packageName || '?'}:${className}`,\n });\n if (schema.columns && Object.keys(schema.columns).length > 0) {\n const merged = manifestColumns.get(schema.tableName) ?? {};\n for (const [name, column] of Object.entries(schema.columns)) {\n if (!(name in merged)) merged[name] = column;\n }\n manifestColumns.set(schema.tableName, merged);\n }\n }\n }\n\n const tables = new Map<string, AggregatedTable>();\n for (const [tableName, table] of collectManifestTables(entries)) {\n const rendered = renderCollectedManifestTable(table, engine);\n tables.set(tableName, {\n tableName,\n ddl: rendered.createTable,\n indexes: rendered.indexes,\n sources: table.sources,\n columns: manifestColumns.get(tableName),\n definition: table.definition,\n structured: table.structured,\n legacyDdl: table.legacyDdl,\n });\n }\n\n return tables;\n }\n\n /**\n * Apply minimal filtering to remove infrastructure/collection tables.\n */\n private applyMinimalFilter(\n tables: Map<string, AggregatedTable>,\n options: AggregateOptions,\n ): Set<string> {\n const skipPatterns =\n options.minimalSkipPatterns || DEFAULT_MINIMAL_SKIP_PATTERNS;\n const skipTables = new Set(options.minimalSkipTables || []);\n const removed = new Set<string>();\n\n for (const tableName of Array.from(tables.keys())) {\n if (skipTables.has(tableName)) {\n tables.delete(tableName);\n removed.add(tableName);\n continue;\n }\n for (const pattern of skipPatterns) {\n if (pattern.test(tableName)) {\n tables.delete(tableName);\n removed.add(tableName);\n break;\n }\n }\n }\n return removed;\n }\n\n /**\n * Generate combined SQL from aggregated tables.\n */\n private generateSQL(\n tables: Map<string, AggregatedTable>,\n options: AggregateOptions,\n filteredTables: ReadonlySet<string>,\n ): string {\n const lines: string[] = [\n '-- Combined SMRT Database Schema',\n `-- Generated: ${new Date().toISOString()}`,\n '',\n ];\n\n if (options.dialect === 'sqlite') {\n lines.push('PRAGMA foreign_keys = ON;', '');\n }\n\n const engine: DatabaseEngine =\n options.dialect === 'sqlite' ? 'sqlite' : 'postgres';\n if (filteredTables.size > 0) {\n for (const table of tables.values()) {\n const unsafeLegacyTarget =\n !table.structured &&\n referencesFilteredTable(table.legacyDdl, filteredTables);\n if (unsafeLegacyTarget) {\n throw new Error(\n `[SchemaAggregator] Cannot safely generate minimal schema: legacy cached DDL for \"${table.tableName}\" references filtered table \"${unsafeLegacyTarget}\". Publish structured columns/foreignKeys for every contributor or keep the referenced table in the aggregate.`,\n );\n }\n table.definition = {\n ...table.definition,\n columns: Object.fromEntries(\n Object.entries(table.definition.columns).map(([name, column]) => [\n name,\n column.foreignKey && filteredTables.has(column.foreignKey.table)\n ? { ...column, foreignKey: undefined }\n : column,\n ]),\n ),\n foreignKeys: table.definition.foreignKeys.filter(\n (foreignKey) => !filteredTables.has(foreignKey.referencesTable),\n ),\n dependencies: table.definition.dependencies.filter(\n (dependency) => !filteredTables.has(dependency),\n ),\n };\n }\n }\n const plan = planForeignKeyCreation(\n [...tables.values()].map((table) => table.definition),\n engine,\n );\n\n for (const definition of plan.schemas) {\n const tableName = definition.tableName;\n const table = tables.get(tableName) as AggregatedTable;\n table.definition = definition;\n const rendered = renderCollectedManifestTable(\n {\n tableName,\n definition,\n structured: table.structured,\n legacyDdl: table.legacyDdl,\n sources: table.sources,\n },\n engine,\n );\n // Keep the structured result and its cached presentation consistent with\n // the executable aggregate after minimal filtering and cycle planning.\n table.ddl = rendered.createTable;\n table.indexes = rendered.indexes;\n lines.push(`-- Table: ${tableName}`);\n if (table.sources.length > 1) {\n lines.push(`-- STI table, sources: ${table.sources.join(', ')}`);\n } else {\n lines.push(`-- Source: ${table.sources[0]}`);\n }\n lines.push(rendered.createTable);\n lines.push('');\n\n if (rendered.indexes.length > 0) {\n for (const idx of rendered.indexes) {\n lines.push(idx);\n }\n lines.push('');\n }\n }\n\n if (plan.deferredStatements.length > 0) {\n lines.push('-- Deferred foreign-key constraints');\n lines.push(...plan.deferredStatements, '');\n }\n\n return lines.join('\\n');\n }\n}\n"],"mappings":"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"}
|
|
@@ -66,6 +66,7 @@ export declare class SchemaManager {
|
|
|
66
66
|
* @param schemas - Array of schema definitions
|
|
67
67
|
*/
|
|
68
68
|
ensureTables(schemas: SchemaDefinition[]): Promise<void>;
|
|
69
|
+
private ensurePostgresDeferredForeignKey;
|
|
69
70
|
/**
|
|
70
71
|
* Generate DDL without executing (useful for debugging/preview)
|
|
71
72
|
*
|
|
@@ -77,12 +78,6 @@ export declare class SchemaManager {
|
|
|
77
78
|
* Get the detected/configured database engine
|
|
78
79
|
*/
|
|
79
80
|
getEngine(): DatabaseEngine;
|
|
80
|
-
/**
|
|
81
|
-
* Sort schemas by dependencies (topological sort)
|
|
82
|
-
*
|
|
83
|
-
* Ensures tables are created in the correct order based on foreign key dependencies.
|
|
84
|
-
*/
|
|
85
|
-
private sortByDependencies;
|
|
86
81
|
}
|
|
87
82
|
/**
|
|
88
83
|
* Create a SchemaManager instance
|
|
@@ -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;AAQxB,OAAO,KAAK,EAAwB,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;IAoChC;;;;;;;;;;;;;OAaG;YACW,iBAAiB;IAqE/B,OAAO,CAAC,eAAe;IAIvB;;;;OAIG;IACG,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAWhD,gCAAgC;IAgD9C;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB;IASxD;;OAEG;IACH,SAAS,IAAI,cAAc;CAG5B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,iBAAiB,EACrB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,aAAa,CAEf"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { foreignKeyConstraintName, renderForeignKeyConstraint, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair } from "./foreign-key-ddl.js";
|
|
1
2
|
import { detectEngine, getDDLStrategy } from "./ddl/index.js";
|
|
3
|
+
import { planForeignKeyCreation } from "./foreign-key-planner.js";
|
|
2
4
|
import { createLogger } from "@happyvertical/logger";
|
|
3
5
|
//#region src/schema/schema-manager.ts
|
|
4
6
|
/**
|
|
@@ -164,8 +166,30 @@ var SchemaManager = class {
|
|
|
164
166
|
* @param schemas - Array of schema definitions
|
|
165
167
|
*/
|
|
166
168
|
async ensureTables(schemas) {
|
|
167
|
-
const
|
|
168
|
-
for (const schema of
|
|
169
|
+
const plan = planForeignKeyCreation(schemas, this.engine);
|
|
170
|
+
for (const schema of plan.schemas) await this.ensureTable(schema);
|
|
171
|
+
for (const { table, foreignKey } of plan.deferredConstraints) await this.ensurePostgresDeferredForeignKey(table, foreignKey);
|
|
172
|
+
}
|
|
173
|
+
async ensurePostgresDeferredForeignKey(tableName, foreignKey) {
|
|
174
|
+
const constraintName = foreignKeyConstraintName(tableName, foreignKey);
|
|
175
|
+
let existing;
|
|
176
|
+
try {
|
|
177
|
+
existing = await this.db.query(`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`, [tableName, constraintName]);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new Error(`[SchemaManager] Cannot verify deferred foreign key ${constraintName}; refusing an unsafe duplicate/add attempt: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
180
|
+
}
|
|
181
|
+
const existingRows = extractRows(existing);
|
|
182
|
+
if (existingRows[0]?.convalidated === true) return;
|
|
183
|
+
const detector = renderForeignKeyOrphanDetector(tableName, foreignKey, { limitOne: true });
|
|
184
|
+
let orphanResult;
|
|
185
|
+
try {
|
|
186
|
+
orphanResult = await this.db.query(detector);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new Error(`[SchemaManager] Cannot probe ${tableName}.${foreignKey.column} for orphan rows; refusing to add ${constraintName}. Run the detector manually: ${detector}`, { cause: error });
|
|
189
|
+
}
|
|
190
|
+
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)}`);
|
|
191
|
+
if (existingRows.length === 0) await this.db.query(`ALTER TABLE ${this.quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`);
|
|
192
|
+
await this.db.query(`ALTER TABLE ${this.quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${this.quoteIdentifier(constraintName)}`);
|
|
169
193
|
}
|
|
170
194
|
/**
|
|
171
195
|
* Generate DDL without executing (useful for debugging/preview)
|
|
@@ -187,34 +211,6 @@ var SchemaManager = class {
|
|
|
187
211
|
getEngine() {
|
|
188
212
|
return this.engine;
|
|
189
213
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Sort schemas by dependencies (topological sort)
|
|
192
|
-
*
|
|
193
|
-
* Ensures tables are created in the correct order based on foreign key dependencies.
|
|
194
|
-
*/
|
|
195
|
-
sortByDependencies(schemas) {
|
|
196
|
-
const byName = /* @__PURE__ */ new Map();
|
|
197
|
-
const sorted = [];
|
|
198
|
-
const visited = /* @__PURE__ */ new Set();
|
|
199
|
-
const visiting = /* @__PURE__ */ new Set();
|
|
200
|
-
for (const schema of schemas) byName.set(schema.tableName, schema);
|
|
201
|
-
const visit = (tableName) => {
|
|
202
|
-
if (visited.has(tableName)) return;
|
|
203
|
-
if (visiting.has(tableName)) {
|
|
204
|
-
this.logger.warn(`[SchemaManager] Circular dependency detected involving ${tableName}`);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const schema = byName.get(tableName);
|
|
208
|
-
if (!schema) return;
|
|
209
|
-
visiting.add(tableName);
|
|
210
|
-
for (const dep of schema.dependencies || []) visit(dep);
|
|
211
|
-
visiting.delete(tableName);
|
|
212
|
-
visited.add(tableName);
|
|
213
|
-
sorted.push(schema);
|
|
214
|
-
};
|
|
215
|
-
for (const schema of schemas) visit(schema.tableName);
|
|
216
|
-
return sorted;
|
|
217
|
-
}
|
|
218
214
|
};
|
|
219
215
|
/**
|
|
220
216
|
* Create a SchemaManager instance
|
|
@@ -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 type { 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: information_schema.columns\n *\n * @param tableName - Name of the table to inspect\n * @returns Set of existing column names (lowercase)\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 column_name FROM information_schema.columns WHERE table_name = $1',\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 // Sort by dependencies (topological sort)\n const sorted = this.sortByDependencies(schemas);\n\n for (const schema of sorted) {\n await this.ensureTable(schema);\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 * Sort schemas by dependencies (topological sort)\n *\n * Ensures tables are created in the correct order based on foreign key dependencies.\n */\n private sortByDependencies(schemas: SchemaDefinition[]): SchemaDefinition[] {\n const byName = new Map<string, SchemaDefinition>();\n const sorted: SchemaDefinition[] = [];\n const visited = new Set<string>();\n const visiting = new Set<string>();\n\n // Index by table name\n for (const schema of schemas) {\n byName.set(schema.tableName, schema);\n }\n\n // DFS topological sort\n const visit = (tableName: string) => {\n if (visited.has(tableName)) return;\n if (visiting.has(tableName)) {\n // Circular dependency - continue anyway (FK might be nullable)\n this.logger.warn(\n `[SchemaManager] Circular dependency detected involving ${tableName}`,\n );\n return;\n }\n\n const schema = byName.get(tableName);\n if (!schema) return; // External dependency, skip\n\n visiting.add(tableName);\n\n // Visit dependencies first\n for (const dep of schema.dependencies || []) {\n visit(dep);\n }\n\n visiting.delete(tableName);\n visited.add(tableName);\n sorted.push(schema);\n };\n\n // Visit all schemas\n for (const schema of schemas) {\n visit(schema.tableName);\n }\n\n return sorted;\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 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: information_schema.columns\n *\n * @param tableName - Name of the table to inspect\n * @returns Set of existing column names (lowercase)\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 column_name FROM information_schema.columns WHERE table_name = $1',\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.ensurePostgresDeferredForeignKey(table, foreignKey);\n }\n }\n\n private async ensurePostgresDeferredForeignKey(\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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"}
|