packages/sql-semantics/tests/Unit/Core/Binding/FromBinderTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Core\Binding;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Medium;
9use PHPUnit\Framework\Attributes\TestWith;
10use PHPUnit\Framework\TestCase;
11use SqlSemantics\Core\Binder;
12use SqlSemantics\Core\Dialect;
13use SqlSemantics\Core\SchemaBuilder;
14use SqlSemantics\Core\SemanticException;
15use SqlSemantics\Core\Type\Nullability;
16use SqlSemantics\Platform\MySql\Dialect as MySqlDialect;
17use SqlSemantics\Platform\PostgreSql\Dialect as PostgreSqlDialect;
18use SqlSemantics\Platform\Sqlite\Dialect as SqliteDialect;
19
20#[CoversClass(\SqlSemantics\Core\Binding\FromBinder::class)]
21#[CoversClass(\SqlSemantics\Core\Binding\ExpressionBinder::class)]
22#[CoversClass(\SqlSemantics\Core\Binding\ExpressionRules::class)]
23#[CoversClass(\SqlSemantics\Core\Binding\LiteralBinder::class)]
24#[CoversClass(\SqlSemantics\Core\Binding\NullFacts::class)]
25#[CoversClass(\SqlSemantics\Core\Binding\ProjectionBinder::class)]
26#[CoversClass(\SqlSemantics\Core\Binding\SelectBinder::class)]
27#[CoversClass(\SqlSemantics\Core\Binding\SyntaxGuard::class)]
28#[CoversClass(\SqlSemantics\Core\Binding\SelectModifiersBinder::class)]
29#[CoversClass(\SqlSemantics\Core\Binding\TypeResolution::class)]
30#[CoversClass(Binder::class)]
31#[CoversClass(SchemaBuilder::class)]
32#[CoversClass(\SqlSemantics\Core\Ast\DialectParser::class)]
33#[CoversClass(\SqlSemantics\Core\Ast\ColumnReader::class)]
34#[CoversClass(\SqlSemantics\Core\Ast\ConstraintReader::class)]
35#[CoversClass(\SqlSemantics\Core\Ast\Identifiers::class)]
36#[CoversClass(\SqlSemantics\Core\Ast\SchemaReader::class)]
37#[CoversClass(\SqlSemantics\Core\Ast\StatementList::class)]
38#[CoversClass(\SqlSemantics\Core\Ast\TokenGroups::class)]
39#[CoversClass(\SqlSemantics\Core\Ast\Tree::class)]
40#[CoversClass(\SqlSemantics\Core\Ast\TypeReader::class)]
41#[CoversClass(\SqlSemantics\Core\Binding\BoundRelation::class)]
42#[CoversClass(\SqlSemantics\Core\Binding\IdentitySequence::class)]
43#[CoversClass(\SqlSemantics\Core\Binding\Scope::class)]
44#[CoversClass(\SqlSemantics\Core\Binding\TableResolver::class)]
45#[CoversClass(\SqlSemantics\Core\Model\ColumnBinding::class)]
46#[CoversClass(\SqlSemantics\Core\Model\Expression::class)]
47#[CoversClass(\SqlSemantics\Core\Model\Join::class)]
48#[CoversClass(\SqlSemantics\Core\Model\Ordering::class)]
49#[CoversClass(\SqlSemantics\Core\Model\OutputColumn::class)]
50#[CoversClass(\SqlSemantics\Core\Model\BoundSelect::class)]
51#[CoversClass(\SqlSemantics\Core\Model\TableUse::class)]
52#[CoversClass(\SqlSemantics\Core\Schema::class)]
53#[CoversClass(\SqlSemantics\Core\Schema\ColumnDefinition::class)]
54#[CoversClass(\SqlSemantics\Core\Schema\TableConstraint::class)]
55#[CoversClass(\SqlSemantics\Core\Schema\TableDefinition::class)]
56#[CoversClass(SemanticException::class)]
57#[CoversClass(\SqlSemantics\Core\Type\TypeDescriptor::class)]
58#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Core\Policy\SyntaxRules::class)]
59#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\QueryRules::class)]
60#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\Platform::class)]
61#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\TypeRules::class)]
62#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\NameRules::class)]
63#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\PostgreSql\SchemaRules::class)]
64#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\QueryRules::class)]
65#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\Platform::class)]
66#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\TypeRules::class)]
67#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\NameRules::class)]
68#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\Sqlite\SchemaRules::class)]
69#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\QueryRules::class)]
70#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\Platform::class)]
71#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\TypeRules::class)]
72#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\NameRules::class)]
73#[\PHPUnit\Framework\Attributes\UsesClass(\SqlSemantics\Platform\MySql\SchemaRules::class)]
74#[Medium]
75final class FromBinderTest extends TestCase
76{
77 #[TestWith([PostgreSqlDialect::PostgreSql])]
78 #[TestWith([MySqlDialect::MySql])]
79 #[TestWith([SqliteDialect::Sqlite])]
80 public function testJoinedBindsOnBeforeIntroducingThisJoinsNulls(Dialect $dialect): void
81 {
82 $schema = (new SchemaBuilder($dialect))->build('CREATE TABLE users (id INTEGER PRIMARY KEY, parent_id INTEGER, score INTEGER NOT NULL)');
83 $statement = (new Binder($schema))->bind('SELECT b.score FROM users a LEFT JOIN users b ON a.id=b.id WHERE b.score > 0');
84 self::assertInstanceOf(\SqlSemantics\Core\Model\Join::class, $statement->from);
85 self::assertNotNull($statement->from->condition);
86 self::assertSame(Nullability::NotNull, $statement->from->condition->operands[1]->nullability);
87 self::assertNotNull($statement->where);
88 self::assertSame(Nullability::MaybeNull, $statement->where->operands[0]->nullability);
89 self::assertSame(['j0'], $statement->where->operands[0]->nullExtendedBy);
90 }
91
92 public function testJoinPropagatesNestedOuterJoinProvenance(): void
93 {
94 $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build('CREATE TABLE users (id INTEGER PRIMARY KEY, parent_id INTEGER, score INTEGER NOT NULL)');
95 $statement = (new Binder($schema))->bind('SELECT a.id, b.id, c.id FROM users a LEFT JOIN users b ON a.id=b.id RIGHT JOIN users c ON b.id=c.id');
96 self::assertSame(['j0'], $statement->outputs[0]->expression->nullExtendedBy);
97 self::assertSame(['j1', 'j0'], $statement->outputs[1]->expression->nullExtendedBy);
98 self::assertSame([], $statement->outputs[2]->expression->nullExtendedBy);
99 }
100
101 public function testRelationFullJoinExtendsBothSides(): void
102 {
103 $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build('CREATE TABLE users (id INTEGER PRIMARY KEY, parent_id INTEGER, score INTEGER NOT NULL)');
104 $statement = (new Binder($schema))->bind('SELECT a.id, b.id FROM users a FULL JOIN users b ON a.id=b.id');
105 self::assertSame(['j0'], $statement->outputs[0]->expression->nullExtendedBy);
106 self::assertSame(['j0'], $statement->outputs[1]->expression->nullExtendedBy);
107 }
108
109 #[TestWith([PostgreSqlDialect::PostgreSql])]
110 #[TestWith([MySqlDialect::MySql])]
111 #[TestWith([SqliteDialect::Sqlite])]
112 public function testBindCrossJoinHasNoMatchPredicate(Dialect $dialect): void
113 {
114 $schema = (new SchemaBuilder($dialect))->build('CREATE TABLE users (id INTEGER PRIMARY KEY, parent_id INTEGER, score INTEGER NOT NULL)');
115 $statement = (new Binder($schema))->bind('SELECT a.id FROM users a CROSS JOIN users b');
116 self::assertInstanceOf(\SqlSemantics\Core\Model\Join::class, $statement->from);
117 self::assertSame(\SqlSemantics\Core\Model\JoinKind::Cross, $statement->from->kind);
118 self::assertNull($statement->from->condition);
119 }
120
121 public function testSqliteRespectsExplicitDatabaseNames(): void
122 {
123 $builder = new SchemaBuilder(SqliteDialect::Sqlite);
124 $schema = $builder->build('CREATE TABLE main.users (id INTEGER)');
125 $statement = (new Binder($schema))->bind('SELECT u.id FROM main.users AS u');
126 self::assertSame('main', $statement->relations[0]->declaration->schema);
127 }
128
129 public function testTableRejectsAliasColumnLists(): void
130 {
131 $schema = (new SchemaBuilder(PostgreSqlDialect::PostgreSql))->build('CREATE TABLE users (id INTEGER PRIMARY KEY, parent_id INTEGER, score INTEGER NOT NULL)');
132 $this->expectException(SemanticException::class);
133 (new Binder($schema))->bind('SELECT renamed FROM users AS u(renamed)');
134 }
135
136 public function testKindRecognizesRightAndRejectsNatural(): void
137 {
138 $builder = new SchemaBuilder(PostgreSqlDialect::PostgreSql);
139 $tables = new \SqlSemantics\Core\Binding\TableResolver($builder->build('CREATE TABLE users (id INTEGER PRIMARY KEY, parent_id INTEGER, score INTEGER NOT NULL)'), new \SqlSemantics\Core\Ast\Identifiers(PostgreSqlDialect::PostgreSql), 'public');
140 $reader = new \SqlSemantics\Core\Binding\FromBinder($tables, new \SqlSemantics\Core\Binding\IdentitySequence());
141 $source = (new \SqlParser\PostgreSql\PostgreSqlParser())->parse('SELECT 1');
142 self::assertSame(\SqlSemantics\Core\Model\JoinKind::Right, $reader->kind('RIGHT OUTER JOIN', $source));
143 $this->expectException(SemanticException::class);
144 $reader->kind('NATURAL JOIN', $source);
145 }
146}
147