packages/sql-semantics/tests/Unit/Core/Ast/TreeTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Core\Ast;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Medium;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlSemantics\Core\SemanticException;
12
13#[CoversClass(\SqlSemantics\Core\Ast\Tree::class)]
14#[UsesClass(\SqlSemantics\Core\Binding\ExpressionBinder::class)]
15#[UsesClass(\SqlSemantics\Core\Binding\ExpressionRules::class)]
16#[UsesClass(\SqlSemantics\Core\Binding\FromBinder::class)]
17#[UsesClass(\SqlSemantics\Core\Binding\LiteralBinder::class)]
18#[UsesClass(\SqlSemantics\Core\Binding\NullFacts::class)]
19#[UsesClass(\SqlSemantics\Core\Binding\ProjectionBinder::class)]
20#[UsesClass(\SqlSemantics\Core\Binding\SelectBinder::class)]
21#[UsesClass(\SqlSemantics\Core\Binding\SyntaxGuard::class)]
22#[UsesClass(\SqlSemantics\Core\Binding\SelectModifiersBinder::class)]
23#[UsesClass(\SqlSemantics\Core\Binding\TypeResolution::class)]
24#[UsesClass(\SqlSemantics\Core\Binder::class)]
25#[UsesClass(\SqlSemantics\Core\SchemaBuilder::class)]
26#[UsesClass(\SqlSemantics\Core\Ast\DialectParser::class)]
27#[UsesClass(\SqlSemantics\Core\Ast\ColumnReader::class)]
28#[UsesClass(\SqlSemantics\Core\Ast\ConstraintReader::class)]
29#[UsesClass(\SqlSemantics\Core\Ast\Identifiers::class)]
30#[UsesClass(\SqlSemantics\Core\Ast\SchemaReader::class)]
31#[UsesClass(\SqlSemantics\Core\Ast\StatementList::class)]
32#[UsesClass(\SqlSemantics\Core\Ast\TokenGroups::class)]
33#[UsesClass(\SqlSemantics\Core\Ast\TypeReader::class)]
34#[UsesClass(\SqlSemantics\Core\Binding\BoundRelation::class)]
35#[UsesClass(\SqlSemantics\Core\Binding\IdentitySequence::class)]
36#[UsesClass(\SqlSemantics\Core\Binding\Scope::class)]
37#[UsesClass(\SqlSemantics\Core\Binding\TableResolver::class)]
38#[UsesClass(\SqlSemantics\Core\Model\ColumnBinding::class)]
39#[UsesClass(\SqlSemantics\Core\Model\Expression::class)]
40#[UsesClass(\SqlSemantics\Core\Model\Join::class)]
41#[UsesClass(\SqlSemantics\Core\Model\Ordering::class)]
42#[UsesClass(\SqlSemantics\Core\Model\OutputColumn::class)]
43#[UsesClass(\SqlSemantics\Core\Model\BoundSelect::class)]
44#[UsesClass(\SqlSemantics\Core\Model\TableUse::class)]
45#[UsesClass(\SqlSemantics\Core\Schema::class)]
46#[UsesClass(\SqlSemantics\Core\Schema\ColumnDefinition::class)]
47#[UsesClass(\SqlSemantics\Core\Schema\TableConstraint::class)]
48#[UsesClass(\SqlSemantics\Core\Schema\TableDefinition::class)]
49#[UsesClass(SemanticException::class)]
50#[UsesClass(\SqlSemantics\Core\Type\TypeDescriptor::class)]
51#[UsesClass(\SqlSemantics\Core\Policy\SyntaxRules::class)]
52#[UsesClass(\SqlSemantics\Platform\PostgreSql\QueryRules::class)]
53#[UsesClass(\SqlSemantics\Platform\PostgreSql\Platform::class)]
54#[UsesClass(\SqlSemantics\Platform\PostgreSql\TypeRules::class)]
55#[UsesClass(\SqlSemantics\Platform\PostgreSql\NameRules::class)]
56#[UsesClass(\SqlSemantics\Platform\PostgreSql\SchemaRules::class)]
57#[UsesClass(\SqlSemantics\Platform\Sqlite\QueryRules::class)]
58#[UsesClass(\SqlSemantics\Platform\Sqlite\Platform::class)]
59#[UsesClass(\SqlSemantics\Platform\Sqlite\TypeRules::class)]
60#[UsesClass(\SqlSemantics\Platform\Sqlite\NameRules::class)]
61#[UsesClass(\SqlSemantics\Platform\Sqlite\SchemaRules::class)]
62#[UsesClass(\SqlSemantics\Platform\MySql\QueryRules::class)]
63#[UsesClass(\SqlSemantics\Platform\MySql\Platform::class)]
64#[UsesClass(\SqlSemantics\Platform\MySql\TypeRules::class)]
65#[UsesClass(\SqlSemantics\Platform\MySql\NameRules::class)]
66#[UsesClass(\SqlSemantics\Platform\MySql\SchemaRules::class)]
67#[Medium]
68final class TreeTest extends TestCase
69{
70    public function testOuterStopsAtTheRequestedGrammarBoundary(): void
71    {
72        $tree = (new \SqlParser\PostgreSql\PostgreSqlParser())->parse('SELECT 1+2');
73        $nodes = \SqlSemantics\Core\Ast\Tree::outer($tree, ['a_expr']);
74        self::assertCount(1, $nodes);
75        self::assertSame('1 + 2', \SqlSemantics\Core\Ast\Tree::text($nodes[0]));
76        self::assertCount(3, $tree->find('a_expr'));
77    }
78
79    public function testChildDoesNotSearchNestedScopes(): void
80    {
81        $tree = (new \SqlParser\PostgreSql\PostgreSqlParser())->parse('SELECT id FROM users');
82        self::assertNull(\SqlSemantics\Core\Ast\Tree::child($tree, ['columnref']));
83        self::assertNotNull(\SqlSemantics\Core\Ast\Tree::child($tree->find('c_expr')[0], ['columnref']));
84    }
85
86    public function testSignificantRemovesEmptyProductions(): void
87    {
88        $token = new \SqlParser\Lexer\Token(1, 'ICONST', '1', 7);
89        $node = new \SqlParser\Parser\Node('expr', 0, [new \SqlParser\Parser\Node('empty', 0, []), $token]);
90        self::assertSame([$token], \SqlSemantics\Core\Ast\Tree::significant($node));
91    }
92
93    public function testTextKeepsTerminalSpellings(): void
94    {
95        $token = new \SqlParser\Lexer\Token(1, 'SCONST', "'a b'", 0);
96        self::assertSame("'a b'", \SqlSemantics\Core\Ast\Tree::text($token));
97    }
98
99    public function testUnsupportedCarriesOriginalSyntax(): void
100    {
101        $node = new \SqlParser\Parser\Node('expr', 0, []);
102        $this->expectException(SemanticException::class);
103        $this->expectExceptionMessage('Unsupported custom operation');
104        \SqlSemantics\Core\Ast\Tree::unsupported($node, 'custom operation');
105    }
106
107    public function testAssertChildrenRejectsUnknownClauses(): void
108    {
109        $tree = (new \SqlParser\PostgreSql\PostgreSqlParser())->parse('SELECT id FROM users');
110        $this->expectException(SemanticException::class);
111        \SqlSemantics\Core\Ast\Tree::assertChildren($tree->find('simple_select')[0], [], ['SELECT']);
112    }
113}
114