From 2c03ec862a202fe7c5559100752ba7b1764235f1 Mon Sep 17 00:00:00 2001 From: Sascha Nowak Date: Wed, 31 Jul 2024 10:06:22 +0200 Subject: [PATCH] feature: add Postgre SQL support --- .gitignore | 3 + composer.json | 5 +- src/Upsert.php | 165 ++++++++++++++++++++++++++++++------------------- 3 files changed, 109 insertions(+), 64 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ffdd8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +.phpunit.result.cache +composer.lock diff --git a/composer.json b/composer.json index 87ff495..f3f1490 100644 --- a/composer.json +++ b/composer.json @@ -4,10 +4,11 @@ "license": "MIT", "type": "library", "require": { - "doctrine/dbal": "^2.9" + "php": ">=8.1", + "doctrine/dbal": "^2.13 || ^3.8" }, "require-dev": { - "phpunit/phpunit": "^6.0", + "phpunit/phpunit": "^10.5", "ext-pdo_sqlite": "*" }, "autoload": { diff --git a/src/Upsert.php b/src/Upsert.php index 694ab20..72a3d0d 100644 --- a/src/Upsert.php +++ b/src/Upsert.php @@ -1,38 +1,34 @@ connection = $connection; + return new self($connection); } public function forTable(string $table): self @@ -42,15 +38,16 @@ public function forTable(string $table): self return $this; } - public function withIdentifier(string $column, $value, int $parameterType = ParameterType::STRING): self + public function withIdentifier(string $column, mixed $value, int $parameterType = ParameterType::STRING): self { - if (array_key_exists($column, $this->identifiers)) { - throw new Exception\IdentifierAlreadyInUse(sprintf('The identifier "%s" has already been set!', $column), 1603196381); - } - if (array_key_exists($column, $this->fields)) { - throw new Exception\IdentifierRegisteredAsField(sprintf('The identifier "%s" has already been set as field!', $column), 1603197666); + $this->throwErrorIsColumnExists($column); + + if (is_object($value) && method_exists($value, 'rawType')) { + $parameterType = $value->rawType(); } + $value = $this->getValue($value); + $this->identifiers[$column] = [ 'value' => $value, 'type' => $parameterType, @@ -59,18 +56,24 @@ public function withIdentifier(string $column, $value, int $parameterType = Para return $this; } - public function withField(string $column, $value, int $parameterType = ParameterType::STRING): self - { - if (array_key_exists($column, $this->fields)) { - throw new Exception\FieldAlreadyInUse(sprintf('The field "%s" has already been set!', $column), 1603196457); - } - if (array_key_exists($column, $this->identifiers)) { - throw new Exception\FieldRegisteredAsIdentifier(sprintf('The field "%s" has already been set as identifier!', $column), 1603197691); + public function withField( + string $column, + mixed $value, + int $parameterType = ParameterType::STRING, + bool $insertOnly = false + ): self { + $this->throwErrorIsColumnExists($column); + + if (is_object($value) && method_exists($value, 'rawType')) { + $parameterType = $value->rawType(); } + $value = $this->getValue($value); + $this->fields[$column] = [ 'value' => $value, 'type' => $parameterType, + 'insertOnly' => $insertOnly, ]; return $this; @@ -78,25 +81,30 @@ public function withField(string $column, $value, int $parameterType = Parameter public function execute(): int { - if (!$this->table) { + if ($this->table === null) { throw new Exception\NoTableGiven('No table name has been set!', 1603199471); } - if (count($this->identifiers) === 0 || count($this->fields) === 0) { + + if ($this->identifiers === [] || $this->fields === []) { throw new Exception\EmptyUpsert('No columns have been specified for upsert!', 1603199389); } + $identifiers = implode(', ', array_keys($this->identifiers)); + $allFields = array_merge($this->fields, $this->identifiers); $columns = implode(', ', array_keys($allFields)); - $values = implode(', ', array_map(function(string $column) { - return ':' . $column; - }, array_keys($allFields))); - - $updates = implode(', ', array_map(function(string $column) { - return $column . ' = :' . $column; - }, array_keys($this->fields))); + $values = implode(', ', array_map(static fn (string $column): string => ':' . $column, array_keys($allFields))); + + $updates = implode( + ', ', + array_map( + static fn (string $column): string => $column . ' = :' . $column, + array_keys(array_filter($this->fields, static fn (array $field): bool => !$field['insertOnly'])) + ) + ); - $sql = $this->buildQuery($columns, $values, $updates); + $sql = $this->buildQuery($identifiers, $columns, $values, $updates); $result = $this->connection->executeQuery( $sql, @@ -107,33 +115,66 @@ public function execute(): int return $result->rowCount(); } - protected function buildQuery(string $columns, string $values, string $updates): string + protected function buildQuery(string $identifiers, string $columns, string $values, string $updates): string { - switch($this->connection->getDatabasePlatform()->getName()) { - case 'mysql': - return <<connection->getDatabasePlatform(); + + return match (true) { + $platform instanceof PostgreSQLPlatform => <<table}" ({$columns}) +VALUES ({$values}) +ON CONFLICT ({$identifiers}) DO UPDATE SET {$updates} +POSTGRESQL + , + $platform instanceof AbstractMySQLPlatform => <<table} ({$columns}) VALUES ({$values}) ON DUPLICATE KEY UPDATE {$updates} -MYSQL; - case 'sqlite': - $conflictColumns = implode(', ', array_keys($this->identifiers)); - return << <<table} ({$columns}) VALUES ({$values}) -ON CONFLICT({$conflictColumns}) DO UPDATE SET {$updates} -SQLITE; - default: - throw new Exception\UnsupportedDatabasePlatform( - sprintf('The database platform %s is not supported!', $this->connection->getDatabasePlatform()->getName()), - 1603199935 - ); - } +ON CONFLICT({$identifiers}) DO UPDATE SET {$updates} +SQLITE + ,default => throw new RuntimeException( + sprintf('The database platform %s is not supported!', $platform::class), + 1_603_199_935 + ) + }; } - public static function fromConnection(Connection $connection): self + private function getValue(mixed $value): int|string|float|bool|null { - return new static($connection); + if (is_array($value)) { + $value = json_encode($value, JSON_THROW_ON_ERROR); + } + + if ($value instanceof Stringable) { + $value = (string) $value; + } + + if ($value instanceof BackedEnum) { + $value = (string) $value->value; + } + + if (is_object($value) && method_exists($value, 'rawValue')) { + return $value->rawValue(); + } + + return $value; } + private function throwErrorIsColumnExists(string $column): void + { + if (array_key_exists($column, $this->fields)) { + throw new Exception\FieldAlreadyInUse(sprintf('The field "%s" has already been set!', $column), 1603196457); + } + + if (array_key_exists($column, $this->identifiers)) { + throw new Exception\IdentifierRegisteredAsField( + sprintf('The field "%s" has already been set as identifier!', $column), + 1603197691 + ); + } + } }