Skip to content

Commit

Permalink
Possibility to append expressions to query output
Browse files Browse the repository at this point in the history
  • Loading branch information
sad-spirit committed Aug 25, 2023
1 parent f4eacb4 commit be43e4b
Show file tree
Hide file tree
Showing 6 changed files with 383 additions and 3 deletions.
58 changes: 58 additions & 0 deletions src/fragments/target_list/ConditionAppender.php
@@ -0,0 +1,58 @@
<?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\target_list;

use sad_spirit\pg_gateway\{
Condition,
TableLocator,
fragments\TargetListManipulator
};
use sad_spirit\pg_builder\nodes\{
Identifier,
TargetElement,
lists\TargetList
};

/**
* Adds an expression created by Condition to the TargetList
*/
class ConditionAppender extends TargetListManipulator
{
private Condition $condition;
private ?string $alias;

public function __construct(Condition $condition, ?string $alias = null)
{
$this->condition = $condition;
$this->alias = $alias;
}

public function modifyTargetList(TargetList $targetList): void
{
$targetList[] = new TargetElement(
$this->condition->generateExpression(),
null === $this->alias ? null : new Identifier($this->alias)
);
}

public function getKey(): ?string
{
$conditionKey = $this->condition->getKey();
$aliasKey = null === $this->alias ? '' : '.' . TableLocator::hash($this->alias);

return null === $conditionKey
? null
: 'output.' . $conditionKey . $aliasKey;
}
}
61 changes: 61 additions & 0 deletions src/fragments/target_list/SqlStringAppender.php
@@ -0,0 +1,61 @@
<?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\target_list;

use sad_spirit\pg_gateway\{
TableLocator,
exceptions\LogicException,
fragments\TargetListManipulator
};
use sad_spirit\pg_builder\Parser;
use sad_spirit\pg_builder\nodes\{
Identifier,
TargetElement,
lists\TargetList
};

/**
* Parses the given SQL string and merge()s it to the TargetList
*/
class SqlStringAppender extends TargetListManipulator
{
private Parser $parser;
private string $sql;
private ?string $alias;

public function __construct(Parser $parser, string $sql, ?string $alias = null)
{
$this->parser = $parser;
$this->sql = $sql;
$this->alias = $alias;
}

public function modifyTargetList(TargetList $targetList): void
{
$parsed = $this->parser->parseTargetList($this->sql);

if (null === $this->alias) {
$targetList->merge($parsed);
} elseif (1 === \count($parsed)) {
$targetList[] = new TargetElement(clone $parsed[0]->expression, new Identifier($this->alias));
} else {
throw new LogicException("Parsing resulted in multiple expressions, cannot apply alias");
}
}

public function getKey(): ?string
{
return TableLocator::hash([self::class, $this->sql, $this->alias]);
}
}
63 changes: 60 additions & 3 deletions src/gateways/GenericTableGateway.php
Expand Up @@ -14,6 +14,7 @@
namespace sad_spirit\pg_gateway\gateways;

use sad_spirit\pg_gateway\{
Condition,
FragmentList,
SelectProxy,
TableGateway,
Expand All @@ -24,9 +25,6 @@
builders\JoinBuilder,
builders\ScalarSubqueryBuilder,
exceptions\InvalidArgumentException,
fragments\ClosureFragment,
fragments\InsertSelectFragment,
fragments\SetClauseFragment,
metadata\Columns,
metadata\PrimaryKey,
metadata\References
Expand All @@ -41,6 +39,16 @@
column\NotAllCondition,
column\OperatorCondition
};
use sad_spirit\pg_gateway\fragments\{
ClosureFragment,
InsertSelectFragment,
ReturningClauseFragment,
SelectListFragment,
SetClauseFragment,
TargetListManipulator,
target_list\ConditionAppender,
target_list\SqlStringAppender
};
use sad_spirit\pg_builder\{
Delete,
Insert,
Expand Down Expand Up @@ -446,6 +454,8 @@ public function returningColumns(): ColumnsBuilder
/**
* Creates a builder for configuring a scalar subquery to be added to the output list of a SELECT statement
*
* While the companion `returningSubquery()` method is possible, it's unlikely to be used
*
* @param SelectProxy $select
* @return ScalarSubqueryBuilder
*/
Expand All @@ -454,6 +464,53 @@ public function outputSubquery(SelectProxy $select): ScalarSubqueryBuilder
return new ScalarSubqueryBuilder($this, $select);
}

/**
* Adds expression(s) to the list of columns returned by a SELECT statement
*
* @param string|Condition $expression
* @param string|null $alias
* @return SelectListFragment
*/
public function outputExpression($expression, ?string $alias = null): SelectListFragment
{
return new SelectListFragment($this->expressionToManipulator($expression, $alias));
}

/**
* Adds expression(s) to the list of columns in the RETURNING clause
*
* @param string|Condition $expression
* @param string|null $alias
* @return ReturningClauseFragment
*/
public function returningExpression($expression, ?string $alias = null): ReturningClauseFragment
{
return new ReturningClauseFragment($this->expressionToManipulator($expression, $alias));
}

/**
* Returns the proper TargetListManipulator for the given expression
*
* @param string|Condition $expression
* @param string|null $alias
* @return TargetListManipulator
* @psalm-suppress RedundantConditionGivenDocblockType
* @psalm-suppress DocblockTypeContradiction
*/
private function expressionToManipulator($expression, ?string $alias = null): TargetListManipulator
{
if (\is_string($expression)) {
return new SqlStringAppender($this->tableLocator->getParser(), $expression, $alias);
} elseif ($expression instanceof Condition) {
return new ConditionAppender($expression, $alias);
} else {
throw new InvalidArgumentException(\sprintf(
"An SQL string or Condition instance expected, %s given",
\is_object($expression) ? 'object(' . \get_class($expression) . ')' : \gettype($expression)
));
}
}

/**
* Creates a Builder for configuring a join to the given table
*
Expand Down
86 changes: 86 additions & 0 deletions tests/fragments/target_list/ConditionAppenderTest.php
@@ -0,0 +1,86 @@
<?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\fragments\target_list;

use PHPUnit\Framework\TestCase;
use sad_spirit\pg_gateway\fragments\target_list\ConditionAppender;
use sad_spirit\pg_gateway\tests\NormalizeWhitespace;
use sad_spirit\pg_gateway\tests\assets\ConditionImplementation;
use sad_spirit\pg_builder\Select;
use sad_spirit\pg_builder\StatementFactory;
use sad_spirit\pg_builder\nodes\expressions\KeywordConstant;

class ConditionAppenderTest extends TestCase
{
use NormalizeWhitespace;

public function testKeyIsNullIfConditionKeyIsNull(): void
{
$manipulator = new ConditionAppender(
new ConditionImplementation(new KeywordConstant(KeywordConstant::FALSE), null)
);

$this::assertNull($manipulator->getKey());
}

public function testKeyDependsOnConditionKey(): void
{
$manipulator = new ConditionAppender(
new ConditionImplementation(new KeywordConstant(KeywordConstant::FALSE), 'some_key')
);

$this::assertNotNull($manipulator->getKey());
$this::assertStringContainsString('some_key', $manipulator->getKey());
}

public function testKeyDependsOnAlias(): void
{
$manipulatorOne = new ConditionAppender(
new ConditionImplementation(new KeywordConstant(KeywordConstant::FALSE), 'some_key'),
'alias_one'
);
$manipulatorTwo = new ConditionAppender(
new ConditionImplementation(new KeywordConstant(KeywordConstant::FALSE), 'some_key'),
'alias_two'
);
$manipulatorThree = new ConditionAppender(
new ConditionImplementation(new KeywordConstant(KeywordConstant::FALSE), 'some_key'),
'alias_two'
);

$this::assertNotEquals($manipulatorOne->getKey(), $manipulatorTwo->getKey());
$this::assertEquals($manipulatorTwo->getKey(), $manipulatorThree->getKey());
}

public function testModifyTargetList(): void
{
$factory = new StatementFactory();

/** @var Select $select */
$select = $factory->createFromString('select self.foo as bar, quux.xyzzy');
(new ConditionAppender(
new ConditionImplementation(new KeywordConstant(KeywordConstant::NULL)),
'baz'
))->modifyTargetList($select->list);

(new ConditionAppender(
new ConditionImplementation(new KeywordConstant(KeywordConstant::TRUE)),
))->modifyTargetList($select->list);

$this::assertStringEqualsStringNormalizingWhitespace(
'select self.foo as bar, quux.xyzzy, null as baz, true',
$factory->createFromAST($select)->getSql()
);
}
}

0 comments on commit be43e4b

Please sign in to comment.