diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index fb5b1fc96a1..6afa680e9f0 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -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 diff --git a/framework/db/JsonExpressionBuilder.php b/framework/db/JsonExpressionBuilder.php new file mode 100644 index 00000000000..51fb60208ef --- /dev/null +++ b/framework/db/JsonExpressionBuilder.php @@ -0,0 +1,39 @@ +getValue(); + + if ($value instanceof Query) { + list($sql, $params) = $this->queryBuilder->build($value, $params); + return "($sql)"; + } + + return $this->queryBuilder->bindParam(Json::encode($value), $params); + } +} diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index e0b87441ae7..123563e50f1 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -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', ]; } @@ -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; } diff --git a/framework/db/oci/QueryBuilder.php b/framework/db/oci/QueryBuilder.php index b1d4d4d155a..cf41517ef1f 100644 --- a/framework/db/oci/QueryBuilder.php +++ b/framework/db/oci/QueryBuilder.php @@ -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; @@ -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; } diff --git a/framework/db/pgsql/QueryBuilder.php b/framework/db/pgsql/QueryBuilder.php index aef24f8e318..44d94879b6d 100644 --- a/framework/db/pgsql/QueryBuilder.php +++ b/framework/db/pgsql/QueryBuilder.php @@ -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; @@ -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; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 832e4080aa2..d8ccbf5f22c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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 diff --git a/tests/framework/db/QueryBuilderTest.php b/tests/framework/db/QueryBuilderTest.php index 7556987e907..97aa6648f14 100644 --- a/tests/framework/db/QueryBuilderTest.php +++ b/tests/framework/db/QueryBuilderTest.php @@ -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; @@ -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 [ diff --git a/tests/framework/db/oci/QueryBuilderTest.php b/tests/framework/db/oci/QueryBuilderTest.php index 6cb03e53044..f848eda7c57 100644 --- a/tests/framework/db/oci/QueryBuilderTest.php +++ b/tests/framework/db/oci/QueryBuilderTest.php @@ -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 */