@happyvertical/smrt-core 0.40.14 → 0.40.16

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 (92) hide show
  1. package/README.md +85 -11
  2. package/dist/browser.js +2 -2
  3. package/dist/change-feed.d.ts.map +1 -1
  4. package/dist/change-feed.js +21 -4
  5. package/dist/change-feed.js.map +1 -1
  6. package/dist/class.d.ts.map +1 -1
  7. package/dist/class.js +5 -3
  8. package/dist/class.js.map +1 -1
  9. package/dist/dispatch/bus.d.ts.map +1 -1
  10. package/dist/dispatch/bus.js +11 -6
  11. package/dist/dispatch/bus.js.map +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/manifest/static-manifest.js +2 -2
  14. package/dist/manifest/static-manifest.js.map +1 -1
  15. package/dist/manifest/store.js +1 -1
  16. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  17. package/dist/manifest/test-manifest-stub.js +132 -2
  18. package/dist/manifest/test-manifest-stub.js.map +1 -1
  19. package/dist/manifest.json +2 -2
  20. package/dist/migrations/backfill-tracker.d.ts +1 -0
  21. package/dist/migrations/backfill-tracker.d.ts.map +1 -1
  22. package/dist/migrations/backfill-tracker.js +9 -3
  23. package/dist/migrations/backfill-tracker.js.map +1 -1
  24. package/dist/migrations/differ.d.ts +18 -0
  25. package/dist/migrations/differ.d.ts.map +1 -1
  26. package/dist/migrations/differ.js +28 -5
  27. package/dist/migrations/differ.js.map +1 -1
  28. package/dist/migrations/generator.d.ts +8 -0
  29. package/dist/migrations/generator.d.ts.map +1 -1
  30. package/dist/migrations/generator.js +5 -1
  31. package/dist/migrations/generator.js.map +1 -1
  32. package/dist/migrations/orchestrate.d.ts +10 -0
  33. package/dist/migrations/orchestrate.d.ts.map +1 -1
  34. package/dist/migrations/orchestrate.js +8 -2
  35. package/dist/migrations/orchestrate.js.map +1 -1
  36. package/dist/migrations/tracker.d.ts.map +1 -1
  37. package/dist/migrations/tracker.js +4 -2
  38. package/dist/migrations/tracker.js.map +1 -1
  39. package/dist/registry/schema-builder.d.ts +13 -6
  40. package/dist/registry/schema-builder.d.ts.map +1 -1
  41. package/dist/registry/schema-builder.js +54 -15
  42. package/dist/registry/schema-builder.js.map +1 -1
  43. package/dist/registry.d.ts +3 -2
  44. package/dist/registry.d.ts.map +1 -1
  45. package/dist/registry.js +4 -4
  46. package/dist/registry.js.map +1 -1
  47. package/dist/schema/code-generator.js +123 -0
  48. package/dist/schema/code-generator.js.map +1 -0
  49. package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
  50. package/dist/schema/ddl/base-strategy.js +2 -2
  51. package/dist/schema/ddl/base-strategy.js.map +1 -1
  52. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  53. package/dist/schema/ddl/duckdb-strategy.js +2 -3
  54. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  55. package/dist/schema/ddl/index.js +26 -2
  56. package/dist/schema/ddl/index.js.map +1 -1
  57. package/dist/schema/ddl/materialize-manifest.d.ts +36 -0
  58. package/dist/schema/ddl/materialize-manifest.d.ts.map +1 -0
  59. package/dist/schema/ddl/materialize-manifest.js +361 -0
  60. package/dist/schema/ddl/materialize-manifest.js.map +1 -0
  61. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  62. package/dist/schema/ddl/postgres-strategy.js +1 -1
  63. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  64. package/dist/schema/index.js +12 -0
  65. package/dist/schema/override-system.js +205 -0
  66. package/dist/schema/override-system.js.map +1 -0
  67. package/dist/schema/schema-aggregator.d.ts.map +1 -1
  68. package/dist/schema/schema-aggregator.js +261 -0
  69. package/dist/schema/schema-aggregator.js.map +1 -0
  70. package/dist/schema/schema-manager.js +11 -1
  71. package/dist/schema/schema-manager.js.map +1 -1
  72. package/dist/schema/utils.d.ts +1 -0
  73. package/dist/schema/utils.d.ts.map +1 -1
  74. package/dist/schema/utils.js +2 -1
  75. package/dist/schema/utils.js.map +1 -1
  76. package/dist/schema.d.ts +2 -0
  77. package/dist/schema.d.ts.map +1 -0
  78. package/dist/schema.js +13 -0
  79. package/dist/smrt-knowledge.json +5 -4
  80. package/dist/system/compatibility.d.ts +25 -0
  81. package/dist/system/compatibility.d.ts.map +1 -1
  82. package/dist/system/compatibility.js +61 -1
  83. package/dist/system/compatibility.js.map +1 -1
  84. package/dist/system/index.js +2 -2
  85. package/dist/system/schema.d.ts +17 -9
  86. package/dist/system/schema.d.ts.map +1 -1
  87. package/dist/system/schema.js +25 -11
  88. package/dist/system/schema.js.map +1 -1
  89. package/dist/testing/database.d.ts.map +1 -1
  90. package/dist/testing/database.js +5 -2
  91. package/dist/testing/database.js.map +1 -1
  92. package/package.json +10 -5
@@ -42,11 +42,19 @@ export declare class MigrationGenerator {
42
42
  private format;
43
43
  private includeDown;
44
44
  private packageName?;
45
+ private materializeStructuredSchema;
45
46
  constructor(options?: {
46
47
  engine?: DatabaseEngine;
47
48
  format?: 'sql' | 'typescript';
48
49
  includeDown?: boolean;
49
50
  packageName?: string;
51
+ /**
52
+ * Ignore cached manifest DDL and materialize structured columns for the
53
+ * selected engine. Framework-owned executable paths should enable this;
54
+ * the default preserves the published custom-DDL contract while still
55
+ * materializing bare PostgreSQL TIMESTAMP columns as TIMESTAMPTZ.
56
+ */
57
+ materializeStructuredSchema?: boolean;
50
58
  });
51
59
  /**
52
60
  * Generate a migration from a schema diff
@@ -1 +1 @@
1
- {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/migrations/generator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/migrations/generator.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { renderIndexTarget } from "../schema/index-utils.js";
2
2
  import { getDDLStrategy } from "../schema/ddl/index.js";
3
+ import { materializeManifestDDLForEngine } from "../schema/ddl/materialize-manifest.js";
3
4
  import "../schema/utils.js";
4
5
  import { computeChecksum } from "./checksum.js";
5
6
  //#region src/migrations/generator.ts
@@ -17,11 +18,13 @@ var MigrationGenerator = class {
17
18
  format;
18
19
  includeDown;
19
20
  packageName;
21
+ materializeStructuredSchema;
20
22
  constructor(options = {}) {
21
23
  this.engine = options.engine || "sqlite";
22
24
  this.format = options.format || "sql";
23
25
  this.includeDown = options.includeDown ?? true;
24
26
  this.packageName = options.packageName;
27
+ this.materializeStructuredSchema = options.materializeStructuredSchema ?? false;
25
28
  }
26
29
  /**
27
30
  * Generate a migration from a schema diff
@@ -129,7 +132,8 @@ ${downStatementsStr}
129
132
  * Generate CREATE TABLE SQL from schema definition
130
133
  */
131
134
  generateCreateTable(schema) {
132
- if (schema.ddl) return schema.ddl;
135
+ if (Object.keys(schema.columns).length === 0 && schema.ddl?.trim()) return materializeManifestDDLForEngine(schema.ddl, this.engine);
136
+ if (!this.materializeStructuredSchema && schema.ddl?.trim()) return materializeManifestDDLForEngine(schema.ddl, this.engine);
133
137
  return getDDLStrategy(this.engine).generateCreateTable(schema);
134
138
  }
135
139
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"generator.js","names":[],"sources":["../../src/migrations/generator.ts"],"sourcesContent":["/**\n * Migration Generator\n *\n * Generates migration files from schema diffs.\n * Supports both SQL and TypeScript formats.\n */\n\nimport { getDDLStrategy } from '../schema/ddl/index.js';\nimport type {\n MigrationDefinition,\n SchemaChange,\n SchemaDefinition,\n SchemaDiff,\n} from '../schema/types.js';\nimport { renderIndexTarget } from '../schema/utils.js';\nimport { computeChecksum } from './checksum.js';\nimport type { DatabaseEngine } from './types.js';\n\n/**\n * Options for migration generation\n */\nexport interface GenerateMigrationOptions {\n /** Migration name (without sequence number) */\n name: string;\n /** Output format */\n format?: 'sql' | 'typescript';\n /** Database engine for engine-specific SQL */\n engine?: DatabaseEngine;\n /** Include generated DOWN statements */\n includeDown?: boolean;\n /** Package name for metadata */\n packageName?: string;\n /** Description for the migration */\n description?: string;\n}\n\n/**\n * Result of migration generation\n */\nexport interface GeneratedMigration {\n /** Full migration ID (e.g., '0001_add_users') */\n id: string;\n /** Filename (e.g., '0001_add_users.sql') */\n filename: string;\n /** File content */\n content: string;\n /** UP SQL statements */\n up: string[];\n /** DOWN SQL statements (if includeDown is true) */\n down: string[];\n /** Checksum of the migration */\n checksum: string;\n}\n\n/**\n * MigrationGenerator class for creating migration files\n */\nexport class MigrationGenerator {\n private engine: DatabaseEngine;\n private format: 'sql' | 'typescript';\n private includeDown: boolean;\n private packageName?: string;\n\n constructor(\n options: {\n engine?: DatabaseEngine;\n format?: 'sql' | 'typescript';\n includeDown?: boolean;\n packageName?: string;\n } = {},\n ) {\n this.engine = options.engine || 'sqlite';\n this.format = options.format || 'sql';\n this.includeDown = options.includeDown ?? true;\n this.packageName = options.packageName;\n }\n\n /**\n * Generate a migration from a schema diff\n */\n generateFromDiff(\n diff: SchemaDiff,\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n const upStatements: string[] = [];\n const downStatements: string[] = [];\n\n // Handle new tables\n for (const schema of diff.added_tables) {\n upStatements.push(this.generateCreateTable(schema));\n if (this.includeDown || options.includeDown) {\n downStatements.push(this.generateDropTable(schema.tableName));\n }\n }\n\n // Handle column and index changes\n for (const change of diff.changes) {\n const { up, down } = this.generateChangeSQL(change);\n upStatements.push(...up);\n if ((this.includeDown || options.includeDown) && down.length > 0) {\n downStatements.push(...down);\n }\n }\n\n // Handle dropped tables (already in SQL form if present)\n for (const tableName of diff.dropped_tables) {\n upStatements.push(this.generateDropTable(tableName));\n // No automatic DOWN for drops - too dangerous\n }\n\n return this.generateMigration(upStatements, downStatements, options);\n }\n\n /**\n * Generate a migration from explicit SQL statements\n */\n generateFromSQL(\n upStatements: string[],\n downStatements: string[],\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n return this.generateMigration(upStatements, downStatements, options);\n }\n\n /**\n * Generate an empty migration template\n */\n generateEmpty(options: GenerateMigrationOptions): GeneratedMigration {\n return this.generateMigration([], [], options);\n }\n\n /**\n * Core migration generation logic\n */\n private generateMigration(\n upStatements: string[],\n downStatements: string[],\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n const id = options.name;\n const description = options.description || `Migration: ${options.name}`;\n const format = options.format || this.format;\n const filename = `${id}.${format === 'sql' ? 'sql' : 'ts'}`;\n const checksum = computeChecksum(upStatements);\n\n let content: string;\n if (format === 'sql') {\n content = this.generateSQLFile(\n id,\n description,\n upStatements,\n downStatements,\n );\n } else {\n content = this.generateTypeScriptFile(\n id,\n description,\n upStatements,\n downStatements,\n );\n }\n\n return {\n id,\n filename,\n content,\n up: upStatements,\n down: downStatements,\n checksum,\n };\n }\n\n /**\n * Generate SQL migration file content\n */\n private generateSQLFile(\n id: string,\n description: string,\n upStatements: string[],\n downStatements: string[],\n ): string {\n const lines: string[] = [\n `-- @id: ${id}`,\n `-- @description: ${description}`,\n `-- @generated: ${new Date().toISOString()}`,\n ];\n\n if (this.packageName) {\n lines.push(`-- @package: ${this.packageName}`);\n }\n\n lines.push('');\n lines.push('-- @up');\n\n if (upStatements.length === 0) {\n lines.push('-- Add your UP statements here');\n } else {\n for (const stmt of upStatements) {\n lines.push(`${stmt};`);\n }\n }\n\n lines.push('');\n lines.push('-- @down');\n\n if (downStatements.length === 0) {\n lines.push('-- Add your DOWN statements here');\n } else {\n for (const stmt of downStatements) {\n lines.push(`${stmt};`);\n }\n }\n\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * Generate TypeScript migration file content\n */\n private generateTypeScriptFile(\n id: string,\n description: string,\n upStatements: string[],\n downStatements: string[],\n ): string {\n const upStatementsStr =\n upStatements.length > 0\n ? upStatements\n .map((s) => ` await db.query(\\`${this.escapeTemplate(s)}\\`);`)\n .join('\\n')\n : ' // Add your UP statements here';\n\n const downStatementsStr =\n downStatements.length > 0\n ? downStatements\n .map((s) => ` await db.query(\\`${this.escapeTemplate(s)}\\`);`)\n .join('\\n')\n : ' // Add your DOWN statements here';\n\n return `/**\n * Migration: ${id}\n * ${description}\n *\n * Generated: ${new Date().toISOString()}\n */\n\nimport type { Migration } from '@happyvertical/smrt-core';\n\nexport default {\n id: '${id}',\n description: '${description.replace(/'/g, \"\\\\'\")}',\n version: '1.0.0',\n\n up: async (db) => {\n${upStatementsStr}\n },\n\n down: async (db) => {\n${downStatementsStr}\n },\n} satisfies Migration;\n`;\n }\n\n /**\n * Generate CREATE TABLE SQL from schema definition\n */\n private generateCreateTable(schema: SchemaDefinition): string {\n // If DDL is already provided, use it\n if (schema.ddl) {\n return schema.ddl;\n }\n\n // No pre-generated DDL: delegate to the engine's DDL strategy, exactly as\n // the migration orchestrator does (orchestrate.ts). Building the statement\n // inline here previously emitted the *abstract* column type verbatim\n // (JSON/REAL/UUID) instead of the per-engine type (JSONB/DOUBLE\n // PRECISION/native uuid), so the very next `compare()` flagged spurious\n // type drift. The strategy also escapes identifiers/defaults safely. (Like\n // the orchestrator's CREATE TABLE path, foreign-key constraints are not\n // emitted here — SMRT manages relationships via cross-package refs and\n // avoids circular DDL FKs, see #1333.)\n return getDDLStrategy(this.engine).generateCreateTable(schema);\n }\n\n /**\n * Generate DROP TABLE SQL\n */\n private generateDropTable(tableName: string): string {\n return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;\n }\n\n /**\n * Generate SQL for a schema change\n */\n private generateChangeSQL(change: SchemaChange): {\n up: string[];\n down: string[];\n } {\n const up: string[] = [];\n const down: string[] = [];\n const sqlStatements =\n change.sqlStatements ?? (change.sql ? [change.sql] : []);\n\n switch (change.type) {\n case 'add_column':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.column && change.name) {\n up.push(\n this.generateAddColumn(change.table, change.name, change.column),\n );\n }\n if (change.name) {\n down.push(this.generateDropColumn(change.table, change.name));\n }\n break;\n\n case 'drop_column':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.name) {\n up.push(this.generateDropColumn(change.table, change.name));\n }\n // No automatic DOWN for drops\n break;\n\n case 'add_index':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.index) {\n up.push(this.generateAddIndex(change.table, change.index));\n }\n if (change.name) {\n down.push(this.generateDropIndex(change.name));\n }\n break;\n\n case 'drop_index':\n if (change.name) {\n up.push(this.generateDropIndex(change.name));\n }\n // No automatic DOWN for index drops\n break;\n\n case 'type_upgrade':\n // Type upgrades are safe conversions that SMRT can handle automatically\n // The SQL is generated by the differ based on engine-specific requirements\n if (sqlStatements.length > 0) {\n for (const sql of sqlStatements) {\n // Skip comment-only SQL (indicates a no-op migration)\n if (!sql.startsWith('--')) {\n up.push(sql);\n }\n }\n }\n // No automatic DOWN for type changes - could lose data\n break;\n\n case 'type_mismatch':\n // Type mismatches require manual intervention\n up.push(`-- WARNING: Type mismatch for ${change.table}.${change.name}`);\n up.push(\n `-- Expected: ${change.mismatch?.expected}, Actual: ${change.mismatch?.actual}`,\n );\n up.push(`-- Manual migration required`);\n break;\n\n default:\n // Unknown change type\n break;\n }\n\n return { up, down };\n }\n\n /**\n * Generate ADD COLUMN SQL\n */\n private generateAddColumn(\n tableName: string,\n colName: string,\n colDef: import('../schema/types.js').ColumnDefinition,\n ): string {\n const parts: string[] = [this.quoteIdentifier(colName), colDef.type];\n\n if (colDef.notNull) {\n parts.push('NOT NULL');\n }\n if (colDef.unique) {\n parts.push('UNIQUE');\n }\n if (colDef.defaultValue !== undefined) {\n parts.push(`DEFAULT ${this.formatDefaultValue(colDef.defaultValue)}`);\n }\n\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${parts.join(' ')}`;\n }\n\n /**\n * Generate DROP COLUMN SQL\n */\n private generateDropColumn(tableName: string, colName: string): string {\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(colName)}`;\n }\n\n /**\n * Generate ADD INDEX SQL\n */\n private generateAddIndex(\n tableName: string,\n index: import('../schema/types.js').IndexDefinition,\n ): string {\n const uniqueStr = index.unique ? 'UNIQUE ' : '';\n const target = renderIndexTarget(index, this.engine);\n\n if (this.engine === 'postgres') {\n // PostgreSQL: use CONCURRENTLY for production safety\n return `CREATE ${uniqueStr}INDEX CONCURRENTLY IF NOT EXISTS ${this.quoteIdentifier(index.name)} ON ${this.quoteIdentifier(tableName)} (${target})`;\n }\n\n return `CREATE ${uniqueStr}INDEX IF NOT EXISTS ${this.quoteIdentifier(index.name)} ON ${this.quoteIdentifier(tableName)} (${target})`;\n }\n\n /**\n * Generate DROP INDEX SQL\n */\n private generateDropIndex(indexName: string): string {\n if (this.engine === 'postgres') {\n return `DROP INDEX CONCURRENTLY IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n return `DROP INDEX IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n\n /**\n * Quote a SQL identifier\n */\n private quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n }\n\n /**\n * Format a default value for SQL\n */\n private formatDefaultValue(value: unknown): string {\n if (value === null) return 'NULL';\n if (typeof value === 'string') return `'${value.replace(/'/g, \"''\")}'`;\n if (typeof value === 'boolean') return value ? '1' : '0';\n if (typeof value === 'number') return String(value);\n return String(value);\n }\n\n /**\n * Escape template literal content\n */\n private escapeTemplate(str: string): string {\n return str.replace(/`/g, '\\\\`').replace(/\\$/g, '\\\\$');\n }\n}\n\n/**\n * Generate a migration definition from UP and DOWN statements\n */\nexport function createMigrationDefinition(\n id: string,\n upStatements: string[],\n downStatements: string[] = [],\n options: {\n description?: string;\n version?: string;\n packageName?: string;\n sourceFile?: string;\n } = {},\n): MigrationDefinition {\n return {\n id,\n description: options.description || `Migration: ${id}`,\n version: options.version || '1.0.0',\n up: upStatements,\n down: downStatements,\n package_name: options.packageName,\n source_file: options.sourceFile,\n };\n}\n\n/**\n * Generate a sequence number for a new migration\n */\nexport function generateMigrationSequence(existingIds: string[]): string {\n const sequences = existingIds\n .map((id) => {\n const match = id.match(/^(\\d+)_/);\n return match ? parseInt(match[1], 10) : 0;\n })\n .filter((n) => !Number.isNaN(n));\n\n const maxSequence = sequences.length > 0 ? Math.max(...sequences) : 0;\n return String(maxSequence + 1).padStart(4, '0');\n}\n\n/**\n * Generate a timestamp-based migration ID\n */\nexport function generateMigrationTimestamp(): string {\n const now = new Date();\n return [\n now.getFullYear(),\n String(now.getMonth() + 1).padStart(2, '0'),\n String(now.getDate()).padStart(2, '0'),\n String(now.getHours()).padStart(2, '0'),\n String(now.getMinutes()).padStart(2, '0'),\n String(now.getSeconds()).padStart(2, '0'),\n ].join('');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"generator.js","names":[],"sources":["../../src/migrations/generator.ts"],"sourcesContent":["/**\n * Migration Generator\n *\n * Generates migration files from schema diffs.\n * Supports both SQL and TypeScript formats.\n */\n\nimport { getDDLStrategy } from '../schema/ddl/index.js';\nimport { materializeManifestDDLForEngine } from '../schema/ddl/materialize-manifest.js';\nimport type {\n MigrationDefinition,\n SchemaChange,\n SchemaDefinition,\n SchemaDiff,\n} from '../schema/types.js';\nimport { renderIndexTarget } from '../schema/utils.js';\nimport { computeChecksum } from './checksum.js';\nimport type { DatabaseEngine } from './types.js';\n\n/**\n * Options for migration generation\n */\nexport interface GenerateMigrationOptions {\n /** Migration name (without sequence number) */\n name: string;\n /** Output format */\n format?: 'sql' | 'typescript';\n /** Database engine for engine-specific SQL */\n engine?: DatabaseEngine;\n /** Include generated DOWN statements */\n includeDown?: boolean;\n /** Package name for metadata */\n packageName?: string;\n /** Description for the migration */\n description?: string;\n}\n\n/**\n * Result of migration generation\n */\nexport interface GeneratedMigration {\n /** Full migration ID (e.g., '0001_add_users') */\n id: string;\n /** Filename (e.g., '0001_add_users.sql') */\n filename: string;\n /** File content */\n content: string;\n /** UP SQL statements */\n up: string[];\n /** DOWN SQL statements (if includeDown is true) */\n down: string[];\n /** Checksum of the migration */\n checksum: string;\n}\n\n/**\n * MigrationGenerator class for creating migration files\n */\nexport class MigrationGenerator {\n private engine: DatabaseEngine;\n private format: 'sql' | 'typescript';\n private includeDown: boolean;\n private packageName?: string;\n private materializeStructuredSchema: boolean;\n\n constructor(\n options: {\n engine?: DatabaseEngine;\n format?: 'sql' | 'typescript';\n includeDown?: boolean;\n packageName?: string;\n /**\n * Ignore cached manifest DDL and materialize structured columns for the\n * selected engine. Framework-owned executable paths should enable this;\n * the default preserves the published custom-DDL contract while still\n * materializing bare PostgreSQL TIMESTAMP columns as TIMESTAMPTZ.\n */\n materializeStructuredSchema?: boolean;\n } = {},\n ) {\n this.engine = options.engine || 'sqlite';\n this.format = options.format || 'sql';\n this.includeDown = options.includeDown ?? true;\n this.packageName = options.packageName;\n this.materializeStructuredSchema =\n options.materializeStructuredSchema ?? false;\n }\n\n /**\n * Generate a migration from a schema diff\n */\n generateFromDiff(\n diff: SchemaDiff,\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n const upStatements: string[] = [];\n const downStatements: string[] = [];\n\n // Handle new tables\n for (const schema of diff.added_tables) {\n upStatements.push(this.generateCreateTable(schema));\n if (this.includeDown || options.includeDown) {\n downStatements.push(this.generateDropTable(schema.tableName));\n }\n }\n\n // Handle column and index changes\n for (const change of diff.changes) {\n const { up, down } = this.generateChangeSQL(change);\n upStatements.push(...up);\n if ((this.includeDown || options.includeDown) && down.length > 0) {\n downStatements.push(...down);\n }\n }\n\n // Handle dropped tables (already in SQL form if present)\n for (const tableName of diff.dropped_tables) {\n upStatements.push(this.generateDropTable(tableName));\n // No automatic DOWN for drops - too dangerous\n }\n\n return this.generateMigration(upStatements, downStatements, options);\n }\n\n /**\n * Generate a migration from explicit SQL statements\n */\n generateFromSQL(\n upStatements: string[],\n downStatements: string[],\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n return this.generateMigration(upStatements, downStatements, options);\n }\n\n /**\n * Generate an empty migration template\n */\n generateEmpty(options: GenerateMigrationOptions): GeneratedMigration {\n return this.generateMigration([], [], options);\n }\n\n /**\n * Core migration generation logic\n */\n private generateMigration(\n upStatements: string[],\n downStatements: string[],\n options: GenerateMigrationOptions,\n ): GeneratedMigration {\n const id = options.name;\n const description = options.description || `Migration: ${options.name}`;\n const format = options.format || this.format;\n const filename = `${id}.${format === 'sql' ? 'sql' : 'ts'}`;\n const checksum = computeChecksum(upStatements);\n\n let content: string;\n if (format === 'sql') {\n content = this.generateSQLFile(\n id,\n description,\n upStatements,\n downStatements,\n );\n } else {\n content = this.generateTypeScriptFile(\n id,\n description,\n upStatements,\n downStatements,\n );\n }\n\n return {\n id,\n filename,\n content,\n up: upStatements,\n down: downStatements,\n checksum,\n };\n }\n\n /**\n * Generate SQL migration file content\n */\n private generateSQLFile(\n id: string,\n description: string,\n upStatements: string[],\n downStatements: string[],\n ): string {\n const lines: string[] = [\n `-- @id: ${id}`,\n `-- @description: ${description}`,\n `-- @generated: ${new Date().toISOString()}`,\n ];\n\n if (this.packageName) {\n lines.push(`-- @package: ${this.packageName}`);\n }\n\n lines.push('');\n lines.push('-- @up');\n\n if (upStatements.length === 0) {\n lines.push('-- Add your UP statements here');\n } else {\n for (const stmt of upStatements) {\n lines.push(`${stmt};`);\n }\n }\n\n lines.push('');\n lines.push('-- @down');\n\n if (downStatements.length === 0) {\n lines.push('-- Add your DOWN statements here');\n } else {\n for (const stmt of downStatements) {\n lines.push(`${stmt};`);\n }\n }\n\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * Generate TypeScript migration file content\n */\n private generateTypeScriptFile(\n id: string,\n description: string,\n upStatements: string[],\n downStatements: string[],\n ): string {\n const upStatementsStr =\n upStatements.length > 0\n ? upStatements\n .map((s) => ` await db.query(\\`${this.escapeTemplate(s)}\\`);`)\n .join('\\n')\n : ' // Add your UP statements here';\n\n const downStatementsStr =\n downStatements.length > 0\n ? downStatements\n .map((s) => ` await db.query(\\`${this.escapeTemplate(s)}\\`);`)\n .join('\\n')\n : ' // Add your DOWN statements here';\n\n return `/**\n * Migration: ${id}\n * ${description}\n *\n * Generated: ${new Date().toISOString()}\n */\n\nimport type { Migration } from '@happyvertical/smrt-core';\n\nexport default {\n id: '${id}',\n description: '${description.replace(/'/g, \"\\\\'\")}',\n version: '1.0.0',\n\n up: async (db) => {\n${upStatementsStr}\n },\n\n down: async (db) => {\n${downStatementsStr}\n },\n} satisfies Migration;\n`;\n }\n\n /**\n * Generate CREATE TABLE SQL from schema definition\n */\n private generateCreateTable(schema: SchemaDefinition): string {\n if (Object.keys(schema.columns).length === 0 && schema.ddl?.trim()) {\n // Structured materialization cannot preserve constraints or vendor SQL\n // when an older/custom manifest exposes only executable DDL. Retain the\n // only valid representation even when materialization was requested.\n return materializeManifestDDLForEngine(schema.ddl, this.engine);\n }\n\n if (!this.materializeStructuredSchema && schema.ddl?.trim()) {\n // Preserve the documented custom-DDL contract by default, including\n // table-level constraints that structured ColumnDefinition cannot\n // represent. Engine-aware framework callers explicitly opt into the\n // structured materialization path below.\n return materializeManifestDDLForEngine(schema.ddl, this.engine);\n }\n\n // The manifest's pre-generated `ddl` is deliberately engine-neutral and\n // may contain abstract TIMESTAMP/JSON/UUID types. Always materialize the\n // structured columns through the selected engine strategy; executing the\n // cached string on PostgreSQL would otherwise bypass TIMESTAMPTZ and lose\n // JavaScript Date offsets (#2069). This matches the migration orchestrator\n // and also prevents JSON/REAL/UUID drift after a create.\n return getDDLStrategy(this.engine).generateCreateTable(schema);\n }\n\n /**\n * Generate DROP TABLE SQL\n */\n private generateDropTable(tableName: string): string {\n return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;\n }\n\n /**\n * Generate SQL for a schema change\n */\n private generateChangeSQL(change: SchemaChange): {\n up: string[];\n down: string[];\n } {\n const up: string[] = [];\n const down: string[] = [];\n const sqlStatements =\n change.sqlStatements ?? (change.sql ? [change.sql] : []);\n\n switch (change.type) {\n case 'add_column':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.column && change.name) {\n up.push(\n this.generateAddColumn(change.table, change.name, change.column),\n );\n }\n if (change.name) {\n down.push(this.generateDropColumn(change.table, change.name));\n }\n break;\n\n case 'drop_column':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.name) {\n up.push(this.generateDropColumn(change.table, change.name));\n }\n // No automatic DOWN for drops\n break;\n\n case 'add_index':\n if (sqlStatements.length > 0) {\n up.push(...sqlStatements);\n } else if (change.index) {\n up.push(this.generateAddIndex(change.table, change.index));\n }\n if (change.name) {\n down.push(this.generateDropIndex(change.name));\n }\n break;\n\n case 'drop_index':\n if (change.name) {\n up.push(this.generateDropIndex(change.name));\n }\n // No automatic DOWN for index drops\n break;\n\n case 'type_upgrade':\n // Type upgrades are safe conversions that SMRT can handle automatically\n // The SQL is generated by the differ based on engine-specific requirements\n if (sqlStatements.length > 0) {\n for (const sql of sqlStatements) {\n // Skip comment-only SQL (indicates a no-op migration)\n if (!sql.startsWith('--')) {\n up.push(sql);\n }\n }\n }\n // No automatic DOWN for type changes - could lose data\n break;\n\n case 'type_mismatch':\n // Type mismatches require manual intervention\n up.push(`-- WARNING: Type mismatch for ${change.table}.${change.name}`);\n up.push(\n `-- Expected: ${change.mismatch?.expected}, Actual: ${change.mismatch?.actual}`,\n );\n up.push(`-- Manual migration required`);\n break;\n\n default:\n // Unknown change type\n break;\n }\n\n return { up, down };\n }\n\n /**\n * Generate ADD COLUMN SQL\n */\n private generateAddColumn(\n tableName: string,\n colName: string,\n colDef: import('../schema/types.js').ColumnDefinition,\n ): string {\n const parts: string[] = [this.quoteIdentifier(colName), colDef.type];\n\n if (colDef.notNull) {\n parts.push('NOT NULL');\n }\n if (colDef.unique) {\n parts.push('UNIQUE');\n }\n if (colDef.defaultValue !== undefined) {\n parts.push(`DEFAULT ${this.formatDefaultValue(colDef.defaultValue)}`);\n }\n\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${parts.join(' ')}`;\n }\n\n /**\n * Generate DROP COLUMN SQL\n */\n private generateDropColumn(tableName: string, colName: string): string {\n return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(colName)}`;\n }\n\n /**\n * Generate ADD INDEX SQL\n */\n private generateAddIndex(\n tableName: string,\n index: import('../schema/types.js').IndexDefinition,\n ): string {\n const uniqueStr = index.unique ? 'UNIQUE ' : '';\n const target = renderIndexTarget(index, this.engine);\n\n if (this.engine === 'postgres') {\n // PostgreSQL: use CONCURRENTLY for production safety\n return `CREATE ${uniqueStr}INDEX CONCURRENTLY IF NOT EXISTS ${this.quoteIdentifier(index.name)} ON ${this.quoteIdentifier(tableName)} (${target})`;\n }\n\n return `CREATE ${uniqueStr}INDEX IF NOT EXISTS ${this.quoteIdentifier(index.name)} ON ${this.quoteIdentifier(tableName)} (${target})`;\n }\n\n /**\n * Generate DROP INDEX SQL\n */\n private generateDropIndex(indexName: string): string {\n if (this.engine === 'postgres') {\n return `DROP INDEX CONCURRENTLY IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n return `DROP INDEX IF EXISTS ${this.quoteIdentifier(indexName)}`;\n }\n\n /**\n * Quote a SQL identifier\n */\n private quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n }\n\n /**\n * Format a default value for SQL\n */\n private formatDefaultValue(value: unknown): string {\n if (value === null) return 'NULL';\n if (typeof value === 'string') return `'${value.replace(/'/g, \"''\")}'`;\n if (typeof value === 'boolean') return value ? '1' : '0';\n if (typeof value === 'number') return String(value);\n return String(value);\n }\n\n /**\n * Escape template literal content\n */\n private escapeTemplate(str: string): string {\n return str.replace(/`/g, '\\\\`').replace(/\\$/g, '\\\\$');\n }\n}\n\n/**\n * Generate a migration definition from UP and DOWN statements\n */\nexport function createMigrationDefinition(\n id: string,\n upStatements: string[],\n downStatements: string[] = [],\n options: {\n description?: string;\n version?: string;\n packageName?: string;\n sourceFile?: string;\n } = {},\n): MigrationDefinition {\n return {\n id,\n description: options.description || `Migration: ${id}`,\n version: options.version || '1.0.0',\n up: upStatements,\n down: downStatements,\n package_name: options.packageName,\n source_file: options.sourceFile,\n };\n}\n\n/**\n * Generate a sequence number for a new migration\n */\nexport function generateMigrationSequence(existingIds: string[]): string {\n const sequences = existingIds\n .map((id) => {\n const match = id.match(/^(\\d+)_/);\n return match ? parseInt(match[1], 10) : 0;\n })\n .filter((n) => !Number.isNaN(n));\n\n const maxSequence = sequences.length > 0 ? Math.max(...sequences) : 0;\n return String(maxSequence + 1).padStart(4, '0');\n}\n\n/**\n * Generate a timestamp-based migration ID\n */\nexport function generateMigrationTimestamp(): string {\n const now = new Date();\n return [\n now.getFullYear(),\n String(now.getMonth() + 1).padStart(2, '0'),\n String(now.getDate()).padStart(2, '0'),\n String(now.getHours()).padStart(2, '0'),\n String(now.getMinutes()).padStart(2, '0'),\n String(now.getSeconds()).padStart(2, '0'),\n ].join('');\n}\n"],"mappings":"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"}
@@ -49,6 +49,13 @@ export interface MigrateSmrtSchemasOptions {
49
49
  * compile time rather than silently falling back to `'sqlite'` at runtime.
50
50
  */
51
51
  engineHint?: DatabaseEngine;
52
+ /**
53
+ * Confirm that historical PostgreSQL Date writers used UTC wall times before
54
+ * reinterpreting legacy TIMESTAMP/TEXT/JSON columns as TIMESTAMPTZ.
55
+ */
56
+ postgresTimestampMigration?: {
57
+ legacyTimezone: 'UTC';
58
+ };
52
59
  }
53
60
  export interface MigrateSmrtSchemasResult {
54
61
  applied: boolean;
@@ -108,6 +115,9 @@ export interface PendingSchemaStatementsResult {
108
115
  */
109
116
  export declare function getPendingSchemaStatements(db: DatabaseInterface, options?: {
110
117
  engineHint?: DatabaseEngine;
118
+ postgresTimestampMigration?: {
119
+ legacyTimezone: 'UTC';
120
+ };
111
121
  }): Promise<PendingSchemaStatementsResult>;
112
122
  /**
113
123
  * Apply any pending schema changes from `ObjectRegistry` to `db`.
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrate.d.ts","sourceRoot":"","sources":["../../src/migrations/orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAOrB,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,iBAAiB,CAAC;IACtB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;OAOG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,cAAc,CAAA;CAAO,GAC5C,OAAO,CAAC,6BAA6B,CAAC,CAoBxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAyDnC"}
1
+ {"version":3,"file":"orchestrate.d.ts","sourceRoot":"","sources":["../../src/migrations/orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAOrB,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,iBAAiB,CAAC;IACtB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;OAGG;IACH,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;;;OAOG;IACH,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0BAA0B,CAAC,EAAE;QAAE,cAAc,EAAE,KAAK,CAAA;KAAE,CAAC;CACnD,GACL,OAAO,CAAC,6BAA6B,CAAC,CAqBxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA0DnC"}
@@ -22,7 +22,10 @@ import { MigrationTracker } from "./tracker.js";
22
22
  */
23
23
  async function getPendingSchemaStatements(db, options = {}) {
24
24
  const schemas = ObjectRegistry.getAllSchemasAsDefinitions();
25
- const diff = await generateSchemaDiff(db, schemas, { engineHint: options.engineHint });
25
+ const diff = await generateSchemaDiff(db, schemas, {
26
+ engineHint: options.engineHint,
27
+ postgresTimestampMigration: options.postgresTimestampMigration
28
+ });
26
29
  const statements = collectStatementsFromDiff(diff, db, options.engineHint);
27
30
  const unactionableChanges = collectUnactionableChanges(diff);
28
31
  return {
@@ -70,7 +73,10 @@ async function getPendingSchemaStatements(db, options = {}) {
70
73
  * field, the result would look indistinguishable from "already in sync."
71
74
  */
72
75
  async function migrateSmrtSchemas(options) {
73
- const pending = await getPendingSchemaStatements(options.db, { engineHint: options.engineHint });
76
+ const pending = await getPendingSchemaStatements(options.db, {
77
+ engineHint: options.engineHint,
78
+ postgresTimestampMigration: options.postgresTimestampMigration
79
+ });
74
80
  if (!pending.hasChanges || pending.statements.length === 0) return {
75
81
  applied: false,
76
82
  results: [],
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrate.js","names":[],"sources":["../../src/migrations/orchestrate.ts"],"sourcesContent":["/**\n * High-level \"make my SMRT app's schema match its database\" orchestration.\n *\n * Wraps `ObjectRegistry.getAllSchemasAsDefinitions()` → `generateSchemaDiff`\n * → `MigrationTracker.applyAll`. Apps that just want to run pending\n * schema changes call `migrateSmrtSchemas({ db, packageName, version })`\n * and don't have to assemble the migration definition by hand.\n *\n * The migration name is timestamped, so re-running on an up-to-date\n * database is a no-op (the diff is empty, no statements are generated,\n * `applied: false` is returned).\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ObjectRegistry } from '../registry.js';\nimport { detectEngine, getDDLStrategy } from '../schema/ddl/index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport type { MigrationResult, SchemaChange } from '../schema/types.js';\nimport {\n generateSchemaDiff,\n getSQLFromDiff,\n hasActionableChanges,\n} from './differ.js';\nimport {\n createMigrationDefinition,\n generateMigrationTimestamp,\n} from './generator.js';\nimport { MigrationTracker } from './tracker.js';\n\nexport interface MigrateSmrtSchemasOptions {\n db: DatabaseInterface;\n /** Stored on the migration record so the audit log shows which app applied it. */\n packageName: string;\n /**\n * Stored on the migration record. Typically the app's package.json\n * version. Optional because most apps don't have a runtime-readable\n * version handy and would hardcode a stale string if forced to supply\n * one — `'unknown'` is honest. Pass an explicit value when you can\n * (e.g. `version: process.env.APP_VERSION`).\n */\n version?: string;\n /** Override the description on the synthetic migration definition. */\n description?: string;\n /** Override the migration name (default: `<timestamp>_smrt_schema_sync`). */\n name?: string;\n /** Forwarded to MigrationTracker — defaults to true for pg-safe runs. */\n postgresSafe?: boolean;\n /** Forwarded to MigrationTracker — defaults to true. */\n reconcile?: boolean;\n /** Forwarded to MigrationTracker — defaults to true for `CREATE INDEX CONCURRENTLY`. */\n useConcurrentIndexes?: boolean;\n /**\n * Forwarded to MigrationTracker (Postgres lock timeout, milliseconds).\n * Lets apps that customize tracker timeouts directly preserve their\n * tuning when going through the orchestrator. Falls back to the\n * tracker's default (30000ms) when omitted.\n */\n lockTimeout?: number;\n /**\n * Forwarded to MigrationTracker (Postgres statement timeout, ms). Falls\n * back to the tracker's default (60000ms) when omitted.\n */\n statementTimeout?: number;\n /**\n * Explicit engine hint forwarded to `detectEngine`, `SchemaComparer`, and\n * `MigrationTracker`. Useful when `db.url` doesn't unambiguously identify\n * the engine (e.g. the JSON adapter, which uses DuckDB internally and may\n * not be recognizable by URL alone). All three layers honor the same hint\n * so the generated DDL, drift comparison, and execution path stay\n * consistent — without it, an empty-URL connection could produce\n * Postgres-flavored DDL but run through the SQLite tracker path.\n *\n * Typed as `DatabaseEngine` so unsupported values (e.g. `'mysql'`) fail at\n * compile time rather than silently falling back to `'sqlite'` at runtime.\n */\n engineHint?: DatabaseEngine;\n}\n\nexport interface MigrateSmrtSchemasResult {\n applied: boolean;\n results: MigrationResult[];\n statements: string[];\n schemaCount: number;\n /**\n * Schema changes the orchestrator detected but cannot apply automatically\n * (incompatible type mismatches, SQLite type upgrades that require table\n * recreation, etc.). Empty when the schema is fully reconciled. Inspect\n * this to surface a \"manual migration required\" warning in CLI output.\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Lets a caller write\n * `if (result.hasManualDrift) ...` without iterating the array first,\n * and makes the case visible to anyone scanning the result shape (the\n * full array is easy to gloss over when `applied: false`).\n */\n hasManualDrift: boolean;\n}\n\nexport interface PendingSchemaStatementsResult {\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>;\n statements: string[];\n schemaCount: number;\n hasChanges: boolean;\n /**\n * Schema changes the differ detected but that have no executable SQL\n * (incompatible `type_mismatch` entries, or `type_upgrade` entries whose\n * only generated SQL is an advisory comment because the engine cannot\n * upgrade the column in place — e.g. SQLite type widening). Surfacing\n * these lets status commands distinguish \"no drift\" from \"drift the\n * orchestrator can't fix on its own.\"\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Mirrors the\n * `MigrateSmrtSchemasResult.hasManualDrift` shortcut.\n */\n hasManualDrift: boolean;\n}\n\n/**\n * Compute the SQL statements needed to bring `db` in sync with the\n * currently-registered SMRT object schemas. Pure inspection — no writes.\n *\n * Useful for status commands (\"how far behind is the schema?\") and as the\n * inner step of `migrateSmrtSchemas`.\n *\n * **Note on the returned `statements`:** these are the engine-correct DDL\n * statements as the differ + DDL strategy produce them, *prior to* any\n * Postgres-specific rewrites the `MigrationTracker` applies at execution\n * time (e.g. rewriting `CREATE INDEX` → `CREATE INDEX CONCURRENTLY` and\n * moving CONCURRENTLY statements outside the surrounding transaction).\n * The list is suitable for preview/status/\"what would change?\" use cases.\n * For the exact byte-for-byte SQL the tracker ran, inspect the\n * `MigrationResult`s from `migrateSmrtSchemas` or the tracker's own logs.\n */\nexport async function getPendingSchemaStatements(\n db: DatabaseInterface,\n options: { engineHint?: DatabaseEngine } = {},\n): Promise<PendingSchemaStatementsResult> {\n const schemas = ObjectRegistry.getAllSchemasAsDefinitions();\n // Forward engineHint into the diff itself so the SchemaComparer's\n // existing-table SQL (ALTER/index drift) uses the same DDL strategy\n // we use for newly-added tables — otherwise the two halves of the\n // statement list can land on different engines (split-brain) when\n // `db.url` is empty or ambiguous.\n const diff = await generateSchemaDiff(db, schemas, {\n engineHint: options.engineHint,\n });\n const statements = collectStatementsFromDiff(diff, db, options.engineHint);\n const unactionableChanges = collectUnactionableChanges(diff);\n return {\n diff,\n schemaCount: Object.keys(schemas).length,\n statements,\n hasChanges: hasActionableChanges(diff),\n unactionableChanges,\n hasManualDrift: unactionableChanges.length > 0,\n };\n}\n\n/**\n * Apply any pending schema changes from `ObjectRegistry` to `db`.\n *\n * Returns `applied: false` (with no error) if the database is already in\n * sync. Throws on the first migration failure with the underlying error\n * preserved (callers should let it propagate to the CLI).\n *\n * **Note on the returned `statements`:** mirrors `getPendingSchemaStatements`\n * — the list is the planned DDL, prior to Postgres-specific tracker rewrites\n * (CONCURRENTLY, transaction reordering). The actual executed SQL is\n * tracked in the returned `results`.\n *\n * **Note on `applied` semantics under `reconcile: true` (the default):**\n * the tracker may re-execute a migration that is already in `completed`\n * state when the checksum matches — that counts as `applied: true` in the\n * returned `MigrationResult`. So `result.applied === true` means \"the\n * tracker ran the migration\", not specifically \"the database changed for\n * the first time.\" Pass `reconcile: false` if you need to distinguish a\n * fresh apply from a reconcile-replay over an existing completed record.\n *\n * **Note on repeated failures:** the migration name defaults to a fresh\n * timestamp on every call, so a sequence of failed runs leaves one\n * `failed` row in `_smrt_schema_migrations` per attempt. Pass an explicit\n * `name` if you want to overwrite the same record across retries.\n *\n * **Note on `unactionableChanges`:** the differ can detect schema drift\n * the migration step can't auto-resolve — incompatible type mismatches,\n * SQLite type widening that needs table recreation, etc. Those changes\n * appear in `result.unactionableChanges` and are NOT applied (the\n * statements list is filtered to executable DDL only). When the diff\n * contains *only* unactionable changes, the function returns\n * `applied: false` with a populated `unactionableChanges` so callers can\n * surface a \"manual migration required\" signal. Without inspecting that\n * field, the result would look indistinguishable from \"already in sync.\"\n */\nexport async function migrateSmrtSchemas(\n options: MigrateSmrtSchemasOptions,\n): Promise<MigrateSmrtSchemasResult> {\n const pending = await getPendingSchemaStatements(options.db, {\n engineHint: options.engineHint,\n });\n if (!pending.hasChanges || pending.statements.length === 0) {\n return {\n applied: false,\n results: [],\n statements: [],\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n }\n\n const tracker = new MigrationTracker({\n db: options.db,\n engineHint: options.engineHint,\n lockTimeout: options.lockTimeout,\n statementTimeout: options.statementTimeout,\n useConcurrentIndexes: options.useConcurrentIndexes ?? true,\n });\n const migration = createMigrationDefinition(\n options.name ?? `${generateMigrationTimestamp()}_smrt_schema_sync`,\n pending.statements,\n [],\n {\n description: options.description ?? 'Synchronize SMRT object schemas',\n packageName: options.packageName,\n version: options.version ?? 'unknown',\n },\n );\n\n const results = await tracker.applyAll([migration], {\n postgresSafe: options.postgresSafe ?? true,\n reconcile: options.reconcile ?? true,\n });\n\n const failed = results.find((result) => !result.success);\n if (failed) {\n throw failed.error instanceof Error\n ? failed.error\n : new Error(String(failed.error ?? `Migration ${failed.name} failed`));\n }\n\n const applied = results.some(\n (result) => result.applied !== false && result.skipped !== true,\n );\n\n return {\n applied,\n results,\n statements: pending.statements,\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n}\n\n/** Mirror of the `getDatabaseUrl` helper in class.ts — some adapters\n * (JSON, certain in-memory wrappers) leave `db.url` undefined and expose\n * the URL on `db.config?.url` instead. Detecting the engine from a raw\n * `db.url` would throw or silently fall through to the sqlite default.\n */\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n return db.url || dbWithConfig.config?.url || '';\n}\n\n/**\n * Materialize the diff into the engine-correct SQL statements.\n *\n * For new tables we delegate to the engine's `DDLStrategy` so column types\n * (`REAL` → `DOUBLE PRECISION` on Postgres, `JSON` → `JSONB`, etc.),\n * partial-index `WHERE` clauses, and trigger syntax all match what the\n * SchemaComparer would expect on subsequent runs. Emitting the raw\n * abstract `SchemaDefinition.ddl` here would cause immediate type-drift\n * migrations on Postgres/DuckDB.\n *\n * Column/index/trigger changes for existing tables continue to come from\n * `getSQLFromDiff`, which is already engine-aware via the differ's per-\n * change `sqlStatements`.\n */\nfunction collectStatementsFromDiff(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n db: DatabaseInterface,\n engineHint?: DatabaseEngine,\n): string[] {\n const strategy = getDDLStrategy(\n detectEngine(resolveDatabaseUrl(db), engineHint),\n );\n const statements: string[] = [];\n for (const schema of diff.added_tables) {\n statements.push(strategy.generateCreateTable(schema));\n statements.push(...strategy.generateIndexes(schema));\n statements.push(...strategy.generateTriggers(schema));\n }\n statements.push(...getSQLFromDiff(diff));\n // Drop empty and comment-only entries. SQLite type-widening upgrades\n // surface as `-- SQLite: Type upgrade for X requires table recreation`\n // — passing those through to the tracker records a successful migration\n // without actually fixing the column, so the same drift re-appears on\n // every subsequent run. Those changes are surfaced separately via\n // `unactionableChanges` so callers can prompt for manual remediation.\n return statements.filter((statement) => {\n const trimmed = statement.trim();\n if (trimmed.length === 0) return false;\n return !isCommentOnlySql(trimmed);\n });\n}\n\n/**\n * True if `sql` contains no executable DDL — only line comments (`--`)\n * and/or block comments (`/* … *​/`). Used to discard advisory comments\n * the differ emits when a column upgrade can't be performed in place\n * (e.g. SQLite type widening). Defensive: strips block comments first so\n * a future differ that emits `/* type_upgrade: no-op *​/` doesn't slip\n * through.\n */\nfunction isCommentOnlySql(sql: string): boolean {\n // Strip /* ... */ block comments (non-greedy, spans newlines).\n const withoutBlockComments = sql.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n const lines = withoutBlockComments\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n if (lines.length === 0) return sql.trim().length > 0;\n return lines.every((line) => line.startsWith('--'));\n}\n\n/**\n * Surface changes the differ produced but that the migrator cannot apply\n * automatically — either `type_mismatch` entries (the differ explicitly\n * gives up on these) or `type_upgrade` entries whose generated SQL is\n * advisory-comment-only (SQLite table-recreation cases, etc.). Callers\n * use this to distinguish \"schema is in sync\" from \"schema is drifted but\n * we can't fix it from here.\"\n */\nfunction collectUnactionableChanges(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n): SchemaChange[] {\n const unactionable: SchemaChange[] = [];\n for (const change of diff.changes) {\n if (change.type === 'type_mismatch') {\n unactionable.push(change);\n continue;\n }\n const statements = change.sqlStatements ?? (change.sql ? [change.sql] : []);\n if (\n statements.length > 0 &&\n statements.every((stmt) => isCommentOnlySql(stmt.trim()))\n ) {\n unactionable.push(change);\n }\n }\n return unactionable;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"orchestrate.js","names":[],"sources":["../../src/migrations/orchestrate.ts"],"sourcesContent":["/**\n * High-level \"make my SMRT app's schema match its database\" orchestration.\n *\n * Wraps `ObjectRegistry.getAllSchemasAsDefinitions()` → `generateSchemaDiff`\n * → `MigrationTracker.applyAll`. Apps that just want to run pending\n * schema changes call `migrateSmrtSchemas({ db, packageName, version })`\n * and don't have to assemble the migration definition by hand.\n *\n * The migration name is timestamped, so re-running on an up-to-date\n * database is a no-op (the diff is empty, no statements are generated,\n * `applied: false` is returned).\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ObjectRegistry } from '../registry.js';\nimport { detectEngine, getDDLStrategy } from '../schema/ddl/index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport type { MigrationResult, SchemaChange } from '../schema/types.js';\nimport {\n generateSchemaDiff,\n getSQLFromDiff,\n hasActionableChanges,\n} from './differ.js';\nimport {\n createMigrationDefinition,\n generateMigrationTimestamp,\n} from './generator.js';\nimport { MigrationTracker } from './tracker.js';\n\nexport interface MigrateSmrtSchemasOptions {\n db: DatabaseInterface;\n /** Stored on the migration record so the audit log shows which app applied it. */\n packageName: string;\n /**\n * Stored on the migration record. Typically the app's package.json\n * version. Optional because most apps don't have a runtime-readable\n * version handy and would hardcode a stale string if forced to supply\n * one — `'unknown'` is honest. Pass an explicit value when you can\n * (e.g. `version: process.env.APP_VERSION`).\n */\n version?: string;\n /** Override the description on the synthetic migration definition. */\n description?: string;\n /** Override the migration name (default: `<timestamp>_smrt_schema_sync`). */\n name?: string;\n /** Forwarded to MigrationTracker — defaults to true for pg-safe runs. */\n postgresSafe?: boolean;\n /** Forwarded to MigrationTracker — defaults to true. */\n reconcile?: boolean;\n /** Forwarded to MigrationTracker — defaults to true for `CREATE INDEX CONCURRENTLY`. */\n useConcurrentIndexes?: boolean;\n /**\n * Forwarded to MigrationTracker (Postgres lock timeout, milliseconds).\n * Lets apps that customize tracker timeouts directly preserve their\n * tuning when going through the orchestrator. Falls back to the\n * tracker's default (30000ms) when omitted.\n */\n lockTimeout?: number;\n /**\n * Forwarded to MigrationTracker (Postgres statement timeout, ms). Falls\n * back to the tracker's default (60000ms) when omitted.\n */\n statementTimeout?: number;\n /**\n * Explicit engine hint forwarded to `detectEngine`, `SchemaComparer`, and\n * `MigrationTracker`. Useful when `db.url` doesn't unambiguously identify\n * the engine (e.g. the JSON adapter, which uses DuckDB internally and may\n * not be recognizable by URL alone). All three layers honor the same hint\n * so the generated DDL, drift comparison, and execution path stay\n * consistent — without it, an empty-URL connection could produce\n * Postgres-flavored DDL but run through the SQLite tracker path.\n *\n * Typed as `DatabaseEngine` so unsupported values (e.g. `'mysql'`) fail at\n * compile time rather than silently falling back to `'sqlite'` at runtime.\n */\n engineHint?: DatabaseEngine;\n /**\n * Confirm that historical PostgreSQL Date writers used UTC wall times before\n * reinterpreting legacy TIMESTAMP/TEXT/JSON columns as TIMESTAMPTZ.\n */\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n}\n\nexport interface MigrateSmrtSchemasResult {\n applied: boolean;\n results: MigrationResult[];\n statements: string[];\n schemaCount: number;\n /**\n * Schema changes the orchestrator detected but cannot apply automatically\n * (incompatible type mismatches, SQLite type upgrades that require table\n * recreation, etc.). Empty when the schema is fully reconciled. Inspect\n * this to surface a \"manual migration required\" warning in CLI output.\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Lets a caller write\n * `if (result.hasManualDrift) ...` without iterating the array first,\n * and makes the case visible to anyone scanning the result shape (the\n * full array is easy to gloss over when `applied: false`).\n */\n hasManualDrift: boolean;\n}\n\nexport interface PendingSchemaStatementsResult {\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>;\n statements: string[];\n schemaCount: number;\n hasChanges: boolean;\n /**\n * Schema changes the differ detected but that have no executable SQL\n * (incompatible `type_mismatch` entries, or `type_upgrade` entries whose\n * only generated SQL is an advisory comment because the engine cannot\n * upgrade the column in place — e.g. SQLite type widening). Surfacing\n * these lets status commands distinguish \"no drift\" from \"drift the\n * orchestrator can't fix on its own.\"\n */\n unactionableChanges: SchemaChange[];\n /**\n * Convenience: `unactionableChanges.length > 0`. Mirrors the\n * `MigrateSmrtSchemasResult.hasManualDrift` shortcut.\n */\n hasManualDrift: boolean;\n}\n\n/**\n * Compute the SQL statements needed to bring `db` in sync with the\n * currently-registered SMRT object schemas. Pure inspection — no writes.\n *\n * Useful for status commands (\"how far behind is the schema?\") and as the\n * inner step of `migrateSmrtSchemas`.\n *\n * **Note on the returned `statements`:** these are the engine-correct DDL\n * statements as the differ + DDL strategy produce them, *prior to* any\n * Postgres-specific rewrites the `MigrationTracker` applies at execution\n * time (e.g. rewriting `CREATE INDEX` → `CREATE INDEX CONCURRENTLY` and\n * moving CONCURRENTLY statements outside the surrounding transaction).\n * The list is suitable for preview/status/\"what would change?\" use cases.\n * For the exact byte-for-byte SQL the tracker ran, inspect the\n * `MigrationResult`s from `migrateSmrtSchemas` or the tracker's own logs.\n */\nexport async function getPendingSchemaStatements(\n db: DatabaseInterface,\n options: {\n engineHint?: DatabaseEngine;\n postgresTimestampMigration?: { legacyTimezone: 'UTC' };\n } = {},\n): Promise<PendingSchemaStatementsResult> {\n const schemas = ObjectRegistry.getAllSchemasAsDefinitions();\n // Forward engineHint into the diff itself so the SchemaComparer's\n // existing-table SQL (ALTER/index drift) uses the same DDL strategy\n // we use for newly-added tables — otherwise the two halves of the\n // statement list can land on different engines (split-brain) when\n // `db.url` is empty or ambiguous.\n const diff = await generateSchemaDiff(db, schemas, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n });\n const statements = collectStatementsFromDiff(diff, db, options.engineHint);\n const unactionableChanges = collectUnactionableChanges(diff);\n return {\n diff,\n schemaCount: Object.keys(schemas).length,\n statements,\n hasChanges: hasActionableChanges(diff),\n unactionableChanges,\n hasManualDrift: unactionableChanges.length > 0,\n };\n}\n\n/**\n * Apply any pending schema changes from `ObjectRegistry` to `db`.\n *\n * Returns `applied: false` (with no error) if the database is already in\n * sync. Throws on the first migration failure with the underlying error\n * preserved (callers should let it propagate to the CLI).\n *\n * **Note on the returned `statements`:** mirrors `getPendingSchemaStatements`\n * — the list is the planned DDL, prior to Postgres-specific tracker rewrites\n * (CONCURRENTLY, transaction reordering). The actual executed SQL is\n * tracked in the returned `results`.\n *\n * **Note on `applied` semantics under `reconcile: true` (the default):**\n * the tracker may re-execute a migration that is already in `completed`\n * state when the checksum matches — that counts as `applied: true` in the\n * returned `MigrationResult`. So `result.applied === true` means \"the\n * tracker ran the migration\", not specifically \"the database changed for\n * the first time.\" Pass `reconcile: false` if you need to distinguish a\n * fresh apply from a reconcile-replay over an existing completed record.\n *\n * **Note on repeated failures:** the migration name defaults to a fresh\n * timestamp on every call, so a sequence of failed runs leaves one\n * `failed` row in `_smrt_schema_migrations` per attempt. Pass an explicit\n * `name` if you want to overwrite the same record across retries.\n *\n * **Note on `unactionableChanges`:** the differ can detect schema drift\n * the migration step can't auto-resolve — incompatible type mismatches,\n * SQLite type widening that needs table recreation, etc. Those changes\n * appear in `result.unactionableChanges` and are NOT applied (the\n * statements list is filtered to executable DDL only). When the diff\n * contains *only* unactionable changes, the function returns\n * `applied: false` with a populated `unactionableChanges` so callers can\n * surface a \"manual migration required\" signal. Without inspecting that\n * field, the result would look indistinguishable from \"already in sync.\"\n */\nexport async function migrateSmrtSchemas(\n options: MigrateSmrtSchemasOptions,\n): Promise<MigrateSmrtSchemasResult> {\n const pending = await getPendingSchemaStatements(options.db, {\n engineHint: options.engineHint,\n postgresTimestampMigration: options.postgresTimestampMigration,\n });\n if (!pending.hasChanges || pending.statements.length === 0) {\n return {\n applied: false,\n results: [],\n statements: [],\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n }\n\n const tracker = new MigrationTracker({\n db: options.db,\n engineHint: options.engineHint,\n lockTimeout: options.lockTimeout,\n statementTimeout: options.statementTimeout,\n useConcurrentIndexes: options.useConcurrentIndexes ?? true,\n });\n const migration = createMigrationDefinition(\n options.name ?? `${generateMigrationTimestamp()}_smrt_schema_sync`,\n pending.statements,\n [],\n {\n description: options.description ?? 'Synchronize SMRT object schemas',\n packageName: options.packageName,\n version: options.version ?? 'unknown',\n },\n );\n\n const results = await tracker.applyAll([migration], {\n postgresSafe: options.postgresSafe ?? true,\n reconcile: options.reconcile ?? true,\n });\n\n const failed = results.find((result) => !result.success);\n if (failed) {\n throw failed.error instanceof Error\n ? failed.error\n : new Error(String(failed.error ?? `Migration ${failed.name} failed`));\n }\n\n const applied = results.some(\n (result) => result.applied !== false && result.skipped !== true,\n );\n\n return {\n applied,\n results,\n statements: pending.statements,\n schemaCount: pending.schemaCount,\n unactionableChanges: pending.unactionableChanges,\n hasManualDrift: pending.hasManualDrift,\n };\n}\n\n/** Mirror of the `getDatabaseUrl` helper in class.ts — some adapters\n * (JSON, certain in-memory wrappers) leave `db.url` undefined and expose\n * the URL on `db.config?.url` instead. Detecting the engine from a raw\n * `db.url` would throw or silently fall through to the sqlite default.\n */\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n return db.url || dbWithConfig.config?.url || '';\n}\n\n/**\n * Materialize the diff into the engine-correct SQL statements.\n *\n * For new tables we delegate to the engine's `DDLStrategy` so column types\n * (`REAL` → `DOUBLE PRECISION` on Postgres, `JSON` → `JSONB`, etc.),\n * partial-index `WHERE` clauses, and trigger syntax all match what the\n * SchemaComparer would expect on subsequent runs. Emitting the raw\n * abstract `SchemaDefinition.ddl` here would cause immediate type-drift\n * migrations on Postgres/DuckDB.\n *\n * Column/index/trigger changes for existing tables continue to come from\n * `getSQLFromDiff`, which is already engine-aware via the differ's per-\n * change `sqlStatements`.\n */\nfunction collectStatementsFromDiff(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n db: DatabaseInterface,\n engineHint?: DatabaseEngine,\n): string[] {\n const strategy = getDDLStrategy(\n detectEngine(resolveDatabaseUrl(db), engineHint),\n );\n const statements: string[] = [];\n for (const schema of diff.added_tables) {\n statements.push(strategy.generateCreateTable(schema));\n statements.push(...strategy.generateIndexes(schema));\n statements.push(...strategy.generateTriggers(schema));\n }\n statements.push(...getSQLFromDiff(diff));\n // Drop empty and comment-only entries. SQLite type-widening upgrades\n // surface as `-- SQLite: Type upgrade for X requires table recreation`\n // — passing those through to the tracker records a successful migration\n // without actually fixing the column, so the same drift re-appears on\n // every subsequent run. Those changes are surfaced separately via\n // `unactionableChanges` so callers can prompt for manual remediation.\n return statements.filter((statement) => {\n const trimmed = statement.trim();\n if (trimmed.length === 0) return false;\n return !isCommentOnlySql(trimmed);\n });\n}\n\n/**\n * True if `sql` contains no executable DDL — only line comments (`--`)\n * and/or block comments (`/* … *​/`). Used to discard advisory comments\n * the differ emits when a column upgrade can't be performed in place\n * (e.g. SQLite type widening). Defensive: strips block comments first so\n * a future differ that emits `/* type_upgrade: no-op *​/` doesn't slip\n * through.\n */\nfunction isCommentOnlySql(sql: string): boolean {\n // Strip /* ... */ block comments (non-greedy, spans newlines).\n const withoutBlockComments = sql.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n const lines = withoutBlockComments\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n if (lines.length === 0) return sql.trim().length > 0;\n return lines.every((line) => line.startsWith('--'));\n}\n\n/**\n * Surface changes the differ produced but that the migrator cannot apply\n * automatically — either `type_mismatch` entries (the differ explicitly\n * gives up on these) or `type_upgrade` entries whose generated SQL is\n * advisory-comment-only (SQLite table-recreation cases, etc.). Callers\n * use this to distinguish \"schema is in sync\" from \"schema is drifted but\n * we can't fix it from here.\"\n */\nfunction collectUnactionableChanges(\n diff: Awaited<ReturnType<typeof generateSchemaDiff>>,\n): SchemaChange[] {\n const unactionable: SchemaChange[] = [];\n for (const change of diff.changes) {\n if (change.type === 'type_mismatch') {\n unactionable.push(change);\n continue;\n }\n const statements = change.sqlStatements ?? (change.sql ? [change.sql] : []);\n if (\n statements.length > 0 &&\n statements.every((stmt) => isCommentOnlySql(stmt.trim()))\n ) {\n unactionable.push(change);\n }\n }\n return unactionable;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/tracker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EACV,sBAAsB,EAEtB,uBAAuB,EACvB,eAAe,EAChB,MAAM,YAAY,CAAC;AAkEpB;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,OAAO,CAA+D;IAC9E,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAiB;IACjC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,YAAY,CAAuB;gBAE/B,OAAO,EAAE,uBAAuB;IAa5C;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAQtB;;OAEG;IACH,SAAS,IAAI,cAAc;IAI3B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAUnB,gBAAgB;IAW9B;;OAEG;IACG,oBAAoB,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAe9D;;OAEG;IACG,oBAAoB,CACxB,WAAW,EAAE,mBAAmB,EAAE,GACjC,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAMjC;;OAEG;IACG,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWtD;;OAEG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAmBvE;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAWrC;;OAEG;IACG,WAAW,CACf,WAAW,EAAE,mBAAmB,EAAE,GACjC,OAAO,CAAC,WAAW,EAAE,CAAC;IAkDzB;;OAEG;IACG,KAAK,CACT,UAAU,EAAE,mBAAmB,EAC/B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,eAAe,CAAC;IA6L3B;;OAEG;IACG,QAAQ,CACZ,WAAW,EAAE,mBAAmB,EAAE,EAClC,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,eAAe,EAAE,CAAC;IAsF7B;;OAEG;IACG,QAAQ,CACZ,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,mBAAmB,EAC/B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC;IAiF3B;;OAEG;IACG,UAAU,CACd,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,eAAe,CAAA;KAAO,GACvE,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAiCnC;;OAEG;YACW,iBAAiB;IAW/B;;OAEG;YACW,sBAAsB;IAepC;;;;;;;;;;;OAWG;YACW,yBAAyB;CAqCxC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAAE,EACpB,oBAAoB,EAAE,OAAO,GAC5B;IAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAgC7C"}
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/tracker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,KAAK,EACV,sBAAsB,EAEtB,uBAAuB,EACvB,eAAe,EAChB,MAAM,YAAY,CAAC;AAkEpB;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,OAAO,CAA+D;IAC9E,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAiB;IACjC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,YAAY,CAAuB;gBAE/B,OAAO,EAAE,uBAAuB;IAa5C;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAQtB;;OAEG;IACH,SAAS,IAAI,cAAc;IAI3B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAUnB,gBAAgB;IAiB9B;;OAEG;IACG,oBAAoB,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAe9D;;OAEG;IACG,oBAAoB,CACxB,WAAW,EAAE,mBAAmB,EAAE,GACjC,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAMjC;;OAEG;IACG,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWtD;;OAEG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAmBvE;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAWrC;;OAEG;IACG,WAAW,CACf,WAAW,EAAE,mBAAmB,EAAE,GACjC,OAAO,CAAC,WAAW,EAAE,CAAC;IAkDzB;;OAEG;IACG,KAAK,CACT,UAAU,EAAE,mBAAmB,EAC/B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,eAAe,CAAC;IA6L3B;;OAEG;IACG,QAAQ,CACZ,WAAW,EAAE,mBAAmB,EAAE,EAClC,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,eAAe,EAAE,CAAC;IAsF7B;;OAEG;IACG,QAAQ,CACZ,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,mBAAmB,EAC/B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC;IAiF3B;;OAEG;IACG,UAAU,CACd,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,eAAe,CAAA;KAAO,GACvE,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAiCnC;;OAEG;YACW,iBAAiB;IAW/B;;OAEG;YACW,sBAAsB;IAepC;;;;;;;;;;;OAWG;YACW,yBAAyB;CAqCxC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAAE,EACpB,oBAAoB,EAAE,OAAO,GAC5B;IAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAgC7C"}
@@ -1,5 +1,6 @@
1
1
  import { detectEngine } from "../schema/ddl/index.js";
2
- import { CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE } from "../system/schema.js";
2
+ import { CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, getSystemTableDDLForEngine } from "../system/schema.js";
3
+ import { assertPostgresSystemTimestampsCurrent } from "../system/compatibility.js";
3
4
  import { computeChecksum, verifyChecksum } from "./checksum.js";
4
5
  import { createLogger } from "@happyvertical/logger";
5
6
  import { randomUUID } from "node:crypto";
@@ -103,8 +104,9 @@ var MigrationTracker = class MigrationTracker {
103
104
  return this.initializePromise;
104
105
  }
105
106
  async runInitializeDdl() {
106
- const statements = CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
107
+ const statements = getSystemTableDDLForEngine(CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, this.dbEngine).split(";").map((s) => s.trim()).filter((s) => s.length > 0);
107
108
  for (const statement of statements) await this.db.query(statement);
109
+ await assertPostgresSystemTimestampsCurrent(this.db, this.engineHint);
108
110
  }
109
111
  /**
110
112
  * Get all applied migrations ordered by applied_at
@@ -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 { 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 { CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE } 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\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 * 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 constructor(options: MigrationTrackerOptions) {\n this.db = options.db;\n this.options = {\n lockTimeout: options.lockTimeout ?? DEFAULT_OPTIONS.lockTimeout,\n statementTimeout:\n options.statementTimeout ?? DEFAULT_OPTIONS.statementTimeout,\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 = CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE.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\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 try {\n // Execute the migration\n await this.executeStatements(definition.up, options);\n\n // Mark as completed\n const executionTime = Date.now() - startTime;\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 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 if (this.dbEngine === 'postgres') {\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.`,\n );\n }\n }\n\n try {\n return 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 const txTracker = new MigrationTracker({\n db: txDb,\n lockTimeout: this.options.lockTimeout,\n statementTimeout: this.options.statementTimeout,\n useConcurrentIndexes: false,\n });\n const results = await txTracker.applyAll(definitions, {\n ...options,\n atomic: false,\n continueOnError: false,\n postgresSafe: false,\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 } 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 * 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 = '${this.options.lockTimeout}ms'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${this.options.statementTimeout}ms'`,\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 // Execute CONCURRENTLY statements outside transaction\n for (const sql of finalConcurrent) {\n // Set lock timeout for the session\n await this.db.query(`SET lock_timeout = '${this.options.lockTimeout}ms'`);\n await this.db.query(sql);\n }\n }\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 { 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\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 * 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 constructor(options: MigrationTrackerOptions) {\n this.db = options.db;\n this.options = {\n lockTimeout: options.lockTimeout ?? DEFAULT_OPTIONS.lockTimeout,\n statementTimeout:\n options.statementTimeout ?? DEFAULT_OPTIONS.statementTimeout,\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 try {\n // Execute the migration\n await this.executeStatements(definition.up, options);\n\n // Mark as completed\n const executionTime = Date.now() - startTime;\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 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 if (this.dbEngine === 'postgres') {\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.`,\n );\n }\n }\n\n try {\n return 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 const txTracker = new MigrationTracker({\n db: txDb,\n lockTimeout: this.options.lockTimeout,\n statementTimeout: this.options.statementTimeout,\n useConcurrentIndexes: false,\n });\n const results = await txTracker.applyAll(definitions, {\n ...options,\n atomic: false,\n continueOnError: false,\n postgresSafe: false,\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 } 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 * 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 = '${this.options.lockTimeout}ms'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${this.options.statementTimeout}ms'`,\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 // Execute CONCURRENTLY statements outside transaction\n for (const sql of finalConcurrent) {\n // Set lock timeout for the session\n await this.db.query(`SET lock_timeout = '${this.options.lockTimeout}ms'`);\n await this.db.query(sql);\n }\n }\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,4 +1,5 @@
1
1
  import { FieldDefinition } from '../scanner/types.js';
2
+ import { DatabaseEngine } from '../schema/ddl/types.js';
2
3
  import { ColumnDefinition, SchemaDefinition, SQLDataType } from '../schema/types.js';
3
4
  /**
4
5
  * Get cached schema definition for a registered class
@@ -17,14 +18,20 @@ export declare function getSchema(name: string): SchemaDefinition | undefined;
17
18
  * Get SQL DDL statement for a registered class
18
19
  *
19
20
  * @param name - Name of the registered class
20
- * @returns SQL DDL statement or undefined if not found
21
+ * Cached manifest DDL contains abstract types and is not safe to execute on
22
+ * PostgreSQL. New executable paths must pass a target engine. Omitting it is
23
+ * retained for backward compatibility with consumers that only inspect the
24
+ * legacy engine-neutral manifest string.
25
+ *
26
+ * @param engine - Database engine that will execute the DDL
27
+ * @returns Engine-specific SQL DDL, or the legacy cached DDL when omitted
21
28
  * @example
22
29
  * ```typescript
23
- * const ddl = ObjectRegistry.getSchemaDDL('Product');
30
+ * const ddl = ObjectRegistry.getSchemaDDL('Product', 'postgres');
24
31
  * await db.query(ddl);
25
32
  * ```
26
33
  */
27
- export declare function getSchemaDDL(name: string): string | undefined;
34
+ export declare function getSchemaDDL(name: string, engine?: DatabaseEngine): string | undefined;
28
35
  /**
29
36
  * Get table name for a registered class
30
37
  *
@@ -48,11 +55,11 @@ export declare function getTableName(name: string): string | undefined;
48
55
  * @returns Record of table names to schema definitions
49
56
  * @example
50
57
  * ```typescript
51
- * const schemas = ObjectRegistry.getAllSchemas();
58
+ * const schemas = ObjectRegistry.getAllSchemas('json');
52
59
  * const db = await getDatabase({ type: 'json', url: './data', schemas });
53
60
  * ```
54
61
  */
55
- export declare function getAllSchemas(): Record<string, {
62
+ export declare function getAllSchemas(engine?: DatabaseEngine): Record<string, {
56
63
  tableName: string;
57
64
  ddl: string;
58
65
  indexes?: string[];
@@ -79,7 +86,7 @@ export declare function getAllSchemasAsDefinitions(): Record<string, SchemaDefin
79
86
  * @param columns - Column definitions
80
87
  * @returns DDL CREATE TABLE statement
81
88
  */
82
- export declare function generateDDLFromColumns(tableName: string, columns: Record<string, ColumnDefinition>, isSTI?: boolean): string;
89
+ export declare function generateDDLFromColumns(tableName: string, columns: Record<string, ColumnDefinition>, isSTI?: boolean, engine?: DatabaseEngine): string;
83
90
  /**
84
91
  * Format default value for SQL DDL.
85
92
  *
@@ -1 +1 @@
1
- {"version":3,"file":"schema-builder.d.ts","sourceRoot":"","sources":["../../src/registry/schema-builder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAK3D,OAAO,KAAK,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AA6I5B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAIpE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAkB7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,IAAI,MAAM,CACrC,MAAM,EACN;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CACvD,CAwMA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAgK7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,KAAK,UAAQ,GACZ,MAAM,CAqDR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAgFlC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,CAAC,MAAM,CAAC,GACjC,WAAW,CAqBb"}
1
+ {"version":3,"file":"schema-builder.d.ts","sourceRoot":"","sources":["../../src/registry/schema-builder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAK7D,OAAO,KAAK,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AA6I5B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAIpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAkB7D;AAyCD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA+OxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CA2K7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,KAAK,UAAQ,EACb,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CAwDR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAgFlC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,CAAC,MAAM,CAAC,GACjC,WAAW,CAqBb"}