Skip to content

Commit 03580e7

Browse files
committed
chore(changeset): objectql minor, BREAKING, for the number arm's string half (#20309)
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
1 parent 94e0692 commit 03580e7

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
"@objectstack/objectql": minor
3+
---
4+
5+
fix(objectql)!: a number, currency, percent, rating, slider or progress field reads a string by the platform's numeric grammar and stores the number it denotes (#20309)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING**: shipped as `minor` under the launch-window convention
10+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
11+
this banner and the ADR-0087 disposition below, never by the level). The
12+
narrowing: a string that `Number()` reads as a finite number but the platform's
13+
numeric grammar does not is now refused with `400 VALIDATION_FAILED` /
14+
`invalid_number`. It used to be accepted.
15+
16+
**FROM → TO, and the one-line fix.** These strings, written to one of those
17+
fields, were accepted and are now refused, with nothing written:
18+
19+
- a radix literal: `'0x10'`, `'0X1A'`, `'0o17'`, `'0b101'`;
20+
- a whitespace-padded number: `' 12 '`, `'12\n'`, `'\t-3'`;
21+
- a spelling that is not a JSON number: `'+5'`, `'.5'`, `'5.'`, `'007'`.
22+
23+
Fix: send a JS number, or the number's plain JSON spelling — `'16'`, `'12'`,
24+
`'-3'`, `'5'`, `'0.5'`, `'7'`. `String(n)` of any finite number always
25+
qualifies, exponent forms included (`'1e-7'`, `'1e+21'`).
26+
27+
## What was wrong
28+
29+
This is the separate change the earlier #20309 note (arrays, booleans and
30+
objects refused) left open. The record validator judged a string by `Number()`
31+
while the write carried the string itself, so an accepted string reached the
32+
driver as sent:
33+
34+
- **memory** stored `'12'` as the string `'12'` and read it back as a string;
35+
- **SQLite** stored `'0x10'` as the TEXT `'0x10'` (read back as `16`), and the
36+
other accepted strings as numbers through the column's affinity.
37+
38+
One write, two stored shapes, depending on the backend.
39+
40+
## What changes
41+
42+
- A string is judged by `parseNumericString` from `@objectstack/spec/data`, the
43+
one numeric grammar the filter door also reads: the whole string is a JSON
44+
number literal naming a finite double. Its case table,
45+
`NUMERIC_STRING_GRAMMAR_CASES`, decides every form. No second grammar lives in
46+
the engine.
47+
- An admitted string is stored as the number it denotes, on every backend:
48+
`'12'` is written as `12`, `'1e3'` as `1000`. The rewrite runs at the write
49+
door, before the middleware, the hooks, the `readonlyWhen` locks and
50+
validation read the payload, so a `before*` hook now sees the number. The
51+
caller's own object is not mutated.
52+
- `min`, `max`, `scale` and `precision` read that number, exactly as they read
53+
a number: `'12.50'` passes `scale: 1` (it is `12.5`), and `'150'` over
54+
`max: 100` is `max_value`.
55+
- This holds on every engine, REST, batch and updateMany door, and in
56+
`validate` (the dry run). The server `/import` route is unchanged: its own
57+
cell reader turns a numeric cell into a number before the write, so the
58+
grammar never sees a string from it.
59+
- A blank is still `null` before the check (#20308). `summary` is still not
60+
judged. A number, and an array, boolean or object, are answered as before.
61+
62+
## Who sends numeric strings
63+
64+
objectui's CSV import wizard, on its legacy per-row fallback (`legacyImport`,
65+
used only when the connected client cannot reach the server `/import` route),
66+
posts each raw cell to `create` after a client check of
67+
`!isNaN(Number(value))`. Its parser trims cells, so of the refused forms it can
68+
send the radix literals and the non-JSON spellings. Those rows now fail with
69+
`invalid_number` instead of storing a string. The fix there is the wizard's
70+
default path: import through the server `/import` route, whose cell reader
71+
converts the number before the write. Every interactive form widget sends a JS
72+
number or `null`, and is unaffected.
73+
74+
## Rows already stored
75+
76+
This judges new writes only; a stored value is never re-read by the check. On
77+
memory, an accepted string stayed a string until the record is next written.
78+
On SQLite, the earlier #20309 note's query finds a numeric column holding TEXT
79+
(such as `'0x10'`):
80+
81+
```sql
82+
SELECT id, "FIELD" FROM "OBJECT" WHERE typeof("FIELD") = 'text';
83+
```
84+
85+
OBJECT is the object name and FIELD is the field name. Nothing here rewrites
86+
such a cell; decide its number by hand.
87+
88+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authored moves: `packages/spec` is untouched and no metadata key is added, removed or reshaped, so `objectstack migrate meta` has nothing to rewrite and the ledger has no row to gain. What narrows is the set of caller-written string VALUES a number-typed field accepts at the write door, judged by the spec's existing numeric grammar; stored rows are never re-read, and a caller that sends a number, a blank or a grammar-admitted string is unaffected. The other categories are closed on facts: the package publishes (not `unpublished`); no ADR-0087 id covers a write-door value check (not `registered` / `already-registered`); and the change is runtime behaviour, not a TypeScript declaration (not `runtime-interface-only` / `type-surface-only`). -->

0 commit comments

Comments
 (0)