Skip to content

Commit

Permalink
Added a means to transform a generated Select into a new one
Browse files Browse the repository at this point in the history
  • Loading branch information
sad-spirit committed Aug 22, 2023
1 parent 85f0930 commit 5390fb2
Show file tree
Hide file tree
Showing 3 changed files with 293 additions and 0 deletions.
147 changes: 147 additions & 0 deletions src/SelectTransformer.php
@@ -0,0 +1,147 @@
<?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;
use sad_spirit\pg_wrapper\Connection;
use sad_spirit\pg_builder\{
SelectCommon,
NativeStatement,
nodes\QualifiedName
};

/**
* A decorator for a SelectProxy replacing its generated Select statement with another one
*
* Fragments can only modify the child Nodes of an existing Statement, sometimes it is needed to return a new one.
* A subclass of SelectTransformer may e.g.
* - Combine the given Select with another one using UNION, returning a new SetOpSelect object
* - Put the original Select into a CTE or a sub-query in FROM, returning the outer Select
*/
abstract class SelectTransformer implements SelectProxy
{
protected TableLocator $tableLocator;
protected SelectProxy $wrapped;
private ?string $key;

/**
* Constructor, sets the Select being decorated and additional dependencies
*
* @param SelectProxy $wrapped
* @param TableLocator $tableLocator
* @param string|null $key Passing null as the key will make the generated statement non-cacheable
*/
public function __construct(SelectProxy $wrapped, TableLocator $tableLocator, ?string $key = null)
{
$this->wrapped = $wrapped;
$this->tableLocator = $tableLocator;
$this->key = $key;
}

public function getKey(): ?string
{
if (
null === ($wrappedKey = $this->wrapped->getKey())
|| null === $this->key
) {
return null;
}
return TableLocator::hash([
$this->key,
$wrappedKey
]);
}

public function getParameterHolder(): ParameterHolder
{
return $this->wrapped->getParameterHolder();
}

public function executeCount()
{
return $this->wrapped->executeCount();
}

public function getIterator(): ResultSet
{
$native = $this->createSelectStatement();
return [] === $native->getParameterTypes()
? $this->wrapped->getConnection()->execute($native->getSql())
: $native->executeParams($this->wrapped->getConnection(), $this->getParameterHolder()->getParameters());
}

public function createSelectStatement(): NativeStatement
{
if (
null === ($wrappedKey = $this->wrapped->getKey())
|| null === $this->key
) {
$cacheKey = null;
} else {
$cacheKey = \sprintf(
'%s.%s.%s.%s',
$this->getConnection()->getConnectionId(),
TableGateway::STATEMENT_SELECT,
TableLocator::hash([
$this->getName(),
$this->key
]),
$wrappedKey
);
}

return $this->tableLocator->createNativeStatementUsingCache(
\Closure::fromCallable([$this, 'createSelectAST']),
$cacheKey
);
}

public function createSelectAST(): SelectCommon
{
return $this->transform($this->wrapped->createSelectAST());
}

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

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

public function getColumns(): metadata\Columns
{
return $this->wrapped->getColumns();
}

public function getPrimaryKey(): metadata\PrimaryKey
{
return $this->wrapped->getPrimaryKey();
}

public function getReferences(): metadata\References
{
return $this->wrapped->getReferences();
}

/**
* Transforms the given statement returning a new one
*
* @param SelectCommon $original
* @return SelectCommon
*/
abstract protected function transform(SelectCommon $original): SelectCommon;
}
120 changes: 120 additions & 0 deletions tests/SelectTransformerTest.php
@@ -0,0 +1,120 @@
<?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.
*/

/** @noinspection SqlResolve */

declare(strict_types=1);

namespace sad_spirit\pg_gateway\tests;

use sad_spirit\pg_gateway\SelectProxy;
use sad_spirit\pg_gateway\TableLocator;
use sad_spirit\pg_builder\nodes\QualifiedName;
use sad_spirit\pg_gateway\gateways\GenericTableGateway;
use sad_spirit\pg_gateway\tests\assets\SelectTransformerImplementation;

class SelectTransformerTest extends DatabaseBackedTest
{
use NormalizeWhitespace;

protected static ?TableLocator $tableLocator;

public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
self::$tableLocator = new TableLocator(self::$connection);
self::executeSqlFromFile(self::$connection, 'delete-drop.sql', 'delete-create.sql');
}

public static function tearDownAfterClass(): void
{
self::executeSqlFromFile(self::$connection, 'delete-drop.sql');
self::$tableLocator = null;
self::$connection = null;
}

public function testKeyIsNullIfOwnKeyIsNull(): void
{
$tableLocator = new TableLocator(self::$connection, null, null, $this->getMockForNoCache());

$mockSelect = $this::getMockBuilder(SelectProxy::class)
->onlyMethods(['getKey', 'createSelectAST'])
->getMockForAbstractClass();
$mockSelect->expects($this::atLeastOnce())
->method('getKey')
->willReturn('a select key');
$mockSelect->expects($this::atLeastOnce())
->method('createSelectAST')
->willReturnCallback(
fn() => $tableLocator->getStatementFactory()->createFromString('select self.* from foo as self')
);

$transformer = new SelectTransformerImplementation($mockSelect, $tableLocator);

$this::assertEquals(null, $transformer->getKey());
$transformer->createSelectStatement();
}

public function testKeyIsNullIfSelectKeyIsNull(): void
{
$tableLocator = new TableLocator(self::$connection, null, null, $this->getMockForNoCache());

$mockSelect = $this::getMockBuilder(SelectProxy::class)
->onlyMethods(['getKey', 'createSelectAST'])
->getMockForAbstractClass();
$mockSelect->expects($this::atLeastOnce())
->method('getKey')
->willReturn(null);
$mockSelect->expects($this::atLeastOnce())
->method('createSelectAST')
->willReturnCallback(
fn() => $tableLocator->getStatementFactory()->createFromString('select self.* from foo as self')
);

$transformer = new SelectTransformerImplementation($mockSelect, $tableLocator, 'a transformer key');

$this::assertEquals(null, $transformer->getKey());
$transformer->createSelectStatement();
}

public function testDelegatesToWrapped(): void
{
$gateway = new GenericTableGateway(new QualifiedName('victim'), self::$tableLocator);
$select = $gateway->select(null, ['foo' => 'bar']);
$transformer = new SelectTransformerImplementation($select, self::$tableLocator);

$this::assertSame($select->getConnection(), $transformer->getConnection());
$this::assertEquals($select->getName(), $transformer->getName());
$this::assertSame($select->getColumns(), $transformer->getColumns());
$this::assertSame($select->getPrimaryKey(), $transformer->getPrimaryKey());
$this::assertSame($select->getReferences(), $transformer->getReferences());
$this::assertEquals($select->getParameterHolder(), $transformer->getParameterHolder());
}

public function testSelectCountIsNotTransformed(): void
{
$gateway = new GenericTableGateway(new QualifiedName('victim'), self::$tableLocator);
$transformer = new SelectTransformerImplementation($gateway->select(), self::$tableLocator);

$this::assertEquals(4, $transformer->executeCount());
}

public function testTransformSelect(): void
{
$gateway = new GenericTableGateway(new QualifiedName('victim'), self::$tableLocator);
$transformer = new SelectTransformerImplementation($gateway->select(), self::$tableLocator, 'a key');

$this::assertStringEqualsStringNormalizingWhitespace(
"select self.* from victim as self union all select self.* from victim as self",
$transformer->createSelectStatement()->getSql()
);
}
}
26 changes: 26 additions & 0 deletions tests/assets/SelectTransformerImplementation.php
@@ -0,0 +1,26 @@
<?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\tests\assets;

use sad_spirit\pg_builder\SetOpSelect;
use sad_spirit\pg_builder\SelectCommon;
use sad_spirit\pg_gateway\SelectTransformer;

class SelectTransformerImplementation extends SelectTransformer
{
protected function transform(SelectCommon $original): SelectCommon
{
return new SetOpSelect(clone $original, clone $original, SetOpSelect::UNION_ALL);
}
}

0 comments on commit 5390fb2

Please sign in to comment.