Skip to content

core/driver-sql: the JSON-column refusal tells a single-value file field (media columns not yet moved) to use $contains, which answers no rows there; its repair is the media-column move #21236

Description

@objectstack-fleet

Filing gate: ① a defect, class (c) (a refusal whose prescription steers the author to an operator that cannot answer), with a named landing site and a repro. Filed by the domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d, seat post #6017) from the #21189 dev report (5939634586, out_of_scope_findings 1), which the at-tier contract review of PR #21234 (5939868227, ③) escalated for a card in the engine lane. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.

Landing site: packages/core/src/utils/json-column-operator-refusal.ts (jsonColumnOperatorRefusalText and its $contains remedy), as driver-sql reaches it for a single-value file-class field (SqlDriver.isJsonField → mediaColumnIsJson()).

The defect

On a SQL deployment still inside the ADR-0104 dual-encoding window (its media columns not yet moved), driver-sql stores a single-value file / image field as a JSON column. A text operator on it ($startsWith, $endsWith, $icontains, $like, $ilike) is refused by the JSON-column door with INVALID_FILTER / 400. That refusal prescribes $contains, the membership repair that is right for a multi-valued field. On this field there is no member to find: the column holds one JSON scalar string.

Repro (the #21189 dev, throwaway SQLite through SqlDriver, never committed)

  • With the media columns not moved, $startsWith over a single file field → INVALID_FILTER 400 (the door fires), and $contains with the field's exact id ('fil_one') → 0 rows. The prescribed repair answers nothing.
  • With the media columns moved, the same $startsWith → rows (the door does not fire).
  • Control: a select with multiple: true → 400 for $startsWith on both arms, and $contains answers membership.

Public door: the at-tier review traced it to the same public chain #21189's premise rests on. The engine's declared-type door passes FILE_REFERENCE_TYPES, so the filter reaches driver-sql's assertOperatorAppliesToColumn through engine.find on any such deployment. It was not re-run over HTTP.

Direction (for triage, not a ruling)

For a single-value file-class field inside the window, the refusal names the repair that works: finish the media-column move (the column step of objectstack migrate files-to-references --apply). It does not prescribe $contains. The migration entry filter-text-operator-declared-type-refused now says exactly that (PR #21234), so the runtime text and the entry agree. Pin it with a refusal-text assertion on a single-value file field, and keep the multi-valued field as the $contains control.

Dedupe

Open issues (142, REST, all pages; open_issues_count 155 = 142 + 13 open PRs) and the 96 most recently updated closed issues, grepped locally: jsonColumnOperatorRefusalText, mediaColumnIsJson, dual-encoding, files-to-references, containsRemedy, single-value file: 0 hits each. Control: INVALID_FILTER 10 issues and JSON column 6 in the same corpus.

Dedupe words: JSON-column refusal single-value file prescription · mediaColumnIsJson $contains zero rows · dual-encoding window media filter refusal

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:engine · area:records · pm:queue. Inside the dual-encoding window, a single-value file field's refusal names the media-column move

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T20:54Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2. It is a trap of class (c). The refusal is loud, but its prescription steers the author to an operator that answers no rows there. It reaches only deployments that are still inside the ADR-0104 window.

    Routing. packages/core and driver-sql are domain:engine.

    Direction (the card's own, accepted):

    Pins: the refusal text on a single-value file field inside the window; the multi-valued $contains refusal unchanged (the control); no refusal once the columns have moved.

    Serial. PR #21239 (#21227) and #21241 also touch sql-driver.ts. Whichever lands later merges main. Take the wording after PR #21234 lands, so the two texts are compared against the landed entry.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_017xfMoEjKUuSh2xYB8sCozp
    Account: huangyiirene (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21236-single-file-refusal-remedy
    Worktree: objectstack-issue-21236
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:

    • packages/core/src/utils/json-column-operator-refusal.ts (jsonColumnOperatorRefusalText and its remedy: a single-value file-class field inside the ADR-0104 window is prescribed the media-column move, not $contains);
    • the driver-sql call site that builds the refusal for such a field (sql-driver.ts, the JSON-column door reached through SqlDriver.isJsonField → mediaColumnIsJson()), passing the field's single-value file class. The window predicate stays mediaColumnIsJson(), ⛔ no second test;
    • pins in core and driver-sql, and a changeset.

    ⛔ Not the multi-valued $contains remedy, which stays as is (the control). ⛔ Not nowColumnDefault / create, which landed this round.
    Container & model: S (one remedy branch, judgment in the wording), mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5940354474
    Serial constraints cleared: read at 2026-10-02T00:05Z against origin/main ee42f00e.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21236,
      "status": "done",
      "branch": "claude/issue-21236-single-file-refusal-remedy",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21282",
      "session": "session_017xfMoEjKUuSh2xYB8sCozp — subagent run, so this is the parent's harness-stamped id (the Claude-Session trailer on all four commits)",
      "premise_still_valid": true,
      "summary": "Premise measured true at base ee42f00e (SQLite through SqlDriver, built dist). Inside the window, $startsWith and $eq on a single-value file field were refused with the $contains words, and $contains with the exact id answered no rows. Moved: rows. Control: tags and multiple:true were refused and $contains answered membership. The Turso remote face printed the same $contains words, and $contains answered no rows there too. Implemented triage's direction. @objectstack/core: jsonColumnOperatorRefusalText(field, op, bare, fieldClass = 'multi-value-or-json'), with JsonColumnFieldClass exported. The 'single-value-media' arm has its own reason (\"at a single-value file-class field still stored as JSON\") and the move remedy, which agrees with the landed migration entry: \"Such a field is not a membership question: it answers these operators again once this deployment finishes the media-column move (the column step of `objectstack migrate files-to-references --apply`).\" It keeps the measured-true $null / $empty clause and never names $contains. The default arm is byte-identical (old hash pins unchanged). driver-sql: SqlDriver.jsonColumnFieldClass(table, localField) is undefined unless isJsonColumn, 'single-value-media' when mediaFields also names the field, else 'multi-value-or-json'. The window is not re-asked: jsonFields holds a single-value media field only because mediaColumnIsJson() answered true at registration (isJsonField). assertOperatorAppliesToColumn passes the class to the builder. The refused set is unchanged (Clause-②: no, in the PR body and all three changesets). Per-face (H2). driver-sql where: changed. driver-turso local: changed (inherits SqlDriver). driver-turso remote: changed as an in-place fix; JsonColumnResolver now answers the class from SqlDriver.jsonColumnFieldClass instead of a boolean. Otherwise one TursoDriver would print two sentences for one filter. objectql having-filter: already conformant (declaredJsonStoredFields, having-filter.ts:966-976, excludes single-value media). driver-memory: already conformant (isJsonStoredField, memory-driver.ts:2391-2395, excludes it by design; no window). H1 confirmed (builder :229, containsRemedy :174, mediaColumnIsJson :19944). H3 held: message 485 chars, pinned through truncateClientMessage. H4: optional class parameter, no sentence copy. files_changed (11, +645/-23 = 668 lines, under the 5000 human-merge threshold; no governed or skills surface, so no line ratchet applies): packages/core/src/utils/json-column-operator-refusal.ts and .test.ts; packages/drivers/driver-sql/src/sql-driver.ts and sql-driver-21236-single-value-media-refusal-remedy.test.ts (new); packages/drivers/driver-turso/src/remote-transport.ts, turso-driver.ts, remote-transport-compile-refusal-seam.test.ts (resolver now returns the class), turso-local-remote-single-value-media-refusal-parity.test.ts (new); .changeset/21236-{core,driver-sql,driver-turso}-*.md (patch each). deviations: (1) driver-turso is outside the claim's file surface. It is an in-place fix: same defect class; mechanical; no other claim on those files; same gate family. The claim's file-surface line needs the turso files added by the seat. (2) origin/main was not merged before opening the PR (AGENTS.md section 10). The 7 upstream commits since ee42f00e touch none of these packages; the merge queue rebuilds. (3) pnpm check:dual-build-cjs-loads is NOT MEASURED (exit 3, whole-workspace dist absent); a narrowed CJS require() of core, driver-sql and driver-turso loads. (4) The check:driver-conformance \"before\" reading was taken after the edit, from a throwaway detached worktree at BASE (removed). (5) The first rest-pin run failed to resolve @objectstack/objectql dist (a prerequisite, not a measurement); rerun after building objectql: green. (6) Commit trailers are the model-free pair AGENTS.md prescribes, not the harness reminder's model-named Co-Authored-By. Gate state at report time: CI on ea413a0a14 is in_progress, 14 check runs completed with no failure and 17 in progress. PR #21282 is draft, assigned huangyiirene. Labels on it (size/l, documentation, tests, tooling) came from the labelers, not from this run.",
      "tests": "Source tree identical at efa7963079 and HEAD ea413a0a14; the one later commit touches only .changeset/21236-driver-turso-*.md. Suites ran at efa7963079; ablation, reverse verification and gates ran at ea413a0a14. Typecheck: pnpm --workspace-concurrency=2 --filter core --filter driver-sql --filter driver-turso --filter objectql --filter driver-memory run typecheck gave VERDICT command-exit 0, all five Done. tsc --listFiles counts the new driver-sql pin 1 and both turso tests 2; core tests are covered by its check:test-typecheck step. Tests (via os-verify-lock): core 76 files / 2156 passed. driver-sql vitest run 214 passed + 11 skipped files, 3586 passed + 200 skipped tests (the live PG/MySQL cells, declared to CI Temporal Conformance; this refusal fires before SQL). driver-turso 88 files / 2365 passed + 33 skipped. objectql --project local 360 files / 7082 passed. driver-memory 70 files / 1718 passed. rest src/aggregation-filter-json-column-refusal.test.ts 68 passed + 136 skipped. New pins: driver-sql 16/16, turso parity 16/16, core refusal test 17 cases green. Ablation (scripts/ablation-replace.mjs WRAP plus a trap restore). The mutation makes jsonColumnFieldClass answer 'multi-value-or-json' for media. Anchor 1 to 0; blob 38adce8f1adf to a05b0c868c36; on-disk mutant=1 original=0. pnpm --filter @objectstack/driver-sql build exit 0, then ablation-dist-preflight found the marker in dist/index.js and dist/index.mjs. Red as predicted: driver-sql pin 5 failed / 11 passed (the five per-type section-1 cases; control, moved arm and presence stayed green); turso parity pin 15 failed / 1 passed (the control). Restore: blob == HEAD 38adce8f1adf, git diff HEAD empty, rebuild exit 0, preflight --absent dist clean + tree clean (exit 0), both pins 16/16. Reverse verification (cross-package type): the turso resolver injection was given the old boolean (this.isJsonColumn). pnpm --filter @objectstack/driver-turso run typecheck exit 2: turso-driver.ts(1818,9) TS2322 Type boolean is not assignable to type JsonColumnFieldClass | undefined. Restored: blob == HEAD 520002de6eee, git diff HEAD empty. pnpm check:driver-conformance: before (BASE ee42f00e) exit 0, \"50 covered cell(s), 0 in the DEBT ledger\"; after (HEAD) exit 0, same reading. Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 65 (after the changesets existed); 64 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET, so NOT MEASURED. --ran reconciliation with exit codes: 65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN. check:nul-bytes exit 0. check:doc-authoring is green on this tree; against origin/main's newer prose-id baseline it reds only in packages/qa/dogfood/test/authz-conformance.matrix.ts (46 lines, rewritten upstream with that baseline, untouched here).",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "3 — each through the fleet-write relay (one repository_dispatch, executed as objectstack-fleet[bot]): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (#21282, draft; read-back 10452 bytes sent = stored); (2) label-write assign, POST /repos/objectstack-ai/objectstack/issues/21282/assignees (huangyiirene; read-back matches); (3) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/21236/comments. Not REST: git push x5 to claude/issue-21236-single-file-refusal-remedy.",
      "open_questions": [
        {
          "question": "On the Turso REMOTE transport the prescribed media-column move is not available. The planner refuses the column step (NOT_IMPLEMENTED/501), and the remote face never reads a completed move. So the new words are a loud dead end there, where the old $contains words gave a silent zero-row answer. Keep the shared sentence, or treat the remote face differently?",
          "options": [
            "A (as shipped): one sentence on both Turso faces, the move. Business need: no measured remote deployment filtering a file field by id. Long-term: one sentence per column class, no per-transport dialect. AI-error: the author is never steered to $contains, and the command explains the 501. Startup focus: adds nothing.",
            "B: a remote-only third wording (\"this transport keeps the column JSON; only $null / $empty answer\"). Long-term: a transport branch in the shared builder. AI-error: accurate, but a second sentence for one filter on one driver. Startup focus: new text surface with no pull.",
            "C: implement the remote media-column move (resolver and planner on the remote face). This closes the gap at its root. It is a capability expansion of its own, worth doing only on a named remote user."
          ],
          "recommendation": "A, because it keeps one sentence per column class and makes the refusal loud on every face. The remote dead end is the existing remote media-move gap, which C closes only if a remote deployment needs it. No decision blocks this PR."
        }
      ],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed — Turso remote transport: a single-value file-class field is a JSON column on every remote deployment (setFileColumnsMovedResolver answers false in remote mode; refuseRemoteMediaColumnMove answers 501), so $eq / $in / text operators on it are refused with no transport-local remedy. This PR keeps that refusal and changes only its words. Measured at TursoDriver.find on the libsql stub, not at a public door; a pre-existing declared capability gap, so not class a/b/c. Dedupe words: turso remote media column move · remote single-value file field JSON filter · files-to-references remote NOT_IMPLEMENTED"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (amendment: the file surface records the driver-turso face the dev changed; same session and branch)
    Session: session_017xfMoEjKUuSh2xYB8sCozp
    Account: huangyiirene (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21236-single-file-refusal-remedy
    Worktree: objectstack-issue-21236
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface: as the claim (5943048346), plus packages/drivers/driver-turso/src/ (remote-transport.ts, turso-driver.ts, the resolver seam's test, and a new local/remote parity pin) and a driver-turso changeset.

    • Recorded after the fact. The dispatch asked for a per-face conclusion on the builder's other printers (changed / already conformant / out of scope). The dev found the remote transport printing the same $contains words for the same field, changed it in place, and reported the edit as a deviation (os-dev-report 5943890476) instead of stopping first.
    • The seat accepts the surface: it is the same defect class, driver-turso is this lane's, and no other claim holds those files. The stop-first rule stands for the next card.
      Container & model: S, mode:subagent, model: opus (unchanged)
      Clause-②: no
      Thread-read: 5943890476
      Serial constraints cleared: unchanged. No in-flight claim names packages/drivers/driver-turso.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21282 at head 09243923ba1757cdfa0922b6b358cb5f97e149b9

    domain:engine#1 · session_017xfMoEjKUuSh2xYB8sCozp · read at 2026-10-02T03:03Z. Judged against GitHub.

    • Shape: draft, base main, Fixes #21236 and no other closing keyword. No path is governed. 11 files: core's refusal builder and its test, driver-sql's call site and a new pin, driver-turso's remote transport and driver with two pins, and three changesets. The driver-turso surface is recorded by amendment 5943928162.
    • The fix:
      • jsonColumnOperatorRefusalText gains an optional class parameter.
      • For a single-value file-class field still stored as JSON, the refusal now prescribes the media-column move: the column step of objectstack migrate files-to-references --apply, in the landed migration entry's words. It no longer prescribes $contains, which answered no rows there.
      • The default arm is byte-identical, and the earlier hash pins are unchanged.
      • driver-sql answers the class from its registries, so the window is not asked a second time. The Turso remote face answers the same class, so one filter gets one sentence on both Turso faces.
      • The refused set is unchanged, so Clause-②: no.
    • Measured (dev): at the base, $startsWith / $eq on such a field were refused with the $contains words, and $contains with the exact id answered no rows, on driver-sql and on the Turso remote face. The multi-valued control is unchanged.
    • Verification:
      • The ablation reddens 5 of 16 driver-sql pins and 15 of 16 Turso parity pins, with the controls green.
      • A cross-package type reverse check fails typecheck as predicted.
      • Suites: core 2156, driver-sql 3586, driver-turso 2365, objectql 7082 and driver-memory 1718 pass. check:driver-conformance is unchanged.
      • Gates: 65 derived, 64 run, and 1 NOT MEASURED locally (check:dual-build-cjs-loads), which CI's Build Core covers.
    • Contract review: PASS at CONTRACT_REVIEW_TIER on ea413a0a14, record 5944589344. It escalated two points to the seat:
    • The dev's open question (the remote transport's wording, A/B/C): the seat answers A, one sentence per column class on both faces. The review judged A right. It needs no escalation: triage's direction (5940354474) decides it, and A keeps every face loud.
    • Deviations:
    • CI on this head, read by the seat in this act: 48 check runs, 41 success with every required context, and 0 failures. The 7 skips are roster entries in check-expected-skips.mjs: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in), and the body-edit re-runs of Auto Label and Check PR Size (two each). The PR body's attribution footer, dropped by the dev's semver edit, was restored by the seat.
    • Out-of-scope findings: the remote media-column move gap: dropped, with the no-card ruling above.

    Next: pr_ready, then automerge_enable, as two relay acts. Fixes closes this card at merge. sql-driver.ts is then free for #20595 stage 4.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21282 → 30af17e41 on main, verified at 2026-10-02T03:33Z. domain:engine#1 · session_017xfMoEjKUuSh2xYB8sCozp.

    • The squash has one parent (c2cd65154) and is an ancestor of origin/main. Its diffstat matches the PR: 11 files, +645/-23.
    • jsonColumnFieldClass is present in packages/drivers/driver-sql/src/sql-driver.ts at the squash (4) and absent at its parent (0). The three changesets are present: core patch, driver-sql patch, and driver-turso minor, as the seat ruled.
    • Records it landed on: contract review PASS 5944589344, and ACCEPT 5944851097 on this card. The surface amendment is 5943928162.
    • Fixes #21236 closed this card. This act strips pm:dispatched and clears the assignee. No other card was named by a closing keyword.
    • sql-driver.ts is now free in this lane's hot-file queue. dead tracker citations in the domain:engine packages (645 sites, 160 numbers, 104 files): the ruling C+D stage for this lane (from #20556) #20595 stage 4 (driver-sql comment prose) takes the next slot.

    Generated by Claude Code

  7. added a commit that references this issue on Oct 7, 2026
    30af17e
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

    area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions