Repository navigation
fix(objectql): $search emits no text clause over a column that can never hold text - #29502
Conversation
…le column The #15661 text-operator door judges only the caller's own where, before expandSearchOnAst runs, so the expansion's $icontains over a declared number / boolean / date / json search field reached the driver unjudged. One json field in a declared searchableFields failed every search on the object with 400 INVALID_FILTER (PostgreSQL, SQLite, memory alike), and driver-memory matched a date by ISO substring where the SQL faces answer no-match. fieldClausesForTerm now skips a field whose declared type gets the door's own door-refusal verdict (textOperatorDoorVerdict, read from the spec, no second list). The resolved set is unchanged, so the #24284 / #28120 refusals and the $searchFields door are unmoved; a set of only such fields expands to an $or with no arm, FALSE: no rows, never an error. Claude-Session: https://claude.ai/code/session_01ADHkmRnyoAJD6oToQ2QZUK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 20 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a28a7e732bbee61294b55f468c3eb00a96575d4f && git checkout a28a7e732bbee61294b55f468c3eb00a96575d4f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a92277937217769906d70fc86591b457d7f052b5 18f7a010436b8d069ccf78e579e05691fecd85ae && git checkout -B drift-repro a92277937217769906d70fc86591b457d7f052b5 && git merge --no-ff 18f7a010436b8d069ccf78e579e05691fecd85ae
node scripts/docs-audit/affected-docs.mjs --json a92277937217769906d70fc86591b457d7f052b5 |
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Contract reviewServed-tier: Inputs: card #29394 (body, triage 6580220020, claim 6580847761, amendment 6582701047, os-dev-report 6582659644), PR #29502 body and net diff against ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentNot owed on the declaration face (no
CI on the head ( Implemented-by: VERDICT: PASS |
…true) #29385 landed: with no searchableFields, a field marked searchable: true joins the auto set of any type. A flagged number / date / json field is admitted to the set and still gets no text clause; the text control is scanned, and naming a flagged field is accepted and matches nothing. On the engine (AST) and the SQLite Data API door. Claude-Session: https://claude.ai/code/session_01ADHkmRnyoAJD6oToQ2QZUK Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt 席位核对ACCEPT · There is no maintainer ruling on this card; triage graded it p2 (6580220020). It does not depart from the context ruling 6573569376 (letter A on #29008, searchable of any type).
Generated by Claude Code |
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt
Requested by Steve · project thread
Fixes #29394
Clause-②: no
Metadata-protocol: none
Clause-② basis (contract review 6583544380): nothing the contract refuses is newly accepted. The declared-branch contract, in the
resolveSearchFieldResolutiondocblock ofpackages/spec/src/data/search-fields.ts, admits a declaredjsoncolumn as the author's explicit choice ("executed by the engine as a$containsover the stored JSON text"). #4254 puts the loudness for the search set at the ingress, which judges the same set before and after this change. Ruling 6573569376 letter A admits a field markedsearchable: true"of any type". The400 INVALID_FILTERthat a json field caused was a driver mis-refusal of a declared, admitted request, so turning it into a 200 is not a widening.维护者速读
$search展开成"每个可搜索字段一个模糊文本匹配"时,不再为 number / boolean / 日期时间 / json 这类永远存不了文本的字段生成匹配;文本、选项、关联、文件字段照旧。searchableFields里只要有一个 json 字段,该对象上的每一次搜索都会 400 失败(实测 PostgreSQL、SQLite、内存驱动都一样);内存驱动还会按日期的文本命中(搜2026搜到 2026 年的日期),和 SQL 驱动不一致。packages/objectql/src/search-filter.ts的neverHoldsText与fieldClausesForTerm第一行;②packages/rest/src/data-search-non-text-column-door.test.ts(真实 SQLite 驱动)。pnpm --filter @objectstack/rest exec vitest run src/data-search-non-text-column-door.test.ts,预期 6/6 通过:$search=qq9返回 200 和两行,不再是 400 INVALID_FILTER。What changes
packages/objectql/src/search-filter.ts.fieldClausesForTermreturns no clause for a field whose declared type gets the [Decision] refuse a text operator ($containsfamily) over a field whose DECLARED type is not textual —INVALID_FILTER400 at the engine's field-aware door (option C of #14079); the textual-type vocabulary is the question #15661 door'sdoor-refusalverdict. The verdict istextOperatorDoorVerdictfrom@objectstack/spec/data, read and not re-listed: the numeric, boolean, calendar-date, instant, clock-time and structured-JSON classes, plus aformulawhose readablereturnTypenames one. The module header records why.resolveSearchFieldsstill answers it whole, so the metadata-protocol / objectql: asearchterm on an object with no searchable field is dropped silently, so the read answers every row (an empty auto-default search set is not refused) #24284 empty-set refusal, the runtime: the stored-metadata reader seam still reads an in-processsearchFields: []as "names none" and serves the narrowed default set #28120 override refusal and the$searchFieldsingress door judge exactly the set they judged before. A set holding ONLY such fields expands to an$orwith no arm for each term. That is FALSE (FILTER_LOGIC_CASES, "empty $or is FALSE"), so the search answers no rows, with 200, and is neither an error nor every row. A$searchFieldsnaming a declared non-text column is still accepted and matches nothing through it.packages/objectql/src/engine.ts. A comment in theexpandSearchOnAstdocblock points at the skip. No code changes there.@objectstack/objectqlpatch, with the claim's two lines at line start.searchableFieldsbranch and the auto branch. feat(spec): a field marked searchable: true is searched when its object declares no searchableFields #29385 has merged (searchable: truejoins the auto set, of any type), and both are pinned. A flaggednumber/date/jsonfield is admitted to the set and gets no clause, and its text control is still scanned.Tests
packages/objectql/src/engine-search-non-text-column.test.ts(new, dialect-neutral pin on the AST the driver receives). It declares a search set over one field perFieldType(the spec'sTEXT_OPERATOR_DOOR_FIXTURE) and asserts that the door's own walk (findTextOperatorOverNonTextField) finds nothing in the emitted search. Its CONTROL is thatf_text,f_select,f_lookupandf_tagsare still scanned. Further cases: a mixedname/qty/born/active/metaset scans onlyname. The terms42,2026andtruematch nothing. A multi-term search carries onlynamein each term's$or. A set of only non-text fields reaches the driver as{ $or: [] }and answers 0 rows onfindandcount, and a blank term still answers every row. AsearchFieldsnamingqtyanswers no rows with no refusal, andqty,namematches throughname. CONTROL: an override outside the set is still400 INVALID_FIELD. Auto branch: with nosearchableFields, flaggedsearchable: truenumber/date/jsonfields get no clause, the unflagged textnameis still scanned (rows r1 and r2), and naming a flagged field answers{ $or: [] }, no rows, with no refusal.packages/rest/src/data-search-non-text-column-door.test.ts(new) runs the Data API door, GET and POST, over a realSqlDriver(SQLite), on the harness ofdata-search-fields-credential-door.test.ts. It covers the auto branch too: a flaggednumber/date/jsonfield with nosearchableFieldsanswers 200 through the text column, and$searchFieldsnaming the flagged json field is accepted and matches nothing. CONTROL: the caller's own{ qty: { $contains: '4' } }is still400 INVALID_FILTERfrom the [Decision] refuse a text operator ($containsfamily) over a field whose DECLARED type is not textual —INVALID_FILTER400 at the engine's field-aware door (option C of #14079); the textual-type vocabulary is the question #15661 door.Acceptance notes
driver-sql's compile-time type gate (driver-memory's reference matcher answers$notContainsNO for every valued NON-STRING row — the live mingo path answers YES #14079 / text operator over a temporal column: SQLite matches the stored ISO text, live Postgres refuses with SQLSTATE 42883, and the JS faces answer by stored representation — no face declares the cell #15683) already turns those clauses into no-match. The real failure isjson: the SQL family anddriver-memoryboth refuse a text operator on a JSON column, so onejsonfield in the set fails every search on the object. The other divergence isdriver-memory, which matched a date stored as text by ISO substring. Readings are below.OS_TEST_POSTGRES_URL) is wired in CI only for driver-sql and metadata-protocol, and an engine-level live cell would need the isolation scaffolding ofcascade-delete-multivalue-lookup-real-driver.integration.test.ts. So the committed pins are the dialect-neutral AST pin and the real SQLite door. The Postgres readings come from a one-off, uncommitted engine plusSqlDriverscript run against a local PostgreSQL 16 in this container.400 INVALID_FILTER(a json field in the set) now answers 200. No request is refused that was accepted. The 400 was a driver refusal of a predicate the caller never wrote, so the claim'sClause-②: nois kept. The seat may re-grade it.Verification
Head
18f7a010436b8d069ccf78e579e05691fecd85ae, a clean merge withorigin/mainatc88467d4(it includes #29385). Round 2 (contract review 6583544380) is test-only: it adds the auto-branch pins and changes no source.Red first on main (7b1ae33), then green on the fix commit bc2269f (the head's full suites are below)
Live readings: engine + SqlDriver on PostgreSQL 16 and SQLite, and the in-memory driver (one-off script, not committed)
Rows:
r1has nameqq9-alpha, qty 42, born 2026-01-05, active true and meta{k:'qq9'};r2has nameother. Each object declaressearchableFields: ['name', X].The
beforecolumn for pg and sqlite was taken under the ablation build below, because the first main run's SQL inserts were refused for a missing organization row and so held no rows. The 400 cells read the same with rows absent. The memorybeforecells come from the first main run, where the memory inserts landed.Ablation: the skip removed, rebuilt, proven in dist, then restored
scripts/ablation-replace.mjs --holdreplacedif (neverHoldsText(meta)) return [];with a guard that never fires, carrying the string markerABLATION_29394_SKIP_OFF(anchor 1 → 0, blob0c1d38a4→8ede2261). Afterpnpm --filter @objectstack/objectql build,ablation-dist-preflight.mjsfound the marker in 4 built files.Every control stayed green in both rounds. Restore (each round): the blob after restore equals HEAD (
0c1d38a4) andgit diff HEADis empty. After a rebuild,--absentfound the marker absent from all 14 built files and the tree clean.Suites, typecheck, lint and derived gates on the head 18f7a01
Generated by Claude Code