Repository navigation
Quote Select, join and GROUP BY column names as identifiers - #241
Open
simon-mundy wants to merge 2 commits into
Open
simon-mundy wants to merge 2 commits into
simon-mundy wants to merge 2 commits into
Conversation
Select resolved each column string through quoteIdentifierInFragment(), which splits on every non-word character, so a column such as x-y rendered as "x""-""y" while its alias rendered as "x-y". Column strings in the select list, join column lists and GROUP BY are now quoted as an identifier chain split only on the table-qualifier dot, with a * part left bare. A string holding an inline " AS " alias keeps fragment quoting, as before.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
A column string now switches to identifier-chain quoting only when fragment quoting would render it broken: a piece between the safe delimiters (space, "." and "*") that the platform would split and quote in parts, as with x-y, a"b or COUNT(id), or two names separated by spaces alone, as with "my col". Strings such as "price * qty" or "a*b" render exactly as before.
This was referenced Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Select fragment-quotes column names that are not plain or dotted words. On the SQL-92 platform:
x-y"foo"."x""-""y" AS "x-y""foo"."x-y" AS "x-y"my col"foo"."my" "col" AS "my col""foo"."my col" AS "my col"a"b"foo"."a""'""b" AS "a'b""foo"."a'b" AS "a'b"The alias was already quoted as a whole name, so the column and its alias disagreed. Join column lists and
GROUP BYrender the same broken way.Cause
processSelect()(select and join columns) andprocessGroup()resolve each column string throughAbstractSql::resolveColumnValue()withisIdentifier. That ends inAbstractPlatform::quoteIdentifierInFragment(), which splits the string on every character outside the platform's word pattern and quotes each piece. Only*, space,.andasare left bare (SAFE_WORDS). Update SET columns had the same cause, fixed in #238.Rule
Select now overrides
resolveColumnValue()for an identifier reference whose column is a string. Whole-name quoting is used only where fragment quoting renders broken SQL today:as(case-insensitive, spaces on both sides) keeps fragment quoting, as before.processSelect()already detects this form withstripos($columnName, ' as ')so it doesn't add a second alias..and*. If fragment quoting renders it as valid SQL, it keeps fragment quoting, so the output is byte-identical to today. That holds when both of these are true:quoteIdentifierInFragment($piece) === quoteIdentifier($piece), so the fragment split never produces"x""-""y";"my" "col".., each part is quoted withquoteIdentifier(), and the parts are joined withgetIdentifierSeparator(). A*part stays bare.Step 2 asks the platform itself, so it follows each platform's own fragment pattern. For example, MySQL treats
-as a word character, sox-ykeeps its existing`x-y`there. WithquoteIdentifiersoff, both quoting methods return the string as given, so every string keeps the fragment path and output does not change.Expressions, sub-selects and every other use of
resolveColumnValue()(Update, Insert) still go through the parent method unchanged.What stays the same
docs/book, the unit tests, TableGateway/RowGateway and phpdb-mysql, phpdb-sqlite and phpdb-pgsql only pass these forms as column strings:
bar)t.bar)*['alias' => 'bar'])SQL in a column always goes through
Expression. All of these render byte-for-byte as before. So does every string that renders as valid SQL today, including:t.*("t".*)price * qty("price" * "qty"),a*b("a"*"b") andt.price * t.qty("t"."price" * "t"."qty")bar AS baz("bar" AS "baz")Behaviour change
This change is not breaking: output only changes for strings whose output was invalid SQL. Those are strings where the fragment split glued quoted pieces together or quoted a delimiter that isn't a safe word:
x-ya"bmy colCOUNT(id), which was"COUNT""(""id"")"and is now"COUNT(id)". It is still not a function call.SQL in a column list should still use
new Expression(...), as the docs show.Tests
test/unit/Sql/SelectColumnNameTest.php(#[Group('unit')]) has three data-provider tests, one each for select columns, join columns and GROUP BY. Cases:*,t.*, hyphen, space, quote character, dotted hyphen or spaceAS(upper and lower case), function callExpressioncolumnsRun against the 0.6.x
Select.php, only the cases broken today fail: hyphen, space, quote character, function call, and arithmetic with a hyphenated name. Every other case, including all the plain arithmetic cases, passes unchanged. The changed lines inSelect.phpare fully covered../vendor/bin/phpunit --testsuite "unit test": OK (2237 tests)mago format --check,mago lint,mago analyze: all exit 0Follow-ups (not in this PR)
processOrder()passes the parsed key straight toquoteIdentifierInFragment(), sox-y DESCrenders"x""-""y" DESC. The cause is the same, but the order string is a small parsed language ('a DESC, b'), so it should be a separate change.t.*in the select list or a join list still getsAS "t.*"appended ("t".* AS "t.*"), which is invalid SQL. A bare*is already exempt.ASin join columns and GROUP BY: the join list appends a second alias ("zac"."bar" AS "baz" AS "bar AS baz"), unlike the select list.'forquoteIdentifierTo, so an embedded"becomes'rather than"".