From bbafae822183cdcf37f005a16ffdf68cc12e2048 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:05:23 +0000 Subject: [PATCH 1/4] fix(model): quote column identifiers in SELECT clause builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WHERE and ORDER BY clause builders already routed column names through the adapter's $quoteIdentifier, but $createSQLFieldList — the SELECT/GROUP BY engine — appended the column part raw. Models backed by tables with reserved-word column names (e.g. `key`, `order`, `group`) blew up on `findAll`/`findOne`/dynamic finders with cryptic SQL syntax errors as soon as the SELECT list mentioned the column. Also strips quote chars from the property extracted by the duplicate-column rename loop so the alias replacement still matches the unquoted ` AS ` form, and updates the empty-pagination columnList extraction in read.cfc to strip identifier quotes before stripping the table prefix. Fixes #2784 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + vendor/wheels/model/read.cfc | 7 ++- vendor/wheels/model/sql.cfc | 12 +++-- vendor/wheels/tests/specs/model/crudSpec.cfc | 6 +-- .../specs/model/reservedColumnQuotingSpec.cfc | 51 +++++++++++++++++++ 5 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc diff --git a/CHANGELOG.md b/CHANGELOG.md index bc365070e2..cde690194b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Fixed +- Model layer SELECT clause builder now routes column identifiers through the adapter's `$quoteIdentifier`, so reserved-word column names (e.g. `key`, `order`, `group`) survive on every supported dialect instead of breaking `findAll` / `findOne` / dynamic finders with cryptic SQL syntax errors. The WHERE / ORDER BY paths already quoted columns; `$createSQLFieldList` and the empty-pagination column-list extraction in `read.cfc` now match. - Linux `.deb` / `.rpm` packages double-nested the framework at `/opt/wheels/module/vendor/wheels/wheels/` instead of `/opt/wheels/module/vendor/wheels/`. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves; the nfpm `type: tree` rule then copied the entire `build/framework/` tree (wrapper and all) into the destination, leaving `Injector.cfc` one level too deep. Every fresh `wheels new` install on Ubuntu/Fedora then crashed on first request with `could not find component or class with name [wheels.Injector]`, cascading into the cryptic `The key [WO] does not exist.` error in `onError`. The brew formula handles this correctly via `(share/"wheels/framework/wheels").install Dir["*"]`; the Linux nfpm configs now pin `src` at `./build/framework/wheels/` to match. Regression spec at `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` (#2773) - `onError` in the generated app template and demo `public/Application.cfc` now guards `application.wo` with `StructKeyExists(application, "wo")` after the recovery try/catch. When `new wheels.Injector(...)` fails during `onApplicationStart` (e.g. a stale `/wheels` mapping under Lucee Express 7), the original error is preserved via a minimal HTML fallback instead of cascading into the cryptic "The key [WO] does not exist" exception that hit "Your First 15 Minutes" tutorial users on fresh installs diff --git a/vendor/wheels/model/read.cfc b/vendor/wheels/model/read.cfc index 787427986d..0c60e9d190 100644 --- a/vendor/wheels/model/read.cfc +++ b/vendor/wheels/model/read.cfc @@ -214,7 +214,12 @@ component { list = arguments.select, returnAs = arguments.returnAs ); - local.columns = ReReplace(local.columns, "[`""\[\]\w]*?\.([\w\s]*?)(,|$)", "\1\2", "all"); + // Strip any identifier-quote characters first ($createSQLFieldList routes + // both table and column names through $quoteIdentifier, so this list arrives + // dialect-quoted on MSSQL/MySQL/PostgreSQL/SQLite/H2). The downstream + // strip-table-prefix regex only handles bare identifiers. + local.columns = variables.wheels.class.adapter.$stripIdentifierQuotes(local.columns); + local.columns = ReReplace(local.columns, "[\w]*?\.([\w\s]*?)(,|$)", "\1\2", "all"); local.columns = ReReplace(local.columns, "\(.*?\)\sAS\s([\w\s]*?)(,|$)", "\1\2", "all"); local.columns = ReReplace(local.columns, "\w*?\sAS\s([\w\s]*?)(,|$)", "\1\2", "all"); local.rv = QueryNew(local.columns); diff --git a/vendor/wheels/model/sql.cfc b/vendor/wheels/model/sql.cfc index c7040bf2aa..66a533d91f 100644 --- a/vendor/wheels/model/sql.cfc +++ b/vendor/wheels/model/sql.cfc @@ -567,9 +567,9 @@ component { if (StructKeyExists(local.classData.propertyStruct, local.iItem)) { local.toAppend &= variables.wheels.class.adapter.$quoteIdentifier(local.classData.tableName) & "."; if (StructKeyExists(local.classData.columnStruct, local.iItem)) { - local.toAppend &= local.iItem; + local.toAppend &= variables.wheels.class.adapter.$quoteIdentifier(local.iItem); } else { - local.toAppend &= local.classData.properties[local.iItem].column; + local.toAppend &= variables.wheels.class.adapter.$quoteIdentifier(local.classData.properties[local.iItem].column); if (arguments.clause == "select") { local.toAppend &= " AS " & local.iItem; } @@ -631,6 +631,12 @@ component { // get the property part, done by taking everything from the end of the string to a . or a space (which would be found when using " AS ") local.property = Reverse(SpanExcluding(Reverse(local.iItem), ". ")); + // $createSQLFieldList now routes column identifiers through $quoteIdentifier, so + // items without an AS alias arrive here as `"table"."col"` and the extraction above + // captures the quoted column. Strip quote chars so downstream concatenation produces + // a usable alias and the ReplaceNoCase below can match the unquoted ` AS ` form. + local.property = variables.wheels.class.adapter.$stripIdentifierQuotes(local.property); + // check if this one has been flagged as a duplicate, we get the number of classes to skip and also remove the flagged info from the item local.duplicateCount = 0; local.matches = ReFind("^\[\[duplicate\]\](\d+)(.+)$", local.iItem, 1, true); @@ -668,7 +674,7 @@ component { local.newItem = ReplaceNoCase(local.iItem, " AS " & local.property, " AS " & local.newProperty); } else { if (local.aliasFound) { - local.newItem = local.alias & "." & local.property & " AS " & local.newProperty; + local.newItem = local.alias & "." & variables.wheels.class.adapter.$quoteIdentifier(local.property) & " AS " & local.newProperty; } else { local.newItem = local.iItem & " AS " & local.newProperty; } diff --git a/vendor/wheels/tests/specs/model/crudSpec.cfc b/vendor/wheels/tests/specs/model/crudSpec.cfc index e58fe597a0..a151074d85 100644 --- a/vendor/wheels/tests/specs/model/crudSpec.cfc +++ b/vendor/wheels/tests/specs/model/crudSpec.cfc @@ -573,7 +573,7 @@ component extends="wheels.WheelsTest" { // trim extra whitespace actual = Trim(actual) - expected = "SELECT #qi('c_o_r_e_authors')#.id FROM #qi('c_o_r_e_authors')#" + expected = "SELECT #qi('c_o_r_e_authors')#.#qi('id')# FROM #qi('c_o_r_e_authors')#" expect(actual).toBe(expected) }) @@ -1671,7 +1671,7 @@ component extends="wheels.WheelsTest" { local.a = qi("c_o_r_e_authors"); local.p = qi("c_o_r_e_posts"); - expect(columnList).toBe("#local.a#.firstname,#local.a#.id,#local.a#.id AS Authorid,#local.a#.lastname,#local.p#.averagerating AS postaveragerating,#local.p#.body AS postbody,#local.p#.createdat AS postcreatedat,#local.p#.deletedat AS postdeletedat,#local.p#.id AS postid,#local.p#.status AS poststatus,#local.p#.title AS posttitle,#local.p#.updatedat AS postupdatedat,#local.p#.views AS postviews") + expect(columnList).toBe("#local.a#.#qi('firstname')#,#local.a#.#qi('id')#,#local.a#.#qi('id')# AS Authorid,#local.a#.#qi('lastname')#,#local.p#.#qi('averagerating')# AS postaveragerating,#local.p#.#qi('body')# AS postbody,#local.p#.#qi('createdat')# AS postcreatedat,#local.p#.#qi('deletedat')# AS postdeletedat,#local.p#.#qi('id')# AS postid,#local.p#.#qi('status')# AS poststatus,#local.p#.#qi('title')# AS posttitle,#local.p#.#qi('updatedat')# AS postupdatedat,#local.p#.#qi('views')# AS postviews") }) it("works with association with expanded aliases disabled", () => { @@ -1688,7 +1688,7 @@ component extends="wheels.WheelsTest" { local.a = qi("c_o_r_e_authors"); local.p = qi("c_o_r_e_posts"); - expect(columnList).toBe("#local.a#.firstname,#local.a#.id,#local.a#.id AS Authorid,#local.a#.lastname,#local.p#.averagerating,#local.p#.body,#local.p#.createdat,#local.p#.deletedat,#local.p#.id AS postid,#local.p#.status,#local.p#.title,#local.p#.updatedat,#local.p#.views") + expect(columnList).toBe("#local.a#.#qi('firstname')#,#local.a#.#qi('id')#,#local.a#.#qi('id')# AS Authorid,#local.a#.#qi('lastname')#,#local.p#.#qi('averagerating')#,#local.p#.#qi('body')#,#local.p#.#qi('createdat')#,#local.p#.#qi('deletedat')#,#local.p#.#qi('id')# AS postid,#local.p#.#qi('status')#,#local.p#.#qi('title')#,#local.p#.#qi('updatedat')#,#local.p#.#qi('views')#") }) it("works on calculated property", () => { diff --git a/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc b/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc new file mode 100644 index 0000000000..62c572be17 --- /dev/null +++ b/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc @@ -0,0 +1,51 @@ +component extends="wheels.WheelsTest" { + + function run() { + + g = application.wo; + + describe("$createSQLFieldList column identifier quoting", () => { + + it("quotes the underlying column when a property aliases a different column name", () => { + // City has `property(name="id", column="countyid")`, so SELECT id must + // render the column part quoted to survive databases where the column + // name is a reserved word (e.g. `key` on MSSQL, `order` everywhere). + var m = g.model("city"); + var actual = m.$selectClause(select = "id", include = "", returnAs = "objects"); + var qTable = m.$quoteColumn("c_o_r_e_cities"); + var qCol = m.$quoteColumn("countyid"); + + expect(actual).toInclude(qTable & "." & qCol); + }); + + it("quotes the column when the property name matches the column name", () => { + // Author.firstName has no column mapping (property == column). SELECT + // firstName must still wrap the column identifier so reserved words + // like `order` or `key` survive the SELECT clause. + var m = g.model("author"); + var actual = m.$selectClause(select = "firstName", include = "", returnAs = "objects"); + var qTable = m.$quoteColumn("c_o_r_e_authors"); + var qCol = m.$quoteColumn("firstName"); + + expect(actual).toInclude(qTable & "." & qCol); + }); + + it("quotes column identifiers in the GROUP BY clause too", () => { + // $groupByClause routes through the same $createSQLFieldList builder. + var m = g.model("city"); + var actual = m.$groupByClause( + select = "id", + include = "", + group = "id", + distinct = false, + returnAs = "objects" + ); + var qTable = m.$quoteColumn("c_o_r_e_cities"); + var qCol = m.$quoteColumn("countyid"); + + expect(actual).toInclude(qTable & "." & qCol); + }); + + }); + } +} From c81c45dbeb4b8b06e5c5a849da4c30b1ab1c86bc Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:08:39 +0000 Subject: [PATCH 2/4] docs(web/guides): note reserved-word column support via property alias in models guide Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/sites/guides/src/content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx b/web/sites/guides/src/content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx index 68967c41f9..7ad5c0acfd 100644 --- a/web/sites/guides/src/content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx +++ b/web/sites/guides/src/content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx @@ -51,7 +51,7 @@ When the table, key, or column names don't match the defaults, override them in - `tableName("old_blog_entries")` — non-convention table name. - `setPrimaryKey("entryId")` — non-`id` primary key. - `dataSource("legacy_db")` — non-default datasource. See [Database and Multiple Datasources](/v4-0-1-snapshot/basics/database-and-multiple-datasources/) when this lands for the wider multi-database story. -- `property(name="publishedAt", column="pub_date")` — the property on the model is `publishedAt`, the column in the table is `pub_date`. The view and controller never see the column name. +- `property(name="publishedAt", column="pub_date")` — the property on the model is `publishedAt`, the column in the table is `pub_date`. The view and controller never see the column name. This is also the correct pattern when a column name is a SQL reserved word (`key`, `order`, `group`, etc.) — Wheels quotes all column identifiers in SELECT and GROUP BY, so the alias stays clean while the generated SQL brackets the underlying column name for the target dialect. ```cfm {test:compile} component extends="Model" { From b49a25b92751bb124ea2751995586df1a12ce249 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:25:08 +0000 Subject: [PATCH 3/4] fix(model): address Reviewer A/B consensus findings (round 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Condense 4-line block comments at vendor/wheels/model/read.cfc:217 and vendor/wheels/model/sql.cfc:634 to single-line comments (CLAUDE.md: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max"). - Stop using $quoteColumn() for the table-name argument in vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc; switch to the model's public $quotedTableName() helper so the spec names match what each helper actually quotes. - Add a zero-row paginated findAll spec to reservedColumnQuotingSpec.cfc that exercises the QueryNew branch in vendor/wheels/model/read.cfc:225 with an aliased column, covering the path the original spec did not reach. - Mention ORDER BY alongside SELECT and GROUP BY in web/sites/guides/src/content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx so readers do not infer ORDER BY is unsafe with reserved-word columns (ORDER BY already routes through $quoteIdentifier). Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- vendor/wheels/model/read.cfc | 5 +--- vendor/wheels/model/sql.cfc | 5 +--- .../specs/model/reservedColumnQuotingSpec.cfc | 27 ++++++++++++------- .../basics/models-and-the-orm.mdx | 2 +- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/vendor/wheels/model/read.cfc b/vendor/wheels/model/read.cfc index 0c60e9d190..8b0b1d53c6 100644 --- a/vendor/wheels/model/read.cfc +++ b/vendor/wheels/model/read.cfc @@ -214,10 +214,7 @@ component { list = arguments.select, returnAs = arguments.returnAs ); - // Strip any identifier-quote characters first ($createSQLFieldList routes - // both table and column names through $quoteIdentifier, so this list arrives - // dialect-quoted on MSSQL/MySQL/PostgreSQL/SQLite/H2). The downstream - // strip-table-prefix regex only handles bare identifiers. + // Strip dialect quotes: $createSQLFieldList now quotes identifiers; the bare-identifier regex below requires unquoted input. local.columns = variables.wheels.class.adapter.$stripIdentifierQuotes(local.columns); local.columns = ReReplace(local.columns, "[\w]*?\.([\w\s]*?)(,|$)", "\1\2", "all"); local.columns = ReReplace(local.columns, "\(.*?\)\sAS\s([\w\s]*?)(,|$)", "\1\2", "all"); diff --git a/vendor/wheels/model/sql.cfc b/vendor/wheels/model/sql.cfc index 66a533d91f..f62ec78958 100644 --- a/vendor/wheels/model/sql.cfc +++ b/vendor/wheels/model/sql.cfc @@ -631,10 +631,7 @@ component { // get the property part, done by taking everything from the end of the string to a . or a space (which would be found when using " AS ") local.property = Reverse(SpanExcluding(Reverse(local.iItem), ". ")); - // $createSQLFieldList now routes column identifiers through $quoteIdentifier, so - // items without an AS alias arrive here as `"table"."col"` and the extraction above - // captures the quoted column. Strip quote chars so downstream concatenation produces - // a usable alias and the ReplaceNoCase below can match the unquoted ` AS ` form. + // Strip dialect quotes added above so alias matching and downstream concatenation work on bare identifiers. local.property = variables.wheels.class.adapter.$stripIdentifierQuotes(local.property); // check if this one has been flagged as a duplicate, we get the number of classes to skip and also remove the flagged info from the item diff --git a/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc b/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc index 62c572be17..0d5851fe30 100644 --- a/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc +++ b/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc @@ -12,10 +12,8 @@ component extends="wheels.WheelsTest" { // name is a reserved word (e.g. `key` on MSSQL, `order` everywhere). var m = g.model("city"); var actual = m.$selectClause(select = "id", include = "", returnAs = "objects"); - var qTable = m.$quoteColumn("c_o_r_e_cities"); - var qCol = m.$quoteColumn("countyid"); - expect(actual).toInclude(qTable & "." & qCol); + expect(actual).toInclude(m.$quotedTableName() & "." & m.$quoteColumn("countyid")); }); it("quotes the column when the property name matches the column name", () => { @@ -24,10 +22,8 @@ component extends="wheels.WheelsTest" { // like `order` or `key` survive the SELECT clause. var m = g.model("author"); var actual = m.$selectClause(select = "firstName", include = "", returnAs = "objects"); - var qTable = m.$quoteColumn("c_o_r_e_authors"); - var qCol = m.$quoteColumn("firstName"); - expect(actual).toInclude(qTable & "." & qCol); + expect(actual).toInclude(m.$quotedTableName() & "." & m.$quoteColumn("firstName")); }); it("quotes column identifiers in the GROUP BY clause too", () => { @@ -40,10 +36,23 @@ component extends="wheels.WheelsTest" { distinct = false, returnAs = "objects" ); - var qTable = m.$quoteColumn("c_o_r_e_cities"); - var qCol = m.$quoteColumn("countyid"); - expect(actual).toInclude(qTable & "." & qCol); + expect(actual).toInclude(m.$quotedTableName() & "." & m.$quoteColumn("countyid")); + }); + + it("returns a well-formed empty query when paginated findAll matches zero rows on an aliased column", () => { + // Exercises the read.cfc QueryNew(local.columns) branch: when the + // paginated count is zero, the column list is built from the SELECT + // clause (now identifier-quoted) and must be stripped back to bare + // property names before being handed to QueryNew. Without the + // adapter $stripIdentifierQuotes call, dialect quote chars would + // leak into QueryNew column names. + var m = g.model("city"); + var rv = m.findAll(select = "id", where = "id = -1", page = 1, perPage = 25); + + expect(IsQuery(rv)).toBeTrue(); + expect(rv.recordCount).toBe(0); + expect(ListFindNoCase(rv.columnList, "id")).toBeGT(0); }); }); diff --git a/web/sites/guides/src/content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx b/web/sites/guides/src/content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx index 7ad5c0acfd..a4cbcaee1f 100644 --- a/web/sites/guides/src/content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx +++ b/web/sites/guides/src/content/docs/v4-0-1-snapshot/basics/models-and-the-orm.mdx @@ -51,7 +51,7 @@ When the table, key, or column names don't match the defaults, override them in - `tableName("old_blog_entries")` — non-convention table name. - `setPrimaryKey("entryId")` — non-`id` primary key. - `dataSource("legacy_db")` — non-default datasource. See [Database and Multiple Datasources](/v4-0-1-snapshot/basics/database-and-multiple-datasources/) when this lands for the wider multi-database story. -- `property(name="publishedAt", column="pub_date")` — the property on the model is `publishedAt`, the column in the table is `pub_date`. The view and controller never see the column name. This is also the correct pattern when a column name is a SQL reserved word (`key`, `order`, `group`, etc.) — Wheels quotes all column identifiers in SELECT and GROUP BY, so the alias stays clean while the generated SQL brackets the underlying column name for the target dialect. +- `property(name="publishedAt", column="pub_date")` — the property on the model is `publishedAt`, the column in the table is `pub_date`. The view and controller never see the column name. This is also the correct pattern when a column name is a SQL reserved word (`key`, `order`, `group`, etc.) — Wheels quotes all column identifiers in SELECT, GROUP BY, and ORDER BY clauses, so the alias stays clean while the generated SQL brackets the underlying column name for the target dialect. ```cfm {test:compile} component extends="Model" { From 49744bc721355bbf3613478767c2166e7ae907fd Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:45:06 +0000 Subject: [PATCH 4/4] fix(model): address Reviewer A/B consensus findings (round 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Condense the remaining multi-line comment blocks in reservedColumnQuotingSpec.cfc to single lines per CLAUDE.md ("Never write multi-paragraph docstrings or multi-line comment blocks — one short line max"): - vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc:10 — 3-line block about City's id -> countyid alias condensed. - vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc:18 — 3-line block about Author.firstName (property == column) condensed. - vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc:40 — 6-line block added in round 1 inside the zero-row pagination it() condensed to the single-line form Reviewer A supplied. The line-30 GROUP BY comment was already single-line; A's "30-32" citation was off-by-one for that one. No production code changed; pure comment-style fix. Test totals unchanged at 4 pass / 0 fail in the spec; full model suite remains 839 pass / 0 fail / 0 error / 11 skipped across 35 bundles. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../specs/model/reservedColumnQuotingSpec.cfc | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc b/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc index 0d5851fe30..f9eef9aa04 100644 --- a/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc +++ b/vendor/wheels/tests/specs/model/reservedColumnQuotingSpec.cfc @@ -7,9 +7,7 @@ component extends="wheels.WheelsTest" { describe("$createSQLFieldList column identifier quoting", () => { it("quotes the underlying column when a property aliases a different column name", () => { - // City has `property(name="id", column="countyid")`, so SELECT id must - // render the column part quoted to survive databases where the column - // name is a reserved word (e.g. `key` on MSSQL, `order` everywhere). + // City aliases id -> countyid, so the SELECT column must be quoted in case it is a reserved word. var m = g.model("city"); var actual = m.$selectClause(select = "id", include = "", returnAs = "objects"); @@ -17,9 +15,7 @@ component extends="wheels.WheelsTest" { }); it("quotes the column when the property name matches the column name", () => { - // Author.firstName has no column mapping (property == column). SELECT - // firstName must still wrap the column identifier so reserved words - // like `order` or `key` survive the SELECT clause. + // Author.firstName has no column mapping (property == column); SELECT must still quote the identifier. var m = g.model("author"); var actual = m.$selectClause(select = "firstName", include = "", returnAs = "objects"); @@ -41,12 +37,7 @@ component extends="wheels.WheelsTest" { }); it("returns a well-formed empty query when paginated findAll matches zero rows on an aliased column", () => { - // Exercises the read.cfc QueryNew(local.columns) branch: when the - // paginated count is zero, the column list is built from the SELECT - // clause (now identifier-quoted) and must be stripped back to bare - // property names before being handed to QueryNew. Without the - // adapter $stripIdentifierQuotes call, dialect quote chars would - // leak into QueryNew column names. + // Without $stripIdentifierQuotes, dialect quote chars from $createSQLFieldList leak into QueryNew column names. var m = g.model("city"); var rv = m.findAll(select = "id", where = "id = -1", page = 1, perPage = 25);