Repository navigation
Quote predicate names as identifiers where fragment quoting renders broken SQL - #244
Open
simon-mundy wants to merge 12 commits into
Open
simon-mundy wants to merge 12 commits into
simon-mundy wants to merge 12 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.
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.
…roken SQL Predicate identifiers render through quoteIdentifierInFragment(), which quotes every piece split out on a non-word character other than a space, dot, * or AS. A name such as x-y, a"b, nâme or my col therefore renders as adjacent quoted pieces or a bare sequence of identifiers. Such names are now quoted whole as an identifier chain, split only on the qualifier dot. Every name whose fragment quoting renders valid SQL keeps its current rendering, judged against the platform's own fragment pattern.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Replace Select's own column-quoting helpers with IdentifierQuoter, which the ORDER BY and predicate fixes can share: - A plain name of word characters and qualifier dots takes a fast path straight to fragment quoting, as before. - Otherwise IdentifierShape reads the name in pieces. Fragment quoting is kept where it renders as written; a name it would split (x-y, my col, t.as) is quoted whole per qualifier part, with each part trimmed. - Expression syntax that no identifier quoting renders as written, such as COUNT(id), a - b or x-y * 2, throws InvalidArgumentException, as does an empty name or qualifier part. - An inline column alias such as 'x-y as z' renders "x-y" AS "z". TrustingHyphenWordPlatform becomes the shared hyphen-as-word test platform.
Replace quoteIdentifierArgument() with the shared IdentifierQuoter, so identifier arguments follow the same rule as columns and ORDER BY names. Expression syntax that no identifier quoting renders as written, such as a+b or COUNT(id), and an empty name now throw InvalidArgumentException instead of rendering broken SQL.
# Conflicts: # src/Sql/Select.php
This was referenced Nov 4, 2026
The test restricts coverage to the factories it lists, so the new forEmptyIdentifierPart() and forUnquotableIdentifier() counted as uncovered and failed codecov/patch.
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
A predicate's left-hand name is rendered through
AbstractPlatform::quoteIdentifierInFragment(). It splits the name on every character outside the platform's word class and quotes each piece, except spaces, dots,*andAS. A name that holds any other delimiter therefore renders as a different, invalid identifier:where->equalTo('x-y', 1)"x""-""y" = '1'"x-y" = '1'where(['t-1.x-y' => 1])"t""-""1"."x""-""y" = '1'"t-1"."x-y" = '1'where->like('a"b', …)"a""'""b" LIKE …"a'b" LIKE …where->isNull('nâme')"n""\xC3""\xA2""me" IS NULL(split bytes)"nâme" IS NULLhaving->greaterThan('my col', 1)"my" "col" > '1'"my col" > '1'This affects these predicates in WHERE and HAVING on Select, and in WHERE on Update and Delete:
equalTo,notEqualTolessThan,lessThanOrEqualTo,greaterThan,greaterThanOrEqualTolike,notLikein,notInbetween,notBetweenisNull,isNotNullCause
AbstractSql::processExpression()renders everyArgument\IdentifierthroughquoteIdentifierInFragment(), and so doesprocessIdentifiersArgument()forArgument\Identifiers. Fragment quoting is meant for loose SQL, not for a single name. Select columns (#241), ORDER BY names (#243) and Update SET columns (#238) have the same cause.Fix
Both now quote with
IdentifierQuoter::quote(), the shared rule added in #241:price*qty,a * bandx AS y, keep fragment quoting.InvalidArgumentException. Examples area - b,COUNT(id), anda+bon a platform where-is a word character. An empty name or qualifier part throws too.The check asks the platform itself, so phpdb-mysql, whose fragment pattern treats
-as a word character, keeps`a-b` * `c`unchanged.The same rule also applies to the other
Identifierarguments rendered throughprocessExpression(): identifier-typedExpressionparameters and DDL names. DDL names should always be quoted whole; that is listed as a follow-up in #241.What stays the same
t.*,price*qty,a * b,t.a * t.b,x as y/x AS yanda,brender exactly as before.Expression,Literaland string predicates are untouched, and so are values and string joinONclauses, which callquoteIdentifierInFragment()directly with extra safe words.Sql\Exception\InvalidArgumentException.Behaviour change
Output changes only for names that rendered invalid SQL before. Those that now throw (expression syntax and empty names) used to render broken SQL that only failed at the database. That includes the empty name, which rendered as nothing, e.g.
= '1'.Tests
test/unit/Sql/PredicateIdentifierNameTest.php(#[Group('unit')]) uses data providers to cover:null), HAVING, Update WHERE and Delete WHERE;Argument::identifiers()list;TrustingHyphenWordPlatform;COUNT(id),a - b,x-y * 2,a+bon the hyphen platform,''anda..b;Expression,Literaland string predicates left unchanged.Checks:
mago format --check,mago lintandmago analyzeexit 0 on PHP 8.3, 8.4 and 8.5, and so doesmago guard.