Skip to content

Commit

Permalink
feature: add Postgre SQL support
Browse files Browse the repository at this point in the history
  • Loading branch information
saschanowak committed Jul 31, 2024
1 parent a43182f commit 2c03ec8
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 64 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/vendor/
.phpunit.result.cache
composer.lock
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
165 changes: 103 additions & 62 deletions src/Upsert.php
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
<?php

declare(strict_types=1);

namespace Netlogix\Doctrine\Upsert;

use BackedEnum;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use Netlogix\Doctrine\Upsert\Exception;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use RuntimeException;
use Stringable;

class Upsert
final class Upsert
{
private ?string $table = null;

/**
* @var Connection
*/
private $connection;

/**
* @var string
*/
private $table;
private array $identifiers = [];

/**
* @var array
*/
private $identifiers = [];
private array $fields = [];

/**
* @var array
*/
private $fields = [];
private function __construct(
private readonly Connection $connection
) {
}

private function __construct(Connection $connection)
public static function fromConnection(Connection $connection): self
{
$this->connection = $connection;
return new self($connection);
}

public function forTable(string $table): self
Expand All @@ -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,
Expand All @@ -59,44 +56,55 @@ 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;
}

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,
Expand All @@ -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 <<<MYSQL
$platform = $this->connection->getDatabasePlatform();

return match (true) {
$platform instanceof PostgreSQLPlatform => <<<POSTGRESQL
INSERT INTO "{$this->table}" ({$columns})
VALUES ({$values})
ON CONFLICT ({$identifiers}) DO UPDATE SET {$updates}
POSTGRESQL
,
$platform instanceof AbstractMySQLPlatform => <<<MYSQL
INSERT INTO {$this->table} ({$columns})
VALUES ({$values})
ON DUPLICATE KEY UPDATE {$updates}
MYSQL;
case 'sqlite':
$conflictColumns = implode(', ', array_keys($this->identifiers));
return <<<SQLITE
MYSQL
,$platform instanceof SqlitePlatform => <<<SQLITE
INSERT INTO {$this->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
);
}
}
}

0 comments on commit 2c03ec8

Please sign in to comment.