Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Table gateway for tables with a primary key defined
- Loading branch information
1 parent
417237d
commit c1277f5
Showing
5 changed files
with
561 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of sad_spirit/pg_gateway package | ||
* | ||
* (c) Alexey Borzov <avb@php.net> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace sad_spirit\pg_gateway; | ||
|
||
use sad_spirit\pg_wrapper\ResultSet; | ||
|
||
/** | ||
* Interface for gateways to tables that have a primary key defined | ||
*/ | ||
interface PrimaryKeyAccess | ||
{ | ||
/** | ||
* Deletes a row with the given primary key | ||
* | ||
* @param mixed $primaryKey | ||
* @return ResultSet | ||
*/ | ||
public function deleteByPrimaryKey($primaryKey): ResultSet; | ||
|
||
/** | ||
* Returns an object that can SELECT a row with the given primary key | ||
* | ||
* @param mixed $primaryKey | ||
* @return SelectProxy | ||
*/ | ||
public function selectByPrimaryKey($primaryKey): SelectProxy; | ||
|
||
/** | ||
* Updates a row with the given primary key using the given values | ||
* | ||
* @param mixed $primaryKey | ||
* @param array $set | ||
* @return ResultSet | ||
*/ | ||
public function updateByPrimaryKey($primaryKey, array $set): ResultSet; | ||
|
||
/** | ||
* Executes an "UPSERT" (INSERT ... ON CONFLICT DO UPDATE ...) query with the given values | ||
* | ||
* @param array $values | ||
* @return array Primary key of the row inserted / updated | ||
*/ | ||
public function upsert(array $values): array; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of sad_spirit/pg_gateway package | ||
* | ||
* (c) Alexey Borzov <avb@php.net> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace sad_spirit\pg_gateway\conditions; | ||
|
||
use sad_spirit\pg_gateway\{ | ||
Condition, | ||
Fragment, | ||
TableGateway, | ||
TableLocator, | ||
exceptions\InvalidArgumentException, | ||
exceptions\UnexpectedValueException, | ||
fragments\WhereClauseFragment, | ||
metadata\PrimaryKey | ||
}; | ||
use sad_spirit\pg_builder\nodes\{ | ||
ColumnReference, | ||
ScalarExpression, | ||
expressions\LogicalExpression, | ||
expressions\NamedParameter, | ||
expressions\OperatorExpression, | ||
expressions\TypecastExpression | ||
}; | ||
use sad_spirit\pg_builder\converters\TypeNameNodeHandler; | ||
|
||
/** | ||
* Generates a WHERE clause fragment for finding a table row by its primary key | ||
*/ | ||
final class PrimaryKeyCondition extends Condition | ||
{ | ||
private PrimaryKey $primaryKey; | ||
private TypeNameNodeHandler $converterFactory; | ||
|
||
public function __construct(PrimaryKey $primaryKey, TypeNameNodeHandler $converterFactory) | ||
{ | ||
// Sanity check: primary key actually is defined | ||
if (0 === \count($primaryKey)) { | ||
throw new UnexpectedValueException("No columns in table's primary key"); | ||
} | ||
$this->primaryKey = $primaryKey; | ||
$this->converterFactory = $converterFactory; | ||
} | ||
|
||
public function getFragment(): Fragment | ||
{ | ||
return new WhereClauseFragment($this, Fragment::PRIORITY_HIGHEST); | ||
} | ||
|
||
/** | ||
* Possibly converts the given PK value to an array and checks that array keys are corresponding to PK columns | ||
* | ||
* @param mixed $value Either an array ['primary key column' => value, ...] or a value for a | ||
* single-column primary key | ||
* @return array Array of the format ['primary key column' => value, ...] | ||
*/ | ||
public function normalizeValue($value): array | ||
{ | ||
$columns = $this->primaryKey->getNames(); | ||
|
||
if (!\is_array($value)) { | ||
if (1 === \count($columns)) { | ||
return [$columns[0] => $value]; | ||
} else { | ||
throw new InvalidArgumentException(\sprintf( | ||
"Expecting an array for a composite primary key value, %s given", | ||
\is_object($value) ? 'object(' . \get_class($value) . ')' : \gettype($value) | ||
)); | ||
} | ||
} | ||
|
||
foreach ($columns as $column) { | ||
if (!\array_key_exists($column, $value)) { | ||
throw new InvalidArgumentException("Primary key column '$column' not found in array"); | ||
} | ||
} | ||
if (\count($value) > \count($columns)) { | ||
$unknown = \array_diff(\array_keys($value), $columns); | ||
throw new InvalidArgumentException( | ||
"Indexes '" . \implode("', '", $unknown) | ||
. "' in array do not correspond to primary key columns" | ||
); | ||
} | ||
|
||
return $value; | ||
} | ||
|
||
protected function generateExpressionImpl(): ScalarExpression | ||
{ | ||
$expression = new LogicalExpression([], LogicalExpression::AND); | ||
foreach ($this->primaryKey as $column) { | ||
$expression[] = new OperatorExpression( | ||
'=', | ||
new ColumnReference(TableGateway::ALIAS_SELF, $column->getName()), | ||
new TypecastExpression( | ||
new NamedParameter($column->getName()), | ||
$this->converterFactory->createTypeNameNodeForOID($column->getTypeOID()) | ||
) | ||
); | ||
} | ||
return $expression; | ||
} | ||
|
||
public function getKey(): ?string | ||
{ | ||
return TableLocator::hash([self::class, $this->primaryKey->getAll()]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of sad_spirit/pg_gateway package | ||
* | ||
* (c) Alexey Borzov <avb@php.net> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace sad_spirit\pg_gateway\gateways; | ||
|
||
use sad_spirit\pg_gateway\{ | ||
conditions\ParametrizedCondition, | ||
FragmentList, | ||
PrimaryKeyAccess, | ||
SelectProxy, | ||
TableSelect, | ||
conditions\PrimaryKeyCondition, | ||
fragments\SetClauseFragment | ||
}; | ||
use sad_spirit\pg_builder\Insert; | ||
use sad_spirit\pg_builder\NativeStatement; | ||
use sad_spirit\pg_builder\nodes\{ | ||
ColumnReference, | ||
Identifier, | ||
IndexElement, | ||
IndexParameters, | ||
OnConflictClause, | ||
SetTargetElement, | ||
SingleSetClause, | ||
TargetElement, | ||
lists\SetClauseList, | ||
range\InsertTarget, | ||
}; | ||
use sad_spirit\pg_wrapper\ResultSet; | ||
|
||
/** | ||
* Table gateway implementation for tables that have a primary key defined | ||
*/ | ||
class PrimaryKeyTableGateway extends GenericTableGateway implements PrimaryKeyAccess | ||
{ | ||
public function deleteByPrimaryKey($primaryKey): ResultSet | ||
{ | ||
$list = new FragmentList($this->primaryKey($primaryKey)); | ||
|
||
return $this->createDeleteStatement($list) | ||
->executeParams($this->getConnection(), $list->getParameters()); | ||
} | ||
|
||
public function selectByPrimaryKey($primaryKey): SelectProxy | ||
{ | ||
$condition = new PrimaryKeyCondition($this->getPrimaryKey(), $this->tableLocator->getTypeConverterFactory()); | ||
|
||
return new TableSelect($this->tableLocator, $this, $condition, $condition->normalizeValue($primaryKey)); | ||
} | ||
|
||
public function updateByPrimaryKey($primaryKey, array $set): ResultSet | ||
{ | ||
$list = new FragmentList( | ||
new SetClauseFragment($this->getColumns(), $this->tableLocator, $set), | ||
$this->primaryKey($primaryKey) | ||
); | ||
|
||
return $this->createUpdateStatement($list) | ||
->executeParams($this->getConnection(), $list->getParameters()); | ||
} | ||
|
||
/** | ||
* Creates a condition on a primary key, can be used to combine with other Fragments | ||
* | ||
* @param mixed $value | ||
* @return ParametrizedCondition | ||
*/ | ||
public function primaryKey($value): ParametrizedCondition | ||
{ | ||
$condition = new PrimaryKeyCondition($this->getPrimaryKey(), $this->tableLocator->getTypeConverterFactory()); | ||
return new ParametrizedCondition($condition, $condition->normalizeValue($value)); | ||
} | ||
|
||
public function upsert(array $values): array | ||
{ | ||
$valuesClause = new SetClauseFragment($this->getColumns(), $this->tableLocator, $values); | ||
$native = $this->createUpsertStatement(new FragmentList($valuesClause)); | ||
if ([] === $native->getParameterTypes()) { | ||
return $this->getConnection()->execute($native->getSql())->current(); | ||
} else { | ||
return $native->executeParams( | ||
$this->getConnection(), | ||
$valuesClause->getParameterHolder()->getParameters() | ||
)->current(); | ||
} | ||
} | ||
|
||
/** | ||
* Generates an "UPSERT" (INSERT ... ON CONFLICT DO UPDATE ...) statement using given fragments | ||
* | ||
* @param FragmentList $fragments | ||
* @return NativeStatement | ||
*/ | ||
public function createUpsertStatement(FragmentList $fragments): NativeStatement | ||
{ | ||
return $this->tableLocator->createNativeStatementUsingCache( | ||
function () use ($fragments): Insert { | ||
$insert = $this->createBaseUpsertAST(); | ||
$fragments->applyTo($insert); | ||
return $insert; | ||
}, | ||
$this->generateStatementKey(self::STATEMENT_UPSERT, $fragments) | ||
); | ||
} | ||
|
||
/** | ||
* Generates base AST for "INSERT ... ON CONFLICT DO UPDATE ..." statement executed by upsert() | ||
* | ||
* @return Insert | ||
*/ | ||
protected function createBaseUpsertAST(): Insert | ||
{ | ||
$insert = $this->tableLocator->getStatementFactory()->insert(new InsertTarget( | ||
$this->getName(), | ||
new Identifier(self::ALIAS_SELF) | ||
)); | ||
|
||
$target = new IndexParameters(); | ||
$set = new SetClauseList(); | ||
$primaryKeyColumns = $this->getPrimaryKey()->getNames(); | ||
$nonPrimaryKey = \array_diff($this->getColumns()->getNames(), $primaryKeyColumns); | ||
|
||
foreach ($primaryKeyColumns as $pk) { | ||
$target[] = new IndexElement(new Identifier($pk)); | ||
$insert->returning[] = new TargetElement(new ColumnReference($pk)); | ||
} | ||
|
||
// "DO INSTEAD NOTHING" clause leads to no rows returned by RETURNING clause, thus a fake update | ||
if ([] === $nonPrimaryKey) { | ||
$pkey = reset($primaryKeyColumns); | ||
$set[] = new SingleSetClause(new SetTargetElement($pkey), new ColumnReference('excluded', $pkey)); | ||
} else { | ||
foreach ($nonPrimaryKey as $column) { | ||
$set[] = new SingleSetClause( | ||
new SetTargetElement($column), | ||
new ColumnReference('excluded', $column) | ||
); | ||
} | ||
} | ||
|
||
$insert->onConflict = new OnConflictClause(OnConflictClause::UPDATE, $target, $set); | ||
|
||
return $insert; | ||
} | ||
} |
Oops, something went wrong.