Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support JSON columns for MariaDB 10.4 or higher. #20171

Merged
merged 3 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(),
);
}
}
Loading