Skip to content

Quote predicate names as identifiers where fragment quoting renders broken SQL - #244

Open
simon-mundy wants to merge 1 commit into
php-db:0.6.xfrom
simon-mundy:fix/predicate-identifier
Open

simon-mundy wants to merge 1 commit into
php-db:0.6.xfrom
simon-mundy:fix/predicate-identifier

Conversation

@simon-mundy

Copy link
Copy Markdown
Member

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, * and AS. A name that holds any other delimiter therefore renders as a different, invalid identifier:

Call Today (SQL-92) After
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 NULL
having->greaterThan('my col', 1) "my" "col" > '1' "my col" > '1'

This affects equalTo, notEqualTo, lessThan(OrEqualTo), greaterThan(OrEqualTo), like/notLike, in/notIn, between/notBetween, isNull/isNotNull and where-array keys, in WHERE and HAVING on Select, and in WHERE on Update and Delete. It has the same root cause as #238 (Update SET columns) and #241 (Select, join and GROUP BY columns).

Cause

AbstractSql::processExpression() renders every Argument\Identifier through quoteIdentifierInFragment(), and so does processIdentifiersArgument() for Argument\Identifiers. Fragment quoting is meant for loose SQL, not for a single name.

The non-breaking rule

On 0.6.x only output that is broken today may change. A new private AbstractSql::quoteIdentifierArgument() decides per name:

The check asks the platform itself, so a platform whose fragment pattern treats - as a word character, as phpdb-mysql's does, keeps `a-b` * `c` unchanged.

What stays the same

  • Plain names, dotted names, t.*, price*qty, a * b, t.a * t.b, x as y / x AS y, a,b and the empty string render exactly as before.
  • Expression, Literal and string predicates are untouched, and so are values and string join ON clauses (they call quoteIdentifierInFragment() directly with extra safe words).
  • No public or protected API changes.

The same fix also reaches the other Identifier arguments rendered through processExpression(): identifier-typed Expression parameters and DDL names (column, index and constraint names). For those too, only names that render broken today change.

Context

The maintainer's long-term direction (D18) is that identifier slots take identifiers only, with SQL passed as an Expression. That change breaks forms like price*qty, so it belongs in a later major version. This PR fixes only what is broken today.

Tests

test/unit/Sql/PredicateIdentifierNameTest.php (#[Group('unit')]) uses data providers to cover:

  • Broken forms now quoted as one identifier: hyphen, space, quote character, non-ASCII, tab, dotted hyphen, dotted space and hyphenated qualified star. Each runs across equalTo, notEqualTo, lessThan, greaterThan, like, in, between and isNull, where-array keys with a value and with null, HAVING, Update WHERE and Delete WHERE.
  • Valid forms proven byte-identical across the same paths: plain, dotted, t.*, price*qty, a * b, t.a * t.b, x as y, x AS y, a,b and empty.
  • An Argument::identifiers() list, with each name quoted by the same rule.
  • A platform with a MySQL-style fragment pattern (new TrustingHyphenWordPlatform test asset), which keeps hyphenated names and arithmetic as before.
  • Expression, Literal and string predicates left unchanged.

The changed lines have 100% line coverage. The unit suite passes, and mago format --check, mago lint and mago analyze are clean.

…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

codecov Bot commented Oct 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant