@happyvertical/smrt-core 0.40.68 → 0.40.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/AGENTS.md +29 -4
  2. package/README.md +20 -1
  3. package/agents/query-bounds.md +45 -0
  4. package/agents/schema-paths.md +328 -0
  5. package/dist/browser.d.ts +1 -0
  6. package/dist/browser.d.ts.map +1 -1
  7. package/dist/browser.js +2 -1
  8. package/dist/class.d.ts.map +1 -1
  9. package/dist/class.js +28 -4
  10. package/dist/class.js.map +1 -1
  11. package/dist/collection.d.ts +101 -2
  12. package/dist/collection.d.ts.map +1 -1
  13. package/dist/collection.js +224 -43
  14. package/dist/collection.js.map +1 -1
  15. package/dist/database.d.ts +28 -0
  16. package/dist/database.d.ts.map +1 -1
  17. package/dist/database.js +16 -8
  18. package/dist/database.js.map +1 -1
  19. package/dist/db-errors.d.ts +105 -0
  20. package/dist/db-errors.d.ts.map +1 -0
  21. package/dist/db-errors.js +382 -0
  22. package/dist/db-errors.js.map +1 -0
  23. package/dist/decorators/index.d.ts +17 -1
  24. package/dist/decorators/index.d.ts.map +1 -1
  25. package/dist/decorators/index.js +65 -7
  26. package/dist/decorators/index.js.map +1 -1
  27. package/dist/dispatch/bus.d.ts.map +1 -1
  28. package/dist/dispatch/bus.js +4 -3
  29. package/dist/dispatch/bus.js.map +1 -1
  30. package/dist/errors.d.ts +30 -2
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js +34 -2
  33. package/dist/errors.js.map +1 -1
  34. package/dist/generators/changes-route.d.ts.map +1 -1
  35. package/dist/generators/changes-route.js +6 -3
  36. package/dist/generators/changes-route.js.map +1 -1
  37. package/dist/generators/mcp-runtime-template.d.ts +8 -0
  38. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  39. package/dist/generators/mcp-runtime-template.js +38 -4
  40. package/dist/generators/mcp-runtime-template.js.map +1 -1
  41. package/dist/generators/mcp.d.ts +16 -0
  42. package/dist/generators/mcp.d.ts.map +1 -1
  43. package/dist/generators/mcp.js +41 -3
  44. package/dist/generators/mcp.js.map +1 -1
  45. package/dist/generators/rest.d.ts +22 -0
  46. package/dist/generators/rest.d.ts.map +1 -1
  47. package/dist/generators/rest.js +34 -3
  48. package/dist/generators/rest.js.map +1 -1
  49. package/dist/index.d.ts +6 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +8 -3
  52. package/dist/manifest/static-manifest.js +1 -1
  53. package/dist/manifest/static-manifest.js.map +1 -1
  54. package/dist/manifest/store.js +1 -1
  55. package/dist/manifest.json +1 -1
  56. package/dist/migrations/differ.d.ts +202 -8
  57. package/dist/migrations/differ.d.ts.map +1 -1
  58. package/dist/migrations/differ.js +604 -46
  59. package/dist/migrations/differ.js.map +1 -1
  60. package/dist/migrations/generator.d.ts +31 -4
  61. package/dist/migrations/generator.d.ts.map +1 -1
  62. package/dist/migrations/generator.js +49 -5
  63. package/dist/migrations/generator.js.map +1 -1
  64. package/dist/migrations/index.d.ts +3 -2
  65. package/dist/migrations/index.d.ts.map +1 -1
  66. package/dist/migrations/index.js +5 -3
  67. package/dist/migrations/orchestrate.js +35 -6
  68. package/dist/migrations/orchestrate.js.map +1 -1
  69. package/dist/migrations/sqlite-rebuild.d.ts +142 -0
  70. package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
  71. package/dist/migrations/sqlite-rebuild.js +514 -0
  72. package/dist/migrations/sqlite-rebuild.js.map +1 -0
  73. package/dist/migrations/tracker.d.ts +114 -1
  74. package/dist/migrations/tracker.d.ts.map +1 -1
  75. package/dist/migrations/tracker.js +331 -16
  76. package/dist/migrations/tracker.js.map +1 -1
  77. package/dist/migrations/types.d.ts +19 -4
  78. package/dist/migrations/types.d.ts.map +1 -1
  79. package/dist/migrations.js +5 -3
  80. package/dist/object.d.ts +45 -6
  81. package/dist/object.d.ts.map +1 -1
  82. package/dist/object.js +60 -13
  83. package/dist/object.js.map +1 -1
  84. package/dist/postgres-timeouts.d.ts +240 -0
  85. package/dist/postgres-timeouts.d.ts.map +1 -0
  86. package/dist/postgres-timeouts.js +204 -0
  87. package/dist/postgres-timeouts.js.map +1 -0
  88. package/dist/query-bounds.d.ts +97 -0
  89. package/dist/query-bounds.d.ts.map +1 -0
  90. package/dist/query-bounds.js +173 -0
  91. package/dist/query-bounds.js.map +1 -0
  92. package/dist/registry/types.d.ts +32 -1
  93. package/dist/registry/types.d.ts.map +1 -1
  94. package/dist/registry.js +1 -1
  95. package/dist/scanner/types.d.ts +5 -0
  96. package/dist/scanner/types.d.ts.map +1 -1
  97. package/dist/scanner/types.js.map +1 -1
  98. package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
  99. package/dist/schema/ddl/base-strategy.js +2 -2
  100. package/dist/schema/ddl/base-strategy.js.map +1 -1
  101. package/dist/schema/generator.d.ts +70 -5
  102. package/dist/schema/generator.d.ts.map +1 -1
  103. package/dist/schema/generator.js +124 -6
  104. package/dist/schema/generator.js.map +1 -1
  105. package/dist/schema/index.d.ts +3 -0
  106. package/dist/schema/index.d.ts.map +1 -1
  107. package/dist/schema/index.js +4 -1
  108. package/dist/schema/live-parity.d.ts +90 -0
  109. package/dist/schema/live-parity.d.ts.map +1 -0
  110. package/dist/schema/live-parity.js +600 -0
  111. package/dist/schema/live-parity.js.map +1 -0
  112. package/dist/schema/manifest-schema.d.ts +121 -0
  113. package/dist/schema/manifest-schema.d.ts.map +1 -0
  114. package/dist/schema/manifest-schema.js +267 -0
  115. package/dist/schema/manifest-schema.js.map +1 -0
  116. package/dist/schema/schema-aggregator.d.ts +24 -10
  117. package/dist/schema/schema-aggregator.d.ts.map +1 -1
  118. package/dist/schema/schema-aggregator.js +35 -90
  119. package/dist/schema/schema-aggregator.js.map +1 -1
  120. package/dist/schema/system-table-shapes.d.ts +52 -0
  121. package/dist/schema/system-table-shapes.d.ts.map +1 -0
  122. package/dist/schema/system-table-shapes.js +174 -0
  123. package/dist/schema/system-table-shapes.js.map +1 -0
  124. package/dist/schema/types.d.ts +103 -4
  125. package/dist/schema/types.d.ts.map +1 -1
  126. package/dist/schema/utils.d.ts +1 -0
  127. package/dist/schema/utils.d.ts.map +1 -1
  128. package/dist/schema/utils.js +3 -1
  129. package/dist/schema/utils.js.map +1 -1
  130. package/dist/schema.js +4 -1
  131. package/dist/smrt-knowledge.json +18 -6
  132. package/dist/sync/apply.d.ts.map +1 -1
  133. package/dist/sync/apply.js +9 -16
  134. package/dist/sync/apply.js.map +1 -1
  135. package/dist/testing/database.d.ts.map +1 -1
  136. package/dist/testing/database.js +1 -0
  137. package/dist/testing/database.js.map +1 -1
  138. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  139. package/dist/vite-plugin/sveltekit-generator.js +97 -13
  140. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  141. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  142. package/dist/vite-plugin/web-collections.js +6 -4
  143. package/dist/vite-plugin/web-collections.js.map +1 -1
  144. package/package.json +11 -11
@@ -1,6 +1,36 @@
1
+ import { parsePostgresTimeoutMs } from '../postgres-timeouts.js';
1
2
  import { DatabaseEngine } from '../schema/ddl/types.js';
2
3
  import { DriftReport, MigrationDefinition, MigrationResult, MigrationStatus, SchemaMigrationRecord } from '../schema/types.js';
3
4
  import { ApplyMigrationsOptions, MigrationTrackerOptions, RollbackOptions } from './types.js';
5
+ /**
6
+ * Extract the index name from a `CREATE [UNIQUE] INDEX …` statement.
7
+ *
8
+ * Returns the unquoted identifier, or `null` when the statement is not a
9
+ * CREATE INDEX (a drop, a reindex, or unrelated DDL).
10
+ *
11
+ * Exported for unit testing and because the CLI's index rollout needs the
12
+ * same correlation to explain what it repaired.
13
+ */
14
+ export declare function extractCreatedIndexName(sql: string): string | null;
15
+ /**
16
+ * Parse a PostgreSQL timeout string into milliseconds.
17
+ *
18
+ * The implementation lives in `src/postgres-timeouts.ts`, the leaf module the
19
+ * runtime pool bounds are built from (#2377), so `migrations.postgres.*` and a
20
+ * runtime `timeouts` config are parsed by literally the same function rather
21
+ * than by two copies that can drift. It is re-exported here — and from
22
+ * `./index.ts` — because `@happyvertical/smrt-core/migrations` is the public
23
+ * subpath `db:migrate` reads it through.
24
+ */
25
+ export { parsePostgresTimeoutMs };
26
+ /**
27
+ * Statements split out of the atomic batch for PostgreSQL concurrent-index
28
+ * mode, keyed by migration definition id.
29
+ */
30
+ type ConcurrentIndexPlan = Map<string, {
31
+ regular: string[];
32
+ concurrent: string[];
33
+ }>;
4
34
  /**
5
35
  * MigrationTracker class
6
36
  *
@@ -26,6 +56,15 @@ export declare class MigrationTracker {
26
56
  */
27
57
  private initializePromise;
28
58
  private currentBatch;
59
+ /**
60
+ * Statements deferred out of the current atomic batch by PostgreSQL
61
+ * concurrent-index mode. The parent tracker sets this on the
62
+ * transaction-scoped tracker before phase 1 so `apply()` executes only the
63
+ * transaction-safe statements and leaves the migration record in `running`;
64
+ * the parent finalizes those records after the transaction commits and the
65
+ * `CONCURRENTLY` DDL has run. `null` on every ordinary path.
66
+ */
67
+ private concurrentIndexPlan;
29
68
  constructor(options: MigrationTrackerOptions);
30
69
  /**
31
70
  * Detect the database engine. Honors an explicit `engineHint` (passed via
@@ -73,9 +112,71 @@ export declare class MigrationTracker {
73
112
  */
74
113
  apply(definition: MigrationDefinition, options?: ApplyMigrationsOptions): Promise<MigrationResult>;
75
114
  /**
76
- * Apply multiple migrations in order
115
+ * Apply multiple migrations in order.
116
+ *
117
+ * On PostgreSQL the atomic path always bounds itself with
118
+ * `SET LOCAL lock_timeout` / `SET LOCAL statement_timeout` so a migration
119
+ * that queues behind a long-running writer fails fast and rolls back
120
+ * instead of stalling every writer behind its already-held locks
121
+ * (issue #2362).
122
+ *
123
+ * `options.postgresSafe` additionally enables **concurrent-index mode**:
124
+ * non-index DDL still runs in the one atomic transaction, then every index
125
+ * statement runs `CONCURRENTLY` on a pinned session after that transaction
126
+ * commits. That mode trades atomicity for availability — a failure part way
127
+ * through the index phase leaves the committed non-index DDL in place, and
128
+ * the unfinished index migrations are recorded `failed` so the next run
129
+ * retries them, resuming at the index phase rather than re-running the
130
+ * non-index statements that already committed.
77
131
  */
78
132
  applyAll(definitions: MigrationDefinition[], options?: ApplyMigrationsOptions): Promise<MigrationResult[]>;
133
+ /**
134
+ * Phase 2 of PostgreSQL concurrent-index mode.
135
+ *
136
+ * Runs every deferred index statement on one pinned session, outside the
137
+ * committed transaction, bounded by session `lock_timeout` /
138
+ * `statement_timeout`. INVALID indexes left behind by an earlier failed
139
+ * `CREATE INDEX CONCURRENTLY` are dropped first so the retry rebuilds them
140
+ * rather than skipping over a stump that reads as present.
141
+ *
142
+ * On failure the batch stops: the failing migration and every migration it
143
+ * did not reach are recorded `failed`, which the CLI's reconciling
144
+ * `db:migrate` retries on the next run.
145
+ */
146
+ private applyDeferredConcurrentIndexes;
147
+ /**
148
+ * Pin one connection for the concurrent-index phase.
149
+ *
150
+ * PostgreSQL pools hand each top-level `query` an arbitrary connection, so
151
+ * a `SET lock_timeout` issued through the pool may not apply to the DDL
152
+ * that follows. Adapters that expose `acquireSession` give us a pinned
153
+ * connection; anything else (SQLite, DuckDB, test doubles) is
154
+ * single-connection anyway and falls back to the plain interface.
155
+ */
156
+ private acquireMigrationSession;
157
+ /**
158
+ * Read the names of INVALID indexes that an unqualified index name in this
159
+ * session would actually resolve to.
160
+ *
161
+ * `pg_indexes` (what the SQL adapter's introspection reads) lists an
162
+ * INVALID index exactly like a healthy one, so the schema differ believes a
163
+ * failed `CREATE INDEX CONCURRENTLY` succeeded. `pg_index.indisvalid` is the
164
+ * authoritative flag.
165
+ *
166
+ * Scoped to `current_schemas(false)` — the session's search path — because
167
+ * migration DDL names indexes unqualified, and so does the
168
+ * `DROP INDEX CONCURRENTLY IF EXISTS "<name>"` repair. Scanning every schema
169
+ * would let an unrelated invalid index of the same name in another schema
170
+ * trigger a drop that then resolves to a *different* object. Where the same
171
+ * name appears in several search-path schemas, the earliest on the path wins
172
+ * — the same rule PostgreSQL uses to resolve the name.
173
+ *
174
+ * A role without catalog access degrades to "no known invalid indexes"
175
+ * rather than aborting the migration.
176
+ */
177
+ private readInvalidIndexNames;
178
+ private recordConcurrentIndexSuccess;
179
+ private recordConcurrentIndexFailure;
79
180
  /**
80
181
  * Rollback a migration
81
182
  */
@@ -110,6 +211,18 @@ export declare class MigrationTracker {
110
211
  */
111
212
  private executePostgresStatements;
112
213
  }
214
+ /**
215
+ * Build the PostgreSQL concurrent-index plan for an atomic batch.
216
+ *
217
+ * Returns one entry per migration definition that has at least one statement
218
+ * which must leave the transaction, mapping it to the transaction-safe
219
+ * statements (phase 1) and the `CONCURRENTLY` statements (phase 2).
220
+ * Definitions with nothing to defer are omitted so the caller can treat
221
+ * `plan.has(id)` as "this migration finishes in phase 2".
222
+ *
223
+ * Exported for unit testing.
224
+ */
225
+ export declare function buildConcurrentIndexPlan(definitions: MigrationDefinition[], useConcurrentIndexes: boolean): ConcurrentIndexPlan;
113
226
  /**
114
227
  * Split a batch of SQL statements into a transaction-safe set and a
115
228
  * concurrent set, applying `--postgres-safe` rewriting if requested.
@@ -1 +1 @@
1
- {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/tracker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAO5B,OAAO,KAAK,EACV,sBAAsB,EAEtB,uBAAuB,EACvB,eAAe,EAChB,MAAM,YAAY,CAAC;AAkEpB;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,OAAO,CAA+D;IAC9E,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAiB;IACjC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,YAAY,CAAuB;gBAE/B,OAAO,EAAE,uBAAuB;IAa5C;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAQtB;;OAEG;IACH,SAAS,IAAI,cAAc;IAI3B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAUnB,gBAAgB;IAiB9B;;OAEG;IACG,oBAAoB,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAe9D;;OAEG;IACG,oBAAoB,CACxB,WAAW,EAAE,mBAAmB,EAAE,GACjC,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAMjC;;OAEG;IACG,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWtD;;OAEG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAmBvE;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAWrC;;OAEG;IACG,WAAW,CACf,WAAW,EAAE,mBAAmB,EAAE,GACjC,OAAO,CAAC,WAAW,EAAE,CAAC;IAkDzB;;OAEG;IACG,KAAK,CACT,UAAU,EAAE,mBAAmB,EAC/B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,eAAe,CAAC;IA6L3B;;OAEG;IACG,QAAQ,CACZ,WAAW,EAAE,mBAAmB,EAAE,EAClC,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,eAAe,EAAE,CAAC;IAsF7B;;OAEG;IACG,QAAQ,CACZ,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,mBAAmB,EAC/B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC;IAiF3B;;OAEG;IACG,UAAU,CACd,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,eAAe,CAAA;KAAO,GACvE,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAiCnC;;OAEG;YACW,iBAAiB;IAW/B;;OAEG;YACW,sBAAsB;IAepC;;;;;;;;;;;OAWG;YACW,yBAAyB;CAqCxC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAAE,EACpB,oBAAoB,EAAE,OAAO,GAC5B;IAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAgC7C"}
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/tracker.ts"],"names":[],"mappings":"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"}
@@ -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 ?? DEFAULT_OPTIONS.lockTimeout,
73
- statementTimeout: options.statementTimeout ?? DEFAULT_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
- if (this.dbEngine === "postgres") {
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
- return await this.db.transaction(async (tx) => {
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
- const results = await new MigrationTracker({
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
- }).applyAll(definitions, {
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}ms'`);
476
- await tx.query(`SET LOCAL statement_timeout = '${this.options.statementTimeout}ms'`);
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
- for (const sql of finalConcurrent) {
481
- await this.db.query(`SET lock_timeout = '${this.options.lockTimeout}ms'`);
482
- await this.db.query(sql);
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