@happyvertical/smrt-core 0.51.0 → 0.51.1
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/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +126 -52
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +337 -128
- package/dist/migrations/differ.js.map +1 -1
- package/dist/schema/column-data-probes.d.ts +38 -0
- package/dist/schema/column-data-probes.d.ts.map +1 -0
- package/dist/schema/column-data-probes.js +106 -0
- package/dist/schema/column-data-probes.js.map +1 -0
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +1 -107
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/package.json +4 -4
|
@@ -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 { renderNullEqualConflictIndex } from '../schema/ddl/null-equal-index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport {\n CANONICAL_UUID_PATTERN,\n CANONICAL_UUID_SQLITE_GLOB_PATTERN,\n foreignKeyConstraintName,\n foreignKeyRelationshipKey,\n renderForeignKeyAddStatements,\n renderForeignKeyOrphanDetector,\n renderForeignKeyOrphanRepair,\n schemaForeignKeys,\n schemaForeignKeysForEngine,\n} from '../schema/foreign-key-ddl.js';\nimport {\n normalizeForeignKeyAction,\n requireForeignKeyAction,\n} from '../schema/foreign-key-policy.js';\nimport {\n maskSampleValue,\n probeCastSafety,\n renderJsonbColumnConversion,\n renderTimestamptzColumnConversion,\n type ShapeProbeResult,\n} from '../schema/text-cast-probe.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 describeForeignKeyTypeConflict,\n planUuidConvergence,\n renderUuidConvergenceRepairHint,\n renderUuidShapeProbe,\n type UuidConvergenceLiveForeignKey,\n type UuidConvergencePlan,\n} from '../schema/uuid-convergence.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 */\n/**\n * Single- vs double-precision float classification for #2770's float-width\n * drift detection. `null` for anything that isn't unambiguously one or the\n * other (DECIMAL/NUMERIC have no fixed binary width and are out of scope).\n */\nfunction floatPrecisionOf(\n type: string,\n engine: 'postgres' | 'duckdb',\n): 'single' | 'double' | null {\n const upper = type\n .toUpperCase()\n .trim()\n .replace(/\\(\\s*\\d+(\\s*,\\s*\\d+)?\\s*\\)/g, '');\n if (/^(REAL|FLOAT4)$/.test(upper)) return 'single';\n // DuckDB's information_schema normalizes REAL/FLOAT4 to the bare string\n // `FLOAT` (never `REAL`) and reports its double-precision type as\n // `DOUBLE` (never `FLOAT`) — the opposite of PostgreSQL, where bare\n // `FLOAT` never appears live and previously defaulted safely to\n // double-precision. Treating DuckDB's `FLOAT` as double-precision made\n // every converged DuckDB REAL column permanently misreport as narrowing\n // drift (review finding on #2770).\n if (engine === 'duckdb' && upper === 'FLOAT') return 'single';\n if (/^(FLOAT8|DOUBLE|DOUBLE PRECISION|FLOAT)$/.test(upper)) return 'double';\n return null;\n}\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 * Live schemas read during one `compare()` run. The uuid convergence plan\n * (#2608) needs the same introspection the per-table comparison does, and\n * re-reading `getTableSchema` per relationship would multiply catalog\n * round-trips on a wide schema.\n */\n private liveSchemas = new Map<\n string,\n SqlTableSchemaInfo | null | undefined\n >();\n /** Convergence plan for this run; `null` on non-PostgreSQL engines. */\n private uuidConvergence: UuidConvergencePlan | null = null;\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 // A comparer instance may be reused across successive compares (apply a\n // migration, then diff again). Live introspection must not be memoized\n // across those runs.\n this.liveSchemas.clear();\n\n // Get list of existing tables\n const existingTables = await this.getExistingTables();\n\n // #2608: plan the pre-R11 `text` -> `uuid` convergence before anything\n // else so its statements lead the migration stream. Every foreign key in\n // this diff — including the deferred constraints the orchestrator emits\n // for newly created tables — depends on the columns it rewrites.\n this.uuidConvergence = await this.buildUuidConvergencePlan(\n manifestSchemas,\n existingTables,\n );\n for (const change of this.uuidConvergenceChanges(manifestSchemas)) {\n diff.changes.push(change);\n if (!isInfoOnlyChange(change)) diff.has_changes = true;\n }\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(\n tableName,\n schema,\n manifestSchemas,\n );\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 manifestSchemas: Record<string, SchemaDefinition> = {},\n ): Promise<SchemaChange[]> {\n const changes: SchemaChange[] = [];\n\n // Get current table schema from database\n const dbSchema = await this.getLiveSchema(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 changes.push(\n ...(await this.compareForeignKeys(\n tableName,\n manifest,\n dbSchema,\n manifestSchemas,\n )),\n );\n\n return changes;\n }\n\n /**\n * Read (and memoize for this run) one table's live schema. Returns\n * `undefined` when the table does not exist or the adapter cannot\n * introspect it.\n */\n private async getLiveSchema(\n tableName: string,\n ): Promise<SqlTableSchemaInfo | undefined> {\n if (this.liveSchemas.has(tableName)) {\n return this.liveSchemas.get(tableName) ?? undefined;\n }\n const schema = await this.db.getTableSchema?.(tableName);\n this.liveSchemas.set(tableName, schema);\n return schema ?? undefined;\n }\n\n /**\n * Plan the pre-R11 `text` -> `uuid` convergence (#2608).\n *\n * PostgreSQL only: SQLite stores UUIDs as text by design and DuckDB cannot\n * rewrite a column type in place, so both engines return `null` and emit\n * nothing for this drift.\n */\n private async buildUuidConvergencePlan(\n manifestSchemas: Record<string, SchemaDefinition>,\n existingTables: Set<string>,\n ): Promise<UuidConvergencePlan | null> {\n if (this.engine !== 'postgres') return null;\n\n for (const tableName of Object.keys(manifestSchemas)) {\n if (!existingTables.has(tableName)) continue;\n await this.getLiveSchema(tableName);\n }\n\n const relationships = Object.entries(manifestSchemas).flatMap(\n ([tableName, schema]) =>\n schemaForeignKeysForEngine(schema, 'postgres').map((foreignKey) => ({\n childTable: tableName,\n childColumn: foreignKey.column,\n parentTable: foreignKey.referencesTable,\n parentColumn: foreignKey.referencesColumn,\n })),\n );\n\n // Columns this plan could rewrite: a live `text` side of a relationship\n // the manifest declares UUID on both ends.\n const conversionCandidates = new Set<string>();\n for (const relationship of relationships) {\n const childManifest =\n manifestSchemas[relationship.childTable]?.columns[\n relationship.childColumn\n ];\n const parentManifest =\n manifestSchemas[relationship.parentTable]?.columns[\n relationship.parentColumn\n ];\n if (childManifest?.type !== 'UUID' || parentManifest?.type !== 'UUID') {\n continue;\n }\n for (const [table, column] of [\n [relationship.childTable, relationship.childColumn],\n [relationship.parentTable, relationship.parentColumn],\n ] as const) {\n if (!existingTables.has(table)) continue;\n const live = this.liveSchemas.get(table)?.columns[column];\n if (!live) continue;\n if (this.normalizeType(live.type) !== 'TEXT') continue;\n conversionCandidates.add(`${table} ${column}`);\n }\n }\n\n // #2608: a table the manifest no longer declares can still hold a live\n // foreign key onto a column this plan would rewrite. PostgreSQL refuses\n // `ALTER COLUMN … TYPE` while any constraint depends on the column, so an\n // uninspected orphan table turns the promised \"blocked\" advisory into an\n // aborted migration. Introspect those tables too — but only when there is\n // actually a conversion to protect, so an already-converged database pays\n // nothing.\n if (conversionCandidates.size > 0) {\n for (const tableName of existingTables) {\n if (manifestSchemas[tableName]) continue;\n if (tableName.startsWith('sqlite_')) continue;\n await this.getLiveSchema(tableName);\n }\n }\n\n const liveForeignKeys: UuidConvergenceLiveForeignKey[] = [];\n for (const [tableName, schema] of this.liveSchemas) {\n for (const live of schema?.foreignKeys ?? []) {\n liveForeignKeys.push({\n table: tableName,\n column: live.column,\n referencesTable: live.referencesTable,\n referencesColumn: live.referencesColumn,\n });\n }\n }\n\n return planUuidConvergence({\n manifestSchemas,\n relationships,\n liveForeignKeys,\n getLiveColumn: (table, column) => {\n if (!existingTables.has(table)) return undefined;\n const live = this.liveSchemas.get(table)?.columns[column];\n if (!live) return undefined;\n return {\n normalizedType: this.normalizeType(live.type),\n rawType: live.type,\n hasDefault:\n live.defaultValue !== null && live.defaultValue !== undefined,\n exists: true,\n };\n },\n probeUuidShape: (table, column) => this.probeUuidShape(table, column),\n });\n }\n\n /**\n * Count values a legacy `text` identifier column holds that cannot be\n * reinterpreted as `uuid`. Any failure resolves to `unavailable`, which the\n * planner treats as a block — SMRT never coerces identifier values.\n */\n private async probeUuidShape(\n tableName: string,\n columnName: string,\n ): Promise<\n | { status: 'clean' }\n | { status: 'dirty'; count: number; sample?: string }\n | { status: 'unavailable'; reason: string }\n > {\n try {\n const result = await this.db.query(\n renderUuidShapeProbe(tableName, columnName),\n );\n const rows = (Array.isArray(result) ? result : result.rows || []) as {\n invalid_count?: unknown;\n sample_value?: unknown;\n }[];\n const count = Number(rows[0]?.invalid_count ?? 0);\n if (!Number.isFinite(count)) {\n return {\n status: 'unavailable',\n reason: 'probe returned a non-numeric count',\n };\n }\n if (count === 0) return { status: 'clean' };\n const sample = rows[0]?.sample_value;\n return {\n status: 'dirty',\n count,\n ...(typeof sample === 'string' ? { sample } : {}),\n };\n } catch (error) {\n return {\n status: 'unavailable',\n reason: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n /**\n * Probe a candidate `text` -> `timestamptz`/`jsonb` column (#2771, #2772)\n * with a real, exception-safe cast attempt (see `text-cast-probe.ts`).\n * Unlike {@link probeUuidShape}, a failed/unrealistic probe\n * (`unavailable`) is the caller's cue to fall back to the pre-existing\n * behavior for that pair rather than surface a new finding — this keeps\n * the new convergence strictly additive for every case that isn't\n * affirmatively confirmed safe or affirmatively confirmed unsafe.\n */\n private async probeTextCastShape(\n tableName: string,\n columnName: string,\n kind: 'timestamptz' | 'jsonb',\n ): Promise<ShapeProbeResult> {\n return probeCastSafety(this.db, tableName, columnName, kind);\n }\n\n /**\n * Turn the convergence plan into diff entries: executable `type_upgrade`\n * conversions marked `phase: 'pre_foreign_key'`, plus report-only warnings\n * for components the planner refused.\n */\n private uuidConvergenceChanges(\n manifestSchemas: Record<string, SchemaDefinition>,\n ): SchemaChange[] {\n const plan = this.uuidConvergence;\n if (!plan) return [];\n // Every consumer of a `type_upgrade` entry reads `change.column` for the\n // target shape — `partitionSchemaChanges()` in `@happyvertical/smrt-cli`\n // skips an entry without one, which would drop these conversions out of\n // the `db:migrate` batch while `compareForeignKeys()` still assumed their\n // converged type and emitted the dependent constraint (SQLSTATE 42804).\n const manifestColumn = (table: string, column: string): ColumnDefinition =>\n manifestSchemas[table]?.columns[column] ?? { type: 'UUID' };\n const changes: SchemaChange[] = [];\n for (const conversion of plan.conversions) {\n changes.push({\n type: 'type_upgrade',\n table: conversion.table,\n name: conversion.column,\n column: manifestColumn(conversion.table, conversion.column),\n phase: 'pre_foreign_key',\n mismatch: { expected: 'UUID', actual: conversion.liveType },\n sql: conversion.statements[0],\n sqlStatements: conversion.statements,\n });\n }\n for (const block of plan.blocks) {\n const [first] = block.targets.length > 0 ? block.targets : block.nodes;\n changes.push({\n type: 'type_upgrade',\n table: first?.table ?? '',\n name: first?.column,\n column: manifestColumn(first?.table ?? '', first?.column ?? ''),\n phase: 'pre_foreign_key',\n mismatch: { expected: 'UUID', actual: 'mixed uuid/text' },\n advisory: {\n severity: 'warning',\n message:\n 'blocked: incompatible column types. The manifest declares UUID for ' +\n `${block.nodes\n .map((node) => `${node.table}.${node.column}`)\n .join(', ')}, but ${block.reason}.`,\n suggestedSql: block.suggestedSql,\n },\n });\n }\n return changes;\n }\n\n /**\n * Live-type verdict for one foreign key once this run's conversions land.\n * `undefined` means the pair is compatible (or unknowable) and the ordinary\n * add path applies.\n */\n private async describeForeignKeyTypeBlock(\n tableName: string,\n dbSchema: SqlTableSchemaInfo,\n foreignKey: import('../schema/types.js').ForeignKeyDefinition,\n ): Promise<string | undefined> {\n if (this.engine !== 'postgres') return undefined;\n const parentSchema =\n foreignKey.referencesTable === tableName\n ? dbSchema\n : await this.getLiveSchema(foreignKey.referencesTable);\n const childType = dbSchema.columns[foreignKey.column]?.type;\n const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;\n // A column this migration is about to add materializes with the manifest\n // type, so there is nothing live to conflict with yet.\n if (!childType || !parentType) return undefined;\n\n const plan = this.uuidConvergence;\n const childEffective =\n plan?.effectiveType(tableName, foreignKey.column) ??\n this.normalizeType(childType);\n const parentEffective =\n plan?.effectiveType(\n foreignKey.referencesTable,\n foreignKey.referencesColumn,\n ) ?? this.normalizeType(parentType);\n if (childEffective === parentEffective) return undefined;\n\n return describeForeignKeyTypeConflict({\n childTable: tableName,\n childColumn: foreignKey.column,\n childType,\n parentTable: foreignKey.referencesTable,\n parentColumn: foreignKey.referencesColumn,\n parentType,\n });\n }\n\n private async compareForeignKeys(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n manifestSchemas: Record<string, SchemaDefinition>,\n ): Promise<SchemaChange[]> {\n const changes: SchemaChange[] = [];\n const liveForeignKeys = dbSchema.foreignKeys || [];\n const declaredForeignKeys = schemaForeignKeys(manifest);\n const enabledForeignKeys = schemaForeignKeysForEngine(\n manifest,\n this.engine,\n );\n const enabledKeys = new Set(\n enabledForeignKeys.map(foreignKeyRelationshipKey),\n );\n const disabledForeignKeys = declaredForeignKeys.filter(\n (foreignKey) => !enabledKeys.has(foreignKeyRelationshipKey(foreignKey)),\n );\n\n for (const foreignKey of disabledForeignKeys) {\n const live = liveForeignKeys.find(\n (candidate) =>\n foreignKeyRelationshipKey(candidate) ===\n foreignKeyRelationshipKey(foreignKey),\n );\n if (!live) continue;\n\n const constraintName = foreignKeyConstraintName(tableName, foreignKey);\n if (this.engine === 'postgres') {\n changes.push({\n type: 'drop_foreign_key',\n table: tableName,\n name: constraintName,\n foreignKey,\n advisory: {\n severity: 'warning',\n message:\n `Cannot safely remove the now-disabled physical foreign key ${tableName}.${foreignKey.column}: ` +\n '@happyvertical/sql schema introspection does not expose the live PostgreSQL constraint name. Drop the live constraint deliberately, then rerun the migration.',\n },\n });\n } else {\n changes.push({\n type: 'drop_foreign_key',\n table: tableName,\n name: constraintName,\n foreignKey,\n advisory: {\n severity: 'warning',\n message:\n `${this.engine === 'sqlite' ? 'SQLite' : 'DuckDB'} requires a table rebuild to remove the now-disabled physical foreign key ` +\n `${tableName}.${foreignKey.column}. Rebuild the table from the current manifest; relationship metadata and application-side enforcement remain active.`,\n },\n });\n }\n }\n\n for (const foreignKey of enabledForeignKeys) {\n const expectedDelete =\n foreignKey.onDelete === undefined\n ? 'NO ACTION'\n : requireForeignKeyAction(\n foreignKey.onDelete,\n `${tableName}.${foreignKey.column} ON DELETE`,\n );\n const expectedUpdate =\n foreignKey.onUpdate === undefined\n ? 'NO ACTION'\n : requireForeignKeyAction(\n foreignKey.onUpdate,\n `${tableName}.${foreignKey.column} ON UPDATE`,\n );\n const exact = liveForeignKeys.some(\n (live) =>\n live.column === foreignKey.column &&\n live.referencesTable === foreignKey.referencesTable &&\n live.referencesColumn === foreignKey.referencesColumn &&\n (normalizeForeignKeyAction(live.onDelete) || 'NO ACTION') ===\n expectedDelete &&\n (normalizeForeignKeyAction(live.onUpdate) || 'NO ACTION') ===\n expectedUpdate,\n );\n if (exact) continue;\n\n const orphanOptions = await this.getForeignKeyOrphanOptions(\n tableName,\n manifest,\n dbSchema,\n foreignKey,\n manifestSchemas,\n );\n\n const detectorSql = renderForeignKeyOrphanDetector(\n tableName,\n foreignKey,\n {\n engine: this.engine,\n uuidComparison: orphanOptions.uuidComparison,\n uuidCastSide: orphanOptions.uuidCastSide,\n },\n );\n const repairSql = renderForeignKeyOrphanRepair(tableName, foreignKey, {\n engine: this.engine,\n uuidComparison: orphanOptions.uuidComparison,\n uuidCastSide: orphanOptions.uuidCastSide,\n nullable: orphanOptions.nullable,\n });\n const sameColumn = liveForeignKeys.some(\n (live) => live.column === foreignKey.column,\n );\n if (sameColumn) {\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: foreignKeyConstraintName(tableName, foreignKey),\n foreignKey,\n advisory: {\n severity: 'warning',\n message:\n `Foreign key ${tableName}.${foreignKey.column} exists with a different target or action. ` +\n 'Drop the old constraint deliberately, repair any orphan rows, then rerun the migration.',\n suggestedSql: [detectorSql, repairSql],\n },\n });\n continue;\n }\n\n if (this.engine !== 'postgres') {\n const engineReason =\n this.engine === 'sqlite'\n ? 'SQLite requires a table rebuild to add a foreign key to an existing table.'\n : 'DuckDB does not support ALTER TABLE ADD CONSTRAINT.';\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: foreignKeyConstraintName(tableName, foreignKey),\n foreignKey,\n engineUnsupported: true,\n advisory: {\n severity: 'warning',\n message: `${engineReason} Run the orphan detector, repair rows, and rebuild the table with the generated constraint.`,\n suggestedSql: [detectorSql, repairSql],\n },\n });\n continue;\n }\n\n // #2608: refuse — and *report* — a constraint whose live child and\n // parent columns have different physical types and that this run's\n // uuid convergence will not repair. PostgreSQL rejects such an ADD\n // CONSTRAINT with SQLSTATE 42804, so calling it \"pending\" in\n // `db:status` promises a migration that cannot succeed.\n const typeBlock = await this.describeForeignKeyTypeBlock(\n tableName,\n dbSchema,\n foreignKey,\n );\n if (typeBlock) {\n // The uuid convergence is only the right remediation when the\n // manifest actually declares UUID on both sides. Any other\n // incompatible pair (text/integer, timestamp/text, …) reaches this\n // branch too, and telling the operator to cast both columns to uuid\n // there would be wrong.\n const uuidRelationship =\n manifest.columns[foreignKey.column]?.type === 'UUID' &&\n manifestSchemas[foreignKey.referencesTable]?.columns[\n foreignKey.referencesColumn\n ]?.type === 'UUID';\n const textNodes = uuidRelationship\n ? [\n this.normalizeType(\n dbSchema.columns[foreignKey.column]?.type ?? '',\n ) === 'TEXT'\n ? { table: tableName, column: foreignKey.column }\n : undefined,\n this.normalizeType(\n (foreignKey.referencesTable === tableName\n ? dbSchema\n : this.liveSchemas.get(foreignKey.referencesTable)\n )?.columns[foreignKey.referencesColumn]?.type ?? '',\n ) === 'TEXT'\n ? {\n table: foreignKey.referencesTable,\n column: foreignKey.referencesColumn,\n }\n : undefined,\n ].filter((node) => node !== undefined)\n : [];\n const suggestedSql = renderUuidConvergenceRepairHint(textNodes);\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: foreignKeyConstraintName(tableName, foreignKey),\n foreignKey,\n advisory: {\n severity: 'warning',\n message:\n `blocked: incompatible column types. ${typeBlock}. ` +\n (textNodes.length > 0\n ? 'Converge the columns to UUID, then rerun the migration.'\n : 'Align both columns on one physical type deliberately, then rerun the migration.'),\n ...(suggestedSql.length > 0 ? { suggestedSql } : {}),\n },\n });\n continue;\n }\n\n // A missing child column is added earlier in this same table diff, so\n // probing the pre-migration schema would fail and be misclassified as\n // an orphan. The subsequent NOT VALID + VALIDATE statements remain the\n // authoritative safety check once the prerequisite column exists.\n const childColumnExists = Boolean(dbSchema.columns[foreignKey.column]);\n if (\n childColumnExists &&\n (await this.foreignKeyHasOrphans(tableName, foreignKey, orphanOptions))\n ) {\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: foreignKeyConstraintName(tableName, foreignKey),\n foreignKey,\n orphanBlocked: true,\n orphanNullable: orphanOptions.nullable,\n advisory: {\n severity: 'warning',\n message:\n `Cannot add foreign key ${tableName}.${foreignKey.column}: existing rows do not match ` +\n `${foreignKey.referencesTable}.${foreignKey.referencesColumn}. Repair them, then rerun.`,\n suggestedSql: [detectorSql, repairSql],\n },\n });\n continue;\n }\n\n const constraintName = foreignKeyConstraintName(tableName, foreignKey);\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: constraintName,\n foreignKey,\n sqlStatements: renderForeignKeyAddStatements(tableName, foreignKey),\n });\n }\n return changes;\n }\n\n private async foreignKeyHasOrphans(\n tableName: string,\n foreignKey: import('../schema/types.js').ForeignKeyDefinition,\n options: {\n uuidComparison: boolean;\n uuidCastSide?: 'child' | 'parent' | 'both';\n },\n ): Promise<boolean> {\n try {\n const result = await this.db.query(\n renderForeignKeyOrphanDetector(tableName, foreignKey, {\n engine: this.engine,\n limitOne: true,\n uuidComparison: options.uuidComparison,\n uuidCastSide: options.uuidCastSide,\n }),\n );\n const rows = Array.isArray(result) ? result : result.rows || [];\n return rows.length > 0;\n } catch (error) {\n throw new Error(\n `[SchemaComparer] Cannot probe ${tableName}.${foreignKey.column} for orphan rows; refusing automatic constraint addition: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n private async getForeignKeyOrphanOptions(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n foreignKey: import('../schema/types.js').ForeignKeyDefinition,\n manifestSchemas: Record<string, SchemaDefinition>,\n ): Promise<{\n nullable: boolean;\n uuidComparison: boolean;\n uuidCastSide?: 'child' | 'parent' | 'both';\n }> {\n const sourceColumn = manifest.columns[foreignKey.column];\n const targetColumn =\n manifestSchemas[foreignKey.referencesTable]?.columns[\n foreignKey.referencesColumn\n ];\n // #2748 review: nullability for the orphan disposition (does the\n // null-out repair even apply?) must reflect the LIVE column, not only\n // the manifest. A manifest that now declares the column nullable while\n // the live column is still physically NOT NULL (nullability relaxation\n // pending, not yet applied — the same drift `--relax-columns` handles\n // for plain columns) would otherwise report `nullable: true` and let\n // `--null-orphans` attempt an UPDATE that PostgreSQL rejects outright,\n // failing the whole atomic batch instead of refusing this one\n // relationship. Nullable only when BOTH sides agree.\n const manifestNullable = sourceColumn?.notNull !== true;\n const liveNotNull = dbSchema.columns[foreignKey.column]?.notNull === true;\n const nullable = manifestNullable && !liveNotNull;\n const uuidComparison =\n sourceColumn?.type === 'UUID' &&\n (targetColumn === undefined || targetColumn.type === 'UUID');\n if (!uuidComparison) return { nullable, uuidComparison };\n\n const parentSchema =\n foreignKey.referencesTable === tableName\n ? dbSchema\n : await this.getLiveSchema(foreignKey.referencesTable);\n const childType = dbSchema.columns[foreignKey.column]?.type;\n const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;\n if (!childType || !parentType) return { nullable, uuidComparison };\n\n const childTypeNormalized = this.normalizeType(childType);\n const parentTypeNormalized = this.normalizeType(parentType);\n if (childTypeNormalized === parentTypeNormalized) {\n return { nullable, uuidComparison: false };\n }\n\n if (childTypeNormalized === 'UUID') {\n return { nullable, uuidComparison, uuidCastSide: 'parent' };\n }\n if (parentTypeNormalized === 'UUID') {\n return { nullable, uuidComparison, uuidCastSide: 'child' };\n }\n return { nullable, uuidComparison, uuidCastSide: 'both' };\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 // #2770: REAL/DOUBLE PRECISION both normalize to the same 'REAL'\n // bucket above (matching DECIMAL/NUMERIC tolerance), so the general\n // equality gate below never sees single- vs double-precision float\n // drift. Detect it here, the same way `legacy_integer_width` detects\n // int4-vs-int8 drift the general INTEGER bucket also hides. Widening\n // (float4 -> float8) is lossless and safe to auto-plan; narrowing\n // (float8 -> float4) can lose precision, so it stays advisory-only.\n if (\n (this.engine === 'postgres' || this.engine === 'duckdb') &&\n normalizedExpected === 'REAL' &&\n normalizedActual === 'REAL'\n ) {\n const expectedPrecision = floatPrecisionOf(\n expectedEngineType,\n this.engine,\n );\n const actualPrecision = floatPrecisionOf(dbCol.type, this.engine);\n if (\n expectedPrecision &&\n actualPrecision &&\n expectedPrecision !== actualPrecision\n ) {\n typeDrifted = true;\n const table = this.quoteIdentifier(tableName);\n const column = this.quoteIdentifier(colName);\n if (\n expectedPrecision === 'double' &&\n actualPrecision === 'single'\n ) {\n const targetType =\n this.engine === 'postgres' ? 'DOUBLE PRECISION' : 'DOUBLE';\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: expectedEngineType, actual: dbCol.type },\n sql: `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${targetType} USING ${column}::${targetType}`,\n });\n } else {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: expectedEngineType, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} is declared single-precision ` +\n `(${expectedEngineType}) but the live column is double-precision ` +\n `(${dbCol.type}). Narrowing loses precision, so this stays manual: ` +\n 'confirm the narrower declaration is intentional, or widen the ' +\n 'manifest field instead of the column.',\n suggestedSql: [\n `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${expectedEngineType} USING ${column}::${expectedEngineType}`,\n ],\n },\n });\n }\n }\n }\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 // #2772: on PostgreSQL, a manifest-declared JSON column backed by a\n // live `text` column is a real, repairable drift on a table SMRT\n // itself owns — not the enum-mis-inferred canary the tolerance below\n // exists for. Probe first (never trust the manifest's intent over\n // the live data): a clean probe converts the tolerance below into an\n // executable `type_upgrade`; a dirty probe converts it into a\n // visible, fail-closed advisory instead of staying silent. An\n // unavailable probe (missing table mid-run, a test double without a\n // realistic response) preserves the pre-existing silent tolerance —\n // this feature never invents a new finding it cannot back with data.\n const jsonUpgradeCandidate =\n this.engine === 'postgres' &&\n normalizedExpected === 'JSON' &&\n normalizedActual === 'TEXT';\n let jsonProbe: ShapeProbeResult | undefined;\n if (jsonUpgradeCandidate) {\n jsonProbe = await this.probeTextCastShape(\n tableName,\n colName,\n 'jsonb',\n );\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') — tolerated only\n // when the #2772 probe above could not affirmatively clear or\n // convict the column (see `jsonUpgradeCandidate` above).\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 === 'TEXT' && normalizedActual === 'JSON') ||\n (normalizedExpected === 'JSON' &&\n normalizedActual === 'TEXT' &&\n (!jsonUpgradeCandidate || jsonProbe?.status === 'unavailable'));\n\n // #2771: shape-probe a manifest TIMESTAMP column (-> TIMESTAMPTZ on\n // PostgreSQL) backed by a live `text` column, independent of the\n // `postgresTimestampMigration` opt-in (which exists for genuinely\n // ambiguous naive wall-clock strings). Every value SMRT itself ever\n // wrote carries an explicit UTC/offset designator, so a clean probe\n // here converts losslessly with no operator confirmation needed.\n const timestamptzUpgradeCandidate =\n this.engine === 'postgres' &&\n // `normalizedExpected` is the MAPPED engine bucket ('TIMESTAMPTZ'\n // on PostgreSQL); `isCompatibleTypeUpgrade` below keys off the RAW\n // abstract manifest bucket ('TIMESTAMP') instead, so this checks\n // the same raw bucket to stay consistent with that gate.\n this.normalizeType(colDef.type) === 'TIMESTAMP' &&\n normalizedActual === 'TEXT' &&\n normalizedExpected === 'TIMESTAMPTZ' &&\n !this.isCompatibleTypeUpgrade(colDef.type, dbCol.type);\n let timestamptzProbe: ShapeProbeResult | undefined;\n if (timestamptzUpgradeCandidate) {\n timestamptzProbe = await this.probeTextCastShape(\n tableName,\n colName,\n 'timestamptz',\n );\n }\n\n if (\n normalizedExpected !== normalizedActual &&\n !isUuidTextEquivalent &&\n !isJsonTextEquivalent\n ) {\n typeDrifted = true;\n\n // #2771/#2772 review finding: PostgreSQL rejects `ALTER COLUMN\n // ... TYPE` outright whenever the column has ANY existing default\n // that can't auto-cast to the target type -- regardless of\n // whether the manifest itself wants a default -- so DROP DEFAULT\n // must be gated on the LIVE default, not the manifest's. SET\n // DEFAULT afterward stays gated on the manifest default only: a\n // live-only default the manifest no longer declares must not be\n // silently resurrected.\n const hasLiveDefault =\n dbCol.defaultValue !== null && dbCol.defaultValue !== undefined;\n const conversionOptions = {\n hasLiveDefault,\n manifestDefaultValue: colDef.defaultValue,\n };\n\n // Review finding: dropping a live default the manifest no longer\n // declares is the same \"relaxation\" `compareColumnConstraints`\n // already gates behind `relaxColumns` elsewhere (see the\n // `dbHasDefault` branch above) — auto-executing it here as a side\n // effect of the type conversion would silently weaken the column\n // with no advisory and no opt-in. When there's no manifest\n // default to restore and the operator hasn't opted into\n // relaxation, surface it the same way a dirty probe does: a\n // fail-closed advisory naming the blocker, with the would-be SQL\n // attached for visibility, instead of executing.\n const liveOnlyDefaultNeedsRelaxOptIn =\n hasLiveDefault &&\n colDef.defaultValue === undefined &&\n !this.options.relaxColumns;\n\n if (\n jsonUpgradeCandidate &&\n jsonProbe?.status === 'clean' &&\n liveOnlyDefaultNeedsRelaxOptIn\n ) {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} has a live default ` +\n `(${String(dbCol.defaultValue)}) the manifest no longer ` +\n 'declares; converging to jsonb requires dropping it ' +\n `first. ${this.relaxHint('drop it as part of this conversion')}`,\n suggestedSql: renderJsonbColumnConversion(\n tableName,\n colName,\n conversionOptions,\n ),\n },\n });\n } else if (jsonUpgradeCandidate && jsonProbe?.status === 'clean') {\n const statements = renderJsonbColumnConversion(\n tableName,\n colName,\n conversionOptions,\n );\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n sql: statements[statements.length - 1],\n sqlStatements: statements,\n });\n } else if (jsonUpgradeCandidate && jsonProbe?.status === 'dirty') {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} is declared JSON but ${jsonProbe.count} ` +\n `live value(s) are not valid JSON (sample: ${\n jsonProbe.sample\n ? maskSampleValue(jsonProbe.sample)\n : 'unavailable'\n }). Repair or clear the offending value(s), then rerun ` +\n '`smrt db:migrate`.',\n suggestedSql: renderJsonbColumnConversion(\n tableName,\n colName,\n conversionOptions,\n ),\n },\n });\n } else if (\n timestamptzUpgradeCandidate &&\n timestamptzProbe?.status === 'clean' &&\n liveOnlyDefaultNeedsRelaxOptIn\n ) {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} has a live default ` +\n `(${String(dbCol.defaultValue)}) the manifest no longer ` +\n 'declares; converging to timestamptz requires dropping ' +\n `it first. ${this.relaxHint('drop it as part of this conversion')}`,\n suggestedSql: renderTimestamptzColumnConversion(\n tableName,\n colName,\n conversionOptions,\n ),\n },\n });\n } else if (\n timestamptzUpgradeCandidate &&\n timestamptzProbe?.status === 'clean'\n ) {\n const statements = renderTimestamptzColumnConversion(\n tableName,\n colName,\n conversionOptions,\n );\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n sql: statements[statements.length - 1],\n sqlStatements: statements,\n });\n } else if (\n timestamptzUpgradeCandidate &&\n timestamptzProbe?.status === 'dirty'\n ) {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} is declared TIMESTAMP but ${timestamptzProbe.count} ` +\n `live value(s) do not parse as an unambiguous timestamp (sample: ${\n timestamptzProbe.sample\n ? maskSampleValue(timestamptzProbe.sample)\n : 'unavailable'\n }). Repair the offending value(s), or confirm legacy naive ` +\n 'wall-clock provenance with `smrt db:migrate --postgres-timestamp-legacy-timezone=UTC`, then rerun.',\n suggestedSql: renderTimestamptzColumnConversion(\n tableName,\n colName,\n conversionOptions,\n ),\n },\n });\n } else 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 changes.push(\n ...(await this.detectRenameDataPending(tableName, manifest, dbSchema)),\n );\n\n return changes;\n }\n\n /**\n * Detect a pending rename backfill (#2752): a manifest-declared column\n * that exists live but holds no data, paired with an orphan (undeclared)\n * live column of a compatible type that does hold data — the shape a\n * framework field rename leaves behind when `db:migrate` adds the new\n * column additively and never moves data into it. For each such pair,\n * emit the copy-then-drop repair as an advisory: never executed,\n * PostgreSQL and SQLite only (DuckDB/JSON are out of scope). The repair\n * is genuinely idempotent SQL on PostgreSQL (a self-guarding\n * `DO $$ ... $$` block); SQLite has no conditional-DDL construct, so its\n * repair is operator-mediated instead — a guard query plus instructions,\n * not a blind-rerun-safe statement. See {@link describeRenameDataPending}.\n */\n private async detectRenameDataPending(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n ): Promise<SchemaChange[]> {\n if (this.engine !== 'postgres' && this.engine !== 'sqlite') return [];\n\n const dbColumnNames = new Set(Object.keys(dbSchema.columns));\n const manifestColumnNames = new Set(Object.keys(manifest.columns));\n const orphanColumnNames = [...dbColumnNames].filter(\n (name) => !manifestColumnNames.has(name),\n );\n if (orphanColumnNames.length === 0) return [];\n\n // Declared columns that exist live and are therefore candidates for\n // \"still empty, data may be in an orphan column\" (#2874 review: only\n // these, plus the orphan columns themselves, ever need a live-data\n // probe — never the full manifest).\n const declaredCandidateNames = Object.keys(manifest.columns).filter(\n (colName) => dbSchema.columns[colName],\n );\n if (declaredCandidateNames.length === 0) return [];\n\n // #2874: this used to run one `SELECT 1 ... LIMIT 1` round trip per\n // declared column, then (for the declared columns still empty) one more\n // per type-compatible orphan column, then a third per UUID-shape check —\n // O(declared columns × orphan columns) round trips for a single table.\n // Across a realistic schema (dozens of tables, a handful of legacy\n // columns each) that is thousands of individually sub-millisecond round\n // trips per schema comparison — the statement count dominates, not any\n // one query's execution time (see #2874, matching the #2815 epic's\n // central finding).\n //\n // Two phases below, mirroring the original code's own two gates\n // (#2874 review findings F3 and its residual on the third pass): phase\n // one probes only the declared candidates. A table where every declared\n // candidate already holds data — the ordinary, healthy case — returns\n // here without ever touching an orphan column, exactly like the\n // original `if (declaredHasData) continue;` gate. Only when some\n // declared candidate is confirmed empty does phase two batch-probe the\n // orphan columns type-compatible with *those* empty candidates — an\n // orphan whose type matches nothing (or only already-populated\n // candidates) would either never produce a finding or never even be\n // reachable, so probing it would force a full-table scan (the `LIMIT 1`\n // subquery never finds a qualifying row) for nothing. Each phase is one\n // row of scalar subqueries with each column's own `LIMIT 1` early exit\n // (#2874 review finding F1), so the common case costs one round trip\n // and the rename-pending case costs two — both far below the original\n // per-column O(declared × orphan) shape. `columnsHaveNonEmptyValueBatch`\n // falls back to isolated per-column probes on a batch failure, so one\n // unresolvable column never discards the whole table's detection\n // (#2874 review finding F2) — a column absent from a map below means\n // \"could not be probed\", not \"confirmed empty\".\n const declaredHasData = await this.columnsHaveNonEmptyValueBatch(\n tableName,\n declaredCandidateNames,\n );\n const emptyDeclaredNames = declaredCandidateNames.filter(\n (colName) => !(declaredHasData.get(colName) ?? true),\n );\n if (emptyDeclaredNames.length === 0) return [];\n\n // Only probe an orphan column that is type-compatible with at least one\n // *empty* declared candidate — restores the original per-column code's\n // compatibility gate, narrowed to the columns phase one actually found\n // empty. Costs no extra round trip: every type involved is already\n // known from `manifest`/`dbSchema`, not the live data.\n const compatibleOrphanNames = orphanColumnNames.filter((orphanName) => {\n const orphanNormalized = this.normalizeType(\n dbSchema.columns[orphanName].type,\n );\n return emptyDeclaredNames.some((colName) => {\n const validatedType: SQLDataType = isValidSQLDataType(\n manifest.columns[colName].type,\n )\n ? manifest.columns[colName].type\n : 'TEXT';\n const declaredNormalized = this.normalizeType(\n this.ddlStrategy.mapType(validatedType),\n );\n const requiresShapeCheck =\n validatedType === 'UUID' && orphanNormalized === 'TEXT';\n return requiresShapeCheck || orphanNormalized === declaredNormalized;\n });\n });\n if (compatibleOrphanNames.length === 0) return [];\n\n const orphanHasData = await this.columnsHaveNonEmptyValueBatch(\n tableName,\n compatibleOrphanNames,\n );\n const hasData = new Map([...declaredHasData, ...orphanHasData]);\n\n const changes: SchemaChange[] = [];\n\n // First pass (no queries): for every declared column still empty,\n // collect its type-compatible orphan candidates and note which ones\n // need the UUID-shape probe. The shape probe is a property of the\n // *orphan column* alone (not of the declared/orphan pair), so collecting\n // every table's shape-check candidates once, across every declared\n // column, keeps the follow-up batch query to one per table too.\n type PendingCandidate = {\n colName: string;\n orphanName: string;\n isUuidCast: boolean;\n requiresShapeCheck: boolean;\n };\n const pending: PendingCandidate[] = [];\n const shapeCheckOrphans = new Set<string>();\n\n for (const [colName, colDef] of Object.entries(manifest.columns)) {\n const dbCol = dbSchema.columns[colName];\n if (!dbCol) continue; // add_column already covers a missing declared column\n // Absent from the map means the probe could not run for this column\n // (#2874 review finding F2); default to \"has data\" so it is skipped,\n // matching the original per-column `catch { continue; }` — a probe\n // failure means \"cannot confirm empty\", not \"assume empty and guess\".\n if (hasData.get(colName) ?? true) continue;\n\n const validatedType: SQLDataType = isValidSQLDataType(colDef.type)\n ? colDef.type\n : 'TEXT';\n // Branch on the manifest's *logical* type, not the engine-mapped one:\n // SQLite has no native uuid type, so `mapType('UUID')` collapses to\n // TEXT there and `declaredNormalized` alone can no longer distinguish\n // \"this is logically a UUID column\" from \"this is plain text\" (#2767\n // review). Checking `isLogicalUuid` directly keeps the shape probe in\n // scope for SQLite too, instead of silently downgrading to an\n // unvalidated same-type text copy.\n const isLogicalUuid = validatedType === 'UUID';\n const declaredNormalized = this.normalizeType(\n this.ddlStrategy.mapType(validatedType),\n );\n\n for (const orphanName of compatibleOrphanNames) {\n const orphanCol = dbSchema.columns[orphanName];\n const orphanNormalized = this.normalizeType(orphanCol.type);\n\n // Two independent questions, deliberately not conflated (#2767\n // review, P1): does this pairing need the UUID *shape probe*, and\n // does the emitted repair SQL need the `::uuid` *cast*? A logical\n // UUID column paired with a text-typed orphan always needs the\n // shape probe, on every engine — but only a physically native uuid\n // destination (PostgreSQL/DuckDB; `declaredNormalized === 'UUID'`)\n // needs `::uuid` and the NULL-only empty predicate. SQLite has no\n // native uuid type, so its logical-UUID column is still physically\n // TEXT and must get the same plain copy + `CAST(...) = ''` predicate\n // as any other TEXT column, even though it still needed the probe.\n const requiresShapeCheck = isLogicalUuid && orphanNormalized === 'TEXT';\n const isSameType = orphanNormalized === declaredNormalized;\n if (!requiresShapeCheck && !isSameType) continue;\n // Absent from the map (probe failure) defaults to \"no data\" here,\n // matching the original per-orphan `catch { continue; }`: this\n // specific candidate is excluded, without affecting any other\n // orphan or declared column (#2874 review finding F2).\n if (!(hasData.get(orphanName) ?? false)) continue;\n\n if (requiresShapeCheck) shapeCheckOrphans.add(orphanName);\n pending.push({\n colName,\n orphanName,\n isUuidCast: declaredNormalized === 'UUID',\n requiresShapeCheck,\n });\n }\n }\n\n // `columnsAllValuesUuidShapedBatch` falls back to isolated per-column\n // probes on a batch failure and never throws; a column absent from the\n // result (probe unresolvable) defaults to \"not shaped\" below, excluding\n // just that candidate (#2874 review finding F2).\n const shaped: Map<string, boolean> =\n shapeCheckOrphans.size > 0\n ? await this.columnsAllValuesUuidShapedBatch(tableName, [\n ...shapeCheckOrphans,\n ])\n : new Map();\n\n const candidatesByColumn = new Map<\n string,\n { orphanName: string; isUuidCast: boolean }[]\n >();\n for (const candidate of pending) {\n if (\n candidate.requiresShapeCheck &&\n !(shaped.get(candidate.orphanName) ?? false)\n ) {\n continue;\n }\n const list = candidatesByColumn.get(candidate.colName) ?? [];\n list.push({\n orphanName: candidate.orphanName,\n isUuidCast: candidate.isUuidCast,\n });\n candidatesByColumn.set(candidate.colName, list);\n }\n\n for (const [colName, candidates] of candidatesByColumn) {\n if (candidates.length === 1) {\n changes.push(\n this.describeRenameDataPending(\n tableName,\n colName,\n candidates[0].orphanName,\n candidates[0].isUuidCast,\n ),\n );\n } else if (candidates.length > 1) {\n changes.push(\n this.describeRenameDataPendingAmbiguous(\n tableName,\n colName,\n candidates.map((c) => c.orphanName),\n ),\n );\n }\n }\n\n return changes;\n }\n\n /**\n * Live-data probe, batched across every column named: does each hold any\n * non-null, non-empty value? One round trip regardless of column count\n * (#2874) — one row of uncorrelated scalar subqueries,\n * `(SELECT 1 FROM t WHERE ... LIMIT 1) AS \"col\"`, portable across\n * PostgreSQL and SQLite. Deliberately *not* an aggregate\n * (`MAX(CASE WHEN ...)`) over the whole table: an aggregate forces a full\n * scan for every probed column even when the very first row already\n * answers it, which would turn a healthy, mostly-populated large table\n * into a guaranteed full scan on every comparison — worse than the\n * original per-column probe for exactly the schemas #2874 cares about\n * (#2874 review finding F1). Each subquery keeps the original\n * `LIMIT 1` early exit; only the round trip is batched, not the\n * per-column scan cost.\n *\n * Falls back to {@link columnHasNonEmptyValueSingle} per column when the\n * batched statement itself fails (a column dropped concurrently, or a\n * `CAST` the engine rejects) — #2874 review finding F2: a single bad\n * column must withhold only that column's result, not the whole table's\n * detection. A column absent from the returned map means \"could not be\n * probed\"; callers apply their own fail-closed default.\n */\n private async columnsHaveNonEmptyValueBatch(\n tableName: string,\n colNames: string[],\n ): Promise<Map<string, boolean>> {\n if (colNames.length === 0) return new Map();\n try {\n return await this.columnsHaveNonEmptyValueBatchQuery(tableName, colNames);\n } catch {\n const hasData = new Map<string, boolean>();\n for (const colName of colNames) {\n try {\n hasData.set(\n colName,\n await this.columnHasNonEmptyValueSingle(tableName, colName),\n );\n } catch {\n // Left absent: the caller's own default applies (#2874 review F2).\n }\n }\n return hasData;\n }\n }\n\n private async columnsHaveNonEmptyValueBatchQuery(\n tableName: string,\n colNames: string[],\n ): Promise<Map<string, boolean>> {\n const quotedTable = this.quoteIdentifier(tableName);\n // Positional aliases (`c0`, `c1`, …), not the column name itself\n // (#2874 review finding F2'): PostgreSQL silently truncates a `name`\n // identifier — including a quoted alias — to 63 bytes, so a long column\n // name, or two columns sharing their first 63 bytes, would collide on\n // the same output key and mis-key a result. Positional aliases are\n // immune to identifier length and never collide with each other.\n const selects = colNames.map((colName, index) => {\n const quotedCol = this.quoteIdentifier(colName);\n return (\n `(SELECT 1 FROM ${quotedTable} WHERE ${quotedCol} IS NOT NULL ` +\n `AND CAST(${quotedCol} AS TEXT) <> '' LIMIT 1) AS c${index}`\n );\n });\n const result = await this.db.query(`SELECT ${selects.join(', ')}`);\n const row = (result.rows?.[0] ?? {}) as Record<string, unknown>;\n const hasData = new Map<string, boolean>();\n colNames.forEach((colName, index) => {\n hasData.set(colName, row[`c${index}`] != null);\n });\n return hasData;\n }\n\n /** Single-column fallback for {@link columnsHaveNonEmptyValueBatch}. */\n private async columnHasNonEmptyValueSingle(\n tableName: string,\n colName: string,\n ): Promise<boolean> {\n const quotedTable = this.quoteIdentifier(tableName);\n const quotedCol = this.quoteIdentifier(colName);\n const result = await this.db.query(\n `SELECT 1 AS present FROM ${quotedTable} ` +\n `WHERE ${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> '' LIMIT 1`,\n );\n return (result.rows?.length ?? 0) > 0;\n }\n\n /**\n * Live-data probe, batched across every orphan column named: are every\n * one of each column's non-empty values UUID-shaped\n * ({@link CANONICAL_UUID_PATTERN})? One round trip regardless of column\n * count (#2874), mirroring {@link columnsHaveNonEmptyValueBatch}: one row\n * of uncorrelated scalar subqueries, each\n * `(SELECT 1 FROM t WHERE <non-empty> AND <invalid> LIMIT 1)` — a live\n * value is absent from the result exactly when no invalid row exists, so\n * this also short-circuits on the first invalid row rather than counting\n * every one (an early-exit improvement over the pre-#2874 per-column\n * `count(*)` probe, not just a batching change). PostgreSQL pushes the\n * shape check into its regex operator; SQLite has no regex operator, but\n * its case-sensitive `GLOB` can still express the fixed 36-character\n * canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN} against\n * `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check).\n *\n * Falls back to {@link allNonEmptyValuesUuidShapedSingle} per column on a\n * batch failure, same posture as {@link columnsHaveNonEmptyValueBatch}\n * (#2874 review finding F2).\n */\n private async columnsAllValuesUuidShapedBatch(\n tableName: string,\n colNames: string[],\n ): Promise<Map<string, boolean>> {\n if (colNames.length === 0) return new Map();\n try {\n return await this.columnsAllValuesUuidShapedBatchQuery(\n tableName,\n colNames,\n );\n } catch {\n const shaped = new Map<string, boolean>();\n for (const colName of colNames) {\n try {\n shaped.set(\n colName,\n await this.allNonEmptyValuesUuidShapedSingle(tableName, colName),\n );\n } catch {\n // Left absent: the caller's own default applies (#2874 review F2).\n }\n }\n return shaped;\n }\n }\n\n private async columnsAllValuesUuidShapedBatchQuery(\n tableName: string,\n colNames: string[],\n ): Promise<Map<string, boolean>> {\n const quotedTable = this.quoteIdentifier(tableName);\n // Positional aliases, not the column name (#2874 review finding F2') —\n // see {@link columnsHaveNonEmptyValueBatchQuery}.\n const selects = colNames.map((colName, index) => {\n const quotedCol = this.quoteIdentifier(colName);\n const nonEmptyPredicate = `${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> ''`;\n const invalidPredicate =\n this.engine === 'postgres'\n ? `CAST(${quotedCol} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'`\n : `NOT (LENGTH(CAST(${quotedCol} AS TEXT)) = 36 ` +\n `AND LOWER(CAST(${quotedCol} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`;\n return (\n `(SELECT 1 FROM ${quotedTable} WHERE ${nonEmptyPredicate} ` +\n `AND ${invalidPredicate} LIMIT 1) AS c${index}`\n );\n });\n const result = await this.db.query(`SELECT ${selects.join(', ')}`);\n const row = (result.rows?.[0] ?? {}) as Record<string, unknown>;\n const shaped = new Map<string, boolean>();\n colNames.forEach((colName, index) => {\n shaped.set(colName, row[`c${index}`] == null);\n });\n return shaped;\n }\n\n /** Single-column fallback for {@link columnsAllValuesUuidShapedBatch}. */\n private async allNonEmptyValuesUuidShapedSingle(\n tableName: string,\n colName: string,\n ): Promise<boolean> {\n const quotedTable = this.quoteIdentifier(tableName);\n const quotedCol = this.quoteIdentifier(colName);\n const nonEmptyPredicate = `${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> ''`;\n const invalidPredicate =\n this.engine === 'postgres'\n ? `CAST(${quotedCol} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'`\n : `NOT (LENGTH(CAST(${quotedCol} AS TEXT)) = 36 ` +\n `AND LOWER(CAST(${quotedCol} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`;\n const result = await this.db.query(\n `SELECT 1 AS invalid FROM ${quotedTable} ` +\n `WHERE ${nonEmptyPredicate} AND ${invalidPredicate} LIMIT 1`,\n );\n return (result.rows?.length ?? 0) === 0;\n }\n\n /**\n * Render the repair for one rename-data-pending pair (#2752): copy\n * non-empty `oldColumn` into `newColumn` only where `newColumn` is still\n * empty (casting to `uuid` when the new column is native uuid, otherwise a\n * plain copy), then drop `oldColumn`.\n *\n * Idempotency review findings and the fix:\n *\n * - (P1) The `newColumn`-is-empty predicate previously compared any\n * non-uuid destination to `''` (`col IS NULL OR col = ''`), which is\n * only valid for text-affinity types — PostgreSQL rejects `col = ''`\n * for `integer`/`boolean`/`timestamp`/etc. before a same-type rename\n * pair of one of those types ever copies anything. Fixed by comparing\n * `CAST(col AS TEXT) = ''` instead, mirroring the same portable\n * emptiness check {@link columnsHaveNonEmptyValueBatch} already uses for\n * detection, so the predicate is valid for every column type.\n * - (P2) An `UPDATE` unconditionally naming `oldColumn` fails once that\n * column is gone, on either engine, regardless of the DROP's own\n * idempotency — a guard query placed before the DROP (or even before\n * the UPDATE) still leaves a human decision between \"run\" and \"don't\",\n * which is not genuine SQL-level idempotency. PostgreSQL CAN express a\n * truly self-contained, unconditionally-safe-to-rerun statement here:\n * an anonymous `DO $$ ... $$` block (the same idiom this file already\n * uses for `generatePostgresIntegerPreflightSQL`) that checks\n * `information_schema.columns` and only runs the UPDATE + DROP when the\n * old column still exists — one executable statement, no operator\n * judgment required, safe to rerun any number of times. SQLite has no\n * procedural block or conditional-DDL construct at all (confirmed\n * against a real connection), so there the contract is explicitly\n * downgraded from \"idempotent SQL\" to \"operator-mediated\": a guard\n * query plus instructions, not a single statement that is itself safe\n * to blindly rerun.\n *\n * This is report-only either way: `db:migrate` never executes it.\n */\n private describeRenameDataPending(\n tableName: string,\n newColumn: string,\n oldColumn: string,\n newIsNativeUuid: boolean,\n ): SchemaChange {\n const quotedTable = this.quoteIdentifier(tableName);\n const quotedNew = this.quoteIdentifier(newColumn);\n const quotedOld = this.quoteIdentifier(oldColumn);\n const oldValue = newIsNativeUuid ? `${quotedOld}::uuid` : quotedOld;\n const newEmptyPredicate = newIsNativeUuid\n ? `${quotedNew} IS NULL`\n : `(${quotedNew} IS NULL OR CAST(${quotedNew} AS TEXT) = '')`;\n const copySql =\n `UPDATE ${quotedTable} SET ${quotedNew} = ${oldValue} ` +\n `WHERE ${newEmptyPredicate} AND ${quotedOld} IS NOT NULL AND CAST(${quotedOld} AS TEXT) <> ''`;\n\n let suggestedSql: string[];\n let dropIdempotencyNote: string;\n\n if (this.engine === 'postgres') {\n // A single anonymous block: checks the old column's existence once\n // and only then runs the UPDATE and the DROP, inside the same\n // condition — genuinely idempotent, safe to rerun any number of\n // times with no operator judgment involved.\n // Scoped to `public` (matches this file's other information_schema\n // queries, e.g. the table-listing query below): unscoped, this check\n // matches any schema the connection can see, so a same-named\n // table/column pair in another schema could make the guard report\n // \"still present\" (or, symmetrically, mask an already-dropped\n // relation on `public`) regardless of the actual target relation's\n // state — silently breaking the no-op contract this block exists to\n // provide (#2752 review finding).\n const existsCheck =\n `EXISTS (SELECT 1 FROM information_schema.columns ` +\n `WHERE table_schema = 'public' AND table_name = ${this.quoteLiteral(tableName)} AND column_name = ${this.quoteLiteral(oldColumn)})`;\n const dropSql = `ALTER TABLE ${quotedTable} DROP COLUMN ${quotedOld}`;\n suggestedSql = [\n `DO $$ BEGIN IF ${existsCheck} THEN ${copySql}; ${dropSql}; END IF; END $$`,\n ];\n dropIdempotencyNote =\n 'This single statement is idempotent: rerunning it after the ' +\n `rename is complete is a genuine no-op, because it only touches ` +\n `${oldColumn} when the block finds it still exists.`;\n } else {\n // SQLite has no procedural block or conditional-DDL construct at\n // all, so true statement-level idempotency is not achievable in\n // plain SQL here. The contract is explicitly operator-mediated\n // instead: a guard query, then the two statements to run only when\n // it reports the old column present.\n const guardSql =\n `SELECT count(*) AS old_column_still_present ` +\n `FROM pragma_table_info(${this.quoteLiteral(tableName)}) WHERE name = ${this.quoteLiteral(oldColumn)}`;\n const dropSql = this.generateDropColumnSQL(tableName, oldColumn);\n suggestedSql = [guardSql, copySql, dropSql];\n dropIdempotencyNote =\n 'SQLite has no conditional-DDL construct, so this is operator-mediated, ' +\n `not self-contained idempotent SQL: run the guard query first, and only run ` +\n `the UPDATE and DROP below when it reports ${oldColumn} still present.`;\n }\n\n return {\n type: 'rename_data_pending',\n table: tableName,\n name: newColumn,\n mismatch: {\n expected: `data in ${newColumn}`,\n actual: `data appears to still be in ${oldColumn}`,\n },\n advisory: {\n severity: 'warning',\n message:\n `${tableName}.${newColumn} is declared but empty, while undeclared column ${tableName}.${oldColumn} ` +\n 'holds data of a compatible type. This looks like a framework field rename whose data was never ' +\n `moved (db:migrate adds columns additively and never drops the old one). ${dropIdempotencyNote}`,\n suggestedSql,\n },\n };\n }\n\n /**\n * Render the advisory for an ambiguous rename-data-pending match (#2767\n * review): more than one orphan column is a populated, type-compatible\n * candidate for the same empty declared column, so the real rename source\n * cannot be inferred. Unlike {@link describeRenameDataPending}, this\n * carries no `suggestedSql` — running a per-candidate copy-and-drop would\n * let output order decide which candidate's data wins and then drop every\n * one of them, which is not a repair an advisory should suggest blindly.\n */\n private describeRenameDataPendingAmbiguous(\n tableName: string,\n newColumn: string,\n candidateColumns: string[],\n ): SchemaChange {\n const candidateList = candidateColumns\n .map((name) => `${tableName}.${name}`)\n .join(', ');\n return {\n type: 'rename_data_pending',\n table: tableName,\n name: newColumn,\n mismatch: {\n expected: `data in ${newColumn}`,\n actual: `data appears to still be in one of several columns: ${candidateList}`,\n },\n advisory: {\n severity: 'warning',\n message:\n `${tableName}.${newColumn} is declared but empty, while ${candidateColumns.length} undeclared ` +\n `columns of a compatible type hold data (${candidateList}). This looks like a framework field ` +\n 'rename, but which column is the actual rename source is ambiguous — no suggested repair SQL is ' +\n 'printed. An operator must identify the correct source column and write its own targeted copy-then-drop.',\n },\n };\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)::bigint`;\n } else if (\n manifestNormalized === 'INTEGER' &&\n dbNormalized === 'REAL'\n ) {\n typeClause += ` USING ${quotedCol}::bigint`;\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 if (this.engine === 'postgres' && idx.nullsNotDistinct) {\n return renderNullEqualConflictIndex(tableName, idx);\n }\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 // Add table creation (requires full DDL generation - not included here)\n // This is typically handled by ensureSchema()\n return getSQLFromChanges(diff.changes);\n}\n\n/**\n * Executable SQL for an explicit subset of changes, in the order given.\n * Callers that have to interleave diff changes with statements produced\n * elsewhere — the orchestrator, which emits `CREATE TABLE` and deferred\n * foreign keys of its own — partition `diff.changes` by\n * {@link SchemaChange.phase} and render each part through this helper.\n */\nexport function getSQLFromChanges(changes: readonly SchemaChange[]): string[] {\n const statements: string[] = [];\n for (const change of changes) {\n if (change.type !== 'type_mismatch' && !isAdvisoryOnlyChange(change)) {\n statements.push(\n ...(change.sqlStatements ?? (change.sql ? [change.sql] : [])),\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 {\n columnsAllValuesUuidShapedBatch,\n columnsHaveNonEmptyValueBatch,\n nonEmptyValuePredicate,\n uuidInvalidShapePredicate,\n} from '../schema/column-data-probes.js';\nimport { detectEngine, getDDLStrategy } from '../schema/ddl/index.js';\nimport { renderNullEqualConflictIndex } from '../schema/ddl/null-equal-index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport {\n foreignKeyConstraintName,\n foreignKeyRelationshipKey,\n renderForeignKeyAddStatements,\n renderForeignKeyOrphanDetector,\n renderForeignKeyOrphanRepair,\n schemaForeignKeys,\n schemaForeignKeysForEngine,\n} from '../schema/foreign-key-ddl.js';\nimport {\n normalizeForeignKeyAction,\n requireForeignKeyAction,\n} from '../schema/foreign-key-policy.js';\nimport {\n maskSampleValue,\n probeCastSafety,\n renderJsonbColumnConversion,\n renderTimestamptzColumnConversion,\n type ShapeProbeResult,\n} from '../schema/text-cast-probe.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 describeForeignKeyTypeConflict,\n planUuidConvergence,\n renderUuidConvergenceRepairHint,\n renderUuidShapeProbe,\n type UuidConvergenceLiveForeignKey,\n type UuidConvergencePlan,\n} from '../schema/uuid-convergence.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 */\n/**\n * Single- vs double-precision float classification for #2770's float-width\n * drift detection. `null` for anything that isn't unambiguously one or the\n * other (DECIMAL/NUMERIC have no fixed binary width and are out of scope).\n */\nfunction floatPrecisionOf(\n type: string,\n engine: 'postgres' | 'duckdb',\n): 'single' | 'double' | null {\n const upper = type\n .toUpperCase()\n .trim()\n .replace(/\\(\\s*\\d+(\\s*,\\s*\\d+)?\\s*\\)/g, '');\n if (/^(REAL|FLOAT4)$/.test(upper)) return 'single';\n // DuckDB's information_schema normalizes REAL/FLOAT4 to the bare string\n // `FLOAT` (never `REAL`) and reports its double-precision type as\n // `DOUBLE` (never `FLOAT`) — the opposite of PostgreSQL, where bare\n // `FLOAT` never appears live and previously defaulted safely to\n // double-precision. Treating DuckDB's `FLOAT` as double-precision made\n // every converged DuckDB REAL column permanently misreport as narrowing\n // drift (review finding on #2770).\n if (engine === 'duckdb' && upper === 'FLOAT') return 'single';\n if (/^(FLOAT8|DOUBLE|DOUBLE PRECISION|FLOAT)$/.test(upper)) return 'double';\n return null;\n}\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 * Live schemas read during one `compare()` run. The uuid convergence plan\n * (#2608) needs the same introspection the per-table comparison does, and\n * re-reading `getTableSchema` per relationship would multiply catalog\n * round-trips on a wide schema.\n */\n private liveSchemas = new Map<\n string,\n SqlTableSchemaInfo | null | undefined\n >();\n /** Convergence plan for this run; `null` on non-PostgreSQL engines. */\n private uuidConvergence: UuidConvergencePlan | null = null;\n\n /**\n * Rename-pending advisory findings for this `compare()` run, keyed by\n * table name (#2878). `compareTable()` reads from here when it is being\n * driven by `compare()` over the full manifest, which lets the probe be\n * batched *across every table in one round trip* instead of paying at\n * least one round trip per table (#2876 batched a table's own columns\n * into one round trip but never batched across tables — with a\n * realistic 71-table schema that per-table floor was itself the entire\n * residual). `null` means \"not precomputed for this run\" (a standalone\n * `compareTable()` call outside `compare()`, or a non-PostgreSQL/SQLite\n * engine), in which case `detectRenameDataPending()` falls back to the\n * original single-table probe so standalone callers keep working\n * unchanged. Cleared every `compare()` call, same as `liveSchemas` —\n * this is a per-run cache, never reused across runs, so a live schema\n * change is always seen on the next comparison (no invalidation story\n * needed because nothing survives past one `compare()` call).\n */\n private renameDataPendingCache: Map<string, SchemaChange[]> | null = null;\n\n /**\n * Cross-table batch queries stay bounded regardless of schema shape: this\n * caps how many scalar-subquery columns one probe statement packs into a\n * single row. Conservative relative to PostgreSQL's ~1600 column limit\n * per result row, and SQLite has no such ceiling but benefits from the\n * same bound for query-text size. Applied twice (PR #2888 review): once\n * per table, so a single pathologically wide table (more probed columns\n * than this cap) is itself sliced across more than one statement rather\n * than landing in one oversized chunk; and again across tables, packing\n * every (possibly sliced) entry into as few chunks as this cap allows.\n * A schema with no single table wider than this cap needs more than\n * `MAX_CROSS_TABLE_PROBE_COLUMNS` probed columns *in total* before a\n * second statement is needed at all — still O(1)-ish round trips for a\n * realistic schema.\n */\n private static readonly MAX_CROSS_TABLE_PROBE_COLUMNS = 400;\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 // A comparer instance may be reused across successive compares (apply a\n // migration, then diff again). Live introspection must not be memoized\n // across those runs.\n this.liveSchemas.clear();\n this.renameDataPendingCache = null;\n\n // #2878 review: `renameDataPendingCache` is populated below for the\n // duration of this call only. The `finally` clears it again on the way\n // out (success *or* a mid-run throw) so a `compareTable()` call made\n // directly on this instance after `compare()` returns — its own public\n // method, reachable by any holder of this `SchemaComparer` — can never\n // read a stale entry computed from a previous manifest and a previous\n // run's live data. Without this, a cache hit for a table absent from\n // the *next* standalone call's manifest/dbSchema would silently return\n // a stale advisory rather than falling back to\n // `detectRenameDataPendingSingleTable()`, and the doc comment on\n // `renameDataPendingCache` claiming \"nothing survives past one\n // `compare()` call\" would be false.\n try {\n // Get list of existing tables\n const existingTables = await this.getExistingTables();\n\n // #2878: precompute every table's rename-pending advisory findings in\n // one batched pass, across the whole manifest, before the per-table\n // loop below reads them one table at a time via `detectRenameDataPending`.\n // See `renameDataPendingCache` for why this is safe to compute ahead of\n // (and share with) that loop.\n await this.precomputeRenameDataPending(manifestSchemas, existingTables);\n\n // #2608: plan the pre-R11 `text` -> `uuid` convergence before anything\n // else so its statements lead the migration stream. Every foreign key in\n // this diff — including the deferred constraints the orchestrator emits\n // for newly created tables — depends on the columns it rewrites.\n this.uuidConvergence = await this.buildUuidConvergencePlan(\n manifestSchemas,\n existingTables,\n );\n for (const change of this.uuidConvergenceChanges(manifestSchemas)) {\n diff.changes.push(change);\n if (!isInfoOnlyChange(change)) diff.has_changes = true;\n }\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(\n tableName,\n schema,\n manifestSchemas,\n );\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 } finally {\n this.renameDataPendingCache = null;\n }\n }\n\n /**\n * Compare a single table's schema to manifest\n */\n async compareTable(\n tableName: string,\n manifest: SchemaDefinition,\n manifestSchemas: Record<string, SchemaDefinition> = {},\n ): Promise<SchemaChange[]> {\n const changes: SchemaChange[] = [];\n\n // Get current table schema from database\n const dbSchema = await this.getLiveSchema(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 changes.push(\n ...(await this.compareForeignKeys(\n tableName,\n manifest,\n dbSchema,\n manifestSchemas,\n )),\n );\n\n return changes;\n }\n\n /**\n * Read (and memoize for this run) one table's live schema. Returns\n * `undefined` when the table does not exist or the adapter cannot\n * introspect it.\n */\n private async getLiveSchema(\n tableName: string,\n ): Promise<SqlTableSchemaInfo | undefined> {\n if (this.liveSchemas.has(tableName)) {\n return this.liveSchemas.get(tableName) ?? undefined;\n }\n const schema = await this.db.getTableSchema?.(tableName);\n this.liveSchemas.set(tableName, schema);\n return schema ?? undefined;\n }\n\n /**\n * Plan the pre-R11 `text` -> `uuid` convergence (#2608).\n *\n * PostgreSQL only: SQLite stores UUIDs as text by design and DuckDB cannot\n * rewrite a column type in place, so both engines return `null` and emit\n * nothing for this drift.\n */\n private async buildUuidConvergencePlan(\n manifestSchemas: Record<string, SchemaDefinition>,\n existingTables: Set<string>,\n ): Promise<UuidConvergencePlan | null> {\n if (this.engine !== 'postgres') return null;\n\n for (const tableName of Object.keys(manifestSchemas)) {\n if (!existingTables.has(tableName)) continue;\n await this.getLiveSchema(tableName);\n }\n\n const relationships = Object.entries(manifestSchemas).flatMap(\n ([tableName, schema]) =>\n schemaForeignKeysForEngine(schema, 'postgres').map((foreignKey) => ({\n childTable: tableName,\n childColumn: foreignKey.column,\n parentTable: foreignKey.referencesTable,\n parentColumn: foreignKey.referencesColumn,\n })),\n );\n\n // Columns this plan could rewrite: a live `text` side of a relationship\n // the manifest declares UUID on both ends.\n const conversionCandidates = new Set<string>();\n for (const relationship of relationships) {\n const childManifest =\n manifestSchemas[relationship.childTable]?.columns[\n relationship.childColumn\n ];\n const parentManifest =\n manifestSchemas[relationship.parentTable]?.columns[\n relationship.parentColumn\n ];\n if (childManifest?.type !== 'UUID' || parentManifest?.type !== 'UUID') {\n continue;\n }\n for (const [table, column] of [\n [relationship.childTable, relationship.childColumn],\n [relationship.parentTable, relationship.parentColumn],\n ] as const) {\n if (!existingTables.has(table)) continue;\n const live = this.liveSchemas.get(table)?.columns[column];\n if (!live) continue;\n if (this.normalizeType(live.type) !== 'TEXT') continue;\n conversionCandidates.add(`${table} ${column}`);\n }\n }\n\n // #2608: a table the manifest no longer declares can still hold a live\n // foreign key onto a column this plan would rewrite. PostgreSQL refuses\n // `ALTER COLUMN … TYPE` while any constraint depends on the column, so an\n // uninspected orphan table turns the promised \"blocked\" advisory into an\n // aborted migration. Introspect those tables too — but only when there is\n // actually a conversion to protect, so an already-converged database pays\n // nothing.\n if (conversionCandidates.size > 0) {\n for (const tableName of existingTables) {\n if (manifestSchemas[tableName]) continue;\n if (tableName.startsWith('sqlite_')) continue;\n await this.getLiveSchema(tableName);\n }\n }\n\n const liveForeignKeys: UuidConvergenceLiveForeignKey[] = [];\n for (const [tableName, schema] of this.liveSchemas) {\n for (const live of schema?.foreignKeys ?? []) {\n liveForeignKeys.push({\n table: tableName,\n column: live.column,\n referencesTable: live.referencesTable,\n referencesColumn: live.referencesColumn,\n });\n }\n }\n\n return planUuidConvergence({\n manifestSchemas,\n relationships,\n liveForeignKeys,\n getLiveColumn: (table, column) => {\n if (!existingTables.has(table)) return undefined;\n const live = this.liveSchemas.get(table)?.columns[column];\n if (!live) return undefined;\n return {\n normalizedType: this.normalizeType(live.type),\n rawType: live.type,\n hasDefault:\n live.defaultValue !== null && live.defaultValue !== undefined,\n exists: true,\n };\n },\n probeUuidShape: (table, column) => this.probeUuidShape(table, column),\n });\n }\n\n /**\n * Count values a legacy `text` identifier column holds that cannot be\n * reinterpreted as `uuid`. Any failure resolves to `unavailable`, which the\n * planner treats as a block — SMRT never coerces identifier values.\n */\n private async probeUuidShape(\n tableName: string,\n columnName: string,\n ): Promise<\n | { status: 'clean' }\n | { status: 'dirty'; count: number; sample?: string }\n | { status: 'unavailable'; reason: string }\n > {\n try {\n const result = await this.db.query(\n renderUuidShapeProbe(tableName, columnName),\n );\n const rows = (Array.isArray(result) ? result : result.rows || []) as {\n invalid_count?: unknown;\n sample_value?: unknown;\n }[];\n const count = Number(rows[0]?.invalid_count ?? 0);\n if (!Number.isFinite(count)) {\n return {\n status: 'unavailable',\n reason: 'probe returned a non-numeric count',\n };\n }\n if (count === 0) return { status: 'clean' };\n const sample = rows[0]?.sample_value;\n return {\n status: 'dirty',\n count,\n ...(typeof sample === 'string' ? { sample } : {}),\n };\n } catch (error) {\n return {\n status: 'unavailable',\n reason: error instanceof Error ? error.message : String(error),\n };\n }\n }\n\n /**\n * Probe a candidate `text` -> `timestamptz`/`jsonb` column (#2771, #2772)\n * with a real, exception-safe cast attempt (see `text-cast-probe.ts`).\n * Unlike {@link probeUuidShape}, a failed/unrealistic probe\n * (`unavailable`) is the caller's cue to fall back to the pre-existing\n * behavior for that pair rather than surface a new finding — this keeps\n * the new convergence strictly additive for every case that isn't\n * affirmatively confirmed safe or affirmatively confirmed unsafe.\n */\n private async probeTextCastShape(\n tableName: string,\n columnName: string,\n kind: 'timestamptz' | 'jsonb',\n ): Promise<ShapeProbeResult> {\n return probeCastSafety(this.db, tableName, columnName, kind);\n }\n\n /**\n * Turn the convergence plan into diff entries: executable `type_upgrade`\n * conversions marked `phase: 'pre_foreign_key'`, plus report-only warnings\n * for components the planner refused.\n */\n private uuidConvergenceChanges(\n manifestSchemas: Record<string, SchemaDefinition>,\n ): SchemaChange[] {\n const plan = this.uuidConvergence;\n if (!plan) return [];\n // Every consumer of a `type_upgrade` entry reads `change.column` for the\n // target shape — `partitionSchemaChanges()` in `@happyvertical/smrt-cli`\n // skips an entry without one, which would drop these conversions out of\n // the `db:migrate` batch while `compareForeignKeys()` still assumed their\n // converged type and emitted the dependent constraint (SQLSTATE 42804).\n const manifestColumn = (table: string, column: string): ColumnDefinition =>\n manifestSchemas[table]?.columns[column] ?? { type: 'UUID' };\n const changes: SchemaChange[] = [];\n for (const conversion of plan.conversions) {\n changes.push({\n type: 'type_upgrade',\n table: conversion.table,\n name: conversion.column,\n column: manifestColumn(conversion.table, conversion.column),\n phase: 'pre_foreign_key',\n mismatch: { expected: 'UUID', actual: conversion.liveType },\n sql: conversion.statements[0],\n sqlStatements: conversion.statements,\n });\n }\n for (const block of plan.blocks) {\n const [first] = block.targets.length > 0 ? block.targets : block.nodes;\n changes.push({\n type: 'type_upgrade',\n table: first?.table ?? '',\n name: first?.column,\n column: manifestColumn(first?.table ?? '', first?.column ?? ''),\n phase: 'pre_foreign_key',\n mismatch: { expected: 'UUID', actual: 'mixed uuid/text' },\n advisory: {\n severity: 'warning',\n message:\n 'blocked: incompatible column types. The manifest declares UUID for ' +\n `${block.nodes\n .map((node) => `${node.table}.${node.column}`)\n .join(', ')}, but ${block.reason}.`,\n suggestedSql: block.suggestedSql,\n },\n });\n }\n return changes;\n }\n\n /**\n * Live-type verdict for one foreign key once this run's conversions land.\n * `undefined` means the pair is compatible (or unknowable) and the ordinary\n * add path applies.\n */\n private async describeForeignKeyTypeBlock(\n tableName: string,\n dbSchema: SqlTableSchemaInfo,\n foreignKey: import('../schema/types.js').ForeignKeyDefinition,\n ): Promise<string | undefined> {\n if (this.engine !== 'postgres') return undefined;\n const parentSchema =\n foreignKey.referencesTable === tableName\n ? dbSchema\n : await this.getLiveSchema(foreignKey.referencesTable);\n const childType = dbSchema.columns[foreignKey.column]?.type;\n const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;\n // A column this migration is about to add materializes with the manifest\n // type, so there is nothing live to conflict with yet.\n if (!childType || !parentType) return undefined;\n\n const plan = this.uuidConvergence;\n const childEffective =\n plan?.effectiveType(tableName, foreignKey.column) ??\n this.normalizeType(childType);\n const parentEffective =\n plan?.effectiveType(\n foreignKey.referencesTable,\n foreignKey.referencesColumn,\n ) ?? this.normalizeType(parentType);\n if (childEffective === parentEffective) return undefined;\n\n return describeForeignKeyTypeConflict({\n childTable: tableName,\n childColumn: foreignKey.column,\n childType,\n parentTable: foreignKey.referencesTable,\n parentColumn: foreignKey.referencesColumn,\n parentType,\n });\n }\n\n private async compareForeignKeys(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n manifestSchemas: Record<string, SchemaDefinition>,\n ): Promise<SchemaChange[]> {\n const changes: SchemaChange[] = [];\n const liveForeignKeys = dbSchema.foreignKeys || [];\n const declaredForeignKeys = schemaForeignKeys(manifest);\n const enabledForeignKeys = schemaForeignKeysForEngine(\n manifest,\n this.engine,\n );\n const enabledKeys = new Set(\n enabledForeignKeys.map(foreignKeyRelationshipKey),\n );\n const disabledForeignKeys = declaredForeignKeys.filter(\n (foreignKey) => !enabledKeys.has(foreignKeyRelationshipKey(foreignKey)),\n );\n\n for (const foreignKey of disabledForeignKeys) {\n const live = liveForeignKeys.find(\n (candidate) =>\n foreignKeyRelationshipKey(candidate) ===\n foreignKeyRelationshipKey(foreignKey),\n );\n if (!live) continue;\n\n const constraintName = foreignKeyConstraintName(tableName, foreignKey);\n if (this.engine === 'postgres') {\n changes.push({\n type: 'drop_foreign_key',\n table: tableName,\n name: constraintName,\n foreignKey,\n advisory: {\n severity: 'warning',\n message:\n `Cannot safely remove the now-disabled physical foreign key ${tableName}.${foreignKey.column}: ` +\n '@happyvertical/sql schema introspection does not expose the live PostgreSQL constraint name. Drop the live constraint deliberately, then rerun the migration.',\n },\n });\n } else {\n changes.push({\n type: 'drop_foreign_key',\n table: tableName,\n name: constraintName,\n foreignKey,\n advisory: {\n severity: 'warning',\n message:\n `${this.engine === 'sqlite' ? 'SQLite' : 'DuckDB'} requires a table rebuild to remove the now-disabled physical foreign key ` +\n `${tableName}.${foreignKey.column}. Rebuild the table from the current manifest; relationship metadata and application-side enforcement remain active.`,\n },\n });\n }\n }\n\n for (const foreignKey of enabledForeignKeys) {\n const expectedDelete =\n foreignKey.onDelete === undefined\n ? 'NO ACTION'\n : requireForeignKeyAction(\n foreignKey.onDelete,\n `${tableName}.${foreignKey.column} ON DELETE`,\n );\n const expectedUpdate =\n foreignKey.onUpdate === undefined\n ? 'NO ACTION'\n : requireForeignKeyAction(\n foreignKey.onUpdate,\n `${tableName}.${foreignKey.column} ON UPDATE`,\n );\n const exact = liveForeignKeys.some(\n (live) =>\n live.column === foreignKey.column &&\n live.referencesTable === foreignKey.referencesTable &&\n live.referencesColumn === foreignKey.referencesColumn &&\n (normalizeForeignKeyAction(live.onDelete) || 'NO ACTION') ===\n expectedDelete &&\n (normalizeForeignKeyAction(live.onUpdate) || 'NO ACTION') ===\n expectedUpdate,\n );\n if (exact) continue;\n\n const orphanOptions = await this.getForeignKeyOrphanOptions(\n tableName,\n manifest,\n dbSchema,\n foreignKey,\n manifestSchemas,\n );\n\n const detectorSql = renderForeignKeyOrphanDetector(\n tableName,\n foreignKey,\n {\n engine: this.engine,\n uuidComparison: orphanOptions.uuidComparison,\n uuidCastSide: orphanOptions.uuidCastSide,\n },\n );\n const repairSql = renderForeignKeyOrphanRepair(tableName, foreignKey, {\n engine: this.engine,\n uuidComparison: orphanOptions.uuidComparison,\n uuidCastSide: orphanOptions.uuidCastSide,\n nullable: orphanOptions.nullable,\n });\n const sameColumn = liveForeignKeys.some(\n (live) => live.column === foreignKey.column,\n );\n if (sameColumn) {\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: foreignKeyConstraintName(tableName, foreignKey),\n foreignKey,\n advisory: {\n severity: 'warning',\n message:\n `Foreign key ${tableName}.${foreignKey.column} exists with a different target or action. ` +\n 'Drop the old constraint deliberately, repair any orphan rows, then rerun the migration.',\n suggestedSql: [detectorSql, repairSql],\n },\n });\n continue;\n }\n\n if (this.engine !== 'postgres') {\n const engineReason =\n this.engine === 'sqlite'\n ? 'SQLite requires a table rebuild to add a foreign key to an existing table.'\n : 'DuckDB does not support ALTER TABLE ADD CONSTRAINT.';\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: foreignKeyConstraintName(tableName, foreignKey),\n foreignKey,\n engineUnsupported: true,\n advisory: {\n severity: 'warning',\n message: `${engineReason} Run the orphan detector, repair rows, and rebuild the table with the generated constraint.`,\n suggestedSql: [detectorSql, repairSql],\n },\n });\n continue;\n }\n\n // #2608: refuse — and *report* — a constraint whose live child and\n // parent columns have different physical types and that this run's\n // uuid convergence will not repair. PostgreSQL rejects such an ADD\n // CONSTRAINT with SQLSTATE 42804, so calling it \"pending\" in\n // `db:status` promises a migration that cannot succeed.\n const typeBlock = await this.describeForeignKeyTypeBlock(\n tableName,\n dbSchema,\n foreignKey,\n );\n if (typeBlock) {\n // The uuid convergence is only the right remediation when the\n // manifest actually declares UUID on both sides. Any other\n // incompatible pair (text/integer, timestamp/text, …) reaches this\n // branch too, and telling the operator to cast both columns to uuid\n // there would be wrong.\n const uuidRelationship =\n manifest.columns[foreignKey.column]?.type === 'UUID' &&\n manifestSchemas[foreignKey.referencesTable]?.columns[\n foreignKey.referencesColumn\n ]?.type === 'UUID';\n const textNodes = uuidRelationship\n ? [\n this.normalizeType(\n dbSchema.columns[foreignKey.column]?.type ?? '',\n ) === 'TEXT'\n ? { table: tableName, column: foreignKey.column }\n : undefined,\n this.normalizeType(\n (foreignKey.referencesTable === tableName\n ? dbSchema\n : this.liveSchemas.get(foreignKey.referencesTable)\n )?.columns[foreignKey.referencesColumn]?.type ?? '',\n ) === 'TEXT'\n ? {\n table: foreignKey.referencesTable,\n column: foreignKey.referencesColumn,\n }\n : undefined,\n ].filter((node) => node !== undefined)\n : [];\n const suggestedSql = renderUuidConvergenceRepairHint(textNodes);\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: foreignKeyConstraintName(tableName, foreignKey),\n foreignKey,\n advisory: {\n severity: 'warning',\n message:\n `blocked: incompatible column types. ${typeBlock}. ` +\n (textNodes.length > 0\n ? 'Converge the columns to UUID, then rerun the migration.'\n : 'Align both columns on one physical type deliberately, then rerun the migration.'),\n ...(suggestedSql.length > 0 ? { suggestedSql } : {}),\n },\n });\n continue;\n }\n\n // A missing child column is added earlier in this same table diff, so\n // probing the pre-migration schema would fail and be misclassified as\n // an orphan. The subsequent NOT VALID + VALIDATE statements remain the\n // authoritative safety check once the prerequisite column exists.\n const childColumnExists = Boolean(dbSchema.columns[foreignKey.column]);\n if (\n childColumnExists &&\n (await this.foreignKeyHasOrphans(tableName, foreignKey, orphanOptions))\n ) {\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: foreignKeyConstraintName(tableName, foreignKey),\n foreignKey,\n orphanBlocked: true,\n orphanNullable: orphanOptions.nullable,\n advisory: {\n severity: 'warning',\n message:\n `Cannot add foreign key ${tableName}.${foreignKey.column}: existing rows do not match ` +\n `${foreignKey.referencesTable}.${foreignKey.referencesColumn}. Repair them, then rerun.`,\n suggestedSql: [detectorSql, repairSql],\n },\n });\n continue;\n }\n\n const constraintName = foreignKeyConstraintName(tableName, foreignKey);\n changes.push({\n type: 'add_foreign_key',\n table: tableName,\n name: constraintName,\n foreignKey,\n sqlStatements: renderForeignKeyAddStatements(tableName, foreignKey),\n });\n }\n return changes;\n }\n\n private async foreignKeyHasOrphans(\n tableName: string,\n foreignKey: import('../schema/types.js').ForeignKeyDefinition,\n options: {\n uuidComparison: boolean;\n uuidCastSide?: 'child' | 'parent' | 'both';\n },\n ): Promise<boolean> {\n try {\n const result = await this.db.query(\n renderForeignKeyOrphanDetector(tableName, foreignKey, {\n engine: this.engine,\n limitOne: true,\n uuidComparison: options.uuidComparison,\n uuidCastSide: options.uuidCastSide,\n }),\n );\n const rows = Array.isArray(result) ? result : result.rows || [];\n return rows.length > 0;\n } catch (error) {\n throw new Error(\n `[SchemaComparer] Cannot probe ${tableName}.${foreignKey.column} for orphan rows; refusing automatic constraint addition: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n private async getForeignKeyOrphanOptions(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n foreignKey: import('../schema/types.js').ForeignKeyDefinition,\n manifestSchemas: Record<string, SchemaDefinition>,\n ): Promise<{\n nullable: boolean;\n uuidComparison: boolean;\n uuidCastSide?: 'child' | 'parent' | 'both';\n }> {\n const sourceColumn = manifest.columns[foreignKey.column];\n const targetColumn =\n manifestSchemas[foreignKey.referencesTable]?.columns[\n foreignKey.referencesColumn\n ];\n // #2748 review: nullability for the orphan disposition (does the\n // null-out repair even apply?) must reflect the LIVE column, not only\n // the manifest. A manifest that now declares the column nullable while\n // the live column is still physically NOT NULL (nullability relaxation\n // pending, not yet applied — the same drift `--relax-columns` handles\n // for plain columns) would otherwise report `nullable: true` and let\n // `--null-orphans` attempt an UPDATE that PostgreSQL rejects outright,\n // failing the whole atomic batch instead of refusing this one\n // relationship. Nullable only when BOTH sides agree.\n const manifestNullable = sourceColumn?.notNull !== true;\n const liveNotNull = dbSchema.columns[foreignKey.column]?.notNull === true;\n const nullable = manifestNullable && !liveNotNull;\n const uuidComparison =\n sourceColumn?.type === 'UUID' &&\n (targetColumn === undefined || targetColumn.type === 'UUID');\n if (!uuidComparison) return { nullable, uuidComparison };\n\n const parentSchema =\n foreignKey.referencesTable === tableName\n ? dbSchema\n : await this.getLiveSchema(foreignKey.referencesTable);\n const childType = dbSchema.columns[foreignKey.column]?.type;\n const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;\n if (!childType || !parentType) return { nullable, uuidComparison };\n\n const childTypeNormalized = this.normalizeType(childType);\n const parentTypeNormalized = this.normalizeType(parentType);\n if (childTypeNormalized === parentTypeNormalized) {\n return { nullable, uuidComparison: false };\n }\n\n if (childTypeNormalized === 'UUID') {\n return { nullable, uuidComparison, uuidCastSide: 'parent' };\n }\n if (parentTypeNormalized === 'UUID') {\n return { nullable, uuidComparison, uuidCastSide: 'child' };\n }\n return { nullable, uuidComparison, uuidCastSide: 'both' };\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 // #2770: REAL/DOUBLE PRECISION both normalize to the same 'REAL'\n // bucket above (matching DECIMAL/NUMERIC tolerance), so the general\n // equality gate below never sees single- vs double-precision float\n // drift. Detect it here, the same way `legacy_integer_width` detects\n // int4-vs-int8 drift the general INTEGER bucket also hides. Widening\n // (float4 -> float8) is lossless and safe to auto-plan; narrowing\n // (float8 -> float4) can lose precision, so it stays advisory-only.\n if (\n (this.engine === 'postgres' || this.engine === 'duckdb') &&\n normalizedExpected === 'REAL' &&\n normalizedActual === 'REAL'\n ) {\n const expectedPrecision = floatPrecisionOf(\n expectedEngineType,\n this.engine,\n );\n const actualPrecision = floatPrecisionOf(dbCol.type, this.engine);\n if (\n expectedPrecision &&\n actualPrecision &&\n expectedPrecision !== actualPrecision\n ) {\n typeDrifted = true;\n const table = this.quoteIdentifier(tableName);\n const column = this.quoteIdentifier(colName);\n if (\n expectedPrecision === 'double' &&\n actualPrecision === 'single'\n ) {\n const targetType =\n this.engine === 'postgres' ? 'DOUBLE PRECISION' : 'DOUBLE';\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: expectedEngineType, actual: dbCol.type },\n sql: `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${targetType} USING ${column}::${targetType}`,\n });\n } else {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: expectedEngineType, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} is declared single-precision ` +\n `(${expectedEngineType}) but the live column is double-precision ` +\n `(${dbCol.type}). Narrowing loses precision, so this stays manual: ` +\n 'confirm the narrower declaration is intentional, or widen the ' +\n 'manifest field instead of the column.',\n suggestedSql: [\n `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${expectedEngineType} USING ${column}::${expectedEngineType}`,\n ],\n },\n });\n }\n }\n }\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 // #2772: on PostgreSQL, a manifest-declared JSON column backed by a\n // live `text` column is a real, repairable drift on a table SMRT\n // itself owns — not the enum-mis-inferred canary the tolerance below\n // exists for. Probe first (never trust the manifest's intent over\n // the live data): a clean probe converts the tolerance below into an\n // executable `type_upgrade`; a dirty probe converts it into a\n // visible, fail-closed advisory instead of staying silent. An\n // unavailable probe (missing table mid-run, a test double without a\n // realistic response) preserves the pre-existing silent tolerance —\n // this feature never invents a new finding it cannot back with data.\n const jsonUpgradeCandidate =\n this.engine === 'postgres' &&\n normalizedExpected === 'JSON' &&\n normalizedActual === 'TEXT';\n let jsonProbe: ShapeProbeResult | undefined;\n if (jsonUpgradeCandidate) {\n jsonProbe = await this.probeTextCastShape(\n tableName,\n colName,\n 'jsonb',\n );\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') — tolerated only\n // when the #2772 probe above could not affirmatively clear or\n // convict the column (see `jsonUpgradeCandidate` above).\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 === 'TEXT' && normalizedActual === 'JSON') ||\n (normalizedExpected === 'JSON' &&\n normalizedActual === 'TEXT' &&\n (!jsonUpgradeCandidate || jsonProbe?.status === 'unavailable'));\n\n // #2771: shape-probe a manifest TIMESTAMP column (-> TIMESTAMPTZ on\n // PostgreSQL) backed by a live `text` column, independent of the\n // `postgresTimestampMigration` opt-in (which exists for genuinely\n // ambiguous naive wall-clock strings). Every value SMRT itself ever\n // wrote carries an explicit UTC/offset designator, so a clean probe\n // here converts losslessly with no operator confirmation needed.\n const timestamptzUpgradeCandidate =\n this.engine === 'postgres' &&\n // `normalizedExpected` is the MAPPED engine bucket ('TIMESTAMPTZ'\n // on PostgreSQL); `isCompatibleTypeUpgrade` below keys off the RAW\n // abstract manifest bucket ('TIMESTAMP') instead, so this checks\n // the same raw bucket to stay consistent with that gate.\n this.normalizeType(colDef.type) === 'TIMESTAMP' &&\n normalizedActual === 'TEXT' &&\n normalizedExpected === 'TIMESTAMPTZ' &&\n !this.isCompatibleTypeUpgrade(colDef.type, dbCol.type);\n let timestamptzProbe: ShapeProbeResult | undefined;\n if (timestamptzUpgradeCandidate) {\n timestamptzProbe = await this.probeTextCastShape(\n tableName,\n colName,\n 'timestamptz',\n );\n }\n\n if (\n normalizedExpected !== normalizedActual &&\n !isUuidTextEquivalent &&\n !isJsonTextEquivalent\n ) {\n typeDrifted = true;\n\n // #2771/#2772 review finding: PostgreSQL rejects `ALTER COLUMN\n // ... TYPE` outright whenever the column has ANY existing default\n // that can't auto-cast to the target type -- regardless of\n // whether the manifest itself wants a default -- so DROP DEFAULT\n // must be gated on the LIVE default, not the manifest's. SET\n // DEFAULT afterward stays gated on the manifest default only: a\n // live-only default the manifest no longer declares must not be\n // silently resurrected.\n const hasLiveDefault =\n dbCol.defaultValue !== null && dbCol.defaultValue !== undefined;\n const conversionOptions = {\n hasLiveDefault,\n manifestDefaultValue: colDef.defaultValue,\n };\n\n // Review finding: dropping a live default the manifest no longer\n // declares is the same \"relaxation\" `compareColumnConstraints`\n // already gates behind `relaxColumns` elsewhere (see the\n // `dbHasDefault` branch above) — auto-executing it here as a side\n // effect of the type conversion would silently weaken the column\n // with no advisory and no opt-in. When there's no manifest\n // default to restore and the operator hasn't opted into\n // relaxation, surface it the same way a dirty probe does: a\n // fail-closed advisory naming the blocker, with the would-be SQL\n // attached for visibility, instead of executing.\n const liveOnlyDefaultNeedsRelaxOptIn =\n hasLiveDefault &&\n colDef.defaultValue === undefined &&\n !this.options.relaxColumns;\n\n if (\n jsonUpgradeCandidate &&\n jsonProbe?.status === 'clean' &&\n liveOnlyDefaultNeedsRelaxOptIn\n ) {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} has a live default ` +\n `(${String(dbCol.defaultValue)}) the manifest no longer ` +\n 'declares; converging to jsonb requires dropping it ' +\n `first. ${this.relaxHint('drop it as part of this conversion')}`,\n suggestedSql: renderJsonbColumnConversion(\n tableName,\n colName,\n conversionOptions,\n ),\n },\n });\n } else if (jsonUpgradeCandidate && jsonProbe?.status === 'clean') {\n const statements = renderJsonbColumnConversion(\n tableName,\n colName,\n conversionOptions,\n );\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n sql: statements[statements.length - 1],\n sqlStatements: statements,\n });\n } else if (jsonUpgradeCandidate && jsonProbe?.status === 'dirty') {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} is declared JSON but ${jsonProbe.count} ` +\n `live value(s) are not valid JSON (sample: ${\n jsonProbe.sample\n ? maskSampleValue(jsonProbe.sample)\n : 'unavailable'\n }). Repair or clear the offending value(s), then rerun ` +\n '`smrt db:migrate`.',\n suggestedSql: renderJsonbColumnConversion(\n tableName,\n colName,\n conversionOptions,\n ),\n },\n });\n } else if (\n timestamptzUpgradeCandidate &&\n timestamptzProbe?.status === 'clean' &&\n liveOnlyDefaultNeedsRelaxOptIn\n ) {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} has a live default ` +\n `(${String(dbCol.defaultValue)}) the manifest no longer ` +\n 'declares; converging to timestamptz requires dropping ' +\n `it first. ${this.relaxHint('drop it as part of this conversion')}`,\n suggestedSql: renderTimestamptzColumnConversion(\n tableName,\n colName,\n conversionOptions,\n ),\n },\n });\n } else if (\n timestamptzUpgradeCandidate &&\n timestamptzProbe?.status === 'clean'\n ) {\n const statements = renderTimestamptzColumnConversion(\n tableName,\n colName,\n conversionOptions,\n );\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n sql: statements[statements.length - 1],\n sqlStatements: statements,\n });\n } else if (\n timestamptzUpgradeCandidate &&\n timestamptzProbe?.status === 'dirty'\n ) {\n changes.push({\n type: 'type_upgrade',\n table: tableName,\n name: colName,\n column: colDef,\n mismatch: { expected: colDef.type, actual: dbCol.type },\n advisory: {\n severity: 'warning',\n message:\n `blocked: ${tableName}.${colName} is declared TIMESTAMP but ${timestamptzProbe.count} ` +\n `live value(s) do not parse as an unambiguous timestamp (sample: ${\n timestamptzProbe.sample\n ? maskSampleValue(timestamptzProbe.sample)\n : 'unavailable'\n }). Repair the offending value(s), or confirm legacy naive ` +\n 'wall-clock provenance with `smrt db:migrate --postgres-timestamp-legacy-timezone=UTC`, then rerun.',\n suggestedSql: renderTimestamptzColumnConversion(\n tableName,\n colName,\n conversionOptions,\n ),\n },\n });\n } else 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 changes.push(\n ...(await this.detectRenameDataPending(tableName, manifest, dbSchema)),\n );\n\n return changes;\n }\n\n /**\n * Detect a pending rename backfill (#2752) for one table. Reads\n * `renameDataPendingCache` when `compare()` has already batched this\n * table's probe across the whole manifest (#2878); otherwise falls back\n * to {@link detectRenameDataPendingSingleTable}, so a standalone\n * `compareTable()` call (outside `compare()`) still gets a correct\n * answer, just without the cross-table batching.\n */\n private async detectRenameDataPending(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n ): Promise<SchemaChange[]> {\n const cached = this.renameDataPendingCache?.get(tableName);\n if (cached !== undefined) return cached;\n return this.detectRenameDataPendingSingleTable(\n tableName,\n manifest,\n dbSchema,\n );\n }\n\n /**\n * Detect a pending rename backfill (#2752): a manifest-declared column\n * that exists live but holds no data, paired with an orphan (undeclared)\n * live column of a compatible type that does hold data — the shape a\n * framework field rename leaves behind when `db:migrate` adds the new\n * column additively and never moves data into it. For each such pair,\n * emit the copy-then-drop repair as an advisory: never executed,\n * PostgreSQL and SQLite only (DuckDB/JSON are out of scope). The repair\n * is genuinely idempotent SQL on PostgreSQL (a self-guarding\n * `DO $$ ... $$` block); SQLite has no conditional-DDL construct, so its\n * repair is operator-mediated instead — a guard query plus instructions,\n * not a blind-rerun-safe statement. See {@link describeRenameDataPending}.\n *\n * Single-table probe: at least one round trip for this table alone.\n * `compare()` does not call this directly — see\n * {@link precomputeRenameDataPending} for the cross-table batched path\n * that every ordinary `compare()` run takes instead (#2878). This stays\n * as the fallback for a standalone `compareTable()` call.\n */\n private async detectRenameDataPendingSingleTable(\n tableName: string,\n manifest: SchemaDefinition,\n dbSchema: SqlTableSchemaInfo,\n ): Promise<SchemaChange[]> {\n if (this.engine !== 'postgres' && this.engine !== 'sqlite') return [];\n\n const dbColumnNames = new Set(Object.keys(dbSchema.columns));\n const manifestColumnNames = new Set(Object.keys(manifest.columns));\n const orphanColumnNames = [...dbColumnNames].filter(\n (name) => !manifestColumnNames.has(name),\n );\n if (orphanColumnNames.length === 0) return [];\n\n // Declared columns that exist live and are therefore candidates for\n // \"still empty, data may be in an orphan column\" (#2874 review: only\n // these, plus the orphan columns themselves, ever need a live-data\n // probe — never the full manifest).\n const declaredCandidateNames = Object.keys(manifest.columns).filter(\n (colName) => dbSchema.columns[colName],\n );\n if (declaredCandidateNames.length === 0) return [];\n\n // #2874: this used to run one `SELECT 1 ... LIMIT 1` round trip per\n // declared column, then (for the declared columns still empty) one more\n // per type-compatible orphan column, then a third per UUID-shape check —\n // O(declared columns × orphan columns) round trips for a single table.\n // Across a realistic schema (dozens of tables, a handful of legacy\n // columns each) that is thousands of individually sub-millisecond round\n // trips per schema comparison — the statement count dominates, not any\n // one query's execution time (see #2874, matching the #2815 epic's\n // central finding).\n //\n // Two phases below, mirroring the original code's own two gates\n // (#2874 review findings F3 and its residual on the third pass): phase\n // one probes only the declared candidates. A table where every declared\n // candidate already holds data — the ordinary, healthy case — returns\n // here without ever touching an orphan column, exactly like the\n // original `if (declaredHasData) continue;` gate. Only when some\n // declared candidate is confirmed empty does phase two batch-probe the\n // orphan columns type-compatible with *those* empty candidates — an\n // orphan whose type matches nothing (or only already-populated\n // candidates) would either never produce a finding or never even be\n // reachable, so probing it would force a full-table scan (the `LIMIT 1`\n // subquery never finds a qualifying row) for nothing. Each phase is one\n // row of scalar subqueries with each column's own `LIMIT 1` early exit\n // (#2874 review finding F1), so the common case costs one round trip\n // and the rename-pending case costs two — both far below the original\n // per-column O(declared × orphan) shape. `columnsHaveNonEmptyValueBatch`\n // falls back to isolated per-column probes on a batch failure, so one\n // unresolvable column never discards the whole table's detection\n // (#2874 review finding F2) — a column absent from a map below means\n // \"could not be probed\", not \"confirmed empty\".\n const declaredHasData = await columnsHaveNonEmptyValueBatch(\n this.db,\n tableName,\n declaredCandidateNames,\n );\n const emptyDeclaredNames = declaredCandidateNames.filter(\n (colName) => !(declaredHasData.get(colName) ?? true),\n );\n if (emptyDeclaredNames.length === 0) return [];\n\n // Only probe an orphan column that is type-compatible with at least one\n // *empty* declared candidate — restores the original per-column code's\n // compatibility gate, narrowed to the columns phase one actually found\n // empty. Costs no extra round trip: every type involved is already\n // known from `manifest`/`dbSchema`, not the live data.\n const compatibleOrphanNames = orphanColumnNames.filter((orphanName) => {\n const orphanNormalized = this.normalizeType(\n dbSchema.columns[orphanName].type,\n );\n return emptyDeclaredNames.some((colName) => {\n const validatedType: SQLDataType = isValidSQLDataType(\n manifest.columns[colName].type,\n )\n ? manifest.columns[colName].type\n : 'TEXT';\n const declaredNormalized = this.normalizeType(\n this.ddlStrategy.mapType(validatedType),\n );\n const requiresShapeCheck =\n validatedType === 'UUID' && orphanNormalized === 'TEXT';\n return requiresShapeCheck || orphanNormalized === declaredNormalized;\n });\n });\n if (compatibleOrphanNames.length === 0) return [];\n\n const orphanHasData = await columnsHaveNonEmptyValueBatch(\n this.db,\n tableName,\n compatibleOrphanNames,\n );\n const hasData = new Map([...declaredHasData, ...orphanHasData]);\n\n const changes: SchemaChange[] = [];\n\n // First pass (no queries): for every declared column still empty,\n // collect its type-compatible orphan candidates and note which ones\n // need the UUID-shape probe. The shape probe is a property of the\n // *orphan column* alone (not of the declared/orphan pair), so collecting\n // every table's shape-check candidates once, across every declared\n // column, keeps the follow-up batch query to one per table too.\n type PendingCandidate = {\n colName: string;\n orphanName: string;\n isUuidCast: boolean;\n requiresShapeCheck: boolean;\n };\n const pending: PendingCandidate[] = [];\n const shapeCheckOrphans = new Set<string>();\n\n for (const [colName, colDef] of Object.entries(manifest.columns)) {\n const dbCol = dbSchema.columns[colName];\n if (!dbCol) continue; // add_column already covers a missing declared column\n // Absent from the map means the probe could not run for this column\n // (#2874 review finding F2); default to \"has data\" so it is skipped,\n // matching the original per-column `catch { continue; }` — a probe\n // failure means \"cannot confirm empty\", not \"assume empty and guess\".\n if (hasData.get(colName) ?? true) continue;\n\n const validatedType: SQLDataType = isValidSQLDataType(colDef.type)\n ? colDef.type\n : 'TEXT';\n // Branch on the manifest's *logical* type, not the engine-mapped one:\n // SQLite has no native uuid type, so `mapType('UUID')` collapses to\n // TEXT there and `declaredNormalized` alone can no longer distinguish\n // \"this is logically a UUID column\" from \"this is plain text\" (#2767\n // review). Checking `isLogicalUuid` directly keeps the shape probe in\n // scope for SQLite too, instead of silently downgrading to an\n // unvalidated same-type text copy.\n const isLogicalUuid = validatedType === 'UUID';\n const declaredNormalized = this.normalizeType(\n this.ddlStrategy.mapType(validatedType),\n );\n\n for (const orphanName of compatibleOrphanNames) {\n const orphanCol = dbSchema.columns[orphanName];\n const orphanNormalized = this.normalizeType(orphanCol.type);\n\n // Two independent questions, deliberately not conflated (#2767\n // review, P1): does this pairing need the UUID *shape probe*, and\n // does the emitted repair SQL need the `::uuid` *cast*? A logical\n // UUID column paired with a text-typed orphan always needs the\n // shape probe, on every engine — but only a physically native uuid\n // destination (PostgreSQL/DuckDB; `declaredNormalized === 'UUID'`)\n // needs `::uuid` and the NULL-only empty predicate. SQLite has no\n // native uuid type, so its logical-UUID column is still physically\n // TEXT and must get the same plain copy + `CAST(...) = ''` predicate\n // as any other TEXT column, even though it still needed the probe.\n const requiresShapeCheck = isLogicalUuid && orphanNormalized === 'TEXT';\n const isSameType = orphanNormalized === declaredNormalized;\n if (!requiresShapeCheck && !isSameType) continue;\n // Absent from the map (probe failure) defaults to \"no data\" here,\n // matching the original per-orphan `catch { continue; }`: this\n // specific candidate is excluded, without affecting any other\n // orphan or declared column (#2874 review finding F2).\n if (!(hasData.get(orphanName) ?? false)) continue;\n\n if (requiresShapeCheck) shapeCheckOrphans.add(orphanName);\n pending.push({\n colName,\n orphanName,\n isUuidCast: declaredNormalized === 'UUID',\n requiresShapeCheck,\n });\n }\n }\n\n // `columnsAllValuesUuidShapedBatch` falls back to isolated per-column\n // probes on a batch failure and never throws; a column absent from the\n // result (probe unresolvable) defaults to \"not shaped\" below, excluding\n // just that candidate (#2874 review finding F2).\n const shaped: Map<string, boolean> =\n shapeCheckOrphans.size > 0\n ? await columnsAllValuesUuidShapedBatch(\n this.db,\n this.engine,\n tableName,\n [...shapeCheckOrphans],\n )\n : new Map();\n\n const candidatesByColumn = new Map<\n string,\n { orphanName: string; isUuidCast: boolean }[]\n >();\n for (const candidate of pending) {\n if (\n candidate.requiresShapeCheck &&\n !(shaped.get(candidate.orphanName) ?? false)\n ) {\n continue;\n }\n const list = candidatesByColumn.get(candidate.colName) ?? [];\n list.push({\n orphanName: candidate.orphanName,\n isUuidCast: candidate.isUuidCast,\n });\n candidatesByColumn.set(candidate.colName, list);\n }\n\n for (const [colName, candidates] of candidatesByColumn) {\n if (candidates.length === 1) {\n changes.push(\n this.describeRenameDataPending(\n tableName,\n colName,\n candidates[0].orphanName,\n candidates[0].isUuidCast,\n ),\n );\n } else if (candidates.length > 1) {\n changes.push(\n this.describeRenameDataPendingAmbiguous(\n tableName,\n colName,\n candidates.map((c) => c.orphanName),\n ),\n );\n }\n }\n\n return changes;\n }\n\n /**\n * Batch every table's rename-pending advisory probe (#2752/#2878) into a\n * small, table-count-independent number of round trips, and populate\n * {@link renameDataPendingCache} so `compareTable()`'s per-table\n * `detectRenameDataPending()` call becomes a cache read for the rest of\n * this `compare()` run.\n *\n * Mirrors {@link detectRenameDataPendingSingleTable}'s phases exactly —\n * same gates, same type-compatibility rules, same per-column `LIMIT 1`\n * early exit — just resequenced so each phase's *query* covers every\n * table that needs it in one statement, instead of one statement per\n * table. With a 71-table schema this is the difference between a\n * 71-statement floor (#2878) and a handful of statements for the whole\n * schema, independent of table count.\n *\n * No caching beyond this: the cache this populates is cleared at the top\n * of every `compare()` call, so a live schema change is always visible\n * on the next comparison.\n */\n private async precomputeRenameDataPending(\n manifestSchemas: Record<string, SchemaDefinition>,\n existingTables: Set<string>,\n ): Promise<void> {\n this.renameDataPendingCache = new Map();\n if (this.engine !== 'postgres' && this.engine !== 'sqlite') return;\n\n interface TableCtx {\n tableName: string;\n manifest: SchemaDefinition;\n dbSchema: SqlTableSchemaInfo;\n declaredCandidateNames: string[];\n orphanColumnNames: string[];\n }\n const tables: TableCtx[] = [];\n\n // Phase 0 (no queries): gate exactly like the single-table method's own\n // early returns, using the live schema already fetched (or memoized)\n // via `getLiveSchema` — this never issues an extra round trip beyond\n // what `compareTable()` was going to do anyway.\n for (const [tableName, manifest] of Object.entries(manifestSchemas)) {\n if (!existingTables.has(tableName)) continue;\n const dbSchema = await this.getLiveSchema(tableName);\n if (!dbSchema) {\n this.renameDataPendingCache.set(tableName, []);\n continue;\n }\n\n const dbColumnNames = new Set(Object.keys(dbSchema.columns));\n const manifestColumnNames = new Set(Object.keys(manifest.columns));\n const orphanColumnNames = [...dbColumnNames].filter(\n (name) => !manifestColumnNames.has(name),\n );\n if (orphanColumnNames.length === 0) {\n this.renameDataPendingCache.set(tableName, []);\n continue;\n }\n\n const declaredCandidateNames = Object.keys(manifest.columns).filter(\n (colName) => dbSchema.columns[colName],\n );\n if (declaredCandidateNames.length === 0) {\n this.renameDataPendingCache.set(tableName, []);\n continue;\n }\n\n tables.push({\n tableName,\n manifest,\n dbSchema,\n declaredCandidateNames,\n orphanColumnNames,\n });\n }\n if (tables.length === 0) return;\n\n // Phase 1: batch the declared-candidate \"has any data\" probe across\n // every eligible table in as few round trips as the column-count cap\n // allows (typically one for a realistic schema).\n const declaredHasDataByTable =\n await this.crossTableColumnsHaveNonEmptyValueBatch(\n tables.map((t) => ({\n tableName: t.tableName,\n colNames: t.declaredCandidateNames,\n })),\n );\n\n // Determine, per table, which declared candidates are confirmed empty\n // (phase one's early exit, per table): a table where every declared\n // candidate already holds data drops out here without ever reaching an\n // orphan-column probe, exactly like the single-table method's\n // `if (emptyDeclaredNames.length === 0) return [];`.\n const phase2Tables: {\n tableName: string;\n compatibleOrphanNames: string[];\n }[] = [];\n for (const t of tables) {\n const declaredHasData =\n declaredHasDataByTable.get(t.tableName) ?? new Map<string, boolean>();\n const emptyDeclaredNames = t.declaredCandidateNames.filter(\n (colName) => !(declaredHasData.get(colName) ?? true),\n );\n if (emptyDeclaredNames.length === 0) {\n this.renameDataPendingCache.set(t.tableName, []);\n continue;\n }\n\n const compatibleOrphanNames = t.orphanColumnNames.filter((orphanName) => {\n const orphanNormalized = this.normalizeType(\n t.dbSchema.columns[orphanName].type,\n );\n return emptyDeclaredNames.some((colName) => {\n const validatedType: SQLDataType = isValidSQLDataType(\n t.manifest.columns[colName].type,\n )\n ? t.manifest.columns[colName].type\n : 'TEXT';\n const declaredNormalized = this.normalizeType(\n this.ddlStrategy.mapType(validatedType),\n );\n const requiresShapeCheck =\n validatedType === 'UUID' && orphanNormalized === 'TEXT';\n return requiresShapeCheck || orphanNormalized === declaredNormalized;\n });\n });\n if (compatibleOrphanNames.length === 0) {\n this.renameDataPendingCache.set(t.tableName, []);\n continue;\n }\n phase2Tables.push({ tableName: t.tableName, compatibleOrphanNames });\n }\n if (phase2Tables.length === 0) return;\n\n // Phase 2: batch the compatible-orphan \"has any data\" probe, again in\n // as few round trips as the cap allows, across only the tables that\n // still have a candidate after phase one.\n const orphanHasDataByTable =\n await this.crossTableColumnsHaveNonEmptyValueBatch(\n phase2Tables.map((t) => ({\n tableName: t.tableName,\n colNames: t.compatibleOrphanNames,\n })),\n );\n\n const byName = new Map(tables.map((t) => [t.tableName, t]));\n interface PendingCandidate {\n colName: string;\n orphanName: string;\n isUuidCast: boolean;\n requiresShapeCheck: boolean;\n }\n const pendingByTable = new Map<string, PendingCandidate[]>();\n const shapeCheckByTable = new Map<string, Set<string>>();\n\n // Phase 3 (no queries): identical pairing logic to the single-table\n // method, now driven by the batched hasData maps.\n for (const { tableName, compatibleOrphanNames } of phase2Tables) {\n const t = byName.get(tableName);\n if (!t) continue;\n const declaredHasData =\n declaredHasDataByTable.get(tableName) ?? new Map<string, boolean>();\n const orphanHasData =\n orphanHasDataByTable.get(tableName) ?? new Map<string, boolean>();\n const hasData = new Map([...declaredHasData, ...orphanHasData]);\n\n const pending: PendingCandidate[] = [];\n const shapeCheckOrphans = new Set<string>();\n\n for (const [colName, colDef] of Object.entries(t.manifest.columns)) {\n const dbCol = t.dbSchema.columns[colName];\n if (!dbCol) continue;\n if (hasData.get(colName) ?? true) continue;\n\n const validatedType: SQLDataType = isValidSQLDataType(colDef.type)\n ? colDef.type\n : 'TEXT';\n const isLogicalUuid = validatedType === 'UUID';\n const declaredNormalized = this.normalizeType(\n this.ddlStrategy.mapType(validatedType),\n );\n\n for (const orphanName of compatibleOrphanNames) {\n const orphanCol = t.dbSchema.columns[orphanName];\n const orphanNormalized = this.normalizeType(orphanCol.type);\n const requiresShapeCheck =\n isLogicalUuid && orphanNormalized === 'TEXT';\n const isSameType = orphanNormalized === declaredNormalized;\n if (!requiresShapeCheck && !isSameType) continue;\n if (!(hasData.get(orphanName) ?? false)) continue;\n\n if (requiresShapeCheck) shapeCheckOrphans.add(orphanName);\n pending.push({\n colName,\n orphanName,\n isUuidCast: declaredNormalized === 'UUID',\n requiresShapeCheck,\n });\n }\n }\n\n pendingByTable.set(tableName, pending);\n if (shapeCheckOrphans.size > 0) {\n shapeCheckByTable.set(tableName, shapeCheckOrphans);\n }\n }\n\n // Phase 4: batch the UUID-shape probe across every table that needs it,\n // again in as few round trips as the cap allows.\n let shapedByTable = new Map<string, Map<string, boolean>>();\n if (shapeCheckByTable.size > 0) {\n shapedByTable = await this.crossTableColumnsAllValuesUuidShapedBatch(\n [...shapeCheckByTable.entries()].map(([tableName, cols]) => ({\n tableName,\n colNames: [...cols],\n })),\n );\n }\n\n // Phase 5 (no queries): render the final advisory changes exactly like\n // the single-table method's own tail end.\n for (const [tableName, pending] of pendingByTable) {\n const shaped = shapedByTable.get(tableName) ?? new Map<string, boolean>();\n const changes: SchemaChange[] = [];\n const candidatesByColumn = new Map<\n string,\n { orphanName: string; isUuidCast: boolean }[]\n >();\n for (const candidate of pending) {\n if (\n candidate.requiresShapeCheck &&\n !(shaped.get(candidate.orphanName) ?? false)\n ) {\n continue;\n }\n const list = candidatesByColumn.get(candidate.colName) ?? [];\n list.push({\n orphanName: candidate.orphanName,\n isUuidCast: candidate.isUuidCast,\n });\n candidatesByColumn.set(candidate.colName, list);\n }\n\n for (const [colName, candidates] of candidatesByColumn) {\n if (candidates.length === 1) {\n changes.push(\n this.describeRenameDataPending(\n tableName,\n colName,\n candidates[0].orphanName,\n candidates[0].isUuidCast,\n ),\n );\n } else if (candidates.length > 1) {\n changes.push(\n this.describeRenameDataPendingAmbiguous(\n tableName,\n colName,\n candidates.map((c) => c.orphanName),\n ),\n );\n }\n }\n this.renameDataPendingCache.set(tableName, changes);\n }\n }\n\n /**\n * Cross-table variant of {@link columnsHaveNonEmptyValueBatch}: the same\n * uncorrelated-scalar-subquery, per-column `LIMIT 1` shape, but packed\n * into one row *per query* across every named table's columns instead of\n * one row per table — the #2878 lever. Chunks at\n * {@link MAX_CROSS_TABLE_PROBE_COLUMNS} columns per statement so a very\n * large schema still issues a small, bounded number of round trips\n * rather than one arbitrarily wide row.\n *\n * Falls back to the existing per-table batch (itself falling back\n * further to per-column) for any chunk whose combined statement fails,\n * so one bad table never discards another table's detection — the same\n * failure-isolation guarantee {@link columnsHaveNonEmptyValueBatch}\n * already gives per-column, extended one level up.\n */\n private async crossTableColumnsHaveNonEmptyValueBatch(\n tables: { tableName: string; colNames: string[] }[],\n ): Promise<Map<string, Map<string, boolean>>> {\n return this.crossTableProbeBatch(\n tables,\n (t) => columnsHaveNonEmptyValueBatch(this.db, t.tableName, t.colNames),\n (quotedTable, quotedCol, alias) =>\n `(SELECT 1 FROM ${quotedTable} WHERE ${nonEmptyValuePredicate(quotedCol)} ` +\n `LIMIT 1) AS ${alias}`,\n (value) => value != null,\n );\n }\n\n /**\n * Cross-table variant of {@link columnsAllValuesUuidShapedBatch} — same\n * shape as {@link crossTableColumnsHaveNonEmptyValueBatch}, engine-aware\n * invalid-shape predicate included.\n */\n private async crossTableColumnsAllValuesUuidShapedBatch(\n tables: { tableName: string; colNames: string[] }[],\n ): Promise<Map<string, Map<string, boolean>>> {\n return this.crossTableProbeBatch(\n tables,\n (t) =>\n columnsAllValuesUuidShapedBatch(\n this.db,\n this.engine,\n t.tableName,\n t.colNames,\n ),\n (quotedTable, quotedCol, alias) =>\n `(SELECT 1 FROM ${quotedTable} WHERE ${nonEmptyValuePredicate(quotedCol)} ` +\n `AND ${uuidInvalidShapePredicate(this.engine, quotedCol)} LIMIT 1) AS ${alias}`,\n (value) => value == null,\n );\n }\n\n /**\n * Shared cross-table batching engine for the two probes above. Builds one\n * `SELECT` per chunk with a scalar subquery per (table, column) pair,\n * globally aliased `t{tableIndex}_c{colIndex}` — PostgreSQL truncates a\n * `name` identifier to 63 bytes, so a positional alias is used instead of\n * the real column name for the same reason the single-table batch does\n * (#2874 review finding F2'). On a chunk's query failure, that chunk's\n * tables fall back to the single-table batch path individually (each of\n * which falls back further to per-column) rather than discarding every\n * table's result.\n *\n * Known lock-footprint change (#2878 final review F2, deferred to\n * #2887): one chunked statement takes `ACCESS SHARE` on every\n * table in that chunk *simultaneously*, where the pre-#2878 per-table\n * probe held at most one table's `ACCESS SHARE` at a time (each\n * autocommitted statement releases its lock before the next one runs).\n * That is a materially different PostgreSQL locking pattern: it is now\n * possible, in principle, for this session to be one side of a deadlock\n * with a concurrent multi-table DDL transaction (e.g. a `db:migrate` in\n * flight) in a way the old per-table probe structurally could not be.\n * PostgreSQL's deadlock detector resolves any such cycle by aborting one\n * side with a loud `deadlock detected` error — never silently, and never\n * with partial/corrupt state — so if this session is the victim, the\n * `catch` above already degrades it to the safe per-table fallback; if\n * the *other* side (the migration) is the victim, that transaction rolls\n * back cleanly and is safe to re-run. No test in this repository exercises\n * concurrent DDL against the probed tables, so this risk is unverified by\n * suite rather than disproven. Bounding it further — chunking by table\n * count as well as column count, and/or a short `lock_timeout` on these\n * statements so this session always yields first — is deferred to #2887\n * rather than fixed here.\n */\n private async crossTableProbeBatch(\n tables: { tableName: string; colNames: string[] }[],\n singleTableFallback: (t: {\n tableName: string;\n colNames: string[];\n }) => Promise<Map<string, boolean>>,\n renderSelect: (\n quotedTable: string,\n quotedCol: string,\n alias: string,\n ) => string,\n isTrue: (value: unknown) => boolean,\n ): Promise<Map<string, Map<string, boolean>>> {\n const nonEmpty = tables.filter((t) => t.colNames.length > 0);\n if (nonEmpty.length === 0) return new Map();\n\n // Slice each table's own column list to the cap first (PR #2888 review):\n // packing whole tables into chunks, as this used to do, bounds the\n // cumulative width across *many* normal-sized tables but not a single\n // pathologically wide one — a table with more probed columns than the\n // cap would still land in one oversized chunk by itself, since the\n // \"start a new chunk\" gate below only fires when the current chunk\n // already holds something. Slicing up front means no single chunk\n // entry is ever wider than the cap, so the same table can appear in\n // more than one chunk; `result` (built across every chunk, table names\n // merged via `result.get(tableName) ?? new Map()`) already gathers\n // those pieces back into one map per table either way.\n const slices: { tableName: string; colNames: string[] }[] = [];\n for (const t of nonEmpty) {\n for (\n let i = 0;\n i < t.colNames.length;\n i += SchemaComparer.MAX_CROSS_TABLE_PROBE_COLUMNS\n ) {\n slices.push({\n tableName: t.tableName,\n colNames: t.colNames.slice(\n i,\n i + SchemaComparer.MAX_CROSS_TABLE_PROBE_COLUMNS,\n ),\n });\n }\n }\n\n const chunks: { tableName: string; colNames: string[] }[][] = [];\n let current: { tableName: string; colNames: string[] }[] = [];\n let currentWidth = 0;\n for (const slice of slices) {\n if (\n currentWidth + slice.colNames.length >\n SchemaComparer.MAX_CROSS_TABLE_PROBE_COLUMNS &&\n current.length > 0\n ) {\n chunks.push(current);\n current = [];\n currentWidth = 0;\n }\n current.push(slice);\n currentWidth += slice.colNames.length;\n }\n if (current.length > 0) chunks.push(current);\n\n const result = new Map<string, Map<string, boolean>>();\n for (const chunk of chunks) {\n const selects: string[] = [];\n const index: { tableName: string; colName: string; alias: string }[] = [];\n chunk.forEach((t, ti) => {\n const quotedTable = this.quoteIdentifier(t.tableName);\n t.colNames.forEach((colName, ci) => {\n const quotedCol = this.quoteIdentifier(colName);\n const alias = `t${ti}_c${ci}`;\n selects.push(renderSelect(quotedTable, quotedCol, alias));\n index.push({ tableName: t.tableName, colName, alias });\n });\n });\n try {\n const queryResult = await this.db.query(`SELECT ${selects.join(', ')}`);\n const row = (queryResult.rows?.[0] ?? {}) as Record<string, unknown>;\n for (const { tableName, colName, alias } of index) {\n const m = result.get(tableName) ?? new Map<string, boolean>();\n m.set(colName, isTrue(row[alias]));\n result.set(tableName, m);\n }\n } catch {\n // Merge into any existing entry rather than overwriting it: a wide\n // table sliced across two chunk entries in this same chunk (or a\n // prior chunk) already has a partial result in `result`, and a\n // plain `result.set` here would discard it instead of adding this\n // slice's columns alongside it.\n for (const t of chunk) {\n const m = result.get(t.tableName) ?? new Map<string, boolean>();\n const fallback = await singleTableFallback(t);\n for (const [colName, value] of fallback) m.set(colName, value);\n result.set(t.tableName, m);\n }\n }\n }\n return result;\n }\n /**\n * Render the repair for one rename-data-pending pair (#2752): copy\n * non-empty `oldColumn` into `newColumn` only where `newColumn` is still\n * empty (casting to `uuid` when the new column is native uuid, otherwise a\n * plain copy), then drop `oldColumn`.\n *\n * Idempotency review findings and the fix:\n *\n * - (P1) The `newColumn`-is-empty predicate previously compared any\n * non-uuid destination to `''` (`col IS NULL OR col = ''`), which is\n * only valid for text-affinity types — PostgreSQL rejects `col = ''`\n * for `integer`/`boolean`/`timestamp`/etc. before a same-type rename\n * pair of one of those types ever copies anything. Fixed by comparing\n * `CAST(col AS TEXT) = ''` instead, mirroring the same portable\n * emptiness check {@link columnsHaveNonEmptyValueBatch} already uses for\n * detection, so the predicate is valid for every column type.\n * - (P2) An `UPDATE` unconditionally naming `oldColumn` fails once that\n * column is gone, on either engine, regardless of the DROP's own\n * idempotency — a guard query placed before the DROP (or even before\n * the UPDATE) still leaves a human decision between \"run\" and \"don't\",\n * which is not genuine SQL-level idempotency. PostgreSQL CAN express a\n * truly self-contained, unconditionally-safe-to-rerun statement here:\n * an anonymous `DO $$ ... $$` block (the same idiom this file already\n * uses for `generatePostgresIntegerPreflightSQL`) that checks\n * `information_schema.columns` and only runs the UPDATE + DROP when the\n * old column still exists — one executable statement, no operator\n * judgment required, safe to rerun any number of times. SQLite has no\n * procedural block or conditional-DDL construct at all (confirmed\n * against a real connection), so there the contract is explicitly\n * downgraded from \"idempotent SQL\" to \"operator-mediated\": a guard\n * query plus instructions, not a single statement that is itself safe\n * to blindly rerun.\n *\n * This is report-only either way: `db:migrate` never executes it.\n */\n private describeRenameDataPending(\n tableName: string,\n newColumn: string,\n oldColumn: string,\n newIsNativeUuid: boolean,\n ): SchemaChange {\n const quotedTable = this.quoteIdentifier(tableName);\n const quotedNew = this.quoteIdentifier(newColumn);\n const quotedOld = this.quoteIdentifier(oldColumn);\n const oldValue = newIsNativeUuid ? `${quotedOld}::uuid` : quotedOld;\n const newEmptyPredicate = newIsNativeUuid\n ? `${quotedNew} IS NULL`\n : `(${quotedNew} IS NULL OR CAST(${quotedNew} AS TEXT) = '')`;\n const copySql =\n `UPDATE ${quotedTable} SET ${quotedNew} = ${oldValue} ` +\n `WHERE ${newEmptyPredicate} AND ${quotedOld} IS NOT NULL AND CAST(${quotedOld} AS TEXT) <> ''`;\n\n let suggestedSql: string[];\n let dropIdempotencyNote: string;\n\n if (this.engine === 'postgres') {\n // A single anonymous block: checks the old column's existence once\n // and only then runs the UPDATE and the DROP, inside the same\n // condition — genuinely idempotent, safe to rerun any number of\n // times with no operator judgment involved.\n // Scoped to `public` (matches this file's other information_schema\n // queries, e.g. the table-listing query below): unscoped, this check\n // matches any schema the connection can see, so a same-named\n // table/column pair in another schema could make the guard report\n // \"still present\" (or, symmetrically, mask an already-dropped\n // relation on `public`) regardless of the actual target relation's\n // state — silently breaking the no-op contract this block exists to\n // provide (#2752 review finding).\n const existsCheck =\n `EXISTS (SELECT 1 FROM information_schema.columns ` +\n `WHERE table_schema = 'public' AND table_name = ${this.quoteLiteral(tableName)} AND column_name = ${this.quoteLiteral(oldColumn)})`;\n const dropSql = `ALTER TABLE ${quotedTable} DROP COLUMN ${quotedOld}`;\n suggestedSql = [\n `DO $$ BEGIN IF ${existsCheck} THEN ${copySql}; ${dropSql}; END IF; END $$`,\n ];\n dropIdempotencyNote =\n 'This single statement is idempotent: rerunning it after the ' +\n `rename is complete is a genuine no-op, because it only touches ` +\n `${oldColumn} when the block finds it still exists.`;\n } else {\n // SQLite has no procedural block or conditional-DDL construct at\n // all, so true statement-level idempotency is not achievable in\n // plain SQL here. The contract is explicitly operator-mediated\n // instead: a guard query, then the two statements to run only when\n // it reports the old column present.\n const guardSql =\n `SELECT count(*) AS old_column_still_present ` +\n `FROM pragma_table_info(${this.quoteLiteral(tableName)}) WHERE name = ${this.quoteLiteral(oldColumn)}`;\n const dropSql = this.generateDropColumnSQL(tableName, oldColumn);\n suggestedSql = [guardSql, copySql, dropSql];\n dropIdempotencyNote =\n 'SQLite has no conditional-DDL construct, so this is operator-mediated, ' +\n `not self-contained idempotent SQL: run the guard query first, and only run ` +\n `the UPDATE and DROP below when it reports ${oldColumn} still present.`;\n }\n\n return {\n type: 'rename_data_pending',\n table: tableName,\n name: newColumn,\n mismatch: {\n expected: `data in ${newColumn}`,\n actual: `data appears to still be in ${oldColumn}`,\n },\n advisory: {\n severity: 'warning',\n message:\n `${tableName}.${newColumn} is declared but empty, while undeclared column ${tableName}.${oldColumn} ` +\n 'holds data of a compatible type. This looks like a framework field rename whose data was never ' +\n `moved (db:migrate adds columns additively and never drops the old one). ${dropIdempotencyNote}`,\n suggestedSql,\n },\n };\n }\n\n /**\n * Render the advisory for an ambiguous rename-data-pending match (#2767\n * review): more than one orphan column is a populated, type-compatible\n * candidate for the same empty declared column, so the real rename source\n * cannot be inferred. Unlike {@link describeRenameDataPending}, this\n * carries no `suggestedSql` — running a per-candidate copy-and-drop would\n * let output order decide which candidate's data wins and then drop every\n * one of them, which is not a repair an advisory should suggest blindly.\n */\n private describeRenameDataPendingAmbiguous(\n tableName: string,\n newColumn: string,\n candidateColumns: string[],\n ): SchemaChange {\n const candidateList = candidateColumns\n .map((name) => `${tableName}.${name}`)\n .join(', ');\n return {\n type: 'rename_data_pending',\n table: tableName,\n name: newColumn,\n mismatch: {\n expected: `data in ${newColumn}`,\n actual: `data appears to still be in one of several columns: ${candidateList}`,\n },\n advisory: {\n severity: 'warning',\n message:\n `${tableName}.${newColumn} is declared but empty, while ${candidateColumns.length} undeclared ` +\n `columns of a compatible type hold data (${candidateList}). This looks like a framework field ` +\n 'rename, but which column is the actual rename source is ambiguous — no suggested repair SQL is ' +\n 'printed. An operator must identify the correct source column and write its own targeted copy-then-drop.',\n },\n };\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)::bigint`;\n } else if (\n manifestNormalized === 'INTEGER' &&\n dbNormalized === 'REAL'\n ) {\n typeClause += ` USING ${quotedCol}::bigint`;\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 if (this.engine === 'postgres' && idx.nullsNotDistinct) {\n return renderNullEqualConflictIndex(tableName, idx);\n }\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 // Add table creation (requires full DDL generation - not included here)\n // This is typically handled by ensureSchema()\n return getSQLFromChanges(diff.changes);\n}\n\n/**\n * Executable SQL for an explicit subset of changes, in the order given.\n * Callers that have to interleave diff changes with statements produced\n * elsewhere — the orchestrator, which emits `CREATE TABLE` and deferred\n * foreign keys of its own — partition `diff.changes` by\n * {@link SchemaChange.phase} and render each part through this helper.\n */\nexport function getSQLFromChanges(changes: readonly SchemaChange[]): string[] {\n const statements: string[] = [];\n for (const change of changes) {\n if (change.type !== 'type_mismatch' && !isAdvisoryOnlyChange(change)) {\n statements.push(\n ...(change.sqlStatements ?? (change.sql ? [change.sql] : [])),\n );\n }\n }\n return statements;\n}\n"],"mappings":"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"}
|