Skip to content

Commit b7b29ca

Browse files
authored
feature: expose tables through from statement of select statement (#2119)
1 parent d8b57b4 commit b7b29ca

File tree

4 files changed

+364
-9
lines changed

4 files changed

+364
-9
lines changed

src/lib/postgresql/src/Flow/PostgreSql/AST/Nodes/From.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ public function isEmpty() : bool
7373
return $this->count() === 0;
7474
}
7575

76+
public function tables() : Tables
77+
{
78+
$tables = [];
79+
80+
foreach ($this->nodes as $node) {
81+
$rangeVar = $node->getRangeVar();
82+
83+
if ($rangeVar !== null) {
84+
$tables[] = new Table($rangeVar);
85+
}
86+
}
87+
88+
return new Tables($tables);
89+
}
90+
7691
private function isValidFromNode(Node $node) : bool
7792
{
7893
return $node->getRangeVar() !== null
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\PostgreSql\AST\Nodes;
6+
7+
/**
8+
* @implements \IteratorAggregate<int, Table>
9+
*/
10+
final readonly class Tables implements \Countable, \IteratorAggregate
11+
{
12+
/**
13+
* @param array<int, Table> $tables
14+
*/
15+
public function __construct(
16+
private array $tables,
17+
) {
18+
}
19+
20+
/**
21+
* @return array<int, Table>
22+
*/
23+
public function all() : array
24+
{
25+
return $this->tables;
26+
}
27+
28+
public function count() : int
29+
{
30+
return \count($this->tables);
31+
}
32+
33+
public function first() : ?Table
34+
{
35+
return $this->tables[0] ?? null;
36+
}
37+
38+
public function get(int $index) : ?Table
39+
{
40+
return $this->tables[$index] ?? null;
41+
}
42+
43+
/**
44+
* @return \Traversable<int, Table>
45+
*/
46+
public function getIterator() : \Traversable
47+
{
48+
return new \ArrayIterator($this->tables);
49+
}
50+
51+
/**
52+
* @phpstan-assert-if-false Table $this->first()
53+
* @phpstan-assert-if-false Table $this->last()
54+
*/
55+
public function isEmpty() : bool
56+
{
57+
return \count($this->tables) === 0;
58+
}
59+
60+
/**
61+
* @phpstan-assert-if-true Table $this->first()
62+
* @phpstan-assert-if-true Table $this->last()
63+
*/
64+
public function isSingle() : bool
65+
{
66+
return \count($this->tables) === 1;
67+
}
68+
69+
public function last() : ?Table
70+
{
71+
if (\count($this->tables) === 0) {
72+
return null;
73+
}
74+
75+
return $this->tables[\count($this->tables) - 1];
76+
}
77+
}

src/lib/postgresql/tests/Flow/PostgreSql/Tests/Unit/AST/Nodes/FromTest.php

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

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

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

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

2424
public function test_accepts_join_expr_node() : void
2525
{
26-
$statement = sql_parse('SELECT * FROM users JOIN orders ON users.id = orders.user_id')->statements()->first();
26+
$statement = sql_parse(
27+
select(star())
28+
->from(table('users'))
29+
->join(table('orders'), eq(col('id', 'users'), col('user_id', 'orders')))
30+
->toSql()
31+
)->statements()->first();
2732
self::assertInstanceOf(SelectStatement::class, $statement);
2833

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

3338
public function test_accepts_range_function_node() : void
3439
{
35-
$statement = sql_parse('SELECT * FROM generate_series(1, 10)')->statements()->first();
40+
$statement = sql_parse(
41+
select(star())
42+
->from(table_func(func('generate_series', [literal(1), literal(10)])))
43+
->toSql()
44+
)->statements()->first();
3645
self::assertInstanceOf(SelectStatement::class, $statement);
3746

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

4251
public function test_accepts_range_subselect_node() : void
4352
{
44-
$statement = sql_parse('SELECT * FROM (SELECT 1) AS t')->statements()->first();
53+
$statement = sql_parse(
54+
select(star())
55+
->from(derived(select(literal(1)), 't'))
56+
->toSql()
57+
)->statements()->first();
4558
self::assertInstanceOf(SelectStatement::class, $statement);
4659

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

5164
public function test_accepts_range_var_node() : void
5265
{
53-
$statement = sql_parse('SELECT * FROM users')->statements()->first();
66+
$statement = sql_parse(
67+
select(star())->from(table('users'))->toSql()
68+
)->statements()->first();
5469
self::assertInstanceOf(SelectStatement::class, $statement);
5570

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

6075
public function test_count_returns_number_of_from_nodes() : void
6176
{
62-
$statement = sql_parse('SELECT * FROM users, orders')->statements()->first();
77+
$statement = sql_parse(
78+
select(star())->from(table('users'), table('orders'))->toSql()
79+
)->statements()->first();
6380
self::assertInstanceOf(SelectStatement::class, $statement);
6481

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

8097
public function test_has_function_returns_false_for_regular_table() : void
8198
{
82-
$statement = sql_parse('SELECT * FROM users')->statements()->first();
99+
$statement = sql_parse(
100+
select(star())->from(table('users'))->toSql()
101+
)->statements()->first();
83102
self::assertInstanceOf(SelectStatement::class, $statement);
84103

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

88107
public function test_has_function_returns_true_for_function_in_from() : void
89108
{
90-
$statement = sql_parse('SELECT * FROM generate_series(1, 10)')->statements()->first();
109+
$statement = sql_parse(
110+
select(star())
111+
->from(table_func(func('generate_series', [literal(1), literal(10)])))
112+
->toSql()
113+
)->statements()->first();
91114
self::assertInstanceOf(SelectStatement::class, $statement);
92115

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

127+
public function test_tables_returns_empty_collection_for_empty_from() : void
128+
{
129+
$from = new From([]);
130+
131+
$tables = $from->tables();
132+
133+
self::assertInstanceOf(Tables::class, $tables);
134+
self::assertTrue($tables->isEmpty());
135+
}
136+
137+
public function test_tables_returns_empty_collection_for_function() : void
138+
{
139+
$statement = sql_parse(
140+
select(star())
141+
->from(table_func(func('generate_series', [literal(1), literal(10)])))
142+
->toSql()
143+
)->statements()->first();
144+
self::assertInstanceOf(SelectStatement::class, $statement);
145+
146+
$tables = $statement->from()->tables();
147+
148+
self::assertTrue($tables->isEmpty());
149+
}
150+
151+
public function test_tables_returns_empty_collection_for_join() : void
152+
{
153+
$statement = sql_parse(
154+
select(star())
155+
->from(table('users'))
156+
->join(table('orders'), eq(col('id', 'users'), col('user_id', 'orders')))
157+
->toSql()
158+
)->statements()->first();
159+
self::assertInstanceOf(SelectStatement::class, $statement);
160+
161+
$tables = $statement->from()->tables();
162+
163+
self::assertTrue($tables->isEmpty());
164+
}
165+
166+
public function test_tables_returns_empty_collection_for_subquery() : void
167+
{
168+
$statement = sql_parse(
169+
select(star())
170+
->from(derived(select(literal(1)), 't'))
171+
->toSql()
172+
)->statements()->first();
173+
self::assertInstanceOf(SelectStatement::class, $statement);
174+
175+
$tables = $statement->from()->tables();
176+
177+
self::assertTrue($tables->isEmpty());
178+
}
179+
180+
public function test_tables_returns_multiple_tables() : void
181+
{
182+
$statement = sql_parse(
183+
select(star())->from(table('users'), table('orders'))->toSql()
184+
)->statements()->first();
185+
self::assertInstanceOf(SelectStatement::class, $statement);
186+
187+
$tables = $statement->from()->tables();
188+
189+
self::assertCount(2, $tables);
190+
self::assertSame('users', $tables->first()?->name());
191+
self::assertSame('orders', $tables->last()?->name());
192+
}
193+
194+
public function test_tables_returns_single_table() : void
195+
{
196+
$statement = sql_parse(
197+
select(star())->from(table('users'))->toSql()
198+
)->statements()->first();
199+
self::assertInstanceOf(SelectStatement::class, $statement);
200+
201+
$tables = $statement->from()->tables();
202+
203+
self::assertTrue($tables->isSingle());
204+
self::assertSame('users', $tables->first()?->name());
205+
}
206+
104207
public function test_throws_exception_for_invalid_node() : void
105208
{
106209
$invalidNode = new Node();

0 commit comments

Comments
 (0)