Repository navigation
fix(fields): no empty-string member for "is empty" on a numeric or boolean column (objectui#10813) - #10834
fix(fields): no empty-string member for "is empty" on a numeric or boolean column (objectui#10813)#10834objectstack-fleet[bot] wants to merge 3 commits into
Conversation
…ean or temporal column (objectui#10813)
Step 2 of objectui#10813. FilterConditionField wrote "is empty" as
`{ $or: [{ F: { $in: [''] } }, { F: { $null: true } }] }` and "is not empty"
as `{ F: { $nin: [''], $null: false } }` for every field type, so a number,
date or boolean column received `IN ('')` / `NOT IN ('')`, which the SQL
driver binds as-is. A field in the spec's numeric, boolean, calendar-day,
instant or time-of-day value class now gets the `$null` half alone; every
string-stored column, and a field of unknown type, keeps today's bytes.
Reading back is unchanged: every stored shape the builder opened before
still opens on the same operator, and nothing is rewritten on read.
Claude-Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY
Co-authored-by: Claude <noreply@anthropic.com>
…i#10813) vitest reads `$null` in an `it.each` title as the row's `null` key and printed "writes the undefined half alone". Claude-Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY Co-authored-by: Claude <noreply@anthropic.com>
…(objectui#10813) Measured: an edit form sends a cleared date, date-time or time box as `''` (a cleared number box as `null`), and the platform stores `''` unchanged where a temporal column is text, which the spec names a dialect question. Dropping the `''` member on those columns would change which records a stored rule matches on such a backend, which the card's stop valve rules out. The `$null`-only shape now covers the numeric and boolean value classes alone; the temporal classes are carried back to the card as an open question. Claude-Session: https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEY Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: 187/187 Read: PR objectui#10834 (body, 4-file list, diff; head ① Derived judgments1. No stored rule re-scoped (the card's hard line) — WRONG. A numeric or boolean column CAN hold
2. Which field types fall in each bucket — partly RIGHT, and WRONG on consistency with the hard line. The widget's 3. Read-back — RIGHT. 4. Pending changesets — WRONG (secondary defect). 5. Pins — RIGHT. On the merge-base the two arms are unconditional ( ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL |
|
Closed unmerged by the The at-tier review above ( The fix order this points to (producer and write door first, then the widget) goes back to the card with the readings. The branch Generated by Claude Code |
Part of #10813
Clause-②: no
This lands step 2 of the card's execution notes and stops at the card's stop valve, so it leaves the card open: step 3 (one meaning of 「is empty」 across
FilterConditionField, the dataset bridge and the spec) waits for triage's routing with the readings below.datasetFilterCondition.tsis untouched.What changed
FilterConditionField(@object-ui/fields): 「is empty」 / 「is not empty」 on a field whose type is in@objectstack/spec'sNUMERIC_VALUE_TYPESorBOOLEAN_VALUE_TYPESnow write{ FIELD: { $null: true } }/{ FIELD: { $null: false } }, with no''member. The classes are imported from@objectstack/spec/data, not hand-listed.{ $or: [{ FIELD: { $in: [''] } }, { FIELD: { $null: true } }] }/{ FIELD: { $nin: [''], $null: false } }): the string-stored types (text, select, lookup, …),date/datetime/time(see the deviation below), and a field whose type the widget does not know (schema not loaded, or a hidden field).is_null/is_not_nullwrite, so a reopened rule shows 「Is null」 / 「Is not null」, which is the same predicate on such a column.NON_TEXT_STORED_VALUE_TYPES: in the installed spec it is exactly numeric ∪ boolean, but it belongs to the text-operator gate, and the spec's main line has widened it to the temporal classes by ruling. Adopting it would silently move temporal columns onto$nullon a spec upgrade.Readings (a) (b) (c)
(a) Cleared text is stored as
'', and the objectstack write path does not normalise it.ObjectFormedit of a record withstage: 'open', text input cleared and submitted. Each of the flat, modal and drawer routes calleddataSource.update('deal', 'd1', {"stage":""}, {ifMatch}). The hops, by symbol:TextFieldonChange(e.target.value)→formWritePayload→sanitizeFormData(no''arm) →dirtyEditPayload/isSameStoredValue("''is NOT a blank here") →ObjectStackAdapter.updateforwardsdataverbatim.{"due_on":""}for a cleared date,{"met_at":""}for datetime,{"starts":""}for time, and{"amount":null}for number.17bd3187(read only, no probe: the shared checkout has no builtobjectql/ driverdist):isMissingcounts''as missing for validation only and never rewrites it;encryptSecretFields,refuseEmptyPasswordFields);formatInputhas no''arm, andtemporalStorageFormreturns''unchanged by design ("Total, on purpose");VALUE_ROUNDTRIP_CASESrowstr_emptypins "empty string must not become null", and the memory / sql / sqlite-wasm / turso conformance suites run it.(b)
numeric_col IN ('')/date_col IN ('')on Postgres: not measured, because no Postgres is reachable.psqlclient and PG 16 server binaries (/usr/lib/postgresql/16/bin) are present but not running, and the dispatch forbids starting one.''into those columns.$in→whereIn(field, coerced), and$nin→applyNullSafeNegative(… orWhereNotIn …).coerceFilterValuereturns a non-temporal value unchanged, and a temporal one throughtemporalStorageForm, which hands''back unchanged. The engine's temporal comparand door scopes''out by ruling (isUninterpretableTemporalComparand: "The empty-string cell is its own card").assertOperatorAppliesToColumnrefuses only on JSON columns.integer/decimal(numericColumnFor),boolean/togglebecomeboolean,datebecomesdate,datetimebecomestimestamp, andtimebecomestime.invalid input syntax for type integerfor a non-numeric string bound into an integer column on insert.(c) Stored sharing rules / roll-up filters carrying the widget's shape.
33e58d8and 0 in objectstack @17bd3187.examples/has 0 hits in both repos: the example sharing rules use a CELcondition, and no example roll-up has a filter.Valve verdict: it trips on (a), and on (c) under ruling P2 (production is unreadable, so it counts as not cleared). The PR stops after step 2.
Deviation from the dispatch's example list (measured)
lookupkeeps''.REFERENCE_VALUE_TYPESis stored as "a record-id string". Driver-sql makeslookup/useravarchar(table.string), so no cast is involved. The engine'sisEmptyReferenceValuereads''as "no link" and stores it unchanged, so dropping''there would re-scope stored rules.date/datetime/timekeep''. The form sends a cleared temporal box as''(probe above). The spec calls their stored form a dialect question (ISO TEXT on SQLite), and on SQLite the''is kept. Dropping the member would change which rows a rule matches there, which is the P2 access-boundary change. This goes back to the card as an open question in the report. The tests pin it as a CONTROL row, so moving them later is a visible, deliberate change.Verification (at
3654509)pnpm --filter @object-ui/fields type-check: exit 0. The output echoes@object-ui/fields@17.6.0 type-check.tsc -p tsconfig.test.json --listFilesincludes both touched test files.pnpm exec vitest run --maxWorkers=2 packages/fields/ packages/app-shell/src/views/metadata-admin/inspectors/filter-builder-protocol-ids-census-9306.test.ts:Test Files 217 passed | 1 skipped (218),Tests 3662 passed | 7 skipped (3669). The census file is the only test outsidepackages/fieldsthat calls the emitter.check:spec-floors: the gate's ownanalyze()with the population set to@object-ui/fields, afterpnpm --filter @object-ui/fields build. It judged 7 (subpath, symbol) pairs against@objectstack/spec@17.0.0./data, with 0 findings, and the builtdist/index.jsimportsNUMERIC_VALUE_TYPES/BOOLEAN_VALUE_TYPES. The full gate needs every workspace package built, so its repo-wide run is left to CI. Every other package's floor is judged against its owndist, which this diff does not touch.check:spec-symbols,check:phantom-deps,check:new-line-citations(0 new),check:control-bytes,check:test-path-roots,check:vi-mock-specifiers/-inherit/-override-shape,check:pending-changeset-literals,check:changeset-claims,check:installed-pin-claims,changeset:check, andcheck-changeset-presence.mjs(1 changeset for 3 published source files).--no-inline-config) on the 3 touched source/test files:--format jsoncounts 3 files, 0 errors and 33 warnings. All 33 are pre-existing: the base version of the widget file gives the identical 33 by rule. Type-aware linting is not enabled ineslint.config.js, so this diff cannot move the verdict of an untouched file. The repo-widepnpm lintis left to CI.Ablation (one-off, no permanent file)
ablation-replace.mjs. The anchor hit exactly once (1 → 0), and the blob went2e41853e…→65d3c08c….FilterConditionField.typedEmpty-10813.test.tsx: 15 failed | 39 passed (54), against a baseline of 54/54. The 15 were predicted before the run and match: 4 WRITER rows (number and toggle × 2 operators), 9 CLASSES rows (numeric ∪ boolean), and 2 RE-SAVE rows.2e41853e…) andgit diff HEADis empty.Acceptance notes
multiselect/checkboxes/tags, andmultiple: truefields) are JSON columns in driver-sql. Its column-type gate refuses$in/$ninthere (JSON_COLUMN_INCOMPATIBLE_OPERATORS, pinned bysql-driver-compile-refusal-seam.test.ts), yet the widget still writes the''member for them.$nullalone is not the fix either, because a cleared multi-value field may store[]. That is a meaning question for the card.formulakeeps today's shape: its value class is its declaredreturnType, which this widget does not read..changeset/10790-empty-operators-accepted-shape.mdsays every column stores the$or/$ninshapes. After this change that holds for every column except numeric and boolean. Both changesets publish together, and this one names the narrowing. The 10790 file is left untouched.filter-builder.tsx's operator comment ("which also treat '' as empty") is now true only of columns that can store''. That file is PR objectui#10823's, so it is left untouched.Session for this change:
https://claude.ai/code/session_01MCg3z5cGrV51xEddpGcxEYGenerated by Claude Code