From 20d0e9f64a437b2415199e441ec8defd86f68a97 Mon Sep 17 00:00:00 2001 From: Piotr Reclik Date: Tue, 11 Mar 2025 21:30:15 +0100 Subject: [PATCH 1/2] feat: add limit functions and tests for mysql, postgresql and sqlite --- README.md | 6 ++-- config/mysql.yml | 1 + config/postgres.yml | 1 + config/sqlite.yml | 1 + src/Query/Mysql/Limit.php | 45 ++++++++++++++++++++++++++++ src/Query/Postgresql/Limit.php | 45 ++++++++++++++++++++++++++++ src/Query/Sqlite/Limit.php | 45 ++++++++++++++++++++++++++++ tests/Query/Mysql/LimitTest.php | 16 ++++++++++ tests/Query/Postgresql/LimitTest.php | 16 ++++++++++ tests/Query/Sqlite/LimitTest.php | 16 ++++++++++ 10 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 src/Query/Mysql/Limit.php create mode 100644 src/Query/Postgresql/Limit.php create mode 100644 src/Query/Sqlite/Limit.php create mode 100644 tests/Query/Mysql/LimitTest.php create mode 100644 tests/Query/Postgresql/LimitTest.php create mode 100644 tests/Query/Sqlite/LimitTest.php diff --git a/README.md b/README.md index e9e87bfca..48f7d1e94 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ MySQL, Oracle, PostgreSQL and SQLite. | DB | Functions | |:--:|:---------:| -| MySQL | `ACOS, ADDTIME, AES_DECRYPT, AES_ENCRYPT, ANY_VALUE, ASCII, ASIN, ATAN, ATAN2, BINARY, BIT_COUNT, BIT_XOR, CAST, CEIL, CHAR_LENGTH, COLLATE, CONCAT_WS, CONVERT_TZ, COS, COT, COUNTIF, CRC32, DATE, DATE_FORMAT, DATEADD, DATEDIFF, DATESUB, DAY, DAYNAME, DAYOFWEEK, DAYOFYEAR, DEGREES, DIV, EXP, EXTRACT, FIELD, FIND_IN_SET, FLOOR, FORMAT, FROM_BASE64, FROM_UNIXTIME, GREATEST, GROUP_CONCAT, HEX, HOUR, IFELSE, IFNULL, INET_ATON, INET_NTOA, INET6_ATON, INET6_NTOA, INSTR, IS_IPV4, IS_IPV4_COMPAT, IS_IPV4_MAPPED, IS_IPV6, JSON_CONTAINS, JSON_DEPTH, JSON_LENGTH, LAG, LAST_DAY, LEAD, LEAST, LOG, LOG10, LOG2, LPAD, MAKEDATE, MATCH, MD5, MINUTE, MONTH, MONTHNAME, NOW, NULLIF, OVER, PERIOD_DIFF, PI, POWER, QUARTER, RADIANS, RAND, REGEXP, REPLACE, ROUND, RPAD, SECOND, SECTOTIME, SHA1, SHA2, SIN, SOUNDEX, STD, STDDEV, STRTODATE, STR_TO_DATE, SUBSTRING_INDEX, TAN, TIME, TIMEDIFF, TIMESTAMPADD, TIMESTAMPDIFF, TIMETOSEC, TRUNCATE, UNHEX, UNIX_TIMESTAMP, UTC_TIMESTAMP, UUID_SHORT, VARIANCE, WEEK, WEEKDAY, WEEKOFYEAR, YEAR, YEARMONTH, YEARWEEK` | +| MySQL | `ACOS, ADDTIME, AES_DECRYPT, AES_ENCRYPT, ANY_VALUE, ASCII, ASIN, ATAN, ATAN2, BINARY, BIT_COUNT, BIT_XOR, CAST, CEIL, CHAR_LENGTH, COLLATE, CONCAT_WS, CONVERT_TZ, COS, COT, COUNTIF, CRC32, DATE, DATE_FORMAT, DATEADD, DATEDIFF, DATESUB, DAY, DAYNAME, DAYOFWEEK, DAYOFYEAR, DEGREES, DIV, EXP, EXTRACT, FIELD, FIND_IN_SET, FLOOR, FORMAT, FROM_BASE64, FROM_UNIXTIME, GREATEST, GROUP_CONCAT, HEX, HOUR, IFELSE, IFNULL, INET_ATON, INET_NTOA, INET6_ATON, INET6_NTOA, INSTR, IS_IPV4, IS_IPV4_COMPAT, IS_IPV4_MAPPED, IS_IPV6, JSON_CONTAINS, JSON_DEPTH, JSON_LENGTH, LAG, LAST_DAY, LEAD, LEAST, LIMIT, LOG, LOG10, LOG2, LPAD, MAKEDATE, MATCH, MD5, MINUTE, MONTH, MONTHNAME, NOW, NULLIF, OVER, PERIOD_DIFF, PI, POWER, QUARTER, RADIANS, RAND, REGEXP, REPLACE, ROUND, RPAD, SECOND, SECTOTIME, SHA1, SHA2, SIN, SOUNDEX, STD, STDDEV, STRTODATE, STR_TO_DATE, SUBSTRING_INDEX, TAN, TIME, TIMEDIFF, TIMESTAMPADD, TIMESTAMPDIFF, TIMETOSEC, TRUNCATE, UNHEX, UNIX_TIMESTAMP, UTC_TIMESTAMP, UUID_SHORT, VARIANCE, WEEK, WEEKDAY, WEEKOFYEAR, YEAR, YEARMONTH, YEARWEEK` | | Oracle | `CEIL, DAY, FLOOR, HOUR, LISTAGG, MINUTE, MONTH, NVL, SECOND, TO_CHAR, TO_DATE, TRUNC, YEAR` | -| SQLite | `CASE WHEN THEN ELSE END, DATE, DATE_FORMAT*, DAY, HOUR, IFNULL, JULIANDAY, MINUTE, MONTH, REPLACE, ROUND, SECOND, STRFTIME, WEEK, WEEKDAY, YEAR` | -| PostgreSQL | `AT_TIME_ZONE, COUNT_FILTER, DATE, DATE_PART, DATE_TRUNC, DAY, EXTRACT, GREATEST, HOUR, LEAST, MINUTE, MONTH, REGEXP_REPLACE, SECOND, STRING_AGG, TO_CHAR, TO_DATE, YEAR` | +| SQLite | `CASE WHEN THEN ELSE END, DATE, DATE_FORMAT*, DAY, HOUR, IFNULL, JULIANDAY, LIMIT, MINUTE, MONTH, REPLACE, ROUND, SECOND, STRFTIME, WEEK, WEEKDAY, YEAR` | +| PostgreSQL | `AT_TIME_ZONE, COUNT_FILTER, DATE, DATE_PART, DATE_TRUNC, DAY, EXTRACT, GREATEST, HOUR, LEAST, LIMIT, MINUTE, MONTH, REGEXP_REPLACE, SECOND, STRING_AGG, TO_CHAR, TO_DATE, YEAR` | > Note: SQLite date functions are implemented as `strftime(format, value)`. SQLite only supports the [most common formats](https://www.sqlite.org/lang_datefunc.html), diff --git a/config/mysql.yml b/config/mysql.yml index a05c74e1e..f66348db1 100644 --- a/config/mysql.yml +++ b/config/mysql.yml @@ -107,6 +107,7 @@ doctrine: lag: DoctrineExtensions\Query\Mysql\Lag lead: DoctrineExtensions\Query\Mysql\Lead least: DoctrineExtensions\Query\Mysql\Least + limit: DoctrineExtensions\Query\Mysql\Limit lpad: DoctrineExtensions\Query\Mysql\Lpad match: DoctrineExtensions\Query\Mysql\MatchAgainst md5: DoctrineExtensions\Query\Mysql\Md5 diff --git a/config/postgres.yml b/config/postgres.yml index 0a36cc64c..50e1f718c 100644 --- a/config/postgres.yml +++ b/config/postgres.yml @@ -21,4 +21,5 @@ doctrine: string_agg: DoctrineExtensions\Query\Postgresql\StringAgg greatest: DoctrineExtensions\Query\Postgresql\Greatest least: DoctrineExtensions\Query\Postgresql\Least + limit: DoctrineExtensions\Query\Postgresql\Limit regexp_replace: DoctrineExtensions\Query\Postgresql\RegexpReplace diff --git a/config/sqlite.yml b/config/sqlite.yml index d0073502c..4ea0c67be 100644 --- a/config/sqlite.yml +++ b/config/sqlite.yml @@ -37,6 +37,7 @@ doctrine: ifelse: DoctrineExtensions\Query\Sqlite\IfElse ifnull: DoctrineExtensions\Query\Sqlite\IfNull least: DoctrineExtensions\Query\Sqlite\Least + limit: DoctrineExtensions\Query\Sqlite\Limit # match: DoctrineExtensions\Query\Sqlite\MatchAgainst # md5: DoctrineExtensions\Query\Sqlite\Md5 # nullif: DoctrineExtensions\Query\Sqlite\NullIf diff --git a/src/Query/Mysql/Limit.php b/src/Query/Mysql/Limit.php new file mode 100644 index 000000000..7ec3fc519 --- /dev/null +++ b/src/Query/Mysql/Limit.php @@ -0,0 +1,45 @@ + + * @example SELECT LIMIT((sub-query), limitValue) + */ +class Limit extends FunctionNode +{ + /** @var Subselect */ + private $subSelect; + + /** @var Literal */ + private $limit; + + public function getSql(SqlWalker $sqlWalker): string + { + return "({$this->subSelect->dispatch($sqlWalker)} LIMIT {$this->limit->dispatch($sqlWalker)})"; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->subSelect = $parser->Subselect(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + + $parser->match(TokenType::T_COMMA); + + $this->limit = $parser->ArithmeticPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} diff --git a/src/Query/Postgresql/Limit.php b/src/Query/Postgresql/Limit.php new file mode 100644 index 000000000..9ae8eee0d --- /dev/null +++ b/src/Query/Postgresql/Limit.php @@ -0,0 +1,45 @@ + + * @example SELECT LIMIT((sub-query), limitValue) + */ +class Limit extends FunctionNode +{ + /** @var Subselect */ + private $subSelect; + + /** @var Literal */ + private $limit; + + public function getSql(SqlWalker $sqlWalker): string + { + return "({$this->subSelect->dispatch($sqlWalker)} LIMIT {$this->limit->dispatch($sqlWalker)})"; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->subSelect = $parser->Subselect(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + + $parser->match(TokenType::T_COMMA); + + $this->limit = $parser->ArithmeticPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} diff --git a/src/Query/Sqlite/Limit.php b/src/Query/Sqlite/Limit.php new file mode 100644 index 000000000..c4aee1391 --- /dev/null +++ b/src/Query/Sqlite/Limit.php @@ -0,0 +1,45 @@ + + * @example SELECT LIMIT((sub-query), limitValue) + */ +class Limit extends FunctionNode +{ + /** @var Subselect */ + private $subSelect; + + /** @var Literal */ + private $limit; + + public function getSql(SqlWalker $sqlWalker): string + { + return "({$this->subSelect->dispatch($sqlWalker)} LIMIT {$this->limit->dispatch($sqlWalker)})"; + } + + public function parse(Parser $parser): void + { + $parser->match(TokenType::T_IDENTIFIER); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + $parser->match(TokenType::T_OPEN_PARENTHESIS); + + $this->subSelect = $parser->Subselect(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + + $parser->match(TokenType::T_COMMA); + + $this->limit = $parser->ArithmeticPrimary(); + + $parser->match(TokenType::T_CLOSE_PARENTHESIS); + } +} diff --git a/tests/Query/Mysql/LimitTest.php b/tests/Query/Mysql/LimitTest.php new file mode 100644 index 000000000..a80f71a35 --- /dev/null +++ b/tests/Query/Mysql/LimitTest.php @@ -0,0 +1,16 @@ +assertDqlProducesSql( + 'SELECT b0 FROM DoctrineExtensions\Tests\Entities\Blank b0 WHERE b0.id = LIMIT((SELECT b1 FROM DoctrineExtensions\Tests\Entities\Blank b1 ORDER BY b1.id DESC), 1)', + 'SELECT b0_.id AS id_0 FROM Blank b0_ WHERE b0_.id = (SELECT b1_.id FROM Blank b1_ ORDER BY b1_.id DESC LIMIT 1)' + ); + } +} diff --git a/tests/Query/Postgresql/LimitTest.php b/tests/Query/Postgresql/LimitTest.php new file mode 100644 index 000000000..64ca7e8b0 --- /dev/null +++ b/tests/Query/Postgresql/LimitTest.php @@ -0,0 +1,16 @@ +assertDqlProducesSql( + 'SELECT b0 FROM DoctrineExtensions\Tests\Entities\Blank b0 WHERE b0.id = LIMIT((SELECT b1 FROM DoctrineExtensions\Tests\Entities\Blank b1 ORDER BY b1.id DESC), 1)', + 'SELECT b0_.id AS id_0 FROM Blank b0_ WHERE b0_.id = (SELECT b1_.id FROM Blank b1_ ORDER BY b1_.id DESC LIMIT 1)' + ); + } +} diff --git a/tests/Query/Sqlite/LimitTest.php b/tests/Query/Sqlite/LimitTest.php new file mode 100644 index 000000000..b9c7ae22c --- /dev/null +++ b/tests/Query/Sqlite/LimitTest.php @@ -0,0 +1,16 @@ +assertDqlProducesSql( + 'SELECT b0 FROM DoctrineExtensions\Tests\Entities\Blank b0 WHERE b0.id = LIMIT((SELECT b1 FROM DoctrineExtensions\Tests\Entities\Blank b1 ORDER BY b1.id DESC), 1)', + 'SELECT b0_.id AS id_0 FROM Blank b0_ WHERE b0_.id = (SELECT b1_.id FROM Blank b1_ ORDER BY b1_.id DESC LIMIT 1)' + ); + } +} \ No newline at end of file From 69f0ce7f26ece298e159889bb57b30cfee43e2a1 Mon Sep 17 00:00:00 2001 From: Piotr Reclik Date: Tue, 11 Mar 2025 21:47:26 +0100 Subject: [PATCH 2/2] feat: add missing newline --- tests/Query/Sqlite/LimitTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Query/Sqlite/LimitTest.php b/tests/Query/Sqlite/LimitTest.php index b9c7ae22c..c83dea83f 100644 --- a/tests/Query/Sqlite/LimitTest.php +++ b/tests/Query/Sqlite/LimitTest.php @@ -13,4 +13,4 @@ public function testLimit(): void 'SELECT b0_.id AS id_0 FROM Blank b0_ WHERE b0_.id = (SELECT b1_.id FROM Blank b1_ ORDER BY b1_.id DESC LIMIT 1)' ); } -} \ No newline at end of file +}