diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 48630a5dcc7..0a25c23a24c 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -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 ------------------------- diff --git a/framework/db/mysql/JsonExpressionBuilder.php b/framework/db/mysql/JsonExpressionBuilder.php index fe7fc8b7869..cb2b35c83b2 100644 --- a/framework/db/mysql/JsonExpressionBuilder.php +++ b/framework/db/mysql/JsonExpressionBuilder.php @@ -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; } } diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php index 7a60c620f21..e9678392f67 100644 --- a/framework/db/mysql/Schema.php +++ b/framework/db/mysql/Schema.php @@ -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) { @@ -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; + } } diff --git a/tests/framework/db/mysql/QueryBuilderTest.php b/tests/framework/db/mysql/QueryBuilderTest.php index f0f8cdfb397..7097b8d2b61 100644 --- a/tests/framework/db/mysql/QueryBuilderTest.php +++ b/tests/framework/db/mysql/QueryBuilderTest.php @@ -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]))], @@ -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'], @@ -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, diff --git a/tests/framework/db/mysql/type/JsonTest.php b/tests/framework/db/mysql/type/JsonTest.php new file mode 100644 index 00000000000..b955c7221cd --- /dev/null +++ b/tests/framework/db/mysql/type/JsonTest.php @@ -0,0 +1,85 @@ +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( + <<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( + <<queryScalar(), + ); + } +}