@voltro/sql-mssql 0.10.0 → 0.11.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 +84 -0
- package/dist/index.js +51 -49
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,90 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.11.0] — 2026-07-22
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/client, @voltro/web** — **@voltro/client** — `useSequence` steps gain **`covers`** and **`when`**, and the undo contract that was implicit is now written down.
|
|
47
|
+
|
|
48
|
+
**`covers` — overlapping undos.** Every succeeded step's undo runs, and the runner has no idea whether two of them reverse the same thing. Reported from a Jira rollback: `deleteJiraDraftTicket` deletes the issue *and* discards the draft, so the earlier `discardDraft` undo ran on something already gone. It worked only because discarding is idempotent — and **nothing said that was load-bearing**. For a refund or a cancellation email the double-run is a defect, not a nuisance.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
.step('draft', createDraft, { undo: discardDraft })
|
|
52
|
+
.step('jira', createTicket, { undo: deleteJiraDraftTicket, covers: ['draft'] })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`covers` is static rather than a runtime signal on purpose: "this reverse also reverses that one" is a property of the operation, legible where it is defined and checkable against the step names in scope.
|
|
56
|
+
|
|
57
|
+
**When the covering undo FAILS**, the covered steps are neither run nor claimed. Whether the cascade got that far is genuinely unknown — running the covered undo risks the double reverse, skipping it risks an orphan — so both guesses are refused and the steps come back in `compensationUncertain` with the step that was supposed to cover them. Same principle as never compensating the step that failed: surface the ambiguity, don't resolve it by assumption.
|
|
58
|
+
|
|
59
|
+
**`when` — one optional step.** 13 multi-await blocks in one app; only 5 could migrate. Several of the rest were linear *except* for one conditional step ("schedule the summary only if the set changed", `if (assigneeKey) assign else unassign`) and fell back to `try/catch` entirely, though 80% of the flow was a clean pipeline. An optional step is a different shape from a loop, and the scoping was treating them the same.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
.step('summary', (c) => scheduleSummary.run({ id: c.save.id }), { when: (c) => c.save.changed })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A skipped step contributes `undefined` to the context — the overload says so, so a later step has to acknowledge it — and gets no undo, since it had no effect to reverse. Loops and real branches still keep their `try/catch`; this deliberately does not widen to them.
|
|
66
|
+
|
|
67
|
+
**The break:** `StepOptions` gained a type parameter — `StepOptions<Result>` became `StepOptions<Ctx, Result>`, because `covers` and `when` both need to see the accumulated context (`covers` is checked against the step names in scope; `when` receives it). Call sites that pass an object literal to `.step()` are unaffected — the type is inferred — but anyone who *named* the type explicitly gets a compile error. Filed BREAKING rather than Added for the same reason a widened union is: the test is "can this turn code that compiled into code that does not", and it can. `@voltro/web` is listed because it re-exports the client surface — the third time in this round that coupling has decided where a change lands.
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
- **@voltro/cli** — **Repo gate** — a new `Claimed-wiring check` (`scripts/check-claimed-wirings.mjs`, wired into CI and therefore into `pnpm gate`): a doc comment that says something wires a symbol up must be telling the truth.
|
|
72
|
+
|
|
73
|
+
`setSystemStoreHandle`'s comment read *"Process-wide handle, registered by the runtime boot (dev.ts / start.ts)"*. Nothing registered it, in either path, through an entire release — so `runAsSystem` threw for every consumer, and the comment was the only evidence anyone had that it should work. The shape is not rare: a comment gets written when the wiring is planned, the wiring gets deferred, and the comment never finds out. It then reads as documentation of behaviour rather than of intention, and the more confidently it is phrased the less likely anyone is to check it.
|
|
74
|
+
|
|
75
|
+
Two things it does that the obvious version does not, both learned by watching it report the bug as clean:
|
|
76
|
+
|
|
77
|
+
- **it counts real call expressions, not text.** The first version matched regexes and found `setSystemStoreHandle({ … })` inside `runAsSystem`'s own error-message string — a text match cannot tell a call from a sentence about a call. (Precisely the defect fixed in the hand-roll detector one commit earlier, repeated one file later.) - **it checks the NAMED caller, not any caller.** The second version asked "does anything call this"; `@voltro/testing` calls it from a test harness, so the bug read clean again. A claim that the runtime boot registers something is not satisfied by a test helper registering it.
|
|
78
|
+
|
|
79
|
+
Verified the only way this kind of check can be: by removing the wiring and confirming it fails, with the diagnosis that would have saved the original investigation — `called by: packages/testing/src/testContext.ts ← none of these is the boot`.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **@voltro/database, @voltro/runtime, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — **@voltro/database + every store** — a row inserted into a table that declares an `id()` scheme now gets one **at the store**, not only when the caller happened to go through `wrapStoreWithMixinBehaviour`.
|
|
84
|
+
|
|
85
|
+
Id generation was sitting one layer too high. It is the single stamped field that needs no subject — the scheme is a property of the declared table — yet it lived in the subject-aware wrapper. So every insert through an unwrapped store reached the database with `id: null`:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
null value in column "id" of relation "_voltro_seeds" violates not-null constraint
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
That was a real shipped bug in the seed ledger (the runner holds the raw store), and a survey found the same shape waiting elsewhere: `plugin-rbac/userRoleStore.ts`, `plugin-governance/consent.ts`, and `plugin-sso-saml/saml-cache.ts` all insert without an id into tables that declare one. Whether any of them broke came down to how their caller happened to obtain its store — and *"depends on how the caller obtained its store"* is not a contract, it is a coin flip with a NOT NULL constraint on the other side.
|
|
92
|
+
|
|
93
|
+
`stampGeneratedId` now runs in every store's insert path (all four dialects plus the in-memory store, at the private `executeInsert` / `executeInsertMany` / `executeInsertIgnore` choke points that every public and namespace-view path funnels through). It preserves the semantics the wrapper had: an explicit id is never overwritten, a `numeric` scheme deletes the key so the dialect's SERIAL fires, and an unregistered table is passed through untouched rather than guessed at.
|
|
94
|
+
|
|
95
|
+
The wrapper still stamps — it holds the schema registry and does the subject-derived fields in the same pass — and now finds the id already set. That is a floor, not a second implementation of a rule: the invariant is "a row that reaches the database has an id when its table declares a generating scheme", and only the store can promise that for *every* caller, including a `DataStore` someone implemented themselves.
|
|
96
|
+
- **@voltro/cli** — **Repo tests** — the liveness backstop for real-listener / real-child-process suites goes from 60s to 120s, in `vitest.config.ts` and the CI flag that overrides it.
|
|
97
|
+
|
|
98
|
+
Worth stating plainly what this number is, because raising a timeout is the classic way to bury a problem: it asserts nothing about performance. A real `serveApi` boot is ~0.2s in isolation. The value is pure headroom against residual starvation that the four existing mitigations — the unit/integration project split, group ordering, `fileParallelism: false`, per-package mssql databases — cannot reach, because the remaining contention is turbo running two *packages* concurrently alongside the docker stack.
|
|
99
|
+
|
|
100
|
+
60s tripped twice in one session, on two different files (`connectionServe.test.ts`, then `serveApi.test.ts`), each passing in ~2s alone. Different file each time, always in the same family, never reproducible in isolation: that is the signature of starvation rather than of a slow test, and it cost two full gate runs.
|
|
101
|
+
|
|
102
|
+
What would not be honest is treating a green run afterwards as evidence the contention is gone. It is not. If a third file trips this, the answer is to stop running two heavy packages concurrently — not to raise it again.
|
|
103
|
+
- **@voltro/cli, @voltro/database** — **@voltro/cli, @voltro/database** — two fixes to the 0.10.0 seed ledger, both reported from its first real use, both mine.
|
|
104
|
+
|
|
105
|
+
**The ledger never wrote.** `store.insert('_voltro_seeds', …)` ran against the RAW store, and auto-id lives in `wrapStoreWithMixinBehaviour` — so the row reached postgres with `id: null` and died on the NOT NULL constraint. Every boot reported `ran=1 skipped=0` regardless of fingerprint: the feature shipped doing nothing. The ledger now stamps its own id, derived from `_voltroSeedsTable`'s declared scheme rather than a hardcoded prefix, so it no longer depends on how a caller happens to have wrapped its store.
|
|
106
|
+
|
|
107
|
+
**Worse than the bug was the logging.** I put the failure on `debug` and swallowed it, reasoning that "it degrades to re-running, which is only a performance regression". That reasoning is exactly what made it undiagnosable: a silently unwritten ledger looks identical to a working one whose seeds all changed — no symptom, nothing to grep, and the reporter had to read the error out of a debug stream to find it. Both the write and the read path now warn, naming the table and the consequence.
|
|
108
|
+
|
|
109
|
+
**And the reason it shipped:** the test fake *invented an id* when the row lacked one, making it more permissive than any database. A fake that supplies what the subject under test forgot is not a test — it is the subject testing itself. It now rejects an id-less insert with the real constraint's message; reintroducing the bug fails five cases.
|
|
110
|
+
|
|
111
|
+
**A seed step could not reach a usable store.** `SeedStore` exposed only `query`/`insert`/`update`/`delete`, and `query` took just `{ table, predicate }`. A restore of 1361 rows across 167 tables, multi-pass for FK order, needs an idempotent insert and a read it can page — and `upsertByUnique` is neither: a read per row, and it overwrites what it finds, which is wrong whenever the live row is newer than the snapshot. `SeedStore` now carries **`insertIgnore`** (one statement per row, native on every dialect) and a full descriptor read (`order` / `take` / `skip` / `projection`).
|
|
112
|
+
|
|
113
|
+
Seed reads are unscoped and include soft-deleted rows *by construction* — a seed runs at boot with no request and therefore no subject, so nothing applies a tenant filter or the `deletedAt IS NULL` predicate. That is now documented on the type rather than left to be discovered, since the absence of `.unscoped()` / `.withDeleted()` reads as a missing feature until you know why they cannot exist here.
|
|
114
|
+
|
|
115
|
+
*(`apiSurface: compatible`: the golden churn in `@voltro/database` is two `(undocumented)` markers disappearing because `SeedStore` and its new member gained TSDoc — a comment cannot break a caller. The added `insertIgnore` member is additive for consumers, which is everyone: a `SeedStore` is what `ctx.store` IS, handed to you by the runner. Nobody constructs one, so nobody can be missing a member.)*
|
|
116
|
+
- **@voltro/cli** — **@voltro/cli** — `runAsSystem` now works. The process-wide system store is registered at boot by **both** `voltro dev` and `voltro serve`; until now neither did, so every call threw `no data store available — register one at boot via setSystemStoreHandle`.
|
|
117
|
+
|
|
118
|
+
`setSystemStoreHandle`'s own doc comment reads "registered by the runtime boot (dev.ts / start.ts)". It describes wiring that was never written: the only callers in the repo were its unit test and a note in `@voltro/testing`. So the failure was not a lifecycle-ordering subtlety — the handle was never set at any point, in any command, and `runAsSystem` was unusable for every consumer.
|
|
119
|
+
|
|
120
|
+
Surfaced by someone reporting it as "not registered *yet* at seed time", which implied it worked later. Checking that framing rather than the symptom is what turned a scheduling question into a missing-wiring one. It is registered before the seed runner in dev, since seeds are the earliest thing that can plausibly want it.
|
|
121
|
+
|
|
122
|
+
Same class as the seed lifecycle table and the `@voltro/web` re-export: a documented behaviour with nothing behind it, where the doc is the only evidence anyone has.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
42
126
|
## [0.10.0] — 2026-07-22
|
|
43
127
|
|
|
44
128
|
### ⚠ BREAKING
|
package/dist/index.js
CHANGED
|
@@ -3,14 +3,14 @@ import { Config as t, Effect as n, Layer as r, ManagedRuntime as i, Option as a,
|
|
|
3
3
|
import { EventEmitter as c } from "node:events";
|
|
4
4
|
import { createLogger as l } from "@voltro/logger";
|
|
5
5
|
import { SqlClient as u, TransactionConnection as d } from "@effect/sql/SqlClient";
|
|
6
|
-
import { CDC_OFFSETS_TABLE as f, EagerCardinalityError as p, _voltroMssqlCdcOffsetsTable as m, attachEagerLoads as h, compileEagerJson as g, compilePredicate as _, compileRawFragment as v, compileSelect as y, decodeRowsFromSchema as b, hasEagerLoads as x, qualifyTable as S, raiseChangeListenerCeiling as C, requireTable as w } from "@voltro/database";
|
|
6
|
+
import { CDC_OFFSETS_TABLE as f, EagerCardinalityError as p, _voltroMssqlCdcOffsetsTable as m, attachEagerLoads as h, compileEagerJson as g, compilePredicate as _, compileRawFragment as v, compileSelect as y, decodeRowsFromSchema as b, hasEagerLoads as x, qualifyTable as S, raiseChangeListenerCeiling as C, requireTable as w, stampGeneratedId as T, stampGeneratedIds as E } from "@voltro/database";
|
|
7
7
|
//#region src/sqlLayer.ts
|
|
8
|
-
var
|
|
8
|
+
var D = {
|
|
9
9
|
...e.defaultParameterTypes,
|
|
10
10
|
null: e.defaultParameterTypes.object
|
|
11
|
-
},
|
|
11
|
+
}, O = (n) => e.layerConfig({
|
|
12
12
|
server: t.succeed(n.server),
|
|
13
|
-
parameterTypes: t.succeed(
|
|
13
|
+
parameterTypes: t.succeed(D),
|
|
14
14
|
...n.port === void 0 ? {} : { port: t.succeed(n.port) },
|
|
15
15
|
...n.database === void 0 ? {} : { database: t.succeed(n.database) },
|
|
16
16
|
...n.username === void 0 ? {} : { username: t.succeed(n.username) },
|
|
@@ -18,7 +18,7 @@ var T = {
|
|
|
18
18
|
...n.encrypt === void 0 ? {} : { encrypt: t.succeed(n.encrypt) },
|
|
19
19
|
...n.trustServer === void 0 ? {} : { trustServer: t.succeed(n.trustServer) },
|
|
20
20
|
...n.maxConnections === void 0 ? {} : { maxConnections: t.succeed(n.maxConnections) }
|
|
21
|
-
}),
|
|
21
|
+
}), k = (e) => {
|
|
22
22
|
if (e.url) {
|
|
23
23
|
let t = new URL(e.url);
|
|
24
24
|
return {
|
|
@@ -40,7 +40,7 @@ var T = {
|
|
|
40
40
|
trustServer: !0,
|
|
41
41
|
...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections }
|
|
42
42
|
};
|
|
43
|
-
},
|
|
43
|
+
}, A = (e) => O(k(e)), j = /* @__PURE__ */ new Set(["1205"]), M = (e) => {
|
|
44
44
|
let t = e;
|
|
45
45
|
for (let e = 0; e < 5 && typeof t == "object" && t; e++) {
|
|
46
46
|
let e = t.number;
|
|
@@ -49,17 +49,17 @@ var T = {
|
|
|
49
49
|
if (typeof n == "string") return n;
|
|
50
50
|
t = t.cause;
|
|
51
51
|
}
|
|
52
|
-
},
|
|
53
|
-
let t =
|
|
54
|
-
return t !== void 0 &&
|
|
55
|
-
},
|
|
56
|
-
let t =
|
|
57
|
-
return t !== void 0 &&
|
|
58
|
-
},
|
|
52
|
+
}, N = (e) => {
|
|
53
|
+
let t = M(e);
|
|
54
|
+
return t !== void 0 && j.has(t);
|
|
55
|
+
}, P = /* @__PURE__ */ new Set(["2601", "2627"]), F = (e) => {
|
|
56
|
+
let t = M(e);
|
|
57
|
+
return t !== void 0 && P.has(t);
|
|
58
|
+
}, I = (e) => N(e) ? "retry" : "noRetry", L = {
|
|
59
59
|
I: "insert",
|
|
60
60
|
U: "update",
|
|
61
61
|
D: "delete"
|
|
62
|
-
},
|
|
62
|
+
}, R = (e, t) => e(n.gen(function* () {
|
|
63
63
|
let e = yield* u;
|
|
64
64
|
if (((yield* e`
|
|
65
65
|
SELECT COUNT(*) AS ${e("on")}
|
|
@@ -67,18 +67,18 @@ var T = {
|
|
|
67
67
|
WHERE database_id = DB_ID()`)[0]?.on ?? 0) === 0) return yield* n.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."));
|
|
68
68
|
for (let n of t) yield* e.unsafe(`IF NOT EXISTS (
|
|
69
69
|
SELECT 1 FROM sys.change_tracking_tables
|
|
70
|
-
WHERE object_id = OBJECT_ID(${
|
|
70
|
+
WHERE object_id = OBJECT_ID(${z(n)})
|
|
71
71
|
)
|
|
72
|
-
ALTER TABLE ${
|
|
73
|
-
})).then(() => void 0),
|
|
72
|
+
ALTER TABLE ${B(n)} ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = OFF)`);
|
|
73
|
+
})).then(() => void 0), z = (e) => `'${e.replace(/'/g, "''")}'`, B = (e) => `[${e.replace(/\]/g, "]]")}]`, V = async (e) => {
|
|
74
74
|
let t = l({ scope: "voltro:mssql:cdc" }), r = e.pollIntervalMs ?? 500;
|
|
75
|
-
await
|
|
75
|
+
await R(e.run, e.tables);
|
|
76
76
|
let i = async () => (await e.run(n.gen(function* () {
|
|
77
77
|
return yield* (yield* u)`SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_CURRENT_VERSION()) AS v`;
|
|
78
78
|
})))[0]?.v ?? "0", a = e.startVersion ?? await i(), o = !1, s = null, c = !1, d = (e, t) => BigInt(e) > BigInt(t), f = async (r, o) => {
|
|
79
79
|
let s = await e.run(n.gen(function* () {
|
|
80
|
-
let e = yield* u, t = e.unsafe(
|
|
81
|
-
SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(${e.unsafe(
|
|
80
|
+
let e = yield* u, t = e.unsafe(B(r)), n = (yield* e`
|
|
81
|
+
SELECT CONVERT(VARCHAR(40), CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(${e.unsafe(z(r))}))) AS floor`)[0]?.floor;
|
|
82
82
|
return n != null && BigInt(o) < BigInt(n) ? {
|
|
83
83
|
resync: !0,
|
|
84
84
|
rows: []
|
|
@@ -96,7 +96,7 @@ var T = {
|
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
for (let t of s.rows) {
|
|
99
|
-
let n =
|
|
99
|
+
let n = L[t.__ct_op];
|
|
100
100
|
if (!n) continue;
|
|
101
101
|
let { __ct_op: i, __ct_id: a, ...o } = t;
|
|
102
102
|
if (n === "delete") e.onChange({
|
|
@@ -147,12 +147,12 @@ var T = {
|
|
|
147
147
|
},
|
|
148
148
|
currentVersion: () => a
|
|
149
149
|
};
|
|
150
|
-
},
|
|
150
|
+
}, H = l({ scope: "voltro:mssql" }), U = async (e) => {
|
|
151
151
|
let t = e.changeStrategy ?? "inline", n = t;
|
|
152
|
-
t === "cdc" && !e.cdcConfig && (
|
|
153
|
-
let a = e.tracerLayer ? r.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, o = i.make(a), s = new
|
|
152
|
+
t === "cdc" && !e.cdcConfig && (H.warn("changeStrategy='cdc' requires cdcConfig (replicaId + includeTables); falling back to 'inline'."), n = "inline");
|
|
153
|
+
let a = e.tracerLayer ? r.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, o = i.make(a), s = new W(await o.runPromise(u), o, n);
|
|
154
154
|
return n === "cdc" && e.cdcConfig && await s.startCdcConsumer(e.cdcConfig), s;
|
|
155
|
-
},
|
|
155
|
+
}, W = class e {
|
|
156
156
|
sql;
|
|
157
157
|
runtime;
|
|
158
158
|
changeStrategy;
|
|
@@ -182,6 +182,7 @@ var T = {
|
|
|
182
182
|
return b(await this.runtime.runPromise(i), e.table, "mssql");
|
|
183
183
|
}
|
|
184
184
|
async executeInsert(e, t, r, i) {
|
|
185
|
+
t = T(e, t);
|
|
185
186
|
let a = this.sql, o = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(t).returning("*")}`, s = r ? n.provideService(o, d, r) : o, c = (await this.runtime.runPromise(s))[0];
|
|
186
187
|
if (!c) throw Error(`MssqlStore.insert: row not returned post-insert in '${e}'`);
|
|
187
188
|
return this.routeEvent({
|
|
@@ -201,7 +202,7 @@ var T = {
|
|
|
201
202
|
}, a), l) : null;
|
|
202
203
|
}
|
|
203
204
|
async executeInsertMany(e, t, r, i) {
|
|
204
|
-
if (t.length === 0) return [];
|
|
205
|
+
if (t = E(e, t), t.length === 0) return [];
|
|
205
206
|
let a = this.sql, o = a`INSERT INTO ${a(this.nsT(e))} ${a.insert(t).returning("*")}`, s = r ? n.provideService(o, d, r) : o, c = await this.runtime.runPromise(s), l = t.map((e) => e.id), u = new Map(c.map((e) => [e.id, e])), f = l.map((e) => u.get(e)).filter((e) => e !== void 0), p = f.length === c.length ? f : [...c];
|
|
206
207
|
for (let t of p) this.routeEvent({
|
|
207
208
|
table: e,
|
|
@@ -247,7 +248,7 @@ var T = {
|
|
|
247
248
|
return r.decode(i);
|
|
248
249
|
} catch (e) {
|
|
249
250
|
if (e instanceof p) throw e;
|
|
250
|
-
|
|
251
|
+
H.warn("mssql JSON-agg eager-load failed; falling back to walker", { err: e });
|
|
251
252
|
}
|
|
252
253
|
return h(await this.executeQuery(e, t), e.eager, e.sourceTable ?? w(e.table), (e) => this.executeQuery(e, t));
|
|
253
254
|
}
|
|
@@ -316,7 +317,7 @@ var T = {
|
|
|
316
317
|
try {
|
|
317
318
|
return await this.executeInsert(e, t, r, i);
|
|
318
319
|
} catch (a) {
|
|
319
|
-
if (!
|
|
320
|
+
if (!F(a)) throw a;
|
|
320
321
|
let o = await this.findByConflict(e, t, n.conflictColumns, r);
|
|
321
322
|
if (!o) throw a;
|
|
322
323
|
return await this.executeUpdate(e, o.id, this.upsertPatch(t, o, n), r, i) ?? o;
|
|
@@ -340,12 +341,13 @@ var T = {
|
|
|
340
341
|
}, a), x;
|
|
341
342
|
}
|
|
342
343
|
async executeInsertIgnore(e, t, n, r, i) {
|
|
344
|
+
t = T(e, t);
|
|
343
345
|
let a = await this.findByConflict(e, t, n.conflictColumns, r);
|
|
344
346
|
if (a) return a;
|
|
345
347
|
try {
|
|
346
348
|
return await this.executeInsert(e, t, r, i);
|
|
347
349
|
} catch (i) {
|
|
348
|
-
if (!
|
|
350
|
+
if (!F(i)) throw i;
|
|
349
351
|
let a = await this.findByConflict(e, t, n.conflictColumns, r);
|
|
350
352
|
if (!a) throw i;
|
|
351
353
|
return a;
|
|
@@ -363,9 +365,9 @@ var T = {
|
|
|
363
365
|
if (this.cdcHandle) return;
|
|
364
366
|
this.cdcReplicaId = e.replicaId;
|
|
365
367
|
let t = e.includeTables ?? [];
|
|
366
|
-
t.length === 0 &&
|
|
368
|
+
t.length === 0 && H.warn("cdc: no reactive tables to tail; Change Tracking reader idle. Set includeTables to enable.");
|
|
367
369
|
let n = await this.readCdcOffset(e.replicaId);
|
|
368
|
-
this.cdcHandle = await
|
|
370
|
+
this.cdcHandle = await V({
|
|
369
371
|
run: this.__mssqlReplicationFriend.runEffect,
|
|
370
372
|
tables: t,
|
|
371
373
|
startVersion: n,
|
|
@@ -373,9 +375,9 @@ var T = {
|
|
|
373
375
|
onVersion: (e) => {
|
|
374
376
|
this.cdcPendingVersion = e;
|
|
375
377
|
},
|
|
376
|
-
onError: (e) =>
|
|
378
|
+
onError: (e) => H.warn("cdc: consumer error", {}, e),
|
|
377
379
|
onResync: () => {
|
|
378
|
-
|
|
380
|
+
H.warn("cdc: resynced to current CT version after retention gap — subscriptions self-heal on the next change");
|
|
379
381
|
}
|
|
380
382
|
}), this.cdcCheckpointTimer = setInterval(() => {
|
|
381
383
|
this.flushCdcOffset();
|
|
@@ -386,12 +388,12 @@ var T = {
|
|
|
386
388
|
let t = this.sql, n = (await this.runtime.runPromise(t`
|
|
387
389
|
SELECT TOP 1 ${t("ctVersion")} FROM ${t(f)}
|
|
388
390
|
WHERE ${t("id")} = ${e}`))[0]?.ctVersion;
|
|
389
|
-
return n ? (
|
|
391
|
+
return n ? (H.info("cdc: resuming from persisted CT version", {
|
|
390
392
|
replicaId: e,
|
|
391
393
|
version: n
|
|
392
394
|
}), n) : null;
|
|
393
395
|
} catch (t) {
|
|
394
|
-
return
|
|
396
|
+
return H.warn("cdc: no readable offset; starting at current CT version", { replicaId: e }, t), null;
|
|
395
397
|
}
|
|
396
398
|
}
|
|
397
399
|
async flushCdcOffset() {
|
|
@@ -411,7 +413,7 @@ var T = {
|
|
|
411
413
|
update: ["ctVersion", "updatedAt"]
|
|
412
414
|
}, null, null);
|
|
413
415
|
} catch (t) {
|
|
414
|
-
this.cdcPendingVersion = e,
|
|
416
|
+
this.cdcPendingVersion = e, H.warn("cdc: checkpoint write failed", {}, t);
|
|
415
417
|
}
|
|
416
418
|
}
|
|
417
419
|
}
|
|
@@ -445,7 +447,7 @@ var T = {
|
|
|
445
447
|
let r = ++t;
|
|
446
448
|
return this.sql.withTransaction(n.flatMap(n.serviceOption(d), (t) => {
|
|
447
449
|
if (a.isNone(t)) return n.fail(/* @__PURE__ */ Error("MssqlStore.transactional: TransactionConnection missing."));
|
|
448
|
-
let i = new
|
|
450
|
+
let i = new G(this, t.value);
|
|
449
451
|
return n.tryPromise({
|
|
450
452
|
try: () => e(i).then((e) => ({
|
|
451
453
|
result: e,
|
|
@@ -455,7 +457,7 @@ var T = {
|
|
|
455
457
|
catch: (e) => e
|
|
456
458
|
});
|
|
457
459
|
}));
|
|
458
|
-
}), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(
|
|
460
|
+
}), i = s.exponential("10 millis").pipe(s.compose(s.recurs(3)), s.whileInput(N)), o = r.pipe(n.retry(i), n.withSpan("store.transactional", { attributes: {
|
|
459
461
|
"db.system": "mssql",
|
|
460
462
|
"db.operation": "transaction"
|
|
461
463
|
} }));
|
|
@@ -491,7 +493,7 @@ var T = {
|
|
|
491
493
|
if (this.inflightTxns > 0) {
|
|
492
494
|
let t = Date.now() + e;
|
|
493
495
|
for (; this.inflightTxns > 0 && Date.now() < t;) await new Promise((e) => setTimeout(e, 25));
|
|
494
|
-
this.inflightTxns > 0 &&
|
|
496
|
+
this.inflightTxns > 0 && H.warn("close: grace period expired with in-flight transactions", {
|
|
495
497
|
gracePeriodMs: e,
|
|
496
498
|
inflight: this.inflightTxns
|
|
497
499
|
});
|
|
@@ -501,7 +503,7 @@ var T = {
|
|
|
501
503
|
async ping() {
|
|
502
504
|
await this.runtime.runPromise(this.sql`SELECT 1`);
|
|
503
505
|
}
|
|
504
|
-
},
|
|
506
|
+
}, G = class {
|
|
505
507
|
parent;
|
|
506
508
|
txn;
|
|
507
509
|
events = [];
|
|
@@ -573,9 +575,9 @@ var T = {
|
|
|
573
575
|
this.events.length = 0;
|
|
574
576
|
}
|
|
575
577
|
}
|
|
576
|
-
},
|
|
578
|
+
}, K = (e) => e.__mssqlReplicationFriend ?? null, q = (e, t) => e === t ? 0 : e < t ? -1 : 1, J = () => ({
|
|
577
579
|
async capturePrimaryPosition(e) {
|
|
578
|
-
let t =
|
|
580
|
+
let t = K(e);
|
|
579
581
|
if (t === null) throw Error("mssqlReplicationAdapter: primary is not an MssqlStore (missing __mssqlReplicationFriend).");
|
|
580
582
|
return t.runEffect(n.gen(function* () {
|
|
581
583
|
return (yield* (yield* u)`
|
|
@@ -588,7 +590,7 @@ var T = {
|
|
|
588
590
|
}));
|
|
589
591
|
},
|
|
590
592
|
async probeReplicaPosition(e) {
|
|
591
|
-
let t =
|
|
593
|
+
let t = K(e);
|
|
592
594
|
if (t === null) throw Error("mssqlReplicationAdapter: replica is not an MssqlStore.");
|
|
593
595
|
return t.runEffect(n.gen(function* () {
|
|
594
596
|
return (yield* (yield* u)`
|
|
@@ -600,14 +602,14 @@ var T = {
|
|
|
600
602
|
}));
|
|
601
603
|
},
|
|
602
604
|
compare(e, t) {
|
|
603
|
-
return
|
|
605
|
+
return q(t, e) >= 0 ? "caught-up" : "behind";
|
|
604
606
|
}
|
|
605
|
-
}),
|
|
607
|
+
}), Y = {
|
|
606
608
|
id: "mssql",
|
|
607
|
-
makeSqlLayer: (e) =>
|
|
608
|
-
makeStore: (e) =>
|
|
609
|
+
makeSqlLayer: (e) => A(e),
|
|
610
|
+
makeStore: (e) => U(e),
|
|
609
611
|
compileContains: (e, t, n) => n`${e} LIKE ${`%${t.replace(/[\\%_[\]]/g, (e) => `\\${e}`)}%`} ESCAPE '\\'`,
|
|
610
|
-
retryFilter:
|
|
612
|
+
retryFilter: I
|
|
611
613
|
};
|
|
612
614
|
//#endregion
|
|
613
|
-
export { f as CDC_OFFSETS_TABLE, e as MssqlClient, m as _voltroMssqlCdcOffsetsTable,
|
|
615
|
+
export { f as CDC_OFFSETS_TABLE, e as MssqlClient, m as _voltroMssqlCdcOffsetsTable, k as connectionFromConfig, R as ensureChangeTracking, O as makeMssqlSqlLayer, A as makeMssqlSqlLayerFromConfig, U as makeMssqlStore, Y as mssqlDialect, J as mssqlReplicationAdapter, I as mssqlRetryFilter, V as startChangeTrackingCdc };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/sql-mssql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@effect/sql": "^0.51.1",
|
|
36
36
|
"@effect/sql-mssql": "^0.52.0",
|
|
37
|
-
"@voltro/database": "0.
|
|
38
|
-
"@voltro/logger": "0.
|
|
37
|
+
"@voltro/database": "0.11.0",
|
|
38
|
+
"@voltro/logger": "0.11.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"effect": "^3.21.4"
|