@happyvertical/smrt-core 0.51.39 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +8 -2
  3. package/agents/model-moves.md +114 -0
  4. package/agents/object-runtime.md +3 -1
  5. package/agents/run-once.md +16 -7
  6. package/agents/schema-paths.md +79 -0
  7. package/dist/cascade.d.ts.map +1 -1
  8. package/dist/cascade.js +6 -3
  9. package/dist/cascade.js.map +1 -1
  10. package/dist/collection.d.ts +10 -1
  11. package/dist/collection.d.ts.map +1 -1
  12. package/dist/collection.js +14 -4
  13. package/dist/collection.js.map +1 -1
  14. package/dist/errors.d.ts +18 -0
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/errors.js +21 -0
  17. package/dist/errors.js.map +1 -1
  18. package/dist/generators/preflight-route.d.ts.map +1 -1
  19. package/dist/generators/preflight-route.js +1 -1
  20. package/dist/generators/preflight-route.js.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/knowledge.d.ts.map +1 -1
  25. package/dist/knowledge.js +3 -0
  26. package/dist/knowledge.js.map +1 -1
  27. package/dist/manifest/static-manifest.d.ts.map +1 -1
  28. package/dist/manifest/static-manifest.js +9 -1
  29. package/dist/manifest/static-manifest.js.map +1 -1
  30. package/dist/manifest/store.js +1 -1
  31. package/dist/manifest/store.js.map +1 -1
  32. package/dist/manifest.json +9 -1
  33. package/dist/migrations/differ.d.ts +56 -0
  34. package/dist/migrations/differ.d.ts.map +1 -1
  35. package/dist/migrations/differ.js +152 -27
  36. package/dist/migrations/differ.js.map +1 -1
  37. package/dist/migrations/index.d.ts +1 -0
  38. package/dist/migrations/index.d.ts.map +1 -1
  39. package/dist/migrations/index.js +2 -1
  40. package/dist/migrations/null-equal-indexes.js +1 -1
  41. package/dist/migrations/orchestrate.d.ts +7 -0
  42. package/dist/migrations/orchestrate.d.ts.map +1 -1
  43. package/dist/migrations/orchestrate.js +4 -2
  44. package/dist/migrations/orchestrate.js.map +1 -1
  45. package/dist/migrations/qualified-name-aliases.d.ts +134 -0
  46. package/dist/migrations/qualified-name-aliases.d.ts.map +1 -0
  47. package/dist/migrations/qualified-name-aliases.js +294 -0
  48. package/dist/migrations/qualified-name-aliases.js.map +1 -0
  49. package/dist/migrations/tracker.d.ts +29 -1
  50. package/dist/migrations/tracker.d.ts.map +1 -1
  51. package/dist/migrations/tracker.js +92 -7
  52. package/dist/migrations/tracker.js.map +1 -1
  53. package/dist/migrations.js +2 -1
  54. package/dist/object.d.ts +128 -0
  55. package/dist/object.d.ts.map +1 -1
  56. package/dist/object.js +333 -44
  57. package/dist/object.js.map +1 -1
  58. package/dist/polymorphic-association.d.ts +18 -0
  59. package/dist/polymorphic-association.d.ts.map +1 -1
  60. package/dist/polymorphic-association.js +27 -2
  61. package/dist/polymorphic-association.js.map +1 -1
  62. package/dist/registry/class-registration.d.ts.map +1 -1
  63. package/dist/registry/class-registration.js +27 -4
  64. package/dist/registry/class-registration.js.map +1 -1
  65. package/dist/registry/name-resolver.d.ts +8 -2
  66. package/dist/registry/name-resolver.d.ts.map +1 -1
  67. package/dist/registry/name-resolver.js +17 -8
  68. package/dist/registry/name-resolver.js.map +1 -1
  69. package/dist/registry/qualified-name-aliases.d.ts +109 -0
  70. package/dist/registry/qualified-name-aliases.d.ts.map +1 -0
  71. package/dist/registry/qualified-name-aliases.js +286 -0
  72. package/dist/registry/qualified-name-aliases.js.map +1 -0
  73. package/dist/registry/relationship-graph.d.ts.map +1 -1
  74. package/dist/registry/relationship-graph.js +1 -1
  75. package/dist/registry/relationship-graph.js.map +1 -1
  76. package/dist/registry/schema-builder.d.ts.map +1 -1
  77. package/dist/registry/schema-builder.js +7 -5
  78. package/dist/registry/schema-builder.js.map +1 -1
  79. package/dist/registry/types.d.ts +26 -0
  80. package/dist/registry/types.d.ts.map +1 -1
  81. package/dist/registry.d.ts +127 -6
  82. package/dist/registry.d.ts.map +1 -1
  83. package/dist/registry.js +234 -14
  84. package/dist/registry.js.map +1 -1
  85. package/dist/scanner/index.d.ts +1 -1
  86. package/dist/scanner/index.d.ts.map +1 -1
  87. package/dist/scanner/manifest-generator.d.ts +14 -2
  88. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  89. package/dist/scanner/manifest-generator.js +48 -9
  90. package/dist/scanner/manifest-generator.js.map +1 -1
  91. package/dist/scanner/types.d.ts +21 -0
  92. package/dist/scanner/types.d.ts.map +1 -1
  93. package/dist/scanner/types.js.map +1 -1
  94. package/dist/schema/conflict-target.d.ts +68 -11
  95. package/dist/schema/conflict-target.d.ts.map +1 -1
  96. package/dist/schema/conflict-target.js +75 -12
  97. package/dist/schema/conflict-target.js.map +1 -1
  98. package/dist/schema/foreign-key-ddl.d.ts +17 -1
  99. package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
  100. package/dist/schema/foreign-key-ddl.js +29 -3
  101. package/dist/schema/foreign-key-ddl.js.map +1 -1
  102. package/dist/schema/foreign-key-policy.d.ts +8 -0
  103. package/dist/schema/foreign-key-policy.d.ts.map +1 -1
  104. package/dist/schema/foreign-key-policy.js +2 -1
  105. package/dist/schema/foreign-key-policy.js.map +1 -1
  106. package/dist/schema/generator.d.ts +7 -0
  107. package/dist/schema/generator.d.ts.map +1 -1
  108. package/dist/schema/generator.js +18 -10
  109. package/dist/schema/generator.js.map +1 -1
  110. package/dist/schema/text-cast-probe.d.ts +50 -1
  111. package/dist/schema/text-cast-probe.d.ts.map +1 -1
  112. package/dist/schema/text-cast-probe.js +46 -11
  113. package/dist/schema/text-cast-probe.js.map +1 -1
  114. package/dist/schema/types.d.ts +6 -0
  115. package/dist/schema/types.d.ts.map +1 -1
  116. package/dist/scripts/browser-plain-json-regression.mjs +4 -1
  117. package/dist/smrt-knowledge.json +22 -11
  118. package/dist/utils/qualified-names.d.ts +11 -0
  119. package/dist/utils/qualified-names.d.ts.map +1 -1
  120. package/dist/utils/qualified-names.js +21 -1
  121. package/dist/utils/qualified-names.js.map +1 -1
  122. package/package.json +11 -11
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qualified-name-aliases.js","names":[],"sources":["../../src/migrations/qualified-name-aliases.ts"],"sourcesContent":["/**\n * Stored references to deprecated qualified names (#3338).\n *\n * `@smrt({ previousQualifiedNames })` keeps an old qualified name resolving\n * after a class moves package; it never rewrites stored data. This module is\n * the operator side of that bridge:\n *\n * - {@link countLegacyQualifiedNameReferences} — read-only counts of rows that\n * still store an old name, which `smrt doctor --db` reports so a consumer\n * can see when an alias is safe to remove;\n * - {@link backfillLegacyQualifiedNames} — an opt-in, idempotent rewrite of\n * those rows to the current name. It never runs automatically.\n *\n * Stored references covered:\n *\n * - `meta_type` of every registered `SmrtPolymorphicAssociation` subclass\n * table (any class can be a polymorphic target, so every alias is counted);\n * - `_meta_type` of an STI table whose family includes an aliased class.\n *\n * Removing an alias is a breaking change for any row this still counts.\n */\n\nimport { createHash } from 'node:crypto';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ObjectRegistry } from '../registry';\nimport { isSmrtCollectionExtendsName } from '../registry/collection-resolution.js';\nimport { isFrameworkBaseClass } from '../registry/framework-base-classes.js';\nimport {\n IMPLICIT_TENANT_COLUMN,\n IMPLICIT_TENANT_FIELD,\n} from '../schema/conflict-target.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { tableExists } from '../system/compatibility.js';\nimport { toSnakeCase } from '../utils/naming.js';\nimport { BackfillTracker } from './backfill-tracker.js';\n\n/** Prefix of the `_smrt_backfills` marker {@link backfillLegacyQualifiedNames} records. */\nexport const LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX =\n '@happyvertical/smrt-core:qualified-name-aliases:v1';\n\n/** One table column that can store a class's qualified name. */\nexport interface LegacyQualifiedNameTarget {\n /** Registered class that owns the table (qualified when available). */\n className: string;\n table: string;\n /** `meta_type` (polymorphic association) or `_meta_type` (STI discriminator). */\n column: string;\n kind: 'polymorphic' | 'sti';\n /** Deprecated names this column may still hold. */\n aliases: string[];\n /** Tenant column of the table, when tenant-owned. */\n tenantColumn?: string;\n /**\n * Upsert identity columns that include `column`. A rewrite that would\n * duplicate a current-name row on these columns is skipped, never forced.\n */\n conflictColumns?: string[];\n}\n\n/** Rows of one target that still store one deprecated name. */\nexport interface LegacyQualifiedNameReference {\n table: string;\n column: string;\n kind: 'polymorphic' | 'sti';\n alias: string;\n current: string;\n count: number;\n}\n\nexport interface LegacyQualifiedNameReport {\n /** Declared alias → current qualified name, sorted by alias. */\n aliases: Array<{ alias: string; current: string }>;\n /** Non-zero counts only, sorted by table, column, alias. */\n references: LegacyQualifiedNameReference[];\n /** Sum of `references[].count`. */\n total: number;\n /** Target tables absent from this database (nothing to count). */\n missingTables: string[];\n /** Target tables excluded from a tenant-scoped run (no tenant column). */\n untenantedTables: string[];\n}\n\nexport interface LegacyQualifiedNameOptions {\n /**\n * Restrict counting/rewriting to one tenant's rows. Tables without a tenant\n * column are then skipped (reported in `untenantedTables`), never touched.\n */\n tenantId?: string;\n /** Engine hint when the adapter URL does not identify it. */\n engineHint?: string;\n}\n\nexport interface LegacyQualifiedNameBackfillOptions\n extends LegacyQualifiedNameOptions {\n /** Report what would change without writing (no marker is recorded). */\n dryRun?: boolean;\n /** Re-run even when this alias set's marker is already recorded. */\n force?: boolean;\n}\n\nexport interface LegacyQualifiedNameBackfillResult {\n /** `false` when the marker was already recorded (and `force` was unset) or on a dry run. */\n ran: boolean;\n dryRun: boolean;\n /** The `_smrt_backfills` marker for this alias set and scope. */\n backfillName: string;\n /** Counts before the rewrite. */\n before: LegacyQualifiedNameReport;\n /** Rows rewritten per table/column/alias. */\n rewritten: LegacyQualifiedNameReference[];\n /**\n * Rows left under their old name because the rewrite would duplicate a row\n * already stored under the current name (resolve those by hand, then re-run).\n */\n skippedDuplicates: LegacyQualifiedNameReference[];\n /** Whether the marker was recorded (only when nothing remains in scope). */\n recorded: boolean;\n}\n\ntype AliasPair = { alias: string; current: string };\n\nfunction compareText(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\nfunction aliasPairs(): AliasPair[] {\n return [...ObjectRegistry.getQualifiedNameAliases()].map(\n ([alias, current]) => ({ alias, current }),\n );\n}\n\nfunction quoteIdentifier(name: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {\n throw new Error(`Refusing to quote unexpected identifier \"${name}\"`);\n }\n return `\"${name}\"`;\n}\n\nfunction isPolymorphicAssociationFields(fields: Map<string, unknown>): boolean {\n // Same rule as delete planning (cascade.ts): all three columns\n // `SmrtPolymorphicAssociation` contributes.\n return fields.has('metaType') && fields.has('metaId') && fields.has('role');\n}\n\nfunction safeConflictColumns(className: string): string[] {\n try {\n return ObjectRegistry.getConflictColumns(className).map((column) =>\n toSnakeCase(column),\n );\n } catch {\n return [];\n }\n}\n\nfunction safeTenantColumn(className: string): string | undefined {\n let declared: string | undefined;\n try {\n declared = ObjectRegistry.getTenantColumn(className);\n } catch {\n declared = undefined;\n }\n if (declared) return declared;\n // A `tenantId` field makes rows tenant-owned without any declaration.\n return ObjectRegistry.getFields(className).has(IMPLICIT_TENANT_FIELD)\n ? IMPLICIT_TENANT_COLUMN\n : undefined;\n}\n\n/**\n * Every table column that can store a deprecated qualified name, derived from\n * the booted registry. Empty when no class declares `previousQualifiedNames`.\n */\nexport function collectLegacyQualifiedNameTargets(): LegacyQualifiedNameTarget[] {\n const pairs = aliasPairs();\n if (pairs.length === 0) return [];\n const allAliases = pairs.map((pair) => pair.alias);\n const byKey = new Map<string, LegacyQualifiedNameTarget>();\n\n const add = (target: LegacyQualifiedNameTarget) => {\n const key = `${target.table}\\u0000${target.column}`;\n const existing = byKey.get(key);\n if (!existing) {\n byKey.set(key, target);\n return;\n }\n existing.aliases = [\n ...new Set([...existing.aliases, ...target.aliases]),\n ].sort(compareText);\n };\n\n for (const className of ObjectRegistry.getQualifiedClassNames()) {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) continue;\n if (isSmrtCollectionExtendsName(registered.extends)) continue;\n if (isFrameworkBaseClass(registered.name, registered.packageName)) continue;\n if (!isPolymorphicAssociationFields(registered.fields)) continue;\n const table = ObjectRegistry.getTableName(className);\n if (!table) continue;\n const conflictColumns = safeConflictColumns(className);\n const tenantColumn = safeTenantColumn(className);\n add({\n className,\n table,\n column: 'meta_type',\n kind: 'polymorphic',\n aliases: [...allAliases].sort(compareText),\n ...(tenantColumn ? { tenantColumn } : {}),\n ...(conflictColumns.includes('meta_type') ? { conflictColumns } : {}),\n });\n }\n\n for (const { alias, current } of pairs) {\n if (ObjectRegistry.getTableStrategy(current) !== 'sti') continue;\n const table = ObjectRegistry.getTableName(current);\n if (!table) continue;\n const conflictColumns = safeConflictColumns(current);\n const tenantColumn = safeTenantColumn(current);\n add({\n className: current,\n table,\n column: '_meta_type',\n kind: 'sti',\n aliases: [alias],\n ...(tenantColumn ? { tenantColumn } : {}),\n ...(conflictColumns.includes('_meta_type') ? { conflictColumns } : {}),\n });\n }\n\n return [...byKey.values()].sort(\n (left, right) =>\n compareText(left.table, right.table) ||\n compareText(left.column, right.column),\n );\n}\n\nfunction rowsOf(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) return rows as Record<string, unknown>[];\n }\n return [];\n}\n\nfunction resolveEngine(db: DatabaseInterface, engineHint?: string): string {\n const withConfig = db as DatabaseInterface & {\n config?: { url?: string; type?: string };\n type?: string;\n };\n return detectEngine(\n db.url || withConfig.config?.url || '',\n engineHint || withConfig.type || withConfig.config?.type,\n );\n}\n\nasync function countTarget(\n db: DatabaseInterface,\n target: LegacyQualifiedNameTarget,\n tenantId: string | undefined,\n): Promise<Map<string, number>> {\n const counts = new Map<string, number>();\n if (target.aliases.length === 0) return counts;\n const column = quoteIdentifier(target.column);\n const params: unknown[] = [...target.aliases];\n let sql =\n `SELECT ${column} AS name, COUNT(*) AS n FROM ${quoteIdentifier(target.table)} ` +\n `WHERE ${column} IN (${target.aliases.map(() => '?').join(', ')})`;\n if (tenantId !== undefined && target.tenantColumn) {\n sql += ` AND ${quoteIdentifier(target.tenantColumn)} = ?`;\n params.push(tenantId);\n }\n sql += ` GROUP BY ${column}`;\n for (const row of rowsOf(await db.query(sql, ...params))) {\n const count = Number(row.n ?? 0);\n if (count > 0) counts.set(String(row.name), count);\n }\n return counts;\n}\n\n/**\n * Count rows that still store a deprecated qualified name. Read-only.\n *\n * Absent tables are reported in `missingTables`, not as errors: the registry\n * can carry classes this database never migrated.\n */\nexport async function countLegacyQualifiedNameReferences(\n db: DatabaseInterface,\n options: LegacyQualifiedNameOptions = {},\n): Promise<LegacyQualifiedNameReport> {\n const pairs = aliasPairs();\n const currentByAlias = new Map(pairs.map((pair) => [pair.alias, pair]));\n const report: LegacyQualifiedNameReport = {\n aliases: pairs,\n references: [],\n total: 0,\n missingTables: [],\n untenantedTables: [],\n };\n const engine = resolveEngine(db, options.engineHint);\n\n for (const target of collectLegacyQualifiedNameTargets()) {\n if (options.tenantId !== undefined && !target.tenantColumn) {\n if (!report.untenantedTables.includes(target.table)) {\n report.untenantedTables.push(target.table);\n }\n continue;\n }\n if (!(await tableExists(db, target.table, engine))) {\n if (!report.missingTables.includes(target.table)) {\n report.missingTables.push(target.table);\n }\n continue;\n }\n const counts = await countTarget(db, target, options.tenantId);\n for (const alias of target.aliases) {\n const count = counts.get(alias) ?? 0;\n if (count === 0) continue;\n report.references.push({\n table: target.table,\n column: target.column,\n kind: target.kind,\n alias,\n current: currentByAlias.get(alias)?.current ?? alias,\n count,\n });\n report.total += count;\n }\n }\n\n report.missingTables.sort(compareText);\n report.untenantedTables.sort(compareText);\n return report;\n}\n\n/**\n * Stable `_smrt_backfills` marker for the declared alias set and scope. A new\n * alias (another model move) produces a new marker, so the backfill runs\n * again for it.\n */\nexport function legacyQualifiedNamesBackfillName(\n pairs: Array<{ alias: string; current: string }> = aliasPairs(),\n tenantId?: string,\n): string {\n const digest = createHash('sha256')\n .update(\n [...pairs]\n .sort((left, right) => compareText(left.alias, right.alias))\n .map(({ alias, current }) => `${alias}=>${current}`)\n .join('\\n'),\n )\n .digest('hex')\n .slice(0, 16);\n return tenantId === undefined\n ? `${LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX}:${digest}`\n : `${LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX}:${digest}:tenant:${tenantId}`;\n}\n\nfunction nullSafeEquals(engine: string, left: string, right: string): string {\n return engine === 'sqlite'\n ? `${left} IS ${right}`\n : `${left} IS NOT DISTINCT FROM ${right}`;\n}\n\nfunction buildRewriteStatement(\n engine: string,\n target: LegacyQualifiedNameTarget,\n tenantId: string | undefined,\n alias: string,\n current: string,\n): { sql: string; params: unknown[] } {\n const table = quoteIdentifier(target.table);\n const column = quoteIdentifier(target.column);\n const params: unknown[] = [current, alias];\n let sql = `UPDATE ${table} SET ${column} = ? WHERE ${column} = ?`;\n if (tenantId !== undefined && target.tenantColumn) {\n sql += ` AND ${quoteIdentifier(target.tenantColumn)} = ?`;\n params.push(tenantId);\n }\n const identity = (target.conflictColumns ?? []).filter(\n (name) => name !== target.column,\n );\n if (target.conflictColumns) {\n // Never rewrite a row onto the identity of one already stored under the\n // current name: that is a duplicate association, which an operator must\n // resolve, not a unique-violation the rewrite should provoke.\n const correlated = identity\n .map((name) =>\n nullSafeEquals(\n engine,\n `__smrt_current.${quoteIdentifier(name)}`,\n `${table}.${quoteIdentifier(name)}`,\n ),\n )\n .join(' AND ');\n sql +=\n ` AND NOT EXISTS (SELECT 1 FROM ${table} AS __smrt_current ` +\n `WHERE __smrt_current.${column} = ?${correlated ? ` AND ${correlated}` : ''})`;\n params.push(current);\n }\n return { sql, params };\n}\n\n/**\n * Rewrite stored deprecated qualified names to their current names.\n *\n * Opt-in and idempotent: it never runs on its own, rewrites only rows whose\n * value is exactly a declared old name, and records a `_smrt_backfills`\n * marker (per alias set and tenant scope) once nothing in scope remains, so a\n * second call is a no-op unless `force` is set. Rows are rewritten inside one\n * transaction where the adapter provides one; the tenant column, ids and all\n * other values are never touched, and a tenant-scoped run only touches that\n * tenant's rows in tenant-owned tables. A row whose rewrite would duplicate a\n * row already stored under the current name is skipped and reported.\n */\nexport async function backfillLegacyQualifiedNames(\n db: DatabaseInterface,\n options: LegacyQualifiedNameBackfillOptions = {},\n): Promise<LegacyQualifiedNameBackfillResult> {\n const pairs = aliasPairs();\n const backfillName = legacyQualifiedNamesBackfillName(\n pairs,\n options.tenantId,\n );\n const dryRun = options.dryRun === true;\n const before = await countLegacyQualifiedNameReferences(db, options);\n const result: LegacyQualifiedNameBackfillResult = {\n ran: false,\n dryRun,\n backfillName,\n before,\n rewritten: [],\n skippedDuplicates: [],\n recorded: false,\n };\n if (pairs.length === 0 || dryRun) return result;\n\n const tracker = new BackfillTracker({ db });\n if (!options.force && (await tracker.isApplied(backfillName))) {\n return result;\n }\n\n const engine = resolveEngine(db, options.engineHint);\n const targets = new Map(\n collectLegacyQualifiedNameTargets().map((target) => [\n `${target.table}\\u0000${target.column}`,\n target,\n ]),\n );\n\n const rewrite = async (handle: DatabaseInterface) => {\n for (const reference of before.references) {\n const target = targets.get(`${reference.table}\\u0000${reference.column}`);\n if (!target) continue;\n const { sql, params } = buildRewriteStatement(\n engine,\n target,\n options.tenantId,\n reference.alias,\n reference.current,\n );\n await handle.query(sql, ...params);\n }\n };\n if (typeof db.transaction === 'function') {\n await db.transaction(async (tx) => rewrite(tx as DatabaseInterface));\n } else {\n await rewrite(db);\n }\n\n const after = await countLegacyQualifiedNameReferences(db, options);\n const remaining = new Map(\n after.references.map((reference) => [\n `${reference.table}\\u0000${reference.column}\\u0000${reference.alias}`,\n reference,\n ]),\n );\n for (const reference of before.references) {\n const left = remaining.get(\n `${reference.table}\\u0000${reference.column}\\u0000${reference.alias}`,\n );\n const rewrittenCount = reference.count - (left?.count ?? 0);\n if (rewrittenCount > 0) {\n result.rewritten.push({ ...reference, count: rewrittenCount });\n }\n }\n result.skippedDuplicates = after.references;\n result.ran = true;\n\n if (after.total === 0) {\n await tracker.recordApplied(backfillName, {\n description:\n 'Stored deprecated qualified names rewritten to current names: ' +\n pairs.map(({ alias, current }) => `${alias} -> ${current}`).join(', '),\n packageName: '@happyvertical/smrt-core',\n });\n result.recorded = true;\n }\n return result;\n}\n"],"mappings":"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"}
@@ -229,6 +229,25 @@ export declare class MigrationTracker {
229
229
  * Exported for unit testing.
230
230
  */
231
231
  export declare function buildConcurrentIndexPlan(definitions: MigrationDefinition[], useConcurrentIndexes: boolean): ConcurrentIndexPlan;
232
+ /**
233
+ * Keep a unique index in place while `--postgres-safe` recreates it.
234
+ *
235
+ * A same-name shape-drift repair is `DROP INDEX` followed by `CREATE UNIQUE
236
+ * INDEX` under the same name. Run CONCURRENTLY, the drop commits before the
237
+ * build starts, leaving a window with NO unique index: every
238
+ * `ON CONFLICT (…)` upsert fails (42P10) and plain inserts can write
239
+ * duplicates, which then make the build itself fail. This rewrites the drop
240
+ * into build-then-swap — `CREATE UNIQUE INDEX CONCURRENTLY` under a
241
+ * temporary name, then `DROP INDEX CONCURRENTLY` of the old one, then
242
+ * `ALTER INDEX … RENAME` — so the table always has a unique index over the
243
+ * old or the new key. The later create keeps its `IF NOT EXISTS` and becomes
244
+ * a no-op. Only rewritten when the create carries `IF NOT EXISTS` (the
245
+ * differ's DDL always does).
246
+ *
247
+ * @param lists - concurrent statement lists in execution order
248
+ * @returns the rewritten lists (same length and order)
249
+ */
250
+ export declare function swapUniqueIndexRecreates(lists: string[][]): string[][];
232
251
  /**
233
252
  * Split a batch of SQL statements into a transaction-safe set and a
234
253
  * concurrent set, applying `--postgres-safe` rewriting if requested.
@@ -245,8 +264,17 @@ export declare function buildConcurrentIndexPlan(definitions: MigrationDefinitio
245
264
  * the concurrent set. This is what the CLI `--postgres-safe` flag and
246
265
  * the auto-migrate path rely on for issue #1165's shape-drift drops.
247
266
  * - All other statements stay in the regular (transaction) set.
267
+ * - `ALTER TABLE … VALIDATE CONSTRAINT` (a foreign key added `NOT VALID`)
268
+ * and every later statement of the same migration run outside the
269
+ * transaction, in order: the validation scan then holds only SHARE UPDATE
270
+ * EXCLUSIVE (reads and writes continue) instead of the batch's locks.
271
+ * - A same-name UNIQUE recreate (`DROP INDEX` then `CREATE UNIQUE INDEX`) is
272
+ * rewritten into build-then-swap ({@link swapUniqueIndexRecreates}) so the
273
+ * table never runs without a unique index between the two statements.
248
274
  */
249
- export declare function planPostgresStatements(statements: string[], useConcurrentIndexes: boolean): {
275
+ export declare function planPostgresStatements(statements: string[], useConcurrentIndexes: boolean, options?: {
276
+ swapUniqueRecreates?: boolean;
277
+ }): {
250
278
  concurrent: string[];
251
279
  regular: string[];
252
280
  };
@@ -1 +1 @@
1
- {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/tracker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/tracker.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { toSafeBooleanInteger, toSafeInteger } from "../utils/safe-integer.js";
2
+ import { shortenIdentifier } from "../schema/index-utils.js";
2
3
  import "../schema/ddl/null-equal-index.js";
3
4
  import { detectEngine } from "../schema/ddl/index.js";
4
5
  import { CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, getSystemTableDDLForEngine } from "../system/schema.js";
@@ -801,12 +802,79 @@ function quoteIndexName(name) {
801
802
  */
802
803
  function buildConcurrentIndexPlan(definitions, useConcurrentIndexes) {
803
804
  const plan = /* @__PURE__ */ new Map();
804
- for (const definition of definitions) {
805
- const split = planPostgresStatements(definition.up, useConcurrentIndexes);
806
- if (split.concurrent.length > 0) plan.set(definition.id, split);
807
- }
805
+ const splits = definitions.map((definition) => ({
806
+ id: definition.id,
807
+ split: planPostgresStatements(definition.up, useConcurrentIndexes, { swapUniqueRecreates: false })
808
+ }));
809
+ const swapped = swapUniqueIndexRecreates(splits.map(({ split }) => split.concurrent));
810
+ splits.forEach(({ id, split }, index) => {
811
+ const concurrent = swapped[index] ?? split.concurrent;
812
+ if (concurrent.length > 0) plan.set(id, {
813
+ regular: split.regular,
814
+ concurrent
815
+ });
816
+ });
808
817
  return plan;
809
818
  }
819
+ var VALIDATE_CONSTRAINT_RE = /\bVALIDATE\s+CONSTRAINT\b/i;
820
+ var DROP_INDEX_CONCURRENTLY_RE = /^\s*DROP\s+INDEX\s+CONCURRENTLY\s+(?:IF\s+EXISTS\s+)?(?:"((?:[^"]|"")+)"|([A-Za-z_][\w$]*))\s*;?\s*$/i;
821
+ var CREATE_UNIQUE_INDEX_CONCURRENTLY_IF_NOT_EXISTS_RE = /^(\s*CREATE\s+UNIQUE\s+INDEX\s+CONCURRENTLY\s+IF\s+NOT\s+EXISTS\s+)(?:"((?:[^"]|"")+)"|([A-Za-z_][\w$]*))/i;
822
+ function unquoteIndexName(quoted, bare) {
823
+ if (quoted !== void 0) return quoted.replace(/""/g, "\"");
824
+ return bare ?? null;
825
+ }
826
+ /**
827
+ * Keep a unique index in place while `--postgres-safe` recreates it.
828
+ *
829
+ * A same-name shape-drift repair is `DROP INDEX` followed by `CREATE UNIQUE
830
+ * INDEX` under the same name. Run CONCURRENTLY, the drop commits before the
831
+ * build starts, leaving a window with NO unique index: every
832
+ * `ON CONFLICT (…)` upsert fails (42P10) and plain inserts can write
833
+ * duplicates, which then make the build itself fail. This rewrites the drop
834
+ * into build-then-swap — `CREATE UNIQUE INDEX CONCURRENTLY` under a
835
+ * temporary name, then `DROP INDEX CONCURRENTLY` of the old one, then
836
+ * `ALTER INDEX … RENAME` — so the table always has a unique index over the
837
+ * old or the new key. The later create keeps its `IF NOT EXISTS` and becomes
838
+ * a no-op. Only rewritten when the create carries `IF NOT EXISTS` (the
839
+ * differ's DDL always does).
840
+ *
841
+ * @param lists - concurrent statement lists in execution order
842
+ * @returns the rewritten lists (same length and order)
843
+ */
844
+ function swapUniqueIndexRecreates(lists) {
845
+ const result = lists.map((list) => [...list]);
846
+ for (let li = 0; li < result.length; li++) {
847
+ const list = result[li] ?? [];
848
+ for (let si = 0; si < list.length; si++) {
849
+ const drop = DROP_INDEX_CONCURRENTLY_RE.exec(list[si] ?? "");
850
+ if (!drop) continue;
851
+ const name = unquoteIndexName(drop[1], drop[2]);
852
+ if (!name) continue;
853
+ const create = findLaterUniqueCreate(result, li, si, name);
854
+ if (!create) continue;
855
+ const temporary = shortenIdentifier(`${name}_smrt_swap`);
856
+ const buildTemporary = create.replace(CREATE_UNIQUE_INDEX_CONCURRENTLY_IF_NOT_EXISTS_RE, (_match, head) => `${head}${quoteIndexName(temporary)}`);
857
+ list.splice(si, 1, buildTemporary, `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(name)}`, `ALTER INDEX IF EXISTS ${quoteIndexName(temporary)} RENAME TO ${quoteIndexName(name)}`);
858
+ si += 2;
859
+ }
860
+ }
861
+ return result;
862
+ }
863
+ function findLaterUniqueCreate(lists, fromList, fromStatement, name) {
864
+ for (let li = fromList; li < lists.length; li++) {
865
+ const list = lists[li] ?? [];
866
+ const start = li === fromList ? fromStatement + 1 : 0;
867
+ for (let si = start; si < list.length; si++) {
868
+ const sql = list[si] ?? "";
869
+ const match = CREATE_UNIQUE_INDEX_CONCURRENTLY_IF_NOT_EXISTS_RE.exec(sql);
870
+ if (match && unquoteIndexName(match[2], match[3]) === name) return sql;
871
+ if (extractCreatedIndexName(sql) === name) return null;
872
+ const dropped = DROP_INDEX_CONCURRENTLY_RE.exec(sql);
873
+ if (dropped && unquoteIndexName(dropped[1], dropped[2]) === name) return null;
874
+ }
875
+ }
876
+ return null;
877
+ }
810
878
  /**
811
879
  * Split a batch of SQL statements into a transaction-safe set and a
812
880
  * concurrent set, applying `--postgres-safe` rewriting if requested.
@@ -823,12 +891,25 @@ function buildConcurrentIndexPlan(definitions, useConcurrentIndexes) {
823
891
  * the concurrent set. This is what the CLI `--postgres-safe` flag and
824
892
  * the auto-migrate path rely on for issue #1165's shape-drift drops.
825
893
  * - All other statements stay in the regular (transaction) set.
894
+ * - `ALTER TABLE … VALIDATE CONSTRAINT` (a foreign key added `NOT VALID`)
895
+ * and every later statement of the same migration run outside the
896
+ * transaction, in order: the validation scan then holds only SHARE UPDATE
897
+ * EXCLUSIVE (reads and writes continue) instead of the batch's locks.
898
+ * - A same-name UNIQUE recreate (`DROP INDEX` then `CREATE UNIQUE INDEX`) is
899
+ * rewritten into build-then-swap ({@link swapUniqueIndexRecreates}) so the
900
+ * table never runs without a unique index between the two statements.
826
901
  */
827
- function planPostgresStatements(statements, useConcurrentIndexes) {
902
+ function planPostgresStatements(statements, useConcurrentIndexes, options = {}) {
828
903
  const concurrentRegex = CONCURRENT_INDEX_STATEMENT_RE;
829
904
  const concurrent = [];
830
905
  const regular = [];
906
+ let deferRest = false;
831
907
  for (const sql of statements) {
908
+ if (useConcurrentIndexes && !deferRest && VALIDATE_CONSTRAINT_RE.test(sql)) deferRest = true;
909
+ if (deferRest && !sql.trimStart().startsWith("-- smrt:null-equal-conflict-index")) {
910
+ concurrent.push(/DROP\s+INDEX\s+(?!CONCURRENTLY)/i.test(sql) ? sql.replace(/DROP\s+INDEX\s+/i, "DROP INDEX CONCURRENTLY ") : /CREATE\s+(UNIQUE\s+)?INDEX\s+(?!CONCURRENTLY)/i.test(sql) ? sql.replace(/CREATE\s+(UNIQUE\s+)?INDEX\s+/i, "CREATE $1INDEX CONCURRENTLY ") : sql);
911
+ continue;
912
+ }
832
913
  if (sql.trimStart().startsWith("-- smrt:null-equal-conflict-index")) {
833
914
  if (useConcurrentIndexes) throw new Error("NULL-equal conflict index DDL requires an atomic maintenance-window migration; retry without --postgres-safe. Existing indexes require db:migrate-null-equal-indexes.");
834
915
  regular.push(sql);
@@ -850,12 +931,16 @@ function planPostgresStatements(statements, useConcurrentIndexes) {
850
931
  }
851
932
  regular.push(sql);
852
933
  }
853
- return {
934
+ if (options.swapUniqueRecreates === false) return {
854
935
  concurrent,
855
936
  regular
856
937
  };
938
+ return {
939
+ concurrent: swapUniqueIndexRecreates([concurrent])[0] ?? concurrent,
940
+ regular
941
+ };
857
942
  }
858
943
  //#endregion
859
- export { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, parsePostgresTimeoutMs, planPostgresStatements };
944
+ export { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, parsePostgresTimeoutMs, planPostgresStatements, swapUniqueIndexRecreates };
860
945
 
861
946
  //# sourceMappingURL=tracker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracker.js","names":[],"sources":["../../src/migrations/tracker.ts"],"sourcesContent":["/**\n * Migration Tracker\n *\n * Manages migration state, tracking which migrations have been applied,\n * and providing idempotent migration application.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { hostname } from 'node:os';\nimport { createLogger } from '@happyvertical/logger';\nimport { parsePostgresTimeoutMs } from '../postgres-timeouts.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { NULL_EQUAL_INDEX_MARKER } from '../schema/ddl/null-equal-index.js';\n// DatabaseEngine comes from the DDL layer (the json-inclusive type that\n// `detectEngine` actually returns), matching `SchemaComparer` in differ.ts.\n// The migration stack supports the JSON adapter post-R11, so the tracker's\n// engine type must include 'json' too — the narrower `migrations/types`\n// alias would reject it.\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport type {\n DriftReport,\n MigrationDefinition,\n MigrationResult,\n MigrationStatus,\n SchemaMigrationRecord,\n} from '../schema/types.js';\nimport { assertPostgresSystemTimestampsCurrent } from '../system/compatibility.js';\nimport {\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\nimport { toSafeBooleanInteger, toSafeInteger } from '../utils/safe-integer.js';\nimport { computeChecksum, verifyChecksum } from './checksum.js';\nimport type {\n ApplyMigrationsOptions,\n DatabaseInterface,\n MigrationTrackerOptions,\n RollbackOptions,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Row type for _smrt_schema_migrations table\n */\ninterface MigrationRow {\n id: string;\n name: string;\n version: string;\n checksum: string;\n applied_checksum?: string | null;\n applied_at: string;\n execution_time_ms?: unknown | null;\n package_name?: string | null;\n source_file?: string | null;\n status: string;\n error_message?: string | null;\n attempts: unknown;\n is_reversible: unknown;\n rolled_back_at?: string | null;\n applied_by?: string | null;\n batch: unknown | null;\n}\n\n/** Hydrate integer-backed system-table values at the database boundary. */\nfunction hydrateMigrationRow(row: MigrationRow): SchemaMigrationRecord {\n return {\n id: row.id,\n name: row.name,\n version: row.version,\n checksum: row.checksum,\n applied_checksum: row.applied_checksum ?? null,\n applied_at: new Date(row.applied_at),\n execution_time_ms:\n row.execution_time_ms === null || row.execution_time_ms === undefined\n ? null\n : toSafeInteger(row.execution_time_ms, 'Migration execution time'),\n attempts: toSafeInteger(row.attempts ?? 0, 'Migration attempts'),\n is_reversible: toSafeBooleanInteger(\n row.is_reversible ?? 1,\n 'Migration reversibility flag',\n ),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n package_name: row.package_name ?? null,\n source_file: row.source_file ?? null,\n status: row.status as MigrationStatus,\n error_message: row.error_message ?? null,\n applied_by: row.applied_by ?? null,\n batch:\n row.batch === null || row.batch === undefined\n ? null\n : toSafeInteger(row.batch, 'Migration batch'),\n };\n}\n\n/**\n * Default options for MigrationTracker\n */\nconst DEFAULT_OPTIONS = {\n lockTimeout: 30000,\n statementTimeout: 60000,\n useConcurrentIndexes: true,\n};\n\nclass AtomicMigrationRollback extends Error {\n constructor(\n public readonly results: MigrationResult[],\n public readonly failed: MigrationResult,\n ) {\n super(`Atomic migration batch failed at ${failed.name}`);\n }\n}\n\nconst CONCURRENT_INDEX_STATEMENT_RE =\n /(?:(?:CREATE\\s+(?:UNIQUE\\s+)?INDEX|DROP\\s+INDEX)\\s+CONCURRENTLY|REINDEX(?:\\s*\\([^)]*\\))?\\s+(?:INDEX|TABLE|SCHEMA|DATABASE|SYSTEM)\\s+CONCURRENTLY)/i;\n\n/**\n * Matches the leading clause of a `CREATE [UNIQUE] INDEX` statement and\n * captures the index name, whether it is written bare or double-quoted, and\n * regardless of the optional `CONCURRENTLY` / `IF NOT EXISTS` clauses.\n *\n * Used by the PostgreSQL concurrent-index mode to correlate a pending create\n * with an INVALID index left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` (issue #2362).\n */\nconst CREATE_INDEX_NAME_RE =\n /^\\s*CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:\"((?:[^\"]|\"\")+)\"|([A-Za-z_][\\w$]*))/i;\n\nfunction findConcurrentIndexStatement(\n definitions: MigrationDefinition[],\n): { migrationName: string; statement: string } | null {\n for (const definition of definitions) {\n const statement = definition.up.find((sql) =>\n CONCURRENT_INDEX_STATEMENT_RE.test(sql),\n );\n\n if (statement) {\n return {\n migrationName: definition.id,\n statement,\n };\n }\n }\n\n return null;\n}\n\n/**\n * Extract the index name from a `CREATE [UNIQUE] INDEX …` statement.\n *\n * Returns the unquoted identifier, or `null` when the statement is not a\n * CREATE INDEX (a drop, a reindex, or unrelated DDL).\n *\n * Exported for unit testing and because the CLI's index rollout needs the\n * same correlation to explain what it repaired.\n */\nexport function extractCreatedIndexName(sql: string): string | null {\n const match = CREATE_INDEX_NAME_RE.exec(sql);\n if (!match) {\n return null;\n }\n\n // Group 1 is the quoted form (with `\"\"` escapes), group 2 the bare form.\n return match[1] !== undefined ? match[1].replace(/\"\"/g, '\"') : match[2];\n}\n\n/**\n * Coerce a caller-supplied timeout to a usable millisecond count.\n *\n * Applied once, in the constructor, so every later read of\n * `this.options.lockTimeout` / `.statementTimeout` is already finite and\n * non-negative. Each timeout falls back to *its own* default — a bad\n * `lockTimeout` must not silently inherit the (much larger) statement-timeout\n * default and weaken the lock bound it was meant to impose.\n */\nfunction resolveTimeoutOption(\n value: number | undefined,\n fallback: number,\n): number {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0\n ? Math.trunc(value)\n : fallback;\n}\n\n/**\n * Render a millisecond timeout as a PostgreSQL interval literal.\n *\n * PostgreSQL reads `lock_timeout`/`statement_timeout` as milliseconds when\n * given a bare number, but the explicit `ms` unit keeps the emitted SQL\n * self-documenting in migration logs. `0` disables the timeout, which is\n * PostgreSQL's own semantic and therefore preserved verbatim.\n *\n * Input is already normalized by {@link resolveTimeoutOption}.\n */\nfunction formatPostgresTimeout(milliseconds: number): string {\n return `${Math.max(0, Math.trunc(milliseconds))}ms`;\n}\n\n/**\n * Parse a PostgreSQL timeout string into milliseconds.\n *\n * The implementation lives in `src/postgres-timeouts.ts`, the leaf module the\n * runtime pool bounds are built from (#2377), so `migrations.postgres.*` and a\n * runtime `timeouts` config are parsed by literally the same function rather\n * than by two copies that can drift. It is re-exported here — and from\n * `./index.ts` — because `@happyvertical/smrt-core/migrations` is the public\n * subpath `db:migrate` reads it through.\n */\nexport { parsePostgresTimeoutMs };\n\n/**\n * Prefix stamped on the `error_message` of every concurrent-index migration\n * whose transactional half has already committed.\n *\n * This is the resume marker: it is the only durable record that phase 1's\n * statements ran, so a retry can skip straight to the index build. It must\n * therefore be carried by *both* the interim write at the end of phase 1 and\n * every failure recorded during phase 2 — a lock timeout on the index build\n * leaves the non-index DDL committed just as surely as a crash does.\n */\nconst CONCURRENT_INDEX_PHASE1_MARKER =\n '[smrt: concurrent-index phase 1 committed]';\n\n/**\n * Interim `error_message` for a migration whose transactional half has\n * committed but whose `CONCURRENTLY` index build has not run yet.\n */\nconst CONCURRENT_INDEX_PENDING_MESSAGE = `${CONCURRENT_INDEX_PHASE1_MARKER} Concurrent index build pending: the non-index half of this migration committed but the CONCURRENTLY index DDL had not completed. Re-run db:migrate to finish it.`;\n\n/**\n * Statements split out of the atomic batch for PostgreSQL concurrent-index\n * mode, keyed by migration definition id.\n */\ntype ConcurrentIndexPlan = Map<\n string,\n { regular: string[]; concurrent: string[] }\n>;\n\n/**\n * Minimal pinned-session shape used by the concurrent-index phase.\n *\n * `CREATE INDEX CONCURRENTLY` must run outside a transaction, and the\n * `SET lock_timeout` / `SET statement_timeout` that bound it are *session*\n * state — on a pooled adapter a top-level `db.query` may land on a different\n * connection, so the SET would silently not apply to the DDL (issue #2362).\n */\ninterface MigrationSession {\n query: (\n sql: string,\n ...vars: unknown[]\n ) => Promise<{ rows: Record<string, unknown>[]; rowCount?: number }>;\n release: () => Promise<void>;\n}\n\n/**\n * MigrationTracker class\n *\n * Provides migration tracking and state management:\n * - Initializes the _smrt_schema_migrations tracking table\n * - Tracks applied migrations with checksums\n * - Provides idempotent migration application\n * - Supports rollback via DOWN scripts\n * - Handles PostgreSQL-specific concerns (CONCURRENTLY, lock timeout)\n */\nexport class MigrationTracker {\n private db: DatabaseInterface;\n private options: Required<Omit<MigrationTrackerOptions, 'db' | 'engineHint'>>;\n private engineHint?: string;\n private dbEngine: DatabaseEngine;\n /**\n * Memoized initialization promise. Storing the in-flight promise (rather\n * than a `boolean` flag set after the DDL completes) makes `initialize()`\n * safe under concurrency: multiple parallel callers all `await` the same\n * promise instead of independently re-running the DDL. On error the slot\n * is cleared so the next caller retries — a transient failure doesn't\n * permanently poison the instance.\n */\n private initializePromise: Promise<void> | null = null;\n private currentBatch: number | null = null;\n /**\n * Statements deferred out of the current atomic batch by PostgreSQL\n * concurrent-index mode. The parent tracker sets this on the\n * transaction-scoped tracker before phase 1 so `apply()` executes only the\n * transaction-safe statements and leaves the migration record in `running`;\n * the parent finalizes those records after the transaction commits and the\n * `CONCURRENTLY` DDL has run. `null` on every ordinary path.\n */\n private concurrentIndexPlan: ConcurrentIndexPlan | null = null;\n\n constructor(options: MigrationTrackerOptions) {\n this.db = options.db;\n this.options = {\n lockTimeout: resolveTimeoutOption(\n options.lockTimeout,\n DEFAULT_OPTIONS.lockTimeout,\n ),\n statementTimeout: resolveTimeoutOption(\n options.statementTimeout,\n DEFAULT_OPTIONS.statementTimeout,\n ),\n useConcurrentIndexes:\n options.useConcurrentIndexes ?? DEFAULT_OPTIONS.useConcurrentIndexes,\n };\n this.engineHint = options.engineHint;\n this.dbEngine = this.detectDbEngine();\n }\n\n /**\n * Detect the database engine. Honors an explicit `engineHint` (passed via\n * `MigrationTrackerOptions`) before falling back to URL parsing — this is\n * the same `detectEngine` contract used by `SchemaComparer` and the rest\n * of the migration stack, so the tracker stays consistent with the DDL\n * the orchestrator generated.\n */\n private detectDbEngine(): DatabaseEngine {\n const dbWithConfig = this.db as DatabaseInterface & {\n config?: { url?: string };\n };\n const url = this.db.url || dbWithConfig.config?.url || '';\n return detectEngine(url, this.engineHint);\n }\n\n /**\n * Get the current database engine\n */\n getEngine(): DatabaseEngine {\n return this.dbEngine;\n }\n\n /**\n * Initialize the migrations tracking table\n */\n async initialize(): Promise<void> {\n if (!this.initializePromise) {\n this.initializePromise = this.runInitializeDdl().catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n private async runInitializeDdl(): Promise<void> {\n // Parse and execute each statement in the DDL\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n this.dbEngine,\n )\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n\n for (const statement of statements) {\n await this.db.query(statement);\n }\n\n await assertPostgresSystemTimestampsCurrent(this.db, this.engineHint);\n }\n\n /**\n * Get all applied migrations ordered by applied_at\n */\n async getAppliedMigrations(): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE status = 'completed' ORDER BY applied_at ASC`,\n );\n\n return (result.rows as MigrationRow[]).map(hydrateMigrationRow);\n }\n\n /**\n * Get pending migrations (defined but not applied)\n */\n async getPendingMigrations(\n definitions: MigrationDefinition[],\n ): Promise<MigrationDefinition[]> {\n const applied = await this.getAppliedMigrations();\n const appliedNames = new Set(applied.map((m) => m.name));\n return definitions.filter((d) => !appliedNames.has(d.id));\n }\n\n /**\n * Check if a migration has been applied\n */\n async isApplied(migrationId: string): Promise<boolean> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT 1 FROM _smrt_schema_migrations WHERE name = ? AND status = 'completed' LIMIT 1`,\n migrationId,\n );\n\n return result.rows.length > 0;\n }\n\n /**\n * Get a single migration record by name\n */\n async getMigration(name: string): Promise<SchemaMigrationRecord | null> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE name = ? LIMIT 1`,\n name,\n );\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as MigrationRow;\n return hydrateMigrationRow(row);\n }\n\n /**\n * Get the next batch number\n */\n async getNextBatch(): Promise<number> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT MAX(batch) as max_batch FROM _smrt_schema_migrations`,\n );\n\n const row = result.rows[0] as { max_batch?: unknown } | undefined;\n const maxBatch = toSafeInteger(row?.max_batch ?? 0, 'Migration batch');\n return toSafeInteger(maxBatch + 1, 'Next migration batch');\n }\n\n /**\n * Detect drift between migration definitions and applied migrations\n */\n async detectDrift(\n definitions: MigrationDefinition[],\n ): Promise<DriftReport[]> {\n const applied = await this.getAppliedMigrations();\n const reports: DriftReport[] = [];\n\n const appliedMap = new Map(applied.map((m) => [m.name, m]));\n const definitionMap = new Map(definitions.map((d) => [d.id, d]));\n\n // Check each applied migration against definitions\n for (const record of applied) {\n const definition = definitionMap.get(record.name);\n\n if (!definition) {\n // Migration applied but definition is missing\n reports.push({\n migration_name: record.name,\n expected_checksum: 'N/A',\n actual_checksum: record.checksum,\n drift_type: 'unknown_in_db',\n recommendation:\n 'Migration exists in database but definition is missing. Manual cleanup may be required.',\n });\n continue;\n }\n\n // Check checksum\n const currentChecksum = computeChecksum(definition.up);\n const verification = verifyChecksum(\n currentChecksum,\n record.checksum,\n record.applied_checksum,\n );\n\n if (!verification.valid) {\n const driftType = verification.drift ?? 'db_modified';\n reports.push({\n migration_name: record.name,\n expected_checksum: record.checksum,\n actual_checksum: currentChecksum,\n drift_type: driftType,\n recommendation:\n driftType === 'file_modified'\n ? 'Migration file was modified after application. Create a new migration instead.'\n : 'Database schema was modified outside of migrations. Review and reconcile manually.',\n });\n }\n }\n\n return reports;\n }\n\n /**\n * Apply a single migration\n */\n async apply(\n definition: MigrationDefinition,\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.up);\n const force = Boolean(\n options.force || options.forceMigrations?.includes(definition.id),\n );\n const hasScopedForce = Boolean(options.forceMigrations?.length);\n\n // Check if migration record already exists\n const existing = await this.getMigration(definition.id);\n\n if (existing) {\n // Handle based on existing status\n switch (existing.status) {\n case 'completed':\n if (!force && existing.checksum !== checksum) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} checksum mismatch. Was already applied with different checksum. Use --force-migration ${definition.id} to override only this migration, or --force to override the whole batch.`,\n };\n }\n\n if (!force && !options.reconcile) {\n // Already applied, skip\n return {\n success: true,\n applied: false,\n skipped: true,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n };\n }\n break;\n\n case 'failed':\n if (!force && (!options.reconcile || hasScopedForce)) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} previously failed: ${existing.error_message || 'Unknown error'}. Use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'running':\n if (!force) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} is currently running or was interrupted. If the previous run crashed, use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'rolled_back':\n // Rolled back migrations can be re-applied without --force\n break;\n }\n }\n\n if (options.dryRun) {\n return {\n success: true,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n };\n }\n\n // Get batch number\n if (this.currentBatch === null) {\n this.currentBatch = await this.getNextBatch();\n }\n\n // Determine if we're updating an existing record or inserting new\n const id = existing?.id || randomUUID();\n const attempts = toSafeInteger(\n (existing?.attempts ?? 0) + 1,\n 'Migration attempts',\n );\n\n if (existing) {\n // Update existing record to 'running'\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'running', checksum = ?, attempts = ?, error_message = NULL, batch = ?, applied_by = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n checksum,\n attempts,\n this.currentBatch,\n hostname(),\n id,\n );\n } else {\n // Insert new record\n await this.db.query(\n `INSERT INTO _smrt_schema_migrations\n (id, name, version, checksum, status, attempts, is_reversible, package_name, source_file, applied_by, batch)\n VALUES (?, ?, ?, ?, 'running', 1, ?, ?, ?, ?, ?)`,\n id,\n definition.id,\n definition.version,\n checksum,\n (definition.is_reversible ?? definition.down.length > 0) ? 1 : 0,\n definition.package_name ?? null,\n definition.source_file ?? null,\n hostname(),\n this.currentBatch,\n );\n }\n\n // PostgreSQL concurrent-index mode splits this definition: the\n // transaction-safe statements run here, the CONCURRENTLY index DDL runs\n // after the batch commits.\n const split = this.concurrentIndexPlan?.get(definition.id);\n const deferredConcurrent = (split?.concurrent.length ?? 0) > 0;\n\n // A retry of an interrupted concurrent-index migration must not re-run the\n // half that already committed. The marker on the previous attempt's\n // `error_message` says it did, and the checksum says it was this same\n // definition. Without this, a definition that mixes non-idempotent DDL\n // (`ALTER TABLE … ADD COLUMN`, which the differ emits without\n // `IF NOT EXISTS`) with index DDL would fail on \"column already exists\"\n // forever, never reaching the index build it was retrying for.\n const phase1AlreadyCommitted =\n deferredConcurrent &&\n existing?.status === 'failed' &&\n existing.checksum === checksum &&\n typeof existing.error_message === 'string' &&\n existing.error_message.startsWith(CONCURRENT_INDEX_PHASE1_MARKER);\n\n try {\n // Execute the migration\n await this.executeStatements(\n phase1AlreadyCommitted ? [] : split ? split.regular : definition.up,\n options,\n );\n\n const executionTime = Date.now() - startTime;\n\n if (deferredConcurrent) {\n // Commit this record as `failed`, not `completed` and not `running`.\n // The index does not exist yet, so `completed` would be a lie; and\n // unlike the single-transaction path — where a crash rolls the record\n // insert back with the DDL — this row is about to be committed, so a\n // crash before the concurrent phase finishes would strand a `running`\n // record that the next `db:migrate` can only clear with\n // `--force-migration`. `failed` is retryable under the CLI's\n // reconciling re-run, which is what an interrupted index rollout\n // needs. The concurrent phase overwrites this a moment later.\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', execution_time_ms = ?, error_message = ?\n WHERE id = ?`,\n executionTime,\n CONCURRENT_INDEX_PENDING_MESSAGE,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n }\n\n // Mark as completed\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n executionTime,\n checksum,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const migrationError =\n error instanceof Error ? error : new Error(String(error));\n\n // Mark as failed. In an outer atomic Postgres transaction, a DDL error\n // aborts the transaction and this status write must be skipped so the\n // caller can roll back the whole batch while preserving the root error.\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE id = ?`,\n migrationError.message,\n id,\n );\n } catch (persistError) {\n // Preserve the migration failure; status persistence is secondary and\n // may be impossible until the surrounding transaction rolls back.\n const persistMessage =\n persistError instanceof Error\n ? persistError.message\n : String(persistError);\n logger.error(\n `Failed to persist failed status for migration ${definition.id}: ${persistMessage}`,\n );\n }\n\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: migrationError,\n };\n }\n }\n\n /**\n * Apply multiple migrations in order.\n *\n * On PostgreSQL the atomic path always bounds itself with\n * `SET LOCAL lock_timeout` / `SET LOCAL statement_timeout` so a migration\n * that queues behind a long-running writer fails fast and rolls back\n * instead of stalling every writer behind its already-held locks\n * (issue #2362).\n *\n * `options.postgresSafe` additionally enables **concurrent-index mode**:\n * non-index DDL still runs in the one atomic transaction, then every index\n * statement runs `CONCURRENTLY` on a pinned session after that transaction\n * commits. That mode trades atomicity for availability — a failure part way\n * through the index phase leaves the committed non-index DDL in place, and\n * the unfinished index migrations are recorded `failed` so the next run\n * retries them, resuming at the index phase rather than re-running the\n * non-index statements that already committed.\n */\n async applyAll(\n definitions: MigrationDefinition[],\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult[]> {\n if (options.atomic && !options.dryRun) {\n if (!this.db.transaction) {\n throw new Error(\n 'Atomic migration batches require a database adapter with transaction support.',\n );\n }\n\n const isPostgres = this.dbEngine === 'postgres';\n const concurrentIndexMode = isPostgres && Boolean(options.postgresSafe);\n const plan = concurrentIndexMode\n ? buildConcurrentIndexPlan(\n definitions,\n this.options.useConcurrentIndexes,\n )\n : null;\n\n // Outside concurrent-index mode a CONCURRENTLY statement can only fail:\n // PostgreSQL rejects it inside a transaction block. Fail before opening\n // the transaction so the error names the offending migration.\n if (isPostgres && !concurrentIndexMode) {\n const concurrentStatement = findConcurrentIndexStatement(definitions);\n if (concurrentStatement) {\n throw new Error(\n `Atomic migration batch cannot include CONCURRENTLY index DDL in ${concurrentStatement.migrationName}; PostgreSQL forbids CONCURRENTLY inside a transaction. Re-run with PostgreSQL concurrent-index mode (smrt db:migrate --postgres-safe) to build those indexes outside the batch.`,\n );\n }\n }\n\n const deferred = plan && plan.size > 0 ? plan : null;\n\n try {\n const committed = await this.db.transaction(async (tx) => {\n const txDb = {\n ...tx,\n transaction: async <T>(\n callback: (transactionDb: DatabaseInterface) => Promise<T>,\n ) => callback(tx as DatabaseInterface),\n } as DatabaseInterface;\n\n // Bound the whole batch. `SET LOCAL` reverts at COMMIT/ROLLBACK, so\n // this cannot leak onto a pooled connection's next checkout.\n if (isPostgres) {\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n }\n\n const txTracker = new MigrationTracker({\n db: txDb,\n lockTimeout: this.options.lockTimeout,\n statementTimeout: this.options.statementTimeout,\n useConcurrentIndexes: false,\n });\n txTracker.concurrentIndexPlan = deferred;\n const results = await txTracker.applyAll(definitions, {\n ...options,\n atomic: false,\n continueOnError: false,\n postgresSafe: false,\n // Deferred migrations report progress from the concurrent phase\n // once they have actually finished, not when phase 1 commits.\n onProgress: options.onProgress\n ? (result) => {\n if (!deferred?.has(result.name)) {\n options.onProgress?.(result);\n }\n }\n : undefined,\n });\n const failed = results.find((result) => !result.success);\n\n if (failed) {\n throw new AtomicMigrationRollback(results, failed);\n }\n\n return results;\n });\n\n if (!deferred) {\n return committed;\n }\n\n return await this.applyDeferredConcurrentIndexes(\n committed,\n deferred,\n options,\n );\n } catch (error) {\n if (error instanceof AtomicMigrationRollback) {\n return error.results.map((result) => {\n if (!result.success || !result.applied) {\n return result;\n }\n\n return {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n rolled_back: true,\n error: new Error(\n `Rolled back because migration ${error.failed.name} failed`,\n ),\n };\n });\n }\n\n throw error;\n }\n }\n\n const results: MigrationResult[] = [];\n this.currentBatch = await this.getNextBatch();\n\n for (const definition of definitions) {\n const result = await this.apply(definition, options);\n results.push(result);\n options.onProgress?.(result);\n\n if (!result.success && !options.continueOnError) {\n break;\n }\n }\n\n this.currentBatch = null;\n return results;\n }\n\n /**\n * Phase 2 of PostgreSQL concurrent-index mode.\n *\n * Runs every deferred index statement on one pinned session, outside the\n * committed transaction, bounded by session `lock_timeout` /\n * `statement_timeout`. INVALID indexes left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` are dropped first so the retry rebuilds them\n * rather than skipping over a stump that reads as present.\n *\n * On failure the batch stops: the failing migration and every migration it\n * did not reach are recorded `failed`, which the CLI's reconciling\n * `db:migrate` retries on the next run.\n */\n private async applyDeferredConcurrentIndexes(\n committed: MigrationResult[],\n plan: ConcurrentIndexPlan,\n options: ApplyMigrationsOptions,\n ): Promise<MigrationResult[]> {\n const results = [...committed];\n const session = await this.acquireMigrationSession();\n\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n let stoppedBy: string | null = null;\n\n for (let index = 0; index < results.length; index++) {\n const result = results[index];\n const split = plan.get(result.name);\n if (!split || split.concurrent.length === 0) {\n continue;\n }\n\n // A migration phase 1 skipped (already applied, checksum matched) is\n // already `completed` in the tracking table — its index exists and\n // must not be rebuilt or have its record rewritten.\n if (result.skipped) {\n continue;\n }\n\n if (stoppedBy) {\n const error = new Error(\n `Not attempted: concurrent index phase stopped after ${stoppedBy} failed`,\n );\n await this.recordConcurrentIndexFailure(result.name, error.message);\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n error,\n };\n options.onProgress?.(results[index]);\n continue;\n }\n\n const startedAt = Date.now();\n\n try {\n for (const sql of split.concurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n // An INVALID index is not usable by the planner and blocks the\n // name, but `CREATE INDEX … IF NOT EXISTS` treats it as present.\n // Drop it so the create below actually rebuilds it.\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n\n // Floor at 1ms: callers (the CLI progress reporter, the applied vs\n // skipped counters) read `execution_time_ms === 0` as \"nothing ran\".\n const executionTime = Math.max(\n 1,\n (result.execution_time_ms ?? 0) + (Date.now() - startedAt),\n );\n await this.recordConcurrentIndexSuccess(\n result.name,\n result.checksum,\n executionTime,\n );\n results[index] = {\n ...result,\n success: true,\n applied: true,\n skipped: false,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const indexError =\n error instanceof Error ? error : new Error(String(error));\n if (!options.continueOnError) {\n stoppedBy = result.name;\n }\n await this.recordConcurrentIndexFailure(\n result.name,\n indexError.message,\n );\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n execution_time_ms: Date.now() - startedAt,\n error: indexError,\n };\n }\n\n options.onProgress?.(results[index]);\n }\n\n return results;\n } finally {\n await session.release();\n }\n }\n\n /**\n * Pin one connection for the concurrent-index phase.\n *\n * PostgreSQL pools hand each top-level `query` an arbitrary connection, so\n * a `SET lock_timeout` issued through the pool may not apply to the DDL\n * that follows. Adapters that expose `acquireSession` give us a pinned\n * connection; anything else (SQLite, DuckDB, test doubles) is\n * single-connection anyway and falls back to the plain interface.\n */\n private async acquireMigrationSession(): Promise<MigrationSession> {\n if (typeof this.db.acquireSession === 'function') {\n const session = await this.db.acquireSession();\n return {\n query: (sql, ...vars) => session.query(sql, ...vars),\n release: () => session.release(),\n };\n }\n\n return {\n query: (sql, ...vars) => this.db.query(sql, ...vars),\n release: async () => {},\n };\n }\n\n /**\n * Read the names of INVALID indexes that an unqualified index name in this\n * session would actually resolve to.\n *\n * `pg_indexes` (what the SQL adapter's introspection reads) lists an\n * INVALID index exactly like a healthy one, so the schema differ believes a\n * failed `CREATE INDEX CONCURRENTLY` succeeded. `pg_index.indisvalid` is the\n * authoritative flag.\n *\n * Scoped to `current_schemas(false)` — the session's search path — because\n * migration DDL names indexes unqualified, and so does the\n * `DROP INDEX CONCURRENTLY IF EXISTS \"<name>\"` repair. Scanning every schema\n * would let an unrelated invalid index of the same name in another schema\n * trigger a drop that then resolves to a *different* object. Where the same\n * name appears in several search-path schemas, the earliest on the path wins\n * — the same rule PostgreSQL uses to resolve the name.\n *\n * A role without catalog access degrades to \"no known invalid indexes\"\n * rather than aborting the migration.\n */\n private async readInvalidIndexNames(\n session: MigrationSession,\n ): Promise<Set<string>> {\n const names = new Set<string>();\n\n try {\n const result = await session.query(\n `SELECT DISTINCT ON (c.relname) c.relname AS index_name\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE i.indisvalid = false\n AND n.nspname = ANY (current_schemas(false))\n ORDER BY c.relname,\n array_position(current_schemas(false), n.nspname::text)`,\n );\n\n for (const row of result.rows) {\n const name = (row as { index_name?: unknown }).index_name;\n if (typeof name === 'string') {\n names.add(name);\n }\n }\n } catch (error) {\n logger.warn(\n `Could not read pg_index.indisvalid; skipping INVALID index repair: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n return names;\n }\n\n private async recordConcurrentIndexSuccess(\n name: string,\n checksum: string,\n executionTime: number,\n ): Promise<void> {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, error_message = NULL, applied_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n executionTime,\n checksum,\n name,\n );\n }\n\n private async recordConcurrentIndexFailure(\n name: string,\n message: string,\n ): Promise<void> {\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE name = ?`,\n // Keep the resume marker: this failure happened *after* phase 1\n // committed, so the retry must skip the non-index statements too.\n `${CONCURRENT_INDEX_PHASE1_MARKER} ${message}`,\n name,\n );\n } catch (persistError) {\n logger.error(\n `Failed to persist failed status for migration ${name}: ${\n persistError instanceof Error\n ? persistError.message\n : String(persistError)\n }`,\n );\n }\n }\n\n /**\n * Rollback a migration\n */\n async rollback(\n name: string,\n definition: MigrationDefinition,\n options: RollbackOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.down);\n\n const record = await this.getMigration(name);\n if (!record) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(`Migration ${name} not found in database`),\n };\n }\n\n if (record.status !== 'completed') {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not in completed state (current: ${record.status})`,\n ),\n };\n }\n\n if (!definition.down.length) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not reversible (no down statements)`,\n ),\n };\n }\n\n if (options.dryRun) {\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n }\n\n try {\n // Execute DOWN statements\n await this.executeStatements(definition.down, {});\n\n // Mark as rolled back\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'rolled_back', rolled_back_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n name,\n );\n\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n } catch (error) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: error instanceof Error ? error : new Error(String(error)),\n rolled_back: false,\n };\n }\n }\n\n /**\n * Get migration history with optional filtering\n */\n async getHistory(\n options: { limit?: number; since?: Date; status?: MigrationStatus } = {},\n ): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n let sql = `SELECT * FROM _smrt_schema_migrations WHERE 1=1`;\n const params: unknown[] = [];\n\n if (options.status) {\n sql += ` AND status = ?`;\n params.push(options.status);\n }\n\n if (options.since) {\n sql += ` AND applied_at >= ?`;\n params.push(options.since.toISOString());\n }\n\n sql += ` ORDER BY applied_at DESC`;\n\n if (options.limit) {\n sql += ` LIMIT ?`;\n params.push(options.limit);\n }\n\n const result = await this.db.query(sql, ...params);\n\n return (result.rows as MigrationRow[]).map(hydrateMigrationRow);\n }\n\n /**\n * Execute SQL statements with appropriate transaction handling\n */\n private async executeStatements(\n statements: string[],\n options: ApplyMigrationsOptions,\n ): Promise<void> {\n if (this.dbEngine === 'postgres' && options.postgresSafe) {\n await this.executePostgresStatements(statements);\n } else {\n await this.executeWithTransaction(statements);\n }\n }\n\n /**\n * Execute statements within a transaction (SQLite/DuckDB)\n */\n private async executeWithTransaction(statements: string[]): Promise<void> {\n if (this.db.transaction) {\n await this.db.transaction(async (tx) => {\n for (const sql of statements) {\n await tx.query(sql);\n }\n });\n } else {\n // Fallback to sequential execution\n for (const sql of statements) {\n await this.db.query(sql);\n }\n }\n }\n\n /**\n * Execute statements with PostgreSQL-specific handling\n *\n * - CONCURRENTLY statements run outside transaction\n * - Regular statements run in transaction with lock_timeout\n *\n * Both CREATE and DROP variants of CONCURRENTLY must be detected — Postgres\n * forbids `DROP INDEX CONCURRENTLY` inside a transaction block, just like\n * the create variant. Issue #1165: the migration generator emits\n * `DROP INDEX CONCURRENTLY` for shape-drift recreates and orphan-index\n * cleanups, so the regex needs to cover both keywords.\n */\n private async executePostgresStatements(statements: string[]): Promise<void> {\n const { concurrent: finalConcurrent, regular: finalRegular } =\n planPostgresStatements(statements, !!this.options.useConcurrentIndexes);\n\n // CONCURRENTLY statements come from `planPostgresStatements`, which\n // also pushes any post-transform CREATE/DROP CONCURRENTLY into the\n // outside-transaction set. The original `concurrentStatements` list\n // is folded in there too so callers below see one merged stream.\n\n // Execute regular statements in transaction with timeouts\n if (finalRegular.length > 0 && this.db.transaction) {\n await this.db.transaction(async (tx) => {\n // Set session-level timeouts\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n for (const sql of finalRegular) {\n await tx.query(sql);\n }\n });\n } else if (finalRegular.length > 0) {\n for (const sql of finalRegular) {\n await this.db.query(sql);\n }\n }\n\n if (finalConcurrent.length === 0) {\n return;\n }\n\n // Execute CONCURRENTLY statements outside the transaction, on one pinned\n // connection so `SET lock_timeout` / `SET statement_timeout` actually\n // govern the DDL that follows them (a pooled `db.query` may not reuse the\n // connection the SET landed on).\n const session = await this.acquireMigrationSession();\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n\n for (const sql of finalConcurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n } finally {\n await session.release();\n }\n }\n}\n\n/**\n * Quote an index identifier for PostgreSQL, escaping embedded double quotes.\n */\nfunction quoteIndexName(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Build the PostgreSQL concurrent-index plan for an atomic batch.\n *\n * Returns one entry per migration definition that has at least one statement\n * which must leave the transaction, mapping it to the transaction-safe\n * statements (phase 1) and the `CONCURRENTLY` statements (phase 2).\n * Definitions with nothing to defer are omitted so the caller can treat\n * `plan.has(id)` as \"this migration finishes in phase 2\".\n *\n * Exported for unit testing.\n */\nexport function buildConcurrentIndexPlan(\n definitions: MigrationDefinition[],\n useConcurrentIndexes: boolean,\n): ConcurrentIndexPlan {\n const plan: ConcurrentIndexPlan = new Map();\n\n for (const definition of definitions) {\n const split = planPostgresStatements(definition.up, useConcurrentIndexes);\n if (split.concurrent.length > 0) {\n plan.set(definition.id, split);\n }\n }\n\n return plan;\n}\n\n/**\n * Split a batch of SQL statements into a transaction-safe set and a\n * concurrent set, applying `--postgres-safe` rewriting if requested.\n *\n * Exported for unit testing and to give external callers a way to\n * reason about what the tracker would execute in PostgreSQL safe mode.\n *\n * Rules:\n * - Statements already containing `CONCURRENTLY` for CREATE/DROP INDEX or\n * REINDEX operations always go to the concurrent set — Postgres rejects\n * them inside transactions.\n * - When `useConcurrentIndexes` is true, plain `CREATE INDEX` and plain\n * `DROP INDEX` are rewritten to use `CONCURRENTLY`, then routed to\n * the concurrent set. This is what the CLI `--postgres-safe` flag and\n * the auto-migrate path rely on for issue #1165's shape-drift drops.\n * - All other statements stay in the regular (transaction) set.\n */\nexport function planPostgresStatements(\n statements: string[],\n useConcurrentIndexes: boolean,\n): { concurrent: string[]; regular: string[] } {\n const concurrentRegex = CONCURRENT_INDEX_STATEMENT_RE;\n\n const concurrent: string[] = [];\n const regular: string[] = [];\n\n for (const sql of statements) {\n if (sql.trimStart().startsWith(NULL_EQUAL_INDEX_MARKER)) {\n if (useConcurrentIndexes) {\n throw new Error(\n 'NULL-equal conflict index DDL requires an atomic maintenance-window migration; retry without --postgres-safe. Existing indexes require db:migrate-null-equal-indexes.',\n );\n }\n regular.push(sql);\n continue;\n }\n if (concurrentRegex.test(sql)) {\n concurrent.push(sql);\n continue;\n }\n if (useConcurrentIndexes) {\n if (/CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+/i,\n 'CREATE $1INDEX CONCURRENTLY ',\n ),\n );\n continue;\n }\n if (/DROP\\s+INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(/DROP\\s+INDEX\\s+/i, 'DROP INDEX CONCURRENTLY '),\n );\n continue;\n }\n }\n regular.push(sql);\n }\n\n return { concurrent, regular };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tracker.js","names":[],"sources":["../../src/migrations/tracker.ts"],"sourcesContent":["/**\n * Migration Tracker\n *\n * Manages migration state, tracking which migrations have been applied,\n * and providing idempotent migration application.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { hostname } from 'node:os';\nimport { createLogger } from '@happyvertical/logger';\nimport { parsePostgresTimeoutMs } from '../postgres-timeouts.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { NULL_EQUAL_INDEX_MARKER } from '../schema/ddl/null-equal-index.js';\n// DatabaseEngine comes from the DDL layer (the json-inclusive type that\n// `detectEngine` actually returns), matching `SchemaComparer` in differ.ts.\n// The migration stack supports the JSON adapter post-R11, so the tracker's\n// engine type must include 'json' too — the narrower `migrations/types`\n// alias would reject it.\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport { shortenIdentifier } from '../schema/index-utils.js';\nimport type {\n DriftReport,\n MigrationDefinition,\n MigrationResult,\n MigrationStatus,\n SchemaMigrationRecord,\n} from '../schema/types.js';\nimport { assertPostgresSystemTimestampsCurrent } from '../system/compatibility.js';\nimport {\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\nimport { toSafeBooleanInteger, toSafeInteger } from '../utils/safe-integer.js';\nimport { computeChecksum, verifyChecksum } from './checksum.js';\nimport type {\n ApplyMigrationsOptions,\n DatabaseInterface,\n MigrationTrackerOptions,\n RollbackOptions,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Row type for _smrt_schema_migrations table\n */\ninterface MigrationRow {\n id: string;\n name: string;\n version: string;\n checksum: string;\n applied_checksum?: string | null;\n applied_at: string;\n execution_time_ms?: unknown | null;\n package_name?: string | null;\n source_file?: string | null;\n status: string;\n error_message?: string | null;\n attempts: unknown;\n is_reversible: unknown;\n rolled_back_at?: string | null;\n applied_by?: string | null;\n batch: unknown | null;\n}\n\n/** Hydrate integer-backed system-table values at the database boundary. */\nfunction hydrateMigrationRow(row: MigrationRow): SchemaMigrationRecord {\n return {\n id: row.id,\n name: row.name,\n version: row.version,\n checksum: row.checksum,\n applied_checksum: row.applied_checksum ?? null,\n applied_at: new Date(row.applied_at),\n execution_time_ms:\n row.execution_time_ms === null || row.execution_time_ms === undefined\n ? null\n : toSafeInteger(row.execution_time_ms, 'Migration execution time'),\n attempts: toSafeInteger(row.attempts ?? 0, 'Migration attempts'),\n is_reversible: toSafeBooleanInteger(\n row.is_reversible ?? 1,\n 'Migration reversibility flag',\n ),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n package_name: row.package_name ?? null,\n source_file: row.source_file ?? null,\n status: row.status as MigrationStatus,\n error_message: row.error_message ?? null,\n applied_by: row.applied_by ?? null,\n batch:\n row.batch === null || row.batch === undefined\n ? null\n : toSafeInteger(row.batch, 'Migration batch'),\n };\n}\n\n/**\n * Default options for MigrationTracker\n */\nconst DEFAULT_OPTIONS = {\n lockTimeout: 30000,\n statementTimeout: 60000,\n useConcurrentIndexes: true,\n};\n\nclass AtomicMigrationRollback extends Error {\n constructor(\n public readonly results: MigrationResult[],\n public readonly failed: MigrationResult,\n ) {\n super(`Atomic migration batch failed at ${failed.name}`);\n }\n}\n\nconst CONCURRENT_INDEX_STATEMENT_RE =\n /(?:(?:CREATE\\s+(?:UNIQUE\\s+)?INDEX|DROP\\s+INDEX)\\s+CONCURRENTLY|REINDEX(?:\\s*\\([^)]*\\))?\\s+(?:INDEX|TABLE|SCHEMA|DATABASE|SYSTEM)\\s+CONCURRENTLY)/i;\n\n/**\n * Matches the leading clause of a `CREATE [UNIQUE] INDEX` statement and\n * captures the index name, whether it is written bare or double-quoted, and\n * regardless of the optional `CONCURRENTLY` / `IF NOT EXISTS` clauses.\n *\n * Used by the PostgreSQL concurrent-index mode to correlate a pending create\n * with an INVALID index left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` (issue #2362).\n */\nconst CREATE_INDEX_NAME_RE =\n /^\\s*CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:\"((?:[^\"]|\"\")+)\"|([A-Za-z_][\\w$]*))/i;\n\nfunction findConcurrentIndexStatement(\n definitions: MigrationDefinition[],\n): { migrationName: string; statement: string } | null {\n for (const definition of definitions) {\n const statement = definition.up.find((sql) =>\n CONCURRENT_INDEX_STATEMENT_RE.test(sql),\n );\n\n if (statement) {\n return {\n migrationName: definition.id,\n statement,\n };\n }\n }\n\n return null;\n}\n\n/**\n * Extract the index name from a `CREATE [UNIQUE] INDEX …` statement.\n *\n * Returns the unquoted identifier, or `null` when the statement is not a\n * CREATE INDEX (a drop, a reindex, or unrelated DDL).\n *\n * Exported for unit testing and because the CLI's index rollout needs the\n * same correlation to explain what it repaired.\n */\nexport function extractCreatedIndexName(sql: string): string | null {\n const match = CREATE_INDEX_NAME_RE.exec(sql);\n if (!match) {\n return null;\n }\n\n // Group 1 is the quoted form (with `\"\"` escapes), group 2 the bare form.\n return match[1] !== undefined ? match[1].replace(/\"\"/g, '\"') : match[2];\n}\n\n/**\n * Coerce a caller-supplied timeout to a usable millisecond count.\n *\n * Applied once, in the constructor, so every later read of\n * `this.options.lockTimeout` / `.statementTimeout` is already finite and\n * non-negative. Each timeout falls back to *its own* default — a bad\n * `lockTimeout` must not silently inherit the (much larger) statement-timeout\n * default and weaken the lock bound it was meant to impose.\n */\nfunction resolveTimeoutOption(\n value: number | undefined,\n fallback: number,\n): number {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0\n ? Math.trunc(value)\n : fallback;\n}\n\n/**\n * Render a millisecond timeout as a PostgreSQL interval literal.\n *\n * PostgreSQL reads `lock_timeout`/`statement_timeout` as milliseconds when\n * given a bare number, but the explicit `ms` unit keeps the emitted SQL\n * self-documenting in migration logs. `0` disables the timeout, which is\n * PostgreSQL's own semantic and therefore preserved verbatim.\n *\n * Input is already normalized by {@link resolveTimeoutOption}.\n */\nfunction formatPostgresTimeout(milliseconds: number): string {\n return `${Math.max(0, Math.trunc(milliseconds))}ms`;\n}\n\n/**\n * Parse a PostgreSQL timeout string into milliseconds.\n *\n * The implementation lives in `src/postgres-timeouts.ts`, the leaf module the\n * runtime pool bounds are built from (#2377), so `migrations.postgres.*` and a\n * runtime `timeouts` config are parsed by literally the same function rather\n * than by two copies that can drift. It is re-exported here — and from\n * `./index.ts` — because `@happyvertical/smrt-core/migrations` is the public\n * subpath `db:migrate` reads it through.\n */\nexport { parsePostgresTimeoutMs };\n\n/**\n * Prefix stamped on the `error_message` of every concurrent-index migration\n * whose transactional half has already committed.\n *\n * This is the resume marker: it is the only durable record that phase 1's\n * statements ran, so a retry can skip straight to the index build. It must\n * therefore be carried by *both* the interim write at the end of phase 1 and\n * every failure recorded during phase 2 — a lock timeout on the index build\n * leaves the non-index DDL committed just as surely as a crash does.\n */\nconst CONCURRENT_INDEX_PHASE1_MARKER =\n '[smrt: concurrent-index phase 1 committed]';\n\n/**\n * Interim `error_message` for a migration whose transactional half has\n * committed but whose `CONCURRENTLY` index build has not run yet.\n */\nconst CONCURRENT_INDEX_PENDING_MESSAGE = `${CONCURRENT_INDEX_PHASE1_MARKER} Concurrent index build pending: the non-index half of this migration committed but the CONCURRENTLY index DDL had not completed. Re-run db:migrate to finish it.`;\n\n/**\n * Statements split out of the atomic batch for PostgreSQL concurrent-index\n * mode, keyed by migration definition id.\n */\ntype ConcurrentIndexPlan = Map<\n string,\n { regular: string[]; concurrent: string[] }\n>;\n\n/**\n * Minimal pinned-session shape used by the concurrent-index phase.\n *\n * `CREATE INDEX CONCURRENTLY` must run outside a transaction, and the\n * `SET lock_timeout` / `SET statement_timeout` that bound it are *session*\n * state — on a pooled adapter a top-level `db.query` may land on a different\n * connection, so the SET would silently not apply to the DDL (issue #2362).\n */\ninterface MigrationSession {\n query: (\n sql: string,\n ...vars: unknown[]\n ) => Promise<{ rows: Record<string, unknown>[]; rowCount?: number }>;\n release: () => Promise<void>;\n}\n\n/**\n * MigrationTracker class\n *\n * Provides migration tracking and state management:\n * - Initializes the _smrt_schema_migrations tracking table\n * - Tracks applied migrations with checksums\n * - Provides idempotent migration application\n * - Supports rollback via DOWN scripts\n * - Handles PostgreSQL-specific concerns (CONCURRENTLY, lock timeout)\n */\nexport class MigrationTracker {\n private db: DatabaseInterface;\n private options: Required<Omit<MigrationTrackerOptions, 'db' | 'engineHint'>>;\n private engineHint?: string;\n private dbEngine: DatabaseEngine;\n /**\n * Memoized initialization promise. Storing the in-flight promise (rather\n * than a `boolean` flag set after the DDL completes) makes `initialize()`\n * safe under concurrency: multiple parallel callers all `await` the same\n * promise instead of independently re-running the DDL. On error the slot\n * is cleared so the next caller retries — a transient failure doesn't\n * permanently poison the instance.\n */\n private initializePromise: Promise<void> | null = null;\n private currentBatch: number | null = null;\n /**\n * Statements deferred out of the current atomic batch by PostgreSQL\n * concurrent-index mode. The parent tracker sets this on the\n * transaction-scoped tracker before phase 1 so `apply()` executes only the\n * transaction-safe statements and leaves the migration record in `running`;\n * the parent finalizes those records after the transaction commits and the\n * `CONCURRENTLY` DDL has run. `null` on every ordinary path.\n */\n private concurrentIndexPlan: ConcurrentIndexPlan | null = null;\n\n constructor(options: MigrationTrackerOptions) {\n this.db = options.db;\n this.options = {\n lockTimeout: resolveTimeoutOption(\n options.lockTimeout,\n DEFAULT_OPTIONS.lockTimeout,\n ),\n statementTimeout: resolveTimeoutOption(\n options.statementTimeout,\n DEFAULT_OPTIONS.statementTimeout,\n ),\n useConcurrentIndexes:\n options.useConcurrentIndexes ?? DEFAULT_OPTIONS.useConcurrentIndexes,\n };\n this.engineHint = options.engineHint;\n this.dbEngine = this.detectDbEngine();\n }\n\n /**\n * Detect the database engine. Honors an explicit `engineHint` (passed via\n * `MigrationTrackerOptions`) before falling back to URL parsing — this is\n * the same `detectEngine` contract used by `SchemaComparer` and the rest\n * of the migration stack, so the tracker stays consistent with the DDL\n * the orchestrator generated.\n */\n private detectDbEngine(): DatabaseEngine {\n const dbWithConfig = this.db as DatabaseInterface & {\n config?: { url?: string };\n };\n const url = this.db.url || dbWithConfig.config?.url || '';\n return detectEngine(url, this.engineHint);\n }\n\n /**\n * Get the current database engine\n */\n getEngine(): DatabaseEngine {\n return this.dbEngine;\n }\n\n /**\n * Initialize the migrations tracking table\n */\n async initialize(): Promise<void> {\n if (!this.initializePromise) {\n this.initializePromise = this.runInitializeDdl().catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n private async runInitializeDdl(): Promise<void> {\n // Parse and execute each statement in the DDL\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n this.dbEngine,\n )\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n\n for (const statement of statements) {\n await this.db.query(statement);\n }\n\n await assertPostgresSystemTimestampsCurrent(this.db, this.engineHint);\n }\n\n /**\n * Get all applied migrations ordered by applied_at\n */\n async getAppliedMigrations(): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE status = 'completed' ORDER BY applied_at ASC`,\n );\n\n return (result.rows as MigrationRow[]).map(hydrateMigrationRow);\n }\n\n /**\n * Get pending migrations (defined but not applied)\n */\n async getPendingMigrations(\n definitions: MigrationDefinition[],\n ): Promise<MigrationDefinition[]> {\n const applied = await this.getAppliedMigrations();\n const appliedNames = new Set(applied.map((m) => m.name));\n return definitions.filter((d) => !appliedNames.has(d.id));\n }\n\n /**\n * Check if a migration has been applied\n */\n async isApplied(migrationId: string): Promise<boolean> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT 1 FROM _smrt_schema_migrations WHERE name = ? AND status = 'completed' LIMIT 1`,\n migrationId,\n );\n\n return result.rows.length > 0;\n }\n\n /**\n * Get a single migration record by name\n */\n async getMigration(name: string): Promise<SchemaMigrationRecord | null> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE name = ? LIMIT 1`,\n name,\n );\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as MigrationRow;\n return hydrateMigrationRow(row);\n }\n\n /**\n * Get the next batch number\n */\n async getNextBatch(): Promise<number> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT MAX(batch) as max_batch FROM _smrt_schema_migrations`,\n );\n\n const row = result.rows[0] as { max_batch?: unknown } | undefined;\n const maxBatch = toSafeInteger(row?.max_batch ?? 0, 'Migration batch');\n return toSafeInteger(maxBatch + 1, 'Next migration batch');\n }\n\n /**\n * Detect drift between migration definitions and applied migrations\n */\n async detectDrift(\n definitions: MigrationDefinition[],\n ): Promise<DriftReport[]> {\n const applied = await this.getAppliedMigrations();\n const reports: DriftReport[] = [];\n\n const appliedMap = new Map(applied.map((m) => [m.name, m]));\n const definitionMap = new Map(definitions.map((d) => [d.id, d]));\n\n // Check each applied migration against definitions\n for (const record of applied) {\n const definition = definitionMap.get(record.name);\n\n if (!definition) {\n // Migration applied but definition is missing\n reports.push({\n migration_name: record.name,\n expected_checksum: 'N/A',\n actual_checksum: record.checksum,\n drift_type: 'unknown_in_db',\n recommendation:\n 'Migration exists in database but definition is missing. Manual cleanup may be required.',\n });\n continue;\n }\n\n // Check checksum\n const currentChecksum = computeChecksum(definition.up);\n const verification = verifyChecksum(\n currentChecksum,\n record.checksum,\n record.applied_checksum,\n );\n\n if (!verification.valid) {\n const driftType = verification.drift ?? 'db_modified';\n reports.push({\n migration_name: record.name,\n expected_checksum: record.checksum,\n actual_checksum: currentChecksum,\n drift_type: driftType,\n recommendation:\n driftType === 'file_modified'\n ? 'Migration file was modified after application. Create a new migration instead.'\n : 'Database schema was modified outside of migrations. Review and reconcile manually.',\n });\n }\n }\n\n return reports;\n }\n\n /**\n * Apply a single migration\n */\n async apply(\n definition: MigrationDefinition,\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.up);\n const force = Boolean(\n options.force || options.forceMigrations?.includes(definition.id),\n );\n const hasScopedForce = Boolean(options.forceMigrations?.length);\n\n // Check if migration record already exists\n const existing = await this.getMigration(definition.id);\n\n if (existing) {\n // Handle based on existing status\n switch (existing.status) {\n case 'completed':\n if (!force && existing.checksum !== checksum) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} checksum mismatch. Was already applied with different checksum. Use --force-migration ${definition.id} to override only this migration, or --force to override the whole batch.`,\n };\n }\n\n if (!force && !options.reconcile) {\n // Already applied, skip\n return {\n success: true,\n applied: false,\n skipped: true,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n };\n }\n break;\n\n case 'failed':\n if (!force && (!options.reconcile || hasScopedForce)) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} previously failed: ${existing.error_message || 'Unknown error'}. Use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'running':\n if (!force) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} is currently running or was interrupted. If the previous run crashed, use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'rolled_back':\n // Rolled back migrations can be re-applied without --force\n break;\n }\n }\n\n if (options.dryRun) {\n return {\n success: true,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n };\n }\n\n // Get batch number\n if (this.currentBatch === null) {\n this.currentBatch = await this.getNextBatch();\n }\n\n // Determine if we're updating an existing record or inserting new\n const id = existing?.id || randomUUID();\n const attempts = toSafeInteger(\n (existing?.attempts ?? 0) + 1,\n 'Migration attempts',\n );\n\n if (existing) {\n // Update existing record to 'running'\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'running', checksum = ?, attempts = ?, error_message = NULL, batch = ?, applied_by = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n checksum,\n attempts,\n this.currentBatch,\n hostname(),\n id,\n );\n } else {\n // Insert new record\n await this.db.query(\n `INSERT INTO _smrt_schema_migrations\n (id, name, version, checksum, status, attempts, is_reversible, package_name, source_file, applied_by, batch)\n VALUES (?, ?, ?, ?, 'running', 1, ?, ?, ?, ?, ?)`,\n id,\n definition.id,\n definition.version,\n checksum,\n (definition.is_reversible ?? definition.down.length > 0) ? 1 : 0,\n definition.package_name ?? null,\n definition.source_file ?? null,\n hostname(),\n this.currentBatch,\n );\n }\n\n // PostgreSQL concurrent-index mode splits this definition: the\n // transaction-safe statements run here, the CONCURRENTLY index DDL runs\n // after the batch commits.\n const split = this.concurrentIndexPlan?.get(definition.id);\n const deferredConcurrent = (split?.concurrent.length ?? 0) > 0;\n\n // A retry of an interrupted concurrent-index migration must not re-run the\n // half that already committed. The marker on the previous attempt's\n // `error_message` says it did, and the checksum says it was this same\n // definition. Without this, a definition that mixes non-idempotent DDL\n // (`ALTER TABLE … ADD COLUMN`, which the differ emits without\n // `IF NOT EXISTS`) with index DDL would fail on \"column already exists\"\n // forever, never reaching the index build it was retrying for.\n const phase1AlreadyCommitted =\n deferredConcurrent &&\n existing?.status === 'failed' &&\n existing.checksum === checksum &&\n typeof existing.error_message === 'string' &&\n existing.error_message.startsWith(CONCURRENT_INDEX_PHASE1_MARKER);\n\n try {\n // Execute the migration\n await this.executeStatements(\n phase1AlreadyCommitted ? [] : split ? split.regular : definition.up,\n options,\n );\n\n const executionTime = Date.now() - startTime;\n\n if (deferredConcurrent) {\n // Commit this record as `failed`, not `completed` and not `running`.\n // The index does not exist yet, so `completed` would be a lie; and\n // unlike the single-transaction path — where a crash rolls the record\n // insert back with the DDL — this row is about to be committed, so a\n // crash before the concurrent phase finishes would strand a `running`\n // record that the next `db:migrate` can only clear with\n // `--force-migration`. `failed` is retryable under the CLI's\n // reconciling re-run, which is what an interrupted index rollout\n // needs. The concurrent phase overwrites this a moment later.\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', execution_time_ms = ?, error_message = ?\n WHERE id = ?`,\n executionTime,\n CONCURRENT_INDEX_PENDING_MESSAGE,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n }\n\n // Mark as completed\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n executionTime,\n checksum,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const migrationError =\n error instanceof Error ? error : new Error(String(error));\n\n // Mark as failed. In an outer atomic Postgres transaction, a DDL error\n // aborts the transaction and this status write must be skipped so the\n // caller can roll back the whole batch while preserving the root error.\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE id = ?`,\n migrationError.message,\n id,\n );\n } catch (persistError) {\n // Preserve the migration failure; status persistence is secondary and\n // may be impossible until the surrounding transaction rolls back.\n const persistMessage =\n persistError instanceof Error\n ? persistError.message\n : String(persistError);\n logger.error(\n `Failed to persist failed status for migration ${definition.id}: ${persistMessage}`,\n );\n }\n\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: migrationError,\n };\n }\n }\n\n /**\n * Apply multiple migrations in order.\n *\n * On PostgreSQL the atomic path always bounds itself with\n * `SET LOCAL lock_timeout` / `SET LOCAL statement_timeout` so a migration\n * that queues behind a long-running writer fails fast and rolls back\n * instead of stalling every writer behind its already-held locks\n * (issue #2362).\n *\n * `options.postgresSafe` additionally enables **concurrent-index mode**:\n * non-index DDL still runs in the one atomic transaction, then every index\n * statement runs `CONCURRENTLY` on a pinned session after that transaction\n * commits. That mode trades atomicity for availability — a failure part way\n * through the index phase leaves the committed non-index DDL in place, and\n * the unfinished index migrations are recorded `failed` so the next run\n * retries them, resuming at the index phase rather than re-running the\n * non-index statements that already committed.\n */\n async applyAll(\n definitions: MigrationDefinition[],\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult[]> {\n if (options.atomic && !options.dryRun) {\n if (!this.db.transaction) {\n throw new Error(\n 'Atomic migration batches require a database adapter with transaction support.',\n );\n }\n\n const isPostgres = this.dbEngine === 'postgres';\n const concurrentIndexMode = isPostgres && Boolean(options.postgresSafe);\n const plan = concurrentIndexMode\n ? buildConcurrentIndexPlan(\n definitions,\n this.options.useConcurrentIndexes,\n )\n : null;\n\n // Outside concurrent-index mode a CONCURRENTLY statement can only fail:\n // PostgreSQL rejects it inside a transaction block. Fail before opening\n // the transaction so the error names the offending migration.\n if (isPostgres && !concurrentIndexMode) {\n const concurrentStatement = findConcurrentIndexStatement(definitions);\n if (concurrentStatement) {\n throw new Error(\n `Atomic migration batch cannot include CONCURRENTLY index DDL in ${concurrentStatement.migrationName}; PostgreSQL forbids CONCURRENTLY inside a transaction. Re-run with PostgreSQL concurrent-index mode (smrt db:migrate --postgres-safe) to build those indexes outside the batch.`,\n );\n }\n }\n\n const deferred = plan && plan.size > 0 ? plan : null;\n\n try {\n const committed = await this.db.transaction(async (tx) => {\n const txDb = {\n ...tx,\n transaction: async <T>(\n callback: (transactionDb: DatabaseInterface) => Promise<T>,\n ) => callback(tx as DatabaseInterface),\n } as DatabaseInterface;\n\n // Bound the whole batch. `SET LOCAL` reverts at COMMIT/ROLLBACK, so\n // this cannot leak onto a pooled connection's next checkout.\n if (isPostgres) {\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n }\n\n const txTracker = new MigrationTracker({\n db: txDb,\n lockTimeout: this.options.lockTimeout,\n statementTimeout: this.options.statementTimeout,\n useConcurrentIndexes: false,\n });\n txTracker.concurrentIndexPlan = deferred;\n const results = await txTracker.applyAll(definitions, {\n ...options,\n atomic: false,\n continueOnError: false,\n postgresSafe: false,\n // Deferred migrations report progress from the concurrent phase\n // once they have actually finished, not when phase 1 commits.\n onProgress: options.onProgress\n ? (result) => {\n if (!deferred?.has(result.name)) {\n options.onProgress?.(result);\n }\n }\n : undefined,\n });\n const failed = results.find((result) => !result.success);\n\n if (failed) {\n throw new AtomicMigrationRollback(results, failed);\n }\n\n return results;\n });\n\n if (!deferred) {\n return committed;\n }\n\n return await this.applyDeferredConcurrentIndexes(\n committed,\n deferred,\n options,\n );\n } catch (error) {\n if (error instanceof AtomicMigrationRollback) {\n return error.results.map((result) => {\n if (!result.success || !result.applied) {\n return result;\n }\n\n return {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n rolled_back: true,\n error: new Error(\n `Rolled back because migration ${error.failed.name} failed`,\n ),\n };\n });\n }\n\n throw error;\n }\n }\n\n const results: MigrationResult[] = [];\n this.currentBatch = await this.getNextBatch();\n\n for (const definition of definitions) {\n const result = await this.apply(definition, options);\n results.push(result);\n options.onProgress?.(result);\n\n if (!result.success && !options.continueOnError) {\n break;\n }\n }\n\n this.currentBatch = null;\n return results;\n }\n\n /**\n * Phase 2 of PostgreSQL concurrent-index mode.\n *\n * Runs every deferred index statement on one pinned session, outside the\n * committed transaction, bounded by session `lock_timeout` /\n * `statement_timeout`. INVALID indexes left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` are dropped first so the retry rebuilds them\n * rather than skipping over a stump that reads as present.\n *\n * On failure the batch stops: the failing migration and every migration it\n * did not reach are recorded `failed`, which the CLI's reconciling\n * `db:migrate` retries on the next run.\n */\n private async applyDeferredConcurrentIndexes(\n committed: MigrationResult[],\n plan: ConcurrentIndexPlan,\n options: ApplyMigrationsOptions,\n ): Promise<MigrationResult[]> {\n const results = [...committed];\n const session = await this.acquireMigrationSession();\n\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n let stoppedBy: string | null = null;\n\n for (let index = 0; index < results.length; index++) {\n const result = results[index];\n const split = plan.get(result.name);\n if (!split || split.concurrent.length === 0) {\n continue;\n }\n\n // A migration phase 1 skipped (already applied, checksum matched) is\n // already `completed` in the tracking table — its index exists and\n // must not be rebuilt or have its record rewritten.\n if (result.skipped) {\n continue;\n }\n\n if (stoppedBy) {\n const error = new Error(\n `Not attempted: concurrent index phase stopped after ${stoppedBy} failed`,\n );\n await this.recordConcurrentIndexFailure(result.name, error.message);\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n error,\n };\n options.onProgress?.(results[index]);\n continue;\n }\n\n const startedAt = Date.now();\n\n try {\n for (const sql of split.concurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n // An INVALID index is not usable by the planner and blocks the\n // name, but `CREATE INDEX … IF NOT EXISTS` treats it as present.\n // Drop it so the create below actually rebuilds it.\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n\n // Floor at 1ms: callers (the CLI progress reporter, the applied vs\n // skipped counters) read `execution_time_ms === 0` as \"nothing ran\".\n const executionTime = Math.max(\n 1,\n (result.execution_time_ms ?? 0) + (Date.now() - startedAt),\n );\n await this.recordConcurrentIndexSuccess(\n result.name,\n result.checksum,\n executionTime,\n );\n results[index] = {\n ...result,\n success: true,\n applied: true,\n skipped: false,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const indexError =\n error instanceof Error ? error : new Error(String(error));\n if (!options.continueOnError) {\n stoppedBy = result.name;\n }\n await this.recordConcurrentIndexFailure(\n result.name,\n indexError.message,\n );\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n execution_time_ms: Date.now() - startedAt,\n error: indexError,\n };\n }\n\n options.onProgress?.(results[index]);\n }\n\n return results;\n } finally {\n await session.release();\n }\n }\n\n /**\n * Pin one connection for the concurrent-index phase.\n *\n * PostgreSQL pools hand each top-level `query` an arbitrary connection, so\n * a `SET lock_timeout` issued through the pool may not apply to the DDL\n * that follows. Adapters that expose `acquireSession` give us a pinned\n * connection; anything else (SQLite, DuckDB, test doubles) is\n * single-connection anyway and falls back to the plain interface.\n */\n private async acquireMigrationSession(): Promise<MigrationSession> {\n if (typeof this.db.acquireSession === 'function') {\n const session = await this.db.acquireSession();\n return {\n query: (sql, ...vars) => session.query(sql, ...vars),\n release: () => session.release(),\n };\n }\n\n return {\n query: (sql, ...vars) => this.db.query(sql, ...vars),\n release: async () => {},\n };\n }\n\n /**\n * Read the names of INVALID indexes that an unqualified index name in this\n * session would actually resolve to.\n *\n * `pg_indexes` (what the SQL adapter's introspection reads) lists an\n * INVALID index exactly like a healthy one, so the schema differ believes a\n * failed `CREATE INDEX CONCURRENTLY` succeeded. `pg_index.indisvalid` is the\n * authoritative flag.\n *\n * Scoped to `current_schemas(false)` — the session's search path — because\n * migration DDL names indexes unqualified, and so does the\n * `DROP INDEX CONCURRENTLY IF EXISTS \"<name>\"` repair. Scanning every schema\n * would let an unrelated invalid index of the same name in another schema\n * trigger a drop that then resolves to a *different* object. Where the same\n * name appears in several search-path schemas, the earliest on the path wins\n * — the same rule PostgreSQL uses to resolve the name.\n *\n * A role without catalog access degrades to \"no known invalid indexes\"\n * rather than aborting the migration.\n */\n private async readInvalidIndexNames(\n session: MigrationSession,\n ): Promise<Set<string>> {\n const names = new Set<string>();\n\n try {\n const result = await session.query(\n `SELECT DISTINCT ON (c.relname) c.relname AS index_name\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE i.indisvalid = false\n AND n.nspname = ANY (current_schemas(false))\n ORDER BY c.relname,\n array_position(current_schemas(false), n.nspname::text)`,\n );\n\n for (const row of result.rows) {\n const name = (row as { index_name?: unknown }).index_name;\n if (typeof name === 'string') {\n names.add(name);\n }\n }\n } catch (error) {\n logger.warn(\n `Could not read pg_index.indisvalid; skipping INVALID index repair: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n return names;\n }\n\n private async recordConcurrentIndexSuccess(\n name: string,\n checksum: string,\n executionTime: number,\n ): Promise<void> {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, error_message = NULL, applied_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n executionTime,\n checksum,\n name,\n );\n }\n\n private async recordConcurrentIndexFailure(\n name: string,\n message: string,\n ): Promise<void> {\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE name = ?`,\n // Keep the resume marker: this failure happened *after* phase 1\n // committed, so the retry must skip the non-index statements too.\n `${CONCURRENT_INDEX_PHASE1_MARKER} ${message}`,\n name,\n );\n } catch (persistError) {\n logger.error(\n `Failed to persist failed status for migration ${name}: ${\n persistError instanceof Error\n ? persistError.message\n : String(persistError)\n }`,\n );\n }\n }\n\n /**\n * Rollback a migration\n */\n async rollback(\n name: string,\n definition: MigrationDefinition,\n options: RollbackOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.down);\n\n const record = await this.getMigration(name);\n if (!record) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(`Migration ${name} not found in database`),\n };\n }\n\n if (record.status !== 'completed') {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not in completed state (current: ${record.status})`,\n ),\n };\n }\n\n if (!definition.down.length) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not reversible (no down statements)`,\n ),\n };\n }\n\n if (options.dryRun) {\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n }\n\n try {\n // Execute DOWN statements\n await this.executeStatements(definition.down, {});\n\n // Mark as rolled back\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'rolled_back', rolled_back_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n name,\n );\n\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n } catch (error) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: error instanceof Error ? error : new Error(String(error)),\n rolled_back: false,\n };\n }\n }\n\n /**\n * Get migration history with optional filtering\n */\n async getHistory(\n options: { limit?: number; since?: Date; status?: MigrationStatus } = {},\n ): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n let sql = `SELECT * FROM _smrt_schema_migrations WHERE 1=1`;\n const params: unknown[] = [];\n\n if (options.status) {\n sql += ` AND status = ?`;\n params.push(options.status);\n }\n\n if (options.since) {\n sql += ` AND applied_at >= ?`;\n params.push(options.since.toISOString());\n }\n\n sql += ` ORDER BY applied_at DESC`;\n\n if (options.limit) {\n sql += ` LIMIT ?`;\n params.push(options.limit);\n }\n\n const result = await this.db.query(sql, ...params);\n\n return (result.rows as MigrationRow[]).map(hydrateMigrationRow);\n }\n\n /**\n * Execute SQL statements with appropriate transaction handling\n */\n private async executeStatements(\n statements: string[],\n options: ApplyMigrationsOptions,\n ): Promise<void> {\n if (this.dbEngine === 'postgres' && options.postgresSafe) {\n await this.executePostgresStatements(statements);\n } else {\n await this.executeWithTransaction(statements);\n }\n }\n\n /**\n * Execute statements within a transaction (SQLite/DuckDB)\n */\n private async executeWithTransaction(statements: string[]): Promise<void> {\n if (this.db.transaction) {\n await this.db.transaction(async (tx) => {\n for (const sql of statements) {\n await tx.query(sql);\n }\n });\n } else {\n // Fallback to sequential execution\n for (const sql of statements) {\n await this.db.query(sql);\n }\n }\n }\n\n /**\n * Execute statements with PostgreSQL-specific handling\n *\n * - CONCURRENTLY statements run outside transaction\n * - Regular statements run in transaction with lock_timeout\n *\n * Both CREATE and DROP variants of CONCURRENTLY must be detected — Postgres\n * forbids `DROP INDEX CONCURRENTLY` inside a transaction block, just like\n * the create variant. Issue #1165: the migration generator emits\n * `DROP INDEX CONCURRENTLY` for shape-drift recreates and orphan-index\n * cleanups, so the regex needs to cover both keywords.\n */\n private async executePostgresStatements(statements: string[]): Promise<void> {\n const { concurrent: finalConcurrent, regular: finalRegular } =\n planPostgresStatements(statements, !!this.options.useConcurrentIndexes);\n\n // CONCURRENTLY statements come from `planPostgresStatements`, which\n // also pushes any post-transform CREATE/DROP CONCURRENTLY into the\n // outside-transaction set. The original `concurrentStatements` list\n // is folded in there too so callers below see one merged stream.\n\n // Execute regular statements in transaction with timeouts\n if (finalRegular.length > 0 && this.db.transaction) {\n await this.db.transaction(async (tx) => {\n // Set session-level timeouts\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n for (const sql of finalRegular) {\n await tx.query(sql);\n }\n });\n } else if (finalRegular.length > 0) {\n for (const sql of finalRegular) {\n await this.db.query(sql);\n }\n }\n\n if (finalConcurrent.length === 0) {\n return;\n }\n\n // Execute CONCURRENTLY statements outside the transaction, on one pinned\n // connection so `SET lock_timeout` / `SET statement_timeout` actually\n // govern the DDL that follows them (a pooled `db.query` may not reuse the\n // connection the SET landed on).\n const session = await this.acquireMigrationSession();\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n\n for (const sql of finalConcurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n } finally {\n await session.release();\n }\n }\n}\n\n/**\n * Quote an index identifier for PostgreSQL, escaping embedded double quotes.\n */\nfunction quoteIndexName(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Build the PostgreSQL concurrent-index plan for an atomic batch.\n *\n * Returns one entry per migration definition that has at least one statement\n * which must leave the transaction, mapping it to the transaction-safe\n * statements (phase 1) and the `CONCURRENTLY` statements (phase 2).\n * Definitions with nothing to defer are omitted so the caller can treat\n * `plan.has(id)` as \"this migration finishes in phase 2\".\n *\n * Exported for unit testing.\n */\nexport function buildConcurrentIndexPlan(\n definitions: MigrationDefinition[],\n useConcurrentIndexes: boolean,\n): ConcurrentIndexPlan {\n const plan: ConcurrentIndexPlan = new Map();\n\n const splits = definitions.map((definition) => ({\n id: definition.id,\n split: planPostgresStatements(definition.up, useConcurrentIndexes, {\n swapUniqueRecreates: false,\n }),\n }));\n // A same-name UNIQUE recreate spans two definitions (the differ emits\n // `drop_index` then `add_index`); phase 2 runs them in batch order.\n const swapped = swapUniqueIndexRecreates(\n splits.map(({ split }) => split.concurrent),\n );\n splits.forEach(({ id, split }, index) => {\n const concurrent = swapped[index] ?? split.concurrent;\n if (concurrent.length > 0) {\n plan.set(id, { regular: split.regular, concurrent });\n }\n });\n\n return plan;\n}\n\nconst VALIDATE_CONSTRAINT_RE = /\\bVALIDATE\\s+CONSTRAINT\\b/i;\n\nconst DROP_INDEX_CONCURRENTLY_RE =\n /^\\s*DROP\\s+INDEX\\s+CONCURRENTLY\\s+(?:IF\\s+EXISTS\\s+)?(?:\"((?:[^\"]|\"\")+)\"|([A-Za-z_][\\w$]*))\\s*;?\\s*$/i;\n\nconst CREATE_UNIQUE_INDEX_CONCURRENTLY_IF_NOT_EXISTS_RE =\n /^(\\s*CREATE\\s+UNIQUE\\s+INDEX\\s+CONCURRENTLY\\s+IF\\s+NOT\\s+EXISTS\\s+)(?:\"((?:[^\"]|\"\")+)\"|([A-Za-z_][\\w$]*))/i;\n\nfunction unquoteIndexName(quoted?: string, bare?: string): string | null {\n if (quoted !== undefined) return quoted.replace(/\"\"/g, '\"');\n return bare ?? null;\n}\n\n/**\n * Keep a unique index in place while `--postgres-safe` recreates it.\n *\n * A same-name shape-drift repair is `DROP INDEX` followed by `CREATE UNIQUE\n * INDEX` under the same name. Run CONCURRENTLY, the drop commits before the\n * build starts, leaving a window with NO unique index: every\n * `ON CONFLICT (…)` upsert fails (42P10) and plain inserts can write\n * duplicates, which then make the build itself fail. This rewrites the drop\n * into build-then-swap — `CREATE UNIQUE INDEX CONCURRENTLY` under a\n * temporary name, then `DROP INDEX CONCURRENTLY` of the old one, then\n * `ALTER INDEX … RENAME` — so the table always has a unique index over the\n * old or the new key. The later create keeps its `IF NOT EXISTS` and becomes\n * a no-op. Only rewritten when the create carries `IF NOT EXISTS` (the\n * differ's DDL always does).\n *\n * @param lists - concurrent statement lists in execution order\n * @returns the rewritten lists (same length and order)\n */\nexport function swapUniqueIndexRecreates(lists: string[][]): string[][] {\n const result = lists.map((list) => [...list]);\n for (let li = 0; li < result.length; li++) {\n const list = result[li] ?? [];\n for (let si = 0; si < list.length; si++) {\n const drop = DROP_INDEX_CONCURRENTLY_RE.exec(list[si] ?? '');\n if (!drop) continue;\n const name = unquoteIndexName(drop[1], drop[2]);\n if (!name) continue;\n const create = findLaterUniqueCreate(result, li, si, name);\n if (!create) continue;\n const temporary = shortenIdentifier(`${name}_smrt_swap`);\n const buildTemporary = create.replace(\n CREATE_UNIQUE_INDEX_CONCURRENTLY_IF_NOT_EXISTS_RE,\n (_match, head: string) => `${head}${quoteIndexName(temporary)}`,\n );\n list.splice(\n si,\n 1,\n buildTemporary,\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(name)}`,\n `ALTER INDEX IF EXISTS ${quoteIndexName(temporary)} RENAME TO ${quoteIndexName(name)}`,\n );\n si += 2;\n }\n }\n return result;\n}\n\nfunction findLaterUniqueCreate(\n lists: string[][],\n fromList: number,\n fromStatement: number,\n name: string,\n): string | null {\n for (let li = fromList; li < lists.length; li++) {\n const list = lists[li] ?? [];\n const start = li === fromList ? fromStatement + 1 : 0;\n for (let si = start; si < list.length; si++) {\n const sql = list[si] ?? '';\n const match = CREATE_UNIQUE_INDEX_CONCURRENTLY_IF_NOT_EXISTS_RE.exec(sql);\n if (match && unquoteIndexName(match[2], match[3]) === name) return sql;\n const created = extractCreatedIndexName(sql);\n // Any other create/drop of the same name first ends the search.\n if (created === name) return null;\n const dropped = DROP_INDEX_CONCURRENTLY_RE.exec(sql);\n if (dropped && unquoteIndexName(dropped[1], dropped[2]) === name) {\n return null;\n }\n }\n }\n return null;\n}\n\n/**\n * Split a batch of SQL statements into a transaction-safe set and a\n * concurrent set, applying `--postgres-safe` rewriting if requested.\n *\n * Exported for unit testing and to give external callers a way to\n * reason about what the tracker would execute in PostgreSQL safe mode.\n *\n * Rules:\n * - Statements already containing `CONCURRENTLY` for CREATE/DROP INDEX or\n * REINDEX operations always go to the concurrent set — Postgres rejects\n * them inside transactions.\n * - When `useConcurrentIndexes` is true, plain `CREATE INDEX` and plain\n * `DROP INDEX` are rewritten to use `CONCURRENTLY`, then routed to\n * the concurrent set. This is what the CLI `--postgres-safe` flag and\n * the auto-migrate path rely on for issue #1165's shape-drift drops.\n * - All other statements stay in the regular (transaction) set.\n * - `ALTER TABLE … VALIDATE CONSTRAINT` (a foreign key added `NOT VALID`)\n * and every later statement of the same migration run outside the\n * transaction, in order: the validation scan then holds only SHARE UPDATE\n * EXCLUSIVE (reads and writes continue) instead of the batch's locks.\n * - A same-name UNIQUE recreate (`DROP INDEX` then `CREATE UNIQUE INDEX`) is\n * rewritten into build-then-swap ({@link swapUniqueIndexRecreates}) so the\n * table never runs without a unique index between the two statements.\n */\nexport function planPostgresStatements(\n statements: string[],\n useConcurrentIndexes: boolean,\n options: { swapUniqueRecreates?: boolean } = {},\n): { concurrent: string[]; regular: string[] } {\n const concurrentRegex = CONCURRENT_INDEX_STATEMENT_RE;\n\n const concurrent: string[] = [];\n const regular: string[] = [];\n // Once a VALIDATE CONSTRAINT leaves the transaction, every later statement\n // of the same migration follows it, in order (a foreign-key replacement\n // validates its staged constraint, then drops the old one and renames).\n let deferRest = false;\n\n for (const sql of statements) {\n if (\n useConcurrentIndexes &&\n !deferRest &&\n VALIDATE_CONSTRAINT_RE.test(sql)\n ) {\n deferRest = true;\n }\n if (deferRest && !sql.trimStart().startsWith(NULL_EQUAL_INDEX_MARKER)) {\n concurrent.push(\n /DROP\\s+INDEX\\s+(?!CONCURRENTLY)/i.test(sql)\n ? sql.replace(/DROP\\s+INDEX\\s+/i, 'DROP INDEX CONCURRENTLY ')\n : /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?!CONCURRENTLY)/i.test(sql)\n ? sql.replace(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+/i,\n 'CREATE $1INDEX CONCURRENTLY ',\n )\n : sql,\n );\n continue;\n }\n if (sql.trimStart().startsWith(NULL_EQUAL_INDEX_MARKER)) {\n if (useConcurrentIndexes) {\n throw new Error(\n 'NULL-equal conflict index DDL requires an atomic maintenance-window migration; retry without --postgres-safe. Existing indexes require db:migrate-null-equal-indexes.',\n );\n }\n regular.push(sql);\n continue;\n }\n if (concurrentRegex.test(sql)) {\n concurrent.push(sql);\n continue;\n }\n if (useConcurrentIndexes) {\n if (/CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+/i,\n 'CREATE $1INDEX CONCURRENTLY ',\n ),\n );\n continue;\n }\n if (/DROP\\s+INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(/DROP\\s+INDEX\\s+/i, 'DROP INDEX CONCURRENTLY '),\n );\n continue;\n }\n }\n regular.push(sql);\n }\n\n if (options.swapUniqueRecreates === false) {\n return { concurrent, regular };\n }\n return {\n concurrent: swapUniqueIndexRecreates([concurrent])[0] ?? concurrent,\n regular,\n };\n}\n"],"mappings":"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"}
@@ -10,5 +10,6 @@ import { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnit
10
10
  import { collectNullEqualIndexTargets, migrateNullEqualIndexes, nullEqualDuplicateDetector, nullEqualIndexStatements, preflightNullEqualIndexes } from "./migrations/null-equal-indexes.js";
11
11
  import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./migrations/tracker.js";
12
12
  import { getPendingSchemaStatements, migrateSmrtSchemas } from "./migrations/orchestrate.js";
13
+ import { LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, backfillLegacyQualifiedNames, collectLegacyQualifiedNameTargets, countLegacyQualifiedNameReferences, legacyQualifiedNamesBackfillName } from "./migrations/qualified-name-aliases.js";
13
14
  import "./migrations/index.js";
14
- export { BackfillTableUnavailableError, BackfillTracker, FRAMEWORK_BASE_TABLE_NAMES, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, REQUIRED_COLUMN_NOT_ADDED, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, collectNullEqualIndexTargets, computeChecksum, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateNullEqualIndexes, migrateSmrtSchemas, normalizeSQL, nullEqualDuplicateDetector, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, preflightNullEqualIndexes, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
15
+ export { BackfillTableUnavailableError, BackfillTracker, FRAMEWORK_BASE_TABLE_NAMES, LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, REQUIRED_COLUMN_NOT_ADDED, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, backfillLegacyQualifiedNames, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, collectLegacyQualifiedNameTargets, collectNullEqualIndexTargets, computeChecksum, countLegacyQualifiedNameReferences, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, legacyQualifiedNamesBackfillName, migrateNullEqualIndexes, migrateSmrtSchemas, normalizeSQL, nullEqualDuplicateDetector, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, preflightNullEqualIndexes, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };