Skip to content

Commit

Permalink
Optimize Expression::escapeStringLiteral() using 10-ary CONCATs (#1225)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek authored Jun 1, 2024
1 parent bd01571 commit ba72cd0
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 77 deletions.
35 changes: 35 additions & 0 deletions src/Persistence/Sql/Expression.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,41 @@ protected function escapeParam($value): string
return $name;
}

/**
* @template TValue
* @template TNode
*
* @param list<TValue> $values
* @param int<2, max> $n
* @param \Closure(list<TValue|TNode>): TNode $mapNodeFx
*
* @return ($mapNodeFx is null ? TValue|list<TValue|list<mixed>> : 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.
*/
Expand Down
15 changes: 6 additions & 9 deletions src/Persistence/Sql/Mssql/ExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
15 changes: 5 additions & 10 deletions src/Persistence/Sql/Mysql/ExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
51 changes: 18 additions & 33 deletions src/Persistence/Sql/Oracle/ExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -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) . ')';
});
}

/**
Expand Down Expand Up @@ -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]
Expand Down
15 changes: 5 additions & 10 deletions src/Persistence/Sql/Postgresql/ExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
15 changes: 5 additions & 10 deletions src/Persistence/Sql/Sqlite/ExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions src/Schema/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
10 changes: 5 additions & 5 deletions tests/Persistence/Sql/ExpressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
Expand Down

0 comments on commit ba72cd0

Please sign in to comment.