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

Refactor DMLQueryBuilder #233

Merged
merged 14 commits into from
Nov 1, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 1.1.1 under development

- Bug #233: Refactor `DMLQueryBuilder`, related with yiisoft/db#746 (@Tigrov)
- Enh #230: Improve column type #230 (@Tigrov)
- Bug #240: Remove `RECURSIVE` expression from CTE queries (@Tigrov)

Expand Down
3 changes: 3 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<MixedAssignment errorLevel="suppress" />
</issueHandlers>
</psalm>
110 changes: 40 additions & 70 deletions src/DMLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Yiisoft\Db\Oracle;

use Generator;
use JsonException;
use Yiisoft\Db\Exception\Exception;
use Yiisoft\Db\Exception\InvalidArgumentException;
Expand All @@ -15,54 +14,47 @@
use Yiisoft\Db\Query\QueryInterface;
use Yiisoft\Db\QueryBuilder\AbstractDMLQueryBuilder;

use function array_map;
use function implode;
use function ltrim;
use function strrpos;
use function count;
use function reset;

/**
* Implements a DML (Data Manipulation Language) SQL statements for Oracle Server.
*/
final class DMLQueryBuilder extends AbstractDMLQueryBuilder
{
/**
* @psalm-suppress MixedArrayOffset
*
* @throws Exception
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws NotSupportedException
*/
public function batchInsert(string $table, array $columns, iterable|Generator $rows, array &$params = []): string
public function batchInsert(string $table, array $columns, iterable $rows, array &$params = []): string
{
if (empty($rows)) {
return '';
}

if (($tableSchema = $this->schema->getTableSchema($table)) !== null) {
$columnSchemas = $tableSchema->getColumns();
} else {
$columnSchemas = [];
}

$mappedNames = $this->getNormalizeColumnNames($table, $columns);
$values = [];
$columns = $this->getNormalizeColumnNames('', $columns);
$columnSchemas = $this->schema->getTableSchema($table)?->getColumns() ?? [];

/** @psalm-var array<array-key, array<array-key, string>> $rows */
foreach ($rows as $row) {
$i = 0;
$placeholders = [];
foreach ($row as $index => $value) {
if (isset($columns[$index], $mappedNames[$columns[$index]], $columnSchemas[$mappedNames[$columns[$index]]])) {
/** @var mixed $value */
$value = $this->getTypecastValue($value, $columnSchemas[$mappedNames[$columns[$index]]]);

foreach ($row as $value) {
if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->dbTypecast($value);
}

if ($value instanceof ExpressionInterface) {
$placeholders[] = $this->queryBuilder->buildExpression($value, $params);
} else {
$placeholders[] = $this->queryBuilder->bindParam($value, $params);
}

++$i;
}
$values[] = '(' . implode(', ', $placeholders) . ')';
}
Expand All @@ -71,9 +63,10 @@ public function batchInsert(string $table, array $columns, iterable|Generator $r
return '';
}

foreach ($columns as $i => $name) {
$columns[$i] = $this->quoter->quoteColumnName($mappedNames[$name]);
}
$columns = array_map(
[$this->quoter, 'quoteColumnName'],
$columns,
);

$tableAndColumns = ' INTO ' . $this->quoter->quoteTableName($table)
. ' (' . implode(', ', $columns) . ') VALUES ';
Expand Down Expand Up @@ -105,7 +98,6 @@ public function upsert(
array|bool $updateColumns,
array &$params = []
): string {
$usingValues = null;
$constraints = [];

[$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
Expand All @@ -119,16 +111,11 @@ public function upsert(
return $this->insert($table, $insertColumns, $params);
}

if ($updateNames === []) {
/** there are no columns to update */
$updateColumns = false;
}

$onCondition = ['or'];
$quotedTableName = $this->quoter->quoteTableName($table);

foreach ($constraints as $constraint) {
$columnNames = $constraint->getColumnNames() ?? [];
$columnNames = (array) $constraint->getColumnNames();
$constraintCondition = ['and'];
/** @psalm-var string[] $columnNames */
foreach ($columnNames as $name) {
Expand All @@ -140,89 +127,72 @@ public function upsert(
}

$on = $this->queryBuilder->buildCondition($onCondition, $params);
/** @psalm-var string[] $placeholders */

[, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);

if (!empty($placeholders)) {
$usingSelectValues = [];
/** @psalm-var string[] $insertNames */

foreach ($insertNames as $index => $name) {
$usingSelectValues[$name] = new Expression($placeholders[$index]);
}

/** @psalm-var array $params */
$usingValues = $this->queryBuilder->buildSelect($usingSelectValues, $params) . ' ' . $this->queryBuilder->buildFrom(['DUAL'], $params);
$values = $this->queryBuilder->buildSelect($usingSelectValues, $params)
. ' ' . $this->queryBuilder->buildFrom(['DUAL'], $params);
}

$insertValues = [];
$mergeSql = 'MERGE INTO '
. $this->quoter->quoteTableName($table)
. ' '
. 'USING (' . ($usingValues ?? ltrim((string) $values, ' '))
. ') "EXCLUDED" '
. "ON ($on)";

/** @psalm-var string[] $insertNames */
foreach ($insertNames as $name) {
$quotedName = $this->quoter->quoteColumnName($name);

if (strrpos($quotedName, '.') === false) {
$quotedName = '"EXCLUDED".' . $quotedName;
}
$mergeSql = 'MERGE INTO ' . $quotedTableName . ' USING (' . $values . ') "EXCLUDED" ON (' . $on . ')';

$insertValues[] = $quotedName;
foreach ($insertNames as $quotedName) {
$insertValues[] = '"EXCLUDED".' . $quotedName;
}

$insertSql = 'INSERT (' . implode(', ', $insertNames) . ')' . ' VALUES (' . implode(', ', $insertValues) . ')';

if ($updateColumns === false) {
if ($updateColumns === false || $updateNames === []) {
vjik marked this conversation as resolved.
Show resolved Hide resolved
/** there are no columns to update */
return "$mergeSql WHEN NOT MATCHED THEN $insertSql";
}

if ($updateColumns === true) {
$updateColumns = [];
/** @psalm-var string[] $updateNames */
foreach ($updateNames as $name) {
$quotedName = $this->quoter->quoteColumnName($name);

if (strrpos($quotedName, '.') === false) {
$quotedName = '"EXCLUDED".' . $quotedName;
}
$updateColumns[$name] = new Expression($quotedName);
foreach ($updateNames as $quotedName) {
$updateColumns[$quotedName] = new Expression('"EXCLUDED".' . $quotedName);
}
}

/** @psalm-var string[] $updates */
[$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, (array) $params);
[$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
$updateSql = 'UPDATE SET ' . implode(', ', $updates);

return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql";
}

protected function prepareInsertValues(string $table, array|QueryInterface $columns, array $params = []): array
{
/**
* @var array $names
* @var array $placeholders
*/
[$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);

if (!$columns instanceof QueryInterface && empty($names)) {
if (empty($columns)) {
$names = [];
$placeholders = [];
$tableSchema = $this->schema->getTableSchema($table);

if ($tableSchema !== null) {
$tableColumns = $tableSchema->getColumns();
$columns = !empty($tableSchema->getPrimaryKey())
? $tableSchema->getPrimaryKey() : [reset($tableColumns)->getName()];
if (!empty($tableSchema->getPrimaryKey())) {
$columns = $tableSchema->getPrimaryKey();
} else {
$columns = [current($tableSchema->getColumns())->getName()];
}

foreach ($columns as $name) {
/** @psalm-var mixed */
$names[] = $this->quoter->quoteColumnName($name);
$placeholders[] = 'DEFAULT';
}
}

return [$names, $placeholders, '', $params];
}

return [$names, $placeholders, $values, $params];
return parent::prepareInsertValues($table, $columns, $params);
}

public function resetSequence(string $table, int|string $value = null): string
Expand Down
7 changes: 5 additions & 2 deletions tests/Provider/CommandProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ public static function batchInsert(): array
DbHelper::changeSqlForOracleBatchInsert($batchInsert['issue11242']['expected']);
$batchInsert['issue11242']['expectedParams'][':qp3'] = '1';

DbHelper::changeSqlForOracleBatchInsert($batchInsert['wrongBehavior']['expected']);
$batchInsert['wrongBehavior']['expectedParams'][':qp3'] = '0';
DbHelper::changeSqlForOracleBatchInsert($batchInsert['table name with column name with brackets']['expected']);
$batchInsert['table name with column name with brackets']['expectedParams'][':qp3'] = '0';

DbHelper::changeSqlForOracleBatchInsert($batchInsert['batchInsert binds params from expression']['expected']);
$batchInsert['batchInsert binds params from expression']['expectedParams'][':qp3'] = '0';

DbHelper::changeSqlForOracleBatchInsert($batchInsert['with associative values']['expected']);
$batchInsert['with associative values']['expectedParams'][':qp3'] = '1';

return $batchInsert;
}

Expand Down
6 changes: 6 additions & 0 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public static function batchInsert(): array
SQL;

DbHelper::changeSqlForOracleBatchInsert($batchInsert['bool-false, time-now()']['expected']);
DbHelper::changeSqlForOracleBatchInsert($batchInsert['column table names are not checked']['expected']);

return $batchInsert;
}
Expand Down Expand Up @@ -124,6 +125,11 @@ public static function upsert(): array
MERGE INTO "T_upsert" USING (SELECT :qp0 AS "email", :qp1 AS "address", :qp2 AS "status", :qp3 AS "profile_id" FROM "DUAL") "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"="EXCLUDED"."address", "status"="EXCLUDED"."status", "profile_id"="EXCLUDED"."profile_id" WHEN NOT MATCHED THEN INSERT ("email", "address", "status", "profile_id") VALUES ("EXCLUDED"."email", "EXCLUDED"."address", "EXCLUDED"."status", "EXCLUDED"."profile_id")
SQL,
],
'regular values with unique at not the first position' => [
3 => <<<SQL
MERGE INTO "T_upsert" USING (SELECT :qp0 AS "address", :qp1 AS "email", :qp2 AS "status", :qp3 AS "profile_id" FROM "DUAL") "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN MATCHED THEN UPDATE SET "address"="EXCLUDED"."address", "status"="EXCLUDED"."status", "profile_id"="EXCLUDED"."profile_id" WHEN NOT MATCHED THEN INSERT ("address", "email", "status", "profile_id") VALUES ("EXCLUDED"."address", "EXCLUDED"."email", "EXCLUDED"."status", "EXCLUDED"."profile_id")
SQL,
],
'regular values with update part' => [
2 => ['address' => 'foo {{city}}', 'status' => 2, 'orders' => new Expression('"T_upsert"."orders" + 1')],
3 => <<<SQL
Expand Down
15 changes: 13 additions & 2 deletions tests/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Yiisoft\Db\Oracle\Tests;

use Generator;
use Throwable;
use Yiisoft\Db\Exception\Exception;
use Yiisoft\Db\Exception\InvalidArgumentException;
Expand Down Expand Up @@ -125,7 +124,7 @@ public function testAlterColumn(): void
* @throws InvalidArgumentException
* @throws NotSupportedException
*/
public function testBatchInsert(string $table, array $columns, iterable|Generator $rows, string $expected): void
public function testBatchInsert(string $table, array $columns, iterable $rows, string $expected): void
{
parent::testBatchInsert($table, $columns, $rows, $expected);
}
Expand Down Expand Up @@ -633,4 +632,16 @@ public function testUpsertExecute(
): void {
parent::testUpsertExecute($table, $insertColumns, $updateColumns);
}

public function testDefaultValues(): void
{
$db = $this->getConnection();
$queryBuilder = $db->getQueryBuilder();

// Non-primary key columns should have DEFAULT as value
$this->assertSame(
'INSERT INTO "negative_default_values" ("tinyint_col") VALUES (DEFAULT)',
$queryBuilder->insert('negative_default_values', []),
);
}
}
Loading