@ultimat3/query 4.1.0 → 5.0.1
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/CLAUDE.md +20 -1
- package/package.json +6 -6
- package/src/matcher.ts +36 -0
package/CLAUDE.md
CHANGED
|
@@ -21,7 +21,7 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
|
|
|
21
21
|
| `naming.ts` | export name → `/_x/query/<kebab>`. Pure string math. **Paths only** — no tool name |
|
|
22
22
|
| `registry.ts` | export-name registration, `describeQueries()`, and the `registerPrimitiveRegistrar('query', …)` announcement |
|
|
23
23
|
| `live.ts` | `LiveQuery` descriptor + cursor arithmetic |
|
|
24
|
-
| `matcher.ts` | change event → minimal patch, or `X_MATCHER_UNSUPPORTED` |
|
|
24
|
+
| `matcher.ts` | change event → minimal patch, `refill` when the window cannot place the row, or `X_MATCHER_UNSUPPORTED` |
|
|
25
25
|
| `pagination.ts` | `paginate()` over core's cursor codec — no offset, ever |
|
|
26
26
|
| `cursor-value.ts` | what a sort value becomes inside a cursor, and what it becomes again |
|
|
27
27
|
| `input-shape.ts` | what a read's `input:` may be, given that its route is a query STRING |
|
|
@@ -445,3 +445,22 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
|
|
|
445
445
|
bun test packages/query
|
|
446
446
|
bun run typecheck
|
|
447
447
|
```
|
|
448
|
+
|
|
449
|
+
**A position is decided against the rows the WINDOW holds, so a window that cannot answer gets a
|
|
450
|
+
`refill` rather than a guess** — `unprojectedOrderKey`, `As of 2026-08-20`. A result set is whatever
|
|
451
|
+
the query's `sql` returned, and a PROJECTION that drops an ordering column left `compareRows`
|
|
452
|
+
measuring the change row's real value against nothing: never equal on the update path, so every
|
|
453
|
+
change read as a move, and arbitrary on the insert path, so a row created last landed wherever
|
|
454
|
+
`undefined` sorted. `examples/dummy`'s feed ordered by `createdAt` and projected without it, and one
|
|
455
|
+
publish became a `remove` + `insert` whose re-inserted row was the raw table row (#230).
|
|
456
|
+
|
|
457
|
+
The discriminator is **`Object.hasOwn`, never a value check**: a nullable column that IS null still
|
|
458
|
+
carries its key, and everywhere else in this package an absent key and a SQL NULL are one absence
|
|
459
|
+
(`isNull` says so). Here they are different facts — "this row's value is nothing" versus "this shape
|
|
460
|
+
cannot answer" — and only the key tells them apart. Asked of the row the window holds and never of
|
|
461
|
+
the change row, which comes off the table and can always answer. A **delete** never reaches the rule:
|
|
462
|
+
it is addressed by the index its id was found at, so a projected query still removes incrementally.
|
|
463
|
+
|
|
464
|
+
The rule generalises what `assertSeekable` already applies to a cursor: **a sort key has to be
|
|
465
|
+
readable on the row.** A live query whose rows omit one still works — it re-reads instead of
|
|
466
|
+
patching — which is correct and slower, and the fix is to project the key.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/query",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/cache": "
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/http": "
|
|
37
|
-
"@ultimat3/policy": "
|
|
38
|
-
"@ultimat3/schema": "
|
|
34
|
+
"@ultimat3/cache": "5.0.1",
|
|
35
|
+
"@ultimat3/core": "5.0.1",
|
|
36
|
+
"@ultimat3/http": "5.0.1",
|
|
37
|
+
"@ultimat3/policy": "5.0.1",
|
|
38
|
+
"@ultimat3/schema": "5.0.1"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/matcher.ts
CHANGED
|
@@ -64,6 +64,23 @@ export function match<TRow extends object>(
|
|
|
64
64
|
return inSet ? removeAt(shape, index, id, true, rows.length) : [];
|
|
65
65
|
}
|
|
66
66
|
if (!belongs) return [];
|
|
67
|
+
|
|
68
|
+
// Everything below this line decides a POSITION, and a position is decided against the rows the
|
|
69
|
+
// window HOLDS. A result set is whatever the query's `sql` returned, so a PROJECTION that drops an
|
|
70
|
+
// ordering column leaves `compareRows` measuring the change row's real value against nothing —
|
|
71
|
+
// never equal on the update path, so every change read as a move, and arbitrary on the insert
|
|
72
|
+
// path, so a new row landed wherever `undefined` sorted. In `examples/dummy` that turned one
|
|
73
|
+
// publish into a `remove` + `insert` whose re-inserted row was the raw table row (#230).
|
|
74
|
+
//
|
|
75
|
+
// Refusing to decide is the honest answer, and `refill` already means it: "the window is a guess,
|
|
76
|
+
// re-read it". The node turns it into one read plus a re-snapshot. A DELETE never reaches here —
|
|
77
|
+
// it is addressed by the index the id was found at, which needs no ordering — so a projected
|
|
78
|
+
// query still removes rows incrementally.
|
|
79
|
+
const sample = rows[0];
|
|
80
|
+
if (sample !== undefined && unprojectedOrderKey(shape, sample) !== undefined) {
|
|
81
|
+
return [{ kind: 'refill', from: 0 }];
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
if (!inSet) return insert(shape, rows, event.row);
|
|
68
85
|
|
|
69
86
|
// Present and still matching: a change to an ordering column is a move, not an update.
|
|
@@ -110,6 +127,25 @@ function insert<TRow extends object>(
|
|
|
110
127
|
return patches;
|
|
111
128
|
}
|
|
112
129
|
|
|
130
|
+
/**
|
|
131
|
+
* The first `orderBy` column the result set's own rows do not carry, or `undefined` when every one
|
|
132
|
+
* of them is readable there.
|
|
133
|
+
*
|
|
134
|
+
* `Object.hasOwn`, never a value check: a nullable column that IS null still has its key, and
|
|
135
|
+
* `isNull` deliberately treats an absent key and a SQL NULL as one absence everywhere else. Here
|
|
136
|
+
* they are different facts — "this row's value is nothing" versus "this shape cannot answer" — and
|
|
137
|
+
* only the key tells them apart.
|
|
138
|
+
*
|
|
139
|
+
* Asked of the row the WINDOW holds, never of the change row: the change row comes off the table
|
|
140
|
+
* and carries every column, so it can always answer. What decides is whether the client's copy can.
|
|
141
|
+
*/
|
|
142
|
+
export function unprojectedOrderKey(shape: QueryShape, held: object): string | undefined {
|
|
143
|
+
for (const key of shape.orderBy) {
|
|
144
|
+
if (!Object.hasOwn(held, key.column)) return key.column;
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
113
149
|
/**
|
|
114
150
|
* `held` is how many rows the window actually holds, and it is the whole condition on the refill.
|
|
115
151
|
*
|