@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 +87 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +121 -101
- package/package.json +3 -3
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,
|
|
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
|
|
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
|
-
}),
|
|
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
|
-
},
|
|
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
|
-
]),
|
|
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) =>
|
|
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 &&
|
|
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)), !
|
|
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
|
-
},
|
|
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
|
-
|
|
129
|
+
S("binlog reader error", e);
|
|
130
130
|
}), o.start(y(r));
|
|
131
|
-
}),
|
|
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
|
|
141
|
-
let r = o, i = b(n), s = !i &&
|
|
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
|
|
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
|
|
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
|
|
164
|
+
a = n?.filename ?? null, e.onResync?.(), await H(500), await x(n);
|
|
165
165
|
} else throw n;
|
|
166
166
|
}
|
|
167
|
-
let
|
|
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 =
|
|
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
|
-
}),
|
|
185
|
+
}), S("watchdog: stream stalled (no events while primary advanced)", /* @__PURE__ */ Error("cdc watchdog stall"));
|
|
186
186
|
};
|
|
187
187
|
return m = setInterval(() => {
|
|
188
|
-
|
|
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
|
-
},
|
|
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
|
-
},
|
|
210
|
-
let t =
|
|
211
|
-
return t !== void 0 &&
|
|
212
|
-
},
|
|
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(
|
|
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)}:${
|
|
221
|
-
},
|
|
222
|
-
let t =
|
|
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
|
-
},
|
|
225
|
+
}, X = (e, t) => {
|
|
226
226
|
let n = setTimeout(e, t);
|
|
227
227
|
typeof n.unref == "function" && n.unref();
|
|
228
|
-
},
|
|
228
|
+
}, re = class {
|
|
229
229
|
variant;
|
|
230
230
|
ttlMs;
|
|
231
231
|
schedule;
|
|
232
232
|
seen = /* @__PURE__ */ new Map();
|
|
233
|
-
constructor(e, t = 6e4, n =
|
|
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 =
|
|
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)} ${
|
|
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
|
-
},
|
|
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
|
|
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
|
-
},
|
|
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(),
|
|
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
|
|
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 =
|
|
302
|
-
return
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
371
|
-
let a = this.sql, o = t.map((t) =>
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
},
|
|
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
|
-
}),
|
|
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 =
|
|
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 =
|
|
525
|
-
let o = this.sql, s = o`INSERT IGNORE INTO ${o(this.nsT(e))} ${o.insert(
|
|
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 =
|
|
564
|
+
let n = x(e, this.sql);
|
|
548
565
|
return this.runtime.runPromise(n);
|
|
549
566
|
}
|
|
550
567
|
async runWithEager(e, t) {
|
|
551
|
-
if (!
|
|
552
|
-
let r = this.variant === "mariadb" ? "mariadb" : "mysql", i = this.namespace === 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 ??
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
784
|
-
let
|
|
785
|
-
return this.sql.withTransaction(n.flatMap(n.serviceOption(d), (
|
|
786
|
-
if (a.isNone(
|
|
787
|
-
let
|
|
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(
|
|
806
|
+
try: () => t(() => e(o)).then((e) => ({
|
|
790
807
|
result: e,
|
|
791
|
-
view:
|
|
792
|
-
attempt:
|
|
808
|
+
view: o,
|
|
809
|
+
attempt: i
|
|
793
810
|
})),
|
|
794
811
|
catch: (e) => e
|
|
795
812
|
});
|
|
796
813
|
}));
|
|
797
|
-
}),
|
|
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(
|
|
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) ||
|
|
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
|
-
},
|
|
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,
|
|
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
|
-
}),
|
|
955
|
+
}), se = {
|
|
936
956
|
id: "mysql",
|
|
937
|
-
makeSqlLayer: (e) =>
|
|
938
|
-
makeStore: (e) =>
|
|
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:
|
|
944
|
-
},
|
|
963
|
+
retryFilter: q
|
|
964
|
+
}, ce = {
|
|
945
965
|
id: "mariadb",
|
|
946
|
-
makeSqlLayer: (e) =>
|
|
947
|
-
makeStore: (e) =>
|
|
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:
|
|
972
|
+
retryFilter: q
|
|
953
973
|
};
|
|
954
974
|
//#endregion
|
|
955
|
-
export { f as CDC_OFFSETS_TABLE, e as MysqlClient, m as _voltroCdcOffsetsTable,
|
|
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.
|
|
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.
|
|
38
|
-
"@voltro/logger": "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"
|