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