diff --git a/src/Model.php b/src/Model.php index cc5c43485..28626ea2a 100644 --- a/src/Model.php +++ b/src/Model.php @@ -1662,8 +1662,8 @@ public function insert(array $row) $entity = $this->createEntity(); $hasRefs = false; - foreach ($row as $v) { - if (is_array($v)) { + foreach ($row as $k => $v) { + if (is_array($v) && $this->hasReference($k)) { $hasRefs = true; break; diff --git a/src/Persistence/Sql.php b/src/Persistence/Sql.php index cc00190c7..2d0bf1e75 100644 --- a/src/Persistence/Sql.php +++ b/src/Persistence/Sql.php @@ -25,6 +25,7 @@ class Sql extends Persistence { use Sql\BinaryTypeCompatibilityTypecastTrait; + use Sql\JsonTypeCompatibilityTypecastTrait; public const HOOK_INIT_SELECT_QUERY = self::class . '@initSelectQuery'; public const HOOK_BEFORE_INSERT_QUERY = self::class . '@beforeInsertQuery'; @@ -648,8 +649,12 @@ public function typecastSaveField(Field $field, $value) { $value = parent::typecastSaveField($field, $value); - if ($value !== null && !$value instanceof Expression && $this->binaryTypeIsEncodeNeeded($field->type)) { - $value = $this->binaryTypeValueEncode($value); + if ($value !== null && !$value instanceof Expression) { + if ($this->binaryTypeIsEncodeNeeded($field->type)) { + $value = $this->binaryTypeValueEncode($value); + } elseif ($this->jsonTypeIsEncodeNeeded($field->type)) { + $value = $this->jsonTypeValueEncode($value); + } } return $value; @@ -658,12 +663,16 @@ public function typecastSaveField(Field $field, $value) #[\Override] public function typecastLoadField(Field $field, $value) { - $value = parent::typecastLoadField($field, $value); - - if ($value !== null && $this->binaryTypeIsDecodeNeeded($field->type, $value)) { - $value = $this->binaryTypeValueDecode($value); + if ($value !== null) { + if ($this->binaryTypeIsDecodeNeeded($field->type, $value)) { + $value = $this->binaryTypeValueDecode($value); + } elseif ($this->jsonTypeIsDecodeNeeded($field->type, $value)) { + $value = $this->jsonTypeValueDecode($value); + } } + $value = parent::typecastLoadField($field, $value); + return $value; } diff --git a/src/Persistence/Sql/Expression.php b/src/Persistence/Sql/Expression.php index b61cc423b..e23714c08 100644 --- a/src/Persistence/Sql/Expression.php +++ b/src/Persistence/Sql/Expression.php @@ -567,6 +567,8 @@ protected function _execute(?object $connection, bool $fromExecuteStatement) if (\Closure::bind(static fn () => $dummyPersistence->binaryTypeValueIsEncoded($val), null, Persistence\Sql::class)()) { $val = \Closure::bind(static fn () => $dummyPersistence->binaryTypeValueDecode($val), null, Persistence\Sql::class)(); $type = ParameterType::BINARY; + } elseif (\Closure::bind(static fn () => $dummyPersistence->jsonTypeValueIsEncoded($val), null, Persistence\Sql::class)()) { + $val = \Closure::bind(static fn () => $dummyPersistence->jsonTypeValueDecode($val), null, Persistence\Sql::class)(); } } } elseif (is_resource($val)) { diff --git a/src/Persistence/Sql/JsonTypeCompatibilityTypecastTrait.php b/src/Persistence/Sql/JsonTypeCompatibilityTypecastTrait.php new file mode 100644 index 000000000..3c938108b --- /dev/null +++ b/src/Persistence/Sql/JsonTypeCompatibilityTypecastTrait.php @@ -0,0 +1,74 @@ +jsonTypeValueGetPrefixConst() . hash('crc32b', $value) . $value; + } + + private function jsonTypeValueIsEncoded(string $value): bool + { + return str_starts_with($value, $this->jsonTypeValueGetPrefixConst()); + } + + private function jsonTypeValueDecode(string $value): string + { + if (!$this->jsonTypeValueIsEncoded($value)) { + throw new Exception('Unexpected unencoded json value'); + } + + $resCrc = substr($value, strlen($this->jsonTypeValueGetPrefixConst()), 8); + $res = substr($value, strlen($this->jsonTypeValueGetPrefixConst()) + 8); + if ($resCrc !== hash('crc32b', $res)) { + throw new Exception('Unexpected json value crc'); + } + + if ($this->jsonTypeValueIsEncoded($res)) { + throw new Exception('Unexpected double encoded json value'); + } + + return $res; + } + + private function jsonTypeIsEncodeNeeded(string $type): bool + { + // json values for PostgreSQL database are stored natively, but we need + // to encode first to hold the json type info for PDO parameter type binding + + $platform = $this->getDatabasePlatform(); + if ($platform instanceof PostgreSQLPlatform) { + if ($type === 'json') { + return true; + } + } + + return false; + } + + /** + * @param scalar $value + */ + private function jsonTypeIsDecodeNeeded(string $type, $value): bool + { + if ($this->jsonTypeIsEncodeNeeded($type)) { + if ($this->jsonTypeValueIsEncoded($value)) { + return true; + } + } + + return false; + } +} diff --git a/src/Persistence/Sql/Mssql/Query.php b/src/Persistence/Sql/Mssql/Query.php index e64d21436..8677f9480 100644 --- a/src/Persistence/Sql/Mssql/Query.php +++ b/src/Persistence/Sql/Mssql/Query.php @@ -64,10 +64,10 @@ static function ($sqlLeft, $sqlRight) use ($reuse, $makeSqlFx, $nullFromArgsOnly #[\Override] protected function _renderConditionLikeOperator(bool $negated, string $sqlLeft, string $sqlRight): string { - return $this->_renderConditionBinaryReuseBool( + return ($negated ? 'not ' : '') . $this->_renderConditionBinaryReuseBool( $sqlLeft, $sqlRight, - function ($sqlLeft, $sqlRight) use ($negated) { + function ($sqlLeft, $sqlRight) { $iifNtextFx = static function ($valueSql, $trueSql, $falseSql) { $isNtextFx = static function ($sql, $negate) { // "select top 0 ..." is always optimized into constant expression @@ -90,7 +90,7 @@ function ($sqlLeft, $sqlRight) use ($negated) { . ' or (' . $isBinaryFx($valueSql, true) . ' and ' . $falseSql . '))'; }; - $makeSqlFx = function ($isNtext, $isBinary) use ($sqlLeft, $sqlRight, $negated) { + $makeSqlFx = function ($isNtext, $isBinary) use ($sqlLeft, $sqlRight) { $quoteStringFx = fn (string $v) => $isNtext ? $this->escapeStringLiteral($v) : '0x' . bin2hex($v); @@ -114,7 +114,7 @@ function ($sqlLeft, $sqlRight) use ($negated) { $sqlRightEscaped = $replaceFx($sqlRightEscaped, '[', '\['); - return $sqlLeft . ($negated ? ' not' : '') . ' like ' . $sqlRightEscaped + return $sqlLeft . ' like ' . $sqlRightEscaped . ($isBinary ? ' collate Latin1_General_BIN' : '') . ' escape ' . $quoteStringFx('\\'); }; diff --git a/src/Persistence/Sql/Postgresql/Connection.php b/src/Persistence/Sql/Postgresql/Connection.php index 78daa9269..ce2e1af49 100644 --- a/src/Persistence/Sql/Postgresql/Connection.php +++ b/src/Persistence/Sql/Postgresql/Connection.php @@ -5,9 +5,23 @@ namespace Atk4\Data\Persistence\Sql\Postgresql; use Atk4\Data\Persistence\Sql\Connection as BaseConnection; +use Doctrine\DBAL\Configuration; class Connection extends BaseConnection { protected string $expressionClass = Expression::class; protected string $queryClass = Query::class; + + #[\Override] + protected static function createDbalConfiguration(): Configuration + { + $configuration = parent::createDbalConfiguration(); + + $configuration->setMiddlewares([ + ...$configuration->getMiddlewares(), + new InitializeSessionMiddleware(), + ]); + + return $configuration; + } } diff --git a/src/Persistence/Sql/Postgresql/ExpressionTrait.php b/src/Persistence/Sql/Postgresql/ExpressionTrait.php index 7a22681a0..f9d8bd630 100644 --- a/src/Persistence/Sql/Postgresql/ExpressionTrait.php +++ b/src/Persistence/Sql/Postgresql/ExpressionTrait.php @@ -79,6 +79,17 @@ static function ($matches) use ($params) { $sql = 'cast(' . $sql . ' as BIGINT)'; } elseif (is_float($value)) { $sql = 'cast(' . $sql . ' as DOUBLE PRECISION)'; + } elseif (is_string($value)) { + $dummyPersistence = (new \ReflectionClass(Persistence\Sql::class))->newInstanceWithoutConstructor(); + if (\Closure::bind(static fn () => $dummyPersistence->binaryTypeValueIsEncoded($value), null, Persistence\Sql::class)()) { + $sql = 'cast(' . $sql . ' as bytea)'; + } elseif (\Closure::bind(static fn () => $dummyPersistence->jsonTypeValueIsEncoded($value), null, Persistence\Sql::class)()) { + $sql = 'cast(' . $sql . ' as json)'; + } else { + $sql = 'cast(' . $sql . ' as citext)'; + } + } else { + $sql = 'cast(' . $sql . ' as unknown)'; } return $sql; diff --git a/src/Persistence/Sql/Postgresql/InitializeSessionMiddleware.php b/src/Persistence/Sql/Postgresql/InitializeSessionMiddleware.php new file mode 100644 index 000000000..1ef3e3940 --- /dev/null +++ b/src/Persistence/Sql/Postgresql/InitializeSessionMiddleware.php @@ -0,0 +1,40 @@ +query('SELECT to_regtype(\'citext\')')->fetchOne() === null) { + // "CREATE EXTENSION IF NOT EXISTS ..." cannot be used as it requires + // CREATE privilege even if the extension is already installed + $connection->query('CREATE EXTENSION citext'); + } + + return $connection; + } + }; + } +} diff --git a/src/Persistence/Sql/Postgresql/PlatformTrait.php b/src/Persistence/Sql/Postgresql/PlatformTrait.php index b42fa5dc4..0dac0809b 100644 --- a/src/Persistence/Sql/Postgresql/PlatformTrait.php +++ b/src/Persistence/Sql/Postgresql/PlatformTrait.php @@ -22,7 +22,9 @@ private function getCreateCaseInsensitiveDomainsSql(): array $sqls[] = 'DO' . "\n" . '$$' . "\n" . 'BEGIN' . "\n" - . ' CREATE EXTENSION IF NOT EXISTS citext;' . "\n" + . ' IF to_regtype(\'citext\') IS NULL THEN' . "\n" + . ' CREATE EXTENSION citext;' . "\n" + . ' END IF;' . "\n" . implode("\n", array_map(static function (string $domain): string { return ' IF to_regtype(\'' . $domain . '\') IS NULL THEN' . "\n" . ' CREATE DOMAIN ' . $domain . ' AS citext;' . "\n" diff --git a/src/Persistence/Sql/Postgresql/Query.php b/src/Persistence/Sql/Postgresql/Query.php index d23896969..fd924309d 100644 --- a/src/Persistence/Sql/Postgresql/Query.php +++ b/src/Persistence/Sql/Postgresql/Query.php @@ -70,8 +70,8 @@ function ($sqlLeft, $sqlRight) use ($makeSqlFx) { return $iifByteaSqlFx( $sqlLeft, - $makeSqlFx($escapeNonUtf8Fx($sqlLeft), $escapeNonUtf8Fx($sqlRight, true)), - $makeSqlFx('cast(' . $sqlLeft . ' as citext)', $sqlRight) + $makeSqlFx($escapeNonUtf8Fx($sqlLeft), $escapeNonUtf8Fx($sqlRight)), + $makeSqlFx('cast(' . $sqlLeft . ' as citext)', 'cast(' . $sqlRight . ' as citext)') ); } ); @@ -80,13 +80,13 @@ function ($sqlLeft, $sqlRight) use ($makeSqlFx) { #[\Override] protected function _renderConditionLikeOperator(bool $negated, string $sqlLeft, string $sqlRight): string { - return $this->_renderConditionConditionalCastToText($sqlLeft, $sqlRight, function ($sqlLeft, $sqlRight) use ($negated) { + return ($negated ? 'not ' : '') . $this->_renderConditionConditionalCastToText($sqlLeft, $sqlRight, function ($sqlLeft, $sqlRight) { $sqlRightEscaped = 'regexp_replace(' . $sqlRight . ', ' . $this->escapeStringLiteral('(\\\[\\\_%])|(\\\)') . ', ' . $this->escapeStringLiteral('\1\2\2') . ', ' . $this->escapeStringLiteral('g') . ')'; - return $sqlLeft . ($negated ? ' not' : '') . ' like ' . $sqlRightEscaped + return $sqlLeft . ' like ' . $sqlRightEscaped . ' escape ' . $this->escapeStringLiteral('\\'); }); } @@ -95,8 +95,8 @@ protected function _renderConditionLikeOperator(bool $negated, string $sqlLeft, #[\Override] protected function _renderConditionRegexpOperator(bool $negated, string $sqlLeft, string $sqlRight, bool $binary = false): string { - return $this->_renderConditionConditionalCastToText($sqlLeft, $sqlRight, static function ($sqlLeft, $sqlRight) use ($negated) { - return $sqlLeft . ' ' . ($negated ? '!' : '') . '~ ' . $sqlRight; + return ($negated ? 'not ' : '') . $this->_renderConditionConditionalCastToText($sqlLeft, $sqlRight, static function ($sqlLeft, $sqlRight) { + return $sqlLeft . ' ~ ' . $sqlRight; }); } diff --git a/src/Schema/TestCase.php b/src/Schema/TestCase.php index 28134990b..0a6bd220d 100644 --- a/src/Schema/TestCase.php +++ b/src/Schema/TestCase.php @@ -101,7 +101,7 @@ protected function logQuery(string $sql, array $params, array $types): void $i = 0; $quotedTokenRegex = $this->getConnection()->expr()::QUOTED_TOKEN_REGEX; $sql = preg_replace_callback( - '~' . $quotedTokenRegex . '\K|(\?)|cast\((\?|:\w+) as (BOOLEAN|INTEGER|BIGINT|DOUBLE PRECISION|BINARY_DOUBLE)\)|\((\?|:\w+) \+ 0\.00\)~', + '~' . $quotedTokenRegex . '\K|(\?)|cast\((\?|:\w+) as (BOOLEAN|INTEGER|BIGINT|DOUBLE PRECISION|BINARY_DOUBLE|citext|bytea|unknown)\)|\((\?|:\w+) \+ 0\.00\)~', static function ($matches) use (&$types, &$params, &$i) { if ($matches[0] === '') { return ''; @@ -113,7 +113,9 @@ static function ($matches) use (&$types, &$params, &$i) { return $matches[0]; } - $k = isset($matches[4]) ? ($matches[4] === '?' ? ++$i : $matches[4]) : ($matches[2] === '?' ? ++$i : $matches[2]); + $k = isset($matches[4]) + ? ($matches[4] === '?' ? ++$i : $matches[4]) + : ($matches[2] === '?' ? ++$i : $matches[2]); if ($matches[3] === 'BOOLEAN' && ($types[$k] === ParameterType::BOOLEAN || $types[$k] === ParameterType::INTEGER) && (is_bool($params[$k]) || $params[$k] === '0' || $params[$k] === '1') @@ -131,6 +133,10 @@ static function ($matches) use (&$types, &$params, &$i) { $params[$k] = (float) $params[$k]; return $matches[4] ?? $matches[2]; + } elseif (($matches[3] === 'citext' || $matches[3] === 'bytea') && is_string($params[$k])) { + return $matches[2]; + } elseif ($matches[3] === 'unknown' && $params[$k] === null) { + return $matches[2]; } return $matches[0]; diff --git a/tests/JoinSqlTest.php b/tests/JoinSqlTest.php index b3e08f77b..f340afada 100644 --- a/tests/JoinSqlTest.php +++ b/tests/JoinSqlTest.php @@ -817,7 +817,7 @@ public function testJoinActualFieldNamesAndPrefix(): void $j->addField('phone', ['actual' => 'contact_phone']); // reverse join $j2 = $user->join('salaries.' . $userForeignIdFieldName, ['prefix' => 'j2_']); - $j2->addField('salary', ['actual' => 'amount']); + $j2->addField('salary', ['actual' => 'amount', 'type' => 'integer']); // update $user2 = $user->load(1); diff --git a/tests/Persistence/Sql/QueryTest.php b/tests/Persistence/Sql/QueryTest.php index 4cb605370..836c4beb6 100644 --- a/tests/Persistence/Sql/QueryTest.php +++ b/tests/Persistence/Sql/QueryTest.php @@ -883,12 +883,12 @@ public function testWhereLike(): void self::assertSame( version_compare(SqliteConnection::getDriverVersion(), '3.45') < 0 ? <<<'EOF' - where case case when instr(sum("b"), '_') != 0 then 1 else sum("a") like regexp_replace(sum("b"), '(\\[\\_%])|(\\)', '\1\2\2') escape '\' end when 1 then regexp_like(sum("a"), concat('^',regexp_replace(regexp_replace(regexp_replace(regexp_replace(sum("b"), '\\(?:(?=[_%])|\K\\)|(?=[.\\+*?[^\]$(){}|])', '\\'), '(?where($this->e('sum({})', ['a']), 'like', $this->e('sum({})', ['b']))->render()[0] + (new SqliteQuery('[where]'))->where($this->e('sum({})', ['a']), 'not like', $this->e('sum({})', ['b']))->render()[0] ); foreach (['5.7.0', '8.0.0', 'MariaDB-11.0.0'] as $serverVersion) { @@ -905,26 +905,26 @@ public function testWhereLike(): void self::assertSame( $serverVersion === '5.7.0' ? <<<'EOF' - where sum("a") like replace(replace(replace(replace(replace(replace(replace(replace(sum("b"), '\\\\', '\\\\*'), '\\_', '\\_*'), '\\%', '\\%*'), '\\', '\\\\'), '\\\\_*', '\\_'), '\\\\%*', '\\%'), '\\\\\\\\*', '\\\\'), '%\\', '%\\\\') escape '\\' + where sum("a") not like replace(replace(replace(replace(replace(replace(replace(replace(sum("b"), '\\\\', '\\\\*'), '\\_', '\\_*'), '\\%', '\\%*'), '\\', '\\\\'), '\\\\_*', '\\_'), '\\\\%*', '\\%'), '\\\\\\\\*', '\\\\'), '%\\', '%\\\\') escape '\\' EOF : <<<'EOF' - where sum("a") like regexp_replace(sum("b"), '\\\\\\\\|\\\\(?![_%])', '\\\\\\\\') escape '\\' + where sum("a") not like regexp_replace(sum("b"), '\\\\\\\\|\\\\(?![_%])', '\\\\\\\\') escape '\\' EOF, - $this->createMysqlQuery($serverVersion, '[where]')->where($this->e('sum({})', ['a']), 'like', $this->e('sum({})', ['b']))->render()[0] + $this->createMysqlQuery($serverVersion, '[where]')->where($this->e('sum({})', ['a']), 'not like', $this->e('sum({})', ['b']))->render()[0] ); } self::assertSame( <<<'EOF' - where case when pg_typeof("name") = 'bytea'::regtype then replace(regexp_replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then replace(substring(cast("name" as text) from 3), chr(92), repeat(chr(92), 2)) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), '(?where('name', 'like', 'foo')->render()[0] ); self::assertSame( <<<'EOF' - where (select case when pg_typeof("__atk4_reuse_left__") = 'bytea'::regtype then replace(regexp_replace(encode(case when pg_typeof("__atk4_reuse_left__") = 'bytea'::regtype then decode(case when pg_typeof("__atk4_reuse_left__") = 'bytea'::regtype then replace(substring(cast("__atk4_reuse_left__" as text) from 3), chr(92), repeat(chr(92), 2)) else '' end, 'hex') else cast(replace(cast("__atk4_reuse_left__" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), '(?where($this->e('sum({})', ['a']), 'like', $this->e('sum({})', ['b']))->render()[0] + (new PostgresqlQuery('[where]'))->where($this->e('sum({})', ['a']), 'not like', $this->e('sum({})', ['b']))->render()[0] ); self::assertSame( @@ -935,9 +935,9 @@ public function testWhereLike(): void ); self::assertSame( <<<'EOF' - where (select iif(not(((datalength(concat((select top 0 [__atk4_reuse_left__]), 0x30)) = 2 and [__atk4_reuse_left__] like replace(replace(replace(replace(replace(replace(replace(replace([__atk4_reuse_right__], N'\\', N'\\*'), N'\_', N'\_*'), N'\%', N'\%*'), N'\', N'\\'), N'\\_*', N'\_'), N'\\%*', N'\%'), N'\\\\*', N'\\'), N'[', N'\[') escape N'\') or (datalength(concat((select top 0 [__atk4_reuse_left__]), 0x30)) != 2 and ((isnull((select top 0 [__atk4_reuse_left__]), 0x41) != 0x61 and [__atk4_reuse_left__] like replace(replace(replace(replace(replace(replace(replace(replace([__atk4_reuse_right__], 0x5c5c, 0x5c5c2a), 0x5c5f, 0x5c5f2a), 0x5c25, 0x5c252a), 0x5c, 0x5c5c), 0x5c5c5f2a, 0x5c5f), 0x5c5c252a, 0x5c25), 0x5c5c5c5c2a, 0x5c5c), 0x5b, 0x5c5b) collate Latin1_General_BIN escape 0x5c) or (isnull((select top 0 [__atk4_reuse_left__]), 0x41) = 0x61 and [__atk4_reuse_left__] like replace(replace(replace(replace(replace(replace(replace(replace([__atk4_reuse_right__], 0x5c5c, 0x5c5c2a), 0x5c5f, 0x5c5f2a), 0x5c25, 0x5c252a), 0x5c, 0x5c5c), 0x5c5c5f2a, 0x5c5f), 0x5c5c252a, 0x5c25), 0x5c5c5c5c2a, 0x5c5c), 0x5b, 0x5c5b) escape 0x5c))))), 0, iif([__atk4_reuse_left__] is not null and [__atk4_reuse_right__] is not null, 1, null)) from (select sum("a") [__atk4_reuse_left__], sum("b") [__atk4_reuse_right__]) [__atk4_reuse_tmp__]) = 1 + where not (select iif(not(((datalength(concat((select top 0 [__atk4_reuse_left__]), 0x30)) = 2 and [__atk4_reuse_left__] like replace(replace(replace(replace(replace(replace(replace(replace([__atk4_reuse_right__], N'\\', N'\\*'), N'\_', N'\_*'), N'\%', N'\%*'), N'\', N'\\'), N'\\_*', N'\_'), N'\\%*', N'\%'), N'\\\\*', N'\\'), N'[', N'\[') escape N'\') or (datalength(concat((select top 0 [__atk4_reuse_left__]), 0x30)) != 2 and ((isnull((select top 0 [__atk4_reuse_left__]), 0x41) != 0x61 and [__atk4_reuse_left__] like replace(replace(replace(replace(replace(replace(replace(replace([__atk4_reuse_right__], 0x5c5c, 0x5c5c2a), 0x5c5f, 0x5c5f2a), 0x5c25, 0x5c252a), 0x5c, 0x5c5c), 0x5c5c5f2a, 0x5c5f), 0x5c5c252a, 0x5c25), 0x5c5c5c5c2a, 0x5c5c), 0x5b, 0x5c5b) collate Latin1_General_BIN escape 0x5c) or (isnull((select top 0 [__atk4_reuse_left__]), 0x41) = 0x61 and [__atk4_reuse_left__] like replace(replace(replace(replace(replace(replace(replace(replace([__atk4_reuse_right__], 0x5c5c, 0x5c5c2a), 0x5c5f, 0x5c5f2a), 0x5c25, 0x5c252a), 0x5c, 0x5c5c), 0x5c5c5f2a, 0x5c5f), 0x5c5c252a, 0x5c25), 0x5c5c5c5c2a, 0x5c5c), 0x5b, 0x5c5b) escape 0x5c))))), 0, iif([__atk4_reuse_left__] is not null and [__atk4_reuse_right__] is not null, 1, null)) from (select sum("a") [__atk4_reuse_left__], sum("b") [__atk4_reuse_right__]) [__atk4_reuse_tmp__]) = 1 EOF, - (new MssqlQuery('[where]'))->where($this->e('sum({})', ['a']), 'like', $this->e('sum({})', ['b']))->render()[0] + (new MssqlQuery('[where]'))->where($this->e('sum({})', ['a']), 'not like', $this->e('sum({})', ['b']))->render()[0] ); $binaryPrefix = "atk4_binary\ru5f8mzx4vsm8g2c9\r"; @@ -949,9 +949,9 @@ public function testWhereLike(): void ); self::assertSame( <<<'EOF' - where (select case when not(case when "__atk4_reuse_left__" is null or "__atk4_reuse_right__" is null then null when "__atk4_reuse_left__" like 'BBB________%' or "__atk4_reuse_right__" like 'BBB________%' then case when regexp_like(case when "__atk4_reuse_left__" like 'BBB________%' then to_char(substr("__atk4_reuse_left__", 38)) else rawtohex(utl_raw.cast_to_raw("__atk4_reuse_left__")) end, concat('^', concat(replace(replace(replace(replace(replace(replace(replace(replace(replace(case when "__atk4_reuse_right__" like 'BBB________%' then to_char(substr("__atk4_reuse_right__", 38)) else rawtohex(utl_raw.cast_to_raw("__atk4_reuse_right__")) end, '5c5c', 'x'), '5c5f', 'y'), '5c25', 'z'), '5c', 'x'), '5f', '..'), '25', '(..)*'), 'x', '5c'), 'y', '5f'), 'z', '25'), '$')), 'in') then 1 else 0 end else case when "__atk4_reuse_left__" like regexp_replace("__atk4_reuse_right__", '(\\[\\_%])|(\\)', '\1\2\2') escape chr(92) then 1 else 0 end end = 1) then 0 else case when "__atk4_reuse_left__" is not null and "__atk4_reuse_right__" is not null then 1 end end from (select sum("a") "__atk4_reuse_left__", sum("b") "__atk4_reuse_right__" from DUAL) "__atk4_reuse_tmp__") = 1 + where not (select case when not(case when "__atk4_reuse_left__" is null or "__atk4_reuse_right__" is null then null when "__atk4_reuse_left__" like 'BBB________%' or "__atk4_reuse_right__" like 'BBB________%' then case when regexp_like(case when "__atk4_reuse_left__" like 'BBB________%' then to_char(substr("__atk4_reuse_left__", 38)) else rawtohex(utl_raw.cast_to_raw("__atk4_reuse_left__")) end, concat('^', concat(replace(replace(replace(replace(replace(replace(replace(replace(replace(case when "__atk4_reuse_right__" like 'BBB________%' then to_char(substr("__atk4_reuse_right__", 38)) else rawtohex(utl_raw.cast_to_raw("__atk4_reuse_right__")) end, '5c5c', 'x'), '5c5f', 'y'), '5c25', 'z'), '5c', 'x'), '5f', '..'), '25', '(..)*'), 'x', '5c'), 'y', '5f'), 'z', '25'), '$')), 'in') then 1 else 0 end else case when "__atk4_reuse_left__" like regexp_replace("__atk4_reuse_right__", '(\\[\\_%])|(\\)', '\1\2\2') escape chr(92) then 1 else 0 end end = 1) then 0 else case when "__atk4_reuse_left__" is not null and "__atk4_reuse_right__" is not null then 1 end end from (select sum("a") "__atk4_reuse_left__", sum("b") "__atk4_reuse_right__" from DUAL) "__atk4_reuse_tmp__") = 1 EOF, - str_replace($binaryPrefix, 'BBB', (new OracleQuery('[where]'))->where($this->e('sum({})', ['a']), 'like', $this->e('sum({})', ['b']))->render()[0]) + str_replace($binaryPrefix, 'BBB', (new OracleQuery('[where]'))->where($this->e('sum({})', ['a']), 'not like', $this->e('sum({})', ['b']))->render()[0]) ); } @@ -975,12 +975,12 @@ public function testWhereRegexp(): void self::assertSame( version_compare(SqliteConnection::getDriverVersion(), '3.45') < 0 ? <<<'EOF' - where regexp_like(sum("a"), sum("b"), case when (select __atk4_case_v__ = 'a' from (select sum("a") __atk4_case_v__ where 0 union all select 'A') __atk4_case_tmp__) then 'is' else '-us' end) + where not regexp_like(sum("a"), sum("b"), case when (select __atk4_case_v__ = 'a' from (select sum("a") __atk4_case_v__ where 0 union all select 'A') __atk4_case_tmp__) then 'is' else '-us' end) EOF : <<<'EOF' - where (select regexp_like(`__atk4_reuse_left__`, sum("b"), case when (select __atk4_case_v__ = 'a' from (select `__atk4_reuse_left__` __atk4_case_v__ where 0 union all select 'A') __atk4_case_tmp__) then 'is' else '-us' end) from (select sum("a") `__atk4_reuse_left__`) `__atk4_reuse_tmp__`) + where not (select regexp_like(`__atk4_reuse_left__`, sum("b"), case when (select __atk4_case_v__ = 'a' from (select `__atk4_reuse_left__` __atk4_case_v__ where 0 union all select 'A') __atk4_case_tmp__) then 'is' else '-us' end) from (select sum("a") `__atk4_reuse_left__`) `__atk4_reuse_tmp__`) EOF, - (new SqliteQuery('[where]'))->where($this->e('sum({})', ['a']), 'regexp', $this->e('sum({})', ['b']))->render()[0] + (new SqliteQuery('[where]'))->where($this->e('sum({})', ['a']), 'not regexp', $this->e('sum({})', ['b']))->render()[0] ); foreach (['5.7.0', '8.0.0', 'MariaDB-11.0.0'] as $serverVersion) { @@ -992,23 +992,23 @@ public function testWhereRegexp(): void ); self::assertSame( $serverVersion === '5.7.0' - ? 'where sum("a") regexp concat(\'@?\', sum("b"))' - : 'where sum("a") regexp concat(\'(?s)\', sum("b"))', - $this->createMysqlQuery($serverVersion, '[where]')->where($this->e('sum({})', ['a']), 'regexp', $this->e('sum({})', ['b']))->render()[0] + ? 'where sum("a") not regexp concat(\'@?\', sum("b"))' + : 'where sum("a") not regexp concat(\'(?s)\', sum("b"))', + $this->createMysqlQuery($serverVersion, '[where]')->where($this->e('sum({})', ['a']), 'not regexp', $this->e('sum({})', ['b']))->render()[0] ); } self::assertSame( <<<'EOF' - where case when pg_typeof("name") = 'bytea'::regtype then replace(regexp_replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then replace(substring(cast("name" as text) from 3), chr(92), repeat(chr(92), 2)) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), '(?where('name', 'regexp', 'foo')->render()[0] ); self::assertSame( <<<'EOF' - where (select case when pg_typeof("__atk4_reuse_left__") = 'bytea'::regtype then replace(regexp_replace(encode(case when pg_typeof("__atk4_reuse_left__") = 'bytea'::regtype then decode(case when pg_typeof("__atk4_reuse_left__") = 'bytea'::regtype then replace(substring(cast("__atk4_reuse_left__" as text) from 3), chr(92), repeat(chr(92), 2)) else '' end, 'hex') else cast(replace(cast("__atk4_reuse_left__" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), '(?where($this->e('sum({})', ['a']), 'regexp', $this->e('sum({})', ['b']))->render()[0] + (new PostgresqlQuery('[where]'))->where($this->e('sum({})', ['a']), 'not regexp', $this->e('sum({})', ['b']))->render()[0] ); // TODO test MssqlQuery here once REGEXP is supported https://devblogs.microsoft.com/azure-sql/introducing-regular-expression-regex-support-in-azure-sql-db/ @@ -1018,8 +1018,8 @@ public function testWhereRegexp(): void (new OracleQuery('[where]'))->where('name', 'regexp', 'foo')->render()[0] ); self::assertSame( - 'where regexp_like(sum("a"), sum("b"), \'in\')', - (new OracleQuery('[where]'))->where($this->e('sum({})', ['a']), 'regexp', $this->e('sum({})', ['b']))->render()[0] + 'where not regexp_like(sum("a"), sum("b"), \'in\')', + (new OracleQuery('[where]'))->where($this->e('sum({})', ['a']), 'not regexp', $this->e('sum({})', ['b']))->render()[0] ); } diff --git a/tests/RandomTest.php b/tests/RandomTest.php index 6ab3c9062..96a465af1 100644 --- a/tests/RandomTest.php +++ b/tests/RandomTest.php @@ -100,7 +100,7 @@ public function testTitleImport(): void $m = new Model($this->db, ['table' => 'user']); $m->addField('name'); - $m->addField('salary', ['default' => 10]); + $m->addField('salary', ['type' => 'integer', 'default' => 10]); $m->import([['name' => 'Peter'], ['name' => 'Steve', 'salary' => 30]]); $m->insert(['name' => 'Sue']); diff --git a/tests/Schema/TestCaseTest.php b/tests/Schema/TestCaseTest.php index 90077a2b6..0610adb21 100644 --- a/tests/Schema/TestCaseTest.php +++ b/tests/Schema/TestCaseTest.php @@ -17,6 +17,8 @@ public function testLogQuery(): void { $m = new Model($this->db, ['table' => 't']); $m->addField('name'); + $m->addField('file', ['type' => 'blob']); + $m->addField('json', ['type' => 'json']); $m->addField('int', ['type' => 'integer']); $m->addField('float', ['type' => 'float']); $m->addField('bool', ['type' => 'boolean']); @@ -30,7 +32,14 @@ public function testLogQuery(): void $this->debug = true; $m->atomic(static function () use ($m) { - $m->insert(['name' => 'Ewa', 'int' => 1, 'float' => 1, 'bool' => 1]); + $m->insert([ + 'name' => 'Ewa', + 'file' => 'x y', + 'json' => ['z'], + 'int' => 1, + 'float' => 1, + 'bool' => 1, + ]); }); self::assertSame(1, $m->loadAny()->getId()); @@ -65,22 +74,30 @@ public function testLogQuery(): void begin try insert into `t` ( - `name`, `int`, `float`, - `bool`, `null` + `name`, `file`, `json`, + `int`, `float`, `bool`, + `null` ) values - ('Ewa', 1, 1.0, 1, NULL); + ( + 'Ewa', 'x y', '["z"]', + 1, 1.0, 1, NULL + ); end try begin catch if ERROR_NUMBER() = 544 begin set IDENTITY_INSERT `t` on; begin try insert into `t` ( - `name`, `int`, `float`, - `bool`, `null` + `name`, `file`, `json`, + `int`, `float`, `bool`, + `null` ) values - ('Ewa', 1, 1.0, 1, NULL); + ( + 'Ewa', 'x y', '["z"]', + 1, 1.0, 1, NULL + ); set IDENTITY_INSERT `t` off; end try begin @@ -102,14 +119,26 @@ public function testLogQuery(): void . <<<'EOF' insert into `t` ( - `name`, `int`, `float`, - `bool`, `null` + `name`, `file`, `json`, + `int`, `float`, `bool`, + `null` ) values + ( + EOF - . "\n ('Ewa', 1, 1.0, " - . ($this->getDatabasePlatform() instanceof PostgreSQLPlatform ? 'true' : '1') - . ", NULL);\n\n" + . ($this->getDatabasePlatform() instanceof PostgreSQLPlatform ? <<<'EOF' + 'Ewa', + 'x y', + cast('["z"]' as json), + 1, + 1.0, + true, + NULL + EOF : " 'Ewa', '" . ($this->getDatabasePlatform() instanceof OraclePlatform + ? "atk4_binary\ru5f8mzx4vsm8g2c9\r287e8d9e78202079" + : 'x y') . "', '[\"z\"]',\n 1, 1.0, 1, NULL") + . "\n );\n\n" . ($this->getDatabasePlatform() instanceof PostgreSQLPlatform ? "\n\"RELEASE SAVEPOINT\";\n\n" : '')) . ($this->getDatabasePlatform() instanceof OraclePlatform ? <<<'EOF' @@ -125,6 +154,8 @@ public function testLogQuery(): void select `id`, `name`, + `file`, + `json`, `int`, `float`, `bool`, @@ -146,6 +177,8 @@ public function testLogQuery(): void select `id`, `name`, + `file`, + `json`, `int`, `float`, `bool`, @@ -158,7 +191,7 @@ public function testLogQuery(): void . $makeLimitSqlFx(1) . ";\n\n", $this->getDatabasePlatform() instanceof SQLServerPlatform - ? str_replace('(\'Ewa\', 1, 1.0, 1, NULL)', '(N\'Ewa\', 1, 1.0, 1, NULL)', $output) + ? str_replace('\'Ewa\', \'x y\', \'["z"]\'', 'N\'Ewa\', N\'x y\', N\'["z"]\'', $output) : $output ); } diff --git a/tests/SerializeTest.php b/tests/SerializeTest.php index e8622b373..37a3e9e76 100644 --- a/tests/SerializeTest.php +++ b/tests/SerializeTest.php @@ -7,6 +7,7 @@ use Atk4\Data\Exception; use Atk4\Data\Model; use Atk4\Data\Schema\TestCase; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; class SerializeTest extends TestCase { @@ -32,7 +33,7 @@ public function testBasicSerialize(): void $m->getField('data')->type = 'json'; self::assertSame( - ['data' => '{"foo":"bar"}'], + ['data' => ($this->getDatabasePlatform() instanceof PostgreSQLPlatform ? "atk4_json\ru5f8mzx4vsm8g2c9\rfe2bf5ef" : '') . '{"foo":"bar"}'], $this->db->typecastSaveRow( $m, ['data' => ['foo' => 'bar']]