Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added a means to transform a generated Select into a new one
- Loading branch information
1 parent
85f0930
commit 5390fb2
Showing
3 changed files
with
293 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,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; | ||
} |
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,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() | ||
); | ||
} | ||
} |
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,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); | ||
} | ||
} |