@happyvertical/smrt-core 0.40.69 → 0.41.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 +29 -4
- package/README.md +20 -1
- package/agents/change-feed.md +1 -1
- package/agents/query-bounds.md +45 -0
- package/agents/schema-paths.md +786 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +5 -3
- package/dist/cascade.d.ts +120 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +430 -0
- package/dist/cascade.js.map +1 -0
- package/dist/change-feed.d.ts +34 -2
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +52 -11
- package/dist/change-feed.js.map +1 -1
- package/dist/class.d.ts +36 -3
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +87 -9
- package/dist/class.js.map +1 -1
- package/dist/collection-cache.js +0 -0
- package/dist/collection-cache.js.map +1 -1
- package/dist/collection.d.ts +130 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +290 -57
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/database.d.ts +8 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +16 -8
- package/dist/database.js.map +1 -1
- package/dist/db-errors.d.ts +105 -0
- package/dist/db-errors.d.ts.map +1 -0
- package/dist/db-errors.js +382 -0
- package/dist/db-errors.js.map +1 -0
- package/dist/decorators/index.d.ts +80 -6
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +102 -12
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +4 -3
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +19 -4
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/types.d.ts +5 -0
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/embedded-write-queue.d.ts +46 -0
- package/dist/embedded-write-queue.d.ts.map +1 -0
- package/dist/embedded-write-queue.js +66 -0
- package/dist/embedded-write-queue.js.map +1 -0
- package/dist/embeddings/storage.d.ts +7 -0
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +29 -12
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/errors.d.ts +31 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -2
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +6 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +38 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +16 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +41 -3
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +22 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +34 -3
- package/dist/generators/rest.js.map +1 -1
- package/dist/hierarchical.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/interceptors.d.ts +21 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +27 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +4 -7
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/static-manifest.js +10 -10
- 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 +19 -19
- package/dist/migrations/differ.d.ts +211 -9
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +613 -50
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts +31 -4
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +49 -5
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +4 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +6 -3
- package/dist/migrations/minor-units.d.ts +162 -0
- package/dist/migrations/minor-units.d.ts.map +1 -0
- package/dist/migrations/minor-units.js +381 -0
- package/dist/migrations/minor-units.js.map +1 -0
- package/dist/migrations/orchestrate.js +35 -6
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/sqlite-rebuild.d.ts +142 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
- package/dist/migrations/sqlite-rebuild.js +514 -0
- package/dist/migrations/sqlite-rebuild.js.map +1 -0
- package/dist/migrations/tracker.d.ts +114 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +331 -16
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +19 -4
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations.js +6 -3
- package/dist/object.d.ts +142 -10
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +196 -41
- package/dist/object.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +240 -0
- package/dist/postgres-timeouts.d.ts.map +1 -0
- package/dist/postgres-timeouts.js +204 -0
- package/dist/postgres-timeouts.js.map +1 -0
- package/dist/query-bounds.d.ts +101 -0
- package/dist/query-bounds.d.ts.map +1 -0
- package/dist/query-bounds.js +177 -0
- package/dist/query-bounds.js.map +1 -0
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +3 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/manifest-field-merge.d.ts +12 -0
- package/dist/registry/manifest-field-merge.d.ts.map +1 -1
- package/dist/registry/manifest-field-merge.js +14 -2
- package/dist/registry/manifest-field-merge.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +22 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +205 -165
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +35 -3
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +41 -46
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +61 -83
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +45 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +92 -28
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +5 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +104 -0
- package/dist/schema/conflict-target.d.ts.map +1 -0
- package/dist/schema/conflict-target.js +129 -0
- package/dist/schema/conflict-target.js.map +1 -0
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -2
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +2 -1
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +12 -1
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +307 -6
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +510 -87
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index-utils.d.ts +120 -0
- package/dist/schema/index-utils.d.ts.map +1 -1
- package/dist/schema/index-utils.js +242 -1
- package/dist/schema/index-utils.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/live-parity.d.ts +90 -0
- package/dist/schema/live-parity.d.ts.map +1 -0
- package/dist/schema/live-parity.js +602 -0
- package/dist/schema/live-parity.js.map +1 -0
- package/dist/schema/manifest-schema.d.ts +121 -0
- package/dist/schema/manifest-schema.d.ts.map +1 -0
- package/dist/schema/manifest-schema.js +267 -0
- package/dist/schema/manifest-schema.js.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +24 -10
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +35 -90
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +65 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -0
- package/dist/schema/system-table-shapes.js +187 -0
- package/dist/schema/system-table-shapes.js.map +1 -0
- package/dist/schema/types.d.ts +103 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +2 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +5 -3
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +20 -8
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js +9 -16
- package/dist/sync/apply.js.map +1 -1
- package/dist/system/compatibility.d.ts +42 -0
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +182 -9
- package/dist/system/compatibility.js.map +1 -1
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +3 -2
- package/dist/system/retention.d.ts +237 -0
- package/dist/system/retention.d.ts.map +1 -0
- package/dist/system/retention.js +497 -0
- package/dist/system/retention.js.map +1 -0
- package/dist/system/schema.d.ts +100 -15
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +81 -45
- package/dist/system/schema.js.map +1 -1
- package/dist/system/types.d.ts +0 -2
- package/dist/system/types.d.ts.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +1 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +4 -9
- 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 +71 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +6 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-parity.js","names":[],"sources":["../../src/schema/live-parity.ts"],"sourcesContent":["/**\n * Live-schema parity check (#2368)\n *\n * Compares a **live** database against the shape the model layer assumes.\n * Three inputs define \"expected\":\n *\n * 1. **Declared table shapes** — the manifest schemas the caller passes in.\n * These give tables, columns, types, nullability, and declared indexes.\n * 2. **Hand-DDL system tables** — `_smrt_*` tables parsed out of\n * `src/system/schema.ts` (see `system-table-shapes.ts`). They never enter\n * the manifest, so nothing else has ever diffed them.\n * 3. **An expected-shape index policy that does not consult the manifest** —\n * every foreign-key / cross-package-ref / tenant column carries a leading\n * index, every declared upsert conflict target is backed by a matching\n * UNIQUE index, and every `unique: true` column is actually unique in the\n * live database.\n *\n * Point 3 is the reason this module exists rather than reusing the migration\n * differ: the differ compares the live database to the manifest, i.e. to the\n * artifact that dropped the index in the first place, which is why a database\n * missing 164 tenant-column indexes reported \"in sync\" (#2356).\n *\n * This module is read-only. It emits findings; it never repairs.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { RETIRED_SYSTEM_TABLES } from '../system/schema.js';\nimport { detectEngine, getDDLStrategy } from './ddl/index.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport { getSystemTableShapes } from './system-table-shapes.js';\nimport type { ColumnDefinition, SchemaDefinition } from './types.js';\n\n/** How badly a finding breaks the deployment. */\nexport type LiveParitySeverity = 'error' | 'warning' | 'info';\n\n/** The class of drift a finding describes. */\nexport type LiveParityFindingKind =\n | 'missing_table'\n | 'extra_table'\n | 'missing_column'\n | 'extra_column'\n | 'column_type_drift'\n | 'column_nullability_drift'\n | 'missing_index'\n | 'extra_index'\n | 'unindexed_reference'\n | 'conflict_target_unindexed'\n | 'conflict_target_not_unique'\n | 'unique_constraint_missing'\n | 'invalid_index';\n\n/** One difference between the live database and the expected shape. */\nexport interface LiveParityFinding {\n kind: LiveParityFindingKind;\n severity: LiveParitySeverity;\n /** Table the finding is about. */\n table: string;\n /** Column or index the finding is about, when it is narrower than a table. */\n target?: string;\n /** Whether the table is an application table or a `_smrt_*` system table. */\n origin: ExpectedTableOrigin;\n message: string;\n recommendation: string;\n details?: Record<string, unknown>;\n}\n\n/** Aggregate result of one parity run. */\nexport interface LiveSchemaParityReport {\n engine: DatabaseEngine;\n /** Expected tables that exist in the live database and were compared. */\n tablesChecked: number;\n /** Expected tables that are absent from the live database. */\n tablesMissing: number;\n /** Whether `_smrt_*` system tables participated. */\n systemTablesIncluded: boolean;\n /**\n * `full` when index metadata (uniqueness, partial predicates, PostgreSQL\n * validity) could be read for this engine; `unavailable` when it could not,\n * in which case every index-class check is skipped rather than guessed.\n */\n indexIntrospection: 'full' | 'unavailable';\n findings: LiveParityFinding[];\n counts: Record<LiveParitySeverity, number>;\n /** True when no `error`-severity finding was produced. */\n ok: boolean;\n}\n\n/** A declared upsert conflict target for one table. */\nexport interface ConflictTargetInput {\n columns: string[];\n /** Where the target came from, e.g. a class name — used in messages. */\n source?: string;\n}\n\nexport interface LiveSchemaParityOptions {\n db: DatabaseInterface;\n /** Expected application table shapes, keyed by table name. */\n schemas?: Record<string, SchemaDefinition>;\n /** Declared upsert conflict targets, keyed by table name. */\n conflictTargets?: Record<string, ConflictTargetInput[]>;\n /** Include `_smrt_*` system tables (default true). */\n includeSystemTables?: boolean;\n /** Explicit engine hint for adapters whose URL is empty or ambiguous. */\n engineHint?: string;\n /** Report live tables no expected shape covers (default true). */\n reportExtraTables?: boolean;\n}\n\n/** Whether an expected table is an application table or a system table. */\nexport type ExpectedTableOrigin = 'application' | 'system';\n\ninterface ExpectedColumn {\n name: string;\n /** Engine-materialized type text. */\n type: string;\n primaryKey: boolean;\n notNull: boolean;\n unique: boolean;\n hasDefault: boolean;\n reference?: ColumnDefinition['referenceKind'];\n}\n\ninterface ExpectedIndex {\n name: string;\n columns: string[];\n unique: boolean;\n}\n\ninterface ExpectedTable {\n name: string;\n origin: ExpectedTableOrigin;\n columns: ExpectedColumn[];\n indexes: ExpectedIndex[];\n conflictTargets: ConflictTargetInput[];\n}\n\ninterface LiveColumn {\n name: string;\n type: string;\n notNull: boolean;\n primaryKey: boolean;\n hasDefault: boolean;\n}\n\ninterface LiveIndex {\n name: string;\n columns: string[];\n unique: boolean;\n /** PostgreSQL `indisvalid`; always true on engines without the concept. */\n valid: boolean;\n /** True for the index backing a primary key. */\n primary: boolean;\n /** True when the index is partial (has a WHERE predicate). */\n partial: boolean;\n}\n\n/**\n * Thrown when the live database cannot be introspected. Callers fail closed:\n * an unreadable database is never reported as \"in sync\".\n */\nexport class LiveSchemaParityError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'LiveSchemaParityError';\n }\n}\n\n/**\n * Compare a live database against the expected shape and return every\n * difference found.\n *\n * @throws {LiveSchemaParityError} when the database cannot be reached or the\n * adapter cannot describe a table. Connection problems must fail closed.\n */\nexport async function checkLiveSchemaParity(\n options: LiveSchemaParityOptions,\n): Promise<LiveSchemaParityReport> {\n const {\n db,\n schemas = {},\n conflictTargets = {},\n includeSystemTables = true,\n engineHint,\n reportExtraTables = true,\n } = options;\n\n const engine = resolveEngine(db, engineHint);\n const expected = buildExpectedTables({\n schemas,\n conflictTargets,\n includeSystemTables,\n engine,\n });\n\n const liveTableNames = await listLiveTables(db, engine);\n const indexCatalog = await readIndexCatalog(db, engine);\n const findings: LiveParityFinding[] = [];\n\n let tablesChecked = 0;\n let tablesMissing = 0;\n\n for (const table of expected) {\n if (!liveTableNames.has(table.name)) {\n tablesMissing++;\n findings.push({\n kind: 'missing_table',\n severity: 'error',\n table: table.name,\n origin: table.origin,\n message: `Table \\`${table.name}\\` is declared but does not exist in the live database.`,\n recommendation:\n table.origin === 'system'\n ? 'Initialize SMRT system tables against this database (they are created on framework bootstrap).'\n : 'Run `smrt db:migrate` to create the missing table.',\n });\n continue;\n }\n\n tablesChecked++;\n const liveColumns = await readLiveColumns(db, table.name);\n findings.push(...compareColumns(table, liveColumns, engine));\n\n if (indexCatalog) {\n const liveIndexes = await indexCatalog.forTable(table.name);\n findings.push(...compareIndexes(table, liveColumns, liveIndexes));\n }\n }\n\n if (reportExtraTables) {\n const expectedNames = new Set(expected.map((table) => table.name));\n for (const liveName of liveTableNames) {\n if (expectedNames.has(liveName)) continue;\n if (isInternalTableName(liveName)) continue;\n // Without system tables in scope, every `_smrt_*` table would be\n // reported as unexplained; that is a scoping artifact, not drift.\n if (!includeSystemTables && liveName.startsWith('_smrt_')) continue;\n // A retired system table is not unexplained drift — the framework\n // deliberately stopped creating it and deliberately does not drop it\n // (issue #2376). Name the exact remedy instead of the generic advice.\n const retired = RETIRED_SYSTEM_TABLES.includes(liveName);\n findings.push({\n kind: 'extra_table',\n severity: 'info',\n table: liveName,\n origin: liveName.startsWith('_smrt_') ? 'system' : 'application',\n message: retired\n ? `Live table \\`${liveName}\\` is a retired SMRT system table; nothing reads or writes it.`\n : `Live table \\`${liveName}\\` is not covered by any declared schema.`,\n recommendation: retired\n ? `Drop it once you have confirmed it is unused: DROP TABLE IF EXISTS ${liveName};`\n : 'Confirm the table belongs to another application sharing this database, or remove it once its owning model is gone.',\n });\n }\n }\n\n const counts: Record<LiveParitySeverity, number> = {\n error: 0,\n warning: 0,\n info: 0,\n };\n for (const finding of findings) {\n counts[finding.severity]++;\n }\n\n return {\n engine,\n tablesChecked,\n tablesMissing,\n systemTablesIncluded: includeSystemTables,\n indexIntrospection: indexCatalog ? 'full' : 'unavailable',\n findings,\n counts,\n ok: counts.error === 0,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Expected shape\n// ---------------------------------------------------------------------------\n\nfunction buildExpectedTables(input: {\n schemas: Record<string, SchemaDefinition>;\n conflictTargets: Record<string, ConflictTargetInput[]>;\n includeSystemTables: boolean;\n engine: DatabaseEngine;\n}): ExpectedTable[] {\n const { schemas, conflictTargets, includeSystemTables, engine } = input;\n const strategy = getDDLStrategy(engine);\n const tables: ExpectedTable[] = [];\n\n for (const [key, schema] of Object.entries(schemas)) {\n const tableName = schema?.tableName || key;\n if (!tableName) continue;\n\n const columns: ExpectedColumn[] = Object.entries(schema.columns ?? {}).map(\n ([columnName, column]) => ({\n name: columnName,\n type: strategy.mapType(column.type),\n primaryKey: column.primaryKey === true,\n notNull: column.notNull === true || column.primaryKey === true,\n unique: column.unique === true,\n hasDefault: column.defaultValue !== undefined,\n reference: column.foreignKey ? 'foreignKey' : column.referenceKind,\n }),\n );\n\n tables.push({\n name: tableName,\n origin: 'application',\n columns,\n indexes: (schema.indexes ?? [])\n // Expression indexes (`@meta({ indexed: true })`) surface with empty\n // or null column lists in introspection, so a column-based comparison\n // can only produce false drift for them.\n .filter((index) => !index.jsonPath && (index.columns?.length ?? 0) > 0)\n .map((index) => ({\n name: index.name,\n columns: index.columns,\n unique: index.unique === true,\n })),\n conflictTargets: dedupeConflictTargets(conflictTargets[tableName] ?? []),\n });\n }\n\n if (includeSystemTables) {\n for (const shape of getSystemTableShapes(engine).values()) {\n tables.push({\n name: shape.tableName,\n origin: 'system',\n columns: shape.columns.map((column) => ({\n name: column.name,\n type: column.type,\n primaryKey: column.primaryKey,\n notNull: column.notNull || column.primaryKey,\n unique: column.unique,\n hasDefault: column.hasDefault,\n })),\n indexes: shape.indexes.map((index) => ({\n name: index.name,\n columns: index.columns,\n unique: index.unique,\n })),\n // Inline `UNIQUE (...)` constraints are what the framework upserts\n // against on system tables, so they are the conflict targets here.\n conflictTargets: dedupeConflictTargets(\n shape.uniqueConstraints.map((columns) => ({\n columns,\n source: `${shape.tableName} DDL`,\n })),\n ),\n });\n }\n }\n\n return tables;\n}\n\nfunction dedupeConflictTargets(\n targets: ConflictTargetInput[],\n): ConflictTargetInput[] {\n const seen = new Map<string, ConflictTargetInput>();\n for (const target of targets) {\n const columns = (target.columns ?? []).filter(Boolean);\n if (columns.length === 0) continue;\n const key = [...columns].sort().join(',');\n if (!seen.has(key)) {\n seen.set(key, { columns, source: target.source });\n }\n }\n return [...seen.values()];\n}\n\n// ---------------------------------------------------------------------------\n// Column comparison\n// ---------------------------------------------------------------------------\n\nfunction compareColumns(\n table: ExpectedTable,\n liveColumns: Map<string, LiveColumn>,\n engine: DatabaseEngine,\n): LiveParityFinding[] {\n const findings: LiveParityFinding[] = [];\n const expectedNames = new Set(table.columns.map((column) => column.name));\n\n for (const column of table.columns) {\n const live = liveColumns.get(column.name);\n if (!live) {\n findings.push({\n kind: 'missing_column',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is declared but missing from the live database.`,\n recommendation:\n 'Run `smrt db:migrate` to add the column before application writes reach this table.',\n details: { expectedType: column.type },\n });\n continue;\n }\n\n if (!typesAreEquivalent(column, live, engine)) {\n findings.push({\n kind: 'column_type_drift',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is \\`${live.type}\\` in the live database but declared \\`${column.type}\\`.`,\n recommendation:\n 'Reconcile the column type with `smrt db:migrate`, or repair the declaration if the live type is correct.',\n details: { expected: column.type, actual: live.type },\n });\n }\n\n // A declared-NOT NULL column that is nullable live silently accepts rows\n // the model layer believes cannot exist; the reverse breaks every insert\n // that omits the column, which is the worse failure.\n if (!column.primaryKey && column.notNull && !live.notNull) {\n findings.push({\n kind: 'column_nullability_drift',\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is declared NOT NULL but is nullable in the live database.`,\n recommendation:\n 'Backfill any NULL values and add the NOT NULL constraint, or relax the declaration.',\n });\n } else if (!column.notNull && live.notNull && !live.hasDefault) {\n findings.push({\n kind: 'column_nullability_drift',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is NOT NULL in the live database with no default, but is not declared NOT NULL. Inserts that omit it will fail.`,\n recommendation:\n 'Drop the orphan NOT NULL constraint, give the column a default, or declare the column required.',\n });\n }\n }\n\n for (const live of liveColumns.values()) {\n if (expectedNames.has(live.name)) continue;\n\n const breaksInserts = live.notNull && !live.hasDefault;\n findings.push({\n kind: 'extra_column',\n severity: breaksInserts ? 'error' : 'info',\n table: table.name,\n target: live.name,\n origin: table.origin,\n message: breaksInserts\n ? `Live column \\`${table.name}.${live.name}\\` is undeclared, NOT NULL, and has no default — every framework insert into this table will fail.`\n : `Live column \\`${table.name}.${live.name}\\` is not declared by the current schema.`,\n recommendation: breaksInserts\n ? 'Drop the orphan column or give it a default; this is the residue of a renamed or removed required field.'\n : 'Confirm the column is intentional legacy data, or drop it once nothing reads it.',\n details: { type: live.type },\n });\n }\n\n return findings;\n}\n\n/**\n * Whether a live column type satisfies its declaration.\n *\n * Two tolerances match the model layer's actual conventions, and mirror the\n * migration differ so the two tools never disagree:\n *\n * - `TEXT` ↔ `UUID` for structural identifier/reference columns, because\n * SQLite has no uuid type and older PostgreSQL deployments legitimately\n * still carry text ids (R11).\n * - `TEXT` ↔ `JSON`/`JSONB` in both directions, because SMRT serializes JSON\n * values into text columns and a native json column already holds exactly\n * that (#1335).\n */\nfunction typesAreEquivalent(\n expected: ExpectedColumn,\n live: LiveColumn,\n engine: DatabaseEngine,\n): boolean {\n const expectedType = normalizeSqlType(expected.type);\n const actualType = normalizeSqlType(live.type);\n if (expectedType === actualType) return true;\n\n const uuidTextPair =\n (expectedType === 'UUID' && actualType === 'TEXT') ||\n (expectedType === 'TEXT' && actualType === 'UUID');\n if (uuidTextPair && isStructuralReference(expected)) {\n return true;\n }\n\n const jsonTextPair =\n (expectedType === 'JSON' && actualType === 'TEXT') ||\n (expectedType === 'TEXT' && actualType === 'JSON');\n if (jsonTextPair) return true;\n\n // SQLite has dynamic typing: BOOLEAN/TIMESTAMP columns are declared with\n // affinities that pragma reports verbatim, and INTEGER/NUMERIC storage is\n // interchangeable for the values SMRT writes.\n if (engine === 'sqlite') {\n const numericPair =\n (expectedType === 'BOOLEAN' && actualType === 'INTEGER') ||\n (expectedType === 'INTEGER' && actualType === 'BOOLEAN');\n if (numericPair) return true;\n }\n\n return false;\n}\n\nfunction isStructuralReference(column: ExpectedColumn): boolean {\n return (\n column.primaryKey ||\n column.reference === 'id' ||\n column.reference === 'foreignKey' ||\n column.reference === 'crossPackageRef' ||\n column.reference === 'tenantId'\n );\n}\n\n/** Fold engine-specific type spellings into comparable buckets. */\nexport function normalizeSqlType(type: string): string {\n const upper = String(type ?? '')\n .toUpperCase()\n .trim()\n .replace(/\\(\\s*[^)]*\\s*\\)/g, '')\n .replace(/\\[\\]$/, '')\n .trim();\n\n if (\n /^(INTEGER|INT|INT2|INT4|INT8|BIGINT|SMALLINT|TINYINT|SERIAL|BIGSERIAL)$/.test(\n upper,\n )\n )\n return 'INTEGER';\n if (\n /^(TEXT|CLOB|STRING|VARCHAR|CHAR|CHARACTER|CHARACTER VARYING|NAME)$/.test(\n upper,\n )\n )\n return 'TEXT';\n if (upper === 'UUID') return 'UUID';\n if (\n /^(REAL|FLOAT|FLOAT4|FLOAT8|DOUBLE|DOUBLE PRECISION|DECIMAL|NUMERIC|NUMBER)$/.test(\n upper,\n )\n )\n return 'REAL';\n if (/^(BOOLEAN|BOOL)$/.test(upper)) return 'BOOLEAN';\n if (/^(TIMESTAMPTZ|TIMESTAMP WITH TIME ZONE)$/.test(upper))\n return 'TIMESTAMPTZ';\n if (\n /^(DATETIME|TIMESTAMP|TIMESTAMP WITHOUT TIME ZONE|DATE|TIME)$/.test(upper)\n )\n return 'TIMESTAMP';\n if (/^(BLOB|BINARY|BYTEA)$/.test(upper)) return 'BLOB';\n if (/^(JSON|JSONB)$/.test(upper)) return 'JSON';\n\n return upper;\n}\n\n// ---------------------------------------------------------------------------\n// Index comparison — the manifest-independent policy lives here\n// ---------------------------------------------------------------------------\n\nfunction compareIndexes(\n table: ExpectedTable,\n liveColumns: Map<string, LiveColumn>,\n liveIndexes: LiveIndex[],\n): LiveParityFinding[] {\n const findings: LiveParityFinding[] = [];\n const byName = new Map(liveIndexes.map((index) => [index.name, index]));\n const signatures = new Set(\n liveIndexes.map((index) => indexSignature(index.columns, index.unique)),\n );\n\n for (const index of liveIndexes) {\n if (index.valid) continue;\n findings.push({\n kind: 'invalid_index',\n severity: 'error',\n table: table.name,\n target: index.name,\n origin: table.origin,\n message: `Index \\`${index.name}\\` on \\`${table.name}\\` is INVALID — PostgreSQL will not use it and a unique index in this state enforces nothing.`,\n recommendation:\n 'Drop and rebuild it (`REINDEX INDEX CONCURRENTLY`), then re-run the parity check.',\n });\n }\n\n // 1. Declared indexes the live database does not carry in that shape.\n // Matching is by column/uniqueness signature rather than by name so an\n // equivalent index under a different name is not reported as missing\n // (#741), and so a same-name index whose uniqueness or columns drifted\n // still is.\n for (const index of table.indexes) {\n if (signatures.has(indexSignature(index.columns, index.unique))) continue;\n // Conflict-target coverage is reported below with a message that explains\n // the actual consequence; do not report the same index twice.\n if (\n table.conflictTargets.some((target) =>\n sameColumnSet(index.columns, target.columns),\n )\n ) {\n continue;\n }\n\n const sameName = byName.get(index.name);\n findings.push({\n kind: 'missing_index',\n severity: 'warning',\n table: table.name,\n target: index.name,\n origin: table.origin,\n message: sameName\n ? `Index \\`${index.name}\\` on \\`${table.name}\\` exists but its shape drifted: declared (${index.columns.join(', ')})${index.unique ? ' UNIQUE' : ''}, live (${sameName.columns.join(', ')})${sameName.unique ? ' UNIQUE' : ''}.`\n : `Declared index \\`${index.name}\\` on \\`${table.name}\\` (${index.columns.join(', ')}) is missing from the live database.`,\n recommendation:\n table.origin === 'system'\n ? 'Re-run SMRT system-table initialization to create the missing system index.'\n : 'Run `smrt db:migrate` to create the missing index.',\n });\n }\n\n // 2. Conflict targets. An upsert whose conflict target has no matching\n // UNIQUE index either errors outright (PostgreSQL) or silently inserts\n // duplicates, so this is the one index class that is an error.\n for (const target of table.conflictTargets) {\n if (target.columns.some((column) => !liveColumns.has(column))) {\n // A missing column is already reported; do not double-report it here.\n continue;\n }\n\n const candidates = liveIndexes.filter((index) =>\n sameColumnSet(index.columns, target.columns),\n );\n // A *partial* unique index cannot arbitrate a plain `ON CONFLICT (cols)`:\n // PostgreSQL only infers an index whose predicate the statement repeats,\n // and on other engines it simply does not constrain the excluded rows.\n if (candidates.some((index) => index.unique && !index.partial)) {\n continue;\n }\n\n const partialUnique = candidates.find(\n (index) => index.unique && index.partial,\n );\n const nonUnique = candidates.find((index) => !index.unique);\n const columnList = target.columns.join(', ');\n const suffix = target.source ? ` (declared by ${target.source})` : '';\n\n if (nonUnique) {\n findings.push({\n kind: 'conflict_target_not_unique',\n severity: 'error',\n table: table.name,\n target: nonUnique.name,\n origin: table.origin,\n message: `Upsert conflict target \\`${table.name}\\` (${columnList})${suffix} is backed by \\`${nonUnique.name}\\`, which is not UNIQUE.`,\n recommendation:\n 'Recreate the index as UNIQUE (`smrt db:migrate`); until then upserts insert duplicates instead of updating.',\n });\n continue;\n }\n\n findings.push({\n kind: 'conflict_target_unindexed',\n severity: 'error',\n table: table.name,\n target: partialUnique ? partialUnique.name : columnList,\n origin: table.origin,\n message: partialUnique\n ? `Upsert conflict target \\`${table.name}\\` (${columnList})${suffix} is backed only by the partial index \\`${partialUnique.name}\\`, which cannot arbitrate the upsert.`\n : `Upsert conflict target \\`${table.name}\\` (${columnList})${suffix} has no matching UNIQUE index in the live database.`,\n recommendation:\n 'Create a full UNIQUE index over exactly these columns (`smrt db:migrate`); PostgreSQL rejects the upsert outright and other engines duplicate rows.',\n });\n }\n\n // 3. Columns declared unique that are not actually unique live.\n for (const column of table.columns) {\n if (!column.unique) continue;\n if (!liveColumns.has(column.name)) continue;\n // A partial unique index constrains only the rows its predicate selects,\n // so it does not make the column unique as declared.\n const unique = liveIndexes.some(\n (index) =>\n index.unique &&\n !index.partial &&\n sameColumnSet(index.columns, [column.name]),\n );\n if (unique) continue;\n\n findings.push({\n kind: 'unique_constraint_missing',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is declared unique but no UNIQUE index enforces it in the live database.`,\n recommendation:\n 'De-duplicate existing values and create the UNIQUE index; the declaration is currently decorative.',\n });\n }\n\n // 4. Manifest-independent coverage policy: every foreign-key, cross-package\n // reference and tenant column needs an index it *leads*, or every lookup\n // and every tenant-scoped list is a full scan (#2356).\n for (const column of table.columns) {\n if (!column.reference) continue;\n if (column.reference === 'id') continue;\n if (!liveColumns.has(column.name)) continue;\n\n const covered = liveIndexes.some(\n (index) => index.columns[0] === column.name,\n );\n if (covered) continue;\n\n findings.push({\n kind: 'unindexed_reference',\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Reference column \\`${table.name}.${column.name}\\` (${column.reference}) leads no index in the live database.`,\n recommendation:\n 'Add an index led by this column; relationship loads, joins and tenant-scoped reads currently scan the whole table.',\n details: { referenceKind: column.reference },\n });\n }\n\n // 5. Live indexes nothing declares. Informational: an operator may have\n // hand-added them, and nothing will recreate them after a rebuild.\n // Indexes that exist because rule 4 demands them are explained, even\n // though the manifest does not declare them.\n const policyLeadColumns = new Set(\n table.columns\n .filter((column) => column.reference && column.reference !== 'id')\n .map((column) => column.name),\n );\n const declaredNames = new Set(table.indexes.map((index) => index.name));\n const declaredSignatures = new Set(\n table.indexes.map((index) => indexSignature(index.columns, index.unique)),\n );\n for (const index of liveIndexes) {\n if (index.primary) continue;\n // Constraint-owned indexes (`sqlite_autoindex_*`, PostgreSQL `*_key` /\n // `*_pkey`) exist because of a table constraint, not because anyone\n // declared an index; reporting them as undeclared is pure noise.\n if (isConstraintOwnedIndexName(index.name)) continue;\n if (policyLeadColumns.has(index.columns[0])) continue;\n if (declaredNames.has(index.name)) continue;\n if (declaredSignatures.has(indexSignature(index.columns, index.unique)))\n continue;\n if (\n table.conflictTargets.some((target) =>\n sameColumnSet(index.columns, target.columns),\n )\n ) {\n continue;\n }\n\n findings.push({\n kind: 'extra_index',\n severity: 'info',\n table: table.name,\n target: index.name,\n origin: table.origin,\n message: `Live index \\`${index.name}\\` on \\`${table.name}\\` (${index.columns.join(', ') || 'expression'}) is not declared by the current schema.`,\n recommendation:\n 'Declare it so a rebuilt database keeps it, or drop it if it is obsolete.',\n details: { unique: index.unique, partial: index.partial },\n });\n }\n\n return findings;\n}\n\nfunction isConstraintOwnedIndexName(name: string): boolean {\n return (\n name.startsWith('sqlite_autoindex_') ||\n name.endsWith('_pkey') ||\n name.endsWith('_key')\n );\n}\n\nfunction indexSignature(columns: string[], unique: boolean): string {\n return `${columns.join(',')}:${unique}`;\n}\n\n/**\n * Conflict targets and unique constraints are order-insensitive: PostgreSQL\n * matches `ON CONFLICT (a, b)` against a unique index on `(b, a)`.\n */\nfunction sameColumnSet(left: string[], right: string[]): boolean {\n if (left.length !== right.length) return false;\n const sortedLeft = [...left].sort();\n const sortedRight = [...right].sort();\n return sortedLeft.every((column, index) => column === sortedRight[index]);\n}\n\n// ---------------------------------------------------------------------------\n// Live introspection\n// ---------------------------------------------------------------------------\n\nfunction resolveEngine(\n db: DatabaseInterface,\n engineHint?: string,\n): DatabaseEngine {\n if (typeof (db as { exportTable?: unknown }).exportTable === 'function') {\n return 'json';\n }\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n return detectEngine(db.url || dbWithConfig.config?.url || '', engineHint);\n}\n\nfunction isInternalTableName(name: string): boolean {\n return (\n name.startsWith('sqlite_') ||\n name.startsWith('pg_') ||\n name.startsWith('duckdb_') ||\n name === 'information_schema'\n );\n}\n\nasync function listLiveTables(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n): Promise<Set<string>> {\n const sql =\n engine === 'postgres'\n ? `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`\n : `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;\n\n try {\n const result = await db.query(sql);\n const rows = (result?.rows ?? []) as {\n name?: string;\n table_name?: string;\n }[];\n return new Set(\n rows.map((row) => row.name || row.table_name || '').filter(Boolean),\n );\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not list tables in the live database: ${describeError(error)}`,\n { cause: error },\n );\n }\n}\n\nasync function readLiveColumns(\n db: DatabaseInterface,\n tableName: string,\n): Promise<Map<string, LiveColumn>> {\n if (typeof db.getTableSchema !== 'function') {\n throw new LiveSchemaParityError(\n 'The configured database adapter cannot describe tables (`getTableSchema` is unavailable), so live-schema parity cannot be verified.',\n );\n }\n\n let schema: Awaited<\n ReturnType<NonNullable<DatabaseInterface['getTableSchema']>>\n >;\n try {\n schema = await db.getTableSchema(tableName);\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not read the live schema of \\`${tableName}\\`: ${describeError(error)}`,\n { cause: error },\n );\n }\n\n const columns = new Map<string, LiveColumn>();\n for (const [name, column] of Object.entries(schema?.columns ?? {})) {\n columns.set(name, {\n name,\n type: String(column?.type ?? ''),\n notNull: column?.notNull === true,\n primaryKey: column?.primaryKey === true,\n hasDefault:\n column?.defaultValue !== undefined && column?.defaultValue !== null,\n });\n }\n return columns;\n}\n\ninterface IndexCatalog {\n forTable(tableName: string): Promise<LiveIndex[]>;\n}\n\n/**\n * Build an index reader for the engine, or `null` when index metadata cannot\n * be read. A null catalog disables every index-class check rather than\n * reporting indexes as missing on an engine we cannot introspect.\n */\nasync function readIndexCatalog(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n): Promise<IndexCatalog | null> {\n if (engine === 'postgres') {\n return readPostgresIndexCatalog(db);\n }\n if (engine === 'sqlite') {\n return { forTable: (tableName) => readSqliteIndexes(db, tableName) };\n }\n return readDuckDbIndexCatalog(db);\n}\n\nasync function readPostgresIndexCatalog(\n db: DatabaseInterface,\n): Promise<IndexCatalog> {\n // One catalog pass for the whole schema: `pg_index` carries uniqueness,\n // primary-key ownership and — unlike `pg_indexes` — `indisvalid`, which is\n // the only way to see an index left INVALID by a failed CONCURRENTLY build.\n const sql = `\n SELECT t.relname AS table_name,\n c.relname AS index_name,\n i.indisunique AS is_unique,\n i.indisprimary AS is_primary,\n i.indisvalid AS is_valid,\n (i.indpred IS NOT NULL) AS is_partial,\n pg_get_indexdef(i.indexrelid) AS index_def\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = 'public'`;\n\n let rows: Record<string, unknown>[];\n try {\n const result = await db.query(sql);\n rows = (result?.rows ?? []) as Record<string, unknown>[];\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not read the PostgreSQL index catalog: ${describeError(error)}`,\n { cause: error },\n );\n }\n\n const byTable = new Map<string, LiveIndex[]>();\n for (const row of rows) {\n const tableName = String(row.table_name ?? '');\n if (!tableName) continue;\n const bucket = byTable.get(tableName) ?? [];\n bucket.push({\n name: String(row.index_name ?? ''),\n columns: parseIndexDefColumns(String(row.index_def ?? '')),\n unique: toBoolean(row.is_unique),\n primary: toBoolean(row.is_primary),\n valid: row.is_valid === undefined ? true : toBoolean(row.is_valid),\n partial: toBoolean(row.is_partial),\n });\n byTable.set(tableName, bucket);\n }\n\n return { forTable: async (tableName) => byTable.get(tableName) ?? [] };\n}\n\n/**\n * Extract the indexed column list from a `CREATE INDEX` definition.\n *\n * Expression components (`lower(name)`, `(meta ->> 'x')`) are kept verbatim so\n * they never collide with a plain column of the same name; ordering is\n * preserved because leading-column coverage is what the policy checks.\n */\nexport function parseIndexDefColumns(indexDef: string): string[] {\n const open = indexDef.indexOf('(');\n if (open === -1) return [];\n\n let depth = 0;\n let end = -1;\n for (let i = open; i < indexDef.length; i++) {\n if (indexDef[i] === '(') depth++;\n else if (indexDef[i] === ')') {\n depth--;\n if (depth === 0) {\n end = i;\n break;\n }\n }\n }\n if (end === -1) return [];\n\n const body = indexDef.slice(open + 1, end);\n const parts: string[] = [];\n let current = '';\n depth = 0;\n for (const char of body) {\n if (char === '(') depth++;\n if (char === ')') depth--;\n if (char === ',' && depth === 0) {\n parts.push(current);\n current = '';\n continue;\n }\n current += char;\n }\n parts.push(current);\n\n return parts\n .map((part) =>\n part\n .trim()\n // Drop operator classes and per-column modifiers PostgreSQL renders.\n .replace(/\\s+(ASC|DESC|NULLS\\s+(FIRST|LAST)|[a-z_]+_ops)\\b/gi, '')\n .trim()\n .replace(/^\"(.*)\"$/, '$1'),\n )\n .filter((part) => part.length > 0);\n}\n\nasync function readSqliteIndexes(\n db: DatabaseInterface,\n tableName: string,\n): Promise<LiveIndex[]> {\n // `pragma_index_list` — unlike `getTableSchema()` — also reports the\n // implicit indexes SQLite creates for inline UNIQUE/PRIMARY KEY table\n // constraints (`sqlite_autoindex_*`), which is exactly where system-table\n // uniqueness lives.\n let listRows: Record<string, unknown>[];\n try {\n const result = await db.query(\n `SELECT * FROM pragma_index_list(${quoteLiteral(tableName)})`,\n );\n listRows = (result?.rows ?? []) as Record<string, unknown>[];\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not read SQLite indexes for \\`${tableName}\\`: ${describeError(error)}`,\n { cause: error },\n );\n }\n\n const indexes: LiveIndex[] = [];\n for (const row of listRows) {\n const name = String(row.name ?? '');\n if (!name) continue;\n\n const infoResult = await db.query(\n `SELECT * FROM pragma_index_info(${quoteLiteral(name)})`,\n );\n const infoRows = (infoResult?.rows ?? []) as Record<string, unknown>[];\n const columns = infoRows\n .slice()\n .sort((left, right) => Number(left.seqno ?? 0) - Number(right.seqno ?? 0))\n .map((infoRow) => (infoRow.name == null ? '' : String(infoRow.name)))\n .filter((column) => column.length > 0);\n\n indexes.push({\n name,\n columns,\n unique: toBoolean(row.unique),\n primary: String(row.origin ?? '') === 'pk',\n valid: true,\n partial: toBoolean(row.partial),\n });\n }\n\n return indexes;\n}\n\nasync function readDuckDbIndexCatalog(\n db: DatabaseInterface,\n): Promise<IndexCatalog | null> {\n // DuckDB (and the DuckDB-backed JSON adapter) expose declared indexes\n // through `duckdb_indexes()`; uniqueness constraints live in\n // `duckdb_constraints()`. Both are best-effort: if either catalog is\n // unavailable the caller skips index checks entirely.\n const byTable = new Map<string, LiveIndex[]>();\n try {\n const result = await db.query(\n `SELECT table_name, index_name, is_unique, sql FROM duckdb_indexes()`,\n );\n for (const row of (result?.rows ?? []) as Record<string, unknown>[]) {\n const tableName = String(row.table_name ?? '');\n if (!tableName) continue;\n const bucket = byTable.get(tableName) ?? [];\n bucket.push({\n name: String(row.index_name ?? ''),\n columns: parseIndexDefColumns(String(row.sql ?? '')),\n unique: toBoolean(row.is_unique),\n primary: false,\n valid: true,\n partial: false,\n });\n byTable.set(tableName, bucket);\n }\n } catch {\n return null;\n }\n\n try {\n const result = await db.query(\n `SELECT table_name, constraint_type, constraint_column_names FROM duckdb_constraints()`,\n );\n for (const row of (result?.rows ?? []) as Record<string, unknown>[]) {\n const constraintType = String(row.constraint_type ?? '').toUpperCase();\n if (constraintType !== 'UNIQUE' && constraintType !== 'PRIMARY KEY') {\n continue;\n }\n const tableName = String(row.table_name ?? '');\n const columns = normalizeColumnNameList(row.constraint_column_names);\n if (!tableName || columns.length === 0) continue;\n\n const bucket = byTable.get(tableName) ?? [];\n bucket.push({\n name: `${tableName}_${columns.join('_')}_${constraintType === 'UNIQUE' ? 'key' : 'pkey'}`,\n columns,\n unique: true,\n primary: constraintType === 'PRIMARY KEY',\n valid: true,\n partial: false,\n });\n byTable.set(tableName, bucket);\n }\n } catch {\n // Constraints are not optional metadata on this engine: DuckDB requires an\n // inline UNIQUE constraint for upsert, so uniqueness lives almost entirely\n // in `duckdb_constraints()` rather than in `duckdb_indexes()`. Without it,\n // `conflict_target_unindexed` and `unique_constraint_missing` would fire as\n // *errors* against a correct database. Degrade the whole catalog instead —\n // the caller then skips every index check and the report says\n // `indexIntrospection: 'unavailable'`.\n return null;\n }\n\n return { forTable: async (tableName) => byTable.get(tableName) ?? [] };\n}\n\nfunction normalizeColumnNameList(value: unknown): string[] {\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry)).filter(Boolean);\n }\n if (typeof value === 'string') {\n return value\n .replace(/^[[{]|[\\]}]$/g, '')\n .split(',')\n .map((entry) => entry.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter(Boolean);\n }\n return [];\n}\n\nfunction toBoolean(value: unknown): boolean {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'number') return value !== 0;\n if (typeof value === 'string') {\n return value === '1' || value.toLowerCase() === 'true' || value === 't';\n }\n return false;\n}\n\nfunction quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":";;;;;;;;AAgKA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,YAAY,SAAiB,SAA+B;EAC1D,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,eAAsB,sBACpB,SACiC;CACjC,MAAM,EACJ,IACA,UAAU,CAAC,GACX,kBAAkB,CAAC,GACnB,sBAAsB,MACtB,YACA,oBAAoB,SAClB;CAEJ,MAAM,SAAS,cAAc,IAAI,UAAU;CAC3C,MAAM,WAAW,oBAAoB;EACnC;EACA;EACA;EACA;CACF,CAAC;CAED,MAAM,iBAAiB,MAAM,eAAe,IAAI,MAAM;CACtD,MAAM,eAAe,MAAM,iBAAiB,IAAI,MAAM;CACtD,MAAM,WAAgC,CAAC;CAEvC,IAAI,gBAAgB;CACpB,IAAI,gBAAgB;CAEpB,KAAK,MAAM,SAAS,UAAU;EAC5B,IAAI,CAAC,eAAe,IAAI,MAAM,IAAI,GAAG;GACnC;GACA,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO,MAAM;IACb,QAAQ,MAAM;IACd,SAAS,WAAW,MAAM,KAAK;IAC/B,gBACE,MAAM,WAAW,WACb,mGACA;GACR,CAAC;GACD;EACF;EAEA;EACA,MAAM,cAAc,MAAM,gBAAgB,IAAI,MAAM,IAAI;EACxD,SAAS,KAAK,GAAG,eAAe,OAAO,aAAa,MAAM,CAAC;EAE3D,IAAI,cAAc;GAChB,MAAM,cAAc,MAAM,aAAa,SAAS,MAAM,IAAI;GAC1D,SAAS,KAAK,GAAG,eAAe,OAAO,aAAa,WAAW,CAAC;EAClE;CACF;CAEA,IAAI,mBAAmB;EACrB,MAAM,gBAAgB,IAAI,IAAI,SAAS,KAAK,UAAU,MAAM,IAAI,CAAC;EACjE,KAAK,MAAM,YAAY,gBAAgB;GACrC,IAAI,cAAc,IAAI,QAAQ,GAAG;GACjC,IAAI,oBAAoB,QAAQ,GAAG;GAGnC,IAAI,CAAC,uBAAuB,SAAS,WAAW,QAAQ,GAAG;GAI3D,MAAM,UAAU,sBAAsB,SAAS,QAAQ;GACvD,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO;IACP,QAAQ,SAAS,WAAW,QAAQ,IAAI,WAAW;IACnD,SAAS,UACL,gBAAgB,SAAS,kEACzB,gBAAgB,SAAS;IAC7B,gBAAgB,UACZ,sEAAsE,SAAS,KAC/E;GACN,CAAC;EACH;CACF;CAEA,MAAM,SAA6C;EACjD,OAAO;EACP,SAAS;EACT,MAAM;CACR;CACA,KAAK,MAAM,WAAW,UACpB,OAAO,QAAQ,SAAS;CAG1B,OAAO;EACL;EACA;EACA;EACA,sBAAsB;EACtB,oBAAoB,eAAe,SAAS;EAC5C;EACA;EACA,IAAI,OAAO,UAAU;CACvB;AACF;AAMA,SAAS,oBAAoB,OAKT;CAClB,MAAM,EAAE,SAAS,iBAAiB,qBAAqB,WAAW;CAClE,MAAM,WAAW,eAAe,MAAM;CACtC,MAAM,SAA0B,CAAC;CAEjC,KAAK,MAAM,CAAC,KAAK,WAAW,OAAO,QAAQ,OAAO,GAAG;EACnD,MAAM,YAAY,QAAQ,aAAa;EACvC,IAAI,CAAC,WAAW;EAEhB,MAAM,UAA4B,OAAO,QAAQ,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,KACpE,CAAC,YAAY,aAAa;GACzB,MAAM;GACN,MAAM,SAAS,QAAQ,OAAO,IAAI;GAClC,YAAY,OAAO,eAAe;GAClC,SAAS,OAAO,YAAY,QAAQ,OAAO,eAAe;GAC1D,QAAQ,OAAO,WAAW;GAC1B,YAAY,OAAO,iBAAiB,KAAA;GACpC,WAAW,OAAO,aAAa,eAAe,OAAO;EACvD,EACF;EAEA,OAAO,KAAK;GACV,MAAM;GACN,QAAQ;GACR;GACA,UAAU,OAAO,WAAW,CAAC,EAAA,CAI1B,QAAQ,UAAU,CAAC,MAAM,aAAa,MAAM,SAAS,UAAU,KAAK,CAAC,CAAC,CACtE,KAAK,WAAW;IACf,MAAM,MAAM;IACZ,SAAS,MAAM;IACf,QAAQ,MAAM,WAAW;GAC3B,EAAE;GACJ,iBAAiB,sBAAsB,gBAAgB,cAAc,CAAC,CAAC;EACzE,CAAC;CACH;CAEA,IAAI,qBACF,KAAK,MAAM,SAAS,qBAAqB,MAAM,CAAC,CAAC,OAAO,GACtD,OAAO,KAAK;EACV,MAAM,MAAM;EACZ,QAAQ;EACR,SAAS,MAAM,QAAQ,KAAK,YAAY;GACtC,MAAM,OAAO;GACb,MAAM,OAAO;GACb,YAAY,OAAO;GACnB,SAAS,OAAO,WAAW,OAAO;GAClC,QAAQ,OAAO;GACf,YAAY,OAAO;EACrB,EAAE;EACF,SAAS,MAAM,QAAQ,KAAK,WAAW;GACrC,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,QAAQ,MAAM;EAChB,EAAE;EAGF,iBAAiB,sBACf,MAAM,kBAAkB,KAAK,aAAa;GACxC;GACA,QAAQ,GAAG,MAAM,UAAU;EAC7B,EAAE,CACJ;CACF,CAAC;CAIL,OAAO;AACT;AAEA,SAAS,sBACP,SACuB;CACvB,MAAM,uBAAO,IAAI,IAAiC;CAClD,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,WAAW,OAAO,WAAW,CAAC,EAAA,CAAG,OAAO,OAAO;EACrD,IAAI,QAAQ,WAAW,GAAG;EAC1B,MAAM,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG;EACxC,IAAI,CAAC,KAAK,IAAI,GAAG,GACf,KAAK,IAAI,KAAK;GAAE;GAAS,QAAQ,OAAO;EAAO,CAAC;CAEpD;CACA,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B;AAMA,SAAS,eACP,OACA,aACA,QACqB;CACrB,MAAM,WAAgC,CAAC;CACvC,MAAM,gBAAgB,IAAI,IAAI,MAAM,QAAQ,KAAK,WAAW,OAAO,IAAI,CAAC;CAExE,KAAK,MAAM,UAAU,MAAM,SAAS;EAClC,MAAM,OAAO,YAAY,IAAI,OAAO,IAAI;EACxC,IAAI,CAAC,MAAM;GACT,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO,MAAM;IACb,QAAQ,OAAO;IACf,QAAQ,MAAM;IACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK;IAC/C,gBACE;IACF,SAAS,EAAE,cAAc,OAAO,KAAK;GACvC,CAAC;GACD;EACF;EAEA,IAAI,CAAC,mBAAmB,QAAQ,MAAM,MAAM,GAC1C,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK,UAAU,KAAK,KAAK,yCAAyC,OAAO,KAAK;GACxH,gBACE;GACF,SAAS;IAAE,UAAU,OAAO;IAAM,QAAQ,KAAK;GAAK;EACtD,CAAC;EAMH,IAAI,CAAC,OAAO,cAAc,OAAO,WAAW,CAAC,KAAK,SAChD,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK;GAC/C,gBACE;EACJ,CAAC;OACI,IAAI,CAAC,OAAO,WAAW,KAAK,WAAW,CAAC,KAAK,YAClD,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK;GAC/C,gBACE;EACJ,CAAC;CAEL;CAEA,KAAK,MAAM,QAAQ,YAAY,OAAO,GAAG;EACvC,IAAI,cAAc,IAAI,KAAK,IAAI,GAAG;EAElC,MAAM,gBAAgB,KAAK,WAAW,CAAC,KAAK;EAC5C,SAAS,KAAK;GACZ,MAAM;GACN,UAAU,gBAAgB,UAAU;GACpC,OAAO,MAAM;GACb,QAAQ,KAAK;GACb,QAAQ,MAAM;GACd,SAAS,gBACL,iBAAiB,MAAM,KAAK,GAAG,KAAK,KAAK,sGACzC,iBAAiB,MAAM,KAAK,GAAG,KAAK,KAAK;GAC7C,gBAAgB,gBACZ,6GACA;GACJ,SAAS,EAAE,MAAM,KAAK,KAAK;EAC7B,CAAC;CACH;CAEA,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAS,mBACP,UACA,MACA,QACS;CACT,MAAM,eAAe,iBAAiB,SAAS,IAAI;CACnD,MAAM,aAAa,iBAAiB,KAAK,IAAI;CAC7C,IAAI,iBAAiB,YAAY,OAAO;CAKxC,KAFG,iBAAiB,UAAU,eAAe,UAC1C,iBAAiB,UAAU,eAAe,WACzB,sBAAsB,QAAQ,GAChD,OAAO;CAMT,IAFG,iBAAiB,UAAU,eAAe,UAC1C,iBAAiB,UAAU,eAAe,QAC3B,OAAO;CAKzB,IAAI,WAAW;MAEV,iBAAiB,aAAa,eAAe,aAC7C,iBAAiB,aAAa,eAAe,WAC/B,OAAO;CAAA;CAG1B,OAAO;AACT;AAEA,SAAS,sBAAsB,QAAiC;CAC9D,OACE,OAAO,cACP,OAAO,cAAc,QACrB,OAAO,cAAc,gBACrB,OAAO,cAAc,qBACrB,OAAO,cAAc;AAEzB;;AAGA,SAAgB,iBAAiB,MAAsB;CACrD,MAAM,QAAQ,OAAO,QAAQ,EAAE,CAAC,CAC7B,YAAY,CAAC,CACb,KAAK,CAAC,CACN,QAAQ,oBAAoB,EAAE,CAAC,CAC/B,QAAQ,SAAS,EAAE,CAAC,CACpB,KAAK;CAER,IACE,0EAA0E,KACxE,KACF,GAEA,OAAO;CACT,IACE,qEAAqE,KACnE,KACF,GAEA,OAAO;CACT,IAAI,UAAU,QAAQ,OAAO;CAC7B,IACE,8EAA8E,KAC5E,KACF,GAEA,OAAO;CACT,IAAI,mBAAmB,KAAK,KAAK,GAAG,OAAO;CAC3C,IAAI,2CAA2C,KAAK,KAAK,GACvD,OAAO;CACT,IACE,+DAA+D,KAAK,KAAK,GAEzE,OAAO;CACT,IAAI,wBAAwB,KAAK,KAAK,GAAG,OAAO;CAChD,IAAI,iBAAiB,KAAK,KAAK,GAAG,OAAO;CAEzC,OAAO;AACT;AAMA,SAAS,eACP,OACA,aACA,aACqB;CACrB,MAAM,WAAgC,CAAC;CACvC,MAAM,SAAS,IAAI,IAAI,YAAY,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CACtE,MAAM,aAAa,IAAI,IACrB,YAAY,KAAK,UAAU,eAAe,MAAM,SAAS,MAAM,MAAM,CAAC,CACxE;CAEA,KAAK,MAAM,SAAS,aAAa;EAC/B,IAAI,MAAM,OAAO;EACjB,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,WAAW,MAAM,KAAK,UAAU,MAAM,KAAK;GACpD,gBACE;EACJ,CAAC;CACH;CAOA,KAAK,MAAM,SAAS,MAAM,SAAS;EACjC,IAAI,WAAW,IAAI,eAAe,MAAM,SAAS,MAAM,MAAM,CAAC,GAAG;EAGjE,IACE,MAAM,gBAAgB,MAAM,WAC1B,cAAc,MAAM,SAAS,OAAO,OAAO,CAC7C,GAEA;EAGF,MAAM,WAAW,OAAO,IAAI,MAAM,IAAI;EACtC,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,WACL,WAAW,MAAM,KAAK,UAAU,MAAM,KAAK,6CAA6C,MAAM,QAAQ,KAAK,IAAI,EAAE,GAAG,MAAM,SAAS,YAAY,GAAG,UAAU,SAAS,QAAQ,KAAK,IAAI,EAAE,GAAG,SAAS,SAAS,YAAY,GAAG,KAC5N,oBAAoB,MAAM,KAAK,UAAU,MAAM,KAAK,MAAM,MAAM,QAAQ,KAAK,IAAI,EAAE;GACvF,gBACE,MAAM,WAAW,WACb,gFACA;EACR,CAAC;CACH;CAKA,KAAK,MAAM,UAAU,MAAM,iBAAiB;EAC1C,IAAI,OAAO,QAAQ,MAAM,WAAW,CAAC,YAAY,IAAI,MAAM,CAAC,GAE1D;EAGF,MAAM,aAAa,YAAY,QAAQ,UACrC,cAAc,MAAM,SAAS,OAAO,OAAO,CAC7C;EAIA,IAAI,WAAW,MAAM,UAAU,MAAM,UAAU,CAAC,MAAM,OAAO,GAC3D;EAGF,MAAM,gBAAgB,WAAW,MAC9B,UAAU,MAAM,UAAU,MAAM,OACnC;EACA,MAAM,YAAY,WAAW,MAAM,UAAU,CAAC,MAAM,MAAM;EAC1D,MAAM,aAAa,OAAO,QAAQ,KAAK,IAAI;EAC3C,MAAM,SAAS,OAAO,SAAS,iBAAiB,OAAO,OAAO,KAAK;EAEnE,IAAI,WAAW;GACb,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO,MAAM;IACb,QAAQ,UAAU;IAClB,QAAQ,MAAM;IACd,SAAS,4BAA4B,MAAM,KAAK,MAAM,WAAW,GAAG,OAAO,kBAAkB,UAAU,KAAK;IAC5G,gBACE;GACJ,CAAC;GACD;EACF;EAEA,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,gBAAgB,cAAc,OAAO;GAC7C,QAAQ,MAAM;GACd,SAAS,gBACL,4BAA4B,MAAM,KAAK,MAAM,WAAW,GAAG,OAAO,yCAAyC,cAAc,KAAK,0CAC9H,4BAA4B,MAAM,KAAK,MAAM,WAAW,GAAG,OAAO;GACtE,gBACE;EACJ,CAAC;CACH;CAGA,KAAK,MAAM,UAAU,MAAM,SAAS;EAClC,IAAI,CAAC,OAAO,QAAQ;EACpB,IAAI,CAAC,YAAY,IAAI,OAAO,IAAI,GAAG;EASnC,IANe,YAAY,MACxB,UACC,MAAM,UACN,CAAC,MAAM,WACP,cAAc,MAAM,SAAS,CAAC,OAAO,IAAI,CAAC,CAE1C,GAAQ;EAEZ,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK;GAC/C,gBACE;EACJ,CAAC;CACH;CAKA,KAAK,MAAM,UAAU,MAAM,SAAS;EAClC,IAAI,CAAC,OAAO,WAAW;EACvB,IAAI,OAAO,cAAc,MAAM;EAC/B,IAAI,CAAC,YAAY,IAAI,OAAO,IAAI,GAAG;EAKnC,IAHgB,YAAY,MACzB,UAAU,MAAM,QAAQ,OAAO,OAAO,IAErC,GAAS;EAEb,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,sBAAsB,MAAM,KAAK,GAAG,OAAO,KAAK,MAAM,OAAO,UAAU;GAChF,gBACE;GACF,SAAS,EAAE,eAAe,OAAO,UAAU;EAC7C,CAAC;CACH;CAMA,MAAM,oBAAoB,IAAI,IAC5B,MAAM,QACH,QAAQ,WAAW,OAAO,aAAa,OAAO,cAAc,IAAI,CAAC,CACjE,KAAK,WAAW,OAAO,IAAI,CAChC;CACA,MAAM,gBAAgB,IAAI,IAAI,MAAM,QAAQ,KAAK,UAAU,MAAM,IAAI,CAAC;CACtE,MAAM,qBAAqB,IAAI,IAC7B,MAAM,QAAQ,KAAK,UAAU,eAAe,MAAM,SAAS,MAAM,MAAM,CAAC,CAC1E;CACA,KAAK,MAAM,SAAS,aAAa;EAC/B,IAAI,MAAM,SAAS;EAInB,IAAI,2BAA2B,MAAM,IAAI,GAAG;EAC5C,IAAI,kBAAkB,IAAI,MAAM,QAAQ,EAAE,GAAG;EAC7C,IAAI,cAAc,IAAI,MAAM,IAAI,GAAG;EACnC,IAAI,mBAAmB,IAAI,eAAe,MAAM,SAAS,MAAM,MAAM,CAAC,GACpE;EACF,IACE,MAAM,gBAAgB,MAAM,WAC1B,cAAc,MAAM,SAAS,OAAO,OAAO,CAC7C,GAEA;EAGF,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,gBAAgB,MAAM,KAAK,UAAU,MAAM,KAAK,MAAM,MAAM,QAAQ,KAAK,IAAI,KAAK,aAAa;GACxG,gBACE;GACF,SAAS;IAAE,QAAQ,MAAM;IAAQ,SAAS,MAAM;GAAQ;EAC1D,CAAC;CACH;CAEA,OAAO;AACT;AAEA,SAAS,2BAA2B,MAAuB;CACzD,OACE,KAAK,WAAW,mBAAmB,KACnC,KAAK,SAAS,OAAO,KACrB,KAAK,SAAS,MAAM;AAExB;AAEA,SAAS,eAAe,SAAmB,QAAyB;CAClE,OAAO,GAAG,QAAQ,KAAK,GAAG,EAAE,GAAG;AACjC;;;;;AAMA,SAAS,cAAc,MAAgB,OAA0B;CAC/D,IAAI,KAAK,WAAW,MAAM,QAAQ,OAAO;CACzC,MAAM,aAAa,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK;CAClC,MAAM,cAAc,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK;CACpC,OAAO,WAAW,OAAO,QAAQ,UAAU,WAAW,YAAY,MAAM;AAC1E;AAMA,SAAS,cACP,IACA,YACgB;CAChB,IAAI,OAAQ,GAAiC,gBAAgB,YAC3D,OAAO;CAET,MAAM,eAAe;CACrB,OAAO,aAAa,GAAG,OAAO,aAAa,QAAQ,OAAO,IAAI,UAAU;AAC1E;AAEA,SAAS,oBAAoB,MAAuB;CAClD,OACE,KAAK,WAAW,SAAS,KACzB,KAAK,WAAW,KAAK,KACrB,KAAK,WAAW,SAAS,KACzB,SAAS;AAEb;AAEA,eAAe,eACb,IACA,QACsB;CACtB,MAAM,MACJ,WAAW,aACP,mFACA;CAEN,IAAI;EAEF,MAAM,QAAQ,MADO,GAAG,MAAM,GAAG,EAAA,EACX,QAAQ,CAAC;EAI/B,OAAO,IAAI,IACT,KAAK,KAAK,QAAQ,IAAI,QAAQ,IAAI,cAAc,EAAE,CAAC,CAAC,OAAO,OAAO,CACpE;CACF,SAAS,OAAO;EACd,MAAM,IAAI,sBACR,+CAA+C,cAAc,KAAK,KAClE,EAAE,OAAO,MAAM,CACjB;CACF;AACF;AAEA,eAAe,gBACb,IACA,WACkC;CAClC,IAAI,OAAO,GAAG,mBAAmB,YAC/B,MAAM,IAAI,sBACR,qIACF;CAGF,IAAI;CAGJ,IAAI;EACF,SAAS,MAAM,GAAG,eAAe,SAAS;CAC5C,SAAS,OAAO;EACd,MAAM,IAAI,sBACR,uCAAuC,UAAU,MAAM,cAAc,KAAK,KAC1E,EAAE,OAAO,MAAM,CACjB;CACF;CAEA,MAAM,0BAAU,IAAI,IAAwB;CAC5C,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,GAC/D,QAAQ,IAAI,MAAM;EAChB;EACA,MAAM,OAAO,QAAQ,QAAQ,EAAE;EAC/B,SAAS,QAAQ,YAAY;EAC7B,YAAY,QAAQ,eAAe;EACnC,YACE,QAAQ,iBAAiB,KAAA,KAAa,QAAQ,iBAAiB;CACnE,CAAC;CAEH,OAAO;AACT;;;;;;AAWA,eAAe,iBACb,IACA,QAC8B;CAC9B,IAAI,WAAW,YACb,OAAO,yBAAyB,EAAE;CAEpC,IAAI,WAAW,UACb,OAAO,EAAE,WAAW,cAAc,kBAAkB,IAAI,SAAS,EAAE;CAErE,OAAO,uBAAuB,EAAE;AAClC;AAEA,eAAe,yBACb,IACuB;CAIvB,MAAM,MAAM;;;;;;;;;;;;;CAcZ,IAAI;CACJ,IAAI;EAEF,QAAQ,MADa,GAAG,MAAM,GAAG,EAAA,EACjB,QAAQ,CAAC;CAC3B,SAAS,OAAO;EACd,MAAM,IAAI,sBACR,gDAAgD,cAAc,KAAK,KACnE,EAAE,OAAO,MAAM,CACjB;CACF;CAEA,MAAM,0BAAU,IAAI,IAAyB;CAC7C,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,YAAY,OAAO,IAAI,cAAc,EAAE;EAC7C,IAAI,CAAC,WAAW;EAChB,MAAM,SAAS,QAAQ,IAAI,SAAS,KAAK,CAAC;EAC1C,OAAO,KAAK;GACV,MAAM,OAAO,IAAI,cAAc,EAAE;GACjC,SAAS,qBAAqB,OAAO,IAAI,aAAa,EAAE,CAAC;GACzD,QAAQ,UAAU,IAAI,SAAS;GAC/B,SAAS,UAAU,IAAI,UAAU;GACjC,OAAO,IAAI,aAAa,KAAA,IAAY,OAAO,UAAU,IAAI,QAAQ;GACjE,SAAS,UAAU,IAAI,UAAU;EACnC,CAAC;EACD,QAAQ,IAAI,WAAW,MAAM;CAC/B;CAEA,OAAO,EAAE,UAAU,OAAO,cAAc,QAAQ,IAAI,SAAS,KAAK,CAAC,EAAE;AACvE;;;;;;;;AASA,SAAgB,qBAAqB,UAA4B;CAC/D,MAAM,OAAO,SAAS,QAAQ,GAAG;CACjC,IAAI,SAAS,IAAI,OAAO,CAAC;CAEzB,IAAI,QAAQ;CACZ,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,MAAM,IAAI,SAAS,QAAQ,KACtC,IAAI,SAAS,OAAO,KAAK;MACpB,IAAI,SAAS,OAAO,KAAK;EAC5B;EACA,IAAI,UAAU,GAAG;GACf,MAAM;GACN;EACF;CACF;CAEF,IAAI,QAAQ,IAAI,OAAO,CAAC;CAExB,MAAM,OAAO,SAAS,MAAM,OAAO,GAAG,GAAG;CACzC,MAAM,QAAkB,CAAC;CACzB,IAAI,UAAU;CACd,QAAQ;CACR,KAAK,MAAM,QAAQ,MAAM;EACvB,IAAI,SAAS,KAAK;EAClB,IAAI,SAAS,KAAK;EAClB,IAAI,SAAS,OAAO,UAAU,GAAG;GAC/B,MAAM,KAAK,OAAO;GAClB,UAAU;GACV;EACF;EACA,WAAW;CACb;CACA,MAAM,KAAK,OAAO;CAElB,OAAO,MACJ,KAAK,SACJ,KACG,KAAK,CAAC,CAEN,QAAQ,sDAAsD,EAAE,CAAC,CACjE,KAAK,CAAC,CACN,QAAQ,YAAY,IAAI,CAC7B,CAAC,CACA,QAAQ,SAAS,KAAK,SAAS,CAAC;AACrC;AAEA,eAAe,kBACb,IACA,WACsB;CAKtB,IAAI;CACJ,IAAI;EAIF,YAAY,MAHS,GAAG,MACtB,mCAAmC,aAAa,SAAS,EAAE,EAC7D,EAAA,EACoB,QAAQ,CAAC;CAC/B,SAAS,OAAO;EACd,MAAM,IAAI,sBACR,uCAAuC,UAAU,MAAM,cAAc,KAAK,KAC1E,EAAE,OAAO,MAAM,CACjB;CACF;CAEA,MAAM,UAAuB,CAAC;CAC9B,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,OAAO,OAAO,IAAI,QAAQ,EAAE;EAClC,IAAI,CAAC,MAAM;EAMX,MAAM,YADY,MAHO,GAAG,MAC1B,mCAAmC,aAAa,IAAI,EAAE,EACxD,EAAA,EAC8B,QAAQ,CAAC,EAAA,CAEpC,MAAM,CAAC,CACP,MAAM,MAAM,UAAU,OAAO,KAAK,SAAS,CAAC,IAAI,OAAO,MAAM,SAAS,CAAC,CAAC,CAAC,CACzE,KAAK,YAAa,QAAQ,QAAQ,OAAO,KAAK,OAAO,QAAQ,IAAI,CAAE,CAAC,CACpE,QAAQ,WAAW,OAAO,SAAS,CAAC;EAEvC,QAAQ,KAAK;GACX;GACA;GACA,QAAQ,UAAU,IAAI,MAAM;GAC5B,SAAS,OAAO,IAAI,UAAU,EAAE,MAAM;GACtC,OAAO;GACP,SAAS,UAAU,IAAI,OAAO;EAChC,CAAC;CACH;CAEA,OAAO;AACT;AAEA,eAAe,uBACb,IAC8B;CAK9B,MAAM,0BAAU,IAAI,IAAyB;CAC7C,IAAI;EACF,MAAM,SAAS,MAAM,GAAG,MACtB,qEACF;EACA,KAAK,MAAM,OAAQ,QAAQ,QAAQ,CAAC,GAAiC;GACnE,MAAM,YAAY,OAAO,IAAI,cAAc,EAAE;GAC7C,IAAI,CAAC,WAAW;GAChB,MAAM,SAAS,QAAQ,IAAI,SAAS,KAAK,CAAC;GAC1C,OAAO,KAAK;IACV,MAAM,OAAO,IAAI,cAAc,EAAE;IACjC,SAAS,qBAAqB,OAAO,IAAI,OAAO,EAAE,CAAC;IACnD,QAAQ,UAAU,IAAI,SAAS;IAC/B,SAAS;IACT,OAAO;IACP,SAAS;GACX,CAAC;GACD,QAAQ,IAAI,WAAW,MAAM;EAC/B;CACF,QAAQ;EACN,OAAO;CACT;CAEA,IAAI;EACF,MAAM,SAAS,MAAM,GAAG,MACtB,uFACF;EACA,KAAK,MAAM,OAAQ,QAAQ,QAAQ,CAAC,GAAiC;GACnE,MAAM,iBAAiB,OAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,YAAY;GACrE,IAAI,mBAAmB,YAAY,mBAAmB,eACpD;GAEF,MAAM,YAAY,OAAO,IAAI,cAAc,EAAE;GAC7C,MAAM,UAAU,wBAAwB,IAAI,uBAAuB;GACnE,IAAI,CAAC,aAAa,QAAQ,WAAW,GAAG;GAExC,MAAM,SAAS,QAAQ,IAAI,SAAS,KAAK,CAAC;GAC1C,OAAO,KAAK;IACV,MAAM,GAAG,UAAU,GAAG,QAAQ,KAAK,GAAG,EAAE,GAAG,mBAAmB,WAAW,QAAQ;IACjF;IACA,QAAQ;IACR,SAAS,mBAAmB;IAC5B,OAAO;IACP,SAAS;GACX,CAAC;GACD,QAAQ,IAAI,WAAW,MAAM;EAC/B;CACF,QAAQ;EAQN,OAAO;CACT;CAEA,OAAO,EAAE,UAAU,OAAO,cAAc,QAAQ,IAAI,SAAS,KAAK,CAAC,EAAE;AACvE;AAEA,SAAS,wBAAwB,OAA0B;CACzD,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,UAAU,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO;CAE3D,IAAI,OAAO,UAAU,UACnB,OAAO,MACJ,QAAQ,iBAAiB,EAAE,CAAC,CAC5B,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,QAAQ,gBAAgB,EAAE,CAAC,CAAC,CACxD,OAAO,OAAO;CAEnB,OAAO,CAAC;AACV;AAEA,SAAS,UAAU,OAAyB;CAC1C,IAAI,OAAO,UAAU,WAAW,OAAO;CACvC,IAAI,OAAO,UAAU,UAAU,OAAO,UAAU;CAChD,IAAI,OAAO,UAAU,UACnB,OAAO,UAAU,OAAO,MAAM,YAAY,MAAM,UAAU,UAAU;CAEtE,OAAO;AACT;AAEA,SAAS,aAAa,OAAuB;CAC3C,OAAO,IAAI,MAAM,QAAQ,MAAM,IAAI,EAAE;AACvC;AAEA,SAAS,cAAc,OAAwB;CAC7C,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { DatabaseEngine, EngineSpecificDDL } from './ddl/types.js';
|
|
2
|
+
import { ColumnDefinition, IndexDefinition, SchemaDefinition } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Column shape as it appears in raw manifest JSON. `ManifestColumnDefinition`
|
|
5
|
+
* spells the default `default`; registry-side `ColumnDefinition` spells it
|
|
6
|
+
* `defaultValue`. Hand-authored manifests use either.
|
|
7
|
+
*/
|
|
8
|
+
export interface ManifestColumnLike {
|
|
9
|
+
type: string;
|
|
10
|
+
primaryKey?: boolean;
|
|
11
|
+
referenceKind?: ColumnDefinition['referenceKind'];
|
|
12
|
+
notNull?: boolean;
|
|
13
|
+
unique?: boolean;
|
|
14
|
+
default?: unknown;
|
|
15
|
+
defaultValue?: unknown;
|
|
16
|
+
foreignKey?: ColumnDefinition['foreignKey'];
|
|
17
|
+
check?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Index shape as it appears in raw manifest JSON. */
|
|
20
|
+
export interface ManifestIndexLike {
|
|
21
|
+
name: string;
|
|
22
|
+
columns?: string[];
|
|
23
|
+
unique?: boolean;
|
|
24
|
+
where?: string;
|
|
25
|
+
jsonPath?: IndexDefinition['jsonPath'];
|
|
26
|
+
}
|
|
27
|
+
/** The subset of a manifest object's `schema` this module reads. */
|
|
28
|
+
export interface ManifestSchemaLike {
|
|
29
|
+
tableName: string;
|
|
30
|
+
ddl?: string;
|
|
31
|
+
columns?: Record<string, ManifestColumnLike>;
|
|
32
|
+
indexes?: ManifestIndexLike[];
|
|
33
|
+
version?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Convert one raw manifest column map into structured `ColumnDefinition`s.
|
|
37
|
+
* Picks known fields only, so stray manifest keys never leak into DDL.
|
|
38
|
+
*/
|
|
39
|
+
export declare function manifestColumnsToDefinitions(columns: Record<string, ManifestColumnLike> | undefined): Record<string, ColumnDefinition>;
|
|
40
|
+
/**
|
|
41
|
+
* Convert raw manifest indexes into structured `IndexDefinition`s, keeping
|
|
42
|
+
* `where` (partial) and `jsonPath` (expression) targets — the two fields the
|
|
43
|
+
* retired string-based renderers dropped.
|
|
44
|
+
*/
|
|
45
|
+
export declare function manifestIndexesToDefinitions(indexes: ManifestIndexLike[] | undefined): IndexDefinition[];
|
|
46
|
+
/**
|
|
47
|
+
* Convert one manifest object's `schema` into a `SchemaDefinition`.
|
|
48
|
+
*
|
|
49
|
+
* The cached `ddl` string is carried along (non-authoritative) so callers can
|
|
50
|
+
* fall back to it when the manifest exposes no structured columns.
|
|
51
|
+
*/
|
|
52
|
+
export declare function manifestSchemaToDefinition(schema: ManifestSchemaLike): SchemaDefinition;
|
|
53
|
+
/**
|
|
54
|
+
* One physical table collected from one or more manifest objects (STI
|
|
55
|
+
* subclasses share a table and each contributes columns and indexes).
|
|
56
|
+
*/
|
|
57
|
+
export interface CollectedManifestTable {
|
|
58
|
+
tableName: string;
|
|
59
|
+
/**
|
|
60
|
+
* Structured union of every contributor: columns first-wins by name,
|
|
61
|
+
* indexes deduplicated by name.
|
|
62
|
+
*/
|
|
63
|
+
definition: SchemaDefinition;
|
|
64
|
+
/**
|
|
65
|
+
* `true` when every contributor exposed structured columns, i.e. the whole
|
|
66
|
+
* table is rendered by a DDL strategy. `false` means at least one
|
|
67
|
+
* contributor carried only a cached `ddl` string: the CREATE TABLE is then
|
|
68
|
+
* the strategy render of the structured contributors (if any) merged with
|
|
69
|
+
* the cached strings of the column-less ones. The decision is per table,
|
|
70
|
+
* not per contributor.
|
|
71
|
+
*/
|
|
72
|
+
structured: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The contributors' cached `ddl` strings merged column-wise (legacy path).
|
|
75
|
+
* Empty when no contributor carried a `ddl`.
|
|
76
|
+
*/
|
|
77
|
+
legacyDdl: string;
|
|
78
|
+
/** `<package>:<ClassName>` labels of the contributors, in encounter order. */
|
|
79
|
+
sources: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Deterministic STI merge of two structured definitions: existing columns
|
|
83
|
+
* win, new columns are appended, indexes are deduplicated by name.
|
|
84
|
+
*/
|
|
85
|
+
export declare function mergeSchemaDefinitionInto(target: SchemaDefinition, incoming: SchemaDefinition): void;
|
|
86
|
+
/**
|
|
87
|
+
* Collect manifest schemas into physical tables.
|
|
88
|
+
*
|
|
89
|
+
* Accepts the `schema` entries of manifest objects (any package, any manifest)
|
|
90
|
+
* in encounter order and groups them by `tableName`, merging STI contributors
|
|
91
|
+
* structurally. Objects without a `schema`/`tableName` are skipped by the
|
|
92
|
+
* caller; objects with neither columns nor `ddl` are ignored here.
|
|
93
|
+
*/
|
|
94
|
+
export declare function collectManifestTables(entries: Iterable<{
|
|
95
|
+
schema: ManifestSchemaLike;
|
|
96
|
+
source: string;
|
|
97
|
+
}>): Map<string, CollectedManifestTable>;
|
|
98
|
+
/**
|
|
99
|
+
* Render a collected table for one engine.
|
|
100
|
+
*
|
|
101
|
+
* Structured tables go through the engine strategy for the CREATE TABLE
|
|
102
|
+
* (per-engine types, inline UNIQUE where the engine needs it) and for every
|
|
103
|
+
* index (partial `WHERE`, JSON-path expressions, engine-specific syntax).
|
|
104
|
+
* Tables with a column-less contributor keep that contributor's cached `ddl`
|
|
105
|
+
* string, made minimally safe with `materializeManifestDDLForEngine`, merged
|
|
106
|
+
* on top of the strategy render of any structured contributors — so a
|
|
107
|
+
* structured STI base never loses its engine typing to a hand-authored
|
|
108
|
+
* subclass, and the subclass's extra columns and table constraints survive.
|
|
109
|
+
* Indexes always render through the strategy — the string never carried them.
|
|
110
|
+
*/
|
|
111
|
+
export declare function renderCollectedManifestTable(table: CollectedManifestTable, engine: DatabaseEngine): EngineSpecificDDL;
|
|
112
|
+
/**
|
|
113
|
+
* Table-level constraints (`UNIQUE(...)`, `CHECK (...)`, `CONSTRAINT ...`,
|
|
114
|
+
* ...) declared in a cached CREATE TABLE string. Structured `columns` cannot
|
|
115
|
+
* represent them, so a structured render drops them — exactly as `db:migrate`
|
|
116
|
+
* does. Exposed so callers can warn instead of losing them silently.
|
|
117
|
+
*
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
export declare function cachedDdlTableConstraints(ddl: string): string[];
|
|
121
|
+
//# sourceMappingURL=manifest-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-schema.d.ts","sourceRoot":"","sources":["../../src/schema/manifest-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAYH,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAEjB,MAAM,YAAY,CAAC;AAIpB;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAC;IAClD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;CACxC;AAED,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,SAAS,GACtD,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAqBlC;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,iBAAiB,EAAE,GAAG,SAAS,GACvC,eAAe,EAAE,CAgBnB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,kBAAkB,GACzB,gBAAgB,CAWlB;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,EAAE,gBAAgB,CAAC;IAC7B;;;;;;;OAOG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,gBAAgB,GACzB,IAAI,CAaN;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,QAAQ,CAAC;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,GAChE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAwDrC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,sBAAsB,EAC7B,MAAM,EAAE,cAAc,GACrB,iBAAiB,CAwDnB;AAgCD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAE/D"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { quoteIdentifier } from "./sql-identifiers.js";
|
|
2
|
+
import { getDDLStrategy } from "./ddl/index.js";
|
|
3
|
+
import { findCreateTableBodyRange, getSQLDefinitionComparisonKey, getSQLDefinitionIdentifier, isSQLTableConstraintDefinition, materializeManifestDDLForEngine, tokenizeSQLDDLBody } from "./ddl/materialize-manifest.js";
|
|
4
|
+
import { createLogger } from "@happyvertical/logger";
|
|
5
|
+
//#region src/schema/manifest-schema.ts
|
|
6
|
+
/**
|
|
7
|
+
* Manifest schema → structured `SchemaDefinition` helpers (#2358).
|
|
8
|
+
*
|
|
9
|
+
* A manifest's `schema.ddl` string is an engine-neutral CREATE TABLE preview:
|
|
10
|
+
* it carries no indexes and no per-engine type mapping, so nothing that needs
|
|
11
|
+
* an executable schema may treat it as authoritative. Every consumer that
|
|
12
|
+
* reads raw manifest JSON — `SchemaAggregator`, `createIsolatedTestDbFromManifest`
|
|
13
|
+
* in `@happyvertical/smrt-vitest`, third-party tooling — should collect the
|
|
14
|
+
* structured `columns` / `indexes` through this module and render them with
|
|
15
|
+
* `getDDLStrategy(engine)`, exactly as `db:migrate` does.
|
|
16
|
+
*
|
|
17
|
+
* The cached `ddl` string is used only when a manifest object exposes no
|
|
18
|
+
* structured columns at all (hand-authored or pre-structured manifests); that
|
|
19
|
+
* legacy path keeps working but is not engine-complete.
|
|
20
|
+
*
|
|
21
|
+
* Keep this module light — type-only imports plus the DDL strategies and the
|
|
22
|
+
* logger — so it can be re-exported from lightweight entry points without
|
|
23
|
+
* pulling the registry/collection module graph in by itself.
|
|
24
|
+
*/
|
|
25
|
+
var logger = createLogger({ level: "info" });
|
|
26
|
+
/**
|
|
27
|
+
* Convert one raw manifest column map into structured `ColumnDefinition`s.
|
|
28
|
+
* Picks known fields only, so stray manifest keys never leak into DDL.
|
|
29
|
+
*/
|
|
30
|
+
function manifestColumnsToDefinitions(columns) {
|
|
31
|
+
const result = {};
|
|
32
|
+
for (const [name, column] of Object.entries(columns || {})) {
|
|
33
|
+
if (!column || typeof column.type !== "string") continue;
|
|
34
|
+
const definition = { type: column.type };
|
|
35
|
+
if (column.primaryKey !== void 0) definition.primaryKey = column.primaryKey;
|
|
36
|
+
if (column.referenceKind !== void 0) definition.referenceKind = column.referenceKind;
|
|
37
|
+
if (column.notNull !== void 0) definition.notNull = column.notNull;
|
|
38
|
+
if (column.unique !== void 0) definition.unique = column.unique;
|
|
39
|
+
const defaultValue = column.defaultValue !== void 0 ? column.defaultValue : column.default;
|
|
40
|
+
if (defaultValue !== void 0) definition.defaultValue = defaultValue;
|
|
41
|
+
if (column.foreignKey) definition.foreignKey = { ...column.foreignKey };
|
|
42
|
+
if (column.check) definition.check = column.check;
|
|
43
|
+
result[name] = definition;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Convert raw manifest indexes into structured `IndexDefinition`s, keeping
|
|
49
|
+
* `where` (partial) and `jsonPath` (expression) targets — the two fields the
|
|
50
|
+
* retired string-based renderers dropped.
|
|
51
|
+
*/
|
|
52
|
+
function manifestIndexesToDefinitions(indexes) {
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const index of indexes || []) {
|
|
55
|
+
if (!index?.name) continue;
|
|
56
|
+
const definition = {
|
|
57
|
+
name: index.name,
|
|
58
|
+
columns: Array.isArray(index.columns) ? [...index.columns] : []
|
|
59
|
+
};
|
|
60
|
+
if (index.unique !== void 0) definition.unique = index.unique;
|
|
61
|
+
if (index.where) definition.where = index.where;
|
|
62
|
+
if (index.jsonPath?.column && index.jsonPath.path) definition.jsonPath = { ...index.jsonPath };
|
|
63
|
+
result.push(definition);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Convert one manifest object's `schema` into a `SchemaDefinition`.
|
|
69
|
+
*
|
|
70
|
+
* The cached `ddl` string is carried along (non-authoritative) so callers can
|
|
71
|
+
* fall back to it when the manifest exposes no structured columns.
|
|
72
|
+
*/
|
|
73
|
+
function manifestSchemaToDefinition(schema) {
|
|
74
|
+
return {
|
|
75
|
+
tableName: schema.tableName,
|
|
76
|
+
ddl: schema.ddl,
|
|
77
|
+
columns: manifestColumnsToDefinitions(schema.columns),
|
|
78
|
+
indexes: manifestIndexesToDefinitions(schema.indexes),
|
|
79
|
+
triggers: [],
|
|
80
|
+
foreignKeys: [],
|
|
81
|
+
dependencies: [],
|
|
82
|
+
version: schema.version ?? ""
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Deterministic STI merge of two structured definitions: existing columns
|
|
87
|
+
* win, new columns are appended, indexes are deduplicated by name.
|
|
88
|
+
*/
|
|
89
|
+
function mergeSchemaDefinitionInto(target, incoming) {
|
|
90
|
+
for (const [name, column] of Object.entries(incoming.columns)) if (!(name in target.columns)) target.columns[name] = column;
|
|
91
|
+
const indexNames = new Set(target.indexes.map((index) => index.name));
|
|
92
|
+
for (const index of incoming.indexes) if (!indexNames.has(index.name)) {
|
|
93
|
+
indexNames.add(index.name);
|
|
94
|
+
target.indexes.push(index);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Collect manifest schemas into physical tables.
|
|
99
|
+
*
|
|
100
|
+
* Accepts the `schema` entries of manifest objects (any package, any manifest)
|
|
101
|
+
* in encounter order and groups them by `tableName`, merging STI contributors
|
|
102
|
+
* structurally. Objects without a `schema`/`tableName` are skipped by the
|
|
103
|
+
* caller; objects with neither columns nor `ddl` are ignored here.
|
|
104
|
+
*/
|
|
105
|
+
function collectManifestTables(entries) {
|
|
106
|
+
const tables = /* @__PURE__ */ new Map();
|
|
107
|
+
const constraintCarriers = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const { schema, source } of entries) {
|
|
109
|
+
if (!schema?.tableName) continue;
|
|
110
|
+
const definition = manifestSchemaToDefinition(schema);
|
|
111
|
+
const hasColumns = Object.keys(definition.columns).length > 0;
|
|
112
|
+
const ddl = typeof schema.ddl === "string" ? schema.ddl : "";
|
|
113
|
+
if (!hasColumns && !ddl.trim()) continue;
|
|
114
|
+
if (hasColumns && ddl.trim()) {
|
|
115
|
+
const constraints = cachedDdlTableConstraints(ddl);
|
|
116
|
+
if (constraints.length > 0) {
|
|
117
|
+
const carriers = constraintCarriers.get(schema.tableName) ?? [];
|
|
118
|
+
carriers.push({
|
|
119
|
+
source,
|
|
120
|
+
constraints
|
|
121
|
+
});
|
|
122
|
+
constraintCarriers.set(schema.tableName, carriers);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const existing = tables.get(schema.tableName);
|
|
126
|
+
if (!existing) {
|
|
127
|
+
tables.set(schema.tableName, {
|
|
128
|
+
tableName: schema.tableName,
|
|
129
|
+
definition,
|
|
130
|
+
structured: hasColumns,
|
|
131
|
+
legacyDdl: ddl,
|
|
132
|
+
sources: [source]
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
mergeSchemaDefinitionInto(existing.definition, definition);
|
|
137
|
+
existing.structured = existing.structured && hasColumns;
|
|
138
|
+
existing.legacyDdl = existing.legacyDdl ? ddl ? mergeManifestDDL(existing.legacyDdl, ddl) : existing.legacyDdl : ddl;
|
|
139
|
+
existing.sources.push(source);
|
|
140
|
+
}
|
|
141
|
+
for (const [tableName, carriers] of constraintCarriers) {
|
|
142
|
+
if (!tables.get(tableName)?.structured) continue;
|
|
143
|
+
for (const { source, constraints } of carriers) warnAboutDroppedTableConstraints(tableName, source, constraints);
|
|
144
|
+
}
|
|
145
|
+
return tables;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Render a collected table for one engine.
|
|
149
|
+
*
|
|
150
|
+
* Structured tables go through the engine strategy for the CREATE TABLE
|
|
151
|
+
* (per-engine types, inline UNIQUE where the engine needs it) and for every
|
|
152
|
+
* index (partial `WHERE`, JSON-path expressions, engine-specific syntax).
|
|
153
|
+
* Tables with a column-less contributor keep that contributor's cached `ddl`
|
|
154
|
+
* string, made minimally safe with `materializeManifestDDLForEngine`, merged
|
|
155
|
+
* on top of the strategy render of any structured contributors — so a
|
|
156
|
+
* structured STI base never loses its engine typing to a hand-authored
|
|
157
|
+
* subclass, and the subclass's extra columns and table constraints survive.
|
|
158
|
+
* Indexes always render through the strategy — the string never carried them.
|
|
159
|
+
*/
|
|
160
|
+
function renderCollectedManifestTable(table, engine) {
|
|
161
|
+
const strategy = getDDLStrategy(engine);
|
|
162
|
+
const hasColumns = Object.keys(table.definition.columns).length > 0;
|
|
163
|
+
let createTable;
|
|
164
|
+
if (table.structured) createTable = strategy.generateCreateTable(table.definition);
|
|
165
|
+
else if (hasColumns) createTable = mergeManifestDDL(strategy.generateCreateTable(table.definition), materializeManifestDDLForEngine(table.legacyDdl, engine));
|
|
166
|
+
else createTable = materializeManifestDDLForEngine(table.legacyDdl, engine);
|
|
167
|
+
if (!table.structured && strategy.requiresInlineUnique()) {
|
|
168
|
+
const uniqueElements = table.definition.indexes.filter((index) => index.unique && !index.where && !index.jsonPath && index.columns.length > 0).map((index) => `UNIQUE(${index.columns.map((column) => quoteIdentifier(column)).join(", ")})`);
|
|
169
|
+
const declared = new Set(cachedDdlTableConstraints(createTable).map(normalizeTableElement));
|
|
170
|
+
const missing = uniqueElements.filter((element) => !declared.has(normalizeTableElement(element)));
|
|
171
|
+
if (missing.length > 0) createTable = mergeManifestDDL(createTable, `CREATE TABLE ${quoteIdentifier(table.tableName)} (\n ${missing.join(",\n ")}\n)`);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
createTable,
|
|
175
|
+
indexes: strategy.generateIndexes(table.definition),
|
|
176
|
+
triggers: strategy.generateTriggers(table.definition)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function parseDefinitionsFromDDL(ddl) {
|
|
180
|
+
const columns = /* @__PURE__ */ new Map();
|
|
181
|
+
const tableElements = [];
|
|
182
|
+
const bodyRange = findCreateTableBodyRange(ddl);
|
|
183
|
+
if (!bodyRange) return {
|
|
184
|
+
columns,
|
|
185
|
+
tableElements
|
|
186
|
+
};
|
|
187
|
+
const [bodyStart, bodyEnd] = bodyRange;
|
|
188
|
+
const segments = tokenizeSQLDDLBody(ddl.slice(bodyStart + 1, bodyEnd));
|
|
189
|
+
for (const segment of segments) {
|
|
190
|
+
const identifier = getSQLDefinitionIdentifier(segment);
|
|
191
|
+
if (!identifier) continue;
|
|
192
|
+
if (isSQLTableConstraintDefinition(segment)) {
|
|
193
|
+
tableElements.push(segment);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
columns.set(identifier.name, segment);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
columns,
|
|
200
|
+
tableElements
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Table-level constraints (`UNIQUE(...)`, `CHECK (...)`, `CONSTRAINT ...`,
|
|
205
|
+
* ...) declared in a cached CREATE TABLE string. Structured `columns` cannot
|
|
206
|
+
* represent them, so a structured render drops them — exactly as `db:migrate`
|
|
207
|
+
* does. Exposed so callers can warn instead of losing them silently.
|
|
208
|
+
*
|
|
209
|
+
* @internal
|
|
210
|
+
*/
|
|
211
|
+
function cachedDdlTableConstraints(ddl) {
|
|
212
|
+
return parseDefinitionsFromDDL(ddl).tableElements;
|
|
213
|
+
}
|
|
214
|
+
/** Quote/whitespace/case-insensitive key for a table constraint element. */
|
|
215
|
+
function normalizeTableElement(element) {
|
|
216
|
+
return element.replace(/"/g, "").replace(/\s+/g, "").toLowerCase();
|
|
217
|
+
}
|
|
218
|
+
var warnedDroppedConstraints = /* @__PURE__ */ new Set();
|
|
219
|
+
/**
|
|
220
|
+
* Warn once per process per table+source when a structured contributor's
|
|
221
|
+
* cached `ddl` declares table constraints that the structured render cannot
|
|
222
|
+
* carry. The structured schema is authoritative: a constraint that lives only
|
|
223
|
+
* in the string never reaches `db:migrate` either, so rendering it here would
|
|
224
|
+
* recreate the test/production drift #2358 removes.
|
|
225
|
+
*/
|
|
226
|
+
function warnAboutDroppedTableConstraints(tableName, source, constraints) {
|
|
227
|
+
const key = `${tableName}\0${source}`;
|
|
228
|
+
if (warnedDroppedConstraints.has(key)) return;
|
|
229
|
+
warnedDroppedConstraints.add(key);
|
|
230
|
+
logger.warn(`[manifest-schema] ${source}: cached ddl for table "${tableName}" declares ${constraints.length} table constraint(s) that structured columns cannot carry (${constraints.join("; ")}). They are not rendered — the structured schema is authoritative (#2358). Declare uniqueness through indexes/conflictColumns or a column-level constraint instead.`);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Merge two cached CREATE TABLE strings for the same table (STI subclasses):
|
|
234
|
+
* the union of columns, existing definitions winning, new columns inserted
|
|
235
|
+
* before any trailing table constraints, missing table constraints appended.
|
|
236
|
+
*
|
|
237
|
+
* Only the legacy column-less manifest path uses this; structured manifests
|
|
238
|
+
* merge through `mergeSchemaDefinitionInto`.
|
|
239
|
+
*/
|
|
240
|
+
function mergeManifestDDL(existingDDL, newDDL) {
|
|
241
|
+
const existingDefinitions = parseDefinitionsFromDDL(existingDDL);
|
|
242
|
+
const newDefinitions = parseDefinitionsFromDDL(newDDL);
|
|
243
|
+
const missingCols = [];
|
|
244
|
+
for (const [name, def] of newDefinitions.columns) if (!existingDefinitions.columns.has(name)) missingCols.push(def);
|
|
245
|
+
const existingTableElements = new Set(existingDefinitions.tableElements.map(getSQLDefinitionComparisonKey));
|
|
246
|
+
const missingTableElements = newDefinitions.tableElements.filter((definition) => !existingTableElements.has(getSQLDefinitionComparisonKey(definition)));
|
|
247
|
+
if (missingCols.length === 0 && missingTableElements.length === 0) return existingDDL;
|
|
248
|
+
const bodyRange = findCreateTableBodyRange(existingDDL);
|
|
249
|
+
if (!bodyRange) return existingDDL;
|
|
250
|
+
const [bodyStart, bodyEnd] = bodyRange;
|
|
251
|
+
const prefix = existingDDL.slice(0, bodyStart + 1);
|
|
252
|
+
const body = existingDDL.slice(bodyStart + 1, bodyEnd);
|
|
253
|
+
const suffix = existingDDL.slice(bodyEnd);
|
|
254
|
+
const segments = tokenizeSQLDDLBody(body);
|
|
255
|
+
const firstConstraintIndex = segments.findIndex((segment) => isSQLTableConstraintDefinition(segment));
|
|
256
|
+
const insertionIndex = firstConstraintIndex === -1 ? segments.length : firstConstraintIndex;
|
|
257
|
+
return `${prefix}\n${[
|
|
258
|
+
...segments.slice(0, insertionIndex),
|
|
259
|
+
...missingCols,
|
|
260
|
+
...segments.slice(insertionIndex),
|
|
261
|
+
...missingTableElements
|
|
262
|
+
].map((segment) => ` ${segment}`).join(",\n")}\n${suffix}`;
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
export { cachedDdlTableConstraints, collectManifestTables, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, renderCollectedManifestTable };
|
|
266
|
+
|
|
267
|
+
//# sourceMappingURL=manifest-schema.js.map
|
|
@@ -0,0 +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,18 +1,33 @@
|
|
|
1
1
|
import { SmartObjectManifest } from '../scanner/types.js';
|
|
2
|
+
import { ManifestColumnLike } from './manifest-schema.js';
|
|
2
3
|
/**
|
|
3
4
|
* Aggregated table information including DDL, indexes, and source tracking.
|
|
4
5
|
*/
|
|
5
6
|
export interface AggregatedTable {
|
|
6
7
|
/** Table name */
|
|
7
8
|
tableName: string;
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* CREATE TABLE statement rendered by the dialect's DDL strategy from the
|
|
11
|
+
* merged structured columns of every contributing class (STI union). A
|
|
12
|
+
* table with a column-less contributor (hand-authored manifest) has that
|
|
13
|
+
* contributor's cached `ddl` merged on top; a table with no structured
|
|
14
|
+
* columns at all is the merged cached `ddl`.
|
|
15
|
+
*/
|
|
9
16
|
ddl: string;
|
|
10
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* CREATE INDEX statements rendered by the same strategy (partial `WHERE`
|
|
19
|
+
* and JSON-path targets included), deduplicated by index name across
|
|
20
|
+
* sources.
|
|
21
|
+
*/
|
|
11
22
|
indexes: string[];
|
|
12
23
|
/** Package:className sources that contribute to this table */
|
|
13
24
|
sources: string[];
|
|
14
|
-
/**
|
|
15
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Manifest column metadata as published in `manifest.json` (`default`,
|
|
27
|
+
* not `defaultValue`), merged across contributors — first contributor wins
|
|
28
|
+
* per column name, matching the rendered DDL.
|
|
29
|
+
*/
|
|
30
|
+
columns?: Record<string, ManifestColumnLike>;
|
|
16
31
|
}
|
|
17
32
|
/**
|
|
18
33
|
* Result of schema aggregation across packages.
|
|
@@ -92,14 +107,13 @@ export declare class SchemaAggregator {
|
|
|
92
107
|
/**
|
|
93
108
|
* Extract schemas from manifests, deduplicating STI tables.
|
|
94
109
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
110
|
+
* STI tables (shared by multiple classes across packages) are merged
|
|
111
|
+
* structurally — columns first-wins by name, indexes deduplicated by name —
|
|
112
|
+
* and every table is rendered through the dialect's DDL strategy, the same
|
|
113
|
+
* renderer `db:migrate` uses. The manifest's cached `schema.ddl` string is
|
|
114
|
+
* consulted only for objects that expose no structured columns (#2358).
|
|
97
115
|
*/
|
|
98
116
|
private extractSchemas;
|
|
99
|
-
/**
|
|
100
|
-
* Format an index definition into a CREATE INDEX statement.
|
|
101
|
-
*/
|
|
102
|
-
private formatIndexDdl;
|
|
103
117
|
/**
|
|
104
118
|
* Apply minimal filtering to remove infrastructure/collection tables.
|
|
105
119
|
*/
|