@voltro/database 0.68.0 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -39,6 +39,228 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.69.0] — 2026-09-10
43
+
44
+ ### ⚠ BREAKING
45
+
46
+ - **@voltro/runtime** — A typed store error now reaches the caller as itself instead of being re-wrapped as `StoreOperationFailed`.
47
+
48
+ The Effect store's catch boundary passed two of the six `StoreError` variants through by name and wrapped everything else. The wrapper builds `cause` with `wireSafeCause`, which reads DRIVER fields — and a typed error carries none, so it landed on its own class name. A consumer's postgres answered `23503` with `ai_generation_requests_createdBy_fkey`; the store classified it into a `ConstraintViolation` carrying that name, and the client received `StoreOperationFailed { cause: "ConstraintViolation" }`. The constraint name was extracted correctly and then discarded by re-wrapping — the half that turns a half-hour search into a one-line diagnosis, and the half `cause` promises in its own doc comment.
49
+
50
+ `ConstraintViolation`, `TableValidationFailed` and `ServerOnlyColumnWrite` now arrive typed, with their constraint, issues and columns intact. The rule is over the SET — every `StoreError` variant — because naming variants individually is what produced this.
51
+
52
+ **Check your descriptors' `error:` unions.** A handler that declared only `StoreOperationFailed` to catch constraint failures will now see a `ConstraintViolation` it does not declare, which reaches the client as an untagged internal error instead. Declare the variant you actually want to catch, or `StoreError` for all of them.
53
+
54
+ **`voltro update` carries you across this** — codemod `0.69.0/06_typed-store-errors-are-not-rewrapped`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.69.0).
55
+ - **@voltro/runtime, @voltro/voltro** — Issuing an API key now plants its `actors` row, and `ApiKeyStore` gained a required `ensureActor`.
56
+
57
+ `actors.kind` is declared by the framework as `'user' | 'serviceAccount' | 'apiKey' | 'system'` and `audit()` makes every `createdBy` a foreign key onto that table — so an API key is explicitly an audit subject. `issue()` wrote only `_voltro_api_keys`, so the row never existed: measured in a consumer's database, `actors with kind='apiKey'` came back 0, and the first audit-stamped insert by a key-authenticated caller died on the foreign key. `apiKeys:` and `audit()` are both on by default and were not combinable.
58
+
59
+ The row is planted BEFORE the key row, so a failure leaves no usable key rather than one that authenticates and then dies on its first audited write. Its id equals the key id, which is what `apiKeyStrategy` presents as `subject.id` — the `actors.id == subject.id` convention the core table documents. Revoking leaves the row: it is the author of everything that key wrote.
60
+
61
+ `@voltro/voltro` is named too because it re-exports the runtime surface, so the break reaches an app importing from the aggregate exactly as it reaches one importing from `@voltro/runtime`. (`memoryApiKeyStore`'s return type also widened to expose the planted rows for tests — an intersection, still assignable to `ApiKeyStore`, so that half breaks nobody.)
62
+
63
+ `ensureActor` is REQUIRED rather than optional. An optional hook would be the same defect with a nicer name — the mistake was omission, and an optional field is omissible. Both framework stores implement it; only an app with its own `ApiKeyStore` has anything to do.
64
+
65
+ **`voltro update` carries you across this** — codemod `0.69.0/05_api-key-store-plants-its-actor`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.69.0).
66
+ - **@voltro/ai, @voltro/plugin-billing, @voltro/plugin-comments** — `ai`, `@ai-sdk/provider`, `@ai-sdk/gateway` and `stripe` are peer dependencies now — and `@voltro/plugin-comments` stops shipping its own `effect`.
67
+
68
+ `@voltro/ai` declared `ai: ^7.0.92` under `dependencies`. An app that configures a provider directly declares `ai` itself, and one that pinned it below our floor got a SECOND physical copy — with it a second `@ai-sdk/provider-utils`. Those types are branded with a symbol, so two structurally identical copies are not assignable to each other:
69
+
70
+ Argument of type '…provider-utils@5.0.39/…ToolSet | undefined' is not assignable to parameter of type '…provider-utils@5.0.33/…ToolSet | undefined'
71
+
72
+ Nothing fails at install and nothing fails at runtime. It surfaces as a typecheck error, only at the seams where an SDK type crosses the package boundary — which for `@voltro/ai` is thirteen types in its own api golden, `ToolSet` among them. `effect` has been a peer from the start for exactly this argument; it had never been applied to the SDKs whose types cross the same line.
73
+
74
+ **The criterion is narrower than "the type crosses the boundary", and the difference is what keeps this from becoming churn.** Counted over the repo, 21 packages have a third-party dependency whose types appear in their api golden. A second copy needs a second DECLARER, so the question is whether the APP is expected to declare the package too. It is for `effect` (every handler imports it), for `ai` / `@ai-sdk/*` (you declare them to configure a provider) and for `stripe` (`normalizeStripeEvent` takes an event built by the app's own SDK instance). It is not for `@effect/sql`, the dialect drivers, `ioredis`, `@opentelemetry/*` or `@radix-ui/*` — one range is declared in one place and one copy resolves.
75
+
76
+ `scripts/check-boundary-peers.mjs` (CI + `pnpm check:boundary-peers`) asserts that rule and PRINTS the judged-invisible set on every run, because a check that has quietly narrowed reads exactly like a clean one. It ships a `--selftest` that re-introduces `ai` as a dependency and requires the rule to fire.
77
+
78
+ `@voltro/plugin-comments` was the one package declaring `effect` under `dependencies` while every other peers it — the two-copies rule the repo already holds absolutely, with a single unapplied exception.
79
+
80
+ Most apps do nothing: the installer resolves a peer it can satisfy. An install that now warns about an unmet peer is the change working — it is saying that your pin and ours do not intersect, which is what silently produced the second copy before.
81
+
82
+ **`voltro update` carries you across this** — codemod `0.69.0/03_sdk-peers-are-declared-by-the-app`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.69.0).
83
+ - **@voltro/plugin-storage** — The self-service upload routes refuse a caller with no identity, unless the app declares `storagePlugin({ anonymousUploads: true })`.
84
+
85
+ Those routes are `openAccess`, and each justified itself in source with "the ticket binds the calling subject + tenant, caps size, and the finalize path runs limits + scan". Every clause after the first depends on there BEING a calling subject, and `openAccess` does not establish one — an unauthenticated request resolves to an anonymous subject carrying `id: null`. So a visitor could mint a ticket with `sub: null, tenant: null`, and the object it finalized belonged to no owner and sat in no tenant: unreachable by every later owner check, tenant filter and quota, with a subject binding that compares `null` against `null` and therefore passes for every subsequent caller.
86
+
87
+ The framework already refuses exactly that comparison one level up — `defineMutation` rejects `requiresApproval` beside `openAccess` because "two anonymous callers both compare as `null`". Same shape, one layer down.
88
+
89
+ Whether a caller must be identified is not a wire question and the descriptor cannot answer it: only the app knows whether it wants a public intake. So the WIRE stays open — `openAccess` is unchanged and still means "anyone who can reach this may call it" — and the identity requirement lives in the plugin, where the app's own answer is in scope. `anonymousUploads` defaults to false; an app that turns it on is choosing ownerless objects deliberately, and should pair it with `limits` and a `scan`.
90
+
91
+ It is fixed over the SET rather than at the route that shows it best. Five handlers took the subject straight off the request — the ticket mint, the presigned mint, the resumable begin, the multipart begin and the direct upload — and `ctx.request.subject.id` reads as locally correct at every one of them; a sixth is one paste away. There is one resolver now, `ingestUrl` is routed through it too so write attribution has a single spelling in the file, and a test asserts over the source that no `sub:` or `ownerId:` takes its value from the request.
92
+
93
+ The finalize and multipart-completion comparisons said "the token is bound to the minting subject; a different caller can't finalize it", which is false for a ticket that was minted anonymously. That is one named function now, and it says what the binding actually is in that case: possession of the signed, short-lived token, and nothing else.
94
+
95
+ **`voltro update` carries you across this** — codemod `0.69.0/02_anonymous-uploads-must-be-declared`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.69.0).
96
+ - **@voltro/database** — `releaseMigrationLock` is gone; `acquireMigrationLock` returns the release.
97
+
98
+ Because the MySQL migration lock was being released while the migration was still running.
99
+
100
+ Every dialect but sqlite implements it as a SESSION-scoped object — `GET_LOCK`, `pg_advisory_lock`, `sp_getapplock @LockOwner = 'Session'` — and all three die with the connection that took them. Taken through the pooled client, that stated an intent the plumbing did not keep. Measured on MySQL 8, with a second connection watching both the lock and the process list:
101
+
102
+ direkt nach acquire gehalten von conn 131 +500ms lock=FREI conn 131 lebt=false
103
+
104
+ The holder was gone inside half a second — closed, not returned to the pool — so the server dropped the lock and two `voltro db apply` runs against one server could both proceed past the thing that exists to stop exactly that. Postgres survived the same window, which is a driver's idle policy rather than a property: `@effect/sql-pg` keeps its connections and `@effect/sql-mysql2` does not. The lock now RESERVES its connection for as long as it is held, on every session-scoped dialect. sqlite is exempt and must stay exempt — its lock is a no-op and reserving its single connection would starve the work.
105
+
106
+ `acquireMigrationLock` returns the release instead of there being a free `releaseMigrationLock(sql, scope)`. A free release cannot know which connection took the lock, and on all three dialects a release on the wrong connection is a no-op that reports success — which is what it had been doing.
107
+
108
+ **Fixing that alone would have wedged every migration after a failed one**, and that is the more important half. `applyPlan` and the file-based runner released the lock from a `try/finally` inside `Effect.gen`, and a `finally` there does NOT run when a yielded effect FAILS: Effect short-circuits without resuming the generator. Measured — a deliberately failing operation reached neither the finalizer nor the release. The hole is as old as those functions and was invisible for as long as the release did nothing and the lock dissolved on its own. Two defects cancelling reads exactly like neither existing. Every acquisition is bracketed with `Effect.acquireUseRelease` now, which also covers defects and interruption, and a guard asserts it over the SET of call sites rather than the one that was found — three had the same shape, and the shape looks correct.
109
+
110
+ The fakes gained the `reserve` a `SqlClient` always owed, from one shared definition rather than six copies. It suspends: a real connection method BUILDS an effect rather than running the statement, and a fake that records on call instead of on run reports `RELEASE_LOCK` before the DDL it follows — a fake inventing an ordering violation, which reads exactly like a real one.
111
+
112
+ **`voltro update` carries you across this** — codemod `0.69.0/01_migration-lock-returns-its-release`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.69.0).
113
+ - **@voltro/protocol** — `workflowToRpc` preserves the literal workflow name and the payload schema, so a workflow no longer widens the generated client.
114
+
115
+ It was typed `Rpc.Rpc<string, Schema.Schema.Any, …>`. A by-name lookup in an rpc group matches members by tag and every tag is assignable to `string`, so that one member joined the lookup for every OTHER procedure: a single workflow erased typed access to the entire group.
116
+
117
+ Filed BREAKING under this file's own "more precise is still BREAKING" rule — nothing was removed, the client simply gained the type information it should always have had, and that is strictly more ways to fail `tsc`. Apps without a workflow are unaffected.
118
+
119
+ Migration: run `voltro codegen` (or boot `voltro dev`), then typecheck. The new errors are real disagreements — a payload that never matched its schema and failed at runtime instead, a now-redundant cast, or a cast that refuses because the two shapes genuinely differ. Do not silence any of them with `as unknown as`; that restores exactly the blindness this removes.
120
+
121
+ **`voltro update` carries you across this** — codemod `0.69.0/04_workflow-rpc-types-are-precise`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.69.0).
122
+
123
+ ### Added
124
+
125
+ - **@voltro/database, @voltro/runtime** — `upsertRowOutcome(store, table, row, options)` — the upsert, plus whether it INSERTED or UPDATED.
126
+
127
+ `upsert` returns the row and not what happened to it, so an app that has to report "created" reads first and then upserts. Measured on a real ingest: one extra query per row, purely to set a flag — and a window, because another writer between the read and the write makes the answer wrong.
128
+
129
+ It costs nothing to answer. The store wrapper mints the row's `id` before the statement, and on the conflict path every dialect hands back the EXISTING row's id — a fact `upsertBinding`'s header already relied on for the encrypted-column binding. Two ids that differ ARE the answer. No dialect implements anything: the outcome is decided from what the wrapper already had in hand.
130
+
131
+ **`'inserted'` is never a stand-in for "could not tell",** and that is the half worth reading. The free comparison needs an id the wrapper minted — a caller-supplied id is unchanged on both paths, and a database-assigned one does not exist yet — so in those two cases the destination is READ first and the answer stays exact. Only a call that asked for the outcome pays for it; plain `upsert` is byte-for-byte the call it was.
132
+
133
+ That required separating two facts `upsertDestinationId` had collapsed. It returns the OFFERED id both when nothing conflicts and when the conflicting row's id happens to BE the offered one — identical answers for binding a ciphertext, opposite answers for "what did this do". `upsertConflictId` reports existence; the destination is derived from it.
134
+
135
+ The helper is `async` so its refusal (a hand-written `DataStore` without the method) arrives as a rejection rather than a synchronous throw: a function whose type says `Promise` and which sometimes throws before returning one splits every caller's error handling in two.
136
+
137
+ **No `upsertMany`.** The batch form was asked for alongside this, and the reported batches are ~30 rows — at that size the round trips are not the cost, the missing `created` flag was, and it is now free. A conflict-write batch still belongs in ONE `store.transactional` with a capped input, which is what the `bulk-write-in-loop` advice says.
138
+ - **@voltro/cli** — `VOLTRO_DASHBOARD=on` — the counterpart `off` never had.
139
+
140
+ The launcher skips the dashboard when stdout is not a TTY, which is a good stand-in for "nobody is watching": CI, a smoke run, a piped script. It stops being one the moment a task runner is in the way. `turbo run dev` — how a multi-app project is normally brought up — captures each task's output to prefix it (24 731 prefixed lines in one gate log), so `isTTY` is false for a developer sitting right in front of it, and the dashboard could not be turned on at all.
141
+
142
+ Measured on the same non-TTY boot, before and after:
143
+
144
+ without dashboard not launched: stdout is not a TTY (CI/smoke) with auto-launching dashboard at http://localhost:5179
145
+
146
+ It overrides only the guess. The recursion guard, the launch lock and the port-in-use check are measurements rather than proxies and still apply — so two apps started together still share one dashboard, and the second reports `already-running` instead of racing for the port.
147
+
148
+ The skip reason names the variable, because a guess you cannot overrule is a wall.
149
+ - **@voltro/devtools-ui** — Show automatic data synchronization as a connection status, highlight changed cells and new visible rows, and briefly retain confirmed deleted rows in red. Query navigation stays quiet, active editors retain focus, and reduced motion uses a static tint. Local and Cloud providers carry CDC deletion identities.
150
+ - **@voltro/ui-shadcn, @voltro/devtools-ui** — Cloud and local Devtools now share `SignalWorkspace`, `SignalWorkspaceHeader` and `AppInspectorHeader`: one masthead, background, heading rhythm and app identity. Local navigation and live charts keep their full functionality while using the shared workspace styling and semantic theme colors.
151
+ - **@voltro/database** — `insertManyRows(store, table, rows)` — `insertRow`'s batch, typed on the element.
152
+
153
+ It forwards to `store.insertMany` unchanged, so the chunking, the dialect behaviour and the conflict semantics stay exactly what the string-keyed call already did. What it adds is the type link the string-keyed form cannot have: every element is checked against the table's insert row, so an element missing a required column, misspelling one, or passing a `number` where the column is a timestamp is a compile error at the call site rather than a dialect error somewhere inside the batch.
154
+
155
+ The batch needed its own helper rather than a loop over `insertRow` because of where the mismatches actually come from: a batch is usually MAPPED — from a parsed import, an API page — and the loop body's inferred element type is only as good as whatever produced the array. That is the shape a mapped `Date` versus `number` survives review in.
156
+
157
+ The set-based conflict forms (`insertManyIgnore`, `upsertMany`) remain unbuilt, for the reasons already recorded against them; this is a typing gap, not a conflict-handling one.
158
+
159
+ ### Fixed
160
+
161
+ - **@voltro/protocol** — An auth strategy that declares a `claimsBearerPrefix` is now asked BEFORE strategies that declare none, for a token carrying that prefix.
162
+
163
+ Turning on `apiKeys:` made every API key unusable as soon as a JWT strategy sat in the chain. Two reasonable pieces produced a third thing nobody chose: `apiKeys:` appends the framework key strategy LAST, and `jwtBearerStrategy` — so all six catalog strategies (supabase, auth0, clerk, kinde, oidc, workos) — claims EVERY `Authorization: Bearer`. A token it cannot verify comes back `failed`, not `skip`, and a `failed` ends the chain by design, because a credential another IdP rejected could be an attack masquerading as ours. So a valid key was refused by the JWT verifier before the key strategy was ever asked. Nothing logged an error; the key simply never worked.
164
+
165
+ The boot check could not see it: it warns when TWO strategies declare the same prefix, and the JWT strategy declares none while claiming everything.
166
+
167
+ `failed` still ends the chain — that property is untouched. What changed is who is asked first: a declared prefix is a strategy saying "this token shape is mine", which is more specific than claiming everything by declaring nothing. A token matching no declared prefix leaves the chain in exactly the order the app wrote it, so a real JWT is still claimed, and still rejected, by its own verifier.
168
+ - **@voltro/cli** — `voltro build` fails when the SSR bundle it just wrote cannot be imported — and `build: { ssrExternal: [...] }` is the exit that `ssr.noExternal: true` did not have.
169
+
170
+ The web SSR build bundles everything the graph reaches so a production image needs nothing from `node_modules`. A CommonJS dependency pulled into that ES module carries its `__dirname` / `__filename` along, and neither exists there. Measured against a real app: the build is GREEN, the bundle is written, and the artefact dies on its first import with `ReferenceError: __dirname is not defined in ES module scope`. The same dependency externalised: exit 0.
171
+
172
+ Two things were missing, and only fixing both helps. `noExternal: true` was hard-wired with no config evaluation beside it, so an app had no way to keep one package a runtime import — there was no app-side workaround at all. And the build reported success, so the failure arrived at deploy time as a boot crash three layers from its cause.
173
+
174
+ The externalisation is deliberately NOT automatic. Externalising on the framework's own initiative puts a package back among the image's runtime requirements; an image built on the promise that it needs nothing from `node_modules` would then fail to boot for a second reason, with nothing said about either.
175
+
176
+ **The detector reads the artefact, and it was wrong once before it was right.** Run over the 357 built ESM files in this repo it flagged exactly one — a false positive, in `@voltro/cli`'s own bundle, on the line
177
+
178
+ "import { join as __join, dirname as __dirname } from 'node:path'"
179
+
180
+ which is a string the build WRITES into a generated file. That is the same trap this repo already recorded once, for a different rule in the same package: a regex cannot tell a specifier from a specifier-shaped string. String literals are blanked before the scan and an import alias counts as a definition.
181
+
182
+ The correction had a second edge. A parameter arm added for the CommonJS wrapper (`function (…, __dirname)`) read `join(__dirname, 'assets')` as a declaration — a call argument and a parameter are the same characters — and silenced the exact reference the check exists for. It is gone; no shim of that shape appears in any of the 357 files, so it bought nothing and cost the rule. Both directions are asserted, and the false positive is kept as a fixture.
183
+ - **@voltro/devtools-ui, @voltro/ui-shadcn** — Keep the dashboard masthead, tabs and inspector context compact so working tables start higher in the viewport. Group the app return link, identity, address and status in one responsive header shared by local and Cloud inspectors.
184
+ - **@voltro/devtools-ui, @voltro/ui-shadcn** — Use the shared styled Checkbox for dashboard filters, workflow confirmations, inspect access and schema-driven checkbox forms, matching Cloud configuration controls while preserving native label, keyboard and form behavior. Checked and indeterminate fills now retain the primary color in dark mode.
185
+ - **@voltro/cli** — The auto-launched dashboard died 85 ms after it was announced, silently.
186
+
187
+ `bin/voltro.mjs` is the only entry that registers the tsx ESM loader, and the CLI cannot run without it — it imports the app's own `app.config.ts` at runtime. The launcher derived the child's bin from `process.argv[1]` and handled ONE spelling: `<cli>/src/bin.ts` → `<cli>/bin/voltro.mjs`. Everything else fell through to argv[1] itself.
188
+
189
+ But `bin/voltro.mjs` re-execs, so inside the CLI argv[1] is the entry BEHIND it. Measured on a normal boot:
190
+
191
+ argv[1] = …/packages/cli/dist/bin.js bin = …/packages/cli/dist/bin.js
192
+
193
+ That is what every installed user runs, and every maintainer tree that has been built once. The child was handed the bundled entry with no loader and died on its first TypeScript import:
194
+
195
+ ERR_MODULE_NOT_FOUND: Cannot find module '…/packages/logger/src/logger' imported from '…/packages/logger/src/index.ts'
196
+
197
+ Nothing said so. The child is spawned with `stdio: 'ignore'` and nothing watched its exit, so the last word was the launcher's own `dashboard launched at http://localhost:5179` — a URL that answered nothing. The `/src/bin.ts` branch is why this was invisible from inside the framework repo: running unbuilt from source is the one case that worked.
198
+
199
+ The bin is resolved by walking up from the launcher's own module until `bin/voltro.mjs` is found, so `src/` and `dist/` reach the same answer — the point being that it must not depend on which of the two is running. Verified end to end from a `voltro dev` boot: `dashboard launched`, then HTTP 200 with the dashboard's own title.
200
+ - **@voltro/cli** — The recommended dashboard install line contradicted the stability contract.
201
+
202
+ `pnpm add -D @voltro/dashboard` writes a caret range. The contract at the top of `CHANGELOG.md` tells users to pin exact versions and never `^`, and a project that follows it ends up with one dependency spelled differently from all the others — because we told it to. The message and both docs pages now say `--save-exact`.
203
+ - **@voltro/cli** — An interrupted dashboard build made the launcher choose the one mode that cannot run in it — and then said nothing when the child died.
204
+
205
+ The mode was chosen by `existsSync(.framework/dist/index.html)`. A build writes that file EARLY and writes `dist/server/ssrEntry.js` LATE, so a build that dies in between — the prerender step is the one that does — leaves a tree that looks built. Measured on exactly such a tree:
206
+
207
+ serving the Voltro dashboard on :5199 … mode: start fatal: production `voltro start` requires a precompiled SSR bundle at …/.framework/dist/server/ssrEntry.js but it is missing
208
+
209
+ Keyed on the bundle instead, the same tree falls back to `dev`: vite ready in 169 ms, the port answers.
210
+
211
+ There were TWO deciders, spelled differently — the auto-launcher and the standalone `voltro dashboard` — and both were wrong the same way. One `dashboardIsPrebuilt` now, asserted over the set, because a third is one paste away.
212
+
213
+ **And the death was silent.** The launcher spawns with `stdio: 'ignore'`, which throws the child's own diagnosis away, and had no `exit` or `error` handler at all. So the last word on a dead dashboard was the parent's own `auto-launching dashboard at http://localhost:…` — a promise that was already false by the time anyone read it. A non-zero exit is reported now, and a signal stays quiet because that is how Ctrl-C reaches the whole process group.
214
+ - **@voltro/devtools-ui** — Rework the shared data explorer around compact resizable columns, debounced search, explicit filter application, accessible record editing and searchable references. Preserve database defaults when creating rows, distinguish empty text from null, retain invalid drafts and write failures, confirm destructive actions and unsaved changes, and expose an optional host refresh callback for current snapshots.
215
+ - **@voltro/devtools-ui** — Resize the data explorer's table list independently, read complete table names in delayed themed tooltips, and resize columns with visible pointer/keyboard handles. Double-click a divider to restore its default width.
216
+ - **@voltro/cli** — Resolve local dashboard stream credentials for the inspected app in both dev and production, matching snapshot reads, and flush the development stream handshake immediately so live data updates can connect without repeated authorization failures.
217
+ - **@voltro/devtools-ui** — Make inspector tasks directly discoverable through a grouped sidebar, replace flat route cards with a shared expandable route explorer, and keep Data explorer visible for frontend apps through an API data-source picker. Bound the mobile table list and support keyboard activation of editable cells and foreign-key navigation.
218
+ - **@voltro/client** — A `title` annotation on a schema field now becomes the field's label — where the title is one somebody wrote.
219
+
220
+ `schemaToFields` / `schemaToColumns` derived every label from the humanised property name and ignored `title` entirely, so an app that had already annotated its schema (for OpenAPI output, say) had to say the same thing twice through `formField({ label })`, with the two free to drift.
221
+
222
+ `title` could not simply be read, and the reason is worth stating because the obvious rule is wrong. effect writes its OWN titles into that field, and it writes them INLINE on the property node — not only into a `$defs` entry behind a `$ref`. Measured:
223
+
224
+ Schema.NonEmptyString.annotations({ description: 'Der Kundenname' }) -> { title: 'nonEmptyString', description: 'Der Kundenname', … } Schema.String.pipe(Schema.minLength(2)) -> { title: 'minLength(2)', … }
225
+
226
+ So "use the title unless it arrived through a `$ref`" puts `minLength(2)` on a form field. What separates the two is the SHAPE of the name effect generates: a lowerCamel type name, optionally carrying the filter's arguments in parens. A title matching that is declined; everything else is the author's.
227
+
228
+ Two properties keep that honest. The failure direction is safe — a declined title falls back to the humanised name, which is what every field got before, so the rule can never do worse than the old behaviour. And it is not left as a claim about a dependency: the test regenerates the corpus through a live `JSONSchema.make` and fails if any generated title stops matching, so a change in effect goes red in the suite instead of wrong on somebody's form.
229
+
230
+ `formField({ label })` still wins over both — it is the one channel effect cannot write into.
231
+ - **@voltro/cli** — `voltro update` no longer rewrites a package.json that does not consume the framework.
232
+
233
+ Reported after an upgrade changed `services/api/package.json` — an independent Strapi 4 backend with NO voltro dependency — from React 18 to 19. Nothing in the command's output named the file, and the app avoided it on the next round by setting versions by hand and running its own installer, which is a sidestep and not a fix.
234
+
235
+ The peer alignment already had three conditions, and all three are about the DEPENDENCY: only a peer the manifest already declares, only when its floor is below ours, only ranges both sides can parse. Every one of them was satisfied here — React was declared, 18 is below 19, both ranges parse. None of them asked whether the package was ours to edit.
236
+
237
+ It is not a new judgement, it is an unapplied one. `planWorkspaceBump` — the `@voltro/*` half of the same command — has always dropped manifests that declare no `@voltro/*` dep. The peer alignment was added later and did not inherit it. It does now: a manifest is in scope when it declares `voltro` or any `@voltro/*` in its dependencies, devDependencies or peerDependencies, and one package being out of scope does not take its siblings with it.
238
+
239
+ `consumesVoltro` is deliberately the whole test. A peer requirement comes FROM a `@voltro/*` package; a workspace member that declares none is not bound by it, however many packages it happens to share a name with.
240
+ - **@voltro/cli** — `input/uncapped-array` is silent on `internal: true`, and `raw-fetch` is silent on a `fetch` an SDK asked for as an option.
241
+
242
+ Both rules were reporting a set their own sentence does not describe, and both were found by an app that read every remaining finding instead of filing them.
243
+
244
+ **`input/uncapped-array` says "the caller chooses how many".** `internal: true` takes a procedure off the wire, so there is no such caller — only the app's own code calling its own procedure. Measured on one codebase: after every wire procedure was bounded, 212 findings remained and ALL 212 were internal. So the rule spent its entire remaining output on the case its premise excludes, which is how a rule stops being read. `imperative-scope` has skipped `internal: true` for the same reason; this applies the existing decision rather than making a new one. A procedure on the wire is reported exactly as before, which is the half that keeps the exemption from being a hole.
245
+
246
+ **`raw-fetch` recommends `yield* HttpClient`.** Several provider factories take a `fetch`-shaped function as an option (`createGoogleGenerativeAI({ fetch })`). Inside it the URL is the SDK's, never a caller's, so the SSRF half has nothing to bite on — and the remedy is not available either: the seam requires a plain callback, so `yield* HttpClient` names an API that cannot be used at that call site. A rule whose only remedy does not apply teaches the reader to skim the line, which is the one place a genuinely caller-supplied URL would have been named.
247
+
248
+ The seam is SUBTRACTED, not exempted: only calls inside the function that is the value of a `fetch:` property are discounted, so a file that supplies such a seam AND calls `fetch` on its own account is still reported for the second one. Without that, adding one option object would switch the rule off for a file.
249
+
250
+ ### Internal (no consumer-facing effect)
251
+
252
+ - **@voltro/database** — The migration lock is released by its bracket and nowhere else.
253
+
254
+ Correcting unreleased work in this same release, so no shipped behaviour changes: `applyPlan` gained an `Effect.acquireUseRelease` bracket and KEPT the `try { … } finally { yield* held.release }` it replaced. The overlap was documented as free — "releasing twice is harmless: the second `RELEASE_LOCK` finds nothing held on that connection".
255
+
256
+ That holds for `RELEASE_LOCK` and for `pg_advisory_unlock`. It is false for the third dialect: `sp_releaseapplock` RAISES on a lock the session does not hold, so every mssql apply died on the second release with `Cannot release the application lock … because it is not currently held`. A claim about three dialects, verified on two.
257
+
258
+ Removing it is also the better release POINT — the `finally` ran before the convergence proof, handing the lock back while the work it protects was still running. The bracket holds it to the end and covers failure, defect and interruption, which a `finally` inside `Effect.gen` does not.
259
+
260
+ `migrationLockIsBracketed.test.ts` asserted that an acquisition IS bracketed; it now also asserts there is exactly ONE release. Those are different claims and only the first was being made, which is how this rode through.
261
+
262
+ ---
263
+
42
264
  ## [0.68.0] — 2026-09-09
43
265
 
44
266
  ### ⚠ BREAKING
package/dist/index.d.ts CHANGED
@@ -2345,6 +2345,28 @@ export declare interface DataStore {
2345
2345
  * - memory store: linear scan + match-by-conflict-columns
2346
2346
  */
2347
2347
  upsert(table: string, row: Row, options: UpsertOptions): Promise<Row>;
2348
+ /**
2349
+ * `upsert`, plus the one thing it cannot report: whether the row was INSERTED
2350
+ * or UPDATED.
2351
+ *
2352
+ * An app that needs to know does the read itself today — look for the row,
2353
+ * then upsert — which costs a round trip AND opens a window in which the
2354
+ * answer stops being true. It does not have to: the framework's store wrapper
2355
+ * mints the id before the statement, and on the conflict path every dialect
2356
+ * hands back the EXISTING row's id. Two ids that differ ARE the answer, for
2357
+ * free.
2358
+ *
2359
+ * Optional, and provided by the framework's wrapper rather than by a dialect
2360
+ * store — the outcome is decided from what the wrapper already knows, so no
2361
+ * dialect implements anything. It is present on `ctx.store` in a handler; a
2362
+ * hand-written `DataStore` need not have it. The typed helper is
2363
+ * `upsertRowOutcome`.
2364
+ *
2365
+ * `'inserted'` is exact, never a default for "could not tell": where the free
2366
+ * comparison cannot answer (the caller supplied the id, or the table's id is
2367
+ * database-assigned) the destination is read first.
2368
+ */
2369
+ upsertWithOutcome?(table: string, row: Row, options: UpsertOptions): Promise<UpsertResult>;
2348
2370
  /**
2349
2371
  * Insert-or-ignore: INSERT a row, but if it conflicts on
2350
2372
  * `conflictColumns`, leave the existing row UNCHANGED and skip the
@@ -4049,6 +4071,29 @@ export declare type InferViewRow<V> = V extends View<string, infer F> ? InferRow
4049
4071
  */
4050
4072
  export declare const INITIAL_VERSION = 1;
4051
4073
 
4074
+ /**
4075
+ * Insert MANY rows, typed against the table — `insertRow`'s batch, and nothing
4076
+ * else: it forwards to `store.insertMany` unchanged, so the chunking, the
4077
+ * dialect behaviour and the conflict semantics are exactly what the untyped
4078
+ * call already does. What it adds is the one thing the string-keyed form cannot
4079
+ * have: every element is checked against `InferInsertRow<T>`, so a row that
4080
+ * omits a required column, misspells one, or passes a `number` where the column
4081
+ * is a timestamp is a compile error at the call site instead of a dialect error
4082
+ * somewhere inside the batch.
4083
+ *
4084
+ * That last shape is why the batch form needs its own helper rather than a loop
4085
+ * over `insertRow`: a batch is usually MAPPED from something (a parsed import,
4086
+ * an API page), and a mapping is exactly where a `Date`-vs-`number` mismatch
4087
+ * survives review — the loop body's inferred element type is only as good as
4088
+ * whatever produced the array.
4089
+ *
4090
+ * await insertManyRows(ctx.store, roadmapEpicStats, rows)
4091
+ *
4092
+ * An empty array is a legal no-op, so a caller does not have to guard the call
4093
+ * — `store.insertMany` decides what that means, and it means "insert nothing".
4094
+ */
4095
+ export declare const insertManyRows: <T extends AnyTable>(store: TypedInsertManyStore, table: T, rows: ReadonlyArray<InferInsertRow<T>>) => Promise<ReadonlyArray<InferRow<T>>>;
4096
+
4052
4097
  /**
4053
4098
  * Insert a row, typed against the table: the payload must carry every required
4054
4099
  * column (NOT NULL, no default, not auto-filled) or it is a compile error.
@@ -8122,6 +8167,11 @@ export declare interface TxnRecorderPort {
8122
8167
  readonly maxOf: TxnMaxOf;
8123
8168
  }
8124
8169
 
8170
+ /** The minimal store surface `insertManyRows` needs. */
8171
+ export declare interface TypedInsertManyStore {
8172
+ readonly insertMany: (table: string, rows: ReadonlyArray<Row>) => Promise<ReadonlyArray<Row>>;
8173
+ }
8174
+
8125
8175
  /** The minimal store surface `insertRow` needs — satisfied by `ctx.store`. */
8126
8176
  export declare interface TypedInsertStore {
8127
8177
  readonly insert: (table: string, row: Row) => Promise<Row>;
@@ -8134,6 +8184,14 @@ export declare interface TypedUpdateManyStore {
8134
8184
  }) => Promise<number>;
8135
8185
  }
8136
8186
 
8187
+ /** The minimal store surface `upsertRowOutcome` needs. */
8188
+ export declare interface TypedUpsertOutcomeStore {
8189
+ readonly upsertWithOutcome?: (table: string, row: Row, options: UpsertOptions) => Promise<{
8190
+ readonly row: Row;
8191
+ readonly outcome: 'inserted' | 'updated';
8192
+ }>;
8193
+ }
8194
+
8137
8195
  /** The minimal store surface `upsertRow` needs. */
8138
8196
  export declare interface TypedUpsertStore {
8139
8197
  readonly upsert: (table: string, row: Row, options: UpsertOptions) => Promise<Row>;
@@ -8243,6 +8301,13 @@ export declare interface UpsertOptions {
8243
8301
  readonly updateValues?: Readonly<Record<string, unknown>>;
8244
8302
  }
8245
8303
 
8304
+ /** What an `upsertWithOutcome` did, beside the row it wrote. */
8305
+ export declare interface UpsertResult {
8306
+ readonly row: Row;
8307
+ /** `'updated'` means the statement landed on a row that already existed. */
8308
+ readonly outcome: 'inserted' | 'updated';
8309
+ }
8310
+
8246
8311
  /**
8247
8312
  * Upsert a row, typed the same way. `conflictColumns` is constrained to the
8248
8313
  * table's own column names, so a typo'd conflict key is a compile error too.
@@ -8253,6 +8318,34 @@ export declare const upsertRow: <T extends AnyTable>(store: TypedUpsertStore, ta
8253
8318
  readonly updateValues?: Readonly<Partial<Omit<InferRow<T>, "id">>>;
8254
8319
  }) => Promise<InferRow<T>>;
8255
8320
 
8321
+ /**
8322
+ * `upsertRow`, plus whether the row was INSERTED or UPDATED.
8323
+ *
8324
+ * The reason this exists is one an app measured: to report "created" it had to
8325
+ * run a second query before every upsert — a round trip per row, and a window
8326
+ * in which another writer makes the answer wrong. The store already knows: it
8327
+ * mints the id before the statement, and on the conflict path every dialect
8328
+ * returns the EXISTING row's id.
8329
+ *
8330
+ * const { row, outcome } = await upsertRowOutcome(ctx.store, players, player, {
8331
+ * conflictColumns: ['tenantId', 'externalId'],
8332
+ * update: ['name', 'score'],
8333
+ * })
8334
+ * if (outcome === 'inserted') await notifyNewPlayer(row)
8335
+ *
8336
+ * `'inserted'` is never a stand-in for "could not tell". Where the id cannot
8337
+ * decide it — the caller supplied one, or the table's id is database-assigned —
8338
+ * the store reads the destination first and answers exactly.
8339
+ */
8340
+ export declare const upsertRowOutcome: <T extends AnyTable>(store: TypedUpsertOutcomeStore, table: T, row: InferInsertRow<T>, options: {
8341
+ readonly conflictColumns: ReadonlyArray<keyof InferRow<T> & string>;
8342
+ readonly update?: ReadonlyArray<keyof InferRow<T> & string> | ((existing: InferRow<T>) => Readonly<Partial<InferRow<T>>>);
8343
+ readonly updateValues?: Readonly<Partial<Omit<InferRow<T>, "id">>>;
8344
+ }) => Promise<{
8345
+ readonly row: InferRow<T>;
8346
+ readonly outcome: "inserted" | "updated";
8347
+ }>;
8348
+
8256
8349
  /** Conflict patch for stores/callback paths that already have the existing row. */
8257
8350
  export declare const upsertUpdatePatch: (row: Row, existing: Row, options: UpsertOptions) => Row;
8258
8351
 
package/dist/index.js CHANGED
@@ -2670,29 +2670,36 @@ ${e.all.map((e) => {
2670
2670
  reason: "null"
2671
2671
  }));
2672
2672
  return a;
2673
- }, ml = (e, t, n) => e.insert(t.tableName, n), hl = (e, t, n, r) => e.upsert(t.tableName, n, {
2673
+ }, ml = (e, t, n) => e.insert(t.tableName, n), hl = (e, t, n) => e.insertMany(t.tableName, n), gl = (e, t, n, r) => e.upsert(t.tableName, n, {
2674
2674
  conflictColumns: r.conflictColumns,
2675
2675
  ...r.update === void 0 ? {} : { update: r.update },
2676
2676
  ...r.updateValues === void 0 ? {} : { updateValues: r.updateValues }
2677
- }), gl = (e, t, n, r) => e.updateMany(t.tableName, n, { where: r.where }), _l = 512, vl = (e) => {
2677
+ }), _l = async (e, t, n, r) => {
2678
+ if (e.upsertWithOutcome === void 0) throw Error("upsertRowOutcome: this store does not implement `upsertWithOutcome`. It is provided by the framework's store wrapper, so `ctx.store` has it; a hand-written DataStore must add it.");
2679
+ return e.upsertWithOutcome(t.tableName, n, {
2680
+ conflictColumns: r.conflictColumns,
2681
+ ...r.update === void 0 ? {} : { update: r.update },
2682
+ ...r.updateValues === void 0 ? {} : { updateValues: r.updateValues }
2683
+ });
2684
+ }, vl = (e, t, n, r) => e.updateMany(t.tableName, n, { where: r.where }), yl = 512, bl = (e) => {
2678
2685
  e.setMaxListeners(512);
2679
- }, yl = (e, t, n) => ({
2686
+ }, xl = (e, t, n) => ({
2680
2687
  _tag: "PrimaryKeyConflictError",
2681
2688
  table: e,
2682
2689
  key: t,
2683
2690
  cause: n
2684
- }), bl = (e) => typeof e == "object" && !!e && e._tag === "PrimaryKeyConflictError", xl = {
2691
+ }), Sl = (e) => typeof e == "object" && !!e && e._tag === "PrimaryKeyConflictError", Cl = {
2685
2692
  postgres: 5432,
2686
2693
  mysql: 3306,
2687
2694
  mariadb: 3306,
2688
2695
  mssql: 1433
2689
- }, Sl = /* @__PURE__ */ new Set([
2696
+ }, wl = /* @__PURE__ */ new Set([
2690
2697
  "localhost",
2691
2698
  "127.0.0.1",
2692
2699
  "::1",
2693
2700
  "[::1]",
2694
2701
  "0.0.0.0"
2695
- ]), Cl = (e) => {
2702
+ ]), Tl = (e) => {
2696
2703
  let t = e.host, n = e.port, r = e.database;
2697
2704
  if (e.url !== void 0 && e.url !== "") try {
2698
2705
  let i = new URL(e.url);
@@ -2701,29 +2708,29 @@ ${e.all.map((e) => {
2701
2708
  return;
2702
2709
  }
2703
2710
  if (t === void 0 || t === "" || r === void 0 || r === "") return;
2704
- let i = n ?? xl[(e.dialect ?? "").toLowerCase()];
2711
+ let i = n ?? Cl[(e.dialect ?? "").toLowerCase()];
2705
2712
  if (i === void 0 || !Number.isFinite(i)) return;
2706
2713
  let a = t.toLowerCase();
2707
2714
  return {
2708
- host: Sl.has(a) ? "localhost" : a,
2715
+ host: wl.has(a) ? "localhost" : a,
2709
2716
  port: i,
2710
2717
  database: r.toLowerCase()
2711
2718
  };
2712
- }, wl = (e) => {
2713
- let t = Cl(e);
2719
+ }, El = (e) => {
2720
+ let t = Tl(e);
2714
2721
  if (t !== void 0) return qt("sha256").update(`${t.host}\u0000${t.port}\u0000${t.database}`).digest("hex").slice(0, 32);
2715
- }, Tl = (e, t) => e === void 0 || t === void 0 ? "unknown" : e === t ? "same" : "different", El = {
2722
+ }, Dl = (e, t) => e === void 0 || t === void 0 ? "unknown" : e === t ? "same" : "different", Ol = {
2716
2723
  app: 3,
2717
2724
  plugin: 2,
2718
2725
  framework: 1
2719
- }, Dl = /* @__PURE__ */ new Map(), Ol = [], kl = () => [...Ol], Al = (e) => {
2726
+ }, kl = /* @__PURE__ */ new Map(), Al = [], jl = () => [...Al], Ml = (e) => {
2720
2727
  if (!e.table || !e.timeColumn) throw Error("registerRetention: table + timeColumn are required");
2721
2728
  if (!Number.isFinite(e.ttlMs) || e.ttlMs <= 0) throw Error(`registerRetention(${e.table}): ttlMs must be a positive number`);
2722
- let t = e.source ?? "app", n = Dl.get(e.table);
2729
+ let t = e.source ?? "app", n = kl.get(e.table);
2723
2730
  if (n !== void 0) {
2724
2731
  let r = n.source ?? "app", i = n.ttlMs !== e.ttlMs || n.where !== void 0 != (e.where !== void 0);
2725
- if (El[t] < El[r]) {
2726
- i && Ol.push({
2732
+ if (Ol[t] < Ol[r]) {
2733
+ i && Al.push({
2727
2734
  table: e.table,
2728
2735
  kept: {
2729
2736
  source: r,
@@ -2736,7 +2743,7 @@ ${e.all.map((e) => {
2736
2743
  });
2737
2744
  return;
2738
2745
  }
2739
- i && r !== t && Ol.push({
2746
+ i && r !== t && Al.push({
2740
2747
  table: e.table,
2741
2748
  kept: {
2742
2749
  source: t,
@@ -2748,13 +2755,13 @@ ${e.all.map((e) => {
2748
2755
  }
2749
2756
  });
2750
2757
  }
2751
- Dl.set(e.table, {
2758
+ kl.set(e.table, {
2752
2759
  ...e,
2753
2760
  source: t
2754
2761
  });
2755
- }, jl = () => [...Dl.values()], Ml = () => {
2756
- Dl.clear(), Ol.length = 0;
2757
- }, Nl = async (e, t, n, r = 2e4) => {
2762
+ }, Nl = () => [...kl.values()], Pl = () => {
2763
+ kl.clear(), Al.length = 0;
2764
+ }, Fl = async (e, t, n, r = 2e4) => {
2758
2765
  let i = new Date(t - e.ttlMs), a = 0;
2759
2766
  try {
2760
2767
  for (;;) {
@@ -2762,26 +2769,26 @@ ${e.all.map((e) => {
2762
2769
  if (a += t, t < r) break;
2763
2770
  }
2764
2771
  } catch (t) {
2765
- throw k.runSync(A.increment(A.tagged(Fl, "table", e.table))), t;
2772
+ throw k.runSync(A.increment(A.tagged(Ll, "table", e.table))), t;
2766
2773
  }
2767
- return a > 0 && k.runSync(A.incrementBy(A.tagged(Pl, "table", e.table), a)), a;
2768
- }, Pl = A.counter("voltro_retention_swept_rows_total"), Fl = A.counter("voltro_retention_sweep_errors_total"), Il = (e, t) => {
2774
+ return a > 0 && k.runSync(A.incrementBy(A.tagged(Il, "table", e.table), a)), a;
2775
+ }, Il = A.counter("voltro_retention_swept_rows_total"), Ll = A.counter("voltro_retention_sweep_errors_total"), Rl = (e, t) => {
2769
2776
  let n = Number(e ?? String(t));
2770
2777
  return (Number.isFinite(n) && n > 0 ? n : t) * 36e5;
2771
- }, Ll = "_voltro_cdc_offsets", Rl = T(Ll, {
2778
+ }, zl = "_voltro_cdc_offsets", Bl = T(zl, {
2772
2779
  id: D({ prefix: "cdcoff" }),
2773
2780
  replicaId: w(),
2774
2781
  streamName: w(),
2775
2782
  binlogFile: w(),
2776
2783
  binlogPosition: w(),
2777
2784
  updatedAt: E()
2778
- }).index(["replicaId"]), zl = T(Ll, {
2785
+ }).index(["replicaId"]), Vl = T(zl, {
2779
2786
  id: D({ prefix: "cdcoff" }),
2780
2787
  replicaId: w(),
2781
2788
  streamName: w(),
2782
2789
  ctVersion: w(),
2783
2790
  updatedAt: E()
2784
- }).index(["replicaId"]), Bl = "_voltro_data_transfers", Vl = T(Bl, {
2791
+ }).index(["replicaId"]), Hl = "_voltro_data_transfers", Ul = T(Hl, {
2785
2792
  id: D({ prefix: "dimp" }),
2786
2793
  startedAt: E().default("now"),
2787
2794
  finishedAt: E().nullable(),
@@ -2794,13 +2801,13 @@ ${e.all.map((e) => {
2794
2801
  rowsWritten: y().default(0),
2795
2802
  failure: w().nullable(),
2796
2803
  staged: p().nullable()
2797
- }).renamedFrom("_voltro_data_imports").nonReactive(), Hl = (e) => {
2804
+ }).renamedFrom("_voltro_data_imports").nonReactive(), Wl = (e) => {
2798
2805
  let t = Number.isFinite(e.startedAt.getTime()) ? e.startedAt.toISOString() : "an unknown time", n = e.direction === "import" ? e.mode : `${e.direction} ${e.mode}`, r = e.failure === null ? e.finishedAt === null ? `${n} — started and never reported finishing` : e.direction === "backup" || e.direction === "restore" ? `${n} — finished${e.rowsWritten > 0 ? ` (${e.rowsWritten} blob(s))` : ""}` : `${n} — ${e.rowsWritten} row(s) across ${e.tablesWritten} table(s)` : `FAILED ${n} — ${e.failure}`, i = e.staged === null ? "" : e.staged ? " · staged (target untouched until the swap)" : " · NOT staged (target emptied first)";
2799
2806
  return `${t} · ${r} · via ${e.via}${i}` + (e.bundle === null ? "" : ` · from ${e.bundle}`) + (e.sourceFingerprint === null ? "" : ` · source schema ${e.sourceFingerprint}`);
2800
- }, Ul = 255, Wl = (e) => e == null ? "null" : typeof e == "string" || typeof e == "number" || typeof e == "boolean" ? JSON.stringify(e) : Array.isArray(e) ? `[${e.map(Wl).join(",")}]` : typeof e == "object" ? `{${Object.entries(e).filter(([, e]) => e !== void 0).sort(([e], [t]) => e < t ? -1 : +(e > t)).map(([e, t]) => `${JSON.stringify(e)}:${Wl(t)}`).join(",")}}` : JSON.stringify(String(e)), Gl = (e) => {
2807
+ }, Gl = 255, Kl = (e) => e == null ? "null" : typeof e == "string" || typeof e == "number" || typeof e == "boolean" ? JSON.stringify(e) : Array.isArray(e) ? `[${e.map(Kl).join(",")}]` : typeof e == "object" ? `{${Object.entries(e).filter(([, e]) => e !== void 0).sort(([e], [t]) => e < t ? -1 : +(e > t)).map(([e, t]) => `${JSON.stringify(e)}:${Kl(t)}`).join(",")}}` : JSON.stringify(String(e)), ql = (e) => {
2801
2808
  if (e == null) return null;
2802
- let t = typeof e == "string" ? e : Wl(e);
2809
+ let t = typeof e == "string" ? e : Kl(e);
2803
2810
  return t.length > 255 ? t.slice(0, 255) : t;
2804
- }, Kl = "postgres";
2811
+ }, Jl = "postgres";
2805
2812
  //#endregion
2806
- export { ct as AUTO_FILLED_COLUMNS, an as BOUND_ENCRYPTED_PREFIX, cl as BULK_INSERT_LIMITS, Y as BranchNameInvalid, tc as BranchTransitionInvalid, Ll as CDC_OFFSETS_TABLE, _o as CHANGE_CLAIM_TTL_ENV, _l as CHANGE_LISTENER_CEILING, Ze as ColumnBuilder, Bl as DATA_TRANSFERS_TABLE, ot as DEFAULT_ACQUIRE_TIMEOUT_MS, vo as DEFAULT_CHANGE_CLAIM_TTL_HOURS, ys as DEFAULT_EAGER_FALLBACK_WARN_INTERVAL_MS, vt as DEFAULT_SOFT_DROP_RETENTION_DAYS, pr as DEFAULT_SUBJECT_GRAPH_DEPTH, rn as ENCRYPTED_PREFIX, Kl as ENGINE, Br as EagerCardinalityError, xt as FILE_MIGRATION_PATTERN, it as FRAMEWORK_LIVE_TABLES, cn as FieldDecryptionError, Vo as ID_COLUMN_MAX_LENGTH, It as INITIAL_VERSION, Ha as MATCHES_NO_ROWS, wt as MAX_IDENTIFIER_LENGTH, As as MSSQL_TRANSIENT_NUMBERS, Os as MYSQL_TRANSIENT_CODES, go as OccurrenceCounter, ks as POSTGRES_TRANSIENT_STATES, Zc as REPLACE_IN_PROGRESS_TABLE, Ul as SCOPE_KEY_MAX_LENGTH, ae as SELF_REFERENCE_TARGET, js as SQLITE_TRANSIENT_CODES, $c as STAGING_STALE_MINUTES, ft as STAGING_TABLE_PREFIX, Xt as SqlClient, Ms as TURSO_TRANSIENT_MESSAGE, Us as TableStreamError, ei as TenantNamespaceInvalid, $r as TenantNamespaceUnresolved, bc as TenantRegionUnavailable, yc as TenantResidencyUnresolved, Ls as VOLTRO_EXPIRES_MIXIN_ID, zs as VOLTRO_LOCAL_FIRST_MIXIN_ID, Nt as VersionConflict, kt as WARN_IDENTIFIER_LENGTH, Jc as _voltroApiKeysTable, Rl as _voltroCdcOffsetsTable, Vl as _voltroDataTransfersTable, Kc as _voltroIdempotencyTable, qc as _voltroKvTable, Gc as _voltroMigrationPlansTable, Uc as _voltroMigrationsTable, zl as _voltroMssqlCdcOffsetsTable, Qc as _voltroReplaceInProgressTable, Yc as _voltroReplicaObservationsTable, Wc as _voltroSeedsTable, Oa as actorsTable, mc as admitBranch, qe as agedSoftDropSnapshots, ye as allEnumRenames, ur as allRegisteredRelations, te as allRegisteredTables, Ka as and, Rr as applyJsonPatch, se as array, za as arrayContains, Va as arrayHas, Ba as arrayOverlaps, Hr as attachEagerLoads, Co as attributionFields, Do as attributionKey, Pc as auditAllTableIndexes, Nc as auditTableIndexes, Tt as avg, ve as avgOver, Io as beginLocalWrite, de as bigint, Oc as bindResidentStore, p as boolean, Qs as branchNamespaceName, ll as bulkInsertLimitsFor, ee as bytes, xn as causeBranches, lo as changeDelete, po as changeDigest, so as changeInsert, uo as changeSoftDelete, co as changeUpdate, dl as chunkRowsForInsert, Ao as claimPendingAttribution, jn as classifyConstraintViolation, d as clearEnumRenames, zo as clearPendingAttribution, fr as clearRelationsRegistry, wc as clearResidencyConfig, Ml as clearRetentions, De as clearTableRegistry, Jo as clearWriteRecorders, W as coercePredicateForSqlite, ci as coercePredicateValue, li as coercePredicateValues, ba as coerceValueForSqlite, io as collectSubqueries, u as column, Bn as columnSchema, ul as columnsPerRow, Ci as compileEagerJson, zr as compileJsonPatch, L as compilePredicate, ui as compileRawFragment, Si as compileSelect, mo as composeChangeKey, Js as computeEmbedding, wl as connectionIdentity, La as contains, c as count, ce as countDistinct, ie as crdtDoc, he as crdtManagedColumns, jt as crdtManagedColumnsOf, a as crdtText, So as currentWriteAttribution, Qe as databaseHandle, Ce as date, _ as dbEnum, ps as dbMetricTagCacheSizeForTests, gs as dbOperationsInFlight, xe as decimal, l as declareEnumRename, pn as decodeFieldValue, Dr as decodeRowsFromSchema, yn as decryptFieldsOnRead, Vc as defineMigration, St as defineMixin, lt as defineSeed, n as denseRank, br as deriveSubjectGraph, ut as deriveTypeIdPrefix, Wo as derivedRowId, Go as derivedRowIdLength, Me as describeAgedSoftDropSnapshots, Pn as describeDriverError, Fn as describeRowBindings, Hl as describeTransferRun, nn as deserializeArraysOnRead, Fe as drainIdentifierWarnings, Te as dropped, dn as encodeFieldValue, kr as encodeRowForSchema, vn as encryptFieldsForWrite, gn as encryptedColumnsOf, Lo as endLocalWrite, dc as enforceBranchLimits, Ee as ensureTableRegistered, ka as eq, di as escapeLike, ro as evaluatePredicate, Xe as except, $a as exists, Rs as expires, oo as externalChangeEvent, wn as extractDbCause, on as fieldBinding, nt as fireSchemaChangeSeeds, Ge as fireTenantCreateSeeds, zc as formatIndexAuditIssue, xr as formatSubjectGraph, Xc as frameworkTables, rt as generateId, We as generateSnowflake, h as geography, re as geometry, le as getEnumRenames, lr as getRelation, cr as getRelations, Cc as getResidencyConfig, C as getTable, ja as gt, Ma as gte, Qr as hasEagerLoads, qs as hybridSearch, D as id, Fa as inSet, Za as inSubquery, $n as inferForeignKey, ml as insertRow, y as integer, nl as interruptedReplaceRefusal, x as intersect, Oe as interval, sn as isBoundCiphertext, $s as isBranchNamespace, Pr as isCompositeRowKey, Jn as isEmptyRelationsSpec, un as isEncrypted, ln as isFieldDecryptionError, Le as isFileMigration, Mn as isForeignKeyViolation, st as isFrameworkOwnedLiveTable, Vs as isLocalFirst, Hc as isMigrationDefinition, mt as isMysqlFamily, ac as isNeonConnection, Wa as isNotNull, Ua as isNull, bl as isPrimaryKeyConflictError, Nn as isQueryTimeout, mn as isRawEncoding, Ec as isRegionServable, Yn as isRelationsSpec, Ue as isSeedDefinition, ht as isSoftDropSnapshotName, Ye as isSqliteFamily, Ie as isStagingTableName, fe as isTableReactive, Fs as isTransientContention, Pe as isView, S as json, Ya as jsonField, _t as jsonIndex, Fr as jsonPatchAssignments, oe as lag, ue as lead, kl as listRetentionConflicts, jl as listRetentions, Bs as localFirst, Hs as localFirstTables, Na as lt, Pa as lte, Ss as makeEagerFallbackReporter, vc as makeNamespaceBranchExecutor, Zn as many, Qn as manyToMany, ke as materializeFields, f as max, Ve as migration, v as min, pl as missingConflictColumns, fl as missingRequiredColumns, Re as mixin, Aa as neq, nc as nextBranchState, Cl as normaliseTarget, Lr as normalizeJsonPatchValue, Ja as not, eo as notExists, Ia as notInSet, Qa as notInSubquery, pe as numeric, vs as observeDbOp, yo as occurrenceWindowMs, Xn as one, Vr as oneCardinalityMessage, qa as or, b as orderClausesOf, me as paginateBy, At as paginateById, ho as parseChangeKey, Bo as pendingAttributionCount, lc as planBranch, rc as planBranchProvision, ic as planBranchTeardown, uc as planBranchTeardownFor, sc as planNeonBranchProvision, cc as planNeonBranchTeardown, Mt as planVersionedUpdate, $e as pluginRef, r as pluginRefSpecOf, yl as primaryKeyConflictError, fc as provisionBranch, kc as provisionResidentTenant, ai as qualifyTable, i as queryFor, Dt as queryForView, Ne as quoteIdent, vl as raiseChangeListenerCeiling, Se as rank, t as raw, ze as readableByColumns, yt as real, _s as recordDbOp, $o as recordsTable, at as reference, Rn as refuseWrite, wa as registerCoreTables, sr as registerDiscoveredRelations, ko as registerPendingAttribution, or as registerRelations, Al as registerRetention, e as registerTable, Ko as registerWriteRecorder, qn as relations, Ea as requireActors, Be as requireTable, Da as requireTenants, fs as resetDbMetricTagCacheForTests, je as resetSnowflake, Dc as residentPlacement, Qt as resolveActorSnapshot, oc as resolveBranchMechanism, xs as resolveEagerFallbackWarnIntervalMs, Ro as resolveEchoAttribution, g as resolveFullTextIndex, Ke as resolveIdScheme, He as resolveMixinGraph, Tc as resolveTenantHome, ii as resolveTenantNamespace, Il as retentionTtlMsFromEnv, Mr as rowKeyColumns, Nr as rowKeyIdentity, be as rowNumber, Vn as rowSchema, Es as runRetryingTransaction, Ds as runStoreTransaction, xo as runWithWriteAttribution, es as runWriteRecorders, Tl as sameDatabase, ri as sanitizeIdentifierFragment, Gl as scopeKeyOf, pt as selfReference, tn as serializeArraysForWrite, Je as serverOnlyColumns, Sc as setResidencyConfig, Ae as setSchemaChangeSeedHook, Ct as setTenantCreateSeedHook, Cs as settleTransactionExit, Ft as settleVersionedUpdate, et as softDropSnapshotStamp, Ga as spatialPredicate, il as stagingLeftoverReport, rl as stagingLeftovers, tl as stagingNamesOf, el as stagingStaleAfterMs, ol as stampGeneratedId, sl as stampGeneratedIds, Ra as startsWith, zn as storeRefusalOf, Ks as streamTable, we as sum, m as sumOver, Xo as suspendWriteRecorders, Zo as suspendWriteRecordersEffect, Nl as sweepRetention, T as table, pc as teardownBranch, w as text, E as timestamp, Rt as timestampMs, Pt as timestampMsOrNull, ne as union, dt as unionAll, bt as unregisterTable, qo as unregisterWriteRecorder, gl as updateManyRow, hl as upsertRow, jr as upsertUpdatePatch, Ar as upsertUpdateValues, Ot as validateColumnName, _e as validateIndexName, dr as validateRegisteredRelations, rr as validateRelationConfig, s as validateTableName, Et as validateTypeIdPrefix, gt as vector, tt as vectorDistanceToOpclass, Ys as vectorEmbedding, Lt as versionColumnOf, ge as view, o as viewSql, ao as walkLeaves, Tn as wireSafeCause, wo as withCapturedAttribution, Qo as writeRecordersSuspended };
2813
+ export { ct as AUTO_FILLED_COLUMNS, an as BOUND_ENCRYPTED_PREFIX, cl as BULK_INSERT_LIMITS, Y as BranchNameInvalid, tc as BranchTransitionInvalid, zl as CDC_OFFSETS_TABLE, _o as CHANGE_CLAIM_TTL_ENV, yl as CHANGE_LISTENER_CEILING, Ze as ColumnBuilder, Hl as DATA_TRANSFERS_TABLE, ot as DEFAULT_ACQUIRE_TIMEOUT_MS, vo as DEFAULT_CHANGE_CLAIM_TTL_HOURS, ys as DEFAULT_EAGER_FALLBACK_WARN_INTERVAL_MS, vt as DEFAULT_SOFT_DROP_RETENTION_DAYS, pr as DEFAULT_SUBJECT_GRAPH_DEPTH, rn as ENCRYPTED_PREFIX, Jl as ENGINE, Br as EagerCardinalityError, xt as FILE_MIGRATION_PATTERN, it as FRAMEWORK_LIVE_TABLES, cn as FieldDecryptionError, Vo as ID_COLUMN_MAX_LENGTH, It as INITIAL_VERSION, Ha as MATCHES_NO_ROWS, wt as MAX_IDENTIFIER_LENGTH, As as MSSQL_TRANSIENT_NUMBERS, Os as MYSQL_TRANSIENT_CODES, go as OccurrenceCounter, ks as POSTGRES_TRANSIENT_STATES, Zc as REPLACE_IN_PROGRESS_TABLE, Gl as SCOPE_KEY_MAX_LENGTH, ae as SELF_REFERENCE_TARGET, js as SQLITE_TRANSIENT_CODES, $c as STAGING_STALE_MINUTES, ft as STAGING_TABLE_PREFIX, Xt as SqlClient, Ms as TURSO_TRANSIENT_MESSAGE, Us as TableStreamError, ei as TenantNamespaceInvalid, $r as TenantNamespaceUnresolved, bc as TenantRegionUnavailable, yc as TenantResidencyUnresolved, Ls as VOLTRO_EXPIRES_MIXIN_ID, zs as VOLTRO_LOCAL_FIRST_MIXIN_ID, Nt as VersionConflict, kt as WARN_IDENTIFIER_LENGTH, Jc as _voltroApiKeysTable, Bl as _voltroCdcOffsetsTable, Ul as _voltroDataTransfersTable, Kc as _voltroIdempotencyTable, qc as _voltroKvTable, Gc as _voltroMigrationPlansTable, Uc as _voltroMigrationsTable, Vl as _voltroMssqlCdcOffsetsTable, Qc as _voltroReplaceInProgressTable, Yc as _voltroReplicaObservationsTable, Wc as _voltroSeedsTable, Oa as actorsTable, mc as admitBranch, qe as agedSoftDropSnapshots, ye as allEnumRenames, ur as allRegisteredRelations, te as allRegisteredTables, Ka as and, Rr as applyJsonPatch, se as array, za as arrayContains, Va as arrayHas, Ba as arrayOverlaps, Hr as attachEagerLoads, Co as attributionFields, Do as attributionKey, Pc as auditAllTableIndexes, Nc as auditTableIndexes, Tt as avg, ve as avgOver, Io as beginLocalWrite, de as bigint, Oc as bindResidentStore, p as boolean, Qs as branchNamespaceName, ll as bulkInsertLimitsFor, ee as bytes, xn as causeBranches, lo as changeDelete, po as changeDigest, so as changeInsert, uo as changeSoftDelete, co as changeUpdate, dl as chunkRowsForInsert, Ao as claimPendingAttribution, jn as classifyConstraintViolation, d as clearEnumRenames, zo as clearPendingAttribution, fr as clearRelationsRegistry, wc as clearResidencyConfig, Pl as clearRetentions, De as clearTableRegistry, Jo as clearWriteRecorders, W as coercePredicateForSqlite, ci as coercePredicateValue, li as coercePredicateValues, ba as coerceValueForSqlite, io as collectSubqueries, u as column, Bn as columnSchema, ul as columnsPerRow, Ci as compileEagerJson, zr as compileJsonPatch, L as compilePredicate, ui as compileRawFragment, Si as compileSelect, mo as composeChangeKey, Js as computeEmbedding, El as connectionIdentity, La as contains, c as count, ce as countDistinct, ie as crdtDoc, he as crdtManagedColumns, jt as crdtManagedColumnsOf, a as crdtText, So as currentWriteAttribution, Qe as databaseHandle, Ce as date, _ as dbEnum, ps as dbMetricTagCacheSizeForTests, gs as dbOperationsInFlight, xe as decimal, l as declareEnumRename, pn as decodeFieldValue, Dr as decodeRowsFromSchema, yn as decryptFieldsOnRead, Vc as defineMigration, St as defineMixin, lt as defineSeed, n as denseRank, br as deriveSubjectGraph, ut as deriveTypeIdPrefix, Wo as derivedRowId, Go as derivedRowIdLength, Me as describeAgedSoftDropSnapshots, Pn as describeDriverError, Fn as describeRowBindings, Wl as describeTransferRun, nn as deserializeArraysOnRead, Fe as drainIdentifierWarnings, Te as dropped, dn as encodeFieldValue, kr as encodeRowForSchema, vn as encryptFieldsForWrite, gn as encryptedColumnsOf, Lo as endLocalWrite, dc as enforceBranchLimits, Ee as ensureTableRegistered, ka as eq, di as escapeLike, ro as evaluatePredicate, Xe as except, $a as exists, Rs as expires, oo as externalChangeEvent, wn as extractDbCause, on as fieldBinding, nt as fireSchemaChangeSeeds, Ge as fireTenantCreateSeeds, zc as formatIndexAuditIssue, xr as formatSubjectGraph, Xc as frameworkTables, rt as generateId, We as generateSnowflake, h as geography, re as geometry, le as getEnumRenames, lr as getRelation, cr as getRelations, Cc as getResidencyConfig, C as getTable, ja as gt, Ma as gte, Qr as hasEagerLoads, qs as hybridSearch, D as id, Fa as inSet, Za as inSubquery, $n as inferForeignKey, hl as insertManyRows, ml as insertRow, y as integer, nl as interruptedReplaceRefusal, x as intersect, Oe as interval, sn as isBoundCiphertext, $s as isBranchNamespace, Pr as isCompositeRowKey, Jn as isEmptyRelationsSpec, un as isEncrypted, ln as isFieldDecryptionError, Le as isFileMigration, Mn as isForeignKeyViolation, st as isFrameworkOwnedLiveTable, Vs as isLocalFirst, Hc as isMigrationDefinition, mt as isMysqlFamily, ac as isNeonConnection, Wa as isNotNull, Ua as isNull, Sl as isPrimaryKeyConflictError, Nn as isQueryTimeout, mn as isRawEncoding, Ec as isRegionServable, Yn as isRelationsSpec, Ue as isSeedDefinition, ht as isSoftDropSnapshotName, Ye as isSqliteFamily, Ie as isStagingTableName, fe as isTableReactive, Fs as isTransientContention, Pe as isView, S as json, Ya as jsonField, _t as jsonIndex, Fr as jsonPatchAssignments, oe as lag, ue as lead, jl as listRetentionConflicts, Nl as listRetentions, Bs as localFirst, Hs as localFirstTables, Na as lt, Pa as lte, Ss as makeEagerFallbackReporter, vc as makeNamespaceBranchExecutor, Zn as many, Qn as manyToMany, ke as materializeFields, f as max, Ve as migration, v as min, pl as missingConflictColumns, fl as missingRequiredColumns, Re as mixin, Aa as neq, nc as nextBranchState, Tl as normaliseTarget, Lr as normalizeJsonPatchValue, Ja as not, eo as notExists, Ia as notInSet, Qa as notInSubquery, pe as numeric, vs as observeDbOp, yo as occurrenceWindowMs, Xn as one, Vr as oneCardinalityMessage, qa as or, b as orderClausesOf, me as paginateBy, At as paginateById, ho as parseChangeKey, Bo as pendingAttributionCount, lc as planBranch, rc as planBranchProvision, ic as planBranchTeardown, uc as planBranchTeardownFor, sc as planNeonBranchProvision, cc as planNeonBranchTeardown, Mt as planVersionedUpdate, $e as pluginRef, r as pluginRefSpecOf, xl as primaryKeyConflictError, fc as provisionBranch, kc as provisionResidentTenant, ai as qualifyTable, i as queryFor, Dt as queryForView, Ne as quoteIdent, bl as raiseChangeListenerCeiling, Se as rank, t as raw, ze as readableByColumns, yt as real, _s as recordDbOp, $o as recordsTable, at as reference, Rn as refuseWrite, wa as registerCoreTables, sr as registerDiscoveredRelations, ko as registerPendingAttribution, or as registerRelations, Ml as registerRetention, e as registerTable, Ko as registerWriteRecorder, qn as relations, Ea as requireActors, Be as requireTable, Da as requireTenants, fs as resetDbMetricTagCacheForTests, je as resetSnowflake, Dc as residentPlacement, Qt as resolveActorSnapshot, oc as resolveBranchMechanism, xs as resolveEagerFallbackWarnIntervalMs, Ro as resolveEchoAttribution, g as resolveFullTextIndex, Ke as resolveIdScheme, He as resolveMixinGraph, Tc as resolveTenantHome, ii as resolveTenantNamespace, Rl as retentionTtlMsFromEnv, Mr as rowKeyColumns, Nr as rowKeyIdentity, be as rowNumber, Vn as rowSchema, Es as runRetryingTransaction, Ds as runStoreTransaction, xo as runWithWriteAttribution, es as runWriteRecorders, Dl as sameDatabase, ri as sanitizeIdentifierFragment, ql as scopeKeyOf, pt as selfReference, tn as serializeArraysForWrite, Je as serverOnlyColumns, Sc as setResidencyConfig, Ae as setSchemaChangeSeedHook, Ct as setTenantCreateSeedHook, Cs as settleTransactionExit, Ft as settleVersionedUpdate, et as softDropSnapshotStamp, Ga as spatialPredicate, il as stagingLeftoverReport, rl as stagingLeftovers, tl as stagingNamesOf, el as stagingStaleAfterMs, ol as stampGeneratedId, sl as stampGeneratedIds, Ra as startsWith, zn as storeRefusalOf, Ks as streamTable, we as sum, m as sumOver, Xo as suspendWriteRecorders, Zo as suspendWriteRecordersEffect, Fl as sweepRetention, T as table, pc as teardownBranch, w as text, E as timestamp, Rt as timestampMs, Pt as timestampMsOrNull, ne as union, dt as unionAll, bt as unregisterTable, qo as unregisterWriteRecorder, vl as updateManyRow, gl as upsertRow, _l as upsertRowOutcome, jr as upsertUpdatePatch, Ar as upsertUpdateValues, Ot as validateColumnName, _e as validateIndexName, dr as validateRegisteredRelations, rr as validateRelationConfig, s as validateTableName, Et as validateTypeIdPrefix, gt as vector, tt as vectorDistanceToOpclass, Ys as vectorEmbedding, Lt as versionColumnOf, ge as view, o as viewSql, ao as walkLeaves, Tn as wireSafeCause, wo as withCapturedAttribution, Qo as writeRecordersSuspended };