@voltro/sql-mysql 0.10.0 → 0.11.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 +84 -0
- package/dist/index.js +50 -49
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,90 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.11.0] — 2026-07-22
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/client, @voltro/web** — **@voltro/client** — `useSequence` steps gain **`covers`** and **`when`**, and the undo contract that was implicit is now written down.
|
|
47
|
+
|
|
48
|
+
**`covers` — overlapping undos.** Every succeeded step's undo runs, and the runner has no idea whether two of them reverse the same thing. Reported from a Jira rollback: `deleteJiraDraftTicket` deletes the issue *and* discards the draft, so the earlier `discardDraft` undo ran on something already gone. It worked only because discarding is idempotent — and **nothing said that was load-bearing**. For a refund or a cancellation email the double-run is a defect, not a nuisance.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
.step('draft', createDraft, { undo: discardDraft })
|
|
52
|
+
.step('jira', createTicket, { undo: deleteJiraDraftTicket, covers: ['draft'] })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`covers` is static rather than a runtime signal on purpose: "this reverse also reverses that one" is a property of the operation, legible where it is defined and checkable against the step names in scope.
|
|
56
|
+
|
|
57
|
+
**When the covering undo FAILS**, the covered steps are neither run nor claimed. Whether the cascade got that far is genuinely unknown — running the covered undo risks the double reverse, skipping it risks an orphan — so both guesses are refused and the steps come back in `compensationUncertain` with the step that was supposed to cover them. Same principle as never compensating the step that failed: surface the ambiguity, don't resolve it by assumption.
|
|
58
|
+
|
|
59
|
+
**`when` — one optional step.** 13 multi-await blocks in one app; only 5 could migrate. Several of the rest were linear *except* for one conditional step ("schedule the summary only if the set changed", `if (assigneeKey) assign else unassign`) and fell back to `try/catch` entirely, though 80% of the flow was a clean pipeline. An optional step is a different shape from a loop, and the scoping was treating them the same.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
.step('summary', (c) => scheduleSummary.run({ id: c.save.id }), { when: (c) => c.save.changed })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A skipped step contributes `undefined` to the context — the overload says so, so a later step has to acknowledge it — and gets no undo, since it had no effect to reverse. Loops and real branches still keep their `try/catch`; this deliberately does not widen to them.
|
|
66
|
+
|
|
67
|
+
**The break:** `StepOptions` gained a type parameter — `StepOptions<Result>` became `StepOptions<Ctx, Result>`, because `covers` and `when` both need to see the accumulated context (`covers` is checked against the step names in scope; `when` receives it). Call sites that pass an object literal to `.step()` are unaffected — the type is inferred — but anyone who *named* the type explicitly gets a compile error. Filed BREAKING rather than Added for the same reason a widened union is: the test is "can this turn code that compiled into code that does not", and it can. `@voltro/web` is listed because it re-exports the client surface — the third time in this round that coupling has decided where a change lands.
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
- **@voltro/cli** — **Repo gate** — a new `Claimed-wiring check` (`scripts/check-claimed-wirings.mjs`, wired into CI and therefore into `pnpm gate`): a doc comment that says something wires a symbol up must be telling the truth.
|
|
72
|
+
|
|
73
|
+
`setSystemStoreHandle`'s comment read *"Process-wide handle, registered by the runtime boot (dev.ts / start.ts)"*. Nothing registered it, in either path, through an entire release — so `runAsSystem` threw for every consumer, and the comment was the only evidence anyone had that it should work. The shape is not rare: a comment gets written when the wiring is planned, the wiring gets deferred, and the comment never finds out. It then reads as documentation of behaviour rather than of intention, and the more confidently it is phrased the less likely anyone is to check it.
|
|
74
|
+
|
|
75
|
+
Two things it does that the obvious version does not, both learned by watching it report the bug as clean:
|
|
76
|
+
|
|
77
|
+
- **it counts real call expressions, not text.** The first version matched regexes and found `setSystemStoreHandle({ … })` inside `runAsSystem`'s own error-message string — a text match cannot tell a call from a sentence about a call. (Precisely the defect fixed in the hand-roll detector one commit earlier, repeated one file later.) - **it checks the NAMED caller, not any caller.** The second version asked "does anything call this"; `@voltro/testing` calls it from a test harness, so the bug read clean again. A claim that the runtime boot registers something is not satisfied by a test helper registering it.
|
|
78
|
+
|
|
79
|
+
Verified the only way this kind of check can be: by removing the wiring and confirming it fails, with the diagnosis that would have saved the original investigation — `called by: packages/testing/src/testContext.ts ← none of these is the boot`.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **@voltro/database, @voltro/runtime, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — **@voltro/database + every store** — a row inserted into a table that declares an `id()` scheme now gets one **at the store**, not only when the caller happened to go through `wrapStoreWithMixinBehaviour`.
|
|
84
|
+
|
|
85
|
+
Id generation was sitting one layer too high. It is the single stamped field that needs no subject — the scheme is a property of the declared table — yet it lived in the subject-aware wrapper. So every insert through an unwrapped store reached the database with `id: null`:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
null value in column "id" of relation "_voltro_seeds" violates not-null constraint
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
That was a real shipped bug in the seed ledger (the runner holds the raw store), and a survey found the same shape waiting elsewhere: `plugin-rbac/userRoleStore.ts`, `plugin-governance/consent.ts`, and `plugin-sso-saml/saml-cache.ts` all insert without an id into tables that declare one. Whether any of them broke came down to how their caller happened to obtain its store — and *"depends on how the caller obtained its store"* is not a contract, it is a coin flip with a NOT NULL constraint on the other side.
|
|
92
|
+
|
|
93
|
+
`stampGeneratedId` now runs in every store's insert path (all four dialects plus the in-memory store, at the private `executeInsert` / `executeInsertMany` / `executeInsertIgnore` choke points that every public and namespace-view path funnels through). It preserves the semantics the wrapper had: an explicit id is never overwritten, a `numeric` scheme deletes the key so the dialect's SERIAL fires, and an unregistered table is passed through untouched rather than guessed at.
|
|
94
|
+
|
|
95
|
+
The wrapper still stamps — it holds the schema registry and does the subject-derived fields in the same pass — and now finds the id already set. That is a floor, not a second implementation of a rule: the invariant is "a row that reaches the database has an id when its table declares a generating scheme", and only the store can promise that for *every* caller, including a `DataStore` someone implemented themselves.
|
|
96
|
+
- **@voltro/cli** — **Repo tests** — the liveness backstop for real-listener / real-child-process suites goes from 60s to 120s, in `vitest.config.ts` and the CI flag that overrides it.
|
|
97
|
+
|
|
98
|
+
Worth stating plainly what this number is, because raising a timeout is the classic way to bury a problem: it asserts nothing about performance. A real `serveApi` boot is ~0.2s in isolation. The value is pure headroom against residual starvation that the four existing mitigations — the unit/integration project split, group ordering, `fileParallelism: false`, per-package mssql databases — cannot reach, because the remaining contention is turbo running two *packages* concurrently alongside the docker stack.
|
|
99
|
+
|
|
100
|
+
60s tripped twice in one session, on two different files (`connectionServe.test.ts`, then `serveApi.test.ts`), each passing in ~2s alone. Different file each time, always in the same family, never reproducible in isolation: that is the signature of starvation rather than of a slow test, and it cost two full gate runs.
|
|
101
|
+
|
|
102
|
+
What would not be honest is treating a green run afterwards as evidence the contention is gone. It is not. If a third file trips this, the answer is to stop running two heavy packages concurrently — not to raise it again.
|
|
103
|
+
- **@voltro/cli, @voltro/database** — **@voltro/cli, @voltro/database** — two fixes to the 0.10.0 seed ledger, both reported from its first real use, both mine.
|
|
104
|
+
|
|
105
|
+
**The ledger never wrote.** `store.insert('_voltro_seeds', …)` ran against the RAW store, and auto-id lives in `wrapStoreWithMixinBehaviour` — so the row reached postgres with `id: null` and died on the NOT NULL constraint. Every boot reported `ran=1 skipped=0` regardless of fingerprint: the feature shipped doing nothing. The ledger now stamps its own id, derived from `_voltroSeedsTable`'s declared scheme rather than a hardcoded prefix, so it no longer depends on how a caller happens to have wrapped its store.
|
|
106
|
+
|
|
107
|
+
**Worse than the bug was the logging.** I put the failure on `debug` and swallowed it, reasoning that "it degrades to re-running, which is only a performance regression". That reasoning is exactly what made it undiagnosable: a silently unwritten ledger looks identical to a working one whose seeds all changed — no symptom, nothing to grep, and the reporter had to read the error out of a debug stream to find it. Both the write and the read path now warn, naming the table and the consequence.
|
|
108
|
+
|
|
109
|
+
**And the reason it shipped:** the test fake *invented an id* when the row lacked one, making it more permissive than any database. A fake that supplies what the subject under test forgot is not a test — it is the subject testing itself. It now rejects an id-less insert with the real constraint's message; reintroducing the bug fails five cases.
|
|
110
|
+
|
|
111
|
+
**A seed step could not reach a usable store.** `SeedStore` exposed only `query`/`insert`/`update`/`delete`, and `query` took just `{ table, predicate }`. A restore of 1361 rows across 167 tables, multi-pass for FK order, needs an idempotent insert and a read it can page — and `upsertByUnique` is neither: a read per row, and it overwrites what it finds, which is wrong whenever the live row is newer than the snapshot. `SeedStore` now carries **`insertIgnore`** (one statement per row, native on every dialect) and a full descriptor read (`order` / `take` / `skip` / `projection`).
|
|
112
|
+
|
|
113
|
+
Seed reads are unscoped and include soft-deleted rows *by construction* — a seed runs at boot with no request and therefore no subject, so nothing applies a tenant filter or the `deletedAt IS NULL` predicate. That is now documented on the type rather than left to be discovered, since the absence of `.unscoped()` / `.withDeleted()` reads as a missing feature until you know why they cannot exist here.
|
|
114
|
+
|
|
115
|
+
*(`apiSurface: compatible`: the golden churn in `@voltro/database` is two `(undocumented)` markers disappearing because `SeedStore` and its new member gained TSDoc — a comment cannot break a caller. The added `insertIgnore` member is additive for consumers, which is everyone: a `SeedStore` is what `ctx.store` IS, handed to you by the runner. Nobody constructs one, so nobody can be missing a member.)*
|
|
116
|
+
- **@voltro/cli** — **@voltro/cli** — `runAsSystem` now works. The process-wide system store is registered at boot by **both** `voltro dev` and `voltro serve`; until now neither did, so every call threw `no data store available — register one at boot via setSystemStoreHandle`.
|
|
117
|
+
|
|
118
|
+
`setSystemStoreHandle`'s own doc comment reads "registered by the runtime boot (dev.ts / start.ts)". It describes wiring that was never written: the only callers in the repo were its unit test and a note in `@voltro/testing`. So the failure was not a lifecycle-ordering subtlety — the handle was never set at any point, in any command, and `runAsSystem` was unusable for every consumer.
|
|
119
|
+
|
|
120
|
+
Surfaced by someone reporting it as "not registered *yet* at seed time", which implied it worked later. Checking that framing rather than the symptom is what turned a scheduling question into a missing-wiring one. It is registered before the seed runner in dev, since seeds are the earliest thing that can plausibly want it.
|
|
121
|
+
|
|
122
|
+
Same class as the seed lifecycle table and the `@voltro/web` re-export: a documented behaviour with nothing behind it, where the doc is the only evidence anyone has.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
42
126
|
## [0.10.0] — 2026-07-22
|
|
43
127
|
|
|
44
128
|
### ⚠ BREAKING
|
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, compileEagerJson as g, compilePredicate as _, compileRawFragment as v, compileSelect as y, decodeRowsFromSchema as b, encodeRowForSchema as x, hasEagerLoads as S, qualifyTable as C, raiseChangeListenerCeiling as w, requireTable as T } from "@voltro/database";
|
|
6
|
+
import { CDC_OFFSETS_TABLE as f, EagerCardinalityError as p, _voltroCdcOffsetsTable as m, attachEagerLoads as h, compileEagerJson as g, compilePredicate as _, compileRawFragment as v, compileSelect as y, decodeRowsFromSchema as b, encodeRowForSchema as x, hasEagerLoads as S, qualifyTable as C, raiseChangeListenerCeiling as w, requireTable as T, stampGeneratedId as E, stampGeneratedIds as D } from "@voltro/database";
|
|
7
7
|
//#region src/sqlLayer.ts
|
|
8
|
-
var
|
|
8
|
+
var O = (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
|
+
}), k = (e) => {
|
|
16
16
|
if (e.url) {
|
|
17
17
|
let t = new URL(e.url);
|
|
18
18
|
return {
|
|
@@ -32,11 +32,11 @@ var E = (n) => e.layerConfig({
|
|
|
32
32
|
database: e.database ?? "app",
|
|
33
33
|
...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections }
|
|
34
34
|
};
|
|
35
|
-
},
|
|
35
|
+
}, A = (e) => O(k(e)), j = (e, t) => e === null ? !1 : t === null ? !0 : e.filename === t.filename ? e.position > t.position : e.filename > t.filename, M = (e) => e.msSinceProgress < e.stallThresholdMs ? "healthy" : e.primary !== null && !j(e.primary, e.reader) ? "idle-caught-up" : "reconnect", N = (e) => (e instanceof Error ? e.message : String(e ?? "")).includes("schema changed between binlog event and metadata fetch"), P = /* @__PURE__ */ new Set([
|
|
36
36
|
"writerows",
|
|
37
37
|
"updaterows",
|
|
38
38
|
"deleterows"
|
|
39
|
-
]),
|
|
39
|
+
]), F = /\b(alter|rename|drop|create)\s+(table|column)?/i, I = (e) => new Promise((t) => setTimeout(t, e)), L = async (e) => {
|
|
40
40
|
let t = l({ scope: `voltro:${e.variant}:cdc` }), n;
|
|
41
41
|
try {
|
|
42
42
|
n = (await import("@vlasky/zongji")).default;
|
|
@@ -52,14 +52,14 @@ var E = (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 && F.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)), !P.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;
|
|
@@ -137,8 +137,8 @@ var E = (n) => e.layerConfig({
|
|
|
137
137
|
try {
|
|
138
138
|
c?.stop();
|
|
139
139
|
} catch {}
|
|
140
|
-
if (d++, await
|
|
141
|
-
let r = o, i = x(n), s = !i &&
|
|
140
|
+
if (d++, await I(Math.min(3e4, 500 * 2 ** Math.min(d, 6))), u) return;
|
|
141
|
+
let r = o, i = x(n), s = !i && N(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
144
|
await S(r), d = 0, _();
|
|
@@ -161,14 +161,14 @@ var E = (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 I(500), await S(n);
|
|
165
165
|
} else throw n;
|
|
166
166
|
}
|
|
167
167
|
let w = 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 = M({
|
|
172
172
|
msSinceProgress: Date.now() - f,
|
|
173
173
|
stallThresholdMs: h,
|
|
174
174
|
primary: n,
|
|
@@ -197,7 +197,7 @@ var E = (n) => e.layerConfig({
|
|
|
197
197
|
},
|
|
198
198
|
currentPosition: () => o
|
|
199
199
|
};
|
|
200
|
-
},
|
|
200
|
+
}, R = /* @__PURE__ */ new Set(["1213", "1205"]), z = (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 E = (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
|
+
}, B = (e) => {
|
|
210
|
+
let t = z(e);
|
|
211
|
+
return t !== void 0 && R.has(t);
|
|
212
|
+
}, V = (e) => B(e) ? "retry" : "noRetry", H = (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(H).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)}:${H(n[e])}`).join(",")}}`;
|
|
221
|
+
}, U = (e) => {
|
|
222
|
+
let t = H(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
|
+
}, W = (e, t) => {
|
|
226
226
|
let n = setTimeout(e, t);
|
|
227
227
|
typeof n.unref == "function" && n.unref();
|
|
228
|
-
},
|
|
228
|
+
}, G = 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 = W) {
|
|
234
234
|
this.variant = e, this.ttlMs = t, this.schedule = n;
|
|
235
235
|
}
|
|
236
236
|
key(e) {
|
|
@@ -244,7 +244,7 @@ var E = (n) => e.layerConfig({
|
|
|
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)} ${U(r)}`;
|
|
248
248
|
}
|
|
249
249
|
admit(e) {
|
|
250
250
|
let t = this.key(e);
|
|
@@ -259,12 +259,12 @@ var E = (n) => e.layerConfig({
|
|
|
259
259
|
get pending() {
|
|
260
260
|
return this.seen.size;
|
|
261
261
|
}
|
|
262
|
-
},
|
|
262
|
+
}, K = 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 q(await c.runPromise(u), c, t, o);
|
|
266
266
|
return o === "cdc" && e.cdcConfig && await d.startCdcConsumer(e.cdcConfig), d;
|
|
267
|
-
},
|
|
267
|
+
}, q = class e {
|
|
268
268
|
sql;
|
|
269
269
|
runtime;
|
|
270
270
|
variant;
|
|
@@ -280,7 +280,7 @@ var E = (n) => e.layerConfig({
|
|
|
280
280
|
cdcStreamName = "default";
|
|
281
281
|
cdcGate;
|
|
282
282
|
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(), w(this.emitter), this.cdcGate = o ?? new
|
|
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(), w(this.emitter), this.cdcGate = o ?? new G(n);
|
|
284
284
|
}
|
|
285
285
|
withNamespace(t) {
|
|
286
286
|
return t === this.namespace ? this : new e(this.sql, this.runtime, this.variant, this.changeStrategy, t, this.emitter, this.cdcGate);
|
|
@@ -311,6 +311,7 @@ var E = (n) => e.layerConfig({
|
|
|
311
311
|
return !1;
|
|
312
312
|
}
|
|
313
313
|
async executeInsert(e, t, r, i) {
|
|
314
|
+
t = E(e, t);
|
|
314
315
|
let a = this.sql;
|
|
315
316
|
if (this.supportsInsertReturning) {
|
|
316
317
|
let o = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(x(t, e))} RETURNING *`, s = r ? n.provideService(o, d, r) : o, c = (await this.runtime.runPromise(s))[0];
|
|
@@ -356,7 +357,7 @@ var E = (n) => e.layerConfig({
|
|
|
356
357
|
if (e) return this.runtime.runPromise(t(e));
|
|
357
358
|
this.inflightTxns++;
|
|
358
359
|
try {
|
|
359
|
-
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(
|
|
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(B)), o = e.pipe(n.retry(i), n.withSpan("store.insert", { attributes: {
|
|
360
361
|
"db.system": this.variant,
|
|
361
362
|
"db.operation": r
|
|
362
363
|
} }));
|
|
@@ -366,7 +367,7 @@ var E = (n) => e.layerConfig({
|
|
|
366
367
|
}
|
|
367
368
|
}
|
|
368
369
|
async executeInsertMany(e, t, r, i) {
|
|
369
|
-
if (t.length === 0) return [];
|
|
370
|
+
if (t = D(e, t), t.length === 0) return [];
|
|
370
371
|
let a = this.sql, o = t.map((t) => x(t, e));
|
|
371
372
|
if (this.supportsInsertReturning) {
|
|
372
373
|
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);
|
|
@@ -506,7 +507,7 @@ var E = (n) => e.layerConfig({
|
|
|
506
507
|
}, a), m;
|
|
507
508
|
}
|
|
508
509
|
async executeInsertIgnore(e, t, r, i, a) {
|
|
509
|
-
if (this.variant === "mariadb") {
|
|
510
|
+
if (t = E(e, t), this.variant === "mariadb") {
|
|
510
511
|
let o = this.sql, s = o`INSERT IGNORE INTO ${o(this.nsT(e))} ${o.insert(x(t, e))} RETURNING *`, c = i ? n.provideService(s, d, i) : s, l = (await this.runtime.runPromise(c))[0];
|
|
511
512
|
if (l) return this.routeEvent({
|
|
512
513
|
table: e,
|
|
@@ -590,7 +591,7 @@ var E = (n) => e.layerConfig({
|
|
|
590
591
|
let r = t.map((e) => e.id), a = i`SELECT * FROM ${i(this.nsT(e))} WHERE ${i("id")} IN ${i.in(r)}`;
|
|
591
592
|
return n.flatMap(n.provideService(l, d, s), () => n.provideService(a, d, s));
|
|
592
593
|
});
|
|
593
|
-
}))), c = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(
|
|
594
|
+
}))), c = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(B)), l = r.pipe(n.retry(c), n.withSpan("store.updateMany", { attributes: {
|
|
594
595
|
"db.system": this.variant,
|
|
595
596
|
"db.operation": "update"
|
|
596
597
|
} })), u = await this.runtime.runPromise(l);
|
|
@@ -625,7 +626,7 @@ var E = (n) => e.layerConfig({
|
|
|
625
626
|
if (a.isNone(t)) return n.fail(/* @__PURE__ */ Error("MysqlStore.deleteMany: TransactionConnection missing."));
|
|
626
627
|
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}`;
|
|
627
628
|
return n.flatMap(n.provideService(s, d, o), (e) => e.length === 0 ? n.succeed(e) : n.as(n.provideService(c, d, o), e));
|
|
628
|
-
}))), o = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(
|
|
629
|
+
}))), o = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(B)), c = t.pipe(n.retry(o), n.withSpan("store.deleteMany", { attributes: {
|
|
629
630
|
"db.system": this.variant,
|
|
630
631
|
"db.operation": "delete"
|
|
631
632
|
} })), l = await this.runtime.runPromise(c);
|
|
@@ -649,7 +650,7 @@ var E = (n) => e.layerConfig({
|
|
|
649
650
|
if (this.cdcHandle) return;
|
|
650
651
|
await this.assertBinlogConfig(), this.cdcReplicaId = e.replicaId;
|
|
651
652
|
let t = await this.readCdcOffset(e.replicaId) ?? await this.resolveBinlogEnd();
|
|
652
|
-
this.cdcHandle = await
|
|
653
|
+
this.cdcHandle = await L({
|
|
653
654
|
connection: e.connection,
|
|
654
655
|
serverId: e.serverId,
|
|
655
656
|
variant: this.variant,
|
|
@@ -769,7 +770,7 @@ var E = (n) => e.layerConfig({
|
|
|
769
770
|
let r = ++t;
|
|
770
771
|
return this.sql.withTransaction(n.flatMap(n.serviceOption(d), (t) => {
|
|
771
772
|
if (a.isNone(t)) return n.fail(/* @__PURE__ */ Error("MysqlStore.transactional: TransactionConnection missing."));
|
|
772
|
-
let i = new
|
|
773
|
+
let i = new J(this, t.value);
|
|
773
774
|
return n.tryPromise({
|
|
774
775
|
try: () => e(i).then((e) => ({
|
|
775
776
|
result: e,
|
|
@@ -779,7 +780,7 @@ var E = (n) => e.layerConfig({
|
|
|
779
780
|
catch: (e) => e
|
|
780
781
|
});
|
|
781
782
|
}));
|
|
782
|
-
}), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(
|
|
783
|
+
}), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(B)), o = r.pipe(n.retry(i), n.withSpan("store.transactional", { attributes: {
|
|
783
784
|
"db.system": this.variant,
|
|
784
785
|
"db.operation": "transaction"
|
|
785
786
|
} }));
|
|
@@ -825,7 +826,7 @@ var E = (n) => e.layerConfig({
|
|
|
825
826
|
async ping() {
|
|
826
827
|
await this.runtime.runPromise(this.sql`SELECT 1`);
|
|
827
828
|
}
|
|
828
|
-
},
|
|
829
|
+
}, J = class {
|
|
829
830
|
parent;
|
|
830
831
|
txn;
|
|
831
832
|
events = [];
|
|
@@ -897,9 +898,9 @@ var E = (n) => e.layerConfig({
|
|
|
897
898
|
this.events.length = 0;
|
|
898
899
|
}
|
|
899
900
|
}
|
|
900
|
-
},
|
|
901
|
+
}, Y = (e) => e.__mysqlReplicationFriend ?? null, X = () => ({
|
|
901
902
|
async capturePrimaryPosition(e) {
|
|
902
|
-
let t =
|
|
903
|
+
let t = Y(e);
|
|
903
904
|
if (t === null) throw Error("mysqlReplicationAdapter: primary is not a MysqlStore (missing __mysqlReplicationFriend).");
|
|
904
905
|
return t.runEffect(n.gen(function* () {
|
|
905
906
|
let e = yield* u, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
|
|
@@ -907,7 +908,7 @@ var E = (n) => e.layerConfig({
|
|
|
907
908
|
}));
|
|
908
909
|
},
|
|
909
910
|
async probeReplicaPosition(e) {
|
|
910
|
-
let t =
|
|
911
|
+
let t = Y(e);
|
|
911
912
|
if (t === null) throw Error("mysqlReplicationAdapter: replica is not a MysqlStore.");
|
|
912
913
|
return t.runEffect(n.gen(function* () {
|
|
913
914
|
let e = yield* u, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
|
|
@@ -917,24 +918,24 @@ var E = (n) => e.layerConfig({
|
|
|
917
918
|
compare(e, t) {
|
|
918
919
|
return "behind";
|
|
919
920
|
}
|
|
920
|
-
}),
|
|
921
|
+
}), Z = {
|
|
921
922
|
id: "mysql",
|
|
922
|
-
makeSqlLayer: (e) =>
|
|
923
|
-
makeStore: (e) =>
|
|
923
|
+
makeSqlLayer: (e) => A(e),
|
|
924
|
+
makeStore: (e) => K({
|
|
924
925
|
...e,
|
|
925
926
|
variant: "mysql"
|
|
926
927
|
}),
|
|
927
928
|
compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
|
|
928
|
-
retryFilter:
|
|
929
|
-
},
|
|
929
|
+
retryFilter: V
|
|
930
|
+
}, Q = {
|
|
930
931
|
id: "mariadb",
|
|
931
|
-
makeSqlLayer: (e) =>
|
|
932
|
-
makeStore: (e) =>
|
|
932
|
+
makeSqlLayer: (e) => A(e),
|
|
933
|
+
makeStore: (e) => K({
|
|
933
934
|
...e,
|
|
934
935
|
variant: "mariadb"
|
|
935
936
|
}),
|
|
936
937
|
compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
|
|
937
|
-
retryFilter:
|
|
938
|
+
retryFilter: V
|
|
938
939
|
};
|
|
939
940
|
//#endregion
|
|
940
|
-
export { f as CDC_OFFSETS_TABLE, e as MysqlClient, m as _voltroCdcOffsetsTable,
|
|
941
|
+
export { f as CDC_OFFSETS_TABLE, e as MysqlClient, m as _voltroCdcOffsetsTable, k as connectionFromConfig, O as makeMysqlSqlLayer, A as makeMysqlSqlLayerFromConfig, K as makeMysqlStore, Q as mariadbDialect, Z as mysqlDialect, X as mysqlReplicationAdapter, V as mysqlRetryFilter, L as startBinlogCdc };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/sql-mysql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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.11.0",
|
|
38
|
+
"@voltro/logger": "0.11.0"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
41
|
"@vlasky/zongji": "^0.9.0"
|