packages/sql-catalog/tests/Unit/Core/Sql/SqlLexerTest.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\SqlLexer;
12use SqlCatalog\Core\Sql\SqlToken;
13use SqlCatalog\Core\Sql\SqlTokenKind;
14
15#[CoversClass(SqlLexer::class)]
16#[UsesClass(SqlToken::class)]
17final class SqlLexerTest extends TestCase
18{
19 public function testTokenizeSeparatesWordsAndSymbols(): void
20 {
21 $tokens = (new SqlLexer())->tokenize('SELECT a, b');
22 self::assertSame(['SELECT', 'a', ',', 'b'], array_map(
23 static fn (SqlToken $token): string => $token->text,
24 $tokens,
25 ));
26 }
27
28 public function testTokenizeTracksParenthesisDepth(): void
29 {
30 $tokens = (new SqlLexer())->tokenize('SELECT (1)');
31 self::assertSame([0, 1, 1, 0], array_map(
32 static fn (SqlToken $token): int => $token->depth,
33 $tokens,
34 ));
35 }
36
37 public function testTokenizeNeverLosesGroundOnUnbalancedParentheses(): void
38 {
39 $tokens = (new SqlLexer())->tokenize('SELECT 1)');
40 self::assertSame(0, $tokens[count($tokens) - 1]->depth);
41 }
42
43 public function testTokenizeKeepsStringBodiesTogether(): void
44 {
45 $tokens = (new SqlLexer())->tokenize("SELECT 'a, b' FROM t");
46 self::assertSame("'a, b'", $tokens[1]->text);
47 self::assertSame(SqlTokenKind::Text, $tokens[1]->kind);
48 }
49
50 #[DataProvider('providerReadToken')]
51 public function testReadToken(string $sql, SqlTokenKind $kind, string $text): void
52 {
53 $token = (new SqlLexer())->readToken($sql, 0, 0);
54 self::assertSame($kind, $token->kind);
55 self::assertSame($text, $token->text);
56 }
57
58 /**
59 * @return list<array{string, SqlTokenKind, string}>
60 */
61 public static function providerReadToken(): array
62 {
63 return [
64 ['-- a comment', SqlTokenKind::Comment, '-- a comment'],
65 ['users', SqlTokenKind::Word, 'users'],
66 ['12.5', SqlTokenKind::Number, '12.5'],
67 ['"quoted"', SqlTokenKind::Identifier, '"quoted"'],
68 ['?', SqlTokenKind::Parameter, '?'],
69 [':name', SqlTokenKind::Parameter, ':name'],
70 ['::text', SqlTokenKind::Symbol, '::'],
71 ['+', SqlTokenKind::Symbol, '+'],
72 ];
73 }
74
75 public function testReadCommentRecognisesEveryCommentSyntax(): void
76 {
77 $lexer = new SqlLexer();
78 self::assertSame('# a', $lexer->readComment('# a', 0));
79 self::assertSame('/* a */', $lexer->readComment('/* a */ SELECT', 0));
80 self::assertNull($lexer->readComment('SELECT', 0));
81 }
82
83 public function testReadQuotedHandlesEscapesAndDoubling(): void
84 {
85 $lexer = new SqlLexer();
86 self::assertSame("'it''s'", $lexer->readQuoted("'it''s' x", 0));
87 self::assertSame("'a\\'b'", $lexer->readQuoted("'a\\'b' x", 0));
88 self::assertSame('`t`', $lexer->readQuoted('`t`', 0));
89 self::assertNull($lexer->readQuoted('abc', 0));
90 }
91
92 public function testReadQuotedRunsToTheEndWhenTheQuoteIsNeverClosed(): void
93 {
94 self::assertSame("'abc", (new SqlLexer())->readQuoted("'abc", 0));
95 }
96
97 public function testReadParameterRecognisesEveryParameterSyntax(): void
98 {
99 $lexer = new SqlLexer();
100 self::assertSame('?', $lexer->readParameter('?', 0));
101 self::assertSame('?1', $lexer->readParameter('?1', 0));
102 self::assertSame(':id', $lexer->readParameter(':id', 0));
103 self::assertSame('$2', $lexer->readParameter('$2', 0));
104 self::assertNull($lexer->readParameter('a', 0));
105 }
106}
107