@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"differ.js","names":[],"sources":["../../src/migrations/differ.ts"],"sourcesContent":["/**\n * Schema Differ\n *\n * Compares manifest schemas to database schemas and generates\n * a SchemaDiff with the differences.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { detectEngine, getDDLStrategy } from '../schema/ddl/index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport type {\n ColumnDefinition,\n IndexDefinition,\n SchemaChange,\n SchemaDefinition,\n SchemaDiff,\n SQLDataType,\n} from '../schema/types.js';\nimport { isJsonPathIndex, renderIndexTarget } from '../schema/utils.js';\nimport type { DatabaseInterface, SqlTableSchemaInfo } from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Valid SQLDataType values for validation\n */\nconst VALID_SQL_DATA_TYPES: Set<SQLDataType> = new Set([\n 'TEXT',\n 'INTEGER',\n 'REAL',\n 'BLOB',\n 'BOOLEAN',\n 'JSON',\n 'TIMESTAMP',\n 'UUID',\n]);\n\ninterface GeneratedTypeUpgradeSQL {\n sql: string;\n statements?: string[];\n}\n\n/**\n * Check if a string is a valid SQLDataType\n */\nfunction isValidSQLDataType(type: string): type is SQLDataType {\n return VALID_SQL_DATA_TYPES.has(type as SQLDataType);\n}\n\n/**\n * Options for schema comparison\n */\nexport interface DiffOptions {\n /** Include dropped tables in diff (default: false for safety) */\n includeDroppedTables?: boolean;\n /** Include dropped columns in diff (default: false for safety) */\n includeDroppedColumns?: boolean;\n /**\n * Drop indexes that exist in the database but are absent from the manifest\n * AND are not functionally equivalent to anything in the manifest. Default:\n * false. The differ always skips primary-key indexes (`*_pkey`) and the\n * implicit indexes that PostgreSQL creates from inline UNIQUE constraints\n * (`*_key`) — those are owned by table-level constraints, not by the\n * index list, and dropping them here would break the constraint.\n */\n includeDroppedIndexes?: boolean;\n /** Ignore type mismatches (just log warnings) */\n ignoreTypeMismatches?: boolean;\n /**\n * Explicit engine hint forwarded to `detectEngine` when picking the DDL\n * strategy used for *existing-table* SQL (ALTER/CREATE INDEX/etc.). Use\n * this when `db.url` is empty or ambiguous (e.g. JSON adapter, in-memory\n * wrappers where the URL lives on `db.config?.url`). Without it the\n * comparer falls back to URL-only detection, which can produce SQLite-\n * flavored SQL on a connection whose caller meant Postgres or DuckDB.\n */\n engineHint?: string;\n /**\n * Explicit confirmation that every historical writer of legacy PostgreSQL\n * Date columns used UTC wall times. Without this acknowledgement,\n * TIMESTAMP/TEXT/JSON to TIMESTAMPTZ drift is reported as manual rather\n * than reinterpreted automatically.\n */\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n}\n\n/**\n * Suffix patterns for indexes that the differ refuses to drop.\n * - `_pkey`: PostgreSQL primary key implicit index.\n * - `_key`: PostgreSQL implicit index for inline `UNIQUE (...)` table\n * constraints. Dropping these by name does not drop the underlying\n * constraint, and dropping the constraint requires a separate DDL path\n * (`ALTER TABLE ... DROP CONSTRAINT`) the differ does not emit today.\n */\nconst PROTECTED_INDEX_SUFFIXES = ['_pkey', '_key'];\n\nfunction isProtectedDbIndexName(name: string): boolean {\n return PROTECTED_INDEX_SUFFIXES.some((suffix) => name.endsWith(suffix));\n}\n\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n return db.url || dbWithConfig.config?.url || '';\n}\n\n/**\n * Normalize a partial-index `WHERE` predicate so semantically-identical\n * clauses from different sources compare equal (issue #1692).\n *\n * The manifest stores predicates roughly as written (`_meta_type = 'Article'`),\n * SQLite/DuckDB echo the original CREATE INDEX text verbatim, and PostgreSQL\n * re-renders them with type casts and extra parentheses\n * (`((_meta_type)::text = 'Article'::text)`). Normalization:\n *\n * - strips a leading `WHERE` keyword,\n * - removes PostgreSQL `::type` casts (single-word type names — the only kind\n * SMRT-generated partial predicates produce, e.g. `_meta_type::text`),\n * - removes parentheses (SMRT only emits simple `col = 'literal'` predicates,\n * so grouping carries no meaning here),\n * - lowercases everything OUTSIDE single-quoted string literals (SQL keywords\n * and identifiers are case-insensitive; literals such as STI discriminator\n * class names are case-sensitive, so they are preserved verbatim),\n * - collapses whitespace and tightens spacing around comparison operators.\n *\n * Returns '' for an absent/empty predicate (i.e. a non-partial index).\n */\nexport function normalizeIndexPredicate(where?: string | null): string {\n if (!where) return '';\n const stripped = where.trim().replace(/^WHERE\\s+/i, '');\n if (!stripped) return '';\n\n let out = '';\n let i = 0;\n while (i < stripped.length) {\n if (stripped[i] === \"'\") {\n // Consume a single-quoted string literal verbatim, honoring the SQL\n // `''` escape for an embedded quote.\n let literal = \"'\";\n i++;\n while (i < stripped.length) {\n if (stripped[i] === \"'\") {\n if (stripped[i + 1] === \"'\") {\n literal += \"''\";\n i += 2;\n continue;\n }\n literal += \"'\";\n i++;\n break;\n }\n literal += stripped[i];\n i++;\n }\n out += literal;\n } else {\n let run = '';\n while (i < stripped.length && stripped[i] !== \"'\") {\n run += stripped[i];\n i++;\n }\n out += normalizeNonLiteralPredicateRun(run);\n }\n }\n return out;\n}\n\n/**\n * Normalize the non-literal portion of a predicate (everything outside a\n * single-quoted string literal). Type casts and parentheses are dropped,\n * keywords/identifiers are lowercased, and whitespace is canonicalized.\n */\nfunction normalizeNonLiteralPredicateRun(run: string): string {\n return run\n .replace(/::[A-Za-z_]\\w*/g, '') // drop single-word PostgreSQL type casts\n .replace(/[()]/g, '') // drop parentheses\n .toLowerCase()\n .replace(/\\s+/g, ' ')\n .replace(/\\s*([=<>!]+)\\s*/g, '$1') // tighten comparison operators\n .trim();\n}\n\n/**\n * Extract the normalized partial-index predicate from a `CREATE INDEX`\n * statement — the `WHERE` tail that follows the column-list close paren.\n * Works for both SQLite/DuckDB `sqlite_master.sql` text and PostgreSQL\n * `pg_indexes.indexdef`. Returns '' for a non-partial index.\n */\nexport function extractIndexPredicate(createIndexSql: string): string {\n // The predicate is the tail after the column-list ')': `... (cols) WHERE x`.\n // Anchoring on `) WHERE` (rather than a bare `WHERE`) avoids matching a\n // column literally named \"where\" inside the indexed column list.\n const match = createIndexSql.match(/\\)\\s*WHERE\\s+([\\s\\S]+?)\\s*;?\\s*$/i);\n if (!match) return '';\n return normalizeIndexPredicate(match[1]);\n}\n\n/**\n * SchemaComparer class for comparing manifest schemas to database\n */\nexport class SchemaComparer {\n private db: DatabaseInterface;\n private options: DiffOptions;\n private engine: DatabaseEngine;\n private ddlStrategy: ReturnType<typeof getDDLStrategy>;\n\n constructor(db: DatabaseInterface, options: DiffOptions = {}) {\n this.db = db;\n this.options = {\n includeDroppedTables: false,\n includeDroppedColumns: false,\n includeDroppedIndexes: false,\n ignoreTypeMismatches: false,\n ...options,\n };\n // Use the shared detectEngine utility for consistent detection.\n // Handles :memory:, .json, and other edge cases. The JSON adapter is\n // detected structurally (it exposes `exportTable`) because its url can be\n // empty; otherwise fall back to URL-based detection, where `engineHint`\n // lets callers override when `db.url` is empty or points at an adapter\n // whose engine isn't obvious from the URL alone (some in-memory wrappers).\n this.engine =\n typeof (this.db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : detectEngine(resolveDatabaseUrl(this.db), this.options.engineHint);\n this.ddlStrategy = getDDLStrategy(this.engine);\n }\n\n /**\n * Compare manifest schemas to database and return differences\n */\n async compare(\n manifestSchemas: Record<string, SchemaDefinition>,\n ): Promise<SchemaDiff> {\n const diff: SchemaDiff = {\n added_tables: [],\n dropped_tables: [],\n changes: [],\n has_changes: false,\n };\n\n // Get list of existing tables\n const existingTables = await this.getExistingTables();\n\n // Check each manifest schema against database\n for (const [tableName, schema] of Object.entries(manifestSchemas)) {\n if (!existingTables.has(tableName)) {\n // Table doesn't exist - add to added_tables\n diff.added_tables.push(schema);\n diff.has_changes = true;\n } else {\n // Table exists - compare columns and indexes\n const tableChanges = await this.compareTable(tableName, schema);\n if (tableChanges.length > 0) {\n diff.changes.push(...tableChanges);\n diff.has_changes = true;\n }\n }\n }\n\n // Check for dropped tables (if enabled)\n if (this.options.includeDroppedTables) {\n for (const tableName of existingTables) {\n // Skip system tables\n if (tableName.startsWith('_smrt_') || tableName.startsWith('sqlite_')) {\n continue;\n }\n if (!manifestSchemas[tableName]) {\n diff.dropped_tables.push(tableName);\n diff.has_changes = true;\n }\n }\n }\n\n return diff;\n }\n\n /**\n * Compare a single table's schema to manifest\n */\n async compareTable(\n tableName: string,\n manifest: SchemaDefinition,\n ): Promise<SchemaChange[]> {\n const changes: SchemaChange[] = [];\n\n // Get current table schema from database\n const dbSchema = await this.db.getTableSchema?.(tableName);\n if (!dbSchema) {\n // Table doesn't exist - this is an add_table case\n return changes;\n }\n\n // Compare columns\n const columnChanges = this.compareColumns(tableName, manifest, dbSchema);\n changes.push(...columnChanges);\n\n // Partial-index predicates are not surfaced by `getTableSchema()` (the\n // @happyvertical/sql introspection returns only name/columns/unique), so\n // introspect them separately. Without this, two indexes on the same\n // column(s) that differ only by their WHERE clause — e.g. distinct STI\n // child partial indexes — would compare equal and the differ would miss\n // adds/drops/changes of the predicate (issue #1692).\n const dbIndexPredicates = await this.getDbIndexPredicates(tableName);\n\n // Compare indexes\n const indexChanges = this.compareIndexes(\n tableName,\n manifest,\n dbSchema,\n dbIndexPredicates,\n );\n changes.push(...indexChanges);\n\n return changes;\n }\n\n /**\n * Compare columns between manifest and database\n */\n private compareColumns(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n ): SchemaChange[] {\n const changes: SchemaChange[] = [];\n const dbColumnNames = new Set(Object.keys(dbSchema.columns));\n\n // Check for new and modified columns\n for (const [colName, colDef] of Object.entries(manifest.columns)) {\n if (!dbColumnNames.has(colName)) {\n // Column doesn't exist - add it\n changes.push({\n type: 'add_column',\n table: tableName,\n name: colName,\n column: colDef,\n sql: this.generateAddColumnSQL(tableName, colName, colDef),\n });\n } else {\n // Column exists - check for type mismatch\n const dbCol = dbSchema.columns[colName];\n\n // Map manifest's abstract type to engine-specific type\n // e.g., JSON → TEXT for SQLite, JSON → JSONB for PostgreSQL\n // Validate the manifest type before mapping\n const manifestType = colDef.type;\n if (!isValidSQLDataType(manifestType)) {\n // Invalid manifest type - treat as TEXT (safest fallback)\n logger.warn(\n `[SchemaComparer] Invalid manifest type \"${manifestType}\" for ${tableName}.${colName}, treating as TEXT`,\n );\n }\n const validatedType: SQLDataType = isValidSQLDataType(manifestType)\n ? manifestType\n : 'TEXT';\n const expectedEngineType = this.ddlStrategy.mapType(validatedType);\n const normalizedExpected = this.normalizeType(expectedEngineType);\n const normalizedActual = this.normalizeType(dbCol.type);\n\n // R11: native `uuid` and `text` are interchangeable for SMRT-owned\n // identifiers/references, but not for arbitrary provenance text. Keep\n // the tolerance directional:\n // - manifest UUID + DB text is tolerated for structural ID/ref\n // columns so old deployments are not forced into native UUID.\n // - manifest TEXT + DB uuid is tolerated only for structural ID/ref\n // columns that are intentionally UUID-compatible.\n // Plain TEXT columns with DB uuid now surface as repairable drift.\n const isUuidTextEquivalent = this.isUuidTextEquivalentColumn(\n colName,\n colDef,\n normalizedExpected,\n normalizedActual,\n );\n\n // #1335: native `json`/`jsonb` (DB) and `text` (manifest) are\n // interchangeable for SMRT — the convention is to serialize JSON values\n // into TEXT columns, and a native-json column already holds exactly that\n // data. So the differ must NOT flag a json<->text difference in EITHER\n // direction:\n // - manifest TEXT vs DB json (native-json column, text-convention manifest)\n // - manifest JSON vs DB text (the canary case: an enum/plain field\n // mis-inferred as JSON by a downstream scanner, sitting on a real\n // `text` column holding bare values like 'active')\n // Generating an ALTER here is pure churn at best and data-destroying at\n // worst: `status::jsonb` on a column holding 'active' raises\n // \"invalid input syntax for type json\" and aborts the whole atomic\n // migration. Like the uuid/text tolerance, this lives at the equality\n // gate only (not in `normalizeType`) so `isCompatibleTypeUpgrade` still\n // treats JSON and TEXT as distinct buckets for OTHER upgrade paths.\n const isJsonTextEquivalent =\n (normalizedExpected === 'JSON' && normalizedActual === 'TEXT') ||\n (normalizedExpected === 'TEXT' && normalizedActual === 'JSON');\n\n if (\n normalizedExpected !== normalizedActual &&\n !isUuidTextEquivalent &&\n !isJsonTextEquivalent\n ) {\n // Check if this is a safe type upgrade that SMRT can handle\n // Since SMRT owns the data lifecycle, we know the intent from the manifest\n if (this.isCompatibleTypeUpgrade(colDef.type, dbCol.type)) {\n // Generate type upgrade SQL\n const generatedSQL = this.generateTypeUpgradeSQL(\n tableName,\n colName,\n colDef,\n dbCol.type,\n );\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: {\n expected: colDef.type,\n actual: dbCol.type,\n },\n sql: generatedSQL.sql,\n ...(generatedSQL.statements\n ? { sqlStatements: generatedSQL.statements }\n : {}),\n });\n } else if (!this.options.ignoreTypeMismatches) {\n changes.push({\n type: 'type_mismatch',\n table: tableName,\n name: colName,\n mismatch: {\n expected: colDef.type,\n actual: dbCol.type,\n },\n });\n }\n }\n }\n }\n\n // Check for dropped columns (if enabled)\n if (this.options.includeDroppedColumns) {\n const manifestColumnNames = new Set(Object.keys(manifest.columns));\n for (const colName of dbColumnNames) {\n if (!manifestColumnNames.has(colName)) {\n changes.push({\n type: 'drop_column',\n table: tableName,\n name: colName,\n sql: this.generateDropColumnSQL(tableName, colName),\n });\n }\n }\n }\n\n return changes;\n }\n\n private isUuidTextEquivalentColumn(\n columnName: string,\n colDef: ColumnDefinition,\n normalizedExpected: string,\n normalizedActual: string,\n ): boolean {\n const expectedUuidActualText =\n normalizedExpected === 'UUID' && normalizedActual === 'TEXT';\n const expectedTextActualUuid =\n normalizedExpected === 'TEXT' && normalizedActual === 'UUID';\n\n if (!expectedUuidActualText && !expectedTextActualUuid) {\n return false;\n }\n\n return this.isStructuralUuidCompatibleColumn(columnName, colDef);\n }\n\n private isStructuralUuidCompatibleColumn(\n columnName: string,\n colDef: ColumnDefinition,\n ): boolean {\n return (\n colDef.primaryKey === true ||\n Boolean(colDef.foreignKey) ||\n colDef.referenceKind === 'id' ||\n colDef.referenceKind === 'foreignKey' ||\n colDef.referenceKind === 'crossPackageRef' ||\n colDef.referenceKind === 'tenantId' ||\n (columnName === 'id' && colDef.type === 'TEXT')\n );\n }\n\n /**\n * Compare indexes between manifest and database\n *\n * Four classes of drift the differ now detects:\n *\n * 1. **Missing index** — manifest has an index neither the DB has by name\n * nor any equivalent-by-signature. Emit `add_index`. (Issue #741: the\n * signature check protects against creating duplicates when STI child\n * classes register indexes with different name prefixes.)\n *\n * 2. **Same-name shape drift** — DB has an index with the manifest's name,\n * but its columns, uniqueness flag, or partial-index `WHERE` predicate\n * differ. This covers the uniqueness flip in issue #1165\n * (`tenants_slug_context_meta_type_idx` materialized non-unique while\n * the manifest declares it unique) and the predicate drift in issue\n * #1692 (a partial index whose `WHERE` clause was added, removed, or\n * altered). Emit `drop_index` + `add_index` so the next migrate cycle\n * recreates it with the correct shape.\n *\n * 3. **Orphan in DB** — DB has an index with no manifest counterpart by\n * name and no signature equivalent. Emit `drop_index` *only* when the\n * caller opts in via `includeDroppedIndexes`, and even then never for\n * PostgreSQL implicit indexes (`*_pkey`, `*_key`) — those are owned by\n * table-level constraints and need a separate `DROP CONSTRAINT` path\n * that the differ does not emit yet.\n *\n * 4. **Partial-index predicate drift / collision** — two indexes on the\n * same column(s) and uniqueness that differ only by their `WHERE`\n * predicate (e.g. distinct STI child partial indexes) are no longer\n * collapsed to one signature, so the signature-equivalence path (b)\n * won't claim one for the other.\n *\n * @param dbIndexPredicates - Normalized `WHERE` predicate per DB index\n * name from {@link getDbIndexPredicates}. `null` means predicate\n * introspection was unavailable for this engine/adapter, in which case\n * the comparison falls back to predicate-unaware signatures (the prior\n * behavior) so existing partial indexes are never flagged as false drift.\n */\n private compareIndexes(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n dbIndexPredicates: Map<string, string> | null = null,\n ): SchemaChange[] {\n const changes: SchemaChange[] = [];\n\n // Only fold predicates into signatures when we actually read them back\n // from the live DB. If introspection was unavailable both sides use the\n // empty predicate, which reproduces the prior column+unique-only behavior\n // and cannot manufacture false positives on existing partial indexes.\n const predicateAware = dbIndexPredicates !== null;\n const dbPredicateFor = (name: string): string =>\n predicateAware ? (dbIndexPredicates?.get(name) ?? '') : '';\n const manifestPredicateFor = (idx: IndexDefinition): string =>\n predicateAware ? normalizeIndexPredicate(idx.where) : '';\n\n // Index DB indexes by name and by signature for fast lookup.\n // dbIndexSignatures groups *all* DB index names sharing a signature so\n // duplicates under different names all get claimed by a single matching\n // manifest entry — otherwise the orphan sweep would drop the un-claimed\n // siblings even though they are functionally equivalent to the manifest.\n const dbIndexesByName = new Map<\n string,\n { columns: string[]; unique: boolean }\n >();\n const dbIndexSignatures = new Map<string, Set<string>>();\n for (const idx of dbSchema.indexes) {\n const unique = idx.unique ?? false;\n dbIndexesByName.set(idx.name, { columns: idx.columns, unique });\n const signature = this.getIndexSignature(\n idx.columns,\n unique,\n dbPredicateFor(idx.name),\n );\n let bucket = dbIndexSignatures.get(signature);\n if (!bucket) {\n bucket = new Set();\n dbIndexSignatures.set(signature, bucket);\n }\n bucket.add(idx.name);\n }\n\n // Manifest signatures — used during the orphan sweep to skip DB indexes\n // that match a manifest entry's signature even if no specific manifest\n // entry \"claimed\" them by name.\n const manifestSignatureSet = new Set<string>();\n for (const idx of manifest.indexes) {\n manifestSignatureSet.add(\n this.getIndexSignature(idx, undefined, manifestPredicateFor(idx)),\n );\n }\n\n // Track which DB indexes a manifest entry has claimed, so the orphan\n // pass below doesn't re-flag indexes that match by signature alone.\n const claimedDbIndexes = new Set<string>();\n\n for (const idx of manifest.indexes) {\n const manifestSignature = this.getIndexSignature(\n idx,\n undefined,\n manifestPredicateFor(idx),\n );\n\n // (a) Same name in DB — verify shape matches.\n const dbByName = dbIndexesByName.get(idx.name);\n if (dbByName) {\n claimedDbIndexes.add(idx.name);\n\n // JSON-path indexes (`@meta({ indexed: true })`) cannot be reliably\n // compared by DB introspection — SQLite expression indexes surface\n // as `[null]` columns, so the signature would always mismatch and\n // every diff run would emit drop+recreate. Trust the name match\n // here; if the json path itself changes, the index name changes\n // too (we encode the field name into it), so a name match is a\n // stronger guarantee than the column list for this index family.\n if (isJsonPathIndex(idx)) {\n continue;\n }\n\n const dbSignature = this.getIndexSignature(\n dbByName.columns,\n dbByName.unique,\n dbPredicateFor(idx.name),\n );\n if (dbSignature === manifestSignature) {\n continue; // Same name, same shape — nothing to do.\n }\n\n // Same name, drifted shape. Most often this is a uniqueness flip\n // (issue #1165: 3-column index materialized non-unique). Recreate.\n //\n // Rollback caveat: the auto-generated DOWN script for the\n // `add_index` half drops the (newly correct) index, and the\n // `drop_index` half has no DOWN. Rolling back a recreate leaves\n // the table without the index entirely instead of restoring the\n // wrong-shape original. Capturing the original shape would\n // require richer DB introspection than the differ currently has,\n // so this asymmetry is accepted — the failure mode after an\n // un-rolled-back recreate is \"missing index\" rather than\n // \"permanently broken UPSERT,\" which is recoverable.\n changes.push({\n type: 'drop_index',\n table: tableName,\n name: idx.name,\n sql: this.generateDropIndexSQL(idx.name),\n });\n changes.push({\n type: 'add_index',\n table: tableName,\n name: idx.name,\n index: idx,\n sql: this.generateAddIndexSQL(tableName, idx),\n });\n continue;\n }\n\n // (b) Different name in DB but functionally equivalent — keep as-is.\n // Claim *every* DB name sharing this signature so duplicate-shape\n // indexes (e.g., a stale `<name>_idx` plus the implicit `<name>_key`\n // from the same constraint) all survive the orphan sweep.\n const equivalentIndexNames = dbIndexSignatures.get(manifestSignature);\n if (equivalentIndexNames && equivalentIndexNames.size > 0) {\n for (const name of equivalentIndexNames) {\n claimedDbIndexes.add(name);\n }\n continue;\n }\n\n // (c) Genuinely missing — add it.\n changes.push({\n type: 'add_index',\n table: tableName,\n name: idx.name,\n index: idx,\n sql: this.generateAddIndexSQL(tableName, idx),\n });\n }\n\n // Orphan-index sweep (opt-in via includeDroppedIndexes).\n if (this.options.includeDroppedIndexes) {\n for (const idx of dbSchema.indexes) {\n if (claimedDbIndexes.has(idx.name)) continue;\n if (isProtectedDbIndexName(idx.name)) continue;\n\n // Belt-and-suspenders: even if a DB index wasn't formally claimed\n // by a manifest entry (e.g., shape-drift recreate consumed the\n // claim slot), don't drop it if its signature still matches\n // something the manifest declares. That would contradict the\n // option doc (\"not functionally equivalent to anything in the\n // manifest\") and risk dropping a still-needed index.\n const idxSignature = this.getIndexSignature(\n idx.columns,\n idx.unique ?? false,\n dbPredicateFor(idx.name),\n );\n if (manifestSignatureSet.has(idxSignature)) continue;\n\n changes.push({\n type: 'drop_index',\n table: tableName,\n name: idx.name,\n sql: this.generateDropIndexSQL(idx.name),\n });\n }\n }\n\n return changes;\n }\n\n /**\n * Generate a signature for an index based on its columns, uniqueness, and\n * (normalized) partial-index predicate. Used for functional equivalence\n * checking (Issue #741) and predicate-drift detection (Issue #1692).\n *\n * Note: Column order is preserved because it is semantically significant for\n * composite indexes. An index on (a, b) is NOT equivalent to (b, a) - they\n * have different query performance characteristics.\n *\n * The trailing predicate component distinguishes partial indexes that share\n * columns and uniqueness but differ by their `WHERE` clause (e.g. distinct\n * STI child partial indexes). Callers pass the already-normalized predicate\n * so both the manifest (desired) and introspected (DB) sides compare equal\n * for semantically-identical clauses. An empty string means \"no predicate\"\n * (a non-partial index) and is also used on both sides when predicate\n * introspection is unavailable, preserving the prior behavior.\n *\n * For JSON-path indexes (`@meta({ indexed: true })`) the signature is\n * derived from the JSON path instead of an empty column list, so the\n * differ can distinguish two jsonPath indexes against different paths.\n *\n * @param idxOrColumns - Either an IndexDefinition or a column array (legacy)\n * @param uniqueArg - Unique flag (used when first arg is a column array)\n * @param predicateArg - Normalized partial-index predicate (default '')\n * @returns Signature string\n */\n private getIndexSignature(\n idxOrColumns: IndexDefinition | string[],\n uniqueArg?: boolean,\n predicateArg = '',\n ): string {\n if (Array.isArray(idxOrColumns)) {\n return `${idxOrColumns.join(',')}:${Boolean(uniqueArg)}:${predicateArg}`;\n }\n const idx = idxOrColumns;\n if (isJsonPathIndex(idx) && idx.jsonPath) {\n return `json:${idx.jsonPath.column}.${idx.jsonPath.path}:${Boolean(idx.unique)}:${predicateArg}`;\n }\n return `${(idx.columns ?? []).join(',')}:${Boolean(idx.unique)}:${predicateArg}`;\n }\n\n /**\n * Whether the active engine supports partial indexes (`CREATE INDEX … WHERE`).\n *\n * SQLite and PostgreSQL do. DuckDB rejects them outright, and the JSON\n * adapter is DuckDB-backed, so on those engines a \"partial\" index can only\n * ever exist as a full index. Treating the predicate as significant there\n * would (a) flag existing full indexes as false drift and (b) emit\n * `CREATE INDEX … WHERE` DDL the engine rejects, breaking the migration.\n * Gating on this keeps both the comparison and the generated DDL aligned\n * with what the engine actually accepts (a partial index degrades to a\n * full index), matching the pre-#1692 behavior on those engines.\n */\n private supportsPartialIndexes(): boolean {\n return this.engine === 'sqlite' || this.engine === 'postgres';\n }\n\n /**\n * Introspect partial-index predicates for a table, keyed by index name.\n *\n * `getTableSchema()` (the @happyvertical/sql introspection) returns only\n * name/columns/unique, so the `WHERE` predicate is read directly here:\n *\n * - PostgreSQL: `pg_indexes.indexdef` carries the full CREATE INDEX text.\n * - SQLite: the `sqlite_master.sql` column carries the original CREATE INDEX\n * text.\n *\n * Engines that don't support partial indexes (DuckDB / the DuckDB-backed\n * JSON adapter) short-circuit to `null` so the comparison stays\n * predicate-unaware there — see {@link supportsPartialIndexes}.\n *\n * Non-partial indexes are omitted from the map (callers treat a missing\n * entry as the empty predicate). Returns `null` when the catalog query\n * fails — e.g. an adapter exposing neither catalog — so the index\n * comparison can fall back to predicate-unaware behavior rather than\n * flagging every existing partial index as false drift.\n */\n private async getDbIndexPredicates(\n tableName: string,\n ): Promise<Map<string, string> | null> {\n if (!this.supportsPartialIndexes()) {\n return null;\n }\n const predicates = new Map<string, string>();\n try {\n if (this.engine === 'postgres') {\n const result = await this.db.query(\n `SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename = ${this.quoteLiteral(\n tableName,\n )}`,\n );\n for (const row of result.rows as {\n indexname?: string;\n indexdef?: string;\n }[]) {\n if (!row.indexname || !row.indexdef) continue;\n const predicate = extractIndexPredicate(row.indexdef);\n if (predicate) predicates.set(row.indexname, predicate);\n }\n return predicates;\n }\n\n // SQLite exposes the `sqlite_master` catalog whose `sql` column preserves\n // the original CREATE INDEX text. Implicit indexes carry a NULL `sql` and\n // are skipped. (DuckDB / JSON already short-circuited above.)\n const result = await this.db.query(\n `SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ${this.quoteLiteral(\n tableName,\n )} AND name NOT LIKE 'sqlite_%'`,\n );\n for (const row of result.rows as {\n name?: string;\n sql?: string | null;\n }[]) {\n if (!row.name || !row.sql) continue;\n const predicate = extractIndexPredicate(row.sql);\n if (predicate) predicates.set(row.name, predicate);\n }\n return predicates;\n } catch (err) {\n logger.debug(\n `[SchemaComparer] Partial-index predicate introspection unavailable for ${tableName}; falling back to predicate-unaware index comparison`,\n { error: err instanceof Error ? err.message : String(err) },\n );\n return null;\n }\n }\n\n /**\n * Get list of existing tables from database\n */\n private async getExistingTables(): Promise<Set<string>> {\n let query: string;\n if (this.engine === 'postgres') {\n query = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`;\n } else {\n // SQLite and DuckDB both expose the SQLite-compatible `sqlite_master`\n // catalog — DuckDB ships it as a built-in compatibility view, so a single\n // introspection query covers both engines. Verified against a live DuckDB\n // v1.4.3 (json mode): `SELECT name FROM sqlite_master WHERE type='table'`\n // returns user tables with the expected `name` column (#1579).\n query = `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;\n }\n\n const result = await this.db.query(query);\n const rows = result.rows as { name?: string; table_name?: string }[];\n return new Set(\n rows.map((r) => r.name || r.table_name || '').filter(Boolean),\n );\n }\n\n /**\n * Normalize SQL types for comparison\n */\n private normalizeType(type: string): string {\n const upper = type\n .toUpperCase()\n .trim()\n .replace(/\\(\\s*\\d+\\s*\\)/g, '');\n\n // Integer types\n if (/^(INTEGER|INT|BIGINT|SMALLINT|TINYINT)$/i.test(upper)) {\n return 'INTEGER';\n }\n\n // Text types\n if (/^(TEXT|CLOB|STRING|VARCHAR|CHAR)/i.test(upper)) {\n return 'TEXT';\n }\n\n // UUID normalizes to its own bucket — deliberately NOT folded into TEXT.\n // The text<->uuid drift tolerance (R11) is applied at the equality gate\n // in `compare()` only, so that `isCompatibleTypeUpgrade` (which also\n // calls normalizeType) still treats `uuid` as distinct from text and\n // won't mis-classify e.g. `uuid`->`timestamp` as a compatible\n // \"TEXT->TIMESTAMP\" upgrade.\n if (/^UUID$/i.test(upper)) {\n return 'UUID';\n }\n\n // Decimal types\n if (/^(REAL|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER)/i.test(upper)) {\n return 'REAL';\n }\n\n // Boolean types\n if (/^(BOOLEAN|BOOL)/i.test(upper)) {\n return 'BOOLEAN';\n }\n\n // PostgreSQL distinguishes instants from timezone-naive wall times. Keep\n // those in separate comparison buckets so legacy TIMESTAMP columns are\n // migrated to the TIMESTAMPTZ representation used for JavaScript Date.\n if (/^(TIMESTAMPTZ|TIMESTAMP\\s+WITH\\s+TIME\\s+ZONE)$/i.test(upper)) {\n return 'TIMESTAMPTZ';\n }\n\n // Date/time types without timezone semantics\n if (\n /^(DATETIME|TIMESTAMP(?:\\s+WITHOUT\\s+TIME\\s+ZONE)?|DATE|TIME)$/i.test(\n upper,\n )\n ) {\n return 'TIMESTAMP';\n }\n\n // Blob types\n if (/^(BLOB|BINARY|BYTEA)/i.test(upper)) {\n return 'BLOB';\n }\n\n // JSON types\n if (/^(JSON|JSONB)/i.test(upper)) {\n return 'JSON';\n }\n\n return upper;\n }\n\n /**\n * Check if a type change is a safe upgrade that can be auto-migrated.\n *\n * SMRT controls the data lifecycle for these columns, so we know:\n * - TEXT→JSON: The column stores JSON data serialized as text (arrays, objects)\n * - TEXT/JSON→TIMESTAMP: Legacy system columns stored timestamp strings\n * before newer manifests normalized the column type.\n * - PostgreSQL TIMESTAMP→TIMESTAMPTZ: Legacy SMRT Date values were written\n * as UTC ISO wall times, so interpreting the naive value as UTC preserves\n * the originally supplied instant.\n * - INTEGER→REAL: Safe widening of integer to floating point\n * - TEXT/REAL→INTEGER on PostgreSQL: explicit data-checked repairs for\n * legacy integer columns that were previously stored as text/real.\n *\n * @param manifestType - The abstract type from the manifest (e.g., 'JSON')\n * @param dbType - The actual type in the database (e.g., 'TEXT')\n * @returns true if the change from dbType to manifestType is a safe upgrade\n */\n private isCompatibleTypeUpgrade(\n manifestType: string,\n dbType: string,\n ): boolean {\n const manifest = this.normalizeType(manifestType);\n const db = this.normalizeType(dbType);\n\n if (\n this.engine === 'postgres' &&\n manifest === 'TIMESTAMP' &&\n ['TIMESTAMP', 'TEXT', 'JSON'].includes(db) &&\n this.ddlStrategy.mapType('TIMESTAMP') === 'TIMESTAMPTZ'\n ) {\n return this.options.postgresTimestampMigration?.legacyTimezone === 'UTC';\n }\n\n // TEXT → JSON is safe: SMRT serializes arrays/objects as JSON text\n // When the manifest says JSON, the data is already valid JSON in TEXT column\n if (manifest === 'JSON' && db === 'TEXT') {\n return true;\n }\n\n // JSON → TEXT is also safe (downgrade, but data is preserved as-is)\n if (manifest === 'TEXT' && db === 'JSON') {\n return true;\n }\n\n // UUID → TEXT is safe for plain text/provenance fields that were\n // mistakenly materialized as native uuid: the stored UUID value can be\n // rendered losslessly as text.\n if (manifest === 'TEXT' && db === 'UUID') {\n return true;\n }\n\n // INTEGER → REAL is safe (widening)\n if (manifest === 'REAL' && db === 'INTEGER') {\n return true;\n }\n\n // PostgreSQL can safely repair legacy integer columns when the generated\n // migration validates that every existing value is already an integer.\n if (\n this.engine === 'postgres' &&\n manifest === 'INTEGER' &&\n (db === 'TEXT' || db === 'REAL')\n ) {\n return true;\n }\n\n // TEXT/JSON → TIMESTAMP is a legacy-drift repair. Invalid values fail\n // explicitly during migration rather than being silently coerced.\n if (\n manifest === 'TIMESTAMP' &&\n (db === 'TEXT' || db === 'JSON') &&\n this.engine !== 'postgres'\n ) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Generate SQL for a type upgrade migration.\n *\n * Engine-specific SQL:\n * - SQLite: TEXT and JSON are equivalent, no-op or comment\n * - DuckDB: ALTER COLUMN TYPE (native type conversion)\n * - PostgreSQL: ALTER COLUMN TYPE with USING clause\n */\n private generateTypeUpgradeSQL(\n tableName: string,\n colName: string,\n colDef: ColumnDefinition,\n dbType: string,\n ): GeneratedTypeUpgradeSQL {\n const quotedTable = this.quoteIdentifier(tableName);\n const quotedCol = this.quoteIdentifier(colName);\n const manifestType = colDef.type;\n\n // Validate manifestType before mapping\n const validatedType: SQLDataType = isValidSQLDataType(manifestType)\n ? manifestType\n : 'TEXT';\n const targetType = this.ddlStrategy.mapType(validatedType);\n\n switch (this.engine) {\n case 'sqlite':\n // SQLite has dynamic typing - TEXT and JSON are functionally equivalent\n // For SQLite, we just return a comment since no actual change is needed\n if (\n this.normalizeType(manifestType) === 'JSON' &&\n this.normalizeType(dbType) === 'TEXT'\n ) {\n return {\n sql: `-- SQLite: ${quotedCol} already stores JSON as TEXT (no change needed)`,\n };\n }\n // For other type upgrades, SQLite requires recreating the table\n return {\n sql: `-- SQLite: Type upgrade for ${quotedCol} requires table recreation`,\n };\n\n case 'postgres': {\n // PostgreSQL defaults must be dropped/reset around some type changes\n // so drift repairs can succeed even when an existing default cannot\n // be cast automatically to the target type.\n const manifestNormalized = this.normalizeType(manifestType);\n const dbNormalized = this.normalizeType(dbType);\n const preflightSQL =\n manifestNormalized === 'INTEGER' &&\n (dbNormalized === 'TEXT' || dbNormalized === 'REAL')\n ? this.generatePostgresIntegerPreflightSQL(\n quotedTable,\n quotedCol,\n tableName,\n colName,\n dbNormalized,\n )\n : manifestNormalized === 'TIMESTAMP' &&\n ['TIMESTAMP', 'TEXT', 'JSON'].includes(dbNormalized) &&\n this.normalizeType(targetType) === 'TIMESTAMPTZ'\n ? this.generatePostgresTimestampPreflightSQL(tableName, colName)\n : null;\n const clauses: string[] = [];\n\n if (colDef.defaultValue !== undefined) {\n clauses.push(`ALTER COLUMN ${quotedCol} DROP DEFAULT`);\n }\n\n let typeClause = `ALTER COLUMN ${quotedCol} TYPE ${targetType}`;\n if (manifestNormalized === 'JSON' && dbNormalized === 'TEXT') {\n // #1335: a bare `${col}::jsonb` cast raises \"invalid input syntax for\n // type json\" on any row whose text is not already valid JSON (e.g. a\n // legacy enum column holding 'active'). `to_jsonb(col)` instead wraps\n // ANY text value as a JSON string and never errors, so a genuine\n // TEXT->JSON widening survives non-JSON legacy data. (Note: with the\n // json<->text equality tolerance added in this fix, the normal\n // compare path no longer reaches here; this keeps the SQL safe for\n // callers that construct a type_upgrade directly.)\n typeClause += ` USING to_jsonb(${quotedCol})`;\n } else if (manifestNormalized === 'TEXT' && dbNormalized === 'JSON') {\n // #1335: a native-json column cast back to text is value-preserving\n // (`::text` renders the stored JSON as its text form), so this arm is\n // safe. (Note: like the TEXT->JSON arm above, the json<->text equality\n // tolerance means the normal compare path no longer reaches here; this\n // keeps the SQL safe for callers that construct a type_upgrade\n // directly.)\n typeClause += ` USING ${quotedCol}::text`;\n } else if (manifestNormalized === 'TEXT' && dbNormalized === 'UUID') {\n typeClause += ` USING ${quotedCol}::text`;\n } else if (\n manifestNormalized === 'INTEGER' &&\n dbNormalized === 'TEXT'\n ) {\n typeClause += ` USING trim(${quotedCol}::text)::integer`;\n } else if (\n manifestNormalized === 'INTEGER' &&\n dbNormalized === 'REAL'\n ) {\n typeClause += ` USING ${quotedCol}::integer`;\n } else if (\n manifestNormalized === 'TIMESTAMP' &&\n (dbNormalized === 'TEXT' || dbNormalized === 'JSON')\n ) {\n typeClause += ` USING NULLIF(NULLIF(trim(both '\"' from ${quotedCol}::text), ''), 'null')::timestamptz`;\n } else if (\n manifestNormalized === 'TIMESTAMP' &&\n dbNormalized === 'TIMESTAMP' &&\n this.normalizeType(targetType) === 'TIMESTAMPTZ'\n ) {\n // SMRT's PostgreSQL adapter serialized Date values to UTC ISO strings.\n // A legacy TIMESTAMP column discarded the trailing offset but kept\n // that UTC wall time. Reattach UTC explicitly so migration is\n // deterministic regardless of the database/session timezone.\n typeClause += ` USING ${quotedCol} AT TIME ZONE 'UTC'`;\n }\n\n clauses.push(typeClause);\n\n if (colDef.defaultValue !== undefined) {\n const formattedDefault = this.ddlStrategy.formatDefaultValue(\n colDef.defaultValue,\n validatedType,\n );\n const defaultSql =\n manifestNormalized === 'JSON'\n ? `${formattedDefault}::${targetType.toLowerCase()}`\n : formattedDefault;\n clauses.push(`ALTER COLUMN ${quotedCol} SET DEFAULT ${defaultSql}`);\n }\n\n const alterSql = `ALTER TABLE ${quotedTable} ${clauses.join(', ')}`;\n\n return preflightSQL\n ? { sql: alterSql, statements: [preflightSQL, alterSql] }\n : { sql: alterSql };\n }\n\n case 'duckdb':\n // DuckDB supports ALTER COLUMN TYPE for type conversions\n return {\n sql: `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} TYPE ${targetType}`,\n };\n\n default: {\n // Escape special characters in type names for safe comment generation\n const safeDbType = dbType.replace(/[^\\w]/g, '_');\n const safeManifestType = manifestType.replace(/[^\\w]/g, '_');\n return {\n sql: `-- Type upgrade for ${quotedCol}: ${safeDbType} → ${safeManifestType}`,\n };\n }\n }\n }\n\n /**\n * Quote a SQL identifier\n */\n private quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n }\n\n /**\n * Generate a PostgreSQL preflight guard for narrowing legacy columns to\n * INTEGER. PostgreSQL's REAL→INTEGER cast rounds fractional values, so the\n * generated migration must fail before the ALTER can silently change data.\n */\n private generatePostgresIntegerPreflightSQL(\n quotedTable: string,\n quotedCol: string,\n tableName: string,\n colName: string,\n dbNormalized: string,\n ): string {\n const invalidCondition =\n dbNormalized === 'REAL'\n ? `${quotedCol} IS NOT NULL AND ${quotedCol} <> trunc(${quotedCol})`\n : `${quotedCol} IS NOT NULL AND trim(${quotedCol}::text) !~ '^[+-]?[0-9]+$'`;\n const message = `Cannot convert ${tableName}.${colName} to INTEGER: found non-integer values`;\n\n return `DO $$ BEGIN IF EXISTS (SELECT 1 FROM ${quotedTable} WHERE ${invalidCondition}) THEN RAISE EXCEPTION ${this.quoteLiteral(message)}; END IF; END $$`;\n }\n\n /**\n * Require a UTC migration session before reattaching the offset discarded by\n * legacy PostgreSQL TIMESTAMP columns. This is a guardrail, not provenance:\n * operators must also confirm historical default/raw writers used UTC.\n */\n private generatePostgresTimestampPreflightSQL(\n tableName: string,\n colName: string,\n ): string {\n const message =\n `Cannot convert ${tableName}.${colName} to TIMESTAMPTZ outside a UTC ` +\n 'PostgreSQL session; confirm historical writers used UTC and SET TIME ZONE UTC';\n return `DO $$ BEGIN IF upper(current_setting('TimeZone')) NOT IN ('UTC', 'ETC/UTC', 'GMT') THEN RAISE EXCEPTION ${this.quoteLiteral(message)}; END IF; END $$`;\n }\n\n private quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n }\n\n /**\n * Generate SQL for adding a column\n */\n private generateAddColumnSQL(\n tableName: string,\n colName: string,\n colDef: ColumnDefinition,\n ): string {\n // Build the ADD COLUMN definition inline (main) rather than delegating to\n // the DDL strategy's generateColumnDefinition: that builder is for CREATE\n // TABLE and would emit `PRIMARY KEY` (invalid in ALTER ... ADD COLUMN) and\n // suppress single-column UNIQUE on engines that require inline unique at\n // table-create time (DuckDB) — but an ADD COLUMN has no inline-constraint\n // pass, so the UNIQUE must be emitted here. mapType still maps abstract\n // types per dialect (UUID→native uuid / TEXT — R11); invalid types fall\n // back to TEXT, matching the compareColumns guard.\n const validatedType: SQLDataType = isValidSQLDataType(colDef.type)\n ? colDef.type\n : 'TEXT';\n if (!isValidSQLDataType(colDef.type)) {\n logger.warn(\n `[SchemaComparer] Invalid manifest type \"${colDef.type}\" for ${tableName}.${colName}, treating as TEXT`,\n );\n }\n\n const parts: string[] = [\n this.quoteIdentifier(colName),\n this.ddlStrategy.mapType(validatedType),\n ];\n\n if (colDef.notNull) {\n parts.push('NOT NULL');\n }\n if (colDef.unique) {\n parts.push('UNIQUE');\n }\n if (colDef.defaultValue !== undefined) {\n const defaultVal = this.ddlStrategy.formatDefaultValue(\n colDef.defaultValue,\n validatedType,\n );\n parts.push(`DEFAULT ${defaultVal}`);\n }\n if (colDef.check) {\n parts.push(`CHECK (${colDef.check})`);\n }\n\n const columnDefinition = parts.join(' ');\n\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${columnDefinition}`;\n }\n\n /**\n * Generate SQL for dropping a column\n */\n private generateDropColumnSQL(tableName: string, colName: string): string {\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(colName)}`;\n }\n\n /**\n * Generate SQL for adding an index.\n *\n * On engines that support partial indexes (SQLite/PostgreSQL) this mirrors\n * the canonical CREATE INDEX path in the DDL strategies: a partial index\n * appends its `WHERE` predicate so a detected predicate add/alter (issue\n * #1692) recreates the index with the correct partial condition rather than\n * silently widening it to a full index. On DuckDB / the JSON adapter — which\n * reject partial indexes — the predicate is dropped so the emitted DDL stays\n * executable (a partial index degrades to a full index there). The predicate\n * is trimmed and a redundant leading `WHERE` stripped for robustness.\n */\n private generateAddIndexSQL(tableName: string, idx: IndexDefinition): string {\n const uniqueStr = idx.unique ? 'UNIQUE ' : '';\n const target = renderIndexTarget(idx, this.engine);\n let sql = `CREATE ${uniqueStr}INDEX ${this.quoteIdentifier(idx.name)} ON ${this.quoteIdentifier(tableName)} (${target})`;\n const where = idx.where?.trim().replace(/^WHERE\\s+/i, '');\n if (this.supportsPartialIndexes() && where) {\n sql += ` WHERE ${where}`;\n }\n return sql;\n }\n\n /**\n * Generate SQL for dropping an index.\n *\n * This SQL is consumed by the manifest-driven execution path:\n *\n * - `db:migrate` runs the SQL we put in `change.sql` directly, with the\n * tracker's `executePostgresStatements` adding CONCURRENTLY when\n * `--postgres-safe` is on.\n *\n * PostgreSQL ends up with `CONCURRENTLY` when it should.\n * Keeping this method engine-agnostic also means the diff preview text\n * stays readable (no engine-specific noise) for engines like SQLite\n * where CONCURRENTLY isn't a thing.\n */\n private generateDropIndexSQL(indexName: string): string {\n return `DROP INDEX IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n}\n\n/**\n * Generate a SchemaDiff from manifest and database\n */\nexport async function generateSchemaDiff(\n db: DatabaseInterface,\n manifestSchemas: Record<string, SchemaDefinition>,\n options: DiffOptions = {},\n): Promise<SchemaDiff> {\n const comparer = new SchemaComparer(db, options);\n return comparer.compare(manifestSchemas);\n}\n\n/**\n * Check if a diff has any actionable changes (excluding type mismatches)\n */\nexport function hasActionableChanges(diff: SchemaDiff): boolean {\n if (diff.added_tables.length > 0) return true;\n if (diff.dropped_tables.length > 0) return true;\n return diff.changes.some((c) => c.type !== 'type_mismatch');\n}\n\n/**\n * Get SQL statements from a diff for execution\n */\nexport function getSQLFromDiff(diff: SchemaDiff): string[] {\n const statements: string[] = [];\n\n // Add table creation (requires full DDL generation - not included here)\n // This is typically handled by ensureSchema()\n\n // Add column and index changes\n for (const change of diff.changes) {\n if (change.type !== 'type_mismatch') {\n statements.push(\n ...(change.sqlStatements ?? (change.sql ? [change.sql] : [])),\n );\n }\n }\n\n return statements;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"differ.js","names":[],"sources":["../../src/migrations/differ.ts"],"sourcesContent":["/**\n * Schema Differ\n *\n * Compares manifest schemas to database schemas and generates\n * a SchemaDiff with the differences.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { detectEngine, getDDLStrategy } from '../schema/ddl/index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport type {\n ColumnAlteration,\n ColumnDefinition,\n IndexDefinition,\n SchemaChange,\n SchemaDefinition,\n SchemaDiff,\n SQLDataType,\n} from '../schema/types.js';\nimport {\n isJsonPathIndex,\n isStiSubtypeUniqueIndex,\n renderIndexTarget,\n} from '../schema/utils.js';\nimport {\n planSqliteTableRebuilds,\n sqliteRebuildPlaceholderSql,\n} from './sqlite-rebuild.js';\nimport type { DatabaseInterface, SqlTableSchemaInfo } from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Valid SQLDataType values for validation\n */\nconst VALID_SQL_DATA_TYPES: Set<SQLDataType> = new Set([\n 'TEXT',\n 'INTEGER',\n 'REAL',\n 'BLOB',\n 'BOOLEAN',\n 'JSON',\n 'TIMESTAMP',\n 'UUID',\n]);\n\ninterface GeneratedTypeUpgradeSQL {\n sql: string;\n statements?: string[];\n}\n\n/**\n * Check if a string is a valid SQLDataType\n */\nfunction isValidSQLDataType(type: string): type is SQLDataType {\n return VALID_SQL_DATA_TYPES.has(type as SQLDataType);\n}\n\n/**\n * Options for schema comparison\n */\nexport interface DiffOptions {\n /**\n * Emit executable `DROP TABLE` entries (`dropped_tables`) for tables that\n * exist in the database but not in the manifest. Default: false. Orphan\n * tables are always *reported* via `SchemaDiff.orphan_tables` regardless.\n */\n includeDroppedTables?: boolean;\n /**\n * Emit executable `drop_column` changes for columns that exist in the\n * database but not in the manifest. Default: false. Orphan columns are\n * always *reported* as `orphan_column` advisories regardless — a NOT NULL\n * orphan without a default is flagged as a warning because every ORM\n * insert (which never supplies the column) will fail on it (#2369).\n */\n includeDroppedColumns?: boolean;\n /**\n * Execute constraint relaxations instead of only reporting them:\n * `DROP NOT NULL` / `DROP DEFAULT` on live columns that are stricter than\n * the manifest, and `DROP NOT NULL` on orphan NOT NULL columns. Default:\n * false — relaxations are reported as advisories with the suggested SQL.\n * Strengthening repairs (`SET NOT NULL`, `SET DEFAULT`) are always emitted\n * where the engine supports `ALTER COLUMN` (#2369).\n */\n relaxColumns?: boolean;\n /**\n * Drop indexes that exist in the database but are absent from the manifest\n * AND are not functionally equivalent to anything in the manifest. Default:\n * false. The differ always skips primary-key indexes (`*_pkey`) and the\n * implicit indexes that PostgreSQL creates from inline UNIQUE constraints\n * (`*_key`) — those are owned by table-level constraints, not by the\n * index list, and dropping them here would break the constraint.\n *\n * Independent of this flag, a redundant single-column index over the\n * table's primary key that the manifest no longer declares (the legacy\n * `<table>_id_idx`, #2359) is always dropped: the primary-key constraint\n * keeps serving every lookup, so the drop can only save write cost.\n */\n includeDroppedIndexes?: boolean;\n /** Ignore type mismatches (just log warnings) */\n ignoreTypeMismatches?: boolean;\n /**\n * Explicit engine hint forwarded to `detectEngine` when picking the DDL\n * strategy used for *existing-table* SQL (ALTER/CREATE INDEX/etc.). Use\n * this when `db.url` is empty or ambiguous (e.g. JSON adapter, in-memory\n * wrappers where the URL lives on `db.config?.url`). Without it the\n * comparer falls back to URL-only detection, which can produce SQLite-\n * flavored SQL on a connection whose caller meant Postgres or DuckDB.\n */\n engineHint?: string;\n /**\n * Explicit confirmation that every historical writer of legacy PostgreSQL\n * Date columns used UTC wall times. Without this acknowledgement,\n * TIMESTAMP/TEXT/JSON to TIMESTAMPTZ drift is reported as manual rather\n * than reinterpreted automatically.\n */\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n}\n\n/**\n * Suffix patterns for indexes that the differ refuses to drop.\n * - `_pkey`: PostgreSQL primary key implicit index.\n * - `_key`: PostgreSQL implicit index for inline `UNIQUE (...)` table\n * constraints, and the unique index the differ itself creates for a\n * `unique: true` column added via ADD COLUMN on engines that reject an\n * inline UNIQUE there (SQLite/DuckDB — see {@link uniqueColumnIndexName}).\n * Dropping these by name does not drop the underlying constraint on\n * PostgreSQL, and dropping the constraint requires a separate DDL path\n * (`ALTER TABLE ... DROP CONSTRAINT`) the differ does not emit today.\n * Unclaimed `_key` unique indexes are instead *reported* as\n * `orphan_index` advisories (#2369).\n */\nconst PROTECTED_INDEX_SUFFIXES = ['_pkey', '_key'];\n\nfunction isProtectedDbIndexName(name: string): boolean {\n return PROTECTED_INDEX_SUFFIXES.some((suffix) => name.endsWith(suffix));\n}\n\n/**\n * Name of the unique index that backs a `unique: true` column. Mirrors the\n * `<table>_<column>_key` name PostgreSQL gives the implicit index of an\n * inline column UNIQUE constraint, so the same column ends up with the same\n * index name on every engine and the orphan-index sweep treats it as\n * constraint-owned.\n */\nexport function uniqueColumnIndexName(\n tableName: string,\n columnName: string,\n): string {\n return `${tableName}_${columnName}_key`;\n}\n\n/**\n * True when a change is report-only: it carries an advisory and no\n * executable statement. Such changes never reach the migration stream.\n */\nexport function isAdvisoryOnlyChange(change: SchemaChange): boolean {\n if (!change.advisory) return false;\n const statements = change.sqlStatements ?? (change.sql ? [change.sql] : []);\n return statements.length === 0;\n}\n\n/**\n * True for an info-level report-only change: listed in `SchemaDiff.changes`\n * for visibility but not counted by `has_changes` (a harmless orphan column,\n * a live default the manifest no longer declares).\n */\nexport function isInfoOnlyChange(change: SchemaChange): boolean {\n return isAdvisoryOnlyChange(change) && change.advisory?.severity === 'info';\n}\n\n/**\n * True when a change has no executable DDL: `type_mismatch`, an advisory-only\n * change, or a change whose only SQL is an advisory comment (e.g. a SQLite\n * `ALTER COLUMN` the engine cannot perform in place).\n */\nexport function isManualOrAdvisoryChange(change: SchemaChange): boolean {\n if (change.type === 'type_mismatch') return true;\n if (isAdvisoryOnlyChange(change)) return true;\n const statements = change.sqlStatements ?? (change.sql ? [change.sql] : []);\n return (\n statements.length > 0 &&\n statements.every((statement) => statement.trim().startsWith('--'))\n );\n}\n\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n return db.url || dbWithConfig.config?.url || '';\n}\n\n/**\n * Normalize a partial-index `WHERE` predicate so semantically-identical\n * clauses from different sources compare equal (issue #1692).\n *\n * The manifest stores predicates roughly as written (`_meta_type = 'Article'`),\n * SQLite/DuckDB echo the original CREATE INDEX text verbatim, and PostgreSQL\n * re-renders them with type casts and extra parentheses\n * (`((_meta_type)::text = 'Article'::text)`). Normalization:\n *\n * - strips a leading `WHERE` keyword,\n * - removes PostgreSQL `::type` casts (single-word type names — the only kind\n * SMRT-generated partial predicates produce, e.g. `_meta_type::text`),\n * - removes parentheses (SMRT only emits simple `col = 'literal'` predicates,\n * so grouping carries no meaning here),\n * - lowercases everything OUTSIDE single-quoted string literals (SQL keywords\n * and identifiers are case-insensitive; literals such as STI discriminator\n * class names are case-sensitive, so they are preserved verbatim),\n * - collapses whitespace and tightens spacing around comparison operators.\n *\n * Returns '' for an absent/empty predicate (i.e. a non-partial index).\n */\nexport function normalizeIndexPredicate(where?: string | null): string {\n if (!where) return '';\n const stripped = where.trim().replace(/^WHERE\\s+/i, '');\n if (!stripped) return '';\n\n let out = '';\n let i = 0;\n while (i < stripped.length) {\n if (stripped[i] === \"'\") {\n // Consume a single-quoted string literal verbatim, honoring the SQL\n // `''` escape for an embedded quote.\n let literal = \"'\";\n i++;\n while (i < stripped.length) {\n if (stripped[i] === \"'\") {\n if (stripped[i + 1] === \"'\") {\n literal += \"''\";\n i += 2;\n continue;\n }\n literal += \"'\";\n i++;\n break;\n }\n literal += stripped[i];\n i++;\n }\n out += literal;\n } else {\n let run = '';\n while (i < stripped.length && stripped[i] !== \"'\") {\n run += stripped[i];\n i++;\n }\n out += normalizeNonLiteralPredicateRun(run);\n }\n }\n return out;\n}\n\n/**\n * Normalize the non-literal portion of a predicate (everything outside a\n * single-quoted string literal). Type casts and parentheses are dropped,\n * keywords/identifiers are lowercased, and whitespace is canonicalized.\n */\nfunction normalizeNonLiteralPredicateRun(run: string): string {\n return run\n .replace(/::[A-Za-z_]\\w*/g, '') // drop single-word PostgreSQL type casts\n .replace(/[()]/g, '') // drop parentheses\n .toLowerCase()\n .replace(/\\s+/g, ' ')\n .replace(/\\s*([=<>!]+)\\s*/g, '$1') // tighten comparison operators\n .trim();\n}\n\n/**\n * Extract the normalized partial-index predicate from a `CREATE INDEX`\n * statement — the `WHERE` tail that follows the column-list close paren.\n * Works for both SQLite/DuckDB `sqlite_master.sql` text and PostgreSQL\n * `pg_indexes.indexdef`. Returns '' for a non-partial index.\n */\nexport function extractIndexPredicate(createIndexSql: string): string {\n // The predicate is the tail after the column-list ')': `... (cols) WHERE x`.\n // Anchoring on `) WHERE` (rather than a bare `WHERE`) avoids matching a\n // column literally named \"where\" inside the indexed column list.\n const match = createIndexSql.match(/\\)\\s*WHERE\\s+([\\s\\S]+?)\\s*;?\\s*$/i);\n if (!match) return '';\n return normalizeIndexPredicate(match[1]);\n}\n\n/**\n * Canonical form of a column default for drift comparison (#2369).\n *\n * The manifest side is the DDL fragment `formatDefaultValue()` would emit\n * (`'anon'`, `0`, `TRUE`, `'{}'`, `current_timestamp`); the database side is\n * whatever the catalog reports, which each engine renders differently:\n * PostgreSQL appends casts (`'anon'::text`, `'{}'::jsonb`, `'-1'::integer`)\n * and upper-cases `CURRENT_TIMESTAMP`, DuckDB wraps booleans\n * (`CAST('t' AS BOOLEAN)`), SQLite echoes the literal verbatim. Both sides\n * are reduced by the *manifest column type* to a `{kind, key}` pair:\n *\n * - `none` — no default (`undefined`, empty, or the `NULL` keyword)\n * - `now` — the current-timestamp family (`current_timestamp`, `now()`,\n * `transaction_timestamp()`, `datetime('now')`)\n * - `number` — numeric columns, compared by numeric value\n * - `bool` — boolean columns (`true`/`false`/`t`/`f`/`1`/`0`)\n * - `text` — string literal contents (JSON columns compare the parsed\n * value when both sides parse)\n * - `func` — other function calls / keywords, compared lowercase and\n * whitespace-collapsed\n * - `raw` — could not be classified; callers must skip the comparison\n * rather than risk a false positive\n */\nexport function canonicalizeDefault(\n fragment: string | undefined,\n columnType: SQLDataType,\n): {\n kind: 'none' | 'now' | 'number' | 'bool' | 'text' | 'func' | 'raw';\n key: string;\n} {\n if (fragment === undefined) return { kind: 'none', key: '' };\n let s = fragment.trim();\n if (s === '') return { kind: 'none', key: '' };\n\n // Unwrap DuckDB `CAST(<inner> AS <type>)` and PostgreSQL trailing casts /\n // outer parentheses. Loop because forms can nest (`('x'::text)::text`).\n for (let guard = 0; guard < 8; guard++) {\n const before = s;\n const castMatch = s.match(/^CAST\\s*\\((.*)\\s+AS\\s+[A-Za-z_][\\w\\s()]*\\)$/is);\n if (castMatch) s = castMatch[1].trim();\n if (s.startsWith('(') && s.endsWith(')')) s = s.slice(1, -1).trim();\n // Trailing PostgreSQL cast: `::type`, `::character varying`, `::type[]`,\n // `::timestamp with time zone`, `::numeric(10,2)`.\n s = s\n .replace(/::[A-Za-z_][\\w ]*(\\(\\s*\\d+(\\s*,\\s*\\d+)?\\s*\\))?(\\[\\])?\\s*$/i, '')\n .trim();\n if (s === before) break;\n }\n\n if (/^null$/i.test(s)) return { kind: 'none', key: '' };\n\n const lower = s.toLowerCase().replace(/\\s+/g, ' ');\n if (\n lower === 'current_timestamp' ||\n lower === 'current_timestamp()' ||\n lower === 'now()' ||\n lower === 'transaction_timestamp()' ||\n lower === \"datetime('now')\" ||\n lower === 'localtimestamp'\n ) {\n return { kind: 'now', key: 'now' };\n }\n\n const upperType = String(columnType).toUpperCase();\n const isNumericType =\n upperType === 'INTEGER' || upperType === 'REAL' || upperType === 'DECIMAL';\n const isBooleanType = upperType === 'BOOLEAN';\n const isJsonType = upperType === 'JSON';\n\n // String literal (single-quoted, '' escape).\n const literalMatch = s.match(/^'((?:[^']|'')*)'$/s);\n const literal =\n literalMatch !== null ? literalMatch[1].replace(/''/g, \"'\") : null;\n\n if (isBooleanType) {\n const token = (literal ?? s).trim().toLowerCase();\n if (['true', 't', '1', 'yes', 'on'].includes(token))\n return { kind: 'bool', key: 'true' };\n if (['false', 'f', '0', 'no', 'off'].includes(token))\n return { kind: 'bool', key: 'false' };\n return { kind: 'raw', key: lower };\n }\n\n if (isNumericType) {\n const token = (literal ?? s).trim();\n if (/^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$/.test(token)) {\n return { kind: 'number', key: String(Number(token)) };\n }\n if (literal !== null) {\n return { kind: 'text', key: literal };\n }\n // Not a numeric literal — fall through to function/keyword detection\n // (e.g. `nextval('seq'::regclass)`).\n }\n\n if (literal !== null) {\n if (isJsonType) {\n try {\n return { kind: 'text', key: JSON.stringify(JSON.parse(literal)) };\n } catch {\n // Not JSON — compare as plain text below.\n }\n }\n return { kind: 'text', key: literal };\n }\n\n // Bare numeric literal on a non-numeric column (e.g. SQLite `DEFAULT 0` on\n // a TEXT column) — compare textually.\n if (/^[+-]?(\\d+\\.?\\d*|\\.\\d+)$/.test(s)) {\n return { kind: 'text', key: String(Number(s)) };\n }\n\n // Function call / keyword.\n if (/^[A-Za-z_][\\w.]*\\s*\\(.*\\)$/s.test(s) || /^[A-Za-z_]\\w*$/.test(s)) {\n return { kind: 'func', key: lower };\n }\n\n return { kind: 'raw', key: lower };\n}\n\n/**\n * SchemaComparer class for comparing manifest schemas to database\n */\nexport class SchemaComparer {\n private db: DatabaseInterface;\n private options: DiffOptions;\n private engine: DatabaseEngine;\n private ddlStrategy: ReturnType<typeof getDDLStrategy>;\n\n constructor(db: DatabaseInterface, options: DiffOptions = {}) {\n this.db = db;\n this.options = {\n includeDroppedTables: false,\n includeDroppedColumns: false,\n includeDroppedIndexes: false,\n ignoreTypeMismatches: false,\n ...options,\n };\n // Use the shared detectEngine utility for consistent detection.\n // Handles :memory:, .json, and other edge cases. The JSON adapter is\n // detected structurally (it exposes `exportTable`) because its url can be\n // empty; otherwise fall back to URL-based detection, where `engineHint`\n // lets callers override when `db.url` is empty or points at an adapter\n // whose engine isn't obvious from the URL alone (some in-memory wrappers).\n this.engine =\n typeof (this.db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : detectEngine(resolveDatabaseUrl(this.db), this.options.engineHint);\n this.ddlStrategy = getDDLStrategy(this.engine);\n }\n\n /**\n * Compare manifest schemas to database and return differences\n */\n async compare(\n manifestSchemas: Record<string, SchemaDefinition>,\n ): Promise<SchemaDiff> {\n const diff: SchemaDiff = {\n added_tables: [],\n dropped_tables: [],\n changes: [],\n has_changes: false,\n };\n\n // Get list of existing tables\n const existingTables = await this.getExistingTables();\n\n // Check each manifest schema against database\n for (const [tableName, schema] of Object.entries(manifestSchemas)) {\n if (!existingTables.has(tableName)) {\n // Table doesn't exist - add to added_tables\n diff.added_tables.push(schema);\n diff.has_changes = true;\n } else {\n // Table exists - compare columns and indexes\n const tableChanges = await this.compareTable(tableName, schema);\n if (tableChanges.length > 0) {\n diff.changes.push(...tableChanges);\n // Info-level report-only notes (a harmless orphan column, a stale\n // default the manifest no longer declares) are listed but do not\n // make the schema \"changed\": executable, manual, and warning-level\n // findings do (#2369).\n if (tableChanges.some((change) => !isInfoOnlyChange(change))) {\n diff.has_changes = true;\n }\n }\n }\n }\n\n // Orphan tables are always reported (never silently retained); the\n // executable DROP TABLE stays opt-in via `includeDroppedTables` (#2369).\n const orphanTables: string[] = [];\n for (const tableName of existingTables) {\n // Skip system tables\n if (tableName.startsWith('_smrt_') || tableName.startsWith('sqlite_')) {\n continue;\n }\n if (!manifestSchemas[tableName]) {\n orphanTables.push(tableName);\n }\n }\n orphanTables.sort();\n diff.orphan_tables = orphanTables;\n if (this.options.includeDroppedTables && orphanTables.length > 0) {\n diff.dropped_tables.push(...orphanTables);\n diff.has_changes = true;\n }\n\n return diff;\n }\n\n /**\n * Compare a single table's schema to manifest\n */\n async compareTable(\n tableName: string,\n manifest: SchemaDefinition,\n ): Promise<SchemaChange[]> {\n const changes: SchemaChange[] = [];\n\n // Get current table schema from database\n const dbSchema = await this.db.getTableSchema?.(tableName);\n if (!dbSchema) {\n // Table doesn't exist - this is an add_table case\n return changes;\n }\n\n // Compare columns\n const columnChanges = await this.compareColumns(\n tableName,\n manifest,\n dbSchema,\n );\n // #2370: SQLite cannot change a column's type in place. Turn the\n // \"requires table recreation\" placeholders those comparisons emit into an\n // executable table rebuild (see `./sqlite-rebuild.ts`); anything the\n // rebuild can't do safely stays manual drift, as before. The planner only\n // consumes `type_upgrade` placeholders — SQLite `alter_column`\n // nullability/default drift (#2369) stays manual until the rebuild also\n // rewrites constraints.\n changes.push(\n ...(this.engine === 'sqlite'\n ? await planSqliteTableRebuilds({\n db: this.db,\n tableName,\n changes: columnChanges,\n mapType: (type) => this.ddlStrategy.mapType(type),\n })\n : columnChanges),\n );\n\n // Partial-index predicates are not surfaced by `getTableSchema()` (the\n // @happyvertical/sql introspection returns only name/columns/unique), so\n // introspect them separately. Without this, two indexes on the same\n // column(s) that differ only by their WHERE clause — e.g. distinct STI\n // child partial indexes — would compare equal and the differ would miss\n // adds/drops/changes of the predicate (issue #1692).\n const dbIndexPredicates = await this.getDbIndexPredicates(tableName);\n\n // Compare indexes\n const indexChanges = this.compareIndexes(\n tableName,\n manifest,\n dbSchema,\n dbIndexPredicates,\n );\n changes.push(...indexChanges);\n\n return changes;\n }\n\n /**\n * Compare columns between manifest and database.\n *\n * Three families of drift (#2369):\n *\n * 1. **Missing column** — emit `add_column` with an executable plan for the\n * engine (see {@link generateAddColumnChanges}).\n * 2. **Existing column** — type drift (as before), then nullability and\n * default drift (see {@link compareColumnConstraints}).\n * 3. **Orphan column** (in DB, not in manifest) — always reported. With\n * `includeDroppedColumns` it becomes an executable `drop_column`; a\n * NOT NULL orphan without a default is a `warning` advisory because ORM\n * inserts never supply it and will fail (`23502`); with `relaxColumns`\n * that orphan gets an executable `DROP NOT NULL` where the engine allows.\n */\n private async compareColumns(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n ): Promise<SchemaChange[]> {\n const changes: SchemaChange[] = [];\n const dbColumnNames = new Set(Object.keys(dbSchema.columns));\n\n // Check for new and modified columns\n for (const [colName, colDef] of Object.entries(manifest.columns)) {\n if (!dbColumnNames.has(colName)) {\n // Column doesn't exist - add it\n changes.push(\n ...(await this.generateAddColumnChanges(tableName, colName, colDef)),\n );\n } else {\n // Column exists - check for type mismatch\n const dbCol = dbSchema.columns[colName];\n let typeDrifted = false;\n\n // Map manifest's abstract type to engine-specific type\n // e.g., JSON → TEXT for SQLite, JSON → JSONB for PostgreSQL\n // Validate the manifest type before mapping\n const manifestType = colDef.type;\n if (!isValidSQLDataType(manifestType)) {\n // Invalid manifest type - treat as TEXT (safest fallback)\n logger.warn(\n `[SchemaComparer] Invalid manifest type \"${manifestType}\" for ${tableName}.${colName}, treating as TEXT`,\n );\n }\n const validatedType: SQLDataType = isValidSQLDataType(manifestType)\n ? manifestType\n : 'TEXT';\n const expectedEngineType = this.ddlStrategy.mapType(validatedType);\n const normalizedExpected = this.normalizeType(expectedEngineType);\n const normalizedActual = this.normalizeType(dbCol.type);\n\n // R11: native `uuid` and `text` are interchangeable for SMRT-owned\n // identifiers/references, but not for arbitrary provenance text. Keep\n // the tolerance directional:\n // - manifest UUID + DB text is tolerated for structural ID/ref\n // columns so old deployments are not forced into native UUID.\n // - manifest TEXT + DB uuid is tolerated only for structural ID/ref\n // columns that are intentionally UUID-compatible.\n // Plain TEXT columns with DB uuid now surface as repairable drift.\n const isUuidTextEquivalent = this.isUuidTextEquivalentColumn(\n colName,\n colDef,\n normalizedExpected,\n normalizedActual,\n );\n\n // #1335: native `json`/`jsonb` (DB) and `text` (manifest) are\n // interchangeable for SMRT — the convention is to serialize JSON values\n // into TEXT columns, and a native-json column already holds exactly that\n // data. So the differ must NOT flag a json<->text difference in EITHER\n // direction:\n // - manifest TEXT vs DB json (native-json column, text-convention manifest)\n // - manifest JSON vs DB text (the canary case: an enum/plain field\n // mis-inferred as JSON by a downstream scanner, sitting on a real\n // `text` column holding bare values like 'active')\n // Generating an ALTER here is pure churn at best and data-destroying at\n // worst: `status::jsonb` on a column holding 'active' raises\n // \"invalid input syntax for type json\" and aborts the whole atomic\n // migration. Like the uuid/text tolerance, this lives at the equality\n // gate only (not in `normalizeType`) so `isCompatibleTypeUpgrade` still\n // treats JSON and TEXT as distinct buckets for OTHER upgrade paths.\n const isJsonTextEquivalent =\n (normalizedExpected === 'JSON' && normalizedActual === 'TEXT') ||\n (normalizedExpected === 'TEXT' && normalizedActual === 'JSON');\n\n if (\n normalizedExpected !== normalizedActual &&\n !isUuidTextEquivalent &&\n !isJsonTextEquivalent\n ) {\n typeDrifted = true;\n // Check if this is a safe type upgrade that SMRT can handle\n // Since SMRT owns the data lifecycle, we know the intent from the manifest\n if (this.isCompatibleTypeUpgrade(colDef.type, dbCol.type)) {\n // Generate type upgrade SQL\n const generatedSQL = this.generateTypeUpgradeSQL(\n tableName,\n colName,\n colDef,\n dbCol.type,\n );\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: {\n expected: colDef.type,\n actual: dbCol.type,\n },\n sql: generatedSQL.sql,\n ...(generatedSQL.statements\n ? { sqlStatements: generatedSQL.statements }\n : {}),\n });\n } else if (!this.options.ignoreTypeMismatches) {\n changes.push({\n type: 'type_mismatch',\n table: tableName,\n name: colName,\n mismatch: {\n expected: colDef.type,\n actual: dbCol.type,\n },\n });\n }\n }\n\n // Nullability / default drift. Skipped while the type itself is\n // drifting: a type repair rewrites the default (PostgreSQL DROP/SET\n // DEFAULT around ALTER TYPE) and an incompatible mismatch needs a\n // manual migration anyway, so constraint drift on top would only be\n // noise until the type is settled.\n if (!typeDrifted) {\n changes.push(\n ...(await this.compareColumnConstraints(\n tableName,\n colName,\n colDef,\n validatedType,\n dbCol,\n )),\n );\n }\n }\n }\n\n // Orphan columns: always reported, dropped only on request (#2369).\n const manifestColumnNames = new Set(Object.keys(manifest.columns));\n for (const colName of dbColumnNames) {\n if (manifestColumnNames.has(colName)) continue;\n const dbCol = dbSchema.columns[colName];\n if (this.options.includeDroppedColumns) {\n changes.push({\n type: 'drop_column',\n table: tableName,\n name: colName,\n mismatch: {\n expected: '(not in manifest)',\n actual: this.describeDbColumn(dbCol),\n },\n sql: this.generateDropColumnSQL(tableName, colName),\n });\n continue;\n }\n changes.push(this.describeOrphanColumn(tableName, colName, dbCol));\n }\n\n return changes;\n }\n\n /**\n * Compare nullability and default between a manifest column and the live\n * column, emitting `alter_column` changes (#2369).\n *\n * Rules:\n * - Primary-key columns are owned by the PK constraint (SQLite reports a\n * `TEXT PRIMARY KEY` as nullable while PostgreSQL/DuckDB imply NOT NULL),\n * so they are skipped entirely.\n * - Strengthening (`SET NOT NULL`, `SET DEFAULT`) is emitted as executable\n * SQL on engines with `ALTER COLUMN` (PostgreSQL/DuckDB). A `SET NOT NULL`\n * is preceded by a backfill of the manifest default when there is one;\n * without a default the live data is probed and, if NULLs exist, the\n * change is reported as manual with the suggested backfill instead of\n * emitting an ALTER the engine would reject mid-batch.\n * - Relaxing (`DROP NOT NULL`, `DROP DEFAULT`) is reported as an advisory\n * unless `relaxColumns` is set, in which case it is executable.\n * - SQLite has no `ALTER COLUMN`; every alteration is reported as manual\n * (advisory-comment SQL) until the table-rebuild path (#2370) lands.\n * - Defaults are compared through {@link canonicalizeDefault}; when either\n * side cannot be canonicalized the comparison is skipped rather than\n * risking a false positive that would churn every run.\n */\n private async compareColumnConstraints(\n tableName: string,\n colName: string,\n colDef: ColumnDefinition,\n validatedType: SQLDataType,\n dbCol: SqlTableSchemaInfo['columns'][string],\n ): Promise<SchemaChange[]> {\n if (colDef.primaryKey || dbCol.primaryKey) {\n return [];\n }\n\n const changes: SchemaChange[] = [];\n const manifestNotNull = colDef.notNull === true;\n const dbNotNull = dbCol.notNull === true;\n const manifestDefaultSql =\n colDef.defaultValue !== undefined\n ? this.ddlStrategy.formatDefaultValue(\n colDef.defaultValue,\n validatedType,\n )\n : undefined;\n const manifestDefaultCanon = canonicalizeDefault(\n manifestDefaultSql,\n validatedType,\n );\n const dbDefaultCanon = canonicalizeDefault(\n dbCol.defaultValue === null || dbCol.defaultValue === undefined\n ? undefined\n : String(dbCol.defaultValue),\n validatedType,\n );\n // A manifest `null` default renders as the SQL NULL keyword, i.e. \"no\n // default\" — same bucket as an absent default.\n const hasManifestDefault = manifestDefaultCanon.kind !== 'none';\n const defaultsComparable =\n manifestDefaultCanon.kind !== 'raw' && dbDefaultCanon.kind !== 'raw';\n const defaultDrifted =\n defaultsComparable &&\n `${manifestDefaultCanon.kind}:${manifestDefaultCanon.key}` !==\n `${dbDefaultCanon.kind}:${dbDefaultCanon.key}`;\n const dbHasDefault = dbDefaultCanon.kind !== 'none';\n\n // --- Default drift ---------------------------------------------------\n if (defaultDrifted) {\n if (hasManifestDefault && manifestDefaultSql !== undefined) {\n changes.push(\n this.buildAlterColumnChange({\n tableName,\n colName,\n colDef,\n alteration: 'set_default',\n expected: `DEFAULT ${manifestDefaultSql}`,\n actual: dbHasDefault\n ? `DEFAULT ${String(dbCol.defaultValue)}`\n : '(no default)',\n statements: this.supportsAlterColumn()\n ? [\n this.generateSetDefaultSQL(\n tableName,\n colName,\n manifestDefaultSql,\n validatedType,\n ),\n ]\n : null,\n }),\n );\n } else if (dbHasDefault) {\n // Live column carries a default the manifest no longer declares.\n changes.push(\n this.buildAlterColumnChange({\n tableName,\n colName,\n colDef,\n alteration: 'drop_default',\n expected: '(no default)',\n actual: `DEFAULT ${String(dbCol.defaultValue)}`,\n statements: this.supportsAlterColumn()\n ? [\n `ALTER TABLE ${this.quoteIdentifier(tableName)} ALTER COLUMN ${this.quoteIdentifier(colName)} DROP DEFAULT`,\n ]\n : null,\n relaxation: {\n severity: 'info',\n message: `${tableName}.${colName} has a live default (${String(dbCol.defaultValue)}) the manifest no longer declares; inserts that omit the column keep receiving it. ${this.relaxHint('drop it')}`,\n },\n }),\n );\n }\n }\n\n // --- Nullability drift -----------------------------------------------\n if (manifestNotNull && !dbNotNull) {\n const quotedTable = this.quoteIdentifier(tableName);\n const quotedCol = this.quoteIdentifier(colName);\n const setNotNull = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET NOT NULL`;\n if (!this.supportsAlterColumn()) {\n changes.push(\n this.buildAlterColumnChange({\n tableName,\n colName,\n colDef,\n alteration: 'set_not_null',\n expected: 'NOT NULL',\n actual: 'NULL',\n statements: null,\n }),\n );\n } else if (hasManifestDefault && manifestDefaultSql !== undefined) {\n // Backfill NULLs with the manifest default so the constraint can be\n // enforced on a populated table, then tighten.\n changes.push(\n this.buildAlterColumnChange({\n tableName,\n colName,\n colDef,\n alteration: 'set_not_null',\n expected: 'NOT NULL',\n actual: 'NULL',\n statements: [\n `UPDATE ${quotedTable} SET ${quotedCol} = ${manifestDefaultSql} WHERE ${quotedCol} IS NULL`,\n setNotNull,\n ],\n }),\n );\n } else if (await this.columnHasNulls(tableName, colName)) {\n // No default to backfill with and live NULLs exist: the engine would\n // reject SET NOT NULL and abort the whole atomic batch. Report it\n // with the exact remediation instead.\n changes.push({\n type: 'alter_column',\n table: tableName,\n name: colName,\n column: colDef,\n alteration: 'set_not_null',\n mismatch: { expected: 'NOT NULL', actual: 'NULL' },\n sql: `-- ${tableName}.${colName} is required by the manifest but live rows hold NULL and the column has no default to backfill; UPDATE those rows, then run: ${setNotNull}`,\n advisory: {\n severity: 'warning',\n message: `${tableName}.${colName} is required by the manifest but live rows hold NULL and there is no default to backfill. Backfill the NULLs, then rerun db:migrate (or declare a default on the field).`,\n suggestedSql: [\n `UPDATE ${quotedTable} SET ${quotedCol} = <value> WHERE ${quotedCol} IS NULL`,\n setNotNull,\n ],\n },\n });\n } else {\n changes.push(\n this.buildAlterColumnChange({\n tableName,\n colName,\n colDef,\n alteration: 'set_not_null',\n expected: 'NOT NULL',\n actual: 'NULL',\n statements: [setNotNull],\n }),\n );\n }\n } else if (!manifestNotNull && dbNotNull) {\n changes.push(\n this.buildAlterColumnChange({\n tableName,\n colName,\n colDef,\n alteration: 'drop_not_null',\n expected: 'NULL',\n actual: 'NOT NULL',\n statements: this.supportsAlterColumn()\n ? [\n `ALTER TABLE ${this.quoteIdentifier(tableName)} ALTER COLUMN ${this.quoteIdentifier(colName)} DROP NOT NULL`,\n ]\n : null,\n relaxation: {\n severity: 'warning',\n message: `${tableName}.${colName} is NOT NULL in the database but nullable in the manifest; writes that store NULL will fail. ${this.relaxHint('drop the constraint')}`,\n },\n }),\n );\n }\n\n return changes;\n }\n\n /**\n * Assemble an `alter_column` change.\n *\n * - `statements: null` means the engine cannot alter the column in place\n * (SQLite): the change is reported as manual with advisory-comment SQL.\n * - `relaxation` marks a weakening change: without `relaxColumns` it is\n * emitted as a report-only advisory (no SQL); with it, the statements are\n * executable.\n */\n private buildAlterColumnChange(input: {\n tableName: string;\n colName: string;\n colDef: ColumnDefinition;\n alteration: ColumnAlteration;\n expected: string;\n actual: string;\n statements: string[] | null;\n relaxation?: { severity: 'warning' | 'info'; message: string };\n }): SchemaChange {\n const {\n tableName,\n colName,\n colDef,\n alteration,\n expected,\n actual,\n statements,\n relaxation,\n } = input;\n const base: SchemaChange = {\n type: 'alter_column',\n table: tableName,\n name: colName,\n column: colDef,\n alteration,\n mismatch: { expected, actual },\n };\n\n if (relaxation && !this.options.relaxColumns) {\n return {\n ...base,\n advisory: {\n severity: relaxation.severity,\n message: relaxation.message,\n suggestedSql: statements ?? [\n `-- SQLite cannot ALTER COLUMN; ${alteration === 'drop_not_null' ? 'dropping NOT NULL' : 'dropping the default'} requires a table rebuild (#2370)`,\n ],\n },\n };\n }\n\n if (statements === null) {\n const what =\n alteration === 'set_not_null'\n ? `enforcing NOT NULL on ${colName}`\n : alteration === 'drop_not_null'\n ? `dropping NOT NULL from ${colName}`\n : alteration === 'set_default'\n ? `setting the default on ${colName}`\n : `dropping the default from ${colName}`;\n return {\n ...base,\n sql: `-- SQLite: ${what} requires table recreation (expected ${expected}, found ${actual}; #2370)`,\n };\n }\n\n return {\n ...base,\n sql: statements[statements.length - 1],\n ...(statements.length > 1 ? { sqlStatements: statements } : {}),\n };\n }\n\n /**\n * Describe a DB column that has no manifest counterpart. Blocking when it\n * is NOT NULL without a default: ORM inserts never supply it, so every\n * insert fails with a not-null violation while `db:status` would otherwise\n * say the schema is in sync (#2369).\n */\n private describeOrphanColumn(\n tableName: string,\n colName: string,\n dbCol: SqlTableSchemaInfo['columns'][string],\n ): SchemaChange {\n const dbHasDefault =\n dbCol.defaultValue !== null && dbCol.defaultValue !== undefined;\n const blocking = dbCol.notNull === true && !dbHasDefault;\n const quotedTable = this.quoteIdentifier(tableName);\n const quotedCol = this.quoteIdentifier(colName);\n const dropSql = `ALTER TABLE ${quotedTable} DROP COLUMN ${quotedCol}`;\n const relaxSql = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} DROP NOT NULL`;\n const shape = this.describeDbColumn(dbCol);\n const base: SchemaChange = {\n type: 'orphan_column',\n table: tableName,\n name: colName,\n mismatch: { expected: '(not in manifest)', actual: shape },\n };\n\n if (!blocking) {\n return {\n ...base,\n advisory: {\n severity: 'info',\n message: `${tableName}.${colName} exists in the database but not in the manifest (${shape}). Harmless for inserts; pass includeDroppedColumns / --drop-columns to drop it.`,\n suggestedSql: [dropSql],\n },\n };\n }\n\n if (this.options.relaxColumns && this.supportsAlterColumn()) {\n // Opted-in, engine can relax in place: make it executable so inserts\n // keep working without destroying the column's data.\n return {\n type: 'alter_column',\n table: tableName,\n name: colName,\n alteration: 'drop_not_null',\n mismatch: { expected: '(not in manifest)', actual: shape },\n sql: relaxSql,\n };\n }\n\n const fix = this.supportsAlterColumn()\n ? `Pass relaxColumns / --relax-columns to drop the NOT NULL (keeps data), or includeDroppedColumns / --drop-columns to drop the column.`\n : `SQLite cannot drop NOT NULL in place; pass includeDroppedColumns / --drop-columns to drop the column (or rebuild the table, #2370).`;\n return {\n ...base,\n advisory: {\n severity: 'warning',\n message: `${tableName}.${colName} is NOT NULL without a default and is not in the manifest — every ORM insert into ${tableName} will fail (not-null violation). ${fix}`,\n suggestedSql: this.supportsAlterColumn()\n ? [relaxSql, dropSql]\n : [dropSql],\n },\n };\n }\n\n /**\n * Remediation hint for a relaxation advisory. On engines with `ALTER\n * COLUMN` the flag executes it; SQLite cannot relax in place, so the hint\n * says so instead of implying the flag will act (#2370).\n */\n private relaxHint(action: string): string {\n return this.supportsAlterColumn()\n ? `Pass relaxColumns / --relax-columns to ${action}.`\n : `SQLite cannot ${action} in place (no ALTER COLUMN); it requires a table rebuild (#2370) — relaxColumns / --relax-columns only reports it as manual here.`;\n }\n\n private describeDbColumn(\n dbCol: SqlTableSchemaInfo['columns'][string],\n ): string {\n const parts = [String(dbCol.type ?? '').toUpperCase() || 'UNKNOWN'];\n if (dbCol.notNull) parts.push('NOT NULL');\n if (dbCol.defaultValue !== null && dbCol.defaultValue !== undefined) {\n parts.push(`DEFAULT ${String(dbCol.defaultValue)}`);\n }\n return parts.join(' ');\n }\n\n /**\n * Whether the engine can alter nullability/defaults of an existing column\n * in place. SQLite cannot (`ALTER TABLE ... ALTER COLUMN` does not exist);\n * PostgreSQL, DuckDB and the DuckDB-backed JSON adapter can.\n */\n private supportsAlterColumn(): boolean {\n return this.engine !== 'sqlite';\n }\n\n private generateSetDefaultSQL(\n tableName: string,\n colName: string,\n formattedDefault: string,\n validatedType: SQLDataType,\n ): string {\n // Mirror the type-upgrade path: PostgreSQL JSON defaults are cast so the\n // stored default is the jsonb literal rather than an untyped string.\n const defaultSql =\n this.engine === 'postgres' && this.normalizeType(validatedType) === 'JSON'\n ? `${formattedDefault}::${this.ddlStrategy.mapType(validatedType).toLowerCase()}`\n : formattedDefault;\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} ALTER COLUMN ${this.quoteIdentifier(colName)} SET DEFAULT ${defaultSql}`;\n }\n\n /**\n * Live-data probe: does the table hold any row? Used to decide whether an\n * added required column without a default can be enforced NOT NULL right\n * away. Errors (adapter without raw query, etc.) resolve to `true` so the\n * caller takes the conservative path.\n */\n private async tableHasRows(tableName: string): Promise<boolean> {\n try {\n const result = await this.db.query(\n `SELECT 1 AS present FROM ${this.quoteIdentifier(tableName)} LIMIT 1`,\n );\n return (result.rows?.length ?? 0) > 0;\n } catch (err) {\n logger.debug(\n `[SchemaComparer] Row probe unavailable for ${tableName}; assuming populated`,\n { error: err instanceof Error ? err.message : String(err) },\n );\n return true;\n }\n }\n\n /**\n * Live-data probe: does the column hold any NULL? Used before emitting a\n * `SET NOT NULL` that has no default to backfill with. Errors resolve to\n * `true` (conservative: report instead of emitting an ALTER that fails).\n */\n private async columnHasNulls(\n tableName: string,\n colName: string,\n ): Promise<boolean> {\n try {\n const result = await this.db.query(\n `SELECT 1 AS present FROM ${this.quoteIdentifier(tableName)} WHERE ${this.quoteIdentifier(colName)} IS NULL LIMIT 1`,\n );\n return (result.rows?.length ?? 0) > 0;\n } catch (err) {\n logger.debug(\n `[SchemaComparer] NULL probe unavailable for ${tableName}.${colName}; assuming NULLs exist`,\n { error: err instanceof Error ? err.message : String(err) },\n );\n return true;\n }\n }\n\n private isUuidTextEquivalentColumn(\n columnName: string,\n colDef: ColumnDefinition,\n normalizedExpected: string,\n normalizedActual: string,\n ): boolean {\n const expectedUuidActualText =\n normalizedExpected === 'UUID' && normalizedActual === 'TEXT';\n const expectedTextActualUuid =\n normalizedExpected === 'TEXT' && normalizedActual === 'UUID';\n\n if (!expectedUuidActualText && !expectedTextActualUuid) {\n return false;\n }\n\n return this.isStructuralUuidCompatibleColumn(columnName, colDef);\n }\n\n private isStructuralUuidCompatibleColumn(\n columnName: string,\n colDef: ColumnDefinition,\n ): boolean {\n return (\n colDef.primaryKey === true ||\n Boolean(colDef.foreignKey) ||\n colDef.referenceKind === 'id' ||\n colDef.referenceKind === 'foreignKey' ||\n colDef.referenceKind === 'crossPackageRef' ||\n colDef.referenceKind === 'tenantId' ||\n (columnName === 'id' && colDef.type === 'TEXT')\n );\n }\n\n /**\n * Compare indexes between manifest and database\n *\n * Four classes of drift the differ now detects:\n *\n * 1. **Missing index** — manifest has an index neither the DB has by name\n * nor any equivalent-by-signature. Emit `add_index`. (Issue #741: the\n * signature check protects against creating duplicates when STI child\n * classes register indexes with different name prefixes.)\n *\n * 2. **Same-name shape drift** — DB has an index with the manifest's name,\n * but its columns, uniqueness flag, or partial-index `WHERE` predicate\n * differ. This covers the uniqueness flip in issue #1165\n * (`tenants_slug_context_meta_type_idx` materialized non-unique while\n * the manifest declares it unique) and the predicate drift in issue\n * #1692 (a partial index whose `WHERE` clause was added, removed, or\n * altered). Emit `drop_index` + `add_index` so the next migrate cycle\n * recreates it with the correct shape.\n *\n * 3. **Orphan in DB** — DB has an index with no manifest counterpart by\n * name and no signature equivalent. Emit `drop_index` *only* when the\n * caller opts in via `includeDroppedIndexes`, and even then never for\n * PostgreSQL implicit indexes (`*_pkey`, `*_key`) — those are owned by\n * table-level constraints and need a separate `DROP CONSTRAINT` path\n * that the differ does not emit yet. One exception is unconditional: a\n * redundant single-column index over the live primary key (the legacy\n * `<table>_id_idx`, #2359) is dropped even without the opt-in, because\n * the primary-key constraint already serves it.\n *\n * 4. **Partial-index predicate drift / collision** — two indexes on the\n * same column(s) and uniqueness that differ only by their `WHERE`\n * predicate (e.g. distinct STI child partial indexes) are no longer\n * collapsed to one signature, so the signature-equivalence path (b)\n * won't claim one for the other.\n *\n * @param dbIndexPredicates - Normalized `WHERE` predicate per DB index\n * name from {@link getDbIndexPredicates}. `null` means predicate\n * introspection was unavailable for this engine/adapter, in which case\n * the comparison falls back to predicate-unaware signatures (the prior\n * behavior) so existing partial indexes are never flagged as false drift.\n */\n private compareIndexes(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n dbIndexPredicates: Map<string, string> | null = null,\n ): SchemaChange[] {\n const changes: SchemaChange[] = [];\n\n // Only fold predicates into signatures when we actually read them back\n // from the live DB. If introspection was unavailable both sides use the\n // empty predicate, which reproduces the prior column+unique-only behavior\n // and cannot manufacture false positives on existing partial indexes.\n const predicateAware = dbIndexPredicates !== null;\n const dbPredicateFor = (name: string): string =>\n predicateAware ? (dbIndexPredicates?.get(name) ?? '') : '';\n const manifestPredicateFor = (idx: IndexDefinition): string =>\n predicateAware ? normalizeIndexPredicate(idx.where) : '';\n\n // An STI subtype-scoped UNIQUE index (`unique: true` declared only on a\n // descendant, #2359) has no faithful shape on an engine without partial\n // indexes: degrading it to a full unique index would enforce one\n // subtype's constraint across every sibling's rows, so it is skipped\n // there — same as the DuckDB DDL strategy — rather than compared or\n // created. Other partial indexes keep degrading to full ones as before.\n const manifestIndexes = this.supportsPartialIndexes()\n ? manifest.indexes\n : manifest.indexes.filter((idx) => !isStiSubtypeUniqueIndex(idx));\n\n // The live table's primary-key columns, for the redundant-PK-index\n // sweep below.\n const primaryKeyColumns = Object.entries(dbSchema.columns)\n .filter(([, column]) => column.primaryKey === true)\n .map(([name]) => name);\n\n // Index DB indexes by name and by signature for fast lookup.\n // dbIndexSignatures groups *all* DB index names sharing a signature so\n // duplicates under different names all get claimed by a single matching\n // manifest entry — otherwise the orphan sweep would drop the un-claimed\n // siblings even though they are functionally equivalent to the manifest.\n const dbIndexesByName = new Map<\n string,\n { columns: string[]; unique: boolean }\n >();\n const dbIndexSignatures = new Map<string, Set<string>>();\n for (const idx of dbSchema.indexes) {\n const unique = idx.unique ?? false;\n dbIndexesByName.set(idx.name, { columns: idx.columns, unique });\n const signature = this.getIndexSignature(\n idx.columns,\n unique,\n dbPredicateFor(idx.name),\n );\n let bucket = dbIndexSignatures.get(signature);\n if (!bucket) {\n bucket = new Set();\n dbIndexSignatures.set(signature, bucket);\n }\n bucket.add(idx.name);\n }\n\n // Manifest signatures — used during the orphan sweep to skip DB indexes\n // that match a manifest entry's signature even if no specific manifest\n // entry \"claimed\" them by name.\n const manifestSignatureSet = new Set<string>();\n for (const idx of manifestIndexes) {\n manifestSignatureSet.add(\n this.getIndexSignature(idx, undefined, manifestPredicateFor(idx)),\n );\n }\n\n // Track which DB indexes a manifest entry has claimed, so the orphan\n // pass below doesn't re-flag indexes that match by signature alone.\n const claimedDbIndexes = new Set<string>();\n\n for (const idx of manifestIndexes) {\n const manifestSignature = this.getIndexSignature(\n idx,\n undefined,\n manifestPredicateFor(idx),\n );\n\n // (a) Same name in DB — verify shape matches.\n const dbByName = dbIndexesByName.get(idx.name);\n if (dbByName) {\n claimedDbIndexes.add(idx.name);\n\n // JSON-path indexes (`@meta({ indexed: true })`) cannot be reliably\n // compared by DB introspection — SQLite expression indexes surface\n // as `[null]` columns, so the signature would always mismatch and\n // every diff run would emit drop+recreate. Trust the name match\n // here; if the json path itself changes, the index name changes\n // too (we encode the field name into it), so a name match is a\n // stronger guarantee than the column list for this index family.\n if (isJsonPathIndex(idx)) {\n continue;\n }\n\n const dbSignature = this.getIndexSignature(\n dbByName.columns,\n dbByName.unique,\n dbPredicateFor(idx.name),\n );\n if (dbSignature === manifestSignature) {\n continue; // Same name, same shape — nothing to do.\n }\n\n // Same name, drifted shape. Most often this is a uniqueness flip\n // (issue #1165: 3-column index materialized non-unique). Recreate.\n //\n // Rollback caveat: the auto-generated DOWN script for the\n // `add_index` half drops the (newly correct) index, and the\n // `drop_index` half has no DOWN. Rolling back a recreate leaves\n // the table without the index entirely instead of restoring the\n // wrong-shape original. Capturing the original shape would\n // require richer DB introspection than the differ currently has,\n // so this asymmetry is accepted — the failure mode after an\n // un-rolled-back recreate is \"missing index\" rather than\n // \"permanently broken UPSERT,\" which is recoverable.\n changes.push({\n type: 'drop_index',\n table: tableName,\n name: idx.name,\n sql: this.generateDropIndexSQL(idx.name),\n });\n changes.push({\n type: 'add_index',\n table: tableName,\n name: idx.name,\n index: idx,\n sql: this.generateAddIndexSQL(tableName, idx),\n });\n continue;\n }\n\n // (b) Different name in DB but functionally equivalent — keep as-is.\n // Claim *every* DB name sharing this signature so duplicate-shape\n // indexes (e.g., a stale `<name>_idx` plus the implicit `<name>_key`\n // from the same constraint) all survive the orphan sweep.\n const equivalentIndexNames = dbIndexSignatures.get(manifestSignature);\n if (equivalentIndexNames && equivalentIndexNames.size > 0) {\n for (const name of equivalentIndexNames) {\n claimedDbIndexes.add(name);\n }\n continue;\n }\n\n // (c) Genuinely missing — add it.\n changes.push({\n type: 'add_index',\n table: tableName,\n name: idx.name,\n index: idx,\n sql: this.generateAddIndexSQL(tableName, idx),\n });\n }\n\n // Orphan-index sweep. Three tiers:\n //\n // - A redundant primary-key index — a non-unique single-column index over\n // the table's sole primary-key column that the manifest no longer\n // declares — is dropped unconditionally. Every generator path used to\n // emit `<table>_id_idx` beside the engine's own primary-key index\n // (#2359, A5); the constraint keeps serving every lookup, so the drop\n // is a pure write-cost saving and `db:migrate` cleans it up without\n // `--drop-indexes`.\n // - Unclaimed constraint-owned unique indexes (`*_key`) are never dropped\n // but always *reported* (#2369) — a stale inline UNIQUE keeps rejecting\n // duplicates the model now allows.\n // - Everything else is dropped only when the caller opts in via\n // includeDroppedIndexes.\n for (const idx of dbSchema.indexes) {\n if (claimedDbIndexes.has(idx.name)) continue;\n\n // Belt-and-suspenders: even if a DB index wasn't formally claimed\n // by a manifest entry (e.g., shape-drift recreate consumed the\n // claim slot), don't drop it if its signature still matches\n // something the manifest declares. That would contradict the\n // option doc (\"not functionally equivalent to anything in the\n // manifest\") and risk dropping a still-needed index.\n const idxSignature = this.getIndexSignature(\n idx.columns,\n idx.unique ?? false,\n dbPredicateFor(idx.name),\n );\n if (manifestSignatureSet.has(idxSignature)) continue;\n\n if (isProtectedDbIndexName(idx.name)) {\n if (!idx.unique || idx.name.endsWith('_pkey')) continue;\n // A unique constraint index whose column(s) the manifest no longer\n // declares unique (column `unique: true` flipped off, or the field\n // was renamed/removed). Column-level `unique` is not represented in\n // `manifest.indexes`, so check the column definitions too.\n const backedByUniqueColumn =\n idx.columns.length === 1 &&\n manifest.columns[idx.columns[0]]?.unique === true;\n if (backedByUniqueColumn) continue;\n const cols = idx.columns.map((c) => this.quoteIdentifier(c)).join(', ');\n changes.push({\n type: 'orphan_index',\n table: tableName,\n name: idx.name,\n mismatch: {\n expected: '(not in manifest)',\n actual: `UNIQUE (${idx.columns.join(', ')})`,\n },\n advisory: {\n severity: 'warning',\n message: `Unique constraint ${idx.name} on ${tableName}(${idx.columns.join(', ')}) is no longer declared by the manifest; duplicate values the model now allows will still be rejected. Drop it manually.`,\n suggestedSql:\n this.engine === 'postgres'\n ? [\n `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP CONSTRAINT IF EXISTS ${this.quoteIdentifier(idx.name)}`,\n `DROP INDEX IF EXISTS ${this.quoteIdentifier(idx.name)} -- if it is a plain unique index on ${cols} rather than a constraint`,\n ]\n : [this.generateDropIndexSQL(idx.name)],\n },\n });\n continue;\n }\n\n // \"Sole primary-key column\": a single-column index on one column of\n // a COMPOSITE primary key is not redundant (the PK index only serves\n // prefixes), so require the table to have exactly one PK column.\n // Non-unique only: the legacy `<table>_id_idx` never was unique, and a\n // UNIQUE single-column index on the PK may be the index that backs a\n // custom-named PRIMARY KEY constraint on PostgreSQL (only `*_pkey` is\n // filtered by introspection) — `DROP INDEX` on it fails and, because\n // migrate is atomic, would roll back the whole batch (#2359).\n const redundantPrimaryKeyIndex =\n idx.unique !== true &&\n idx.columns.length === 1 &&\n dbPredicateFor(idx.name) === '' &&\n primaryKeyColumns.length === 1 &&\n primaryKeyColumns[0] === idx.columns[0];\n if (!redundantPrimaryKeyIndex && !this.options.includeDroppedIndexes) {\n continue;\n }\n\n changes.push({\n type: 'drop_index',\n table: tableName,\n name: idx.name,\n sql: this.generateDropIndexSQL(idx.name),\n });\n }\n\n return changes;\n }\n\n /**\n * Generate a signature for an index based on its columns, uniqueness, and\n * (normalized) partial-index predicate. Used for functional equivalence\n * checking (Issue #741) and predicate-drift detection (Issue #1692).\n *\n * Note: Column order is preserved because it is semantically significant for\n * composite indexes. An index on (a, b) is NOT equivalent to (b, a) - they\n * have different query performance characteristics.\n *\n * The trailing predicate component distinguishes partial indexes that share\n * columns and uniqueness but differ by their `WHERE` clause (e.g. distinct\n * STI child partial indexes). Callers pass the already-normalized predicate\n * so both the manifest (desired) and introspected (DB) sides compare equal\n * for semantically-identical clauses. An empty string means \"no predicate\"\n * (a non-partial index) and is also used on both sides when predicate\n * introspection is unavailable, preserving the prior behavior.\n *\n * For JSON-path indexes (`@meta({ indexed: true })`) the signature is\n * derived from the JSON path instead of an empty column list, so the\n * differ can distinguish two jsonPath indexes against different paths.\n *\n * @param idxOrColumns - Either an IndexDefinition or a column array (legacy)\n * @param uniqueArg - Unique flag (used when first arg is a column array)\n * @param predicateArg - Normalized partial-index predicate (default '')\n * @returns Signature string\n */\n private getIndexSignature(\n idxOrColumns: IndexDefinition | string[],\n uniqueArg?: boolean,\n predicateArg = '',\n ): string {\n if (Array.isArray(idxOrColumns)) {\n return `${idxOrColumns.join(',')}:${Boolean(uniqueArg)}:${predicateArg}`;\n }\n const idx = idxOrColumns;\n if (isJsonPathIndex(idx) && idx.jsonPath) {\n return `json:${idx.jsonPath.column}.${idx.jsonPath.path}:${Boolean(idx.unique)}:${predicateArg}`;\n }\n return `${(idx.columns ?? []).join(',')}:${Boolean(idx.unique)}:${predicateArg}`;\n }\n\n /**\n * Whether the active engine supports partial indexes (`CREATE INDEX … WHERE`).\n *\n * SQLite and PostgreSQL do. DuckDB rejects them outright, and the JSON\n * adapter is DuckDB-backed, so on those engines a \"partial\" index can only\n * ever exist as a full index. Treating the predicate as significant there\n * would (a) flag existing full indexes as false drift and (b) emit\n * `CREATE INDEX … WHERE` DDL the engine rejects, breaking the migration.\n * Gating on this keeps both the comparison and the generated DDL aligned\n * with what the engine actually accepts (a partial index degrades to a\n * full index), matching the pre-#1692 behavior on those engines.\n */\n private supportsPartialIndexes(): boolean {\n return this.engine === 'sqlite' || this.engine === 'postgres';\n }\n\n /**\n * Introspect partial-index predicates for a table, keyed by index name.\n *\n * `getTableSchema()` (the @happyvertical/sql introspection) returns only\n * name/columns/unique, so the `WHERE` predicate is read directly here:\n *\n * - PostgreSQL: `pg_indexes.indexdef` carries the full CREATE INDEX text.\n * - SQLite: the `sqlite_master.sql` column carries the original CREATE INDEX\n * text.\n *\n * Engines that don't support partial indexes (DuckDB / the DuckDB-backed\n * JSON adapter) short-circuit to `null` so the comparison stays\n * predicate-unaware there — see {@link supportsPartialIndexes}.\n *\n * Non-partial indexes are omitted from the map (callers treat a missing\n * entry as the empty predicate). Returns `null` when the catalog query\n * fails — e.g. an adapter exposing neither catalog — so the index\n * comparison can fall back to predicate-unaware behavior rather than\n * flagging every existing partial index as false drift.\n */\n private async getDbIndexPredicates(\n tableName: string,\n ): Promise<Map<string, string> | null> {\n if (!this.supportsPartialIndexes()) {\n return null;\n }\n const predicates = new Map<string, string>();\n try {\n if (this.engine === 'postgres') {\n const result = await this.db.query(\n `SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename = ${this.quoteLiteral(\n tableName,\n )}`,\n );\n for (const row of result.rows as {\n indexname?: string;\n indexdef?: string;\n }[]) {\n if (!row.indexname || !row.indexdef) continue;\n const predicate = extractIndexPredicate(row.indexdef);\n if (predicate) predicates.set(row.indexname, predicate);\n }\n return predicates;\n }\n\n // SQLite exposes the `sqlite_master` catalog whose `sql` column preserves\n // the original CREATE INDEX text. Implicit indexes carry a NULL `sql` and\n // are skipped. (DuckDB / JSON already short-circuited above.)\n const result = await this.db.query(\n `SELECT name, sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ${this.quoteLiteral(\n tableName,\n )} AND name NOT LIKE 'sqlite_%'`,\n );\n for (const row of result.rows as {\n name?: string;\n sql?: string | null;\n }[]) {\n if (!row.name || !row.sql) continue;\n const predicate = extractIndexPredicate(row.sql);\n if (predicate) predicates.set(row.name, predicate);\n }\n return predicates;\n } catch (err) {\n logger.debug(\n `[SchemaComparer] Partial-index predicate introspection unavailable for ${tableName}; falling back to predicate-unaware index comparison`,\n { error: err instanceof Error ? err.message : String(err) },\n );\n return null;\n }\n }\n\n /**\n * Get list of existing tables from database\n */\n private async getExistingTables(): Promise<Set<string>> {\n let query: string;\n if (this.engine === 'postgres') {\n query = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`;\n } else {\n // SQLite and DuckDB both expose the SQLite-compatible `sqlite_master`\n // catalog — DuckDB ships it as a built-in compatibility view, so a single\n // introspection query covers both engines. Verified against a live DuckDB\n // v1.4.3 (json mode): `SELECT name FROM sqlite_master WHERE type='table'`\n // returns user tables with the expected `name` column (#1579).\n query = `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;\n }\n\n const result = await this.db.query(query);\n const rows = result.rows as { name?: string; table_name?: string }[];\n return new Set(\n rows.map((r) => r.name || r.table_name || '').filter(Boolean),\n );\n }\n\n /**\n * Normalize SQL types for comparison\n */\n private normalizeType(type: string): string {\n const upper = type\n .toUpperCase()\n .trim()\n .replace(/\\(\\s*\\d+\\s*\\)/g, '');\n\n // Integer types\n if (/^(INTEGER|INT|BIGINT|SMALLINT|TINYINT)$/i.test(upper)) {\n return 'INTEGER';\n }\n\n // Text types\n if (/^(TEXT|CLOB|STRING|VARCHAR|CHAR)/i.test(upper)) {\n return 'TEXT';\n }\n\n // UUID normalizes to its own bucket — deliberately NOT folded into TEXT.\n // The text<->uuid drift tolerance (R11) is applied at the equality gate\n // in `compare()` only, so that `isCompatibleTypeUpgrade` (which also\n // calls normalizeType) still treats `uuid` as distinct from text and\n // won't mis-classify e.g. `uuid`->`timestamp` as a compatible\n // \"TEXT->TIMESTAMP\" upgrade.\n if (/^UUID$/i.test(upper)) {\n return 'UUID';\n }\n\n // Decimal types\n if (/^(REAL|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER)/i.test(upper)) {\n return 'REAL';\n }\n\n // Boolean types\n if (/^(BOOLEAN|BOOL)/i.test(upper)) {\n return 'BOOLEAN';\n }\n\n // PostgreSQL distinguishes instants from timezone-naive wall times. Keep\n // those in separate comparison buckets so legacy TIMESTAMP columns are\n // migrated to the TIMESTAMPTZ representation used for JavaScript Date.\n if (/^(TIMESTAMPTZ|TIMESTAMP\\s+WITH\\s+TIME\\s+ZONE)$/i.test(upper)) {\n return 'TIMESTAMPTZ';\n }\n\n // Date/time types without timezone semantics\n if (\n /^(DATETIME|TIMESTAMP(?:\\s+WITHOUT\\s+TIME\\s+ZONE)?|DATE|TIME)$/i.test(\n upper,\n )\n ) {\n return 'TIMESTAMP';\n }\n\n // Blob types\n if (/^(BLOB|BINARY|BYTEA)/i.test(upper)) {\n return 'BLOB';\n }\n\n // JSON types\n if (/^(JSON|JSONB)/i.test(upper)) {\n return 'JSON';\n }\n\n return upper;\n }\n\n /**\n * Check if a type change is a safe upgrade that can be auto-migrated.\n *\n * SMRT controls the data lifecycle for these columns, so we know:\n * - TEXT→JSON: The column stores JSON data serialized as text (arrays, objects)\n * - TEXT/JSON→TIMESTAMP: Legacy system columns stored timestamp strings\n * before newer manifests normalized the column type.\n * - PostgreSQL TIMESTAMP→TIMESTAMPTZ: Legacy SMRT Date values were written\n * as UTC ISO wall times, so interpreting the naive value as UTC preserves\n * the originally supplied instant.\n * - INTEGER→REAL: Safe widening of integer to floating point\n * - TEXT/REAL→INTEGER on PostgreSQL: explicit data-checked repairs for\n * legacy integer columns that were previously stored as text/real.\n *\n * @param manifestType - The abstract type from the manifest (e.g., 'JSON')\n * @param dbType - The actual type in the database (e.g., 'TEXT')\n * @returns true if the change from dbType to manifestType is a safe upgrade\n */\n private isCompatibleTypeUpgrade(\n manifestType: string,\n dbType: string,\n ): boolean {\n const manifest = this.normalizeType(manifestType);\n const db = this.normalizeType(dbType);\n\n if (\n this.engine === 'postgres' &&\n manifest === 'TIMESTAMP' &&\n ['TIMESTAMP', 'TEXT', 'JSON'].includes(db) &&\n this.ddlStrategy.mapType('TIMESTAMP') === 'TIMESTAMPTZ'\n ) {\n return this.options.postgresTimestampMigration?.legacyTimezone === 'UTC';\n }\n\n // TEXT → JSON is safe: SMRT serializes arrays/objects as JSON text\n // When the manifest says JSON, the data is already valid JSON in TEXT column\n if (manifest === 'JSON' && db === 'TEXT') {\n return true;\n }\n\n // JSON → TEXT is also safe (downgrade, but data is preserved as-is)\n if (manifest === 'TEXT' && db === 'JSON') {\n return true;\n }\n\n // UUID → TEXT is safe for plain text/provenance fields that were\n // mistakenly materialized as native uuid: the stored UUID value can be\n // rendered losslessly as text.\n if (manifest === 'TEXT' && db === 'UUID') {\n return true;\n }\n\n // INTEGER → REAL is safe (widening)\n if (manifest === 'REAL' && db === 'INTEGER') {\n return true;\n }\n\n // PostgreSQL can safely repair legacy integer columns when the generated\n // migration validates that every existing value is already an integer.\n if (\n this.engine === 'postgres' &&\n manifest === 'INTEGER' &&\n (db === 'TEXT' || db === 'REAL')\n ) {\n return true;\n }\n\n // TEXT/JSON → TIMESTAMP is a legacy-drift repair. Invalid values fail\n // explicitly during migration rather than being silently coerced.\n if (\n manifest === 'TIMESTAMP' &&\n (db === 'TEXT' || db === 'JSON') &&\n this.engine !== 'postgres'\n ) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Generate SQL for a type upgrade migration.\n *\n * Engine-specific SQL:\n * - SQLite: TEXT and JSON are equivalent, no-op or comment\n * - DuckDB: ALTER COLUMN TYPE (native type conversion)\n * - PostgreSQL: ALTER COLUMN TYPE with USING clause\n */\n private generateTypeUpgradeSQL(\n tableName: string,\n colName: string,\n colDef: ColumnDefinition,\n dbType: string,\n ): GeneratedTypeUpgradeSQL {\n const quotedTable = this.quoteIdentifier(tableName);\n const quotedCol = this.quoteIdentifier(colName);\n const manifestType = colDef.type;\n\n // Validate manifestType before mapping\n const validatedType: SQLDataType = isValidSQLDataType(manifestType)\n ? manifestType\n : 'TEXT';\n const targetType = this.ddlStrategy.mapType(validatedType);\n\n switch (this.engine) {\n case 'sqlite':\n // SQLite has dynamic typing - TEXT and JSON are functionally equivalent\n // For SQLite, we just return a comment since no actual change is needed\n if (\n this.normalizeType(manifestType) === 'JSON' &&\n this.normalizeType(dbType) === 'TEXT'\n ) {\n return {\n sql: `-- SQLite: ${quotedCol} already stores JSON as TEXT (no change needed)`,\n };\n }\n // Every other upgrade needs the whole table rebuilt. `compareTable`\n // replaces this placeholder with the executable plan (#2370); it\n // survives only when the rebuild is unsafe or unavailable, and then\n // means the same thing it always did — manual intervention.\n return { sql: sqliteRebuildPlaceholderSql(colName) };\n\n case 'postgres': {\n // PostgreSQL defaults must be dropped/reset around some type changes\n // so drift repairs can succeed even when an existing default cannot\n // be cast automatically to the target type.\n const manifestNormalized = this.normalizeType(manifestType);\n const dbNormalized = this.normalizeType(dbType);\n const preflightSQL =\n manifestNormalized === 'INTEGER' &&\n (dbNormalized === 'TEXT' || dbNormalized === 'REAL')\n ? this.generatePostgresIntegerPreflightSQL(\n quotedTable,\n quotedCol,\n tableName,\n colName,\n dbNormalized,\n )\n : manifestNormalized === 'TIMESTAMP' &&\n ['TIMESTAMP', 'TEXT', 'JSON'].includes(dbNormalized) &&\n this.normalizeType(targetType) === 'TIMESTAMPTZ'\n ? this.generatePostgresTimestampPreflightSQL(tableName, colName)\n : null;\n const clauses: string[] = [];\n\n if (colDef.defaultValue !== undefined) {\n clauses.push(`ALTER COLUMN ${quotedCol} DROP DEFAULT`);\n }\n\n let typeClause = `ALTER COLUMN ${quotedCol} TYPE ${targetType}`;\n if (manifestNormalized === 'JSON' && dbNormalized === 'TEXT') {\n // #1335: a bare `${col}::jsonb` cast raises \"invalid input syntax for\n // type json\" on any row whose text is not already valid JSON (e.g. a\n // legacy enum column holding 'active'). `to_jsonb(col)` instead wraps\n // ANY text value as a JSON string and never errors, so a genuine\n // TEXT->JSON widening survives non-JSON legacy data. (Note: with the\n // json<->text equality tolerance added in this fix, the normal\n // compare path no longer reaches here; this keeps the SQL safe for\n // callers that construct a type_upgrade directly.)\n typeClause += ` USING to_jsonb(${quotedCol})`;\n } else if (manifestNormalized === 'TEXT' && dbNormalized === 'JSON') {\n // #1335: a native-json column cast back to text is value-preserving\n // (`::text` renders the stored JSON as its text form), so this arm is\n // safe. (Note: like the TEXT->JSON arm above, the json<->text equality\n // tolerance means the normal compare path no longer reaches here; this\n // keeps the SQL safe for callers that construct a type_upgrade\n // directly.)\n typeClause += ` USING ${quotedCol}::text`;\n } else if (manifestNormalized === 'TEXT' && dbNormalized === 'UUID') {\n typeClause += ` USING ${quotedCol}::text`;\n } else if (\n manifestNormalized === 'INTEGER' &&\n dbNormalized === 'TEXT'\n ) {\n typeClause += ` USING trim(${quotedCol}::text)::integer`;\n } else if (\n manifestNormalized === 'INTEGER' &&\n dbNormalized === 'REAL'\n ) {\n typeClause += ` USING ${quotedCol}::integer`;\n } else if (\n manifestNormalized === 'TIMESTAMP' &&\n (dbNormalized === 'TEXT' || dbNormalized === 'JSON')\n ) {\n typeClause += ` USING NULLIF(NULLIF(trim(both '\"' from ${quotedCol}::text), ''), 'null')::timestamptz`;\n } else if (\n manifestNormalized === 'TIMESTAMP' &&\n dbNormalized === 'TIMESTAMP' &&\n this.normalizeType(targetType) === 'TIMESTAMPTZ'\n ) {\n // SMRT's PostgreSQL adapter serialized Date values to UTC ISO strings.\n // A legacy TIMESTAMP column discarded the trailing offset but kept\n // that UTC wall time. Reattach UTC explicitly so migration is\n // deterministic regardless of the database/session timezone.\n typeClause += ` USING ${quotedCol} AT TIME ZONE 'UTC'`;\n }\n\n clauses.push(typeClause);\n\n if (colDef.defaultValue !== undefined) {\n const formattedDefault = this.ddlStrategy.formatDefaultValue(\n colDef.defaultValue,\n validatedType,\n );\n const defaultSql =\n manifestNormalized === 'JSON'\n ? `${formattedDefault}::${targetType.toLowerCase()}`\n : formattedDefault;\n clauses.push(`ALTER COLUMN ${quotedCol} SET DEFAULT ${defaultSql}`);\n }\n\n const alterSql = `ALTER TABLE ${quotedTable} ${clauses.join(', ')}`;\n\n return preflightSQL\n ? { sql: alterSql, statements: [preflightSQL, alterSql] }\n : { sql: alterSql };\n }\n\n case 'duckdb':\n // DuckDB supports ALTER COLUMN TYPE for type conversions\n return {\n sql: `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} TYPE ${targetType}`,\n };\n\n default: {\n // Escape special characters in type names for safe comment generation\n const safeDbType = dbType.replace(/[^\\w]/g, '_');\n const safeManifestType = manifestType.replace(/[^\\w]/g, '_');\n return {\n sql: `-- Type upgrade for ${quotedCol}: ${safeDbType} → ${safeManifestType}`,\n };\n }\n }\n }\n\n /**\n * Quote a SQL identifier\n */\n private quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n }\n\n /**\n * Generate a PostgreSQL preflight guard for narrowing legacy columns to\n * INTEGER. PostgreSQL's REAL→INTEGER cast rounds fractional values, so the\n * generated migration must fail before the ALTER can silently change data.\n */\n private generatePostgresIntegerPreflightSQL(\n quotedTable: string,\n quotedCol: string,\n tableName: string,\n colName: string,\n dbNormalized: string,\n ): string {\n const invalidCondition =\n dbNormalized === 'REAL'\n ? `${quotedCol} IS NOT NULL AND ${quotedCol} <> trunc(${quotedCol})`\n : `${quotedCol} IS NOT NULL AND trim(${quotedCol}::text) !~ '^[+-]?[0-9]+$'`;\n const message = `Cannot convert ${tableName}.${colName} to INTEGER: found non-integer values`;\n\n return `DO $$ BEGIN IF EXISTS (SELECT 1 FROM ${quotedTable} WHERE ${invalidCondition}) THEN RAISE EXCEPTION ${this.quoteLiteral(message)}; END IF; END $$`;\n }\n\n /**\n * Require a UTC migration session before reattaching the offset discarded by\n * legacy PostgreSQL TIMESTAMP columns. This is a guardrail, not provenance:\n * operators must also confirm historical default/raw writers used UTC.\n */\n private generatePostgresTimestampPreflightSQL(\n tableName: string,\n colName: string,\n ): string {\n const message =\n `Cannot convert ${tableName}.${colName} to TIMESTAMPTZ outside a UTC ` +\n 'PostgreSQL session; confirm historical writers used UTC and SET TIME ZONE UTC';\n return `DO $$ BEGIN IF upper(current_setting('TimeZone')) NOT IN ('UTC', 'ETC/UTC', 'GMT') THEN RAISE EXCEPTION ${this.quoteLiteral(message)}; END IF; END $$`;\n }\n\n private quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n }\n\n /**\n * Plan the changes that add a missing column so the emitted DDL is\n * executable on the active engine (#2369).\n *\n * Engines disagree on what `ALTER TABLE ... ADD COLUMN` may carry inline\n * (probed on SQLite 3.4x/3.5x, DuckDB 1.4.x, PostgreSQL 16/17):\n *\n * | clause | SQLite | DuckDB | PostgreSQL |\n * |-----------------|--------------------------------|----------|---------------------|\n * | NOT NULL + DEF | ok | rejected | ok |\n * | NOT NULL, no DEF| ok on empty, rejected populated| rejected | ok empty, rej. pop. |\n * | UNIQUE | rejected | rejected | ok |\n * | DEFAULT | ok | ok | ok |\n * | CHECK | ok | rejected | ok |\n *\n * so the plan is:\n * - `NOT NULL` with a default: inline on SQLite/PostgreSQL (existing rows\n * receive the default); DuckDB adds with `DEFAULT` then a separate\n * `ALTER COLUMN ... SET NOT NULL`.\n * - `NOT NULL` without a default: enforced only when the table is empty\n * (inline on SQLite/PostgreSQL, `SET NOT NULL` step on DuckDB). On a\n * populated table there is nothing to backfill with, so the column is\n * added nullable and a manual `alter_column` (`set_not_null`) is reported\n * with the exact remediation instead of emitting DDL every engine rejects.\n * - `UNIQUE`: inline on PostgreSQL (a real constraint); SQLite/DuckDB get a\n * separate `CREATE UNIQUE INDEX <table>_<col>_key` (the PostgreSQL\n * constraint-index name, so the orphan sweep leaves it alone).\n * - `CHECK`: inline where accepted; DuckDB rejects it in ADD COLUMN, so it\n * is dropped there with a warning (SMRT's generators never emit `check`).\n *\n * The primary `add_column` change carries the whole plan in\n * `sqlStatements` (and the last statement in `sql`) so both the CLI and the\n * orchestrator execute it as one unit.\n */\n private async generateAddColumnChanges(\n tableName: string,\n colName: string,\n colDef: ColumnDefinition,\n ): Promise<SchemaChange[]> {\n // mapType maps abstract types per dialect (UUID→native uuid / TEXT — R11);\n // invalid types fall back to TEXT, matching the compareColumns guard.\n const validatedType: SQLDataType = isValidSQLDataType(colDef.type)\n ? colDef.type\n : 'TEXT';\n if (!isValidSQLDataType(colDef.type)) {\n logger.warn(\n `[SchemaComparer] Invalid manifest type \"${colDef.type}\" for ${tableName}.${colName}, treating as TEXT`,\n );\n }\n\n const quotedTable = this.quoteIdentifier(tableName);\n const quotedCol = this.quoteIdentifier(colName);\n const parts: string[] = [\n quotedCol,\n this.ddlStrategy.mapType(validatedType),\n ];\n const followUps: string[] = [];\n const extraChanges: SchemaChange[] = [];\n\n const hasDefault =\n colDef.defaultValue !== undefined && colDef.defaultValue !== null;\n const formattedDefault = hasDefault\n ? this.ddlStrategy.formatDefaultValue(colDef.defaultValue, validatedType)\n : undefined;\n const inlineConstraintsAllowed =\n this.engine !== 'duckdb' && this.engine !== 'json';\n const setNotNull = `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET NOT NULL`;\n\n let enforceNotNull = false;\n if (colDef.notNull) {\n if (hasDefault) {\n enforceNotNull = true;\n } else if (!(await this.tableHasRows(tableName))) {\n enforceNotNull = true;\n } else {\n // Populated table, no default: nothing to backfill with. Add the\n // column nullable and report the constraint as a manual follow-up.\n extraChanges.push({\n type: 'alter_column',\n table: tableName,\n name: colName,\n column: colDef,\n alteration: 'set_not_null',\n mismatch: { expected: 'NOT NULL', actual: 'NULL' },\n sql: `-- ${tableName}.${colName} is required by the manifest but has no default to backfill existing rows; the column is added nullable. Backfill it, then run: ${setNotNull}`,\n advisory: {\n severity: 'warning',\n message: `${tableName}.${colName} is required by the manifest but has no default, and ${tableName} already holds rows — no engine can add it NOT NULL. It is added nullable; backfill it, then rerun db:migrate (or declare a default on the field).`,\n suggestedSql: [\n `UPDATE ${quotedTable} SET ${quotedCol} = <value> WHERE ${quotedCol} IS NULL`,\n this.supportsAlterColumn()\n ? setNotNull\n : `-- SQLite: enforcing NOT NULL afterwards requires a table rebuild (#2370)`,\n ],\n },\n });\n }\n }\n\n if (enforceNotNull && inlineConstraintsAllowed) {\n parts.push('NOT NULL');\n }\n if (colDef.unique && this.engine === 'postgres') {\n parts.push('UNIQUE');\n }\n if (formattedDefault !== undefined) {\n parts.push(`DEFAULT ${formattedDefault}`);\n }\n if (colDef.check) {\n if (inlineConstraintsAllowed) {\n parts.push(`CHECK (${colDef.check})`);\n } else {\n logger.warn(\n `[SchemaComparer] ${this.engine} rejects CHECK constraints in ADD COLUMN; ${tableName}.${colName} is added without CHECK (${colDef.check})`,\n );\n }\n }\n\n const addColumn = `ALTER TABLE ${quotedTable} ADD COLUMN ${parts.join(' ')}`;\n\n if (enforceNotNull && !inlineConstraintsAllowed) {\n followUps.push(setNotNull);\n }\n if (colDef.unique && this.engine !== 'postgres') {\n followUps.push(\n `CREATE UNIQUE INDEX ${this.quoteIdentifier(uniqueColumnIndexName(tableName, colName))} ON ${quotedTable} (${quotedCol})`,\n );\n // DuckDB has no `ADD CONSTRAINT`, so this is the only way to add\n // uniqueness to an existing table there. The DuckDB strategy's\n // `requiresInlineUnique()` note (DuckDB #12684: ON CONFLICT ignored\n // separate unique indexes) no longer holds on the bundled DuckDB 1.4.x —\n // `INSERT … ON CONFLICT (col) DO UPDATE` resolves through this index;\n // the #2369 DuckDB test pins that. Older DuckDB builds may still need\n // the inline form for upserts on such a column.\n }\n\n const statements = [addColumn, ...followUps];\n const primary: SchemaChange = {\n type: 'add_column',\n table: tableName,\n name: colName,\n column: colDef,\n // `sql` stays the ADD COLUMN itself for single-statement consumers;\n // `sqlStatements` carries the full ordered plan when there is more.\n sql: addColumn,\n ...(statements.length > 1 ? { sqlStatements: statements } : {}),\n };\n\n return [primary, ...extraChanges];\n }\n\n /**\n * Generate SQL for dropping a column\n */\n private generateDropColumnSQL(tableName: string, colName: string): string {\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(colName)}`;\n }\n\n /**\n * Generate SQL for adding an index.\n *\n * On engines that support partial indexes (SQLite/PostgreSQL) this mirrors\n * the canonical CREATE INDEX path in the DDL strategies: a partial index\n * appends its `WHERE` predicate so a detected predicate add/alter (issue\n * #1692) recreates the index with the correct partial condition rather than\n * silently widening it to a full index. On DuckDB / the JSON adapter — which\n * reject partial indexes — the predicate is dropped so the emitted DDL stays\n * executable (a partial index degrades to a full index there). The predicate\n * is trimmed and a redundant leading `WHERE` stripped for robustness.\n *\n * `IF NOT EXISTS` matches the canonical `generateIndexes()` DDL path and\n * makes a retry after a partially applied batch repair the schema instead of\n * erroring on the indexes that did land (issue #2362). All three engines\n * (SQLite, PostgreSQL, DuckDB) accept the clause.\n */\n private generateAddIndexSQL(tableName: string, idx: IndexDefinition): string {\n const uniqueStr = idx.unique ? 'UNIQUE ' : '';\n const target = renderIndexTarget(idx, this.engine);\n let sql = `CREATE ${uniqueStr}INDEX IF NOT EXISTS ${this.quoteIdentifier(idx.name)} ON ${this.quoteIdentifier(tableName)} (${target})`;\n const where = idx.where?.trim().replace(/^WHERE\\s+/i, '');\n if (this.supportsPartialIndexes() && where) {\n sql += ` WHERE ${where}`;\n }\n return sql;\n }\n\n /**\n * Generate SQL for dropping an index.\n *\n * This SQL is consumed by the manifest-driven execution path:\n *\n * - `db:migrate` runs the SQL we put in `change.sql` directly, with the\n * tracker's `executePostgresStatements` adding CONCURRENTLY when\n * `--postgres-safe` is on.\n *\n * PostgreSQL ends up with `CONCURRENTLY` when it should.\n * Keeping this method engine-agnostic also means the diff preview text\n * stays readable (no engine-specific noise) for engines like SQLite\n * where CONCURRENTLY isn't a thing.\n */\n private generateDropIndexSQL(indexName: string): string {\n return `DROP INDEX IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n}\n\n/**\n * Generate a SchemaDiff from manifest and database\n */\nexport async function generateSchemaDiff(\n db: DatabaseInterface,\n manifestSchemas: Record<string, SchemaDefinition>,\n options: DiffOptions = {},\n): Promise<SchemaDiff> {\n const comparer = new SchemaComparer(db, options);\n return comparer.compare(manifestSchemas);\n}\n\n/**\n * Check if a diff has any actionable changes — a table to create or drop, or\n * a change with executable DDL. Incompatible type mismatches, report-only\n * advisories (orphan columns/indexes, relaxations the caller has not opted\n * into) and comment-only changes (SQLite in-place limitations, live data\n * blocking a repair) are not actionable; they surface through\n * `unactionableChanges` / the CLI's manual bucket instead.\n */\nexport function hasActionableChanges(diff: SchemaDiff): boolean {\n if (diff.added_tables.length > 0) return true;\n if (diff.dropped_tables.length > 0) return true;\n return diff.changes.some((c) => !isManualOrAdvisoryChange(c));\n}\n\n/**\n * Get SQL statements from a diff for execution. Advisory-only changes carry\n * no statements and are skipped; comment-only statements (SQLite in-place\n * limitations) pass through unchanged so callers can filter them the way\n * they already do for `type_upgrade`.\n */\nexport function getSQLFromDiff(diff: SchemaDiff): string[] {\n const statements: string[] = [];\n\n // Add table creation (requires full DDL generation - not included here)\n // This is typically handled by ensureSchema()\n\n // Add column and index changes\n for (const change of diff.changes) {\n if (change.type !== 'type_mismatch' && !isAdvisoryOnlyChange(change)) {\n statements.push(\n ...(change.sqlStatements ?? (change.sql ? [change.sql] : [])),\n );\n }\n }\n\n return statements;\n}\n"],"mappings":"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"}
|
|
@@ -49,10 +49,19 @@ export declare class MigrationGenerator {
|
|
|
49
49
|
includeDown?: boolean;
|
|
50
50
|
packageName?: string;
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* Render new tables from their structured columns through the engine's
|
|
53
|
+
* DDL strategy (default, `true`) — the same renderer `db:migrate` uses,
|
|
54
|
+
* so column types, indexes and triggers match what the differ expects
|
|
55
|
+
* on the next run.
|
|
56
|
+
*
|
|
57
|
+
* @deprecated Passing `false` re-enables the retired cached-`ddl` string
|
|
58
|
+
* path for the CREATE TABLE only (#2358): it emits the manifest's
|
|
59
|
+
* engine-neutral CREATE TABLE with only bare PostgreSQL `TIMESTAMP`
|
|
60
|
+
* rewritten, so `REAL`/`JSON` drift on PostgreSQL. The table's indexes
|
|
61
|
+
* and triggers are still rendered through the engine strategy either
|
|
62
|
+
* way — the flag never suppresses them. Schemas that expose no
|
|
63
|
+
* structured columns always fall back to their cached `ddl` regardless
|
|
64
|
+
* of this flag.
|
|
56
65
|
*/
|
|
57
66
|
materializeStructuredSchema?: boolean;
|
|
58
67
|
});
|
|
@@ -84,6 +93,16 @@ export declare class MigrationGenerator {
|
|
|
84
93
|
* Generate CREATE TABLE SQL from schema definition
|
|
85
94
|
*/
|
|
86
95
|
private generateCreateTable;
|
|
96
|
+
/**
|
|
97
|
+
* Generate the CREATE INDEX statements for a new table through the engine
|
|
98
|
+
* strategy (partial `WHERE`, JSON-path targets, inline-UNIQUE engines).
|
|
99
|
+
*/
|
|
100
|
+
private generateTableIndexes;
|
|
101
|
+
/**
|
|
102
|
+
* Generate the CREATE TRIGGER statements for a new table (engines without
|
|
103
|
+
* trigger support return none).
|
|
104
|
+
*/
|
|
105
|
+
private generateTableTriggers;
|
|
87
106
|
/**
|
|
88
107
|
* Generate DROP TABLE SQL
|
|
89
108
|
*/
|
|
@@ -116,6 +135,14 @@ export declare class MigrationGenerator {
|
|
|
116
135
|
* Format a default value for SQL
|
|
117
136
|
*/
|
|
118
137
|
private formatDefaultValue;
|
|
138
|
+
/**
|
|
139
|
+
* Terminate a statement with exactly one `;` for the SQL file format.
|
|
140
|
+
*
|
|
141
|
+
* Strategy-rendered statements (`generateCreateTable`, `generateIndexes`,
|
|
142
|
+
* `generateTriggers`) already end with `;`, while differ-produced ALTERs do
|
|
143
|
+
* not — appending unconditionally wrote `;;` (#2406 review).
|
|
144
|
+
*/
|
|
145
|
+
private terminateStatement;
|
|
119
146
|
/**
|
|
120
147
|
* Escape template literal content
|
|
121
148
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/migrations/generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/migrations/generator.ts"],"names":[],"mappings":"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"}
|