Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/lib/postgresql/src/Flow/PostgreSql/AST/Nodes/From.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ public function isEmpty() : bool
return $this->count() === 0;
}

public function tables() : Tables
{
$tables = [];

foreach ($this->nodes as $node) {
$rangeVar = $node->getRangeVar();

if ($rangeVar !== null) {
$tables[] = new Table($rangeVar);
}
}

return new Tables($tables);
}

private function isValidFromNode(Node $node) : bool
{
return $node->getRangeVar() !== null
Expand Down
77 changes: 77 additions & 0 deletions src/lib/postgresql/src/Flow/PostgreSql/AST/Nodes/Tables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Flow\PostgreSql\AST\Nodes;

/**
* @implements \IteratorAggregate<int, Table>
*/
final readonly class Tables implements \Countable, \IteratorAggregate
{
/**
* @param array<int, Table> $tables
*/
public function __construct(
private array $tables,
) {
}

/**
* @return array<int, Table>
*/
public function all() : array
{
return $this->tables;
}

public function count() : int
{
return \count($this->tables);
}

public function first() : ?Table
{
return $this->tables[0] ?? null;
}

public function get(int $index) : ?Table
{
return $this->tables[$index] ?? null;
}

/**
* @return \Traversable<int, Table>
*/
public function getIterator() : \Traversable
{
return new \ArrayIterator($this->tables);
}

/**
* @phpstan-assert-if-false Table $this->first()
* @phpstan-assert-if-false Table $this->last()
*/
public function isEmpty() : bool
{
return \count($this->tables) === 0;
}

/**
* @phpstan-assert-if-true Table $this->first()
* @phpstan-assert-if-true Table $this->last()
*/
public function isSingle() : bool
{
return \count($this->tables) === 1;
}

public function last() : ?Table
{
if (\count($this->tables) === 0) {
return null;
}

return $this->tables[\count($this->tables) - 1];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

namespace Flow\PostgreSql\Tests\Unit\AST\Nodes;

use function Flow\PostgreSql\DSL\sql_parse;
use function Flow\PostgreSql\DSL\{col, derived, eq, func, literal, select, sql_parse, star, table, table_func};

use Flow\PostgreSql\AST\Nodes\Exception\InvalidFromNodeException;
use Flow\PostgreSql\AST\Nodes\From;
use Flow\PostgreSql\AST\Nodes\{From, Tables};
use Flow\PostgreSql\AST\Nodes\Statement\SelectStatement;
use Flow\PostgreSql\Protobuf\AST\Node;
use PHPUnit\Framework\TestCase;
Expand All @@ -23,7 +23,12 @@ protected function setUp() : void

public function test_accepts_join_expr_node() : void
{
$statement = sql_parse('SELECT * FROM users JOIN orders ON users.id = orders.user_id')->statements()->first();
$statement = sql_parse(
select(star())
->from(table('users'))
->join(table('orders'), eq(col('id', 'users'), col('user_id', 'orders')))
->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

$from = $statement->from();
Expand All @@ -32,7 +37,11 @@ public function test_accepts_join_expr_node() : void

public function test_accepts_range_function_node() : void
{
$statement = sql_parse('SELECT * FROM generate_series(1, 10)')->statements()->first();
$statement = sql_parse(
select(star())
->from(table_func(func('generate_series', [literal(1), literal(10)])))
->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

$from = $statement->from();
Expand All @@ -41,7 +50,11 @@ public function test_accepts_range_function_node() : void

public function test_accepts_range_subselect_node() : void
{
$statement = sql_parse('SELECT * FROM (SELECT 1) AS t')->statements()->first();
$statement = sql_parse(
select(star())
->from(derived(select(literal(1)), 't'))
->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

$from = $statement->from();
Expand All @@ -50,7 +63,9 @@ public function test_accepts_range_subselect_node() : void

public function test_accepts_range_var_node() : void
{
$statement = sql_parse('SELECT * FROM users')->statements()->first();
$statement = sql_parse(
select(star())->from(table('users'))->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

$from = $statement->from();
Expand All @@ -59,7 +74,9 @@ public function test_accepts_range_var_node() : void

public function test_count_returns_number_of_from_nodes() : void
{
$statement = sql_parse('SELECT * FROM users, orders')->statements()->first();
$statement = sql_parse(
select(star())->from(table('users'), table('orders'))->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

self::assertCount(2, $statement->from());
Expand All @@ -79,15 +96,21 @@ public function test_empty_nodes_array_creates_empty_from() : void

public function test_has_function_returns_false_for_regular_table() : void
{
$statement = sql_parse('SELECT * FROM users')->statements()->first();
$statement = sql_parse(
select(star())->from(table('users'))->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

self::assertFalse($statement->from()->hasFunction());
}

public function test_has_function_returns_true_for_function_in_from() : void
{
$statement = sql_parse('SELECT * FROM generate_series(1, 10)')->statements()->first();
$statement = sql_parse(
select(star())
->from(table_func(func('generate_series', [literal(1), literal(10)])))
->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

self::assertTrue($statement->from()->hasFunction());
Expand All @@ -101,6 +124,86 @@ public function test_has_function_returns_true_for_unnest_function() : void
self::assertTrue($statement->from()->hasFunction());
}

public function test_tables_returns_empty_collection_for_empty_from() : void
{
$from = new From([]);

$tables = $from->tables();

self::assertInstanceOf(Tables::class, $tables);
self::assertTrue($tables->isEmpty());
}

public function test_tables_returns_empty_collection_for_function() : void
{
$statement = sql_parse(
select(star())
->from(table_func(func('generate_series', [literal(1), literal(10)])))
->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

$tables = $statement->from()->tables();

self::assertTrue($tables->isEmpty());
}

public function test_tables_returns_empty_collection_for_join() : void
{
$statement = sql_parse(
select(star())
->from(table('users'))
->join(table('orders'), eq(col('id', 'users'), col('user_id', 'orders')))
->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

$tables = $statement->from()->tables();

self::assertTrue($tables->isEmpty());
}

public function test_tables_returns_empty_collection_for_subquery() : void
{
$statement = sql_parse(
select(star())
->from(derived(select(literal(1)), 't'))
->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

$tables = $statement->from()->tables();

self::assertTrue($tables->isEmpty());
}

public function test_tables_returns_multiple_tables() : void
{
$statement = sql_parse(
select(star())->from(table('users'), table('orders'))->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

$tables = $statement->from()->tables();

self::assertCount(2, $tables);
self::assertSame('users', $tables->first()?->name());
self::assertSame('orders', $tables->last()?->name());
}

public function test_tables_returns_single_table() : void
{
$statement = sql_parse(
select(star())->from(table('users'))->toSql()
)->statements()->first();
self::assertInstanceOf(SelectStatement::class, $statement);

$tables = $statement->from()->tables();

self::assertTrue($tables->isSingle());
self::assertSame('users', $tables->first()?->name());
}

public function test_throws_exception_for_invalid_node() : void
{
$invalidNode = new Node();
Expand Down
Loading
Loading