Skip to content

feat(model): add includeCalculated to additively opt in select=false SQL properties#3254

Open
wheels-bot[bot] wants to merge 2 commits into
developfrom
fix/bot-3252-allow-opt-in-inclusion-of-non-default-selected-com
Open

feat(model): add includeCalculated to additively opt in select=false SQL properties#3254
wheels-bot[bot] wants to merge 2 commits into
developfrom
fix/bot-3252-allow-opt-in-inclusion-of-non-default-selected-com

Conversation

@wheels-bot

@wheels-bot wheels-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an includeCalculated argument to findAll(), findOne(), and findByKey() so a calculated SQL property declared select=false can be opted back into a single finder by name — model("User").findAll(includeCalculated="fullName"). Unlike select, it is additive: the named calculated properties are merged on top of the default column list inside $createSQLFieldList rather than replacing it, so the rest of the record is still returned. This closes the inverse of the existing property(name=..., sql=..., select=false) declaration — a property kept off the hot path can now be pulled back in per-call without hand-listing every other column. Unknown names throw Wheels.CalculatedPropertyNotFound in development/testing and are ignored in production, mirroring existing dev-only validation patterns.

This follows the cross-framework research recommendation (additive, by-name, declared-once opt-in) verbatim, including the argument name includeCalculated.

Recommended path from research: #3252 (comment)

Related Issue

Fixes #3252

Type of Change

  • New feature

Feature Completeness Checklist

  • DCO sign-off -- Every commit carries Signed-off-by: (git commit -s)
  • Tests -- vendor/wheels/tests/specs/model/includeCalculatedSpec.cfc (failing -> passing): additive SELECT generation, comma-list opt-in, populated property on a real finder, absence on the default finder, and Wheels.CalculatedPropertyNotFound on unknown names
  • Framework Docs -- handled separately by bot-update-docs.yml
  • AI Reference Docs -- handled separately by bot-update-docs.yml
  • CLAUDE.md -- handled separately by bot-update-docs.yml
  • Changelog fragment -- changelog.d/3252-include-calculated.added.md
  • Test runner passes -- Run against the local Lucee 7 + SQLite server: the new spec returns 5 passed / 0 failed / 0 errors, and the full wheels.tests.specs.model layer returns HTTP 200 (no failures or errors)

Test Plan

// Post declares: property(name="titleAlias", sql="title", select=false)
model("post").findOne();                                 // titleAlias absent (default)
model("post").findOne(includeCalculated="titleAlias");   // titleAlias populated, base columns intact
model("post").findOne(includeCalculated="nope");         // throws Wheels.CalculatedPropertyNotFound in dev/testing

Implementation

  • vendor/wheels/model/sql.cfc -- $selectClause and $createSQLFieldList accept a new includeCalculated argument; the names are validated against the model's calculatedProperties and additively merged into the select list (pure list manipulation, no cross-engine traps).
  • vendor/wheels/model/read.cfc -- findAll / findOne / findByKey accept and thread includeCalculated through to the select-clause builder.

…SQL properties

Adds an `includeCalculated` argument to findAll(), findOne(), and
findByKey() that opts already-declared calculated SQL properties
(property(name=..., sql=..., select=false)) back into a single finder.

Unlike `select`, it is additive — the named calculated properties are
merged on top of the default column list inside $createSQLFieldList
rather than replacing it, so the rest of the record is still returned.
This closes the inverse of the existing select=false declaration: a
property kept off the hot path can now be pulled back in per-call
without hand-listing every other column.

Unknown names throw Wheels.CalculatedPropertyNotFound in
development/testing and are ignored in production, mirroring existing
dev-only validation patterns. Pure list manipulation — no closures,
struct member functions, or other cross-engine traps.

Fixes #3252

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — This PR adds an additive includeCalculated argument to findAll() / findOne() / findByKey() so a select=false calculated property can be opted back into a single finder by name without re-listing every column. The implementation is correct, cross-engine-safe, and backed by a focused BDD spec. Verdict: comment — no blocking issues; two minor, non-blocking observations below.

Correctness

The threading and additive merge are sound:

  • findByKey only adds the argument to its signature, but it forwards via argumentCollection = arguments to findOne -> findAll (vendor/wheels/model/read.cfc:452, :511), so includeCalculated reaches $selectClause without an explicit pass-through. Correct.
  • Both $createSQLFieldList call sites in findAll thread the argument — the empty-result column-shell path (read.cfc:220) and the main $selectClause path (read.cfc:254) — so the opted-in property survives even on an empty returnAs="query" result.
  • includeCalculated is part of arguments when $hashedKey builds local.queryShellKey (read.cfc:243), so an includeCalculated="titleAlias" finder gets its own SQL-cache entry and will not collide with the default finder. Good — this is the kind of thing that silently breaks otherwise.
  • The dedup guard if (!ListFindNoCase(arguments.list, local.calcName)) (sql.cfc:534) correctly avoids a double-add when the property is already select=true or was named in select.
  • No injection surface: names are validated against declared calculatedProperties (throw in dev/testing, silent skip in production), so only declared identifiers ever reach the field list.

Cross-engine

Clean. The new block in $createSQLFieldList (sql.cfc:510-538) is pure list/struct manipulation — classic numeric for loop, ListToArray, Trim, StructKeyExists, ListFindNoCase, ListAppend, Throw. No closures, no obj.map(), no Left(str,0), not inside a finally, no reserved-scope param names. get("environment") is the established dev-gate accessor used two lines up in the same file (sql.cfc:456). No engine-specific traps.

Tests

includeCalculatedSpec.cfc follows the WheelsTest BDD convention (g = application.wo, toInclude/notToInclude) and covers the additive SELECT, comma-list, populated-on-real-finder, absent-on-default, and the Wheels.CalculatedPropertyNotFound throw. Two minor gaps, neither blocking:

  1. No spec exercises findByKey(includeCalculated=...) directly. The path is covered transitively through findOne, so this is a completeness nit rather than a coverage hole.
  2. No assertion for the production no-op branch (unknown name silently ignored, sql.cfc:531). Worth a line if a set(environment="production") toggle is feasible in the suite, but the dev-throw is the load-bearing path and it is tested.

Conventions

@includeCalculated docstrings are added to findAll, findByKey, and findOne with the [see:findAll] cross-reference idiom. Argument ordering is consistent across all three signatures.

One scope note (not a defect): validation only checks variables.wheels.class.calculatedProperties — the current model (sql.cfc:521). A calculated property declared on an include-ed association would throw CalculatedPropertyNotFound even though the later resolution loop iterates all classes. This matches the PR description ("the model's calculated properties") and is a reasonable v1 boundary; consider documenting the current-model-only limitation in the user guide when docs land via bot-update-docs.yml.

Commits / Docs

  • Commit a5b58eafeat(model): ..., valid type/scope, header under 100 chars, carries Signed-off-by:. Conforms to commitlint.
  • Changelog fragment changelog.d/3252-include-calculated.added.md with added type — correct fragment-file approach, no direct [Unreleased] edit.
  • Framework/AI/CLAUDE.md docs deferred to bot-update-docs.yml per the checklist.

Nice, well-scoped change.

… properties

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
@wheels-bot

wheels-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Wheels Bot — Docs updated

Added a doc commit to this PR:

  • web/sites/guides/src/content/docs/v4-0-0/basics/models-and-the-orm.mdx — new "Calculated SQL properties" section explaining property(sql=..., select=false) and the includeCalculated opt-in pattern with a code example
  • .ai/wheels/snippets/model-snippets.md — updated Calculated Properties snippet to show select=false declaration and includeCalculated per-call usage
  • CLAUDE.md — added property(name="fullName", sql="...", select=false) example to the Model Quick Reference config() block, and a note on findAll(includeCalculated="fullName") in the finders summary line

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — This PR adds an additive includeCalculated argument to findAll() / findOne() / findByKey() so a select=false calculated SQL property can be opted back into a single finder by name without re-listing every column. The core implementation in sql.cfc is correct, cross-engine safe, threaded cleanly through all three finders, and backed by a focused BDD spec against real select=false fixtures (titleAlias, createdAtAlias on the Post test model). The only issue I found is a broken Markdown fence in the internal snippet doc — a docs nit, not a blocking finding. Verdict: comment.

Docs

  • .ai/wheels/snippets/model-snippets.md:164 — the new edit inserts a closing code fence mid-section, ejecting the pre-existing method-based calculated property example out of the cfm block. The original section was a single fenced block; this PR raises the file fence count from 12 (balanced, per git grep -c on origin/develop) to 13 (unbalanced). As a result lines 166–177 (// Method-based calculated propertyfunction isExpired()) now render as plain prose instead of code, and the trailing fence at line 178 becomes an orphan opening fence with no closer. Fix: drop the inserted closing fence (line 164) and the blank line after it so the method-based example stays inside the same cfm block, restoring even fence parity. (The user-facing guide models-and-the-orm.mdx is unaffected — its fences stay balanced, 18→20.)

Correctness (verified, no findings)

  • The appended bare calc name resolves correctly: $createSQLFieldList matches it via StructKeyExists(local.classData.calculatedProperties, local.iItem) (vendor/wheels/model/sql.cfc:598) and emits the <sql> AS <name> column, confirmed by the toInclude("AS titleAlias") assertions.
  • Additive guarantee holds — the includeCalculated block (vendor/wheels/model/sql.cfc:513-538) runs after the default-population block (sql.cfc:495-508) and only ListAppends, never replaces arguments.list.
  • Dedup guard ListFindNoCase(arguments.list, local.calcName) (vendor/wheels/model/sql.cfc:534) prevents a double-add when a select=true calc property is also named.
  • New arg is optional with default "" on both $selectClause and $createSQLFieldList, so existing callers are unaffected; both internal call sites in read.cfc are updated.
  • findByKey -> findOne -> findAll all thread includeCalculated via argumentCollection, so the doc-claimed findByKey support is real.

Cross-engine (verified, no findings)

The new block is pure list/struct manipulation (ListToArray, Trim, Len, StructKeyExists, ListFindNoCase, ListAppend, Throw) — no member-function .map(), no obj["key"](), no closures, no Left(str,0), and the for loop is not inside a finally. get("environment") matches established prior art in the same file (vendor/wheels/model/sql.cfc:456). Gating the dev-only throw on development,testing (rather than development alone) is necessary and correct — the suite runs in testing, which is why the toThrow(type="Wheels.CalculatedPropertyNotFound") assertion fires.

Tests (verified, no findings)

includeCalculatedSpec.cfc extends wheels.WheelsTest, covers additive SELECT generation, comma-list opt-in, population on a live finder, absence on the default finder, and the unknown-name throw — both happy and error paths. Fixtures (vendor/wheels/tests/_assets/models/Post.cfc:9,11) genuinely declare the two properties select=false, so the additive claim is actually exercised.

Commits (verified, no findings)

Both commits conform to commitlint.config.js: feat(model): … and docs: … — valid types, subjects within length, not ALL-CAPS. Changelog fragment changelog.d/3252-include-calculated.added.md uses the fragment system with a valid added type rather than editing CHANGELOG.md directly.

@bpamiri bpamiri marked this pull request as ready for review June 24, 2026 03:16
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.

Allow opt-in inclusion of non-default-selected computed SQL properties in find*()

0 participants