From d3ead8742ee7c9a870815f47b779dc847ca822f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 26 Jul 2023 23:23:25 +0200 Subject: [PATCH] Implement a special field2 function, to migitiate the argument count limit in sqlite This fixes issue #332 on SQLite DBs --- config/packages/doctrine.yaml | 2 +- src/Doctrine/Functions/CustomField.php | 114 +++++++++++++++++++++++++ src/Doctrine/SQLiteRegexExtension.php | 18 +++- 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/Doctrine/Functions/CustomField.php diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index c8c39632..37d74b14 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -38,7 +38,7 @@ doctrine: string_functions: regexp: DoctrineExtensions\Query\Mysql\Regexp ifnull: DoctrineExtensions\Query\Mysql\IfNull - field: DoctrineExtensions\Query\Mysql\Field + field: App\Doctrine\Functions\CustomField when@test: doctrine: diff --git a/src/Doctrine/Functions/CustomField.php b/src/Doctrine/Functions/CustomField.php new file mode 100644 index 00000000..06092234 --- /dev/null +++ b/src/Doctrine/Functions/CustomField.php @@ -0,0 +1,114 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Doctrine\Functions; + +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; +use Doctrine\ORM\Query\AST\Functions\FunctionNode; +use Doctrine\ORM\Query\AST\Node; +use Doctrine\ORM\Query\Lexer; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\SqlWalker; +use DoctrineExtensions\Query\Mysql\Field; + +class CustomField extends Field +{ + + protected Node|null|string $field = null; + + protected array $values = []; + + + public function parse(\Doctrine\ORM\Query\Parser $parser): void + { + //If we are on MySQL, we can just call the parent method, as these values are not needed in that class then + if ($parser->getEntityManager()->getConnection()->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + parent::parse($parser); + return; + } + + //Otherwise we have to do the same as the parent class, so we can use the same getSql method + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + // Do the field. + $this->field = $parser->ArithmeticPrimary(); + + // Add the strings to the values array. FIELD must + // be used with at least 1 string not including the field. + + $lexer = $parser->getLexer(); + + while (count($this->values) < 1 || + $lexer->lookahead['type'] != Lexer::T_CLOSE_PARENTHESIS) { + $parser->match(Lexer::T_COMMA); + $this->values[] = $parser->ArithmeticPrimary(); + } + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } + + public function getSql(SqlWalker $sqlWalker): string + { + //If we are on MySQL, we can use the builtin FIELD function and just call the parent method + if ($sqlWalker->getConnection()->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + return parent::getSql($sqlWalker); + } + + //When we are on SQLite, we have to emulate it with the FIELD2 function + if ($sqlWalker->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) { + return $this->getSqlForSQLite($sqlWalker); + } + + throw new \LogicException('Unsupported database platform'); + } + + /** + * Very similar to the parent method, but uses custom implmented FIELD2 function, which takes the values as a comma separated list + * instead of an array to migigate the argument count limit of SQLite. + * @param SqlWalker $sqlWalker + * @return string + * @throws \Doctrine\ORM\Query\AST\ASTException + */ + private function getSqlForSQLite(SqlWalker $sqlWalker): string + { + $query = 'FIELD2('; + + $query .= $this->field->dispatch($sqlWalker); + + $query .= ', "'; + + for ($i = 0, $iMax = count($this->values); $i < $iMax; $i++) { + if ($i > 0) { + $query .= ','; + } + + $query .= $this->values[$i]->dispatch($sqlWalker); + } + + $query .= '")'; + + return $query; + } +} \ No newline at end of file diff --git a/src/Doctrine/SQLiteRegexExtension.php b/src/Doctrine/SQLiteRegexExtension.php index 27348716..b1972a85 100644 --- a/src/Doctrine/SQLiteRegexExtension.php +++ b/src/Doctrine/SQLiteRegexExtension.php @@ -47,8 +47,9 @@ class SQLiteRegexExtension //Ensure that the function really exists on the connection, as it is marked as experimental according to PHP documentation if($native_connection instanceof \PDO && method_exists($native_connection, 'sqliteCreateFunction' )) { - $native_connection->sqliteCreateFunction('REGEXP', $this->regexp(...), 2); - $native_connection->sqliteCreateFunction('FIELD', $this->field(...)); + $native_connection->sqliteCreateFunction('REGEXP', $this->regexp(...), 2, \PDO::SQLITE_DETERMINISTIC); + $native_connection->sqliteCreateFunction('FIELD', $this->field(...), -1, \PDO::SQLITE_DETERMINISTIC); + $native_connection->sqliteCreateFunction('FIELD2', $this->field2(...), 2, \PDO::SQLITE_DETERMINISTIC); } } } @@ -68,6 +69,19 @@ class SQLiteRegexExtension } } + /** + * Very similar to the field function, but takes the array values as a comma separated string. + * This is needed as SQLite has a pretty low argument count limit. + * @param string|int|null $value + * @param string $imploded_array + * @return int + */ + private function field2(string|int|null $value, string $imploded_array): int + { + $array = explode(',', $imploded_array); + return $this->field($value, ...$array); + } + /** * This function emulates the MySQL field function for SQLite * This function returns the index (position) of the first argument in the subsequent arguments.#