@objectstack/driver-sqlite-wasm 17.0.0-rc.5 → 17.0.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/CHANGELOG.md +554 -0
  2. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,559 @@
1
1
  # @objectstack/driver-sqlite-wasm
2
2
 
3
+ ## 17.0.0-rc.6
4
+
5
+ ### Major Changes
6
+
7
+ - d367f03: refactor(drivers)!: 五个驱动的 query 参数跟进 `DriverQuery`,休眠的类型谎言就此没有藏身处 (#6075)
8
+
9
+ #5181(PR #6076)把 `IDataDriver.find/findOne/count/updateMany/deleteMany/explain` 的 query 参数收窄为 `DriverQuery`(`Omit<QueryAST, 'object'>`),并在同一条 changeset 里写明:「把驱动签名一并迁到 `DriverQuery` 是后续的机械收尾」。这就是那次收尾。
10
+
11
+ 在此之前,五个驱动的实现仍旧声明 `query: QueryAST`(turso 侧是 `query: any`)。**它不红,也不会红** —— 方法参数按双变比较,实现声明得比契约宽照样满足契约。但调用方现在**有权**省略 `object`,于是这些实现的类型说 `query.object` 是 `string`,运行期却可能是 `undefined`:一句休眠的谎言,没有任何门拦得住下一个照着它写代码的人。
12
+
13
+ 收尾之后,「驱动读 `query.object`」直接变成编译错误:
14
+
15
+ ```ts
16
+ // 收窄前:编译通过,运行期可能是 undefined —— 谎言
17
+ // 收窄后:error TS2339: Property 'object' does not exist on type 'DriverQuery'.
18
+ const name = query.object;
19
+ ```
20
+
21
+ **零运行时改动。** 本次改的全部是类型注解:五个驱动的六个契约方法签名,以及为让类型自洽而必须跟进的少量私有辅助方法参数(mongodb 的 `buildFindOptions` / `buildSortSpec`,sql 的 `findRows` / `orderKeysFor`,turso 的 `toRemoteQuery` / `toRemoteReadQuery`,memory 的 `performAggregation`)—— 它们都只转发或读取 `where` / `orderBy` / `groupBy` 这些字段,本来就不读 `object`。turso 的几处 `query: any` 一并收紧,多拿回一批本已放弃的检查。emit 无差异,测试全绿(memory 524、mongodb 206、sql 906、sqlite-wasm 254、turso 788)。
22
+
23
+ **迁移面:删掉驱动调用字面量里的 `object:` 键**,与 #5181 是同一句话,只是现在也覆盖了直接按具体驱动类(`SqlDriver` / `MemoryDriver` / …)而非按 `IDataDriver` 取类型的调用方。编译器会逐处指出来(TS2353 `'object' does not exist in type 'DriverQuery'`)。本仓下游 25 个包实测零处需要改动,改动只落在五个驱动自己的测试里。
24
+
25
+ 标 major 的依据与 #5181 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。
26
+
27
+ `aggregate` / `distinct` / `syncSchemasBatch` 不在本次范围内 —— 它们不是 `IDataDriver` 收窄的那六个方法,其中 `syncSchemasBatch` 的条目里 `object` 是被真实读取的必填键,`expand` 条目里的 `object` 同理命名的是关联对象,都不是冗余。
28
+
29
+ - 62159bd: refactor(driver-sql)!: `SqlDriver.distinct` 的第三参收成裸 `FilterCondition`,一个静默返回全集的写法就此编译不过 (#6320)
30
+
31
+ `distinct` 不在 `IDataDriver` 上,所以 #5181(PR #6076)与 #6075(PR #6210)的收窄都没走到它,#6212 批 A+E(#6355)收的是 `analyzeQuery` / `findWithWindowFunctions`,也没覆盖它。它的方法体一直说得很清楚——`applyFilters(builder, filters)` 拿的是**实参本身**,因此它要的是 `find()` 放在 `query.where` 里的那个值,**不是 query 信封**;`filters?: any` 只是没把这句话写进类型里。
32
+
33
+ ```ts
34
+ // 收窄前后都成立,一处调用点都不用改
35
+ await driver.distinct("orders", "product", { status: "completed" });
36
+ ```
37
+
38
+ **收窄真正买到的东西,是实测出来的,不是推断的。** 三行数据(`Laptop`/`Mouse` 为 `completed`,`Ghost` 为 `pending`),逐个形状喂给 `distinct('orders','product', …)`:
39
+
40
+ | 第三参 | 收窄前 | 收窄后 |
41
+ | :--------------------------- | :--------------------------------- | :----------- |
42
+ | `{ status: 'completed' }` | 返回 `["Laptop","Mouse"]` | 不变 |
43
+ | 省略 | 返回全集 | 不变 |
44
+ | `'completed'`(标量) | **编译通过,返回全集** | **编译错误** |
45
+ | `{ object, where }`(信封) | 抛 `INVALID_FILTER` / 400 | 不变 |
46
+ | `['status','=','completed']` | 抛 `INVALID_FILTER` / 400(#5158) | 不变 |
47
+
48
+ 第三行就是本次消掉的那一格:一个真心想问「completed 订单里有哪些商品」的调用,编译通过,然后拿到**每一个**商品。`applyFilters` 对「真值但非对象、非数组」的 filter 不发射任何谓词(该方法尾注写着这件事),于是过滤条件被整条丢掉。方向是**放宽**——这正是 #6320 与 #5234 同族的那类「静默错答案」。
49
+
50
+ **有一格是任何类型都关不上的,本次如实写进注释而不是假装关上了。** `FilterCondition` 的键**就是字段名**,所以它是开放映射(`[key: string]: any`):`{ object, where }` 在结构上是一个完全合法的 filter——约束两个分别叫 `object` 和 `where` 的列。没有任何注解能把它和正当 filter 分开。#6320 提出的「让反向错配也编译不过」在这个参数上**不可达**,实测确认;能拿到的保证是**运行期响亮失败**:信封里的 `where` 是对象,而没有任何比较值可以是对象,于是 `assertCompilableComparand` 抛 `INVALID_FILTER` / 400。这半边 driver-sql 从来就不是静默的;`driver-memory` 那半边(裸 filter 交给它会静默返回全集)留在 #5499 冻结面内,本次不碰。
51
+
52
+ **零运行时改动**:非测试改动 100% 是一个类型注解加一段注释,无逻辑、无行为、无 emit 差异。
53
+
54
+ **逐处复核了全部 14 个调用点**(本单正文记的是 3 处,实测偏低):driver-sql 11 处、driver-sqlite-wasm 3 处、driver-turso 0 处;其中真正传第三参的是 4 处(driver-sql 2 + driver-sqlite-wasm 2),全部本来就写的裸 filter,**零报错、零 fixture 改动**。
55
+
56
+ **driver-sqlite-wasm 也标 major**:`SqliteWasmDriver extends SqlDriver` 且不覆写 `distinct`,所以它**已发布的 `.d.ts`** 里这个方法的签名同样收窄,它的使用者看到的是同一个变化。该包读的是 driver-sql 构建后的 `dist/*.d.ts` 而非源码,是一处已知门禁盲区,本次用「往参数类型里临时塞一个调用方不可能满足的成员、重建、看调用点是否逐一变红」证明它确实读到了新 d.ts:driver-sql 6 处红、driver-sqlite-wasm 3 处红,与预判逐一相符。
57
+
58
+ ### 迁移
59
+
60
+ 调用点若把**标量**(或任何非 `FilterCondition` 值)交给第三参,编译器会指出来:
61
+
62
+ ```
63
+ error TS2345: Argument of type 'string' is not assignable to parameter of type 'FilterCondition'.
64
+ ```
65
+
66
+ 改法是把它写成它本来就该是的裸 filter 对象(`'completed'` → `{ status: 'completed' }`)。⚠️ 这类调用点在收窄前拿到的是**未过滤的全集**,所以这不是一次等价改写:修完之后返回值会变,而变化后的那个才是调用方本来想要的答案。本仓零处这样的调用点。
67
+
68
+ ⚠️ 无类型的 JS 调用方**既不会拿到编译错误、也不会有任何行为变化**(本次零运行时改动)。对他们而言,上面那条是「你一直没在过滤」的**唯一通知渠道** —— 这也是本次记台账条目的理由,见下。
69
+
70
+ <!-- adr-0087: registered driver-sql-distinct-bare-filter-typed -->
71
+
72
+ ### Minor Changes
73
+
74
+ - 92a67f2: feat(drivers,spec)!: `GroupByNode.alias` is honoured by the SQL faces — one aggregate, one column key (#6401)
75
+
76
+ `GroupByNodeSchema` has declared `alias` ("Alias for the projected group
77
+ value", defaulting to `field`) for as long as the structured `groupBy` entry has
78
+ existed. Exactly one execution path read it. The result: the SAME query came
79
+ back with a different result-column key depending on which path the engine
80
+ happened to take.
81
+
82
+ ```ts
83
+ groupBy: [{ field: "closed_at", dateGranularity: "month", alias: "qtr" }];
84
+ ```
85
+
86
+ - pushed down to a driver ⇒ rows keyed **`closed_at`**
87
+ - run through the in-memory fallback ⇒ rows keyed **`qtr`**
88
+
89
+ And the choice between them is `engine.ts`'s
90
+ `allStructuredSupported && !tzRequiresInMemory` — a driver capability bit and a
91
+ `timezone`, neither of which the caller can see. That is the multi-face
92
+ consistency invariant broken in its quietest form: both answers are valid rows,
93
+ so nothing throws and nothing looks wrong.
94
+
95
+ **Resolved to ENFORCE**, and the leg was chosen by measurement rather than
96
+ taste. ADR-0049 splits on whether the feature already exists: a _dangling_
97
+ promise is removed, a _live_ one with a missing gate is enforced. `alias` is
98
+ live — three consumers read it and change behaviour
99
+ (`in-memory-aggregation.ts`, `MemoryDriver.performAggregation`, and
100
+ `chartAggregateCategoryKey`), and the publish gate _compels_ it:
101
+ `validate-react-page-props.ts` errors `REACT_CHART_AXIS_UNKNOWN` unless a
102
+ chart's category axis is bound to `alias ?? field`, telling the author in so
103
+ many words to "bind it to" the alias. A key the build gate makes you write is
104
+ not a dangling promise. The count of real non-test producers is **zero**, which
105
+ is what makes enforcing safe rather than what argues against it: no shipped
106
+ payload changes its result keys.
107
+
108
+ **What changed, on every SQL face at once** — a fix landing on one and not its
109
+ twin is the #6203 shape, and `TursoDriver` picks its face from `url`:
110
+
111
+ - **`driver-sql`** — both limbs of the structured `groupBy` branch project
112
+ `alias ?? field`: the date-bucket limb aliases the bucket expression to it,
113
+ and the plain limb emits `?? as ??` (only when the name actually moves — an
114
+ alias equal to the field emits no self-rename). `presentedOutput` is now keyed
115
+ by the OUTPUT column, matching how the aggregation branch beside it has always
116
+ worked; an aliased group value went unpresented before.
117
+ - **`driver-turso` REMOTE** — the same projection, `"field" AS "alias"`. The
118
+ alias reaches the statement as a quoted identifier and is therefore held to
119
+ `assertSafeIdentifier`, exactly like `field`.
120
+ - **`driver-sqlite-wasm`** — inherits `SqlDriver`'s compiler; covered by its own
121
+ conformance suite rather than by assumption.
122
+
123
+ **GROUP BY still keys on the FIELD** on every face. Only the projection is
124
+ renamed, so the buckets are unchanged. This is deliberate and pinned: SQLite
125
+ resolves output names in `GROUP BY`, so a face that grouped by the alias would
126
+ look correct here and diverge on a dialect that does not.
127
+
128
+ `having` needed no change and now means one thing: it is applied over the
129
+ aggregated row's own columns, so a filter on a group projection references the
130
+ alias on every path — previously the alias on one path and the field on the
131
+ other.
132
+
133
+ **Conformance.** `AGGREGATION_CASES` (#6409) gains a `groupByAlias` axis and two
134
+ cases. Their VALUES are an existing case verbatim — only the key moves — so they
135
+ can fail only on the key, which is the point: every wrong answer in this area is
136
+ a valid query returning plausible rows. `objectql`'s in-memory fallback is now
137
+ **enrolled** as a fourth face, answering #6409's open question ②: it is the face
138
+ the SQL three were converged onto, so the new behaviour would otherwise be
139
+ pinned against nothing, and reaching it needs no engine at all —
140
+ `applyInMemoryAggregation` is a pure function of rows and an AST.
141
+
142
+ **Reverse verification**, predicted before running. Reverting the in-memory face
143
+ to `g.field`: only the two alias cases move and only ONE fails — the degenerate
144
+ `alias === field` case stays green, which is why both are in the table.
145
+ Reverting the harness to read `c.groupBy` instead of `c.groupByAlias ?? c.groupBy`
146
+ — the copied-neighbour mistake: everything passes on an unmodified face, a false
147
+ GREEN, which is the failure mode that would have made the axis vacuous.
148
+
149
+ **Frozen drivers (#5499), measured from source, not flipped.** `driver-memory`
150
+ already returned `{ field, alias: node.alias ?? node.field }` and projects under
151
+ the alias — it had independently reached the enforce answer, so it needed no
152
+ alignment. `driver-mongodb` is a recorded DEBT row and the defect is wider than
153
+ `alias`: `buildAggregationPipeline` types `groupBy` as `string[]` and builds
154
+ `groupId[field] = '$' + field`, so a structured node — aliased or not — becomes
155
+ the literal key `"[object Object]"`. It cannot take a structured `GroupByNode`
156
+ at all; `mongodb-driver.ts` passes `(query as any).groupBy`, which is why `tsc`
157
+ never saw it. Tracked on #6814.
158
+
159
+ **Compatibility.** A caller who writes `alias` and reads the result under
160
+ `field` on a pushdown path will now find the value under `alias` — which is what
161
+ the key has always meant on the fallback path, and what the chart gate already
162
+ required. Callers who never write `alias` are unaffected: the emitted SQL is
163
+ byte-identical.
164
+
165
+ <!-- adr-0087: not-required (no-migration-prescription) Nothing is retired: `GroupByNodeSchema.alias` keeps its declaration, its spelling and its type — it starts being HONOURED by three faces that parsed and ignored it. There is no tombstone to write and no authored metadata to rewrite, so there is no mechanical transform a migration could prescribe: every stack that validated before validates after, unchanged. The behaviour change is in the RESULT of a runtime query (a result-column key moves from `field` to `alias` on the pushdown path, converging on what the in-memory path and the chart publish gate already required), which the ledger has no channel for and no upgrader could apply a codemod to. The bang is on the changeset because callers who read that column by the field name must move, and the measured non-test producer count for the key is zero. -->
166
+
167
+ - 82397b6: feat(drivers,objectql): `$regex` / `$options` are refused everywhere, and `$icontains` is implemented on the SQL family (#5702)
168
+
169
+ The driver half of the #4706 ruling. #5701 landed the contract (the vocabulary,
170
+ the `RETIRED_FILTER_OPERATORS` prescriptions, the shared text case-set) and
171
+ #5710 flipped the last live producer — `plugin-auth`'s ObjectQL adapter, which
172
+ emitted `$regex` on the authentication path — so the refusal can now land
173
+ without breaking sign-in.
174
+
175
+ **BREAKING for anyone writing `$regex` or `$options` in a filter.** Both are
176
+ refused on every backend with `INVALID_FILTER` / 400 and a message that names
177
+ the replacement. `$regex` was never a declared operator: `driver-sql` compiled
178
+ it to a LIKE-escaped substring (so `a.b` matched only the literal `a.b`),
179
+ `driver-memory` ran it as a real `RegExp` (so the same filter also matched
180
+ `axb`, and an _invalid_ pattern was caught and answered `false` — zero rows, in
181
+ silence), and `objectql`'s `having` did the same. Write `$icontains` for the
182
+ case-insensitive substring search this was almost always used for, `$contains`
183
+ for a case-sensitive one; a pattern that genuinely needs a regex has no
184
+ filter-level replacement.
185
+
186
+ **`$icontains` now runs on the SQL family** — `driver-sql`, `driver-sqlite-wasm`,
187
+ and both of `driver-turso`'s transports (the remote one does not go through
188
+ knex, so it needed its own). It compiles to `LOWER(col) LIKE LOWER(?) ESCAPE ?`
189
+ through the same `applyLike` / `pushLike` that carries the `%` / `_` / `\`
190
+ escaping, as a `fold` parameter rather than a second emitter — a copied emitter
191
+ is where the escape class would have been dropped, and an unescaped `%` matches
192
+ every row. An empty or non-string comparand is refused on the validating walk
193
+ (an empty one matches every row, which widens rather than narrows). On SQLite
194
+ `lower()` folds ASCII only, which IS the contract (#4706 Q1 = A): `$icontains:
195
+ 'café'` does not match `CAFÉ`.
196
+
197
+ <!-- adr-0087: registered filter-regex-options-retired -->
198
+
199
+ `driver-mongodb`'s unknown-operator arm was throwing a bare `Error` with no
200
+ `code` and no `status`, three lines from the helper in its own file that sets
201
+ `INVALID_FILTER` / 400 — a 500-shaped body for a 400-class client mistake. It
202
+ now speaks the same envelope as its three siblings.
203
+
204
+ Two parts of the ruling are deliberately NOT in this change and stay tracked in
205
+ `scripts/check-driver-conformance.mjs`'s ledger: the `$contains` family's
206
+ case-sensitivity (#4706 Q2 = A) needs SQLite's `LIKE` replaced by a case-exact
207
+ construct in the driver, the RLS lowering and the analytics lowering together,
208
+ or one permission rule compiles to two row sets (#6518); and `$icontains` on the
209
+ JS evaluation faces needs the spec vocabulary to take the operator, which cannot
210
+ happen before `driver-memory` has an arm for it (#6520).
211
+
212
+ - 3172831: fix(drivers): text-operator case folding is the CONTRACT's answer, not the dialect's (#6518)
213
+
214
+ The `$contains` family and `$icontains` returned **different rows on different
215
+ databases** for the same filter, because case sensitivity was decided by whatever
216
+ `LIKE` happened to mean on the dialect underneath. Both directions **over-matched**
217
+ — they returned rows the filter excludes, which on an ADR-0021 RLS read scope is
218
+ over-reach rather than a loose filter (#3948):
219
+
220
+ | | `$contains` / `$notContains` / `$startsWith` / `$endsWith` — case-SENSITIVE (#4706 Q2 = A) | `$icontains` — folds ASCII ONLY (#4706 Q1 = A) |
221
+ | :--------------------------- | :----------------------------------------------------------------------------------------- | :--------------------------------------------- |
222
+ | SQLite / turso / sqlite-wasm | ❌ `LIKE` folds ASCII | ✅ `lower()` is ASCII-only |
223
+ | Postgres | ✅ `LIKE` is case-exact | ❌ `LOWER()` folds all of Unicode |
224
+ | MySQL | ❌ follows the column's collation | ❌ `LOWER()` folds all of Unicode |
225
+
226
+ Read across: **each dialect was already right on the half another one got wrong**,
227
+ which is why neither half could be found from one backend alone.
228
+
229
+ ## What now runs
230
+
231
+ The construct is chosen per dialect, in one emitter, so the escaping and the fold
232
+ stay a single code path (an unescaped wildcard is a filter bypass, P0 — #5567):
233
+
234
+ - **SQLite family → `GLOB`.** `LIKE`'s ASCII fold cannot be switched off per
235
+ statement (`PRAGMA case_sensitive_like` is connection-global, so one query would
236
+ redefine every other query on the connection), and `CAST(col AS BLOB) LIKE ?` was
237
+ measured to match _nothing at all_. `GLOB` is case-exact and brings its own
238
+ escaped class — `*`, `?`, `[` as the self-closing classes `[*]`, `[?]`, `[[]`,
239
+ because SQLite's grammar gives `GLOB` no `ESCAPE` clause. `$icontains` keeps
240
+ `lower()` on both operands, still ASCII-only.
241
+ - **Postgres → `LIKE`, unchanged.** Only the fold moved, from `LOWER()` to an
242
+ explicit `translate()` over the 26 ASCII letters. Measured on a live PostgreSQL
243
+ 16 (ICU database): `LOWER('CAFÉ')` is `'café'` — the over-fold — while the
244
+ `translate()` form leaves `É` alone.
245
+ - **MySQL → `LIKE` over `CAST(… AS BINARY)`**, so the comparison is byte-wise and
246
+ no collation decides the case; `$icontains` folds byte-wise over the same binary
247
+ rendering, which is ASCII-only because UTF-8 is self-synchronising.
248
+ - **Any other client** keeps the previous `LIKE` / `LOWER()` shape — it is the only
249
+ form that still runs there — and is recorded as residue rather than left to be
250
+ discovered.
251
+
252
+ `driver-turso`'s remote transport carries the twin (it compiles filters itself and
253
+ inherits nothing), and the two transports are now held to the same rows by a
254
+ parity suite that runs the shared `FILTER_TEXT_CASES` on both.
255
+
256
+ ## Behaviour change — read this before upgrading
257
+
258
+ A filter whose comparand's case did not match the stored text used to match on
259
+ SQLite/turso/sqlite-wasm and may have matched on MySQL. It no longer does:
260
+
261
+ ```ts
262
+ // rows: { id: '1', name: 'ACME Corp' }, { id: '2', name: 'acme corp' }
263
+ {
264
+ name: {
265
+ $contains: "acme";
266
+ }
267
+ } // was ['1','2'] on SQLite → now ['2'] everywhere
268
+ {
269
+ name: {
270
+ $icontains: "acme";
271
+ }
272
+ } // ['1','2'] — unchanged, and now correct on PG/MySQL too
273
+ {
274
+ name: {
275
+ $icontains: "café";
276
+ }
277
+ } // was ['3','4'] on PG/MySQL → now ['4'] everywhere
278
+ ```
279
+
280
+ If you were relying on `$contains` to ignore case, **write `$icontains`** — that is
281
+ the operator for it, and it now folds the same ASCII-only range on every backend.
282
+ Result sets only ever get NARROWER, never wider, so a filter that was already
283
+ correct stays correct.
284
+
285
+ ## Why `minor` rather than `major`
286
+
287
+ No declared surface moves. `$contains` still exists, still takes the same
288
+ comparand, and `filter.zod.ts` is untouched — the case-sensitivity this delivers
289
+ was **already published** as the contract by #5701 (`FILTER_TEXT_CASES`, one
290
+ release earlier in this same v17 major), and the drivers were the half that had
291
+ not caught up. This is Prime Directive #12 applied in the direction it points:
292
+ declared = enforced. It is graded the way its sibling #5702/#6549 was graded for
293
+ the same operator family in the same rc cycle, and it registers nothing in the
294
+ ADR-0087 registries because it retires no authorable key.
295
+
296
+ ## What is deliberately NOT in this change
297
+
298
+ `driver-memory` and `driver-mongodb` still fold case on their query paths — they
299
+ are the #5499 frozen family, so their `FILTER_TEXT_CASES` cells stay honest DEBT
300
+ and are tracked as #6682 (case sensitivity) and #6520 (`$icontains`). The
301
+ `service-analytics` SQL compilers were measured already compliant: they emit
302
+ Postgres-shaped statements, where `LIKE` is case-exact, and that assumption is now
303
+ written down and pinned rather than implied.
304
+
305
+ ### Patch Changes
306
+
307
+ - bee5ffe: drivers: every SQL read door routes through the tenant chokepoint (#6792)
308
+
309
+ `SqlDriver.applyTenantScope()` owns read-side tenant isolation for the whole SQL family —
310
+ the `tenantId` early-out, the "object has no tenant field" early-out, the NULL-org
311
+ platform-row rule (#2734) and the ADR-0105 D2 union posture (#3623). Its own docstring
312
+ said "every CRUD method routes through it". Nothing ever checked that, and it was false
313
+ for as long as it had existed. **Three** read doors built their query through
314
+ `getBuilder()` and never arrived:
315
+
316
+ - **`findWithWindowFunctions()`** — the documented #4286 window door. It returns **rows**,
317
+ so on a deployment where the scope would have applied (`options.tenantId` set, object
318
+ has a tenant field) it returned rows belonging to **every** tenant. Measured with two
319
+ tenants seeded plus one NULL-org platform row: `tenantId: 'org_a'` returned
320
+ `[a1, a2, b1, b2, p1]` here against `find()`'s `[a1, a2, p1]` — another tenant's rows,
321
+ handed over at the driver layer.
322
+ - **`analyzeQuery()` / `explain()`** — returns a **plan**, not rows, so this is a smaller
323
+ fix and it is made on its own merits rather than folded into the one above. It is the
324
+ same defect #6577 fixed on these two methods one builder line lower: a plan is only
325
+ worth reading if it explains the statement `find()` would actually run, and a missing
326
+ tenant predicate changes selectivity and therefore which index the planner picks.
327
+ Compiled `select * from account` where `find()` sent the `organization_id` clause.
328
+ - **`distinct()`** — returns one column's **values** for every tenant. This one was in no
329
+ card. #6792 states the opposite, listing `distinct` among the scoped call sites; the
330
+ 13th read site is `aggregate()`. It was found by measuring the invariant rather than
331
+ re-reading it.
332
+
333
+ All three now call `applyTenantScope()` beside their `getBuilder()` line, the position
334
+ `findRows()` uses. They route through the chokepoint rather than re-deriving a predicate:
335
+ a local equality would silently drop NULL-org platform rows (#2734) and collapse group
336
+ reads to active-org reach (#3623). Both of the chokepoint's early-outs are inherited
337
+ unchanged, so an unscoped admin/seed read (no `tenantId`) and any object without a tenant
338
+ field behave exactly as before.
339
+
340
+ **The durable half is a gate, not the three lines.** `pnpm check:tenant-chokepoint`
341
+ (`scripts/check-tenant-chokepoint.mjs`, wired into `.github/workflows/lint.yml`) re-derives
342
+ the invariant from the AST across the `SqlDriver` family on every run: a method that builds
343
+ through `getBuilder(object, options)` must call `applyTenantScope()` on that builder, or
344
+ carry a written exemption. Insert builders are exempt structurally — write-side tenancy is
345
+ `injectTenantOnInsert` — rather than by a name list. It is keyed on the **builder** and not
346
+ on the method signature, because the signature criterion the card sketches ("takes
347
+ `(object, …, options)` and returns rows") misses `distinct` (no `query` parameter) and
348
+ `analyzeQuery` (returns a plan). Verified red against the pre-fix tree, red against a
349
+ newly-added unscoped door, and silent once that door is scoped.
350
+
351
+ The chokepoint docstring no longer asserts the invariant; it names the gate that proves it.
352
+
353
+ If you call these doors directly on a multi-tenant deployment, pass `options.tenantId` as
354
+ you would to `find()` — that is what now takes effect. Callers that never passed it are
355
+ unaffected; that remains the documented unscoped/admin path.
356
+
357
+ - Updated dependencies [3d5c090]
358
+ - Updated dependencies [e5bd768]
359
+ - Updated dependencies [e027b3e]
360
+ - Updated dependencies [c2429b0]
361
+ - Updated dependencies [445a0c2]
362
+ - Updated dependencies [f6609e6]
363
+ - Updated dependencies [a70358a]
364
+ - Updated dependencies [97e7e3c]
365
+ - Updated dependencies [8828b9e]
366
+ - Updated dependencies [53068c1]
367
+ - Updated dependencies [ee58392]
368
+ - Updated dependencies [f16e54e]
369
+ - Updated dependencies [06be54e]
370
+ - Updated dependencies [29e28a3]
371
+ - Updated dependencies [259459d]
372
+ - Updated dependencies [3f7f14e]
373
+ - Updated dependencies [6968885]
374
+ - Updated dependencies [eaed61f]
375
+ - Updated dependencies [debe2f6]
376
+ - Updated dependencies [97b0798]
377
+ - Updated dependencies [43a7a8d]
378
+ - Updated dependencies [73f69dc]
379
+ - Updated dependencies [04c56aa]
380
+ - Updated dependencies [b3efeb7]
381
+ - Updated dependencies [ddd075a]
382
+ - Updated dependencies [88154be]
383
+ - Updated dependencies [db12b88]
384
+ - Updated dependencies [6f6fec7]
385
+ - Updated dependencies [7d1ff75]
386
+ - Updated dependencies [e8dc61e]
387
+ - Updated dependencies [2f3e793]
388
+ - Updated dependencies [d8e8d9c]
389
+ - Updated dependencies [94e749b]
390
+ - Updated dependencies [ea1d916]
391
+ - Updated dependencies [ae31a19]
392
+ - Updated dependencies [e0f300b]
393
+ - Updated dependencies [62b6a2f]
394
+ - Updated dependencies [5b4780b]
395
+ - Updated dependencies [a933452]
396
+ - Updated dependencies [8140915]
397
+ - Updated dependencies [7b48cf9]
398
+ - Updated dependencies [b5404f4]
399
+ - Updated dependencies [f764691]
400
+ - Updated dependencies [e120a5a]
401
+ - Updated dependencies [e650d67]
402
+ - Updated dependencies [04476e7]
403
+ - Updated dependencies [79228cd]
404
+ - Updated dependencies [b3363e9]
405
+ - Updated dependencies [2ef1807]
406
+ - Updated dependencies [d03fe25]
407
+ - Updated dependencies [2672f85]
408
+ - Updated dependencies [11066f6]
409
+ - Updated dependencies [916af17]
410
+ - Updated dependencies [84c86fb]
411
+ - Updated dependencies [2a2a9fb]
412
+ - Updated dependencies [a2e157c]
413
+ - Updated dependencies [95c4227]
414
+ - Updated dependencies [2a61116]
415
+ - Updated dependencies [d4df105]
416
+ - Updated dependencies [d367f03]
417
+ - Updated dependencies [45e711a]
418
+ - Updated dependencies [465a0fa]
419
+ - Updated dependencies [6de592c]
420
+ - Updated dependencies [d254421]
421
+ - Updated dependencies [e2798fa]
422
+ - Updated dependencies [0fd8556]
423
+ - Updated dependencies [74155c7]
424
+ - Updated dependencies [6908830]
425
+ - Updated dependencies [8b06bba]
426
+ - Updated dependencies [4c54037]
427
+ - Updated dependencies [0f7157b]
428
+ - Updated dependencies [d9bef45]
429
+ - Updated dependencies [f549a0d]
430
+ - Updated dependencies [82da264]
431
+ - Updated dependencies [f586f1a]
432
+ - Updated dependencies [9b9b70f]
433
+ - Updated dependencies [f5a9bc2]
434
+ - Updated dependencies [881a3cc]
435
+ - Updated dependencies [ad6317b]
436
+ - Updated dependencies [8a88885]
437
+ - Updated dependencies [5f7669e]
438
+ - Updated dependencies [becbe53]
439
+ - Updated dependencies [b127c8b]
440
+ - Updated dependencies [a80302a]
441
+ - Updated dependencies [474f131]
442
+ - Updated dependencies [050cd82]
443
+ - Updated dependencies [4d552af]
444
+ - Updated dependencies [44d677c]
445
+ - Updated dependencies [c32944d]
446
+ - Updated dependencies [1dd780f]
447
+ - Updated dependencies [c8d6f6e]
448
+ - Updated dependencies [92a67f2]
449
+ - Updated dependencies [9136327]
450
+ - Updated dependencies [bf0ae99]
451
+ - Updated dependencies [cb3b6cd]
452
+ - Updated dependencies [73b7234]
453
+ - Updated dependencies [d2b97c3]
454
+ - Updated dependencies [59b794f]
455
+ - Updated dependencies [fc3a36a]
456
+ - Updated dependencies [69787f0]
457
+ - Updated dependencies [5d022a1]
458
+ - Updated dependencies [042b9ee]
459
+ - Updated dependencies [f549a0d]
460
+ - Updated dependencies [a36db28]
461
+ - Updated dependencies [3f8817a]
462
+ - Updated dependencies [a2443e3]
463
+ - Updated dependencies [e1554b1]
464
+ - Updated dependencies [4856789]
465
+ - Updated dependencies [c3f4916]
466
+ - Updated dependencies [33e0385]
467
+ - Updated dependencies [2205363]
468
+ - Updated dependencies [09fe58d]
469
+ - Updated dependencies [d0a5ceb]
470
+ - Updated dependencies [ef678d0]
471
+ - Updated dependencies [e18a162]
472
+ - Updated dependencies [d6d1a50]
473
+ - Updated dependencies [d127ff0]
474
+ - Updated dependencies [9b86cf6]
475
+ - Updated dependencies [8825a06]
476
+ - Updated dependencies [5087ac6]
477
+ - Updated dependencies [2d1ddf0]
478
+ - Updated dependencies [354b00f]
479
+ - Updated dependencies [3de535b]
480
+ - Updated dependencies [fe2e15a]
481
+ - Updated dependencies [6146b67]
482
+ - Updated dependencies [c6b6bb4]
483
+ - Updated dependencies [2f59da0]
484
+ - Updated dependencies [8ad609c]
485
+ - Updated dependencies [bbee302]
486
+ - Updated dependencies [08863dd]
487
+ - Updated dependencies [56664f5]
488
+ - Updated dependencies [31cbe90]
489
+ - Updated dependencies [90bbf25]
490
+ - Updated dependencies [eb91eba]
491
+ - Updated dependencies [42da73d]
492
+ - Updated dependencies [643b7c7]
493
+ - Updated dependencies [d0d5205]
494
+ - Updated dependencies [1a15893]
495
+ - Updated dependencies [b70e534]
496
+ - Updated dependencies [2233a85]
497
+ - Updated dependencies [62dd69a]
498
+ - Updated dependencies [e15e679]
499
+ - Updated dependencies [2ab1257]
500
+ - Updated dependencies [4cc4fb7]
501
+ - Updated dependencies [28d1eb7]
502
+ - Updated dependencies [2c26040]
503
+ - Updated dependencies [f758cec]
504
+ - Updated dependencies [78f0be8]
505
+ - Updated dependencies [35f7fb4]
506
+ - Updated dependencies [a5302c7]
507
+ - Updated dependencies [82397b6]
508
+ - Updated dependencies [7084313]
509
+ - Updated dependencies [0e043d8]
510
+ - Updated dependencies [dadd1ad]
511
+ - Updated dependencies [2f2e63c]
512
+ - Updated dependencies [486d526]
513
+ - Updated dependencies [89d7b35]
514
+ - Updated dependencies [85ec26d]
515
+ - Updated dependencies [f6476fc]
516
+ - Updated dependencies [4ac12ef]
517
+ - Updated dependencies [b88f5e8]
518
+ - Updated dependencies [42cc219]
519
+ - Updated dependencies [d7e0b42]
520
+ - Updated dependencies [3510e4a]
521
+ - Updated dependencies [aa4b90d]
522
+ - Updated dependencies [54299ca]
523
+ - Updated dependencies [3264516]
524
+ - Updated dependencies [dc61def]
525
+ - Updated dependencies [251e888]
526
+ - Updated dependencies [183b4c4]
527
+ - Updated dependencies [2fdb36e]
528
+ - Updated dependencies [62159bd]
529
+ - Updated dependencies [d48aad5]
530
+ - Updated dependencies [20526f5]
531
+ - Updated dependencies [c5eef1d]
532
+ - Updated dependencies [e0f300b]
533
+ - Updated dependencies [761a0ba]
534
+ - Updated dependencies [be87153]
535
+ - Updated dependencies [60f0dd8]
536
+ - Updated dependencies [a87c5cd]
537
+ - Updated dependencies [a47f338]
538
+ - Updated dependencies [bee5ffe]
539
+ - Updated dependencies [3172831]
540
+ - Updated dependencies [939f579]
541
+ - Updated dependencies [2598216]
542
+ - Updated dependencies [2c7e62d]
543
+ - Updated dependencies [eb7613c]
544
+ - Updated dependencies [ecc9110]
545
+ - Updated dependencies [f7bd4e2]
546
+ - Updated dependencies [361bd5b]
547
+ - Updated dependencies [1818998]
548
+ - Updated dependencies [09ee21c]
549
+ - Updated dependencies [f549a0d]
550
+ - Updated dependencies [3fc2e48]
551
+ - Updated dependencies [e8f435c]
552
+ - Updated dependencies [41610f6]
553
+ - @objectstack/spec@17.0.0-rc.6
554
+ - @objectstack/driver-sql@17.0.0-rc.6
555
+ - @objectstack/core@17.0.0-rc.6
556
+
3
557
  ## 17.0.0-rc.5
4
558
 
5
559
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/driver-sqlite-wasm",
3
- "version": "17.0.0-rc.5",
3
+ "version": "17.0.0-rc.6",
4
4
  "license": "Apache-2.0",
5
5
  "description": "WASM SQLite Driver for ObjectStack — runs in browser/WebContainer (StackBlitz) without native bindings",
6
6
  "keywords": [
@@ -26,9 +26,9 @@
26
26
  "knex": "^3.3.0",
27
27
  "nanoid": "^6.0.0",
28
28
  "sql.js": "^1.14.1",
29
- "@objectstack/core": "17.0.0-rc.5",
30
- "@objectstack/driver-sql": "17.0.0-rc.5",
31
- "@objectstack/spec": "17.0.0-rc.5"
29
+ "@objectstack/core": "17.0.0-rc.6",
30
+ "@objectstack/driver-sql": "17.0.0-rc.6",
31
+ "@objectstack/spec": "17.0.0-rc.6"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^26.1.2",