diff --git a/.phpstan-dba.cache b/.phpstan-dba.cache index 0ee1a84e3..95ec9ffff 100644 --- a/.phpstan-dba.cache +++ b/.phpstan-dba.cache @@ -909,6 +909,88 @@ )), ), ), + 'SELECT adaid FROM ada LIMIT 1 FOR SHARE' => + array ( + 'error' => NULL, + 'result' => + array ( + 1 => + PHPStan\Type\Constant\ConstantArrayType::__set_state(array( + 'keyType' => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + 'itemType' => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + 'allArrays' => NULL, + 'keyTypes' => + array ( + 0 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + ), + 'valueTypes' => + array ( + 0 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + ), + 'nextAutoIndex' => 0, + 'optionalKeys' => + array ( + ), + )), + ), + ), + 'SELECT adaid FROM ada LIMIT 1 FOR UPDATE' => + array ( + 'error' => NULL, + 'result' => + array ( + 1 => + PHPStan\Type\Constant\ConstantArrayType::__set_state(array( + 'keyType' => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + 'itemType' => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + 'allArrays' => NULL, + 'keyTypes' => + array ( + 0 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + ), + 'valueTypes' => + array ( + 0 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + ), + 'nextAutoIndex' => 0, + 'optionalKeys' => + array ( + ), + )), + ), + ), 'SELECT adaid FROM ada LIMIT 1 OFFSET 1' => array ( 'error' => NULL, @@ -950,6 +1032,88 @@ )), ), ), + 'SELECT adaid FROM ada LIMIT 1 OFFSET 1 ' => + array ( + 'error' => NULL, + 'result' => + array ( + 1 => + PHPStan\Type\Constant\ConstantArrayType::__set_state(array( + 'keyType' => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + 'itemType' => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + 'allArrays' => NULL, + 'keyTypes' => + array ( + 0 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + ), + 'valueTypes' => + array ( + 0 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + ), + 'nextAutoIndex' => 0, + 'optionalKeys' => + array ( + ), + )), + ), + ), + 'SELECT adaid FROM ada LIMIT 1 OFFSET 1 FOR UPDATE' => + array ( + 'error' => NULL, + 'result' => + array ( + 1 => + PHPStan\Type\Constant\ConstantArrayType::__set_state(array( + 'keyType' => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + 'itemType' => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + 'allArrays' => NULL, + 'keyTypes' => + array ( + 0 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + ), + 'valueTypes' => + array ( + 0 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + ), + 'nextAutoIndex' => 0, + 'optionalKeys' => + array ( + ), + )), + ), + ), 'SELECT adaid FROM ada WHERE adaid = 1' => array ( 'error' => NULL, diff --git a/.phpunit-phpstan-dba.cache b/.phpunit-phpstan-dba.cache index 6018e8ce3..20205713b 100644 --- a/.phpunit-phpstan-dba.cache +++ b/.phpunit-phpstan-dba.cache @@ -15,11 +15,30 @@ 'code' => 1064, )), ), + ' + SELECT email, adaid + FROM ada + WHERE gesperrt = \'1\' + FOR UPDATE + ' => + array ( + 'error' => NULL, + ), + ' + SELECT email, adaid + FROM ada + WHERE gesperrt = \'1\' + LIMIT \'1\' + ' => + array ( + 'error' => NULL, + ), ' SELECT email, adaid FROM ada WHERE gesperrt = \'1\' LIMIT \'1\' + FOR UPDATE ' => array ( 'error' => NULL, @@ -34,6 +53,28 @@ array ( 'error' => NULL, ), + ' + SELECT email, adaid + FROM ada + WHERE gesperrt = \'1\' + LIMIT \'1\' + OFFSET \'1\' + FOR SHARE + ' => + array ( + 'error' => NULL, + ), + ' + SELECT email, adaid + FROM ada + WHERE gesperrt = \'1\' + LIMIT \'1\' + OFFSET \'1\' + FOR UPDATE + ' => + array ( + 'error' => NULL, + ), ' SELECT email, adaid FROM ada @@ -102,6 +143,26 @@ array ( 'error' => NULL, ), + ' + SELECT email, adaid + FROM ada + WHERE gesperrt = \'1\' + LIMIT \'1\' + OFFSET \'1\' + ' => + array ( + 'error' => NULL, + ), + ' + SELECT email, adaid + FROM ada + WHERE gesperrt = \'1\' + LIMIT \'1\' + OFFSET 1 + ' => + array ( + 'error' => NULL, + ), ' SELECT email, adaid FROM ada @@ -1029,6 +1090,47 @@ )), ), ), + 'SELECT adaid FROM ada LIMIT 1 FOR SHARE' => + array ( + 'error' => NULL, + 'result' => + array ( + 1 => + PHPStan\Type\Constant\ConstantArrayType::__set_state(array( + 'keyType' => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + 'itemType' => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + 'allArrays' => NULL, + 'keyTypes' => + array ( + 0 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + ), + 'valueTypes' => + array ( + 0 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + ), + 'nextAutoIndex' => 0, + 'optionalKeys' => + array ( + ), + )), + ), + ), 'SELECT adaid FROM ada LIMIT 1 OFFSET 1' => array ( 'error' => NULL, @@ -1070,6 +1172,47 @@ )), ), ), + 'SELECT adaid FROM ada LIMIT 1 OFFSET 1 FOR UPDATE' => + array ( + 'error' => NULL, + 'result' => + array ( + 1 => + PHPStan\Type\Constant\ConstantArrayType::__set_state(array( + 'keyType' => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + 'itemType' => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + 'allArrays' => NULL, + 'keyTypes' => + array ( + 0 => + PHPStan\Type\Constant\ConstantStringType::__set_state(array( + 'value' => 'adaid', + 'isClassString' => false, + )), + ), + 'valueTypes' => + array ( + 0 => + PHPStan\Type\IntegerRangeType::__set_state(array( + 'min' => 0, + 'max' => 4294967295, + )), + ), + 'nextAutoIndex' => 0, + 'optionalKeys' => + array ( + ), + )), + ), + ), 'SELECT adaid FROM ada WHERE adaid IN (\'1\')' => array ( 'error' => NULL, diff --git a/src/QueryReflection/QuerySimulation.php b/src/QueryReflection/QuerySimulation.php index 639f58d69..cc9a27b57 100644 --- a/src/QueryReflection/QuerySimulation.php +++ b/src/QueryReflection/QuerySimulation.php @@ -112,17 +112,23 @@ public static function simulate(string $queryString): ?string private static function stripTraillingLimit(string $queryString): ?string { + // XXX someday we will use a proper SQL parser, $queryString = rtrim($queryString, ';'); + // strip trailling FOR UPDATE/FOR SHARE + $queryString = preg_replace('/(.*)FOR (UPDATE|SHARE)\s*$/i', '$1', $queryString); + + if (null === $queryString) { + throw new ShouldNotHappenException('Could not strip trailling FOR UPDATE/SHARE from query'); + } + // strip trailling OFFSET $queryString = preg_replace('/(.*)OFFSET\s+["\']?\d+["\']?\s*$/i', '$1', $queryString); if (null === $queryString) { - throw new ShouldNotHappenException('Could not strip trailing offset from query'); + throw new ShouldNotHappenException('Could not strip trailing OFFSET from query'); } - // XXX someday we will use a proper SQL parser, - // which would also allow us to support even more complex expressions like SELECT .. LIMIT X, Y FOR UPDATE return preg_replace('/\s*LIMIT\s+["\']?\d+["\']?\s*(,\s*["\']?\d*["\']?)?\s*$/i', '', $queryString); } } diff --git a/tests/data/pdo.php b/tests/data/pdo.php index d3535f7f5..f8998d85e 100644 --- a/tests/data/pdo.php +++ b/tests/data/pdo.php @@ -170,4 +170,15 @@ public function offsetAfterLimit(PDO $pdo, int $limit, int $offset) $stmt = $pdo->query($query, PDO::FETCH_ASSOC); assertType('PDOStatement}>', $stmt); } + + public function readlocks(PDO $pdo, int $limit, int $offset) + { + $query = 'SELECT adaid FROM ada LIMIT '.$limit.' OFFSET '.$offset.' FOR UPDATE'; + $stmt = $pdo->query($query, PDO::FETCH_ASSOC); + assertType('PDOStatement}>', $stmt); + + $query = 'SELECT adaid FROM ada LIMIT '.$limit.' FOR SHARE'; + $stmt = $pdo->query($query, PDO::FETCH_ASSOC); + assertType('PDOStatement}>', $stmt); + } } diff --git a/tests/data/syntax-error-in-prepared-statement.php b/tests/data/syntax-error-in-prepared-statement.php index b66720baf..956d037aa 100644 --- a/tests/data/syntax-error-in-prepared-statement.php +++ b/tests/data/syntax-error-in-prepared-statement.php @@ -246,4 +246,40 @@ public function noErrorOnOffsetAfterLimit(Connection $connection, int $limit, in OFFSET :offset ', [':gesperrt' => 1, ':limit' => $limit, ':offset' => $offset]); } + + public function noErrorOnLockedRead(Connection $connection, int $limit, int $offset) + { + $connection->preparedQuery(' + SELECT email, adaid + FROM ada + WHERE gesperrt = ? + FOR UPDATE + ', [1]); + + $connection->preparedQuery(' + SELECT email, adaid + FROM ada + WHERE gesperrt = ? + LIMIT ? + FOR UPDATE + ', [1, $limit]); + + $connection->preparedQuery(' + SELECT email, adaid + FROM ada + WHERE gesperrt = ? + LIMIT ? + OFFSET ? + FOR UPDATE + ', [1, $limit, $offset]); + + $connection->preparedQuery(' + SELECT email, adaid + FROM ada + WHERE gesperrt = :gesperrt + LIMIT :limit + OFFSET :offset + FOR SHARE + ', [':gesperrt' => 1, ':limit' => $limit, ':offset' => $offset]); + } }