@voltro/sql-mssql 0.43.0 → 0.43.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -39,6 +39,106 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.43.2] — 2026-08-18
43
+
44
+ ### Fixed
45
+
46
+ - **@voltro/sql-mysql, @voltro/database, @voltro/cli** — The mysql-family binlog reader no longer keeps a table excluded after the very migration that fixed it.
47
+
48
+ A UNIQUE on an unbounded text column is a MariaDB hash long-unique, whose hidden `DB_ROW_HASH_n` column the reader can never account for — so the table is held out of binlog capture and the exclusion is reported. `voltro dev` builds its store BEFORE it migrates (kv, cross-replica broadcast and the analytics mirror all need one), so on a boot whose own auto-migration bounds the column, that finding was drawn from a schema that stopped existing about a second later. The exclusion then outlived its cause for the life of the process, and the message — correct when written, and typically the only error line in the boot log — went on describing the pre-migration database.
49
+
50
+ Three changes:
51
+
52
+ - `DataStore.refreshChangeCaptureExclusions()` (optional; implemented by the mysql-family store) re-runs the probe and re-points the LIVE reader, in both directions — schema work that CREATES the condition now excludes the table immediately instead of after three failed writes. `voltro dev` calls it once, after all its schema work. `voltro serve` needs no equivalent: it builds its store after every schema step and never applies DDL itself. - The definitive message now states its own durability — that it is the schema as read at reader attach, and that applying the remedy does not by itself lift the exclusion. - On a boot that will re-check, the finding is reported as a provisional note rather than as a verdict, so a boot that fixes the condition leaves no error line about it. The note escalates to the full verdict on its own if the re-check never runs. - Two reader fixes the re-check depended on: applying a new exclusion set now WAITS for a reconnect already in flight (it is what applies the set, so resolving before it landed meant the caller's next write hit the old filter), and the reconnect loop no longer keeps resuming from an offset it has just jumped away from — that turned one purged offset into a reconnect every watchdog interval, forever, delivering nothing.
53
+ - **@voltro/data-transfer, @voltro/database, @voltro/sql-mysql, @voltro/sql-postgres, @voltro/sql-sqlite, @voltro/sql-mssql, @voltro/cli** — `voltro data import --mode replace` no longer leaves a target in neither state, and now works against schemas that have foreign keys.
54
+
55
+ The delete step ran table by table and stopped at the first refusal, so a run that could not finish left dozens of tables emptied and nothing loaded — and a second attempt destroyed more than the first, because it got further before hitting the same wall. The wall itself was not exotic: MySQL, MariaDB and SQL Server check a foreign key as each ROW is deleted, so a table that references ITSELF cannot be emptied by any ordering of the tables. `createdBy → actors` on the `actors` table is exactly that shape, and it is what an audit mixin on an actor table produces.
56
+
57
+ - `DataStore.emptyTables()` (per dialect) empties the whole set as one unit, in one transaction, with referential integrity suspended for the duration — `FOREIGN_KEY_CHECKS` on the mysql family, a multi-table `TRUNCATE` on postgres, `defer_foreign_keys` on sqlite, per-table `NOCHECK`/`WITH CHECK CHECK` on mssql. All-or-nothing on every engine, including under `--atomic`, where it runs on the transaction the import already holds. - A **pre-flight refusal**: if a table OUTSIDE the bundle holds rows referencing one inside it, the import refuses before deleting anything and names the tables, the columns and the row counts. Those rows cannot be restored from the bundle, so forcing it is not an option. An EMPTY outside table blocks nothing. - Table-level failures carry the driver's own reason and code, the way row-level failures already did. `truncate <table> failed: Failed to execute statement` fits every plausible cause equally; the classification that produced `foreign key <name>: … [ER_NO_REFERENCED_ROW_2/1452]` one level down now applies one level up. The word "truncate" is gone from the message too — the step issues DELETE, and naming a statement it does not run sends whoever reads it to reproduce the wrong thing. - A typed refusal reaching the `--target api` transport keeps its text: the admin import endpoint answers `409` with the reason instead of flattening it to `import failed`, on the one transport where the operator has no other way to see it.
58
+
59
+ The bulk empty emits no change events, where the per-table loop emitted one per row. An import through `--target api` now asks every live subscription to re-read once it lands — the coarse refresh the framework already uses after a broadcast gap — so neither the missing deletes nor a table the bundle carries EMPTY leaves a subscriber holding rows that are gone. Wired where the route is mounted, which is the one place both boot paths share.
60
+
61
+ On postgres the emptying is a `DELETE` per table, not a `TRUNCATE`, and the difference is not performance: postgres refuses `TRUNCATE` on a table with ANY incoming foreign key, rows or not, while the mysql family refuses a DELETE only when rows actually reference the doomed ones. A `TRUNCATE` version made an EMPTY table outside the bundle block a replace on postgres and not on mariadb — one import, refused on one engine and accepted on the other, over a table holding nothing.
62
+
63
+ `--target api` also no longer times out on a full bundle. Both api-target calls went through `fetch`, whose undici default gives up after 300 s — a bound on the caller's database size, on a call whose response arrives only when the import does. They wait as long as the instance needs now, stream the body instead of buffering the whole bundle, and take `--timeout <seconds>` when a deadline is wanted. If one is hit, the message says the instance is probably still importing — and gives different advice for `replace` than for the idempotent modes, since re-running the first while it is mid-flight would empty the target under it.
64
+
65
+ Two more, found by measuring rather than by reading:
66
+
67
+ - **`--atomic` on postgres could not import a bundle that needed the deferred-FK repair at all.** A failed statement aborts the transaction there, and that repair depends on a row whose parent has not loaded yet failing, being held, and being retried — so the first such row poisoned every write after it. Every tolerated write now runs inside a savepoint. Per-row savepoints measured 2.40x the time of none on 5 000 rows, so they are amortised: one savepoint per batch of 200, and a batch that fails rolls back whole and replays row by row. The mysql family and sqlite leave a transaction usable after a failed statement and pay nothing for any of this. - **The replace pre-flight asked the caller's snapshot.** Over `--target api` that is the app's DECLARED schema, which cannot show a table the app stopped declaring but the database still has — and rows in a table nobody declares are exactly the rows nobody is watching. `DataStore.incomingForeignKeys()` reads the live catalog per dialect; the snapshot remains the fallback for stores without one.
68
+
69
+ A bundle bigger than one chunk is now uploaded as a series of short requests, so a proxy body cap or an ingress read timeout has nothing large to choke on, and the switch is automatic — the packer's stream is buffered one chunk ahead, so a small bundle is sent exactly as before and nobody has to know in advance which table is the big one. The import still runs ONCE, at the end, over the whole bundle. Resume is byte-exact (`packBundle` is deterministic over a directory, which this package now asserts), guarded by a bundle key so a different bundle under the same upload id is refused rather than spliced into the partial one, and by a contiguity check so a mis-ordered append cannot produce an archive that only fails later during decode. `--chunk-size <mb>` overrides the 16 MiB default.
70
+
71
+ ---
72
+
73
+ ## [0.43.1] — 2026-08-18
74
+
75
+ ### Fixed
76
+
77
+ - **@voltro/database** — A write recorder that fails inside someone's transaction now says which recorder, on which write, and what the database actually said.
78
+
79
+ `@effect/sql` renders every driver failure as `SqlError: Failed to execute statement` — one sentence that fits a missing column, a dangling foreign key, an over-long value and a duplicate key equally. The driver's own words hang off a SYMBOL on a `FiberFailure`, so a caller who reaches for `.cause` gets `undefined` and concludes there is nothing there.
80
+
81
+ That is expensive precisely where recorders run: the caller's write was ordinary, and what failed was framework machinery one table over. The message now reads
82
+
83
+ ```
84
+ write recorder '_voltro_row_history' failed while recording an update on 'users':
85
+ Duplicate entry 'rowver_…' for key 'PRIMARY' [code=ER_DUP_ENTRY errno=1062 …]
86
+ ```
87
+
88
+ and the original error is kept as `cause` for anyone who does walk the chain. The failure still takes the transaction down — that is the guarantee and it is unchanged.
89
+
90
+ `describeDriverError` (`@voltro/database`) is the shared summariser, built on the existing cause extractor rather than a second walker. It returns nothing when the chain carries nothing driver-shaped, so an ordinary programming error from a recorder arrives as itself instead of wrapped in prose about a database.
91
+ - **@voltro/database** — On MariaDB, a `varchar` column could introspect as `json` because a DIFFERENT table had a json column with the same column name.
92
+
93
+ MariaDB names a column-level CHECK after the COLUMN, and those names are unique per table, not per schema. `information_schema.check_constraints` on MySQL has no `TABLE_NAME`, so the introspector recovered it by joining `table_constraints` on `(schema, constraint_name)` — which on MariaDB cross-products every same-named check across every table. Measured on 11.8:
94
+
95
+ ```
96
+ a.payload LONGTEXT CHECK (json_valid(`payload`))
97
+ b.payload VARCHAR(255) CHECK (`payload` in ('x','y'))
98
+
99
+ join result: a → json_valid, a → in(…), b → json_valid, b → in(…)
100
+ ```
101
+
102
+ So `b.payload` reads as `json`, and `a.payload` picks up an enum it does not have. Downstream that is not a cosmetic label: the planner emits a blocked `alter-column-type` with `from: 'json'` that no `.narrowedFrom()` can honestly acknowledge, because the premise is false — and since the data-transfer manifest records introspected types, the same misreading travels into the bundle and reappears as schema drift on import.
103
+
104
+ MariaDB's own `check_constraints` HAS `TABLE_NAME`. The introspector asks for it directly now and keeps the join as the MySQL path, where check-constraint names are schema-unique and the join is sound. That asymmetry is why a single-engine test could not see this: the wrong query passes on MySQL.
105
+ - **@voltro/database** — `voltro db apply` could not drop a CHECK constraint on the mysql family at all, and each of the three reasons hid the next.
106
+
107
+ **1. `DROP CHECK` is MySQL-8 syntax.** MariaDB has never had it — measured on 11.8, `ALTER TABLE t DROP CHECK c` is `ERROR 1064`, while `DROP CONSTRAINT c` works on both engines. A plan containing a `drop-check` therefore died on the first one, on a family whose migrations are NOT atomic: the run stopped with the earlier statements committed and no rollback.
108
+
109
+ **2. The name was assumed, not read.** The applier dropped `<table>_<column>_check` — which is only what its own `add-check` would have named it. A CHECK created at table bring-up is INLINE and UNNAMED, so the server names it (`CONSTRAINT_1` / `<table>_chk_1` / the column name). Dropping a name that does not exist reports "does not exist", which is indistinguishable from the "already gone" a resume legitimately produces — so the statement succeeded, the constraint stayed, and the plan re-proposed the identical `drop-check` forever. The name comes from the catalog now.
110
+
111
+ **3. A column-level CHECK cannot be dropped by name on MariaDB at all.** Measured: the catalog lists it under the column's name, `DROP CONSTRAINT` on that name answers 1091, and only redefining the column removes it. The applier now drops by catalog name, ASKS whether the constraint survived, and redefines the column when it did — so a rebuild happens only in the case that needs one.
112
+
113
+ **And the same investigation closed the MySQL `.oneOf()` round-trip gap.** `parseEnumCheck` was documented as handling MySQL's rendering and did not: MySQL backslash-escapes the string DELIMITERS (`_utf8mb4\'draft\'`), and the parser rewrote those to the SQL doubling `''` — which is how a quote INSIDE a value is written. Every delimiter became escaped content, every value came back empty, and the filter dropped them. `.oneOf()` now round-trips on MySQL, and the suite that asserted the gap as a known one asserts the round-trip instead.
114
+
115
+ Plus, on a failed apply: the error now states how many operations were already applied and whether this dialect rolls back. The ledger held that number; it never reached the operator, who had to re-plan and diff the counts to learn how far the run got.
116
+ - **@voltro/plugin-versioning** — `versioningPlugin({ timing: 'in-transaction' })` built its history row's primary key from `(rowId, version)` while the version counter three lines above was scoped to `(tableName, rowId)`. Two versioned tables carrying the same row id therefore collided — permanently.
117
+
118
+ The shape is not exotic: `actors.id === users.id` is what the framework's own audit trail asks for, an `actors` row whose id is the user's so an audited write satisfies `createdBy → actors`. In an app that follows it, every user row has a twin.
119
+
120
+ The collision does not heal, and that is what turns a duplicate into an outage. The second table's insert fails, so its history row is never written, so `maxOf` for that table stays `null`, so the next attempt computes the same version and the same id. Every write to that row is dead from then on — surfacing as `ER_DUP_ENTRY` on an ordinary `store.update`, naming a row id in a table the caller never wrote to.
121
+
122
+ The key is `(table, rowId, version)` now — the same shape the post-commit path always built. It stays deterministic (no clock, no process-local counter), which is what lets it survive a replica restart; it just carries every part of the key it claims to be unique over.
123
+
124
+ **No cleanup is needed for rows already written.** They keep their old ids and belong to whichever table wrote them; the new keys cannot collide with them, and `byRow` is not unique. An app blocked by this is unblocked by the upgrade alone.
125
+
126
+ Covered twice: the recorder against a port that refuses duplicates (the mechanism, including that a repeat does not settle), and two versioned tables sharing an id against live postgres (the real primary key, inside the caller's transaction). The suite that existed exercised ONE table, which cannot produce a collision at all — and read exactly like a suite that covered this.
127
+
128
+ ### Internal (no consumer-facing effect)
129
+
130
+ - **@voltro/sql-mysql** — Two test-only defects in `sql-mysql`, both found by a release gate, both of the same family: a check that could not fail, and a failure reported in the wrong place. No product code changed.
131
+
132
+ **An assertion that could not fail.** `dropCheckSyntax.integration.test.ts` fell back to a HAND-BUILT plan when the planner produced no operations — and the fabricated operation was a `drop-check`, which is exactly what the next line asserts the plan contains. So an engine whose planner stopped emitting it would have been handed one and reported green. The fallback is deleted; both engines produce the operation now, which is what this release fixed, and the assertion is load-bearing again (4/4 on mysql AND mariadb without it).
133
+
134
+ It surfaced as a TYPE error rather than a false pass, because the fallback's object widened `plan` into a union `applyPlan` does not accept. Worth noting which check caught it: `vitest` transpiles without type-checking, so the suite was green and only `tsc` objected — the gate's `typecheck` and `lint` steps are what went red.
135
+
136
+ **A wait that gave up in silence.** `waitFor` in both binlog CDC suites looped to a deadline and then RETURNED, so a "prove the reader is live" wait that expired let the test carry on, kill the binlog dump thread, and fail twenty lines later on `expect(ids).toContain('todo_wd_before')` — an assertion about a different claim, in a different place. It throws now, naming the wait and the window, and all twelve call sites carry a label.
137
+
138
+ The window is named too: `FIRST_ATTACH_MS = 30_000`, up from 12 s. The reasoning is the 40 s window already in the same file, whose comment says a re-attach plus binlog catch-up takes longer under a loaded full-suite run — a FIRST attach does both and only skips the backoff, so 12 s beside 40 s was an asymmetry the file's own reasoning did not support. That is an argument from the neighbouring comment, not a measurement; if it expires again, `waitFor` now says which wait and for how long, and that number is the one to argue with rather than raising this one twice.
139
+
140
+ ---
141
+
42
142
  ## [0.43.0] — 2026-08-18
43
143
 
44
144
  ### ⚠ BREAKING
package/dist/index.d.ts CHANGED
@@ -270,6 +270,31 @@ export declare class MssqlStore implements DataStore {
270
270
  /* Excluded from this release type: getInternalExecuteDelete */
271
271
  /* Excluded from this release type: getInternalExecuteUpsert */
272
272
  /* Excluded from this release type: getInternalExecuteInsertIgnore */
273
+ /**
274
+ * Empty `tables` as one unit — see `DataStore.emptyTables`.
275
+ *
276
+ * SQL Server has no session switch for referential integrity, so the
277
+ * suspension is per TABLE: `NOCHECK CONSTRAINT ALL` stops the engine
278
+ * enforcing that table's foreign keys, the whole set is deleted, and then the
279
+ * constraints go back on. `WITH CHECK` on the way back is the half that must
280
+ * not be dropped — re-enabling without it leaves the constraint marked
281
+ * untrusted, which is invisible until the query planner quietly stops using
282
+ * it. The tables are empty by then, so the re-validation it forces is free.
283
+ *
284
+ * `TRUNCATE` is not an option here for the same reason it is not on the mysql
285
+ * family: SQL Server refuses it on any table with an incoming foreign key,
286
+ * whether or not that key has rows behind it.
287
+ */
288
+ emptyTables(tables: ReadonlyArray<string>): Promise<void>;
289
+ /** See `DataStore.incomingForeignKeys`. `sys.foreign_key_columns` is the
290
+ * catalog view that already resolves both ends by object id, so no name
291
+ * matching is involved. */
292
+ incomingForeignKeys(tables: ReadonlyArray<string>): Promise<ReadonlyArray<{
293
+ from: string;
294
+ column: string;
295
+ to: string;
296
+ }>>;
297
+ /* Excluded from this release type: emptyTablesOn */
273
298
  transactional<T>(work: (tx: DataStore) => Promise<T>): Promise<T>;
274
299
  /**
275
300
  * The DIALECT half of the shared transaction bracket
package/dist/index.js CHANGED
@@ -1,21 +1,21 @@
1
1
  import { MssqlClient as e } from "@effect/sql-mssql";
2
2
  import { Config as t, Duration as n, Effect as r, Layer as i, ManagedRuntime as a, Redacted as o } from "effect";
3
- import { CDC_OFFSETS_TABLE as s, DEFAULT_ACQUIRE_TIMEOUT_MS as c, EagerCardinalityError as l, _voltroMssqlCdcOffsetsTable as u, attachEagerLoads as d, attributionFields as f, bulkInsertLimitsFor as p, chunkRowsForInsert as m, compileEagerJson as h, compilePredicate as g, compileRawFragment as _, compileSelect as v, decodeRowsFromSchema as y, externalChangeEvent as b, hasEagerLoads as x, isTableReactive as S, makeEagerFallbackReporter as ee, observeDbOp as C, qualifyTable as w, raiseChangeListenerCeiling as T, recordsTable as E, requireTable as D, runStoreTransaction as te, runWriteRecorders as O, stampGeneratedId as k, stampGeneratedIds as A, withCapturedAttribution as j } from "@voltro/database";
3
+ import { CDC_OFFSETS_TABLE as s, DEFAULT_ACQUIRE_TIMEOUT_MS as c, EagerCardinalityError as l, _voltroMssqlCdcOffsetsTable as u, attachEagerLoads as d, attributionFields as f, bulkInsertLimitsFor as p, chunkRowsForInsert as m, compileEagerJson as h, compilePredicate as g, compileRawFragment as _, compileSelect as v, decodeRowsFromSchema as y, externalChangeEvent as b, hasEagerLoads as ee, isTableReactive as x, makeEagerFallbackReporter as te, observeDbOp as S, qualifyTable as C, quoteIdent as w, raiseChangeListenerCeiling as T, recordsTable as E, requireTable as ne, runStoreTransaction as D, runWriteRecorders as O, stampGeneratedId as k, stampGeneratedIds as A, withCapturedAttribution as j } from "@voltro/database";
4
4
  import { EventEmitter as M } from "node:events";
5
5
  import { createLogger as N } from "@voltro/logger";
6
6
  import { SqlClient as P, TransactionConnection as F } from "@effect/sql/SqlClient";
7
7
  //#region src/sqlLayer.ts
8
- var I = {
8
+ var re = {
9
9
  ...e.defaultParameterTypes,
10
10
  null: e.defaultParameterTypes.object
11
- }, ne = (e) => {
11
+ }, ie = (e) => {
12
12
  let t = e.acquireTimeoutMs ?? c;
13
13
  return t > 0 ? t : void 0;
14
- }, L = (r) => {
15
- let i = ne(r);
14
+ }, I = (r) => {
15
+ let i = ie(r);
16
16
  return e.layerConfig({
17
17
  server: t.succeed(r.server),
18
- parameterTypes: t.succeed(I),
18
+ parameterTypes: t.succeed(re),
19
19
  ...i === void 0 ? {} : { connectTimeout: t.succeed(n.millis(i)) },
20
20
  ...r.port === void 0 ? {} : { port: t.succeed(r.port) },
21
21
  ...r.database === void 0 ? {} : { database: t.succeed(r.database) },
@@ -25,7 +25,7 @@ var I = {
25
25
  ...r.trustServer === void 0 ? {} : { trustServer: t.succeed(r.trustServer) },
26
26
  ...r.maxConnections === void 0 ? {} : { maxConnections: t.succeed(r.maxConnections) }
27
27
  });
28
- }, re = (e) => {
28
+ }, L = (e) => {
29
29
  let t = e.get("sslmode");
30
30
  if (t !== null) {
31
31
  if (t === "require") return !0;
@@ -53,7 +53,7 @@ var I = {
53
53
  username: decodeURIComponent(r.username || "sa"),
54
54
  password: decodeURIComponent(r.password || ""),
55
55
  database: r.pathname.replace(/^\//, "") || "master",
56
- ...n(e.ssl ?? re(r.searchParams)),
56
+ ...n(e.ssl ?? L(r.searchParams)),
57
57
  ...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections },
58
58
  ...t
59
59
  };
@@ -68,7 +68,7 @@ var I = {
68
68
  ...e.maxConnections === void 0 ? {} : { maxConnections: e.maxConnections },
69
69
  ...t
70
70
  };
71
- }, z = (e) => L(R(e)), B = /* @__PURE__ */ new Set(["1205"]), V = (e) => {
71
+ }, z = (e) => I(R(e)), B = /* @__PURE__ */ new Set(["1205"]), V = (e) => {
72
72
  let t = e;
73
73
  for (let e = 0; e < 5 && typeof t == "object" && t; e++) {
74
74
  let e = t.number;
@@ -178,16 +178,16 @@ var I = {
178
178
  }, Z = N({ scope: "voltro:mssql" }), Q = async (e) => {
179
179
  let t = e.changeStrategy ?? "inline", n = t;
180
180
  t === "cdc" && !e.cdcConfig && (Z.warn("changeStrategy='cdc' requires cdcConfig (replicaId + includeTables); falling back to 'inline'."), n = "inline");
181
- let r = e.tracerLayer ? i.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, o = a.make(r), s = new ie(await o.runPromise(P), o, n);
181
+ let r = e.tracerLayer ? i.mergeAll(e.sqlLayer, e.tracerLayer) : e.sqlLayer, o = a.make(r), s = new ae(await o.runPromise(P), o, n);
182
182
  return n === "cdc" && e.cdcConfig && await s.startCdcConsumer(e.cdcConfig), s;
183
- }, ie = class e {
183
+ }, ae = class e {
184
184
  sql;
185
185
  runtime;
186
186
  changeStrategy;
187
187
  namespace;
188
188
  emitter;
189
189
  inflightTxns = 0;
190
- reportEagerFallback = ee(Z);
190
+ reportEagerFallback = te(Z);
191
191
  cdcHandle = null;
192
192
  cdcCheckpointTimer = null;
193
193
  cdcPendingVersion = null;
@@ -203,11 +203,11 @@ var I = {
203
203
  return t === this.namespace ? this : new e(this.sql, this.runtime, this.changeStrategy, t, this.emitter);
204
204
  }
205
205
  nsT(e) {
206
- return w(this.namespace, e);
206
+ return C(this.namespace, e);
207
207
  }
208
208
  __mssqlReplicationFriend = { runEffect: (e) => this.runtime.runPromise(e) };
209
209
  async executeQuery(e, t) {
210
- let n = v(e, this.sql, this.namespace), i = t ? r.provideService(n, F, t) : n, a = await C("mssql", "select", () => this.runtime.runPromise(i));
210
+ let n = v(e, this.sql, this.namespace), i = t ? r.provideService(n, F, t) : n, a = await S("mssql", "select", () => this.runtime.runPromise(i));
211
211
  return y(a, e.table, "mssql");
212
212
  }
213
213
  async executeInsert(e, t, n, i, a) {
@@ -290,20 +290,20 @@ var I = {
290
290
  prev: e.old,
291
291
  traceId: e.traceId,
292
292
  subjectId: e.subjectId
293
- }), S(e.table) && (t === null ? this.inlineEmit && this.emitter.emit("change", e) : t.push(e));
293
+ }), x(e.table) && (t === null ? this.inlineEmit && this.emitter.emit("change", e) : t.push(e));
294
294
  }
295
295
  query(e) {
296
296
  return this.runWithEager(e, null);
297
297
  }
298
298
  raw(e, t) {
299
299
  let n = _(e, this.sql);
300
- return C("mssql", "raw", () => this.runtime.runPromise(n));
300
+ return S("mssql", "raw", () => this.runtime.runPromise(n));
301
301
  }
302
302
  async runWithEager(e, t) {
303
- if (!x(e)) return this.executeQuery(e, t);
303
+ if (!ee(e)) return this.executeQuery(e, t);
304
304
  let n = this.namespace === null ? h(e, this.sql, "mssql") : null;
305
305
  if (n !== null) try {
306
- let e = t ? r.provideService(n.fragment, F, t) : n.fragment, i = await C("mssql", "select", () => this.runtime.runPromise(e));
306
+ let e = t ? r.provideService(n.fragment, F, t) : n.fragment, i = await S("mssql", "select", () => this.runtime.runPromise(e));
307
307
  return n.decode(i);
308
308
  } catch (t) {
309
309
  if (t instanceof l) throw t;
@@ -320,13 +320,13 @@ var I = {
320
320
  reason: "not-compilable"
321
321
  });
322
322
  let i = await this.executeQuery(e, t);
323
- return d(i, e.eager, e.sourceTable ?? D(e.table), (e) => this.executeQuery(e, t));
323
+ return d(i, e.eager, e.sourceTable ?? ne(e.table), (e) => this.executeQuery(e, t));
324
324
  }
325
325
  getInternalRunWithEager() {
326
326
  return this.runWithEager.bind(this);
327
327
  }
328
328
  localWrite(e, t) {
329
- return C("mssql", e, () => j(t));
329
+ return S("mssql", e, () => j(t));
330
330
  }
331
331
  insert(e, t) {
332
332
  return this.localWrite("insert", (n) => this.executeInsert(e, t, null, null, n));
@@ -344,7 +344,7 @@ var I = {
344
344
  return this.localWrite("delete", (n) => this.executeDelete(e, t, null, null, n));
345
345
  }
346
346
  async updateMany(e, t, n) {
347
- let r = this.sql, i = g(n.where, r, this.namespace), a = r`UPDATE ${r(this.nsT(e))} SET ${r.update(t)} OUTPUT INSERTED.* WHERE ${i}`, o = await C("mssql", "update", () => this.runtime.runPromise(a));
347
+ let r = this.sql, i = g(n.where, r, this.namespace), a = r`UPDATE ${r(this.nsT(e))} SET ${r.update(t)} OUTPUT INSERTED.* WHERE ${i}`, o = await S("mssql", "update", () => this.runtime.runPromise(a));
348
348
  for (let t of o) await this.routeEvent({
349
349
  table: e,
350
350
  op: "update",
@@ -354,7 +354,7 @@ var I = {
354
354
  return o.length;
355
355
  }
356
356
  async deleteMany(e, t) {
357
- let n = this.sql, r = g(t.where, n, this.namespace), i = n`DELETE FROM ${n(this.nsT(e))} OUTPUT DELETED.* WHERE ${r}`, a = await C("mssql", "delete", () => this.runtime.runPromise(i));
357
+ let n = this.sql, r = g(t.where, n, this.namespace), i = n`DELETE FROM ${n(this.nsT(e))} OUTPUT DELETED.* WHERE ${r}`, a = await S("mssql", "delete", () => this.runtime.runPromise(i));
358
358
  for (let t of a) await this.routeEvent({
359
359
  table: e,
360
360
  op: "delete",
@@ -514,13 +514,37 @@ var I = {
514
514
  getInternalExecuteInsertIgnore() {
515
515
  return this.executeInsertIgnore.bind(this);
516
516
  }
517
+ async emptyTables(e) {
518
+ await this.emptyTablesOn(e, null);
519
+ }
520
+ async incomingForeignKeys(e) {
521
+ if (e.length === 0) return [];
522
+ let t = this.sql;
523
+ return (await this.runtime.runPromise(t`
524
+ SELECT child.name AS fromTable, col.name AS columnName, parent.name AS toTable
525
+ FROM sys.foreign_key_columns fkc
526
+ JOIN sys.tables child ON child.object_id = fkc.parent_object_id
527
+ JOIN sys.tables parent ON parent.object_id = fkc.referenced_object_id
528
+ JOIN sys.columns col ON col.object_id = fkc.parent_object_id AND col.column_id = fkc.parent_column_id
529
+ WHERE parent.name IN ${t.in(e)}
530
+ AND child.name NOT IN ${t.in(e)}`)).map((e) => ({
531
+ from: e.fromTable,
532
+ column: e.columnName,
533
+ to: e.toTable
534
+ }));
535
+ }
536
+ async emptyTablesOn(e, t) {
537
+ if (e.length === 0) return;
538
+ let n = this.sql, i = (e) => this.nsT(e), a = (e, t) => n.unsafe(`ALTER TABLE ${w(i(e), "mssql")} ${t ? "WITH CHECK CHECK" : "NOCHECK"} CONSTRAINT ALL`), o = r.acquireUseRelease(r.forEach(e, (e) => a(e, !1), { discard: !0 }), () => r.forEach(e, (e) => n`DELETE FROM ${n(i(e))}`, { discard: !0 }), () => r.orDie(r.ignore(r.forEach(e, (e) => a(e, !0), { discard: !0 })))), s = t === null ? n.withTransaction(o) : r.provideService(o, F, t);
539
+ await this.runtime.runPromise(s);
540
+ }
517
541
  async transactional(e) {
518
542
  this.inflightTxns++;
519
543
  try {
520
- return await te({
544
+ return await D({
521
545
  ...this.txnSpec("MssqlStore.transactional"),
522
546
  work: e,
523
- makeView: (e, t) => new ae(this, e, t)
547
+ makeView: (e, t) => new oe(this, e, t)
524
548
  });
525
549
  } finally {
526
550
  this.inflightTxns--;
@@ -551,7 +575,7 @@ var I = {
551
575
  return this.changeStrategy === "cdc" ? "fleet" : "local";
552
576
  }
553
577
  injectExternalChange(e) {
554
- S(e.table) && this.emitter.emit("change", b(e));
578
+ x(e.table) && this.emitter.emit("change", b(e));
555
579
  }
556
580
  run(e) {
557
581
  return this.runtime.runPromise(e);
@@ -574,7 +598,7 @@ var I = {
574
598
  async ping() {
575
599
  await this.runtime.runPromise(this.sql`SELECT 1`);
576
600
  }
577
- }, ae = class {
601
+ }, oe = class {
578
602
  parent;
579
603
  txn;
580
604
  attr;
@@ -586,6 +610,9 @@ var I = {
586
610
  query(e) {
587
611
  return this.parent.getInternalRunWithEager()(e, this.txn);
588
612
  }
613
+ emptyTables(e) {
614
+ return this.parent.emptyTablesOn(e, this.txn);
615
+ }
589
616
  insert(e, t) {
590
617
  return this.parent.getInternalExecuteInsert()(e, t, this.txn, this.events, this.attr);
591
618
  }
@@ -647,7 +674,7 @@ var I = {
647
674
  this.events.length = 0;
648
675
  }
649
676
  }
650
- }, $ = (e) => e.__mssqlReplicationFriend ?? null, oe = (e, t) => e === t ? 0 : e < t ? -1 : 1, se = () => ({
677
+ }, $ = (e) => e.__mssqlReplicationFriend ?? null, se = (e, t) => e === t ? 0 : e < t ? -1 : 1, ce = () => ({
651
678
  async capturePrimaryPosition(e) {
652
679
  let t = $(e);
653
680
  if (t === null) throw Error("mssqlReplicationAdapter: primary is not an MssqlStore (missing __mssqlReplicationFriend).");
@@ -674,9 +701,9 @@ var I = {
674
701
  }));
675
702
  },
676
703
  compare(e, t) {
677
- return oe(t, e) >= 0 ? "caught-up" : "behind";
704
+ return se(t, e) >= 0 ? "caught-up" : "behind";
678
705
  }
679
- }), ce = {
706
+ }), le = {
680
707
  id: "mssql",
681
708
  makeSqlLayer: (e) => z(e),
682
709
  makeStore: (e) => Q(e),
@@ -684,4 +711,4 @@ var I = {
684
711
  retryFilter: G
685
712
  };
686
713
  //#endregion
687
- export { s as CDC_OFFSETS_TABLE, e as MssqlClient, u as _voltroMssqlCdcOffsetsTable, R as connectionFromConfig, q as ensureChangeTracking, L as makeMssqlSqlLayer, z as makeMssqlSqlLayerFromConfig, Q as makeMssqlStore, ce as mssqlDialect, se as mssqlReplicationAdapter, G as mssqlRetryFilter, X as startChangeTrackingCdc };
714
+ export { s as CDC_OFFSETS_TABLE, e as MssqlClient, u as _voltroMssqlCdcOffsetsTable, R as connectionFromConfig, q as ensureChangeTracking, I as makeMssqlSqlLayer, z as makeMssqlSqlLayerFromConfig, Q as makeMssqlStore, le as mssqlDialect, ce as mssqlReplicationAdapter, G as mssqlRetryFilter, X as startChangeTrackingCdc };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/sql-mssql",
3
- "version": "0.43.0",
3
+ "version": "0.43.2",
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.0",
37
37
  "@effect/sql-mssql": "^0.53.0",
38
- "@voltro/database": "0.43.0",
39
- "@voltro/logger": "0.43.0"
38
+ "@voltro/database": "0.43.2",
39
+ "@voltro/logger": "0.43.2"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "effect": "^3.22.0"