@voltro/sql-mssql 0.43.2 → 0.44.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/CHANGELOG.md CHANGED
@@ -39,6 +39,40 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.44.0] — 2026-08-19
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/data-transfer, @voltro/cli** — `runImport` returns an `ImportOutcome` instead of the bundle's `Manifest`, and `--mode replace` runs as ONE transaction by default.
47
+
48
+ **Why the return type changed.** The summary line counted the rows the BUNDLE carries, not the rows the run wrote. Those differ most exactly where it matters: a bundle directory carries its own resume ledger, so a copied directory imports nothing — correctly, with a warning naming the file to delete — and the run then printed `import complete … rows: 242950` over a target it had not touched. The warning was one line above, which is one line too far for anyone piping the output through `tail -1`. The outcome carries `rowsWritten`, `rowsSkipped` and `fullyResumed`, the CLI reports written-vs-carried, and a fully-skipped run says so on its LAST line.
49
+
50
+ Migration: `runImport(...)` now resolves to `{ manifest, tablesWritten, rowsWritten, tablesSkipped, rowsSkipped, fullyResumed }`. Read `.manifest` where you read the manifest before.
51
+
52
+ **Why replace is atomic now.** The all-or-nothing guarantee was written for the emptying step, and read — reasonably — as covering the run. A replace that died partway through the LOAD left the target emptied of its old rows and holding part of the new ones, measured on a live instance over sixteen minutes. There is no useful state for a replace to stop in, so it is a default rather than a flag you have to know about. It also closes the window that produced that failure: with the tables emptied and the load uncommitted, a concurrent writer in the application waits instead of inserting a row the bundle is about to insert too.
53
+
54
+ `--no-atomic` (CLI) / `atomic: false` (API) opts out. The trade is stated where it bites: every write to those tables waits for the load, and on postgres the bulk `COPY` loader cannot join a transaction it does not own — an atomic run now says that once rather than being quietly slower.
55
+
56
+ Two smaller things from the same report: `--mode replace` against a running instance warns about the empty-target window when it is NOT atomic, and the `--target api` upload reports progress per chunk plus a line explaining that the final request stays open for the whole import — sixteen minutes of silence is indistinguishable from a hang, and one operator killed a run that had finished.
57
+
58
+ **`voltro update` carries you across this** — codemod `0.44.0/01_import-outcome`.
59
+
60
+ ### Fixed
61
+
62
+ - **@voltro/sql-mysql, @voltro/sql-postgres, @voltro/sql-sqlite, @voltro/sql-mssql, @voltro/data-transfer** — A write and its write-recorders now succeed or fail together on every SQL dialect, and the data importer's retry is idempotent.
63
+
64
+ A recorder (a versioning trail, an audit log) runs on the caller's connection and is ALLOWED to fail — a recorder that throws must take the write down with it, that is its contract. Outside a caller transaction the two were not one unit: the row's statement committed on its own, and the recorder's INSERT ran afterwards as a second autocommit statement. So a recorder that threw left a COMMITTED row behind a write that reported failure. Measured directly: `insert` throws, the row is in the table, and a second attempt at the same row is `ER_DUP_ENTRY` on PRIMARY.
65
+
66
+ Anything that retries a failed write then meets its own row. The data importer retries by design — it holds a row whose write failed and tries again once the remaining tables have streamed — so a `--mode replace` that had just emptied a table failed on a duplicate key IN that table. From outside, that is the impossible-looking thing: an import that emptied a table and then failed because something was already in it. Reproduced verbatim, including the table name and `[ER_DUP_ENTRY/1062]`, by putting the old retry back.
67
+
68
+ Both ends are closed, and they are independent on purpose:
69
+
70
+ - **The cause.** A table that HAS recorders writes inside a transaction now, so the row and the trail commit together or not at all — on mysql, mariadb, postgres, sqlite and mssql, verified by one suite that asks all five the same question. A table with no recorders — the default — takes the direct path unchanged; `recordsTable` is a Map-size check first, so it costs one comparison. - **The defence.** The importer's retry upserts instead of inserting in `replace` mode. That covers every OTHER way a write can land while reporting failure: a driver timeout on a write the server applied, a connection lost after the commit, a concurrent writer inserting the same key. It is sound precisely because the table was emptied by this same run — there is nothing in it that is not ours.
71
+
72
+ The test that pinned the divergence went red when the fix landed, exactly as its own note said it would, and is inverted with that note kept.
73
+
74
+ ---
75
+
42
76
  ## [0.43.2] — 2026-08-18
43
77
 
44
78
  ### Fixed
package/dist/index.d.ts CHANGED
@@ -222,6 +222,13 @@ export declare class MssqlStore implements DataStore {
222
222
  * stores is what lets the parity test ask the question at all.
223
223
  */
224
224
  private localWrite;
225
+ /**
226
+ * A write and its write-recorders, as ONE unit — see `MysqlStore.
227
+ * writeWithRecorders` for the failure this closes: a recorder that throws
228
+ * after the row's statement committed leaves a committed row behind a write
229
+ * that reported failure, and a caller that retries meets its own row.
230
+ */
231
+ private writeWithRecorders;
225
232
  insert(t: string, r: Row): Promise<Readonly<Record<string, unknown>>>;
226
233
  insertMany(t: string, rows: ReadonlyArray<Row>): Promise<readonly Readonly<Record<string, unknown>>[]>;
227
234
  patchJson(t: string, pk: string, path: string, value: unknown): Promise<Readonly<Record<string, unknown>> | null>;
package/dist/index.js CHANGED
@@ -1,31 +1,31 @@
1
1
  import { MssqlClient as e } from "@effect/sql-mssql";
2
- import { Config as t, Duration as n, Effect as r, Layer as i, ManagedRuntime as a, Redacted as o } from "effect";
3
- import { CDC_OFFSETS_TABLE as s, DEFAULT_ACQUIRE_TIMEOUT_MS as c, EagerCardinalityError as l, _voltroMssqlCdcOffsetsTable as u, attachEagerLoads as d, attributionFields as f, bulkInsertLimitsFor as p, chunkRowsForInsert as m, compileEagerJson as h, compilePredicate as g, compileRawFragment as _, compileSelect as v, decodeRowsFromSchema as y, externalChangeEvent as b, hasEagerLoads as ee, isTableReactive as x, makeEagerFallbackReporter as te, observeDbOp as S, qualifyTable as C, quoteIdent as w, raiseChangeListenerCeiling as T, recordsTable as E, requireTable as ne, runStoreTransaction as D, runWriteRecorders as O, stampGeneratedId as k, stampGeneratedIds as A, withCapturedAttribution as j } from "@voltro/database";
4
- import { EventEmitter as M } from "node:events";
5
- import { createLogger as N } from "@voltro/logger";
6
- import { SqlClient as P, TransactionConnection as F } from "@effect/sql/SqlClient";
2
+ import { Config as t, Duration as n, Effect as r, Layer as i, ManagedRuntime as a, Option as o, Redacted as s } from "effect";
3
+ import { CDC_OFFSETS_TABLE as c, DEFAULT_ACQUIRE_TIMEOUT_MS as l, EagerCardinalityError as u, _voltroMssqlCdcOffsetsTable as d, attachEagerLoads as f, attributionFields as p, bulkInsertLimitsFor as m, chunkRowsForInsert as h, compileEagerJson as g, compilePredicate as _, compileRawFragment as v, compileSelect as y, decodeRowsFromSchema as b, externalChangeEvent as ee, hasEagerLoads as x, isTableReactive as S, makeEagerFallbackReporter as te, observeDbOp as C, qualifyTable as ne, quoteIdent as w, raiseChangeListenerCeiling as T, recordsTable as E, requireTable as D, runStoreTransaction as O, runWriteRecorders as k, stampGeneratedId as A, stampGeneratedIds as j, withCapturedAttribution as re } from "@voltro/database";
4
+ import { EventEmitter as ie } from "node:events";
5
+ import { createLogger as M } from "@voltro/logger";
6
+ import { SqlClient as N, TransactionConnection as P } from "@effect/sql/SqlClient";
7
7
  //#region src/sqlLayer.ts
8
- var re = {
8
+ var ae = {
9
9
  ...e.defaultParameterTypes,
10
10
  null: e.defaultParameterTypes.object
11
- }, ie = (e) => {
12
- let t = e.acquireTimeoutMs ?? c;
11
+ }, oe = (e) => {
12
+ let t = e.acquireTimeoutMs ?? l;
13
13
  return t > 0 ? t : void 0;
14
- }, I = (r) => {
15
- let i = ie(r);
14
+ }, F = (r) => {
15
+ let i = oe(r);
16
16
  return e.layerConfig({
17
17
  server: t.succeed(r.server),
18
- parameterTypes: t.succeed(re),
18
+ parameterTypes: t.succeed(ae),
19
19
  ...i === void 0 ? {} : { connectTimeout: t.succeed(n.millis(i)) },
20
20
  ...r.port === void 0 ? {} : { port: t.succeed(r.port) },
21
21
  ...r.database === void 0 ? {} : { database: t.succeed(r.database) },
22
22
  ...r.username === void 0 ? {} : { username: t.succeed(r.username) },
23
- ...r.password === void 0 ? {} : { password: t.succeed(o.make(r.password)) },
23
+ ...r.password === void 0 ? {} : { password: t.succeed(s.make(r.password)) },
24
24
  ...r.encrypt === void 0 ? {} : { encrypt: t.succeed(r.encrypt) },
25
25
  ...r.trustServer === void 0 ? {} : { trustServer: t.succeed(r.trustServer) },
26
26
  ...r.maxConnections === void 0 ? {} : { maxConnections: t.succeed(r.maxConnections) }
27
27
  });
28
- }, L = (e) => {
28
+ }, I = (e) => {
29
29
  let t = e.get("sslmode");
30
30
  if (t !== null) {
31
31
  if (t === "require") return !0;
@@ -40,7 +40,7 @@ var re = {
40
40
  throw Error(`DB_URL '?${t}=${n}' is not supported by the mssql dialect — use 'true'/'1' or 'false'/'0'.`);
41
41
  }
42
42
  }
43
- }, R = (e) => {
43
+ }, L = (e) => {
44
44
  let t = e.acquireTimeoutMs === void 0 ? {} : { acquireTimeoutMs: e.acquireTimeoutMs }, n = (e) => e === void 0 ? {} : {
45
45
  encrypt: e,
46
46
  ...e ? { trustServer: !0 } : {}
@@ -53,7 +53,7 @@ var re = {
53
53
  username: decodeURIComponent(r.username || "sa"),
54
54
  password: decodeURIComponent(r.password || ""),
55
55
  database: r.pathname.replace(/^\//, "") || "master",
56
- ...n(e.ssl ?? L(r.searchParams)),
56
+ ...n(e.ssl ?? I(r.searchParams)),
57
57
  ...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections },
58
58
  ...t
59
59
  };
@@ -68,7 +68,7 @@ var re = {
68
68
  ...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections },
69
69
  ...t
70
70
  };
71
- }, z = (e) => I(R(e)), B = /* @__PURE__ */ new Set(["1205"]), V = (e) => {
71
+ }, R = (e) => F(L(e)), z = /* @__PURE__ */ new Set(["1205"]), B = (e) => {
72
72
  let t = e;
73
73
  for (let e = 0; e < 5 && typeof t == "object" && t; e++) {
74
74
  let e = t.number;
@@ -77,36 +77,36 @@ var re = {
77
77
  if (typeof n == "string") return n;
78
78
  t = t.cause;
79
79
  }
80
- }, H = (e) => {
81
- let t = V(e);
82
- return t !== void 0 && B.has(t);
83
- }, U = /* @__PURE__ */ new Set(["2601", "2627"]), W = (e) => {
84
- let t = V(e);
85
- return t !== void 0 && U.has(t);
86
- }, G = (e) => H(e) ? "retry" : "noRetry", K = {
80
+ }, V = (e) => {
81
+ let t = B(e);
82
+ return t !== void 0 && z.has(t);
83
+ }, H = /* @__PURE__ */ new Set(["2601", "2627"]), U = (e) => {
84
+ let t = B(e);
85
+ return t !== void 0 && H.has(t);
86
+ }, W = (e) => V(e) ? "retry" : "noRetry", G = {
87
87
  I: "insert",
88
88
  U: "update",
89
89
  D: "delete"
90
- }, q = (e, t) => e(r.gen(function* () {
91
- let e = yield* P;
90
+ }, K = (e, t) => e(r.gen(function* () {
91
+ let e = yield* N;
92
92
  if (((yield* e`
93
93
  SELECT COUNT(*) AS ${e("on")}
94
94
  FROM sys.change_tracking_databases
95
95
  WHERE database_id = DB_ID()`)[0]?.on ?? 0) === 0) return yield* r.fail(/* @__PURE__ */ Error("mssql Change Tracking CDC requires CT enabled on the database. Run `ALTER DATABASE <db> SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON)`, or set CDC=0 for inline emit."));
96
96
  for (let n of t) yield* e.unsafe(`IF NOT EXISTS (
97
97
  SELECT 1 FROM sys.change_tracking_tables
98
- WHERE object_id = OBJECT_ID(${J(n)})
98
+ WHERE object_id = OBJECT_ID(${q(n)})
99
99
  )
100
- ALTER TABLE ${Y(n)} ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = OFF)`);
101
- })).then(() => void 0), J = (e) => `'${e.replace(/'/g, "''")}'`, Y = (e) => `[${e.replace(/\]/g, "]]")}]`, X = async (e) => {
102
- let t = N({ scope: "voltro:mssql:cdc" }), n = e.pollIntervalMs ?? 500;
103
- await q(e.run, e.tables);
100
+ ALTER TABLE ${J(n)} ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = OFF)`);
101
+ })).then(() => void 0), q = (e) => `'${e.replace(/'/g, "''")}'`, J = (e) => `[${e.replace(/\]/g, "]]")}]`, Y = async (e) => {
102
+ let t = M({ scope: "voltro:mssql:cdc" }), n = e.pollIntervalMs ?? 500;
103
+ await K(e.run, e.tables);
104
104
  let i = async () => (await e.run(r.gen(function* () {
105
- return yield* (yield* P)`SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_CURRENT_VERSION()) AS v`;
105
+ return yield* (yield* N)`SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_CURRENT_VERSION()) AS v`;
106
106
  })))[0]?.v ?? "0", a = e.startVersion ?? await i(), o = !1, s = null, c = !1, l = (e, t) => BigInt(e) > BigInt(t), u = async (n, o) => {
107
107
  let s = await e.run(r.gen(function* () {
108
- let e = yield* P, t = e.unsafe(Y(n)), r = (yield* e`
109
- SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(${e.unsafe(J(n))}))) AS floor`)[0]?.floor;
108
+ let e = yield* N, t = e.unsafe(J(n)), r = (yield* e`
109
+ SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(${e.unsafe(q(n))}))) AS floor`)[0]?.floor;
110
110
  return r != null && BigInt(o) < BigInt(r) ? {
111
111
  resync: !0,
112
112
  rows: []
@@ -124,7 +124,7 @@ var re = {
124
124
  return;
125
125
  }
126
126
  for (let t of s.rows) {
127
- let r = K[t.__ct_op];
127
+ let r = G[t.__ct_op];
128
128
  if (!r) continue;
129
129
  let { __ct_op: i, __ct_id: a, ...o } = t;
130
130
  if (r === "delete") e.onChange({
@@ -134,7 +134,7 @@ var re = {
134
134
  new: null
135
135
  });
136
136
  else {
137
- let t = y([o], n, "mssql")[0];
137
+ let t = b([o], n, "mssql")[0];
138
138
  e.onChange({
139
139
  table: n,
140
140
  op: r,
@@ -175,26 +175,26 @@ var re = {
175
175
  },
176
176
  currentVersion: () => a
177
177
  };
178
- }, Z = N({ scope: "voltro:mssql" }), Q = async (e) => {
178
+ }, X = M({ scope: "voltro:mssql" }), Z = async (e) => {
179
179
  let t = e.changeStrategy ?? "inline", n = t;
180
- t === "cdc" && !e.cdcConfig && (Z.warn("changeStrategy='cdc' requires cdcConfig (replicaId + includeTables); falling back to 'inline'."), n = "inline");
181
- let r = e.tracerLayer ? i.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, o = a.make(r), s = new ae(await o.runPromise(P), o, n);
180
+ t === "cdc" && !e.cdcConfig && (X.warn("changeStrategy='cdc' requires cdcConfig (replicaId + includeTables); falling back to 'inline'."), n = "inline");
181
+ let r = e.tracerLayer ? i.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, o = a.make(r), s = new se(await o.runPromise(N), o, n);
182
182
  return n === "cdc" && e.cdcConfig && await s.startCdcConsumer(e.cdcConfig), s;
183
- }, ae = class e {
183
+ }, se = class e {
184
184
  sql;
185
185
  runtime;
186
186
  changeStrategy;
187
187
  namespace;
188
188
  emitter;
189
189
  inflightTxns = 0;
190
- reportEagerFallback = te(Z);
190
+ reportEagerFallback = te(X);
191
191
  cdcHandle = null;
192
192
  cdcCheckpointTimer = null;
193
193
  cdcPendingVersion = null;
194
194
  cdcReplicaId = "";
195
195
  cdcStreamName = "default";
196
196
  constructor(e, t, n = "inline", r = null, i) {
197
- this.sql = e, this.runtime = t, this.changeStrategy = n, this.namespace = r, this.emitter = i ?? new M(), T(this.emitter);
197
+ this.sql = e, this.runtime = t, this.changeStrategy = n, this.namespace = r, this.emitter = i ?? new ie(), T(this.emitter);
198
198
  }
199
199
  get inlineEmit() {
200
200
  return this.changeStrategy === "inline";
@@ -203,16 +203,16 @@ var re = {
203
203
  return t === this.namespace ? this : new e(this.sql, this.runtime, this.changeStrategy, t, this.emitter);
204
204
  }
205
205
  nsT(e) {
206
- return C(this.namespace, e);
206
+ return ne(this.namespace, e);
207
207
  }
208
208
  __mssqlReplicationFriend = { runEffect: (e) => this.runtime.runPromise(e) };
209
209
  async executeQuery(e, t) {
210
- let n = v(e, this.sql, this.namespace), i = t ? r.provideService(n, F, t) : n, a = await S("mssql", "select", () => this.runtime.runPromise(i));
211
- return y(a, e.table, "mssql");
210
+ let n = y(e, this.sql, this.namespace), i = t ? r.provideService(n, P, t) : n, a = await C("mssql", "select", () => this.runtime.runPromise(i));
211
+ return b(a, e.table, "mssql");
212
212
  }
213
213
  async executeInsert(e, t, n, i, a) {
214
- t = k(e, t);
215
- let o = this.sql, s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t).returning("*")}`, c = n ? r.provideService(s, F, n) : s, l = (await this.runtime.runPromise(c))[0];
214
+ t = A(e, t);
215
+ let o = this.sql, s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t).returning("*")}`, c = n ? r.provideService(s, P, n) : s, l = (await this.runtime.runPromise(c))[0];
216
216
  if (!l) throw Error(`MssqlStore.insert: row not returned post-insert in '${e}'`);
217
217
  return await this.routeEvent({
218
218
  table: e,
@@ -222,7 +222,7 @@ var re = {
222
222
  }, i, n, a), l;
223
223
  }
224
224
  async executeUpdate(e, t, n, i, a, o) {
225
- let s = this.sql, c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(n).returning("*")} WHERE ${s("id")} = ${t}`, l = i ? r.provideService(c, F, i) : c, u = (await this.runtime.runPromise(l))[0];
225
+ let s = this.sql, c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(n).returning("*")} WHERE ${s("id")} = ${t}`, l = i ? r.provideService(c, P, i) : c, u = (await this.runtime.runPromise(l))[0];
226
226
  return u ? (await this.routeEvent({
227
227
  table: e,
228
228
  op: "update",
@@ -231,27 +231,27 @@ var re = {
231
231
  }, a, i, o), u) : null;
232
232
  }
233
233
  async executeInsertMany(e, t, n, i, a) {
234
- if (t = A(e, t), t.length === 0) return [];
235
- let o = this.sql, s = m(t, p("mssql")), c = (t) => o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t).returning("*")}`, l;
234
+ if (t = j(e, t), t.length === 0) return [];
235
+ let o = this.sql, s = h(t, m("mssql")), c = (t) => o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t).returning("*")}`, l;
236
236
  if (s.length === 1) {
237
- let e = c(s[0]), t = n ? r.provideService(e, F, n) : e;
237
+ let e = c(s[0]), t = n ? r.provideService(e, P, n) : e;
238
238
  l = await this.runtime.runPromise(t);
239
239
  } else if (n) {
240
240
  let e = [];
241
- for (let t of s) e.push(...await this.runtime.runPromise(r.provideService(c(t), F, n)));
241
+ for (let t of s) e.push(...await this.runtime.runPromise(r.provideService(c(t), P, n)));
242
242
  l = e;
243
243
  } else l = await this.runtime.runPromise(o.withTransaction(r.map(r.forEach(s, c, { concurrency: 1 }), (e) => e.flat())));
244
- let u = t.map((e) => e.id), d = new Map(l.map((e) => [e.id, e])), f = u.map((e) => d.get(e)).filter((e) => e !== void 0), h = f.length === l.length ? f : [...l];
245
- for (let t of h) await this.routeEvent({
244
+ let u = t.map((e) => e.id), d = new Map(l.map((e) => [e.id, e])), f = u.map((e) => d.get(e)).filter((e) => e !== void 0), p = f.length === l.length ? f : [...l];
245
+ for (let t of p) await this.routeEvent({
246
246
  table: e,
247
247
  op: "insert",
248
248
  old: null,
249
249
  new: t
250
250
  }, i, n, a);
251
- return h;
251
+ return p;
252
252
  }
253
253
  async executePatchJson(e, t, n, i, a, o, s) {
254
- let c = this.sql, l = n.split("."), u = l[0], d = l.slice(1), f = d.length === 0 ? "$" : `$.${d.join(".")}`, p = JSON.stringify(i ?? null), m = c`UPDATE ${c(this.nsT(e))} SET ${c(u)} = JSON_MODIFY(${c(u)}, ${f}, JSON_QUERY(${p})) OUTPUT INSERTED.* WHERE ${c("id")} = ${t}`, h = a ? r.provideService(m, F, a) : m, g = (await this.runtime.runPromise(h))[0];
254
+ let c = this.sql, l = n.split("."), u = l[0], d = l.slice(1), f = d.length === 0 ? "$" : `$.${d.join(".")}`, p = JSON.stringify(i ?? null), m = c`UPDATE ${c(this.nsT(e))} SET ${c(u)} = JSON_MODIFY(${c(u)}, ${f}, JSON_QUERY(${p})) OUTPUT INSERTED.* WHERE ${c("id")} = ${t}`, h = a ? r.provideService(m, P, a) : m, g = (await this.runtime.runPromise(h))[0];
255
255
  return g ? (await this.routeEvent({
256
256
  table: e,
257
257
  op: "update",
@@ -260,7 +260,7 @@ var re = {
260
260
  }, o, a, s), g) : null;
261
261
  }
262
262
  async executeDelete(e, t, n, i, a) {
263
- let o = this.sql, s = o`DELETE FROM ${o(this.nsT(e))} OUTPUT DELETED.* WHERE ${o("id")} = ${t}`, c = n ? r.provideService(s, F, n) : s, l = (await this.runtime.runPromise(c))[0];
263
+ let o = this.sql, s = o`DELETE FROM ${o(this.nsT(e))} OUTPUT DELETED.* WHERE ${o("id")} = ${t}`, c = n ? r.provideService(s, P, n) : s, l = (await this.runtime.runPromise(c))[0];
264
264
  return l ? (await this.routeEvent({
265
265
  table: e,
266
266
  op: "delete",
@@ -270,17 +270,17 @@ var re = {
270
270
  }
271
271
  async appendInTxn(e, t, n) {
272
272
  let i = this.sql, a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(t)}`;
273
- await this.runtime.runPromise(n ? r.provideService(a, F, n) : a);
273
+ await this.runtime.runPromise(n ? r.provideService(a, P, n) : a);
274
274
  }
275
275
  async maxInTxn(e, t, n, i) {
276
- let a = this.sql, o = Object.entries(n).map(([e, t]) => a`${a(e)} = ${t}`), s = a`SELECT MAX(${a(t)}) AS ${a("m")} FROM ${a(this.nsT(e))} WHERE ${a.and(o)}`, c = (await this.runtime.runPromise(i ? r.provideService(s, F, i) : s))[0]?.m;
276
+ let a = this.sql, o = Object.entries(n).map(([e, t]) => a`${a(e)} = ${t}`), s = a`SELECT MAX(${a(t)}) AS ${a("m")} FROM ${a(this.nsT(e))} WHERE ${a.and(o)}`, c = (await this.runtime.runPromise(i ? r.provideService(s, P, i) : s))[0]?.m;
277
277
  return c == null ? null : Number(c);
278
278
  }
279
279
  async routeEvent(e, t, n = null, r) {
280
280
  e = {
281
- ...f(r),
281
+ ...p(r),
282
282
  ...e
283
- }, E(e.table) && await O({
283
+ }, E(e.table) && await k({
284
284
  append: (e, t) => this.appendInTxn(e, t, n),
285
285
  maxOf: (e, t, r) => this.maxInTxn(e, t, r, n)
286
286
  }, {
@@ -290,23 +290,23 @@ var re = {
290
290
  prev: e.old,
291
291
  traceId: e.traceId,
292
292
  subjectId: e.subjectId
293
- }), x(e.table) && (t === null ? this.inlineEmit && this.emitter.emit("change", e) : t.push(e));
293
+ }), S(e.table) && (t === null ? this.inlineEmit && this.emitter.emit("change", e) : t.push(e));
294
294
  }
295
295
  query(e) {
296
296
  return this.runWithEager(e, null);
297
297
  }
298
298
  raw(e, t) {
299
- let n = _(e, this.sql);
300
- return S("mssql", "raw", () => this.runtime.runPromise(n));
299
+ let n = v(e, this.sql);
300
+ return C("mssql", "raw", () => this.runtime.runPromise(n));
301
301
  }
302
302
  async runWithEager(e, t) {
303
- if (!ee(e)) return this.executeQuery(e, t);
304
- let n = this.namespace === null ? h(e, this.sql, "mssql") : null;
303
+ if (!x(e)) return this.executeQuery(e, t);
304
+ let n = this.namespace === null ? g(e, this.sql, "mssql") : null;
305
305
  if (n !== null) try {
306
- let e = t ? r.provideService(n.fragment, F, t) : n.fragment, i = await S("mssql", "select", () => this.runtime.runPromise(e));
306
+ let e = t ? r.provideService(n.fragment, P, t) : n.fragment, i = await C("mssql", "select", () => this.runtime.runPromise(e));
307
307
  return n.decode(i);
308
308
  } catch (t) {
309
- if (t instanceof l) throw t;
309
+ if (t instanceof u) throw t;
310
310
  this.reportEagerFallback({
311
311
  dialect: "mssql",
312
312
  table: e.table,
@@ -320,31 +320,40 @@ var re = {
320
320
  reason: "not-compilable"
321
321
  });
322
322
  let i = await this.executeQuery(e, t);
323
- return d(i, e.eager, e.sourceTable ?? ne(e.table), (e) => this.executeQuery(e, t));
323
+ return f(i, e.eager, e.sourceTable ?? D(e.table), (e) => this.executeQuery(e, t));
324
324
  }
325
325
  getInternalRunWithEager() {
326
326
  return this.runWithEager.bind(this);
327
327
  }
328
- localWrite(e, t) {
329
- return S("mssql", e, () => j(t));
328
+ localWrite(e, t, n) {
329
+ let r = (e) => E(t) ? this.writeWithRecorders(n, e) : n(e, null, null);
330
+ return C("mssql", e, () => re(r));
331
+ }
332
+ async writeWithRecorders(e, t) {
333
+ let n = [], i = this.sql, a = await this.runtime.runPromise(i.withTransaction(r.flatMap(r.serviceOption(P), (i) => o.isNone(i) ? r.fail(/* @__PURE__ */ Error("MssqlStore.write: TransactionConnection missing.")) : r.tryPromise({
334
+ try: () => e(t, i.value, n),
335
+ catch: (e) => e
336
+ }))));
337
+ for (let e of n) this.emitChange(e);
338
+ return a;
330
339
  }
331
340
  insert(e, t) {
332
- return this.localWrite("insert", (n) => this.executeInsert(e, t, null, null, n));
341
+ return this.localWrite("insert", e, (n, r, i) => this.executeInsert(e, t, r, i, n));
333
342
  }
334
343
  insertMany(e, t) {
335
- return this.localWrite("insert", (n) => this.executeInsertMany(e, t, null, null, n));
344
+ return this.localWrite("insert", e, (n, r, i) => this.executeInsertMany(e, t, r, i, n));
336
345
  }
337
346
  patchJson(e, t, n, r) {
338
- return this.localWrite("update", (i) => this.executePatchJson(e, t, n, r, null, null, i));
347
+ return this.localWrite("update", e, (i, a, o) => this.executePatchJson(e, t, n, r, a, o, i));
339
348
  }
340
349
  update(e, t, n) {
341
- return this.localWrite("update", (r) => this.executeUpdate(e, t, n, null, null, r));
350
+ return this.localWrite("update", e, (r, i, a) => this.executeUpdate(e, t, n, i, a, r));
342
351
  }
343
352
  delete(e, t) {
344
- return this.localWrite("delete", (n) => this.executeDelete(e, t, null, null, n));
353
+ return this.localWrite("delete", e, (n, r, i) => this.executeDelete(e, t, r, i, n));
345
354
  }
346
355
  async updateMany(e, t, n) {
347
- let r = this.sql, i = g(n.where, r, this.namespace), a = r`UPDATE ${r(this.nsT(e))} SET ${r.update(t)} OUTPUT INSERTED.* WHERE ${i}`, o = await S("mssql", "update", () => this.runtime.runPromise(a));
356
+ let r = this.sql, i = _(n.where, r, this.namespace), a = r`UPDATE ${r(this.nsT(e))} SET ${r.update(t)} OUTPUT INSERTED.* WHERE ${i}`, o = await C("mssql", "update", () => this.runtime.runPromise(a));
348
357
  for (let t of o) await this.routeEvent({
349
358
  table: e,
350
359
  op: "update",
@@ -354,7 +363,7 @@ var re = {
354
363
  return o.length;
355
364
  }
356
365
  async deleteMany(e, t) {
357
- let n = this.sql, r = g(t.where, n, this.namespace), i = n`DELETE FROM ${n(this.nsT(e))} OUTPUT DELETED.* WHERE ${r}`, a = await S("mssql", "delete", () => this.runtime.runPromise(i));
366
+ let n = this.sql, r = _(t.where, n, this.namespace), i = n`DELETE FROM ${n(this.nsT(e))} OUTPUT DELETED.* WHERE ${r}`, a = await C("mssql", "delete", () => this.runtime.runPromise(i));
358
367
  for (let t of a) await this.routeEvent({
359
368
  table: e,
360
369
  op: "delete",
@@ -364,10 +373,10 @@ var re = {
364
373
  return a.length;
365
374
  }
366
375
  upsert(e, t, n) {
367
- return this.localWrite("upsert", (r) => this.executeUpsert(e, t, n, null, null, r));
376
+ return this.localWrite("upsert", e, (r, i, a) => this.executeUpsert(e, t, n, i, a, r));
368
377
  }
369
378
  insertIgnore(e, t, n) {
370
- return this.localWrite("upsert", (r) => this.executeInsertIgnore(e, t, n, null, null, r));
379
+ return this.localWrite("upsert", e, (r, i, a) => this.executeInsertIgnore(e, t, n, i, a, r));
371
380
  }
372
381
  upsertPatch(e, t, n) {
373
382
  if (typeof n.update == "function") return n.update(t);
@@ -390,7 +399,7 @@ var re = {
390
399
  try {
391
400
  return await this.executeInsert(e, t, r, i);
392
401
  } catch (a) {
393
- if (!W(a)) throw a;
402
+ if (!U(a)) throw a;
394
403
  let o = await this.findByConflict(e, t, n.conflictColumns, r);
395
404
  if (!o) throw a;
396
405
  return await this.executeUpdate(e, o.id, this.upsertPatch(t, o, n), r, i) ?? o;
@@ -403,7 +412,7 @@ var re = {
403
412
  ON ${o.and(d)}
404
413
  WHEN MATCHED THEN UPDATE SET ${o.csv(m)}
405
414
  WHEN NOT MATCHED THEN INSERT (${o.csv(f)}) VALUES (${o.csv(p)})
406
- OUTPUT $action AS ${o("__action")}, INSERTED.*;`, g = i ? r.provideService(h, F, i) : h, _ = (await this.runtime.runPromise(g))[0];
415
+ OUTPUT $action AS ${o("__action")}, INSERTED.*;`, g = i ? r.provideService(h, P, i) : h, _ = (await this.runtime.runPromise(g))[0];
407
416
  if (!_) throw Error(`MssqlStore.upsert: MERGE returned no row for '${e}'`);
408
417
  let v = _.__action, { __action: y, ...b } = _;
409
418
  return this.routeEvent({
@@ -414,13 +423,13 @@ var re = {
414
423
  }, a), b;
415
424
  }
416
425
  async executeInsertIgnore(e, t, n, r, i, a) {
417
- t = k(e, t);
426
+ t = A(e, t);
418
427
  let o = await this.findByConflict(e, t, n.conflictColumns, r);
419
428
  if (o) return o;
420
429
  try {
421
430
  return await this.executeInsert(e, t, r, i);
422
431
  } catch (i) {
423
- if (!W(i)) throw i;
432
+ if (!U(i)) throw i;
424
433
  let a = await this.findByConflict(e, t, n.conflictColumns, r);
425
434
  if (!a) throw i;
426
435
  return a;
@@ -428,7 +437,7 @@ var re = {
428
437
  }
429
438
  async findByConflict(e, t, n, i) {
430
439
  if (n.length === 0) return;
431
- let a = this.sql, o = n.map((e) => a`${a(e)} = ${t[e]}`), s = a`SELECT TOP 1 * FROM ${a(this.nsT(e))} WHERE ${a.and(o)}`, c = i ? r.provideService(s, F, i) : s;
440
+ let a = this.sql, o = n.map((e) => a`${a(e)} = ${t[e]}`), s = a`SELECT TOP 1 * FROM ${a(this.nsT(e))} WHERE ${a.and(o)}`, c = i ? r.provideService(s, P, i) : s;
432
441
  return (await this.runtime.runPromise(c))[0];
433
442
  }
434
443
  emitChange(e) {
@@ -438,9 +447,9 @@ var re = {
438
447
  if (this.cdcHandle) return;
439
448
  this.cdcReplicaId = e.replicaId;
440
449
  let t = e.includeTables ?? [];
441
- t.length === 0 && Z.warn("cdc: no reactive tables to tail; Change Tracking reader idle. Set includeTables to enable.");
450
+ t.length === 0 && X.warn("cdc: no reactive tables to tail; Change Tracking reader idle. Set includeTables to enable.");
442
451
  let n = await this.readCdcOffset(e.replicaId);
443
- this.cdcHandle = await X({
452
+ this.cdcHandle = await Y({
444
453
  run: this.__mssqlReplicationFriend.runEffect,
445
454
  tables: t,
446
455
  startVersion: n,
@@ -448,9 +457,9 @@ var re = {
448
457
  onVersion: (e) => {
449
458
  this.cdcPendingVersion = e;
450
459
  },
451
- onError: (e) => Z.warn("cdc: consumer error", {}, e),
460
+ onError: (e) => X.warn("cdc: consumer error", {}, e),
452
461
  onResync: () => {
453
- Z.warn("cdc: resynced to current CT version after retention gap — subscriptions self-heal on the next change");
462
+ X.warn("cdc: resynced to current CT version after retention gap — subscriptions self-heal on the next change");
454
463
  }
455
464
  }), this.cdcCheckpointTimer = setInterval(() => {
456
465
  this.flushCdcOffset();
@@ -459,14 +468,14 @@ var re = {
459
468
  async readCdcOffset(e) {
460
469
  try {
461
470
  let t = this.sql, n = (await this.runtime.runPromise(t`
462
- SELECT TOP 1 ${t("ctVersion")} FROM ${t(s)}
471
+ SELECT TOP 1 ${t("ctVersion")} FROM ${t(c)}
463
472
  WHERE ${t("id")} = ${e}`))[0]?.ctVersion;
464
- return n ? (Z.info("cdc: resuming from persisted CT version", {
473
+ return n ? (X.info("cdc: resuming from persisted CT version", {
465
474
  replicaId: e,
466
475
  version: n
467
476
  }), n) : null;
468
477
  } catch (t) {
469
- return Z.warn("cdc: no readable offset; starting at current CT version", { replicaId: e }, t), null;
478
+ return X.warn("cdc: no readable offset; starting at current CT version", { replicaId: e }, t), null;
470
479
  }
471
480
  }
472
481
  async flushCdcOffset() {
@@ -475,7 +484,7 @@ var re = {
475
484
  this.cdcPendingVersion = null;
476
485
  try {
477
486
  let t = this.cdcReplicaId;
478
- await this.executeUpsert(s, {
487
+ await this.executeUpsert(c, {
479
488
  id: t,
480
489
  replicaId: t,
481
490
  streamName: this.cdcStreamName,
@@ -486,7 +495,7 @@ var re = {
486
495
  update: ["ctVersion", "updatedAt"]
487
496
  }, null, null);
488
497
  } catch (t) {
489
- this.cdcPendingVersion = e, Z.warn("cdc: checkpoint write failed", {}, t);
498
+ this.cdcPendingVersion = e, X.warn("cdc: checkpoint write failed", {}, t);
490
499
  }
491
500
  }
492
501
  }
@@ -535,16 +544,16 @@ var re = {
535
544
  }
536
545
  async emptyTablesOn(e, t) {
537
546
  if (e.length === 0) return;
538
- let n = this.sql, i = (e) => this.nsT(e), a = (e, t) => n.unsafe(`ALTER TABLE ${w(i(e), "mssql")} ${t ? "WITH CHECK CHECK" : "NOCHECK"} CONSTRAINT ALL`), o = r.acquireUseRelease(r.forEach(e, (e) => a(e, !1), { discard: !0 }), () => r.forEach(e, (e) => n`DELETE FROM ${n(i(e))}`, { discard: !0 }), () => r.orDie(r.ignore(r.forEach(e, (e) => a(e, !0), { discard: !0 })))), s = t === null ? n.withTransaction(o) : r.provideService(o, F, t);
547
+ let n = this.sql, i = (e) => this.nsT(e), a = (e, t) => n.unsafe(`ALTER TABLE ${w(i(e), "mssql")} ${t ? "WITH CHECK CHECK" : "NOCHECK"} CONSTRAINT ALL`), o = r.acquireUseRelease(r.forEach(e, (e) => a(e, !1), { discard: !0 }), () => r.forEach(e, (e) => n`DELETE FROM ${n(i(e))}`, { discard: !0 }), () => r.orDie(r.ignore(r.forEach(e, (e) => a(e, !0), { discard: !0 })))), s = t === null ? n.withTransaction(o) : r.provideService(o, P, t);
539
548
  await this.runtime.runPromise(s);
540
549
  }
541
550
  async transactional(e) {
542
551
  this.inflightTxns++;
543
552
  try {
544
- return await D({
553
+ return await O({
545
554
  ...this.txnSpec("MssqlStore.transactional"),
546
555
  work: e,
547
- makeView: (e, t) => new oe(this, e, t)
556
+ makeView: (e, t) => new ce(this, e, t)
548
557
  });
549
558
  } finally {
550
559
  this.inflightTxns--;
@@ -556,7 +565,7 @@ var re = {
556
565
  dialect: "mssql",
557
566
  withTransaction: (e) => this.sql.withTransaction(e),
558
567
  runPromiseExit: (e) => this.runtime.runPromiseExit(e),
559
- isRetryable: H,
568
+ isRetryable: V,
560
569
  span: {
561
570
  name: "store.transactional",
562
571
  attributes: {
@@ -575,7 +584,7 @@ var re = {
575
584
  return this.changeStrategy === "cdc" ? "fleet" : "local";
576
585
  }
577
586
  injectExternalChange(e) {
578
- x(e.table) && this.emitter.emit("change", b(e));
587
+ S(e.table) && this.emitter.emit("change", ee(e));
579
588
  }
580
589
  run(e) {
581
590
  return this.runtime.runPromise(e);
@@ -588,7 +597,7 @@ var re = {
588
597
  if (this.inflightTxns > 0) {
589
598
  let t = Date.now() + e;
590
599
  for (; this.inflightTxns > 0 && Date.now() < t;) await new Promise((e) => setTimeout(e, 25));
591
- this.inflightTxns > 0 && Z.warn("close: grace period expired with in-flight transactions", {
600
+ this.inflightTxns > 0 && X.warn("close: grace period expired with in-flight transactions", {
592
601
  gracePeriodMs: e,
593
602
  inflight: this.inflightTxns
594
603
  });
@@ -598,7 +607,7 @@ var re = {
598
607
  async ping() {
599
608
  await this.runtime.runPromise(this.sql`SELECT 1`);
600
609
  }
601
- }, oe = class {
610
+ }, ce = class {
602
611
  parent;
603
612
  txn;
604
613
  attr;
@@ -674,12 +683,12 @@ var re = {
674
683
  this.events.length = 0;
675
684
  }
676
685
  }
677
- }, $ = (e) => e.__mssqlReplicationFriend ?? null, se = (e, t) => e === t ? 0 : e < t ? -1 : 1, ce = () => ({
686
+ }, Q = (e) => e.__mssqlReplicationFriend ?? null, le = (e, t) => e === t ? 0 : e < t ? -1 : 1, $ = () => ({
678
687
  async capturePrimaryPosition(e) {
679
- let t = $(e);
688
+ let t = Q(e);
680
689
  if (t === null) throw Error("mssqlReplicationAdapter: primary is not an MssqlStore (missing __mssqlReplicationFriend).");
681
690
  return t.runEffect(r.gen(function* () {
682
- return (yield* (yield* P)`
691
+ return (yield* (yield* N)`
683
692
  SELECT CONVERT(VARCHAR(40), end_of_log_lsn) AS lsn
684
693
  FROM sys.dm_hadr_database_replica_states
685
694
  WHERE database_id = DB_ID()
@@ -689,10 +698,10 @@ var re = {
689
698
  }));
690
699
  },
691
700
  async probeReplicaPosition(e) {
692
- let t = $(e);
701
+ let t = Q(e);
693
702
  if (t === null) throw Error("mssqlReplicationAdapter: replica is not an MssqlStore.");
694
703
  return t.runEffect(r.gen(function* () {
695
- return (yield* (yield* P)`
704
+ return (yield* (yield* N)`
696
705
  SELECT CONVERT(VARCHAR(40), last_hardened_lsn) AS lsn
697
706
  FROM sys.dm_hadr_database_replica_states
698
707
  WHERE database_id = DB_ID()
@@ -701,14 +710,14 @@ var re = {
701
710
  }));
702
711
  },
703
712
  compare(e, t) {
704
- return se(t, e) >= 0 ? "caught-up" : "behind";
713
+ return le(t, e) >= 0 ? "caught-up" : "behind";
705
714
  }
706
- }), le = {
715
+ }), ue = {
707
716
  id: "mssql",
708
- makeSqlLayer: (e) => z(e),
709
- makeStore: (e) => Q(e),
717
+ makeSqlLayer: (e) => R(e),
718
+ makeStore: (e) => Z(e),
710
719
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_[\]]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
711
- retryFilter: G
720
+ retryFilter: W
712
721
  };
713
722
  //#endregion
714
- export { s as CDC_OFFSETS_TABLE, e as MssqlClient, u as _voltroMssqlCdcOffsetsTable, R as connectionFromConfig, q as ensureChangeTracking, I as makeMssqlSqlLayer, z as makeMssqlSqlLayerFromConfig, Q as makeMssqlStore, le as mssqlDialect, ce as mssqlReplicationAdapter, G as mssqlRetryFilter, X as startChangeTrackingCdc };
723
+ export { c as CDC_OFFSETS_TABLE, e as MssqlClient, d as _voltroMssqlCdcOffsetsTable, L as connectionFromConfig, K as ensureChangeTracking, F as makeMssqlSqlLayer, R as makeMssqlSqlLayerFromConfig, Z as makeMssqlStore, ue as mssqlDialect, $ as mssqlReplicationAdapter, W as mssqlRetryFilter, Y as startChangeTrackingCdc };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/sql-mssql",
3
- "version": "0.43.2",
3
+ "version": "0.44.0",
4
4
  "description": "SQL Server (mssql) dialect adapter for Voltro's cross-dialect DataStore (OUTPUT-clause returning, OFFSET/FETCH paging, inline reactivity).",
5
5
  "keywords": [
6
6
  "voltro",
@@ -35,8 +35,8 @@
35
35
  "dependencies": {
36
36
  "@effect/sql": "^0.52.0",
37
37
  "@effect/sql-mssql": "^0.53.0",
38
- "@voltro/database": "0.43.2",
39
- "@voltro/logger": "0.43.2"
38
+ "@voltro/database": "0.44.0",
39
+ "@voltro/logger": "0.44.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "effect": "^3.22.0"