Skip to content

Commit

Permalink
Generic TableGateway implementation, without select()
Browse files Browse the repository at this point in the history
  • Loading branch information
sad-spirit committed Aug 15, 2023
1 parent 0146df7 commit 724cb82
Show file tree
Hide file tree
Showing 9 changed files with 763 additions and 0 deletions.
65 changes: 65 additions & 0 deletions src/fragments/InsertSelectFragment.php
@@ -0,0 +1,65 @@
<?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\fragments;

use sad_spirit\pg_gateway\{
Fragment,
ParameterHolder,
Parametrized,
SelectProxy,
exceptions\InvalidArgumentException
};
use sad_spirit\pg_builder\{
Insert,
Statement
};

/**
* Wrapper for SelectProxy object passed as $values to GenericTableGateway::insert()
*/
class InsertSelectFragment implements Fragment, Parametrized
{
private SelectProxy $select;

public function __construct(SelectProxy $select)
{
$this->select = $select;
}

public function applyTo(Statement $statement): void
{
if (!$statement instanceof Insert) {
throw new InvalidArgumentException(\sprintf(
"This fragment can only be added to INSERT statements, instance of %s given",
\get_class($statement)
));
}
$statement->values = $this->select->createSelectAST();
}

public function getPriority(): int
{
return Fragment::PRIORITY_HIGHEST;
}

public function getKey(): ?string
{
return $this->select->getKey();
}

public function getParameterHolder(): ?ParameterHolder
{
return $this->select->getParameterHolder();
}
}
248 changes: 248 additions & 0 deletions src/gateways/GenericTableGateway.php
@@ -0,0 +1,248 @@
<?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\{
FragmentList,
SelectProxy,
TableGateway,
TableLocator,
fragments\ClosureFragment,
fragments\InsertSelectFragment,
fragments\SetClauseFragment,
exceptions\InvalidArgumentException,
exceptions\LogicException,
metadata\Columns,
metadata\PrimaryKey,
metadata\References
};
use sad_spirit\pg_builder\{
Delete,
Insert,
NativeStatement,
SelectCommon,
Update
};
use sad_spirit\pg_builder\nodes\{
Identifier,
QualifiedName,
lists\SetClauseList,
range\InsertTarget,
range\UpdateOrDeleteTarget
};
use sad_spirit\pg_wrapper\{
Connection,
ResultSet
};

/**
* A generic implementation of TableGateway
*/
class GenericTableGateway implements TableGateway
{
private QualifiedName $name;
protected TableLocator $tableLocator;
private ?Columns $columns = null;
private ?PrimaryKey $primaryKey = null;
private ?References $references = null;

public function __construct(QualifiedName $name, TableLocator $tableLocator)
{
$this->name = $name;
$this->tableLocator = $tableLocator;
}

public function getName(): QualifiedName
{
return clone $this->name;
}

public function getConnection(): Connection
{
return $this->tableLocator->getConnection();
}

public function getColumns(): Columns
{
return $this->columns ??= new Columns($this->getConnection(), $this->name);
}

public function getPrimaryKey(): PrimaryKey
{
return $this->primaryKey ??= new PrimaryKey($this->getConnection(), $this->name);
}

public function getReferences(): References
{
return $this->references ??= new References($this->getConnection(), $this->name);
}

public function delete($fragments = null, array $parameters = []): ResultSet
{
$fragmentList = FragmentList::normalize($fragments)
->mergeParameters($parameters);

return $this->execute($this->createDeleteStatement($fragmentList), $fragmentList);
}

/**
* {@inheritDoc}
* @psalm-suppress RedundantConditionGivenDocblockType
*/
public function insert($values, $fragments = null, array $parameters = []): ResultSet
{
$fragmentList = FragmentList::normalize($fragments)
->mergeParameters($parameters);

if ($values instanceof SelectProxy) {
$fragmentList->add(new InsertSelectFragment($values));
} elseif ($values instanceof SelectCommon) {
$fragmentList->add(new ClosureFragment(
static function (Insert $insert) use ($values) {
$insert->values = $values;
}
));
} elseif (\is_array($values)) {
if ([] !== $values) {
$fragmentList->add(new SetClauseFragment(
$this->getColumns(),
$this->tableLocator,
$values
));
}
} else {
throw new InvalidArgumentException(sprintf(
"\$values should be either of: an array, an instance of SelectCommon,"
. " an implementation of SelectProxy; %s given",
\is_object($values) ? 'object(' . \get_class($values) . ')' : \gettype($values)
));
}

return $this->execute($this->createInsertStatement($fragmentList), $fragmentList);
}

public function select($fragments = null, array $parameters = []): SelectProxy
{
throw new LogicException('Not implemented yet');
}

public function update(array $set, $fragments = null, array $parameters = []): ResultSet
{
$native = $this->createUpdateStatement($list = new FragmentList(
new SetClauseFragment($this->getColumns(), $this->tableLocator, $set),
FragmentList::normalize($fragments)
->mergeParameters($parameters)
));

return $this->execute($native, $list);
}

/**
* Executes the given $statement possibly using parameters from $fragments
*
* @param NativeStatement $statement
* @param FragmentList $fragments
* @return ResultSet
*/
private function execute(NativeStatement $statement, FragmentList $fragments): ResultSet
{
return [] === $statement->getParameterTypes()
? $this->getConnection()->execute($statement->getSql())
: $statement->executeParams($this->getConnection(), $fragments->getParameters());
}

/**
* Generates a DELETE statement using given fragments
*
* @param FragmentList $fragments
* @return NativeStatement
*/
public function createDeleteStatement(FragmentList $fragments): NativeStatement
{
return $this->tableLocator->createNativeStatementUsingCache(
function () use ($fragments): Delete {
$delete = $this->tableLocator->getStatementFactory()->delete(new UpdateOrDeleteTarget(
$this->getName(),
new Identifier(self::ALIAS_SELF)
));
$fragments->applyTo($delete);

return $delete;
},
$this->generateStatementKey(self::STATEMENT_DELETE, $fragments)
);
}

/**
* Generates an INSERT statement using given fragments
*
* @param FragmentList $fragments
* @return NativeStatement
*/
public function createInsertStatement(FragmentList $fragments): NativeStatement
{
return $this->tableLocator->createNativeStatementUsingCache(
function () use ($fragments): Insert {
$insert = $this->tableLocator->getStatementFactory()->insert(new InsertTarget(
$this->getName(),
new Identifier(TableGateway::ALIAS_SELF)
));
$fragments->applyTo($insert);
return $insert;
},
$this->generateStatementKey(self::STATEMENT_INSERT, $fragments)
);
}

/**
* Generates an UPDATE statement using given fragments
*
* @param FragmentList $fragments
* @return NativeStatement
*/
public function createUpdateStatement(FragmentList $fragments): NativeStatement
{
return $this->tableLocator->createNativeStatementUsingCache(
function () use ($fragments): Update {
$update = $this->tableLocator->getStatementFactory()->update(
new UpdateOrDeleteTarget(
$this->getName(),
new Identifier(TableGateway::ALIAS_SELF)
),
new SetClauseList()
);
$fragments->applyTo($update);
return $update;
},
$this->generateStatementKey(self::STATEMENT_UPDATE, $fragments)
);
}

/**
* Returns a cache key for the statement being generated
*/
protected function generateStatementKey(string $statementType, FragmentList $fragments): ?string
{
if (null === ($fragmentKey = $fragments->getKey())) {
return null;
}
return \sprintf(
'%s.%s.%s.%s',
$this->getConnection()->getConnectionId(),
$statementType,
TableLocator::hash($this->getName()),
$fragmentKey
);
}
}
36 changes: 36 additions & 0 deletions tests/assets/delete-create.sql
@@ -0,0 +1,36 @@
-- Fixture for DeleteTest

create table victim (
id integer not null
);

insert into victim
values (1),
(2),
(3),
(10);

create table foo (
id integer not null,
name text not null,
constraint foo_pkey primary key (id)
);

insert into foo values (1, 'one');
insert into foo values (2, 'two');
insert into foo values (3, 'many');


create table bar (
id integer not null,
foo_id integer null,
name text not null,
constraint bar_pkey primary key (id),
constraint foo_fkey foreign key (foo_id)
references foo (id)
on delete restrict
);

insert into bar values (1, null, 'some stuff');
insert into bar values (2, 2, 'a pair of something');
insert into bar values (3, 2, 'a third one');
7 changes: 7 additions & 0 deletions tests/assets/delete-drop.sql
@@ -0,0 +1,7 @@
-- Cleanup after DeleteTest

drop table if exists victim;

drop table if exists foo cascade;

drop table if exists bar;
17 changes: 17 additions & 0 deletions tests/assets/insert-create.sql
@@ -0,0 +1,17 @@
-- Fixture for InsertTest

create table insert_test (
id integer not null generated by default as identity,
title text default 'Some default title',
added timestamp with time zone default now()
);

create table source_test (
id integer,
title text,

constraint source_test_pkey primary key (id)
);

insert into source_test values (-1, 'Minus first title');
insert into source_test values (-2, 'Minus second title');
5 changes: 5 additions & 0 deletions tests/assets/insert-drop.sql
@@ -0,0 +1,5 @@
-- Cleanup after InsertTest

drop table if exists insert_test;

drop table if exists source_test;

0 comments on commit 724cb82

Please sign in to comment.