feat: PHPStan extension inferring SQL return shapes#98
Draft
marcreichel wants to merge 5 commits into
Draft
Conversation
Adds a custom PHPStan extension that parses the literal SQL string passed to `Connection::getPArray()`, `getPRow()`, and `getGenerator()` and narrows the return type to a constant array shape derived from the SELECT list. Also adds a `PlaceholderCountRule` that flags mismatches between literal `?` placeholders and the literal `$params` array length. The extension is shipped inside this package and auto-registered for downstream consumers via `extra.phpstan.includes`, picked up by phpstan-extension-installer. Schema-agnostic: column shapes are inferred, all values typed as `mixed`. SELECT *, UNION, non-literal SQL, and non-SELECT statements gracefully fall back to the declared return type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-applied Pint formatting (blank lines before continue/return, @internal annotation on RuleTestCase, blank line between import groups). Removed two stale Connection.php baseline entries that PHPStan on CI doesn't report, which were failing the baseline-match check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously getPRow returned `array{k1?: mixed, k2?: mixed}` (each key
independently optional). After narrowing away the empty case with
`if ($row === []) return null;`, PHPStan left every key still optional,
making downstream calls that need all keys present (e.g. Token::fromRow)
fail typing.
Now returns `array{}|array{k1: mixed, k2: mixed, ...}` so the empty
check narrows cleanly to the all-required shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`SELECT DISTINCT(col) FROM t` is parsed as DISTINCT modifier + the expression `(col)`, so the key fell back to the verbatim text `(col)` instead of `col`. Strip matched outer parentheses (validated to be balanced) and unquote the inner identifier before using it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a regression test verifying that GROUP BY queries with aggregates (COUNT, MAX, MIN, SUM), with and without HAVING clauses, yield the expected key list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Adds a custom PHPStan extension that analyses the literal SQL string passed to the connection's query methods and narrows their return type to a constant array shape derived from the SELECT list. Also ships a custom rule that flags placeholder/parameter count mismatches.
The extension is shipped inside this package and auto-registered for downstream consumers via
extra.phpstan.includes(picked up byphpstan/extension-installer). Consumers ofartemeon/databaseget the improved types for free.What's covered
Connection::getPArray()→list<array{...}>Connection::getPRow()→array{}|array{...}Connection::getGenerator()→Generator<int, array{...}>PlaceholderCountRule: errors when literal?count ≠ literal$paramsarray length ongetPArray,getPRow,getGenerator,_pQueryDesign choices
mixed. No DB connection or schema dump required.phpmyadmin/sql-parser(added torequireso analysers downstream don't need to add it).SELECT *→array<string, mixed>. Non-literal SQL, INSERT/UPDATE/DELETE, UNION, CTEs, and unparseable SQL silently keep the declared return type.Tests
tests/PHPStan/SqlReturnShapeAnalyserTest.php— Pest tests on the analyser (8 cases).tests/PHPStan/PlaceholderCountRuleTest.php— PHPStanRuleTestCase.tests/PHPStan/data/return-types.php—assertType()fixture verifying the inferred return types end-to-end (run viatests/PHPStan/phpstan-fixtures.neon).Baseline
phpstan-baseline.neonwas regenerated. The newly-surfaced entries are real findings where existing test code accesses keys ongetPRow()results without first checking the empty-row case — left in the baseline for incremental cleanup.🤖 Generated with Claude Code