@happyvertical/smrt-core 0.40.69 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +29 -4
- package/README.md +20 -1
- package/agents/change-feed.md +1 -1
- package/agents/query-bounds.md +45 -0
- package/agents/schema-paths.md +786 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +5 -3
- package/dist/cascade.d.ts +120 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +430 -0
- package/dist/cascade.js.map +1 -0
- package/dist/change-feed.d.ts +34 -2
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +52 -11
- package/dist/change-feed.js.map +1 -1
- package/dist/class.d.ts +36 -3
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +87 -9
- package/dist/class.js.map +1 -1
- package/dist/collection-cache.js +0 -0
- package/dist/collection-cache.js.map +1 -1
- package/dist/collection.d.ts +130 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +290 -57
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/database.d.ts +8 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +16 -8
- package/dist/database.js.map +1 -1
- package/dist/db-errors.d.ts +105 -0
- package/dist/db-errors.d.ts.map +1 -0
- package/dist/db-errors.js +382 -0
- package/dist/db-errors.js.map +1 -0
- package/dist/decorators/index.d.ts +80 -6
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +102 -12
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +4 -3
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +19 -4
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/types.d.ts +5 -0
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/embedded-write-queue.d.ts +46 -0
- package/dist/embedded-write-queue.d.ts.map +1 -0
- package/dist/embedded-write-queue.js +66 -0
- package/dist/embedded-write-queue.js.map +1 -0
- package/dist/embeddings/storage.d.ts +7 -0
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +29 -12
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/errors.d.ts +31 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -2
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +6 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +38 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +16 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +41 -3
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +22 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +34 -3
- package/dist/generators/rest.js.map +1 -1
- package/dist/hierarchical.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/interceptors.d.ts +21 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +27 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +4 -7
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/static-manifest.js +10 -10
- 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 +19 -19
- package/dist/migrations/differ.d.ts +211 -9
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +613 -50
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts +31 -4
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +49 -5
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +4 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +6 -3
- package/dist/migrations/minor-units.d.ts +162 -0
- package/dist/migrations/minor-units.d.ts.map +1 -0
- package/dist/migrations/minor-units.js +381 -0
- package/dist/migrations/minor-units.js.map +1 -0
- package/dist/migrations/orchestrate.js +35 -6
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/sqlite-rebuild.d.ts +142 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
- package/dist/migrations/sqlite-rebuild.js +514 -0
- package/dist/migrations/sqlite-rebuild.js.map +1 -0
- package/dist/migrations/tracker.d.ts +114 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +331 -16
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +19 -4
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations.js +6 -3
- package/dist/object.d.ts +142 -10
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +196 -41
- package/dist/object.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +240 -0
- package/dist/postgres-timeouts.d.ts.map +1 -0
- package/dist/postgres-timeouts.js +204 -0
- package/dist/postgres-timeouts.js.map +1 -0
- package/dist/query-bounds.d.ts +101 -0
- package/dist/query-bounds.d.ts.map +1 -0
- package/dist/query-bounds.js +177 -0
- package/dist/query-bounds.js.map +1 -0
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +3 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/manifest-field-merge.d.ts +12 -0
- package/dist/registry/manifest-field-merge.d.ts.map +1 -1
- package/dist/registry/manifest-field-merge.js +14 -2
- package/dist/registry/manifest-field-merge.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +22 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +205 -165
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +35 -3
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +41 -46
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +61 -83
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +45 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +92 -28
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +5 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +104 -0
- package/dist/schema/conflict-target.d.ts.map +1 -0
- package/dist/schema/conflict-target.js +129 -0
- package/dist/schema/conflict-target.js.map +1 -0
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -2
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +2 -1
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +12 -1
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +307 -6
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +510 -87
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index-utils.d.ts +120 -0
- package/dist/schema/index-utils.d.ts.map +1 -1
- package/dist/schema/index-utils.js +242 -1
- package/dist/schema/index-utils.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/live-parity.d.ts +90 -0
- package/dist/schema/live-parity.d.ts.map +1 -0
- package/dist/schema/live-parity.js +602 -0
- package/dist/schema/live-parity.js.map +1 -0
- package/dist/schema/manifest-schema.d.ts +121 -0
- package/dist/schema/manifest-schema.d.ts.map +1 -0
- package/dist/schema/manifest-schema.js +267 -0
- package/dist/schema/manifest-schema.js.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +24 -10
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +35 -90
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +65 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -0
- package/dist/schema/system-table-shapes.js +187 -0
- package/dist/schema/system-table-shapes.js.map +1 -0
- package/dist/schema/types.d.ts +103 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +2 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +5 -3
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +20 -8
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js +9 -16
- package/dist/sync/apply.js.map +1 -1
- package/dist/system/compatibility.d.ts +42 -0
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +182 -9
- package/dist/system/compatibility.js.map +1 -1
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +3 -2
- package/dist/system/retention.d.ts +237 -0
- package/dist/system/retention.d.ts.map +1 -0
- package/dist/system/retention.js +497 -0
- package/dist/system/retention.js.map +1 -0
- package/dist/system/schema.d.ts +100 -15
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +81 -45
- package/dist/system/schema.js.map +1 -1
- package/dist/system/types.d.ts +0 -2
- package/dist/system/types.d.ts.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +1 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +4 -9
- 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 +71 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +6 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { detectEngine } from "../schema/ddl/index.js";
|
|
2
2
|
import { CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, getSystemTableDDLForEngine } from "../system/schema.js";
|
|
3
|
+
import { parsePostgresTimeoutMs } from "../postgres-timeouts.js";
|
|
3
4
|
import { assertPostgresSystemTimestampsCurrent } from "../system/compatibility.js";
|
|
4
5
|
import { computeChecksum, verifyChecksum } from "./checksum.js";
|
|
5
6
|
import { createLogger } from "@happyvertical/logger";
|
|
@@ -31,6 +32,16 @@ var AtomicMigrationRollback = class extends Error {
|
|
|
31
32
|
}
|
|
32
33
|
};
|
|
33
34
|
var CONCURRENT_INDEX_STATEMENT_RE = /(?:(?:CREATE\s+(?:UNIQUE\s+)?INDEX|DROP\s+INDEX)\s+CONCURRENTLY|REINDEX(?:\s*\([^)]*\))?\s+(?:INDEX|TABLE|SCHEMA|DATABASE|SYSTEM)\s+CONCURRENTLY)/i;
|
|
35
|
+
/**
|
|
36
|
+
* Matches the leading clause of a `CREATE [UNIQUE] INDEX` statement and
|
|
37
|
+
* captures the index name, whether it is written bare or double-quoted, and
|
|
38
|
+
* regardless of the optional `CONCURRENTLY` / `IF NOT EXISTS` clauses.
|
|
39
|
+
*
|
|
40
|
+
* Used by the PostgreSQL concurrent-index mode to correlate a pending create
|
|
41
|
+
* with an INVALID index left behind by an earlier failed
|
|
42
|
+
* `CREATE INDEX CONCURRENTLY` (issue #2362).
|
|
43
|
+
*/
|
|
44
|
+
var CREATE_INDEX_NAME_RE = /^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(?:"((?:[^"]|"")+)"|([A-Za-z_][\w$]*))/i;
|
|
34
45
|
function findConcurrentIndexStatement(definitions) {
|
|
35
46
|
for (const definition of definitions) {
|
|
36
47
|
const statement = definition.up.find((sql) => CONCURRENT_INDEX_STATEMENT_RE.test(sql));
|
|
@@ -42,6 +53,61 @@ function findConcurrentIndexStatement(definitions) {
|
|
|
42
53
|
return null;
|
|
43
54
|
}
|
|
44
55
|
/**
|
|
56
|
+
* Extract the index name from a `CREATE [UNIQUE] INDEX …` statement.
|
|
57
|
+
*
|
|
58
|
+
* Returns the unquoted identifier, or `null` when the statement is not a
|
|
59
|
+
* CREATE INDEX (a drop, a reindex, or unrelated DDL).
|
|
60
|
+
*
|
|
61
|
+
* Exported for unit testing and because the CLI's index rollout needs the
|
|
62
|
+
* same correlation to explain what it repaired.
|
|
63
|
+
*/
|
|
64
|
+
function extractCreatedIndexName(sql) {
|
|
65
|
+
const match = CREATE_INDEX_NAME_RE.exec(sql);
|
|
66
|
+
if (!match) return null;
|
|
67
|
+
return match[1] !== void 0 ? match[1].replace(/""/g, "\"") : match[2];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Coerce a caller-supplied timeout to a usable millisecond count.
|
|
71
|
+
*
|
|
72
|
+
* Applied once, in the constructor, so every later read of
|
|
73
|
+
* `this.options.lockTimeout` / `.statementTimeout` is already finite and
|
|
74
|
+
* non-negative. Each timeout falls back to *its own* default — a bad
|
|
75
|
+
* `lockTimeout` must not silently inherit the (much larger) statement-timeout
|
|
76
|
+
* default and weaken the lock bound it was meant to impose.
|
|
77
|
+
*/
|
|
78
|
+
function resolveTimeoutOption(value, fallback) {
|
|
79
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.trunc(value) : fallback;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Render a millisecond timeout as a PostgreSQL interval literal.
|
|
83
|
+
*
|
|
84
|
+
* PostgreSQL reads `lock_timeout`/`statement_timeout` as milliseconds when
|
|
85
|
+
* given a bare number, but the explicit `ms` unit keeps the emitted SQL
|
|
86
|
+
* self-documenting in migration logs. `0` disables the timeout, which is
|
|
87
|
+
* PostgreSQL's own semantic and therefore preserved verbatim.
|
|
88
|
+
*
|
|
89
|
+
* Input is already normalized by {@link resolveTimeoutOption}.
|
|
90
|
+
*/
|
|
91
|
+
function formatPostgresTimeout(milliseconds) {
|
|
92
|
+
return `${Math.max(0, Math.trunc(milliseconds))}ms`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Prefix stamped on the `error_message` of every concurrent-index migration
|
|
96
|
+
* whose transactional half has already committed.
|
|
97
|
+
*
|
|
98
|
+
* This is the resume marker: it is the only durable record that phase 1's
|
|
99
|
+
* statements ran, so a retry can skip straight to the index build. It must
|
|
100
|
+
* therefore be carried by *both* the interim write at the end of phase 1 and
|
|
101
|
+
* every failure recorded during phase 2 — a lock timeout on the index build
|
|
102
|
+
* leaves the non-index DDL committed just as surely as a crash does.
|
|
103
|
+
*/
|
|
104
|
+
var CONCURRENT_INDEX_PHASE1_MARKER = "[smrt: concurrent-index phase 1 committed]";
|
|
105
|
+
/**
|
|
106
|
+
* Interim `error_message` for a migration whose transactional half has
|
|
107
|
+
* committed but whose `CONCURRENTLY` index build has not run yet.
|
|
108
|
+
*/
|
|
109
|
+
var 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.`;
|
|
110
|
+
/**
|
|
45
111
|
* MigrationTracker class
|
|
46
112
|
*
|
|
47
113
|
* Provides migration tracking and state management:
|
|
@@ -66,11 +132,20 @@ var MigrationTracker = class MigrationTracker {
|
|
|
66
132
|
*/
|
|
67
133
|
initializePromise = null;
|
|
68
134
|
currentBatch = null;
|
|
135
|
+
/**
|
|
136
|
+
* Statements deferred out of the current atomic batch by PostgreSQL
|
|
137
|
+
* concurrent-index mode. The parent tracker sets this on the
|
|
138
|
+
* transaction-scoped tracker before phase 1 so `apply()` executes only the
|
|
139
|
+
* transaction-safe statements and leaves the migration record in `running`;
|
|
140
|
+
* the parent finalizes those records after the transaction commits and the
|
|
141
|
+
* `CONCURRENTLY` DDL has run. `null` on every ordinary path.
|
|
142
|
+
*/
|
|
143
|
+
concurrentIndexPlan = null;
|
|
69
144
|
constructor(options) {
|
|
70
145
|
this.db = options.db;
|
|
71
146
|
this.options = {
|
|
72
|
-
lockTimeout: options.lockTimeout
|
|
73
|
-
statementTimeout: options.statementTimeout
|
|
147
|
+
lockTimeout: resolveTimeoutOption(options.lockTimeout, DEFAULT_OPTIONS.lockTimeout),
|
|
148
|
+
statementTimeout: resolveTimeoutOption(options.statementTimeout, DEFAULT_OPTIONS.statementTimeout),
|
|
74
149
|
useConcurrentIndexes: options.useConcurrentIndexes ?? DEFAULT_OPTIONS.useConcurrentIndexes
|
|
75
150
|
};
|
|
76
151
|
this.engineHint = options.engineHint;
|
|
@@ -263,9 +338,25 @@ var MigrationTracker = class MigrationTracker {
|
|
|
263
338
|
else await this.db.query(`INSERT INTO _smrt_schema_migrations
|
|
264
339
|
(id, name, version, checksum, status, attempts, is_reversible, package_name, source_file, applied_by, batch)
|
|
265
340
|
VALUES (?, ?, ?, ?, 'running', 1, ?, ?, ?, ?, ?)`, id, definition.id, definition.version, checksum, definition.is_reversible ?? definition.down.length > 0 ? 1 : 0, definition.package_name ?? null, definition.source_file ?? null, hostname(), this.currentBatch);
|
|
341
|
+
const split = this.concurrentIndexPlan?.get(definition.id);
|
|
342
|
+
const deferredConcurrent = (split?.concurrent.length ?? 0) > 0;
|
|
343
|
+
const phase1AlreadyCommitted = deferredConcurrent && existing?.status === "failed" && existing.checksum === checksum && typeof existing.error_message === "string" && existing.error_message.startsWith(CONCURRENT_INDEX_PHASE1_MARKER);
|
|
266
344
|
try {
|
|
267
|
-
await this.executeStatements(definition.up, options);
|
|
345
|
+
await this.executeStatements(phase1AlreadyCommitted ? [] : split ? split.regular : definition.up, options);
|
|
268
346
|
const executionTime = Date.now() - startTime;
|
|
347
|
+
if (deferredConcurrent) {
|
|
348
|
+
await this.db.query(`UPDATE _smrt_schema_migrations
|
|
349
|
+
SET status = 'failed', execution_time_ms = ?, error_message = ?
|
|
350
|
+
WHERE id = ?`, executionTime, CONCURRENT_INDEX_PENDING_MESSAGE, id);
|
|
351
|
+
return {
|
|
352
|
+
success: true,
|
|
353
|
+
applied: true,
|
|
354
|
+
skipped: false,
|
|
355
|
+
name: definition.id,
|
|
356
|
+
checksum,
|
|
357
|
+
execution_time_ms: executionTime
|
|
358
|
+
};
|
|
359
|
+
}
|
|
269
360
|
await this.db.query(`UPDATE _smrt_schema_migrations
|
|
270
361
|
SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, applied_at = CURRENT_TIMESTAMP
|
|
271
362
|
WHERE id = ?`, executionTime, checksum, id);
|
|
@@ -299,36 +390,66 @@ var MigrationTracker = class MigrationTracker {
|
|
|
299
390
|
}
|
|
300
391
|
}
|
|
301
392
|
/**
|
|
302
|
-
* Apply multiple migrations in order
|
|
393
|
+
* Apply multiple migrations in order.
|
|
394
|
+
*
|
|
395
|
+
* On PostgreSQL the atomic path always bounds itself with
|
|
396
|
+
* `SET LOCAL lock_timeout` / `SET LOCAL statement_timeout` so a migration
|
|
397
|
+
* that queues behind a long-running writer fails fast and rolls back
|
|
398
|
+
* instead of stalling every writer behind its already-held locks
|
|
399
|
+
* (issue #2362).
|
|
400
|
+
*
|
|
401
|
+
* `options.postgresSafe` additionally enables **concurrent-index mode**:
|
|
402
|
+
* non-index DDL still runs in the one atomic transaction, then every index
|
|
403
|
+
* statement runs `CONCURRENTLY` on a pinned session after that transaction
|
|
404
|
+
* commits. That mode trades atomicity for availability — a failure part way
|
|
405
|
+
* through the index phase leaves the committed non-index DDL in place, and
|
|
406
|
+
* the unfinished index migrations are recorded `failed` so the next run
|
|
407
|
+
* retries them, resuming at the index phase rather than re-running the
|
|
408
|
+
* non-index statements that already committed.
|
|
303
409
|
*/
|
|
304
410
|
async applyAll(definitions, options = {}) {
|
|
305
411
|
if (options.atomic && !options.dryRun) {
|
|
306
412
|
if (!this.db.transaction) throw new Error("Atomic migration batches require a database adapter with transaction support.");
|
|
307
|
-
|
|
413
|
+
const isPostgres = this.dbEngine === "postgres";
|
|
414
|
+
const concurrentIndexMode = isPostgres && Boolean(options.postgresSafe);
|
|
415
|
+
const plan = concurrentIndexMode ? buildConcurrentIndexPlan(definitions, this.options.useConcurrentIndexes) : null;
|
|
416
|
+
if (isPostgres && !concurrentIndexMode) {
|
|
308
417
|
const concurrentStatement = findConcurrentIndexStatement(definitions);
|
|
309
|
-
if (concurrentStatement) throw new Error(`Atomic migration batch cannot include CONCURRENTLY index DDL in ${concurrentStatement.migrationName}; PostgreSQL forbids CONCURRENTLY inside a transaction.`);
|
|
418
|
+
if (concurrentStatement) throw new Error(`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.`);
|
|
310
419
|
}
|
|
420
|
+
const deferred = plan && plan.size > 0 ? plan : null;
|
|
311
421
|
try {
|
|
312
|
-
|
|
422
|
+
const committed = await this.db.transaction(async (tx) => {
|
|
313
423
|
const txDb = {
|
|
314
424
|
...tx,
|
|
315
425
|
transaction: async (callback) => callback(tx)
|
|
316
426
|
};
|
|
317
|
-
|
|
427
|
+
if (isPostgres) {
|
|
428
|
+
await tx.query(`SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`);
|
|
429
|
+
await tx.query(`SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`);
|
|
430
|
+
}
|
|
431
|
+
const txTracker = new MigrationTracker({
|
|
318
432
|
db: txDb,
|
|
319
433
|
lockTimeout: this.options.lockTimeout,
|
|
320
434
|
statementTimeout: this.options.statementTimeout,
|
|
321
435
|
useConcurrentIndexes: false
|
|
322
|
-
})
|
|
436
|
+
});
|
|
437
|
+
txTracker.concurrentIndexPlan = deferred;
|
|
438
|
+
const results = await txTracker.applyAll(definitions, {
|
|
323
439
|
...options,
|
|
324
440
|
atomic: false,
|
|
325
441
|
continueOnError: false,
|
|
326
|
-
postgresSafe: false
|
|
442
|
+
postgresSafe: false,
|
|
443
|
+
onProgress: options.onProgress ? (result) => {
|
|
444
|
+
if (!deferred?.has(result.name)) options.onProgress?.(result);
|
|
445
|
+
} : void 0
|
|
327
446
|
});
|
|
328
447
|
const failed = results.find((result) => !result.success);
|
|
329
448
|
if (failed) throw new AtomicMigrationRollback(results, failed);
|
|
330
449
|
return results;
|
|
331
450
|
});
|
|
451
|
+
if (!deferred) return committed;
|
|
452
|
+
return await this.applyDeferredConcurrentIndexes(committed, deferred, options);
|
|
332
453
|
} catch (error) {
|
|
333
454
|
if (error instanceof AtomicMigrationRollback) return error.results.map((result) => {
|
|
334
455
|
if (!result.success || !result.applied) return result;
|
|
@@ -356,6 +477,161 @@ var MigrationTracker = class MigrationTracker {
|
|
|
356
477
|
return results;
|
|
357
478
|
}
|
|
358
479
|
/**
|
|
480
|
+
* Phase 2 of PostgreSQL concurrent-index mode.
|
|
481
|
+
*
|
|
482
|
+
* Runs every deferred index statement on one pinned session, outside the
|
|
483
|
+
* committed transaction, bounded by session `lock_timeout` /
|
|
484
|
+
* `statement_timeout`. INVALID indexes left behind by an earlier failed
|
|
485
|
+
* `CREATE INDEX CONCURRENTLY` are dropped first so the retry rebuilds them
|
|
486
|
+
* rather than skipping over a stump that reads as present.
|
|
487
|
+
*
|
|
488
|
+
* On failure the batch stops: the failing migration and every migration it
|
|
489
|
+
* did not reach are recorded `failed`, which the CLI's reconciling
|
|
490
|
+
* `db:migrate` retries on the next run.
|
|
491
|
+
*/
|
|
492
|
+
async applyDeferredConcurrentIndexes(committed, plan, options) {
|
|
493
|
+
const results = [...committed];
|
|
494
|
+
const session = await this.acquireMigrationSession();
|
|
495
|
+
try {
|
|
496
|
+
await session.query(`SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`);
|
|
497
|
+
await session.query(`SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`);
|
|
498
|
+
const invalidIndexes = await this.readInvalidIndexNames(session);
|
|
499
|
+
let stoppedBy = null;
|
|
500
|
+
for (let index = 0; index < results.length; index++) {
|
|
501
|
+
const result = results[index];
|
|
502
|
+
const split = plan.get(result.name);
|
|
503
|
+
if (!split || split.concurrent.length === 0) continue;
|
|
504
|
+
if (result.skipped) continue;
|
|
505
|
+
if (stoppedBy) {
|
|
506
|
+
const error = /* @__PURE__ */ new Error(`Not attempted: concurrent index phase stopped after ${stoppedBy} failed`);
|
|
507
|
+
await this.recordConcurrentIndexFailure(result.name, error.message);
|
|
508
|
+
results[index] = {
|
|
509
|
+
...result,
|
|
510
|
+
success: false,
|
|
511
|
+
applied: false,
|
|
512
|
+
skipped: false,
|
|
513
|
+
error
|
|
514
|
+
};
|
|
515
|
+
options.onProgress?.(results[index]);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const startedAt = Date.now();
|
|
519
|
+
try {
|
|
520
|
+
for (const sql of split.concurrent) {
|
|
521
|
+
const indexName = extractCreatedIndexName(sql);
|
|
522
|
+
if (indexName && invalidIndexes.has(indexName)) {
|
|
523
|
+
logger.warn(`Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`);
|
|
524
|
+
await session.query(`DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`);
|
|
525
|
+
invalidIndexes.delete(indexName);
|
|
526
|
+
}
|
|
527
|
+
await session.query(sql);
|
|
528
|
+
}
|
|
529
|
+
const executionTime = Math.max(1, (result.execution_time_ms ?? 0) + (Date.now() - startedAt));
|
|
530
|
+
await this.recordConcurrentIndexSuccess(result.name, result.checksum, executionTime);
|
|
531
|
+
results[index] = {
|
|
532
|
+
...result,
|
|
533
|
+
success: true,
|
|
534
|
+
applied: true,
|
|
535
|
+
skipped: false,
|
|
536
|
+
execution_time_ms: executionTime
|
|
537
|
+
};
|
|
538
|
+
} catch (error) {
|
|
539
|
+
const indexError = error instanceof Error ? error : new Error(String(error));
|
|
540
|
+
if (!options.continueOnError) stoppedBy = result.name;
|
|
541
|
+
await this.recordConcurrentIndexFailure(result.name, indexError.message);
|
|
542
|
+
results[index] = {
|
|
543
|
+
...result,
|
|
544
|
+
success: false,
|
|
545
|
+
applied: false,
|
|
546
|
+
skipped: false,
|
|
547
|
+
execution_time_ms: Date.now() - startedAt,
|
|
548
|
+
error: indexError
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
options.onProgress?.(results[index]);
|
|
552
|
+
}
|
|
553
|
+
return results;
|
|
554
|
+
} finally {
|
|
555
|
+
await session.release();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Pin one connection for the concurrent-index phase.
|
|
560
|
+
*
|
|
561
|
+
* PostgreSQL pools hand each top-level `query` an arbitrary connection, so
|
|
562
|
+
* a `SET lock_timeout` issued through the pool may not apply to the DDL
|
|
563
|
+
* that follows. Adapters that expose `acquireSession` give us a pinned
|
|
564
|
+
* connection; anything else (SQLite, DuckDB, test doubles) is
|
|
565
|
+
* single-connection anyway and falls back to the plain interface.
|
|
566
|
+
*/
|
|
567
|
+
async acquireMigrationSession() {
|
|
568
|
+
if (typeof this.db.acquireSession === "function") {
|
|
569
|
+
const session = await this.db.acquireSession();
|
|
570
|
+
return {
|
|
571
|
+
query: (sql, ...vars) => session.query(sql, ...vars),
|
|
572
|
+
release: () => session.release()
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
query: (sql, ...vars) => this.db.query(sql, ...vars),
|
|
577
|
+
release: async () => {}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Read the names of INVALID indexes that an unqualified index name in this
|
|
582
|
+
* session would actually resolve to.
|
|
583
|
+
*
|
|
584
|
+
* `pg_indexes` (what the SQL adapter's introspection reads) lists an
|
|
585
|
+
* INVALID index exactly like a healthy one, so the schema differ believes a
|
|
586
|
+
* failed `CREATE INDEX CONCURRENTLY` succeeded. `pg_index.indisvalid` is the
|
|
587
|
+
* authoritative flag.
|
|
588
|
+
*
|
|
589
|
+
* Scoped to `current_schemas(false)` — the session's search path — because
|
|
590
|
+
* migration DDL names indexes unqualified, and so does the
|
|
591
|
+
* `DROP INDEX CONCURRENTLY IF EXISTS "<name>"` repair. Scanning every schema
|
|
592
|
+
* would let an unrelated invalid index of the same name in another schema
|
|
593
|
+
* trigger a drop that then resolves to a *different* object. Where the same
|
|
594
|
+
* name appears in several search-path schemas, the earliest on the path wins
|
|
595
|
+
* — the same rule PostgreSQL uses to resolve the name.
|
|
596
|
+
*
|
|
597
|
+
* A role without catalog access degrades to "no known invalid indexes"
|
|
598
|
+
* rather than aborting the migration.
|
|
599
|
+
*/
|
|
600
|
+
async readInvalidIndexNames(session) {
|
|
601
|
+
const names = /* @__PURE__ */ new Set();
|
|
602
|
+
try {
|
|
603
|
+
const result = await session.query(`SELECT DISTINCT ON (c.relname) c.relname AS index_name
|
|
604
|
+
FROM pg_index i
|
|
605
|
+
JOIN pg_class c ON c.oid = i.indexrelid
|
|
606
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
607
|
+
WHERE i.indisvalid = false
|
|
608
|
+
AND n.nspname = ANY (current_schemas(false))
|
|
609
|
+
ORDER BY c.relname,
|
|
610
|
+
array_position(current_schemas(false), n.nspname::text)`);
|
|
611
|
+
for (const row of result.rows) {
|
|
612
|
+
const name = row.index_name;
|
|
613
|
+
if (typeof name === "string") names.add(name);
|
|
614
|
+
}
|
|
615
|
+
} catch (error) {
|
|
616
|
+
logger.warn(`Could not read pg_index.indisvalid; skipping INVALID index repair: ${error instanceof Error ? error.message : String(error)}`);
|
|
617
|
+
}
|
|
618
|
+
return names;
|
|
619
|
+
}
|
|
620
|
+
async recordConcurrentIndexSuccess(name, checksum, executionTime) {
|
|
621
|
+
await this.db.query(`UPDATE _smrt_schema_migrations
|
|
622
|
+
SET status = 'completed', execution_time_ms = ?, applied_checksum = ?, error_message = NULL, applied_at = CURRENT_TIMESTAMP
|
|
623
|
+
WHERE name = ?`, executionTime, checksum, name);
|
|
624
|
+
}
|
|
625
|
+
async recordConcurrentIndexFailure(name, message) {
|
|
626
|
+
try {
|
|
627
|
+
await this.db.query(`UPDATE _smrt_schema_migrations
|
|
628
|
+
SET status = 'failed', error_message = ?
|
|
629
|
+
WHERE name = ?`, `${CONCURRENT_INDEX_PHASE1_MARKER} ${message}`, name);
|
|
630
|
+
} catch (persistError) {
|
|
631
|
+
logger.error(`Failed to persist failed status for migration ${name}: ${persistError instanceof Error ? persistError.message : String(persistError)}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
359
635
|
* Rollback a migration
|
|
360
636
|
*/
|
|
361
637
|
async rollback(name, definition, options = {}) {
|
|
@@ -472,18 +748,57 @@ var MigrationTracker = class MigrationTracker {
|
|
|
472
748
|
async executePostgresStatements(statements) {
|
|
473
749
|
const { concurrent: finalConcurrent, regular: finalRegular } = planPostgresStatements(statements, !!this.options.useConcurrentIndexes);
|
|
474
750
|
if (finalRegular.length > 0 && this.db.transaction) await this.db.transaction(async (tx) => {
|
|
475
|
-
await tx.query(`SET LOCAL lock_timeout = '${this.options.lockTimeout}
|
|
476
|
-
await tx.query(`SET LOCAL statement_timeout = '${this.options.statementTimeout}
|
|
751
|
+
await tx.query(`SET LOCAL lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`);
|
|
752
|
+
await tx.query(`SET LOCAL statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`);
|
|
477
753
|
for (const sql of finalRegular) await tx.query(sql);
|
|
478
754
|
});
|
|
479
755
|
else if (finalRegular.length > 0) for (const sql of finalRegular) await this.db.query(sql);
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
756
|
+
if (finalConcurrent.length === 0) return;
|
|
757
|
+
const session = await this.acquireMigrationSession();
|
|
758
|
+
try {
|
|
759
|
+
await session.query(`SET lock_timeout = '${formatPostgresTimeout(this.options.lockTimeout)}'`);
|
|
760
|
+
await session.query(`SET statement_timeout = '${formatPostgresTimeout(this.options.statementTimeout)}'`);
|
|
761
|
+
const invalidIndexes = await this.readInvalidIndexNames(session);
|
|
762
|
+
for (const sql of finalConcurrent) {
|
|
763
|
+
const indexName = extractCreatedIndexName(sql);
|
|
764
|
+
if (indexName && invalidIndexes.has(indexName)) {
|
|
765
|
+
logger.warn(`Rebuilding INVALID PostgreSQL index ${indexName} left by a previous failed CREATE INDEX CONCURRENTLY`);
|
|
766
|
+
await session.query(`DROP INDEX CONCURRENTLY IF EXISTS ${quoteIndexName(indexName)}`);
|
|
767
|
+
invalidIndexes.delete(indexName);
|
|
768
|
+
}
|
|
769
|
+
await session.query(sql);
|
|
770
|
+
}
|
|
771
|
+
} finally {
|
|
772
|
+
await session.release();
|
|
483
773
|
}
|
|
484
774
|
}
|
|
485
775
|
};
|
|
486
776
|
/**
|
|
777
|
+
* Quote an index identifier for PostgreSQL, escaping embedded double quotes.
|
|
778
|
+
*/
|
|
779
|
+
function quoteIndexName(name) {
|
|
780
|
+
return `"${name.replace(/"/g, "\"\"")}"`;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Build the PostgreSQL concurrent-index plan for an atomic batch.
|
|
784
|
+
*
|
|
785
|
+
* Returns one entry per migration definition that has at least one statement
|
|
786
|
+
* which must leave the transaction, mapping it to the transaction-safe
|
|
787
|
+
* statements (phase 1) and the `CONCURRENTLY` statements (phase 2).
|
|
788
|
+
* Definitions with nothing to defer are omitted so the caller can treat
|
|
789
|
+
* `plan.has(id)` as "this migration finishes in phase 2".
|
|
790
|
+
*
|
|
791
|
+
* Exported for unit testing.
|
|
792
|
+
*/
|
|
793
|
+
function buildConcurrentIndexPlan(definitions, useConcurrentIndexes) {
|
|
794
|
+
const plan = /* @__PURE__ */ new Map();
|
|
795
|
+
for (const definition of definitions) {
|
|
796
|
+
const split = planPostgresStatements(definition.up, useConcurrentIndexes);
|
|
797
|
+
if (split.concurrent.length > 0) plan.set(definition.id, split);
|
|
798
|
+
}
|
|
799
|
+
return plan;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
487
802
|
* Split a batch of SQL statements into a transaction-safe set and a
|
|
488
803
|
* concurrent set, applying `--postgres-safe` rewriting if requested.
|
|
489
804
|
*
|
|
@@ -527,6 +842,6 @@ function planPostgresStatements(statements, useConcurrentIndexes) {
|
|
|
527
842
|
};
|
|
528
843
|
}
|
|
529
844
|
//#endregion
|
|
530
|
-
export { MigrationTracker, planPostgresStatements };
|
|
845
|
+
export { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, parsePostgresTimeoutMs, planPostgresStatements };
|
|
531
846
|
|
|
532
847
|
//# sourceMappingURL=tracker.js.map
|