Repository navigation
feat(test): assert how many SQL statements a spec's call sends - #4357
Conversation
A spec couldn't assert that a call ran no query, or N queries, so N+1 loads, missing caches and unexpected writes were hard to test. WheelsTest gains assertQueries(count, callback), assertNoQueries(callback), assertQueriesMatch(pattern, callback, count), assertNoQueriesMatch(pattern, callback) and recordQueries(callback). Base.$executeQuery(), which every model statement goes through, adds each statement to the request's active recorders: its SQL with a ? per bound value, never the values, so failure messages can't put data in a log. Idle cost is one key check. Recorders nest (each counts its own window), take an optional dataSource filter, and come off in a catch-free finally, so a throwing or aborting callback can't leave one behind. A query-cache hit sends no SQL and isn't counted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r3 review @ fc08716: code APPROVE (final sign-off after orch1's matrix run on this head and PR CI, Adobe legs especially)
Not blocking, a docs precision point. "No values recorded" holds for bound values. With |
Review @ fc08716: source looks good. Final verdict after matrix 37339528267 and PR CI
INFO (docs, optional). "What counts: every statement the model layer sends" is true for
"Raw The two On the matrix I'll read |
A failure lists each bound value as ?, but a value written into the SQL with parameterize=false appears as written. Raw queryExecute isn't counted, which includes advisory locks, the migration lock, the job store and SQL Server's probes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
Wheels Test Results223 444 tests 218 323 ✅ 1h 25m 49s ⏱️ Results for commit fc08716. |
|
rev1-r3 review @ 9659550: APPROVE (final; code gate is matrix run 37339528267 on
Follow-up, as agreed: the docblocks on |
Summary
A spec couldn't assert that a call ran no query, or a given number of queries. That made N+1 loads, missing caches and unexpected writes hard to test without reaching into framework internals.
wheels.WheelsTestgains these functions, available in every framework and app spec:assertQueries(count, callback)callbacksends exactlycountSQL statementsassertNoQueries(callback)assertQueriesMatch(pattern, callback, count)count) matches the regex, case-insensitiveassertNoQueriesMatch(pattern, callback)recordQueries(callback)[{sql, dataSource}]var post = assertQueries(1, () => model("Post").findByKey(1)); assertNoQueries(() => post.title); assertNoQueriesMatch("comments", () => model("Post").findAll(include = "author"));Design
Base.$executeQuery()is the one place every model statement runs: finders,count(),save()/create()/update()/delete(),updateAll()/deleteAll(), and each adapter's$performQuery(). While a recorder is active, each statement is added to it.queryExecute()/cfquery, including framework features built on it (advisory locks, the migration lock, the job store and SQL Server probes); transaction control; and test-client requests, which run in their own request.$executeQuery(), so they don't make counts noisy.?. Each recorded statement is its SQL with?for every bound value (an IN list shows(?)), so a failure message, which lists the statements, doesn't put bound data in a CI log. A value written into the SQL itself, as withparameterize=false, appears as written. The spec asserts that a boundwherevalue doesn't appear in the message.dataSourcefilter. Every function takes an optionaldataSourcethat counts only statements sent there, for multi-datasource and tenant apps.try/finally, through$stopQueryRecorder(), with no loop in thefinally(invariant 12). A throwing or aborting callback can't leave a recorder behind, andTryCatchFinallyGuardSpecpasses.assertQueries()and the other assertions return the callback's result, so a call can stay inline.Docs
.ai/testing.md(the consumer copy and thewheels newtemplate copy).changelog.d/query-assertions.added.md.Tests
wheelstest/QueryAssertionsSpeccovers:?for each bound value;assertNoQueriesMatch;dataSourcefilter;RED on develop (only the spec added): 7 of the 8 checks fail or error. The idle check is the control.
GREEN locally, full core suite on SQLite, 0 fail / 0 error:
All four CFML engines discovered 542 bundles. The Lucee 7, BoxLang and RustCFML numbers are after the INSERT-check fix. The Adobe runs were before it: that check was their only failure, it failed the same way on every engine, and the fix is spec-only. Adobe wasn't rerun locally afterwards, so the matrix covers it. After rebasing onto develop (with #4349), the Lucee 7 wheelstest, model and security specs and the guides build pass.
Note on the first full run. The INSERT check originally created a
tag.view/formsSpec.cfcregisters presence validations fornameandvirtualon the sharedtagclass and didn't remove them, so in a full run the create failed validation (Virtual property can't be empty) and sent no INSERT. The check now usesauthorand asserts the create succeeded. The leak predates this PR and is fixed separately in #4365. (An earlier version of this note blamedcallbacksSpec; that was wrong.)Docs-only follow-up (9659550, after matrix 37339528267). The wording now says bound values become
?and aparameterize=falsevalue appears as written. The not-counted list now names the framework features built onqueryExecute(). No.cfc/.cfmchanged.🤖 Generated with Claude Code