Skip to content

Bring Sql\Predicate to full Mago compliance - #230

Merged
simon-mundy merged 7 commits into
php-db:0.6.xfrom
simon-mundy:qa/mago-sql-predicate
Oct 12, 2026
Merged

simon-mundy merged 7 commits into
php-db:0.6.xfrom
simon-mundy:qa/mago-sql-predicate

Conversation

@simon-mundy

@simon-mundy simon-mundy commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Takes Sql\Predicate out of both Mago baselines entirely, and fixes two bugs found along the way.

Stacked on #229 (analyzer baseline refresh for Mago 1.56.1). Its single commit appears here until that merges.

Bugs fixed

Upper-case magic properties were ignored. Predicate documents AND, OR, NEST and UNNEST next to their lower-case forms, and docs/book/sql/where-having.md called the names case-insensitive. An earlier refactor dropped the strtolower() in __get(), so upper-case names fell through and returned the predicate unchanged:

$where->equalTo('a', 1)->OR->equalTo('b', 2);
// before: a = ? AND b = ?
// after:  a = ? OR b = ?

addPredicates(['a = ?']) threw a TypeError. A keyless string with a placeholder was built as a Sql\Expression, which is not a PredicateInterface. It is now a Predicate\Expression, as it was in laminas-db.

Behaviour changes

  • __get() now matches exactly the names its @property tags declare: or/OR, and/AND, nest/NEST and unnest/UNNEST. It no longer lower-cases every name it's given. A mixed-case form of one of those names, such as Or, throws InvalidArgumentException::forMixedCaseMagicProperty() rather than silently leaving the next predicate's combination unchanged. Other unknown names still return the predicate. The where-having guide now documents lower or upper case and the exception.

  • A keyless entry that is not a predicate, an expression or a string now throws InvalidArgumentException::forInvalidListPredicate(). It used to throw a TypeError from str_contains().

  • A Select bound to a placeholder key (['id IN (?)' => $select]) is wrapped in Argument\Select and reaches the expression as a parameter. It used to throw a TypeError.

Cleanup

  • addPredicates() builds entries through two private helpers, one per key type. Both are typed against a PredicateValue alias that the where() and having() docblocks on Select, Update and Delete now share.
  • getExpressionData() drops its special cases for zero and one predicate, because the loop gives the same result for both. This clears the halstead and kan-defect findings on PredicateSet.
  • The @property tags for unnest and UNNEST now declare ?Predicate, matching the private ?Predicate $unnest property behind them. Mago applies those tags to $this->unnest inside the class, so with a non-null type it reported the "not nested" null guard as impossible and the null assignments as invalid. __get('unnest') still returns the parent predicate or throws when the predicate is not nested.
  • Every class and interface in the component is tagged @api. None is made final. Mago's finality check accepts @api, so this settles both class-must-be-final and missing-api-or-internal while leaving the classes open to the platform packages.
  • Predicate and the Predicate exception catalogue join the too-many-methods exclusions in mago.toml. It has one fluent method per predicate type, so its method count is the size of its public API.

Result

Before (#229) After
Lint baseline (issues) 225 222
Analyzer baseline (issues) 721 697
src/Sql/Predicate in either baseline 33 0
PHPUnit 2135 tests 2154 tests

Typing the predicate arrays moves six findings onto the where() calls in AbstractTableGateway and AbstractRowGateway. They are baselined for the gateway pass.

Line and method coverage of src/Sql/Predicate is 100%. Infection reports 99% covered-code MSI. The two survivors are in untouched code: a protected-to-private visibility mutant on getNextPredicateCombineOperator(), and the ?? in In::getExpressionData().

The class documents AND, OR, NEST and UNNEST alongside their lower-case
forms, and the docs promise the names are case-insensitive, but __get()
lost its strtolower() in an earlier refactor. Upper-case names fell
through to the default and returned the predicate unchanged, so
$where->OR->equalTo(...) was silently combined with AND.

The private $unnest property is renamed $parent. It shared its name with
the @Property Predicate $unnest tag, so the analyzer read every access
as the magic property and reported the null guard as impossible.

The in(), notIn() and expression() docblocks now carry the value types
their predicates accept.
A string with a placeholder and no key became a Sql\Expression, which
is not a PredicateInterface, so addPredicates(['a = ?']) threw a
TypeError. It now becomes a Predicate\Expression, as it did before the
refactor. A Select bound to a placeholder key is wrapped in an
Argument\Select so it reaches the expression as a parameter.

A list entry that is not a predicate, an expression or a string now
throws InvalidArgumentException::forInvalidListPredicate() rather than
a TypeError from str_contains().

Entries are built by two private helpers, one per key type, typed
against a PredicateValue alias that the where() and having() docblocks
on Select, Update and Delete now share. getExpressionData() drops its
special cases for zero and one predicate; the loop produces the same
result for both.
Every class and interface under Sql\Predicate is public API that
callers build and extend, so each is marked @api rather than final.
Mago's class finality check accepts @api, which settles both the
class-must-be-final and missing-api-or-internal findings without
closing the classes to the platform packages.

Predicate joins the too-many-methods exclusions. It is the fluent
WHERE and HAVING builder, one method per predicate type, so its method
count is the size of its public API.

Sql\Predicate now leaves both baselines entirely. Typing the predicate
arrays moves six findings onto the TableGateway and RowGateway callers
of where(), which are left for the gateway pass.
@codecov

codecov Bot commented Oct 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@simon-mundy
simon-mundy marked this pull request as draft October 11, 2026 23:09
@simon-mundy simon-mundy self-assigned this Oct 11, 2026
@simon-mundy
simon-mundy requested a review from tyrsson October 11, 2026 23:35
@simon-mundy simon-mundy added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request qa Improvements in quality assurance of the project labels Oct 11, 2026
@simon-mundy simon-mundy added this to the 0.6.0 milestone Oct 11, 2026
@github-project-automation github-project-automation Bot moved this to Todo in @phpdb Oct 11, 2026
__get() matches the lower- and upper-case names its @Property tags
declare instead of lower-casing every name it is given. A mixed-case
form of a known name, such as Or, now throws
InvalidArgumentException::forMixedCaseMagicProperty() rather than
falling through and silently leaving the next predicate's combination
unchanged. Other unknown names still return the predicate.

The where-having guide described the names as case-insensitive. It now
documents lower and upper case and the exception, and its example loses
a stray quote.

The Predicate exception catalogue joins the too-many-methods
exclusions alongside src/Sql/Exception/.
@simon-mundy
simon-mundy marked this pull request as ready for review October 11, 2026 23:43
tyrsson
tyrsson previously approved these changes Oct 12, 2026
@simon-mundy
simon-mundy dismissed tyrsson’s stale review October 12, 2026 02:13

The merge-base changed after approval.

simon-mundy and others added 3 commits October 12, 2026 13:14
Mago 1.56.2 reports the PDO driver's docblock as narrower than
DriverInterface::createStatement(), which documents resource|string.
The PDO driver now documents resource as well. It already treats any
argument that is not a PDOStatement or a string as no SQL and prepares
a fresh statement, so behaviour is unchanged.
@simon-mundy
simon-mundy merged commit ceebfbf into php-db:0.6.x Oct 12, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request qa Improvements in quality assurance of the project

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants