@happyvertical/smrt-core 0.49.3 → 0.49.5
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/README.md +20 -0
- package/agents/change-feed.md +19 -0
- package/agents/collection-reads.md +21 -0
- package/agents/generators.md +5 -0
- package/agents/memory.md +23 -3
- package/agents/null-equal-indexes.md +88 -0
- package/agents/object-runtime.md +85 -0
- package/agents/schema-paths.md +7 -7
- package/dist/browser.js +2 -2
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +13 -10
- package/dist/cascade.js.map +1 -1
- package/dist/change-feed.d.ts +22 -0
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +147 -6
- package/dist/change-feed.js.map +1 -1
- package/dist/class.js +1 -1
- package/dist/collection-cache.d.ts +10 -1
- package/dist/collection-cache.d.ts.map +1 -1
- package/dist/collection-cache.js +65 -10
- package/dist/collection-cache.js.map +1 -1
- package/dist/collection.d.ts +69 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +142 -9
- package/dist/collection.js.map +1 -1
- package/dist/consumer-plugin/artifact-publication.d.ts +27 -0
- package/dist/consumer-plugin/artifact-publication.d.ts.map +1 -0
- package/dist/consumer-plugin/artifact-publication.js +80 -0
- package/dist/consumer-plugin/artifact-publication.js.map +1 -0
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +44 -6
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/db-errors.js +1 -1
- package/dist/db-errors.js.map +1 -1
- package/dist/embedded-write-queue.d.ts +1 -1
- package/dist/embedded-write-queue.d.ts.map +1 -1
- package/dist/embedded-write-queue.js +16 -6
- package/dist/embedded-write-queue.js.map +1 -1
- package/dist/embeddings/storage.d.ts +16 -0
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +40 -0
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/interceptors.d.ts +22 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +12 -0
- package/dist/interceptors.js.map +1 -1
- package/dist/junction.d.ts +12 -3
- package/dist/junction.d.ts.map +1 -1
- package/dist/junction.js +34 -4
- package/dist/junction.js.map +1 -1
- package/dist/knowledge-config.d.ts +6 -0
- package/dist/knowledge-config.d.ts.map +1 -0
- package/dist/knowledge-config.js +48 -0
- package/dist/knowledge-config.js.map +1 -0
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +91 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +104 -1
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +2 -0
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +2 -0
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -1
- package/dist/migrations/null-equal-indexes.d.ts +37 -0
- package/dist/migrations/null-equal-indexes.d.ts.map +1 -0
- package/dist/migrations/null-equal-indexes.js +133 -0
- package/dist/migrations/null-equal-indexes.js.map +1 -0
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +6 -0
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations.js +2 -1
- package/dist/object.d.ts +33 -0
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +316 -46
- package/dist/object.js.map +1 -1
- package/dist/plain-json.d.ts +6 -0
- package/dist/plain-json.d.ts.map +1 -0
- package/dist/plain-json.js +41 -0
- package/dist/plain-json.js.map +1 -0
- package/dist/postgres-permissions.d.ts.map +1 -1
- package/dist/postgres-permissions.js +6 -2
- package/dist/postgres-permissions.js.map +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +3 -3
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +4 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +4 -3
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/validator.d.ts +1 -0
- package/dist/registry/validator.d.ts.map +1 -1
- package/dist/registry/validator.js +8 -1
- package/dist/registry/validator.js.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +21 -0
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +2 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +8 -2
- package/dist/schema/conflict-target.d.ts.map +1 -1
- package/dist/schema/conflict-target.js +11 -3
- package/dist/schema/conflict-target.js.map +1 -1
- package/dist/schema/ddl/null-equal-index.d.ts +5 -0
- package/dist/schema/ddl/null-equal-index.d.ts.map +1 -0
- package/dist/schema/ddl/null-equal-index.js +17 -0
- package/dist/schema/ddl/null-equal-index.js.map +1 -0
- package/dist/schema/ddl/postgres-strategy.d.ts +2 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +7 -0
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +8 -3
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/manifest-schema.d.ts +2 -1
- package/dist/schema/manifest-schema.d.ts.map +1 -1
- package/dist/schema/manifest-schema.js +12 -4
- package/dist/schema/manifest-schema.js.map +1 -1
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +1 -1
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/schema/types.d.ts +2 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/scripts/browser-plain-json-regression.mjs +85 -0
- package/dist/smrt-knowledge.json +73 -15
- package/dist/system/schema.d.ts +14 -3
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +120 -2
- package/dist/system/schema.js.map +1 -1
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +9 -7
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +24 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +48 -115
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +2 -2
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +18 -0
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +55 -2
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracker.js","names":[],"sources":["../../src/migrations/tracker.ts"],"sourcesContent":["/**\n * Migration Tracker\n *\n * Manages migration state, tracking which migrations have been applied,\n * and providing idempotent migration application.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { hostname } from 'node:os';\nimport { createLogger } from '@happyvertical/logger';\nimport { parsePostgresTimeoutMs } from '../postgres-timeouts.js';\nimport { detectEngine } from '../schema/ddl/index.js';\n// DatabaseEngine comes from the DDL layer (the json-inclusive type that\n// `detectEngine` actually returns), matching `SchemaComparer` in differ.ts.\n// The migration stack supports the JSON adapter post-R11, so the tracker's\n// engine type must include 'json' too — the narrower `migrations/types`\n// alias would reject it.\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport type {\n DriftReport,\n MigrationDefinition,\n MigrationResult,\n MigrationStatus,\n SchemaMigrationRecord,\n} from '../schema/types.js';\nimport { assertPostgresSystemTimestampsCurrent } from '../system/compatibility.js';\nimport {\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\nimport { toSafeBooleanInteger, toSafeInteger } from '../utils/safe-integer.js';\nimport { computeChecksum, verifyChecksum } from './checksum.js';\nimport type {\n ApplyMigrationsOptions,\n DatabaseInterface,\n MigrationTrackerOptions,\n RollbackOptions,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Row type for _smrt_schema_migrations table\n */\ninterface MigrationRow {\n id: string;\n name: string;\n version: string;\n checksum: string;\n applied_checksum?: string | null;\n applied_at: string;\n execution_time_ms?: unknown | null;\n package_name?: string | null;\n source_file?: string | null;\n status: string;\n error_message?: string | null;\n attempts: unknown;\n is_reversible: unknown;\n rolled_back_at?: string | null;\n applied_by?: string | null;\n batch: unknown | null;\n}\n\n/** Hydrate integer-backed system-table values at the database boundary. */\nfunction hydrateMigrationRow(row: MigrationRow): SchemaMigrationRecord {\n return {\n id: row.id,\n name: row.name,\n version: row.version,\n checksum: row.checksum,\n applied_checksum: row.applied_checksum ?? null,\n applied_at: new Date(row.applied_at),\n execution_time_ms:\n row.execution_time_ms === null || row.execution_time_ms === undefined\n ? null\n : toSafeInteger(row.execution_time_ms, 'Migration execution time'),\n attempts: toSafeInteger(row.attempts ?? 0, 'Migration attempts'),\n is_reversible: toSafeBooleanInteger(\n row.is_reversible ?? 1,\n 'Migration reversibility flag',\n ),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n package_name: row.package_name ?? null,\n source_file: row.source_file ?? null,\n status: row.status as MigrationStatus,\n error_message: row.error_message ?? null,\n applied_by: row.applied_by ?? null,\n batch:\n row.batch === null || row.batch === undefined\n ? null\n : toSafeInteger(row.batch, 'Migration batch'),\n };\n}\n\n/**\n * Default options for MigrationTracker\n */\nconst DEFAULT_OPTIONS = {\n lockTimeout: 30000,\n statementTimeout: 60000,\n useConcurrentIndexes: true,\n};\n\nclass AtomicMigrationRollback extends Error {\n constructor(\n public readonly results: MigrationResult[],\n public readonly failed: MigrationResult,\n ) {\n super(`Atomic migration batch failed at ${failed.name}`);\n }\n}\n\nconst CONCURRENT_INDEX_STATEMENT_RE =\n /(?:(?:CREATE\\s+(?:UNIQUE\\s+)?INDEX|DROP\\s+INDEX)\\s+CONCURRENTLY|REINDEX(?:\\s*\\([^)]*\\))?\\s+(?:INDEX|TABLE|SCHEMA|DATABASE|SYSTEM)\\s+CONCURRENTLY)/i;\n\n/**\n * Matches the leading clause of a `CREATE [UNIQUE] INDEX` statement and\n * captures the index name, whether it is written bare or double-quoted, and\n * regardless of the optional `CONCURRENTLY` / `IF NOT EXISTS` clauses.\n *\n * Used by the PostgreSQL concurrent-index mode to correlate a pending create\n * with an INVALID index left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` (issue #2362).\n */\nconst CREATE_INDEX_NAME_RE =\n /^\\s*CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:\"((?:[^\"]|\"\")+)\"|([A-Za-z_][\\w$]*))/i;\n\nfunction findConcurrentIndexStatement(\n definitions: MigrationDefinition[],\n): { migrationName: string; statement: string } | null {\n for (const definition of definitions) {\n const statement = definition.up.find((sql) =>\n CONCURRENT_INDEX_STATEMENT_RE.test(sql),\n );\n\n if (statement) {\n return {\n migrationName: definition.id,\n statement,\n };\n }\n }\n\n return null;\n}\n\n/**\n * Extract the index name from a `CREATE [UNIQUE] INDEX …` statement.\n *\n * Returns the unquoted identifier, or `null` when the statement is not a\n * CREATE INDEX (a drop, a reindex, or unrelated DDL).\n *\n * Exported for unit testing and because the CLI's index rollout needs the\n * same correlation to explain what it repaired.\n */\nexport function extractCreatedIndexName(sql: string): string | null {\n const match = CREATE_INDEX_NAME_RE.exec(sql);\n if (!match) {\n return null;\n }\n\n // Group 1 is the quoted form (with `\"\"` escapes), group 2 the bare form.\n return match[1] !== undefined ? match[1].replace(/\"\"/g, '\"') : match[2];\n}\n\n/**\n * Coerce a caller-supplied timeout to a usable millisecond count.\n *\n * Applied once, in the constructor, so every later read of\n * `this.options.lockTimeout` / `.statementTimeout` is already finite and\n * non-negative. Each timeout falls back to *its own* default — a bad\n * `lockTimeout` must not silently inherit the (much larger) statement-timeout\n * default and weaken the lock bound it was meant to impose.\n */\nfunction resolveTimeoutOption(\n value: number | undefined,\n fallback: number,\n): number {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0\n ? Math.trunc(value)\n : fallback;\n}\n\n/**\n * Render a millisecond timeout as a PostgreSQL interval literal.\n *\n * PostgreSQL reads `lock_timeout`/`statement_timeout` as milliseconds when\n * given a bare number, but the explicit `ms` unit keeps the emitted SQL\n * self-documenting in migration logs. `0` disables the timeout, which is\n * PostgreSQL's own semantic and therefore preserved verbatim.\n *\n * Input is already normalized by {@link resolveTimeoutOption}.\n */\nfunction formatPostgresTimeout(milliseconds: number): string {\n return `${Math.max(0, Math.trunc(milliseconds))}ms`;\n}\n\n/**\n * Parse a PostgreSQL timeout string into milliseconds.\n *\n * The implementation lives in `src/postgres-timeouts.ts`, the leaf module the\n * runtime pool bounds are built from (#2377), so `migrations.postgres.*` and a\n * runtime `timeouts` config are parsed by literally the same function rather\n * than by two copies that can drift. It is re-exported here — and from\n * `./index.ts` — because `@happyvertical/smrt-core/migrations` is the public\n * subpath `db:migrate` reads it through.\n */\nexport { parsePostgresTimeoutMs };\n\n/**\n * Prefix stamped on the `error_message` of every concurrent-index migration\n * whose transactional half has already committed.\n *\n * This is the resume marker: it is the only durable record that phase 1's\n * statements ran, so a retry can skip straight to the index build. It must\n * therefore be carried by *both* the interim write at the end of phase 1 and\n * every failure recorded during phase 2 — a lock timeout on the index build\n * leaves the non-index DDL committed just as surely as a crash does.\n */\nconst CONCURRENT_INDEX_PHASE1_MARKER =\n '[smrt: concurrent-index phase 1 committed]';\n\n/**\n * Interim `error_message` for a migration whose transactional half has\n * committed but whose `CONCURRENTLY` index build has not run yet.\n */\nconst CONCURRENT_INDEX_PENDING_MESSAGE = `${CONCURRENT_INDEX_PHASE1_MARKER} Concurrent index build pending: the non-index half of this migration committed but the CONCURRENTLY index DDL had not completed. Re-run db:migrate to finish it.`;\n\n/**\n * Statements split out of the atomic batch for PostgreSQL concurrent-index\n * mode, keyed by migration definition id.\n */\ntype ConcurrentIndexPlan = Map<\n string,\n { regular: string[]; concurrent: string[] }\n>;\n\n/**\n * Minimal pinned-session shape used by the concurrent-index phase.\n *\n * `CREATE INDEX CONCURRENTLY` must run outside a transaction, and the\n * `SET lock_timeout` / `SET statement_timeout` that bound it are *session*\n * state — on a pooled adapter a top-level `db.query` may land on a different\n * connection, so the SET would silently not apply to the DDL (issue #2362).\n */\ninterface MigrationSession {\n query: (\n sql: string,\n ...vars: unknown[]\n ) => Promise<{ rows: Record<string, unknown>[]; rowCount?: number }>;\n release: () => Promise<void>;\n}\n\n/**\n * MigrationTracker class\n *\n * Provides migration tracking and state management:\n * - Initializes the _smrt_schema_migrations tracking table\n * - Tracks applied migrations with checksums\n * - Provides idempotent migration application\n * - Supports rollback via DOWN scripts\n * - Handles PostgreSQL-specific concerns (CONCURRENTLY, lock timeout)\n */\nexport class MigrationTracker {\n private db: DatabaseInterface;\n private options: Required<Omit<MigrationTrackerOptions, 'db' | 'engineHint'>>;\n private engineHint?: string;\n private dbEngine: DatabaseEngine;\n /**\n * Memoized initialization promise. Storing the in-flight promise (rather\n * than a `boolean` flag set after the DDL completes) makes `initialize()`\n * safe under concurrency: multiple parallel callers all `await` the same\n * promise instead of independently re-running the DDL. On error the slot\n * is cleared so the next caller retries — a transient failure doesn't\n * permanently poison the instance.\n */\n private initializePromise: Promise<void> | null = null;\n private currentBatch: number | null = null;\n /**\n * Statements deferred out of the current atomic batch by PostgreSQL\n * concurrent-index mode. The parent tracker sets this on the\n * transaction-scoped tracker before phase 1 so `apply()` executes only the\n * transaction-safe statements and leaves the migration record in `running`;\n * the parent finalizes those records after the transaction commits and the\n * `CONCURRENTLY` DDL has run. `null` on every ordinary path.\n */\n private concurrentIndexPlan: ConcurrentIndexPlan | null = null;\n\n constructor(options: MigrationTrackerOptions) {\n this.db = options.db;\n this.options = {\n lockTimeout: resolveTimeoutOption(\n options.lockTimeout,\n DEFAULT_OPTIONS.lockTimeout,\n ),\n statementTimeout: resolveTimeoutOption(\n options.statementTimeout,\n DEFAULT_OPTIONS.statementTimeout,\n ),\n useConcurrentIndexes:\n options.useConcurrentIndexes ?? DEFAULT_OPTIONS.useConcurrentIndexes,\n };\n this.engineHint = options.engineHint;\n this.dbEngine = this.detectDbEngine();\n }\n\n /**\n * Detect the database engine. Honors an explicit `engineHint` (passed via\n * `MigrationTrackerOptions`) before falling back to URL parsing — this is\n * the same `detectEngine` contract used by `SchemaComparer` and the rest\n * of the migration stack, so the tracker stays consistent with the DDL\n * the orchestrator generated.\n */\n private detectDbEngine(): DatabaseEngine {\n const dbWithConfig = this.db as DatabaseInterface & {\n config?: { url?: string };\n };\n const url = this.db.url || dbWithConfig.config?.url || '';\n return detectEngine(url, this.engineHint);\n }\n\n /**\n * Get the current database engine\n */\n getEngine(): DatabaseEngine {\n return this.dbEngine;\n }\n\n /**\n * Initialize the migrations tracking table\n */\n async initialize(): Promise<void> {\n if (!this.initializePromise) {\n this.initializePromise = this.runInitializeDdl().catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n private async runInitializeDdl(): Promise<void> {\n // Parse and execute each statement in the DDL\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n this.dbEngine,\n )\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n\n for (const statement of statements) {\n await this.db.query(statement);\n }\n\n await assertPostgresSystemTimestampsCurrent(this.db, this.engineHint);\n }\n\n /**\n * Get all applied migrations ordered by applied_at\n */\n async getAppliedMigrations(): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE status = 'completed' ORDER BY applied_at ASC`,\n );\n\n return (result.rows as MigrationRow[]).map(hydrateMigrationRow);\n }\n\n /**\n * Get pending migrations (defined but not applied)\n */\n async getPendingMigrations(\n definitions: MigrationDefinition[],\n ): Promise<MigrationDefinition[]> {\n const applied = await this.getAppliedMigrations();\n const appliedNames = new Set(applied.map((m) => m.name));\n return definitions.filter((d) => !appliedNames.has(d.id));\n }\n\n /**\n * Check if a migration has been applied\n */\n async isApplied(migrationId: string): Promise<boolean> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT 1 FROM _smrt_schema_migrations WHERE name = ? AND status = 'completed' LIMIT 1`,\n migrationId,\n );\n\n return result.rows.length > 0;\n }\n\n /**\n * Get a single migration record by name\n */\n async getMigration(name: string): Promise<SchemaMigrationRecord | null> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE name = ? LIMIT 1`,\n name,\n );\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as MigrationRow;\n return hydrateMigrationRow(row);\n }\n\n /**\n * Get the next batch number\n */\n async getNextBatch(): Promise<number> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT MAX(batch) as max_batch FROM _smrt_schema_migrations`,\n );\n\n const row = result.rows[0] as { max_batch?: unknown } | undefined;\n const maxBatch = toSafeInteger(row?.max_batch ?? 0, 'Migration batch');\n return toSafeInteger(maxBatch + 1, 'Next migration batch');\n }\n\n /**\n * Detect drift between migration definitions and applied migrations\n */\n async detectDrift(\n definitions: MigrationDefinition[],\n ): Promise<DriftReport[]> {\n const applied = await this.getAppliedMigrations();\n const reports: DriftReport[] = [];\n\n const appliedMap = new Map(applied.map((m) => [m.name, m]));\n const definitionMap = new Map(definitions.map((d) => [d.id, d]));\n\n // Check each applied migration against definitions\n for (const record of applied) {\n const definition = definitionMap.get(record.name);\n\n if (!definition) {\n // Migration applied but definition is missing\n reports.push({\n migration_name: record.name,\n expected_checksum: 'N/A',\n actual_checksum: record.checksum,\n drift_type: 'unknown_in_db',\n recommendation:\n 'Migration exists in database but definition is missing. Manual cleanup may be required.',\n });\n continue;\n }\n\n // Check checksum\n const currentChecksum = computeChecksum(definition.up);\n const verification = verifyChecksum(\n currentChecksum,\n record.checksum,\n record.applied_checksum,\n );\n\n if (!verification.valid) {\n const driftType = verification.drift ?? 'db_modified';\n reports.push({\n migration_name: record.name,\n expected_checksum: record.checksum,\n actual_checksum: currentChecksum,\n drift_type: driftType,\n recommendation:\n driftType === 'file_modified'\n ? 'Migration file was modified after application. Create a new migration instead.'\n : 'Database schema was modified outside of migrations. Review and reconcile manually.',\n });\n }\n }\n\n return reports;\n }\n\n /**\n * Apply a single migration\n */\n async apply(\n definition: MigrationDefinition,\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.up);\n const force = Boolean(\n options.force || options.forceMigrations?.includes(definition.id),\n );\n const hasScopedForce = Boolean(options.forceMigrations?.length);\n\n // Check if migration record already exists\n const existing = await this.getMigration(definition.id);\n\n if (existing) {\n // Handle based on existing status\n switch (existing.status) {\n case 'completed':\n if (!force && existing.checksum !== checksum) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} checksum mismatch. Was already applied with different checksum. Use --force-migration ${definition.id} to override only this migration, or --force to override the whole batch.`,\n };\n }\n\n if (!force && !options.reconcile) {\n // Already applied, skip\n return {\n success: true,\n applied: false,\n skipped: true,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n };\n }\n break;\n\n case 'failed':\n if (!force && (!options.reconcile || hasScopedForce)) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} previously failed: ${existing.error_message || 'Unknown error'}. Use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'running':\n if (!force) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} is currently running or was interrupted. If the previous run crashed, use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'rolled_back':\n // Rolled back migrations can be re-applied without --force\n break;\n }\n }\n\n if (options.dryRun) {\n return {\n success: true,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n };\n }\n\n // Get batch number\n if (this.currentBatch === null) {\n this.currentBatch = await this.getNextBatch();\n }\n\n // Determine if we're updating an existing record or inserting new\n const id = existing?.id || randomUUID();\n const attempts = toSafeInteger(\n (existing?.attempts ?? 0) + 1,\n 'Migration attempts',\n );\n\n if (existing) {\n // Update existing record to 'running'\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'running', checksum = ?, attempts = ?, error_message = NULL, batch = ?, applied_by = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n checksum,\n attempts,\n this.currentBatch,\n hostname(),\n id,\n );\n } else {\n // Insert new record\n await this.db.query(\n `INSERT INTO _smrt_schema_migrations\n (id, name, version, checksum, status, attempts, is_reversible, package_name, source_file, applied_by, batch)\n VALUES (?, ?, ?, ?, 'running', 1, ?, ?, ?, ?, ?)`,\n id,\n definition.id,\n definition.version,\n checksum,\n (definition.is_reversible ?? definition.down.length > 0) ? 1 : 0,\n definition.package_name ?? null,\n definition.source_file ?? null,\n hostname(),\n this.currentBatch,\n );\n }\n\n // PostgreSQL concurrent-index mode splits this definition: the\n // transaction-safe statements run here, the CONCURRENTLY index DDL runs\n // after the batch commits.\n const split = this.concurrentIndexPlan?.get(definition.id);\n const deferredConcurrent = (split?.concurrent.length ?? 0) > 0;\n\n // A retry of an interrupted concurrent-index migration must not re-run the\n // half that already committed. The marker on the previous attempt's\n // `error_message` says it did, and the checksum says it was this same\n // definition. Without this, a definition that mixes non-idempotent DDL\n // (`ALTER TABLE … ADD COLUMN`, which the differ emits without\n // `IF NOT EXISTS`) with index DDL would fail on \"column already exists\"\n // forever, never reaching the index build it was retrying for.\n const phase1AlreadyCommitted =\n deferredConcurrent &&\n existing?.status === 'failed' &&\n existing.checksum === checksum &&\n typeof existing.error_message === 'string' &&\n existing.error_message.startsWith(CONCURRENT_INDEX_PHASE1_MARKER);\n\n try {\n // Execute the migration\n await this.executeStatements(\n phase1AlreadyCommitted ? [] : split ? split.regular : definition.up,\n options,\n );\n\n const executionTime = Date.now() - startTime;\n\n if (deferredConcurrent) {\n // Commit this record as `failed`, not `completed` and not `running`.\n // The index does not exist yet, so `completed` would be a lie; and\n // unlike the single-transaction path — where a crash rolls the record\n // insert back with the DDL — this row is about to be committed, so a\n // crash before the concurrent phase finishes would strand a `running`\n // record that the next `db:migrate` can only clear with\n // `--force-migration`. `failed` is retryable under the CLI's\n // reconciling re-run, which is what an interrupted index rollout\n // needs. The concurrent phase overwrites this a moment later.\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', execution_time_ms = ?, error_message = ?\n WHERE id = ?`,\n executionTime,\n CONCURRENT_INDEX_PENDING_MESSAGE,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n }\n\n // Mark as completed\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n executionTime,\n checksum,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const migrationError =\n error instanceof Error ? error : new Error(String(error));\n\n // Mark as failed. In an outer atomic Postgres transaction, a DDL error\n // aborts the transaction and this status write must be skipped so the\n // caller can roll back the whole batch while preserving the root error.\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE id = ?`,\n migrationError.message,\n id,\n );\n } catch (persistError) {\n // Preserve the migration failure; status persistence is secondary and\n // may be impossible until the surrounding transaction rolls back.\n const persistMessage =\n persistError instanceof Error\n ? persistError.message\n : String(persistError);\n logger.error(\n `Failed to persist failed status for migration ${definition.id}: ${persistMessage}`,\n );\n }\n\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: migrationError,\n };\n }\n }\n\n /**\n * Apply multiple migrations in order.\n *\n * On PostgreSQL the atomic path always bounds itself with\n * `SET LOCAL lock_timeout` / `SET LOCAL statement_timeout` so a migration\n * that queues behind a long-running writer fails fast and rolls back\n * instead of stalling every writer behind its already-held locks\n * (issue #2362).\n *\n * `options.postgresSafe` additionally enables **concurrent-index mode**:\n * non-index DDL still runs in the one atomic transaction, then every index\n * statement runs `CONCURRENTLY` on a pinned session after that transaction\n * commits. That mode trades atomicity for availability — a failure part way\n * through the index phase leaves the committed non-index DDL in place, and\n * the unfinished index migrations are recorded `failed` so the next run\n * retries them, resuming at the index phase rather than re-running the\n * non-index statements that already committed.\n */\n async applyAll(\n definitions: MigrationDefinition[],\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult[]> {\n if (options.atomic && !options.dryRun) {\n if (!this.db.transaction) {\n throw new Error(\n 'Atomic migration batches require a database adapter with transaction support.',\n );\n }\n\n const isPostgres = this.dbEngine === 'postgres';\n const concurrentIndexMode = isPostgres && Boolean(options.postgresSafe);\n const plan = concurrentIndexMode\n ? buildConcurrentIndexPlan(\n definitions,\n this.options.useConcurrentIndexes,\n )\n : null;\n\n // Outside concurrent-index mode a CONCURRENTLY statement can only fail:\n // PostgreSQL rejects it inside a transaction block. Fail before opening\n // the transaction so the error names the offending migration.\n if (isPostgres && !concurrentIndexMode) {\n const concurrentStatement = findConcurrentIndexStatement(definitions);\n if (concurrentStatement) {\n throw new Error(\n `Atomic migration batch cannot include CONCURRENTLY index DDL in ${concurrentStatement.migrationName}; PostgreSQL forbids CONCURRENTLY inside a transaction. Re-run with PostgreSQL concurrent-index mode (smrt db:migrate --postgres-safe) to build those indexes outside the batch.`,\n );\n }\n }\n\n const deferred = plan && plan.size > 0 ? plan : null;\n\n try {\n const committed = await this.db.transaction(async (tx) => {\n const txDb = {\n ...tx,\n transaction: async <T>(\n callback: (transactionDb: DatabaseInterface) => Promise<T>,\n ) => callback(tx as DatabaseInterface),\n } as DatabaseInterface;\n\n // Bound the whole batch. `SET LOCAL` reverts at COMMIT/ROLLBACK, so\n // this cannot leak onto a pooled connection's next checkout.\n if (isPostgres) {\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n }\n\n const txTracker = new MigrationTracker({\n db: txDb,\n lockTimeout: this.options.lockTimeout,\n statementTimeout: this.options.statementTimeout,\n useConcurrentIndexes: false,\n });\n txTracker.concurrentIndexPlan = deferred;\n const results = await txTracker.applyAll(definitions, {\n ...options,\n atomic: false,\n continueOnError: false,\n postgresSafe: false,\n // Deferred migrations report progress from the concurrent phase\n // once they have actually finished, not when phase 1 commits.\n onProgress: options.onProgress\n ? (result) => {\n if (!deferred?.has(result.name)) {\n options.onProgress?.(result);\n }\n }\n : undefined,\n });\n const failed = results.find((result) => !result.success);\n\n if (failed) {\n throw new AtomicMigrationRollback(results, failed);\n }\n\n return results;\n });\n\n if (!deferred) {\n return committed;\n }\n\n return await this.applyDeferredConcurrentIndexes(\n committed,\n deferred,\n options,\n );\n } catch (error) {\n if (error instanceof AtomicMigrationRollback) {\n return error.results.map((result) => {\n if (!result.success || !result.applied) {\n return result;\n }\n\n return {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n rolled_back: true,\n error: new Error(\n `Rolled back because migration ${error.failed.name} failed`,\n ),\n };\n });\n }\n\n throw error;\n }\n }\n\n const results: MigrationResult[] = [];\n this.currentBatch = await this.getNextBatch();\n\n for (const definition of definitions) {\n const result = await this.apply(definition, options);\n results.push(result);\n options.onProgress?.(result);\n\n if (!result.success && !options.continueOnError) {\n break;\n }\n }\n\n this.currentBatch = null;\n return results;\n }\n\n /**\n * Phase 2 of PostgreSQL concurrent-index mode.\n *\n * Runs every deferred index statement on one pinned session, outside the\n * committed transaction, bounded by session `lock_timeout` /\n * `statement_timeout`. INVALID indexes left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` are dropped first so the retry rebuilds them\n * rather than skipping over a stump that reads as present.\n *\n * On failure the batch stops: the failing migration and every migration it\n * did not reach are recorded `failed`, which the CLI's reconciling\n * `db:migrate` retries on the next run.\n */\n private async applyDeferredConcurrentIndexes(\n committed: MigrationResult[],\n plan: ConcurrentIndexPlan,\n options: ApplyMigrationsOptions,\n ): Promise<MigrationResult[]> {\n const results = [...committed];\n const session = await this.acquireMigrationSession();\n\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n let stoppedBy: string | null = null;\n\n for (let index = 0; index < results.length; index++) {\n const result = results[index];\n const split = plan.get(result.name);\n if (!split || split.concurrent.length === 0) {\n continue;\n }\n\n // A migration phase 1 skipped (already applied, checksum matched) is\n // already `completed` in the tracking table — its index exists and\n // must not be rebuilt or have its record rewritten.\n if (result.skipped) {\n continue;\n }\n\n if (stoppedBy) {\n const error = new Error(\n `Not attempted: concurrent index phase stopped after ${stoppedBy} failed`,\n );\n await this.recordConcurrentIndexFailure(result.name, error.message);\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n error,\n };\n options.onProgress?.(results[index]);\n continue;\n }\n\n const startedAt = Date.now();\n\n try {\n for (const sql of split.concurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n // An INVALID index is not usable by the planner and blocks the\n // name, but `CREATE INDEX … IF NOT EXISTS` treats it as present.\n // Drop it so the create below actually rebuilds it.\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n\n // Floor at 1ms: callers (the CLI progress reporter, the applied vs\n // skipped counters) read `execution_time_ms === 0` as \"nothing ran\".\n const executionTime = Math.max(\n 1,\n (result.execution_time_ms ?? 0) + (Date.now() - startedAt),\n );\n await this.recordConcurrentIndexSuccess(\n result.name,\n result.checksum,\n executionTime,\n );\n results[index] = {\n ...result,\n success: true,\n applied: true,\n skipped: false,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const indexError =\n error instanceof Error ? error : new Error(String(error));\n if (!options.continueOnError) {\n stoppedBy = result.name;\n }\n await this.recordConcurrentIndexFailure(\n result.name,\n indexError.message,\n );\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n execution_time_ms: Date.now() - startedAt,\n error: indexError,\n };\n }\n\n options.onProgress?.(results[index]);\n }\n\n return results;\n } finally {\n await session.release();\n }\n }\n\n /**\n * Pin one connection for the concurrent-index phase.\n *\n * PostgreSQL pools hand each top-level `query` an arbitrary connection, so\n * a `SET lock_timeout` issued through the pool may not apply to the DDL\n * that follows. Adapters that expose `acquireSession` give us a pinned\n * connection; anything else (SQLite, DuckDB, test doubles) is\n * single-connection anyway and falls back to the plain interface.\n */\n private async acquireMigrationSession(): Promise<MigrationSession> {\n if (typeof this.db.acquireSession === 'function') {\n const session = await this.db.acquireSession();\n return {\n query: (sql, ...vars) => session.query(sql, ...vars),\n release: () => session.release(),\n };\n }\n\n return {\n query: (sql, ...vars) => this.db.query(sql, ...vars),\n release: async () => {},\n };\n }\n\n /**\n * Read the names of INVALID indexes that an unqualified index name in this\n * session would actually resolve to.\n *\n * `pg_indexes` (what the SQL adapter's introspection reads) lists an\n * INVALID index exactly like a healthy one, so the schema differ believes a\n * failed `CREATE INDEX CONCURRENTLY` succeeded. `pg_index.indisvalid` is the\n * authoritative flag.\n *\n * Scoped to `current_schemas(false)` — the session's search path — because\n * migration DDL names indexes unqualified, and so does the\n * `DROP INDEX CONCURRENTLY IF EXISTS \"<name>\"` repair. Scanning every schema\n * would let an unrelated invalid index of the same name in another schema\n * trigger a drop that then resolves to a *different* object. Where the same\n * name appears in several search-path schemas, the earliest on the path wins\n * — the same rule PostgreSQL uses to resolve the name.\n *\n * A role without catalog access degrades to \"no known invalid indexes\"\n * rather than aborting the migration.\n */\n private async readInvalidIndexNames(\n session: MigrationSession,\n ): Promise<Set<string>> {\n const names = new Set<string>();\n\n try {\n const result = await session.query(\n `SELECT DISTINCT ON (c.relname) c.relname AS index_name\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE i.indisvalid = false\n AND n.nspname = ANY (current_schemas(false))\n ORDER BY c.relname,\n array_position(current_schemas(false), n.nspname::text)`,\n );\n\n for (const row of result.rows) {\n const name = (row as { index_name?: unknown }).index_name;\n if (typeof name === 'string') {\n names.add(name);\n }\n }\n } catch (error) {\n logger.warn(\n `Could not read pg_index.indisvalid; skipping INVALID index repair: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n return names;\n }\n\n private async recordConcurrentIndexSuccess(\n name: string,\n checksum: string,\n executionTime: number,\n ): Promise<void> {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, error_message = NULL, applied_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n executionTime,\n checksum,\n name,\n );\n }\n\n private async recordConcurrentIndexFailure(\n name: string,\n message: string,\n ): Promise<void> {\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE name = ?`,\n // Keep the resume marker: this failure happened *after* phase 1\n // committed, so the retry must skip the non-index statements too.\n `${CONCURRENT_INDEX_PHASE1_MARKER} ${message}`,\n name,\n );\n } catch (persistError) {\n logger.error(\n `Failed to persist failed status for migration ${name}: ${\n persistError instanceof Error\n ? persistError.message\n : String(persistError)\n }`,\n );\n }\n }\n\n /**\n * Rollback a migration\n */\n async rollback(\n name: string,\n definition: MigrationDefinition,\n options: RollbackOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.down);\n\n const record = await this.getMigration(name);\n if (!record) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(`Migration ${name} not found in database`),\n };\n }\n\n if (record.status !== 'completed') {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not in completed state (current: ${record.status})`,\n ),\n };\n }\n\n if (!definition.down.length) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not reversible (no down statements)`,\n ),\n };\n }\n\n if (options.dryRun) {\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n }\n\n try {\n // Execute DOWN statements\n await this.executeStatements(definition.down, {});\n\n // Mark as rolled back\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'rolled_back', rolled_back_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n name,\n );\n\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n } catch (error) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: error instanceof Error ? error : new Error(String(error)),\n rolled_back: false,\n };\n }\n }\n\n /**\n * Get migration history with optional filtering\n */\n async getHistory(\n options: { limit?: number; since?: Date; status?: MigrationStatus } = {},\n ): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n let sql = `SELECT * FROM _smrt_schema_migrations WHERE 1=1`;\n const params: unknown[] = [];\n\n if (options.status) {\n sql += ` AND status = ?`;\n params.push(options.status);\n }\n\n if (options.since) {\n sql += ` AND applied_at >= ?`;\n params.push(options.since.toISOString());\n }\n\n sql += ` ORDER BY applied_at DESC`;\n\n if (options.limit) {\n sql += ` LIMIT ?`;\n params.push(options.limit);\n }\n\n const result = await this.db.query(sql, ...params);\n\n return (result.rows as MigrationRow[]).map(hydrateMigrationRow);\n }\n\n /**\n * Execute SQL statements with appropriate transaction handling\n */\n private async executeStatements(\n statements: string[],\n options: ApplyMigrationsOptions,\n ): Promise<void> {\n if (this.dbEngine === 'postgres' && options.postgresSafe) {\n await this.executePostgresStatements(statements);\n } else {\n await this.executeWithTransaction(statements);\n }\n }\n\n /**\n * Execute statements within a transaction (SQLite/DuckDB)\n */\n private async executeWithTransaction(statements: string[]): Promise<void> {\n if (this.db.transaction) {\n await this.db.transaction(async (tx) => {\n for (const sql of statements) {\n await tx.query(sql);\n }\n });\n } else {\n // Fallback to sequential execution\n for (const sql of statements) {\n await this.db.query(sql);\n }\n }\n }\n\n /**\n * Execute statements with PostgreSQL-specific handling\n *\n * - CONCURRENTLY statements run outside transaction\n * - Regular statements run in transaction with lock_timeout\n *\n * Both CREATE and DROP variants of CONCURRENTLY must be detected — Postgres\n * forbids `DROP INDEX CONCURRENTLY` inside a transaction block, just like\n * the create variant. Issue #1165: the migration generator emits\n * `DROP INDEX CONCURRENTLY` for shape-drift recreates and orphan-index\n * cleanups, so the regex needs to cover both keywords.\n */\n private async executePostgresStatements(statements: string[]): Promise<void> {\n const { concurrent: finalConcurrent, regular: finalRegular } =\n planPostgresStatements(statements, !!this.options.useConcurrentIndexes);\n\n // CONCURRENTLY statements come from `planPostgresStatements`, which\n // also pushes any post-transform CREATE/DROP CONCURRENTLY into the\n // outside-transaction set. The original `concurrentStatements` list\n // is folded in there too so callers below see one merged stream.\n\n // Execute regular statements in transaction with timeouts\n if (finalRegular.length > 0 && this.db.transaction) {\n await this.db.transaction(async (tx) => {\n // Set session-level timeouts\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n for (const sql of finalRegular) {\n await tx.query(sql);\n }\n });\n } else if (finalRegular.length > 0) {\n for (const sql of finalRegular) {\n await this.db.query(sql);\n }\n }\n\n if (finalConcurrent.length === 0) {\n return;\n }\n\n // Execute CONCURRENTLY statements outside the transaction, on one pinned\n // connection so `SET lock_timeout` / `SET statement_timeout` actually\n // govern the DDL that follows them (a pooled `db.query` may not reuse the\n // connection the SET landed on).\n const session = await this.acquireMigrationSession();\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n\n for (const sql of finalConcurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n } finally {\n await session.release();\n }\n }\n}\n\n/**\n * Quote an index identifier for PostgreSQL, escaping embedded double quotes.\n */\nfunction quoteIndexName(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Build the PostgreSQL concurrent-index plan for an atomic batch.\n *\n * Returns one entry per migration definition that has at least one statement\n * which must leave the transaction, mapping it to the transaction-safe\n * statements (phase 1) and the `CONCURRENTLY` statements (phase 2).\n * Definitions with nothing to defer are omitted so the caller can treat\n * `plan.has(id)` as \"this migration finishes in phase 2\".\n *\n * Exported for unit testing.\n */\nexport function buildConcurrentIndexPlan(\n definitions: MigrationDefinition[],\n useConcurrentIndexes: boolean,\n): ConcurrentIndexPlan {\n const plan: ConcurrentIndexPlan = new Map();\n\n for (const definition of definitions) {\n const split = planPostgresStatements(definition.up, useConcurrentIndexes);\n if (split.concurrent.length > 0) {\n plan.set(definition.id, split);\n }\n }\n\n return plan;\n}\n\n/**\n * Split a batch of SQL statements into a transaction-safe set and a\n * concurrent set, applying `--postgres-safe` rewriting if requested.\n *\n * Exported for unit testing and to give external callers a way to\n * reason about what the tracker would execute in PostgreSQL safe mode.\n *\n * Rules:\n * - Statements already containing `CONCURRENTLY` for CREATE/DROP INDEX or\n * REINDEX operations always go to the concurrent set — Postgres rejects\n * them inside transactions.\n * - When `useConcurrentIndexes` is true, plain `CREATE INDEX` and plain\n * `DROP INDEX` are rewritten to use `CONCURRENTLY`, then routed to\n * the concurrent set. This is what the CLI `--postgres-safe` flag and\n * the auto-migrate path rely on for issue #1165's shape-drift drops.\n * - All other statements stay in the regular (transaction) set.\n */\nexport function planPostgresStatements(\n statements: string[],\n useConcurrentIndexes: boolean,\n): { concurrent: string[]; regular: string[] } {\n const concurrentRegex = CONCURRENT_INDEX_STATEMENT_RE;\n\n const concurrent: string[] = [];\n const regular: string[] = [];\n\n for (const sql of statements) {\n if (concurrentRegex.test(sql)) {\n concurrent.push(sql);\n continue;\n }\n if (useConcurrentIndexes) {\n if (/CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+/i,\n 'CREATE $1INDEX CONCURRENTLY ',\n ),\n );\n continue;\n }\n if (/DROP\\s+INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(/DROP\\s+INDEX\\s+/i, 'DROP INDEX CONCURRENTLY '),\n );\n continue;\n }\n }\n regular.push(sql);\n }\n\n return { concurrent, regular };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tracker.js","names":[],"sources":["../../src/migrations/tracker.ts"],"sourcesContent":["/**\n * Migration Tracker\n *\n * Manages migration state, tracking which migrations have been applied,\n * and providing idempotent migration application.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { hostname } from 'node:os';\nimport { createLogger } from '@happyvertical/logger';\nimport { parsePostgresTimeoutMs } from '../postgres-timeouts.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { NULL_EQUAL_INDEX_MARKER } from '../schema/ddl/null-equal-index.js';\n// DatabaseEngine comes from the DDL layer (the json-inclusive type that\n// `detectEngine` actually returns), matching `SchemaComparer` in differ.ts.\n// The migration stack supports the JSON adapter post-R11, so the tracker's\n// engine type must include 'json' too — the narrower `migrations/types`\n// alias would reject it.\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport type {\n DriftReport,\n MigrationDefinition,\n MigrationResult,\n MigrationStatus,\n SchemaMigrationRecord,\n} from '../schema/types.js';\nimport { assertPostgresSystemTimestampsCurrent } from '../system/compatibility.js';\nimport {\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n getSystemTableDDLForEngine,\n} from '../system/schema.js';\nimport { toSafeBooleanInteger, toSafeInteger } from '../utils/safe-integer.js';\nimport { computeChecksum, verifyChecksum } from './checksum.js';\nimport type {\n ApplyMigrationsOptions,\n DatabaseInterface,\n MigrationTrackerOptions,\n RollbackOptions,\n} from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Row type for _smrt_schema_migrations table\n */\ninterface MigrationRow {\n id: string;\n name: string;\n version: string;\n checksum: string;\n applied_checksum?: string | null;\n applied_at: string;\n execution_time_ms?: unknown | null;\n package_name?: string | null;\n source_file?: string | null;\n status: string;\n error_message?: string | null;\n attempts: unknown;\n is_reversible: unknown;\n rolled_back_at?: string | null;\n applied_by?: string | null;\n batch: unknown | null;\n}\n\n/** Hydrate integer-backed system-table values at the database boundary. */\nfunction hydrateMigrationRow(row: MigrationRow): SchemaMigrationRecord {\n return {\n id: row.id,\n name: row.name,\n version: row.version,\n checksum: row.checksum,\n applied_checksum: row.applied_checksum ?? null,\n applied_at: new Date(row.applied_at),\n execution_time_ms:\n row.execution_time_ms === null || row.execution_time_ms === undefined\n ? null\n : toSafeInteger(row.execution_time_ms, 'Migration execution time'),\n attempts: toSafeInteger(row.attempts ?? 0, 'Migration attempts'),\n is_reversible: toSafeBooleanInteger(\n row.is_reversible ?? 1,\n 'Migration reversibility flag',\n ),\n rolled_back_at: row.rolled_back_at ? new Date(row.rolled_back_at) : null,\n package_name: row.package_name ?? null,\n source_file: row.source_file ?? null,\n status: row.status as MigrationStatus,\n error_message: row.error_message ?? null,\n applied_by: row.applied_by ?? null,\n batch:\n row.batch === null || row.batch === undefined\n ? null\n : toSafeInteger(row.batch, 'Migration batch'),\n };\n}\n\n/**\n * Default options for MigrationTracker\n */\nconst DEFAULT_OPTIONS = {\n lockTimeout: 30000,\n statementTimeout: 60000,\n useConcurrentIndexes: true,\n};\n\nclass AtomicMigrationRollback extends Error {\n constructor(\n public readonly results: MigrationResult[],\n public readonly failed: MigrationResult,\n ) {\n super(`Atomic migration batch failed at ${failed.name}`);\n }\n}\n\nconst CONCURRENT_INDEX_STATEMENT_RE =\n /(?:(?:CREATE\\s+(?:UNIQUE\\s+)?INDEX|DROP\\s+INDEX)\\s+CONCURRENTLY|REINDEX(?:\\s*\\([^)]*\\))?\\s+(?:INDEX|TABLE|SCHEMA|DATABASE|SYSTEM)\\s+CONCURRENTLY)/i;\n\n/**\n * Matches the leading clause of a `CREATE [UNIQUE] INDEX` statement and\n * captures the index name, whether it is written bare or double-quoted, and\n * regardless of the optional `CONCURRENTLY` / `IF NOT EXISTS` clauses.\n *\n * Used by the PostgreSQL concurrent-index mode to correlate a pending create\n * with an INVALID index left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` (issue #2362).\n */\nconst CREATE_INDEX_NAME_RE =\n /^\\s*CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:\"((?:[^\"]|\"\")+)\"|([A-Za-z_][\\w$]*))/i;\n\nfunction findConcurrentIndexStatement(\n definitions: MigrationDefinition[],\n): { migrationName: string; statement: string } | null {\n for (const definition of definitions) {\n const statement = definition.up.find((sql) =>\n CONCURRENT_INDEX_STATEMENT_RE.test(sql),\n );\n\n if (statement) {\n return {\n migrationName: definition.id,\n statement,\n };\n }\n }\n\n return null;\n}\n\n/**\n * Extract the index name from a `CREATE [UNIQUE] INDEX …` statement.\n *\n * Returns the unquoted identifier, or `null` when the statement is not a\n * CREATE INDEX (a drop, a reindex, or unrelated DDL).\n *\n * Exported for unit testing and because the CLI's index rollout needs the\n * same correlation to explain what it repaired.\n */\nexport function extractCreatedIndexName(sql: string): string | null {\n const match = CREATE_INDEX_NAME_RE.exec(sql);\n if (!match) {\n return null;\n }\n\n // Group 1 is the quoted form (with `\"\"` escapes), group 2 the bare form.\n return match[1] !== undefined ? match[1].replace(/\"\"/g, '\"') : match[2];\n}\n\n/**\n * Coerce a caller-supplied timeout to a usable millisecond count.\n *\n * Applied once, in the constructor, so every later read of\n * `this.options.lockTimeout` / `.statementTimeout` is already finite and\n * non-negative. Each timeout falls back to *its own* default — a bad\n * `lockTimeout` must not silently inherit the (much larger) statement-timeout\n * default and weaken the lock bound it was meant to impose.\n */\nfunction resolveTimeoutOption(\n value: number | undefined,\n fallback: number,\n): number {\n return typeof value === 'number' && Number.isFinite(value) && value >= 0\n ? Math.trunc(value)\n : fallback;\n}\n\n/**\n * Render a millisecond timeout as a PostgreSQL interval literal.\n *\n * PostgreSQL reads `lock_timeout`/`statement_timeout` as milliseconds when\n * given a bare number, but the explicit `ms` unit keeps the emitted SQL\n * self-documenting in migration logs. `0` disables the timeout, which is\n * PostgreSQL's own semantic and therefore preserved verbatim.\n *\n * Input is already normalized by {@link resolveTimeoutOption}.\n */\nfunction formatPostgresTimeout(milliseconds: number): string {\n return `${Math.max(0, Math.trunc(milliseconds))}ms`;\n}\n\n/**\n * Parse a PostgreSQL timeout string into milliseconds.\n *\n * The implementation lives in `src/postgres-timeouts.ts`, the leaf module the\n * runtime pool bounds are built from (#2377), so `migrations.postgres.*` and a\n * runtime `timeouts` config are parsed by literally the same function rather\n * than by two copies that can drift. It is re-exported here — and from\n * `./index.ts` — because `@happyvertical/smrt-core/migrations` is the public\n * subpath `db:migrate` reads it through.\n */\nexport { parsePostgresTimeoutMs };\n\n/**\n * Prefix stamped on the `error_message` of every concurrent-index migration\n * whose transactional half has already committed.\n *\n * This is the resume marker: it is the only durable record that phase 1's\n * statements ran, so a retry can skip straight to the index build. It must\n * therefore be carried by *both* the interim write at the end of phase 1 and\n * every failure recorded during phase 2 — a lock timeout on the index build\n * leaves the non-index DDL committed just as surely as a crash does.\n */\nconst CONCURRENT_INDEX_PHASE1_MARKER =\n '[smrt: concurrent-index phase 1 committed]';\n\n/**\n * Interim `error_message` for a migration whose transactional half has\n * committed but whose `CONCURRENTLY` index build has not run yet.\n */\nconst CONCURRENT_INDEX_PENDING_MESSAGE = `${CONCURRENT_INDEX_PHASE1_MARKER} Concurrent index build pending: the non-index half of this migration committed but the CONCURRENTLY index DDL had not completed. Re-run db:migrate to finish it.`;\n\n/**\n * Statements split out of the atomic batch for PostgreSQL concurrent-index\n * mode, keyed by migration definition id.\n */\ntype ConcurrentIndexPlan = Map<\n string,\n { regular: string[]; concurrent: string[] }\n>;\n\n/**\n * Minimal pinned-session shape used by the concurrent-index phase.\n *\n * `CREATE INDEX CONCURRENTLY` must run outside a transaction, and the\n * `SET lock_timeout` / `SET statement_timeout` that bound it are *session*\n * state — on a pooled adapter a top-level `db.query` may land on a different\n * connection, so the SET would silently not apply to the DDL (issue #2362).\n */\ninterface MigrationSession {\n query: (\n sql: string,\n ...vars: unknown[]\n ) => Promise<{ rows: Record<string, unknown>[]; rowCount?: number }>;\n release: () => Promise<void>;\n}\n\n/**\n * MigrationTracker class\n *\n * Provides migration tracking and state management:\n * - Initializes the _smrt_schema_migrations tracking table\n * - Tracks applied migrations with checksums\n * - Provides idempotent migration application\n * - Supports rollback via DOWN scripts\n * - Handles PostgreSQL-specific concerns (CONCURRENTLY, lock timeout)\n */\nexport class MigrationTracker {\n private db: DatabaseInterface;\n private options: Required<Omit<MigrationTrackerOptions, 'db' | 'engineHint'>>;\n private engineHint?: string;\n private dbEngine: DatabaseEngine;\n /**\n * Memoized initialization promise. Storing the in-flight promise (rather\n * than a `boolean` flag set after the DDL completes) makes `initialize()`\n * safe under concurrency: multiple parallel callers all `await` the same\n * promise instead of independently re-running the DDL. On error the slot\n * is cleared so the next caller retries — a transient failure doesn't\n * permanently poison the instance.\n */\n private initializePromise: Promise<void> | null = null;\n private currentBatch: number | null = null;\n /**\n * Statements deferred out of the current atomic batch by PostgreSQL\n * concurrent-index mode. The parent tracker sets this on the\n * transaction-scoped tracker before phase 1 so `apply()` executes only the\n * transaction-safe statements and leaves the migration record in `running`;\n * the parent finalizes those records after the transaction commits and the\n * `CONCURRENTLY` DDL has run. `null` on every ordinary path.\n */\n private concurrentIndexPlan: ConcurrentIndexPlan | null = null;\n\n constructor(options: MigrationTrackerOptions) {\n this.db = options.db;\n this.options = {\n lockTimeout: resolveTimeoutOption(\n options.lockTimeout,\n DEFAULT_OPTIONS.lockTimeout,\n ),\n statementTimeout: resolveTimeoutOption(\n options.statementTimeout,\n DEFAULT_OPTIONS.statementTimeout,\n ),\n useConcurrentIndexes:\n options.useConcurrentIndexes ?? DEFAULT_OPTIONS.useConcurrentIndexes,\n };\n this.engineHint = options.engineHint;\n this.dbEngine = this.detectDbEngine();\n }\n\n /**\n * Detect the database engine. Honors an explicit `engineHint` (passed via\n * `MigrationTrackerOptions`) before falling back to URL parsing — this is\n * the same `detectEngine` contract used by `SchemaComparer` and the rest\n * of the migration stack, so the tracker stays consistent with the DDL\n * the orchestrator generated.\n */\n private detectDbEngine(): DatabaseEngine {\n const dbWithConfig = this.db as DatabaseInterface & {\n config?: { url?: string };\n };\n const url = this.db.url || dbWithConfig.config?.url || '';\n return detectEngine(url, this.engineHint);\n }\n\n /**\n * Get the current database engine\n */\n getEngine(): DatabaseEngine {\n return this.dbEngine;\n }\n\n /**\n * Initialize the migrations tracking table\n */\n async initialize(): Promise<void> {\n if (!this.initializePromise) {\n this.initializePromise = this.runInitializeDdl().catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n private async runInitializeDdl(): Promise<void> {\n // Parse and execute each statement in the DDL\n const statements = getSystemTableDDLForEngine(\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n this.dbEngine,\n )\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n\n for (const statement of statements) {\n await this.db.query(statement);\n }\n\n await assertPostgresSystemTimestampsCurrent(this.db, this.engineHint);\n }\n\n /**\n * Get all applied migrations ordered by applied_at\n */\n async getAppliedMigrations(): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE status = 'completed' ORDER BY applied_at ASC`,\n );\n\n return (result.rows as MigrationRow[]).map(hydrateMigrationRow);\n }\n\n /**\n * Get pending migrations (defined but not applied)\n */\n async getPendingMigrations(\n definitions: MigrationDefinition[],\n ): Promise<MigrationDefinition[]> {\n const applied = await this.getAppliedMigrations();\n const appliedNames = new Set(applied.map((m) => m.name));\n return definitions.filter((d) => !appliedNames.has(d.id));\n }\n\n /**\n * Check if a migration has been applied\n */\n async isApplied(migrationId: string): Promise<boolean> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT 1 FROM _smrt_schema_migrations WHERE name = ? AND status = 'completed' LIMIT 1`,\n migrationId,\n );\n\n return result.rows.length > 0;\n }\n\n /**\n * Get a single migration record by name\n */\n async getMigration(name: string): Promise<SchemaMigrationRecord | null> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT * FROM _smrt_schema_migrations WHERE name = ? LIMIT 1`,\n name,\n );\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as MigrationRow;\n return hydrateMigrationRow(row);\n }\n\n /**\n * Get the next batch number\n */\n async getNextBatch(): Promise<number> {\n await this.initialize();\n\n const result = await this.db.query(\n `SELECT MAX(batch) as max_batch FROM _smrt_schema_migrations`,\n );\n\n const row = result.rows[0] as { max_batch?: unknown } | undefined;\n const maxBatch = toSafeInteger(row?.max_batch ?? 0, 'Migration batch');\n return toSafeInteger(maxBatch + 1, 'Next migration batch');\n }\n\n /**\n * Detect drift between migration definitions and applied migrations\n */\n async detectDrift(\n definitions: MigrationDefinition[],\n ): Promise<DriftReport[]> {\n const applied = await this.getAppliedMigrations();\n const reports: DriftReport[] = [];\n\n const appliedMap = new Map(applied.map((m) => [m.name, m]));\n const definitionMap = new Map(definitions.map((d) => [d.id, d]));\n\n // Check each applied migration against definitions\n for (const record of applied) {\n const definition = definitionMap.get(record.name);\n\n if (!definition) {\n // Migration applied but definition is missing\n reports.push({\n migration_name: record.name,\n expected_checksum: 'N/A',\n actual_checksum: record.checksum,\n drift_type: 'unknown_in_db',\n recommendation:\n 'Migration exists in database but definition is missing. Manual cleanup may be required.',\n });\n continue;\n }\n\n // Check checksum\n const currentChecksum = computeChecksum(definition.up);\n const verification = verifyChecksum(\n currentChecksum,\n record.checksum,\n record.applied_checksum,\n );\n\n if (!verification.valid) {\n const driftType = verification.drift ?? 'db_modified';\n reports.push({\n migration_name: record.name,\n expected_checksum: record.checksum,\n actual_checksum: currentChecksum,\n drift_type: driftType,\n recommendation:\n driftType === 'file_modified'\n ? 'Migration file was modified after application. Create a new migration instead.'\n : 'Database schema was modified outside of migrations. Review and reconcile manually.',\n });\n }\n }\n\n return reports;\n }\n\n /**\n * Apply a single migration\n */\n async apply(\n definition: MigrationDefinition,\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.up);\n const force = Boolean(\n options.force || options.forceMigrations?.includes(definition.id),\n );\n const hasScopedForce = Boolean(options.forceMigrations?.length);\n\n // Check if migration record already exists\n const existing = await this.getMigration(definition.id);\n\n if (existing) {\n // Handle based on existing status\n switch (existing.status) {\n case 'completed':\n if (!force && existing.checksum !== checksum) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} checksum mismatch. Was already applied with different checksum. Use --force-migration ${definition.id} to override only this migration, or --force to override the whole batch.`,\n };\n }\n\n if (!force && !options.reconcile) {\n // Already applied, skip\n return {\n success: true,\n applied: false,\n skipped: true,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n };\n }\n break;\n\n case 'failed':\n if (!force && (!options.reconcile || hasScopedForce)) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} previously failed: ${existing.error_message || 'Unknown error'}. Use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'running':\n if (!force) {\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: 0,\n error: `Migration ${definition.id} is currently running or was interrupted. If the previous run crashed, use --force-migration ${definition.id} to retry only this migration, or --force to retry the whole batch.`,\n };\n }\n // Will retry below\n break;\n\n case 'rolled_back':\n // Rolled back migrations can be re-applied without --force\n break;\n }\n }\n\n if (options.dryRun) {\n return {\n success: true,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n };\n }\n\n // Get batch number\n if (this.currentBatch === null) {\n this.currentBatch = await this.getNextBatch();\n }\n\n // Determine if we're updating an existing record or inserting new\n const id = existing?.id || randomUUID();\n const attempts = toSafeInteger(\n (existing?.attempts ?? 0) + 1,\n 'Migration attempts',\n );\n\n if (existing) {\n // Update existing record to 'running'\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'running', checksum = ?, attempts = ?, error_message = NULL, batch = ?, applied_by = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n checksum,\n attempts,\n this.currentBatch,\n hostname(),\n id,\n );\n } else {\n // Insert new record\n await this.db.query(\n `INSERT INTO _smrt_schema_migrations\n (id, name, version, checksum, status, attempts, is_reversible, package_name, source_file, applied_by, batch)\n VALUES (?, ?, ?, ?, 'running', 1, ?, ?, ?, ?, ?)`,\n id,\n definition.id,\n definition.version,\n checksum,\n (definition.is_reversible ?? definition.down.length > 0) ? 1 : 0,\n definition.package_name ?? null,\n definition.source_file ?? null,\n hostname(),\n this.currentBatch,\n );\n }\n\n // PostgreSQL concurrent-index mode splits this definition: the\n // transaction-safe statements run here, the CONCURRENTLY index DDL runs\n // after the batch commits.\n const split = this.concurrentIndexPlan?.get(definition.id);\n const deferredConcurrent = (split?.concurrent.length ?? 0) > 0;\n\n // A retry of an interrupted concurrent-index migration must not re-run the\n // half that already committed. The marker on the previous attempt's\n // `error_message` says it did, and the checksum says it was this same\n // definition. Without this, a definition that mixes non-idempotent DDL\n // (`ALTER TABLE … ADD COLUMN`, which the differ emits without\n // `IF NOT EXISTS`) with index DDL would fail on \"column already exists\"\n // forever, never reaching the index build it was retrying for.\n const phase1AlreadyCommitted =\n deferredConcurrent &&\n existing?.status === 'failed' &&\n existing.checksum === checksum &&\n typeof existing.error_message === 'string' &&\n existing.error_message.startsWith(CONCURRENT_INDEX_PHASE1_MARKER);\n\n try {\n // Execute the migration\n await this.executeStatements(\n phase1AlreadyCommitted ? [] : split ? split.regular : definition.up,\n options,\n );\n\n const executionTime = Date.now() - startTime;\n\n if (deferredConcurrent) {\n // Commit this record as `failed`, not `completed` and not `running`.\n // The index does not exist yet, so `completed` would be a lie; and\n // unlike the single-transaction path — where a crash rolls the record\n // insert back with the DDL — this row is about to be committed, so a\n // crash before the concurrent phase finishes would strand a `running`\n // record that the next `db:migrate` can only clear with\n // `--force-migration`. `failed` is retryable under the CLI's\n // reconciling re-run, which is what an interrupted index rollout\n // needs. The concurrent phase overwrites this a moment later.\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', execution_time_ms = ?, error_message = ?\n WHERE id = ?`,\n executionTime,\n CONCURRENT_INDEX_PENDING_MESSAGE,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n }\n\n // Mark as completed\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, applied_at = CURRENT_TIMESTAMP\n WHERE id = ?`,\n executionTime,\n checksum,\n id,\n );\n\n return {\n success: true,\n applied: true,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const migrationError =\n error instanceof Error ? error : new Error(String(error));\n\n // Mark as failed. In an outer atomic Postgres transaction, a DDL error\n // aborts the transaction and this status write must be skipped so the\n // caller can roll back the whole batch while preserving the root error.\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE id = ?`,\n migrationError.message,\n id,\n );\n } catch (persistError) {\n // Preserve the migration failure; status persistence is secondary and\n // may be impossible until the surrounding transaction rolls back.\n const persistMessage =\n persistError instanceof Error\n ? persistError.message\n : String(persistError);\n logger.error(\n `Failed to persist failed status for migration ${definition.id}: ${persistMessage}`,\n );\n }\n\n return {\n success: false,\n applied: false,\n skipped: false,\n name: definition.id,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: migrationError,\n };\n }\n }\n\n /**\n * Apply multiple migrations in order.\n *\n * On PostgreSQL the atomic path always bounds itself with\n * `SET LOCAL lock_timeout` / `SET LOCAL statement_timeout` so a migration\n * that queues behind a long-running writer fails fast and rolls back\n * instead of stalling every writer behind its already-held locks\n * (issue #2362).\n *\n * `options.postgresSafe` additionally enables **concurrent-index mode**:\n * non-index DDL still runs in the one atomic transaction, then every index\n * statement runs `CONCURRENTLY` on a pinned session after that transaction\n * commits. That mode trades atomicity for availability — a failure part way\n * through the index phase leaves the committed non-index DDL in place, and\n * the unfinished index migrations are recorded `failed` so the next run\n * retries them, resuming at the index phase rather than re-running the\n * non-index statements that already committed.\n */\n async applyAll(\n definitions: MigrationDefinition[],\n options: ApplyMigrationsOptions = {},\n ): Promise<MigrationResult[]> {\n if (options.atomic && !options.dryRun) {\n if (!this.db.transaction) {\n throw new Error(\n 'Atomic migration batches require a database adapter with transaction support.',\n );\n }\n\n const isPostgres = this.dbEngine === 'postgres';\n const concurrentIndexMode = isPostgres && Boolean(options.postgresSafe);\n const plan = concurrentIndexMode\n ? buildConcurrentIndexPlan(\n definitions,\n this.options.useConcurrentIndexes,\n )\n : null;\n\n // Outside concurrent-index mode a CONCURRENTLY statement can only fail:\n // PostgreSQL rejects it inside a transaction block. Fail before opening\n // the transaction so the error names the offending migration.\n if (isPostgres && !concurrentIndexMode) {\n const concurrentStatement = findConcurrentIndexStatement(definitions);\n if (concurrentStatement) {\n throw new Error(\n `Atomic migration batch cannot include CONCURRENTLY index DDL in ${concurrentStatement.migrationName}; PostgreSQL forbids CONCURRENTLY inside a transaction. Re-run with PostgreSQL concurrent-index mode (smrt db:migrate --postgres-safe) to build those indexes outside the batch.`,\n );\n }\n }\n\n const deferred = plan && plan.size > 0 ? plan : null;\n\n try {\n const committed = await this.db.transaction(async (tx) => {\n const txDb = {\n ...tx,\n transaction: async <T>(\n callback: (transactionDb: DatabaseInterface) => Promise<T>,\n ) => callback(tx as DatabaseInterface),\n } as DatabaseInterface;\n\n // Bound the whole batch. `SET LOCAL` reverts at COMMIT/ROLLBACK, so\n // this cannot leak onto a pooled connection's next checkout.\n if (isPostgres) {\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n }\n\n const txTracker = new MigrationTracker({\n db: txDb,\n lockTimeout: this.options.lockTimeout,\n statementTimeout: this.options.statementTimeout,\n useConcurrentIndexes: false,\n });\n txTracker.concurrentIndexPlan = deferred;\n const results = await txTracker.applyAll(definitions, {\n ...options,\n atomic: false,\n continueOnError: false,\n postgresSafe: false,\n // Deferred migrations report progress from the concurrent phase\n // once they have actually finished, not when phase 1 commits.\n onProgress: options.onProgress\n ? (result) => {\n if (!deferred?.has(result.name)) {\n options.onProgress?.(result);\n }\n }\n : undefined,\n });\n const failed = results.find((result) => !result.success);\n\n if (failed) {\n throw new AtomicMigrationRollback(results, failed);\n }\n\n return results;\n });\n\n if (!deferred) {\n return committed;\n }\n\n return await this.applyDeferredConcurrentIndexes(\n committed,\n deferred,\n options,\n );\n } catch (error) {\n if (error instanceof AtomicMigrationRollback) {\n return error.results.map((result) => {\n if (!result.success || !result.applied) {\n return result;\n }\n\n return {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n rolled_back: true,\n error: new Error(\n `Rolled back because migration ${error.failed.name} failed`,\n ),\n };\n });\n }\n\n throw error;\n }\n }\n\n const results: MigrationResult[] = [];\n this.currentBatch = await this.getNextBatch();\n\n for (const definition of definitions) {\n const result = await this.apply(definition, options);\n results.push(result);\n options.onProgress?.(result);\n\n if (!result.success && !options.continueOnError) {\n break;\n }\n }\n\n this.currentBatch = null;\n return results;\n }\n\n /**\n * Phase 2 of PostgreSQL concurrent-index mode.\n *\n * Runs every deferred index statement on one pinned session, outside the\n * committed transaction, bounded by session `lock_timeout` /\n * `statement_timeout`. INVALID indexes left behind by an earlier failed\n * `CREATE INDEX CONCURRENTLY` are dropped first so the retry rebuilds them\n * rather than skipping over a stump that reads as present.\n *\n * On failure the batch stops: the failing migration and every migration it\n * did not reach are recorded `failed`, which the CLI's reconciling\n * `db:migrate` retries on the next run.\n */\n private async applyDeferredConcurrentIndexes(\n committed: MigrationResult[],\n plan: ConcurrentIndexPlan,\n options: ApplyMigrationsOptions,\n ): Promise<MigrationResult[]> {\n const results = [...committed];\n const session = await this.acquireMigrationSession();\n\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n let stoppedBy: string | null = null;\n\n for (let index = 0; index < results.length; index++) {\n const result = results[index];\n const split = plan.get(result.name);\n if (!split || split.concurrent.length === 0) {\n continue;\n }\n\n // A migration phase 1 skipped (already applied, checksum matched) is\n // already `completed` in the tracking table — its index exists and\n // must not be rebuilt or have its record rewritten.\n if (result.skipped) {\n continue;\n }\n\n if (stoppedBy) {\n const error = new Error(\n `Not attempted: concurrent index phase stopped after ${stoppedBy} failed`,\n );\n await this.recordConcurrentIndexFailure(result.name, error.message);\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n error,\n };\n options.onProgress?.(results[index]);\n continue;\n }\n\n const startedAt = Date.now();\n\n try {\n for (const sql of split.concurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n // An INVALID index is not usable by the planner and blocks the\n // name, but `CREATE INDEX … IF NOT EXISTS` treats it as present.\n // Drop it so the create below actually rebuilds it.\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n\n // Floor at 1ms: callers (the CLI progress reporter, the applied vs\n // skipped counters) read `execution_time_ms === 0` as \"nothing ran\".\n const executionTime = Math.max(\n 1,\n (result.execution_time_ms ?? 0) + (Date.now() - startedAt),\n );\n await this.recordConcurrentIndexSuccess(\n result.name,\n result.checksum,\n executionTime,\n );\n results[index] = {\n ...result,\n success: true,\n applied: true,\n skipped: false,\n execution_time_ms: executionTime,\n };\n } catch (error) {\n const indexError =\n error instanceof Error ? error : new Error(String(error));\n if (!options.continueOnError) {\n stoppedBy = result.name;\n }\n await this.recordConcurrentIndexFailure(\n result.name,\n indexError.message,\n );\n results[index] = {\n ...result,\n success: false,\n applied: false,\n skipped: false,\n execution_time_ms: Date.now() - startedAt,\n error: indexError,\n };\n }\n\n options.onProgress?.(results[index]);\n }\n\n return results;\n } finally {\n await session.release();\n }\n }\n\n /**\n * Pin one connection for the concurrent-index phase.\n *\n * PostgreSQL pools hand each top-level `query` an arbitrary connection, so\n * a `SET lock_timeout` issued through the pool may not apply to the DDL\n * that follows. Adapters that expose `acquireSession` give us a pinned\n * connection; anything else (SQLite, DuckDB, test doubles) is\n * single-connection anyway and falls back to the plain interface.\n */\n private async acquireMigrationSession(): Promise<MigrationSession> {\n if (typeof this.db.acquireSession === 'function') {\n const session = await this.db.acquireSession();\n return {\n query: (sql, ...vars) => session.query(sql, ...vars),\n release: () => session.release(),\n };\n }\n\n return {\n query: (sql, ...vars) => this.db.query(sql, ...vars),\n release: async () => {},\n };\n }\n\n /**\n * Read the names of INVALID indexes that an unqualified index name in this\n * session would actually resolve to.\n *\n * `pg_indexes` (what the SQL adapter's introspection reads) lists an\n * INVALID index exactly like a healthy one, so the schema differ believes a\n * failed `CREATE INDEX CONCURRENTLY` succeeded. `pg_index.indisvalid` is the\n * authoritative flag.\n *\n * Scoped to `current_schemas(false)` — the session's search path — because\n * migration DDL names indexes unqualified, and so does the\n * `DROP INDEX CONCURRENTLY IF EXISTS \"<name>\"` repair. Scanning every schema\n * would let an unrelated invalid index of the same name in another schema\n * trigger a drop that then resolves to a *different* object. Where the same\n * name appears in several search-path schemas, the earliest on the path wins\n * — the same rule PostgreSQL uses to resolve the name.\n *\n * A role without catalog access degrades to \"no known invalid indexes\"\n * rather than aborting the migration.\n */\n private async readInvalidIndexNames(\n session: MigrationSession,\n ): Promise<Set<string>> {\n const names = new Set<string>();\n\n try {\n const result = await session.query(\n `SELECT DISTINCT ON (c.relname) c.relname AS index_name\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE i.indisvalid = false\n AND n.nspname = ANY (current_schemas(false))\n ORDER BY c.relname,\n array_position(current_schemas(false), n.nspname::text)`,\n );\n\n for (const row of result.rows) {\n const name = (row as { index_name?: unknown }).index_name;\n if (typeof name === 'string') {\n names.add(name);\n }\n }\n } catch (error) {\n logger.warn(\n `Could not read pg_index.indisvalid; skipping INVALID index repair: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n\n return names;\n }\n\n private async recordConcurrentIndexSuccess(\n name: string,\n checksum: string,\n executionTime: number,\n ): Promise<void> {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, error_message = NULL, applied_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n executionTime,\n checksum,\n name,\n );\n }\n\n private async recordConcurrentIndexFailure(\n name: string,\n message: string,\n ): Promise<void> {\n try {\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'failed', error_message = ?\n WHERE name = ?`,\n // Keep the resume marker: this failure happened *after* phase 1\n // committed, so the retry must skip the non-index statements too.\n `${CONCURRENT_INDEX_PHASE1_MARKER} ${message}`,\n name,\n );\n } catch (persistError) {\n logger.error(\n `Failed to persist failed status for migration ${name}: ${\n persistError instanceof Error\n ? persistError.message\n : String(persistError)\n }`,\n );\n }\n }\n\n /**\n * Rollback a migration\n */\n async rollback(\n name: string,\n definition: MigrationDefinition,\n options: RollbackOptions = {},\n ): Promise<MigrationResult> {\n await this.initialize();\n const startTime = Date.now();\n const checksum = computeChecksum(definition.down);\n\n const record = await this.getMigration(name);\n if (!record) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(`Migration ${name} not found in database`),\n };\n }\n\n if (record.status !== 'completed') {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not in completed state (current: ${record.status})`,\n ),\n };\n }\n\n if (!definition.down.length) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: 0,\n error: new Error(\n `Migration ${name} is not reversible (no down statements)`,\n ),\n };\n }\n\n if (options.dryRun) {\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n }\n\n try {\n // Execute DOWN statements\n await this.executeStatements(definition.down, {});\n\n // Mark as rolled back\n await this.db.query(\n `UPDATE _smrt_schema_migrations\n SET status = 'rolled_back', rolled_back_at = CURRENT_TIMESTAMP\n WHERE name = ?`,\n name,\n );\n\n return {\n success: true,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n rolled_back: true,\n };\n } catch (error) {\n return {\n success: false,\n name,\n checksum,\n execution_time_ms: Date.now() - startTime,\n error: error instanceof Error ? error : new Error(String(error)),\n rolled_back: false,\n };\n }\n }\n\n /**\n * Get migration history with optional filtering\n */\n async getHistory(\n options: { limit?: number; since?: Date; status?: MigrationStatus } = {},\n ): Promise<SchemaMigrationRecord[]> {\n await this.initialize();\n\n let sql = `SELECT * FROM _smrt_schema_migrations WHERE 1=1`;\n const params: unknown[] = [];\n\n if (options.status) {\n sql += ` AND status = ?`;\n params.push(options.status);\n }\n\n if (options.since) {\n sql += ` AND applied_at >= ?`;\n params.push(options.since.toISOString());\n }\n\n sql += ` ORDER BY applied_at DESC`;\n\n if (options.limit) {\n sql += ` LIMIT ?`;\n params.push(options.limit);\n }\n\n const result = await this.db.query(sql, ...params);\n\n return (result.rows as MigrationRow[]).map(hydrateMigrationRow);\n }\n\n /**\n * Execute SQL statements with appropriate transaction handling\n */\n private async executeStatements(\n statements: string[],\n options: ApplyMigrationsOptions,\n ): Promise<void> {\n if (this.dbEngine === 'postgres' && options.postgresSafe) {\n await this.executePostgresStatements(statements);\n } else {\n await this.executeWithTransaction(statements);\n }\n }\n\n /**\n * Execute statements within a transaction (SQLite/DuckDB)\n */\n private async executeWithTransaction(statements: string[]): Promise<void> {\n if (this.db.transaction) {\n await this.db.transaction(async (tx) => {\n for (const sql of statements) {\n await tx.query(sql);\n }\n });\n } else {\n // Fallback to sequential execution\n for (const sql of statements) {\n await this.db.query(sql);\n }\n }\n }\n\n /**\n * Execute statements with PostgreSQL-specific handling\n *\n * - CONCURRENTLY statements run outside transaction\n * - Regular statements run in transaction with lock_timeout\n *\n * Both CREATE and DROP variants of CONCURRENTLY must be detected — Postgres\n * forbids `DROP INDEX CONCURRENTLY` inside a transaction block, just like\n * the create variant. Issue #1165: the migration generator emits\n * `DROP INDEX CONCURRENTLY` for shape-drift recreates and orphan-index\n * cleanups, so the regex needs to cover both keywords.\n */\n private async executePostgresStatements(statements: string[]): Promise<void> {\n const { concurrent: finalConcurrent, regular: finalRegular } =\n planPostgresStatements(statements, !!this.options.useConcurrentIndexes);\n\n // CONCURRENTLY statements come from `planPostgresStatements`, which\n // also pushes any post-transform CREATE/DROP CONCURRENTLY into the\n // outside-transaction set. The original `concurrentStatements` list\n // is folded in there too so callers below see one merged stream.\n\n // Execute regular statements in transaction with timeouts\n if (finalRegular.length > 0 && this.db.transaction) {\n await this.db.transaction(async (tx) => {\n // Set session-level timeouts\n await tx.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await tx.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n for (const sql of finalRegular) {\n await tx.query(sql);\n }\n });\n } else if (finalRegular.length > 0) {\n for (const sql of finalRegular) {\n await this.db.query(sql);\n }\n }\n\n if (finalConcurrent.length === 0) {\n return;\n }\n\n // Execute CONCURRENTLY statements outside the transaction, on one pinned\n // connection so `SET lock_timeout` / `SET statement_timeout` actually\n // govern the DDL that follows them (a pooled `db.query` may not reuse the\n // connection the SET landed on).\n const session = await this.acquireMigrationSession();\n try {\n await session.query(\n `SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`,\n );\n await session.query(\n `SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`,\n );\n\n const invalidIndexes = await this.readInvalidIndexNames(session);\n\n for (const sql of finalConcurrent) {\n const indexName = extractCreatedIndexName(sql);\n if (indexName && invalidIndexes.has(indexName)) {\n logger.warn(\n `Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`,\n );\n await session.query(\n `DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`,\n );\n invalidIndexes.delete(indexName);\n }\n\n await session.query(sql);\n }\n } finally {\n await session.release();\n }\n }\n}\n\n/**\n * Quote an index identifier for PostgreSQL, escaping embedded double quotes.\n */\nfunction quoteIndexName(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/**\n * Build the PostgreSQL concurrent-index plan for an atomic batch.\n *\n * Returns one entry per migration definition that has at least one statement\n * which must leave the transaction, mapping it to the transaction-safe\n * statements (phase 1) and the `CONCURRENTLY` statements (phase 2).\n * Definitions with nothing to defer are omitted so the caller can treat\n * `plan.has(id)` as \"this migration finishes in phase 2\".\n *\n * Exported for unit testing.\n */\nexport function buildConcurrentIndexPlan(\n definitions: MigrationDefinition[],\n useConcurrentIndexes: boolean,\n): ConcurrentIndexPlan {\n const plan: ConcurrentIndexPlan = new Map();\n\n for (const definition of definitions) {\n const split = planPostgresStatements(definition.up, useConcurrentIndexes);\n if (split.concurrent.length > 0) {\n plan.set(definition.id, split);\n }\n }\n\n return plan;\n}\n\n/**\n * Split a batch of SQL statements into a transaction-safe set and a\n * concurrent set, applying `--postgres-safe` rewriting if requested.\n *\n * Exported for unit testing and to give external callers a way to\n * reason about what the tracker would execute in PostgreSQL safe mode.\n *\n * Rules:\n * - Statements already containing `CONCURRENTLY` for CREATE/DROP INDEX or\n * REINDEX operations always go to the concurrent set — Postgres rejects\n * them inside transactions.\n * - When `useConcurrentIndexes` is true, plain `CREATE INDEX` and plain\n * `DROP INDEX` are rewritten to use `CONCURRENTLY`, then routed to\n * the concurrent set. This is what the CLI `--postgres-safe` flag and\n * the auto-migrate path rely on for issue #1165's shape-drift drops.\n * - All other statements stay in the regular (transaction) set.\n */\nexport function planPostgresStatements(\n statements: string[],\n useConcurrentIndexes: boolean,\n): { concurrent: string[]; regular: string[] } {\n const concurrentRegex = CONCURRENT_INDEX_STATEMENT_RE;\n\n const concurrent: string[] = [];\n const regular: string[] = [];\n\n for (const sql of statements) {\n if (sql.trimStart().startsWith(NULL_EQUAL_INDEX_MARKER)) {\n if (useConcurrentIndexes) {\n throw new Error(\n 'NULL-equal conflict index DDL requires an atomic maintenance-window migration; retry without --postgres-safe. Existing indexes require db:migrate-null-equal-indexes.',\n );\n }\n regular.push(sql);\n continue;\n }\n if (concurrentRegex.test(sql)) {\n concurrent.push(sql);\n continue;\n }\n if (useConcurrentIndexes) {\n if (/CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+/i,\n 'CREATE $1INDEX CONCURRENTLY ',\n ),\n );\n continue;\n }\n if (/DROP\\s+INDEX\\s+(?!CONCURRENTLY)/i.test(sql)) {\n concurrent.push(\n sql.replace(/DROP\\s+INDEX\\s+/i, 'DROP INDEX CONCURRENTLY '),\n );\n continue;\n }\n }\n regular.push(sql);\n }\n\n return { concurrent, regular };\n}\n"],"mappings":"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"}
|
package/dist/migrations.js
CHANGED
|
@@ -7,7 +7,8 @@ import { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum } from "./
|
|
|
7
7
|
import { FRAMEWORK_BASE_TABLE_NAMES, dropFrameworkBaseTables, planFrameworkBaseTableDrop } from "./migrations/framework-base-tables.js";
|
|
8
8
|
import { MigrationGenerator, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp } from "./migrations/generator.js";
|
|
9
9
|
import { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits } from "./migrations/minor-units.js";
|
|
10
|
+
import { collectNullEqualIndexTargets, migrateNullEqualIndexes, nullEqualDuplicateDetector, nullEqualIndexStatements, preflightNullEqualIndexes } from "./migrations/null-equal-indexes.js";
|
|
10
11
|
import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./migrations/tracker.js";
|
|
11
12
|
import { getPendingSchemaStatements, migrateSmrtSchemas } from "./migrations/orchestrate.js";
|
|
12
13
|
import "./migrations/index.js";
|
|
13
|
-
export { BackfillTableUnavailableError, BackfillTracker, FRAMEWORK_BASE_TABLE_NAMES, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, computeChecksum, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateSmrtSchemas, normalizeSQL, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
|
|
14
|
+
export { BackfillTableUnavailableError, BackfillTracker, FRAMEWORK_BASE_TABLE_NAMES, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, collectNullEqualIndexTargets, computeChecksum, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateNullEqualIndexes, migrateSmrtSchemas, normalizeSQL, nullEqualDuplicateDetector, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, preflightNullEqualIndexes, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
|
package/dist/object.d.ts
CHANGED
|
@@ -613,6 +613,25 @@ export declare class SmrtObject extends SmrtClass {
|
|
|
613
613
|
* or consumer-facing type changes. Mutates `data` in place.
|
|
614
614
|
*/
|
|
615
615
|
private coerceEmptyUuidValuesToNull;
|
|
616
|
+
/**
|
|
617
|
+
* Conservative eligibility for junction batching. Additional methods are fine;
|
|
618
|
+
* replacing any runtime method/accessor requires the sequential lifecycle.
|
|
619
|
+
* Re-evaluate for every call: registry and interceptor registration is dynamic.
|
|
620
|
+
* @internal
|
|
621
|
+
*/
|
|
622
|
+
supportsJunctionBatch(): boolean;
|
|
623
|
+
/** Check overrides before initialization can execute custom model code. */
|
|
624
|
+
static hasBaseJunctionLifecycle(candidate: object): boolean;
|
|
625
|
+
/**
|
|
626
|
+
* Persist a bounded compatible junction replacement through owned lifecycle
|
|
627
|
+
* preparation/completion. False means no persistence occurred; callers retain
|
|
628
|
+
* their virtual per-row path. Unsupported shapes never use raw SQL shortcuts.
|
|
629
|
+
* @internal
|
|
630
|
+
*/
|
|
631
|
+
static tryJunctionBatch(removed: SmrtObject[], added: SmrtObject[]): Promise<boolean>;
|
|
632
|
+
/** Shared preparation for ordinary saves and compatible bulk creates. */
|
|
633
|
+
private prepareSave;
|
|
634
|
+
private completeSave;
|
|
616
635
|
/**
|
|
617
636
|
* Persists this object to the database using an upsert (insert or update).
|
|
618
637
|
*
|
|
@@ -717,6 +736,20 @@ export declare class SmrtObject extends SmrtClass {
|
|
|
717
736
|
* class-name fallback for resilience.
|
|
718
737
|
*/
|
|
719
738
|
private isTenantContractError;
|
|
739
|
+
/** Declare permitted snake-case derived schema columns; preserve super entries. */
|
|
740
|
+
protected getPersistenceDerivedColumns(): readonly string[];
|
|
741
|
+
/**
|
|
742
|
+
* Return derived schema-column values for the final persistence row.
|
|
743
|
+
* Runs synchronously after beforeSave, the complete toJSON/transformJSON
|
|
744
|
+
* chain, snake-case mapping and UUID coercion; returned columns are merged
|
|
745
|
+
* before every save branch. This does not change public serialization.
|
|
746
|
+
* Only derive columns from this read-only snapshot: do not modify source,
|
|
747
|
+
* identity, tenant or revision columns, and do not perform I/O. Overrides
|
|
748
|
+
* should preserve any columns returned by super. Declare permitted snake-case
|
|
749
|
+
* schema columns in getPersistenceDerivedColumns(), preserving super entries.
|
|
750
|
+
* Framework identity, tenant, revision and conflict columns cannot be declared.
|
|
751
|
+
*/
|
|
752
|
+
protected normalizePersistenceData(_data: Readonly<Record<string, unknown>>): Record<string, unknown> | undefined;
|
|
720
753
|
/**
|
|
721
754
|
* Validates object state before saving
|
|
722
755
|
* Override in subclasses to add custom validation logic
|
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;
|
|
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;AAYzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAOpC,OAAO,EAGL,KAAK,2BAA2B,EACjC,MAAM,aAAa,CAAC;AAWrB,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AA0BpE,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AA4K/B,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;CACnC;AAED,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;CACnC;AA8FD;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,IAAI,CAAC;IAElB;;OAEG;IACH,UAAU,CAAC,EAAE,IAAI,CAAC;IAElB;;OAEG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;;;;;;;OASG;IAEH,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CACvC;AAqBD;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IACjE;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,UAAW,SAAQ,SAAS;IACvC;;OAEG;IACI,UAAU,EAAG,MAAM,CAAC;IAE3B;;;OAGG;IACH,OAAO,CAAC,oBAAoB,CAAmC;IAE/D;;;;;;;;;OASG;IACH,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;;;;OAOG;IACH,OAAO,CAAC,WAAW,CAAS;IAE5B;;;OAGG;IACY,OAAO,EAAE,iBAAiB,CAAC;IAE1C;;OAEG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAEzC;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAE3C;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAE9C;;OAEG;IACI,UAAU,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAQ;IAElD;;OAEG;IACI,UAAU,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,CAAQ;IAElD;;;;;OAKG;gBACS,OAAO,GAAE,iBAAsB;IA0B3C,OAAO,CAAC,sBAAsB;IAM9B,SAAS,CAAC,oBAAoB,IAAI,MAAM;IAIxC,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAO5C,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,+BAA+B;YAezB,oBAAoB;IAgGlC;;;;OAIG;IACH,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIjC;;;;;OAKG;IACH,IAAW,WAAW,IAAI,OAAO,CAEhC;IAED;;;;;OAKG;IACI,eAAe,IAAI,IAAI;IAI9B;;;;;;;;OAQG;IACI,mBAAmB,IAAI,IAAI;cAIlB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAQnD;;;;;;;;OAQG;IACU,eAAe,CAAC,CAAC,EAC5B,SAAS,EAAE,CAAC,KAAK,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GACrC,OAAO,CAAC,CAAC,CAAC;IAoCb;;;;OAIG;IACH,SAAS,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAM7C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,UAAU;IAsBlB;;;;OAIG;YACW,+BAA+B;IAiD7C;;OAEG;IACH,IAAI,EAAE,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS,CAElC;IAED;;;;;OAKG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAKtC;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS,CAEpC;IAED;;;;;OAKG;IACH,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAMxC;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;;;;OAKG;IACH,IAAI,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAK3C;IAED;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAuBxC;;;;;;;;;OASG;IACH,OAAO,CAAC,2BAA2B;IAiBnC;;;;;;;;;OASG;IACG,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IA6JlD,4EAA4E;cAC5D,4BAA4B,CAC1C,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAiCnC,+EAA+E;cAC/D,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IA6C1C,OAAO,CAAC,cAAc;IAwBtB;;OAEG;IACH,IAAI,SAAS,WAqCZ;IAED;;;;OAIG;IAMG,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC;IAiC/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACH,SAAS,CAAC,aAAa,CACrB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAI1B;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM;IAgMN;;;;;;;;;;;;;;;;;;;OAmBG;IACH,aAAa,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAOxC;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IA8C9B;;;;;;;;;;OAUG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAOlE;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,iBAAiB;IA2CzB;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAoClC;;;;;;;;;;OAUG;IACG,KAAK;IA8BX;;;;;;;;;;;;;;;;;;;OAmBG;IACG,OAAO;IAgCb;;;;;;;;OAQG;IACG,UAAU;IAwBhB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;YACW,kBAAkB;IAyBhC;;;;OAIG;IACG,OAAO;IAKb;;;;;;;;;;;;;;;;;;;;;OAqBG;YACW,sBAAsB;IA8DpC;;;;;;;;;;;OAWG;YACW,2BAA2B;IAazC;;;;;OAKG;IACI,qBAAqB,IAAI,OAAO;IA6BvC,2EAA2E;WAC7D,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IA4BlE;;;;;OAKG;WACiB,gBAAgB,CAClC,OAAO,EAAE,UAAU,EAAE,EACrB,KAAK,EAAE,UAAU,EAAE,GAClB,OAAO,CAAC,OAAO,CAAC;IA6LnB,yEAAyE;YAC3D,WAAW;YA6LX,YAAY;IAkD1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkDG;IACG,IAAI,CAAC,OAAO,GAAE,eAAoB;IAgNxC;;;;;;;OAOG;IACG,aAAa,CAAC,iBAAiB,EAAE,IAAI,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6EpE;;;;;OAKG;cACa,uBAAuB,CACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,IAAI,CAAC;IAchB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,sBAAsB;IAI9B,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,qBAAqB;IAe7B;;;;;;;;;OASG;IACH,OAAO,CAAC,qBAAqB;IAmB7B,mFAAmF;IACnF,SAAS,CAAC,4BAA4B,IAAI,SAAS,MAAM,EAAE;IAI3D;;;;;;;;;;OAUG;IACH,SAAS,CAAC,wBAAwB,CAChC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAItC;;;OAGG;cACa,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IA6DnD;;;;;;;;;;;OAWG;cACa,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC;IAgEzD;;OAEG;IACH,SAAS,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAMnD;;;;;OAKG;IACH,SAAS,CAAC,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAMhD;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,uBAAuB,CAC5B,OAAO,EAAE,MAAM,GACd,QAAQ,GAAG,UAAU,GAAG,IAAI;IAiB/B;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,+BAA+B,CACvC,KAAK,EAAE,KAAK,EACZ,cAAc,EAAE,2BAA2B,GAC1C,MAAM;IAaT;;;;;OAKG;IACH,SAAS,CAAC,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAyB9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACU,UAAU;IA4CvB;;;;;;;;;;;;;;;;;;OAkBG;IACU,YAAY;IAezB;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,oBAAoB;IAkC5B;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACU,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB;IA4BlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACU,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB;IAoBtE;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACU,QAAQ,CAAC,OAAO,GAAE,kBAAuB;IAoBtD;;;;;OAKG;cACa,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2DG;IACU,MAAM,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwGnE;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,6BAA6B;IAyBrC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,OAAO,CAAC,gCAAgC;IAkCxC;;;;;;;;;;;OAWG;IACI,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIlD;;;;;;;;;;;;;;;;OAgBG;IACI,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAItE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IAKU,WAAW,CACtB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,kBAAkB,GAExB,OAAO,CAAC,GAAG,CAAC;IAqGf;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,mBAAmB;IAqC3B;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IAGU,eAAe,CAC1B,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,kBAAkB,GAExB,OAAO,CAAC,GAAG,EAAE,CAAC;IAsHjB;;;;;;OAMG;cACa,qBAAqB,CACnC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,OAAO,YAAY,EAAE,oBAAoB,GACtD,OAAO,CAAC;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IA2CF;;;;;;;;;;;;;;;;;;;;;;OAsBG;IAGU,UAAU,CACrB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,kBAAkB,GAExB,OAAO,CAAC,GAAG,CAAC;IAuCf;;;;;;;;;;;;OAYG;IACI,iBAAiB,IAAI,MAAM,EAAE;IAKpC;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACU,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC;IAOzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;IACU,QAAQ,CAAC,OAAO,EAAE;QAC7B,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,IAAI,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACU,MAAM,CAAC,OAAO,EAAE;QAC3B,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,GAAG,OAAO,CAAC,OAAO,CAAC;IAqEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACU,SAAS,CACpB,OAAO,GAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;KACnB,GACL,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IA+ChC;;;;;;;;;;;;;;;;;;OAkBG;IACU,MAAM,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAa3E;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACU,WAAW,CAAC,OAAO,EAAE;QAChC,KAAK,EAAE,MAAM,CAAC;QACd,kBAAkB,CAAC,EAAE,OAAO,CAAC;KAC9B,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BnB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACU,kBAAkB,CAC7B,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,IAAI,CAAC;IAiJhB;;;;;;;;;;;;;;;;;OAiBG;IACU,YAAY,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC;IA8C3B;;;;;;;;;;;;;;OAcG;IACU,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAC;IA4EnD;;;;;;;;;;;;OAYG;IACU,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAW9C"}
|