diff --git a/CHANGELOG.md b/CHANGELOG.md index 6985c0be8..72ec53fc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - Enh #403, #404: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov) - New #397: Realize `Schema::loadResultColumn()` method (@Tigrov) - New #407: Use `DateTimeColumn` class for datetime column types (@Tigrov) +- New #408: Implement `DMLQueryBuilder::upsertWithReturningPks()` method (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/DMLQueryBuilder.php b/src/DMLQueryBuilder.php index 197229491..bcf432492 100644 --- a/src/DMLQueryBuilder.php +++ b/src/DMLQueryBuilder.php @@ -17,18 +17,11 @@ */ final class DMLQueryBuilder extends AbstractDMLQueryBuilder { - public function insertWithReturningPks(string $table, QueryInterface|array $columns, array &$params = []): string + public function insertWithReturningPks(string $table, array|QueryInterface $columns, array &$params = []): string { $sql = $this->insert($table, $columns, $params); - $returnColumns = $this->schema->getTableSchema($table)?->getPrimaryKey(); - - if (!empty($returnColumns)) { - $returnColumns = array_map($this->quoter->quoteColumnName(...), $returnColumns); - - $sql .= ' RETURNING ' . implode(', ', $returnColumns); - } - return $sql; + return $this->appendReturningPksClause($sql, $table); } public function resetSequence(string $table, int|string|null $value = null): string @@ -60,9 +53,9 @@ public function resetSequence(string $table, int|string|null $value = null): str public function upsert( string $table, - QueryInterface|array $insertColumns, - bool|array $updateColumns, - array &$params = [] + array|QueryInterface $insertColumns, + array|bool $updateColumns = true, + array &$params = [], ): string { $insertSql = $this->insert($table, $insertColumns, $params); @@ -93,4 +86,28 @@ public function upsert( return $insertSql . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET ' . implode(', ', $updates); } + + public function upsertWithReturningPks( + string $table, + array|QueryInterface $insertColumns, + array|bool $updateColumns = true, + array &$params = [], + ): string { + $sql = $this->upsert($table, $insertColumns, $updateColumns, $params); + + return $this->appendReturningPksClause($sql, $table); + } + + private function appendReturningPksClause(string $sql, string $table): string + { + $returnColumns = $this->schema->getTableSchema($table)?->getPrimaryKey(); + + if (!empty($returnColumns)) { + $returnColumns = array_map($this->quoter->quoteColumnName(...), $returnColumns); + + $sql .= ' RETURNING ' . implode(', ', $returnColumns); + } + + return $sql; + } } diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index f14fbe6b0..7de1d182a 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -317,6 +317,36 @@ public static function upsert(): array return $upsert; } + public static function upsertWithReturningPks(): array + { + $upsert = self::upsert(); + + foreach ($upsert as &$data) { + $data[3] .= ' RETURNING "id"'; + } + + $upsert['no columns to update'][3] = 'INSERT INTO "T_upsert_1" ("a") VALUES (:qp0) ON CONFLICT DO NOTHING RETURNING "a"'; + + return [ + ...$upsert, + 'composite primary key' => [ + 'notauto_pk', + ['id_1' => 1, 'id_2' => 2.5, 'type' => 'Test'], + true, + 'INSERT INTO "notauto_pk" ("id_1", "id_2", "type") VALUES (:qp0, :qp1, :qp2)' + . ' ON CONFLICT ("id_1", "id_2") DO UPDATE SET "type"=EXCLUDED."type" RETURNING "id_1", "id_2"', + [':qp0' => 1, ':qp1' => 2.5, ':qp2' => 'Test'], + ], + 'no primary key' => [ + 'type', + ['int_col' => 3, 'char_col' => 'a', 'float_col' => 1.2, 'bool_col' => true], + true, + 'INSERT INTO "type" ("int_col", "char_col", "float_col", "bool_col") VALUES (:qp0, :qp1, :qp2, :qp3)', + [':qp0' => 3, ':qp1' => 'a', ':qp2' => 1.2, ':qp3' => true], + ], + ]; + } + public static function overlapsCondition(): array { $data = parent::overlapsCondition(); diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 682a5d2ab..c205556bc 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -5,11 +5,8 @@ namespace Yiisoft\Db\Pgsql\Tests; use PHPUnit\Framework\Attributes\DataProviderExternal; -use Throwable; use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; -use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\IntegrityException; -use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\ExpressionInterface; @@ -26,8 +23,6 @@ /** * @group pgsql - * - * @psalm-suppress PropertyNotSetInConstructor */ final class QueryBuilderTest extends CommonQueryBuilderTest { @@ -40,10 +35,6 @@ public function getBuildColumnDefinitionProvider(): array protected PdoConnectionInterface $db; - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testAddDefaultValue(): void { $db = $this->getConnection(); @@ -66,9 +57,7 @@ public function testAlterColumn(string|ColumnInterface $type, string $expected): parent::testAlterColumn($type, $expected); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::addForeignKey - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'addForeignKey')] public function testAddForeignKey( string $name, string $table, @@ -82,25 +71,19 @@ public function testAddForeignKey( parent::testAddForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update, $expected); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::addPrimaryKey - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'addPrimaryKey')] public function testAddPrimaryKey(string $name, string $table, array|string $columns, string $expected): void { parent::testAddPrimaryKey($name, $table, $columns, $expected); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::addUnique - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'addUnique')] public function testAddUnique(string $name, string $table, array|string $columns, string $expected): void { parent::testAddUnique($name, $table, $columns, $expected); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::batchInsert - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'batchInsert')] public function testBatchInsert( string $table, iterable $rows, @@ -120,9 +103,7 @@ public function testBuildCondition( parent::testBuildCondition($condition, $expected, $expectedParams); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::buildLikeCondition - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'buildLikeCondition')] public function testBuildLikeCondition( array|ExpressionInterface $condition, string $expected, @@ -131,27 +112,18 @@ public function testBuildLikeCondition( parent::testBuildLikeCondition($condition, $expected, $expectedParams); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::buildFrom - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'buildFrom')] public function testBuildWithFrom(mixed $table, string $expectedSql, array $expectedParams = []): void { parent::testBuildWithFrom($table, $expectedSql, $expectedParams); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::buildWhereExists - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'buildWhereExists')] public function testBuildWithWhereExists(string $cond, string $expectedQuerySql): void { parent::testBuildWithWhereExists($cond, $expectedQuerySql); } - /** - * @throws Exception - * @throws InvalidConfigException - * @throws NotSupportedException - */ public function testCheckIntegrity(): void { $db = $this->getConnection(); @@ -168,11 +140,6 @@ public function testCheckIntegrity(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - * @throws Throwable - */ public function testCheckIntegrityExecute(): void { $db = $this->getConnection(true); @@ -197,10 +164,6 @@ public function testCheckIntegrityExecute(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testCreateTable(): void { $db = $this->getConnection(); @@ -232,18 +195,12 @@ public function testCreateTable(): void $db->close(); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::delete - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'delete')] public function testDelete(string $table, array|string $condition, string $expectedSQL, array $expectedParams): void { parent::testDelete($table, $condition, $expectedSQL, $expectedParams); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testDropCommentFromColumn(): void { $db = $this->getConnection(true); @@ -260,10 +217,6 @@ public function testDropCommentFromColumn(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testDropDefaultValue(): void { $db = $this->getConnection(true); @@ -280,10 +233,6 @@ public function testDropDefaultValue(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testDropIndex(): void { $db = $this->getConnection(); @@ -335,9 +284,7 @@ public function testDropIndex(): void $db->close(); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::insert - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'insert')] public function testInsert( string $table, array|QueryInterface $columns, @@ -348,9 +295,7 @@ public function testInsert( parent::testInsert($table, $columns, $params, $expectedSQL, $expectedParams); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::insertWithReturningPks - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'insertWithReturningPks')] public function testInsertWithReturningPks( string $table, array|QueryInterface $columns, @@ -361,10 +306,6 @@ public function testInsertWithReturningPks( parent::testInsertWithReturningPks($table, $columns, $params, $expectedSQL, $expectedParams); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testRenameTable(): void { $db = $this->getConnection(); @@ -381,11 +322,6 @@ public function testRenameTable(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - * @throws NotSupportedException - */ public function testResetSequence(): void { $db = $this->getConnection(true); @@ -416,11 +352,6 @@ public function testResetSequence(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - * @throws NotSupportedException - */ public function testResetSequencePgsql12(): void { if (version_compare($this->getConnection()->getServerInfo()->getVersion(), '12.0', '<')) { @@ -457,10 +388,6 @@ public function testResetSequencePgsql12(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - */ public function testTruncateTable(): void { $db = $this->getConnection(); @@ -487,9 +414,7 @@ public function testTruncateTable(): void $db->close(); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::update - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'update')] public function testUpdate( string $table, array $columns, @@ -501,31 +426,29 @@ public function testUpdate( parent::testUpdate($table, $columns, $condition, $params, $expectedSql, $expectedParams); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::upsert - */ + #[DataProviderExternal(QueryBuilderProvider::class, 'upsert')] public function testUpsert( string $table, array|QueryInterface $insertColumns, array|bool $updateColumns, - string $expectedSQL, + string $expectedSql, array $expectedParams ): void { - parent::testUpsert($table, $insertColumns, $updateColumns, $expectedSQL, $expectedParams); + parent::testUpsert($table, $insertColumns, $updateColumns, $expectedSql, $expectedParams); } - /** - * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::upsert - */ - public function testUpsertExecute( + #[DataProviderExternal(QueryBuilderProvider::class, 'upsertWithReturningPks')] + public function testUpsertWithReturningPks( string $table, array|QueryInterface $insertColumns, - array|bool $updateColumns + array|bool $updateColumns, + string $expectedSql, + array $expectedParams ): void { - parent::testUpsertExecute($table, $insertColumns, $updateColumns); + parent::testUpsertWithReturningPks($table, $insertColumns, $updateColumns, $expectedSql, $expectedParams); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::selectScalar */ + #[DataProviderExternal(QueryBuilderProvider::class, 'selectScalar')] public function testSelectScalar(array|bool|float|int|string $columns, string $expected): void { parent::testSelectScalar($columns, $expected); @@ -569,7 +492,7 @@ public function testJsonOverlapsConditionBuilder(): void $db->close(); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::overlapsCondition */ + #[DataProviderExternal(QueryBuilderProvider::class, 'overlapsCondition')] public function testOverlapsCondition(iterable|ExpressionInterface $values, int $expectedCount): void { $db = $this->getConnection(); @@ -599,7 +522,7 @@ public function testOverlapsCondition(iterable|ExpressionInterface $values, int $db->close(); } - /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::overlapsCondition */ + #[DataProviderExternal(QueryBuilderProvider::class, 'overlapsCondition')] public function testOverlapsConditionOperator(iterable|ExpressionInterface $values, int $expectedCount): void { $db = $this->getConnection(); diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index ebf64d182..baea6d9bc 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -208,7 +208,7 @@ CREATE TABLE "default_pk" ( CREATE TABLE "notauto_pk" ( id_1 INTEGER, - id_2 INTEGER, + id_2 DECIMAL(5,2), type VARCHAR(255) NOT NULL, PRIMARY KEY (id_1, id_2) );