Skip to content

fix(model): name a where= value that has no model column to bind against - #4531

Merged
bpamiri merged 4 commits into
developfrom
peter/where-literal-errors
Oct 9, 2026
Merged

bpamiri merged 4 commits into
developfrom
peter/where-literal-errors

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #4530

Problem

where= is parsed, not passed through. Each column <operator> literal is bound as a query parameter, and the column must belong to the model or an included model.

A literal compared with another table's column inside a subquery doesn't fit that rule. For example, in id IN (SELECT book_id FROM accesses WHERE user_id = 1), the literal was masked but never got a parameter, so the SQL builder failed with Array index [1] out of range, array size is [0] (sql.cfm). The error named neither the condition nor the cause. A condition with no column, such as where="1 = 0", failed with a Wheels.ColumnNotFound that didn't say why either.

Change

where= accepts model columns compared with values; literal-only predicates are not supported. This PR makes the errors say so.

  • A value with no column to bind against:
    • $whereClause() now throws Wheels.UnbindableWhereValue naming the condition, with its quoted values hidden. A bound value shows as ? and any other quoted literal as '...'. Unquoted numbers in the developer's own where text are shown as written.
    • The error lists the alternatives: compare subquery columns only, use whereIn() with a value array, or run the subquery as its own query first.
  • Wheels.ColumnNotFound from a where= now explains the rule in its detail:
    • for a number in the column's place (1 = 0), that a condition with no column isn't supported, and what to use instead;
    • for a clause with a subquery, the subquery alternatives above.
  • Docs.
    • The query guide's "Which values are bound" section states the rule. It says where="1 = 0" isn't supported, and that for no rows you use whereIn() with an empty array and for every row you leave the condition out. It also covers the subquery case, with a two-step whereIn() example.
    • .ai/models.md and the app template's copy say the same. The consumer-docs check passes.
    • A changelog fragment (fixed) is included.

Tests

New whereUnboundLiteralSpec (model suite):

  • where="1 = 0" throws Wheels.ColumnNotFound whose detail says it isn't supported and names whereIn() with an empty array;
  • whereIn("id", []) returns no rows;
  • a subquery literal throws Wheels.UnbindableWhereValue naming WHERE views=?;
  • the error hides quoted values: with a quoted literal elsewhere in the subquery's SELECT and a quoted value in its WHERE, the message contains neither value, their hex encoding, the mask sentinel, nor Chr(2);
  • a column-only subquery still runs;
  • the subquery hint on ColumnNotFound.

Local results (production code as at bebed30; later commits change wording and a spec name only):

Engine Suite Databases Result
Lucee 7 full core suite SQLite 7294/0/0
Lucee 7 model PostgreSQL, SQL Server, Oracle all 0 fail
BoxLang model SQLite 0 fail
Adobe 2023 model SQLite 0 fail
Adobe 2025 model SQLite, SQL Server all 0 fail
RustCFML (run-suite.sh, TZ=UTC) full core suite SQLite exit 0, no new failures

The complexity and docblock gates pass. The guides site builds, and verify-docs on the query page passes 12/12.

🤖 Generated with Claude Code

where= binds every `column <operator> literal`, and the column must be the model's or an
included model's. Two valid shapes failed with errors that didn't say why:

- A comparison between two numbers (1 = 0, 1 = 1) threw Wheels.ColumnNotFound for a column
  named 1. findAll(), updateAll() and deleteAll() now mark its operator (Chr(8), outside quoted
  literals) so no pass binds or counts its value, and $whereClause() writes it as typed.
- A literal compared with another table's column inside a subquery was masked but never got a
  parameter, so the SQL builder failed with "Array index [1] out of range". It now throws
  Wheels.UnbindableWhereValue naming the condition and the alternatives (whereIn() with a value
  array, or running the subquery first). A ColumnNotFound from a where with a subquery says
  the same.

The query guide and .ai/models.md (and the app template's copy) document the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri bpamiri added this to the v4.3.0 milestone Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Wheels Test Results

37 230 tests   36 483 ✅  10m 40s ⏱️
 2 995 suites     747 💤
     5 files         0 ❌

Results for commit 433ae85.

♻️ This comment has been updated with latest results.

…de values in its error

where= accepts model columns compared with values; a condition with no column, such as
1 = 0, isn't supported. It throws Wheels.ColumnNotFound as before, whose detail now says so and
names the alternatives (whereIn() with an empty array for no rows; leave the condition out
for every row). The guide, .ai/models.md and the changelog say the same.

Wheels.UnbindableWhereValue now hides every value in the condition it echoes: a quoted literal
that wasn't bound is shown as '...' rather than in its masked form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri bpamiri changed the title fix(model): pass 1 = 0 through where= and name a value it can't bind fix(model): name a where= value that has no model column to bind against Oct 9, 2026
bpamiri and others added 2 commits October 9, 2026 08:40
…lue echoes

Wheels.UnbindableWhereValue echoed numeric literals in the condition as written. A number that
isn't part of an identifier is now shown as ..., alongside bound values (?) and quoted
literals ('...'), and the wording says exactly that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
…here= value echoes"

This reverts commit e848f53: only quoted and bound values need hiding, and unquoted numbers
in the developer's own where text may show. The comment, the error, the spec name and the
changelog now say "quoted values hidden" rather than "every value".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri

bpamiri commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Source review approved at 433ae859b.

Unsupported literal-only predicates retain their existing error behavior, with clearer guidance. Independently verified quoted diagnostic redaction on Lucee 7/SQLite with ordinary text, doubled apostrophes and control-character data, under parameterize=true and false; the six PR regression specs also pass.

Final gate remains full matrix 37953211949, narrowed matrix 37954031094, and current-head PR CI.

@bpamiri

bpamiri commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Final approval — OK TO MERGE at 433ae859bc4d.

  • Audited raw JSON for all 40 JVM legs across full run 37953211949 and narrowed run 37954031094: 599 bundles per leg, zero failures/errors. All six whereUnboundLiteralSpec cases executed and passed on every leg, with no skips.
  • Current-head RustCFML: 7268 passed, zero failures/errors, 178 skipped. Both compatibility gates succeeded.
  • Current-head PR CI: 29 passed, 2 skipped, none failed or pending.
  • The delta from the full-matrix head changes wording only; production logic is unchanged. The independent Lucee 7 quoted-value redaction probe also passed, as recorded in the source review.

@bpamiri
bpamiri merged commit 96b31d5 into develop Oct 9, 2026
45 checks passed
@bpamiri
bpamiri deleted the peter/where-literal-errors branch October 9, 2026 17:26
bpamiri added a commit that referenced this pull request Oct 12, 2026
- erase-account rescore flipped seven trials, not eight; note the eighth
  (a Claude Code trial) as unrescorable
- reword the frozen-before-runs claim in sections 1 and 7 so it no longer reads
  as contradicting the two disclosed check corrections in section 5
- drop the "hardest task" label on ar-erase-account (hw-scoped-broadcast is lower)
- note two erase misses were a where= subquery documentation gap since addressed
  (#4531), not model errors
- literal-route: reconcile "six remain" with the twelve not passing (six genuine
  HTTP 301, six unrescorable Claude Code trials)
- drop the redundant raw parens on the Qwen docs cell (raw equals rescored)
- correct #4266's description (a failed enqueue raises) vs #4282 (defer an
  enqueue inside a datasource transaction)
- state the per-trial cost guard plainly
- rename the section 2 heading to "Latest results (October 2026)"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
bpamiri added a commit that referenced this pull request Oct 12, 2026
* docs(web): add AI coding-agent results page at /ai

Add web/sites/landing/src/pages/ai.astro (route wheels.dev/ai), a results
page reporting the Round 4 coding-agent evaluation against real feature work
in a Wheels app: methodology, the full per-cell results table, the bare vs
shipped-docs comparison, the ten tasks, the two post-run check corrections in
full, and the user-facing framework changes the rounds drove.

Built with the shared BaseLayout and @wheels-dev/ui tokens, matching index.astro.
Primary figures are the rescored ones; raw figures are shown in parentheses.
Section headings carry stable slug ids so deep links resolve. Data tables use a
horizontal-scroll wrapper to avoid page-level scroll on small screens.

astro build + astro check pass; Prettier clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

* docs(web): correct AI results page per review

- erase-account rescore flipped seven trials, not eight; note the eighth
  (a Claude Code trial) as unrescorable
- reword the frozen-before-runs claim in sections 1 and 7 so it no longer reads
  as contradicting the two disclosed check corrections in section 5
- drop the "hardest task" label on ar-erase-account (hw-scoped-broadcast is lower)
- note two erase misses were a where= subquery documentation gap since addressed
  (#4531), not model errors
- literal-route: reconcile "six remain" with the twelve not passing (six genuine
  HTTP 301, six unrescorable Claude Code trials)
- drop the redundant raw parens on the Qwen docs cell (raw equals rescored)
- correct #4266's description (a failed enqueue raises) vs #4282 (defer an
  enqueue inside a datasource transaction)
- state the per-trial cost guard plainly
- rename the section 2 heading to "Latest results (October 2026)"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>

---------

Signed-off-by: Peter Amiri <peter@alurium.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(model): where= literals inside a subquery throw an array index error, and "1 = 0" throws ColumnNotFound

1 participant