Skip to content

Commit

Permalink
Fix: #20171: Support JSON columns for MariaDB 10.4 or higher
Browse files Browse the repository at this point in the history
  • Loading branch information
terabytesoftw authored May 29, 2024
1 parent c6145cb commit 5ebc175
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 11 deletions.
1 change: 1 addition & 0 deletions framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Yii Framework 2 Change Log
- Bug #20141: Update `ezyang/htmlpurifier` dependency to version `4.17` (@terabytesoftw)
- Bug #19817: Add MySQL Query `addCheck()` and `dropCheck()` (@bobonov)
- Bug #20165: Adjust pretty name of closures for PHP 8.4 compatibility (@staabm)
- Enh: #20171: Support JSON columns for MariaDB 10.4 or higher (@terabytesoftw)

2.0.49.2 October 12, 2023
-------------------------
Expand Down
2 changes: 1 addition & 1 deletion framework/db/mysql/JsonExpressionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ public function build(ExpressionInterface $expression, array &$params = [])
$placeholder = static::PARAM_PREFIX . count($params);
$params[$placeholder] = Json::encode($value);

return "CAST($placeholder AS JSON)";
return $placeholder;
}
}
25 changes: 25 additions & 0 deletions framework/db/mysql/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -380,10 +380,19 @@ protected function findColumns($table)
}
throw $e;
}


$jsonColumns = $this->getJsonColumns($table);

foreach ($columns as $info) {
if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_LOWER) {
$info = array_change_key_case($info, CASE_LOWER);
}

if (\in_array($info['field'], $jsonColumns, true)) {
$info['type'] = static::TYPE_JSON;
}

$column = $this->loadColumnSchema($info);
$table->columns[$column->name] = $column;
if ($column->isPrimaryKey) {
Expand Down Expand Up @@ -641,4 +650,20 @@ private function loadTableConstraints($tableName, $returnType)

return $result[$returnType];
}

private function getJsonColumns(TableSchema $table): array
{
$sql = $this->getCreateTableSql($table);
$result = [];

$regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi';

if (\preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$result[] = $match[1];
}
}

return $result;
}
}
20 changes: 10 additions & 10 deletions tests/framework/db/mysql/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,35 +267,35 @@ public function conditionProvider()
// json conditions
[
['=', 'jsoncol', new JsonExpression(['lang' => 'uk', 'country' => 'UA'])],
'[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '{"lang":"uk","country":"UA"}'],
'[[jsoncol]] = :qp0', [':qp0' => '{"lang":"uk","country":"UA"}'],
],
[
['=', 'jsoncol', new JsonExpression([false])],
'[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '[false]']
'[[jsoncol]] = :qp0', [':qp0' => '[false]']
],
'object with type. Type is ignored for MySQL' => [
['=', 'prices', new JsonExpression(['seeds' => 15, 'apples' => 25], 'jsonb')],
'[[prices]] = CAST(:qp0 AS JSON)', [':qp0' => '{"seeds":15,"apples":25}'],
'[[prices]] = :qp0', [':qp0' => '{"seeds":15,"apples":25}'],
],
'nested json' => [
['=', 'data', new JsonExpression(['user' => ['login' => 'silverfire', 'password' => 'c4ny0ur34d17?'], 'props' => ['mood' => 'good']])],
'[[data]] = CAST(:qp0 AS JSON)', [':qp0' => '{"user":{"login":"silverfire","password":"c4ny0ur34d17?"},"props":{"mood":"good"}}']
'[[data]] = :qp0', [':qp0' => '{"user":{"login":"silverfire","password":"c4ny0ur34d17?"},"props":{"mood":"good"}}']
],
'null value' => [
['=', 'jsoncol', new JsonExpression(null)],
'[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => 'null']
'[[jsoncol]] = :qp0', [':qp0' => 'null']
],
'null as array value' => [
['=', 'jsoncol', new JsonExpression([null])],
'[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '[null]']
'[[jsoncol]] = :qp0', [':qp0' => '[null]']
],
'null as object value' => [
['=', 'jsoncol', new JsonExpression(['nil' => null])],
'[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '{"nil":null}']
'[[jsoncol]] = :qp0', [':qp0' => '{"nil":null}']
],
'with object as value' => [
['=', 'jsoncol', new JsonExpression(new DynamicModel(['a' => 1, 'b' => 2]))],
'[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '{"a":1,"b":2}']
'[[jsoncol]] = :qp0', [':qp0' => '{"a":1,"b":2}']
],
'query' => [
['=', 'jsoncol', new JsonExpression((new Query())->select('params')->from('user')->where(['id' => 1]))],
Expand All @@ -307,7 +307,7 @@ public function conditionProvider()
],
'nested and combined json expression' => [
['=', 'jsoncol', new JsonExpression(new JsonExpression(['a' => 1, 'b' => 2, 'd' => new JsonExpression(['e' => 3])]))],
"[[jsoncol]] = CAST(:qp0 AS JSON)", [':qp0' => '{"a":1,"b":2,"d":{"e":3}}']
"[[jsoncol]] = :qp0", [':qp0' => '{"a":1,"b":2,"d":{"e":3}}']
],
'search by property in JSON column (issue #15838)' => [
['=', new Expression("(jsoncol->>'$.someKey')"), '42'],
Expand All @@ -328,7 +328,7 @@ public function updateProvider()
[
'id' => 1,
],
$this->replaceQuotes('UPDATE [[profile]] SET [[description]]=CAST(:qp0 AS JSON) WHERE [[id]]=:qp1'),
$this->replaceQuotes('UPDATE [[profile]] SET [[description]]=:qp0 WHERE [[id]]=:qp1'),
[
':qp0' => '{"abc":"def","0":123,"1":null}',
':qp1' => 1,
Expand Down
85 changes: 85 additions & 0 deletions tests/framework/db/mysql/type/JsonTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/

namespace yiiunit\framework\db\mysql\type;

use yii\db\JsonExpression;
use yii\db\mysql\Schema;
use yiiunit\framework\db\DatabaseTestCase;

/**
* @group db
* @group mysql
*/
class JsonTest extends DatabaseTestCase
{
protected $driverName = 'mysql';

public function testCreateTable(): void
{
$db = $this->getConnection();

if ($db->getSchema()->getTableSchema('json') !== null) {
$db->createCommand()->dropTable('json')->execute();
}

$command = $db->createCommand();
$command->createTable('json', ['id' => Schema::TYPE_PK, 'data' => Schema::TYPE_JSON])->execute();

$this->assertTrue($db->getTableSchema('json') !== null);
$this->assertSame('data', $db->getTableSchema('json')->getColumn('data')->name);
$this->assertSame('json', $db->getTableSchema('json')->getColumn('data')->type);
}

public function testInsertAndSelect(): void
{
$db = $this->getConnection(true);
$version = $db->getServerVersion();

$command = $db->createCommand();
$command->insert('storage', ['data' => ['a' => 1, 'b' => 2]])->execute();

if (\stripos($version, 'MariaDb') === false) {
$rowExpected = '{"a": 1, "b": 2}';
} else {
$rowExpected = '{"a":1,"b":2}';
}

$this->assertSame(
$rowExpected,
$command->setSql(
<<<SQL
SELECT `data` FROM `storage`
SQL,
)->queryScalar(),
);
}

public function testInsertJsonExpressionAndSelect(): void
{
$db = $this->getConnection(true);
$version = $db->getServerVersion();

$command = $db->createCommand();
$command->insert('storage', ['data' => new JsonExpression(['a' => 1, 'b' => 2])])->execute();

if (\stripos($version, 'MariaDb') === false) {
$rowExpected = '{"a": 1, "b": 2}';
} else {
$rowExpected = '{"a":1,"b":2}';
}

$this->assertSame(
$rowExpected,
$command->setSql(
<<<SQL
SELECT `data` FROM `storage`
SQL,
)->queryScalar(),
);
}
}

0 comments on commit 5ebc175

Please sign in to comment.