Skip to content

Commit

Permalink
Fix native CLOB support for Oracle
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Nov 22, 2021
1 parent e55d501 commit ae60a32
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 37 deletions.
27 changes: 19 additions & 8 deletions src/Persistence/Sql/Expression.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Atk4\Data\Persistence\Sql;

use Atk4\Core\WarnDynamicPropertyTrait;
use Atk4\Data\Persistence\Sql as SqlPersistence;
use Atk4\Data\Persistence;
use Doctrine\DBAL\Connection as DbalConnection;
use Doctrine\DBAL\Exception as DbalException;
use Doctrine\DBAL\ParameterType;
Expand Down Expand Up @@ -510,14 +510,15 @@ public function execute(object $connection = null): object
if ($connection instanceof DbalConnection) {
$query = $this->render();

$platform = $this->connection->getDatabasePlatform();
try {
$statement = $connection->prepare($query);

foreach ($this->params as $key => $val) {
if (is_int($val)) {
$type = ParameterType::INTEGER;
} elseif (is_bool($val)) {
if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
if ($platform instanceof PostgreSQL94Platform) {
$type = ParameterType::STRING;
$val = $val ? '1' : '0';
} else {
Expand All @@ -531,11 +532,14 @@ public function execute(object $connection = null): object
} elseif (is_string($val)) {
$type = ParameterType::STRING;

if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform
|| $this->connection->getDatabasePlatform() instanceof SQLServer2012Platform) {
$dummySqlPersistence = new SqlPersistence($this->connection);
if (\Closure::bind(fn () => $dummySqlPersistence->binaryTypeValueIsEncoded($val), null, SqlPersistence::class)()) {
$val = \Closure::bind(fn () => $dummySqlPersistence->binaryTypeValueDecode($val), null, SqlPersistence::class)();
if ($platform instanceof PostgreSQL94Platform
|| $platform instanceof SQLServer2012Platform
|| $platform instanceof OraclePlatform) {
$dummyPersistence = new Persistence\Sql($this->connection);
if (\Closure::bind(fn () => $dummyPersistence->binaryTypeValueIsEncoded($val), null, Persistence\Sql::class)()) {
if (!$platform instanceof OraclePlatform) {
$val = \Closure::bind(fn () => $dummyPersistence->binaryTypeValueDecode($val), null, Persistence\Sql::class)();
}
$type = ParameterType::BINARY;
}
}
Expand All @@ -548,7 +552,14 @@ public function execute(object $connection = null): object
->addMoreInfo('type', gettype($val));
}

$bind = $statement->bindValue($key, $val, $type);
if (is_string($val) && $platform instanceof OraclePlatform
&& ($type === ParameterType::BINARY || strlen($val) > 2000)) {
$valRef = $val;
$bind = $statement->bindParam($key, $valRef, ParameterType::STRING, strlen($val));
unset($valRef);
} else {
$bind = $statement->bindValue($key, $val, $type);
}
if ($bind === false) {
throw (new Exception('Unable to bind parameter'))
->addMoreInfo('param', $key)
Expand Down
35 changes: 7 additions & 28 deletions src/Persistence/Sql/Oracle/PlatformTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Atk4\Data\Persistence\Sql\Oracle;

use Atk4\Data\Exception;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Schema\Sequence;

Expand All @@ -18,43 +17,23 @@ protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
return $this->getVarcharTypeDeclarationSQLSnippet($length, $fixed);
}

// Oracle CLOB/BLOB has limited SQL support, see:
// https://stackoverflow.com/questions/12980038/ora-00932-inconsistent-datatypes-expected-got-clob#12980560
// fix this Oracle inconsistency by using VARCHAR/VARBINARY instead (but limited to 4000 bytes)

private function forwardTypeDeclarationSQL(string $targetMethodName, array $column): string
{
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS);
foreach ($backtrace as $frame) {
if ($this === ($frame['object'] ?? null)
&& $targetMethodName === ($frame['function'] ?? null)) {
throw new Exception('Long CLOB/TEXT (4000+ bytes) is not supported for Oracle');
}
}

return $this->{$targetMethodName}($column);
}

public function getClobTypeDeclarationSQL(array $column)
{
$column['length'] = $this->getVarcharMaxLength();

return $this->forwardTypeDeclarationSQL('getVarcharTypeDeclarationSQL', $column);
}
// public function getClobTypeDeclarationSQL(array $column)
// {
// $column['length'] = $this->getVarcharMaxLength();
//
// return $this->getVarcharTypeDeclarationSQL($column);
// }

public function getBlobTypeDeclarationSQL(array $column)
{
$column['length'] = $this->getBinaryMaxLength();

return $this->forwardTypeDeclarationSQL('getBinaryTypeDeclarationSQL', $column);
return $this->getClobTypeDeclarationSQL($column);
}

protected function initializeCommentedDoctrineTypes()
{
parent::initializeCommentedDoctrineTypes();

$this->markDoctrineTypeCommented('binary');
$this->markDoctrineTypeCommented('text');
$this->markDoctrineTypeCommented('blob');
}

Expand Down
70 changes: 70 additions & 0 deletions src/Persistence/Sql/Oracle/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Atk4\Data\Persistence\Sql\Oracle;

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
Expand All @@ -17,6 +20,73 @@ public function render(): string
return parent::render();
}

protected function castTextToClobExpr(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 VARCHAR is limited to 4000 bytes
$lengthBytes = strlen($value);
$startBytes = 0;
$parts = [];
do {
$part = mb_strcut($value, $startBytes, 4000); // TODO check exactly
$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) {
[$field, $value] = $row;
$cond = '=';
} elseif (count($row) >= 3) {
[$field, $cond, $value] = $row;
} else {
// for phpstan only, remove else block once
// https://github.com/phpstan/phpstan/issues/6017 is fixed
$field = null;
$cond = null;
$value = null;
}

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

if ($field->getTypeObject()->getName() !== 'blob') {
$field = $this->expr('LOWER([])', [$field]);
$value = $this->expr('LOWER([])', [$value]);
}

if (in_array($cond, ['=', '!=', '<>'], true)) {
$row = [$this->expr('dbms_lob.compare([], [])', [$field, $value]), $cond, 0];
} else {
throw (new Exception('Unsupported CLOB/BLOB field operator'))
->addMoreInfo('operator', $cond)
->addMoreInfo('type', $field->type);
}
}

return parent::_sub_render_condition($row);
}

public function groupConcat($field, string $delimiter = ',')
{
return $this->expr('listagg({field}, []) within group (order by {field})', ['field' => $field, $delimiter]);
Expand Down
12 changes: 11 additions & 1 deletion tests/Schema/ModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public function testCharacterTypeFieldCaseSensitivity(string $type, bool $isBina
$model->import([['v' => 'mixedcase'], ['v' => 'MIXEDCASE'], ['v' => 'MixedCase']]);

$model->addCondition('v', 'MixedCase');
$model->setOrder('v');
$model->setOrder($this->getDatabasePlatform() instanceof OraclePlatform ? 'id' : 'v');

$this->assertSame($isBinary ? [['id' => 3]] : [['id' => 1], ['id' => 2], ['id' => 3]], $model->export(['id']));
}
Expand Down Expand Up @@ -195,6 +195,16 @@ public function testCharacterTypeFieldLong(string $type, bool $isBinary, int $le
$lengthBytes = min($lengthBytes, 8190);
}

// TODO https://github.com/atk4/data/issues/918
if ($this->getDatabasePlatform() instanceof OraclePlatform && $type === 'blob') {
$lengthBytes = min($lengthBytes, 20000);
}

// TODO remove once "binary" type is fully supported, this is needed to limit the data to 255 bytes
if ($this->getDatabasePlatform() instanceof OraclePlatform && $type === 'binary') {
$lengthBytes = min($lengthBytes, 100);
}

$str = $this->makePseudoRandomString($isBinary, $lengthBytes);
if (!$isBinary) {
$str = preg_replace('~[\x00-\x1f]~', '-', $str);
Expand Down

0 comments on commit ae60a32

Please sign in to comment.