Skip to content

fix(model): read the sort direction in an order string whatever its case - #4346

Merged
bpamiri merged 2 commits into
developfrom
peter/order-direction-case
Oct 5, 2026
Merged

bpamiri merged 2 commits into
developfrom
peter/order-direction-case

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

findAll(order="createdAt desc") sorted ascending. $orderByClause() looked for the direction with case-sensitive Find(" ASC") / Find(" DESC"), so a lowercase or mixed-case direction wasn't seen and " ASC" was appended (model/sql.cfm:262).

The same parser serves all the paths below, so the fix covers each of them:

  • findAll() / findOne(): order="id desc" sorted ascending.
  • Paginated finders (findAll(page=..., perPage=...)): the same.
  • A table.column desc item: it became table.column desc ASC and threw Wheels.InvalidOrderClause, because the dot-notation check is also case-sensitive.
  • The query builder's orderBy("id", "desc"): it validated the direction case-insensitively and then built "id desc", so it too sorted ascending.

Change

  • vendor/wheels/model/sql.cfm: each order item goes through a new $normalizeOrderItem().
    • Runs of whitespace (spaces, tabs) become one space.
    • A trailing asc / desc is read in any case and emitted in upper case.
    • An item without a direction sorts ascending, as before.
    • The rest of $orderByClause() is unchanged. The dot-notation check and SQL Server's paginated ORDER BY reversal then always see an upper-case direction.
  • vendor/wheels/model/query/QueryBuilder.cfc: orderBy() stores the direction in upper case.
  • I checked the other places that read order strings: read.cfm:90 (ReplaceNoCase) and the adapter's $columnAlias() (CFML == is case-insensitive) were already case-insensitive.
  • Changelog: a fixed fragment.

Tests

New vendor/wheels/tests/specs/model/orderDirectionCaseSpec.cfc:

  • desc / Desc / dEsC / DESC all sort descending;
  • asc / Asc / ASC and no direction all sort ascending;
  • repeated spaces, a tab, and surrounding spaces;
  • a multi-column lastName asc, id desc matches the uppercase form;
  • a table.column desc item;
  • a paginated finder;
  • the query builder's orderBy("id", "desc").

RED (develop, Lucee 7 + SQLite): 4 failed and 1 errored. Lowercase sorts ascending in the plain, whitespace, paginated and query-builder cases, and the table.column desc case errors with Invalid dot-notation in ORDER BY clause.

GREEN, model directory (pass / fail / error; orderDirectionCaseSpec 7/7 on every leg):

Engine SQLite MySQL PostgreSQL SQL Server
BoxLang 1402 / 0 / 0 1445 / 0 / 0 1428 / 0 / 0 1457 / 0 / 0
Lucee 7 1402 / 0 / 0 1451 / 0 / 0 1428 / 0 / 0 1457 / 0 / 0
Lucee 6 1402 / 0 / 0 1451 / 0 / 0 1428 / 0 / 0 1457 / 0 / 0
Adobe ColdFusion 2023 1399 / 0 / 0 1442 / 0 / 0 1425 / 0 / 0 1454 / 0 / 0
Adobe ColdFusion 2025 1399 / 0 / 0 1442 / 0 / 0 1425 / 0 / 0 1454 / 0 / 0
  • Full core suite, Lucee 7 + SQLite: 6808 passed.
  • internal specs: 113 passed.
  • RustCFML model directory: 1395 / 0 / 0, with the new spec 7/7.
  • Complexity gate: passes.

🤖 Generated with Claude Code

bpamiri and others added 2 commits October 5, 2026 07:55
$orderByClause() checked for " ASC" / " DESC" with case-sensitive Find(),
so a lowercase direction was missed and " ASC" was appended:
order="id desc" sorted ascending, and a "table.column desc" item became
"table.column desc ASC" and threw Wheels.InvalidOrderClause. The query
builder's orderBy("id", "desc") reached the same code.

Each order item now goes through $normalizeOrderItem(): whitespace runs
(spaces, tabs) collapse to one space, a trailing asc/desc is read in any
case and emitted in upper case, and an item without one sorts ascending.
The query builder stores the direction in upper case too.

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.2.0 milestone Oct 5, 2026
@bpamiri

bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review @ 2938265: source looks good. Final verdict after matrix 37328749853 and PR CI

What was wrong

  • $orderByClause() checked Find(" ASC") / Find(" DESC"), which is case-sensitive. So id desc got ASC appended (id desc ASC), and the later UCase(ListLast(item, " ")) read ASC: a silent ascending sort.
  • On the dot-notation path, the guard regex (\s+(ASC|DESC))?$ is matched with case-sensitive REFind, so table.id desc threw Wheels.InvalidOrderClause.

The fix

  • $normalizeOrderItem() collapses whitespace runs (spaces, tabs) and trims. A trailing token that ListFindNoCase("ASC,DESC") accepts is upper-cased; otherwise ASC is appended, and a lone word (even a column literally named desc) is treated as the property.
  • Everything downstream now sees … ASC / … DESC in upper case:
    • the dot-notation regex passes;
    • UCase(ListLast(…)) emits the right direction;
    • the SQL Server pagination reversal (MicrosoftSQLServerModel :835) and Base.cfc :376/:444 work on that upper-case output.
  • orderBy("id", "desc") already passed the case-insensitive $validateDirection() and now emits DESC.
  • No new surface is accepted. Items with ( are still rejected, the dot-notation regex is unchanged, and anything after the direction (for example NULLS LAST) was never supported and still isn't.

Spec (orderDirectionCaseSpec, 7 cases) covers:

  • desc / DESC / Desc and the asc variants, including no direction;
  • repeated spaces, a tab, and surrounding whitespace;
  • a multi-column order checked against the upper-case equivalent;
  • table.column desc;
  • a paginated finder, which exercises the SQL Server reversal on that leg;
  • orderBy("id", "desc").

No findings. On the matrix I'll read orderDirectionCaseSpec and the model directory on every leg, with SQL Server pagination closest, plus PR CI.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Wheels Test Results

222 804 tests   217 683 ✅  1h 24m 8s ⏱️
 17 344 suites    5 121 💤
     32 files          0 ❌

Results for commit 2938265.

@bpamiri

bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Final: APPROVE @ 2938265 (matrix 37328749853, all 32 legs read; PR CI read)

Matrix

  • 32/32 legs have 0 fail, 0 error, and every job succeeded, including rustcfml and the Compatibility Matrix Gate.
  • orderDirectionCaseSpec passes 7/7 with 0 skipped on every leg (lucee6, lucee7, adobe2023, adobe2025, boxlang × every database). That includes:
    • all five Oracle legs;
    • all five SQL Server legs, where "reads a lowercase direction in a paginated finder" exercises the pagination ORDER BY reversal.
  • So every order-parsing path the spec drives returns the right direction on every engine and database: plain and paginated findAll(), table.column desc, multi-column orders with a direction per item, and orderBy("id", "desc").
  • The rest of the model directory is green everywhere. No double direction was emitted: an id desc ASC-style item would be a SQL syntax error on every database.
  • Skips. The skipped specs on lucee7-sqlserver, adobe2023-oracle and boxlang-mysql are identical to a recent develop-based run (refactor(model): drop the redundant abort rollback from the transactional advisory-lock body #4338's final matrix 37316988407).

PR CI: 17 pass, 3 skipping, 0 fail, 0 pending.

The fixed fragment is accurate. No open findings.

@bpamiri
bpamiri merged commit 60f3af3 into develop Oct 5, 2026
59 checks passed
@bpamiri
bpamiri deleted the peter/order-direction-case branch October 5, 2026 15:24
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.

1 participant