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