@voltro/sql-mysql 0.19.0 → 0.20.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,93 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.20.0] — 2026-07-29
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/plugin-versioning, @voltro/database, @voltro/voltro, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — `versioningPlugin({ timing: 'in-transaction' })` produced a WRONG trail, not merely a slow one. Reported and reproduced against MariaDB 11 by a team that wired both plugins and measured before migrating a single call site.
47
+
48
+ **It recorded every change twice.** The two timings are alternatives, but the post-commit change tap stayed wired when the in-transaction recorder was registered, so both ran. One `bookmarks.create` → two history rows.
49
+
50
+ **And the trail was mis-ordered, which is worse.** Each path numbered independently: one insert plus one update produced versions `0, 0, 1, 2` across four rows. `selectAsOf`, `sortHistory` and `diffVersionRows` all read `version`, so `rowAsOf` returned the wrong snapshot and `diffVersions` found nothing. A duplicate can be deduped; a wrong order cannot be detected from the data.
51
+
52
+ The recorder wrote a constant `version: 0` on purpose, with a design note arguing that ordering could come from `changedAt` and that a read per covered write was too expensive. Both halves were wrong: `changedAt` is millisecond-resolution, so two writes to one row inside one transaction tie routinely, and the number is what every reader consults.
53
+
54
+ **BREAKING —** a `WriteRecorder` now receives a PORT (`{ append, maxOf }`) rather than a bare `append`. `maxOf` is one aggregate with an equality filter on the connection the write already holds; it is what lets an append-only trail number its own entries. A recorder still cannot UPDATE, DELETE or open a nested transaction, and a throw from either operation still rolls the caller's write back. Apps that merely ENABLE the timing need no change — only a hand-written recorder does, and `tsc` names every site.
55
+
56
+ **Cost, stated rather than avoided:** `'in-transaction'` now takes TWO round-trips per recorded write, roughly doubling this timing's published per-write overhead. Both timings number from 1, so switching `timing` no longer shifts version numbers.
57
+
58
+ ### Fixed
59
+
60
+ - **@voltro/database, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — The correlation bridge did not survive a transaction, and did not survive CDC. Both are fixed, and both were found by measuring against live databases after a consumer isolated the symptom in a scratch app.
61
+
62
+ **Every write a framework mutation makes was unattributed.** `transactional()` is entered from the request's async-local scope, but its callback runs from inside the Effect the store builds — and measured against live postgres AND live mariadb, the scope is active at the call site and EMPTY inside the callback. Framework mutations are auto-transactional, so this was every handler write. Same class as the `bindMutation` defect fixed alongside it: a scope covering the construction of an Effect and not its execution. The caller's attribution is now captured at `transactional()` entry and re-established around the callback, in all four dialect stores.
63
+
64
+ **And the CDC transports could not carry it at all.** Under `changeStrategy: 'cdc'` — the DEFAULT — the event a subscriber receives is rebuilt from a postgres NOTIFY payload or a mysql binlog row image, neither of which can hold a request context. `registerPendingAttribution` / `claimPendingAttribution` (`@voltro/database`) let the write path hand its identity to the echo, keyed by `(table, op, id)` and claimed once. A write made on ANOTHER replica has nothing pending and stays unattributed, which is the correct answer rather than a gap.
65
+
66
+ **Plus one nobody had reported, found on the way:** on postgres under CDC the write path skipped `routeEvent` entirely, and `runWriteRecorders` lives inside it — so `versioningPlugin({ timing: 'in-transaction' })` with the default `CDC=1` recorded NOTHING. The mode whose entire promise is "if the change committed, the entry is there" wrote an empty trail, silently. `routeEvent` now runs in both modes; only the DELIVERY decision is strategy-dependent.
67
+
68
+ New live-dialect suites (`cdcAttribution.integration.test.ts` in `sql-postgres` and `sql-mysql`) pin all of it, and were verified red against the previous code.
69
+ - **@voltro/plugin-versioning, @voltro/runtime, @voltro/cli** — `_voltro_row_history.traceId` and `.subjectId` were NULL on every write. Three independent causes, all found from one consumer report whose evidence pinned the diagnosis before we looked: `subjectId` was NULL while `changedBy` on the SAME row carried the acting user — so the identity was known and was not travelling.
70
+
71
+ - **The adapter dropped them.** `dataStoreHistoryStore.append` hand-wrote its insert object and listed `changedBy` but not `traceId` / `subjectId`. This is the second time that shape has bitten in this file — the READ side (`rowToVersion`) had drifted identically. A row built by hand in one place and read by hand in another disagree exactly when a field is ADDED, because nothing fails. Both now spread the row. - **An Effect-returning handler was unattributed.** `bindMutation` established the scope around the CALL, which covers an async executor for its whole run — but an Effect-returning one is only CONSTRUCTED there and runs later. It is now forked inside the scope, with interruption and typed failures preserved (both pinned by tests). Verified by measurement, not assumption: an Effect forked inside an ALS scope keeps seeing it across `sleep`, `yieldNow` and a `setTimeout` promise, while the same effect merely constructed inside sees nothing. - **The devtools `/invoke` path never entered the scope at all.** It bypasses the rpc stack by design, and that also bypassed everything `bindMutation` sets up. The audit plugin recorded a traceId (it reads `requestContext.traceId`, which this path does build) while every write underneath carried none — three consumers of one call disagreeing about its trace. It also synthesised `inspect-<8 random chars>`, which no trace consumer can parse; the reporter's framing is the rule worth keeping — *a synthesised id produces a column that looks joinable and is not; NULL at least fails honestly.* It is a real 32-hex id now, and it reaches all three sinks.
72
+
73
+ `actingUserId` is imported at the new call site rather than re-derived — one answer to "who is writing", shared with what `audit()` stamps.
74
+ - **@voltro/cli** — Three ways a check reported nothing while checking nothing, all found by a consumer verifying the silence instead of trusting it.
75
+
76
+ - **`unexercised-row-filter` never fired on a typed registration.** The match was `/\bsetRowFilter\s*\(/`, which demands the paren directly after the name, so `setRowFilter<Ctx>({…})` — the spelling our own generic signature invites — broke it. The rule was blind for exactly the teams that had wired `load`/`predicate` carefully. It counts CALLS now. - **…and its test-side condition was satisfiable by a COMMENT.** It matched `rowFilter:` in raw text. Comments and string literals are stripped, and the suite must both call `makeTestContext` and bind `rowFilter` in real code. - **The same paren-adjacent shape sat in two shipped codemod gates.** `0.7.0/01` (row filter) and `0.7.0/02` (`invoke`) both gate on a generic export, so a typed call made `voltro update` print nothing at all: the upgrade reads as clean and the behaviour change lands unread. Both now use the shared `callPattern`, which allows type arguments including nested ones.
77
+
78
+ Two false-positive fixes in the hand-roll detector, from the same report:
79
+
80
+ - **A file that WIRES a plugin is no longer told to adopt it.** The `presence` rule reported `app.config.ts` (which calls `presencePlugin()`) to an app that had just deleted its hand-rolled table. Rules that recommend a package now declare it, and a file referencing that package is skipped. - **Generated files and `.d.ts` are out of the scan.** A recommendation aimed at a file the next boot overwrites is never actionable.
81
+
82
+ And one more of the first kind, found while checking why a withdrawn report's probe had stayed silent: `raw-fetch` counted only a BARE `fetch(…)` callee, so `globalThis.fetch(url)` / `self.fetch(url)` in a server file read as clean.
83
+ - **@voltro/cli** — `voltro doctor`'s `serverOnly: NOT CHECKED` line now names the failure, and the field exists in `--json`.
84
+
85
+ The refusal to claim a pass was right. What shipped with it was nothing to act on: the `catch` discarded the error entirely, so there was no reason, no failing module, and — because the field was absent from `--json` — no way for CI to assert "still unchecked" rather than reading silence as a pass.
86
+
87
+ A consumer's verdict, which is the useful part: *"The message is honest and that is the problem."* They had already verified that every descriptor, `app.config.ts` and the generated rpc group imported cleanly under `tsx` on their own, so the difference had to be in what `loadDiscovered` does BEYOND importing — and none of that was visible from outside. It matters more than its size because `.serverOnly()` is what guards their `sessions.tokenHash` and `apiKeys.keyHash`, markers they added after finding a query whose output schema shipped a hash over the wire.
88
+
89
+ `--json` now carries `serverOnly: { checked, reason?, leaks? }`. Gate CI on `checked === false`.
90
+ - **@voltro/plugin-versioning** — A version snapshot no longer copies `.serverOnly()` columns into `_voltro_row_history`. `.encrypted()` columns are KEPT, and that distinction is the whole finding.
91
+
92
+ Reported by a team choosing which tables to version: `sessions` holds `.encrypted()` PATs and a `tokenHash`, `apiKeys` holds a `keyHash`, and they could not determine from outside what the snapshot would contain. They excluded both tables — then went and measured it, which corrected their own report:
93
+
94
+ ```
95
+ probeItems.secret enc:v1:a56iziEV9THLhzmJ:Vk0ux+0bECleTLBJkCa0Rg==:3AtMwP…
96
+ _voltro_row_history {"secret":"enc:v1:a56iziEV9THLhzmJ:Vk0ux+0bECleTLBJkCa0Rg==:…"}
97
+ ```
98
+
99
+ **`.encrypted()` lands as ciphertext, byte-identical to the source column**, so versioning such a table widens nothing — the history is exactly as readable as the row it came from. Withholding it would have cost real audit data to prevent an exposure that does not exist.
100
+
101
+ **`.serverOnly()` is withheld**, and the reason is not "a second copy under different retention" — that argument is weak on its own, since the hash already sits in the source table. The decisive one: `crud.*` STRIPS `.serverOnly()` columns from every row it returns, and a snapshot would smuggle the same value back past that stripping inside a `json()` blob, where no column-level rule applies.
102
+
103
+ Withheld names are listed under `data._omitted`, so a reader can tell "this column was withheld" from "this column did not exist then". Both timings apply the same policy. `.sensitive()` is not involved: it is an export-masking marker for values that are legitimately readable in the app.
104
+ - **@voltro/cli** — A page that RE-EXPORTS its component (`export { default, renderMode } from '../page'`) no longer fails the codegen gate with "exports no default". The check required the literal words `as default`, so the one spelling that lets two routes share a screen without copying it was the one spelling it refused — and it refused in `voltro build`, while dev and tests stayed green because nothing prerenders there. `export { default as Screen }` is still correctly rejected: it renames the default away.
105
+
106
+ Two follow-ons from the same shape:
107
+
108
+ - The refusal message said the file "ends in `.page.tsx`" and offered "drop the `.page` suffix" as a fix. That is the 0.15.0 convention, replaced by directory routing in 0.17.0 — it named a convention that no longer exists and a fix that could not work. It now names `page.tsx` and both real fixes. - `scanRenderProfile` read a forwarded `renderMode` as absent and fell back to `'static'`, so `staticSafe` and the deploy-target classification could call an app CDN-deployable with an `ssr` route in it. The forward is now followed (relative specifiers, depth-capped); an unresolvable one still falls back rather than failing the scan.
109
+ - **@voltro/database** — `VOLTRO_SOFT_DROP=1` could never converge. The applier renames the object to `<name>__dropped_<ts>` instead of dropping it, which leaves it undeclared — and the differ read that as one more forgotten table, planning the drop again. The re-plan inside `applyPlan` then found an operation still outstanding and aborted with "the DDL for these operations is a no-op — this is a framework bug", which was a wrong diagnosis of a real defect: the DDL had worked. No fingerprint was recorded, so the migration counted as unapplied and every later `db apply` / boot hit the same wall. The only exit was a hard drop of the snapshot — exactly the recoverability the flag is chosen for.
110
+
111
+ The planner now treats `<name>__dropped_<YYYYMMDDHHMMSS>` as framework-managed, alongside `_voltro_*` / `cluster_*`. Deliberately not retention-aware: a planner whose output depends on the clock would produce different plans before and after midnight, and `db gc-snapshots` already owns expiry.
112
+
113
+ Reported against tables; the same defect existed one level down for soft-dropped COLUMNS, where it was worse — a re-planned `drop-column` carries no `dropped()` marker and so refuses to plan at all. Both are fixed.
114
+
115
+ The convergence message itself no longer asserts a cause it cannot know. It said "the DDL for these operations is a no-op", which was flatly wrong here and sent the reporter looking for dead DDL. It now names both causes — no-op DDL, and a planner that cannot see what the DDL did — and says which one an operation naming a just-renamed object usually is.
116
+
117
+ ### Internal (no consumer-facing effect)
118
+
119
+ - **The `0.20.0/01_write-recorder-port` codemod gains the gate test its two predecessors have.**
120
+
121
+ `codemodRegistry.test.ts` asserts that every `*.codemod.ts` on disk is registered and that ids are unique — registration, not behaviour. What it cannot see is the one way a `manual` codemod fails in practice: an `appliesTo` that is too broad, so the note prints for projects that have nothing to do. That is not a cosmetic problem. A note which fires on every app is how readers learn to skip notes, and the next one carries a boot refusal.
122
+
123
+ This codemod is the case where the silent direction matters most. The break is a TYPE error, so `tsc` already names every affected site; the note exists only to explain `maxOf`, which the compiler cannot. Apps that merely ENABLE `timing: 'in-transaction'` need to do nothing — `plugin-versioning` ships the recorder and it is already updated — and they are the large majority.
124
+
125
+ Four cases, covering both directions: a project registering its own recorder (note prints, and names `{ append }`, `maxOf`, and the `null`-is-not-zero distinction that a hand-written sequence gets wrong), an app that only enables the timing (silent), the identifier in a comment or a string (silent), and the generic call form `registerWriteRecorder<Row>(…)`, which `callPattern` admits and a naive match would miss.
126
+
127
+ ---
128
+
42
129
  ## [0.19.0] — 2026-07-29
43
130
 
44
131
  ### ⚠ BREAKING
package/dist/index.d.ts CHANGED
@@ -282,6 +282,12 @@ export declare class MysqlStore implements DataStore {
282
282
  * re-enter `routeEvent` and emit a change event for the trail's own table.
283
283
  */
284
284
  private appendInTxn;
285
+ /**
286
+ * The other half of the recorder port: ONE aggregate on the caller's
287
+ * connection, so an append-only trail can number its own entries. Equality
288
+ * filter only — see `writeRecorder.ts` for why it is this narrow.
289
+ */
290
+ private maxInTxn;
285
291
  private routeEvent;
286
292
  private executeUpsert;
287
293
  /**
package/dist/index.js CHANGED
@@ -3,16 +3,16 @@ import { Config as t, Effect as n, Layer as r, ManagedRuntime as i, Option as a,
3
3
  import { EventEmitter as c } from "node:events";
4
4
  import { createLogger as l } from "@voltro/logger";
5
5
  import { SqlClient as u, TransactionConnection as d } from "@effect/sql/SqlClient";
6
- import { CDC_OFFSETS_TABLE as f, EagerCardinalityError as p, _voltroCdcOffsetsTable as m, attachEagerLoads as h, attributionFields as g, compileEagerJson as _, compilePredicate as v, compileRawFragment as y, compileSelect as b, decodeRowsFromSchema as x, encodeRowForSchema as S, hasEagerLoads as C, isTableReactive as w, qualifyTable as T, raiseChangeListenerCeiling as E, recordsTable as D, requireTable as O, runWriteRecorders as k, stampGeneratedId as A, stampGeneratedIds as j } from "@voltro/database";
6
+ import { CDC_OFFSETS_TABLE as f, EagerCardinalityError as p, _voltroCdcOffsetsTable as m, attachEagerLoads as h, attributionFields as g, attributionKey as _, claimPendingAttribution as v, compileEagerJson as y, compilePredicate as b, compileRawFragment as x, compileSelect as S, currentWriteAttribution as C, decodeRowsFromSchema as w, encodeRowForSchema as T, hasEagerLoads as E, isTableReactive as D, qualifyTable as O, raiseChangeListenerCeiling as k, recordsTable as ee, registerPendingAttribution as te, requireTable as A, runWithWriteAttribution as j, runWriteRecorders as M, stampGeneratedId as N, stampGeneratedIds as P } from "@voltro/database";
7
7
  //#region src/sqlLayer.ts
8
- var M = (n) => e.layerConfig({
8
+ var F = (n) => e.layerConfig({
9
9
  host: t.succeed(n.host),
10
10
  port: t.succeed(n.port),
11
11
  username: t.succeed(n.username),
12
12
  password: t.succeed(o.make(n.password)),
13
13
  database: t.succeed(n.database),
14
14
  ...n.maxConnections === void 0 ? {} : { maxConnections: t.succeed(n.maxConnections) }
15
- }), N = (e) => {
15
+ }), I = (e) => {
16
16
  if (e.url) {
17
17
  let t = new URL(e.url);
18
18
  return {
@@ -32,18 +32,18 @@ var M = (n) => e.layerConfig({
32
32
  database: e.database ?? "app",
33
33
  ...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections }
34
34
  };
35
- }, P = (e) => M(N(e)), F = (e, t) => e === null ? !1 : t === null ? !0 : e.filename === t.filename ? e.position > t.position : e.filename > t.filename, I = (e) => e.msSinceProgress < e.stallThresholdMs ? "healthy" : e.primary !== null && !F(e.primary, e.reader) ? "idle-caught-up" : "reconnect", L = (e) => (e instanceof Error ? e.message : String(e ?? "")).includes("schema changed between binlog event and metadata fetch"), R = /* @__PURE__ */ new Set([
35
+ }, L = (e) => F(I(e)), ne = (e, t) => e === null ? !1 : t === null ? !0 : e.filename === t.filename ? e.position > t.position : e.filename > t.filename, R = (e) => e.msSinceProgress < e.stallThresholdMs ? "healthy" : e.primary !== null && !ne(e.primary, e.reader) ? "idle-caught-up" : "reconnect", z = (e) => (e instanceof Error ? e.message : String(e ?? "")).includes("schema changed between binlog event and metadata fetch"), B = /* @__PURE__ */ new Set([
36
36
  "writerows",
37
37
  "updaterows",
38
38
  "deleterows"
39
- ]), z = /\b(alter|rename|drop|create)\s+(table|column)?/i, B = (e) => new Promise((t) => setTimeout(t, e)), V = async (e) => {
39
+ ]), V = /\b(alter|rename|drop|create)\s+(table|column)?/i, H = (e) => new Promise((t) => setTimeout(t, e)), U = async (e) => {
40
40
  let t = l({ scope: `voltro:${e.variant}:cdc` }), n;
41
41
  try {
42
42
  n = (await import("@vlasky/zongji")).default;
43
43
  } catch (t) {
44
44
  throw Error(`changeStrategy='cdc' on ${e.variant} requires the '@vlasky/zongji' optional dependency. Install it (it ships as an optionalDependency of @voltro/sql-mysql) to enable binlog CDC.`, { cause: t });
45
45
  }
46
- let r = e.includeTables ? new Set(e.includeTables) : null, i = e.connection.database, a = e.startPosition?.filename ?? null, o = e.startPosition ?? null, s = (t, n) => x([n], t, e.variant)[0], c = null, u = !1, d = 0, f = Date.now(), p = !1, m = null, h = e.stallThresholdMs ?? 25e3, g = e.watchdogIntervalMs ?? 15e3, _ = () => {
46
+ let r = e.includeTables ? new Set(e.includeTables) : null, i = e.connection.database, a = e.startPosition?.filename ?? null, o = e.startPosition ?? null, s = (t, n) => w([n], t, e.variant)[0], c = null, u = !1, d = 0, f = Date.now(), p = !1, m = null, h = e.stallThresholdMs ?? 25e3, g = e.watchdogIntervalMs ?? 15e3, _ = () => {
47
47
  f = Date.now();
48
48
  }, v = (n) => {
49
49
  _();
@@ -52,14 +52,14 @@ var M = (n) => e.layerConfig({
52
52
  a = n.binlogName;
53
53
  return;
54
54
  }
55
- if (l === "query" && n.query && z.test(n.query)) {
55
+ if (l === "query" && n.query && V.test(n.query)) {
56
56
  c && (c.tableMap = {});
57
57
  return;
58
58
  }
59
59
  if (n.nextPosition && a && (o = {
60
60
  filename: a,
61
61
  position: n.nextPosition
62
- }, e.onPosition?.(o)), !R.has(l)) return;
62
+ }, e.onPosition?.(o)), !B.has(l)) return;
63
63
  let u = n.tableMap[n.tableId];
64
64
  if (!u || u.parentSchema !== i) return;
65
65
  let d = u.tableName;
@@ -105,7 +105,7 @@ var M = (n) => e.layerConfig({
105
105
  }, b = (e) => {
106
106
  let t = e;
107
107
  return t?.errno === 1236 || String(t?.code ?? "").includes("ER_MASTER_FATAL_ERROR_READING_BINLOG");
108
- }, S = (r) => new Promise((i, a) => {
108
+ }, x = (r) => new Promise((i, a) => {
109
109
  let o = new n({
110
110
  host: e.connection.host,
111
111
  port: e.connection.port,
@@ -126,9 +126,9 @@ var M = (n) => e.layerConfig({
126
126
  s = !0, a(e instanceof Error ? e : Error(String(e)));
127
127
  return;
128
128
  }
129
- C("binlog reader error", e);
129
+ S("binlog reader error", e);
130
130
  }), o.start(y(r));
131
- }), C = async (n, r) => {
131
+ }), S = async (n, r) => {
132
132
  if (!(u || p)) {
133
133
  p = !0, t.warn(`cdc: reconnecting — ${n}`, {}, r instanceof Error ? r : void 0), e.onError?.(r);
134
134
  try {
@@ -137,11 +137,11 @@ var M = (n) => e.layerConfig({
137
137
  try {
138
138
  c?.stop();
139
139
  } catch {}
140
- if (d++, await B(Math.min(3e4, 500 * 2 ** Math.min(d, 6))), u) return;
141
- let r = o, i = b(n), s = !i && L(n);
140
+ if (d++, await H(Math.min(3e4, 500 * 2 ** Math.min(d, 6))), u) return;
141
+ let r = o, i = b(n), s = !i && z(n);
142
142
  (i || s) && (t.warn(s ? "cdc: un-replayable backlog event (schema moved past it) — jumping to current end + self-heal" : "cdc: binlog gap (purged/failover) — jumping to current end + self-heal"), r = e.resolveStartPosition ? await e.resolveStartPosition().catch(() => null) : null, a = r?.filename ?? null, e.onResync?.());
143
143
  try {
144
- await S(r), d = 0, _();
144
+ await x(r), d = 0, _();
145
145
  return;
146
146
  } catch (e) {
147
147
  n = e;
@@ -153,7 +153,7 @@ var M = (n) => e.layerConfig({
153
153
  }
154
154
  };
155
155
  try {
156
- await S(e.startPosition ?? null);
156
+ await x(e.startPosition ?? null);
157
157
  } catch (n) {
158
158
  if (b(n) && e.startPosition) {
159
159
  t.warn("cdc: persisted offset purged — starting at current end + self-heal");
@@ -161,14 +161,14 @@ var M = (n) => e.layerConfig({
161
161
  c?.stop();
162
162
  } catch {}
163
163
  let n = e.resolveStartPosition ? await e.resolveStartPosition().catch(() => null) : null;
164
- a = n?.filename ?? null, e.onResync?.(), await B(500), await S(n);
164
+ a = n?.filename ?? null, e.onResync?.(), await H(500), await x(n);
165
165
  } else throw n;
166
166
  }
167
- let w = async () => {
167
+ let C = async () => {
168
168
  if (u || p || Date.now() - f < h) return;
169
169
  let n = null;
170
170
  if (e.resolveStartPosition && (n = await e.resolveStartPosition().catch(() => null)), u || p) return;
171
- let r = I({
171
+ let r = R({
172
172
  msSinceProgress: Date.now() - f,
173
173
  stallThresholdMs: h,
174
174
  primary: n,
@@ -182,10 +182,10 @@ var M = (n) => e.layerConfig({
182
182
  reader: o,
183
183
  primary: n,
184
184
  stalledForMs: Date.now() - f
185
- }), C("watchdog: stream stalled (no events while primary advanced)", /* @__PURE__ */ Error("cdc watchdog stall"));
185
+ }), S("watchdog: stream stalled (no events while primary advanced)", /* @__PURE__ */ Error("cdc watchdog stall"));
186
186
  };
187
187
  return m = setInterval(() => {
188
- w();
188
+ C();
189
189
  }, g), m.unref && m.unref(), {
190
190
  stop: async () => {
191
191
  u = !0, m &&= (clearInterval(m), null);
@@ -197,7 +197,7 @@ var M = (n) => e.layerConfig({
197
197
  },
198
198
  currentPosition: () => o
199
199
  };
200
- }, H = /* @__PURE__ */ new Set(["1213", "1205"]), U = (e) => {
200
+ }, W = /* @__PURE__ */ new Set(["1213", "1205"]), G = (e) => {
201
201
  let t = e;
202
202
  for (let e = 0; e < 5 && typeof t == "object" && t; e++) {
203
203
  let e = t.errno;
@@ -206,31 +206,31 @@ var M = (n) => e.layerConfig({
206
206
  if (typeof n == "string") return n;
207
207
  t = t.cause;
208
208
  }
209
- }, W = (e) => {
210
- let t = U(e);
211
- return t !== void 0 && H.has(t);
212
- }, G = (e) => W(e) ? "retry" : "noRetry", K = (e) => {
209
+ }, K = (e) => {
210
+ let t = G(e);
211
+ return t !== void 0 && W.has(t);
212
+ }, q = (e) => K(e) ? "retry" : "noRetry", J = (e) => {
213
213
  if (e == null) return "null";
214
214
  let t = typeof e;
215
215
  if (t === "bigint") return `${e}n`;
216
216
  if (t !== "object") return JSON.stringify(e);
217
217
  if (e instanceof Date) return `"${e.toISOString()}"`;
218
- if (Array.isArray(e)) return `[${e.map(K).join(",")}]`;
218
+ if (Array.isArray(e)) return `[${e.map(J).join(",")}]`;
219
219
  let n = e;
220
- return `{${Object.keys(n).sort().map((e) => `${JSON.stringify(e)}:${K(n[e])}`).join(",")}}`;
221
- }, q = (e) => {
222
- let t = K(e), n = 2166136261;
220
+ return `{${Object.keys(n).sort().map((e) => `${JSON.stringify(e)}:${J(n[e])}`).join(",")}}`;
221
+ }, Y = (e) => {
222
+ let t = J(e), n = 2166136261;
223
223
  for (let e = 0; e < t.length; e++) n ^= t.charCodeAt(e), n = Math.imul(n, 16777619);
224
224
  return (n >>> 0).toString(36);
225
- }, J = (e, t) => {
225
+ }, X = (e, t) => {
226
226
  let n = setTimeout(e, t);
227
227
  typeof n.unref == "function" && n.unref();
228
- }, Y = class {
228
+ }, re = class {
229
229
  variant;
230
230
  ttlMs;
231
231
  schedule;
232
232
  seen = /* @__PURE__ */ new Map();
233
- constructor(e, t = 6e4, n = J) {
233
+ constructor(e, t = 6e4, n = X) {
234
234
  this.variant = e, this.ttlMs = t, this.schedule = n;
235
235
  }
236
236
  key(e) {
@@ -240,11 +240,11 @@ var M = (n) => e.layerConfig({
240
240
  if (n == null) return null;
241
241
  let r = t;
242
242
  try {
243
- r = x([t], e.table, this.variant)[0] ?? t;
243
+ r = w([t], e.table, this.variant)[0] ?? t;
244
244
  } catch {
245
245
  r = t;
246
246
  }
247
- return `${e.table} ${e.op} ${String(n)} ${q(r)}`;
247
+ return `${e.table} ${e.op} ${String(n)} ${Y(r)}`;
248
248
  }
249
249
  admit(e) {
250
250
  let t = this.key(e);
@@ -259,12 +259,15 @@ var M = (n) => e.layerConfig({
259
259
  get pending() {
260
260
  return this.seen.size;
261
261
  }
262
- }, X = async (e) => {
262
+ }, Z = async (e) => {
263
263
  let t = e.variant ?? "mysql", n = l({ scope: `voltro:${t}` }), a = e.changeStrategy ?? "inline", o = a;
264
264
  a === "cdc" && !e.cdcConfig && (n.warn("changeStrategy='cdc' requires cdcConfig (serverId + connection); falling back to 'inline'."), o = "inline");
265
- let s = e.tracerLayer ? r.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, c = i.make(s), d = new Z(await c.runPromise(u), c, t, o);
265
+ let s = e.tracerLayer ? r.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, c = i.make(s), d = new ie(await c.runPromise(u), c, t, o);
266
266
  return o === "cdc" && e.cdcConfig && await d.startCdcConsumer(e.cdcConfig), d;
267
- }, Z = class e {
267
+ }, Q = () => {
268
+ let e = C();
269
+ return e === void 0 ? (e) => e() : (t) => j(e, t);
270
+ }, ie = class e {
268
271
  sql;
269
272
  runtime;
270
273
  variant;
@@ -280,13 +283,13 @@ var M = (n) => e.layerConfig({
280
283
  cdcStreamName = "default";
281
284
  cdcGate;
282
285
  constructor(e, t, n, r = "inline", i = null, a, o) {
283
- this.sql = e, this.runtime = t, this.variant = n, this.changeStrategy = r, this.namespace = i, this.log = l({ scope: `voltro:${n}` }), this.emitter = a ?? new c(), E(this.emitter), this.cdcGate = o ?? new Y(n);
286
+ this.sql = e, this.runtime = t, this.variant = n, this.changeStrategy = r, this.namespace = i, this.log = l({ scope: `voltro:${n}` }), this.emitter = a ?? new c(), k(this.emitter), this.cdcGate = o ?? new re(n);
284
287
  }
285
288
  withNamespace(t) {
286
289
  return t === this.namespace ? this : new e(this.sql, this.runtime, this.variant, this.changeStrategy, t, this.emitter, this.cdcGate);
287
290
  }
288
291
  nsT(e) {
289
- return T(this.namespace, e);
292
+ return O(this.namespace, e);
290
293
  }
291
294
  get dialectId() {
292
295
  return this.variant;
@@ -298,8 +301,8 @@ var M = (n) => e.layerConfig({
298
301
  };
299
302
  }
300
303
  async executeQuery(e, t) {
301
- let r = b(e, this.sql, this.namespace), i = t ? n.provideService(r, d, t) : r;
302
- return x(await this.runtime.runPromise(i), e.table, this.variant);
304
+ let r = S(e, this.sql, this.namespace), i = t ? n.provideService(r, d, t) : r;
305
+ return w(await this.runtime.runPromise(i), e.table, this.variant);
303
306
  }
304
307
  get supportsInsertReturning() {
305
308
  return this.variant === "mariadb";
@@ -311,10 +314,10 @@ var M = (n) => e.layerConfig({
311
314
  return !1;
312
315
  }
313
316
  async executeInsert(e, t, r, i) {
314
- t = A(e, t);
317
+ t = N(e, t);
315
318
  let a = this.sql;
316
319
  if (this.supportsInsertReturning) {
317
- let o = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(S(t, e))} RETURNING *`, s = r ? n.provideService(o, d, r) : o, c = (await this.runtime.runPromise(s))[0];
320
+ let o = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(T(t, e))} RETURNING *`, s = r ? n.provideService(o, d, r) : o, c = (await this.runtime.runPromise(s))[0];
318
321
  if (!c) throw Error(`MysqlStore.insert: no row returned for table '${e}'`);
319
322
  return await this.routeEvent({
320
323
  table: e,
@@ -323,7 +326,7 @@ var M = (n) => e.layerConfig({
323
326
  new: c
324
327
  }, i, r), c;
325
328
  }
326
- let o = S(t, e), s = t.id;
329
+ let o = T(t, e), s = t.id;
327
330
  if (s === void 0) {
328
331
  let t = await this.insertRecoverAutoId(e, o, r);
329
332
  return await this.routeEvent({
@@ -357,7 +360,7 @@ var M = (n) => e.layerConfig({
357
360
  if (e) return this.runtime.runPromise(t(e));
358
361
  this.inflightTxns++;
359
362
  try {
360
- let e = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(d), (e) => a.isNone(e) ? n.fail(/* @__PURE__ */ Error("MysqlStore.insert: TransactionConnection missing.")) : t(e.value)))), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(W)), o = e.pipe(n.retry(i), n.withSpan("store.insert", { attributes: {
363
+ let e = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(d), (e) => a.isNone(e) ? n.fail(/* @__PURE__ */ Error("MysqlStore.insert: TransactionConnection missing.")) : t(e.value)))), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(K)), o = e.pipe(n.retry(i), n.withSpan("store.insert", { attributes: {
361
364
  "db.system": this.variant,
362
365
  "db.operation": r
363
366
  } }));
@@ -367,8 +370,8 @@ var M = (n) => e.layerConfig({
367
370
  }
368
371
  }
369
372
  async executeInsertMany(e, t, r, i) {
370
- if (t = j(e, t), t.length === 0) return [];
371
- let a = this.sql, o = t.map((t) => S(t, e));
373
+ if (t = P(e, t), t.length === 0) return [];
374
+ let a = this.sql, o = t.map((t) => T(t, e));
372
375
  if (this.supportsInsertReturning) {
373
376
  let t = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(o)} RETURNING *`, s = r ? n.provideService(t, d, r) : t, c = await this.runtime.runPromise(s);
374
377
  for (let t of c) await this.routeEvent({
@@ -427,7 +430,7 @@ var M = (n) => e.layerConfig({
427
430
  async executeUpdate(e, t, r, i, a) {
428
431
  let o = this.sql;
429
432
  if (this.supportsUpdateReturning) {
430
- let s = o`UPDATE ${o(this.nsT(e))} SET ${o.update(S(r, e))} WHERE ${o("id")} = ${t} RETURNING *`, c = i ? n.provideService(s, d, i) : s, l = (await this.runtime.runPromise(c))[0];
433
+ let s = o`UPDATE ${o(this.nsT(e))} SET ${o.update(T(r, e))} WHERE ${o("id")} = ${t} RETURNING *`, c = i ? n.provideService(s, d, i) : s, l = (await this.runtime.runPromise(c))[0];
431
434
  return l ? (await this.routeEvent({
432
435
  table: e,
433
436
  op: "update",
@@ -435,7 +438,7 @@ var M = (n) => e.layerConfig({
435
438
  new: l
436
439
  }, a, i), l) : null;
437
440
  }
438
- let s = o`UPDATE ${o(this.nsT(e))} SET ${o.update(S(r, e))} WHERE ${o("id")} = ${t}`, c = i ? n.provideService(s, d, i) : s, l = await this.runtime.runPromise(c);
441
+ let s = o`UPDATE ${o(this.nsT(e))} SET ${o.update(T(r, e))} WHERE ${o("id")} = ${t}`, c = i ? n.provideService(s, d, i) : s, l = await this.runtime.runPromise(c);
439
442
  if (l && typeof l.affectedRows == "number" && l.affectedRows === 0) return null;
440
443
  let u = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t}`, f = i ? n.provideService(u, d, i) : u, p = (await this.runtime.runPromise(f))[0];
441
444
  return p ? (await this.routeEvent({
@@ -467,21 +470,35 @@ var M = (n) => e.layerConfig({
467
470
  }, i, r), !0;
468
471
  }
469
472
  async appendInTxn(e, t, r) {
470
- let i = this.sql, a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(S(t, e))}`;
473
+ let i = this.sql, a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(T(t, e))}`;
471
474
  await this.runtime.runPromise(r ? n.provideService(a, d, r) : a);
472
475
  }
476
+ async maxInTxn(e, t, r, i) {
477
+ let a = this.sql, o = Object.entries(r).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 ? n.provideService(s, d, i) : s))[0]?.m;
478
+ return c == null ? null : Number(c);
479
+ }
473
480
  async routeEvent(e, t, n = null) {
474
481
  if (e = {
475
482
  ...g(),
476
483
  ...e
477
- }, D(e.table) && await k((e, t) => this.appendInTxn(e, t, n), {
484
+ }, ee(e.table) && await M({
485
+ append: (e, t) => this.appendInTxn(e, t, n),
486
+ maxOf: (e, t, r) => this.maxInTxn(e, t, r, n)
487
+ }, {
478
488
  table: e.table,
479
489
  op: e.op,
480
490
  next: e.new,
481
491
  prev: e.old,
482
492
  traceId: e.traceId,
483
493
  subjectId: e.subjectId
484
- }), t !== null) {
494
+ }), this.changeStrategy === "cdc") {
495
+ let t = (e.op === "delete" ? e.old : e.new)?.id;
496
+ t != null && te(_(e.table, e.op, t), {
497
+ ...e.traceId === void 0 ? {} : { traceId: e.traceId },
498
+ ...e.subjectId === void 0 ? {} : { subjectId: e.subjectId }
499
+ });
500
+ }
501
+ if (t !== null) {
485
502
  t.push(e);
486
503
  return;
487
504
  }
@@ -507,10 +524,10 @@ var M = (n) => e.layerConfig({
507
524
  }
508
525
  return await this.executeUpdate(e, a.id, o, r, i) ?? a;
509
526
  }
510
- return this.executeInsert(e, t, r, i);
527
+ return Q()(() => this.executeInsert(e, t, r, i));
511
528
  }
512
529
  async executeMariadbUpsert(e, t, r, i, a) {
513
- let o = this.sql, s = S(t, e), c = Object.keys(s).filter((e) => s[e] !== void 0), l = r.update === void 0 ? c.filter((e) => e !== "id" && !r.conflictColumns.includes(e)) : r.update, u = l.length > 0 ? o.csv(l.map((e) => o`${o(e)} = VALUES(${o(e)})`)) : o`${o(r.conflictColumns[0])} = VALUES(${o(r.conflictColumns[0])})`, f = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(s)} ON DUPLICATE KEY UPDATE ${u} RETURNING *`, p = i ? n.provideService(f, d, i) : f, m = (await this.runtime.runPromise(p))[0];
530
+ let o = this.sql, s = T(t, e), c = Object.keys(s).filter((e) => s[e] !== void 0), l = r.update === void 0 ? c.filter((e) => e !== "id" && !r.conflictColumns.includes(e)) : r.update, u = l.length > 0 ? o.csv(l.map((e) => o`${o(e)} = VALUES(${o(e)})`)) : o`${o(r.conflictColumns[0])} = VALUES(${o(r.conflictColumns[0])})`, f = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(s)} ON DUPLICATE KEY UPDATE ${u} RETURNING *`, p = i ? n.provideService(f, d, i) : f, m = (await this.runtime.runPromise(p))[0];
514
531
  if (!m) throw Error(`MysqlStore.upsert: no row returned for table '${e}'`);
515
532
  let h = t.id !== void 0 && t.id === m.id ? "insert" : "update";
516
533
  return await this.routeEvent({
@@ -521,8 +538,8 @@ var M = (n) => e.layerConfig({
521
538
  }, a, i), m;
522
539
  }
523
540
  async executeInsertIgnore(e, t, r, i, a) {
524
- if (t = A(e, t), this.variant === "mariadb") {
525
- let o = this.sql, s = o`INSERT IGNORE INTO ${o(this.nsT(e))} ${o.insert(S(t, e))} RETURNING *`, c = i ? n.provideService(s, d, i) : s, l = (await this.runtime.runPromise(c))[0];
541
+ if (t = N(e, t), this.variant === "mariadb") {
542
+ let o = this.sql, s = o`INSERT IGNORE INTO ${o(this.nsT(e))} ${o.insert(T(t, e))} RETURNING *`, c = i ? n.provideService(s, d, i) : s, l = (await this.runtime.runPromise(c))[0];
526
543
  if (l) return await this.routeEvent({
527
544
  table: e,
528
545
  op: "insert",
@@ -533,7 +550,7 @@ var M = (n) => e.layerConfig({
533
550
  if (!u) throw Error(`MysqlStore.insertIgnore: the insert was skipped as a conflict, but no existing row matches conflictColumns [${r.conflictColumns.join(", ")}] on '${e}'. A DIFFERENT unique constraint fired — a second unique index, or the primary key when you named something else. insertIgnore models ONE conflict target: name the columns of the constraint that actually collides, or handle the unique violation yourself.`);
534
551
  return u;
535
552
  }
536
- return await this.findByConflict(e, t, r.conflictColumns, i) || this.executeInsert(e, t, i, a);
553
+ return await this.findByConflict(e, t, r.conflictColumns, i) || Q()(() => this.executeInsert(e, t, i, a));
537
554
  }
538
555
  async findByConflict(e, t, r, i) {
539
556
  if (r.length === 0) return;
@@ -544,12 +561,12 @@ var M = (n) => e.layerConfig({
544
561
  return this.runWithEager(e, null);
545
562
  }
546
563
  raw(e, t) {
547
- let n = y(e, this.sql);
564
+ let n = x(e, this.sql);
548
565
  return this.runtime.runPromise(n);
549
566
  }
550
567
  async runWithEager(e, t) {
551
- if (!C(e)) return this.executeQuery(e, t);
552
- let r = this.variant === "mariadb" ? "mariadb" : "mysql", i = this.namespace === null ? _(e, this.sql, r) : null;
568
+ if (!E(e)) return this.executeQuery(e, t);
569
+ let r = this.variant === "mariadb" ? "mariadb" : "mysql", i = this.namespace === null ? y(e, this.sql, r) : null;
553
570
  if (i !== null) try {
554
571
  let e = t ? n.provideService(i.fragment, d, t) : i.fragment, r = await this.runtime.runPromise(e);
555
572
  return i.decode(r);
@@ -557,35 +574,35 @@ var M = (n) => e.layerConfig({
557
574
  if (e instanceof p) throw e;
558
575
  this.log.warn(`${this.variant} JSON-agg eager-load failed; falling back to walker`, { err: e });
559
576
  }
560
- return h(await this.executeQuery(e, t), e.eager, e.sourceTable ?? O(e.table), (e) => this.executeQuery(e, t));
577
+ return h(await this.executeQuery(e, t), e.eager, e.sourceTable ?? A(e.table), (e) => this.executeQuery(e, t));
561
578
  }
562
579
  getInternalRunWithEager() {
563
580
  return this.runWithEager.bind(this);
564
581
  }
565
582
  insert(e, t) {
566
- return this.executeInsert(e, t, null, null);
583
+ return Q()(() => this.executeInsert(e, t, null, null));
567
584
  }
568
585
  insertMany(e, t) {
569
- return this.executeInsertMany(e, t, null, null);
586
+ return Q()(() => this.executeInsertMany(e, t, null, null));
570
587
  }
571
588
  patchJson(e, t, n, r) {
572
- return this.executePatchJson(e, t, n, r, null, null);
589
+ return Q()(() => this.executePatchJson(e, t, n, r, null, null));
573
590
  }
574
591
  upsert(e, t, n) {
575
- return this.executeUpsert(e, t, n, null, null);
592
+ return Q()(() => this.executeUpsert(e, t, n, null, null));
576
593
  }
577
594
  insertIgnore(e, t, n) {
578
- return this.executeInsertIgnore(e, t, n, null, null);
595
+ return Q()(() => this.executeInsertIgnore(e, t, n, null, null));
579
596
  }
580
597
  update(e, t, n) {
581
- return this.executeUpdate(e, t, n, null, null);
598
+ return Q()(() => this.executeUpdate(e, t, n, null, null));
582
599
  }
583
600
  delete(e, t) {
584
- return this.executeDelete(e, t, null, null);
601
+ return Q()(() => this.executeDelete(e, t, null, null));
585
602
  }
586
603
  async updateMany(e, t, r) {
587
604
  if (this.supportsUpdateReturning) {
588
- let n = this.sql, i = v(r.where, n, this.namespace), a = n`UPDATE ${n(this.nsT(e))} SET ${n.update(S(t, e))} WHERE ${i} RETURNING *`, o = await this.runtime.runPromise(a);
605
+ let n = this.sql, i = b(r.where, n, this.namespace), a = n`UPDATE ${n(this.nsT(e))} SET ${n.update(T(t, e))} WHERE ${i} RETURNING *`, o = await this.runtime.runPromise(a);
589
606
  for (let t of o) await this.routeEvent({
590
607
  table: e,
591
608
  op: "update",
@@ -594,23 +611,23 @@ var M = (n) => e.layerConfig({
594
611
  }, null, null);
595
612
  return o.length;
596
613
  }
597
- let i = this.sql, o = v(r.where, i, this.namespace);
614
+ let i = this.sql, o = b(r.where, i, this.namespace);
598
615
  this.inflightTxns++;
599
616
  try {
600
617
  let r = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(d), (r) => {
601
618
  if (a.isNone(r)) return n.fail(/* @__PURE__ */ Error("MysqlStore.updateMany: TransactionConnection missing."));
602
- let s = r.value, c = i`SELECT id FROM ${i(this.nsT(e))} WHERE ${o} FOR UPDATE`, l = i`UPDATE ${i(this.nsT(e))} SET ${i.update(S(t, e))} WHERE ${o}`;
619
+ let s = r.value, c = i`SELECT id FROM ${i(this.nsT(e))} WHERE ${o} FOR UPDATE`, l = i`UPDATE ${i(this.nsT(e))} SET ${i.update(T(t, e))} WHERE ${o}`;
603
620
  return n.flatMap(n.provideService(c, d, s), (t) => {
604
621
  if (t.length === 0) return n.succeed([]);
605
622
  let r = t.map((e) => e.id), a = i`SELECT * FROM ${i(this.nsT(e))} WHERE ${i("id")} IN ${i.in(r)}`;
606
623
  return n.flatMap(n.provideService(l, d, s), () => n.provideService(a, d, s));
607
624
  });
608
- }))), c = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(W)), l = r.pipe(n.retry(c), n.withSpan("store.updateMany", { attributes: {
625
+ }))), c = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(K)), l = r.pipe(n.retry(c), n.withSpan("store.updateMany", { attributes: {
609
626
  "db.system": this.variant,
610
627
  "db.operation": "update"
611
628
  } })), u = await this.runtime.runPromise(l);
612
629
  if (u.length === 0) return 0;
613
- let f = x(u, e, this.variant);
630
+ let f = w(u, e, this.variant);
614
631
  for (let t of f) await this.routeEvent({
615
632
  table: e,
616
633
  op: "update",
@@ -623,9 +640,9 @@ var M = (n) => e.layerConfig({
623
640
  }
624
641
  }
625
642
  async deleteMany(e, t) {
626
- let r = this.sql, i = v(t.where, r, this.namespace);
643
+ let r = this.sql, i = b(t.where, r, this.namespace);
627
644
  if (this.supportsDeleteReturning) {
628
- let t = r`DELETE FROM ${r(this.nsT(e))} WHERE ${i} RETURNING *`, n = x(await this.runtime.runPromise(t), e, this.variant);
645
+ let t = r`DELETE FROM ${r(this.nsT(e))} WHERE ${i} RETURNING *`, n = w(await this.runtime.runPromise(t), e, this.variant);
629
646
  for (let t of n) await this.routeEvent({
630
647
  table: e,
631
648
  op: "delete",
@@ -640,12 +657,12 @@ var M = (n) => e.layerConfig({
640
657
  if (a.isNone(t)) return n.fail(/* @__PURE__ */ Error("MysqlStore.deleteMany: TransactionConnection missing."));
641
658
  let o = t.value, s = r`SELECT * FROM ${r(this.nsT(e))} WHERE ${i} FOR UPDATE`, c = r`DELETE FROM ${r(this.nsT(e))} WHERE ${i}`;
642
659
  return n.flatMap(n.provideService(s, d, o), (e) => e.length === 0 ? n.succeed(e) : n.as(n.provideService(c, d, o), e));
643
- }))), o = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(W)), c = t.pipe(n.retry(o), n.withSpan("store.deleteMany", { attributes: {
660
+ }))), o = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(K)), c = t.pipe(n.retry(o), n.withSpan("store.deleteMany", { attributes: {
644
661
  "db.system": this.variant,
645
662
  "db.operation": "delete"
646
663
  } })), l = await this.runtime.runPromise(c);
647
664
  if (l.length === 0) return 0;
648
- let u = x(l, e, this.variant);
665
+ let u = w(l, e, this.variant);
649
666
  for (let t of u) await this.routeEvent({
650
667
  table: e,
651
668
  op: "delete",
@@ -658,13 +675,13 @@ var M = (n) => e.layerConfig({
658
675
  }
659
676
  }
660
677
  emitChange(e) {
661
- w(e.table) && (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || this.emitter.emit("change", e));
678
+ D(e.table) && (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || this.emitter.emit("change", e));
662
679
  }
663
680
  async startCdcConsumer(e) {
664
681
  if (this.cdcHandle) return;
665
682
  await this.assertBinlogConfig(), this.cdcReplicaId = e.replicaId;
666
683
  let t = await this.readCdcOffset(e.replicaId) ?? await this.resolveBinlogEnd();
667
- this.cdcHandle = await V({
684
+ this.cdcHandle = await U({
668
685
  connection: e.connection,
669
686
  serverId: e.serverId,
670
687
  variant: this.variant,
@@ -780,26 +797,26 @@ var M = (n) => e.layerConfig({
780
797
  }
781
798
  async transactional(e) {
782
799
  this.inflightTxns++;
783
- let t = 0, r = n.suspend(() => {
784
- let r = ++t;
785
- return this.sql.withTransaction(n.flatMap(n.serviceOption(d), (t) => {
786
- if (a.isNone(t)) return n.fail(/* @__PURE__ */ Error("MysqlStore.transactional: TransactionConnection missing."));
787
- let i = new Q(this, t.value);
800
+ let t = Q(), r = 0, i = n.suspend(() => {
801
+ let i = ++r;
802
+ return this.sql.withTransaction(n.flatMap(n.serviceOption(d), (r) => {
803
+ if (a.isNone(r)) return n.fail(/* @__PURE__ */ Error("MysqlStore.transactional: TransactionConnection missing."));
804
+ let o = new ae(this, r.value);
788
805
  return n.tryPromise({
789
- try: () => e(i).then((e) => ({
806
+ try: () => t(() => e(o)).then((e) => ({
790
807
  result: e,
791
- view: i,
792
- attempt: r
808
+ view: o,
809
+ attempt: i
793
810
  })),
794
811
  catch: (e) => e
795
812
  });
796
813
  }));
797
- }), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(W)), o = r.pipe(n.retry(i), n.withSpan("store.transactional", { attributes: {
814
+ }), o = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(K)), c = i.pipe(n.retry(o), n.withSpan("store.transactional", { attributes: {
798
815
  "db.system": this.variant,
799
816
  "db.operation": "transaction"
800
817
  } }));
801
818
  try {
802
- let e = await this.runtime.runPromise(o);
819
+ let e = await this.runtime.runPromise(c);
803
820
  return e.view.commitEvents(), e.result;
804
821
  } finally {
805
822
  this.inflightTxns--;
@@ -814,7 +831,10 @@ var M = (n) => e.layerConfig({
814
831
  return this.changeStrategy === "cdc" ? "fleet" : "local";
815
832
  }
816
833
  injectExternalChange(e) {
817
- this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || w(e.table) && this.emitter.emit("change", {
834
+ if (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || !D(e.table)) return;
835
+ let t = (e.op === "delete" ? e.old : e.new)?.id, n = t == null ? void 0 : v(_(e.table, e.op, t));
836
+ this.emitter.emit("change", {
837
+ ...n,
818
838
  ...e,
819
839
  origin: "injected"
820
840
  });
@@ -840,7 +860,7 @@ var M = (n) => e.layerConfig({
840
860
  async ping() {
841
861
  await this.runtime.runPromise(this.sql`SELECT 1`);
842
862
  }
843
- }, Q = class {
863
+ }, ae = class {
844
864
  parent;
845
865
  txn;
846
866
  events = [];
@@ -912,7 +932,7 @@ var M = (n) => e.layerConfig({
912
932
  this.events.length = 0;
913
933
  }
914
934
  }
915
- }, $ = (e) => e.__mysqlReplicationFriend ?? null, ee = () => ({
935
+ }, $ = (e) => e.__mysqlReplicationFriend ?? null, oe = () => ({
916
936
  async capturePrimaryPosition(e) {
917
937
  let t = $(e);
918
938
  if (t === null) throw Error("mysqlReplicationAdapter: primary is not a MysqlStore (missing __mysqlReplicationFriend).");
@@ -932,24 +952,24 @@ var M = (n) => e.layerConfig({
932
952
  compare(e, t) {
933
953
  return "behind";
934
954
  }
935
- }), te = {
955
+ }), se = {
936
956
  id: "mysql",
937
- makeSqlLayer: (e) => P(e),
938
- makeStore: (e) => X({
957
+ makeSqlLayer: (e) => L(e),
958
+ makeStore: (e) => Z({
939
959
  ...e,
940
960
  variant: "mysql"
941
961
  }),
942
962
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
943
- retryFilter: G
944
- }, ne = {
963
+ retryFilter: q
964
+ }, ce = {
945
965
  id: "mariadb",
946
- makeSqlLayer: (e) => P(e),
947
- makeStore: (e) => X({
966
+ makeSqlLayer: (e) => L(e),
967
+ makeStore: (e) => Z({
948
968
  ...e,
949
969
  variant: "mariadb"
950
970
  }),
951
971
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
952
- retryFilter: G
972
+ retryFilter: q
953
973
  };
954
974
  //#endregion
955
- export { f as CDC_OFFSETS_TABLE, e as MysqlClient, m as _voltroCdcOffsetsTable, N as connectionFromConfig, M as makeMysqlSqlLayer, P as makeMysqlSqlLayerFromConfig, X as makeMysqlStore, ne as mariadbDialect, te as mysqlDialect, ee as mysqlReplicationAdapter, G as mysqlRetryFilter, V as startBinlogCdc };
975
+ export { f as CDC_OFFSETS_TABLE, e as MysqlClient, m as _voltroCdcOffsetsTable, I as connectionFromConfig, F as makeMysqlSqlLayer, L as makeMysqlSqlLayerFromConfig, Z as makeMysqlStore, ce as mariadbDialect, se as mysqlDialect, oe as mysqlReplicationAdapter, q as mysqlRetryFilter, U as startBinlogCdc };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/sql-mysql",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "MySQL/MariaDB dialect adapter for Voltro's cross-dialect DataStore (mariadb binlog CDC; mysql inline reactivity).",
5
5
  "keywords": [
6
6
  "voltro",
@@ -34,8 +34,8 @@
34
34
  "dependencies": {
35
35
  "@effect/sql": "^0.51.1",
36
36
  "@effect/sql-mysql2": "^0.52.0",
37
- "@voltro/database": "0.19.0",
38
- "@voltro/logger": "0.19.0"
37
+ "@voltro/database": "0.20.0",
38
+ "@voltro/logger": "0.20.0"
39
39
  },
40
40
  "optionalDependencies": {
41
41
  "@vlasky/zongji": "^0.9.0"