Skip to content

Commit

Permalink
Fix long string param support for Oracle (#974)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Mar 13, 2022
1 parent bd6b5aa commit 0897e0f
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 42 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/test-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ jobs:
name: Unit
runs-on: ubuntu-latest
container:
image: ghcr.io/mvorisek/image-php:${{ matrix.php }}
# Alpine support for newer pdo_oci is broken, see https://github.com/mlocati/docker-php-extension-installer/issues/523
# remove once config.m4 checks are working on Alpine
image: ghcr.io/mvorisek/image-php:${{ matrix.php }}-debian
strategy:
fail-fast: false
matrix:
Expand Down
10 changes: 5 additions & 5 deletions src/Persistence/Sql/Mssql/ExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

trait ExpressionTrait
{
private function fixOpenEscapeChar(string $v): string
{
return preg_replace('~(?:\'(?:\'\'|\\\\\'|[^\'])*\')?+\K\]([^\[\]\'"(){}]*?)\]~s', '[$1]', $v);
}

protected function escapeIdentifier(string $value): string
{
return $this->fixOpenEscapeChar(parent::escapeIdentifier($value));
Expand All @@ -16,11 +21,6 @@ protected function escapeIdentifierSoft(string $value): string
return $this->fixOpenEscapeChar(parent::escapeIdentifierSoft($value));
}

private function fixOpenEscapeChar(string $v): string
{
return preg_replace('~(?:\'(?:\'\'|\\\\\'|[^\'])*\')?+\K\]([^\[\]\'"(){}]*?)\]~s', '[$1]', $v);
}

public function render(): array
{
[$sql, $params] = parent::render();
Expand Down
12 changes: 12 additions & 0 deletions src/Persistence/Sql/Oracle/Expression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Persistence\Sql\Oracle;

use Atk4\Data\Persistence\Sql\Expression as BaseExpression;

class Expression extends BaseExpression
{
use ExpressionTrait;
}
81 changes: 81 additions & 0 deletions src/Persistence/Sql/Oracle/ExpressionTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Persistence\Sql\Oracle;

trait ExpressionTrait
{
protected function castLongStringToClobExpr(string $value): Expression
{
$exprArgs = [];
$buildConcatExprFx = function (array $parts) use (&$buildConcatExprFx, &$exprArgs): string {
if (count($parts) > 1) {
$valueLeft = array_slice($parts, 0, intdiv(count($parts), 2));
$valueRight = array_slice($parts, count($valueLeft));

return 'CONCAT(' . $buildConcatExprFx($valueLeft) . ', ' . $buildConcatExprFx($valueRight) . ')';
}

$exprArgs[] = count($parts) > 0 ? reset($parts) : '';

return 'TO_CLOB([])';
};

// Oracle SQL (multibyte) string literal is limited to 1332 bytes
$parts = [];
foreach (mb_str_split($value, 10_000) as $shorterValue) {
$lengthBytes = strlen($shorterValue);
$startBytes = 0;
do {
$part = mb_strcut($shorterValue, $startBytes, 1000);
$startBytes += strlen($part);
$parts[] = $part;
} while ($startBytes < $lengthBytes);
}

$expr = $buildConcatExprFx($parts);

return $this->expr($expr, $exprArgs); // @phpstan-ignore-line
}

protected function updateRenderBeforeExecute(array $render): array
{
[$sql, $params] = parent::updateRenderBeforeExecute($render);

$newParamBase = $this->paramBase;
$newParams = [];
$sql = preg_replace_callback(
'~(?:\'(?:\'\'|\\\\\'|[^\'])*\')?+\K:\w+~s',
function ($matches) use ($params, &$newParams, &$newParamBase) {
$value = $params[$matches[0]];
if (is_string($value) && strlen($value) > 4000) {
$expr = $this->castLongStringToClobExpr($value);
unset($value);
[$exprSql, $exprParams] = $expr->render();
$sql = preg_replace_callback(
'~(?:\'(?:\'\'|\\\\\'|[^\'])*\')?+\K:\w+~s',
function ($matches) use ($exprParams, &$newParams, &$newParamBase) {
$name = ':' . $newParamBase;
++$newParamBase;
$newParams[$name] = $exprParams[$matches[0]];

return $name;
},
$exprSql
);
} else {
$sql = ':' . $newParamBase;
++$newParamBase;

$newParams[$sql] = $value;
}

return $sql;
},
$sql
);

return [$sql, $newParams];
}
}
40 changes: 4 additions & 36 deletions src/Persistence/Sql/Oracle/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@

use Atk4\Data\Exception;
use Atk4\Data\Field;
use Atk4\Data\Persistence\Sql\Expression;
use Atk4\Data\Persistence\Sql\Query as BaseQuery;

class Query extends BaseQuery
{
use ExpressionTrait;

protected $paramBase = 'xxaaaa';

protected $expression_class = Expression::class;

public function render(): array
{
if ($this->mode === 'select' && count($this->args['table'] ?? []) === 0) {
Expand All @@ -28,39 +31,6 @@ public function render(): array
return parent::render();
}

protected function castStringToClobExpr(string $value): Expression
{
$exprArgs = [];
$buildConcatExprFx = function (array $parts) use (&$buildConcatExprFx, &$exprArgs): string {
if (count($parts) > 1) {
$valueLeft = array_slice($parts, 0, intdiv(count($parts), 2));
$valueRight = array_slice($parts, count($valueLeft));

return 'CONCAT(' . $buildConcatExprFx($valueLeft) . ', ' . $buildConcatExprFx($valueRight) . ')';
}

$exprArgs[] = reset($parts);

return 'TO_CLOB([])';
};

// Oracle SQL string literal is limited to 1332 bytes
$parts = [];
foreach (mb_str_split($value, 10_000) as $shorterValue) {
$lengthBytes = strlen($shorterValue);
$startBytes = 0;
do {
$part = mb_strcut($shorterValue, $startBytes, 1000);
$startBytes += strlen($part);
$parts[] = $part;
} while ($startBytes < $lengthBytes);
}

$expr = $buildConcatExprFx($parts);

return $this->expr($expr, $exprArgs);
}

protected function _sub_render_condition(array $row): string
{
if (count($row) === 2) {
Expand All @@ -78,8 +48,6 @@ protected function _sub_render_condition(array $row): string

if (count($row) >= 2 && $field instanceof Field
&& in_array($field->getTypeObject()->getName(), ['text', 'blob'], true)) {
$value = $this->castStringToClobExpr($value);

if ($field->getTypeObject()->getName() === 'text') {
$field = $this->expr('LOWER([])', [$field]);
$value = $this->expr('LOWER([])', [$value]);
Expand Down

0 comments on commit 0897e0f

Please sign in to comment.