packages/ztd-query-sqlite/tests/Unit/Sql/Relation/RelationSourceParserTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Sql\Relation;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Platform\Sqlite\Sql\Relation\RelationSourceParser;
11use ZtdQuery\Platform\Sqlite\Sql\SqliteLexerProfile;
12use ZtdQuery\Sql\SqlToken;
13use ZtdQuery\Sql\SqlTokenStream;
14
15#[CoversClass(RelationSourceParser::class)]
16#[UsesClass(SqliteLexerProfile::class)]
17final class RelationSourceParserTest extends TestCase
18{
19    public function testReferencesFromClauseFindsJoinsButNotSubqueryBodies(): void
20    {
21        $parser = new RelationSourceParser();
22        self::assertSame(['users', 'orders'], array_column($parser->referencesFromClause('main.users u JOIN orders o ON o.user_id = u.id'), 'name'));
23        self::assertSame(['users', 'orders'], array_column($parser->referencesFromClause('(users JOIN orders ON users.id = orders.user_id)'), 'name'));
24        self::assertSame([], $parser->referencesFromClause('(SELECT * FROM hidden)'));
25        self::assertSame([], $parser->referencesFromClause('json_each(items)'));
26        self::assertSame([], $parser->referencesFromClause('()'));
27    }
28
29    public function testNestedReferencesTranslatesOffsets(): void
30    {
31        $parser = new RelationSourceParser();
32        self::assertSame([['name' => 'users', 'start' => 3, 'unqualifiedStart' => 3, 'end' => 8]], $parser->nestedReferences('xx(users)', 3, 8));
33        self::assertSame([], $parser->nestedReferences('(SELECT 1)', 1, 9));
34    }
35
36    public function testClosingTokenReturnsMatchingTopLevelEnd(): void
37    {
38        $parser = new RelationSourceParser();
39        $tokens = SqlTokenStream::tokenize('(users)', SqliteLexerProfile::create())->significantTokens();
40        self::assertSame($tokens[2], $parser->closingToken($tokens, 0));
41        self::assertNull($parser->closingToken([], 0));
42    }
43
44    public function testReferenceAtRetainsQualifiedOffsets(): void
45    {
46        $parser = new RelationSourceParser();
47        $sql = 'main.users';
48        self::assertSame(['name' => 'users', 'start' => 0, 'unqualifiedStart' => 5, 'end' => 10], $parser->referenceAt($sql, SqlTokenStream::tokenize($sql, SqliteLexerProfile::create())->significantTokens(), 0));
49        self::assertNull($parser->referenceAt('SELECT', SqlTokenStream::tokenize('SELECT', SqliteLexerProfile::create())->significantTokens(), 0));
50    }
51
52    public function testIdentifierComponentAtAcceptsWordsAndQuotedNames(): void
53    {
54        $parser = new RelationSourceParser();
55        self::assertSame(['full name', 1, 0, 0, 11], $parser->identifierComponentAt(SqlTokenStream::tokenize('"full name"', SqliteLexerProfile::create())->significantTokens(), 0));
56        self::assertNull($parser->identifierComponentAt([], 0));
57        self::assertNull($parser->identifierComponentAt(SqlTokenStream::tokenize('123', SqliteLexerProfile::create())->significantTokens(), 0));
58    }
59
60    public function testFindFromEndStopsAtClauseBoundary(): void
61    {
62        $parser = new RelationSourceParser();
63        $sql = 'SELECT * FROM users ORDER BY id';
64        $tokens = SqlTokenStream::tokenize($sql, SqliteLexerProfile::create())->significantTokens();
65        self::assertSame(20, $parser->findFromEnd($sql, $tokens, $tokens[2]));
66        self::assertSame(19, $parser->findFromEnd('SELECT * FROM users', array_slice($tokens, 0, 4), $tokens[2]));
67    }
68
69    public function testMatchesKeywordSequenceChecksEveryPart(): void
70    {
71        $parser = new RelationSourceParser();
72        $tokens = SqlTokenStream::tokenize('ORDER BY id', SqliteLexerProfile::create())->significantTokens();
73        self::assertTrue($parser->matchesKeywordSequence($tokens, 0, ['ORDER', 'BY']));
74        self::assertFalse($parser->matchesKeywordSequence($tokens, 0, ['ORDER', 'FROM']));
75        self::assertFalse($parser->matchesKeywordSequence([], 0, ['ORDER']));
76    }
77
78    public function testTokensOmitsCommentsAndWhitespace(): void
79    {
80        self::assertSame(['SELECT', '1'], array_map(static fn (SqlToken $token): string => $token->text, (new RelationSourceParser())->tokens(' SELECT /* x */ 1 ')));
81    }
82
83}
84