From e2d86c0fd5900f2458b4ebe64cb0e1ad547ebcf8 Mon Sep 17 00:00:00 2001 From: Dan Hemberger Date: Thu, 8 Oct 2026 00:05:17 -0700 Subject: [PATCH 1/3] Report ambiguous using column MySQL error This error occurs when the query reflector simulates a query that refers to columns by ambiguous names, usually due to a JOIN where both tables have the same column name that isn't part of the join condition. MySQL reports this as error code 1052, and PDO-MySQL reports it as error code 23000. Previously, no error would be emitted, which silently suppressed this class of MySQL errors. --- src/QueryReflection/BasePdoQueryReflector.php | 3 ++ src/QueryReflection/MysqliQueryReflector.php | 3 ++ .../config/.phpunit-phpstan-dba-mysqli.cache | 15 ++++++++++ ...ErrorInPreparedStatementMethodRuleTest.php | 15 ++++++++++ .../config/.phpunit-phpstan-dba-mysqli.cache | 11 ++++++++ .../.phpunit-phpstan-dba-pdo-mysql.cache | 11 ++++++++ tests/rules/data/ambiguous-using-column.php | 19 +++++++++++++ .../config/.phpunit-phpstan-dba-mysqli.cache | 28 +++++++++---------- .../.phpunit-phpstan-dba-pdo-mysql.cache | 28 +++++++++---------- 9 files changed, 105 insertions(+), 28 deletions(-) create mode 100644 tests/rules/data/ambiguous-using-column.php diff --git a/src/QueryReflection/BasePdoQueryReflector.php b/src/QueryReflection/BasePdoQueryReflector.php index 69ed1875b..c46492a1c 100644 --- a/src/QueryReflection/BasePdoQueryReflector.php +++ b/src/QueryReflection/BasePdoQueryReflector.php @@ -34,6 +34,8 @@ abstract class BasePdoQueryReflector implements QueryReflector, RecordingReflect private const MYSQL_UNKNOWN_TABLE = '42S02'; + private const MYSQL_INTEGRITY_CONSTRAINT_VIOLATION = '23000'; + private const PDO_SYNTAX_ERROR_CODES = [ self::MYSQL_SYNTAX_ERROR_CODE, self::PSQL_SYNTAX_ERROR, @@ -48,6 +50,7 @@ abstract class BasePdoQueryReflector implements QueryReflector, RecordingReflect self::MYSQL_SYNTAX_ERROR_CODE, self::MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST, self::MYSQL_UNKNOWN_TABLE, + self::MYSQL_INTEGRITY_CONSTRAINT_VIOLATION, ]; protected const MAX_CACHE_SIZE = 50; diff --git a/src/QueryReflection/MysqliQueryReflector.php b/src/QueryReflection/MysqliQueryReflector.php index 663db214d..e4c6039e9 100644 --- a/src/QueryReflection/MysqliQueryReflector.php +++ b/src/QueryReflection/MysqliQueryReflector.php @@ -21,6 +21,8 @@ final class MysqliQueryReflector implements QueryReflector, RecordingReflector private const MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST = 1054; + private const MYSQL_AMBIGUOUS_COLUMN_IN_FROM_CLAUSE = 1052; + public const MYSQL_UNKNOWN_TABLE = 1146; private const MYSQL_INCORRECT_TABLE = 1103; @@ -33,6 +35,7 @@ final class MysqliQueryReflector implements QueryReflector, RecordingReflector private const MYSQL_ERROR_CODES = [ self::MYSQL_SYNTAX_ERROR_CODE, self::MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST, + self::MYSQL_AMBIGUOUS_COLUMN_IN_FROM_CLAUSE, self::MYSQL_UNKNOWN_TABLE, self::MYSQL_INCORRECT_TABLE, ]; diff --git a/tests/default/config/.phpunit-phpstan-dba-mysqli.cache b/tests/default/config/.phpunit-phpstan-dba-mysqli.cache index 6dc7269aa..34e1f7e46 100644 --- a/tests/default/config/.phpunit-phpstan-dba-mysqli.cache +++ b/tests/default/config/.phpunit-phpstan-dba-mysqli.cache @@ -73,6 +73,9 @@ FROM ada' => ), 'SELECT adaid FROM ada LIMIT 1 FOR SHARE' => array ( + 'error' => + \staabm\PHPStanDba\Valid::__set_state(array( + )), 'result' => array ( 5 => @@ -123,6 +126,9 @@ FROM ada' => ), 'SELECT adaid FROM ada LIMIT 1 OFFSET 1' => array ( + 'error' => + \staabm\PHPStanDba\Valid::__set_state(array( + )), 'result' => array ( 5 => @@ -133,6 +139,9 @@ FROM ada' => ), 'SELECT adaid FROM ada LIMIT 1 OFFSET 1 FOR UPDATE' => array ( + 'error' => + \staabm\PHPStanDba\Valid::__set_state(array( + )), 'result' => array ( 5 => @@ -223,6 +232,9 @@ FROM ada' => ), 'SELECT akid FROM ak WHERE eadavk>1.0' => array ( + 'error' => + \staabm\PHPStanDba\Valid::__set_state(array( + )), 'result' => array ( 5 => @@ -523,6 +535,9 @@ FROM ada' => ), 'SELECT email, adaid FROM ada WHERE gesperrt = \'1\'' => array ( + 'error' => + \staabm\PHPStanDba\Valid::__set_state(array( + )), 'result' => array ( 5 => diff --git a/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php b/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php index 8826cfb6c..ee91489e4 100644 --- a/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php +++ b/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php @@ -268,6 +268,21 @@ public function testSyntaxErrorInQueryRule(): void $this->analyse([__DIR__ . '/data/syntax-error-in-prepared-statement.php'], $expectedErrors); } + public function test_prepared_statement_method_reports_ambiguous_using_column(): void + { + if (MysqliQueryReflector::NAME === getenv('DBA_REFLECTOR')) { + $expectedError = "Query error: Column 'adaid' in from clause is ambiguous (1052)."; + } elseif (PdoMysqlQueryReflector::NAME === getenv('DBA_REFLECTOR')) { + $expectedError = "Query error: SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'adaid' in from clause is ambiguous (23000)."; + } else { + self::markTestSkipped('This regression only applies to MySQL reflectors.'); + } + + $this->analyse( + [__DIR__ . '/data/ambiguous-using-column.php'], [[$expectedError, 11]], + ); + } + public function testBug94() { if (MysqliQueryReflector::NAME === getenv('DBA_REFLECTOR')) { diff --git a/tests/rules/config/.phpunit-phpstan-dba-mysqli.cache b/tests/rules/config/.phpunit-phpstan-dba-mysqli.cache index 914c53199..8059ff4dc 100644 --- a/tests/rules/config/.phpunit-phpstan-dba-mysqli.cache +++ b/tests/rules/config/.phpunit-phpstan-dba-mysqli.cache @@ -776,6 +776,17 @@ Simulated query: SELECT FROM WHERE LIMIT 0', ), ), ), + 'SELECT first.adaid + FROM ada first + JOIN ada second ON second.adaid = first.adaid + JOIN ada third USING(adaid)' => + array ( + 'error' => + \staabm\PHPStanDba\Error::__set_state(array( + 'message' => 'Column \'adaid\' in from clause is ambiguous', + 'code' => 1052, + )), + ), 'SELECT id FROM table WHERE a=\'1970-01-01\' AND err-or id IN (\'1\')' => array ( 'error' => diff --git a/tests/rules/config/.phpunit-phpstan-dba-pdo-mysql.cache b/tests/rules/config/.phpunit-phpstan-dba-pdo-mysql.cache index 42e89555a..69d0c3a3f 100644 --- a/tests/rules/config/.phpunit-phpstan-dba-pdo-mysql.cache +++ b/tests/rules/config/.phpunit-phpstan-dba-pdo-mysql.cache @@ -784,6 +784,17 @@ Simulated query: SELECT FROM WHERE LIMIT 0', ), ), ), + 'SELECT first.adaid + FROM ada first + JOIN ada second ON second.adaid = first.adaid + JOIN ada third USING(adaid)' => + array ( + 'error' => + \staabm\PHPStanDba\Error::__set_state(array( + 'message' => 'SQLSTATE[23000]: Integrity constraint violation: 1052 Column \'adaid\' in from clause is ambiguous', + 'code' => '23000', + )), + ), 'SELECT with syntax error GROUPY by x' => array ( 'error' => diff --git a/tests/rules/data/ambiguous-using-column.php b/tests/rules/data/ambiguous-using-column.php new file mode 100644 index 000000000..7b8ddd5f9 --- /dev/null +++ b/tests/rules/data/ambiguous-using-column.php @@ -0,0 +1,19 @@ +preparedQuery( + 'SELECT first.adaid + FROM ada first + JOIN ada second ON second.adaid = first.adaid + JOIN ada third USING(adaid)', + [], + ); + } +} diff --git a/tests/sqlAst/config/.phpunit-phpstan-dba-mysqli.cache b/tests/sqlAst/config/.phpunit-phpstan-dba-mysqli.cache index f0b25c33b..33b7e9629 100644 --- a/tests/sqlAst/config/.phpunit-phpstan-dba-mysqli.cache +++ b/tests/sqlAst/config/.phpunit-phpstan-dba-mysqli.cache @@ -164,7 +164,7 @@ array ( 5 => array ( - 'type-description' => 'array{str: string, 0: string}', + 'type-description' => 'array{str: string|null, 0: string|null}', ), ), ), @@ -204,7 +204,7 @@ array ( 5 => array ( - 'type-description' => 'array{str: string, 0: string}', + 'type-description' => 'array{str: string|null, 0: string|null}', ), ), ), @@ -744,7 +744,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -754,7 +754,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -794,7 +794,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -804,7 +804,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -1164,7 +1164,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1214,7 +1214,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1224,7 +1224,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1244,7 +1244,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1254,7 +1254,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -1274,7 +1274,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1614,7 +1614,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1624,7 +1624,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), diff --git a/tests/sqlAst/config/.phpunit-phpstan-dba-pdo-mysql.cache b/tests/sqlAst/config/.phpunit-phpstan-dba-pdo-mysql.cache index f0b25c33b..33b7e9629 100644 --- a/tests/sqlAst/config/.phpunit-phpstan-dba-pdo-mysql.cache +++ b/tests/sqlAst/config/.phpunit-phpstan-dba-pdo-mysql.cache @@ -164,7 +164,7 @@ array ( 5 => array ( - 'type-description' => 'array{str: string, 0: string}', + 'type-description' => 'array{str: string|null, 0: string|null}', ), ), ), @@ -204,7 +204,7 @@ array ( 5 => array ( - 'type-description' => 'array{str: string, 0: string}', + 'type-description' => 'array{str: string|null, 0: string|null}', ), ), ), @@ -744,7 +744,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -754,7 +754,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -794,7 +794,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -804,7 +804,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -1164,7 +1164,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1214,7 +1214,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1224,7 +1224,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1244,7 +1244,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1254,7 +1254,7 @@ array ( 5 => array ( - 'type-description' => 'array{col: string, 0: string}', + 'type-description' => 'array{col: string|null, 0: string|null}', ), ), ), @@ -1274,7 +1274,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1614,7 +1614,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), @@ -1624,7 +1624,7 @@ array ( 5 => array ( - 'type-description' => 'array{field: string, 0: string}', + 'type-description' => 'array{field: string|null, 0: string|null}', ), ), ), From 4aa9ea859d9cf02f1a24280877c841f0f65d2724 Mon Sep 17 00:00:00 2001 From: Dan Hemberger Date: Thu, 8 Oct 2026 01:27:36 -0700 Subject: [PATCH 2/3] Mariadb has a slightly different error message --- .../default/config/.phpunit-phpstan-dba-mysqli.cache | 11 +++++++++++ .../SyntaxErrorInPreparedStatementMethodRuleTest.php | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/default/config/.phpunit-phpstan-dba-mysqli.cache b/tests/default/config/.phpunit-phpstan-dba-mysqli.cache index 34e1f7e46..2912da98a 100644 --- a/tests/default/config/.phpunit-phpstan-dba-mysqli.cache +++ b/tests/default/config/.phpunit-phpstan-dba-mysqli.cache @@ -576,6 +576,17 @@ FROM ada' => ), ), ), + 'SELECT first.adaid + FROM ada first + JOIN ada second ON second.adaid = first.adaid + JOIN ada third USING(adaid)' => + array ( + 'error' => + \staabm\PHPStanDba\Error::__set_state(array( + 'message' => 'Column \'adaid\' in from clause is ambiguous', + 'code' => 1052, + )), + ), 'SELECT max(adaid) FROM ada' => array ( 'result' => diff --git a/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php b/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php index ee91489e4..8b81f2189 100644 --- a/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php +++ b/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php @@ -271,7 +271,11 @@ public function testSyntaxErrorInQueryRule(): void public function test_prepared_statement_method_reports_ambiguous_using_column(): void { if (MysqliQueryReflector::NAME === getenv('DBA_REFLECTOR')) { - $expectedError = "Query error: Column 'adaid' in from clause is ambiguous (1052)."; + if ('mariadb' === $_ENV['DBA_PLATFORM']) { + $expectedError = "Query error: Column 'adaid' in FROM is ambiguous (1052)."; + } else { + $expectedError = "Query error: Column 'adaid' in from clause is ambiguous (1052)."; + } } elseif (PdoMysqlQueryReflector::NAME === getenv('DBA_REFLECTOR')) { $expectedError = "Query error: SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'adaid' in from clause is ambiguous (23000)."; } else { From 52fc573e516e69e43462455e12a357b124d065d6 Mon Sep 17 00:00:00 2001 From: Dan Hemberger <846186+hemberger@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:13:54 -0700 Subject: [PATCH 3/3] Fix php-cs-fixer style check --- tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php b/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php index 8b81f2189..962fe5cfe 100644 --- a/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php +++ b/tests/rules/SyntaxErrorInPreparedStatementMethodRuleTest.php @@ -283,7 +283,8 @@ public function test_prepared_statement_method_reports_ambiguous_using_column(): } $this->analyse( - [__DIR__ . '/data/ambiguous-using-column.php'], [[$expectedError, 11]], + [__DIR__ . '/data/ambiguous-using-column.php'], + [[$expectedError, 11]], ); }