From f40ed1f0e5a90b8561b9e9ddd962784f76806328 Mon Sep 17 00:00:00 2001 From: Jakub Kulhan Date: Wed, 29 May 2024 17:29:09 +0200 Subject: [PATCH] persistence upsert cross-database --- data-access-kit/src/Persistence.php | 60 ++++++++++++++++++------ data-access-kit/test/PersistenceTest.php | 15 ++++++ 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/data-access-kit/src/Persistence.php b/data-access-kit/src/Persistence.php index 2f5d0b3..4d90c8e 100644 --- a/data-access-kit/src/Persistence.php +++ b/data-access-kit/src/Persistence.php @@ -4,10 +4,12 @@ use DataAccessKit\Attribute\Column; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\SQLitePlatform; use LogicException; +use function array_map; use function array_merge; use function count; use function implode; @@ -84,10 +86,11 @@ private function insertUpsertAll(array $objects, ?array $upsertColumns = null): $columnNames = []; $rows = []; $values = []; - $update = []; + $updateColumnNames = []; $generatedColumnNames = []; /** @var Column[] $generatedColumns */ $generatedColumns = []; + $primaryKeyColumnNames = []; /** @var Column|null $primaryKeyColumn */ $primaryKeyColumn = null; $supportsReturning = match (true) { @@ -96,34 +99,49 @@ private function insertUpsertAll(array $objects, ?array $upsertColumns = null): $platform instanceof SQLitePlatform => true, default => false, }; + $supportsDefault = match (true) { + $platform instanceof AbstractMySQLPlatform => true, + $platform instanceof PostgreSQLPlatform => true, + default => false, + }; foreach ($table->columns as $column) { - if ($column->generated) { - if ($column->primary) { - if ($primaryKeyColumn !== null) { - throw new LogicException("Multiple generated primary columns."); - } - $primaryKeyColumn = $column; + if ($column->primary) { + if ($supportsDefault || $upsertColumns !== []) { + $columnNames[] = $platform->quoteSingleIdentifier($column->name); } + $primaryKeyColumnNames[] = $platform->quoteSingleIdentifier($column->name); + if ($primaryKeyColumn !== null) { + throw new LogicException("Multiple generated primary columns."); + } + $primaryKeyColumn = $column; + + if ($column->generated && $supportsReturning) { + $generatedColumnNames[] = $platform->quoteSingleIdentifier($column->name); + $generatedColumns[] = $column; + } + + } else if ($column->generated) { if ($supportsReturning) { $generatedColumnNames[] = $platform->quoteSingleIdentifier($column->name); $generatedColumns[] = $column; } + } else { $columnNames[] = $platform->quoteSingleIdentifier($column->name); if ($upsertColumns === null || in_array($column->name, $upsertColumns, true)) { - $update[] = $platform->quoteSingleIdentifier($column->name) . " = VALUES(" . $platform->quoteSingleIdentifier($column->name) . ")"; + $updateColumnNames[] = $platform->quoteSingleIdentifier($column->name); } } } - foreach ($objects as $object) { + foreach ($objects as $index => $object) { $row = []; foreach ($table->columns as $column) { - if ($column->generated) { + if ($column->generated && !($column->primary && ($supportsDefault || $upsertColumns !== []))) { continue; } @@ -133,8 +151,15 @@ private function insertUpsertAll(array $objects, ?array $upsertColumns = null): $row[] = "?"; $values[] = $value; - } else { + } else if ($supportsDefault) { $row[] = "DEFAULT"; + + } else { + throw new LogicException(sprintf( + "Property [%s] of object at index [%d] not initialized and the database doesn't support default values, cannot insert.", + $column->reflection->getName(), + $index, + )); } } @@ -146,17 +171,22 @@ private function insertUpsertAll(array $objects, ?array $upsertColumns = null): $platform->quoteSingleIdentifier($table->name), implode(", ", $columnNames), implode(", ", $rows), - match (count($update)) { - 0 => "", - default => sprintf(" ON DUPLICATE KEY UPDATE %s", implode(", ", $update)), + match (true) { + count($updateColumnNames) > 0 && $platform instanceof AbstractMySQLPlatform => sprintf(" ON DUPLICATE KEY UPDATE %s", implode(", ", array_map(fn(string $it) => $it . " = VALUES(" . $it . ")", $updateColumnNames))), + count($updateColumnNames) > 0 && ($platform instanceof PostgreSQLPlatform || $platform instanceof SQLitePlatform) => sprintf(" ON CONFLICT (%s) DO UPDATE SET %s", implode(", ", $primaryKeyColumnNames), implode(", ", array_map(fn(string $it) => $it . " = EXCLUDED." . $it, $updateColumnNames))), + count($updateColumnNames) > 0 => throw new LogicException(sprintf( + "Upsert not supported on platform [%s].", + get_class($platform), + )), + default => "", }, match ($supportsReturning) { true => sprintf(" RETURNING %s", implode(", ", $generatedColumnNames)), default => "", }, ); - $result = $this->connection->executeQuery($sql, $values); + if ($supportsReturning && count($generatedColumns) > 0) { foreach ($result->iterateAssociative() as $index => $row) { $object = $objects[$index]; diff --git a/data-access-kit/test/PersistenceTest.php b/data-access-kit/test/PersistenceTest.php index d4ce710..2f62a6b 100644 --- a/data-access-kit/test/PersistenceTest.php +++ b/data-access-kit/test/PersistenceTest.php @@ -118,4 +118,19 @@ public function testInsertAll(): void } } + public function testUpsert(): void + { + $this->setUpUsersTable(); + $user = new User(); + $user->id = 1; + $user->firstName = "Charlie"; + $this->persistence->upsert($user); + $this->assertEquals(1, $user->id); + + $users = iterator_to_array($this->persistence->select(User::class, "SELECT user_id, first_name FROM users WHERE user_id = ?", [$user->id])); + $this->assertCount(1, $users); + $this->assertEquals($user->id, $users[0]->id); + $this->assertEquals($user->firstName, $users[0]->firstName); + } + }