@happyvertical/smrt-core 0.40.69 → 0.40.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +29 -4
- package/README.md +20 -1
- package/agents/query-bounds.md +45 -0
- package/agents/schema-paths.md +328 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -1
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +28 -4
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts +101 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +224 -43
- package/dist/collection.js.map +1 -1
- package/dist/database.d.ts +8 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +16 -8
- package/dist/database.js.map +1 -1
- package/dist/db-errors.d.ts +105 -0
- package/dist/db-errors.d.ts.map +1 -0
- package/dist/db-errors.js +382 -0
- package/dist/db-errors.js.map +1 -0
- package/dist/decorators/index.d.ts +17 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +65 -7
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +4 -3
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/errors.d.ts +30 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -2
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +6 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +38 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +16 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +41 -3
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +22 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +34 -3
- package/dist/generators/rest.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +202 -8
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +604 -46
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts +31 -4
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +49 -5
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +5 -3
- package/dist/migrations/orchestrate.js +35 -6
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/sqlite-rebuild.d.ts +142 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
- package/dist/migrations/sqlite-rebuild.js +514 -0
- package/dist/migrations/sqlite-rebuild.js.map +1 -0
- package/dist/migrations/tracker.d.ts +114 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +331 -16
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +19 -4
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations.js +5 -3
- package/dist/object.d.ts +45 -6
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +60 -13
- package/dist/object.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +240 -0
- package/dist/postgres-timeouts.d.ts.map +1 -0
- package/dist/postgres-timeouts.js +204 -0
- package/dist/postgres-timeouts.js.map +1 -0
- package/dist/query-bounds.d.ts +97 -0
- package/dist/query-bounds.d.ts.map +1 -0
- package/dist/query-bounds.js +173 -0
- package/dist/query-bounds.js.map +1 -0
- package/dist/registry/types.d.ts +32 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/scanner/types.d.ts +5 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -2
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +70 -5
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +124 -6
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/live-parity.d.ts +90 -0
- package/dist/schema/live-parity.d.ts.map +1 -0
- package/dist/schema/live-parity.js +600 -0
- package/dist/schema/live-parity.js.map +1 -0
- package/dist/schema/manifest-schema.d.ts +121 -0
- package/dist/schema/manifest-schema.d.ts.map +1 -0
- package/dist/schema/manifest-schema.js +267 -0
- package/dist/schema/manifest-schema.js.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +24 -10
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +35 -90
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +52 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -0
- package/dist/schema/system-table-shapes.js +174 -0
- package/dist/schema/system-table-shapes.js.map +1 -0
- package/dist/schema/types.d.ts +103 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +1 -0
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +3 -1
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +18 -6
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js +9 -16
- package/dist/sync/apply.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +1 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +71 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +6 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"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
|
+
{"version":3,"file":"tracker.js","names":[],"sources":["../../src/migrations/tracker.ts"],"sourcesContent":["/**\n * Migration Tracker\n *\n * Manages migration state, tracking which migrations have been applied,\n * and providing idempotent migration application.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { hostname } from 'node:os';\nimport { createLogger } from '@happyvertical/logger';\nimport { parsePostgresTimeoutMs } from '../postgres-timeouts.js';\nimport { detectEngine } from '../schema/ddl/index.js';\n// DatabaseEngine comes from the DDL layer (the json-inclusive type that\n// `detectEngine` actually returns), matching `SchemaComparer` in differ.ts.\n// The migration stack supports the JSON adapter post-R11, so the tracker's\n// engine type must include 'json' too — the narrower `migrations/types`\n// alias would reject it.\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport type {\n DriftReport,\n MigrationDefinition,\n MigrationResult,\n MigrationStatus,\n SchemaMigrationRecord,\n} from '../schema/types.js';\nimport { assertPostgresSystemTimestampsCurrent } from '../system/compatibility.js';\nimport {\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\nimport { computeChecksum, verifyChecksum } from './checksum.js';\nimport type {\n ApplyMigrationsOptions,\n DatabaseInterface,\n MigrationTrackerOptions,\n RollbackOptions,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Row type for _smrt_schema_migrations table\n */\ninterface MigrationRow {\n id: string;\n name: string;\n version: string;\n checksum: string;\n applied_checksum?: string | null;\n applied_at: string;\n execution_time_ms?: number | null;\n package_name?: string | null;\n source_file?: string | null;\n status: string;\n error_message?: string | null;\n attempts: number;\n is_reversible: number | boolean;\n rolled_back_at?: string | null;\n applied_by?: string | null;\n batch: number;\n}\n\n/**\n * Default options for MigrationTracker\n */\nconst DEFAULT_OPTIONS = {\n lockTimeout: 30000,\n statementTimeout: 60000,\n useConcurrentIndexes: true,\n};\n\nclass AtomicMigrationRollback extends Error {\n constructor(\n public readonly results: MigrationResult[],\n public readonly failed: MigrationResult,\n ) {\n super(`Atomic migration batch failed at ${failed.name}`);\n }\n}\n\nconst CONCURRENT_INDEX_STATEMENT_RE =\n /(?:(?:CREATE\\s+(?:UNIQUE\\s+)?INDEX|DROP\\s+INDEX)\\s+CONCURRENTLY|REINDEX(?:\\s*\\([^)]*\\))?\\s+(?:INDEX|TABLE|SCHEMA|DATABASE|SYSTEM)\\s+CONCURRENTLY)/i;\n\n/**\n * Matches the leading clause of a `CREATE [UNIQUE] INDEX` statement and\n * captures the index name, whether it is written bare or double-quoted, and\n * regardless of the optional `CONCURRENTLY` / `IF NOT EXISTS` clauses.\n *\n * Used by the PostgreSQL concurrent-index mode to correlate a pending create\n * with an INVALID index left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` (issue #2362).\n */\nconst CREATE_INDEX_NAME_RE =\n /^\\s*CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:\"((?:[^\"]|\"\")+)\"|([A-Za-z_][\\w$]*))/i;\n\nfunction findConcurrentIndexStatement(\n definitions: MigrationDefinition[],\n): { migrationName: string; statement: string } | null {\n for (const definition of definitions) {\n const statement = definition.up.find((sql) =>\n CONCURRENT_INDEX_STATEMENT_RE.test(sql),\n );\n\n if (statement) {\n return {\n migrationName: definition.id,\n statement,\n };\n }\n }\n\n return null;\n}\n\n/**\n * Extract the index name from a `CREATE [UNIQUE] INDEX …` statement.\n *\n * Returns the unquoted identifier, or `null` when the statement is not a\n * CREATE INDEX (a drop, a reindex, or unrelated DDL).\n *\n * Exported for unit testing and because the CLI's index rollout needs the\n * same correlation to explain what it repaired.\n */\nexport function extractCreatedIndexName(sql: string): string | null {\n const match = CREATE_INDEX_NAME_RE.exec(sql);\n if (!match) {\n return null;\n }\n\n // Group 1 is the quoted form (with `\"\"` escapes), group 2 the bare form.\n return match[1] !== undefined ? match[1].replace(/\"\"/g, '\"') : match[2];\n}\n\n/**\n * Coerce a caller-supplied timeout to a usable millisecond count.\n *\n * Applied once, in the constructor, so every later read of\n * `this.options.lockTimeout` / `.statementTimeout` is already finite and\n * non-negative. Each timeout falls back to *its own* default — a bad\n * `lockTimeout` must not silently inherit the (much larger) statement-timeout\n * default and weaken the lock bound it was meant to impose.\n */\nfunction resolveTimeoutOption(\n value: number | undefined,\n fallback: number,\n): number {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0\n ? Math.trunc(value)\n : fallback;\n}\n\n/**\n * Render a millisecond timeout as a PostgreSQL interval literal.\n *\n * PostgreSQL reads `lock_timeout`/`statement_timeout` as milliseconds when\n * given a bare number, but the explicit `ms` unit keeps the emitted SQL\n * self-documenting in migration logs. `0` disables the timeout, which is\n * PostgreSQL's own semantic and therefore preserved verbatim.\n *\n * Input is already normalized by {@link resolveTimeoutOption}.\n */\nfunction formatPostgresTimeout(milliseconds: number): string {\n return `${Math.max(0, Math.trunc(milliseconds))}ms`;\n}\n\n/**\n * Parse a PostgreSQL timeout string into milliseconds.\n *\n * The implementation lives in `src/postgres-timeouts.ts`, the leaf module the\n * runtime pool bounds are built from (#2377), so `migrations.postgres.*` and a\n * runtime `timeouts` config are parsed by literally the same function rather\n * than by two copies that can drift. It is re-exported here — and from\n * `./index.ts` — because `@happyvertical/smrt-core/migrations` is the public\n * subpath `db:migrate` reads it through.\n */\nexport { parsePostgresTimeoutMs };\n\n/**\n * Prefix stamped on the `error_message` of every concurrent-index migration\n * whose transactional half has already committed.\n *\n * This is the resume marker: it is the only durable record that phase 1's\n * statements ran, so a retry can skip straight to the index build. It must\n * therefore be carried by *both* the interim write at the end of phase 1 and\n * every failure recorded during phase 2 — a lock timeout on the index build\n * leaves the non-index DDL committed just as surely as a crash does.\n */\nconst CONCURRENT_INDEX_PHASE1_MARKER =\n '[smrt: concurrent-index phase 1 committed]';\n\n/**\n * Interim `error_message` for a migration whose transactional half has\n * committed but whose `CONCURRENTLY` index build has not run yet.\n */\nconst CONCURRENT_INDEX_PENDING_MESSAGE = `${CONCURRENT_INDEX_PHASE1_MARKER} Concurrent index build pending: the non-index half of this migration committed but the CONCURRENTLY index DDL had not completed. Re-run db:migrate to finish it.`;\n\n/**\n * Statements split out of the atomic batch for PostgreSQL concurrent-index\n * mode, keyed by migration definition id.\n */\ntype ConcurrentIndexPlan = Map<\n string,\n { regular: string[]; concurrent: string[] }\n>;\n\n/**\n * Minimal pinned-session shape used by the concurrent-index phase.\n *\n * `CREATE INDEX CONCURRENTLY` must run outside a transaction, and the\n * `SET lock_timeout` / `SET statement_timeout` that bound it are *session*\n * state — on a pooled adapter a top-level `db.query` may land on a different\n * connection, so the SET would silently not apply to the DDL (issue #2362).\n */\ninterface MigrationSession {\n query: (\n sql: string,\n ...vars: unknown[]\n ) => Promise<{ rows: Record<string, unknown>[]; rowCount?: number }>;\n release: () => Promise<void>;\n}\n\n/**\n * MigrationTracker class\n *\n * Provides migration tracking and state management:\n * - Initializes the _smrt_schema_migrations tracking table\n * - Tracks applied migrations with checksums\n * - Provides idempotent migration application\n * - Supports rollback via DOWN scripts\n * - Handles PostgreSQL-specific concerns (CONCURRENTLY, lock timeout)\n */\nexport class MigrationTracker {\n private db: DatabaseInterface;\n private options: Required<Omit<MigrationTrackerOptions, 'db' | 'engineHint'>>;\n private engineHint?: string;\n private dbEngine: DatabaseEngine;\n /**\n * Memoized initialization promise. Storing the in-flight promise (rather\n * than a `boolean` flag set after the DDL completes) makes `initialize()`\n * safe under concurrency: multiple parallel callers all `await` the same\n * promise instead of independently re-running the DDL. On error the slot\n * is cleared so the next caller retries — a transient failure doesn't\n * permanently poison the instance.\n */\n private initializePromise: Promise<void> | null = null;\n private currentBatch: number | null = null;\n /**\n * Statements deferred out of the current atomic batch by PostgreSQL\n * concurrent-index mode. The parent tracker sets this on the\n * transaction-scoped tracker before phase 1 so `apply()` executes only the\n * transaction-safe statements and leaves the migration record in `running`;\n * the parent finalizes those records after the transaction commits and the\n * `CONCURRENTLY` DDL has run. `null` on every ordinary path.\n */\n private concurrentIndexPlan: ConcurrentIndexPlan | null = null;\n\n constructor(options: MigrationTrackerOptions) {\n this.db = options.db;\n this.options = {\n lockTimeout: resolveTimeoutOption(\n options.lockTimeout,\n DEFAULT_OPTIONS.lockTimeout,\n ),\n statementTimeout: resolveTimeoutOption(\n options.statementTimeout,\n DEFAULT_OPTIONS.statementTimeout,\n ),\n useConcurrentIndexes:\n options.useConcurrentIndexes ?? DEFAULT_OPTIONS.useConcurrentIndexes,\n };\n this.engineHint = options.engineHint;\n this.dbEngine = this.detectDbEngine();\n }\n\n /**\n * Detect the database engine. Honors an explicit `engineHint` (passed via\n * `MigrationTrackerOptions`) before falling back to URL parsing — this is\n * the same `detectEngine` contract used by `SchemaComparer` and the rest\n * of the migration stack, so the tracker stays consistent with the DDL\n * the orchestrator generated.\n */\n private detectDbEngine(): DatabaseEngine {\n const dbWithConfig = this.db as DatabaseInterface & {\n config?: { url?: string };\n };\n const url = this.db.url || dbWithConfig.config?.url || '';\n return detectEngine(url, this.engineHint);\n }\n\n /**\n * Get the current database engine\n */\n getEngine(): DatabaseEngine {\n return this.dbEngine;\n }\n\n /**\n * Initialize the migrations tracking table\n */\n async initialize(): Promise<void> {\n if (!this.initializePromise) {\n this.initializePromise = this.runInitializeDdl().catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n private async runInitializeDdl(): Promise<void> {\n // Parse and execute each statement in the DDL\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n this.dbEngine,\n )\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n\n for (const statement of statements) {\n await this.db.query(statement);\n }\n\n await assertPostgresSystemTimestampsCurrent(this.db, this.engineHint);\n }\n\n /**\n * Get all applied migrations ordered by applied_at\n */\n async getAppliedMigrations(): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE status = 'completed' ORDER BY applied_at ASC`,\n );\n\n return (result.rows as MigrationRow[]).map((row) => ({\n ...row,\n applied_at: new Date(row.applied_at),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n is_reversible: Boolean(row.is_reversible),\n })) as SchemaMigrationRecord[];\n }\n\n /**\n * Get pending migrations (defined but not applied)\n */\n async getPendingMigrations(\n definitions: MigrationDefinition[],\n ): Promise<MigrationDefinition[]> {\n const applied = await this.getAppliedMigrations();\n const appliedNames = new Set(applied.map((m) => m.name));\n return definitions.filter((d) => !appliedNames.has(d.id));\n }\n\n /**\n * Check if a migration has been applied\n */\n async isApplied(migrationId: string): Promise<boolean> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT 1 FROM _smrt_schema_migrations WHERE name = ? AND status = 'completed' LIMIT 1`,\n migrationId,\n );\n\n return result.rows.length > 0;\n }\n\n /**\n * Get a single migration record by name\n */\n async getMigration(name: string): Promise<SchemaMigrationRecord | null> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE name = ? LIMIT 1`,\n name,\n );\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as MigrationRow;\n return {\n ...row,\n applied_at: new Date(row.applied_at),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n is_reversible: Boolean(row.is_reversible),\n } as SchemaMigrationRecord;\n }\n\n /**\n * Get the next batch number\n */\n async getNextBatch(): Promise<number> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT MAX(batch) as max_batch FROM _smrt_schema_migrations`,\n );\n\n const row = result.rows[0] as { max_batch: number | null } | undefined;\n return (row?.max_batch ?? 0) + 1;\n }\n\n /**\n * Detect drift between migration definitions and applied migrations\n */\n async detectDrift(\n definitions: MigrationDefinition[],\n ): Promise<DriftReport[]> {\n const applied = await this.getAppliedMigrations();\n const reports: DriftReport[] = [];\n\n const appliedMap = new Map(applied.map((m) => [m.name, m]));\n const definitionMap = new Map(definitions.map((d) => [d.id, d]));\n\n // Check each applied migration against definitions\n for (const record of applied) {\n const definition = definitionMap.get(record.name);\n\n if (!definition) {\n // Migration applied but definition is missing\n reports.push({\n migration_name: record.name,\n expected_checksum: 'N/A',\n actual_checksum: record.checksum,\n drift_type: 'unknown_in_db',\n recommendation:\n 'Migration exists in database but definition is missing. Manual cleanup may be required.',\n });\n continue;\n }\n\n // Check checksum\n const currentChecksum = computeChecksum(definition.up);\n const verification = verifyChecksum(\n currentChecksum,\n record.checksum,\n record.applied_checksum,\n );\n\n if (!verification.valid) {\n const driftType = verification.drift ?? 'db_modified';\n reports.push({\n migration_name: record.name,\n expected_checksum: record.checksum,\n actual_checksum: currentChecksum,\n drift_type: driftType,\n recommendation:\n driftType === 'file_modified'\n ? 'Migration file was modified after application. Create a new migration instead.'\n : 'Database schema was modified outside of migrations. Review and reconcile manually.',\n });\n }\n }\n\n return reports;\n }\n\n /**\n * Apply a single migration\n */\n async apply(\n definition: MigrationDefinition,\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.up);\n const force = Boolean(\n options.force || options.forceMigrations?.includes(definition.id),\n );\n const hasScopedForce = Boolean(options.forceMigrations?.length);\n\n // Check if migration record already exists\n const existing = await this.getMigration(definition.id);\n\n if (existing) {\n // Handle based on existing status\n switch (existing.status) {\n case 'completed':\n if (!force && existing.checksum !== checksum) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} checksum mismatch. Was already applied with different checksum. Use --force-migration ${definition.id} to override only this migration, or --force to override the whole batch.`,\n };\n }\n\n if (!force && !options.reconcile) {\n // Already applied, skip\n return {\n success: true,\n applied: false,\n skipped: true,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n };\n }\n break;\n\n case 'failed':\n if (!force && (!options.reconcile || hasScopedForce)) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} previously failed: ${existing.error_message || 'Unknown error'}. Use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'running':\n if (!force) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} is currently running or was interrupted. If the previous run crashed, use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'rolled_back':\n // Rolled back migrations can be re-applied without --force\n break;\n }\n }\n\n if (options.dryRun) {\n return {\n success: true,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n };\n }\n\n // Get batch number\n if (this.currentBatch === null) {\n this.currentBatch = await this.getNextBatch();\n }\n\n // Determine if we're updating an existing record or inserting new\n const id = existing?.id || randomUUID();\n const attempts = (existing?.attempts || 0) + 1;\n\n if (existing) {\n // Update existing record to 'running'\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'running', checksum = ?, attempts = ?, error_message = NULL, batch = ?, applied_by = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n checksum,\n attempts,\n this.currentBatch,\n hostname(),\n id,\n );\n } else {\n // Insert new record\n await this.db.query(\n `INSERT INTO _smrt_schema_migrations\n (id, name, version, checksum, status, attempts, is_reversible, package_name, source_file, applied_by, batch)\n VALUES (?, ?, ?, ?, 'running', 1, ?, ?, ?, ?, ?)`,\n id,\n definition.id,\n definition.version,\n checksum,\n (definition.is_reversible ?? definition.down.length > 0) ? 1 : 0,\n definition.package_name ?? null,\n definition.source_file ?? null,\n hostname(),\n this.currentBatch,\n );\n }\n\n // PostgreSQL concurrent-index mode splits this definition: the\n // transaction-safe statements run here, the CONCURRENTLY index DDL runs\n // after the batch commits.\n const split = this.concurrentIndexPlan?.get(definition.id);\n const deferredConcurrent = (split?.concurrent.length ?? 0) > 0;\n\n // A retry of an interrupted concurrent-index migration must not re-run the\n // half that already committed. The marker on the previous attempt's\n // `error_message` says it did, and the checksum says it was this same\n // definition. Without this, a definition that mixes non-idempotent DDL\n // (`ALTER TABLE … ADD COLUMN`, which the differ emits without\n // `IF NOT EXISTS`) with index DDL would fail on \"column already exists\"\n // forever, never reaching the index build it was retrying for.\n const phase1AlreadyCommitted =\n deferredConcurrent &&\n existing?.status === 'failed' &&\n existing.checksum === checksum &&\n typeof existing.error_message === 'string' &&\n existing.error_message.startsWith(CONCURRENT_INDEX_PHASE1_MARKER);\n\n try {\n // Execute the migration\n await this.executeStatements(\n phase1AlreadyCommitted ? [] : split ? split.regular : definition.up,\n options,\n );\n\n const executionTime = Date.now() - startTime;\n\n if (deferredConcurrent) {\n // Commit this record as `failed`, not `completed` and not `running`.\n // The index does not exist yet, so `completed` would be a lie; and\n // unlike the single-transaction path — where a crash rolls the record\n // insert back with the DDL — this row is about to be committed, so a\n // crash before the concurrent phase finishes would strand a `running`\n // record that the next `db:migrate` can only clear with\n // `--force-migration`. `failed` is retryable under the CLI's\n // reconciling re-run, which is what an interrupted index rollout\n // needs. The concurrent phase overwrites this a moment later.\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', execution_time_ms = ?, error_message = ?\n WHERE id = ?`,\n executionTime,\n CONCURRENT_INDEX_PENDING_MESSAGE,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n }\n\n // Mark as completed\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n executionTime,\n checksum,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const migrationError =\n error instanceof Error ? error : new Error(String(error));\n\n // Mark as failed. In an outer atomic Postgres transaction, a DDL error\n // aborts the transaction and this status write must be skipped so the\n // caller can roll back the whole batch while preserving the root error.\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE id = ?`,\n migrationError.message,\n id,\n );\n } catch (persistError) {\n // Preserve the migration failure; status persistence is secondary and\n // may be impossible until the surrounding transaction rolls back.\n const persistMessage =\n persistError instanceof Error\n ? persistError.message\n : String(persistError);\n logger.error(\n `Failed to persist failed status for migration ${definition.id}: ${persistMessage}`,\n );\n }\n\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: migrationError,\n };\n }\n }\n\n /**\n * Apply multiple migrations in order.\n *\n * On PostgreSQL the atomic path always bounds itself with\n * `SET LOCAL lock_timeout` / `SET LOCAL statement_timeout` so a migration\n * that queues behind a long-running writer fails fast and rolls back\n * instead of stalling every writer behind its already-held locks\n * (issue #2362).\n *\n * `options.postgresSafe` additionally enables **concurrent-index mode**:\n * non-index DDL still runs in the one atomic transaction, then every index\n * statement runs `CONCURRENTLY` on a pinned session after that transaction\n * commits. That mode trades atomicity for availability — a failure part way\n * through the index phase leaves the committed non-index DDL in place, and\n * the unfinished index migrations are recorded `failed` so the next run\n * retries them, resuming at the index phase rather than re-running the\n * non-index statements that already committed.\n */\n async applyAll(\n definitions: MigrationDefinition[],\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult[]> {\n if (options.atomic && !options.dryRun) {\n if (!this.db.transaction) {\n throw new Error(\n 'Atomic migration batches require a database adapter with transaction support.',\n );\n }\n\n const isPostgres = this.dbEngine === 'postgres';\n const concurrentIndexMode = isPostgres && Boolean(options.postgresSafe);\n const plan = concurrentIndexMode\n ? buildConcurrentIndexPlan(\n definitions,\n this.options.useConcurrentIndexes,\n )\n : null;\n\n // Outside concurrent-index mode a CONCURRENTLY statement can only fail:\n // PostgreSQL rejects it inside a transaction block. Fail before opening\n // the transaction so the error names the offending migration.\n if (isPostgres && !concurrentIndexMode) {\n const concurrentStatement = findConcurrentIndexStatement(definitions);\n if (concurrentStatement) {\n throw new Error(\n `Atomic migration batch cannot include CONCURRENTLY index DDL in ${concurrentStatement.migrationName}; PostgreSQL forbids CONCURRENTLY inside a transaction. Re-run with PostgreSQL concurrent-index mode (smrt db:migrate --postgres-safe) to build those indexes outside the batch.`,\n );\n }\n }\n\n const deferred = plan && plan.size > 0 ? plan : null;\n\n try {\n const committed = await this.db.transaction(async (tx) => {\n const txDb = {\n ...tx,\n transaction: async <T>(\n callback: (transactionDb: DatabaseInterface) => Promise<T>,\n ) => callback(tx as DatabaseInterface),\n } as DatabaseInterface;\n\n // Bound the whole batch. `SET LOCAL` reverts at COMMIT/ROLLBACK, so\n // this cannot leak onto a pooled connection's next checkout.\n if (isPostgres) {\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n }\n\n const txTracker = new MigrationTracker({\n db: txDb,\n lockTimeout: this.options.lockTimeout,\n statementTimeout: this.options.statementTimeout,\n useConcurrentIndexes: false,\n });\n txTracker.concurrentIndexPlan = deferred;\n const results = await txTracker.applyAll(definitions, {\n ...options,\n atomic: false,\n continueOnError: false,\n postgresSafe: false,\n // Deferred migrations report progress from the concurrent phase\n // once they have actually finished, not when phase 1 commits.\n onProgress: options.onProgress\n ? (result) => {\n if (!deferred?.has(result.name)) {\n options.onProgress?.(result);\n }\n }\n : undefined,\n });\n const failed = results.find((result) => !result.success);\n\n if (failed) {\n throw new AtomicMigrationRollback(results, failed);\n }\n\n return results;\n });\n\n if (!deferred) {\n return committed;\n }\n\n return await this.applyDeferredConcurrentIndexes(\n committed,\n deferred,\n options,\n );\n } catch (error) {\n if (error instanceof AtomicMigrationRollback) {\n return error.results.map((result) => {\n if (!result.success || !result.applied) {\n return result;\n }\n\n return {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n rolled_back: true,\n error: new Error(\n `Rolled back because migration ${error.failed.name} failed`,\n ),\n };\n });\n }\n\n throw error;\n }\n }\n\n const results: MigrationResult[] = [];\n this.currentBatch = await this.getNextBatch();\n\n for (const definition of definitions) {\n const result = await this.apply(definition, options);\n results.push(result);\n options.onProgress?.(result);\n\n if (!result.success && !options.continueOnError) {\n break;\n }\n }\n\n this.currentBatch = null;\n return results;\n }\n\n /**\n * Phase 2 of PostgreSQL concurrent-index mode.\n *\n * Runs every deferred index statement on one pinned session, outside the\n * committed transaction, bounded by session `lock_timeout` /\n * `statement_timeout`. INVALID indexes left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` are dropped first so the retry rebuilds them\n * rather than skipping over a stump that reads as present.\n *\n * On failure the batch stops: the failing migration and every migration it\n * did not reach are recorded `failed`, which the CLI's reconciling\n * `db:migrate` retries on the next run.\n */\n private async applyDeferredConcurrentIndexes(\n committed: MigrationResult[],\n plan: ConcurrentIndexPlan,\n options: ApplyMigrationsOptions,\n ): Promise<MigrationResult[]> {\n const results = [...committed];\n const session = await this.acquireMigrationSession();\n\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n let stoppedBy: string | null = null;\n\n for (let index = 0; index < results.length; index++) {\n const result = results[index];\n const split = plan.get(result.name);\n if (!split || split.concurrent.length === 0) {\n continue;\n }\n\n // A migration phase 1 skipped (already applied, checksum matched) is\n // already `completed` in the tracking table — its index exists and\n // must not be rebuilt or have its record rewritten.\n if (result.skipped) {\n continue;\n }\n\n if (stoppedBy) {\n const error = new Error(\n `Not attempted: concurrent index phase stopped after ${stoppedBy} failed`,\n );\n await this.recordConcurrentIndexFailure(result.name, error.message);\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n error,\n };\n options.onProgress?.(results[index]);\n continue;\n }\n\n const startedAt = Date.now();\n\n try {\n for (const sql of split.concurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n // An INVALID index is not usable by the planner and blocks the\n // name, but `CREATE INDEX … IF NOT EXISTS` treats it as present.\n // Drop it so the create below actually rebuilds it.\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n\n // Floor at 1ms: callers (the CLI progress reporter, the applied vs\n // skipped counters) read `execution_time_ms === 0` as \"nothing ran\".\n const executionTime = Math.max(\n 1,\n (result.execution_time_ms ?? 0) + (Date.now() - startedAt),\n );\n await this.recordConcurrentIndexSuccess(\n result.name,\n result.checksum,\n executionTime,\n );\n results[index] = {\n ...result,\n success: true,\n applied: true,\n skipped: false,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const indexError =\n error instanceof Error ? error : new Error(String(error));\n if (!options.continueOnError) {\n stoppedBy = result.name;\n }\n await this.recordConcurrentIndexFailure(\n result.name,\n indexError.message,\n );\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n execution_time_ms: Date.now() - startedAt,\n error: indexError,\n };\n }\n\n options.onProgress?.(results[index]);\n }\n\n return results;\n } finally {\n await session.release();\n }\n }\n\n /**\n * Pin one connection for the concurrent-index phase.\n *\n * PostgreSQL pools hand each top-level `query` an arbitrary connection, so\n * a `SET lock_timeout` issued through the pool may not apply to the DDL\n * that follows. Adapters that expose `acquireSession` give us a pinned\n * connection; anything else (SQLite, DuckDB, test doubles) is\n * single-connection anyway and falls back to the plain interface.\n */\n private async acquireMigrationSession(): Promise<MigrationSession> {\n if (typeof this.db.acquireSession === 'function') {\n const session = await this.db.acquireSession();\n return {\n query: (sql, ...vars) => session.query(sql, ...vars),\n release: () => session.release(),\n };\n }\n\n return {\n query: (sql, ...vars) => this.db.query(sql, ...vars),\n release: async () => {},\n };\n }\n\n /**\n * Read the names of INVALID indexes that an unqualified index name in this\n * session would actually resolve to.\n *\n * `pg_indexes` (what the SQL adapter's introspection reads) lists an\n * INVALID index exactly like a healthy one, so the schema differ believes a\n * failed `CREATE INDEX CONCURRENTLY` succeeded. `pg_index.indisvalid` is the\n * authoritative flag.\n *\n * Scoped to `current_schemas(false)` — the session's search path — because\n * migration DDL names indexes unqualified, and so does the\n * `DROP INDEX CONCURRENTLY IF EXISTS \"<name>\"` repair. Scanning every schema\n * would let an unrelated invalid index of the same name in another schema\n * trigger a drop that then resolves to a *different* object. Where the same\n * name appears in several search-path schemas, the earliest on the path wins\n * — the same rule PostgreSQL uses to resolve the name.\n *\n * A role without catalog access degrades to \"no known invalid indexes\"\n * rather than aborting the migration.\n */\n private async readInvalidIndexNames(\n session: MigrationSession,\n ): Promise<Set<string>> {\n const names = new Set<string>();\n\n try {\n const result = await session.query(\n `SELECT DISTINCT ON (c.relname) c.relname AS index_name\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE i.indisvalid = false\n AND n.nspname = ANY (current_schemas(false))\n ORDER BY c.relname,\n array_position(current_schemas(false), n.nspname::text)`,\n );\n\n for (const row of result.rows) {\n const name = (row as { index_name?: unknown }).index_name;\n if (typeof name === 'string') {\n names.add(name);\n }\n }\n } catch (error) {\n logger.warn(\n `Could not read pg_index.indisvalid; skipping INVALID index repair: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n return names;\n }\n\n private async recordConcurrentIndexSuccess(\n name: string,\n checksum: string,\n executionTime: number,\n ): Promise<void> {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, error_message = NULL, applied_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n executionTime,\n checksum,\n name,\n );\n }\n\n private async recordConcurrentIndexFailure(\n name: string,\n message: string,\n ): Promise<void> {\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE name = ?`,\n // Keep the resume marker: this failure happened *after* phase 1\n // committed, so the retry must skip the non-index statements too.\n `${CONCURRENT_INDEX_PHASE1_MARKER} ${message}`,\n name,\n );\n } catch (persistError) {\n logger.error(\n `Failed to persist failed status for migration ${name}: ${\n persistError instanceof Error\n ? persistError.message\n : String(persistError)\n }`,\n );\n }\n }\n\n /**\n * Rollback a migration\n */\n async rollback(\n name: string,\n definition: MigrationDefinition,\n options: RollbackOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.down);\n\n const record = await this.getMigration(name);\n if (!record) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(`Migration ${name} not found in database`),\n };\n }\n\n if (record.status !== 'completed') {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not in completed state (current: ${record.status})`,\n ),\n };\n }\n\n if (!definition.down.length) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not reversible (no down statements)`,\n ),\n };\n }\n\n if (options.dryRun) {\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n }\n\n try {\n // Execute DOWN statements\n await this.executeStatements(definition.down, {});\n\n // Mark as rolled back\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'rolled_back', rolled_back_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n name,\n );\n\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n } catch (error) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: error instanceof Error ? error : new Error(String(error)),\n rolled_back: false,\n };\n }\n }\n\n /**\n * Get migration history with optional filtering\n */\n async getHistory(\n options: { limit?: number; since?: Date; status?: MigrationStatus } = {},\n ): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n let sql = `SELECT * FROM _smrt_schema_migrations WHERE 1=1`;\n const params: unknown[] = [];\n\n if (options.status) {\n sql += ` AND status = ?`;\n params.push(options.status);\n }\n\n if (options.since) {\n sql += ` AND applied_at >= ?`;\n params.push(options.since.toISOString());\n }\n\n sql += ` ORDER BY applied_at DESC`;\n\n if (options.limit) {\n sql += ` LIMIT ?`;\n params.push(options.limit);\n }\n\n const result = await this.db.query(sql, ...params);\n\n return (result.rows as MigrationRow[]).map((row) => ({\n ...row,\n applied_at: new Date(row.applied_at),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n is_reversible: Boolean(row.is_reversible),\n })) as SchemaMigrationRecord[];\n }\n\n /**\n * Execute SQL statements with appropriate transaction handling\n */\n private async executeStatements(\n statements: string[],\n options: ApplyMigrationsOptions,\n ): Promise<void> {\n if (this.dbEngine === 'postgres' && options.postgresSafe) {\n await this.executePostgresStatements(statements);\n } else {\n await this.executeWithTransaction(statements);\n }\n }\n\n /**\n * Execute statements within a transaction (SQLite/DuckDB)\n */\n private async executeWithTransaction(statements: string[]): Promise<void> {\n if (this.db.transaction) {\n await this.db.transaction(async (tx) => {\n for (const sql of statements) {\n await tx.query(sql);\n }\n });\n } else {\n // Fallback to sequential execution\n for (const sql of statements) {\n await this.db.query(sql);\n }\n }\n }\n\n /**\n * Execute statements with PostgreSQL-specific handling\n *\n * - CONCURRENTLY statements run outside transaction\n * - Regular statements run in transaction with lock_timeout\n *\n * Both CREATE and DROP variants of CONCURRENTLY must be detected — Postgres\n * forbids `DROP INDEX CONCURRENTLY` inside a transaction block, just like\n * the create variant. Issue #1165: the migration generator emits\n * `DROP INDEX CONCURRENTLY` for shape-drift recreates and orphan-index\n * cleanups, so the regex needs to cover both keywords.\n */\n private async executePostgresStatements(statements: string[]): Promise<void> {\n const { concurrent: finalConcurrent, regular: finalRegular } =\n planPostgresStatements(statements, !!this.options.useConcurrentIndexes);\n\n // CONCURRENTLY statements come from `planPostgresStatements`, which\n // also pushes any post-transform CREATE/DROP CONCURRENTLY into the\n // outside-transaction set. The original `concurrentStatements` list\n // is folded in there too so callers below see one merged stream.\n\n // Execute regular statements in transaction with timeouts\n if (finalRegular.length > 0 && this.db.transaction) {\n await this.db.transaction(async (tx) => {\n // Set session-level timeouts\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n for (const sql of finalRegular) {\n await tx.query(sql);\n }\n });\n } else if (finalRegular.length > 0) {\n for (const sql of finalRegular) {\n await this.db.query(sql);\n }\n }\n\n if (finalConcurrent.length === 0) {\n return;\n }\n\n // Execute CONCURRENTLY statements outside the transaction, on one pinned\n // connection so `SET lock_timeout` / `SET statement_timeout` actually\n // govern the DDL that follows them (a pooled `db.query` may not reuse the\n // connection the SET landed on).\n const session = await this.acquireMigrationSession();\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n\n for (const sql of finalConcurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n } finally {\n await session.release();\n }\n }\n}\n\n/**\n * Quote an index identifier for PostgreSQL, escaping embedded double quotes.\n */\nfunction quoteIndexName(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Build the PostgreSQL concurrent-index plan for an atomic batch.\n *\n * Returns one entry per migration definition that has at least one statement\n * which must leave the transaction, mapping it to the transaction-safe\n * statements (phase 1) and the `CONCURRENTLY` statements (phase 2).\n * Definitions with nothing to defer are omitted so the caller can treat\n * `plan.has(id)` as \"this migration finishes in phase 2\".\n *\n * Exported for unit testing.\n */\nexport function buildConcurrentIndexPlan(\n definitions: MigrationDefinition[],\n useConcurrentIndexes: boolean,\n): ConcurrentIndexPlan {\n const plan: ConcurrentIndexPlan = new Map();\n\n for (const definition of definitions) {\n const split = planPostgresStatements(definition.up, useConcurrentIndexes);\n if (split.concurrent.length > 0) {\n plan.set(definition.id, split);\n }\n }\n\n return plan;\n}\n\n/**\n * Split a batch of SQL statements into a transaction-safe set and a\n * concurrent set, applying `--postgres-safe` rewriting if requested.\n *\n * Exported for unit testing and to give external callers a way to\n * reason about what the tracker would execute in PostgreSQL safe mode.\n *\n * Rules:\n * - Statements already containing `CONCURRENTLY` for CREATE/DROP INDEX or\n * REINDEX operations always go to the concurrent set — Postgres rejects\n * them inside transactions.\n * - When `useConcurrentIndexes` is true, plain `CREATE INDEX` and plain\n * `DROP INDEX` are rewritten to use `CONCURRENTLY`, then routed to\n * the concurrent set. This is what the CLI `--postgres-safe` flag and\n * the auto-migrate path rely on for issue #1165's shape-drift drops.\n * - All other statements stay in the regular (transaction) set.\n */\nexport function planPostgresStatements(\n statements: string[],\n useConcurrentIndexes: boolean,\n): { concurrent: string[]; regular: string[] } {\n const concurrentRegex = CONCURRENT_INDEX_STATEMENT_RE;\n\n const concurrent: string[] = [];\n const regular: string[] = [];\n\n for (const sql of statements) {\n if (concurrentRegex.test(sql)) {\n concurrent.push(sql);\n continue;\n }\n if (useConcurrentIndexes) {\n if (/CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+/i,\n 'CREATE $1INDEX CONCURRENTLY ',\n ),\n );\n continue;\n }\n if (/DROP\\s+INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(/DROP\\s+INDEX\\s+/i, 'DROP INDEX CONCURRENTLY '),\n );\n continue;\n }\n }\n regular.push(sql);\n }\n\n return { concurrent, regular };\n}\n"],"mappings":"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"}
|
|
@@ -114,7 +114,22 @@ export interface ApplyMigrationsOptions {
|
|
|
114
114
|
* is dead.
|
|
115
115
|
*/
|
|
116
116
|
reconcile?: boolean;
|
|
117
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Enable PostgreSQL concurrent-index mode.
|
|
119
|
+
*
|
|
120
|
+
* On the non-atomic path this routes index DDL outside the transaction and
|
|
121
|
+
* bounds it with session `lock_timeout`/`statement_timeout`.
|
|
122
|
+
*
|
|
123
|
+
* On the atomic path (`atomic: true`) it splits the batch: non-index DDL
|
|
124
|
+
* still commits in one transaction, then each index statement runs
|
|
125
|
+
* `CONCURRENTLY` on a pinned session afterwards. **Concurrent mode is not
|
|
126
|
+
* atomic** — the committed non-index DDL survives a later index failure, and
|
|
127
|
+
* unfinished index migrations are recorded `failed` so a reconciling re-run
|
|
128
|
+
* retries them.
|
|
129
|
+
*
|
|
130
|
+
* Ignored on non-PostgreSQL engines. Timeouts are applied to the atomic
|
|
131
|
+
* PostgreSQL path regardless of this flag.
|
|
132
|
+
*/
|
|
118
133
|
postgresSafe?: boolean;
|
|
119
134
|
/** Called after each migration attempt completes, before the batch continues */
|
|
120
135
|
onProgress?: (result: MigrationResult) => void;
|
|
@@ -216,11 +231,11 @@ export interface MigrationsConfig {
|
|
|
216
231
|
autoGenerateDown?: boolean;
|
|
217
232
|
/** PostgreSQL-specific options */
|
|
218
233
|
postgres?: {
|
|
219
|
-
/**
|
|
234
|
+
/** Allow CONCURRENTLY index builds under `db:migrate --postgres-safe` (default: true) */
|
|
220
235
|
useConcurrently?: boolean;
|
|
221
|
-
/**
|
|
236
|
+
/** `lock_timeout` for every PostgreSQL migration statement (default: '30s') */
|
|
222
237
|
lockTimeout?: string;
|
|
223
|
-
/**
|
|
238
|
+
/** `statement_timeout` for every PostgreSQL migration statement (default: '60s') */
|
|
224
239
|
statementTimeout?: string;
|
|
225
240
|
};
|
|
226
241
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"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"}
|
package/dist/migrations.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parsePostgresTimeoutMs } from "./postgres-timeouts.js";
|
|
2
|
+
import { SQLITE_REBUILD_TABLE_PREFIX, buildSqliteRebuildStatements, isSqliteRebuildPlaceholder, planSqliteTableRebuilds, rewriteSqliteCreateTable, sqliteRebuildPlaceholderSql } from "./migrations/sqlite-rebuild.js";
|
|
3
|
+
import { SchemaComparer, canonicalizeDefault, generateSchemaDiff, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, uniqueColumnIndexName } from "./migrations/differ.js";
|
|
2
4
|
import { BackfillTableUnavailableError, BackfillTracker } from "./migrations/backfill-tracker.js";
|
|
3
5
|
import { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum } from "./migrations/checksum.js";
|
|
4
6
|
import { MigrationGenerator, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp } from "./migrations/generator.js";
|
|
5
|
-
import { MigrationTracker, planPostgresStatements } from "./migrations/tracker.js";
|
|
7
|
+
import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./migrations/tracker.js";
|
|
6
8
|
import { getPendingSchemaStatements, migrateSmrtSchemas } from "./migrations/orchestrate.js";
|
|
7
9
|
import "./migrations/index.js";
|
|
8
|
-
export { BackfillTableUnavailableError, BackfillTracker, MigrationGenerator, MigrationTracker, SchemaComparer, computeChecksum, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, migrateSmrtSchemas, normalizeSQL, planPostgresStatements, shortChecksum, verifyChecksum };
|
|
10
|
+
export { BackfillTableUnavailableError, BackfillTracker, MigrationGenerator, MigrationTracker, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildSqliteRebuildStatements, canonicalizeDefault, computeChecksum, createMigrationDefinition, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateSmrtSchemas, normalizeSQL, parsePostgresTimeoutMs, planPostgresStatements, planSqliteTableRebuilds, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum };
|
package/dist/object.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AITextCompletionOptions, AITool } from '@happyvertical/ai';
|
|
2
2
|
import { SmrtClassOptions, SmrtClass } from './class';
|
|
3
|
+
import { DatabaseErrorClassification } from './db-errors';
|
|
3
4
|
import { GenerateEmbeddingsOptions } from './embeddings/types';
|
|
4
5
|
import { ToolCall, ToolCallResult } from './tools/tool-executor';
|
|
5
6
|
/**
|
|
@@ -555,16 +556,34 @@ export declare class SmrtObject extends SmrtClass {
|
|
|
555
556
|
* 3. Assigns a UUID `id` if not already set
|
|
556
557
|
* 4. Generates a `slug` via `getSlug()` if not already set
|
|
557
558
|
* 5. Updates `updated_at` (and sets `created_at` on first save)
|
|
558
|
-
* 6. Upserts the row
|
|
559
|
+
* 6. Upserts the row, retrying only failures a later attempt could survive
|
|
559
560
|
* 7. Executes `afterSave` interceptors
|
|
560
561
|
* 8. Triggers embedding generation in the background if configured
|
|
561
562
|
*
|
|
562
563
|
* For STI classes, validates that `_meta_type` is present and correct
|
|
563
564
|
* before writing to the database.
|
|
564
565
|
*
|
|
566
|
+
* **Error contract (#2366).** The driver error is classified through its
|
|
567
|
+
* whole cause chain, because `@happyvertical/sql` wraps it as
|
|
568
|
+
* `DatabaseError('Failed to upsert record into table', …)` and the
|
|
569
|
+
* constraint wording never reaches the outer `message`. A deterministic
|
|
570
|
+
* failure — unique/not-null/foreign-key/check violation, invalid input
|
|
571
|
+
* syntax, missing table, or a statement issued inside an already-aborted
|
|
572
|
+
* PostgreSQL transaction (`25P02`) — is raised on the **first** attempt with
|
|
573
|
+
* no backoff, so the original cause survives instead of being replaced by
|
|
574
|
+
* `25P02` on a doomed retry inside a caller-managed transaction. Only
|
|
575
|
+
* transient failures (serialization failure, deadlock, lock timeout, dropped
|
|
576
|
+
* connection, `SQLITE_BUSY`) are retried: up to 4 attempts total — the
|
|
577
|
+
* initial try plus 3 retries — sleeping 500, 1000 and 2000 ms between them.
|
|
578
|
+
*
|
|
565
579
|
* @returns This instance after saving (enables chaining)
|
|
566
|
-
* @throws {ValidationError}
|
|
567
|
-
*
|
|
580
|
+
* @throws {ValidationError} `VALIDATION_UNIQUE_CONSTRAINT` on a unique or
|
|
581
|
+
* primary-key violation, `VALIDATION_REQUIRED_FIELD` on a NOT NULL
|
|
582
|
+
* violation — on every adapter, and never retried
|
|
583
|
+
* @throws {DatabaseError} If the table does not exist (`DB_SCHEMA_MISSING`),
|
|
584
|
+
* or the query fails for any other reason (`DB_QUERY_FAILED`), including
|
|
585
|
+
* foreign-key and check violations, whose original driver error is
|
|
586
|
+
* preserved on `cause`
|
|
568
587
|
* @throws {RuntimeError} For any other unexpected failure during save
|
|
569
588
|
*
|
|
570
589
|
* @example
|
|
@@ -635,6 +654,21 @@ export declare class SmrtObject extends SmrtClass {
|
|
|
635
654
|
* @returns `'unique'`, `'not_null'`, or `null`.
|
|
636
655
|
*/
|
|
637
656
|
static classifyConstraintError(message: string): 'unique' | 'not_null' | null;
|
|
657
|
+
/**
|
|
658
|
+
* Recovers the offending column for a constraint violation, searching every
|
|
659
|
+
* message in the driver-error cause chain rather than only the outermost
|
|
660
|
+
* one (#2366).
|
|
661
|
+
*
|
|
662
|
+
* Prefers the column the driver named outright (`node-postgres` sets
|
|
663
|
+
* `error.column` on a `23502`), then falls back to parsing. The adapter's
|
|
664
|
+
* wrapper message ("Failed to upsert record into table") carries no column,
|
|
665
|
+
* and PostgreSQL puts a unique violation's column in the `DETAIL` line
|
|
666
|
+
* ("Key (sku)=(abc) already exists.") — reachable either as `error.detail`
|
|
667
|
+
* on the direct INSERT path or folded into `formatDbError`'s `detail=…`
|
|
668
|
+
* string on the upsert path. Returns `'unknown_field'` only when nothing in
|
|
669
|
+
* the chain names a column.
|
|
670
|
+
*/
|
|
671
|
+
protected extractConstraintFieldFromChain(error: Error, classification: DatabaseErrorClassification): string;
|
|
638
672
|
/**
|
|
639
673
|
* Extracts field name from database constraint error messages.
|
|
640
674
|
*
|
|
@@ -646,15 +680,20 @@ export declare class SmrtObject extends SmrtClass {
|
|
|
646
680
|
* Hydrates this object from the database using its `id` property.
|
|
647
681
|
*
|
|
648
682
|
* Queries the database for a row matching `{ id: this._id }` and calls
|
|
649
|
-
* `loadDataFromDb()` if found.
|
|
650
|
-
*
|
|
683
|
+
* `loadDataFromDb()` if found. Transient failures are retried up to 4 times
|
|
684
|
+
* total — the initial try plus 3 retries — sleeping 250, 500 and 1000 ms
|
|
685
|
+
* between them; deterministic failures are not. A malformed identifier
|
|
686
|
+
* (PostgreSQL `22P02 invalid_text_representation` on a `uuid` column) is a
|
|
687
|
+
* deterministic failure, so it now raises on the first attempt instead of
|
|
688
|
+
* spending three retries re-submitting the same bad literal (#2366).
|
|
651
689
|
*
|
|
652
690
|
* Called automatically by `initialize()` when `options.id` is provided.
|
|
653
691
|
* Typically you do not need to call this directly.
|
|
654
692
|
*
|
|
655
693
|
* @returns Promise that resolves when loading is complete (no-op if not found)
|
|
656
694
|
* @throws {ValidationError} If `this._id` is not set
|
|
657
|
-
* @throws {DatabaseError} If the query fails
|
|
695
|
+
* @throws {DatabaseError} If the query fails; the driver error is preserved
|
|
696
|
+
* on `cause`
|
|
658
697
|
*
|
|
659
698
|
* @example
|
|
660
699
|
* ```typescript
|
package/dist/object.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../src/object.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../src/object.ts"],"names":[],"mappings":"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"}
|
package/dist/object.js
CHANGED
|
@@ -3,6 +3,7 @@ import { GlobalInterceptors, createInterceptorContext } from "./interceptors.js"
|
|
|
3
3
|
import { SmrtClass } from "./class.js";
|
|
4
4
|
import { EmbeddingProvider } from "./embeddings/provider.js";
|
|
5
5
|
import { EmbeddingStorage } from "./embeddings/storage.js";
|
|
6
|
+
import { classifyDatabaseError, classifyDialectMessage } from "./db-errors.js";
|
|
6
7
|
import { DatabaseError, ErrorUtils, RuntimeError, SmrtError, TenantIsolationError, ValidationError } from "./errors.js";
|
|
7
8
|
import { toSnakeCase } from "./utils/naming.js";
|
|
8
9
|
import { ContentHasher } from "./embeddings/hash.js";
|
|
@@ -948,16 +949,34 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
948
949
|
* 3. Assigns a UUID `id` if not already set
|
|
949
950
|
* 4. Generates a `slug` via `getSlug()` if not already set
|
|
950
951
|
* 5. Updates `updated_at` (and sets `created_at` on first save)
|
|
951
|
-
* 6. Upserts the row
|
|
952
|
+
* 6. Upserts the row, retrying only failures a later attempt could survive
|
|
952
953
|
* 7. Executes `afterSave` interceptors
|
|
953
954
|
* 8. Triggers embedding generation in the background if configured
|
|
954
955
|
*
|
|
955
956
|
* For STI classes, validates that `_meta_type` is present and correct
|
|
956
957
|
* before writing to the database.
|
|
957
958
|
*
|
|
959
|
+
* **Error contract (#2366).** The driver error is classified through its
|
|
960
|
+
* whole cause chain, because `@happyvertical/sql` wraps it as
|
|
961
|
+
* `DatabaseError('Failed to upsert record into table', …)` and the
|
|
962
|
+
* constraint wording never reaches the outer `message`. A deterministic
|
|
963
|
+
* failure — unique/not-null/foreign-key/check violation, invalid input
|
|
964
|
+
* syntax, missing table, or a statement issued inside an already-aborted
|
|
965
|
+
* PostgreSQL transaction (`25P02`) — is raised on the **first** attempt with
|
|
966
|
+
* no backoff, so the original cause survives instead of being replaced by
|
|
967
|
+
* `25P02` on a doomed retry inside a caller-managed transaction. Only
|
|
968
|
+
* transient failures (serialization failure, deadlock, lock timeout, dropped
|
|
969
|
+
* connection, `SQLITE_BUSY`) are retried: up to 4 attempts total — the
|
|
970
|
+
* initial try plus 3 retries — sleeping 500, 1000 and 2000 ms between them.
|
|
971
|
+
*
|
|
958
972
|
* @returns This instance after saving (enables chaining)
|
|
959
|
-
* @throws {ValidationError}
|
|
960
|
-
*
|
|
973
|
+
* @throws {ValidationError} `VALIDATION_UNIQUE_CONSTRAINT` on a unique or
|
|
974
|
+
* primary-key violation, `VALIDATION_REQUIRED_FIELD` on a NOT NULL
|
|
975
|
+
* violation — on every adapter, and never retried
|
|
976
|
+
* @throws {DatabaseError} If the table does not exist (`DB_SCHEMA_MISSING`),
|
|
977
|
+
* or the query fails for any other reason (`DB_QUERY_FAILED`), including
|
|
978
|
+
* foreign-key and check violations, whose original driver error is
|
|
979
|
+
* preserved on `cause`
|
|
961
980
|
* @throws {RuntimeError} For any other unexpected failure during save
|
|
962
981
|
*
|
|
963
982
|
* @example
|
|
@@ -1016,13 +1035,13 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1016
1035
|
else await this.db.upsert(this.tableName, upsertConflictColumns, data);
|
|
1017
1036
|
} catch (error) {
|
|
1018
1037
|
if (error instanceof Error) {
|
|
1019
|
-
const
|
|
1020
|
-
if (kind === "
|
|
1021
|
-
const field = this.
|
|
1038
|
+
const classification = classifyDatabaseError(error);
|
|
1039
|
+
if (classification.kind === "unique_violation") {
|
|
1040
|
+
const field = this.extractConstraintFieldFromChain(error, classification);
|
|
1022
1041
|
throw ValidationError.uniqueConstraint(field, this.getFieldValue(field));
|
|
1023
1042
|
}
|
|
1024
|
-
if (kind === "
|
|
1025
|
-
const field = this.
|
|
1043
|
+
if (classification.kind === "not_null_violation") {
|
|
1044
|
+
const field = this.extractConstraintFieldFromChain(error, classification);
|
|
1026
1045
|
throw ValidationError.requiredField(field, className);
|
|
1027
1046
|
}
|
|
1028
1047
|
const operation = writePlan.type === "updateById" ? `UPDATE ${this.tableName} (id-targeted)` : this._insertOnly && !this._persisted ? `INSERT INTO ${this.tableName}` : `UPSERT INTO ${this.tableName}`;
|
|
@@ -1178,11 +1197,34 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1178
1197
|
*/
|
|
1179
1198
|
static classifyConstraintError(message) {
|
|
1180
1199
|
if (!message) return null;
|
|
1181
|
-
|
|
1182
|
-
if (
|
|
1200
|
+
const kind = classifyDialectMessage(message);
|
|
1201
|
+
if (kind === "not_null_violation") return "not_null";
|
|
1202
|
+
if (kind === "unique_violation") return "unique";
|
|
1183
1203
|
return null;
|
|
1184
1204
|
}
|
|
1185
1205
|
/**
|
|
1206
|
+
* Recovers the offending column for a constraint violation, searching every
|
|
1207
|
+
* message in the driver-error cause chain rather than only the outermost
|
|
1208
|
+
* one (#2366).
|
|
1209
|
+
*
|
|
1210
|
+
* Prefers the column the driver named outright (`node-postgres` sets
|
|
1211
|
+
* `error.column` on a `23502`), then falls back to parsing. The adapter's
|
|
1212
|
+
* wrapper message ("Failed to upsert record into table") carries no column,
|
|
1213
|
+
* and PostgreSQL puts a unique violation's column in the `DETAIL` line
|
|
1214
|
+
* ("Key (sku)=(abc) already exists.") — reachable either as `error.detail`
|
|
1215
|
+
* on the direct INSERT path or folded into `formatDbError`'s `detail=…`
|
|
1216
|
+
* string on the upsert path. Returns `'unknown_field'` only when nothing in
|
|
1217
|
+
* the chain names a column.
|
|
1218
|
+
*/
|
|
1219
|
+
extractConstraintFieldFromChain(error, classification) {
|
|
1220
|
+
if (classification.column) return classification.column;
|
|
1221
|
+
for (const message of [error.message, ...classification.driverMessages]) {
|
|
1222
|
+
const field = this.extractConstraintField(message);
|
|
1223
|
+
if (field !== "unknown_field") return field;
|
|
1224
|
+
}
|
|
1225
|
+
return "unknown_field";
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1186
1228
|
* Extracts field name from database constraint error messages.
|
|
1187
1229
|
*
|
|
1188
1230
|
* Handles SQLite, PostgreSQL and DuckDB phrasings. Returns
|
|
@@ -1205,15 +1247,20 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1205
1247
|
* Hydrates this object from the database using its `id` property.
|
|
1206
1248
|
*
|
|
1207
1249
|
* Queries the database for a row matching `{ id: this._id }` and calls
|
|
1208
|
-
* `loadDataFromDb()` if found.
|
|
1209
|
-
*
|
|
1250
|
+
* `loadDataFromDb()` if found. Transient failures are retried up to 4 times
|
|
1251
|
+
* total — the initial try plus 3 retries — sleeping 250, 500 and 1000 ms
|
|
1252
|
+
* between them; deterministic failures are not. A malformed identifier
|
|
1253
|
+
* (PostgreSQL `22P02 invalid_text_representation` on a `uuid` column) is a
|
|
1254
|
+
* deterministic failure, so it now raises on the first attempt instead of
|
|
1255
|
+
* spending three retries re-submitting the same bad literal (#2366).
|
|
1210
1256
|
*
|
|
1211
1257
|
* Called automatically by `initialize()` when `options.id` is provided.
|
|
1212
1258
|
* Typically you do not need to call this directly.
|
|
1213
1259
|
*
|
|
1214
1260
|
* @returns Promise that resolves when loading is complete (no-op if not found)
|
|
1215
1261
|
* @throws {ValidationError} If `this._id` is not set
|
|
1216
|
-
* @throws {DatabaseError} If the query fails
|
|
1262
|
+
* @throws {DatabaseError} If the query fails; the driver error is preserved
|
|
1263
|
+
* on `cause`
|
|
1217
1264
|
*
|
|
1218
1265
|
* @example
|
|
1219
1266
|
* ```typescript
|