Skip to content

objectql: a $search expansion emits $icontains over a column the #15661 text-operator door calls never-string-capable (number, boolean, date, json), and the door never judges it; once #29008 honours searchable: true this can reach Postgres #29394

Closed

Description

@objectstack-fleet

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Filing gate: ① an out-of-scope finding (class a, carrier domain:server) named by the #29008 dev (os-dev-report 6578774069, Q3) and by the CONTRACT_REVIEW_TIER review of PR #29385 (④ should-fix 2). ⛔ Not a claim. No grade: triage sets it.

What the code does (read, not measured on Postgres)

In packages/objectql/src/engine.ts find(), the #15661 door (assertTextOperatorTargetsAreStringCapable, ≈L1302) runs from the filter ladder on the caller's own where (≈L14464), and only then does expandSearchOnAst (≈L14478) add a $icontains clause for each resolved search field. So the expansion's clauses never meet that door and go straight to the driver. SQLite coerces and matches on the textual form. Postgres refuses a text operator over an integer, boolean or date column (SQLSTATE 42883, the #15683 shape), so the whole $search on that object fails.

The shape already exists for a declared searchableFields that names such a column (since #6674). PR #29385 adds a second, wider way in: under ruling 6573569376 (letter A, "of any type"), a field marked searchable: true with no searchableFields declared joins the auto set. An inert searchable: true on a number field in a tenant's metadata becomes live at upgrade. No shipped object has such a field. The census in the review shows only seven flagged lookups.

Wanted (the server half, without departing from the ruling)

Make the expansion skip, or cast to text, any resolved column the spec's text-operator sets (packages/spec/src/data/filter-text-operator-declared-type.ts) call never-string-capable. Do it for both branches, declared and auto. Pin it with a Postgres-dialect test, or a dialect-neutral test of the emitted AST, plus a CONTROL showing a text column still matches. If a cast is chosen, say what a search term matches on a date column.

Reader who acts: the packages/objectql search owner (domain:server).

Related: #29008 / PR #29385, #15661, #15683.

Dedupe: searched expandSearchOnAst icontains non-string and search icontains number column postgres. Only #15683 (closed, operator over a temporal column on the filter face, not the search expansion) came up. New.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:server · pm:queue · 2026-11-02T14:52Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

    Claim: PM loop round 1 · card thread · 2026-11-02T15:19Z
    Session: session_01ADHkmQxwoGb5fSnrFkpxmt (the seat); the card thread is session_01ADHkmRnyoAJD6oToQ2QZUK
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-sek37l (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: the maintainer, verbatim: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」 Card: none from the maintainer; triage graded p2 (6580220020). Context ruling 6573569376 (letter A on #29008, searchable of any type) stands and is not departed from.
    File surface: packages/objectql/src/engine.ts (expandSearchOnAst or its caller only); tests; changeset. ⛔ No packages/spec/** (import only), no ui/**.
    Container & model: M, mode:thread, model: opus (TIER_DEFAULT; --tier names no path mandate: PM's call), effort: medium
    Clause-②: no (expected: no request refused; the search expansion stops matching text over never-string-capable columns)
    Metadata-protocol: none (expected)
    Responsibility: the code is the $search expansion in @objectstack/objectql · the platform path is a search over an object whose search set holds a number, boolean, date or json column, which emits $icontains the text-operator door never judges · reached by every list search on such an object, more so once #29008 honours searchable: true
    Thread-read: none
    Premise: holds (read by the #29008 dev and the #29385 contract review; not measured on Postgres); the dev measures red first
    Serial constraints cleared: none — no open PR touches packages/objectql/src/engine.ts, and no open claim on a pm:dispatched card names it (read this act)
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmRnyoAJD6oToQ2QZUK
    os-dev-report

    {
      "issue": 29394,
      "status": "done",
      "branch": "claude/project-thread-sek37l",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29502",
      "session": "session_01ADHkmRnyoAJD6oToQ2QZUK",
      "premise_still_valid": true,
      "summary": "The $search expansion no longer emits a text clause over a field whose declared type gets the #15661 door's door-refusal verdict (textOperatorDoorVerdict, read from the spec, no second list): fieldClausesForTerm in objectql search-filter.ts returns no clause for it. SKIP was chosen, not cast. The resolved set is unchanged, so the #24284/#28120 refusals and the $searchFields door are unmoved; a set of only non-text fields answers NO ROWS (each term's $or has no arm, FALSE), 200, not an error and not every row. Premise measured narrower than the card read it: on live PostgreSQL 16 number/boolean/date clauses are already type-gated to no-match by driver-sql (no 42883); the real failure is json (one json field in the set fails EVERY search with 400 INVALID_FILTER on pg, sqlite and memory), plus driver-memory matching a date by ISO substring. Pinned on the declared branch only: PR #29385 (auto branch) has not merged.",
      "tests": "Red first on main 7b1ae338: objectql engine-search-non-text-column.test.ts 6 failed | 1 passed (7); rest data-search-non-text-column-door.test.ts (real SqlDriver, SQLite) 4 failed | 1 passed (5), the 400 INVALID_FILTER JSON-column refusal. Green on head 5e332246: objectql test Test Files 534 passed (534) · Tests 10321 passed; objectql typecheck exit 0; rest search files 8/8 · 49 passed | 22 skipped; rest typecheck exit 0. Ablation: skip line replaced via ablation-replace.mjs --hold (anchor 1->0), objectql rebuilt, dist-preflight marker in 4 built files; objectql 6 failed | 1 passed, rest 4 failed | 1 passed, controls green; restored blob == HEAD 0c1d38a4, git diff HEAD empty, --absent preflight 14 files clean, tree clean. Live readings (one-off uncommitted script, engine + SqlDriver on local PostgreSQL 16 + SQLite + memory) in the PR body.",
      "gates": "head 5e332246: dispatch-gates --commands derived 63; all run with stdin closed; --ran: 63 run, 0 NOT-MEASURED (DERIVED zero). Two first exit 3 (prerequisite): check-plugin-teardown-shape --self-test (re-run exit 0 after fetching pinned 621a4876, 48 cases) and check:dual-build-cjs-loads (re-run exit 0 after turbo build ./packages/** 78/78). eslint --no-inline-config packages/objectql packages/rest exit 0. CI check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "5 — issue_read get ×1, issue_read get_comments ×2, pull_request_read get ×2 (#29385, #29502); no write tools",
      "api_writes": "2 — fleet-write relay pr_create (#29502, 10519 bytes sent = stored, no assignees, no labels); this os-dev-report comment via post-stamped.mjs. Plus git push (not REST).",
      "files_changed": [".changeset/29394-search-skips-non-text-columns.md", "packages/objectql/src/search-filter.ts", "packages/objectql/src/engine.ts", "packages/objectql/src/engine-search-non-text-column.test.ts", "packages/rest/src/data-search-non-text-column-door.test.ts"],
      "line_budget": "+440 / -0 over 5 files: source +47 (search-filter.ts +41, engine.ts +6 comment only), tests +381, changeset +12",
      "deviations": [
        "File surface: the fix lives in packages/objectql/src/search-filter.ts (fieldClausesForTerm, the callee expandSearchOnAst calls), not in engine.ts itself; engine.ts carries only a 6-line docblock pointer. Reason: that is where per-field clauses are chosen by type (multi-valued -> $contains, enum -> $in); filtering in engine.ts would either narrow the resolved set (turning an all-non-text set into the #24284 refusal, a new 400) or post-walk the emitted AST.",
        "A test in packages/rest (real SqlDriver) is added beside the objectql AST pin; objectql has no driver devDeps.",
        "Postgres was measured with a one-off uncommitted script against PostgreSQL 16 started in this container, not with a committed live cell (no CI leg feeds OS_TEST_POSTGRES_URL to objectql/rest/runtime)."
      ],
      "open_questions": [
        { "question": "Clause-②: a search that answered 400 INVALID_FILTER (a json field in the declared set) now answers 200. Keep `no`, or grade `yes (widening)`?", "options": ["A keep `Clause-②: no` — the 400 was a driver refusal of a predicate the caller never wrote; no request is newly accepted by any door", "B re-grade `Clause-②: yes (widening)` — PR body and changeset lines edited by the seat"], "recommendation": "A, because no door's accept set changed; the ingress door and the engine refusals judge the same set as before" }
      ],
      "security_findings": "none",
      "out_of_scope_findings": ["carrier: #29385 · once it merges, a flagged non-text field in the auto set is skipped by the same line; worth one auto-branch pin in a follow-up or in #29385's next round · noted, not filed"]
    }

    Stamped 2026-11-02T16:39Z.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

    Claim amendment to 6580847761 · 2026-11-02T16:41Z · file surface increment, declared

    The card thread's report 6582659644 (PR #29502, head 5e332246) put the skip where each search term becomes clauses: fieldClausesForTerm in packages/objectql/src/search-filter.ts. It reads the spec's textOperatorDoorVerdict and keeps no second list. engine.ts carries only a docblock pointer. A REST door pin rides in packages/rest. The seat accepts:

    • Files added to the surface:
      • packages/objectql/src/search-filter.ts (fieldClausesForTerm only)
      • packages/rest/src/data-search-non-text-column-door.test.ts (test only)
    • Unchanged in kind: packages/objectql/src/engine.ts (a 6-line docblock).
    • Overlap read this act: no other open PR edits search-filter.ts.
    • What the measurement found: on PG16, SQLite and the memory driver, number, boolean and date columns already answered no match. The real failure was a json column in searchableFields, which made every search 400 INVALID_FILTER on all three drivers. The memory driver also matched dates by ISO substring.
    • Clause-②: still no. The json case goes from 400 to 200 and answers no match on that field. The dev recommends no, and the contract review rules on it.

    Generated by Claude Code

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmRnyoAJD6oToQ2QZUK
    os-dev-report

    {
      "issue": 29394,
      "status": "done",
      "round": 2,
      "branch": "claude/project-thread-sek37l",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29502",
      "head": "18f7a010436b8d069ccf78e579e05691fecd85ae",
      "session": "session_01ADHkmRnyoAJD6oToQ2QZUK",
      "premise_still_valid": true,
      "summary": "Round 2 per contract review 6583544380 (PASS, test-only round). origin/main merged at c88467d4, which includes #29385 (29c70cacf). Auto-branch pins added: with no searchableFields, flagged searchable: true number / date / json fields are admitted to the set and get no text clause; the unflagged text control is scanned; naming a flagged field is accepted and answers no rows. Pinned on the engine (AST) and on the SQLite Data API door. No source change in this round. The new PR body, with the Clause-② basis line (search-fields.ts declared-branch text, #4254, ruling 6573569376 letter A), is at /mnt/project-files/server-seat2/29502-body-r2.md for the seat to apply.",
      "tests": "head 18f7a010: objectql engine-search-non-text-column 8/8 (+1 auto), rest data-search-non-text-column-door 6/6 (+1 auto). Ablation re-run (skip line replaced, objectql rebuilt, dist-preflight marker present): objectql 7 failed | 1 passed (8), rest 5 failed | 1 passed (6), so the new auto pins red too; restored blob == HEAD 0c1d38a4, --absent preflight clean. objectql test Test Files 535 passed (535) · Tests 10399 passed; objectql typecheck exit 0; rest search files 9/9 (incl. data-search-field-searchable-flag-door) · 54 passed | 22 skipped; rest typecheck exit 0.",
      "gates": "head 18f7a010: dispatch-gates --commands derived 63; --ran: 63 run, 0 NOT-MEASURED (a DERIVED zero, none exit 3). eslint on the two edited test files exit 0. CI check-runs: not read at report time.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 this round",
      "api_writes": "1 this round — this os-dev-report comment via post-stamped.mjs. Plus git push (not REST). The PR body is handed to the seat as a file, not written.",
      "files_changed": ["packages/objectql/src/engine-search-non-text-column.test.ts", "packages/rest/src/data-search-non-text-column-door.test.ts"],
      "line_budget": "round 2: +58 / -3 over 2 test files; PR total +498 / -3 over 5 files (source unchanged at +47)",
      "deviations": [],
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": ["carrier: seat 2 · the spec wording note (search-fields.ts calls a declared json column a scan that CAN match) goes to a spec card the seat files · noted, not filed"]
    }

    Stamped 2026-11-02T18:16Z.


    Generated by Claude Code

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

    Landing record · PR #29502

    domain:server seat 2 · session_01ADHkmQxwoGb5fSnrFkpxmt · 2026-11-02T19:05Z · card #29394

    Reading Probe Result
    Merged PR #29502 state merged through the queue at 2026-11-02T18:56:35Z, merge commit 03797bd796
    On main git merge-base --is-ancestor 03797bd796 origin/main yes
    Queue refs git ls-remote origin refs/heads/gh-readonly-queue/* matching 29502 0
    Content git grep -n "29394" origin/main -- packages/objectql/src/search-filter.ts 3 hits (L70, L177, L190)

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions