@happyvertical/smrt-core 0.41.0 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/AGENTS.md +1 -1
  2. package/agents/schema-paths.md +79 -8
  3. package/dist/change-feed.d.ts.map +1 -1
  4. package/dist/change-feed.js +2 -2
  5. package/dist/change-feed.js.map +1 -1
  6. package/dist/change-signals.d.ts.map +1 -1
  7. package/dist/change-signals.js +8 -1
  8. package/dist/change-signals.js.map +1 -1
  9. package/dist/class.d.ts.map +1 -1
  10. package/dist/class.js +10 -9
  11. package/dist/class.js.map +1 -1
  12. package/dist/collection.d.ts.map +1 -1
  13. package/dist/collection.js +2 -2
  14. package/dist/collection.js.map +1 -1
  15. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
  16. package/dist/dispatch/collections/Dispatches.js +5 -5
  17. package/dist/dispatch/collections/Dispatches.js.map +1 -1
  18. package/dist/dispatch/models/Dispatch.d.ts +1 -1
  19. package/dist/dispatch/models/Dispatch.d.ts.map +1 -1
  20. package/dist/dispatch/models/Dispatch.js +3 -2
  21. package/dist/dispatch/models/Dispatch.js.map +1 -1
  22. package/dist/dispatch/models/DispatchSubscription.d.ts +1 -1
  23. package/dist/dispatch/models/DispatchSubscription.d.ts.map +1 -1
  24. package/dist/dispatch/models/DispatchSubscription.js +2 -1
  25. package/dist/dispatch/models/DispatchSubscription.js.map +1 -1
  26. package/dist/embeddings/storage.d.ts.map +1 -1
  27. package/dist/embeddings/storage.js +2 -1
  28. package/dist/embeddings/storage.js.map +1 -1
  29. package/dist/learning/memory.d.ts.map +1 -1
  30. package/dist/learning/memory.js +5 -4
  31. package/dist/learning/memory.js.map +1 -1
  32. package/dist/manifest/static-manifest.js +1 -1
  33. package/dist/manifest/static-manifest.js.map +1 -1
  34. package/dist/manifest/store.js +1 -1
  35. package/dist/manifest.json +1 -1
  36. package/dist/migrations/differ.js +2 -2
  37. package/dist/migrations/differ.js.map +1 -1
  38. package/dist/migrations/index.d.ts +1 -0
  39. package/dist/migrations/index.d.ts.map +1 -1
  40. package/dist/migrations/index.js +2 -1
  41. package/dist/migrations/integer-width.d.ts +99 -0
  42. package/dist/migrations/integer-width.d.ts.map +1 -0
  43. package/dist/migrations/integer-width.js +237 -0
  44. package/dist/migrations/integer-width.js.map +1 -0
  45. package/dist/migrations/minor-units.d.ts +2 -2
  46. package/dist/migrations/minor-units.d.ts.map +1 -1
  47. package/dist/migrations/minor-units.js +16 -14
  48. package/dist/migrations/minor-units.js.map +1 -1
  49. package/dist/migrations/tracker.d.ts.map +1 -1
  50. package/dist/migrations/tracker.js +28 -20
  51. package/dist/migrations/tracker.js.map +1 -1
  52. package/dist/migrations.js +2 -1
  53. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  54. package/dist/schema/ddl/duckdb-strategy.js +1 -0
  55. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  56. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  57. package/dist/schema/ddl/postgres-strategy.js +2 -0
  58. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  59. package/dist/schema/generator.d.ts +1 -33
  60. package/dist/schema/generator.d.ts.map +1 -1
  61. package/dist/schema/generator.js +1 -150
  62. package/dist/schema/generator.js.map +1 -1
  63. package/dist/schema/index.d.ts +1 -2
  64. package/dist/schema/index.d.ts.map +1 -1
  65. package/dist/schema/index.js +1 -2
  66. package/dist/schema/live-parity.d.ts +3 -1
  67. package/dist/schema/live-parity.d.ts.map +1 -1
  68. package/dist/schema/live-parity.js +19 -0
  69. package/dist/schema/live-parity.js.map +1 -1
  70. package/dist/schema/types.d.ts +17 -10
  71. package/dist/schema/types.d.ts.map +1 -1
  72. package/dist/schema.js +1 -2
  73. package/dist/smrt-knowledge.json +7 -7
  74. package/dist/system/retention.d.ts.map +1 -1
  75. package/dist/system/retention.js +3 -3
  76. package/dist/system/retention.js.map +1 -1
  77. package/dist/system/schema.d.ts +2 -1
  78. package/dist/system/schema.d.ts.map +1 -1
  79. package/dist/system/schema.js +4 -2
  80. package/dist/system/schema.js.map +1 -1
  81. package/dist/utils/safe-integer.d.ts +19 -0
  82. package/dist/utils/safe-integer.d.ts.map +1 -0
  83. package/dist/utils/safe-integer.js +31 -0
  84. package/dist/utils/safe-integer.js.map +1 -0
  85. package/dist/utils.d.ts +2 -1
  86. package/dist/utils.d.ts.map +1 -1
  87. package/dist/utils.js +6 -3
  88. package/dist/utils.js.map +1 -1
  89. package/dist/vite-plugin/index.d.ts.map +1 -1
  90. package/dist/vite-plugin/index.js +0 -53
  91. package/dist/vite-plugin/index.js.map +1 -1
  92. package/package.json +4 -4
  93. package/dist/schema/override-system.d.ts +0 -43
  94. package/dist/schema/override-system.d.ts.map +0 -1
  95. package/dist/schema/override-system.js +0 -205
  96. package/dist/schema/override-system.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"minor-units.js","names":[],"sources":["../../src/migrations/minor-units.ts"],"sourcesContent":["/**\n * Money-column rescale: floating-point major units → integer minor units (#2401).\n *\n * SMRT's rule is that money is exact and is stored as **integer minor units**\n * (cents, satoshis) — `$19.99` is `1999`. Packages that predate the rule\n * declared their money fields `= 0.0`, which compiles to a floating-point\n * column holding *major* units. Flipping the declaration to `= 0` changes the\n * column type, and REAL→INTEGER is deliberately **not** one of the differ's\n * whitelisted upgrades: an automatic `ALTER … TYPE integer` would truncate\n * `19.99` to `19`, silently destroying 99 cents on every row.\n *\n * So the conversion is an explicit, opt-in migration with two halves:\n *\n * 1. {@link preflightMinorUnitsRescale} — read-only. Reports, per column,\n * whether it still needs converting, and lists the rows that would lose\n * information: values whose scaled form is not a whole number (a half-cent\n * that has to be rounded away) and values that overflow PostgreSQL's `int4`\n * once multiplied by the scale. Run it against a production snapshot and\n * read the summary before converting anything.\n * 2. {@link rescaleMoneyColumnsToMinorUnits} — the conversion itself, guarded\n * by a {@link BackfillTracker} marker so re-running it can never multiply a\n * table by 100 twice.\n *\n * Per-engine behaviour:\n *\n * | Engine | Column type | Values |\n * | --- | --- | --- |\n * | PostgreSQL | `ALTER COLUMN … TYPE integer USING round(col * scale)` | converted by the cast |\n * | DuckDB | `ALTER COLUMN … TYPE INTEGER USING round(col * scale)` | converted by the cast |\n * | SQLite | **not changed here** — SQLite cannot alter a column's declared type in place | rescaled with an `UPDATE`, values become exact integers |\n *\n * SQLite's declared type only sets an affinity, so a rescaled column already\n * behaves as integer minor units; bringing the *declaration* into line needs\n * the table-rebuild path (#2370). The result flags that with\n * {@link MinorUnitsRescaleResult.declaredTypeChangePending} rather than\n * pretending the column type moved.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { BackfillTracker } from './backfill-tracker.js';\nimport type { DatabaseInterface } from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** PostgreSQL `int4` bounds — the ceiling this conversion has to respect. */\nconst INT4_MIN = -2147483648;\nconst INT4_MAX = 2147483647;\n\n/**\n * How far a scaled value may sit from a whole number and still count as\n * integral.\n *\n * `19.99 * 100` is `1998.9999999999998` in IEEE-754, so a zero tolerance would\n * report every ordinary two-decimal price as non-integral. A genuine half-cent\n * is off by `0.5`, five orders of magnitude above this bound, so the two cases\n * never blur.\n */\nconst INTEGRALITY_TOLERANCE = 1e-6;\n\n/** Default minor units per major unit — cents. */\nconst DEFAULT_SCALE = 100;\n\n/**\n * Round half away from zero, the way SQL `round()` does — and the way\n * `Math.round` does not.\n *\n * `Math.round` rounds halves toward +∞, so `Math.round(-0.5)` is `-0` while\n * SQLite's and DuckDB's `round(-0.5)` is `-1`. Money here can legitimately be\n * negative (`BillingAdjustment.amount` is a signed credit), so a preflight that\n * predicted the conversion with `Math.round` would report a value one minor\n * unit above what the database actually stores. The PostgreSQL DDL casts to\n * `numeric` for the same reason — see {@link buildMinorUnitsStatements}.\n */\nfunction roundHalfAwayFromZero(value: number): number {\n return Math.sign(value) * Math.round(Math.abs(value));\n}\n\n/** A table and the money columns on it that must move to minor units. */\nexport interface MoneyColumnTarget {\n /** Physical table name, e.g. `payments`. */\n table: string;\n /** Physical column names, e.g. `['amount', 'native_amount']`. */\n columns: string[];\n}\n\n/** What the preflight found about one column. */\nexport type MinorUnitsColumnState =\n /** Floating-point today: needs converting. */\n | 'pending'\n /** Already an integer column: nothing to do. */\n | 'converted'\n /** Table or column absent (package not installed, older schema). */\n | 'missing';\n\n/** One row that cannot be converted without losing information. */\nexport interface MinorUnitsProblemRow {\n /** Primary key, when the table has an `id` column. */\n id: string | null;\n /** The stored major-unit value. */\n value: number;\n /** `value * scale`, before rounding. */\n scaled: number;\n}\n\n/** Preflight findings for a single column. */\nexport interface MinorUnitsColumnReport {\n table: string;\n column: string;\n state: MinorUnitsColumnState;\n /** Declared column type as the database reports it, when resolvable. */\n declaredType: string | null;\n /** Non-null values inspected. `0` unless `state` is `pending`. */\n inspectedRows: number;\n /** Values whose scaled form is not a whole number — rounding loses money. */\n nonIntegral: MinorUnitsProblemRow[];\n /** Values whose scaled form falls outside PostgreSQL `int4`. */\n overflow: MinorUnitsProblemRow[];\n}\n\n/** Result of {@link preflightMinorUnitsRescale}. */\nexport interface MinorUnitsPreflightResult {\n engine: string;\n scale: number;\n columns: MinorUnitsColumnReport[];\n /** Columns still holding floating-point major units. */\n pendingColumns: number;\n /** Columns already integer. */\n convertedColumns: number;\n /** Columns whose table/column does not exist. */\n missingColumns: number;\n /** Total rows that would be rounded. */\n nonIntegralRows: number;\n /** Total rows that would overflow `int4`. */\n overflowRows: number;\n /** No row would lose information. */\n ok: boolean;\n /** Human-readable report, suitable for pasting into a migration log or PR. */\n summary: string;\n}\n\n/** Options shared by the preflight and the rescale. */\nexport interface MinorUnitsOptions {\n /** Minor units per major unit. Default `100` (cents). */\n scale?: number;\n /** Engine hint for adapters whose URL does not identify the engine. */\n engineHint?: string;\n /** Problem rows recorded per column before truncating. Default `20`. */\n maxProblemRows?: number;\n}\n\n/** Options for {@link rescaleMoneyColumnsToMinorUnits}. */\nexport interface MinorUnitsRescaleOptions extends MinorUnitsOptions {\n /**\n * Stable marker name recorded in `_smrt_backfills`, e.g.\n * `@happyvertical/smrt-commerce:money-minor-units:v1`.\n *\n * This is what makes the migration idempotent on SQLite, where the rescale\n * is an `UPDATE` no column type can distinguish from an unconverted table.\n */\n backfillName: string;\n /** Package recorded alongside the marker. */\n packageName?: string;\n /**\n * Convert even when the preflight found rows that would be rounded or\n * overflow. Off by default: silently rounding a half-cent away is exactly\n * the failure this migration exists to prevent.\n */\n force?: boolean;\n}\n\n/** Result of {@link rescaleMoneyColumnsToMinorUnits}. */\nexport interface MinorUnitsRescaleResult {\n /** `false` when the backfill marker was already present. */\n ran: boolean;\n engine: string;\n scale: number;\n /** The preflight that gated the run. */\n preflight: MinorUnitsPreflightResult;\n /** Columns whose values were rescaled. */\n rescaledColumns: Array<{ table: string; column: string }>;\n /** Statements executed, in order — useful for logs and tests. */\n statements: string[];\n /**\n * Columns whose values are now minor units but whose *declared* type is\n * still floating-point. Only ever populated on SQLite, which cannot alter a\n * column type in place; clearing it needs the table-rebuild path (#2370).\n */\n declaredTypeChangePending: Array<{ table: string; column: string }>;\n}\n\n/**\n * A money-losing preflight finding blocked the conversion.\n *\n * Carries the full preflight so a caller can print the summary, fix the data,\n * and retry rather than re-deriving what went wrong.\n */\nexport class MinorUnitsPreflightError extends Error {\n constructor(readonly preflight: MinorUnitsPreflightResult) {\n super(\n `Money minor-units rescale refused: ${preflight.nonIntegralRows} row(s) ` +\n `would be rounded and ${preflight.overflowRows} row(s) would overflow ` +\n 'int4. Fix the data or pass `force: true` to accept the rounding.\\n' +\n preflight.summary,\n );\n this.name = 'MinorUnitsPreflightError';\n }\n}\n\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n return db.url || dbWithConfig.config?.url || '';\n}\n\nfunction resolveEngine(db: DatabaseInterface, engineHint?: string): string {\n return typeof (db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : detectEngine(resolveDatabaseUrl(db), engineHint);\n}\n\n/**\n * Quote an identifier for DDL.\n *\n * Table and column names here come from a package's own migration module, not\n * from user input, but a rejected embedded quote is cheaper than trusting that\n * forever.\n */\nfunction quoteIdentifier(name: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n throw new Error(\n `Money minor-units rescale: '${name}' is not a plain SQL identifier.`,\n );\n }\n return `\"${name}\"`;\n}\n\n/** Is this declared type a floating-point one that still needs converting? */\nfunction isFloatingType(declaredType: string): boolean {\n return /^(REAL|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER)/i.test(\n declaredType.trim(),\n );\n}\n\n/** Is this declared type already an integer one? */\nfunction isIntegerType(declaredType: string): boolean {\n return /^(INTEGER|INT|INT4|INT8|BIGINT|SMALLINT|TINYINT|HUGEINT)\\b/i.test(\n declaredType.trim(),\n );\n}\n\n/**\n * Read declared column types for one table.\n *\n * PostgreSQL and DuckDB both expose `information_schema.columns`; SQLite does\n * not, so it uses `PRAGMA table_info`. Returns an empty map when the table does\n * not exist, which the caller reports as `missing` rather than an error — a\n * package's migration lists its own tables, and a deployment may legitimately\n * not have created all of them yet.\n */\nasync function readColumnTypes(\n db: DatabaseInterface,\n engine: string,\n table: string,\n): Promise<Map<string, string>> {\n const types = new Map<string, string>();\n try {\n if (engine === 'postgres' || engine === 'duckdb') {\n const result = await db.query(\n 'SELECT column_name, data_type FROM information_schema.columns WHERE table_name = ?',\n table,\n );\n for (const row of result.rows as {\n column_name?: string;\n data_type?: string;\n }[]) {\n if (row.column_name) types.set(row.column_name, row.data_type ?? '');\n }\n return types;\n }\n // SQLite. `PRAGMA table_info` does not accept a bound parameter, and the\n // identifier has already been validated by `quoteIdentifier`.\n const result = await db.query(\n `PRAGMA table_info(${quoteIdentifier(table)})`,\n );\n for (const row of result.rows as { name?: string; type?: string }[]) {\n if (row.name) types.set(row.name, row.type ?? '');\n }\n } catch (error) {\n logger.debug(\n `[minor-units] Could not introspect ${table}; treating its columns as missing`,\n { error: error instanceof Error ? error.message : String(error) },\n );\n }\n return types;\n}\n\n/** Does this table expose an `id` column we can name in problem rows? */\nfunction hasIdColumn(types: Map<string, string>): boolean {\n return types.has('id');\n}\n\nfunction toFiniteNumber(value: unknown): number | null {\n if (value == null) return null;\n const parsed = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\n/**\n * Inspect every non-null value in one floating-point money column.\n *\n * Rows are read rather than aggregated in SQL because the integrality test has\n * to survive three dialects' differing `round`/modulo semantics, and a money\n * table small enough to be worth converting by hand is small enough to scan.\n */\nasync function inspectColumn(\n db: DatabaseInterface,\n table: string,\n column: string,\n scale: number,\n maxProblemRows: number,\n withId: boolean,\n): Promise<{\n inspectedRows: number;\n nonIntegral: MinorUnitsProblemRow[];\n overflow: MinorUnitsProblemRow[];\n}> {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n const selection = withId\n ? `\"id\", ${quotedColumn} AS value`\n : `${quotedColumn} AS value`;\n const result = await db.query(\n `SELECT ${selection} FROM ${quotedTable} WHERE ${quotedColumn} IS NOT NULL`,\n );\n\n const nonIntegral: MinorUnitsProblemRow[] = [];\n const overflow: MinorUnitsProblemRow[] = [];\n let inspectedRows = 0;\n\n for (const row of result.rows as { id?: unknown; value?: unknown }[]) {\n const value = toFiniteNumber(row.value);\n if (value === null) continue;\n inspectedRows += 1;\n const scaled = value * scale;\n const rounded = roundHalfAwayFromZero(scaled);\n const id = row.id == null ? null : String(row.id);\n if (Math.abs(scaled - rounded) > INTEGRALITY_TOLERANCE) {\n if (nonIntegral.length < maxProblemRows) {\n nonIntegral.push({ id, value, scaled });\n }\n }\n if (rounded > INT4_MAX || rounded < INT4_MIN) {\n if (overflow.length < maxProblemRows) {\n overflow.push({ id, value, scaled });\n }\n }\n }\n\n return { inspectedRows, nonIntegral, overflow };\n}\n\nfunction renderSummary(result: Omit<MinorUnitsPreflightResult, 'summary'>) {\n const lines: string[] = [\n `Money minor-units preflight (engine=${result.engine}, scale=${result.scale})`,\n ` columns: ${result.pendingColumns} pending, ${result.convertedColumns} already integer, ${result.missingColumns} missing`,\n ` rows that would be rounded: ${result.nonIntegralRows}`,\n ` rows that would overflow int4: ${result.overflowRows}`,\n ];\n for (const column of result.columns) {\n const label = `${column.table}.${column.column}`;\n if (column.state === 'missing') {\n lines.push(` - ${label}: missing`);\n continue;\n }\n if (column.state === 'converted') {\n lines.push(\n ` - ${label}: already integer (${column.declaredType ?? 'unknown'})`,\n );\n continue;\n }\n lines.push(\n ` - ${label}: pending (${column.declaredType ?? 'unknown'}), ` +\n `${column.inspectedRows} row(s) inspected, ` +\n `${column.nonIntegral.length} non-integral, ${column.overflow.length} overflow`,\n );\n for (const row of column.nonIntegral) {\n lines.push(\n ` non-integral id=${row.id ?? '<no id>'} value=${row.value} scaled=${row.scaled}`,\n );\n }\n for (const row of column.overflow) {\n lines.push(\n ` overflow id=${row.id ?? '<no id>'} value=${row.value} scaled=${row.scaled}`,\n );\n }\n }\n return lines.join('\\n');\n}\n\n/**\n * The statements that convert one column, for one engine.\n *\n * Exported so the emitted DDL can be asserted without a live PostgreSQL or\n * DuckDB connection; each owning package's `test:postgres` lane then proves the\n * same SQL runs against a real server.\n *\n * @param engine - `postgres`, `duckdb`, or anything SQLite-shaped.\n * @param table - Physical table name.\n * @param column - Physical column name.\n * @param scale - Minor units per major unit.\n * @returns Statements in execution order.\n */\nexport function buildMinorUnitsStatements(\n engine: string,\n table: string,\n column: string,\n scale: number,\n): string[] {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n if (engine === 'postgres' || engine === 'duckdb') {\n // PostgreSQL's `round(double precision)` is banker's rounding — it breaks\n // ties to even, so `round(-0.5)` is `-0` and `round(2.5)` is `2`. Casting\n // to `numeric` first selects the half-away-from-zero overload, which is\n // what SQLite and DuckDB do and what the preflight predicts. Money can be\n // negative here (a billing credit), so the tie-break is not academic.\n const scaled =\n engine === 'postgres'\n ? `round((${quotedColumn})::numeric * ${scale})`\n : `round(${quotedColumn} * ${scale})`;\n // A `0.0` DEFAULT is not assignable to an integer column on PostgreSQL, so\n // it is dropped before the type change and restored after it. `0` is the\n // framework default for every money field.\n return [\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} DROP DEFAULT`,\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} TYPE INTEGER ` +\n `USING ${scaled}`,\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} SET DEFAULT 0`,\n ];\n }\n // SQLite: the declared type is only an affinity and cannot be altered in\n // place, so rescale the values and leave the declaration to the table-rebuild\n // path (#2370).\n return [\n `UPDATE ${quotedTable} SET ${quotedColumn} = CAST(round(${quotedColumn} * ${scale}) AS INTEGER) ` +\n `WHERE ${quotedColumn} IS NOT NULL`,\n ];\n}\n\n/**\n * Report what {@link rescaleMoneyColumnsToMinorUnits} would do, without\n * writing anything.\n *\n * @param db - Root database handle.\n * @param targets - Tables and money columns owned by the calling package.\n * @param options - Scale, engine hint, problem-row cap.\n * @returns Per-column state plus the rows that would lose information.\n */\nexport async function preflightMinorUnitsRescale(\n db: DatabaseInterface,\n targets: MoneyColumnTarget[],\n options: MinorUnitsOptions = {},\n): Promise<MinorUnitsPreflightResult> {\n const scale = options.scale ?? DEFAULT_SCALE;\n if (!Number.isInteger(scale) || scale <= 0) {\n throw new Error(\n `Money minor-units rescale: scale must be a positive integer (got ${scale}).`,\n );\n }\n const engine = resolveEngine(db, options.engineHint);\n const maxProblemRows = options.maxProblemRows ?? 20;\n\n // Validate every identifier up front. Introspection swallows errors so an\n // absent table reads as `missing`, and a malformed name must not disappear\n // down that same path — it is a bug in the caller's migration module.\n for (const target of targets) {\n quoteIdentifier(target.table);\n for (const column of target.columns) quoteIdentifier(column);\n }\n\n const columns: MinorUnitsColumnReport[] = [];\n for (const target of targets) {\n const types = await readColumnTypes(db, engine, target.table);\n const withId = hasIdColumn(types);\n for (const column of target.columns) {\n const declaredType = types.get(column) ?? null;\n if (declaredType === null) {\n columns.push({\n table: target.table,\n column,\n state: 'missing',\n declaredType: null,\n inspectedRows: 0,\n nonIntegral: [],\n overflow: [],\n });\n continue;\n }\n if (isIntegerType(declaredType) || !isFloatingType(declaredType)) {\n // Anything that is not floating-point is either already converted or a\n // type this migration has no business rewriting.\n columns.push({\n table: target.table,\n column,\n state: 'converted',\n declaredType,\n inspectedRows: 0,\n nonIntegral: [],\n overflow: [],\n });\n continue;\n }\n const inspected = await inspectColumn(\n db,\n target.table,\n column,\n scale,\n maxProblemRows,\n withId,\n );\n columns.push({\n table: target.table,\n column,\n state: 'pending',\n declaredType,\n ...inspected,\n });\n }\n }\n\n const base = {\n engine,\n scale,\n columns,\n pendingColumns: columns.filter((c) => c.state === 'pending').length,\n convertedColumns: columns.filter((c) => c.state === 'converted').length,\n missingColumns: columns.filter((c) => c.state === 'missing').length,\n nonIntegralRows: columns.reduce((sum, c) => sum + c.nonIntegral.length, 0),\n overflowRows: columns.reduce((sum, c) => sum + c.overflow.length, 0),\n };\n const ok = base.nonIntegralRows === 0 && base.overflowRows === 0;\n return { ...base, ok, summary: renderSummary({ ...base, ok }) };\n}\n\n/**\n * Convert floating-point major-unit money columns to integer minor units.\n *\n * Idempotent: the run is wrapped in a {@link BackfillTracker} marker, so a\n * second call is a no-op even on SQLite where the column type cannot record\n * that the conversion already happened.\n *\n * @param db - Root database handle (must support `query`).\n * @param targets - Tables and money columns owned by the calling package.\n * @param options - Marker name, scale, and whether to convert despite\n * information-losing rows.\n * @throws {MinorUnitsPreflightError} when the preflight found rows that would\n * be rounded or overflow and `force` was not set.\n */\nexport async function rescaleMoneyColumnsToMinorUnits(\n db: DatabaseInterface,\n targets: MoneyColumnTarget[],\n options: MinorUnitsRescaleOptions,\n): Promise<MinorUnitsRescaleResult> {\n const scale = options.scale ?? DEFAULT_SCALE;\n const engine = resolveEngine(db, options.engineHint);\n const tracker = new BackfillTracker({ db });\n\n if (await tracker.isApplied(options.backfillName)) {\n return {\n ran: false,\n engine,\n scale,\n preflight: await preflightMinorUnitsRescale(db, targets, options),\n rescaledColumns: [],\n statements: [],\n declaredTypeChangePending: [],\n };\n }\n\n const preflight = await preflightMinorUnitsRescale(db, targets, options);\n if (!preflight.ok && !options.force) {\n throw new MinorUnitsPreflightError(preflight);\n }\n\n const statements: string[] = [];\n const rescaledColumns: Array<{ table: string; column: string }> = [];\n const declaredTypeChangePending: Array<{ table: string; column: string }> =\n [];\n\n const altersColumnType = engine === 'postgres' || engine === 'duckdb';\n for (const report of preflight.columns) {\n if (report.state !== 'pending') continue;\n for (const sql of buildMinorUnitsStatements(\n engine,\n report.table,\n report.column,\n scale,\n )) {\n await db.query(sql);\n statements.push(sql);\n }\n if (!altersColumnType) {\n declaredTypeChangePending.push({\n table: report.table,\n column: report.column,\n });\n }\n rescaledColumns.push({ table: report.table, column: report.column });\n }\n\n await tracker.recordApplied(options.backfillName, {\n description:\n `Money columns rescaled from major units to integer minor units ` +\n `(scale ${scale}).`,\n packageName: options.packageName,\n });\n\n return {\n ran: true,\n engine,\n scale,\n preflight,\n rescaledColumns,\n statements,\n declaredTypeChangePending,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"minor-units.js","names":[],"sources":["../../src/migrations/minor-units.ts"],"sourcesContent":["/**\n * Money-column rescale: floating-point major units → integer minor units (#2401).\n *\n * SMRT's rule is that money is exact and is stored as **integer minor units**\n * at an application-defined scale — `$19.99` is `1999` at a cents scale.\n * Packages that predate the rule\n * declared their money fields `= 0.0`, which compiles to a floating-point\n * column holding *major* units. Flipping the declaration to `= 0` changes the\n * column type, and REAL→INTEGER is deliberately **not** one of the differ's\n * whitelisted upgrades: an automatic `ALTER … TYPE BIGINT` would truncate\n * `19.99` to `19`, silently destroying minor units on every row.\n *\n * So the conversion is an explicit, opt-in migration with two halves:\n *\n * 1. {@link preflightMinorUnitsRescale} — read-only. Reports, per column,\n * whether it still needs converting, and lists the rows that would lose\n * information: values whose scaled form is not a whole number (a half-cent\n * that has to be rounded away) and values that exceed JavaScript's safe\n * integer range once multiplied by the scale. Run it against a production\n * snapshot and\n * read the summary before converting anything.\n * 2. {@link rescaleMoneyColumnsToMinorUnits} — the conversion itself, guarded\n * by a {@link BackfillTracker} marker so re-running it can never multiply a\n * table by 100 twice.\n *\n * Per-engine behaviour:\n *\n * | Engine | Column type | Values |\n * | --- | --- | --- |\n * | PostgreSQL | `ALTER COLUMN … TYPE BIGINT USING round(col * scale)` | converted by the cast |\n * | DuckDB | `ALTER COLUMN … TYPE BIGINT USING round(col * scale)` | converted by the cast |\n * | SQLite | **not changed here** — SQLite cannot alter a column's declared type in place | rescaled with an `UPDATE`, values become exact integers |\n *\n * SQLite's declared type only sets an affinity, so a rescaled column already\n * behaves as integer minor units; bringing the *declaration* into line needs\n * the table-rebuild path (#2370). The result flags that with\n * {@link MinorUnitsRescaleResult.declaredTypeChangePending} rather than\n * pretending the column type moved.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { BackfillTracker } from './backfill-tracker.js';\nimport type { DatabaseInterface } from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Hydrated integers must remain exactly representable in JavaScript. */\nconst MIN_SAFE_INTEGER = -Number.MAX_SAFE_INTEGER;\nconst MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;\n\n/**\n * How far a scaled value may sit from a whole number and still count as\n * integral.\n *\n * `19.99 * 100` is `1998.9999999999998` in IEEE-754, so a zero tolerance would\n * report every ordinary two-decimal price as non-integral. A genuine half-cent\n * is off by `0.5`, five orders of magnitude above this bound, so the two cases\n * never blur.\n */\nconst INTEGRALITY_TOLERANCE = 1e-6;\n\n/** Default minor units per major unit — cents. */\nconst DEFAULT_SCALE = 100;\n\n/**\n * Round half away from zero, the way SQL `round()` does — and the way\n * `Math.round` does not.\n *\n * `Math.round` rounds halves toward +∞, so `Math.round(-0.5)` is `-0` while\n * SQLite's and DuckDB's `round(-0.5)` is `-1`. Money here can legitimately be\n * negative (`BillingAdjustment.amount` is a signed credit), so a preflight that\n * predicted the conversion with `Math.round` would report a value one minor\n * unit above what the database actually stores. The PostgreSQL DDL casts to\n * `numeric` for the same reason — see {@link buildMinorUnitsStatements}.\n */\nfunction roundHalfAwayFromZero(value: number): number {\n return Math.sign(value) * Math.round(Math.abs(value));\n}\n\n/** A table and the money columns on it that must move to minor units. */\nexport interface MoneyColumnTarget {\n /** Physical table name, e.g. `payments`. */\n table: string;\n /** Physical column names, e.g. `['amount', 'native_amount']`. */\n columns: string[];\n}\n\n/** What the preflight found about one column. */\nexport type MinorUnitsColumnState =\n /** Floating-point today: needs converting. */\n | 'pending'\n /** Already an integer column: nothing to do. */\n | 'converted'\n /** Table or column absent (package not installed, older schema). */\n | 'missing';\n\n/** One row that cannot be converted without losing information. */\nexport interface MinorUnitsProblemRow {\n /** Primary key, when the table has an `id` column. */\n id: string | null;\n /** The stored major-unit value. */\n value: number;\n /** `value * scale`, before rounding. */\n scaled: number;\n}\n\n/** Preflight findings for a single column. */\nexport interface MinorUnitsColumnReport {\n table: string;\n column: string;\n state: MinorUnitsColumnState;\n /** Declared column type as the database reports it, when resolvable. */\n declaredType: string | null;\n /** Non-null values inspected. `0` unless `state` is `pending`. */\n inspectedRows: number;\n /** Values whose scaled form is not a whole number — rounding loses money. */\n nonIntegral: MinorUnitsProblemRow[];\n /** Values whose scaled form exceeds JavaScript's safe integer range. */\n overflow: MinorUnitsProblemRow[];\n}\n\n/** Result of {@link preflightMinorUnitsRescale}. */\nexport interface MinorUnitsPreflightResult {\n engine: string;\n scale: number;\n columns: MinorUnitsColumnReport[];\n /** Columns still holding floating-point major units. */\n pendingColumns: number;\n /** Columns already integer. */\n convertedColumns: number;\n /** Columns whose table/column does not exist. */\n missingColumns: number;\n /** Total rows that would be rounded. */\n nonIntegralRows: number;\n /** Total rows that would exceed JavaScript's safe integer range. */\n overflowRows: number;\n /** No row would lose information. */\n ok: boolean;\n /** Human-readable report, suitable for pasting into a migration log or PR. */\n summary: string;\n}\n\n/** Options shared by the preflight and the rescale. */\nexport interface MinorUnitsOptions {\n /** Minor units per major unit. Default `100` (cents). */\n scale?: number;\n /** Engine hint for adapters whose URL does not identify the engine. */\n engineHint?: string;\n /** Problem rows recorded per column before truncating. Default `20`. */\n maxProblemRows?: number;\n}\n\n/** Options for {@link rescaleMoneyColumnsToMinorUnits}. */\nexport interface MinorUnitsRescaleOptions extends MinorUnitsOptions {\n /**\n * Stable marker name recorded in `_smrt_backfills`, e.g.\n * `@happyvertical/smrt-commerce:money-minor-units:v1`.\n *\n * This is what makes the migration idempotent on SQLite, where the rescale\n * is an `UPDATE` no column type can distinguish from an unconverted table.\n */\n backfillName: string;\n /** Package recorded alongside the marker. */\n packageName?: string;\n /**\n * Convert even when the preflight found rows that would be rounded or\n * overflow. Off by default: silently rounding a half-cent away is exactly\n * the failure this migration exists to prevent.\n */\n force?: boolean;\n}\n\n/** Result of {@link rescaleMoneyColumnsToMinorUnits}. */\nexport interface MinorUnitsRescaleResult {\n /** `false` when the backfill marker was already present. */\n ran: boolean;\n engine: string;\n scale: number;\n /** The preflight that gated the run. */\n preflight: MinorUnitsPreflightResult;\n /** Columns whose values were rescaled. */\n rescaledColumns: Array<{ table: string; column: string }>;\n /** Statements executed, in order — useful for logs and tests. */\n statements: string[];\n /**\n * Columns whose values are now minor units but whose *declared* type is\n * still floating-point. Only ever populated on SQLite, which cannot alter a\n * column type in place; clearing it needs the table-rebuild path (#2370).\n */\n declaredTypeChangePending: Array<{ table: string; column: string }>;\n}\n\n/**\n * A money-losing preflight finding blocked the conversion.\n *\n * Carries the full preflight so a caller can print the summary, fix the data,\n * and retry rather than re-deriving what went wrong.\n */\nexport class MinorUnitsPreflightError extends Error {\n constructor(readonly preflight: MinorUnitsPreflightResult) {\n super(\n `Money minor-units rescale refused: ${preflight.nonIntegralRows} row(s) ` +\n `would be rounded and ${preflight.overflowRows} row(s) would overflow ` +\n \"JavaScript's safe integer range. Fix the data or pass `force: true` to accept the rounding.\\n\" +\n preflight.summary,\n );\n this.name = 'MinorUnitsPreflightError';\n }\n}\n\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n return db.url || dbWithConfig.config?.url || '';\n}\n\nfunction resolveEngine(db: DatabaseInterface, engineHint?: string): string {\n return typeof (db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : detectEngine(resolveDatabaseUrl(db), engineHint);\n}\n\n/**\n * Quote an identifier for DDL.\n *\n * Table and column names here come from a package's own migration module, not\n * from user input, but a rejected embedded quote is cheaper than trusting that\n * forever.\n */\nfunction quoteIdentifier(name: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n throw new Error(\n `Money minor-units rescale: '${name}' is not a plain SQL identifier.`,\n );\n }\n return `\"${name}\"`;\n}\n\n/** Is this declared type a floating-point one that still needs converting? */\nfunction isFloatingType(declaredType: string): boolean {\n return /^(REAL|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER)/i.test(\n declaredType.trim(),\n );\n}\n\n/** Is this declared type already an integer one? */\nfunction isIntegerType(declaredType: string): boolean {\n return /^(INTEGER|INT|INT4|INT8|BIGINT|SMALLINT|TINYINT|HUGEINT)\\b/i.test(\n declaredType.trim(),\n );\n}\n\n/**\n * Read declared column types for one table.\n *\n * PostgreSQL and DuckDB both expose `information_schema.columns`; SQLite does\n * not, so it uses `PRAGMA table_info`. Returns an empty map when the table does\n * not exist, which the caller reports as `missing` rather than an error — a\n * package's migration lists its own tables, and a deployment may legitimately\n * not have created all of them yet.\n */\nasync function readColumnTypes(\n db: DatabaseInterface,\n engine: string,\n table: string,\n): Promise<Map<string, string>> {\n const types = new Map<string, string>();\n try {\n if (engine === 'postgres' || engine === 'duckdb') {\n const result = await db.query(\n 'SELECT column_name, data_type FROM information_schema.columns WHERE table_name = ?',\n table,\n );\n for (const row of result.rows as {\n column_name?: string;\n data_type?: string;\n }[]) {\n if (row.column_name) types.set(row.column_name, row.data_type ?? '');\n }\n return types;\n }\n // SQLite. `PRAGMA table_info` does not accept a bound parameter, and the\n // identifier has already been validated by `quoteIdentifier`.\n const result = await db.query(\n `PRAGMA table_info(${quoteIdentifier(table)})`,\n );\n for (const row of result.rows as { name?: string; type?: string }[]) {\n if (row.name) types.set(row.name, row.type ?? '');\n }\n } catch (error) {\n logger.debug(\n `[minor-units] Could not introspect ${table}; treating its columns as missing`,\n { error: error instanceof Error ? error.message : String(error) },\n );\n }\n return types;\n}\n\n/** Does this table expose an `id` column we can name in problem rows? */\nfunction hasIdColumn(types: Map<string, string>): boolean {\n return types.has('id');\n}\n\nfunction toFiniteNumber(value: unknown): number | null {\n if (value == null) return null;\n const parsed = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\n/**\n * Inspect every non-null value in one floating-point money column.\n *\n * Rows are read rather than aggregated in SQL because the integrality test has\n * to survive three dialects' differing `round`/modulo semantics, and a money\n * table small enough to be worth converting by hand is small enough to scan.\n */\nasync function inspectColumn(\n db: DatabaseInterface,\n table: string,\n column: string,\n scale: number,\n maxProblemRows: number,\n withId: boolean,\n): Promise<{\n inspectedRows: number;\n nonIntegral: MinorUnitsProblemRow[];\n overflow: MinorUnitsProblemRow[];\n}> {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n const selection = withId\n ? `\"id\", ${quotedColumn} AS value`\n : `${quotedColumn} AS value`;\n const result = await db.query(\n `SELECT ${selection} FROM ${quotedTable} WHERE ${quotedColumn} IS NOT NULL`,\n );\n\n const nonIntegral: MinorUnitsProblemRow[] = [];\n const overflow: MinorUnitsProblemRow[] = [];\n let inspectedRows = 0;\n\n for (const row of result.rows as { id?: unknown; value?: unknown }[]) {\n const value = toFiniteNumber(row.value);\n if (value === null) continue;\n inspectedRows += 1;\n const scaled = value * scale;\n const rounded = roundHalfAwayFromZero(scaled);\n const id = row.id == null ? null : String(row.id);\n if (Math.abs(scaled - rounded) > INTEGRALITY_TOLERANCE) {\n if (nonIntegral.length < maxProblemRows) {\n nonIntegral.push({ id, value, scaled });\n }\n }\n if (rounded > MAX_SAFE_INTEGER || rounded < MIN_SAFE_INTEGER) {\n if (overflow.length < maxProblemRows) {\n overflow.push({ id, value, scaled });\n }\n }\n }\n\n return { inspectedRows, nonIntegral, overflow };\n}\n\nfunction renderSummary(result: Omit<MinorUnitsPreflightResult, 'summary'>) {\n const lines: string[] = [\n `Money minor-units preflight (engine=${result.engine}, scale=${result.scale})`,\n ` columns: ${result.pendingColumns} pending, ${result.convertedColumns} already integer, ${result.missingColumns} missing`,\n ` rows that would be rounded: ${result.nonIntegralRows}`,\n ` rows that would exceed JavaScript's safe integer range: ${result.overflowRows}`,\n ];\n for (const column of result.columns) {\n const label = `${column.table}.${column.column}`;\n if (column.state === 'missing') {\n lines.push(` - ${label}: missing`);\n continue;\n }\n if (column.state === 'converted') {\n lines.push(\n ` - ${label}: already integer (${column.declaredType ?? 'unknown'})`,\n );\n continue;\n }\n lines.push(\n ` - ${label}: pending (${column.declaredType ?? 'unknown'}), ` +\n `${column.inspectedRows} row(s) inspected, ` +\n `${column.nonIntegral.length} non-integral, ${column.overflow.length} overflow`,\n );\n for (const row of column.nonIntegral) {\n lines.push(\n ` non-integral id=${row.id ?? '<no id>'} value=${row.value} scaled=${row.scaled}`,\n );\n }\n for (const row of column.overflow) {\n lines.push(\n ` overflow id=${row.id ?? '<no id>'} value=${row.value} scaled=${row.scaled}`,\n );\n }\n }\n return lines.join('\\n');\n}\n\n/**\n * The statements that convert one column, for one engine.\n *\n * Exported so the emitted DDL can be asserted without a live PostgreSQL or\n * DuckDB connection; each owning package's `test:postgres` lane then proves the\n * same SQL runs against a real server.\n *\n * @param engine - `postgres`, `duckdb`, or anything SQLite-shaped.\n * @param table - Physical table name.\n * @param column - Physical column name.\n * @param scale - Minor units per major unit.\n * @returns Statements in execution order.\n */\nexport function buildMinorUnitsStatements(\n engine: string,\n table: string,\n column: string,\n scale: number,\n): string[] {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n if (engine === 'postgres' || engine === 'duckdb') {\n // PostgreSQL's `round(double precision)` is banker's rounding — it breaks\n // ties to even, so `round(-0.5)` is `-0` and `round(2.5)` is `2`. Casting\n // to `numeric` first selects the half-away-from-zero overload, which is\n // what SQLite and DuckDB do and what the preflight predicts. Money can be\n // negative here (a billing credit), so the tie-break is not academic.\n const scaled =\n engine === 'postgres'\n ? `round((${quotedColumn})::numeric * ${scale})`\n : `round(${quotedColumn} * ${scale})`;\n // A `0.0` DEFAULT is not assignable to an integer column on PostgreSQL, so\n // it is dropped before the type change and restored after it. `0` is the\n // framework default for every money field.\n return [\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} DROP DEFAULT`,\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} TYPE BIGINT ` +\n `USING ${scaled}`,\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} SET DEFAULT 0`,\n ];\n }\n // SQLite: the declared type is only an affinity and cannot be altered in\n // place, so rescale the values and leave the declaration to the table-rebuild\n // path (#2370).\n return [\n `UPDATE ${quotedTable} SET ${quotedColumn} = CAST(round(${quotedColumn} * ${scale}) AS INTEGER) ` +\n `WHERE ${quotedColumn} IS NOT NULL`,\n ];\n}\n\n/**\n * Report what {@link rescaleMoneyColumnsToMinorUnits} would do, without\n * writing anything.\n *\n * @param db - Root database handle.\n * @param targets - Tables and money columns owned by the calling package.\n * @param options - Scale, engine hint, problem-row cap.\n * @returns Per-column state plus the rows that would lose information.\n */\nexport async function preflightMinorUnitsRescale(\n db: DatabaseInterface,\n targets: MoneyColumnTarget[],\n options: MinorUnitsOptions = {},\n): Promise<MinorUnitsPreflightResult> {\n const scale = options.scale ?? DEFAULT_SCALE;\n if (!Number.isInteger(scale) || scale <= 0) {\n throw new Error(\n `Money minor-units rescale: scale must be a positive integer (got ${scale}).`,\n );\n }\n const engine = resolveEngine(db, options.engineHint);\n const maxProblemRows = options.maxProblemRows ?? 20;\n\n // Validate every identifier up front. Introspection swallows errors so an\n // absent table reads as `missing`, and a malformed name must not disappear\n // down that same path — it is a bug in the caller's migration module.\n for (const target of targets) {\n quoteIdentifier(target.table);\n for (const column of target.columns) quoteIdentifier(column);\n }\n\n const columns: MinorUnitsColumnReport[] = [];\n for (const target of targets) {\n const types = await readColumnTypes(db, engine, target.table);\n const withId = hasIdColumn(types);\n for (const column of target.columns) {\n const declaredType = types.get(column) ?? null;\n if (declaredType === null) {\n columns.push({\n table: target.table,\n column,\n state: 'missing',\n declaredType: null,\n inspectedRows: 0,\n nonIntegral: [],\n overflow: [],\n });\n continue;\n }\n if (isIntegerType(declaredType) || !isFloatingType(declaredType)) {\n // Anything that is not floating-point is either already converted or a\n // type this migration has no business rewriting.\n columns.push({\n table: target.table,\n column,\n state: 'converted',\n declaredType,\n inspectedRows: 0,\n nonIntegral: [],\n overflow: [],\n });\n continue;\n }\n const inspected = await inspectColumn(\n db,\n target.table,\n column,\n scale,\n maxProblemRows,\n withId,\n );\n columns.push({\n table: target.table,\n column,\n state: 'pending',\n declaredType,\n ...inspected,\n });\n }\n }\n\n const base = {\n engine,\n scale,\n columns,\n pendingColumns: columns.filter((c) => c.state === 'pending').length,\n convertedColumns: columns.filter((c) => c.state === 'converted').length,\n missingColumns: columns.filter((c) => c.state === 'missing').length,\n nonIntegralRows: columns.reduce((sum, c) => sum + c.nonIntegral.length, 0),\n overflowRows: columns.reduce((sum, c) => sum + c.overflow.length, 0),\n };\n const ok = base.nonIntegralRows === 0 && base.overflowRows === 0;\n return { ...base, ok, summary: renderSummary({ ...base, ok }) };\n}\n\n/**\n * Convert floating-point major-unit money columns to integer minor units.\n *\n * Idempotent: the run is wrapped in a {@link BackfillTracker} marker, so a\n * second call is a no-op even on SQLite where the column type cannot record\n * that the conversion already happened.\n *\n * @param db - Root database handle (must support `query`).\n * @param targets - Tables and money columns owned by the calling package.\n * @param options - Marker name, scale, and whether to convert despite\n * information-losing rows.\n * @throws {MinorUnitsPreflightError} when the preflight found rows that would\n * be rounded or overflow and `force` was not set.\n */\nexport async function rescaleMoneyColumnsToMinorUnits(\n db: DatabaseInterface,\n targets: MoneyColumnTarget[],\n options: MinorUnitsRescaleOptions,\n): Promise<MinorUnitsRescaleResult> {\n const scale = options.scale ?? DEFAULT_SCALE;\n const engine = resolveEngine(db, options.engineHint);\n const tracker = new BackfillTracker({ db });\n\n if (await tracker.isApplied(options.backfillName)) {\n return {\n ran: false,\n engine,\n scale,\n preflight: await preflightMinorUnitsRescale(db, targets, options),\n rescaledColumns: [],\n statements: [],\n declaredTypeChangePending: [],\n };\n }\n\n const preflight = await preflightMinorUnitsRescale(db, targets, options);\n if (!preflight.ok && !options.force) {\n throw new MinorUnitsPreflightError(preflight);\n }\n\n const statements: string[] = [];\n const rescaledColumns: Array<{ table: string; column: string }> = [];\n const declaredTypeChangePending: Array<{ table: string; column: string }> =\n [];\n\n const altersColumnType = engine === 'postgres' || engine === 'duckdb';\n for (const report of preflight.columns) {\n if (report.state !== 'pending') continue;\n for (const sql of buildMinorUnitsStatements(\n engine,\n report.table,\n report.column,\n scale,\n )) {\n await db.query(sql);\n statements.push(sql);\n }\n if (!altersColumnType) {\n declaredTypeChangePending.push({\n table: report.table,\n column: report.column,\n });\n }\n rescaledColumns.push({ table: report.table, column: report.column });\n }\n\n await tracker.recordApplied(options.backfillName, {\n description:\n `Money columns rescaled from major units to integer minor units ` +\n `(scale ${scale}).`,\n packageName: options.packageName,\n });\n\n return {\n ran: true,\n engine,\n scale,\n preflight,\n rescaledColumns,\n statements,\n declaredTypeChangePending,\n };\n}\n"],"mappings":"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"}
@@ -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,3 +1,4 @@
1
+ import { toSafeBooleanInteger, toSafeInteger } from "../utils/safe-integer.js";
1
2
  import { detectEngine } from "../schema/ddl/index.js";
2
3
  import { CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, getSystemTableDDLForEngine } from "../system/schema.js";
3
4
  import { parsePostgresTimeoutMs } from "../postgres-timeouts.js";
@@ -14,6 +15,27 @@ import { hostname } from "node:os";
14
15
  * and providing idempotent migration application.
15
16
  */
16
17
  var logger = createLogger({ level: "info" });
18
+ /** Hydrate integer-backed system-table values at the database boundary. */
19
+ function hydrateMigrationRow(row) {
20
+ return {
21
+ id: row.id,
22
+ name: row.name,
23
+ version: row.version,
24
+ checksum: row.checksum,
25
+ applied_checksum: row.applied_checksum ?? null,
26
+ applied_at: new Date(row.applied_at),
27
+ execution_time_ms: row.execution_time_ms === null || row.execution_time_ms === void 0 ? null : toSafeInteger(row.execution_time_ms, "Migration execution time"),
28
+ attempts: toSafeInteger(row.attempts ?? 0, "Migration attempts"),
29
+ is_reversible: toSafeBooleanInteger(row.is_reversible ?? 1, "Migration reversibility flag"),
30
+ rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,
31
+ package_name: row.package_name ?? null,
32
+ source_file: row.source_file ?? null,
33
+ status: row.status,
34
+ error_message: row.error_message ?? null,
35
+ applied_by: row.applied_by ?? null,
36
+ batch: row.batch === null || row.batch === void 0 ? null : toSafeInteger(row.batch, "Migration batch")
37
+ };
38
+ }
17
39
  /**
18
40
  * Default options for MigrationTracker
19
41
  */
@@ -188,12 +210,7 @@ var MigrationTracker = class MigrationTracker {
188
210
  */
189
211
  async getAppliedMigrations() {
190
212
  await this.initialize();
191
- return (await this.db.query(`SELECT * FROM _smrt_schema_migrations WHERE status = 'completed' ORDER BY applied_at ASC`)).rows.map((row) => ({
192
- ...row,
193
- applied_at: new Date(row.applied_at),
194
- rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,
195
- is_reversible: Boolean(row.is_reversible)
196
- }));
213
+ return (await this.db.query(`SELECT * FROM _smrt_schema_migrations WHERE status = 'completed' ORDER BY applied_at ASC`)).rows.map(hydrateMigrationRow);
197
214
  }
198
215
  /**
199
216
  * Get pending migrations (defined but not applied)
@@ -218,19 +235,15 @@ var MigrationTracker = class MigrationTracker {
218
235
  const result = await this.db.query(`SELECT * FROM _smrt_schema_migrations WHERE name = ? LIMIT 1`, name);
219
236
  if (result.rows.length === 0) return null;
220
237
  const row = result.rows[0];
221
- return {
222
- ...row,
223
- applied_at: new Date(row.applied_at),
224
- rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,
225
- is_reversible: Boolean(row.is_reversible)
226
- };
238
+ return hydrateMigrationRow(row);
227
239
  }
228
240
  /**
229
241
  * Get the next batch number
230
242
  */
231
243
  async getNextBatch() {
232
244
  await this.initialize();
233
- return ((await this.db.query(`SELECT MAX(batch) as max_batch FROM _smrt_schema_migrations`)).rows[0]?.max_batch ?? 0) + 1;
245
+ const row = (await this.db.query(`SELECT MAX(batch) as max_batch FROM _smrt_schema_migrations`)).rows[0];
246
+ return toSafeInteger(toSafeInteger(row?.max_batch ?? 0, "Migration batch") + 1, "Next migration batch");
234
247
  }
235
248
  /**
236
249
  * Detect drift between migration definitions and applied migrations
@@ -331,7 +344,7 @@ var MigrationTracker = class MigrationTracker {
331
344
  };
332
345
  if (this.currentBatch === null) this.currentBatch = await this.getNextBatch();
333
346
  const id = existing?.id || randomUUID();
334
- const attempts = (existing?.attempts || 0) + 1;
347
+ const attempts = toSafeInteger((existing?.attempts ?? 0) + 1, "Migration attempts");
335
348
  if (existing) await this.db.query(`UPDATE _smrt_schema_migrations
336
349
  SET status = 'running', checksum = ?, attempts = ?, error_message = NULL, batch = ?, applied_by = ?, applied_at = CURRENT_TIMESTAMP
337
350
  WHERE id = ?`, checksum, attempts, this.currentBatch, hostname(), id);
@@ -710,12 +723,7 @@ var MigrationTracker = class MigrationTracker {
710
723
  sql += ` LIMIT ?`;
711
724
  params.push(options.limit);
712
725
  }
713
- return (await this.db.query(sql, ...params)).rows.map((row) => ({
714
- ...row,
715
- applied_at: new Date(row.applied_at),
716
- rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,
717
- is_reversible: Boolean(row.is_reversible)
718
- }));
726
+ return (await this.db.query(sql, ...params)).rows.map(hydrateMigrationRow);
719
727
  }
720
728
  /**
721
729
  * Execute SQL statements with appropriate transaction handling
@@ -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';\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 { 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?: number | null;\n package_name?: string | null;\n source_file?: string | null;\n status: string;\n error_message?: string | null;\n attempts: number;\n is_reversible: number | boolean;\n rolled_back_at?: string | null;\n applied_by?: string | null;\n batch: number;\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((row) => ({\n ...row,\n applied_at: new Date(row.applied_at),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n is_reversible: Boolean(row.is_reversible),\n })) as SchemaMigrationRecord[];\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 {\n ...row,\n applied_at: new Date(row.applied_at),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n is_reversible: Boolean(row.is_reversible),\n } as SchemaMigrationRecord;\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: number | null } | undefined;\n return (row?.max_batch ?? 0) + 1;\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 = (existing?.attempts || 0) + 1;\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((row) => ({\n ...row,\n applied_at: new Date(row.applied_at),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n is_reversible: Boolean(row.is_reversible),\n })) as SchemaMigrationRecord[];\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 (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';\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 (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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"}
@@ -2,10 +2,11 @@ import { parsePostgresTimeoutMs } from "./postgres-timeouts.js";
2
2
  import { SQLITE_REBUILD_TABLE_PREFIX, buildSqliteRebuildStatements, isSqliteRebuildPlaceholder, planSqliteTableRebuilds, rewriteSqliteCreateTable, sqliteRebuildPlaceholderSql } from "./migrations/sqlite-rebuild.js";
3
3
  import { SchemaComparer, canonicalizeDefault, generateSchemaDiff, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, uniqueColumnIndexName } from "./migrations/differ.js";
4
4
  import { BackfillTableUnavailableError, BackfillTracker } from "./migrations/backfill-tracker.js";
5
+ import { buildIntegerWidthStatements, buildIntegerWidthTableStatements, collectIntegerWidthTargets, preflightIntegerWidthWidening, widenIntegerColumnsToBigInt } from "./migrations/integer-width.js";
5
6
  import { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum } from "./migrations/checksum.js";
6
7
  import { MigrationGenerator, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp } from "./migrations/generator.js";
7
8
  import { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits } from "./migrations/minor-units.js";
8
9
  import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./migrations/tracker.js";
9
10
  import { getPendingSchemaStatements, migrateSmrtSchemas } from "./migrations/orchestrate.js";
10
11
  import "./migrations/index.js";
11
- export { BackfillTableUnavailableError, BackfillTracker, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, computeChecksum, createMigrationDefinition, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateSmrtSchemas, normalizeSQL, parsePostgresTimeoutMs, planPostgresStatements, planSqliteTableRebuilds, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum };
12
+ export { BackfillTableUnavailableError, BackfillTracker, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, computeChecksum, createMigrationDefinition, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateSmrtSchemas, normalizeSQL, parsePostgresTimeoutMs, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
@@ -1 +1 @@
1
- {"version":3,"file":"duckdb-strategy.d.ts","sourceRoot":"","sources":["../../../src/schema/ddl/duckdb-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAY;IAE3C;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM;IAalC;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE;IAwCnD;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,EAAE;IAIrD;;OAEG;IACH,gBAAgB,IAAI,OAAO;IAI3B;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO;CAGhC"}
1
+ {"version":3,"file":"duckdb-strategy.d.ts","sourceRoot":"","sources":["../../../src/schema/ddl/duckdb-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAY;IAE3C;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM;IAelC;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE;IAwCnD;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,EAAE;IAIrD;;OAEG;IACH,gBAAgB,IAAI,OAAO;IAI3B;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO;CAGhC"}
@@ -21,6 +21,7 @@ var DuckDBStrategy = class extends BaseDDLStrategy {
21
21
  */
22
22
  mapType(type) {
23
23
  switch (type) {
24
+ case "INTEGER": return "BIGINT";
24
25
  case "TIMESTAMP": return "TIMESTAMP";
25
26
  case "JSON": return "JSON";
26
27
  case "UUID": return "UUID";