Skip to content
Open
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 @@ -23,6 +23,7 @@ Yii Framework 2 Change Log
- Bug #20750: Fix `@return` annotation for `yii\console\Controller::runAction()` (mspirkov)
- Bug #20750: Add the missing `@property-write` annotation to `yii\console\Controller` (mspirkov)
- Bug #20751: Fix `@param` annotation for `$param` parameter in `Sort::parseSortParam()` (mspirkov)
- Bug #20768: Fix `batchInsert()` crash on array values for JSON columns when table schema is unavailable (WarLikeLaux)


2.0.54 January 09, 2026
Expand Down
39 changes: 39 additions & 0 deletions framework/db/JsonExpressionBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/

namespace yii\db;

use yii\helpers\Json;

/**
* Class JsonExpressionBuilder builds [[JsonExpression]] for DBMS that don't provide
* a vendor-specific solution.
*
* @author WarLikeLaux
* @since 2.0.55
*/
class JsonExpressionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;

/**
* {@inheritdoc}
* @param JsonExpression|ExpressionInterface $expression the expression to be built
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
$value = $expression->getValue();

if ($value instanceof Query) {
list($sql, $params) = $this->queryBuilder->build($value, $params);
return "($sql)";
}

return $this->queryBuilder->bindParam(Json::encode($value), $params);
}
}
3 changes: 3 additions & 0 deletions framework/db/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ protected function defaultExpressionBuilders()
'yii\db\conditions\SimpleCondition' => 'yii\db\conditions\SimpleConditionBuilder',
'yii\db\conditions\HashCondition' => 'yii\db\conditions\HashConditionBuilder',
'yii\db\conditions\BetweenColumnsCondition' => 'yii\db\conditions\BetweenColumnsConditionBuilder',
'yii\db\JsonExpression' => 'yii\db\JsonExpressionBuilder',
];
}

Expand Down Expand Up @@ -484,6 +485,8 @@ public function batchInsert($table, $columns, $rows, &$params = [])
$value = 'NULL';
} elseif ($value instanceof ExpressionInterface) {
$value = $this->buildExpression($value, $params);
} elseif (is_array($value)) {
$value = $this->buildExpression(new JsonExpression($value), $params);
}
$vs[] = $value;
}
Expand Down
3 changes: 3 additions & 0 deletions framework/db/oci/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use yii\db\Connection;
use yii\db\Exception;
use yii\db\Expression;
use yii\db\JsonExpression;
use yii\db\Query;
use yii\helpers\StringHelper;
use yii\db\ExpressionInterface;
Expand Down Expand Up @@ -327,6 +328,8 @@ public function batchInsert($table, $columns, $rows, &$params = [])
$value = 'NULL';
} elseif ($value instanceof ExpressionInterface) {
$value = $this->buildExpression($value, $params);
} elseif (is_array($value)) {
$value = $this->buildExpression(new JsonExpression($value), $params);
}
$vs[] = $value;
}
Expand Down
3 changes: 3 additions & 0 deletions framework/db/pgsql/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use yii\base\InvalidArgumentException;
use yii\db\Expression;
use yii\db\ExpressionInterface;
use yii\db\JsonExpression;
use yii\db\Query;
use yii\db\PdoValue;
use yii\helpers\StringHelper;
Expand Down Expand Up @@ -521,6 +522,8 @@ public function batchInsert($table, $columns, $rows, &$params = [])
$value = 'NULL';
} elseif ($value instanceof ExpressionInterface) {
$value = $this->buildExpression($value, $params);
} elseif (is_array($value)) {
$value = $this->buildExpression(new JsonExpression($value), $params);
}
$vs[] = $value;
}
Expand Down
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ parameters:
count: 1
path: framework/grid/DataColumn.php

-
message: "#^Call to an undefined method yii\\\\db\\\\ExpressionInterface\\:\\:getValue\\(\\)\\.$#"
count: 1
path: framework/db/JsonExpressionBuilder.php

-
message: "#^Call to an undefined method yii\\\\db\\\\ExpressionInterface\\:\\:getValue\\(\\)\\.$#"
count: 1
Expand Down
40 changes: 40 additions & 0 deletions tests/framework/db/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use yii\db\conditions\InCondition;
use yii\db\cubrid\QueryBuilder as CubridQueryBuilder;
use yii\db\Expression;
use yii\db\JsonExpression;
use yii\db\mssql\QueryBuilder as MssqlQueryBuilder;
use yii\db\mysql\QueryBuilder as MysqlQueryBuilder;
use yii\db\oci\QueryBuilder as OracleQueryBuilder;
Expand Down Expand Up @@ -2340,6 +2341,45 @@ public function testBatchInsert($table, $columns, $value, $expected, $replaceQuo
$this->assertEquals($expected, $sql);
}

public function testBatchInsertWithArrayValue(): void
{
$queryBuilder = $this->getQueryBuilder();

$params = [];
$sql = $queryBuilder->batchInsert(
'no_such_table',
['json_col'],
[[['key' => 'value', 'num' => 42]]],
$params
);

$expected = $this->replaceQuotes(
'INSERT INTO [[no_such_table]] ([[json_col]]) VALUES (:qp0)'
);
$this->assertSame($expected, $sql);
$this->assertSame([':qp0' => '{"key":"value","num":42}'], $params);
}

/**
* @see https://github.com/yiisoft/yii2/issues/20683
*/
public function testBatchInsertWithJsonExpressionContainingQuery(): void
{
$queryBuilder = $this->getQueryBuilder();

$params = [];
$query = (new Query())->select('data')->from('source');
$sql = $queryBuilder->batchInsert(
'no_such_table',
['json_col'],
[[new JsonExpression($query)]],
$params
);

$this->assertStringContainsString('SELECT', $sql);
$this->assertStringContainsString('source', $sql);
}

public static function updateProvider(): array
{
return [
Expand Down
17 changes: 17 additions & 0 deletions tests/framework/db/oci/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,23 @@ public static function batchInsertProvider(): array
return $data;
}

public function testBatchInsertWithArrayValue(): void
{
$queryBuilder = $this->getQueryBuilder();

$params = [];
$sql = $queryBuilder->batchInsert(
'no_such_table',
['json_col'],
[[['key' => 'value', 'num' => 42]]],
$params
);

$expected = 'INSERT ALL INTO "no_such_table" ("json_col") VALUES (:qp0) SELECT 1 FROM SYS.DUAL';
$this->assertSame($expected, $sql);
$this->assertSame([':qp0' => '{"key":"value","num":42}'], $params);
}

/**
* Dummy test to speed up QB's tests which rely on DB schema
*/
Expand Down
Loading