@voltro/sql-mysql 0.18.0 → 0.19.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,159 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.19.0] — 2026-07-29
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/plugin-audit** — **The durable audit sink stops writing credentials to a log table by default.** `redactInput` defaults to `'all'` — the payload becomes `{ __redacted: 'all' }`, which still proves a payload existed. `redactInput: 'none'` restores the previous behaviour, and a function gives field-level control.
47
+
48
+ `AuditEvent.input` is the raw mutation input, so an unredacted trail is where a password change, an API key at issuance and a PAT land — the one place nobody thinks to look for a credential. Losing payload detail is visible the first time you read a row; leaking a credential is not visible at all, which is why the default moved rather than staying opt-in.
49
+
50
+ **It is deliberately NOT driven by `.serverOnly()` / `.sensitive()`,** which is the design a consumer proposed and the one that cannot work: those markers live on TABLE COLUMNS and this is a mutation's INPUT. `changePassword({ oldPassword, newPassword })` has no column to consult, so a marker-driven default would cover exactly 0% of the motivating case while reading, to whoever configured it, like protection. (`.sensitive()` is also the export axis, not "unsafe to log" — the category error the three-marker table exists to prevent.)
51
+
52
+ Also:
53
+
54
+ - **`record: 'all' | 'errors' | predicate`** — filters by OUTCOME, where `include`/`exclude` filter by tag. `'errors'` is the forensic core and pairs with `plugin-versioning` for the successful writes. **`'all'` stays the default** on purpose: defaulting to errors would silently stop recording successes on upgrade, and "what did this compromised account touch" is answered by successes. - **`errorTag`** — the typed error's `_tag`, flattened out of the `outcome` json and indexable. `null` for an untagged failure rather than a guess: "this had no tag" and "the tag is 'Error'" are different, and a column that invents the second makes every filter on it quietly wrong. - **Two indices for the questions asked under pressure** — `(subjectId, status, at)` and `(tenantId, status, at)`. "Every denied call by subject X in the last 30 days" and "every failure against tenant Y" were both unindexed; the existing `byAuditTag` / `byAuditTrace` cannot serve either. - **Retention registers itself** — 365 days, `VOLTRO_AUDIT_LOG_TTL_HOURS`, drained by the boot sweep. "Pair it with the governance sweep" was a docs sentence rather than a default, so nobody did. - **Erasure is deliberately NOT auto-registered.** Erasing a subject must not delete the record that they were refused four hundred times — that record *is* the evidence. Anonymise instead; the docs carry the `subjectScopes` entry to paste, and it stays a decision the app makes explicitly.
55
+
56
+ **Migration** — `voltro update` prints it (`0.19.0/02_audit-redact-input-default`, `manual`, and it fires only for apps that mount the plugin). Nothing stops compiling and existing rows are untouched; what changes is what the NEXT row records. Keep the new default unless you know your mutation inputs carry no secrets; pass a function for field-level control; or opt back in explicitly with `redactInput: 'none'`. The codemod deliberately does NOT write `'none'` into your config — a transform could do it perfectly, which is exactly why it must not: it would pin every adopter to the behaviour the default moved away from and report the migration as complete.
57
+
58
+ *Why this is `BREAKING` and not `Changed`: it is a silent behaviour change on upgrade. Nothing fails, which is the problem — an operator who never reads this section keeps a trail that has quietly lost its payload detail. `BREAKING` is what routes it into `voltro update`.*
59
+ - **@voltro/cli, @voltro/database** — **`.serverOnly()` now gates where it said it did.** A wire-reachable query that declares a `.serverOnly()` column of its source table in its `output` fails the boot under `voltro serve`, and makes `voltro doctor` exit non-zero. `voltro dev` still warns.
60
+
61
+ It shipped as one `log.warn` and nothing else — in every command — while `ColumnBuilder.serverOnly()`'s own doc comment and the seeded `AGENTS.md` marker table both said "**the boot audit — it FAILS the boot**, it does not warn". A team read the strong sentence, adopted the marker on four credential columns, injected a deliberate leak to check, and watched the server come up serving the leaking query. That is the register this repo keeps meeting from a new angle: *a check that prints instead of gating still reads as coverage* — here on the one marker whose entire job is the enforcement.
62
+
63
+ Two smaller things went with it, both of which had misled the reporter:
64
+
65
+ - **The message names the command.** It was emitted through a module-level logger scoped `voltro:dev`, so a warning from `voltro serve` announced itself as dev — which is why they concluded, and reported, that the audit does not run in production at all. It did; it just misattributed itself and stopped nothing. - **The audit is computed once, in `loadDiscovered`**, the discovery dev / serve / doctor / check all share — the same reasoning `validateRegisteredRelations()` lives there for. Dev and serve may disagree about what a leak COSTS; they must not disagree about what a leak IS.
66
+
67
+ `VOLTRO_SERVER_ONLY` moves the line both ways: `strict` fails `voltro dev` too, `warn` downgrades serve, `off` silences it. The downgrades are documented rather than hidden, because the alternative to a stated escape hatch is deleting the marker, and a check whose only way out is to disable it gets disabled.
68
+
69
+ `voltro check` deliberately does NOT run it: it has a live-api mode with no access to your table definitions, and a rule that fires in one of its two modes is worse than one that fires in neither.
70
+
71
+ **Migration** — `voltro update` prints it (`0.19.0/01_server-only-gates-the-boot`, `manual`, and it fires only for apps that actually use the marker). Run `voltro doctor` BEFORE you deploy: it reports exactly what `voltro serve` will now refuse, with no deploy involved. Each finding has two honest fixes and only its author can choose — the column is not wire-safe (drop it from the query's `output`, keep the marker), or the marker is wrong (drop the `.serverOnly()`). Do not substitute `.encrypted()`: that is the at-rest axis, the runtime decrypts for the handler, and reading it as "safe to expose" is the category error the three-marker table exists to prevent. To ship while triaging, `VOLTRO_SERVER_ONLY=warn` downgrades serve back to a warning — a bridge, not a setting to keep.
72
+
73
+ *Why this is `BREAKING` and not `Changed`: no signature moves and nothing that compiled stops compiling, so the literal type-level test does not catch it. It can still turn a booting production app into one that refuses — which is the point, the boot it refuses is the one shipping the column — and that failure lands at DEPLOY time. Filing it as `Changed` would have kept it out of the one section the stability contract names as the migration path, and out of `voltro update` entirely, because codemods hang off `BREAKING`. Both doc claims were corrected in the same change, so the `.d.ts`, the agent template and the docs site now describe the same behaviour.*
74
+
75
+ ### Added
76
+
77
+ - **@voltro/plugin-audit** — **`auditByTrace` / `auditBySubject` — the read side of the correlation join.**
78
+
79
+ `byAuditTrace` and `byAuditSubjectStatus` shipped in the same release with **no caller**. That is the identical defect the versioning side had and that its own entry points were added to fix, repeated on the other half of the join one file away: an index nobody can enter is a query the app still hand-writes, and the docs then demonstrate a raw select over a framework-internal table.
80
+
81
+ It surfaced by checking an adopting team's design document against the code rather than from memory. Their §6 asks for *"a read-side composition joining `_voltro_row_history` × the audit sink (on `traceId`) × `actors`"* — which needs BOTH halves to have an entry point, or neither is usable.
82
+
83
+ ```ts
84
+ const calls = await auditByTrace(ctx.store, traceId) // who called, and was it refused
85
+ const changed = await historyByTrace(ctx.store, traceId, tenantId) // what it changed
86
+ const denied = await auditBySubject(ctx.store, actorId, { status: 'error' })
87
+ ```
88
+
89
+ `auditBySubject` takes `status` as a real argument rather than leaving the caller to filter in JS — the index is `(subjectId, status, at)`, so a filter applied after fetching would not use it. `limit` defaults to 100, because an actor's history is unbounded and an entry point that returns all of it is one you call once in production.
90
+
91
+ *They were also not exported from the package index when first written — built, tested, and unreachable. Caught before shipping; worth recording because "it has a test" and "a consumer can call it" are different claims.*
92
+ - **@voltro/database, @voltro/runtime, @voltro/protocol, @voltro/cli, @voltro/plugin-versioning, @voltro/plugin-audit, @voltro/voltro** — **`ChangeEvent` carries the calling `traceId` and `subjectId`** — the join key that lets `plugin-versioning` (what changed) and `auditPlugin` (who called, and whether they were refused) be read as one trail.
93
+
94
+ Both halves shipped and neither could be joined to the other. A consumer put it exactly right: *"we are not asking you to build our audit feature. We are asking for the join key that lets anyone build one on the three pieces you have already shipped."*
95
+
96
+ **The mechanism, and the part that was an empirical question rather than a design one.** Identity is known one layer up (`subject` in the middleware, `traceId` at the rpc boundary) and the event is created several layers down, inside each dialect store's private emit. Threading a context argument through every `DataStore` method to reach it would change the port every driver implements, for metadata that is ambient by nature — so it rides an `AsyncLocalStorage` (`@voltro/database`'s `writeAttribution.ts`), the same shape as the existing trace and replica-routing contexts.
97
+
98
+ Whether that survives a SQL store was *not* obvious: the write goes through `ManagedRuntime.runPromise`, so the read happens inside an Effect fiber, and a scheduler draining fibers from a shared loop would run them under the async context of whoever created the drain. If that were true the attribution would be silently ABSENT — a join key that is simply never there, on a trail nobody checks until an incident. Verified against a real sqlite store, including the transactional path (which queues events and flushes them post-commit, *outside* the scope — which is why the stamp goes at event CREATION, not delivery).
99
+
100
+ - **One call site, not one per method.** The scope is established at the rpc executor boundary, so it covers every write the handler makes — `ctx.store`, `EffectStore`, the crud helpers, a plugin interceptor's own writes. Wrapping the store middleware instead would have meant wrapping each mutating method and hoping the next one added remembers. - **`subjectId` is the same identity `audit()` stamps** (`actingUserId`, now exported so there is one source). Two answers to "who wrote this row" on one write would be worse than one; which API *key* was used is recoverable from the audit row sharing the `traceId`. - **Absent is a fact, not a gap** — no request behind the write (seed, startup hook, schedule, workflow step), or an event from another replica, where stamping the local ambient trace would attribute a remote write to a local call. The keys are omitted rather than set to `undefined`, so an unattributed event is byte-identical to one from before this existed. - **`dev.ts` and `serveCommand.ts` no longer hand-mirror the plugin fan-out.** They built that object literal separately in two files; a field added to one and not the other gives a plugin the data in dev and silence in production, and both files typecheck alone. Now `toPluginChangeEvent`.
101
+
102
+ `_voltro_row_history` gains `traceId` / `subjectId` plus `byTrace` and `bySubject` indices — "what did this call touch" and "what did this actor touch" were previously unanswerable at any speed, since `byRow` requires already knowing which row you are asking about. `changedBy` now prefers the caller over the row's `audit()` stamp, which fixes a reported case for free: the stamp is `null` for every write through the boot store, so a login route produced version rows with no actor at all.
103
+
104
+ *`apiSurface: compatible`: every golden line this touches is either a pure addition (the new fields and `actingUserId`) or api-extractor renumbering an import alias — `Subject_2` became `Subject` across `runtime` and the `voltro` re-export because a new import changed the ordering. No declaration changed shape and no call site is affected.*
105
+ - **@voltro/cli** — **`voltro doctor`'s hand-roll detector gains six rules and ranks its findings by file count.**
106
+
107
+ An adopting team audited four apps by hand against the framework's "reach for instead" table and produced twenty items, each with a count (`1631` files with a manual `rows[0]`, `500` queries with `input.offset`, `306` forms, `368` relations declared against `17` uses). **The detector already reported eleven of those twenty, with counts.** These are rules for what it missed — the findings that made a person do work a scan should have done:
108
+
109
+ - **`offset-pagination`** — `.offset(` / `input.offset` / `.skip(` on a list query. `hand-cursor` did not catch this: it wants `hasMore` AND `limit+1`, which is a hand-rolled *keyset* pager. Plain OFFSET is a different smell with a worse ending — O(n) in the page number, so it does not fail, it just stops loading once the table is big, on the tables (audit logs, time entries) that get big first. - **`oauth-token-table`** — an `accessToken`/`refreshToken` column on an app table → `defineConnection({ kind: 'oauth2' })`. The reporting team's version of this leaked: a `json()` column holding a Slack payload with the token inside, readable by every colleague in the tenant. - **`non-incremental-aggregate`** — a `count/sum/avg/min/max` aggregate with no `incremental:`, rescanned in full on every refresh. - **`external-state-library`** — jotai / zustand / mobx / redux → `defineStore`. A second runtime beside the reactive engine, and one that does not participate in the subscription graph. - **`hand-route-module`** — importing a hand-maintained `routes` / `urls` module instead of `.framework/routes.generated`, where a renamed page is a compile error rather than a 404 someone finds in production. - **`hand-permission-check`** — a component reading a permission bag by hand instead of `useCan` / `useResourceCan`.
110
+
111
+ **And the findings are now ordered by file count rather than by the order the rules happen to be declared in.** A reader facing twenty findings acts on the top of the list, so an arbitrary order silently decides what gets fixed. The reporting team ranked their own work by exactly that number and had to count it by hand, because this printed the same facts unranked. Effort we cannot know; magnitude we can.
112
+
113
+ *Two of these rules were caught being wrong by their own tests before shipping: `offset-pagination`'s first version checked only `input.offset` and did not match its own fixture (a narrow rule reports nothing and reads like a clean codebase), and the ranking test passed with the sort deleted until the fixture was rebuilt so declaration order and count order disagree.*
114
+ - **@voltro/plugin-versioning** — **`historyByTrace` / `historyBySubject` — the entry points the new indices existed for.** Plus their Effect twins.
115
+
116
+ The correlation bridge added `byTrace` and `bySubject` to `_voltro_row_history` in the same release, and shipped them with no caller: `rowHistory` requires a `rowId` you already have, which is the wrong way round during an incident, when what you have is a trace or an actor. The docs demonstrated a raw `ctx.store.select('_voltro_row_history')`, which is the shape an index is supposed to save you from writing.
117
+
118
+ Both are tenant-scoped exactly like `rowHistory` (own-tenant rows plus null-tenant rows from untenanted source tables; `undefined` skips the filter, for system paths only). `historyBySubject` takes a `limit`, default 100, because an actor's history is unbounded and an entry point that returns all of it is one you call once in production and never again.
119
+
120
+ **And a bug this surfaced.** The stored-row → `VersionRow` decoder was written inline inside `versionsOf`, before the bridge existed, and silently dropped `traceId` / `subjectId` — so `rowHistory()` returned rows missing the very field the feature exists to carry. Extracted to one `rowToVersion` now shared by all three readers, which is why the drift was possible in the first place.
121
+ - **@voltro/cli, @voltro/database** — **`setRowFilter` is now named in the always-loaded agent core**, with the distinction that makes it findable.
122
+
123
+ A team migrating eleven hand-written row filters reported missing it **twice** — once in a full framework audit, and once while telling a colleague in writing that the framework has no row-filter primitive. The depth doc covers it well; a depth doc is opened by someone who already suspects the topic. The core's SERVER rubric asked *"a permission check?"* and answered with two BOOLEAN questions, so a reader holding a row-VISIBILITY question took the nearest-fitting answer and wrote the filter by hand. Eleven times.
124
+
125
+ The rubric now asks it directly, and the pairing is the point:
126
+
127
+ > `guards:` → *may I call this procedure?* → a typed `ScopeError`. > `setRowFilter` → *which rows may I see?* → the rows are simply absent.
128
+
129
+ Plus the failure it deletes — a `WHERE ownerId = me` in the list handler covers the query and **not** the subscription — and the trap they lost time to: `load` must read through an UNFILTERED store, because applying the filter to its own loader recurses until the stack blows.
130
+
131
+ Also:
132
+
133
+ - **`voltro doctor` gains `unexercised-row-filter`.** Their 7466 tests stayed green when the filter landed because not one passed `rowFilter:` to `makeTestContext`; they only noticed because they sabotage their own predicates before believing a pass, and four sabotages ran green. `makeTestContext({ rowFilter })` being opt-in is right — a process-global in a parallel suite would be worse — but the consequence is a visibility rule nothing verifies. Same class as a suite reporting `passed` with no database. - **`MATCHES_NO_ROWS` is exported from `@voltro/database`.** They wrote `eq('id', '')` for "this caller sees nothing", because the natural spelling `inSet(col, [])` is the one that is dangerous in most query builders: an empty `IN ()` gets dropped, and a dropped predicate does not narrow, it WIDENS to the whole tenant. Here it is safe — the SQL compiler emits `FALSE`, the memory evaluator returns false — but that held by accident, with no test enforcing it. Now pinned in both evaluators, including the mirror case: an empty `notIn` matches EVERYTHING.
134
+
135
+ *The doctor rule was itself wrong when first written: it read the normal source set, which excludes test files, so it could never observe the passing case and would have fired unconditionally on every app. Caught by writing the negative test.*
136
+ - **@voltro/database, @voltro/plugin-versioning, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-mssql, @voltro/sql-sqlite** — **`versioningPlugin({ timing: 'in-transaction' })` — the history row commits or rolls back WITH the change it records.** Default stays `'post-commit'`.
137
+
138
+ Post-commit recording is lossy by construction: between COMMIT and the forked history write there is a window, and a process that dies inside it leaves the change permanent and the trail silent. The size of the window is not the point — the direction is. A missing entry cannot be told apart from "nothing happened", so a trail that can lose entries proves nothing. Retry does not close it either; the process that would retry is the one that died.
139
+
140
+ This was the sole reason an adopting team could not replace their hand-rolled audit writer — called from **315 of 358 mutation handlers**, with the 43 misses being what happens to any rule that depends on someone remembering.
141
+
142
+ **What a recorder receives is an `append`, not a store.** Bound to the caller's transaction, insert-only. It cannot open a nested transaction (which throws by design), cannot read-modify-write its way into a deadlock, and "append-only" stops being a docs claim and becomes the shape of the only thing it is handed.
143
+
144
+ **The rejection is the guarantee, not a defect.** When the history insert fails, a transaction offers exactly two outcomes: the mutation fails with it, or the error is swallowed and the change commits without its entry — which is post-commit's hole with the cost already paid. There is no third option, so recorders do not swallow.
145
+
146
+ **Why the default did not move.** In-transaction makes the history table a hard dependency of every covered write path: its availability becomes your write path's availability, and every covered write holds locks longer. Post-commit loses at worst one entry; in-transaction can at worst stop writes to the covered tables. Right trade for a compliance trail, wrong one for the undo / time-travel use this plugin also serves.
147
+
148
+ **It refuses the in-memory store at boot** rather than silently no-op'ing. `memory` is the default dev store; an option that appears to work where it is cheapest to try and stops where it matters is worse than one that says so. Two limits hold in both timings and are documented: `store.raw()` produces no change event and is absent from the trail, and a write made outside a transaction is recorded immediately after rather than atomically.
149
+
150
+ *Two design claims in the plan for this were wrong and are corrected there. "Twelve write sites across three layers, no funnel" was true of `storeMiddleware` and irrelevant — each dialect store funnels every write through one private `routeEvent`, which is where this hooks. "Bulk writes have no per-row post-image" was simply false: `updateMany` already issues `RETURNING *` and emits one event per affected row, so bulk needed no special case at all. Both were found by looking in the middleware instead of the store — twice.*
151
+
152
+ ### Fixed
153
+
154
+ - **@voltro/cli** — **A `voltro serve` that refuses to boot says why, instead of blaming the serve bundle.** A deliberate refusal — a missing `VOLTRO_SESSION_SECRET`, a `.serverOnly()` leak — came out of the launcher as:
155
+
156
+ ```
157
+ [voltro] serve bundle failed to load: VOLTRO_SESSION_SECRET is not set — refusing to serve. …
158
+ [voltro] FATAL: production `voltro serve` requires a precompiled serve bundle at …
159
+ but it is missing or failed to load. Run `voltro build` before serving …
160
+ ```
161
+
162
+ The real reason is on the first line, under a wrong headline, followed by a louder and more confident wrong instruction. An operator whose first deploy forgot the session secret is told to rebuild an artefact that is fine — and rebuilding it produces the identical output, so the loop has no exit.
163
+
164
+ The cause is a catch that has to exist: `bin/voltro.mjs` imports the precompiled serve bundle inside a `try`, because an unusable bundle must degrade to the tsx path rather than kill the boot. It could not tell "this artefact is broken" from "this app decided not to start". Refusals now carry a marker (`bootRefusal.ts`) and the launcher prints them and exits 1.
165
+
166
+ The marker is the error's `name`, a plain string, rather than a class: the serve bundle INLINES the framework, so the thrown Error crosses an instance boundary where `instanceof` does not survive — the same reason the core-table registry is keyed by `Symbol.for`.
167
+
168
+ Found by running the new `.serverOnly()` gate against a real fixture rather than by reading it, which is also how the misleading pair came into view: the session-secret case had been shipping that way for a while.
169
+ - **@voltro/sql-postgres, @voltro/sql-mysql** — **`insertIgnore` explains a second-unique conflict instead of reporting an internal invariant.** The message was `row conflicted but lookup found nothing`, which tells a caller nothing they can act on.
170
+
171
+ It is reachable by ordinary means, and on mariadb it is the *common* path: `INSERT IGNORE` swallows ANY unique violation, so a row with a fresh `id` and a duplicate `slug` is skipped, and the lookup by the named `conflictColumns` then finds nothing. A team with `tenants (id PK, slug UNIQUE)` hits it on the first duplicate slug.
172
+
173
+ The message now names the columns that were checked, the table, and the actual cause — a different unique constraint fired, and `insertIgnore` models one conflict target. This is step 1 of `plans/framework-insertignore-any-unique.md` and is deliberately independent of the feature: whether or not `conflictColumns: 'any'` ever ships, this error should have been readable.
174
+ - **@voltro/cli** — **`ui/unlinked` and `ui/orphaned` resolve through barrel re-exports.** A `*.component.ui.tsx` reached only via `export { X } from './x'` was reported as unrendered, however many pages actually rendered it.
175
+
176
+ On the app that reported it, `PageContent` is imported by 42 pages — every one of them through `@/components/shared` — and doctor said `imported only by: index.ts, its own test`. It was the last false positive standing after the alias fix took that app from 16 findings to 2.
177
+
178
+ The rules ask "does anything RENDER this". A barrel is a real importer and renders nothing, so stopping at the first importer answers a different question than the one asked — but only in an app that has an `index.ts`, which is why it survived.
179
+
180
+ The walk is narrowed by NAME rather than opened up wholesale: a downstream file counts only if it imports one of the names the barrel republishes from that file (aliases followed, `export *` expanded to the file's own exports, type-only re-exports ignored — they publish nothing at runtime). Without that narrowing, `export *` on a 40-entry barrel would credit its entire readership to every entry and `ui/orphaned` would quietly stop finding anything — trading a visible false positive for a silent false negative. There is a test for exactly that direction: a barrel with a used entry and a dead one must still report the dead one.
181
+
182
+ ### Internal (no consumer-facing effect)
183
+
184
+ - **@voltro/protocol** — **`PluginHttpRouteRequest.store` names all four absences instead of one.** The docstring said "It is not tenant-scoped" and left soft-delete filtering, audit stamping and row-level security to be inferred from "everything that does not need a Subject".
185
+
186
+ A team planning to port 19 raw-SQL sites onto that seam inferred the opposite: they wrote down "a store read adds `deletedAt IS NULL`" as the trap with teeth on their list — a soft-deleted user logging back in would go from "found and revived" to "not found → insert → unique violation on email" — and deferred the whole port partly over it. The store does no such thing; it is the raw store plus the storage codec. Naming exactly one of four absences reads as an exhaustive list.
187
+
188
+ The docstring now carries the same table the `AuthStrategyInput.store` docs do, with the soft-delete row called out for anyone porting: a read here returns tombstones the way their SQL did, so a lookup that must see one needs no opt-out. (`.withDeleted()` is the opt-out on `ctx.store`, which *does* apply the filter.) Doc-only; the behaviour is unchanged and was already correct.
189
+ - **`ci.yml` gains a `workflow_dispatch` trigger.** The full matrix — 11 database services plus the SQL Server AG init containers — is not reachable from a push to `main`: `paths-ignore` plus the job-level `if: github.event_name != 'push'` mean a main push runs static checks only.
190
+
191
+ So the only ways to exercise it were a pull request and the release gate, which meant a change to the workflow itself could sit unrun until it fired for the first time INSIDE a release — where a failure costs a ~40-minute round-trip and blocks the publish. That is precisely the position this repo was in.
192
+
193
+ ---
194
+
42
195
  ## [0.18.0] — 2026-07-28
43
196
 
44
197
  ### ⚠ BREAKING
@@ -116,6 +269,8 @@ _Changes staged for the next release accumulate here (rolled up from
116
269
  The consequence was a silent wrong answer rather than an error. An auth strategy reading an `.encrypted()` column got the literal string `enc:v1:…` back — which compares, concatenates, renders and logs perfectly well, and simply never matches the token it is compared to. The failure surfaces as "wrong credential". On the write side it was worse and unrecoverable: an insert through that store wrote PLAINTEXT into a column the schema declares encrypted.
117
270
 
118
271
  The line is now **everything that does not need a Subject**, not "less than `ctx.store`". Wired in `voltro dev` and `voltro serve` in the same change (`wrapStoreWithBootCodec`); `raw()` is deliberately left as a pass-through, since it is the documented escape hatch for hand-written SQL and re-encoding rows a caller asked for verbatim would be its own surprise.
272
+
273
+ **Upgrading: a reader that compensated for the missing codec by hand now gets the decoded value.** Two shapes, and they land differently. A hand-rolled `decryptField` on the way out is HARMLESS — it passes a non-`enc:v1:` value through unchanged, so the call simply stops doing anything. A hand-rolled `JSON.parse(row.someJsonColumn)` on a column the codec now deserialises is NOT: it is handed an object and throws. If you read an `.encrypted()` or array column through `AuthStrategyInput.store` / `PluginHttpRouteRequest.store` and unpacked it yourself, grep those call sites — the ones typed as a `string | ReadonlyArray<string>` union already survive, a bare `JSON.parse` does not. (Reported by an adopter whose two such reads happened to be written defensively, which is why their upgrade was silent.)
119
274
  - **@voltro/cli** — **The remaining file-moving codemods narrow their projects too**, and the shared project is no longer a stale snapshot.
120
275
 
121
276
  `0.14.0/03_pages-suffix` is gated on `/src/pages/` throughout — plus each app's `app.config.ts`, which is how it tells a real web app's pages from a library that merely keeps components under `src/pages/`. `0.15.0/01_undo-mistaken-taxonomy-renames` only ever renames `*.component[.ui].ts(x)` and an export-less `*.types.ts(x)`. Both now declare that scope and take the importer closure instead of the workspace.
@@ -169,15 +324,15 @@ _Changes staged for the next release accumulate here (rolled up from
169
324
  The catalogue said a `*.component.tsx` promises "exactly one component **(+ types)**", which reads as "types are the only exception". It never was — the rule counts components, and a `const COLUMNS = […]` beside the table that renders it was always allowed. The docs now say so in both languages.
170
325
  - **@voltro/cli** — **The seeded `AGENTS.md` / `CLAUDE.md` never mentioned `.serverOnly()` — the one marker that decides leak vs no leak.**
171
326
 
172
- Reported from a strict-mode pass over five apps: the template documents `.encrypted()`, `audit()`, `tenant()`, `softDelete()`, `.check()`, `validate()` — and not the marker whose absence the **boot audit hard-fails on**. Depth existed (`database/sensitivity`, `data/crud`, both languages); the always-loaded core simply never pointed at it, so an agent or a human following the template never learned the marker exists.
327
+ Reported from a strict-mode pass over five apps: the template documents `.encrypted()`, `audit()`, `tenant()`, `softDelete()`, `.check()`, `validate()` — and not the marker that decides leak vs no leak. *(Corrected after publication: this entry said the boot audit "hard-fails" on it. It did not — it was a single `log.warn`, in every command. The next release makes the claim true; see that entry.)* Depth existed (`database/sensitivity`, `data/crud`, both languages); the always-loaded core simply never pointed at it, so an agent or a human following the template never learned the marker exists.
173
328
 
174
329
  The core now carries a short section on the three markers as ORTHOGONAL questions, because the substitution is the real hazard:
175
330
 
176
- | Marker | Answers | Enforced by | |---|---|---| | `.serverOnly()` | may this leave the server at all? | the boot audit — it FAILS the boot | | `.sensitive()` / `.safe()` | may it appear in an export? | the masking profile, fail-closed | | `.encrypted()` | is it encrypted at rest? | the store's codec |
331
+ | Marker | Answers | Enforced by | |---|---|---| | `.serverOnly()` | may this leave the server at all? | an audit — see the correction above | | `.sensitive()` / `.safe()` | may it appear in an export? | the masking profile, fail-closed | | `.encrypted()` | is it encrypted at rest? | the store's codec |
177
332
 
178
333
  Reading `.encrypted()` as "safe to expose" is a category error and a plausible one — the runtime decrypts for the handler, so an encrypted column reaches a client like any other unless it is ALSO `.serverOnly()`.
179
334
 
180
- **The doc-drift guard is why this survived, so it grew the missing half.** It asserted the core carries the EXPORT axis (`.sensitive(`, fail-closed, the export endpoint) and said nothing about the wire axis. There is now a matching assertion for `.serverOnly()`, the boot audit, and the orthogonality — verified by deleting the section and watching it go red.
335
+ **The doc-drift guard is why this survived, so it grew the missing half.** It asserted the core carries the EXPORT axis (`.sensitive(`, fail-closed, the export endpoint) and said nothing about the wire axis. There is now a matching assertion for `.serverOnly()`, the enforcement, and the orthogonality — verified by deleting the section and watching it go red. *(It asked only for the words "boot audit", which the template supplied while promising a failure nothing performed. The next release makes it name the commands that gate.)*
181
336
  - **@voltro/cli** — **`voltro dev` could not start at all in a strict-pnpm install: the supervisor respawned with a bare `--import tsx`.**
182
337
 
183
338
  Node resolves a bare specifier against the CHILD's working directory. `@voltro/cli` declares tsx; the user's app does not — so under a non-hoisting layout tsx lives inside `node_modules/.pnpm/@voltro+cli@…/node_modules/tsx` and is invisible from the app root. The first respawn died with `Cannot find package 'tsx'`, naming a package the reader never asked for and cannot usefully install.
package/dist/index.d.ts CHANGED
@@ -276,6 +276,12 @@ export declare class MysqlStore implements DataStore {
276
276
  private executePatchJson;
277
277
  private executeUpdate;
278
278
  private executeDelete;
279
+ /**
280
+ * Append a row inside the caller's transaction — the narrow, insert-only path
281
+ * an in-transaction recorder is handed. NOT `executeInsert`: that would
282
+ * re-enter `routeEvent` and emit a change event for the trail's own table.
283
+ */
284
+ private appendInTxn;
279
285
  private routeEvent;
280
286
  private executeUpsert;
281
287
  /**
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, isTableReactive as C, qualifyTable as w, raiseChangeListenerCeiling as T, requireTable as E, stampGeneratedId as D, stampGeneratedIds as O } from "@voltro/database";
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";
7
7
  //#region src/sqlLayer.ts
8
- var k = (n) => e.layerConfig({
8
+ var M = (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
- }), A = (e) => {
15
+ }), N = (e) => {
16
16
  if (e.url) {
17
17
  let t = new URL(e.url);
18
18
  return {
@@ -32,18 +32,18 @@ var k = (n) => e.layerConfig({
32
32
  database: e.database ?? "app",
33
33
  ...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections }
34
34
  };
35
- }, j = (e) => k(A(e)), M = (e, t) => e === null ? !1 : t === null ? !0 : e.filename === t.filename ? e.position > t.position : e.filename > t.filename, N = (e) => e.msSinceProgress < e.stallThresholdMs ? "healthy" : e.primary !== null && !M(e.primary, e.reader) ? "idle-caught-up" : "reconnect", P = (e) => (e instanceof Error ? e.message : String(e ?? "")).includes("schema changed between binlog event and metadata fetch"), F = /* @__PURE__ */ new Set([
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([
36
36
  "writerows",
37
37
  "updaterows",
38
38
  "deleterows"
39
- ]), I = /\b(alter|rename|drop|create)\s+(table|column)?/i, L = (e) => new Promise((t) => setTimeout(t, e)), R = async (e) => {
39
+ ]), z = /\b(alter|rename|drop|create)\s+(table|column)?/i, B = (e) => new Promise((t) => setTimeout(t, e)), V = 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) => b([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) => 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, _ = () => {
47
47
  f = Date.now();
48
48
  }, v = (n) => {
49
49
  _();
@@ -52,14 +52,14 @@ var k = (n) => e.layerConfig({
52
52
  a = n.binlogName;
53
53
  return;
54
54
  }
55
- if (l === "query" && n.query && I.test(n.query)) {
55
+ if (l === "query" && n.query && z.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)), !F.has(l)) return;
62
+ }, e.onPosition?.(o)), !R.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;
@@ -102,7 +102,7 @@ var k = (n) => e.layerConfig({
102
102
  ]
103
103
  };
104
104
  return e.includeTables && e.includeTables.length > 0 && (n.includeSchema = { [i]: [...e.includeTables] }), t ? (n.filename = t.filename, n.position = t.position) : n.startAtEnd = !0, n;
105
- }, x = (e) => {
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
  }, S = (r) => new Promise((i, a) => {
@@ -137,8 +137,8 @@ var k = (n) => e.layerConfig({
137
137
  try {
138
138
  c?.stop();
139
139
  } catch {}
140
- if (d++, await L(Math.min(3e4, 500 * 2 ** Math.min(d, 6))), u) return;
141
- let r = o, i = x(n), s = !i && P(n);
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);
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, _();
@@ -155,20 +155,20 @@ var k = (n) => e.layerConfig({
155
155
  try {
156
156
  await S(e.startPosition ?? null);
157
157
  } catch (n) {
158
- if (x(n) && e.startPosition) {
158
+ if (b(n) && e.startPosition) {
159
159
  t.warn("cdc: persisted offset purged — starting at current end + self-heal");
160
160
  try {
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 L(500), await S(n);
164
+ a = n?.filename ?? null, e.onResync?.(), await B(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 = N({
171
+ let r = I({
172
172
  msSinceProgress: Date.now() - f,
173
173
  stallThresholdMs: h,
174
174
  primary: n,
@@ -197,7 +197,7 @@ var k = (n) => e.layerConfig({
197
197
  },
198
198
  currentPosition: () => o
199
199
  };
200
- }, z = /* @__PURE__ */ new Set(["1213", "1205"]), B = (e) => {
200
+ }, H = /* @__PURE__ */ new Set(["1213", "1205"]), U = (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 k = (n) => e.layerConfig({
206
206
  if (typeof n == "string") return n;
207
207
  t = t.cause;
208
208
  }
209
- }, V = (e) => {
210
- let t = B(e);
211
- return t !== void 0 && z.has(t);
212
- }, H = (e) => V(e) ? "retry" : "noRetry", U = (e) => {
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) => {
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(U).join(",")}]`;
218
+ if (Array.isArray(e)) return `[${e.map(K).join(",")}]`;
219
219
  let n = e;
220
- return `{${Object.keys(n).sort().map((e) => `${JSON.stringify(e)}:${U(n[e])}`).join(",")}}`;
221
- }, W = (e) => {
222
- let t = U(e), n = 2166136261;
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;
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
- }, G = (e, t) => {
225
+ }, J = (e, t) => {
226
226
  let n = setTimeout(e, t);
227
227
  typeof n.unref == "function" && n.unref();
228
- }, K = class {
228
+ }, Y = class {
229
229
  variant;
230
230
  ttlMs;
231
231
  schedule;
232
232
  seen = /* @__PURE__ */ new Map();
233
- constructor(e, t = 6e4, n = G) {
233
+ constructor(e, t = 6e4, n = J) {
234
234
  this.variant = e, this.ttlMs = t, this.schedule = n;
235
235
  }
236
236
  key(e) {
@@ -240,11 +240,11 @@ var k = (n) => e.layerConfig({
240
240
  if (n == null) return null;
241
241
  let r = t;
242
242
  try {
243
- r = b([t], e.table, this.variant)[0] ?? t;
243
+ r = x([t], e.table, this.variant)[0] ?? t;
244
244
  } catch {
245
245
  r = t;
246
246
  }
247
- return `${e.table} ${e.op} ${String(n)} ${W(r)}`;
247
+ return `${e.table} ${e.op} ${String(n)} ${q(r)}`;
248
248
  }
249
249
  admit(e) {
250
250
  let t = this.key(e);
@@ -259,12 +259,12 @@ var k = (n) => e.layerConfig({
259
259
  get pending() {
260
260
  return this.seen.size;
261
261
  }
262
- }, q = async (e) => {
262
+ }, X = 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 J(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 Z(await c.runPromise(u), c, t, o);
266
266
  return o === "cdc" && e.cdcConfig && await d.startCdcConsumer(e.cdcConfig), d;
267
- }, J = class e {
267
+ }, Z = class e {
268
268
  sql;
269
269
  runtime;
270
270
  variant;
@@ -280,13 +280,13 @@ var k = (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(), T(this.emitter), this.cdcGate = o ?? new K(n);
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);
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);
287
287
  }
288
288
  nsT(e) {
289
- return w(this.namespace, e);
289
+ return T(this.namespace, e);
290
290
  }
291
291
  get dialectId() {
292
292
  return this.variant;
@@ -298,8 +298,8 @@ var k = (n) => e.layerConfig({
298
298
  };
299
299
  }
300
300
  async executeQuery(e, t) {
301
- let r = y(e, this.sql, this.namespace), i = t ? n.provideService(r, d, t) : r;
302
- return b(await this.runtime.runPromise(i), e.table, this.variant);
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);
303
303
  }
304
304
  get supportsInsertReturning() {
305
305
  return this.variant === "mariadb";
@@ -311,38 +311,38 @@ var k = (n) => e.layerConfig({
311
311
  return !1;
312
312
  }
313
313
  async executeInsert(e, t, r, i) {
314
- t = D(e, t);
314
+ t = A(e, t);
315
315
  let a = this.sql;
316
316
  if (this.supportsInsertReturning) {
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];
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];
318
318
  if (!c) throw Error(`MysqlStore.insert: no row returned for table '${e}'`);
319
- return this.routeEvent({
319
+ return await this.routeEvent({
320
320
  table: e,
321
321
  op: "insert",
322
322
  old: null,
323
323
  new: c
324
- }, i), c;
324
+ }, i, r), c;
325
325
  }
326
- let o = x(t, e), s = t.id;
326
+ let o = S(t, e), s = t.id;
327
327
  if (s === void 0) {
328
328
  let t = await this.insertRecoverAutoId(e, o, r);
329
- return this.routeEvent({
329
+ return await this.routeEvent({
330
330
  table: e,
331
331
  op: "insert",
332
332
  old: null,
333
333
  new: t
334
- }, i), t;
334
+ }, i, r), t;
335
335
  }
336
336
  let c = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(o)}`, l = r ? n.provideService(c, d, r) : c;
337
337
  await this.runtime.runPromise(l);
338
338
  let u = a`SELECT * FROM ${a(this.nsT(e))} WHERE ${a("id")} = ${s}`, f = r ? n.provideService(u, d, r) : u, p = (await this.runtime.runPromise(f))[0];
339
339
  if (!p) throw Error(`MysqlStore.insert: row not found post-insert in '${e}'`);
340
- return this.routeEvent({
340
+ return await this.routeEvent({
341
341
  table: e,
342
342
  op: "insert",
343
343
  old: null,
344
344
  new: p
345
- }, i), p;
345
+ }, i, r), p;
346
346
  }
347
347
  async insertRecoverAutoId(e, t, r) {
348
348
  let i = this.sql;
@@ -357,7 +357,7 @@ var k = (n) => e.layerConfig({
357
357
  if (e) return this.runtime.runPromise(t(e));
358
358
  this.inflightTxns++;
359
359
  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(V)), o = e.pipe(n.retry(i), n.withSpan("store.insert", { attributes: {
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: {
361
361
  "db.system": this.variant,
362
362
  "db.operation": r
363
363
  } }));
@@ -367,39 +367,39 @@ var k = (n) => e.layerConfig({
367
367
  }
368
368
  }
369
369
  async executeInsertMany(e, t, r, i) {
370
- if (t = O(e, t), t.length === 0) return [];
371
- let a = this.sql, o = t.map((t) => x(t, e));
370
+ if (t = j(e, t), t.length === 0) return [];
371
+ let a = this.sql, o = t.map((t) => S(t, e));
372
372
  if (this.supportsInsertReturning) {
373
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);
374
- for (let t of c) this.routeEvent({
374
+ for (let t of c) await this.routeEvent({
375
375
  table: e,
376
376
  op: "insert",
377
377
  old: null,
378
378
  new: t
379
- }, i);
379
+ }, i, r);
380
380
  return c;
381
381
  }
382
382
  let s = t.map((e) => e.id);
383
383
  if (s.every((e) => e === void 0)) {
384
384
  let t = await this.insertManyRecoverAutoIds(e, o, r);
385
- for (let n of t) this.routeEvent({
385
+ for (let n of t) await this.routeEvent({
386
386
  table: e,
387
387
  op: "insert",
388
388
  old: null,
389
389
  new: n
390
- }, i);
390
+ }, i, r);
391
391
  return t;
392
392
  }
393
393
  if (s.some((e) => e === void 0)) throw Error("MysqlStore.insertMany: rows mix client-supplied and missing 'id's — supply an id for every row or none (AUTO_INCREMENT recovery).");
394
394
  let c = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(o)}`, l = r ? n.provideService(c, d, r) : c;
395
395
  await this.runtime.runPromise(l);
396
396
  let u = a`SELECT * FROM ${a(this.nsT(e))} WHERE ${a("id")} IN ${a.in(s)}`, f = r ? n.provideService(u, d, r) : u, p = await this.runtime.runPromise(f), m = new Map(p.map((e) => [e.id, e])), h = s.map((e) => m.get(e)).filter((e) => e !== void 0);
397
- for (let t of h) this.routeEvent({
397
+ for (let t of h) await this.routeEvent({
398
398
  table: e,
399
399
  op: "insert",
400
400
  old: null,
401
401
  new: t
402
- }, i);
402
+ }, i, r);
403
403
  return h;
404
404
  }
405
405
  async insertManyRecoverAutoIds(e, t, r) {
@@ -417,57 +417,71 @@ var k = (n) => e.layerConfig({
417
417
  let s = this.sql, c = r.split("."), l = c[0], u = c.slice(1), f = JSON.stringify(i ?? null), p = u.length === 0 ? "$" : `$.${u.join(".")}`, m = s`UPDATE ${s(this.nsT(e))} SET ${s(l)} = JSON_SET(COALESCE(${s(l)}, '{}'), ${p}, CAST(${f} AS JSON)) WHERE ${s("id")} = ${t}`, h = a ? n.provideService(m, d, a) : m, g = await this.runtime.runPromise(h);
418
418
  if (g && typeof g.affectedRows == "number" && g.affectedRows === 0) return null;
419
419
  let _ = s`SELECT * FROM ${s(this.nsT(e))} WHERE ${s("id")} = ${t}`, v = a ? n.provideService(_, d, a) : _, y = (await this.runtime.runPromise(v))[0];
420
- return y ? (this.routeEvent({
420
+ return y ? (await this.routeEvent({
421
421
  table: e,
422
422
  op: "update",
423
423
  old: null,
424
424
  new: y
425
- }, o), y) : null;
425
+ }, o, a), y) : null;
426
426
  }
427
427
  async executeUpdate(e, t, r, i, a) {
428
428
  let o = this.sql;
429
429
  if (this.supportsUpdateReturning) {
430
- let s = o`UPDATE ${o(this.nsT(e))} SET ${o.update(x(r, e))} WHERE ${o("id")} = ${t} RETURNING *`, c = i ? n.provideService(s, d, i) : s, l = (await this.runtime.runPromise(c))[0];
431
- return l ? (this.routeEvent({
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];
431
+ return l ? (await this.routeEvent({
432
432
  table: e,
433
433
  op: "update",
434
434
  old: null,
435
435
  new: l
436
- }, a), l) : null;
436
+ }, a, i), l) : null;
437
437
  }
438
- let s = o`UPDATE ${o(this.nsT(e))} SET ${o.update(x(r, e))} WHERE ${o("id")} = ${t}`, c = i ? n.provideService(s, d, i) : s, l = await this.runtime.runPromise(c);
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);
439
439
  if (l && typeof l.affectedRows == "number" && l.affectedRows === 0) return null;
440
440
  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
- return p ? (this.routeEvent({
441
+ return p ? (await this.routeEvent({
442
442
  table: e,
443
443
  op: "update",
444
444
  old: null,
445
445
  new: p
446
- }, a), p) : null;
446
+ }, a, i), p) : null;
447
447
  }
448
448
  async executeDelete(e, t, r, i) {
449
449
  let a = this.sql;
450
450
  if (this.supportsDeleteReturning) {
451
451
  let o = a`DELETE FROM ${a(this.nsT(e))} WHERE ${a("id")} = ${t} RETURNING *`, s = r ? n.provideService(o, d, r) : o, c = (await this.runtime.runPromise(s))[0];
452
- return c ? (this.routeEvent({
452
+ return c ? (await this.routeEvent({
453
453
  table: e,
454
454
  op: "delete",
455
455
  old: c,
456
456
  new: null
457
- }, i), !0) : !1;
457
+ }, i, r), !0) : !1;
458
458
  }
459
459
  let o = a`SELECT * FROM ${a(this.nsT(e))} WHERE ${a("id")} = ${t}`, s = r ? n.provideService(o, d, r) : o, c = (await this.runtime.runPromise(s))[0];
460
460
  if (!c) return !1;
461
461
  let l = a`DELETE FROM ${a(this.nsT(e))} WHERE ${a("id")} = ${t}`, u = r ? n.provideService(l, d, r) : l;
462
- return await this.runtime.runPromise(u), this.routeEvent({
462
+ return await this.runtime.runPromise(u), await this.routeEvent({
463
463
  table: e,
464
464
  op: "delete",
465
465
  old: c,
466
466
  new: null
467
- }, i), !0;
468
- }
469
- routeEvent(e, t) {
470
- if (t !== null) {
467
+ }, i, r), !0;
468
+ }
469
+ async appendInTxn(e, t, r) {
470
+ let i = this.sql, a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(S(t, e))}`;
471
+ await this.runtime.runPromise(r ? n.provideService(a, d, r) : a);
472
+ }
473
+ async routeEvent(e, t, n = null) {
474
+ if (e = {
475
+ ...g(),
476
+ ...e
477
+ }, D(e.table) && await k((e, t) => this.appendInTxn(e, t, n), {
478
+ table: e.table,
479
+ op: e.op,
480
+ next: e.new,
481
+ prev: e.old,
482
+ traceId: e.traceId,
483
+ subjectId: e.subjectId
484
+ }), t !== null) {
471
485
  t.push(e);
472
486
  return;
473
487
  }
@@ -496,27 +510,27 @@ var k = (n) => e.layerConfig({
496
510
  return this.executeInsert(e, t, r, i);
497
511
  }
498
512
  async executeMariadbUpsert(e, t, r, i, a) {
499
- let o = this.sql, s = x(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];
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];
500
514
  if (!m) throw Error(`MysqlStore.upsert: no row returned for table '${e}'`);
501
515
  let h = t.id !== void 0 && t.id === m.id ? "insert" : "update";
502
- return this.routeEvent({
516
+ return await this.routeEvent({
503
517
  table: e,
504
518
  op: h,
505
519
  old: null,
506
520
  new: m
507
- }, a), m;
521
+ }, a, i), m;
508
522
  }
509
523
  async executeInsertIgnore(e, t, r, i, a) {
510
- if (t = D(e, t), this.variant === "mariadb") {
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];
512
- if (l) return this.routeEvent({
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];
526
+ if (l) return await this.routeEvent({
513
527
  table: e,
514
528
  op: "insert",
515
529
  old: null,
516
530
  new: l
517
- }, a), l;
531
+ }, a, i), l;
518
532
  let u = await this.findByConflict(e, t, r.conflictColumns, i);
519
- if (!u) throw Error("MysqlStore.insertIgnore: row conflicted but lookup found nothing");
533
+ 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.`);
520
534
  return u;
521
535
  }
522
536
  return await this.findByConflict(e, t, r.conflictColumns, i) || this.executeInsert(e, t, i, a);
@@ -530,12 +544,12 @@ var k = (n) => e.layerConfig({
530
544
  return this.runWithEager(e, null);
531
545
  }
532
546
  raw(e, t) {
533
- let n = v(e, this.sql);
547
+ let n = y(e, this.sql);
534
548
  return this.runtime.runPromise(n);
535
549
  }
536
550
  async runWithEager(e, t) {
537
- if (!S(e)) return this.executeQuery(e, t);
538
- let r = this.variant === "mariadb" ? "mariadb" : "mysql", i = this.namespace === null ? g(e, this.sql, r) : null;
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;
539
553
  if (i !== null) try {
540
554
  let e = t ? n.provideService(i.fragment, d, t) : i.fragment, r = await this.runtime.runPromise(e);
541
555
  return i.decode(r);
@@ -543,7 +557,7 @@ var k = (n) => e.layerConfig({
543
557
  if (e instanceof p) throw e;
544
558
  this.log.warn(`${this.variant} JSON-agg eager-load failed; falling back to walker`, { err: e });
545
559
  }
546
- return h(await this.executeQuery(e, t), e.eager, e.sourceTable ?? E(e.table), (e) => this.executeQuery(e, t));
560
+ return h(await this.executeQuery(e, t), e.eager, e.sourceTable ?? O(e.table), (e) => this.executeQuery(e, t));
547
561
  }
548
562
  getInternalRunWithEager() {
549
563
  return this.runWithEager.bind(this);
@@ -571,53 +585,53 @@ var k = (n) => e.layerConfig({
571
585
  }
572
586
  async updateMany(e, t, r) {
573
587
  if (this.supportsUpdateReturning) {
574
- let n = this.sql, i = _(r.where, n, this.namespace), a = n`UPDATE ${n(this.nsT(e))} SET ${n.update(x(t, e))} WHERE ${i} RETURNING *`, o = await this.runtime.runPromise(a);
575
- for (let t of o) this.routeEvent({
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);
589
+ for (let t of o) await this.routeEvent({
576
590
  table: e,
577
591
  op: "update",
578
592
  old: null,
579
593
  new: t
580
- }, null);
594
+ }, null, null);
581
595
  return o.length;
582
596
  }
583
- let i = this.sql, o = _(r.where, i, this.namespace);
597
+ let i = this.sql, o = v(r.where, i, this.namespace);
584
598
  this.inflightTxns++;
585
599
  try {
586
600
  let r = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(d), (r) => {
587
601
  if (a.isNone(r)) return n.fail(/* @__PURE__ */ Error("MysqlStore.updateMany: TransactionConnection missing."));
588
- 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(x(t, e))} WHERE ${o}`;
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}`;
589
603
  return n.flatMap(n.provideService(c, d, s), (t) => {
590
604
  if (t.length === 0) return n.succeed([]);
591
605
  let r = t.map((e) => e.id), a = i`SELECT * FROM ${i(this.nsT(e))} WHERE ${i("id")} IN ${i.in(r)}`;
592
606
  return n.flatMap(n.provideService(l, d, s), () => n.provideService(a, d, s));
593
607
  });
594
- }))), c = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(V)), l = r.pipe(n.retry(c), n.withSpan("store.updateMany", { attributes: {
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: {
595
609
  "db.system": this.variant,
596
610
  "db.operation": "update"
597
611
  } })), u = await this.runtime.runPromise(l);
598
612
  if (u.length === 0) return 0;
599
- let f = b(u, e, this.variant);
600
- for (let t of f) this.routeEvent({
613
+ let f = x(u, e, this.variant);
614
+ for (let t of f) await this.routeEvent({
601
615
  table: e,
602
616
  op: "update",
603
617
  old: null,
604
618
  new: t
605
- }, null);
619
+ }, null, null);
606
620
  return f.length;
607
621
  } finally {
608
622
  this.inflightTxns--;
609
623
  }
610
624
  }
611
625
  async deleteMany(e, t) {
612
- let r = this.sql, i = _(t.where, r, this.namespace);
626
+ let r = this.sql, i = v(t.where, r, this.namespace);
613
627
  if (this.supportsDeleteReturning) {
614
- let t = r`DELETE FROM ${r(this.nsT(e))} WHERE ${i} RETURNING *`, n = b(await this.runtime.runPromise(t), e, this.variant);
615
- for (let t of n) this.routeEvent({
628
+ let t = r`DELETE FROM ${r(this.nsT(e))} WHERE ${i} RETURNING *`, n = x(await this.runtime.runPromise(t), e, this.variant);
629
+ for (let t of n) await this.routeEvent({
616
630
  table: e,
617
631
  op: "delete",
618
632
  old: t,
619
633
  new: null
620
- }, null);
634
+ }, null, null);
621
635
  return n.length;
622
636
  }
623
637
  this.inflightTxns++;
@@ -626,31 +640,31 @@ var k = (n) => e.layerConfig({
626
640
  if (a.isNone(t)) return n.fail(/* @__PURE__ */ Error("MysqlStore.deleteMany: TransactionConnection missing."));
627
641
  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}`;
628
642
  return n.flatMap(n.provideService(s, d, o), (e) => e.length === 0 ? n.succeed(e) : n.as(n.provideService(c, d, o), e));
629
- }))), o = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(V)), c = t.pipe(n.retry(o), n.withSpan("store.deleteMany", { attributes: {
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: {
630
644
  "db.system": this.variant,
631
645
  "db.operation": "delete"
632
646
  } })), l = await this.runtime.runPromise(c);
633
647
  if (l.length === 0) return 0;
634
- let u = b(l, e, this.variant);
635
- for (let t of u) this.routeEvent({
648
+ let u = x(l, e, this.variant);
649
+ for (let t of u) await this.routeEvent({
636
650
  table: e,
637
651
  op: "delete",
638
652
  old: t,
639
653
  new: null
640
- }, null);
654
+ }, null, null);
641
655
  return u.length;
642
656
  } finally {
643
657
  this.inflightTxns--;
644
658
  }
645
659
  }
646
660
  emitChange(e) {
647
- C(e.table) && (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || this.emitter.emit("change", e));
661
+ w(e.table) && (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || this.emitter.emit("change", e));
648
662
  }
649
663
  async startCdcConsumer(e) {
650
664
  if (this.cdcHandle) return;
651
665
  await this.assertBinlogConfig(), this.cdcReplicaId = e.replicaId;
652
666
  let t = await this.readCdcOffset(e.replicaId) ?? await this.resolveBinlogEnd();
653
- this.cdcHandle = await R({
667
+ this.cdcHandle = await V({
654
668
  connection: e.connection,
655
669
  serverId: e.serverId,
656
670
  variant: this.variant,
@@ -770,7 +784,7 @@ var k = (n) => e.layerConfig({
770
784
  let r = ++t;
771
785
  return this.sql.withTransaction(n.flatMap(n.serviceOption(d), (t) => {
772
786
  if (a.isNone(t)) return n.fail(/* @__PURE__ */ Error("MysqlStore.transactional: TransactionConnection missing."));
773
- let i = new Y(this, t.value);
787
+ let i = new Q(this, t.value);
774
788
  return n.tryPromise({
775
789
  try: () => e(i).then((e) => ({
776
790
  result: e,
@@ -780,7 +794,7 @@ var k = (n) => e.layerConfig({
780
794
  catch: (e) => e
781
795
  });
782
796
  }));
783
- }), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(V)), o = r.pipe(n.retry(i), n.withSpan("store.transactional", { attributes: {
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: {
784
798
  "db.system": this.variant,
785
799
  "db.operation": "transaction"
786
800
  } }));
@@ -800,7 +814,7 @@ var k = (n) => e.layerConfig({
800
814
  return this.changeStrategy === "cdc" ? "fleet" : "local";
801
815
  }
802
816
  injectExternalChange(e) {
803
- this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || C(e.table) && this.emitter.emit("change", {
817
+ this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || w(e.table) && this.emitter.emit("change", {
804
818
  ...e,
805
819
  origin: "injected"
806
820
  });
@@ -826,7 +840,7 @@ var k = (n) => e.layerConfig({
826
840
  async ping() {
827
841
  await this.runtime.runPromise(this.sql`SELECT 1`);
828
842
  }
829
- }, Y = class {
843
+ }, Q = class {
830
844
  parent;
831
845
  txn;
832
846
  events = [];
@@ -898,9 +912,9 @@ var k = (n) => e.layerConfig({
898
912
  this.events.length = 0;
899
913
  }
900
914
  }
901
- }, X = (e) => e.__mysqlReplicationFriend ?? null, Z = () => ({
915
+ }, $ = (e) => e.__mysqlReplicationFriend ?? null, ee = () => ({
902
916
  async capturePrimaryPosition(e) {
903
- let t = X(e);
917
+ let t = $(e);
904
918
  if (t === null) throw Error("mysqlReplicationAdapter: primary is not a MysqlStore (missing __mysqlReplicationFriend).");
905
919
  return t.runEffect(n.gen(function* () {
906
920
  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;
@@ -908,7 +922,7 @@ var k = (n) => e.layerConfig({
908
922
  }));
909
923
  },
910
924
  async probeReplicaPosition(e) {
911
- let t = X(e);
925
+ let t = $(e);
912
926
  if (t === null) throw Error("mysqlReplicationAdapter: replica is not a MysqlStore.");
913
927
  return t.runEffect(n.gen(function* () {
914
928
  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;
@@ -918,24 +932,24 @@ var k = (n) => e.layerConfig({
918
932
  compare(e, t) {
919
933
  return "behind";
920
934
  }
921
- }), Q = {
935
+ }), te = {
922
936
  id: "mysql",
923
- makeSqlLayer: (e) => j(e),
924
- makeStore: (e) => q({
937
+ makeSqlLayer: (e) => P(e),
938
+ makeStore: (e) => X({
925
939
  ...e,
926
940
  variant: "mysql"
927
941
  }),
928
942
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
929
- retryFilter: H
930
- }, $ = {
943
+ retryFilter: G
944
+ }, ne = {
931
945
  id: "mariadb",
932
- makeSqlLayer: (e) => j(e),
933
- makeStore: (e) => q({
946
+ makeSqlLayer: (e) => P(e),
947
+ makeStore: (e) => X({
934
948
  ...e,
935
949
  variant: "mariadb"
936
950
  }),
937
951
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
938
- retryFilter: H
952
+ retryFilter: G
939
953
  };
940
954
  //#endregion
941
- export { f as CDC_OFFSETS_TABLE, e as MysqlClient, m as _voltroCdcOffsetsTable, A as connectionFromConfig, k as makeMysqlSqlLayer, j as makeMysqlSqlLayerFromConfig, q as makeMysqlStore, $ as mariadbDialect, Q as mysqlDialect, Z as mysqlReplicationAdapter, H as mysqlRetryFilter, R as startBinlogCdc };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/sql-mysql",
3
- "version": "0.18.0",
3
+ "version": "0.19.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.18.0",
38
- "@voltro/logger": "0.18.0"
37
+ "@voltro/database": "0.19.0",
38
+ "@voltro/logger": "0.19.0"
39
39
  },
40
40
  "optionalDependencies": {
41
41
  "@vlasky/zongji": "^0.9.0"