@voltro/sql-mysql 0.34.0 → 0.35.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 +167 -0
  2. package/dist/index.js +117 -121
  3. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -39,6 +39,173 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.35.0] — 2026-08-13
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/runtime, @voltro/cli, @voltro/plugin-clickhouse, @voltro/plugin-duckdb, @voltro/plugin-analytics-postgres** — The analytics CDC-mirror's version is derived from the CHANGE under `changeScope: 'fleet'` (postgres CDC, mysql binlog) — warehouse baseline plus the change's per-key position in the totally-ordered fleet stream — instead of each replica's own clock. An N-replica deployment still issues N duplicate writes per change (every replica observes the whole stream; that is the transport), but they are now BYTE-IDENTICAL — same row image, same version — so the sinks' existing guards (ClickHouse `ReplacingMergeTree(version)`, DuckDB/postgres `excluded.version > version`) dedupe them for free, with no leader election and no clock anywhere. This closes the real defect behind the N× cost: under clock skew larger than the gap between two changes to one row, a peer's duplicate of the OLDER image could take the higher version and win in the warehouse permanently and silently. A replica joining mid-stream seeds each key's numbering from the warehouse's own high-water mark via the new REQUIRED `AnalyticsMirrorImpl.maxVersion` read (all shipped warehouse sinks implement it; tombstoned deletes keep their version so the read answers after a delete — a custom sink follows the codemod note). The once-per-boot `changeScope=fleet` warning that named this cost is REMOVED — the hazard it named is gone. Local-scope stores keep the hybrid-clock version unchanged. New tunable: `VOLTRO_ANALYTICS_MIRROR_VERSION_STATE_LIMIT` bounds the per-key version state (default 100000; least-recently-changed keys re-seed from the warehouse on their next change).
47
+ - **@voltro/cli** — The declared framework table set no longer reads a runtime flag. `CDC`, `VOLTRO_UNDO` and `VOLTRO_TRACING_PERSIST` each moved it before this release; `app.config.ts` gained `schema: { traces?, undo? }` to declare the two that still need a decision.
48
+
49
+ A consumer measured two fingerprints from one source tree, one database and one `NODE_ENV`, differing only in `CDC`. Their Helm chart gives the pre-upgrade migrate Job its own `env:` list — `NODE_ENV`, `DB_*`, the obvious migration inputs — while `CDC: "0"` lives in the pods' block, because change data capture is obviously a runtime concern. Nothing about the name reads as schema-affecting, so it was in none of their three overlays' jobs. The declared set is what the schema fingerprint hashes, so that is a GREEN migrate job followed by every pod refusing to boot. Latent for months, and it would have fired on their next deploy.
50
+
51
+ Counting the family after their report found three, not one — measured on a mariadb app at `NODE_ENV=production`, each flag flipped alone: `CDC=0` removed `_voltro_cdc_offsets`, `VOLTRO_UNDO=on` added `_voltro_undo_log`, `VOLTRO_TRACING_PERSIST=all` added `_voltro_traces`. All three are the kind of value an operator puts on the pods and not on the job, and only one of them had been noticed.
52
+
53
+ `NODE_ENV` had already produced this exact failure in 0.34.0 and was fixed by making one decider resolve it for every command. That fix does not generalise here: a job legitimately does not carry an observability flag, so there is nothing to agree on. The rule is therefore stated rather than patched — **the declared set may depend only on inputs every process in one deployment computes identically** (the source tree, `app.config.ts`, the dialect, and `NODE_ENV`), and `declaredSchemaGates.test.ts` sweeps every `VOLTRO_*` / `CDC` / `DB_*` name the framework reads anywhere in `packages/*/src` and fails if any of them moves the set. Derived rather than listed, because a test naming the three known offenders only re-checks what someone already remembered — which is how the two unreported ones survived.
54
+
55
+ The three got two different answers, deliberately. `CDC` left the derivation entirely: `_voltro_cdc_offsets` follows the DIALECT now, so a mariadb or mssql app declares it whether or not that process drives CDC. The cost is one empty offsets table and it is the same trade `impliesScheduleTables` already makes in writing. `VOLTRO_UNDO` and `VOLTRO_TRACING_PERSIST` could not simply be dropped — both can legitimately turn a table on in production, and a declared set that ignored them would leave capture writing to a table nobody created — so they keep their runtime meaning and lose their declaring power. Turning capture OFF still needs no declaration and never will; turning it ON without one is refused at boot, on both boot paths, with the config field named.
56
+
57
+ The `prod-mismatch` refusal also stopped being two hashes and a command. It prints which of the three decided tables THIS process declared and from which input, because the ledger stores no table set to diff against and the command it used to recommend was the one the operator had just run successfully. That half matters beyond the framework's own tables: a plugin's `extendSchema.tables` is app code and can read anything, so the rule above cannot be enforced for it.
58
+
59
+ **`voltro update` carries you across this** — codemod `0.35.0/05_declared-schema-drops-runtime-flags`.
60
+ - **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/devtools-ui** — A declared event must decide who may listen — the boot gate now covers `defineEvent`, closing SEC-1's sibling. `defineEvent`'s `guards:` was optional and `bindEvent` skipped an empty list, so under `security.defaultDeny` an event with NO access declaration was silently subscribable by anyone who could open the socket, while the identical shape was already refused for every procedure.
61
+
62
+ `defineEvent` now accepts `openAccess: '<reason>'` — mutually exclusive with `guards:`, reason string required — exactly as the four procedure definers do. The erased `{ open }` decision rides the same `guards` array every enforcement path reads; `bindEvent` treats it as "no check" (an open event pays what an unguarded one pays: nothing), and `eventToRpc` no longer unions `ScopeError` into the wire contract for an event that cannot produce a denial.
63
+
64
+ **Breaking for `security.defaultDeny` apps (the default):** an app with a `*.event.ts` declaring neither `guards:` nor `openAccess:` now refuses to boot under `voltro dev` and `voltro serve`, naming every undecided event — the same message, from the same gate, procedures get. `voltro doctor` lists the same set. Migration: give each event a decision (`guards: [{ scope: '…' }]` or `openAccess: '<why anyone may listen>'`); an app that wants the old default-allow declares `security: { defaultDeny: false }` once, in `app.config.ts`. Plugin-declared events are not judged — the gate reads the app's own discovered files only.
65
+
66
+ The events inspect snapshot (and the devtools Events panel) now counts only ENFORCEABLE guards and carries the `openAccess` reason, so a deliberately open event renders as "open access" instead of as "1 guard" over an event anyone may subscribe to.
67
+
68
+ **`voltro update` carries you across this** — codemod `0.35.0/01_event-access-decision`.
69
+ - **@voltro/plugin-ai-flows** — **The breaking half, first:** `RunStepStatus` gained `'skipped'`. A sixth member means an exhaustive switch over it stops compiling and a status-keyed lookup has a hole — so a manual codemod fires on any app that names the type or its literals. Everything else here is additive (optional fields, new exports, one nullable column that rides the declarative differ).
70
+
71
+ Flows can now BRANCH, FAN OUT, and no longer chain without a bound. Three additions, and the third is a defect fix wearing a feature's clothes.
72
+
73
+ **`when:` — a conditional step.** A step runs only if its condition holds against the run context; a false condition SKIPS the step rather than failing it, so it produces no output and anything referencing it sees an absent value. The run timeline carries the rendered reason (`{{mode}} equals "full"`), because a step that silently vanished is indistinguishable from a step nobody declared.
74
+
75
+ The condition is STRUCTURED data (`{ ref, op, value }`), not an expression string, and that is three decisions in one. A flow can be authored as a stored row a user edits in a browser — an expression there is an evaluator running user-authored source on the server. The visual editor can offer a dropdown over a structure and cannot over a string it would have to parse. And `validateFlow` already walks every reference, so a structured `ref` joins that check for free: a typo'd condition would otherwise evaluate absent, take the false branch, and skip its step on every run, forever, with nothing logged.
76
+
77
+ Truthiness here deliberately differs from JavaScript's: `0` and `''` are TRUTHY. A step gated on a generated count or string means "did the producer run", not "is it non-zero" — the second is `{ op: 'neq', value: 0 }`, sayable when meant.
78
+
79
+ **`group:` — concurrent steps.** Consecutive steps sharing a group name run at the same time, each keeping its own durable step, so a replay resolves every branch from the journal exactly as it would sequentially. Kept as a flat field rather than a nested `parallel([...])` because the durable step name, the run timeline and a `human` step's signal name are all INDEX-keyed — nesting would re-index every flow already running.
80
+
81
+ Three rules, all enforced at registration: grouped steps cannot read each other's outputs (they have no order between them), a group must be contiguous (a name that stops and resumes would run as two sequential fan-outs), and a `human` review cannot join a group (it suspends the whole run). Context writes are applied after the whole segment in AUTHORED order — applying them as branches land would make the run context depend on scheduling, which a durable replay must never do.
82
+
83
+ **A chain is bounded — this half is a fix.** `chainTo` carried exactly one guard, a flow could not chain to itself, so `A → B → A` and any deep chain were unbounded: each hop starts a child run with a fresh idempotency key, so nothing collapsed it and nothing was counting the hops. A run now carries the chain that led to it ON THE PAYLOAD — deliberately not reconstructed from the run rows, because a guard whose evidence comes from a query is a guard that permits the loop whenever the query fails. A chain is refused on a cycle, or at `maxChainDepth` (default 5; `aiFlowsPlugin({ maxChainDepth })` or `VOLTRO_AI_FLOW_MAX_CHAIN_DEPTH`), and the refusal lands on the run row's new `chainRefusal` column naming the path. The parent run still SUCCEEDS: a refused follow-up is a configuration problem, not a reason to destroy a completed result.
84
+
85
+ All three are driven through the REAL durable executor in tests, not just their pure helpers — a primitive that is correct and reaches nothing is the defect class this package's own segmentation module exists to prevent.
86
+ - **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/ai, @voltro/plugin-billing, @voltro/plugin-flags, @voltro/plugin-governance, @voltro/plugin-notifications, @voltro/plugin-presence, @voltro/plugin-storage** — Every first-party plugin rpc route now declares an access decision (`guards:` or `openAccess: '<reason>'`), and `security.defaultDeny` is enforced in the DISPATCH spine as defense in depth behind the boot gate: a descriptor that reaches the wire with no decision (a third-party plugin route, an embedder's hand-bound descriptor) is refused per-request with a typed `ScopeError` before the transaction / external I/O. Twelve previously-open routes now require a scope: `billing.startCheckout` / `portalUrl` / `previewChange` / `changePlan` / `changeSeats` / `invoices` → `billing:manage`; `billing.reportUsage` → `billing:report`; `governance.export` / `erase` → `admin:full` (already enforced in-handler, now declared); `storage.mintUploadUrl` / `ingestUrl` → `storage:manage`; `storage.listRefs` → `storage:browse`. Migration: grant the scope to the role/subjects that legitimately hold each capability (rbac role, `resolveScopes`, api-key scopes) — the codemod lists every route and the open-by-design surfaces that did NOT change. `PluginRpcRoute` gains `guards`/`openAccess` fields, carried through the route lift into the enforced descriptor; the synthesized agent/undo/connections built-ins declare `openAccess` so they keep serving under default-deny.
87
+
88
+ ### Added
89
+
90
+ - **@voltro/workflow, @voltro/cli** — `awaitSignal` now logs a one-time hint (once per workflow, never per poll) when its declared `timeoutMs` exceeds a threshold, naming `awaitSignalSuspending` — the drop-in variant that SUSPENDS the run and frees the worker slot for human-approval-length waits (WF-11). Threshold: `workflows: { suspendSignalHintMs }` in `app.config.ts` (default 5 minutes), env override `VOLTRO_WORKFLOW_SUSPEND_HINT_MS`. A hint only — the framework never swaps the variant under a run, because the two journal differently and a silent swap mid-history is a replay trap.
91
+ - **@voltro/data-transfer, @voltro/sql-postgres, @voltro/cli** — The logical importer bulk-loads postgres targets via `COPY … FROM STDIN` (PERF-13). `voltro data import` engages it automatically wherever plain-INSERT semantics provably hold — `--mode replace`, or the default `upsert` into a table that is empty at import time (the fresh-target shape of every cross-dialect migration) — and never under `--atomic`. A refused COPY batch is atomic (nothing landed), so the importer replays exactly that batch through the per-row path with held-row / deferred-FK semantics intact. MEASURED on a 7-column table (text/int/bool/jsonb/timestamptz), 50 000 rows, local postgres: row-by-row 12.8 s (~3.9 k rows/s) vs COPY 0.59 s (~84.6 k rows/s) — **21.7×**. New seams: `ImportOptions.copyLoader` / `copyBatchSize` (default 5000) in `@voltro/data-transfer`, and `makePgCopySession` / `encodeCopyRow` in `@voltro/sql-postgres` (a submittable CopyIn query over the existing `pg` driver — no new dependency). Other dialects keep the per-row writes.
92
+ - **@voltro/cli** — `voltro probe access` asks a RUNNING app whether its declared access is actually enforced — the question none of the existing checks ask.
93
+
94
+ `voltro check`, the boot access gate and `security.defaultDeny` all verify that a decision was DECLARED. None of them verifies that the declaration REFUSES anyone. That distinction is not hypothetical here: the dispatch spine and the boot gate were separate for several releases, a procedure filtered out of the rpc group while still bound in the handler map served silently on one path and crashed the other, and `check` itself counted a decided-open route as guarded. Every one was the declaration and the behaviour disagreeing, found by reading rather than by asking.
95
+
96
+ It calls every guarded procedure with NO credentials and reports three verdicts: `refused` (enforcement works), `admitted` (the finding), and `inconclusive` — the call failed for a reason that is not an access refusal, usually payload validation running before the guard. `inconclusive` is never counted as a pass; `--strict` fails on it, which is what CI wants.
97
+
98
+ It probes ANONYMOUSLY on purpose. That is strictly weaker than scope-by-scope differentiation and strictly safer: the alternative puts credential minting into a command that can be pointed at production. Procedures declared `openAccess:` are skipped — probing them would report every deliberately-public route as a finding and bury the real ones, which is the same signal-to-noise failure `kind: 'open'` was added to the wire to fix.
99
+
100
+ `fetchJson` gained an explicit `anonymous` option for this one caller; it is an opt-out, never a default, and both directions are pinned by a test — a bearer attached here would make every result meaningless while still printing green.
101
+
102
+ **Validated against a live app, and it took two corrections to get there.** The first version sent a readable request id, which the transport converts with `BigInt(id)` — so every probe came back as a Defect before any guard ran, and every app looked broken. The second read a top-level `_tag` off an object while `POST /rpc` answers an ARRAY of envelopes, so a correctly-refused call scored as `admitted`. Both versions had a green unit suite, because the fixtures asserted the shape the code assumed. The fixtures are now copied from a real transcript.
103
+ - **@voltro/cli, @voltro/runtime** — `app.config.ts` gained `reactive: { deliveryConcurrency, rawReadTrackingLimit }` — the delivery-loop tunables were env-only, which left a number the framework picks on the project's behalf undeclarable in the one file that carries every other tunable.
104
+
105
+ Resolution stays inside the Dispatcher constructor (`resolveReactiveConfig`), so neither boot path can drift, and the env vars still win over the declared value: an operator acting on a running deployment outranks the project file. The threading itself is source-pinned across all three files (`dev.ts`, `serveCommand.ts`, `serveApi.ts`) because the serve side is a two-file relay and the union is where an option goes missing invisibly.
106
+ - **@voltro/cli, @voltro/database** — Data residency is DECLARABLE and wired. `tenancy.residency` in `app.config.ts` opens one store per servable region on both boot paths and routes every request to its tenant's home region — or refuses it.
107
+
108
+ The primitives have existed for two rounds (`setResidencyConfig`, `residentPlacement`, `bindResidentStore`), exported and tested, with **zero callers**. A user could reach them, but nothing in the framework did: there was no way to declare residency and no request ever consulted it. That gap was pinned by a test walking every workspace source, which went red on this change and asked for the module header to be corrected — it now names its consumers instead of asserting it has none, so a SECOND unreviewed caller still fails.
109
+
110
+ ```ts
111
+ tenancy: {
112
+ isolation: 'namespace',
113
+ residency: {
114
+ servableRegions: ['eu-west'],
115
+ regionUrlEnv: { 'eu-west': 'DB_URL_EU', 'us-east': 'DB_URL_US' },
116
+ homes: [{ tenantId: 'acme', region: 'eu-west' }],
117
+ },
118
+ }
119
+ ```
120
+
121
+ `regionUrlEnv` names an env VAR, not a URL — a connection string is a secret and `app.config.ts` is committed. Everything else about a region's store (pool bounds, TLS, `search_path`, timeouts) is inherited from the primary connection, so a region cannot silently run with different limits than its deployment.
122
+
123
+ **Every failure is a refusal, never a fallback**, because a residency system that degrades to a default store violates residency at exactly the moment something is misconfigured. Unresolvable tenant, unmapped home, or a home region this deployment does not serve are all typed refusals; the last one names the region so a gateway can route it.
124
+
125
+ Four declarations are refused at BOOT rather than warned about: residency without `isolation: 'namespace'` (the region keeps regions apart, the namespace keeps tenants apart — one without the other is not isolation), a servable region with no env-var name, one whose env var is unset, and a tenant mapped to two regions.
126
+
127
+ Two boundaries worth knowing:
128
+
129
+ - `ctx.storeForTenant(id)` resolves residency for THAT tenant, not the caller's, so a handler acting on another tenant reaches that tenant's region or is refused. Background work (schedules, workflows) runs with no tenant and must use it — `ctx.store` there is the primary store. - A transaction is never re-routed. A caller-supplied store is used as given; it already went through residency to exist, and moving writes off the connection holding the lock is a worse failure than the one residency prevents.
130
+
131
+ Homes resolve once at boot (an array, or a function reading your own table), so adding a tenant home needs a restart — chosen over a cache with a staleness window on a decision whose whole value is that it is never wrong.
132
+ - **@voltro/runtime, @voltro/cli** — `voltro schedule backfill <name> --from <iso> --to <iso> [--yes] [--limit N]` and `POST /_voltro/inspect/schedules/:name/backfill` (WF-14) — fire every cron occurrence of a schedule over an explicit range, sequentially, each recorded against its own cron-derived `scheduledAt` with `trigger: 'manual'`. Fills the gap boot backfill (walks from the last recorded run only) and cluster-cron catch-up (capped at one day) leave open. Bounded and confirmable: above 25 occurrences it refuses without `--yes` (printing the count), above the per-request cap (default 1 000, `--limit` up to a hard ceiling of 10 000) it refuses outright, firing nothing — never a silent prefix. Wired on both boot paths through one shared hook.
133
+ - **@voltro/devtools-ui, @voltro/plugin-search** — The Search dashboard panel now RENDERS the drift surface REL-1 shipped server-side and no dashboard showed (the additive-JSON silent-drift shape the 4-layer rule exists for): per-index `dropped` / `pendingDrift` / `drifted` / last-drift badges, the repair queue itself (`GET /drift` — oldest first, with attempt counts and the engine's last error), and a **Resync now** action (`POST /resync`) gated on the new `canResyncSearch` capability (its own flag — a resync re-reads only the drifted rows; a reindex re-reads the whole table). Landed across all four layers in one change set: shared `SearchPage` + wire types + capability + EN/DE strings here; HTTP fetchers + page wiring in voltro-devtools; tenant-scoped `apps.inspectSearchDrift` / `apps.inspectSearchResync` proxies + hooks + page wiring in voltro-cloud (the indexes proxy schema carries the new fields as OPTIONAL, so a customer app from before the drift ledger still decodes). `search.query` also now carries an explicit access decision (`openAccess`, with the tenant-scoping rationale in source) instead of the undecided SEC-1 shape.
134
+ - **@voltro/cli** — Workflow wakes over the change stream (WF-8): on a fleet where remote changes reach the change spine (Postgres LISTEN/NOTIFY CDC — the common broker-less multi-replica deployment), a remote replica's `signal-sent` event, start context, or run transition now triggers an immediate, coalesced `pollStorage` on every replica, so cross-replica signal/step latency stops being bounded by the 10 s storage poll. Honest subset by design: the cluster engine has no per-run wake seam, so the change event wakes the poll early rather than replacing it — the poll tick stays the safety net. Local-origin changes never wake (a replica waking on its own recorder rows would be a poll storm). Wired by the same `makeWorkflowWake` builder on both boot paths.
135
+ - **@voltro/runtime, @voltro/workflow, @voltro/cli** — `ctx.workflows.start(name, payload, { at: Date })` — delayed one-off starts (WF-13). The start is parked as a durable `_voltro_workflow_pending` row (`mode: 'delayed'`) and fired by the coordinated drainer when `at` arrives, so it survives restarts and fires on whichever replica drains. At `at` it becomes an ordinary ARRIVAL: declared flow control (debounce, singleton, rateLimit, …) judges it as of that moment — `at` never bypasses a control. The handle reports `status: 'queued'` with `deferral: { mode: 'delayed', dueAt }`. An `at` in the past starts immediately; `{ at, wait: true }` is refused. `at` is an absolute instant by design (no `delay` spelling): a delay is ambiguous about its epoch and every queue system answers it differently, while an instant composes with the schedule/backfill surfaces.
136
+ - **@voltro/workflow, @voltro/cli** — `workflows: { recording: 'coarse' }` in `app.config.ts` (env override `VOLTRO_WORKFLOW_RECORDING`) — turns off the two fire-and-forget per-step writes to `_voltro_workflow_run_steps` (WF-10) for hot high-step workflows. Run rows, run events (signals/timers/cancels/stall reports) and the cluster engine's durable journal are unaffected — replay and redrive work exactly as before; the cost is an empty step timeline for runs recorded under coarse. Measured before it was built (`packages/cli/scripts/admission-throughput.mjs`): the recorder costs exactly 2 store writes per step, off the step's critical path — which is why the knob is a skip, not a batcher.
137
+ - **@voltro/workflow, @voltro/cli** — `workflow({ schedule })` — the workflow-side cron declaration (WF-12), with Temporal Schedules' overlap vocabulary about the RUN: `onOverlap: 'skip' | 'buffer' | 'cancelOther'`. Pure sugar over the shipped scheduler: at boot it lowers into a real schedule named `workflow:<name>` (same coordinated claims, run rows, Schedules panel, `voltro schedule` verbs). The synthesised firing awaits the workflow run to completion, which is what makes skip/buffer bind on the run's duration; `cancelOther` cancels only the still-running run this schedule itself started. The firing watchdog (`schedule.maxRuntime`) defaults to 24 h here. Cron and timezone are validated at definition time.
138
+
139
+ ### Changed
140
+
141
+ - **@voltro/plugin-clickhouse** — `clickhouseAnalytics` now BATCHES `track()` inserts by default (PERF-11) — 20 events / 5 s, plugin-posthog's conservative numbers — instead of one HTTP insert (and one MergeTree part) per event. What changes observably for an app that never set `batch`: a successful `track()` now means "buffered", not "ClickHouse accepted the row"; events become readable up to 5 s after they were tracked; a flush failure drops that batch with a warning (a hard crash loses whatever is still buffered — graceful shutdown drains via `dispose`). Opt OUT with `batch: false` to restore one immediate, confirmed insert per event; `batch: { maxSize, flushIntervalMs }` tunes the window. No compile break — `batch` widened to `ClickhouseBatchOptions | false`, and the previous opt-in spelling keeps working (its defaults are now 20/5000 rather than 1000/5000).
142
+ - **@voltro/database, @voltro/cli, @voltro/workflow** — The migration advisory lock is now scoped to the configured schema (`DB_SCHEMA`) instead of one framework-wide constant. A postgres advisory lock is database-scoped and MySQL `GET_LOCK` is server-wide, so two apps sharing one database in different schemas used to serialize each other's migrations and defer each other's boot-time trigger repair — with a log line blaming "another instance". Now: postgres derives a stable 64-bit key from the schema name (FNV-1a 64 of `voltro_migration_lock:<schema>`, sign bit cleared; collisions across schemas are possible and only reintroduce serialization, never a race); mysql/mariadb/mssql suffix the lock NAME with the schema (hashed past MySQL's 64-char `GET_LOCK` cap). Every taker moved together in this change — the declarative applier, the file-based runner, the boot auto-migrate, the CLI's reactive-trigger boot repair, and the workflow cluster first-boot gate (its own distinct key, same derivation). On mysql/mariadb, where `GET_LOCK` is server-wide and `DB_SCHEMA` is not a connection pin, setting `DB_SCHEMA` to your database name is how two apps on one server un-share the lock.
143
+
144
+ **Rolling-deploy story.** An app WITHOUT `DB_SCHEMA` (or with `DB_SCHEMA=public`) keeps the EXACT pre-change lock key and name — old and new replicas contend on the same lock throughout the rollout; nothing to do. An app WITH a non-default `DB_SCHEMA` changes its lock key when it lands this version: during that one rollout window, old-generation and new-generation replicas do not mutually exclude their DDL. The boot auto-migrate DDL is idempotent (`IF NOT EXISTS`-shaped), so the practical exposure is the known postgres `CREATE TABLE IF NOT EXISTS` catalog race — worst case one replica's boot fails and restarts. Avoid running `voltro db apply` concurrently with THAT rollout; after it, everything contends on the schema-scoped key.
145
+ - **@voltro/plugin-search** — `POST /reindex` now STREAMS the source table (keyset-paginated `streamTable`) and upserts one bounded page at a time instead of loading the whole table into memory — the old shape was an OOM on exactly the tables big enough to need a reindex (PERF-12). The page size is a new tunable, `searchPlugin({ sync: { reindexBatchSize } })` (default 1000), and the `/indexes` panel reports it as part of the policy in force. `backfillIndex` keeps its plain-array signature for small explicit seeds. Additive surface only: a new optional `SearchSyncOptions` knob + a new `SYNC_DEFAULTS` key — no existing call site changes meaning.
146
+ - **@voltro/plugin-search** — Sync-stat counters no longer pay a read+CAS against the OLTP primary on EVERY indexed-table write (PERF-14). Counts buffer in memory and flush per window — `searchPlugin({ sync: { statsFlushIntervalMs } })` (default 5000 ms; `0` restores the per-event durable write) with an early flush at `statsFlushMaxBuffered` (default 1000) pending counts. Mirrors the runtime's api-key usage buffer, SHUTDOWN included: plugin deactivate drains the tail on both boot paths, so a graceful deploy loses nothing; a hard crash loses at most the current window of counters (never a change — the drift ledger stays the durable record). `GET /indexes` drains the buffer before reading, so the panel stays truthful mid-window. `StatsStore` gained a delta-applying `add` (the flush target); both shipped impls carry it and nothing consumes user-provided `StatsStore` implementations.
147
+ - **@voltro/database, @voltro/plugin-webhooks, @voltro/testing, @voltro/voltro, @voltro/workflow** — Golden churn from this round's signature WIDENINGS, classified per package:
148
+
149
+ - **@voltro/database** — every migration entry point (`applySchema`, `runMigrate`, `runFrameworkBootstrap`, `applyNamespacedSchema`, `provisionTenantNamespace`, the lock functions) gained a trailing OPTIONAL parameter (`SchemaApplyOptions` / `MigrationLockScope`) for the schema-scoped lock and the dialect retry predicate. Every existing call compiles unchanged; omitting the parameter is exactly the old behavior. - **@voltro/workflow / @voltro/testing / @voltro/voltro** — the same widenings re-exported through the aggregates, plus `PresenceWrite`-adjacent type surface already classified in this release's presence entry. - **@voltro/plugin-webhooks** — `deliverWebhookWorkflow`'s payload type inference had COLLAPSED to `AnyStructSchema | Struct<Fields>`, which made `execute`'s requirements `any` for every consumer: there was no type contract in force to break, only one that silently did not exist. It now infers the real payload struct. The export's only callers are the framework's own boot paths (it exists for cluster-runner registration); an app that passed a wrong-shaped payload under `any` now gets the compile error it should always have had — which is the fix, not collateral.
150
+
151
+ ### Fixed
152
+
153
+ - **@voltro/cli, @voltro/protocol** — `voltro doctor`, the boot refusal, and `GuardSpec.resource`'s own doc comment now name the per-resource form of an access decision. All three listed two ways to decide and there are three.
154
+
155
+ A consumer with 565 undecided procedures set `security: { defaultDeny: false }` across their app, and their reasoning was correct at every step from what they were shown. Their authority is per-team — a viewer in one team, an admin in another — so a subject-global `guards: [{ scope }]` would state a check they do not perform, and the boot refusal warns against exactly that ("reaching for a scope every caller already holds satisfies the gate, reads as protection, and enforces nothing"). `openAccess:` would be untrue. Both offered forms were rightly rejected, so they turned the gate off and kept enforcing in handlers.
156
+
157
+ The form that fits them — `guards: [{ action, resourceType, resource }]`, backed by `defineResourcePolicy` and a tuple source registered over their own tables — has shipped for several releases, is wired on both boot paths, fails closed without a resolver, and is documented under Authentication → Authorization. They looked: they read `GuardSpec.resource`, whose doc comment described the resolver as "a future ReBAC / `accessPolicy()` resolver". That sentence was written before the ReBAC path shipped and never updated, and it is the only thing a reader of that type has. A doc comment that says "future" about something built is not a small inaccuracy — it argued a careful team out of a security gate.
158
+
159
+ An enumeration inside a refusal is read as exhaustive, and the more careful the reader, the more thoroughly they act on it. `accessDecisionForms.test.ts` pins all three forms in all three places, including the `defaultDeny: false` branch — an app that has already given up is precisely the audience that needs to learn there was a third option.
160
+ - **@voltro/protocol, @voltro/database, @voltro/runtime, @voltro/plugin-broadcast, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — A reactivity-channel publish now says where it came from — `origin: 'inline'`, because it happened in THIS process — instead of borrowing the `'injected'` stamp its transport seam applies by default. Two defects came out of that one mislabel, both silent:
161
+
162
+ - **A channel published synchronously from inside a change listener never left the replica.** `plugin-broadcast` suppresses re-publishes while it is injecting, and the bracket was coarse: it dropped EVERY emission made in that window, not just the event it had injected. So `onChange` → `publishReactivity` woke the local node and no peer ever heard it — no error, no log. The one plugin doing cross-replica fan-out (presence) escaped only because it re-publishes from its own transport callback. The guard now suppresses by provenance, so a local publish made inside the bracket travels like any other. - **A replica could not tell its own channel publish from a peer's.** Both arrived `'injected'`, so a listener fanning a channel onward had nothing to key on and needed a boolean per channel to avoid an echo. `origin` answers it now.
163
+
164
+ Two supporting changes, each with its own failure mode:
165
+
166
+ - `origin` no longer survives the wire. It describes how an event reached THIS process, so the receiving replica strips what the sender serialised and stamps its own. Without this the guard fails OPEN — measured, an arrival still claiming `'inline'` amplified one publish into 163 events and killed the test worker. - The transport-origin stamp has one definition (`externalChangeEvent`, `@voltro/database`) instead of five hand copies across the memory store and the four dialect stores. `injectOriginParity.test.ts` fails if any store grows its own again — a store that hand-stamps would override a caller's stated origin, and the visible result is a channel that stops crossing replicas on that dialect only.
167
+
168
+ `DataStore.injectExternalChange` keeps its shape: an event that states no origin is still stamped `'injected'`. A store passed to `plugin-broadcast` that does not stamp at all (the interface is structural) is detected by identity and falls back to the old coarse suppression rather than amplifying.
169
+ - **@voltro/cli, @voltro/devtools-ui** — `voltro check` no longer mistakes a deliberate `openAccess:` mutation for an unguarded one — and no longer mistakes it for a guarded one either. The manifest serialises the decision as a `kind: 'open'` guard entry carrying the reason string; `toInput` now translates it into `openAccess` on the graph procedure with `hasGuards: false` (nothing IS checked), and the `rbac/unguarded-mutation` rule skips a procedure whose author already "confirmed it is intentionally public" — the rule's own fix text. Previously the open entry was counted as a guard, so the finding disappeared for the wrong reason: the open mutation read as protected.
170
+
171
+ `@voltro/devtools-ui` gains the hand copy of the `SerialisedGuard` wire union (it is deliberately dependency-free, so it cannot import the CLI's), renders an access badge on the RPC page — guard count, or "open access" with the reason in the tooltip — and the copy is pinned from the owning side by `serialisedGuardParity.test.ts`, in the style of `migrationOpKindParity`.
172
+ - **@voltro/cli** — `ctx.query` in a loader rejects an `error` event instead of returning it as the query's rows.
173
+
174
+ A consumer put a guard with an unheld scope on a query and called it over HTTP with a valid user JWT. The batch came back 200 with an `error` chunk carrying a `ScopeError` and an `Exit: Success` after it. Both are correct — the transport worked and the guard worked — but `buildLoaderQuery` unwrapped exactly ONE member of the three-member `subscriptionEvent` union and passed the other two through as data. The loader returned the error OBJECT, it went into the SSR seed, and the component called `.map()` on it: `TypeError: kept.map is not a function`, on 135 pages, for every user.
175
+
176
+ The `.catch(() => null)` in our own documented loader pattern could not fire, because nothing was thrown. Neither could `seedPagePreloads`' `onError`, for the same reason. The same rejection over the live socket sets `error` and leaves data empty, so the two transports were saying different things about one event.
177
+
178
+ The unwrap is exhaustive now: `snapshot` yields its rows, `error` rejects with the typed error attached (as `cause` and `voltroError`, so an app can still branch on `ScopeError` rather than parse a string), and a first-event `delta` — structurally impossible today — throws naming itself a framework bug rather than handing a loader an id-keyed patch. `buildLoaderQuery` has one implementation shared by `voltro dev` and `voltro start`, so the fix covers the production path the report did not measure.
179
+
180
+ The HTTP 200 is unchanged, and the reporter is right that it should be: the batch transport did succeed.
181
+ - **@voltro/database, @voltro/cli** — The schema apply (`applySchema` / boot auto-migrate / framework bootstrap) now consults the dialect's own transient-failure predicate instead of dying on the first `SQLITE_BUSY`. The gap was located in `@voltro/sql-turso`: `busy_timeout` cannot retry the deferred-upgrade lock class, the store path already honoured the dialect's `retryFilter`, and the migration applier never consulted it — so a DDL statement that met the schema lock failed on the first attempt while every equivalent DML statement would have been retried. The CLI threads each loaded dialect's `retryFilter` through the new `SchemaApplyOptions`; retries are bounded (`VOLTRO_MIGRATION_DDL_RETRIES`, default 4, exponential backoff with jitter) and only ever re-run statements that are safe to re-run: per-statement on the per-operation dialects (sqlite/turso/mysql/mariadb, `IF NOT EXISTS`-shaped or covered by the duplicate-index tolerance), whole-transaction on postgres/mssql, whose retryable classes (deadlock victim, serialization failure) roll the transaction back cleanly. No `retryFilter` threaded means no retry — exactly the previous behavior. The declarative plan applier (`applyPlan`) is deliberately unchanged: its operations are not uniformly idempotent, and partial failure there is owned by the resume ledger.
182
+ - **@voltro/web, @voltro/client** — The web first load no longer ships `msgpackr` — 190.5 → 180.6 KB gz (−9.9 KB, 5.1% of the whole first load) for every app, measured on the zero-procedure fixture and re-pinned into `bundle-budget.json`.
183
+
184
+ It shipped because `@effect/rpc`'s RpcSerialization module top-level-imports msgpackr while every Voltro path selects `layerJson`, and msgpackr declares no `sideEffects` flag — so no bundler was allowed to drop it. The fix is a dependency patch adding `sideEffects: false`, declared at BOTH workspace roots (the @effect/cluster patch rule: a meta-root `pnpm install` must apply it too, or the two installs fight over node_modules). An app that genuinely calls `makeMsgPack` keeps the library — the flag only permits dropping it when unused.
185
+
186
+ The bundle-budget gate's slack floor is what keeps this fixed: the budget is re-pinned to the new number, so quietly re-inflating past it fails CI.
187
+ - **@voltro/cli, @voltro/database** — Under `tenantIsolation: 'namespace'`, a tenant's namespace is now PROVISIONED on its first use — schema DDL plus the `onTenantCreate` seed lifecycle, memoised per namespace per process.
188
+
189
+ Neither ever happened: `provisionTenantNamespace` — documented as "the entry point the CLI / runtime use, eager at migrate time or lazily on first use" — had zero callers in the entire codebase, `withNamespace` returns a pure view, and so a fresh tenant's first request died on "relation does not exist" while the `onTenantCreate` seeds (recorded as wired) fired only from tests that called the function directly. Found by the claimed-wirings checker the moment it learned to read active-voice claims.
190
+
191
+ Both boot paths build the provisioner from one builder; the namespace view's async methods await the memoised ensure (one resolved-promise await after the first settle), sync members stay sync, and the method classification is a DERIVED guard — a new `DataStore` method fails the test until someone decides whether it must await provisioning. A failed provision is surfaced and forgotten, never cached: one transient DDL failure must not become a permanently broken tenant on a replica. Verified end to end on live postgres, control included: the raw view still fails against a fresh namespace, the provisioned one creates the schema and fires the lifecycle exactly once.
192
+
193
+ Eager provisioning stays the app's move (call `provisionTenantNamespace` from a seed or startup over your own tenant table) — the framework has no tenant registry to enumerate, and the docs now say so instead of implying otherwise.
194
+ - **@voltro/runtime, @voltro/cli, @voltro/testing** — `setRowFilter`'s registration moved from a module-local variable to a `globalThis` cell, and a scoped store that receives no filter while one is registered now throws instead of serving unfiltered rows.
195
+
196
+ A consumer measured four read paths returning every row of the tenant to every employee — 19/19 contracts, 231/231 time-entry requests, 6/6 user settings, 4637/4637 shifts — on both HTTP and WebSocket, with `row filter registered` in the boot log and 27 green tests. One of those paths was the only thing keeping `userSettings.update` safe, so every user could edit every other user's settings.
197
+
198
+ Their exclusion work was exhaustive and right at every step: the load succeeded, `predicateFor` returned predicates for the right tables, nothing was `unconstrained`, no handler bypassed the store, and a `setRowFilter` + `makeTestContext` pair reproduced CORRECT filtering. That left them concluding the remaining variable was the store — memory in the green reproduction, postgres in the red deployment.
199
+
200
+ It is not the store, and the correction is the transferable part: their reproduction ran inside ONE module instance and production does not necessarily. `ROW_FILTER` was a module-local `let`, so an app's `*.startup.tsx` and the framework's serve pipeline write and read different variables whenever they hold different copies of `@voltro/runtime` — the serve bundle inlines the framework while app modules stay external, and strict pnpm resolves one version into two physical directories when two importers have different peer contexts. The pipeline reads `undefined`, correctly interprets it as "this app registered no filter", and serves everything.
201
+
202
+ `@voltro/database`'s `coreTablesRegistry` carries this exact fix with a comment describing this exact failure, for a value whose worst case is a crash at boot. This one's worst case is silent data exposure and it did not have it.
203
+
204
+ The second half answers the reporter's second ask directly: a filter that cannot be applied must fail loudly rather than pass quietly. `undefined` and "we could not tell" had collapsed into one value, and the doc comment on that option already asserted they must not. Every deliberately unfiltered path — system sweeps, `runAsSystem`, change-stream subscribers, the seeding store in a test — now passes `NO_ROW_FILTER` explicitly, because "this app has no filter" and "this path is unfiltered on purpose" are different claims and only the second is a decision somebody made.
205
+ - **@voltro/cli** — A bare `voltro serve` under docker compose now drains on SIGTERM — in-flight requests complete against a fully-alive app, the listener refuses new work, live WebSockets are ended cleanly, and the process exits on its own, well inside `VOLTRO_SHUTDOWN_GRACE_MS`. No preStop hook or endpoint removal required. Two real defects closed (both measured against a live server): a single connected WebSocket wedged `nodeServer.close()` — node's `closeAllConnections()`/`closeIdleConnections()` cannot end an upgraded socket while `close()` still waits on it — so EVERY shutdown with a connected web client ran to the 10s deadline cut and the steps queued behind the close (the store's connection-pool close included) silently never ran; and the shutdown hook deactivated plugins and drained the analytics mirror BEFORE the request drain, so a request finishing during shutdown hit dead services and its writes were never mirrored. The drain is bounded: in-flight requests get 60% of the shutdown grace (floor 500ms), stragglers are then destroyed, and idle keep-alive sockets are swept continuously so a finished response never delays exit. The stale `serveApi` comment claiming `NodeRuntime.runMain` owns SIGTERM (and pointing at a k8s preStop hook as the fix) is rewritten to describe the drain that actually runs. Verify against a real serve with `node scripts/serve-drain-check.mjs`.
206
+
207
+ ---
208
+
42
209
  ## [0.34.0] — 2026-08-12
43
210
 
44
211
  ### ⚠ BREAKING
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, encodeRowForSchema as C, endLocalWrite as w, hasEagerLoads as T, isTableReactive as E, makeEagerFallbackReporter as D, observeDbOp as O, qualifyTable as ee, raiseChangeListenerCeiling as k, recordsTable as te, registerPendingAttribution as ne, requireTable as re, resolveEchoAttribution as ie, runStoreTransaction as ae, runWriteRecorders as oe, stampGeneratedId as A, stampGeneratedIds as se, withCapturedAttribution as j } from "@voltro/database";
4
- import { EventEmitter as M } from "node:events";
5
- import { createLogger as N } from "@voltro/logger";
6
- import { SqlClient as P, TransactionConnection as F } 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, encodeRowForSchema as C, endLocalWrite as w, externalChangeEvent as T, hasEagerLoads as E, isTableReactive as D, makeEagerFallbackReporter as ee, observeDbOp as O, qualifyTable as te, raiseChangeListenerCeiling as ne, recordsTable as re, registerPendingAttribution as ie, requireTable as k, resolveEchoAttribution as A, runStoreTransaction as j, runWriteRecorders as ae, stampGeneratedId as M, stampGeneratedIds as oe, withCapturedAttribution as N } from "@voltro/database";
4
+ import { EventEmitter as se } from "node:events";
5
+ import { createLogger as P } from "@voltro/logger";
6
+ import { SqlClient as F, TransactionConnection as I } from "@effect/sql/SqlClient";
7
7
  //#region src/sqlLayer.ts
8
8
  var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
9
9
  let t = e.acquireTimeoutMs ?? l;
10
10
  return t > 0 ? t : void 0;
11
- }, I = (e) => {
11
+ }, L = (e) => {
12
12
  let t = e.acquireQueueLimit;
13
13
  return t !== void 0 && t > 0 ? t : void 0;
14
- }, L = (e) => {
15
- let t = e.ssl === void 0 ? void 0 : ce(e.ssl), n = le(e), r = I(e);
14
+ }, R = (e) => {
15
+ let t = e.ssl === void 0 ? void 0 : ce(e.ssl), n = le(e), r = L(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
- }, R = (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(L(n)),
27
+ poolConfig: t.succeed(R(n)),
28
28
  ...n.maxConnections === void 0 ? {} : { maxConnections: t.succeed(n.maxConnections) }
29
- }), z = (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 ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (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
- }, B = (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 ?? z(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,21 +67,21 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
67
67
  ...e.ssl === void 0 ? {} : { ssl: e.ssl },
68
68
  ...t
69
69
  };
70
- }, V = (e) => R(B(e)), H = (e, t) => e === null ? !1 : t === null ? !0 : e.filename === t.filename ? e.position > t.position : e.filename > t.filename, U = (e) => e.msSinceProgress < e.stallThresholdMs ? "healthy" : e.primary !== null && !H(e.primary, e.reader) ? "idle-caught-up" : "reconnect", W = (e) => (e instanceof Error ? e.message : String(e ?? "")).includes("schema changed between binlog event and metadata fetch"), ue = (e) => {
70
+ }, H = (e) => z(V(e)), U = (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 && !U(e.primary, e.reader) ? "idle-caught-up" : "reconnect", ue = (e) => (e instanceof Error ? e.message : String(e ?? "")).includes("schema changed between binlog event and metadata fetch"), de = (e) => {
71
71
  let t = e instanceof Error ? e.message : String(e ?? "");
72
72
  return /Table\s+[^\s.]+\.(\S+)\s+schema changed between binlog event and metadata fetch/.exec(t)?.[1] ?? null;
73
- }, G = "\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", K = (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).`, de = 5 * 6e4, fe = 3, pe = (e, t) => {
74
- let n = [...e.filter((e) => t - e < de), t];
73
+ }, fe = "\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", 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).`, pe = 5 * 6e4, me = 3, he = (e, t) => {
74
+ let n = [...e.filter((e) => t - e < pe), t];
75
75
  return {
76
- verdict: n.length >= fe ? "persistent" : "backlog",
76
+ verdict: n.length >= me ? "persistent" : "backlog",
77
77
  hits: n
78
78
  };
79
- }, me = /* @__PURE__ */ new Set([
79
+ }, ge = /* @__PURE__ */ new Set([
80
80
  "writerows",
81
81
  "updaterows",
82
82
  "deleterows"
83
- ]), he = /\b(alter|rename|drop|create)\s+(table|column)?/i, q = (e) => new Promise((t) => setTimeout(t, e)), J = async (e) => {
84
- let t = N({ scope: `voltro:${e.variant}:cdc` }), n;
83
+ ]), _e = /\b(alter|rename|drop|create)\s+(table|column)?/i, K = (e) => new Promise((t) => setTimeout(t, e)), q = async (e) => {
84
+ let t = P({ scope: `voltro:${e.variant}:cdc` }), n;
85
85
  try {
86
86
  n = (await import("@vlasky/zongji")).default;
87
87
  } catch (t) {
@@ -96,14 +96,14 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
96
96
  o = n.binlogName;
97
97
  return;
98
98
  }
99
- if (r === "query" && n.query && he.test(n.query)) {
99
+ if (r === "query" && n.query && _e.test(n.query)) {
100
100
  l && (l.tableMap = {});
101
101
  return;
102
102
  }
103
103
  if (n.nextPosition && o && (s = {
104
104
  filename: o,
105
105
  position: n.nextPosition
106
- }, e.onPosition?.(s)), !me.has(r)) return;
106
+ }, e.onPosition?.(s)), !ge.has(r)) return;
107
107
  let u = n.tableMap[n.tableId];
108
108
  if (!u || u.parentSchema !== a) return;
109
109
  let d = u.tableName;
@@ -181,11 +181,11 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
181
181
  try {
182
182
  l?.stop();
183
183
  } catch {}
184
- if (d++, await q(Math.min(3e4, 500 * 2 ** Math.min(d, 6))), u) return;
185
- let i = s, a = C(n), c = !a && W(n), f = !1;
184
+ if (d++, await K(Math.min(3e4, 500 * 2 ** Math.min(d, 6))), u) return;
185
+ let i = s, a = C(n), c = !a && ue(n), f = !1;
186
186
  if (c) {
187
- let e = ue(n), i = e ?? "<unknown>", { verdict: a, hits: o } = pe(m.get(i) ?? [], Date.now());
188
- m.set(i, o), f = a === "persistent", f && !h.has(i) && (h.add(i), e !== null && r.add(e), t.error(K(i)));
187
+ let e = de(n), i = e ?? "<unknown>", { verdict: a, hits: o } = he(m.get(i) ?? [], Date.now());
188
+ m.set(i, o), f = a === "persistent", f && !h.has(i) && (h.add(i), e !== null && r.add(e), t.error(G(i)));
189
189
  }
190
190
  (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, f || e.onResync?.());
191
191
  try {
@@ -209,14 +209,14 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
209
209
  l?.stop();
210
210
  } catch {}
211
211
  let n = e.resolveStartPosition ? await e.resolveStartPosition().catch(() => null) : null;
212
- o = n?.filename ?? null, e.onResync?.(), await q(500), await w(n);
212
+ o = n?.filename ?? null, e.onResync?.(), await K(500), await w(n);
213
213
  } else throw n;
214
214
  }
215
215
  let E = async () => {
216
216
  if (u || p || Date.now() - f < _) return;
217
217
  let n = null;
218
218
  if (e.resolveStartPosition && (n = await e.resolveStartPosition().catch(() => null)), u || p) return;
219
- let r = U({
219
+ let r = W({
220
220
  msSinceProgress: Date.now() - f,
221
221
  stallThresholdMs: _,
222
222
  primary: n,
@@ -245,7 +245,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
245
245
  },
246
246
  currentPosition: () => s
247
247
  };
248
- }, ge = /* @__PURE__ */ new Set(["1213", "1205"]), _e = (e) => {
248
+ }, ve = /* @__PURE__ */ new Set(["1213", "1205"]), ye = (e) => {
249
249
  let t = e;
250
250
  for (let e = 0; e < 5 && typeof t == "object" && t; e++) {
251
251
  let e = t.errno;
@@ -254,31 +254,31 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
254
254
  if (typeof n == "string") return n;
255
255
  t = t.cause;
256
256
  }
257
- }, Y = (e) => {
258
- let t = _e(e);
259
- return t !== void 0 && ge.has(t);
260
- }, X = (e) => Y(e) ? "retry" : "noRetry", Z = (e) => {
257
+ }, J = (e) => {
258
+ let t = ye(e);
259
+ return t !== void 0 && ve.has(t);
260
+ }, Y = (e) => J(e) ? "retry" : "noRetry", X = (e) => {
261
261
  if (e == null) return "null";
262
262
  let t = typeof e;
263
263
  if (t === "bigint") return `${e}n`;
264
264
  if (t !== "object") return JSON.stringify(e);
265
265
  if (e instanceof Date) return `"${e.toISOString()}"`;
266
- if (Array.isArray(e)) return `[${e.map(Z).join(",")}]`;
266
+ if (Array.isArray(e)) return `[${e.map(X).join(",")}]`;
267
267
  let n = e;
268
- return `{${Object.keys(n).sort().map((e) => `${JSON.stringify(e)}:${Z(n[e])}`).join(",")}}`;
269
- }, ve = (e) => {
270
- let t = Z(e), n = 2166136261;
268
+ return `{${Object.keys(n).sort().map((e) => `${JSON.stringify(e)}:${X(n[e])}`).join(",")}}`;
269
+ }, be = (e) => {
270
+ let t = X(e), n = 2166136261;
271
271
  for (let e = 0; e < t.length; e++) n ^= t.charCodeAt(e), n = Math.imul(n, 16777619);
272
272
  return (n >>> 0).toString(36);
273
- }, ye = (e, t) => {
273
+ }, xe = (e, t) => {
274
274
  let n = setTimeout(e, t);
275
275
  typeof n.unref == "function" && n.unref();
276
- }, be = class {
276
+ }, Se = class {
277
277
  variant;
278
278
  ttlMs;
279
279
  schedule;
280
280
  seen = /* @__PURE__ */ new Map();
281
- constructor(e, t = 6e4, n = ye) {
281
+ constructor(e, t = 6e4, n = xe) {
282
282
  this.variant = e, this.ttlMs = t, this.schedule = n;
283
283
  }
284
284
  key(e) {
@@ -292,7 +292,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
292
292
  } catch {
293
293
  r = t;
294
294
  }
295
- return `${e.table} ${e.op} ${String(n)} ${ve(r)}`;
295
+ return `${e.table} ${e.op} ${String(n)} ${be(r)}`;
296
296
  }
297
297
  admit(e) {
298
298
  let t = this.key(e);
@@ -307,16 +307,16 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
307
307
  get pending() {
308
308
  return this.seen.size;
309
309
  }
310
- }, xe = /* @__PURE__ */ new Set([
310
+ }, Z = /* @__PURE__ */ new Set([
311
311
  1022,
312
312
  1062,
313
313
  1586
314
314
  ]), Q = async (e) => {
315
- let t = e.variant ?? "mysql", n = N({ scope: `voltro:${t}` }), a = e.changeStrategy ?? "inline", o = a;
315
+ let t = e.variant ?? "mysql", n = P({ scope: `voltro:${t}` }), a = e.changeStrategy ?? "inline", o = a;
316
316
  a === "cdc" && !e.cdcConfig && (n.warn("changeStrategy='cdc' requires cdcConfig (serverId + connection); falling back to 'inline'."), o = "inline");
317
- let s = e.tracerLayer ? r.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, c = i.make(s), l = new Se(await c.runPromise(P), c, t, o);
317
+ let s = e.tracerLayer ? r.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, c = i.make(s), l = new Ce(await c.runPromise(F), c, t, o);
318
318
  return o === "cdc" && e.cdcConfig && await l.startCdcConsumer(e.cdcConfig), l;
319
- }, Se = class e {
319
+ }, Ce = class e {
320
320
  sql;
321
321
  runtime;
322
322
  variant;
@@ -333,13 +333,13 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
333
333
  cdcGate;
334
334
  reportEagerFallback;
335
335
  constructor(e, t, n, r = "inline", i = null, a, o) {
336
- this.sql = e, this.runtime = t, this.variant = n, this.changeStrategy = r, this.namespace = i, this.log = N({ scope: `voltro:${n}` }), this.reportEagerFallback = D(this.log), this.emitter = a ?? new M(), k(this.emitter), this.cdcGate = o ?? new be(n);
336
+ this.sql = e, this.runtime = t, this.variant = n, this.changeStrategy = r, this.namespace = i, this.log = P({ scope: `voltro:${n}` }), this.reportEagerFallback = ee(this.log), this.emitter = a ?? new se(), ne(this.emitter), this.cdcGate = o ?? new Se(n);
337
337
  }
338
338
  withNamespace(t) {
339
339
  return t === this.namespace ? this : new e(this.sql, this.runtime, this.variant, this.changeStrategy, t, this.emitter, this.cdcGate);
340
340
  }
341
341
  nsT(e) {
342
- return ee(this.namespace, e);
342
+ return te(this.namespace, e);
343
343
  }
344
344
  get dialectId() {
345
345
  return this.variant;
@@ -351,7 +351,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
351
351
  };
352
352
  }
353
353
  async executeQuery(e, t, r) {
354
- let i = x(e, this.sql, this.namespace), a = t ? n.provideService(i, F, t) : i;
354
+ let i = x(e, this.sql, this.namespace), a = t ? n.provideService(i, I, t) : i;
355
355
  return S(await O(this.variant, "select", () => this.runtime.runPromise(a)), e.table, this.variant);
356
356
  }
357
357
  get supportsInsertReturning() {
@@ -364,10 +364,10 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
364
364
  return !1;
365
365
  }
366
366
  async executeInsert(e, t, r, i, a) {
367
- t = A(e, t);
367
+ t = M(e, t);
368
368
  let o = this.sql;
369
369
  if (this.supportsInsertReturning) {
370
- let s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(C(t, e))} RETURNING *`, c = r ? n.provideService(s, F, r) : s, l = (await this.runtime.runPromise(c))[0];
370
+ let s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(C(t, e))} RETURNING *`, c = r ? n.provideService(s, I, r) : s, l = (await this.runtime.runPromise(c))[0];
371
371
  if (!l) throw Error(`MysqlStore.insert: no row returned for table '${e}'`);
372
372
  return await this.routeEvent({
373
373
  table: e,
@@ -386,9 +386,9 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
386
386
  new: t
387
387
  }, i, r, a), t;
388
388
  }
389
- let l = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(s)}`, u = r ? n.provideService(l, F, r) : l;
389
+ let l = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(s)}`, u = r ? n.provideService(l, I, r) : l;
390
390
  await this.runtime.runPromise(u);
391
- let d = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${c}`, f = r ? n.provideService(d, F, r) : d, p = (await this.runtime.runPromise(f))[0];
391
+ let d = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${c}`, f = r ? n.provideService(d, I, r) : d, p = (await this.runtime.runPromise(f))[0];
392
392
  if (!p) throw Error(`MysqlStore.insert: row not found post-insert in '${e}'`);
393
393
  return await this.routeEvent({
394
394
  table: e,
@@ -401,16 +401,16 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
401
401
  let i = this.sql;
402
402
  return this.runPinned(r, (r) => n.gen(this, function* () {
403
403
  let a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(t)}`;
404
- yield* n.provideService(a, F, r);
405
- let o = (yield* n.provideService(i`SELECT LAST_INSERT_ID() AS ${i("lastId")}`, F, r))[0]?.lastId;
406
- 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}`, F, r))[0] || (yield* n.fail(/* @__PURE__ */ Error(`MysqlStore.insert: row not found post-insert in '${e}'`)));
404
+ yield* n.provideService(a, I, r);
405
+ let o = (yield* n.provideService(i`SELECT LAST_INSERT_ID() AS ${i("lastId")}`, I, r))[0]?.lastId;
406
+ 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}`, I, r))[0] || (yield* n.fail(/* @__PURE__ */ Error(`MysqlStore.insert: row not found post-insert in '${e}'`)));
407
407
  }), "insert");
408
408
  }
409
409
  async runPinned(e, t, r) {
410
410
  if (e) return this.runtime.runPromise(t(e));
411
411
  this.inflightTxns++;
412
412
  try {
413
- let e = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(F), (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(Y)), o = e.pipe(n.retry(i), n.withSpan("store.insert", { attributes: {
413
+ let e = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(I), (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(J)), o = e.pipe(n.retry(i), n.withSpan("store.insert", { attributes: {
414
414
  "db.system": this.variant,
415
415
  "db.operation": r
416
416
  } }));
@@ -420,16 +420,16 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
420
420
  }
421
421
  }
422
422
  async executeInsertMany(e, t, r, i, a) {
423
- if (t = se(e, t), t.length === 0) return [];
423
+ if (t = oe(e, t), t.length === 0) return [];
424
424
  let o = this.sql, s = _(t.map((t) => C(t, e)), g(this.variant));
425
425
  if (this.supportsInsertReturning) {
426
426
  let t = (t) => o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t)} RETURNING *`, c;
427
427
  if (s.length === 1) {
428
- let e = t(s[0]), i = r ? n.provideService(e, F, r) : e;
428
+ let e = t(s[0]), i = r ? n.provideService(e, I, r) : e;
429
429
  c = await this.runtime.runPromise(i);
430
430
  } else if (r) {
431
431
  let e = [];
432
- for (let i of s) e.push(...await this.runtime.runPromise(n.provideService(t(i), F, r)));
432
+ for (let i of s) e.push(...await this.runtime.runPromise(n.provideService(t(i), I, r)));
433
433
  c = e;
434
434
  } else c = await this.runtime.runPromise(o.withTransaction(n.map(n.forEach(s, t, { concurrency: 1 }), (e) => e.flat())));
435
435
  for (let t of c) await this.routeEvent({
@@ -454,16 +454,16 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
454
454
  if (c.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).");
455
455
  let l = (t) => o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t)}`;
456
456
  if (s.length === 1) {
457
- let e = r ? n.provideService(l(s[0]), F, r) : l(s[0]);
457
+ let e = r ? n.provideService(l(s[0]), I, r) : l(s[0]);
458
458
  await this.runtime.runPromise(e);
459
- } else if (r) for (let e of s) await this.runtime.runPromise(n.provideService(l(e), F, r));
459
+ } else if (r) for (let e of s) await this.runtime.runPromise(n.provideService(l(e), I, r));
460
460
  else await this.runtime.runPromise(o.withTransaction(n.forEach(s, l, {
461
461
  concurrency: 1,
462
462
  discard: !0
463
463
  })));
464
464
  let u = _(c.map((e) => ({ id: e })), g(this.variant)), d = [];
465
465
  for (let t of u) {
466
- 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, F, r) : i;
466
+ 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, I, r) : i;
467
467
  d.push(...await this.runtime.runPromise(a));
468
468
  }
469
469
  let f = new Map(d.map((e) => [e.id, e])), p = c.map((e) => f.get(e)).filter((e) => e !== void 0);
@@ -481,19 +481,19 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
481
481
  let a = [];
482
482
  for (let o of t) {
483
483
  let t = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(o)}`;
484
- yield* n.provideService(t, F, r);
485
- let s = (yield* n.provideService(i`SELECT LAST_INSERT_ID() AS ${i("firstId")}`, F, r))[0]?.firstId;
484
+ yield* n.provideService(t, I, r);
485
+ let s = (yield* n.provideService(i`SELECT LAST_INSERT_ID() AS ${i("firstId")}`, I, r))[0]?.firstId;
486
486
  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?`));
487
- 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`, F, r);
487
+ 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`, I, r);
488
488
  a.push(...l);
489
489
  }
490
490
  return a;
491
491
  }), "insert");
492
492
  }
493
493
  async executePatchJson(e, t, r, i, a, o, s) {
494
- 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, F, a) : m, g = await this.runtime.runPromise(h);
494
+ 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, I, a) : m, g = await this.runtime.runPromise(h);
495
495
  if (g && typeof g.affectedRows == "number" && g.affectedRows === 0) return null;
496
- let _ = c`SELECT * FROM ${c(this.nsT(e))} WHERE ${c("id")} = ${t}`, v = a ? n.provideService(_, F, a) : _, y = (await this.runtime.runPromise(v))[0];
496
+ let _ = c`SELECT * FROM ${c(this.nsT(e))} WHERE ${c("id")} = ${t}`, v = a ? n.provideService(_, I, a) : _, y = (await this.runtime.runPromise(v))[0];
497
497
  return y ? (await this.routeEvent({
498
498
  table: e,
499
499
  op: "update",
@@ -504,7 +504,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
504
504
  async executeUpdate(e, t, r, i, a, o) {
505
505
  let s = this.sql;
506
506
  if (this.supportsUpdateReturning) {
507
- let c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(C(r, e))} WHERE ${s("id")} = ${t} RETURNING *`, l = i ? n.provideService(c, F, i) : c, u = (await this.runtime.runPromise(l))[0];
507
+ let c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(C(r, e))} WHERE ${s("id")} = ${t} RETURNING *`, l = i ? n.provideService(c, I, i) : c, u = (await this.runtime.runPromise(l))[0];
508
508
  return u ? (await this.routeEvent({
509
509
  table: e,
510
510
  op: "update",
@@ -512,9 +512,9 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
512
512
  new: u
513
513
  }, a, i, o), u) : null;
514
514
  }
515
- let c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(C(r, e))} WHERE ${s("id")} = ${t}`, l = i ? n.provideService(c, F, i) : c, u = await this.runtime.runPromise(l);
515
+ let c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(C(r, e))} WHERE ${s("id")} = ${t}`, l = i ? n.provideService(c, I, i) : c, u = await this.runtime.runPromise(l);
516
516
  if (u && typeof u.affectedRows == "number" && u.affectedRows === 0) return null;
517
- let d = s`SELECT * FROM ${s(this.nsT(e))} WHERE ${s("id")} = ${t}`, f = i ? n.provideService(d, F, i) : d, p = (await this.runtime.runPromise(f))[0];
517
+ let d = s`SELECT * FROM ${s(this.nsT(e))} WHERE ${s("id")} = ${t}`, f = i ? n.provideService(d, I, i) : d, p = (await this.runtime.runPromise(f))[0];
518
518
  return p ? (await this.routeEvent({
519
519
  table: e,
520
520
  op: "update",
@@ -525,7 +525,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
525
525
  async executeDelete(e, t, r, i, a) {
526
526
  let o = this.sql;
527
527
  if (this.supportsDeleteReturning) {
528
- let s = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t} RETURNING *`, c = r ? n.provideService(s, F, r) : s, l = (await this.runtime.runPromise(c))[0];
528
+ let s = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t} RETURNING *`, c = r ? n.provideService(s, I, r) : s, l = (await this.runtime.runPromise(c))[0];
529
529
  return l ? (await this.routeEvent({
530
530
  table: e,
531
531
  op: "delete",
@@ -533,9 +533,9 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
533
533
  new: null
534
534
  }, i, r, a), !0) : !1;
535
535
  }
536
- let s = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t}`, c = r ? n.provideService(s, F, r) : s, l = (await this.runtime.runPromise(c))[0];
536
+ let s = o`SELECT * FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t}`, c = r ? n.provideService(s, I, r) : s, l = (await this.runtime.runPromise(c))[0];
537
537
  if (!l) return !1;
538
- let u = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t}`, d = r ? n.provideService(u, F, r) : u;
538
+ let u = o`DELETE FROM ${o(this.nsT(e))} WHERE ${o("id")} = ${t}`, d = r ? n.provideService(u, I, r) : u;
539
539
  return await this.runtime.runPromise(d), await this.routeEvent({
540
540
  table: e,
541
541
  op: "delete",
@@ -545,17 +545,17 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
545
545
  }
546
546
  async appendInTxn(e, t, r) {
547
547
  let i = this.sql, a = i`INSERT INTO ${i(this.nsT(e))} ${i.insert(C(t, e))}`;
548
- await this.runtime.runPromise(r ? n.provideService(a, F, r) : a);
548
+ await this.runtime.runPromise(r ? n.provideService(a, I, r) : a);
549
549
  }
550
550
  async maxInTxn(e, t, r, i) {
551
- 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, F, i) : s))[0]?.m;
551
+ 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, I, i) : s))[0]?.m;
552
552
  return c == null ? null : Number(c);
553
553
  }
554
554
  async routeEvent(e, t, n = null, r) {
555
555
  if (e = {
556
556
  ...p(r),
557
557
  ...e
558
- }, te(e.table) && await oe({
558
+ }, re(e.table) && await ae({
559
559
  append: (e, t) => this.appendInTxn(e, t, n),
560
560
  maxOf: (e, t, r) => this.maxInTxn(e, t, r, n)
561
561
  }, {
@@ -567,7 +567,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
567
567
  subjectId: e.subjectId
568
568
  }), this.changeStrategy === "cdc") {
569
569
  let t = (e.op === "delete" ? e.old : e.new)?.id;
570
- t != null && ne(m(e.table, e.op, t), {
570
+ t != null && ie(m(e.table, e.op, t), {
571
571
  ...e.traceId === void 0 ? {} : { traceId: e.traceId },
572
572
  ...e.subjectId === void 0 ? {} : { subjectId: e.subjectId }
573
573
  });
@@ -598,10 +598,10 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
598
598
  }
599
599
  return await this.executeUpdate(e, o.id, a, r, i) ?? o;
600
600
  }
601
- return j((n) => this.executeInsert(e, t, r, i, n));
601
+ return N((n) => this.executeInsert(e, t, r, i, n));
602
602
  }
603
603
  async executeMariadbUpsert(e, t, r, i, a, o) {
604
- let s = this.sql, c = C(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 = s`INSERT INTO ${s(this.nsT(e))} ${s.insert(c)} ON DUPLICATE KEY UPDATE ${d} RETURNING *`, p = i ? n.provideService(f, F, i) : f, m = (await this.runtime.runPromise(p))[0];
604
+ let s = this.sql, c = C(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 = s`INSERT INTO ${s(this.nsT(e))} ${s.insert(c)} ON DUPLICATE KEY UPDATE ${d} RETURNING *`, p = i ? n.provideService(f, I, i) : f, m = (await this.runtime.runPromise(p))[0];
605
605
  if (!m) throw Error(`MysqlStore.upsert: no row returned for table '${e}'`);
606
606
  let h = t.id !== void 0 && t.id === m.id ? "insert" : "update";
607
607
  return await this.routeEvent({
@@ -612,27 +612,27 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
612
612
  }, a, i, o), m;
613
613
  }
614
614
  async executeInsertIgnore(e, t, r, i, a, o) {
615
- if (t = A(e, t), this.variant === "mariadb") {
616
- let s = this.sql, c = s`INSERT IGNORE INTO ${s(this.nsT(e))} ${s.insert(C(t, e))} RETURNING *`, l = i ? n.provideService(c, F, i) : c, u = (await this.runtime.runPromise(l))[0];
615
+ if (t = M(e, t), this.variant === "mariadb") {
616
+ let s = this.sql, c = s`INSERT IGNORE INTO ${s(this.nsT(e))} ${s.insert(C(t, e))} RETURNING *`, l = i ? n.provideService(c, I, i) : c, u = (await this.runtime.runPromise(l))[0];
617
617
  if (u) return await this.routeEvent({
618
618
  table: e,
619
619
  op: "insert",
620
620
  old: null,
621
621
  new: u
622
622
  }, a, i, o), u;
623
- let d = await this.readWarnings(i), f = d.find((e) => !xe.has(e.code));
623
+ let d = await this.readWarnings(i), f = d.find((e) => !Z.has(e.code));
624
624
  if (f !== void 0) throw Error(`MysqlStore.insertIgnore: the insert into '${e}' was REJECTED, not skipped as a conflict. INSERT IGNORE downgrades every error to a warning, and the warning was: [${f.code}] ${f.message}. Nothing was written and nothing conflicted — fix the cause above.`);
625
625
  let p = await this.findByConflict(e, t, r.conflictColumns, i);
626
626
  if (p) return p;
627
627
  let m = d[0];
628
628
  throw Error(`MysqlStore.insertIgnore: the insert was skipped as a conflict, but no existing row matches conflictColumns [${r.conflictColumns.join(", ")}] on '${e}'. ` + (m === void 0 ? "The warning could not be read on this connection, so the constraint that fired is unknown — it may be a second unique index, or the primary key under another name. " : `The constraint that actually fired: [${m.code}] ${m.message}. `) + "insertIgnore models ONE conflict target: name the columns of the constraint that actually collides, or handle the violation yourself.");
629
629
  }
630
- return await this.findByConflict(e, t, r.conflictColumns, i) || j((n) => this.executeInsert(e, t, i, a, n));
630
+ return await this.findByConflict(e, t, r.conflictColumns, i) || N((n) => this.executeInsert(e, t, i, a, n));
631
631
  }
632
632
  async findUndecodableCdcTables(e) {
633
633
  if (this.variant !== "mariadb") return [];
634
634
  try {
635
- let t = (await this.runtime.runPromise(this.sql.unsafe(G))).map((e) => String(e.tableName ?? e.TABLE_NAME ?? "")).filter((e) => e !== "");
635
+ let t = (await this.runtime.runPromise(this.sql.unsafe(fe))).map((e) => String(e.tableName ?? e.TABLE_NAME ?? "")).filter((e) => e !== "");
636
636
  return e === void 0 ? t : t.filter((t) => e.includes(t));
637
637
  } catch (e) {
638
638
  return this.log.debug(`cdc: could not probe for undecodable tables — ${e?.message ?? String(e)}`), [];
@@ -642,7 +642,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
642
642
  if (e === null) return [];
643
643
  try {
644
644
  let t = this.sql`SHOW WARNINGS`;
645
- return (await this.runtime.runPromise(n.provideService(t, F, e))).map((e) => ({
645
+ return (await this.runtime.runPromise(n.provideService(t, I, e))).map((e) => ({
646
646
  code: Number(e.Code ?? e.code ?? 0),
647
647
  message: String(e.Message ?? e.message ?? "")
648
648
  }));
@@ -652,7 +652,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
652
652
  }
653
653
  async findByConflict(e, t, r, i) {
654
654
  if (r.length === 0) return;
655
- 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, F, i) : s;
655
+ 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, I, i) : s;
656
656
  return (await this.runtime.runPromise(c))[0];
657
657
  }
658
658
  query(e) {
@@ -663,10 +663,10 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
663
663
  return O(this.variant, "raw", () => this.runtime.runPromise(n));
664
664
  }
665
665
  async runWithEager(e, t) {
666
- if (!T(e)) return this.executeQuery(e, t);
666
+ if (!E(e)) return this.executeQuery(e, t);
667
667
  let r = this.variant === "mariadb" ? "mariadb" : "mysql", i = this.namespace === null ? v(e, this.sql, r) : null;
668
668
  if (i !== null) try {
669
- let e = t ? n.provideService(i.fragment, F, t) : i.fragment, r = await O(this.variant, "select", () => this.runtime.runPromise(e));
669
+ let e = t ? n.provideService(i.fragment, I, t) : i.fragment, r = await O(this.variant, "select", () => this.runtime.runPromise(e));
670
670
  return i.decode(r);
671
671
  } catch (t) {
672
672
  if (t instanceof u) throw t;
@@ -682,17 +682,17 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
682
682
  table: e.table,
683
683
  reason: "not-compilable"
684
684
  });
685
- return f(await this.executeQuery(e, t), e.eager, e.sourceTable ?? re(e.table), (e) => this.executeQuery(e, t));
685
+ return f(await this.executeQuery(e, t), e.eager, e.sourceTable ?? k(e.table), (e) => this.executeQuery(e, t));
686
686
  }
687
687
  getInternalRunWithEager() {
688
688
  return this.runWithEager.bind(this);
689
689
  }
690
690
  async localWrite(e, t, n) {
691
691
  return O(this.variant, e, async () => {
692
- if (this.changeStrategy !== "cdc") return j(n);
692
+ if (this.changeStrategy !== "cdc") return N(n);
693
693
  h(t);
694
694
  try {
695
- return await j(n);
695
+ return await N(n);
696
696
  } finally {
697
697
  w(t);
698
698
  }
@@ -733,15 +733,15 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
733
733
  let i = this.sql, o = y(r.where, i, this.namespace);
734
734
  this.changeStrategy === "cdc" && h(e), this.inflightTxns++;
735
735
  try {
736
- let r = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(F), (r) => {
736
+ let r = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(I), (r) => {
737
737
  if (a.isNone(r)) return n.fail(/* @__PURE__ */ Error("MysqlStore.updateMany: TransactionConnection missing."));
738
738
  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(C(t, e))} WHERE ${o}`;
739
- return n.flatMap(n.provideService(c, F, s), (t) => {
739
+ return n.flatMap(n.provideService(c, I, s), (t) => {
740
740
  if (t.length === 0) return n.succeed([]);
741
741
  let r = t.map((e) => e.id), a = i`SELECT * FROM ${i(this.nsT(e))} WHERE ${i("id")} IN ${i.in(r)}`;
742
- return n.flatMap(n.provideService(l, F, s), () => n.provideService(a, F, s));
742
+ return n.flatMap(n.provideService(l, I, s), () => n.provideService(a, I, s));
743
743
  });
744
- }))), c = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(Y)), l = r.pipe(n.retry(c), n.withSpan("store.updateMany", { attributes: {
744
+ }))), 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: {
745
745
  "db.system": this.variant,
746
746
  "db.operation": "update"
747
747
  } })), u = await this.runtime.runPromise(l);
@@ -772,11 +772,11 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
772
772
  }
773
773
  this.changeStrategy === "cdc" && h(e), this.inflightTxns++;
774
774
  try {
775
- let t = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(F), (t) => {
775
+ let t = n.suspend(() => this.sql.withTransaction(n.flatMap(n.serviceOption(I), (t) => {
776
776
  if (a.isNone(t)) return n.fail(/* @__PURE__ */ Error("MysqlStore.deleteMany: TransactionConnection missing."));
777
777
  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}`;
778
- return n.flatMap(n.provideService(s, F, o), (e) => e.length === 0 ? n.succeed(e) : n.as(n.provideService(c, F, o), e));
779
- }))), o = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(Y)), c = t.pipe(n.retry(o), n.withSpan("store.deleteMany", { attributes: {
778
+ return n.flatMap(n.provideService(s, I, o), (e) => e.length === 0 ? n.succeed(e) : n.as(n.provideService(c, I, o), e));
779
+ }))), 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: {
780
780
  "db.system": this.variant,
781
781
  "db.operation": "delete"
782
782
  } })), l = await this.runtime.runPromise(c);
@@ -794,14 +794,14 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
794
794
  }
795
795
  }
796
796
  emitChange(e) {
797
- E(e.table) && (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || this.emitter.emit("change", e));
797
+ D(e.table) && (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || this.emitter.emit("change", e));
798
798
  }
799
799
  async startCdcConsumer(e) {
800
800
  if (this.cdcHandle) return;
801
801
  await this.assertBinlogConfig(), this.cdcReplicaId = e.replicaId;
802
802
  let t = await this.readCdcOffset(e.replicaId) ?? await this.resolveBinlogEnd(), n = await this.findUndecodableCdcTables(e.includeTables);
803
- for (let e of n) this.log.error(K(e));
804
- this.cdcHandle = await J({
803
+ for (let e of n) this.log.error(G(e));
804
+ this.cdcHandle = await q({
805
805
  connection: e.connection,
806
806
  serverId: e.serverId,
807
807
  variant: this.variant,
@@ -919,10 +919,10 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
919
919
  async transactional(e) {
920
920
  this.inflightTxns++;
921
921
  try {
922
- return await ae({
922
+ return await j({
923
923
  ...this.txnSpec("MysqlStore.transactional"),
924
924
  work: e,
925
- makeView: (e, t) => new Ce(this, e, t)
925
+ makeView: (e, t) => new we(this, e, t)
926
926
  });
927
927
  } finally {
928
928
  this.inflightTxns--;
@@ -934,7 +934,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
934
934
  dialect: this.variant === "mariadb" ? "mariadb" : "mysql",
935
935
  withTransaction: (e) => this.sql.withTransaction(e),
936
936
  runPromiseExit: (e) => this.runtime.runPromiseExit(e),
937
- isRetryable: Y,
937
+ isRetryable: J,
938
938
  span: {
939
939
  name: "store.transactional",
940
940
  attributes: {
@@ -953,14 +953,10 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
953
953
  return this.changeStrategy === "cdc" ? "fleet" : "local";
954
954
  }
955
955
  injectExternalChange(e) {
956
- if (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || !E(e.table)) return;
956
+ if (this.changeStrategy === "cdc" && !this.cdcGate.admit(e) || !D(e.table)) return;
957
957
  let t = (e.op === "delete" ? e.old : e.new)?.id;
958
- ie(e.table, e.op, t, (t) => {
959
- this.emitter.emit("change", {
960
- ...t,
961
- ...e,
962
- origin: "injected"
963
- });
958
+ A(e.table, e.op, t, (t) => {
959
+ this.emitter.emit("change", T(e, t));
964
960
  });
965
961
  }
966
962
  run(e) {
@@ -984,7 +980,7 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
984
980
  async ping() {
985
981
  await this.runtime.runPromise(this.sql`SELECT 1`);
986
982
  }
987
- }, Ce = class {
983
+ }, we = class {
988
984
  parent;
989
985
  txn;
990
986
  attr;
@@ -1057,12 +1053,12 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
1057
1053
  this.events.length = 0;
1058
1054
  }
1059
1055
  }
1060
- }, $ = (e) => e.__mysqlReplicationFriend ?? null, we = () => ({
1056
+ }, $ = (e) => e.__mysqlReplicationFriend ?? null, Te = () => ({
1061
1057
  async capturePrimaryPosition(e) {
1062
1058
  let t = $(e);
1063
1059
  if (t === null) throw Error("mysqlReplicationAdapter: primary is not a MysqlStore (missing __mysqlReplicationFriend).");
1064
1060
  return t.runEffect(n.gen(function* () {
1065
- let e = yield* P, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
1061
+ let e = yield* F, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
1066
1062
  return typeof r == "string" ? r : yield* n.die("mysql/mariadb did not return a GTID set");
1067
1063
  }));
1068
1064
  },
@@ -1070,31 +1066,31 @@ var ce = (e) => e ? { rejectUnauthorized: !1 } : void 0, le = (e) => {
1070
1066
  let t = $(e);
1071
1067
  if (t === null) throw Error("mysqlReplicationAdapter: replica is not a MysqlStore.");
1072
1068
  return t.runEffect(n.gen(function* () {
1073
- let e = yield* P, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
1069
+ let e = yield* F, r = (t.variant === "mariadb" ? yield* e`SELECT @@global.gtid_current_pos AS gtid` : yield* e`SELECT @@global.gtid_executed AS gtid`)[0]?.gtid;
1074
1070
  return typeof r == "string" ? r : yield* n.die("mysql/mariadb replica did not return a GTID set");
1075
1071
  }));
1076
1072
  },
1077
1073
  compare(e, t) {
1078
1074
  return "behind";
1079
1075
  }
1080
- }), Te = {
1076
+ }), Ee = {
1081
1077
  id: "mysql",
1082
- makeSqlLayer: (e) => V(e),
1078
+ makeSqlLayer: (e) => H(e),
1083
1079
  makeStore: (e) => Q({
1084
1080
  ...e,
1085
1081
  variant: "mysql"
1086
1082
  }),
1087
1083
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
1088
- retryFilter: X
1089
- }, Ee = {
1084
+ retryFilter: Y
1085
+ }, De = {
1090
1086
  id: "mariadb",
1091
- makeSqlLayer: (e) => V(e),
1087
+ makeSqlLayer: (e) => H(e),
1092
1088
  makeStore: (e) => Q({
1093
1089
  ...e,
1094
1090
  variant: "mariadb"
1095
1091
  }),
1096
1092
  compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
1097
- retryFilter: X
1093
+ retryFilter: Y
1098
1094
  };
1099
1095
  //#endregion
1100
- export { c as CDC_OFFSETS_TABLE, e as MysqlClient, d as _voltroCdcOffsetsTable, B as connectionFromConfig, R as makeMysqlSqlLayer, V as makeMysqlSqlLayerFromConfig, Q as makeMysqlStore, Ee as mariadbDialect, Te as mysqlDialect, we as mysqlReplicationAdapter, X as mysqlRetryFilter, J as startBinlogCdc };
1096
+ export { c as CDC_OFFSETS_TABLE, e as MysqlClient, d as _voltroCdcOffsetsTable, V as connectionFromConfig, z as makeMysqlSqlLayer, H as makeMysqlSqlLayerFromConfig, Q as makeMysqlStore, De as mariadbDialect, Ee as mysqlDialect, Te 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.34.0",
3
+ "version": "0.35.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.34.0",
39
- "@voltro/logger": "0.34.0"
38
+ "@voltro/database": "0.35.0",
39
+ "@voltro/logger": "0.35.0"
40
40
  },
41
41
  "optionalDependencies": {
42
42
  "@vlasky/zongji": "^0.9.0"