@voltro/sql-mysql 0.57.0 → 0.58.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/dist/index.js +139 -110
  3. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -39,6 +39,207 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.58.0] — 2026-08-30
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/ai, @voltro/client** — The gateway catalog names eight modalities, `ModelModality` has listed all eight since the union was widened, and the runtime could execute five. Four of the six gaps are closed.
47
+
48
+ **The catalog gives out what the response carries.** `getAvailableModels` asked `@ai-sdk/gateway`'s provider, whose entry type has six fields where the HTTP response has twenty-one — so nothing was discarded, the fifteen never arrived. `readGatewayCatalog()` reads the endpoint and adds `capabilities`, `modalities`, `contextWindow`, `maxOutputTokens`, `releasedAt`, `knowledgeCutoff` and `dataPolicy`.
49
+
50
+ `capabilities` is unioned from `tags`, `supported_parameters` and `modalities`, which overlap and regularly disagree: a model tagged `reasoning` does not always carry the parameter. Taking either alone produces a false negative, and a false "cannot" removes a model from a picker with no way to find out why — so the union is framework knowledge rather than forty lines every app writes differently. `capabilities` and `modalities` stay OPTIONAL: absent means the entry came from the injected test provider, and defaulting to all-false would turn "not known" into "this model has no vision".
51
+
52
+ **The runtime says what it can run.** `executableModalities()` / `canExecute()` replace the hand-kept list an app needs to stop a picker offering a model nothing can execute. It is tied to the exports that justify each entry, so shipping a primitive without adding it turns a test red — which it did, twice, during this change.
53
+
54
+ **`transcribe` exists.** Nine transcription models were unaddressable without pulling the raw SDK past the framework (and resolving gateway credentials twice in one process). `source` takes `{ storageRef }`, `{ url }` or `{ bytes }`, because audio is large and usually already stored — the storage read is INJECTED (`resolveRef`), so `@voltro/ai` never learns about buckets. `segments` is absent rather than `[]` when the provider returned none.
55
+
56
+ **`rerank` reaches the gateway.** It was the one primitive that did not take a `ProviderConfig`: it wanted the vendors' own keys and could not express the gateway at all, so five reranking models in the catalog were unreachable.
57
+
58
+ **A speech provider the gateway does not carry can be registered.** The gateway serves nine speech models; a real voice catalog runs to hundreds behind vendors it does not carry, so an app with its own adapter called its own synthesizer and skipped the primitive — losing the shared error type and cost path. `registerSpeechProvider` gives that adapter a socket. No adapters ship: the framework is not in the business of tracking somebody else's REST API, and the app already has them.
59
+
60
+ Not built: `realtime`. Not for want of a model surface — the gateway exposes one — but because every streaming kind in Voltro is server→client, so relaying a duplex session over the existing authenticated connection needs a transport capability that does not exist. `canExecute` answers `false` for it, which keeps it out of a picker until it does.
61
+
62
+ Migration: `rerank`'s `provider` widened from `'mock' | 'cohere' | 'voyage'` to include `ProviderConfig`. Passing a string is unchanged; reading the field off a typed options object is now a union. See the codemod note.
63
+
64
+ **And the ergonomy over them, because a primitive nobody can reach is half a feature.** `transcribeStep` puts transcription on the durable-step path with `offload: true` — the modality that runs longest was the one without it, and a ninety-minute recording held a worker for the whole call. The queue row carries the source, never the bytes. `retrieveReranked` is the second retrieval stage declared once instead of the twenty lines every app writes (fetch N, reorder, keep K — with its own N, its own K, and half of them forgetting the cost). `useTranscription` makes upload and transcription ONE state: `progress` is scoped to the phase, because a single number across both reaches 1 when the upload finishes and sits there for the whole transcription, and a bar that says finished is worse than no bar.
65
+
66
+ **`voltro update` carries you across this** — codemod `0.58.0/07_rerank_takes_a_provider_config`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.58.0).
67
+ - **@voltro/protocol, @voltro/plugin-auth-supabase, @voltro/cli, @voltro/voltro** — An auth strategy's shared secret may be a function, so `voltro build` no longer needs the app's production credentials.
68
+
69
+ `voltro build` imports `app.config.ts`, so anything the config constructs runs at build time. `supabaseStrategy({ jwtSecret: process.env.SUPABASE_JWT_SECRET })` therefore made an image build need a production secret for a bundle that will not contain one. A placeholder is not available — this framework ships no secret values anywhere, and an `ARG SUPABASE_JWT_SECRET` in a Dockerfile is exactly that — so a deployment stayed on the slower tsx path instead.
70
+
71
+ `jwtSecret` now accepts `string | (() => string | undefined)`, on `jwtBearerStrategy` and on the Supabase strategy that specialises it.
72
+
73
+ **The deferral is paired, and that pairing is the design.** A thunk alone would trade an image-build inconvenience for a runtime surprise: the failure of a MISSING secret would move from boot to the first request that happens to carry a token, and an auth strategy that cannot verify one accepts nobody — a process that started cleanly, serving a login that can never succeed. "Fail at boot" is what the env gate, the plugin env contracts and the migration guard all promise, and this is not the place to sell it.
74
+
75
+ So `AuthStrategy` gained an optional `verifyConfig()`, and the pass over the strategies that `voltro dev` and `voltro serve` already share calls it. A build never reaches that builder. Three moments, three answers: construction is silent, boot decides, verification uses the settled value — resolved once, because a credential re-read per request is a `process.env` hit on the hot path and a value that could change under a running process.
76
+
77
+ A thunk counts as PRESENT at construction: whether it resolves is a boot question, and calling it early to find out would undo the whole point. Passing no credential at all still refuses at construction, unchanged.
78
+
79
+ Migration: passing a value is unchanged; what can stop compiling is READING `jwtSecret` off a typed config, which is now the union. That is the mirror of "more precise is still breaking" — less precise breaks readers — and the population is small, because the interface is one users construct and a literal's own inferred type never widened. `@voltro/voltro` is listed because it re-exports the type.
80
+
81
+ **`voltro update` carries you across this** — codemod `0.58.0/06_jwt_secret_may_be_a_function`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.58.0).
82
+ - **@voltro/runtime, @voltro/cli** — A fleet view no longer counts rows left behind by processes that are gone, and the rows of live replicas actually refresh on MariaDB.
83
+
84
+ Three symptoms, one root cause and two that outlived it.
85
+
86
+ **The rows were frozen.** `writeReplicaObservation` upserts on `UNIQUE (replicaId, kind)` — a conflict key that is not the primary key, with a fresh `id` on every write — which is exactly the shape the MySQL/MariaDB upsert guard was rejecting. Every republish after the first was rolled back, so a publisher writing every 30s produced a row whose age grew second for second.
87
+
88
+ **The failure said nothing.** It was logged at `debug`, on the reasoning that a failed write leaves the previous row and the reader reports it as stale, so the failure is visible in the answer. It is not: a stale row is what a slow, busy or departed replica looks like, and the answer never says a write was REFUSED. It was also invisible where it mattered — `voltro dev` prints debug and `voltro serve` does not, so the unattended deployment got no line at all. The first failure per kind now warns.
89
+
90
+ **Nothing ever removed a row.** The table is keyed `(replicaId, kind)`, so a restart overwrites its own row and a replica that never returns leaves one forever — counted as a responder, counted in the version tally, and old enough to hold `complete` at false for the life of the deployment. Membership is the roster of who is alive, so a row whose replica it no longer lists is now reported as `departed`: separately from `stale`, because a stale row belongs to a process that is running and has stopped refreshing (a fault) and a departed one belongs to a process that is gone (a scale-down). It is reported rather than dropped — a reader who cannot see them cannot tell a fleet that scaled down from a table being written by something nobody is tracking. The publisher also forgets its own rows on shutdown, so the ordinary case leaves nothing to classify.
91
+
92
+ The sharpest consequence of the old behaviour was not the incompleteness: `versions` counted the framework version of a process that had been dead for hours and reported a version SPLIT across a fleet that had none — a line an operator acts on.
93
+
94
+ Migration: `FleetMergeResult.departed` is required, so code that CONSTRUCTS one — a test double for `fleetObservations` — needs the field. Reading one is unaffected. See the codemod note.
95
+
96
+ **`voltro update` carries you across this** — codemod `0.58.0/03_fleet_view_reports_departed_replicas`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.58.0).
97
+ - **@voltro/client, @voltro/web** — An rpc call that fails in the client now reaches the app's error REPORTERS, not only its rpc listeners.
98
+
99
+ There were two buses. `clientErrorBus` is the module-global seam a reporter subscribes to — the route ErrorBoundary publishes render and loader failures there, `reportClientError` publishes manual captures, and the Sentry browser integration listens. `RpcErrorBus` is a per-runtime bus with a different subscriber set, for cross-cutting POLICY: redirect on `Unauthenticated`, toast on a network failure. Nothing connected them, so a call that failed in the client reached policy and never reached reporting.
100
+
101
+ The subscription case is the one that costs. A rejected subscription leaves `data: undefined`, `loading: false` and an empty list — which is also what "nothing matched" looks like — so there is nothing in the UI to notice and nothing in any reporter to find.
102
+
103
+ The bridge is in `RpcErrorBus.emit`, not at the six call sites that emit rpc errors, so a seventh is covered by existing. `ClientErrorEvent.source` gained `rpc.mutation` / `rpc.action` / `rpc.subscription` as three separate labels rather than one `rpc`: a failed mutation is a write the user watched fail, and a failed subscription is a screen that silently never filled. The rpc tag and the `traceId` — the same id the server logged — travel in `context`.
104
+
105
+ Everything is published. A reporter that must not be flooded by a reconnect storm applies its own ceiling: the bus's job is to make the failure observable, and which failures are worth an event is a policy it cannot hold for every subscriber.
106
+
107
+ Migration: only an EXHAUSTIVE switch over `event.source` stops compiling. If you bridged the two buses by hand, delete that bridge or you will report twice — and keep whatever throttling you put around it.
108
+
109
+ `@voltro/web` is listed because it re-exports the type: the union widens in its golden too, and a consumer importing `ClientErrorEvent` from there is affected identically.
110
+
111
+ **`voltro update` carries you across this** — codemod `0.58.0/04_rpc_errors_reach_reporters`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.58.0).
112
+ - **@voltro/plugin-sentry, @voltro/cli** — Sentry performance traces are on by default, and the web entry no longer needs a config block to initialise them.
113
+
114
+ `sentryPlugin({ traces })` defaulted to `false` while the browser half defaulted `tracesSampleRate: 1.0` with `browserTracing` on. That pairing is the worst of the three available defaults: every page load, navigation and rpc call became a browser span that was emitted, paid for, and belonged to a trace with no server half — a browser hop hanging off nothing. Both-off would at least be coherent; both-on is what the two halves were built for, and the docs already said to pass `traces: true` to get it, which made that sentence a workaround note rather than a configuration option.
115
+
116
+ The rate stays 1.0 rather than being quietly lowered — a number the framework picks on your behalf is one nobody can find again — and the boot line names it: `sentry active traces=true tracesSampleRate=1`. Lower it with `tracesSampleRate`, or `traces: false` for errors and breadcrumbs only.
117
+
118
+ The browser half is reached the same way. The generated entry called `initSentryBrowser` only when `app.config.ts` carried a `sentry:` block, which made that block load-bearing for something it does not decide: the api half needs no config object (`sentryPlugin()` reads `SENTRY_DSN`) and the browser half reads `VOLTRO_PUBLIC_SENTRY_DSN`, so an app that had set its variables still had to add an empty `sentry: {}`. The entry now initialises whenever `@voltro/plugin-sentry` is a dependency of the web app — the dependency is the intent — and the block is for overrides. Without a DSN from either source the init returns immediately, so this changes nothing for an app that set none.
119
+
120
+ Migration: decide once whether you want the transactions. See the codemod note.
121
+
122
+ **`voltro update` carries you across this** — codemod `0.58.0/05_sentry_traces_on_by_default`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.58.0).
123
+ - **@voltro/runtime, @voltro/cli** — `SYSTEM_SUBJECT` — what a schedule, a subscriber, a resumed workflow and the webhook trigger context run as — is now a `system` subject. It was `{ type: 'anonymous', id: null }`, and every exemption for "the framework acting as itself" is keyed on `type === 'system'`: the row-filter bypass in `resolveRowFilterScopeFor`, the unfiltered-read refusal in `wrapStoreWithMixinBehaviour`, and `isSystemSubject`. It matched none of them.
124
+
125
+ An app that calls `setRowFilter` therefore had every schedule fire refused at context-build time, before its handler ran — on both boot paths, from the cron timer and from `voltro schedule run` alike. The quieter half is the one that produced no error: a path that RESOLVED a scope for this subject asked the app's own filter what an anonymous caller may see, and applied that answer to a cross-tenant sweep.
126
+
127
+ `ctx.storeForTenant(id)` is fixed alongside it, and needed saying separately. It narrows to a `serviceAccount` subject by design, so a subject whose null tenant means "every tenant" cannot widen back out of the tenant it was just confined to — which also means the exemption stops at the derivation. The row-filter bypass now travels across that narrowing; the tenant confinement does not.
128
+
129
+ Migration: `actingUserId` answers `null` for a system subject, so a `runAsSystem` write stamps no `createdBy` / `updatedBy` instead of `'system'`. `createdBy` is a reference into the app's own `actors` table and the framework seeds no row there, so an id it stamps is one nothing points at — an outright insert failure where that reference is declared. Schedules, subscribers and workflows already stamped nothing and are unchanged. See the codemod note.
130
+
131
+ **`voltro update` carries you across this** — codemod `0.58.0/01_system_writes_stamp_no_actor`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.58.0).
132
+ - **@voltro/runtime, @voltro/cli** — A workflow step now reads through its caller's row filter, resolved at execution time — and until now a workflow started by a user could not read at all.
133
+
134
+ The store refuses a context that carries neither a resolved row-filter scope nor a system subject. A BOOTSTRAP run has the system subject and passed; a run started by a user has neither, so in any app that calls `setRowFilter` every workflow step threw before its executor ran. The gap was invisible from the framework's own suites because the arm that works and the arm that does not differ only in who started the run.
135
+
136
+ Visibility is resolved in the same seam as authority, at the same moment, for the same reason: `resolveAuthority` re-reads what a three-day-old run may DO, and the scope resolved beside it re-reads what that run may SEE. Restoring either from the start-context row would be a fact frozen at start time and made durable. A failure to resolve fails the attempt rather than downgrading it — a run that silently reads fewer rows than it should is indistinguishable from one whose data is not there. An app that registered no filter pays nothing; the resolution short-circuits without touching a store.
137
+
138
+ `BuildAppContext`'s request parameter now declares `rowFilter`. It was read by the implementation and absent from the type, so the request arms were correct by the accident of passing a value the signature did not mention, while a non-request caller had no way to see there was anything to supply.
139
+
140
+ `ResolvedWorkflowCallerContext.rowFilter` is required, so code that CONSTRUCTS one — in practice a test double for a context builder — needs the field. Implementing `buildContext` is unaffected: it receives one more property it may ignore. Required rather than optional is deliberate: optional is what it effectively was, and it left a boot path able to omit the one value without which a user's workflow cannot run.
141
+
142
+ **`voltro update` carries you across this** — codemod `0.58.0/02_workflow_caller_context_carries_a_scope`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.58.0).
143
+
144
+ ### Added
145
+
146
+ - **@voltro/cli** — `voltro doctor`'s `nullable-column-a-mutation-cannot-clear` rule now resolves FRAMEWORK and first-party PLUGIN tables.
147
+
148
+ The rule asks "is this column nullable?" by finding the `table('<name>', { … })` declaration among the sources it was handed — the app's. That is right for the app's own tables and cannot work for ours: `_voltro_webhook_targets` is declared inside `@voltro/plugin-webhooks`, so a mutation writing to it resolved to "I could not look". The rule keeps that answer distinct from "this table has no nullable columns", because the two lead to opposite conclusions — but for our tables it was a permanent unknown, leaving anyone auditing their mutations to carry them as a standing exception and count them by hand.
149
+
150
+ Doctor is a static analyser: it parses files and boots nothing, so it cannot import a plugin to ask a table about itself. The nullability of our own tables is therefore generated from our declarations — with the same extraction rule the doctor applies, so a manifest entry cannot mean something different from what the rule would have concluded — and shipped as data. `scripts/gen-framework-table-nullability.mjs --check` runs in CI, because a stale entry would answer with a value that is no longer declared, which is worse than the honest unknown it replaces.
151
+
152
+ An app's own declaration still wins where both have one: it is the more specific authority and the one a developer can actually edit.
153
+ - **@voltro/plugin-sentry, @voltro/cli** — The browser half of the Sentry integration can now resolve its own configuration — from the public env, and from a module that runs at boot.
154
+
155
+ **It reads `VOLTRO_PUBLIC_SENTRY_DSN` / `_ENVIRONMENT` / `_RELEASE`.** The api half already resolved `SENTRY_DSN`, `SENTRY_ENVIRONMENT` and `SENTRY_RELEASE` from the environment and declared all three, so `sentryPlugin()` takes no arguments. `initSentryBrowser` resolved nothing: without an explicit `dsn` it returned, and the only way to supply one was `sentry:` in `app.config.ts`. An explicit option still wins — `app.config.ts` is the more specific authority.
156
+
157
+ **`sentry.optionsFrom` names a module the generated entry calls.** Everything else in that block is a literal frozen into the bundle, which is right for a DSN and wrong for `environment` the moment one image serves more than one environment — an ordinary pipeline with one build job and several environment-bound deploy jobs. A baked `environment` is then true for at most one of them, and a wrong environment tag is worse than an absent one because Sentry defaults a missing one to `production` and a wrong tag gets acted on.
158
+
159
+ There is no runtime channel to read instead: public values are baked at build time by construction. So the value has to be computed where it is known, which is the browser — and the entry is generated anyway, so it can call something. The resolver's result is spread LAST and may be async.
160
+
161
+ **And `voltro start` now says when a `VOLTRO_PUBLIC_*` cannot reach the browser.** Setting one as a deployment variable looks right, deploys cleanly, and does nothing — no error, no warning, no failed request. The check is exact rather than a guess, because both halves are present at boot: the process environment, and the object the build wrote (`.framework/env.public.json`, emitted alongside the baked module from the same source). A variable set to the value that was baked stays silent; one that was never baked, or that disagrees with the built one, is named with the reason. An unreadable manifest reports every runtime variable rather than none — "we could not check" and "there is nothing to report" are opposite facts.
162
+ - **@voltro/cli, @voltro/plugin-sentry** — `voltro build` uploads source maps to Sentry and then removes them from the output.
163
+
164
+ `web.sourcemaps: 'hidden'` already produced `.map` files, and the note beside it said to upload them in a deploy step and delete them before the image is built. Every word of that was right and all three hard parts belong to the build:
165
+
166
+ - **the moment** — there is no seam in a Dockerfile between "the bundle exists" and "the image is built"; - **the release** — Sentry matches an artifact to an event by release, and the event's comes from `SENTRY_RELEASE` via `sentryPlugin`. Upload under a different value and no frame resolves, silently: an upload that matched nothing looks exactly like one that worked. A configured upload with no release now refuses the build rather than uploading under nothing; - **the deletion** — a `.map` left in `dist` is the app's source, downloadable by anyone, and "we delete it in the deploy step" is a promise a failing build breaks. It is a `finally` here: the maps go even when the upload fails, and a configured upload that did not happen fails the build. Verified against a real build — a failed upload left zero `.map` files and all five chunks.
167
+
168
+ ```ts
169
+ web: { sourcemaps: { mode: 'hidden', upload: { org: 'acme', project: 'web' } } }
170
+ ```
171
+
172
+ Selective by ordinary means: `app.config.ts` is TypeScript, so `sourcemaps: process.env.CI ? { … } : 'hidden'` emits-and-keeps locally and uploads in the pipeline. `keep` defaults to "keep them only if nothing consumed them" — false with an upload, true without.
173
+
174
+ **There is no `authToken` option**, on purpose. It is read from `SENTRY_AUTH_TOKEN`: `app.config.ts` is a committed file, and a token with `project:releases` scope can write to every project in the org. It belongs to the build, not the deployment.
175
+
176
+ The upload runs through `@sentry/cli` rather than a hand-written HTTP client, because Sentry's artifact model is debug-ID based — the ids are injected into the built JS and the map, and the matching happens on them. An uploader that skipped that injection would produce artifacts that upload cleanly and resolve nothing. **You install it: `pnpm add -D @sentry/cli` in the web app.** The framework does not depend on it anywhere, and that is a licence decision rather than a packaging one. `@sentry/cli` is FSL-1.1-MIT, which restricts competing commercial use — so unlike the MPL and LGPL dependencies we do carry, "the consumer's own install fetches it, and we neither distribute nor modify it" does not answer the question: FSL binds a user, not a distributor. An optional dependency of a package we publish is part of that package's licence surface, so it is not one.
177
+
178
+ A configured upload with the package absent refuses the build and names the command, rather than skipping the upload and leaving you to discover months later that no frame resolves.
179
+
180
+ ### Fixed
181
+
182
+ - **@voltro/database, @voltro/protocol, @voltro/runtime, @voltro/sql-mysql** — A write the FRAMEWORK refuses now reaches the caller as a typed `ConstraintViolation` instead of a defect.
183
+
184
+ Every failure the DATABASE reports already had this: `storeMiddleware` reads the driver's error and maps it. A refusal the framework decides had nothing to map from — there is no driver code for our own decision — so it stayed a plain `Error`, which Effect classifies as a defect. It arrived at the client as a `Defect` wrapping a stringified `InternalError`, untagged, for a condition an app can genuinely branch on. A form shows a support id over an input that saved nothing.
185
+
186
+ The MySQL-family upsert raises one: `ON DUPLICATE KEY UPDATE` fires on whichever unique key the incoming row violates, so the store checks afterwards that it reached the row the caller's `conflictColumns` name, and rolls back when it did not. `unique` is the kind — a row did collide on some unique key of that table; what the engine will not say is which, and that silence is the reason the check exists, so `constraint` stays absent rather than guessed at.
187
+
188
+ `ConstraintViolation` gained an optional `detail`, populated only by these refusals and never from a driver message or a row value. The generated sentence exists because the driver's own text carries row data on several dialects; that is a rule about the DRIVER's sentence, not about length, and a sentence the framework authored carries exactly what we put in it.
189
+
190
+ The mark is read BEFORE the driver-parser and never folded into it: giving our own decision the fields `classifyConstraintViolation` reads would make the two indistinguishable inside the one function whose contract is "this is what the database said".
191
+
192
+ Not everything the store throws becomes a failure. An upsert whose conflict key cannot be looked up at all — every column database-GENERATED — stays a defect, because it is a call that cannot be served as written rather than a condition in the data, and typing it would invite an app to swallow a wiring mistake as though it were a busy row.
193
+ - **@voltro/sql-mysql, @voltro/testing** — An upsert whose `conflictColumns` are NOT the primary key now works on MySQL and MariaDB. It threw, rolled back, and reported "Nothing was changed" — accurately, which is the only reason it was not worse.
194
+
195
+ Postgres names its target (`ON CONFLICT (a, b) DO UPDATE`); MySQL and MariaDB have no such form, so `ON DUPLICATE KEY UPDATE` fires on whichever unique key the incoming row violates. `id` is excluded from the update — it identifies the row and must not be silently rewritten — so the store checked afterwards that the returned row was the one it aimed at, by comparing the returned `id` with the one passed in.
196
+
197
+ That is the shape of the dangerous case, and it is also the shape of every correct update: an upsert that legitimately matches on the caller's own columns returns THAT row's id, never the freshly minted one. So the check fired on the case it existed to permit. The `op` classification one line below it (`id unchanged → insert, else update`) is written for a branch the throw made unreachable.
198
+
199
+ What decides it is the conflict-column VALUES, which are excluded from the update too and therefore still carry the matched row's own: equal to the input means the named key is what matched, and no other row can carry those values because the column set is unique. A difference means the statement was sent somewhere the named key could not have sent it — still refused, and the message now names the differing COLUMN instead of asserting a constraint it cannot identify. Not its values: that sentence reaches the client, and a conflict column is as likely to hold an email address as a team id — the same line `ConstraintViolation` draws when it refuses to carry the driver's own sentence. The values go to the server log, where the row already is. A conflict column the database GENERATES cannot be compared; when none of them can, that is said rather than resolved to "fine".
200
+
201
+ The parity suite gained the case that was missing: every upsert scenario in it conflicted on `id`, so the returned id always equalled the one sent and a store could not be caught being wrong about this. It now upserts on a composite unique key from a fresh id on every dialect, with a second row that must NOT be overwritten.
202
+
203
+ The LOOKUP half of the same operation — taken by every MySQL upsert, and on MariaDB by a partial row or a function `update` — now refuses clearly when a conflict column is database-GENERATED, instead of failing inside the driver.
204
+
205
+ It matches by value, and the value of a generated column is not in the row the caller passed: the classic partial-unique index on this engine IS such a column (a STORED expression plus NULL-distinct semantics, so the key constrains only the rows the expression marks). Binding the absent value reached mysql2 as `Bind parameters must not contain undefined` — a TypeError naming no column, no table and no cause, for a write that did nothing. Binding SQL NULL would make the predicate never true, so the lookup finds nothing and falls through to an INSERT the very key rejects; dropping the column would widen the lookup and can match a row the write would not have collided with, which is the one outcome that overwrites somebody else's data.
206
+
207
+ So it refuses, names the column, and points at the path that CAN answer: the single-statement form, where the database evaluates the expression itself. That path is reached on MariaDB with a complete insert row and a column-list `update`.
208
+ - **@voltro/kv** — Closing a RESP client can no longer take the process down.
209
+
210
+ A previous round added a silent `error` listener so an ioredis client without one could not crash the process. That is a different failure, and a deployment on `voltro dev` measured the difference: after the fix, roughly one reload in three still ended in `Error: Connection is closed.` thrown from `ioredis/built/redis/event_handler.js`, unhandled, with no framework frame in the stack.
211
+
212
+ That is not an `error` event. `event_handler.js`'s `close` is where ioredis REJECTS the promises of commands that were in flight when the socket went — and `quit()` is one of those commands. Calling it on a connection that is already closing rejects, and `close: () => c.quit().then(() => undefined)` had no `catch`. A reload closes the client while the previous instance is still shutting down, which is the race.
213
+
214
+ `close()` now cannot reject, and falls back to `disconnect()` so giving up on the error does not mean giving up on the teardown — a `quit` that failed has not necessarily closed anything, and a socket leaked per reload is its own slow failure. Swallowing is right here and nowhere else in that file: a failed COMMAND reaches its caller as a `KvError`, because that is where it can be acted on, and a failed close has no such place.
215
+ - **@voltro/plugin-broadcast** — A subscription's teardown can no longer kill the process.
216
+
217
+ `void sub.unsubscribe(channel)` in the dispose function returned by the redis broadcast provider was fire-and-forget with no `catch`. ioredis rejects the promises of every command in flight when the socket closes — `Connection is closed.`, thrown from its own close handler — so a `voltro dev` reload, which disposes subscriptions while the previous connection is going away, produced an unhandled rejection and node killed the process.
218
+
219
+ This is the THIRD round on one symptom with three different causes: a client with no `error` listener, `quit()`'s unguarded promise, and now a discarded command. Each fix was correct and none of them was the next one, so this round adds a rule instead of a fourth fix: `scripts/check-resp-fire-and-forget.mjs` (CI + gate, with a `--selftest`) fails when a discarded RESP command has no `.catch`.
220
+
221
+ The rule is deliberately narrow. The general property is `no-floating-promises`, which this repo has no eslint to run — and measured before writing it, a blanket `void <call>` rule matches **194** sites in `packages/*/src`, nearly all legitimate. The RESP subset matches 3, all correct. Widening the command list to "any method" costs the 194; the header says so, next to the number.
222
+
223
+ The local half of the teardown still runs unconditionally: detaching the message handler is synchronous and cannot fail, and it is what stops a reloaded module receiving the previous one's messages.
224
+ - **@voltro/runtime** — A schedule firing that is rejected before its handler runs no longer outlives itself. `runHandler` wrote the `running` row, armed the watchdog, armed the heartbeat, and only then built the app context — with the `try` whose `finally` releases all three starting after them. The context build is the one step in that window that can throw on input the scheduler does not control, and when it did, each acquisition survived in its own way:
225
+
226
+ - the watchdog rejected a promise `Promise.race` never received, `maxRuntimeMs` after a firing that never started. Nothing was listening, so the rejection went unhandled and ended the process — with no recorded failure in between to connect the two events. Lowering `maxRuntimeMs`, the obvious response to a run that appears stuck, only widened the gap; - the heartbeat kept stamping `heartbeatAt` on a run that had not begun. That beat is what tells a live run from a corpse, so it certified one: a peer's overlap guard reads a beating row as alive for the life of the process, turning a bounded stale window into a permanent skip for that schedule; - the `catch` never ran either, so the row stayed `running` — the only artefact the firing left behind, saying the opposite of what happened.
227
+
228
+ Everything that acquires now lives inside the `try`, so a throw from anywhere records `failed` with its reason and releases both timers. The watchdog promise additionally carries a discarded rejection handler, which makes the CONSEQUENCE of a future leak impossible rather than only this one: `race` still sees the rejection, and node can never see it as unhandled.
229
+ - **@voltro/runtime, @voltro/cli** — A `defineStream`'s declared `guards:` are enforced — before the executor runs.
230
+
231
+ Measured against a running api: two `POST /rpc` calls with no Authorization header, milliseconds apart, both descriptors declaring the same scope. The QUERY refused with `ScopeError`; the STREAM answered tokens. It had called a model and paid for it, so the missing check was not only a read permission — an open stream is an open wallet.
232
+
233
+ Two defects, and the second survives fixing the first.
234
+
235
+ **Nothing passed the guards.** `bindStream` has taken a `guards` argument since it was written, implements the subscribe-time gate and the per-element re-check, and documents both — and neither boot path passed it. Declared, documented, implemented, wired at zero call sites. The call's input goes with it, because a resource-scoped guard (`{ scope: 'x:read', from: 'id' }`) reads which resource was asked for out of it and otherwise fails closed.
236
+
237
+ **The gate ran after `buildStream`.** That is where the user's executor runs, so an unauthorized caller had already been served by the expensive half by the time authorization was decided. Passing the guards alone would have stopped the tokens reaching the client while still paying for them, and from the client side those two are identical. The doc comment said "before anything is registered"; the code registered first. It is first now.
238
+
239
+ This is the SECOND time this exact defect shipped: `defineEvent` accepted `guards:`, documented them, and nothing checked. That was fixed for events and `defineStream` kept the identical hole. So the test asserts the property over the SET — all five primitives that accept `guards:`, each at its named enforcement point — rather than over the one that was reported. Query, mutation, action and event were all already enforced; the stream was the only gap.
240
+
241
+ ---
242
+
42
243
  ## [0.57.0] — 2026-08-30
43
244
 
44
245
  ### Added
package/dist/index.js CHANGED
@@ -1,32 +1,32 @@
1
1
  import { MysqlClient as e } from "@effect/sql-mysql2";
2
2
  import { Config as t, Effect as n, Layer as r, ManagedRuntime as i, Option as a, Redacted as o, Schedule as s } from "effect";
3
- import { CDC_OFFSETS_TABLE as c, DEFAULT_ACQUIRE_TIMEOUT_MS as l, EagerCardinalityError as u, _voltroCdcOffsetsTable as d, attachEagerLoads as f, attributionFields as p, attributionKey as m, beginLocalWrite as h, bulkInsertLimitsFor as g, chunkRowsForInsert as _, compileEagerJson as v, compilePredicate as y, compileRawFragment as b, compileSelect as x, decodeRowsFromSchema as S, describeRowBindings as C, encodeRowForSchema as w, endLocalWrite as T, externalChangeEvent as E, getTable as D, hasEagerLoads as O, isTableReactive as k, makeEagerFallbackReporter as ee, observeDbOp as A, qualifyTable as te, raiseChangeListenerCeiling as ne, recordsTable as j, registerPendingAttribution as M, requireTable as N, resolveEchoAttribution as re, runStoreTransaction as ie, runWriteRecorders as ae, stampGeneratedId as P, stampGeneratedIds as F, withCapturedAttribution as I } from "@voltro/database";
4
- import { EventEmitter as oe } from "node:events";
5
- import { createLogger as L } from "@voltro/logger";
6
- import { SqlClient as R, TransactionConnection as z } from "@effect/sql/SqlClient";
3
+ import { CDC_OFFSETS_TABLE as c, DEFAULT_ACQUIRE_TIMEOUT_MS as l, EagerCardinalityError as u, _voltroCdcOffsetsTable as d, attachEagerLoads as f, attributionFields as p, attributionKey as m, beginLocalWrite as h, bulkInsertLimitsFor as g, chunkRowsForInsert as _, compileEagerJson as v, compilePredicate as y, compileRawFragment as b, compileSelect as x, decodeRowsFromSchema as S, describeRowBindings as C, encodeRowForSchema as w, endLocalWrite as T, externalChangeEvent as E, getTable as D, hasEagerLoads as O, isTableReactive as k, makeEagerFallbackReporter as ee, observeDbOp as A, qualifyTable as te, raiseChangeListenerCeiling as ne, recordsTable as j, refuseWrite as M, registerPendingAttribution as re, requireTable as ie, resolveEchoAttribution as ae, runStoreTransaction as oe, runWriteRecorders as N, stampGeneratedId as P, stampGeneratedIds as se, withCapturedAttribution as F } from "@voltro/database";
4
+ import { EventEmitter as ce } from "node:events";
5
+ import { createLogger as I } from "@voltro/logger";
6
+ import { SqlClient as L, TransactionConnection as R } from "@effect/sql/SqlClient";
7
7
  //#region src/sqlLayer.ts
8
- var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
8
+ var le = (e) => e ? { rejectUnauthorized: !1 } : void 0, ue = (e) => {
9
9
  let t = e.acquireTimeoutMs ?? l;
10
10
  return t > 0 ? t : void 0;
11
- }, le = (e) => {
11
+ }, de = (e) => {
12
12
  let t = e.acquireQueueLimit;
13
13
  return t !== void 0 && t > 0 ? t : void 0;
14
- }, ue = (e) => {
15
- let t = e.ssl === void 0 ? void 0 : se(e.ssl), n = ce(e), r = le(e);
14
+ }, fe = (e) => {
15
+ let t = e.ssl === void 0 ? void 0 : le(e.ssl), n = ue(e), r = de(e);
16
16
  return {
17
17
  ...t === void 0 ? {} : { ssl: t },
18
18
  ...n === void 0 ? {} : { connectTimeout: n },
19
19
  ...r === void 0 ? {} : { queueLimit: r }
20
20
  };
21
- }, B = (n) => e.layerConfig({
21
+ }, z = (n) => e.layerConfig({
22
22
  host: t.succeed(n.host),
23
23
  port: t.succeed(n.port),
24
24
  username: t.succeed(n.username),
25
25
  password: t.succeed(o.make(n.password)),
26
26
  database: t.succeed(n.database),
27
- poolConfig: t.succeed(ue(n)),
27
+ poolConfig: t.succeed(fe(n)),
28
28
  ...n.maxConnections === void 0 ? {} : { maxConnections: t.succeed(n.maxConnections) }
29
- }), V = (e) => {
29
+ }), B = (e) => {
30
30
  let t = e.get("sslmode");
31
31
  if (t !== null) {
32
32
  if (t === "require") return !0;
@@ -39,13 +39,13 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
39
39
  if (n === "false" || n === "0") return !1;
40
40
  throw Error(`DB_URL '?ssl=${n}' is not supported by the mysql/mariadb dialect — use 'true'/'1' or 'false'/'0'.`);
41
41
  }
42
- }, H = (e) => {
42
+ }, V = (e) => {
43
43
  let t = {
44
44
  ...e.acquireTimeoutMs === void 0 ? {} : { acquireTimeoutMs: e.acquireTimeoutMs },
45
45
  ...e.acquireQueueLimit === void 0 ? {} : { acquireQueueLimit: e.acquireQueueLimit }
46
46
  };
47
47
  if (e.url) {
48
- let n = new URL(e.url), r = e.ssl ?? V(n.searchParams);
48
+ let n = new URL(e.url), r = e.ssl ?? B(n.searchParams);
49
49
  return {
50
50
  host: n.hostname || "localhost",
51
51
  port: n.port ? Number(n.port) : 3306,
@@ -67,10 +67,33 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
67
67
  ...e.ssl === void 0 ? {} : { ssl: e.ssl },
68
68
  ...t
69
69
  };
70
- }, U = (e) => B(H(e)), de = (e, t) => e === null ? !1 : t === null ? !0 : e.filename === t.filename ? e.position > t.position : e.filename > t.filename, fe = (e) => e.msSinceProgress < e.stallThresholdMs ? "healthy" : e.primary !== null && !de(e.primary, e.reader) ? "idle-caught-up" : "reconnect", pe = (e) => (e instanceof Error ? e.message : String(e ?? "")).includes("schema changed between binlog event and metadata fetch"), me = (e) => {
70
+ }, H = (e) => z(V(e)), U = (e) => e == null ? null : e instanceof Date ? String(e.getTime()) : typeof e == "bigint" ? e.toString() : typeof e == "boolean" ? e ? "1" : "0" : String(e), pe = (e, t, n) => {
71
+ let r = e.id;
72
+ if (r == null || U(t.id) === U(r)) return { kind: "sameRow" };
73
+ let i = n.filter((t) => e[t] !== void 0);
74
+ if (i.length === 0) return {
75
+ kind: "undecidable",
76
+ conflictColumns: n
77
+ };
78
+ let a = i.filter((n) => U(t[n]) !== U(e[n])).map((n) => ({
79
+ column: n,
80
+ asked: e[n],
81
+ found: t[n]
82
+ }));
83
+ return a.length > 0 ? {
84
+ kind: "otherKey",
85
+ mismatched: a
86
+ } : {
87
+ kind: "namedKey",
88
+ comparedColumns: i
89
+ };
90
+ }, me = (e, t, n) => {
91
+ let r = `MysqlStore.upsert: the row written to '${e}' is not the row that was passed in. Upserting id '${String(t)}' matched an existing row instead, and `;
92
+ return n.kind === "undecidable" ? r + `nothing could establish which key it matched: none of the conflictColumns [${n.conflictColumns.join(", ")}] were present in the row, so their values could not be compared with the ones that came back. Nothing was changed.\n → this happens when every conflict column is database-GENERATED. Name at least one column you supply yourself, or write the row with insert/update instead of upsert.` : r + `that row carries DIFFERENT values for the conflictColumns you named (${n.mismatched.map((e) => e.column).join(", ")}). Since those columns are excluded from the update, the matched row kept its own — so the statement was sent there by some OTHER unique key on this table, that row would have absorbed your values, and yours would never have been written. Nothing was changed.\n → resolve the duplicate on that other key, or upsert on the columns that actually identify the row. The differing values are in the server log for this write.`;
93
+ }, he = (e, t) => `upsert on '${e}' reached a row that does not carry the named conflict columns: ` + t.mismatched.map((e) => `${e.column}: sent ${JSON.stringify(e.asked)}, matched row has ${JSON.stringify(e.found)}`).join("; "), ge = (e, t) => e === null ? !1 : t === null ? !0 : e.filename === t.filename ? e.position > t.position : e.filename > t.filename, W = (e) => e.msSinceProgress < e.stallThresholdMs ? "healthy" : e.primary !== null && !ge(e.primary, e.reader) ? "idle-caught-up" : "reconnect", _e = (e) => (e instanceof Error ? e.message : String(e ?? "")).includes("schema changed between binlog event and metadata fetch"), ve = (e) => {
71
94
  let t = e instanceof Error ? e.message : String(e ?? "");
72
95
  return /Table\s+[^\s.]+\.(\S+)\s+schema changed between binlog event and metadata fetch/.exec(t)?.[1] ?? null;
73
- }, he = "\n SELECT DISTINCT s.TABLE_NAME AS tableName\n FROM information_schema.STATISTICS s\n JOIN information_schema.COLUMNS c\n ON c.TABLE_SCHEMA = s.TABLE_SCHEMA\n AND c.TABLE_NAME = s.TABLE_NAME\n AND c.COLUMN_NAME = s.COLUMN_NAME\n WHERE s.TABLE_SCHEMA = DATABASE()\n AND s.NON_UNIQUE = 0\n AND s.SUB_PART IS NULL\n AND c.DATA_TYPE IN ('text','tinytext','mediumtext','longtext','blob','tinyblob','mediumblob','longblob')\n", W = "This verdict is the schema as read when the reader attached; applying the remedy does not lift it by itself. A boot that migrates in-process re-checks it once its schema work is done and re-admits the table in the same run; otherwise it holds until the process restarts.", G = (e) => `cdc: table '${e}' is EXCLUDED from binlog capture — its row image carries a hidden column the reader cannot account for. Cause: a UNIQUE constraint on an UNBOUNDED text column, which MariaDB backs with a HASH long-unique index; that index adds a hidden DB_ROW_HASH_n column to the row, present in the binlog and absent from information_schema.COLUMNS. Remedy: bound the column — text().maxLength(n) — so the constraint becomes an ordinary B-tree index with no hidden column. ALTER TABLE FORCE does NOT help: the rebuild recreates the index and the hidden column. Until then, cross-instance change events for this table are lost; own-node reactivity is unaffected (writes still emit inline). ${W}`, ge = (e) => `cdc: table '${e}' looks undecodable (a UNIQUE on an UNBOUNDED text column → MariaDB hash long-unique → a hidden DB_ROW_HASH_n column in the row image). Held OUT of binlog capture for now and re-checked after this boot's schema work — a migration that bounds the column re-admits it in the same run. Not a verdict yet; the definitive line follows.`, _e = (e) => `cdc: table '${e}' re-admitted to binlog capture — the hidden-column condition is gone (the schema moved since the reader attached). Cross-instance change events for it flow again.`, ve = 12e4, ye = (e, t = ve) => {
96
+ }, ye = "\n SELECT DISTINCT s.TABLE_NAME AS tableName\n FROM information_schema.STATISTICS s\n JOIN information_schema.COLUMNS c\n ON c.TABLE_SCHEMA = s.TABLE_SCHEMA\n AND c.TABLE_NAME = s.TABLE_NAME\n AND c.COLUMN_NAME = s.COLUMN_NAME\n WHERE s.TABLE_SCHEMA = DATABASE()\n AND s.NON_UNIQUE = 0\n AND s.SUB_PART IS NULL\n AND c.DATA_TYPE IN ('text','tinytext','mediumtext','longtext','blob','tinyblob','mediumblob','longblob')\n", be = "This verdict is the schema as read when the reader attached; applying the remedy does not lift it by itself. A boot that migrates in-process re-checks it once its schema work is done and re-admits the table in the same run; otherwise it holds until the process restarts.", G = (e) => `cdc: table '${e}' is EXCLUDED from binlog capture — its row image carries a hidden column the reader cannot account for. Cause: a UNIQUE constraint on an UNBOUNDED text column, which MariaDB backs with a HASH long-unique index; that index adds a hidden DB_ROW_HASH_n column to the row, present in the binlog and absent from information_schema.COLUMNS. Remedy: bound the column — text().maxLength(n) — so the constraint becomes an ordinary B-tree index with no hidden column. ALTER TABLE FORCE does NOT help: the rebuild recreates the index and the hidden column. Until then, cross-instance change events for this table are lost; own-node reactivity is unaffected (writes still emit inline). ${be}`, xe = (e) => `cdc: table '${e}' looks undecodable (a UNIQUE on an UNBOUNDED text column → MariaDB hash long-unique → a hidden DB_ROW_HASH_n column in the row image). Held OUT of binlog capture for now and re-checked after this boot's schema work — a migration that bounds the column re-admits it in the same run. Not a verdict yet; the definitive line follows.`, Se = (e) => `cdc: table '${e}' re-admitted to binlog capture — the hidden-column condition is gone (the schema moved since the reader attached). Cross-instance change events for it flow again.`, Ce = 12e4, we = (e, t = Ce) => {
74
97
  let n = null, r = [], i = () => {
75
98
  n &&= (clearTimeout(n), null), r = [];
76
99
  };
@@ -81,7 +104,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
81
104
  for (let t of a) e("error", G(t));
82
105
  return;
83
106
  }
84
- for (let t of a) e("warn", ge(t));
107
+ for (let t of a) e("warn", xe(t));
85
108
  r = a, n = setTimeout(() => {
86
109
  let i = r;
87
110
  n = null, r = [];
@@ -93,29 +116,29 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
93
116
  i();
94
117
  for (let n of t.stillExcluded) e("error", G(n));
95
118
  for (let n of t.newlyExcluded) e("error", G(n));
96
- for (let n of t.readmitted) e("info", _e(n));
119
+ for (let n of t.readmitted) e("info", Se(n));
97
120
  },
98
121
  dispose: i
99
122
  };
100
- }, be = (e, t) => {
123
+ }, Te = (e, t) => {
101
124
  let n = new Set(e), r = new Set(t);
102
125
  return {
103
126
  stillExcluded: t.filter((e) => n.has(e)),
104
127
  readmitted: e.filter((e) => !r.has(e)),
105
128
  newlyExcluded: t.filter((e) => !n.has(e))
106
129
  };
107
- }, xe = 3e5, Se = 3, Ce = (e, t) => {
108
- let n = [...e.filter((e) => t - e < xe), t];
130
+ }, Ee = 3e5, De = 3, Oe = (e, t) => {
131
+ let n = [...e.filter((e) => t - e < Ee), t];
109
132
  return {
110
- verdict: n.length >= Se ? "persistent" : "backlog",
133
+ verdict: n.length >= De ? "persistent" : "backlog",
111
134
  hits: n
112
135
  };
113
- }, we = /* @__PURE__ */ new Set([
136
+ }, ke = /* @__PURE__ */ new Set([
114
137
  "writerows",
115
138
  "updaterows",
116
139
  "deleterows"
117
- ]), Te = /\b(alter|rename|drop|create)\s+(table|column)?/i, K = (e) => new Promise((t) => setTimeout(t, e)), q = async (e) => {
118
- let t = L({ scope: `voltro:${e.variant}:cdc` }), n;
140
+ ]), Ae = /\b(alter|rename|drop|create)\s+(table|column)?/i, K = (e) => new Promise((t) => setTimeout(t, e)), q = async (e) => {
141
+ let t = I({ scope: `voltro:${e.variant}:cdc` }), n;
119
142
  try {
120
143
  n = (await import("@vlasky/zongji")).default;
121
144
  } catch (t) {
@@ -130,14 +153,14 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
130
153
  o = n.binlogName;
131
154
  return;
132
155
  }
133
- if (u === "query" && n.query && Te.test(n.query)) {
156
+ if (u === "query" && n.query && Ae.test(n.query)) {
134
157
  l && (l.tableMap = {});
135
158
  return;
136
159
  }
137
160
  if (n.nextPosition && o && (s = {
138
161
  filename: o,
139
162
  position: n.nextPosition
140
- }, e.onPosition?.(s)), !we.has(u)) return;
163
+ }, e.onPosition?.(s)), !ke.has(u)) return;
141
164
  let d = n.tableMap[n.tableId];
142
165
  if (!d || d.parentSchema !== a) return;
143
166
  let f = d.tableName;
@@ -215,9 +238,9 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
215
238
  l?.stop();
216
239
  } catch {}
217
240
  if (d++, await K(Math.min(3e4, 500 * 2 ** Math.min(d, 6))), u) return;
218
- let i = s, a = w(n), c = !a && pe(n), f = !1;
241
+ let i = s, a = w(n), c = !a && _e(n), f = !1;
219
242
  if (c) {
220
- let e = me(n), i = e ?? "<unknown>", { verdict: a, hits: o } = Ce(h.get(i) ?? [], Date.now());
243
+ let e = ve(n), i = e ?? "<unknown>", { verdict: a, hits: o } = Oe(h.get(i) ?? [], Date.now());
221
244
  h.set(i, o), f = a === "persistent", f && !g.has(i) && (g.add(i), e !== null && r.add(e), t.error(G(i)));
222
245
  }
223
246
  (a || c) && (f || t.warn(c ? "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"), i = e.resolveStartPosition ? await e.resolveStartPosition().catch(() => null) : null, o = i?.filename ?? null, s = i, f || e.onResync?.());
@@ -250,7 +273,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
250
273
  if (u || p || Date.now() - f < v) return;
251
274
  let n = null;
252
275
  if (e.resolveStartPosition && (n = await e.resolveStartPosition().catch(() => null)), u || p) return;
253
- let r = fe({
276
+ let r = W({
254
277
  msSinceProgress: Date.now() - f,
255
278
  stallThresholdMs: v,
256
279
  primary: n,
@@ -298,7 +321,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
298
321
  }
299
322
  }
300
323
  };
301
- }, Ee = /* @__PURE__ */ new Set(["1213", "1205"]), De = (e) => {
324
+ }, je = /* @__PURE__ */ new Set(["1213", "1205"]), Me = (e) => {
302
325
  let t = e;
303
326
  for (let e = 0; e < 5 && typeof t == "object" && t; e++) {
304
327
  let e = t.errno;
@@ -308,8 +331,8 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
308
331
  t = t.cause;
309
332
  }
310
333
  }, J = (e) => {
311
- let t = De(e);
312
- return t !== void 0 && Ee.has(t);
334
+ let t = Me(e);
335
+ return t !== void 0 && je.has(t);
313
336
  }, Y = (e) => J(e) ? "retry" : "noRetry", X = (e) => {
314
337
  if (e == null) return "null";
315
338
  let t = typeof e;
@@ -319,19 +342,19 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
319
342
  if (Array.isArray(e)) return `[${e.map(X).join(",")}]`;
320
343
  let n = e;
321
344
  return `{${Object.keys(n).sort().map((e) => `${JSON.stringify(e)}:${X(n[e])}`).join(",")}}`;
322
- }, Oe = (e) => {
345
+ }, Ne = (e) => {
323
346
  let t = X(e), n = 2166136261;
324
347
  for (let e = 0; e < t.length; e++) n ^= t.charCodeAt(e), n = Math.imul(n, 16777619);
325
348
  return (n >>> 0).toString(36);
326
- }, ke = (e, t) => {
349
+ }, Pe = (e, t) => {
327
350
  let n = setTimeout(e, t);
328
351
  typeof n.unref == "function" && n.unref();
329
- }, Ae = class {
352
+ }, Fe = class {
330
353
  variant;
331
354
  ttlMs;
332
355
  schedule;
333
356
  seen = /* @__PURE__ */ new Map();
334
- constructor(e, t = 6e4, n = ke) {
357
+ constructor(e, t = 6e4, n = Pe) {
335
358
  this.variant = e, this.ttlMs = t, this.schedule = n;
336
359
  }
337
360
  key(e) {
@@ -345,7 +368,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
345
368
  } catch {
346
369
  r = t;
347
370
  }
348
- return `${e.table} ${e.op} ${String(n)} ${Oe(r)}`;
371
+ return `${e.table} ${e.op} ${String(n)} ${Ne(r)}`;
349
372
  }
350
373
  admit(e) {
351
374
  let t = this.key(e);
@@ -365,11 +388,11 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
365
388
  1062,
366
389
  1586
367
390
  ]), Q = async (e) => {
368
- let t = e.variant ?? "mysql", n = L({ scope: `voltro:${t}` }), a = e.changeStrategy ?? "inline", o = a;
391
+ let t = e.variant ?? "mysql", n = I({ scope: `voltro:${t}` }), a = e.changeStrategy ?? "inline", o = a;
369
392
  a === "cdc" && !e.cdcConfig && (n.warn("changeStrategy='cdc' requires cdcConfig (serverId + connection); falling back to 'inline'."), o = "inline");
370
- let s = e.tracerLayer ? r.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, c = i.make(s), l = new je(await c.runPromise(R), c, t, o);
393
+ let s = e.tracerLayer ? r.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, c = i.make(s), l = new Ie(await c.runPromise(L), c, t, o);
371
394
  return o === "cdc" && e.cdcConfig && await l.startCdcConsumer(e.cdcConfig), l;
372
- }, je = class e {
395
+ }, Ie = class e {
373
396
  sql;
374
397
  runtime;
375
398
  variant;
@@ -390,9 +413,9 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
390
413
  cdcGate;
391
414
  reportEagerFallback;
392
415
  constructor(e, t, n, r = "inline", i = null, a, o) {
393
- this.sql = e, this.runtime = t, this.variant = n, this.changeStrategy = r, this.namespace = i, this.log = L({ scope: `voltro:${n}` }), this.cdcReporter = ye((e, t) => {
416
+ this.sql = e, this.runtime = t, this.variant = n, this.changeStrategy = r, this.namespace = i, this.log = I({ scope: `voltro:${n}` }), this.cdcReporter = we((e, t) => {
394
417
  e === "error" ? this.log.error(t) : e === "warn" ? this.log.warn(t) : this.log.info(t);
395
- }), this.reportEagerFallback = ee(this.log), this.emitter = a ?? new oe(), ne(this.emitter), this.cdcGate = o ?? new Ae(n);
418
+ }), this.reportEagerFallback = ee(this.log), this.emitter = a ?? new ce(), ne(this.emitter), this.cdcGate = o ?? new Fe(n);
396
419
  }
397
420
  withNamespace(t) {
398
421
  return t === this.namespace ? this : new e(this.sql, this.runtime, this.variant, this.changeStrategy, t, this.emitter, this.cdcGate);
@@ -410,7 +433,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
410
433
  };
411
434
  }
412
435
  async executeQuery(e, t, r) {
413
- let i = x(e, this.sql, this.namespace), a = t ? n.provideService(i, z, t) : i, o = await A(this.variant, "select", () => this.runtime.runPromise(a));
436
+ let i = x(e, this.sql, this.namespace), a = t ? n.provideService(i, R, t) : i, o = await A(this.variant, "select", () => this.runtime.runPromise(a));
414
437
  return S(o, e.table, this.variant);
415
438
  }
416
439
  get supportsInsertReturning() {
@@ -426,7 +449,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
426
449
  t = P(e, t);
427
450
  let o = this.sql;
428
451
  if (this.supportsInsertReturning) {
429
- let s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(w(t, e))} RETURNING *`, c = r ? n.provideService(s, z, r) : s, l = (await this.runtime.runPromise(c))[0];
452
+ let s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(w(t, e))} RETURNING *`, c = r ? n.provideService(s, R, r) : s, l = (await this.runtime.runPromise(c))[0];
430
453
  if (!l) throw Error(`MysqlStore.insert: no row returned for table '${e}'`);
431
454
  return await this.routeEvent({
432
455
  table: e,
@@ -445,9 +468,9 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
445
468
  new: t
446
469
  }, i, r, a), t;
447
470
  }
448
- let l = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(s)}`, u = r ? n.provideService(l, z, r) : l;
471
+ let l = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(s)}`, u = r ? n.provideService(l, R, r) : l;
449
472
  await this.runtime.runPromise(u);
450
- let d = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${c}`, f = r ? n.provideService(d, z, r) : d, p = (await this.runtime.runPromise(f))[0];
473
+ let d = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${c}`, f = r ? n.provideService(d, R, r) : d, p = (await this.runtime.runPromise(f))[0];
451
474
  if (!p) throw Error(`MysqlStore.insert: row not found post-insert in '${e}'`);
452
475
  return await this.routeEvent({
453
476
  table: e,
@@ -460,16 +483,16 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
460
483
  let i = this.sql;
461
484
  return this.runPinned(r, (r) => n.gen(this, function* () {
462
485
  let a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(t)}`;
463
- yield* n.provideService(a, z, r);
464
- let o = (yield* n.provideService(i`SELECT LAST_INSERT_ID() AS ${i("lastId")}`, z, r))[0]?.lastId;
465
- return o === void 0 || Number(o) === 0 ? yield* n.fail(/* @__PURE__ */ Error(`MysqlStore.insert: LAST_INSERT_ID() returned no id for '${e}' — is the primary key AUTO_INCREMENT?`)) : (yield* n.provideService(i`SELECT * FROM ${i(this.nsT(e))} WHERE ${i("id")} = ${o}`, z, r))[0] || (yield* n.fail(/* @__PURE__ */ Error(`MysqlStore.insert: row not found post-insert in '${e}'`)));
486
+ yield* n.provideService(a, R, r);
487
+ let o = (yield* n.provideService(i`SELECT LAST_INSERT_ID() AS ${i("lastId")}`, R, r))[0]?.lastId;
488
+ return o === void 0 || Number(o) === 0 ? yield* n.fail(/* @__PURE__ */ Error(`MysqlStore.insert: LAST_INSERT_ID() returned no id for '${e}' — is the primary key AUTO_INCREMENT?`)) : (yield* n.provideService(i`SELECT * FROM ${i(this.nsT(e))} WHERE ${i("id")} = ${o}`, R, r))[0] || (yield* n.fail(/* @__PURE__ */ Error(`MysqlStore.insert: row not found post-insert in '${e}'`)));
466
489
  }), "insert");
467
490
  }
468
491
  async runPinned(e, t, r) {
469
492
  if (e) return this.runtime.runPromise(t(e));
470
493
  this.inflightTxns++;
471
494
  try {
472
- let e = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(z), (e) => a.isNone(e) ? n.fail(/* @__PURE__ */ Error(`MysqlStore.${r}: TransactionConnection missing.`)) : t(e.value)))), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(J)), o = e.pipe(n.retry(i), n.withSpan(`store.${r}`, { attributes: {
495
+ let e = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(R), (e) => a.isNone(e) ? n.fail(/* @__PURE__ */ Error(`MysqlStore.${r}: TransactionConnection missing.`)) : t(e.value)))), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(J)), o = e.pipe(n.retry(i), n.withSpan(`store.${r}`, { attributes: {
473
496
  "db.system": this.variant,
474
497
  "db.operation": r
475
498
  } }));
@@ -479,16 +502,16 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
479
502
  }
480
503
  }
481
504
  async executeInsertMany(e, t, r, i, a) {
482
- if (t = F(e, t), t.length === 0) return [];
505
+ if (t = se(e, t), t.length === 0) return [];
483
506
  let o = this.sql, s = t.map((t) => w(t, e)), c = _(s, g(this.variant));
484
507
  if (this.supportsInsertReturning) {
485
508
  let t = (t) => o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t)} RETURNING *`, s;
486
509
  if (c.length === 1) {
487
- let e = t(c[0]), i = r ? n.provideService(e, z, r) : e;
510
+ let e = t(c[0]), i = r ? n.provideService(e, R, r) : e;
488
511
  s = await this.runtime.runPromise(i);
489
512
  } else if (r) {
490
513
  let e = [];
491
- for (let i of c) e.push(...await this.runtime.runPromise(n.provideService(t(i), z, r)));
514
+ for (let i of c) e.push(...await this.runtime.runPromise(n.provideService(t(i), R, r)));
492
515
  s = e;
493
516
  } else s = await this.runtime.runPromise(o.withTransaction(n.map(n.forEach(c, t, { concurrency: 1 }), (e) => e.flat())));
494
517
  for (let t of s) await this.routeEvent({
@@ -513,16 +536,16 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
513
536
  if (l.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).");
514
537
  let u = (t) => o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t)}`;
515
538
  if (c.length === 1) {
516
- let e = r ? n.provideService(u(c[0]), z, r) : u(c[0]);
539
+ let e = r ? n.provideService(u(c[0]), R, r) : u(c[0]);
517
540
  await this.runtime.runPromise(e);
518
- } else if (r) for (let e of c) await this.runtime.runPromise(n.provideService(u(e), z, r));
541
+ } else if (r) for (let e of c) await this.runtime.runPromise(n.provideService(u(e), R, r));
519
542
  else await this.runtime.runPromise(o.withTransaction(n.forEach(c, u, {
520
543
  concurrency: 1,
521
544
  discard: !0
522
545
  })));
523
546
  let d = _(l.map((e) => ({ id: e })), g(this.variant)), f = [];
524
547
  for (let t of d) {
525
- let i = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} IN ${o.in(t.map((e) => e.id))}`, a = r ? n.provideService(i, z, r) : i;
548
+ let i = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} IN ${o.in(t.map((e) => e.id))}`, a = r ? n.provideService(i, R, r) : i;
526
549
  f.push(...await this.runtime.runPromise(a));
527
550
  }
528
551
  let p = new Map(f.map((e) => [e.id, e])), m = l.map((e) => p.get(e)).filter((e) => e !== void 0);
@@ -540,19 +563,19 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
540
563
  let a = [];
541
564
  for (let o of t) {
542
565
  let t = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(o)}`;
543
- yield* n.provideService(t, z, r);
544
- let s = (yield* n.provideService(i`SELECT LAST_INSERT_ID() AS ${i("firstId")}`, z, r))[0]?.firstId;
566
+ yield* n.provideService(t, R, r);
567
+ let s = (yield* n.provideService(i`SELECT LAST_INSERT_ID() AS ${i("firstId")}`, R, r))[0]?.firstId;
545
568
  if (s === void 0 || Number(s) === 0) return yield* n.fail(/* @__PURE__ */ Error(`MysqlStore.insertMany: LAST_INSERT_ID() returned no id for '${e}' — is the primary key AUTO_INCREMENT?`));
546
- let c = Number(s), l = yield* n.provideService(i`SELECT * FROM ${i(this.nsT(e))} WHERE ${i("id")} BETWEEN ${c} AND ${c + o.length - 1} ORDER BY ${i("id")} ASC`, z, r);
569
+ let c = Number(s), l = yield* n.provideService(i`SELECT * FROM ${i(this.nsT(e))} WHERE ${i("id")} BETWEEN ${c} AND ${c + o.length - 1} ORDER BY ${i("id")} ASC`, R, r);
547
570
  a.push(...l);
548
571
  }
549
572
  return a;
550
573
  }), "insert");
551
574
  }
552
575
  async executePatchJson(e, t, r, i, a, o, s) {
553
- let c = this.sql, l = r.split("."), u = l[0], d = l.slice(1), f = JSON.stringify(i ?? null), p = d.length === 0 ? "$" : `$.${d.join(".")}`, m = c`UPDATE ${c(this.nsT(e))} SET ${c(u)} = JSON_SET(COALESCE(${c(u)}, '{}'), ${p}, CAST(${f} AS JSON)) WHERE ${c("id")} = ${t}`, h = a ? n.provideService(m, z, a) : m, g = await this.runtime.runPromise(h);
576
+ let c = this.sql, l = r.split("."), u = l[0], d = l.slice(1), f = JSON.stringify(i ?? null), p = d.length === 0 ? "$" : `$.${d.join(".")}`, m = c`UPDATE ${c(this.nsT(e))} SET ${c(u)} = JSON_SET(COALESCE(${c(u)}, '{}'), ${p}, CAST(${f} AS JSON)) WHERE ${c("id")} = ${t}`, h = a ? n.provideService(m, R, a) : m, g = await this.runtime.runPromise(h);
554
577
  if (g && typeof g.affectedRows == "number" && g.affectedRows === 0) return null;
555
- let _ = c`SELECT * FROM ${c(this.nsT(e))} WHERE ${c("id")} = ${t}`, v = a ? n.provideService(_, z, a) : _, y = (await this.runtime.runPromise(v))[0];
578
+ let _ = c`SELECT * FROM ${c(this.nsT(e))} WHERE ${c("id")} = ${t}`, v = a ? n.provideService(_, R, a) : _, y = (await this.runtime.runPromise(v))[0];
556
579
  return y ? (await this.routeEvent({
557
580
  table: e,
558
581
  op: "update",
@@ -563,7 +586,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
563
586
  async executeUpdate(e, t, r, i, a, o) {
564
587
  let s = this.sql;
565
588
  if (this.supportsUpdateReturning) {
566
- let c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(w(r, e))} WHERE ${s("id")} = ${t} RETURNING *`, l = i ? n.provideService(c, z, i) : c, u = (await this.runtime.runPromise(l))[0];
589
+ let c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(w(r, e))} WHERE ${s("id")} = ${t} RETURNING *`, l = i ? n.provideService(c, R, i) : c, u = (await this.runtime.runPromise(l))[0];
567
590
  return u ? (await this.routeEvent({
568
591
  table: e,
569
592
  op: "update",
@@ -571,9 +594,9 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
571
594
  new: u
572
595
  }, a, i, o), u) : null;
573
596
  }
574
- let c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(w(r, e))} WHERE ${s("id")} = ${t}`, l = i ? n.provideService(c, z, i) : c, u = await this.runtime.runPromise(l);
597
+ let c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(w(r, e))} WHERE ${s("id")} = ${t}`, l = i ? n.provideService(c, R, i) : c, u = await this.runtime.runPromise(l);
575
598
  if (u && typeof u.affectedRows == "number" && u.affectedRows === 0) return null;
576
- let d = s`SELECT * FROM ${s(this.nsT(e))} WHERE ${s("id")} = ${t}`, f = i ? n.provideService(d, z, i) : d, p = (await this.runtime.runPromise(f))[0];
599
+ let d = s`SELECT * FROM ${s(this.nsT(e))} WHERE ${s("id")} = ${t}`, f = i ? n.provideService(d, R, i) : d, p = (await this.runtime.runPromise(f))[0];
577
600
  return p ? (await this.routeEvent({
578
601
  table: e,
579
602
  op: "update",
@@ -584,7 +607,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
584
607
  async executeDelete(e, t, r, i, a) {
585
608
  let o = this.sql;
586
609
  if (this.supportsDeleteReturning) {
587
- let s = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t} RETURNING *`, c = r ? n.provideService(s, z, r) : s, l = (await this.runtime.runPromise(c))[0];
610
+ let s = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t} RETURNING *`, c = r ? n.provideService(s, R, r) : s, l = (await this.runtime.runPromise(c))[0];
588
611
  return l ? (await this.routeEvent({
589
612
  table: e,
590
613
  op: "delete",
@@ -592,9 +615,9 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
592
615
  new: null
593
616
  }, i, r, a), !0) : !1;
594
617
  }
595
- let s = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t}`, c = r ? n.provideService(s, z, r) : s, l = (await this.runtime.runPromise(c))[0];
618
+ let s = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t}`, c = r ? n.provideService(s, R, r) : s, l = (await this.runtime.runPromise(c))[0];
596
619
  if (!l) return !1;
597
- let u = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t}`, d = r ? n.provideService(u, z, r) : u;
620
+ let u = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t}`, d = r ? n.provideService(u, R, r) : u;
598
621
  return await this.runtime.runPromise(d), await this.routeEvent({
599
622
  table: e,
600
623
  op: "delete",
@@ -608,20 +631,20 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
608
631
  async appendInTxn(t, r, i) {
609
632
  let a = this.sql, o = w(r, t), s = a`INSERT INTO ${a(this.nsT(t))} ${a.insert(o)}`;
610
633
  try {
611
- await this.runtime.runPromise(i ? n.provideService(s, z, i) : s);
634
+ await this.runtime.runPromise(i ? n.provideService(s, R, i) : s);
612
635
  } catch (n) {
613
636
  throw e.bindingContextFor(n, t, o);
614
637
  }
615
638
  }
616
639
  async maxInTxn(e, t, r, i) {
617
- let a = this.sql, o = Object.entries(r).map(([e, t]) => a`${a(e)} = ${t}`), s = a`SELECT MAX(${a(t)}) AS ${a("m")} FROM ${a(this.nsT(e))} WHERE ${a.and(o)}`, c = (await this.runtime.runPromise(i ? n.provideService(s, z, i) : s))[0]?.m;
640
+ let a = this.sql, o = Object.entries(r).map(([e, t]) => a`${a(e)} = ${t}`), s = a`SELECT MAX(${a(t)}) AS ${a("m")} FROM ${a(this.nsT(e))} WHERE ${a.and(o)}`, c = (await this.runtime.runPromise(i ? n.provideService(s, R, i) : s))[0]?.m;
618
641
  return c == null ? null : Number(c);
619
642
  }
620
643
  async routeEvent(e, t, n = null, r) {
621
644
  if (e = {
622
645
  ...p(r),
623
646
  ...e
624
- }, j(e.table) && await ae({
647
+ }, j(e.table) && await N({
625
648
  append: (e, t) => this.appendInTxn(e, t, n),
626
649
  maxOf: (e, t, r) => this.maxInTxn(e, t, r, n)
627
650
  }, {
@@ -633,7 +656,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
633
656
  subjectId: e.subjectId
634
657
  }), this.changeStrategy === "cdc") {
635
658
  let t = (e.op === "delete" ? e.old : e.new)?.id;
636
- t != null && M(m(e.table, e.op, t), {
659
+ t != null && re(m(e.table, e.op, t), {
637
660
  ...e.traceId === void 0 ? {} : { traceId: e.traceId },
638
661
  ...e.subjectId === void 0 ? {} : { subjectId: e.subjectId }
639
662
  });
@@ -671,16 +694,20 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
671
694
  }
672
695
  return await this.executeUpdate(e, o.id, a, r, i) ?? o;
673
696
  }
674
- return I((n) => this.executeInsert(e, t, r, i, n));
697
+ return F((n) => this.executeInsert(e, t, r, i, n));
675
698
  }
676
699
  async executeMariadbUpsert(e, t, r, i, a, o) {
677
- let s = this.sql, c = w(t, e), l = Object.keys(c).filter((e) => c[e] !== void 0), u = r.update === void 0 ? l.filter((e) => e !== "id" && !r.conflictColumns.includes(e)) : r.update, d = u.length > 0 ? s.csv(u.map((e) => s`${s(e)} = VALUES(${s(e)})`)) : s`${s(r.conflictColumns[0])} = VALUES(${s(r.conflictColumns[0])})`, f = await this.runPinned(i, (i) => n.tryPromise({
700
+ let s = this.sql, c = w(t, e), l = Object.keys(c).filter((e) => c[e] !== void 0), u = r.update === void 0 ? l.filter((e) => e !== "id" && !r.conflictColumns.includes(e)) : r.update, d = u.length > 0 ? s.csv(u.map((e) => s`${s(e)} = VALUES(${s(e)})`)) : s`${s(r.conflictColumns[0])} = VALUES(${s(r.conflictColumns[0])})`, f = await this.runPinned(i, (t) => n.tryPromise({
678
701
  try: async () => {
679
- let a = s`INSERT INTO ${s(this.nsT(e))} ${s.insert(c)} ON DUPLICATE KEY UPDATE ${d} RETURNING *`, o = (await this.runtime.runPromise(n.provideService(a, z, i)))[0];
680
- if (!o) throw Error(`MysqlStore.upsert: no row returned for table '${e}'`);
681
- let l = t.id;
682
- if (l != null && o.id !== l) throw Error(`MysqlStore.upsert: the row written to '${e}' is not the row that was passed in. Upserting id '${String(l)}' matched an existing row with id '${String(o.id)}' on a DIFFERENT unique constraint than the conflictColumns [${r.conflictColumns.join(", ")}] you named, so that row would have been updated and yours never written. Nothing was changed. Name the constraint that actually collides, or resolve the duplicate first.`);
683
- return o;
702
+ let i = s`INSERT INTO ${s(this.nsT(e))} ${s.insert(c)} ON DUPLICATE KEY UPDATE ${d} RETURNING *`, a = (await this.runtime.runPromise(n.provideService(i, R, t)))[0];
703
+ if (!a) throw Error(`MysqlStore.upsert: no row returned for table '${e}'`);
704
+ let o = pe(c, a, r.conflictColumns);
705
+ if (o.kind === "otherKey" || o.kind === "undecidable") throw o.kind === "otherKey" && this.log.error(he(e, o)), M({
706
+ kind: "unique",
707
+ detail: me(e, c.id, o),
708
+ ...o.kind === "otherKey" && o.mismatched[0] !== void 0 ? { column: o.mismatched[0].column } : {}
709
+ });
710
+ return a;
684
711
  },
685
712
  catch: (e) => e
686
713
  }), "upsert"), p = t.id !== void 0 && t.id === f.id ? "insert" : "update";
@@ -692,7 +719,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
692
719
  }, a, i, o), f;
693
720
  }
694
721
  async executeInsertIgnore(e, t, r, i, a, o) {
695
- if (t = P(e, t), t.id === void 0) return await this.findByConflict(e, t, r.conflictColumns, i) || I((n) => this.executeInsert(e, t, i, a, n));
722
+ if (t = P(e, t), t.id === void 0) return await this.findByConflict(e, t, r.conflictColumns, i) || F((n) => this.executeInsert(e, t, i, a, n));
696
723
  let s = await this.runPinned(i, (i) => n.tryPromise({
697
724
  try: () => this.decideInsertIgnore(e, t, r, i),
698
725
  catch: (e) => e
@@ -705,7 +732,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
705
732
  }, a, i, o), s.row;
706
733
  }
707
734
  async decideInsertIgnore(e, t, r, i) {
708
- let a = this.sql, o = w(t, e), s = (e) => this.runtime.runPromise(n.provideService(e, z, i));
735
+ let a = this.sql, o = w(t, e), s = (e) => this.runtime.runPromise(n.provideService(e, R, i));
709
736
  if (this.supportsInsertReturning) {
710
737
  let n = (await s(a`INSERT IGNORE INTO ${a(this.nsT(e))} ${a.insert(o)} RETURNING *`))[0];
711
738
  if (n) return {
@@ -746,7 +773,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
746
773
  async findUndecodableCdcTables(e) {
747
774
  if (this.variant !== "mariadb") return [];
748
775
  try {
749
- let t = (await this.runtime.runPromise(this.sql.unsafe(he))).map((e) => String(e.tableName ?? e.TABLE_NAME ?? "")).filter((e) => e !== "");
776
+ let t = (await this.runtime.runPromise(this.sql.unsafe(ye))).map((e) => String(e.tableName ?? e.TABLE_NAME ?? "")).filter((e) => e !== "");
750
777
  return e === void 0 ? t : t.filter((t) => e.includes(t));
751
778
  } catch (e) {
752
779
  return this.log.debug(`cdc: could not probe for undecodable tables — ${e?.message ?? String(e)}`), [];
@@ -756,7 +783,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
756
783
  if (e === null) return [];
757
784
  try {
758
785
  let t = this.sql`SHOW WARNINGS`.unprepared;
759
- return (await this.runtime.runPromise(n.provideService(t, z, e))).map((e) => ({
786
+ return (await this.runtime.runPromise(n.provideService(t, R, e))).map((e) => ({
760
787
  code: Number(e.Code ?? e.code ?? 0),
761
788
  message: String(e.Message ?? e.message ?? "")
762
789
  }));
@@ -766,8 +793,10 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
766
793
  }
767
794
  async findByConflict(e, t, r, i) {
768
795
  if (r.length === 0) return;
769
- let a = this.sql, o = r.map((e) => a`${a(e)} = ${t[e]}`), s = a`SELECT * FROM ${a(this.nsT(e))} WHERE ${a.and(o)} LIMIT 1`, c = i ? n.provideService(s, z, i) : s;
770
- return (await this.runtime.runPromise(c))[0];
796
+ let a = this.sql, o = r.filter((e) => t[e] === void 0);
797
+ if (o.length > 0) throw Error(`MysqlStore.upsert: cannot look the row up by conflictColumns [${r.join(", ")}] on '${e}' — ${o.join(", ")} ${o.length === 1 ? "has" : "have"} no value in the row you passed, so there is nothing to match on. Nothing was written.\n This is what a database-GENERATED column looks like here: the server computes it, so a lookup by value cannot ask for it.\n \u2192 let the DATABASE evaluate it instead of looking the row up: on MariaDB an upsert takes the single-statement path when the row is a complete INSERT row and the \`update\` option is a column list rather than a function. Pass every not-null column and use \`update: ['col']\`.\n \u2192 or name only conflict columns you supply yourself.`);
798
+ let s = r.map((e) => a`${a(e)} = ${t[e]}`), c = a`SELECT * FROM ${a(this.nsT(e))} WHERE ${a.and(s)} LIMIT 1`, l = i ? n.provideService(c, R, i) : c;
799
+ return (await this.runtime.runPromise(l))[0];
771
800
  }
772
801
  query(e) {
773
802
  return this.runWithEager(e, null);
@@ -780,7 +809,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
780
809
  if (!O(e)) return this.executeQuery(e, t);
781
810
  let r = this.variant === "mariadb" ? "mariadb" : "mysql", i = this.namespace === null ? v(e, this.sql, r) : null;
782
811
  if (i !== null) try {
783
- let e = t ? n.provideService(i.fragment, z, t) : i.fragment, r = await A(this.variant, "select", () => this.runtime.runPromise(e));
812
+ let e = t ? n.provideService(i.fragment, R, t) : i.fragment, r = await A(this.variant, "select", () => this.runtime.runPromise(e));
784
813
  return i.decode(r);
785
814
  } catch (t) {
786
815
  if (t instanceof u) throw t;
@@ -797,7 +826,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
797
826
  reason: "not-compilable"
798
827
  });
799
828
  let a = await this.executeQuery(e, t);
800
- return f(a, e.eager, e.sourceTable ?? N(e.table), (e) => this.executeQuery(e, t));
829
+ return f(a, e.eager, e.sourceTable ?? ie(e.table), (e) => this.executeQuery(e, t));
801
830
  }
802
831
  getInternalRunWithEager() {
803
832
  return this.runWithEager.bind(this);
@@ -813,10 +842,10 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
813
842
  async localWrite(e, t, n) {
814
843
  let r = (e) => j(t) ? this.writeWithRecorders(n, e) : n(e, null, null);
815
844
  return A(this.variant, e, async () => {
816
- if (this.changeStrategy !== "cdc") return I(r);
845
+ if (this.changeStrategy !== "cdc") return F(r);
817
846
  h(t);
818
847
  try {
819
- return await I(r);
848
+ return await F(r);
820
849
  } finally {
821
850
  T(t);
822
851
  }
@@ -857,13 +886,13 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
857
886
  let i = this.sql, o = y(r.where, i, this.namespace);
858
887
  this.changeStrategy === "cdc" && h(e), this.inflightTxns++;
859
888
  try {
860
- let r = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(z), (r) => {
889
+ let r = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(R), (r) => {
861
890
  if (a.isNone(r)) return n.fail(/* @__PURE__ */ Error("MysqlStore.updateMany: TransactionConnection missing."));
862
891
  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(w(t, e))} WHERE ${o}`;
863
- return n.flatMap(n.provideService(c, z, s), (t) => {
892
+ return n.flatMap(n.provideService(c, R, s), (t) => {
864
893
  if (t.length === 0) return n.succeed([]);
865
894
  let r = t.map((e) => e.id), a = i`SELECT * FROM ${i(this.nsT(e))} WHERE ${i("id")} IN ${i.in(r)}`;
866
- return n.flatMap(n.provideService(l, z, s), () => n.provideService(a, z, s));
895
+ return n.flatMap(n.provideService(l, R, s), () => n.provideService(a, R, s));
867
896
  });
868
897
  }))), c = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(J)), l = r.pipe(n.retry(c), n.withSpan("store.updateMany", { attributes: {
869
898
  "db.system": this.variant,
@@ -896,10 +925,10 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
896
925
  }
897
926
  this.changeStrategy === "cdc" && h(e), this.inflightTxns++;
898
927
  try {
899
- let t = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(z), (t) => {
928
+ let t = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(R), (t) => {
900
929
  if (a.isNone(t)) return n.fail(/* @__PURE__ */ Error("MysqlStore.deleteMany: TransactionConnection missing."));
901
930
  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}`;
902
- return n.flatMap(n.provideService(s, z, o), (e) => e.length === 0 ? n.succeed(e) : n.as(n.provideService(c, z, o), e));
931
+ return n.flatMap(n.provideService(s, R, o), (e) => e.length === 0 ? n.succeed(e) : n.as(n.provideService(c, R, o), e));
903
932
  }))), o = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(J)), c = t.pipe(n.retry(o), n.withSpan("store.deleteMany", { attributes: {
904
933
  "db.system": this.variant,
905
934
  "db.operation": "delete"
@@ -922,7 +951,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
922
951
  }
923
952
  async startCdcConsumer(e) {
924
953
  if (this.cdcHandle) return;
925
- await this.assertBinlogConfig(), this.cdcReplicaId = e.replicaId, this.cdcAdminRuntime = i.make(B({
954
+ await this.assertBinlogConfig(), this.cdcReplicaId = e.replicaId, this.cdcAdminRuntime = i.make(z({
926
955
  ...e.connection,
927
956
  maxConnections: 1
928
957
  }));
@@ -963,7 +992,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
963
992
  let t = this.cdcAdminRuntime;
964
993
  if (t === null) return !0;
965
994
  try {
966
- let r = await t.runPromise(n.flatMap(R, (e) => e`SHOW BINARY LOGS`.unprepared));
995
+ let r = await t.runPromise(n.flatMap(L, (e) => e`SHOW BINARY LOGS`.unprepared));
967
996
  return r.length === 0 || r.some((t) => (t.Log_name ?? t.log_name) === e);
968
997
  } catch (e) {
969
998
  return this.log.debug(`cdc: could not list binary logs (${e?.message ?? String(e)}) — trusting the persisted offset`), !0;
@@ -991,7 +1020,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
991
1020
  async resolveBinlogEndOverCdc() {
992
1021
  let e = this.cdcAdminRuntime;
993
1022
  if (e === null) return null;
994
- let t = (e) => n.flatMap(R, (t) => t`${t.unsafe(e)}`.unprepared);
1023
+ let t = (e) => n.flatMap(L, (t) => t`${t.unsafe(e)}`.unprepared);
995
1024
  for (let n of this.variant === "mysql" ? ["SHOW BINARY LOG STATUS", "SHOW MASTER STATUS"] : ["SHOW MASTER STATUS", "SHOW BINARY LOG STATUS"]) try {
996
1025
  let r = (await e.runPromise(t(n)))[0];
997
1026
  if (r?.File) return {
@@ -1087,16 +1116,16 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
1087
1116
  }
1088
1117
  async emptyTablesOn(e, t) {
1089
1118
  if (e.length === 0) return;
1090
- let r = this.sql, i = (e) => this.nsT(e), a = (e) => r.unsafe(`SET FOREIGN_KEY_CHECKS = ${+!!e}`).unprepared, o = n.acquireUseRelease(a(!1), () => n.forEach(e, (e) => r`DELETE FROM ${r(i(e))}`, { discard: !0 }), () => n.orDie(n.ignore(a(!0)))), s = t === null ? r.withTransaction(o) : n.provideService(o, z, t);
1119
+ let r = this.sql, i = (e) => this.nsT(e), a = (e) => r.unsafe(`SET FOREIGN_KEY_CHECKS = ${+!!e}`).unprepared, o = n.acquireUseRelease(a(!1), () => n.forEach(e, (e) => r`DELETE FROM ${r(i(e))}`, { discard: !0 }), () => n.orDie(n.ignore(a(!0)))), s = t === null ? r.withTransaction(o) : n.provideService(o, R, t);
1091
1120
  await this.runtime.runPromise(s);
1092
1121
  }
1093
1122
  async transactional(e) {
1094
1123
  this.inflightTxns++;
1095
1124
  try {
1096
- return await ie({
1125
+ return await oe({
1097
1126
  ...this.txnSpec("MysqlStore.transactional"),
1098
1127
  work: e,
1099
- makeView: (e, t) => new Me(this, e, t)
1128
+ makeView: (e, t) => new Le(this, e, t)
1100
1129
  });
1101
1130
  } finally {
1102
1131
  this.inflightTxns--;
@@ -1129,7 +1158,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
1129
1158
  injectExternalChange(e) {
1130
1159
  if (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || !k(e.table)) return;
1131
1160
  let t = (e.op === "delete" ? e.old : e.new)?.id;
1132
- re(e.table, e.op, t, (t) => {
1161
+ ae(e.table, e.op, t, (t) => {
1133
1162
  this.emitter.emit("change", E(e, t));
1134
1163
  });
1135
1164
  }
@@ -1142,7 +1171,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
1142
1171
  readmitted: [],
1143
1172
  newlyExcluded: []
1144
1173
  };
1145
- let e = await this.findUndecodableCdcTables(this.cdcIncludeTables), t = be(this.cdcExcluded, e);
1174
+ let e = await this.findUndecodableCdcTables(this.cdcIncludeTables), t = Te(this.cdcExcluded, e);
1146
1175
  return this.cdcReporter.settle(t), this.cdcExcluded = e, (t.readmitted.length > 0 || t.newlyExcluded.length > 0) && await this.cdcHandle.setUndecodableTables(e), t;
1147
1176
  }
1148
1177
  async close(e = 5e3) {
@@ -1167,7 +1196,7 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
1167
1196
  async ping() {
1168
1197
  await this.runtime.runPromise(this.sql`SELECT 1`);
1169
1198
  }
1170
- }, Me = class {
1199
+ }, Le = class {
1171
1200
  parent;
1172
1201
  txn;
1173
1202
  attr;
@@ -1243,12 +1272,12 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
1243
1272
  this.events.length = 0;
1244
1273
  }
1245
1274
  }
1246
- }, $ = (e) => e.__mysqlReplicationFriend ?? null, Ne = () => ({
1275
+ }, $ = (e) => e.__mysqlReplicationFriend ?? null, Re = () => ({
1247
1276
  async capturePrimaryPosition(e) {
1248
1277
  let t = $(e);
1249
1278
  if (t === null) throw Error("mysqlReplicationAdapter: primary is not a MysqlStore (missing __mysqlReplicationFriend).");
1250
1279
  return t.runEffect(n.gen(function* () {
1251
- let e = yield* R, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
1280
+ let e = yield* L, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
1252
1281
  return typeof r == "string" ? r : yield* n.die("mysql/mariadb did not return a GTID set");
1253
1282
  }));
1254
1283
  },
@@ -1256,25 +1285,25 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
1256
1285
  let t = $(e);
1257
1286
  if (t === null) throw Error("mysqlReplicationAdapter: replica is not a MysqlStore.");
1258
1287
  return t.runEffect(n.gen(function* () {
1259
- let e = yield* R, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
1288
+ let e = yield* L, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
1260
1289
  return typeof r == "string" ? r : yield* n.die("mysql/mariadb replica did not return a GTID set");
1261
1290
  }));
1262
1291
  },
1263
1292
  compare(e, t) {
1264
1293
  return "behind";
1265
1294
  }
1266
- }), Pe = {
1295
+ }), ze = {
1267
1296
  id: "mysql",
1268
- makeSqlLayer: (e) => U(e),
1297
+ makeSqlLayer: (e) => H(e),
1269
1298
  makeStore: (e) => Q({
1270
1299
  ...e,
1271
1300
  variant: "mysql"
1272
1301
  }),
1273
1302
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
1274
1303
  retryFilter: Y
1275
- }, Fe = {
1304
+ }, Be = {
1276
1305
  id: "mariadb",
1277
- makeSqlLayer: (e) => U(e),
1306
+ makeSqlLayer: (e) => H(e),
1278
1307
  makeStore: (e) => Q({
1279
1308
  ...e,
1280
1309
  variant: "mariadb"
@@ -1283,4 +1312,4 @@ var se = (e) => e ? { rejectUnauthorized: !1 } : void 0, ce = (e) => {
1283
1312
  retryFilter: Y
1284
1313
  };
1285
1314
  //#endregion
1286
- export { c as CDC_OFFSETS_TABLE, e as MysqlClient, d as _voltroCdcOffsetsTable, H as connectionFromConfig, B as makeMysqlSqlLayer, U as makeMysqlSqlLayerFromConfig, Q as makeMysqlStore, Fe as mariadbDialect, Pe as mysqlDialect, Ne as mysqlReplicationAdapter, Y as mysqlRetryFilter, q as startBinlogCdc };
1315
+ export { c as CDC_OFFSETS_TABLE, e as MysqlClient, d as _voltroCdcOffsetsTable, V as connectionFromConfig, z as makeMysqlSqlLayer, H as makeMysqlSqlLayerFromConfig, Q as makeMysqlStore, Be as mariadbDialect, ze as mysqlDialect, Re as mysqlReplicationAdapter, Y as mysqlRetryFilter, q as startBinlogCdc };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/sql-mysql",
3
- "version": "0.57.0",
3
+ "version": "0.58.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",
@@ -35,8 +35,8 @@
35
35
  "dependencies": {
36
36
  "@effect/sql": "^0.52.0",
37
37
  "@effect/sql-mysql2": "^0.53.0",
38
- "@voltro/database": "0.57.0",
39
- "@voltro/logger": "0.57.0"
38
+ "@voltro/database": "0.58.0",
39
+ "@voltro/logger": "0.58.0"
40
40
  },
41
41
  "optionalDependencies": {
42
42
  "@vlasky/zongji": "^0.9.0"