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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 2.0.1 under development

- Chg #1142: Add `$constraintColumns` parameter to `upsert()`, `upsertReturning()`, and `upsertReturningPks()` methods
to allow explicit specification of columns for the `ON CONFLICT` clause. When `null` (default), the primary key or
the first matching unique constraint is used. This fixes invalid `ON CONFLICT` clause generation when a table has
multiple separate unique constraints (@dadansatria)
- Bug #1127: Fix `AbstractSchema::hasTable()` and `AbstractSchema::hasView()` methods to support names quoted with curly
brackets `{{%table}}` (@batyrmastyr)

Expand Down
11 changes: 7 additions & 4 deletions src/Command/AbstractCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -499,26 +499,28 @@ public function upsert(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
): static {
$params = [];
$sql = $this->getQueryBuilder()->upsert($table, $insertColumns, $updateColumns, $params);
$sql = $this->getQueryBuilder()->upsert($table, $insertColumns, $updateColumns, $constraintColumns, $params);
return $this->setSql($sql)->bindValues($params);
}

public function upsertReturning(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
?array $returnColumns = null,
): array {
if ($returnColumns === []) {
$this->upsert($table, $insertColumns, $updateColumns)->execute();
$this->upsert($table, $insertColumns, $updateColumns, $constraintColumns)->execute();
return [];
}

$params = [];
$sql = $this->getQueryBuilder()
->upsertReturning($table, $insertColumns, $updateColumns, $returnColumns, $params);
->upsertReturning($table, $insertColumns, $updateColumns, $constraintColumns, $returnColumns, $params);

$this->setSql($sql)->bindValues($params);

Expand All @@ -530,9 +532,10 @@ public function upsertReturningPks(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
): array {
$primaryKeys = $this->db->getSchema()->getTableSchema($table)?->getPrimaryKey() ?? [];
return $this->upsertReturning($table, $insertColumns, $updateColumns, $primaryKeys);
return $this->upsertReturning($table, $insertColumns, $updateColumns, $constraintColumns, $primaryKeys);
}

public function withDbTypecasting(bool $dbTypecasting = true): static
Expand Down
9 changes: 9 additions & 0 deletions src/Command/CommandInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,8 @@ public function update(
* @param array|bool $updateColumns The column data (name => value) to update if it already exists.
* If `true` is passed, the column data will be updated to match the insert column data.
* If `false` is passed, no update will be performed if the column data already exist.
* @param string[]|null $constraintColumns The column names to use for the `ON CONFLICT` clause. If `null`,
* the primary key or the first matching unique constraint will be used.
*
* @throws Exception
* @throws InvalidConfigException
Expand All @@ -910,6 +912,7 @@ public function upsert(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
): static;

/**
Expand All @@ -924,6 +927,8 @@ public function upsert(
* @param array|bool $updateColumns The column data (name => value) to update if it already exists.
* If `true` is passed, the column data will be updated to match the insert column data.
* If `false` is passed, no update will be performed if the column data already exist.
* @param string[]|null $constraintColumns The column names to use for the `ON CONFLICT` clause. If `null`,
* the primary key or the first matching unique constraint will be used.
* @param string[]|null $returnColumns The column names to return values from. `null` means all columns.
*
* @throws Exception
Expand All @@ -941,6 +946,7 @@ public function upsertReturning(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
?array $returnColumns = null,
): array;

Expand All @@ -956,6 +962,8 @@ public function upsertReturning(
* @param array|bool $updateColumns The column data (name => value) to update if it already exists.
* If `true` is passed, the column data will be updated to match the insert column data.
* If `false` is passed, no update will be performed if the column data already exist.
* @param string[]|null $constraintColumns The column names to use for the `ON CONFLICT` clause. If `null`,
* the primary key or the first matching unique constraint will be used.
*
* @throws Exception
* @throws InvalidConfigException
Expand All @@ -971,6 +979,7 @@ public function upsertReturningPks(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
): array;

/**
Expand Down
3 changes: 3 additions & 0 deletions src/Debug/CommandInterfaceProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ public function upsert(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
): static {
return new self($this->decorated->{__FUNCTION__}(...func_get_args()), $this->collector);
}
Expand All @@ -506,6 +507,7 @@ public function upsertReturning(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
?array $returnColumns = null,
): array {
/** @psalm-var array<string, mixed> */
Expand All @@ -516,6 +518,7 @@ public function upsertReturningPks(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
): array {
/** @var array */
return $this->decorated->{__FUNCTION__}(...func_get_args());
Expand Down
56 changes: 25 additions & 31 deletions src/QueryBuilder/AbstractDMLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,9 @@
use function array_combine;
use function array_diff;
use function array_fill_keys;
use function array_filter;
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function get_object_vars;
Expand Down Expand Up @@ -149,6 +146,7 @@ public function upsert(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
array &$params = [],
): string {
throw new NotSupportedException(__METHOD__ . ' is not supported by this DBMS.');
Expand All @@ -159,6 +157,7 @@ public function upsertReturning(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
?array $returnColumns = null,
array &$params = [],
): string {
Expand Down Expand Up @@ -474,6 +473,8 @@ protected function prepareUpsertSets(
* Prepare column names and constraints for "upsert" operation.
*
* @param Index[] $constraints
* @param string[]|null $constraintColumns The column names to use for the conflict clause. If `null`,
* the primary key or the first matching unique constraint will be used.
*
* @psalm-param array<string, mixed>|QueryInterface $insertColumns
*
Expand All @@ -485,14 +486,21 @@ protected function prepareUpsertColumns(
array|QueryInterface $insertColumns,
array|bool $updateColumns,
array &$constraints = [],
?array $constraintColumns = null,
): array {
if ($insertColumns instanceof QueryInterface) {
$insertNames = $this->getQueryColumnNames($insertColumns);
} else {
$insertNames = $this->getNormalizedColumnNames(array_keys($insertColumns));
}

$uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
// Use provided constraint columns or auto-detect from table schema
if ($constraintColumns !== null) {
$uniqueNames = $this->getNormalizedColumnNames($constraintColumns);
$constraints = [];
} else {
$uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
}

if ($updateColumns === true) {
return [$uniqueNames, $insertNames, array_diff($insertNames, $uniqueNames)];
Expand Down Expand Up @@ -537,43 +545,29 @@ protected function getNormalizedColumnNames(array $columns): array
}

/**
* Returns all column names belonging to constraints enforcing uniqueness (`PRIMARY KEY`, `UNIQUE INDEX`, etc.)
* for the named table removing constraints which didn't cover the specified column list.
*
* The column list will be unique by column names.
* Returns column names of the first constraint enforcing uniqueness (`PRIMARY KEY`, `UNIQUE INDEX`, etc.)
* for the named table that is covered by the specified column list.
*
* @param string $name The table name, may contain schema name if any. Don't quote the table name.
* @param string[] $columns Source column list.
* @param Index[] $indexes This parameter optionally receives a matched index list.
* The constraints will be unique by their column names.
* @param Index[] $indexes This parameter optionally receives the first matched index,
* or an empty array if no matching index is found.
*
* @return string[] The column names.
* @return string[] The column names of the first matching constraint.
*/
private function getTableUniqueColumnNames(string $name, array $columns, array &$indexes = []): array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private function getTableUniqueColumnNames(string $name, array $columns, array &$indexes = []): array
private function getTableUniqueColumnNames(string $name, array $columns, ?Index &$index = null): array

array can be changed to ?Index with corresponding fixes of code

{
$indexes = $this->schema->getTableUniques($name);
$columnNames = [];

// Remove all indexes which don't cover the specified column list.
$indexes = array_values(
array_filter(
$indexes,
static function (Index $index) use ($columns, &$columnNames): bool {
$result = empty(array_diff($index->columnNames, $columns));

if ($result) {
$columnNames[] = $index->columnNames;
}

return $result;
},
),
);

if (empty($columnNames)) {
return [];
// Find the first index that is fully covered by the specified column list.
foreach ($indexes as $index) {
if (empty(array_diff($index->columnNames, $columns))) {
$indexes = [$index];
return $index->columnNames;
}
}

return array_unique(array_merge(...$columnNames));
$indexes = [];
return [];
}
}
6 changes: 4 additions & 2 deletions src/QueryBuilder/AbstractQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -575,19 +575,21 @@ public function upsert(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
array &$params = [],
): string {
return $this->dmlBuilder->upsert($table, $insertColumns, $updateColumns, $params);
return $this->dmlBuilder->upsert($table, $insertColumns, $updateColumns, $constraintColumns, $params);
}

public function upsertReturning(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
?array $returnColumns = null,
array &$params = [],
): string {
return $this->dmlBuilder->upsertReturning($table, $insertColumns, $updateColumns, $returnColumns, $params);
return $this->dmlBuilder->upsertReturning($table, $insertColumns, $updateColumns, $constraintColumns, $returnColumns, $params);
}

public function withTypecasting(bool $typecasting = true): static
Expand Down
6 changes: 6 additions & 0 deletions src/QueryBuilder/DMLQueryBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ public function update(
* @param array|bool $updateColumns The column data (name => value) to update if they already exist. If `true`
* is passed, the column data will be updated to match the insert column data. If `false` is passed, no update will
* be performed if the column data already exist.
* @param string[]|null $constraintColumns The column names to use for the `ON CONFLICT` clause. If `null`,
* the primary key or the first matching unique constraint will be used.
* @param array $params The binding parameters that will be generated by this method. They should be bound to the DB
* command later.
*
Expand All @@ -243,6 +245,7 @@ public function upsert(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
array &$params = [],
): string;

Expand All @@ -258,6 +261,8 @@ public function upsert(
* @param array|bool $updateColumns The column data (name => value) to update if they already exist. If `true`
* is passed, the column data will be updated to match the insert column data. If `false` is passed, no update will
* be performed if the column data already exist.
* @param string[]|null $constraintColumns The column names to use for the `ON CONFLICT` clause. If `null`,
* the primary key or the first matching unique constraint will be used.
* @param string[]|null $returnColumns The column names to return values from. `null` means all columns.
* @param array $params The binding parameters that will be generated by this method. They should be bound to the DB
* command later.
Expand All @@ -274,6 +279,7 @@ public function upsertReturning(
string $table,
array|QueryInterface $insertColumns,
array|bool $updateColumns = true,
?array $constraintColumns = null,
?array $returnColumns = null,
array &$params = [],
): string;
Expand Down
Loading