diff --git a/src/Persistence/Sql/Expression.php b/src/Persistence/Sql/Expression.php index befe2b1a8..99bcfc9a0 100644 --- a/src/Persistence/Sql/Expression.php +++ b/src/Persistence/Sql/Expression.php @@ -235,6 +235,41 @@ protected function escapeParam($value): string return $name; } + /** + * @template TValue + * @template TNode + * + * @param list $values + * @param int<2, max> $n + * @param \Closure(list): TNode $mapNodeFx + * + * @return ($mapNodeFx is null ? TValue|list> : TNode) + */ + protected function makeNaryTree(array $values, int $n, ?\Closure $mapNodeFx = null) + { + if (count($values) <= $n) { + return $mapNodeFx === null + ? (count($values) === 1 + ? reset($values) + : $values) + : (count($values) === 1 + ? $mapNodeFx($values) + : $mapNodeFx(array_map(static fn ($v) => $mapNodeFx([$v]), $values))); + } + + $maxDepth = (int) ceil(log(count($values), $n)); + $countPerNode = $n ** ($maxDepth - 1); + + $res = array_map( + fn ($values) => $this->makeNaryTree($values, $n, $mapNodeFx), + array_chunk($values, $countPerNode) + ); + + return $mapNodeFx === null + ? $res + : $mapNodeFx($res); + } + /** * This method should be used only when string value cannot be bound. */ diff --git a/src/Persistence/Sql/Mssql/ExpressionTrait.php b/src/Persistence/Sql/Mssql/ExpressionTrait.php index cb7934f22..56cc6161c 100644 --- a/src/Persistence/Sql/Mssql/ExpressionTrait.php +++ b/src/Persistence/Sql/Mssql/ExpressionTrait.php @@ -49,18 +49,15 @@ protected function escapeStringLiteral(string $value): string $parts = ['\'\'']; } - $buildConcatSqlFx = static function (array $parts) use (&$buildConcatSqlFx): string { - if (count($parts) > 1) { - $partsLeft = array_slice($parts, 0, intdiv(count($parts), 2)); - $partsRight = array_slice($parts, count($partsLeft)); - - return 'concat(cast(' . $buildConcatSqlFx($partsLeft) . ' as NVARCHAR(MAX)), ' . $buildConcatSqlFx($partsRight) . ')'; + return $this->makeNaryTree($parts, 10, static function (array $parts) { + if (count($parts) === 1) { + return reset($parts); } - return reset($parts); - }; + $parts[0] = 'cast(' . $parts[0] . ' as NVARCHAR(MAX))'; - return $buildConcatSqlFx($parts); + return 'concat(' . implode(', ', $parts) . ')'; + }); } #[\Override] diff --git a/src/Persistence/Sql/Mysql/ExpressionTrait.php b/src/Persistence/Sql/Mysql/ExpressionTrait.php index 39134e1d6..d414bc96f 100644 --- a/src/Persistence/Sql/Mysql/ExpressionTrait.php +++ b/src/Persistence/Sql/Mysql/ExpressionTrait.php @@ -22,18 +22,13 @@ protected function escapeStringLiteral(string $value): string $parts = ['\'\'']; } - $buildConcatSqlFx = static function (array $parts) use (&$buildConcatSqlFx): string { - if (count($parts) > 1) { - $partsLeft = array_slice($parts, 0, intdiv(count($parts), 2)); - $partsRight = array_slice($parts, count($partsLeft)); - - return 'concat(' . $buildConcatSqlFx($partsLeft) . ', ' . $buildConcatSqlFx($partsRight) . ')'; + return $this->makeNaryTree($parts, 10, static function (array $parts) { + if (count($parts) === 1) { + return reset($parts); } - return reset($parts); - }; - - return $buildConcatSqlFx($parts); + return 'concat(' . implode(', ', $parts) . ')'; + }); } #[\Override] diff --git a/src/Persistence/Sql/Oracle/ExpressionTrait.php b/src/Persistence/Sql/Oracle/ExpressionTrait.php index 00cb0bdd2..8bcd045b6 100644 --- a/src/Persistence/Sql/Oracle/ExpressionTrait.php +++ b/src/Persistence/Sql/Oracle/ExpressionTrait.php @@ -12,18 +12,13 @@ protected function escapeStringLiteral(string $value): string // Oracle (multibyte) string literal is limited to 1332 bytes $parts = $this->splitLongString($value, 1000); if (count($parts) > 1) { - $buildConcatExprFx = function (array $parts) use (&$buildConcatExprFx): string { - if (count($parts) > 1) { - $partsLeft = array_slice($parts, 0, intdiv(count($parts), 2)); - $partsRight = array_slice($parts, count($partsLeft)); - - return 'concat(' . $buildConcatExprFx($partsLeft) . ', ' . $buildConcatExprFx($partsRight) . ')'; + return $this->makeNaryTree($parts, 2, function (array $parts) { + if (count($parts) === 1) { + return 'TO_CLOB(' . $this->escapeStringLiteral(reset($parts)) . ')'; } - return 'TO_CLOB(' . $this->escapeStringLiteral(reset($parts)) . ')'; - }; - - return $buildConcatExprFx($parts); + return 'concat(' . implode(', ', $parts) . ')'; + }); } $parts = []; @@ -50,18 +45,13 @@ protected function escapeStringLiteral(string $value): string $parts = ['\'\'']; } - $buildConcatSqlFx = static function (array $parts) use (&$buildConcatSqlFx): string { - if (count($parts) > 1) { - $partsLeft = array_slice($parts, 0, intdiv(count($parts), 2)); - $partsRight = array_slice($parts, count($partsLeft)); - - return 'concat(' . $buildConcatSqlFx($partsLeft) . ', ' . $buildConcatSqlFx($partsRight) . ')'; + return $this->makeNaryTree($parts, 2, static function (array $parts) { + if (count($parts) === 1) { + return reset($parts); } - return reset($parts); - }; - - return $buildConcatSqlFx($parts); + return 'concat(' . implode(', ', $parts) . ')'; + }); } /** @@ -109,23 +99,18 @@ protected function convertLongStringToClobExpr(string $value): Expression // Oracle (multibyte) string literal is limited to 1332 bytes $parts = $this->splitLongString($value, 1000); - $exprArgs = []; - $buildConcatExprFx = static function (array $parts) use (&$buildConcatExprFx, &$exprArgs): string { - if (count($parts) > 1) { - $partsLeft = array_slice($parts, 0, intdiv(count($parts), 2)); - $partsRight = array_slice($parts, count($partsLeft)); + $sqlArgs = []; + $sql = $this->makeNaryTree($parts, 2, static function (array $parts) use (&$sqlArgs) { + if (count($parts) === 1) { + $sqlArgs[] = reset($parts); - return 'concat(' . $buildConcatExprFx($partsLeft) . ', ' . $buildConcatExprFx($partsRight) . ')'; + return 'TO_CLOB([])'; } - $exprArgs[] = reset($parts); - - return 'TO_CLOB([])'; - }; - - $expr = $buildConcatExprFx($parts); + return 'concat(' . implode(', ', $parts) . ')'; + }); - return $this->expr($expr, $exprArgs); // @phpstan-ignore return.type + return $this->expr($sql, $sqlArgs); // @phpstan-ignore return.type } #[\Override] diff --git a/src/Persistence/Sql/Postgresql/ExpressionTrait.php b/src/Persistence/Sql/Postgresql/ExpressionTrait.php index 53c2f709b..c777666a4 100644 --- a/src/Persistence/Sql/Postgresql/ExpressionTrait.php +++ b/src/Persistence/Sql/Postgresql/ExpressionTrait.php @@ -43,18 +43,13 @@ protected function escapeStringLiteral(string $value): string $parts = ['\'\'']; } - $buildConcatSqlFx = static function (array $parts) use (&$buildConcatSqlFx): string { - if (count($parts) > 1) { - $partsLeft = array_slice($parts, 0, intdiv(count($parts), 2)); - $partsRight = array_slice($parts, count($partsLeft)); - - return 'concat(' . $buildConcatSqlFx($partsLeft) . ', ' . $buildConcatSqlFx($partsRight) . ')'; + return $this->makeNaryTree($parts, 10, static function (array $parts) { + if (count($parts) === 1) { + return reset($parts); } - return reset($parts); - }; - - return $buildConcatSqlFx($parts); + return 'concat(' . implode(', ', $parts) . ')'; + }); } #[\Override] diff --git a/src/Persistence/Sql/Sqlite/ExpressionTrait.php b/src/Persistence/Sql/Sqlite/ExpressionTrait.php index 73c48b09b..2231ad79a 100644 --- a/src/Persistence/Sql/Sqlite/ExpressionTrait.php +++ b/src/Persistence/Sql/Sqlite/ExpressionTrait.php @@ -18,18 +18,13 @@ protected function escapeStringLiteral(string $value): string } } - $buildConcatSqlFx = static function (array $parts) use (&$buildConcatSqlFx): string { - if (count($parts) > 1) { - $partsLeft = array_slice($parts, 0, intdiv(count($parts), 2)); - $partsRight = array_slice($parts, count($partsLeft)); - - return 'concat(' . $buildConcatSqlFx($partsLeft) . ', ' . $buildConcatSqlFx($partsRight) . ')'; + return $this->makeNaryTree($parts, 10, static function (array $parts) { + if (count($parts) === 1) { + return reset($parts); } - return reset($parts); - }; - - return $buildConcatSqlFx($parts); + return 'concat(' . implode(', ', $parts) . ')'; + }); } #[\Override] diff --git a/src/Schema/TestCase.php b/src/Schema/TestCase.php index d913ac58a..375a6b3d8 100644 --- a/src/Schema/TestCase.php +++ b/src/Schema/TestCase.php @@ -436,6 +436,7 @@ protected function markTestIncompleteOnMySQL8xPlatformAsBinaryLikeIsBroken(bool ) { // MySQL v8.0.22 and higher throws SQLSTATE[HY000]: General error: 3995 Character set 'binary' // cannot be used in conjunction with 'utf8mb4_0900_ai_ci' in call to regexp_like. + // TODO report // https://github.com/mysql/mysql-server/blob/72136a6d15/sql/item_regexp_func.cc#L115-L120 // https://dbfiddle.uk/9SA-omyF self::markTestIncomplete('MySQL 8.x has broken binary LIKE support'); diff --git a/tests/Persistence/Sql/ExpressionTest.php b/tests/Persistence/Sql/ExpressionTest.php index 02a494406..a844fd7b2 100644 --- a/tests/Persistence/Sql/ExpressionTest.php +++ b/tests/Persistence/Sql/ExpressionTest.php @@ -257,24 +257,24 @@ public function testEscapeStringLiteral(): void self::assertSame('concat(\'\', x\'00\')', $escapeStringLiteralFx("\0")); self::assertSame('concat(\'a\', x\'0000\')', $escapeStringLiteralFx("a\0\0")); self::assertSame('concat(\'a\', x\'' . str_repeat('00', 10_000) . '\')', $escapeStringLiteralFx('a' . str_repeat("\0", 10_000))); - self::assertSame('concat(\'a\', concat(x\'006200\', \'c\'))', $escapeStringLiteralFx("a\0b\0c")); + self::assertSame('concat(\'a\', x\'006200\', \'c\')', $escapeStringLiteralFx("a\0b\0c")); self::assertSame( - 'concat(\'a\', concat(x\'00' . str_repeat('62', 100) . '00\', \'c\'))', + 'concat(\'a\', x\'00' . str_repeat('62', 100) . '00\', \'c\')', $escapeStringLiteralFx("a\0" . str_repeat('b', 100) . "\0c") ); self::assertSame( - 'concat(concat(\'a\', x\'00\'), concat(\'' . str_repeat('b', 101) . '\', concat(x\'00\', \'c\')))', + 'concat(\'a\', x\'00\', \'' . str_repeat('b', 101) . '\', x\'00\', \'c\')', $escapeStringLiteralFx("a\0" . str_repeat('b', 101) . "\0c") ); self::assertSame('\'foo\'', $escapeStringLiteralFx('foo', MysqlExpression::class)); self::assertSame('x\'00\'', $escapeStringLiteralFx("\0", MysqlExpression::class)); self::assertSame( - 'concat(\'a\', concat(x\'00' . str_repeat('62', 100) . '00\', \'c\'))', + 'concat(\'a\', x\'00' . str_repeat('62', 100) . '00\', \'c\')', $escapeStringLiteralFx("a\0" . str_repeat('b', 100) . "\0c", MysqlExpression::class) ); self::assertSame( - 'concat(concat(\'a\', x\'00\'), concat(\'' . str_repeat('b', 101) . '\', concat(x\'00\', \'c\')))', + 'concat(\'a\', x\'00\', \'' . str_repeat('b', 101) . '\', x\'00\', \'c\')', $escapeStringLiteralFx("a\0" . str_repeat('b', 101) . "\0c", MysqlExpression::class) ); }