Skip to content

Commit effb34a

Browse files
committed
Merge origin/main into claude/issue-20426-reclaim-space-wal-checkpoint
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
2 parents 10300f7 + b285508 commit effb34a

57 files changed

Lines changed: 4075 additions & 725 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
`@objectstack/spec/data` declares the platform's numeric grammar for a string, and the contract of the number-comparand declared-type door: which string comparands a field whose declared type is numeric may be compared against (#20336).
6+
7+
Clause-②: yes
8+
9+
**The grammar.** A string is numeric when its whole content is a JSON number literal (`-?(0|[1-9][0-9]*)(.[0-9]+)?([eE][+-]?[0-9]+)?`) that names a finite number; it then means what the same characters mean as a JSON number. `NUMERIC_STRING_PATTERN`, `parseNumericString(s)` (the number, or `undefined`) and `readNumericString(s)` (the number, or which of eight named forms the string is: `empty`, `padded`, `placeholder`, `radix-prefix`, `non-finite`, `digit-separator`, `non-json-spelling`, `not-a-number`). `NUMERIC_STRING_GRAMMAR_CASES` records every form with the reason it is admitted or refused: `"12"`, `"-3"`, `"12.5"`, `"1e3"` and every string `String(n)` produces for a finite number are admitted; `""`, `" 12 "`, `"0x10"`, `"Infinity"`, `"NaN"`, `"1,000"`, `"+5"`, `".5"`, `"007"` and any `{placeholder}` are not.
10+
11+
**The door's contract.** `numberComparandDoorVerdict(field, comparand)` answers, for a comparand at a value position (implicit equality, `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte`, and each member of `$in` / `$nin` / `$between`) of a filter on a field whose declared type is in `NUMERIC_VALUE_TYPES` (or a `formula` whose `returnType` is `number`): `door-refusal` (`INVALID_FILTER` / 400) for a non-numeric string, `narrows` with the number for a numeric one, `passes` for any other comparand or field, `deferred` for a `formula` without a readable `returnType`. `numberComparandRefusalMessage(site, context?)` is the refusal the door prints: the field, its declared type, the comparand, its position and what is wrong with it. A fixture object and a derived case table (`NUMBER_COMPARAND_DOOR_FIXTURE`, `NUMBER_COMPARAND_DOOR_CASES`) are published for the engine suite that pins the door.
12+
13+
**What moves for consumers.** Nothing yet. This is the contract only, and no door reads it in this release: a non-numeric string compared with a number field still reaches the driver as written (a 500 on PostgreSQL, an empty or different result elsewhere). The engine door that refuses it with `INVALID_FILTER` / 400 and narrows a numeric string lands with #20351, and the record validator's number arm adopts the same grammar for writes with #20309.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
'@objectstack/formula': minor
4+
'@objectstack/driver-sql': patch
5+
'@objectstack/lint': patch
6+
'@objectstack/spec': patch
7+
---
8+
9+
fix(security)!: the RLS write check refuses a field-to-field comparison the read refuses — one comparison class, one answer per policy (#20355)
10+
11+
Clause-②: yes (narrowing)
12+
13+
<!-- adr-0087: registered rls-predicate-cross-class-field-comparison-refused -->
14+
15+
**BREAKING** — an accept-set narrowing on the row-level write check, shipped as `minor`
16+
under the launch-window convention (`check-changeset-no-major` refuses `major` until GA;
17+
breaking-ness is carried by this banner and the ADR-0087 disposition above, not by the
18+
level). The hand-migration prescription is registered under protocol major 18 as
19+
`rls-predicate-cross-class-field-comparison-refused`, one ADR-0087 D3 entry for the whole
20+
family: the authoring arm `os validate` gained in #20347 and this write-check arm.
21+
22+
**What changed.** A row-level policy that compares two fields of no shared comparison
23+
class — `record.status != record.amount` (text and a number), `record.status !=
24+
record.photo` (text and a file field), `record.status != record.is_open` (text and a
25+
formula field), `record.status != record.meta` (text and a json field) — already had
26+
every read it scopes refused with `INVALID_FILTER` / 400 on the SQL drivers, because
27+
driver-sql compiles a column-to-column comparison only within one class. The write
28+
check did not know the rule: it compared the two raw values in-process, so an insert
29+
or update the policy's `check` judges (or its `using`, standing in as the check) was
30+
admitted and stored whenever that comparison happened to hold. Measured through
31+
plugin-security and ObjectQL on SQLite, sqlite-wasm and PostgreSQL. The write check
32+
now refuses the comparison too, with the read's envelope, `INVALID_FILTER` / 400, for
33+
every insert (single or array), by-id update and predicate update it judges, and
34+
nothing is stored. The same-class comparisons it always compared are compared as
35+
before. The 400 names no column of the policy; the server log names the policy and
36+
both columns. A comparison against a json or `multiple` field is refused by its
37+
declared type now, where it used to be judged by the value each record held.
38+
39+
**`@objectstack/formula`.** `matchesFilterCondition(record, filter, options?)` takes an
40+
optional third argument: `options.fields`, the object's declared columns (`type` and
41+
`multiple` per field name). Given it, every `{ $field }` comparison between two
42+
declared columns is judged by `crossFieldComparisonVerdict` from
43+
`@objectstack/spec/data` before any record is read, and one the platform defines no
44+
answer for throws `INVALID_FILTER` / 400. Without it the evaluator behaves exactly as
45+
before. Two new exports go with it: `findCrossFieldClassRefusal(filter, fields)`, the
46+
pure judgement, and `crossFieldClassRefusalCarriedBy(error)`, which reads the refused
47+
comparison off the error for a server-side log.
48+
49+
**`@objectstack/driver-sql`.** `crossFieldComparisonClass` reads the same export
50+
(`crossFieldColumnVerdict`) instead of keeping its own copy of the classification, and
51+
layers above it only its internal type aliases. Every read answers as before.
52+
53+
**`@objectstack/lint`.** The `rls-predicate-unenforceable` finding for such a
54+
comparison now states the write answer the runtime gives: the in-process write check
55+
refuses it by the same classification and stores nothing.
56+
57+
**If a policy of yours is refused.** The platform defines no comparison between those
58+
two columns on any path, so the policy never protected a read either. Compare a field
59+
only with a field of the same class — a number with a number, text with text, a
60+
boolean with a boolean, a date with a date, a datetime with a datetime, a time of day
61+
with a time of day — or, if the two columns do hold comparable values, correct the
62+
declaration of the one declared with the wrong type. `os validate` names every such
63+
comparison.

0 commit comments

Comments
 (0)