packages/sql-catalog/tests/Unit/Core/Sql/TableReaderTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Core\Sql;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlCatalog\Core\Sql\PlaceholderScanner;
12use SqlCatalog\Core\Sql\SqlLexer;
13use SqlCatalog\Core\Sql\SqlToken;
14use SqlCatalog\Core\Sql\SqlTokenKind;
15use SqlCatalog\Core\Sql\TableReader;
16use SqlCatalog\Core\Text\LiteralText;
17use SqlCatalog\Core\Text\Origin;
18use SqlCatalog\Core\Text\TextHole;
19use SqlCatalog\Core\Text\TextPattern;
20use SqlCatalog\Core\Type\TypeShape;
21
22#[CoversClass(TableReader::class)]
23#[UsesClass(PlaceholderScanner::class)]
24#[UsesClass(SqlLexer::class)]
25#[UsesClass(SqlToken::class)]
26#[UsesClass(LiteralText::class)]
27#[UsesClass(TextHole::class)]
28#[UsesClass(TextPattern::class)]
29#[UsesClass(TypeShape::class)]
30final class TableReaderTest extends TestCase
31{
32    /**
33     * @param list<string> $expected
34     */
35    #[DataProvider('providerRead')]
36    public function testRead(string $sql, array $expected): void
37    {
38        self::assertSame($expected, (new TableReader())->read(TextPattern::fromText($sql)));
39    }
40
41    /**
42     * @return list<array{string, list<string>}>
43     */
44    public static function providerRead(): array
45    {
46        return [
47            ['SELECT * FROM users', ['users']],
48            ['SELECT * FROM users u JOIN orders o ON o.user_id = u.id', ['users', 'orders']],
49            ['INSERT INTO users (id) VALUES (1)', ['users']],
50            ['UPDATE users SET a = 1', ['users']],
51            ['TRUNCATE TABLE users', ['users']],
52            ['SELECT * FROM `app`.`users`', ['app.users']],
53            ['SELECT * FROM users, orders', ['users']],
54            ['SELECT 1', []],
55            ['SELECT * FROM (SELECT 1) x', []],
56            ['DROP TABLE IF EXISTS users', ['users']],
57            ['CREATE TABLE IF NOT EXISTS users (id INT)', ['users']],
58            ['SHOW TABLE STATUS', []],
59            ['INSERT INTO users (a) VALUES (1) ON DUPLICATE KEY UPDATE a = VALUES(a)', ['users']],
60            ['SELECT COUNT(*) FROM  WHERE a = 1', []],
61        ];
62    }
63
64    public function testReadStopsAtAGapInsteadOfGuessingATable(): void
65    {
66        $pattern = TextPattern::fromText('SELECT * FROM ')
67            ->concat(TextPattern::fromHole(new TextHole(Origin::Property, TypeShape::unknown())));
68        self::assertSame([], (new TableReader())->read($pattern));
69    }
70
71    public function testReadKeepsANameThatIsKnownInPart(): void
72    {
73        $pattern = TextPattern::fromText('SELECT * FROM ')
74            ->concat(TextPattern::fromHole(new TextHole(Origin::Property, TypeShape::unknown())))
75            ->concat(TextPattern::fromText('posts p JOIN `'))
76            ->concat(TextPattern::fromHole(new TextHole(Origin::Property, TypeShape::unknown())))
77            ->concat(TextPattern::fromText('users` u'));
78        self::assertSame(['{$}posts', '{$}users'], (new TableReader())->read($pattern));
79    }
80
81    public function testReadDoesNotGuessAQuotedNameThatIsAllGap(): void
82    {
83        $pattern = TextPattern::fromText('ALTER TABLE `')
84            ->concat(TextPattern::fromHole(new TextHole(Origin::Property, TypeShape::unknown())))
85            ->concat(TextPattern::fromText('` DROP INDEX x'));
86        self::assertSame([], (new TableReader())->read($pattern));
87    }
88
89    public function testReadNameStopsAtATrailingSeparator(): void
90    {
91        $tokens = (new SqlLexer())->tokenize('app. ');
92        self::assertSame('app', (new TableReader())->readName($tokens));
93    }
94
95    public function testReadNameIsNullWhenNothingNamesATable(): void
96    {
97        self::assertNull((new TableReader())->readName((new SqlLexer())->tokenize('(')));
98    }
99
100    public function testIsNameRejectsKeywordsAndGapMarkers(): void
101    {
102        $reader = new TableReader();
103        self::assertTrue($reader->isName(new SqlToken(SqlTokenKind::Word, 'users', 0, 0)));
104        self::assertFalse($reader->isName(new SqlToken(SqlTokenKind::Word, 'SELECT', 0, 0)));
105        self::assertFalse($reader->isName(new SqlToken(SqlTokenKind::Word, PlaceholderScanner::HOLE_MARKER, 0, 0)));
106        self::assertFalse($reader->isName(new SqlToken(SqlTokenKind::Identifier, '`' . PlaceholderScanner::HOLE_MARKER . '`', 0, 0)));
107        self::assertFalse($reader->isName(new SqlToken(SqlTokenKind::Word, 'WHERE', 0, 0)));
108        self::assertFalse($reader->isName(new SqlToken(SqlTokenKind::Symbol, '(', 0, 0)));
109    }
110
111    public function testUnquoteStripsIdentifierQuoting(): void
112    {
113        $reader = new TableReader();
114        self::assertSame('users', $reader->unquote(new SqlToken(SqlTokenKind::Identifier, '`users`', 0, 0)));
115        self::assertSame('users', $reader->unquote(new SqlToken(SqlTokenKind::Word, 'users', 0, 0)));
116    }
117}
118