@voltro/sql-mssql 0.74.0 → 0.75.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 +319 -0
- package/dist/index.js +114 -101
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,325 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.75.0] — 2026-09-19
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **A storage object shared through an `operationId` is held by per-caller claims** — `@voltro/plugin-storage`
|
|
47
|
+
|
|
48
|
+
Two callers completing the same `operationId` share one ref, and a caller that failed later and removed what it put deleted the file the other caller had already kept — while skipping cleanup left a file behind for every job that finally failed. `put({ operationId, claim })` now registers the caller's own claim when it receives the ref, `delete(id, { claim })` releases only that claim, and the object goes with the last one. A delete that names no claim is still the whole object. A claim without an operation is refused (`'claim-without-operation'`), and a claim released on a ref that holds none is a 409 that deletes nothing. Claims live in `_voltro_storage_object_claims`, migrated like the other framework tables.
|
|
49
|
+
|
|
50
|
+
`put` also reports what it did: it returns `StoredRef`, a `StorageRef` plus an `outcome` of `'created'`, `'resumed'` or `'joined'` — a fact about the call, never stored. Migration: code that reads the result compiles unchanged; a hand-written `StorageServiceShape` implementation answers `{ ...ref, outcome: 'created' }`, and a direct reader of `makeStorageObjectMetadata(...).beginDelete` narrows its result with `'operation' in result`.
|
|
51
|
+
|
|
52
|
+
**`voltro update` carries you across this** — codemod `0.75.0/02_storage-put-reports-its-outcome`. 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.75.0).
|
|
53
|
+
- **A workflow declares who may drive a run — `controls:`** — `@voltro/protocol`, `@voltro/cli`
|
|
54
|
+
|
|
55
|
+
A `workflow()` said who may START it (`guards:` / `openAccess:` / `internal: true`) and nothing about who may DRIVE one of its runs. The built-ins behind `useWorkflow(...)` — `cancel`, `signal`, `update`, `resume` — admitted any caller in the run's tenant. So an app that guarded its approval surface
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
defineAction({ name: 'orders.approve', guards: [{ scope: 'orders:approve' }], … })
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
had a second way in that it never wrote: a colleague the mutation refused could send the same `approval` signal through `__voltro.workflow.signal` and release the order. Measured through a real server, with both callers on real sockets.
|
|
62
|
+
|
|
63
|
+
Driving a run is now its own declaration, in the same vocabulary:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export default workflow({
|
|
67
|
+
name: 'orders.fulfil',
|
|
68
|
+
internal: true,
|
|
69
|
+
controls: { guards: [{ scope: 'orders:approve' }] },
|
|
70
|
+
payload: { orderId: Schema.String },
|
|
71
|
+
success: Schema.String,
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`controls: { openAccess: '<why>' }` says anyone who may see the run may drive it. A resource-scoped guard reads its id from the run's start payload, so one `resource:` function serves the start and the controls.
|
|
76
|
+
|
|
77
|
+
**A workflow that declares no `controls:` cannot be driven over the wire** — a system subject and a caller holding `admin:full` aside — and the refusal names the missing declaration. Starting and driving are different authorities: the customer who starts an order must not signal its own approval, so inheriting the start decision would have been wrong in both directions.
|
|
78
|
+
|
|
79
|
+
Unchanged: reading. The run feeds stay scoped by tenant, so a caller may watch a run it may not drive. Server-side control (`ctx.workflows.signal`, a schedule, a parent workflow) is unaffected. Cancelling a start that has not run yet (`cancelQueued`) is still authorised by the receipt id the start returned plus the tenant — there is no run and no payload for a resource guard to read.
|
|
80
|
+
|
|
81
|
+
**`voltro update` carries you across this** — codemod `0.75.0/03_workflow-controls-decision`. 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.75.0).
|
|
82
|
+
- **A usage receipt recorded twice with different values is a typed conflict** — `@voltro/ai`, `@voltro/voltro`
|
|
83
|
+
|
|
84
|
+
`recordAiUsage` answered `replayed` for any second recording of a receipt, even with different values, and returned the values it was handed — so the caller saw a charge that was never booked and no sign of the disagreement. It now fails with `AiUsageReceiptConflict` (the `recorded` and the `attempted` values), and a genuine replay returns the booked row's breakdown. A failed ledger write is `AiUsageLedgerError` in the error channel instead of a defect.
|
|
85
|
+
|
|
86
|
+
For a call that is already paid for, `recordAiUsageBestEffort` never fails and logs both errors with what is needed to reconcile them. The framework's own recording paths use it; they used to discard a failed write without a trace.
|
|
87
|
+
|
|
88
|
+
**`voltro update` carries you across this** — codemod `0.75.0/01_ai-usage-receipt-conflict`. 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.75.0).
|
|
89
|
+
- **Preserve workflow bulk cancellation receipts through the dashboard** — `@voltro/cli`, `@voltro/devtools-ui`
|
|
90
|
+
|
|
91
|
+
Bulk cancellation returns resident control receipts instead of reporting accepted requests as failures. Both dashboard bulk surfaces persist per-run identities before dispatch and restore receipt observation after reload or a lost response. Cancellation reasons are retained; producer sweeps still require confirmed termination.
|
|
92
|
+
|
|
93
|
+
Custom bulk providers must forward request `controls` and response `targets`/`controls`. Standalone `BulkRunActionsPanel` and `SelectionBar` integrations supply `prepareControls` and `observeControl`; `WorkflowsPage` connects its existing journal. Count only confirmed cancellation as success and use the control reader for pending or uncertain outcomes.
|
|
94
|
+
|
|
95
|
+
**`voltro update` carries you across this** — codemod `0.75.0/01_workflow-bulk-control-receipts`. 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.75.0).
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- **A paid media step keeps its usage and its charge, even when it fails after** — `@voltro/plugin-ai-flows`
|
|
100
|
+
|
|
101
|
+
A media step's result carries `usage` — the quantity it consumed, in the provider's unit (tokens, characters or seconds), with `status: 'reported'` or `'unreported'` — and the run step records it. A step priced `unpriced` used to read the same whether the provider had reported a tally or nothing at all.
|
|
102
|
+
|
|
103
|
+
`FlowCapabilityMissing` can carry a `receipt` (cost, cost source, request id, usage) for a failure that happened AFTER a billed generation, typically when the artifact could not be stored. The engine writes it onto the failed step and adds the charge to the run, and `costCompleteness` counts a failed step that carries one — a run that paid for a generation and then failed to store it reads `incomplete`, not `unknown`. The built-in media adapter fills both. A failed sequential step also updates the run's `costCompleteness` now; it used to keep the verdict of the last step that succeeded.
|
|
104
|
+
- **Inspect individual outcomes of stored schedule backfills** — `@voltro/devtools-ui`
|
|
105
|
+
|
|
106
|
+
Schedule backfills expose paginated recorded outcomes with status, scheduled time and complete run identity, including read-only access and results older than the recent firing history.
|
|
107
|
+
|
|
108
|
+
Each outcome opens its original firing details on demand, including error, timing and replica information. Both dashboard hosts query the exact upstream run independently of recent-history limits. Shared hosts provide useScheduleRun for the detail read.
|
|
109
|
+
- **Generated media is stored with the owner, visibility and tags you give it** — `@voltro/ai`, `@voltro/protocol`, `@voltro/plugin-storage`, `@voltro/voltro`
|
|
110
|
+
|
|
111
|
+
`mediaStep({ persist: { ownerId, visibility, tags } })` hands all three through the queue and the dispatcher to the storage plugin's `mediaPersist`, which stores the artifact with them. Every generated artifact used to land owner-less, private and untagged, although the store takes all three — so deletion, restore and listing by owner or tag did not reach generated media.
|
|
112
|
+
|
|
113
|
+
The capability's `put` and `ingestUrl` gain three OPTIONAL fields. A `mediaPersist` implementation that ignores them stays assignable, and a caller that does not pass them compiles unchanged — which is why the surface change is marked compatible.
|
|
114
|
+
- **Read individual schedule firings by identity** — `@voltro/cli`
|
|
115
|
+
|
|
116
|
+
The schedule firing Inspect endpoint accepts a runId filter combined with the schedule name, so older run details remain queryable outside the recent-history page. Missing or mismatched identities return an empty result.
|
|
117
|
+
- **Stream storage uploads through the local dashboard** — `@voltro/cli`
|
|
118
|
+
|
|
119
|
+
Local dashboard servers can prepare session-bound, one-use storage upload handoffs and stream exact file bytes to the issuing app. Read/write credentials authorize preparation; binary transfers carry only the signed storage capability, enforce the admitted size, reject cross-site requests and redirects, and support named storage instances.
|
|
120
|
+
- **`registerMediaTaskProvider` — media jobs through a provider with a task handle** — `@voltro/ai`
|
|
121
|
+
|
|
122
|
+
A hosted media queue answers a submission with a task handle and is asked for the result later; `mediaStep` had no way to reach one. `registerMediaTaskProvider(name, { submit, poll })` registers the contract, and `mediaStep({ provider: { name } })` routes through it. The dispatcher writes the handle onto the queue row before anything else, polls once per claim without holding the lease across the job, persists the artifacts under the same `<row>#<attempt>#<index>` identity and records usage in the provider's unit.
|
|
123
|
+
|
|
124
|
+
A job whose worker was lost after the handle was written is polled by the next claim — never submitted again and never reported `unknown`. The remaining window is a worker lost between the provider accepting the job and the handle reaching the row: such a row still resumes its run with `outcome: 'unknown'`. `submit` receives an idempotency key derived from the row and the attempt; a registration that declares `idempotentSubmit: true` closes that window too. A `failed` status is terminal unless the provider marks it `retryable`; a task still pending after `taskTimeoutMs` is failed with the reason.
|
|
125
|
+
|
|
126
|
+
`_voltro_ai_inferences` gains the nullable `taskHandle` and `taskSubmittedAt` columns; the declarative differ adds them on the next boot or `db apply`.
|
|
127
|
+
- **`tx.savepoint()` works on every dialect and on the store a handler holds** — `@voltro/database`, `@voltro/runtime`, `@voltro/sql-postgres`, `@voltro/sql-mysql`, `@voltro/sql-mssql`, `@voltro/sql-sqlite`
|
|
128
|
+
|
|
129
|
+
Inside a transaction, `tx.savepoint(fn)` runs `fn` so that a failure undoes all of `fn` — and only `fn` — while the transaction goes on. It existed on Postgres alone and was not passed through to `ctx.store`, so in a handler it was `undefined`; the in-memory store every app test runs on had none either. It is now on Postgres, MySQL, MariaDB, SQL Server, SQLite and Turso, on the in-memory store, and on the store a handler holds.
|
|
130
|
+
|
|
131
|
+
This is the way to skip a failing row and keep the rest: catching the failure without a savepoint ends the transaction on Postgres, and `transactional()` then fails with `TransactionAbortedError` rather than committing nothing.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
await ctx.store.transactional(async (tx) => {
|
|
135
|
+
for (const row of rows) {
|
|
136
|
+
await tx.savepoint!(() => tx.insert('imports', row)).catch(() => skipped.push(row.id))
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
- **Upload files from the shared storage inspector** — `@voltro/devtools-ui`
|
|
141
|
+
|
|
142
|
+
The shared storage inspector supports file uploads through a host-provided mutation. It requires explicit tenant and owner attribution, defaults to private visibility, prevents duplicate submissions, preserves inputs on failure, and refreshes inventory after success. Read-only hosts do not show upload controls.
|
|
143
|
+
|
|
144
|
+
### Fixed
|
|
145
|
+
|
|
146
|
+
- **A payload the client refuses to encode is printed with the field it names** — `@voltro/client`
|
|
147
|
+
|
|
148
|
+
A payload with a field the procedure does not declare fails its schema before it is sent, so the server never sees the request and no server log has it. The error the caller receives names the field, but an app that logs the object rather than its message saw only an opaque `ParseError`. The client now prints the full message to the console once when a value fails its schema on the client — on every path that reduces a failed call, reads and writes alike. A declared failure from a handler is not printed; that one is the app's to handle.
|
|
149
|
+
- **VOLTRO_DESTRUCTIVE_OK no longer takes credit for drops it never covered** — `@voltro/cli`
|
|
150
|
+
|
|
151
|
+
A `db apply` whose lossy operations were all `dropped()` columns printed, with `VOLTRO_DESTRUCTIVE_OK` set,
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
db apply --plan: proceeding with acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
and then refused for want of `--yes`. Neither was true of those operations. A `dropped()` marker is the acknowledgement of its drop — the planner never blocks it — so the variable had nothing to relax, and the same plan applies with `--yes` alone. Read together, the two lines taught the opposite: that `--yes` acknowledges data loss, and that a variable kept "for this release" stops mattering once a pipeline passes `--yes`.
|
|
158
|
+
|
|
159
|
+
- The acknowledgement line appears only when the variable actually unblocked something. When it is set and the plan has nothing blocked, the apply says that instead — `VOLTRO_DESTRUCTIVE_OK is set but this plan has no blocked operation — it changed nothing in this run` — so a variable left in a deploy's configuration is noticed before it covers the next release's undeclared drop. - The `--yes` question on MySQL, MariaDB, SQLite and Turso now says what `--yes` does not answer: the lossy operations were authorised where they are declared, and a drop the schema does not declare stays blocked with or without it. - A refused first run ended with `nothing of this run remains … there was nothing to undo` while the deploy journal that run had created stood in the database. The journal is the runner's own and stays, hidden from every plan; the line now says what it is about and names the table it keeps.
|
|
160
|
+
- **A table whose key is spelled `(id)` can be written by id again** — `@voltro/runtime`
|
|
161
|
+
|
|
162
|
+
`update(table, '<id>', …)` and `delete(table, '<id>')` were refused on any table with an explicit `.primaryKey([...])`, including `.primaryKey(['id'])` — whose key IS the `id` column. The refusal's own message read "has no `id` column — its primary key is (id)". It exists for keys a string cannot name, `(userId, orgId)` or a single column shared with a parent, and those are still refused.
|
|
163
|
+
|
|
164
|
+
Framework tables declare their key this way, so the store a handler holds could not write them by id: `storage.put({ operationId })` failed at its reservation, and clearing a tenant selection threw, turning a revoked membership into `Unauthenticated` instead of a return to the home tenant. The raw store wrote all of them fine, which is why suites built on it stayed green.
|
|
165
|
+
- **`voltro doctor` no longer calls a retry loop an N+1** — `@voltro/cli`
|
|
166
|
+
|
|
167
|
+
`n-plus-one` already ignored reads inside `while` and `do` loops — they advance a cursor or retry, which proves no read per row. `for (;;)` is `while (true)` spelled differently and was reported: a handler that rolls a random code until one is free, one read per attempt over no collection, came back as an N+1. A `for` with no condition is now treated like the `while` it is; a counted `for` over input is still reported.
|
|
168
|
+
- **A failed AI call says why — the driver code joins the message** — `@voltro/ai`
|
|
169
|
+
|
|
170
|
+
A provider failure became an `AiError` carrying only the top-level message, so a dead upstream read `fetch failed` with nothing under it. The error now keeps the raw failure as `cause`, and the codes of its chain (`ECONNREFUSED`, `CERT_HAS_EXPIRED`, …) join the message — including through the SDK's retry wrapper, where the chain continues under `lastError`. Structured calls that used to report only `structured provider call failed` now carry the provider's message as well.
|
|
171
|
+
|
|
172
|
+
`cause` is not enumerable and not a schema field, so it stays on the server: the request body, headers and socket address it can hold never reach an encoded error.
|
|
173
|
+
- **Prepare a new backfill without submitting it** — `@voltro/devtools-ui`
|
|
174
|
+
|
|
175
|
+
Preparing a new schedule backfill now only clears the previous request. The click cannot also submit the newly rendered start control; operators explicitly start and, when required, confirm the new range.
|
|
176
|
+
- **Preserve timestamp precision in the data editor** — `@voltro/devtools-ui`
|
|
177
|
+
|
|
178
|
+
Cell and record timestamp editors preserve milliseconds. Saving an unchanged inline draft issues no write, including during repeated daylight-saving hours. Date validation rejects impossible calendar days instead of silently normalizing them into another month.
|
|
179
|
+
- **Keep record edits isolated from incoming live snapshots** — `@voltro/devtools-ui`
|
|
180
|
+
|
|
181
|
+
Record editors retain their opening snapshot when live rows change. Untouched fields no longer become stale patches that overwrite external changes. Open drafts remain available when their row leaves the current page.
|
|
182
|
+
- **A first production deploy no longer refuses over a table the apply created** — `@voltro/database`, `@voltro/cli`
|
|
183
|
+
|
|
184
|
+
`voltro db apply --plan` refused on its FIRST run against any database that predates the deploy journal, with a drift nobody caused:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
db apply --plan: refusing — the live schema has drifted since this plan was generated.
|
|
188
|
+
planFrom=bfff4758e04fee2f live=d3d95a02dc514886
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Nothing had touched the database between the two commands. The drift came from the refusing run itself: `db apply` creates its own deploy journal (`_voltro_deploys`) before it holds the live fingerprint against the saved plan's, so it introspected a schema that now contained a table its own plan had listed as work still to do.
|
|
192
|
+
|
|
193
|
+
**It hit the only route production allows.** A bare `voltro db apply` is refused there, so `db plan --json` → `db apply --plan` is the prescribed path — and it could not complete on its first run. For a pre-upgrade job without retries that is a stopped rollout. The second run succeeds (the table exists by then), which is why this read like a fluke on a concurrency path rather than like something every first deploy meets.
|
|
194
|
+
|
|
195
|
+
**The fix is two halves, and either one alone is wrong.** The journal is no longer DECLARED in the framework table set, and it is hidden from the introspection snapshot — exactly how `_voltro_migration_ops`, the applier's other bookkeeping table created in the same run for the same reason, has always been treated.
|
|
196
|
+
|
|
197
|
+
- Hiding it while still declaring it is **worse than the original defect**: the planner then proposes `create-table _voltro_deploys` on every run, and `applyPlan`'s convergence proof refuses to record a fingerprint — every apply dies, not just the first. Measured, not reasoned: an integration test on a virgin database caught it. - Undeclaring it without hiding it leaves the fingerprint moving between plan and apply, which is the reported bug unchanged.
|
|
198
|
+
|
|
199
|
+
The cost is the one the bootstrap path already states for the other ledger: the journal's shape cannot evolve through the declarative differ. Affordable for the same reason — it records in-flight deploys, not history anyone rebuilds a schema from. An existing database keeps its journal: an undeclared `_voltro_*` table is never planned for a drop.
|
|
200
|
+
|
|
201
|
+
There was no workaround. No read-only command creates the table, so nothing could be run ahead of the plan; `|| (db plan; db apply)` would swallow a REAL drift just as thoroughly; and creating a framework-internal table by hand in a production database is not something a user should be asked to do.
|
|
202
|
+
- **The bulk-write rule sees the typed write helpers, which are now documented** — `@voltro/cli`
|
|
203
|
+
|
|
204
|
+
`voltro doctor`'s `bulk-write-in-loop` matched `store.insert(` and nothing else. Converting a loop to the typed helper made the finding disappear — and with it the reviewed decision attached to that finding, which the next run then reported as a `Stale review`.
|
|
205
|
+
|
|
206
|
+
A deployment that moved 107 write sites to the typed helpers hit exactly that on one loop and rolled the conversion BACK, with the right reasoning: trading a working rule for a typed check on four scalar fields is the wrong way round. Nobody should have to choose between our type safety and our linting.
|
|
207
|
+
|
|
208
|
+
The rule now matches the typed forms as well — `insertRow`, `insertRowEffect`, `upsertRowEffect`, `upsertRowOutcomeEffect`, `insertIgnoreRowOutcomeEffect` — and maps each onto the store verb it wraps, so the advice stays the one that fits (`insertMany` for a plain insert; one transaction around a conflict verb, which has no set-based form).
|
|
209
|
+
|
|
210
|
+
**The Effect helpers were documented nowhere, and that is fixed in the same change.** The two families live in different packages: the Promise forms `insertRow` / `upsertRow` are `@voltro/database`'s and were documented; the five Effect variants are `@voltro/runtime`'s and appeared on no page in either language. Knowing one family therefore sent readers looking for the other in the wrong package. The mutations page now carries both, with the split stated plainly, the typed error channel that a `Effect.promise` thunk throws away, and the `outcome` value — reading only the row and then recording a `created` event writes an audit entry for a write a conflict skipped.
|
|
211
|
+
|
|
212
|
+
That page already warned that adopting the typed form can blind tooling matching the old spelling. The warning was right and this rule was one of its victims.
|
|
213
|
+
- **A project with a file migration can be applied to an empty database** — `@voltro/database`
|
|
214
|
+
|
|
215
|
+
`voltro db apply` could not create the schema of a project carrying even ONE file under `migrations/`, against a database that did not have `_voltro_migration_plans` yet — every new environment, every reset local database, every hermetic test stack. On postgres it ended like this, and nothing was applied:
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
INFO file-based migration applying id=20260918_040000_example
|
|
219
|
+
ERROR db apply: a file-based migration FAILED — the schema diff was NOT run
|
|
220
|
+
ERROR db apply: rolled back — postgres applies a deploy as one transaction, so
|
|
221
|
+
nothing changed
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Two halves of one cause, and the file pass runs BEFORE the planner that would create the table both of them want.
|
|
225
|
+
|
|
226
|
+
**Reading it.** The discovery step asks the ledger which migrations already ran, as a plain `SELECT` with a catch beside it that read an absent table as "nothing applied". That is correct outside a transaction and wrong inside one: postgres and SQL Server apply a whole deploy as a single transaction, and a failed statement aborts it, so every later statement of the deploy died — starting with the migration's own savepoint. The catch turned an error the database does not forget into one only the runner had forgotten. The question is asked of the catalog now, which answers on an empty database instead of failing.
|
|
227
|
+
|
|
228
|
+
**Writing it.** Recording a migration inserts into that same table, so on an empty database `up()` succeeded and the row could not be written — the one outcome whose message tells you not to re-run. The runner creates the ledger itself when something is pending, the way the resume ledger has always been created: a run needs it before any schema work of its own has happened. Nothing is created for a project with no pending file migration.
|
|
229
|
+
|
|
230
|
+
This was not confined to the dialects that hold a deploy in one transaction. On MySQL, MariaDB, Turso and SQLite the same recording insert failed, without a poisoned transaction to make it loud. Measured on all six dialects, red before the fix and green after it.
|
|
231
|
+
|
|
232
|
+
**A `voltro dev` boot was never affected, and that is measured rather than reasoned:** it creates the bookkeeping tables BEFORE the file pass, so the ledger is already there, and it holds no deploy transaction for a failed statement to poison. A real boot against a virgin schema with a file migration present, on the unfixed code, applies the migration and comes up. So the command to re-run after upgrading is `voltro db apply` — a development boot will not have told you anything was wrong.
|
|
233
|
+
|
|
234
|
+
**The two ledger readers now declare the failure they always had.** `appliedFileMigrationIds` and `appliedFileMigrations` (from `@voltro/database/sql`) were typed `Effect<…, never>` while the read could fail — the catch beside them was exactly the bug above. Their error channel is `SqlError` now. This is filed `apiSurface: compatible` rather than breaking: the old `never` was unsound, no documented path, guide or example reaches either symbol, and inside the framework only `deployCompensation` calls them. A consumer who nonetheless annotated one of them `never` widens that annotation to `SqlError`; nothing else changes, and the runtime behaviour is the one the type now admits.
|
|
235
|
+
- **Two processes no longer run the same file migration** — `@voltro/database`
|
|
236
|
+
|
|
237
|
+
The runner read the pending file migrations before taking the migration lock and never again. A second replica that booted beside the first waited for the lock and then ran the list it had read while the first was still applying it: the body ran twice, and the second replica's boot died on the ledger row the first had written. It now re-reads the ledger under the lock, the same way schema migrations re-plan under theirs. A rollback of one migration from two places no longer runs its `down` twice either.
|
|
238
|
+
|
|
239
|
+
When a ledger row cannot be written because the row already exists, the message now says so — the migration IS recorded, by another process, and the body has run twice. It used to say the row was missing and that the next run would apply the migration again, which invited deleting a correct row.
|
|
240
|
+
- **Delete inspected rows atomically against their selected snapshots** — `@voltro/cli`, `@voltro/devtools-ui`
|
|
241
|
+
|
|
242
|
+
Single and bulk deletion now use one conditional batch request with complete selected-row snapshots. A changed or missing row refuses the entire transaction without partial deletion. Live updates preserve the selection baseline, errors retain the selection or dialog, and duplicate confirmation clicks cannot dispatch another request. Both dashboard hosts use the same batch contract; the sequential single-delete fallback is removed.
|
|
243
|
+
- **Reject stale data editor updates atomically** — `@voltro/cli`, `@voltro/devtools-ui`
|
|
244
|
+
|
|
245
|
+
Data editor updates carry the opening values of edited fields as required `expected` preconditions. Inspection compares them inside the write transaction, returning HTTP 409 for changed or deleted rows instead of overwriting concurrent changes. Cell editors retain their opening value during live refresh, and rejected writes retain the draft. Verified with Memory, Postgres, MySQL, MariaDB, SQL Server, SQLite and Turso. Deletion preconditions are not included.
|
|
246
|
+
|
|
247
|
+
Conditional reads reuse the framework row-lock query so namespace-bound stores lock the same physical table they update or delete.
|
|
248
|
+
- **The migration pool refusal names the variable it actually reads** — `@voltro/cli`, `@voltro/database`
|
|
249
|
+
|
|
250
|
+
A migration needs two connections — one holds the lock, the other runs the statements — and a pool of one is refused up front. The refusal then told you to fix it with a variable nothing reads:
|
|
251
|
+
|
|
252
|
+
```text
|
|
253
|
+
migrate needs ≥ 2 connections, the pool has 1 (DB_POOL_MAX=1) …
|
|
254
|
+
Set DB_POOL_MAX to at least 2 for the migrating process.
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`DB_POOL_MAX` is read nowhere in the framework. The pool size comes from `DB_MAX_CONNECTIONS` (or `PG_MAX_CONNECTIONS`). Following the instruction literally set a variable with no effect, produced the identical refusal, and left no next step — the worst shape a fix line can take, because it looks like the answer while it moves nothing. Both messages name the real variable now, and the one at the failure adds the driver's own `maxConnections` for a pool sized in code rather than in the environment.
|
|
258
|
+
|
|
259
|
+
**The check that exists to catch exactly this could not see it.** The message-API guard verifies that a variable a message tells you to SET is read somewhere — but it matched `VOLTRO_*` only, so every `DB_*` and `PG_*` instruction was outside its reach. The pattern now covers the framework's database variables too, which is what makes this class of defect detectable rather than merely fixed once.
|
|
260
|
+
- **Offloaded calls carry their provider name to the dispatcher** — `@voltro/ai`
|
|
261
|
+
|
|
262
|
+
Every queued call on `_voltro_ai_inferences` stored `provider: 'unknown'`: the step read a key no provider config has. The dispatcher rebuilt the config from that column, so a real dispatcher resolved no model for any offloaded text, object, transcription or media call and gave up after its retries. The row and the step input now carry the config's `name`, and the dispatcher resolves the provider that was declared.
|
|
263
|
+
- **Keep Radix primitives on the web dev server React instance** — `@voltro/cli`
|
|
264
|
+
|
|
265
|
+
The web dev server processes Radix packages through Vite so workspace dependencies share its deduplicated React instance during SSR, preventing invalid hook calls in dashboard navigation.
|
|
266
|
+
- **Storage object claims are not carried between environments** — `@voltro/data-transfer`
|
|
267
|
+
|
|
268
|
+
The per-caller claims that hold a shared storage object were not classified for cross-environment transfer, so `voltro data` carried them by default. A claim names a physical key in one deployment's provider and a caller of that deployment; imported elsewhere, it kept the target retaining a key for a claimant it never had. The table is environment-local now, beside the storage operation and object-head tables it belongs with.
|
|
269
|
+
- **No production warning about anonymous tenants when one is configured** — `@voltro/cli`
|
|
270
|
+
|
|
271
|
+
With `auth.anonymousTenantId` set, every production boot warned that anonymous callers resolve to the tenant their own `x-tenant` header names. A configured anonymous tenant is the server's choice and ignores that header, so the warning stated the opposite of what the server does. It now appears only when the header really decides.
|
|
272
|
+
- **The junction-FK check no longer asks a dropped column for a reference** — `@voltro/cli`
|
|
273
|
+
|
|
274
|
+
`voltro doctor`'s junction-FK audit reported id-shaped columns that are plain scalars and asked for a `reference()`. It reported `dropped()` columns too:
|
|
275
|
+
|
|
276
|
+
```text
|
|
277
|
+
✗ junction FKs: 12 junction tables with an id column that is a plain text()
|
|
278
|
+
'apiKeys': 'keyId' (sits beside a wired reference() on this table)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
where the declaration is `keyId: dropped().safe()`. A tombstone cannot carry a `reference()` — the marker exists so the column disappears — so the finding was unresolvable by construction. Two of twelve entries in that list could never be acted on, and a list with unresolvable entries gets read less as a whole. That argument came from the deployment that reported it and it is the reason this is worth fixing rather than documenting.
|
|
282
|
+
|
|
283
|
+
The information was already there and thrown away: `dropped()` builds a `text` column carrying `dropped: true`, and the mapping into the audit lifted only the name, the type and the external-id marker. It carries the tombstone flag now, and the audit skips those columns.
|
|
284
|
+
- **The pre-apply question no longer promises a rollback the dialect cannot do** — `@voltro/cli`
|
|
285
|
+
|
|
286
|
+
On the dialects that apply a plan one statement at a time, the confirmation you answer before a deploy said:
|
|
287
|
+
|
|
288
|
+
```text
|
|
289
|
+
… if one fails, this run undoes what it applied (the schema back to its
|
|
290
|
+
pre-deploy shape, file migrations by their down) and stops.
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
and the failure message in the SAME run said the opposite:
|
|
294
|
+
|
|
295
|
+
```text
|
|
296
|
+
This dialect does NOT apply a plan atomically: those 20 are COMMITTED and
|
|
297
|
+
there is no rollback.
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The second one is right. The first is not occasionally wrong — the question is asked only where a plan is applied statement by statement, so the promise was wrong at every site it could ever be printed. Someone reading it went into the deploy believing a failure was free.
|
|
301
|
+
|
|
302
|
+
It now says what actually happens: the statements before the failure are COMMITTED, and the runner then ATTEMPTS a compensation. The attempt and its limits are stated together rather than one before the deploy and the other after the failure.
|
|
303
|
+
|
|
304
|
+
The `down` clause needed the same treatment. A migration declared `irreversible()` has no `down`, so for a project whose file migrations are all irreversible the sentence promised something that could not happen in any case. What no undo can reach — the rows a lossy operation removed, and every irreversible migration's effect — is named in the same breath.
|
|
305
|
+
- **`transactional()` no longer resolves over a transaction the database ended** — `@voltro/database`, `@voltro/sql-postgres`, `@voltro/sql-mysql`, `@voltro/sql-mssql`, `@voltro/sql-sqlite`
|
|
306
|
+
|
|
307
|
+
A work callback that caught a failed statement and carried on made `transactional()` resolve — and on Postgres nothing was committed: the failed statement aborts the transaction, every later one is refused, and COMMIT runs as ROLLBACK without raising. The rows that had succeeded were lost too, and the caller was told they were written.
|
|
308
|
+
|
|
309
|
+
It now fails with `TransactionAbortedError`, which names the caught failure and the working form. To skip a failing statement and keep the rest, run it inside `tx.savepoint(() => …)`: only that statement is rolled back. Code that catches store failures inside a transaction without a savepoint will now see this error where it used to see a silent loss.
|
|
310
|
+
|
|
311
|
+
The same guard runs on MySQL, MariaDB, SQL Server and SQLite, where it closes a different hole: a deadlock rolls the WHOLE transaction back and the session leaves it, so a caller that caught the deadlock and kept writing wrote in autocommit — durable at once, and out of reach of any later rollback. After such a failure no further statement is sent, and because a deadlock is retryable the work runs again from the start in a fresh transaction.
|
|
312
|
+
- **An app whose workflows are all internal no longer reports a false mismatch** — `@voltro/cli`
|
|
313
|
+
|
|
314
|
+
An app that declares at least one workflow and marks every one of them `internal: true` logged this on every page load:
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
[voltro] the api client this page loaded does not match what the 'app' server
|
|
318
|
+
binds: __voltro.workflow.cancel, __voltro.workflow.control,
|
|
319
|
+
__voltro.workflow.domainEvents and 10 more. Responses for those procedures may
|
|
320
|
+
decode against the wrong schema …
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Nothing was wrong with the schemas, and the advice the message gives — reload, delete `.framework/.vite` — could not help, because the two sides were answering different questions. Both boot paths bind the 13 workflow built-ins for **any** discovered workflow; the codegen emitted them only when one was reachable over the wire. An internal-only app therefore ran a server binding 13 procedures its generated client had never been given.
|
|
324
|
+
|
|
325
|
+
`internal: true` answers *who may start this workflow over the wire* — it is a start decision. The built-ins start nothing: they read and steer runs that already exist, which is why an internal workflow's runs stay inspectable. So the server was right, and the codegen now follows it.
|
|
326
|
+
|
|
327
|
+
The two questions are separate in the generator now rather than one flag doing both, and the narrower one still decides what it should: an app with no wire-exposed workflow gets no start lift and no unused import for one. An app with no workflow at all is unchanged — emitting the built-ins there would be the same mismatch in the mirror image.
|
|
328
|
+
- **Bind dashboard bulk actions to previewed workflow runs** — `@voltro/devtools-ui`
|
|
329
|
+
|
|
330
|
+
Bulk workflow actions use the eligible run IDs from their preview instead of selecting newly matching runs. Pending requests lock filters and block duplicate submissions; failures discard the preview while retaining the operator's inputs.
|
|
331
|
+
- **Report declined bulk workflow recovery actions accurately** — `@voltro/cli`
|
|
332
|
+
|
|
333
|
+
Bulk replay reports declined redrives with their original reason instead of counting them as successful. Retry requires an execution identity; a missing workflow runtime cannot produce a successful no-op. Accepted recovery actions do not imply completed workflow executions.
|
|
334
|
+
- **Refuse false recovery success for an unchanged retry identity** — `@voltro/cli`, `@voltro/devtools-ui`
|
|
335
|
+
|
|
336
|
+
Bulk retry reports an unchanged execution identity instead of claiming successful recovery of the original failed run. Dashboard retry guidance explains payload resubmission and idempotency rather than promising a fresh execution or empty journal.
|
|
337
|
+
- **Retain operator cancellation causes in control receipts** — `@voltro/cli`
|
|
338
|
+
|
|
339
|
+
Shared cancellation helpers persist the supplied reason and details even when generating a request ID. Repeating an explicit intent preserves its original cause; producer sweeps still require confirmed cancellation rather than mere acceptance.
|
|
340
|
+
- **Workflow controls ask the same access question as the workflow read feeds** — `@voltro/runtime`, `@voltro/cli`
|
|
341
|
+
|
|
342
|
+
The workflow built-ins decided access twice, in two places. The read feeds behind `useWorkflowRuns`, `useWorkflowRun`, `useWorkflowRunSteps` and `useWorkflowRunEvents` let a subject holding `admin:full` see every run; the control actions — `cancel`, `signal`, `update`, `resume` — compared tenants only, so the same subject could see a schedule's run in the list and was refused when it tried to cancel it.
|
|
343
|
+
|
|
344
|
+
Both halves now ask one function. What a caller may drive is what it may read:
|
|
345
|
+
|
|
346
|
+
- a system subject or one holding `admin:full` reads and drives every run; - a run the system started — a schedule's, say, or one of an `internal: true` workflow started from server code — belongs to the operator, and no user reaches it; - a run with a tenant is shared by that tenant's users.
|
|
347
|
+
|
|
348
|
+
Every signed-in subject carries a tenant, so an app that does not use tenants is one whose users share one: they see and drive each other's runs, and none of them reaches a system-started run.
|
|
349
|
+
- **Show complete bulk workflow recovery failures** — `@voltro/devtools-ui`
|
|
350
|
+
|
|
351
|
+
Both workflow bulk-action surfaces expose every per-run failure with its full identity and reason in paginated details. Recovery acceptance is labelled separately from successful workflow completion.
|
|
352
|
+
- **Resolve workflow retry through the original run owner** — `@voltro/cli`
|
|
353
|
+
|
|
354
|
+
Development and production workflow retry resolve the original run’s physical store before reading its payload, matching the Inspect path. Missing or ambiguous ownership refuses dispatch instead of searching the primary store.
|
|
355
|
+
- **Keep workflow selection confirmation bound to reviewed runs** — `@voltro/devtools-ui`
|
|
356
|
+
|
|
357
|
+
Changing checked workflow runs resets their bulk-action preview, including delayed responses for a previous selection. Apply targets only previewed eligible IDs. Pending actions lock controls and reject duplicate submissions; changed reasons and failed requests require a new preview.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
42
361
|
## [0.74.0] — 2026-09-18
|
|
43
362
|
|
|
44
363
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { MssqlClient as e } from "@effect/sql-mssql";
|
|
2
2
|
import { TYPES as t } from "tedious";
|
|
3
3
|
import { Config as n, Duration as r, Effect as i, Layer as a, ManagedRuntime as o, Option as s, Redacted as c } from "effect";
|
|
4
|
-
import { CDC_OFFSETS_TABLE as l, DEFAULT_ACQUIRE_TIMEOUT_MS as u, EagerCardinalityError as d, MSSQL_TRANSIENT_NUMBERS as f, _voltroMssqlCdcOffsetsTable as p, attachEagerLoads as m, attributionFields as h, bulkInsertLimitsFor as g, causeBranches as _, chunkRowsForInsert as v, compileEagerJson as y, compileJsonPatch as b, compilePredicate as x, compileRawFragment as S, compileSelect as C, decodeRowsFromSchema as w, encodeRowForSchema as T, externalChangeEvent as E, hasEagerLoads as ee, isTableReactive as D, jsonPatchAssignments as te, makeEagerFallbackReporter as ne, observeDbOp as O, qualifyTable as
|
|
5
|
-
import { EventEmitter as
|
|
6
|
-
import { createLogger as
|
|
7
|
-
import { SqlClient as
|
|
4
|
+
import { CDC_OFFSETS_TABLE as l, DEFAULT_ACQUIRE_TIMEOUT_MS as u, EagerCardinalityError as d, MSSQL_TRANSIENT_NUMBERS as f, _voltroMssqlCdcOffsetsTable as p, attachEagerLoads as m, attributionFields as h, bulkInsertLimitsFor as g, causeBranches as _, chunkRowsForInsert as v, compileEagerJson as y, compileJsonPatch as b, compilePredicate as x, compileRawFragment as S, compileSelect as C, decodeRowsFromSchema as w, encodeRowForSchema as T, externalChangeEvent as E, hasEagerLoads as ee, isTableReactive as D, jsonPatchAssignments as te, makeEagerFallbackReporter as ne, makeTransactionAbortTracker as re, observeDbOp as O, qualifyTable as ie, quoteIdent as ae, raiseChangeListenerCeiling as oe, recordsTable as k, requireTable as se, rowKeyColumns as A, runSavepoint as ce, runStoreTransaction as le, runWriteRecorders as ue, stampGeneratedId as j, stampGeneratedIds as de, upsertUpdatePatch as M, upsertUpdateValues as N, withCapturedAttribution as fe } from "@voltro/database";
|
|
5
|
+
import { EventEmitter as pe } from "node:events";
|
|
6
|
+
import { createLogger as P } from "@voltro/logger";
|
|
7
|
+
import { SqlClient as F, TransactionConnection as I } from "@effect/sql/SqlClient";
|
|
8
8
|
//#region src/sqlLayer.ts
|
|
9
|
-
var
|
|
9
|
+
var L = {
|
|
10
10
|
...e.defaultParameterTypes,
|
|
11
11
|
null: t.NVarChar,
|
|
12
12
|
Date: t.DateTime2,
|
|
13
13
|
string: t.NVarChar
|
|
14
|
-
},
|
|
14
|
+
}, R = (e) => {
|
|
15
15
|
let t = e.acquireTimeoutMs ?? u;
|
|
16
16
|
return t > 0 ? t : void 0;
|
|
17
17
|
}, z = (t) => {
|
|
18
|
-
let i =
|
|
18
|
+
let i = R(t);
|
|
19
19
|
return e.layerConfig({
|
|
20
20
|
server: n.succeed(t.server),
|
|
21
|
-
parameterTypes: n.succeed(
|
|
21
|
+
parameterTypes: n.succeed(L),
|
|
22
22
|
...i === void 0 ? {} : { connectTimeout: n.succeed(r.millis(i)) },
|
|
23
23
|
...t.port === void 0 ? {} : { port: n.succeed(t.port) },
|
|
24
24
|
...t.database === void 0 ? {} : { database: n.succeed(t.database) },
|
|
@@ -28,7 +28,7 @@ var ue = {
|
|
|
28
28
|
...t.trustServer === void 0 ? {} : { trustServer: n.succeed(t.trustServer) },
|
|
29
29
|
...t.maxConnections === void 0 ? {} : { maxConnections: n.succeed(t.maxConnections) }
|
|
30
30
|
});
|
|
31
|
-
},
|
|
31
|
+
}, me = (e) => {
|
|
32
32
|
let t = e.get("sslmode");
|
|
33
33
|
if (t !== null) {
|
|
34
34
|
if (t === "require") return !0;
|
|
@@ -43,7 +43,7 @@ var ue = {
|
|
|
43
43
|
throw Error(`DB_URL '?${t}=${n}' is not supported by the mssql dialect — use 'true'/'1' or 'false'/'0'.`);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
},
|
|
46
|
+
}, B = (e) => {
|
|
47
47
|
let t = e.acquireTimeoutMs === void 0 ? {} : { acquireTimeoutMs: e.acquireTimeoutMs }, n = (e) => e === void 0 ? {} : {
|
|
48
48
|
encrypt: e,
|
|
49
49
|
...e ? { trustServer: !0 } : {}
|
|
@@ -56,7 +56,7 @@ var ue = {
|
|
|
56
56
|
username: decodeURIComponent(r.username || "sa"),
|
|
57
57
|
password: decodeURIComponent(r.password || ""),
|
|
58
58
|
database: r.pathname.replace(/^\//, "") || "master",
|
|
59
|
-
...n(e.ssl ??
|
|
59
|
+
...n(e.ssl ?? me(r.searchParams)),
|
|
60
60
|
...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections },
|
|
61
61
|
...t
|
|
62
62
|
};
|
|
@@ -71,7 +71,7 @@ var ue = {
|
|
|
71
71
|
...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections },
|
|
72
72
|
...t
|
|
73
73
|
};
|
|
74
|
-
},
|
|
74
|
+
}, V = (e) => z(B(e)), he = f, H = (e) => {
|
|
75
75
|
let t = e;
|
|
76
76
|
for (let e = 0; e < 5 && typeof t == "object" && t; e++) {
|
|
77
77
|
let e = t.number;
|
|
@@ -80,36 +80,36 @@ var ue = {
|
|
|
80
80
|
if (typeof n == "string") return n;
|
|
81
81
|
t = t.cause;
|
|
82
82
|
}
|
|
83
|
-
},
|
|
84
|
-
let t =
|
|
85
|
-
return t !== void 0 &&
|
|
86
|
-
}),
|
|
87
|
-
let t =
|
|
88
|
-
return t !== void 0 &&
|
|
89
|
-
}),
|
|
83
|
+
}, U = (e) => _(e).some((e) => {
|
|
84
|
+
let t = H(e);
|
|
85
|
+
return t !== void 0 && he.has(t);
|
|
86
|
+
}), ge = /* @__PURE__ */ new Set(["2601", "2627"]), W = (e) => _(e).some((e) => {
|
|
87
|
+
let t = H(e);
|
|
88
|
+
return t !== void 0 && ge.has(t);
|
|
89
|
+
}), G = (e) => U(e) ? "retry" : "noRetry", _e = {
|
|
90
90
|
I: "insert",
|
|
91
91
|
U: "update",
|
|
92
92
|
D: "delete"
|
|
93
|
-
},
|
|
94
|
-
let e = yield*
|
|
93
|
+
}, K = (e, t) => e(i.gen(function* () {
|
|
94
|
+
let e = yield* F;
|
|
95
95
|
if (((yield* e`
|
|
96
96
|
SELECT COUNT(*) AS ${e("on")}
|
|
97
97
|
FROM sys.change_tracking_databases
|
|
98
98
|
WHERE database_id = DB_ID()`)[0]?.on ?? 0) === 0) return yield* i.fail(/* @__PURE__ */ Error("mssql Change Tracking CDC requires CT enabled on the database. Run `ALTER DATABASE <db> SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON)`, or set CDC=0 for inline emit."));
|
|
99
99
|
for (let n of t) yield* e.unsafe(`IF NOT EXISTS (
|
|
100
100
|
SELECT 1 FROM sys.change_tracking_tables
|
|
101
|
-
WHERE object_id = OBJECT_ID(${
|
|
101
|
+
WHERE object_id = OBJECT_ID(${q(n)})
|
|
102
102
|
)
|
|
103
|
-
ALTER TABLE ${
|
|
104
|
-
})).then(() => void 0),
|
|
105
|
-
let t =
|
|
106
|
-
await
|
|
103
|
+
ALTER TABLE ${J(n)} ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = OFF)`);
|
|
104
|
+
})).then(() => void 0), q = (e) => `'${e.replace(/'/g, "''")}'`, J = (e) => `[${e.replace(/\]/g, "]]")}]`, Y = async (e) => {
|
|
105
|
+
let t = P({ scope: "voltro:mssql:cdc" }), n = e.pollIntervalMs ?? 500;
|
|
106
|
+
await K(e.run, e.tables);
|
|
107
107
|
let r = async () => (await e.run(i.gen(function* () {
|
|
108
|
-
return yield* (yield*
|
|
108
|
+
return yield* (yield* F)`SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_CURRENT_VERSION()) AS v`;
|
|
109
109
|
})))[0]?.v ?? "0", a = e.startVersion ?? await r(), o = !1, s = null, c = !1, l = (e, t) => BigInt(e) > BigInt(t), u = async (n, o) => {
|
|
110
110
|
let s = await e.run(i.gen(function* () {
|
|
111
|
-
let e = yield*
|
|
112
|
-
SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(${e.unsafe(
|
|
111
|
+
let e = yield* F, t = e.unsafe(J(n)), r = (yield* e`
|
|
112
|
+
SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(${e.unsafe(q(n))}))) AS floor`)[0]?.floor;
|
|
113
113
|
return r != null && BigInt(o) < BigInt(r) ? {
|
|
114
114
|
resync: !0,
|
|
115
115
|
rows: []
|
|
@@ -127,7 +127,7 @@ var ue = {
|
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
129
|
for (let t of s.rows) {
|
|
130
|
-
let r =
|
|
130
|
+
let r = _e[t.__ct_op];
|
|
131
131
|
if (!r) continue;
|
|
132
132
|
let { __ct_op: i, __ct_id: a, ...o } = t;
|
|
133
133
|
if (r === "delete") e.onChange({
|
|
@@ -178,26 +178,26 @@ var ue = {
|
|
|
178
178
|
},
|
|
179
179
|
currentVersion: () => a
|
|
180
180
|
};
|
|
181
|
-
},
|
|
181
|
+
}, X = P({ scope: "voltro:mssql" }), Z = async (e) => {
|
|
182
182
|
let t = e.changeStrategy ?? "inline", n = t;
|
|
183
|
-
t === "cdc" && !e.cdcConfig && (
|
|
184
|
-
let r = e.tracerLayer ? a.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, i = o.make(r), s = new
|
|
183
|
+
t === "cdc" && !e.cdcConfig && (X.warn("changeStrategy='cdc' requires cdcConfig (replicaId + includeTables); falling back to 'inline'."), n = "inline");
|
|
184
|
+
let r = e.tracerLayer ? a.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, i = o.make(r), s = new Q(await i.runPromise(F), i, n);
|
|
185
185
|
return n === "cdc" && e.cdcConfig && await s.startCdcConsumer(e.cdcConfig), s;
|
|
186
|
-
},
|
|
186
|
+
}, Q = class e {
|
|
187
187
|
sql;
|
|
188
188
|
runtime;
|
|
189
189
|
changeStrategy;
|
|
190
190
|
namespace;
|
|
191
191
|
emitter;
|
|
192
192
|
inflightTxns = 0;
|
|
193
|
-
reportEagerFallback = ne(
|
|
193
|
+
reportEagerFallback = ne(X);
|
|
194
194
|
cdcHandle = null;
|
|
195
195
|
cdcCheckpointTimer = null;
|
|
196
196
|
cdcPendingVersion = null;
|
|
197
197
|
cdcReplicaId = "";
|
|
198
198
|
cdcStreamName = "default";
|
|
199
199
|
constructor(e, t, n = "inline", r = null, i) {
|
|
200
|
-
this.sql = e, this.runtime = t, this.changeStrategy = n, this.namespace = r, this.emitter = i ?? new
|
|
200
|
+
this.sql = e, this.runtime = t, this.changeStrategy = n, this.namespace = r, this.emitter = i ?? new pe(), oe(this.emitter);
|
|
201
201
|
}
|
|
202
202
|
get inlineEmit() {
|
|
203
203
|
return this.changeStrategy === "inline";
|
|
@@ -206,19 +206,19 @@ var ue = {
|
|
|
206
206
|
return t === this.namespace ? this : new e(this.sql, this.runtime, this.changeStrategy, t, this.emitter);
|
|
207
207
|
}
|
|
208
208
|
nsT(e) {
|
|
209
|
-
return
|
|
209
|
+
return ie(this.namespace, e);
|
|
210
210
|
}
|
|
211
211
|
__mssqlReplicationFriend = { runEffect: (e) => this.runtime.runPromise(e) };
|
|
212
212
|
async executeQuery(e, t) {
|
|
213
|
-
let n = C(e, this.sql, this.namespace), r = t ? i.provideService(n,
|
|
213
|
+
let n = C(e, this.sql, this.namespace), r = t ? i.provideService(n, I, t) : n, a = await O("mssql", "select", () => this.runtime.runPromise(r));
|
|
214
214
|
return w(a, e.table, "mssql");
|
|
215
215
|
}
|
|
216
216
|
decodeWritten(e, t) {
|
|
217
217
|
return w([t], e, "mssql")[0] ?? t;
|
|
218
218
|
}
|
|
219
219
|
async executeInsert(e, t, n, r, a) {
|
|
220
|
-
t =
|
|
221
|
-
let o = this.sql, s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(T(t, e)).returning("*")}`, c = n ? i.provideService(s,
|
|
220
|
+
t = j(e, t);
|
|
221
|
+
let o = this.sql, s = o`INSERT INTO ${o(this.nsT(e))} ${o.insert(T(t, e)).returning("*")}`, c = n ? i.provideService(s, I, n) : s, l = (await this.runtime.runPromise(c))[0];
|
|
222
222
|
if (!l) throw Error(`MssqlStore.insert: row not returned post-insert in '${e}'`);
|
|
223
223
|
let u = this.decodeWritten(e, l);
|
|
224
224
|
return await this.routeEvent({
|
|
@@ -229,7 +229,7 @@ var ue = {
|
|
|
229
229
|
}, r, n, a), u;
|
|
230
230
|
}
|
|
231
231
|
async executeUpdate(e, t, n, r, a, o) {
|
|
232
|
-
let s = this.sql, c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(T(n, e)).returning("*")} WHERE ${s.and(
|
|
232
|
+
let s = this.sql, c = s`UPDATE ${s(this.nsT(e))} SET ${s.update(T(n, e)).returning("*")} WHERE ${s.and(A(t).map(([e, t]) => s`${s(e)} = ${t}`))}`, l = r ? i.provideService(c, I, r) : c, u = (await this.runtime.runPromise(l))[0];
|
|
233
233
|
if (!u) return null;
|
|
234
234
|
let d = this.decodeWritten(e, u);
|
|
235
235
|
return await this.routeEvent({
|
|
@@ -240,14 +240,14 @@ var ue = {
|
|
|
240
240
|
}, a, r, o), d;
|
|
241
241
|
}
|
|
242
242
|
async executeInsertMany(e, t, n, r, a) {
|
|
243
|
-
if (t =
|
|
243
|
+
if (t = de(e, t), t.length === 0) return [];
|
|
244
244
|
let o = this.sql, s = v(t, g("mssql")), c = (t) => o`INSERT INTO ${o(this.nsT(e))} ${o.insert(t.map((t) => T(t, e))).returning("*")}`, l;
|
|
245
245
|
if (s.length === 1) {
|
|
246
|
-
let e = c(s[0]), t = n ? i.provideService(e,
|
|
246
|
+
let e = c(s[0]), t = n ? i.provideService(e, I, n) : e;
|
|
247
247
|
l = await this.runtime.runPromise(t);
|
|
248
248
|
} else if (n) {
|
|
249
249
|
let e = [];
|
|
250
|
-
for (let t of s) e.push(...await this.runtime.runPromise(i.provideService(c(t),
|
|
250
|
+
for (let t of s) e.push(...await this.runtime.runPromise(i.provideService(c(t), I, n)));
|
|
251
251
|
l = e;
|
|
252
252
|
} else l = await this.runtime.runPromise(o.withTransaction(i.map(i.forEach(s, c, { concurrency: 1 }), (e) => e.flat())));
|
|
253
253
|
let u = t.map((e) => e.id), d = w(l, e, "mssql"), f = new Map(d.map((e) => [e.id, e])), p = u.map((e) => f.get(e)).filter((e) => e !== void 0), m = p.length === l.length ? p : d;
|
|
@@ -260,7 +260,7 @@ var ue = {
|
|
|
260
260
|
return m;
|
|
261
261
|
}
|
|
262
262
|
async executePatchJson(e, t, n, r, a, o, s, c) {
|
|
263
|
-
let l = this.sql, u = T(te(t, n, c), e), d = l.csv([b(l, "mssql", n, r), ...Object.entries(u).map(([e, t]) => l`${l(e)} = ${t}`)]), f = l`UPDATE ${l(this.nsT(e))} SET ${d} OUTPUT INSERTED.* WHERE ${l.and(
|
|
263
|
+
let l = this.sql, u = T(te(t, n, c), e), d = l.csv([b(l, "mssql", n, r), ...Object.entries(u).map(([e, t]) => l`${l(e)} = ${t}`)]), f = l`UPDATE ${l(this.nsT(e))} SET ${d} OUTPUT INSERTED.* WHERE ${l.and(A(t).map(([e, t]) => l`${l(e)} = ${t}`))}`, p = a ? i.provideService(f, I, a) : f, m = (await this.runtime.runPromise(p))[0];
|
|
264
264
|
if (!m) return null;
|
|
265
265
|
let h = this.decodeWritten(e, m);
|
|
266
266
|
return await this.routeEvent({
|
|
@@ -271,7 +271,7 @@ var ue = {
|
|
|
271
271
|
}, o, a, s), h;
|
|
272
272
|
}
|
|
273
273
|
async executeDelete(e, t, n, r, a) {
|
|
274
|
-
let o = this.sql, s = o`DELETE FROM ${o(this.nsT(e))} OUTPUT DELETED.* WHERE ${o.and(
|
|
274
|
+
let o = this.sql, s = o`DELETE FROM ${o(this.nsT(e))} OUTPUT DELETED.* WHERE ${o.and(A(t).map(([e, t]) => o`${o(e)} = ${t}`))}`, c = n ? i.provideService(s, I, n) : s, l = (await this.runtime.runPromise(c))[0];
|
|
275
275
|
if (!l) return !1;
|
|
276
276
|
let u = this.decodeWritten(e, l);
|
|
277
277
|
return await this.routeEvent({
|
|
@@ -283,17 +283,17 @@ var ue = {
|
|
|
283
283
|
}
|
|
284
284
|
async appendInTxn(e, t, n) {
|
|
285
285
|
let r = this.sql, a = r`INSERT INTO ${r(this.nsT(e))} ${r.insert(T(t, e))}`;
|
|
286
|
-
await this.runtime.runPromise(n ? i.provideService(a,
|
|
286
|
+
await this.runtime.runPromise(n ? i.provideService(a, I, n) : a);
|
|
287
287
|
}
|
|
288
288
|
async maxInTxn(e, t, n, r) {
|
|
289
|
-
let a = this.sql, o = Object.entries(n).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(r ? i.provideService(s,
|
|
289
|
+
let a = this.sql, o = Object.entries(n).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(r ? i.provideService(s, I, r) : s))[0]?.m;
|
|
290
290
|
return c == null ? null : Number(c);
|
|
291
291
|
}
|
|
292
292
|
async routeEvent(e, t, n = null, r) {
|
|
293
293
|
e = {
|
|
294
294
|
...h(r),
|
|
295
295
|
...e
|
|
296
|
-
},
|
|
296
|
+
}, k(e.table) && await ue({
|
|
297
297
|
append: (e, t) => this.appendInTxn(e, t, n),
|
|
298
298
|
maxOf: (e, t, r) => this.maxInTxn(e, t, r, n)
|
|
299
299
|
}, {
|
|
@@ -312,14 +312,14 @@ var ue = {
|
|
|
312
312
|
return this.rawOn(e, null);
|
|
313
313
|
}
|
|
314
314
|
rawOn(e, t) {
|
|
315
|
-
let n = S(e, this.sql), r = t ? i.provideService(n,
|
|
315
|
+
let n = S(e, this.sql), r = t ? i.provideService(n, I, t) : n;
|
|
316
316
|
return O("mssql", "raw", () => this.runtime.runPromise(r));
|
|
317
317
|
}
|
|
318
318
|
async runWithEager(e, t) {
|
|
319
319
|
if (!ee(e)) return this.executeQuery(e, t);
|
|
320
320
|
let n = this.namespace === null ? y(e, this.sql, "mssql") : null;
|
|
321
321
|
if (n !== null) try {
|
|
322
|
-
let e = t ? i.provideService(n.fragment,
|
|
322
|
+
let e = t ? i.provideService(n.fragment, I, t) : n.fragment, r = await O("mssql", "select", () => this.runtime.runPromise(e));
|
|
323
323
|
return n.decode(r);
|
|
324
324
|
} catch (t) {
|
|
325
325
|
if (t instanceof d) throw t;
|
|
@@ -336,17 +336,17 @@ var ue = {
|
|
|
336
336
|
reason: "not-compilable"
|
|
337
337
|
});
|
|
338
338
|
let r = await this.executeQuery(e, t);
|
|
339
|
-
return m(r, e.eager, e.sourceTable ??
|
|
339
|
+
return m(r, e.eager, e.sourceTable ?? se(e.table), (e) => this.executeQuery(e, t));
|
|
340
340
|
}
|
|
341
341
|
getInternalRunWithEager() {
|
|
342
342
|
return this.runWithEager.bind(this);
|
|
343
343
|
}
|
|
344
344
|
localWrite(e, t, n) {
|
|
345
|
-
let r = (e) =>
|
|
346
|
-
return O("mssql", e, () =>
|
|
345
|
+
let r = (e) => k(t) ? this.writeWithRecorders(n, e) : n(e, null, null);
|
|
346
|
+
return O("mssql", e, () => fe(r));
|
|
347
347
|
}
|
|
348
348
|
async writeWithRecorders(e, t) {
|
|
349
|
-
let n = [], r = this.sql, a = await this.runtime.runPromise(r.withTransaction(i.flatMap(i.serviceOption(
|
|
349
|
+
let n = [], r = this.sql, a = await this.runtime.runPromise(r.withTransaction(i.flatMap(i.serviceOption(I), (r) => s.isNone(r) ? i.fail(/* @__PURE__ */ Error("MssqlStore.write: TransactionConnection missing.")) : i.tryPromise({
|
|
350
350
|
try: () => e(t, r.value, n),
|
|
351
351
|
catch: (e) => e
|
|
352
352
|
}))));
|
|
@@ -375,7 +375,7 @@ var ue = {
|
|
|
375
375
|
return this.executeDeleteMany(e, t, null, null);
|
|
376
376
|
}
|
|
377
377
|
async executeUpdateMany(e, t, n, r, a, o) {
|
|
378
|
-
let s = this.sql, c = x(n.where, s, this.namespace), l = s`UPDATE ${s(this.nsT(e))} SET ${s.update(T(t, e))} OUTPUT INSERTED.* WHERE ${c}`, u = r ? i.provideService(l,
|
|
378
|
+
let s = this.sql, c = x(n.where, s, this.namespace), l = s`UPDATE ${s(this.nsT(e))} SET ${s.update(T(t, e))} OUTPUT INSERTED.* WHERE ${c}`, u = r ? i.provideService(l, I, r) : l, d = await O("mssql", "update", () => this.runtime.runPromise(u));
|
|
379
379
|
for (let t of w(d, e, "mssql")) await this.routeEvent({
|
|
380
380
|
table: e,
|
|
381
381
|
op: "update",
|
|
@@ -385,7 +385,7 @@ var ue = {
|
|
|
385
385
|
return d.length;
|
|
386
386
|
}
|
|
387
387
|
async executeDeleteMany(e, t, n, r, a) {
|
|
388
|
-
let o = this.sql, s = x(t.where, o, this.namespace), c = o`DELETE FROM ${o(this.nsT(e))} OUTPUT DELETED.* WHERE ${s}`, l = n ? i.provideService(c,
|
|
388
|
+
let o = this.sql, s = x(t.where, o, this.namespace), c = o`DELETE FROM ${o(this.nsT(e))} OUTPUT DELETED.* WHERE ${s}`, l = n ? i.provideService(c, I, n) : c, u = await O("mssql", "delete", () => this.runtime.runPromise(l));
|
|
389
389
|
for (let t of w(u, e, "mssql")) await this.routeEvent({
|
|
390
390
|
table: e,
|
|
391
391
|
op: "delete",
|
|
@@ -404,24 +404,24 @@ var ue = {
|
|
|
404
404
|
return this.localWrite("upsert", e, (r, i, a) => this.executeInsertIgnore(e, t, n, i, a, r));
|
|
405
405
|
}
|
|
406
406
|
async executeUpsert(e, t, n, r, i, a) {
|
|
407
|
-
if (
|
|
407
|
+
if (N(n), t = j(e, t), typeof n.update != "function" && n.conflictColumns.length > 0) return this.mergeUpsert(e, t, {
|
|
408
408
|
conflictColumns: n.conflictColumns,
|
|
409
409
|
...n.update === void 0 ? {} : { update: n.update },
|
|
410
410
|
...n.updateValues === void 0 ? {} : { updateValues: n.updateValues }
|
|
411
411
|
}, r, i, a);
|
|
412
412
|
let o = await this.findByConflict(e, t, n.conflictColumns, r);
|
|
413
|
-
if (o) return await this.executeUpdate(e, o.id,
|
|
413
|
+
if (o) return await this.executeUpdate(e, o.id, M(t, o, n), r, i, a) ?? o;
|
|
414
414
|
try {
|
|
415
415
|
return await this.executeInsert(e, t, r, i, a);
|
|
416
416
|
} catch (o) {
|
|
417
|
-
if (!
|
|
417
|
+
if (!W(o)) throw o;
|
|
418
418
|
let s = await this.findByConflict(e, t, n.conflictColumns, r);
|
|
419
419
|
if (!s) throw o;
|
|
420
|
-
return await this.executeUpdate(e, s.id,
|
|
420
|
+
return await this.executeUpdate(e, s.id, M(t, s, n), r, i, a) ?? s;
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
423
|
async mergeUpsert(e, t, n, r, a, o) {
|
|
424
|
-
let s = this.sql, c = Object.keys(t), l = Array.isArray(n.update) ? n.update.filter((e) => c.includes(e)) : c.filter((e) => e !== "id" && !n.conflictColumns.includes(e)), u = T(t, e), d = c.map((e) => s`${u[e]}`), f = c.map((e) => s`${s(e)}`), p = n.conflictColumns.map((e) => s`tgt.${s(e)} = src.${s(e)}`), m = c.map((e) => s`${s(e)}`), h = c.map((e) => s`src.${s(e)}`), g =
|
|
424
|
+
let s = this.sql, c = Object.keys(t), l = Array.isArray(n.update) ? n.update.filter((e) => c.includes(e)) : c.filter((e) => e !== "id" && !n.conflictColumns.includes(e)), u = T(t, e), d = c.map((e) => s`${u[e]}`), f = c.map((e) => s`${s(e)}`), p = n.conflictColumns.map((e) => s`tgt.${s(e)} = src.${s(e)}`), m = c.map((e) => s`${s(e)}`), h = c.map((e) => s`src.${s(e)}`), g = N(n), _ = T(g, e), v = [...l.filter((e) => !Object.hasOwn(g, e)).map((e) => s`tgt.${s(e)} = src.${s(e)}`), ...Object.entries(_).map(([e, t]) => s`tgt.${s(e)} = ${t}`)];
|
|
425
425
|
v.length === 0 && v.push(s`tgt.${s(n.conflictColumns[0])} = src.${s(n.conflictColumns[0])}`);
|
|
426
426
|
let y = s`
|
|
427
427
|
MERGE ${s(this.nsT(e))} WITH (HOLDLOCK) AS tgt
|
|
@@ -429,7 +429,7 @@ var ue = {
|
|
|
429
429
|
ON ${s.and(p)}
|
|
430
430
|
WHEN MATCHED THEN UPDATE SET ${s.csv(v)}
|
|
431
431
|
WHEN NOT MATCHED THEN INSERT (${s.csv(m)}) VALUES (${s.csv(h)})
|
|
432
|
-
OUTPUT $action AS ${s("__action")}, INSERTED.*;`, b = r ? i.provideService(y,
|
|
432
|
+
OUTPUT $action AS ${s("__action")}, INSERTED.*;`, b = r ? i.provideService(y, I, r) : y, x = (await this.runtime.runPromise(b))[0];
|
|
433
433
|
if (!x) throw Error(`MssqlStore.upsert: MERGE returned no row for '${e}'`);
|
|
434
434
|
let S = x.__action, { __action: C, ...w } = x, E = this.decodeWritten(e, w);
|
|
435
435
|
return await this.routeEvent({
|
|
@@ -440,7 +440,7 @@ var ue = {
|
|
|
440
440
|
}, a, r, o), E;
|
|
441
441
|
}
|
|
442
442
|
async executeInsertIgnore(e, t, n, r, i, a) {
|
|
443
|
-
t =
|
|
443
|
+
t = j(e, t);
|
|
444
444
|
let o = await this.findByConflict(e, t, n.conflictColumns, r);
|
|
445
445
|
if (o) return {
|
|
446
446
|
row: o,
|
|
@@ -452,7 +452,7 @@ var ue = {
|
|
|
452
452
|
outcome: "inserted"
|
|
453
453
|
};
|
|
454
454
|
} catch (i) {
|
|
455
|
-
if (!
|
|
455
|
+
if (!W(i)) throw i;
|
|
456
456
|
let a = await this.findByConflict(e, t, n.conflictColumns, r);
|
|
457
457
|
if (!a) throw i;
|
|
458
458
|
return {
|
|
@@ -463,7 +463,7 @@ var ue = {
|
|
|
463
463
|
}
|
|
464
464
|
async findByConflict(e, t, n, r) {
|
|
465
465
|
if (n.length === 0) return;
|
|
466
|
-
let a = this.sql, o = n.map((e) => a`${a(e)} = ${t[e]}`), s = a`SELECT TOP 1 * FROM ${a(this.nsT(e))} WHERE ${a.and(o)}`, c = r ? i.provideService(s,
|
|
466
|
+
let a = this.sql, o = n.map((e) => a`${a(e)} = ${t[e]}`), s = a`SELECT TOP 1 * FROM ${a(this.nsT(e))} WHERE ${a.and(o)}`, c = r ? i.provideService(s, I, r) : s, l = (await this.runtime.runPromise(c))[0];
|
|
467
467
|
return l ? this.decodeWritten(e, l) : void 0;
|
|
468
468
|
}
|
|
469
469
|
emitChange(e) {
|
|
@@ -473,9 +473,9 @@ var ue = {
|
|
|
473
473
|
if (this.cdcHandle) return;
|
|
474
474
|
this.cdcReplicaId = e.replicaId;
|
|
475
475
|
let t = e.includeTables ?? [];
|
|
476
|
-
t.length === 0 &&
|
|
476
|
+
t.length === 0 && X.warn("cdc: no reactive tables to tail; Change Tracking reader idle. Set includeTables to enable.");
|
|
477
477
|
let n = await this.readCdcOffset(e.replicaId);
|
|
478
|
-
this.cdcHandle = await
|
|
478
|
+
this.cdcHandle = await Y({
|
|
479
479
|
run: this.__mssqlReplicationFriend.runEffect,
|
|
480
480
|
tables: t,
|
|
481
481
|
startVersion: n,
|
|
@@ -483,9 +483,9 @@ var ue = {
|
|
|
483
483
|
onVersion: (e) => {
|
|
484
484
|
this.cdcPendingVersion = e;
|
|
485
485
|
},
|
|
486
|
-
onError: (e) =>
|
|
486
|
+
onError: (e) => X.warn("cdc: consumer error", {}, e),
|
|
487
487
|
onResync: () => {
|
|
488
|
-
|
|
488
|
+
X.warn("cdc: resynced to current CT version after retention gap — subscriptions self-heal on the next change");
|
|
489
489
|
}
|
|
490
490
|
}), this.cdcCheckpointTimer = setInterval(() => {
|
|
491
491
|
this.flushCdcOffset();
|
|
@@ -496,12 +496,12 @@ var ue = {
|
|
|
496
496
|
let t = this.sql, n = (await this.runtime.runPromise(t`
|
|
497
497
|
SELECT TOP 1 ${t("ctVersion")} FROM ${t(l)}
|
|
498
498
|
WHERE ${t("id")} = ${e}`))[0]?.ctVersion;
|
|
499
|
-
return n ? (
|
|
499
|
+
return n ? (X.info("cdc: resuming from persisted CT version", {
|
|
500
500
|
replicaId: e,
|
|
501
501
|
version: n
|
|
502
502
|
}), n) : null;
|
|
503
503
|
} catch (t) {
|
|
504
|
-
return
|
|
504
|
+
return X.warn("cdc: no readable offset; starting at current CT version", { replicaId: e }, t), null;
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
async flushCdcOffset() {
|
|
@@ -521,7 +521,7 @@ var ue = {
|
|
|
521
521
|
update: ["ctVersion", "updatedAt"]
|
|
522
522
|
}, null, null);
|
|
523
523
|
} catch (t) {
|
|
524
|
-
this.cdcPendingVersion = e,
|
|
524
|
+
this.cdcPendingVersion = e, X.warn("cdc: checkpoint write failed", {}, t);
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
527
|
}
|
|
@@ -576,16 +576,16 @@ var ue = {
|
|
|
576
576
|
}
|
|
577
577
|
async emptyTablesOn(e, t) {
|
|
578
578
|
if (e.length === 0) return;
|
|
579
|
-
let n = this.sql, r = (e) => this.nsT(e), a = (e, t) => n.unsafe(`ALTER TABLE ${
|
|
579
|
+
let n = this.sql, r = (e) => this.nsT(e), a = (e, t) => n.unsafe(`ALTER TABLE ${ae(r(e), "mssql")} ${t ? "WITH CHECK CHECK" : "NOCHECK"} CONSTRAINT ALL`), o = i.acquireUseRelease(i.forEach(e, (e) => a(e, !1), { discard: !0 }), () => i.forEach(e, (e) => n`DELETE FROM ${n(r(e))}`, { discard: !0 }), () => i.orDie(i.ignore(i.forEach(e, (e) => a(e, !0), { discard: !0 })))), s = t === null ? n.withTransaction(o) : i.provideService(o, I, t);
|
|
580
580
|
await this.runtime.runPromise(s);
|
|
581
581
|
}
|
|
582
582
|
async transactional(e) {
|
|
583
583
|
this.inflightTxns++;
|
|
584
584
|
try {
|
|
585
|
-
return await
|
|
585
|
+
return await le({
|
|
586
586
|
...this.txnSpec("MssqlStore.transactional"),
|
|
587
587
|
work: e,
|
|
588
|
-
makeView: (e, t) => new
|
|
588
|
+
makeView: (e, t) => new ve(this, e, t)
|
|
589
589
|
});
|
|
590
590
|
} finally {
|
|
591
591
|
this.inflightTxns--;
|
|
@@ -597,7 +597,7 @@ var ue = {
|
|
|
597
597
|
dialect: "mssql",
|
|
598
598
|
withTransaction: (e) => this.sql.withTransaction(e),
|
|
599
599
|
runPromiseExit: (e) => this.runtime.runPromiseExit(e),
|
|
600
|
-
isRetryable:
|
|
600
|
+
isRetryable: U,
|
|
601
601
|
span: {
|
|
602
602
|
name: "store.transactional",
|
|
603
603
|
attributes: {
|
|
@@ -629,7 +629,7 @@ var ue = {
|
|
|
629
629
|
if (this.inflightTxns > 0) {
|
|
630
630
|
let t = Date.now() + e;
|
|
631
631
|
for (; this.inflightTxns > 0 && Date.now() < t;) await new Promise((e) => setTimeout(e, 25));
|
|
632
|
-
this.inflightTxns > 0 &&
|
|
632
|
+
this.inflightTxns > 0 && X.warn("close: grace period expired with in-flight transactions", {
|
|
633
633
|
gracePeriodMs: e,
|
|
634
634
|
inflight: this.inflightTxns
|
|
635
635
|
});
|
|
@@ -639,56 +639,69 @@ var ue = {
|
|
|
639
639
|
async ping() {
|
|
640
640
|
await this.runtime.runPromise(this.sql`SELECT 1`);
|
|
641
641
|
}
|
|
642
|
-
},
|
|
642
|
+
}, ve = class {
|
|
643
643
|
parent;
|
|
644
644
|
txn;
|
|
645
645
|
attr;
|
|
646
646
|
events = [];
|
|
647
|
+
aborts = re((e) => U(e));
|
|
647
648
|
committed = !1;
|
|
648
649
|
constructor(e, t, n = void 0) {
|
|
649
650
|
this.parent = e, this.txn = t, this.attr = n;
|
|
650
651
|
}
|
|
652
|
+
savepoint(e) {
|
|
653
|
+
return ce({
|
|
654
|
+
tracker: this.aborts,
|
|
655
|
+
name: `voltro_sp_${++this.savepointCounter}`,
|
|
656
|
+
spelling: "mssql",
|
|
657
|
+
exec: (e) => this.parent.run(i.provideService(F.pipe(i.flatMap((t) => t.unsafe(e))), I, this.txn))
|
|
658
|
+
}, e);
|
|
659
|
+
}
|
|
660
|
+
savepointCounter = 0;
|
|
651
661
|
run(e) {
|
|
652
|
-
return this.parent.run(i.provideService(e,
|
|
662
|
+
return this.aborts.track(() => this.parent.run(i.provideService(e, I, this.txn)));
|
|
663
|
+
}
|
|
664
|
+
abortedBy() {
|
|
665
|
+
return this.aborts.abortedBy();
|
|
653
666
|
}
|
|
654
667
|
raw(e, t) {
|
|
655
|
-
return this.parent.rawOn(e, this.txn);
|
|
668
|
+
return this.aborts.track(() => this.parent.rawOn(e, this.txn));
|
|
656
669
|
}
|
|
657
670
|
query(e) {
|
|
658
|
-
return this.parent.getInternalRunWithEager()(e, this.txn);
|
|
671
|
+
return this.aborts.track(() => this.parent.getInternalRunWithEager()(e, this.txn));
|
|
659
672
|
}
|
|
660
673
|
emptyTables(e) {
|
|
661
|
-
return this.parent.emptyTablesOn(e, this.txn);
|
|
674
|
+
return this.aborts.track(() => this.parent.emptyTablesOn(e, this.txn));
|
|
662
675
|
}
|
|
663
676
|
insert(e, t) {
|
|
664
|
-
return this.parent.getInternalExecuteInsert()(e, t, this.txn, this.events, this.attr);
|
|
677
|
+
return this.aborts.track(() => this.parent.getInternalExecuteInsert()(e, t, this.txn, this.events, this.attr));
|
|
665
678
|
}
|
|
666
679
|
insertMany(e, t) {
|
|
667
|
-
return this.parent.getInternalExecuteInsertMany()(e, t, this.txn, this.events, this.attr);
|
|
680
|
+
return this.aborts.track(() => this.parent.getInternalExecuteInsertMany()(e, t, this.txn, this.events, this.attr));
|
|
668
681
|
}
|
|
669
682
|
patchJson(e, t, n, r, i) {
|
|
670
|
-
return this.parent.getInternalExecutePatchJson()(e, t, n, r, this.txn, this.events, this.attr, i);
|
|
683
|
+
return this.aborts.track(() => this.parent.getInternalExecutePatchJson()(e, t, n, r, this.txn, this.events, this.attr, i));
|
|
671
684
|
}
|
|
672
685
|
update(e, t, n) {
|
|
673
|
-
return this.parent.getInternalExecuteUpdate()(e, t, n, this.txn, this.events, this.attr);
|
|
686
|
+
return this.aborts.track(() => this.parent.getInternalExecuteUpdate()(e, t, n, this.txn, this.events, this.attr));
|
|
674
687
|
}
|
|
675
688
|
delete(e, t) {
|
|
676
|
-
return this.parent.getInternalExecuteDelete()(e, t, this.txn, this.events, this.attr);
|
|
689
|
+
return this.aborts.track(() => this.parent.getInternalExecuteDelete()(e, t, this.txn, this.events, this.attr));
|
|
677
690
|
}
|
|
678
691
|
updateMany(e, t, n) {
|
|
679
|
-
return this.parent.getInternalExecuteUpdateMany()(e, t, n, this.txn, this.events, this.attr);
|
|
692
|
+
return this.aborts.track(() => this.parent.getInternalExecuteUpdateMany()(e, t, n, this.txn, this.events, this.attr));
|
|
680
693
|
}
|
|
681
694
|
deleteMany(e, t) {
|
|
682
|
-
return this.parent.getInternalExecuteDeleteMany()(e, t, this.txn, this.events, this.attr);
|
|
695
|
+
return this.aborts.track(() => this.parent.getInternalExecuteDeleteMany()(e, t, this.txn, this.events, this.attr));
|
|
683
696
|
}
|
|
684
697
|
upsert(e, t, n) {
|
|
685
|
-
return this.parent.getInternalExecuteUpsert()(e, t, n, this.txn, this.events, this.attr);
|
|
698
|
+
return this.aborts.track(() => this.parent.getInternalExecuteUpsert()(e, t, n, this.txn, this.events, this.attr));
|
|
686
699
|
}
|
|
687
700
|
insertIgnore(e, t, n) {
|
|
688
701
|
return this.insertIgnoreWithOutcome(e, t, n).then((e) => e.row);
|
|
689
702
|
}
|
|
690
703
|
insertIgnoreWithOutcome(e, t, n) {
|
|
691
|
-
return this.parent.getInternalExecuteInsertIgnore()(e, t, n, this.txn, this.events, this.attr);
|
|
704
|
+
return this.aborts.track(() => this.parent.getInternalExecuteInsertIgnore()(e, t, n, this.txn, this.events, this.attr));
|
|
692
705
|
}
|
|
693
706
|
transactional(e) {
|
|
694
707
|
return Promise.reject(/* @__PURE__ */ Error("MssqlStore.transactional: nested transactions are not supported."));
|
|
@@ -706,12 +719,12 @@ var ue = {
|
|
|
706
719
|
this.events.length = 0;
|
|
707
720
|
}
|
|
708
721
|
}
|
|
709
|
-
}, $ = (e) => e.__mssqlReplicationFriend ?? null,
|
|
722
|
+
}, $ = (e) => e.__mssqlReplicationFriend ?? null, ye = (e, t) => e === t ? 0 : e < t ? -1 : 1, be = () => ({
|
|
710
723
|
async capturePrimaryPosition(e) {
|
|
711
724
|
let t = $(e);
|
|
712
725
|
if (t === null) throw Error("mssqlReplicationAdapter: primary is not an MssqlStore (missing __mssqlReplicationFriend).");
|
|
713
726
|
return t.runEffect(i.gen(function* () {
|
|
714
|
-
return (yield* (yield*
|
|
727
|
+
return (yield* (yield* F)`
|
|
715
728
|
SELECT CONVERT(VARCHAR(40), end_of_log_lsn) AS lsn
|
|
716
729
|
FROM sys.dm_hadr_database_replica_states
|
|
717
730
|
WHERE database_id = DB_ID()
|
|
@@ -724,7 +737,7 @@ var ue = {
|
|
|
724
737
|
let t = $(e);
|
|
725
738
|
if (t === null) throw Error("mssqlReplicationAdapter: replica is not an MssqlStore.");
|
|
726
739
|
return t.runEffect(i.gen(function* () {
|
|
727
|
-
return (yield* (yield*
|
|
740
|
+
return (yield* (yield* F)`
|
|
728
741
|
SELECT CONVERT(VARCHAR(40), last_hardened_lsn) AS lsn
|
|
729
742
|
FROM sys.dm_hadr_database_replica_states
|
|
730
743
|
WHERE database_id = DB_ID()
|
|
@@ -733,14 +746,14 @@ var ue = {
|
|
|
733
746
|
}));
|
|
734
747
|
},
|
|
735
748
|
compare(e, t) {
|
|
736
|
-
return
|
|
749
|
+
return ye(t, e) >= 0 ? "caught-up" : "behind";
|
|
737
750
|
}
|
|
738
|
-
}),
|
|
751
|
+
}), xe = {
|
|
739
752
|
id: "mssql",
|
|
740
|
-
makeSqlLayer: (e) =>
|
|
741
|
-
makeStore: (e) =>
|
|
753
|
+
makeSqlLayer: (e) => V(e),
|
|
754
|
+
makeStore: (e) => Z(e),
|
|
742
755
|
compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_[\]]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
|
|
743
|
-
retryFilter:
|
|
756
|
+
retryFilter: G
|
|
744
757
|
};
|
|
745
758
|
//#endregion
|
|
746
|
-
export { l as CDC_OFFSETS_TABLE, e as MssqlClient, p as _voltroMssqlCdcOffsetsTable,
|
|
759
|
+
export { l as CDC_OFFSETS_TABLE, e as MssqlClient, p as _voltroMssqlCdcOffsetsTable, B as connectionFromConfig, K as ensureChangeTracking, z as makeMssqlSqlLayer, V as makeMssqlSqlLayerFromConfig, Z as makeMssqlStore, xe as mssqlDialect, be as mssqlReplicationAdapter, G as mssqlRetryFilter, Y as startChangeTrackingCdc };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/sql-mssql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.0",
|
|
4
4
|
"description": "SQL Server (mssql) dialect adapter for Voltro's cross-dialect DataStore (OUTPUT-clause returning, OFFSET/FETCH paging, inline reactivity).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@effect/sql": "^0.52.1",
|
|
37
37
|
"@effect/sql-mssql": "^0.54.0",
|
|
38
|
-
"@voltro/database": "0.
|
|
39
|
-
"@voltro/logger": "0.
|
|
38
|
+
"@voltro/database": "0.75.0",
|
|
39
|
+
"@voltro/logger": "0.75.0",
|
|
40
40
|
"tedious": "18.6.2"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|