Skip to content

fix(model): compare date values in the query builder and dynamic finders as dates on SQLite - #4281

Merged
bpamiri merged 2 commits into
developfrom
peter/query-builder-date-values
Oct 4, 2026
Merged

bpamiri merged 2 commits into
developfrom
peter/query-builder-date-values

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A date passed to the query builder or a dynamic finder now compares as a date on SQLite.

The values in where(column, operator, value), whereBetween(), whereIn() and dynamic finders (findOneByPublishedAt(date)) go through the adapter's $quoteValue(str), which takes a string. A CFML date object becomes its CFML literal there, {ts '2026-09-28 00:00:00'}, on Lucee, Adobe and BoxLang alike.

  • SQLite: that text is compared with the stored date text (2026-10-01 09:00:00, the format save() writes). It never matches, so where("publishedAt", ">=", someDate) silently returned no rows. A string literal in a findAll(where=...) worked.
  • Other databases: the other six parse the literal back into a timestamp parameter, so they weren't affected.

Change

Base.$quoteValue() first passes the value through a new $unwrapDateLiteral(). A value that is exactly a CFML date literal becomes the text save() writes:

Literal Becomes
{ts 'X'} X
{d 'X'} X 00:00:00
{t 'X'} 1899-12-30 X

That is the same mapping the SQLite migrator applies to stored literals. Any other value is unchanged: a date written as a string, or text that merely looks like a literal, such as {d 'abc'} or {ts 'not a date'}.

Why there is no column-type check. The unwrap applies to any column type and relies only on the exact literal shape. SQLite date columns are often declared TEXT (the core test schema's c_o_r_e_profiles.dateofbirth is one), and Wheels types those as string / cf_sql_varchar, while the same column on MySQL is datetime / cf_sql_timestamp. A "date-like types only" check would leave the bug in place exactly where it bites. The only text that changes is a string that is exactly a CFML date literal, which in practice comes from a date object.

Tests

  • New: model/QueryBuilderDateValueSpec (5 query checks plus 3 for $unwrapDateLiteral(): literal shapes, non-date text left verbatim, and {d 'abc'} quoted verbatim for a text column). Each creates its own rows from date objects in a year no fixture uses and rolls them back. It covers:
    • where() with >= and <;
    • whereBetween();
    • whereIn();
    • a dynamic finder;
    • a string date unchanged.
  • Before the fix (develop + the spec, Lucee 7): 4 of 5 fail on SQLite and all pass on MySQL, PostgreSQL, SQL Server, H2, CockroachDB and Oracle. The bug is SQLite-only.
  • Model specs with the fix (Lucee 7, 0 failures on every database):
Database Pass
SQLite 1342
MySQL 1367
PostgreSQL 1346
SQL Server 1345
H2 1337
CockroachDB 1323
Oracle 1339
  • Full core suite on SQLite:
Engine Pass / fail / error
Lucee 7 6615 / 0 / 0
BoxLang 6615 / 0 / 0
Adobe 2023 6610 / 0 / 0
Adobe 2025 6610 / 0 / 0
RustCFML 6594 / 0 / 0
  • The new spec passes 5/5 on every JVM engine.
  • After making the unwrap shape-only: model specs green on Lucee 7 (SQLite 1345, MySQL 1370), BoxLang 1345, Adobe 2025 1342; RustCFML 6578/0/0.

🤖 Generated with Claude Code

…ers as dates on SQLite

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

bpamiri commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

rev1-r1 review: head bb82c8d30: one MEDIUM, matrix pending

The quoting path is unchanged. $unwrapDateLiteral() runs first in the adapter's $quoteValue(), and its result then takes the existing path:

  • string/date types: wrapped and '-doubled;
  • integer/float/boolean: $validateValueShape().

The regex only matches a whole value of the form {ts|d|t '…'} with no quote inside, so the unwrapped text goes through the same quoting or shape check as any other value. Every caller (the builder, dynamic finders, $keyWhereString, the uniqueness check, updateAll's SET list) keeps its existing escaping, and parameterize=false gets the same escaped literal.

MEDIUM: the unwrap applies to every column type and to any text inside the braces

$unwrapDateLiteral() runs before the type is known, for any string. So a value that merely looks like a date literal is rewritten even when it isn't a date, and the rewrite is silent:

  • a search on a text column for the literal string {d 'abc'} becomes abc 00:00:00;
  • {ts 'x'} becomes x.

That changes user-visible results, for instance from a search box, without any error.

Please unwrap only when:

  • the target type is date-like (the validation type is date/datetime/time, or for the default sqlType path, a date sqlType); and
  • the inner value has the date/time shape (digits, -, :, ., spaces).

That's the same restriction $maskWhereLiterals() already applies to the ODBC forms it recognises, and it keeps the fix scoped to real dates. Specs that pin it:

  • a text column keeps {ts 'x'} verbatim;
  • a datetime column unwraps it.

Matrix 37221090122: I'll read the legs, including whether BoxLang's ToString(date) produces the {ts …} form this relies on, and post the result here.

… value

$unwrapDateLiteral() now changes a value only when it is exactly {ts 'yyyy-mm-dd HH:mm:ss[.f]'},
{d 'yyyy-mm-dd'} or {t 'HH:mm:ss[.f]'}; text such as {d 'abc'} stays verbatim. It still applies
to any column type, because SQLite date columns are often declared TEXT and are typed as strings.

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

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Wheels Test Results

214 247 tests   210 150 ✅  1h 3m 52s ⏱️
 16 192 suites    4 097 💤
     32 files          0 ❌

Results for commit f2c47dd.

@bpamiri

bpamiri commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

rev1-r1 re-review: head f2c47dd84: APPROVE

  • Pattern: $unwrapDateLiteral() now unwraps only the exact shapes a date produces. It's anchored per kind: ts is yyyy-mm-dd HH:mm:ss with an optional .fraction of up to 9 digits, d is yyyy-mm-dd, and t is HH:mm:ss with an optional fraction. Any other text inside the braces is left verbatim (for example {d 'abc'}, which still gets quoted for a text column), and the specs pin both directions.
  • No column-type check: that's deliberate. SQLite date columns declared TEXT report as strings, so a type check would skip the main case.
  • Quoting: the result still takes the adapter's existing quoting or shape check.

Matrix 37221745474 @ f2c47dd84: all 32 engine × DB legs (lucee6/7 × 7 DBs, adobe2023/2025 and boxlang × 6 DBs) discovered 506 bundles with 0 fail / 0 error, and QueryBuilderDateValueSpec passed 8/8 on every leg. That includes boxlang-sqlite, so the fix covers BoxLang's date-to-string form too, and the regex groups that don't participate in a match behave on every engine. The rustcfml job and the gate are green.

OK TO MERGE.

@bpamiri
bpamiri merged commit 264bc6f into develop Oct 4, 2026
62 checks passed
@bpamiri
bpamiri deleted the peter/query-builder-date-values branch October 4, 2026 18:10
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