Skip to content

Bring the Sql DML classes to Mago compliance - #232

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

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

Conversation

@simon-mundy

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

Copy link
Copy Markdown
Member

Takes the DML classes (AbstractSql, Select, Insert, Update, Delete, Combine, Join, Sql, Expression, ArgumentType) from 194 baselined findings to 17, and fixes four bugs along the way. It also clears the three processTable() findings left in Ddl.

Stacked on #231 (Ddl), #230 (Predicate) and #229 (baseline refresh). Only the last two commits are new here.

Bugs fixed

Before After
Preparing new Expression('COALESCE(?, 1)', [null]) threw a TypeError The null is bound as a parameter
(new Update('t'))->set(['a' => 1.5])->getSqlString() threw a TypeError, as did the same value in Insert::values() '1.5' is quoted
$select->join('b', 'a.id = b.id', [new Expression('COUNT(*)')]) threw a TypeError The expression renders as COUNT(*) AS Expression1, as in the main column list
Join::current() past the last join read an undefined index Returns null

Each has a regression test that fails on #231.

Design changes

  • Lazy clauses keep their laziness and their names. Select, Update and Delete store their lazy clauses in ?Where $where, ?Having $having and ?Join $joins. Their @property tags for __get() declared non-null types, and Mago applies those tags to $this->where inside the class too, so it reported every null check and null assignment that makes the clauses lazy (28 findings). The tags now declare ?Where, ?Having and ?Join. __get() still always returns a clause at runtime. The trade-off is that analysers will see $select->where as nullable in calling code.
  • Select's table resolution is its own method. Select::resolveTable() overrode the parent with a different return shape ([table, fromTable]), which forced AbstractSql::resolveTable() to declare string|array|null. The Select logic is now a private resolveSelectTable(), and resolveTable() returns ?string. No platform package calls or overrides it.
  • The specification renderer is split into typed steps. createSqlFromSpecificationAndParameters() now delegates to three private steps: match a format, render each parameter, and format a parameter list. A slot that has no parameter specification now rejects a list with the existing forUnsupportedParameterCountOf() exception. It used to print Array.
  • formatSpecification() replaces the str_replace() calls on $specifications. It substitutes values exactly as before, and throws the new RuntimeException::forStringSpecificationRequired() if a specification isn't a string. That case used to end in a TypeError.
  • Parameters are bound only when there's a driver to format their names. With a container but no driver, values are quoted instead of crashing on null->formatParameterName().
  • New types: a ColumnValue alias types Update::set() and Insert::values(). Sql::getAdapter() and getSqlPlatform() no longer declare a null they never return.
  • @throws tags: the thrown RuntimeException is documented through every method it reaches, including the DDL processors.
  • processExpressionList() replaces four identical loops in AlterTable and CreateTable.
  • @api and mago.toml:

Still baselined (17)

  • Class-level cyclomatic-complexity and kan-defect on AbstractSql, Select, Insert, Update and Combine. Clearing these needs the statement classes split up, for example by extracting the renderer.
  • halstead on Select::reset(), an eleven-way dispatcher.
  • excessive-parameter-list on the protected processExpressionOrSelect().
  • string-member-selector and mixed-assignment in buildSqlString() and localizeVariables(), which dispatch dynamically by design.
  • missing-return-type on __clone(), __isset() and __unset(). These are left untyped so subclass overrides stay compatible.

The typed signatures move 13 findings onto the TableGateway and RowGateway callers, for the gateway pass.

Result

Before (#231) After
Lint baseline (issues) 216 207
Analyzer baseline (issues) 638 492
src/Sql outside Sql\Platform, both baselines 194 17
PHPUnit 2156 tests 2176 tests

Line and method coverage of every changed src/Sql class is 100%. Infection on the changed files reports 92% covered-code MSI. Most of the remaining survivors are protected-to-private visibility mutants and lines this PR does not change.

@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!

Bugs fixed:

- Preparing an expression with a null parameter threw a TypeError.
  processExpressionParameterName() now accepts null.
- A float value in Update::set() or Insert::values() made
  getSqlString() throw a TypeError. resolveColumnValue() accepts floats.
- An unkeyed expression in a join's column list was passed to
  quoteIdentifier() and threw. It is now aliased ExpressionN, as the
  main column list does.
- Join::current() returned an undefined index past the last join. It
  returns null.

Select, Update and Delete keep their WHERE, HAVING and JOIN clauses
lazy, but the backing properties are renamed whereClause, havingClause
and joinClause. They shared their names with the @Property tags for
__get(), so the analyzer read every access as the non-null magic
property.

Select's table resolution moves to a private resolveSelectTable(). It
overrode resolveTable() with a different return shape, which forced
AbstractSql::resolveTable() to declare string|array|null. It now
returns ?string, which also clears the three processTable() findings
left in Ddl.

The specification renderer is split into steps that match a format,
render each parameter and format a parameter list, each with exact
types. A slot without a parameter specification now rejects a list
with the catalogued parameter-count exception instead of printing
"Array". Single-string specifications are read through
formatSpecification(), which throws
RuntimeException::forStringSpecificationRequired() for a specification
that is not a string.

Parameters are bound only when a driver is present to format their
names, falling back to quoting the value. A ColumnValue alias types
Update's set values and Insert's column values. The thrown
RuntimeException is documented through every method it reaches, the
DDL processors among them. processExpressionList() replaces the four
loops in AlterTable and CreateTable that rendered expression lists.
Every top-level Sql class, interface and enum is marked @api rather
than final, as in Predicate and Ddl.

Select, Insert, Update and AbstractSql join the too-many-methods
exclusions, and Select the too-many-properties exclusion: the builders
carry one method or property per clause, and AbstractSql one hook per
clause it renders. ArgumentType gets a scoped analyzer ignore for
invalid-enum-case-value, which rejects ::class constants as values of a
string-backed enum.

Seventeen DML findings stay baselined:

- class-level cyclomatic-complexity and kan-defect on AbstractSql,
  Select, Insert, Update and Combine, which would need the statement
  classes split up;
- halstead on Select::reset(), an eleven-way dispatcher;
- excessive-parameter-list on processExpressionOrSelect(), a protected
  signature;
- string-member-selector and mixed-assignment in buildSqlString() and
  localizeVariables(), which dispatch dynamically by design;
- missing-return-type on __clone(), __isset() and __unset(), left
  untyped so subclass overrides stay compatible.

The typed predicate and column signatures move thirteen findings onto
the TableGateway and RowGateway callers, for the gateway pass.
@simon-mundy
simon-mundy requested a review from tyrsson October 12, 2026 02:48
@simon-mundy simon-mundy self-assigned this Oct 12, 2026
@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 12, 2026
@simon-mundy simon-mundy added this to the 0.6.0 milestone Oct 12, 2026
@github-project-automation github-project-automation Bot moved this to Todo in @phpdb Oct 12, 2026
Predicate's ExpressionParameter import named \PhpDb\Sql\Expression in
full, because Expression alone resolves to Predicate\Expression in this
namespace. It now imports the class under a SqlExpression alias.
The @throws tags added to AlterTable, CreateTable and DropTable named
\PhpDb\Sql\Exception\RuntimeException in full. Each class now imports it.
HydratingResultSet documented \Laminas\Hydrator\Exception\RuntimeException
in full; it now imports it as HydratorRuntimeException, since the class
already imports the ResultSet RuntimeException. TableIdentifierFactory
linked \PhpDb\ConfigProvider in full; it now imports ConfigProvider.
…ullable

This reverts the whereClause, havingClause and joinClause renaming in
Select, Update and Delete. The lazy clauses are stored in $where,
$having and $joins again, so subclasses that use those protected
properties keep working.

The analyzer read every $this->where inside these classes as the
@Property tag for __get(), which declared a non-null Where, and
reported the null checks and null assignments that make the clauses
lazy. The tags now declare ?Where, ?Having and ?Join, matching the
properties. __get() still always returns a clause at runtime.

Also prunes three baseline entries for AbstractPdo that the
createStatement() docblock fix resolved.
This reverts the $parent renaming, to match the clause properties on
Select, Update and Delete. The private property is $unnest again, and
the @Property tags for unnest and UNNEST declare ?Predicate so the
analyzer accepts the null checks and null assignments inside the
class. __get('unnest') still returns the parent or throws when the
predicate is not nested.
@simon-mundy
simon-mundy merged commit e9dd92d 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