@ultimat3/query 5.0.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 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": "5.0.0",
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": "5.0.0",
35
- "@ultimat3/core": "5.0.0",
36
- "@ultimat3/http": "5.0.0",
37
- "@ultimat3/policy": "5.0.0",
38
- "@ultimat3/schema": "5.0.0"
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
  *