packages/ztd-query-core/tests/Unit/Sql/SqlTokenScannerTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Sql;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use Tests\Fake\FakeSqlLexerProfiles;
11use ZtdQuery\Sql\Profile\SqlCommentProfile;
12use ZtdQuery\Sql\Profile\SqlParameterProfile;
13use ZtdQuery\Sql\Profile\SqlQuoteProfile;
14use ZtdQuery\Sql\Profile\SqlSymbolProfile;
15use ZtdQuery\Sql\SqlLexerProfile;
16use ZtdQuery\Sql\SqlToken;
17use ZtdQuery\Sql\SqlTokenKind;
18use ZtdQuery\Sql\SqlTokenScanner;
19
20#[CoversClass(SqlTokenScanner::class)]
21#[UsesClass(SqlToken::class)]
22#[UsesClass(\ZtdQuery\Sql\Reader\SqlLexeme::class)]
23#[UsesClass(\ZtdQuery\Sql\Reader\SqlTriviaReader::class)]
24#[UsesClass(\ZtdQuery\Sql\Reader\SqlBlockCommentReader::class)]
25#[UsesClass(\ZtdQuery\Sql\Reader\SqlDelimitedReader::class)]
26#[UsesClass(\ZtdQuery\Sql\Reader\SqlParameterReader::class)]
27#[UsesClass(\ZtdQuery\Sql\Reader\SqlWordReader::class)]
28#[UsesClass(SqlLexerProfile::class)]
29#[UsesClass(\ZtdQuery\Sql\LexicalDelimiters::class)]
30#[UsesClass(\ZtdQuery\Sql\LexicalPattern::class)]
31#[UsesClass(SqlCommentProfile::class)]
32#[UsesClass(SqlParameterProfile::class)]
33#[UsesClass(SqlQuoteProfile::class)]
34#[UsesClass(SqlSymbolProfile::class)]
35final class SqlTokenScannerTest extends TestCase
36{
37    public function testScanLosesNothingOfTheStatementItRead(): void
38    {
39        $sql = "SELECT /* c */ \"a\", 'b' -- tail\nFROM t";
40
41        $tokens = (new SqlTokenScanner())->scan($sql, FakeSqlLexerProfiles::standard());
42
43        self::assertSame($sql, implode('', array_map(static fn (SqlToken $t): string => $t->text, $tokens)));
44    }
45
46    public function testScanReadsEachLexemeAsWhatItIs(): void
47    {
48        $tokens = (new SqlTokenScanner())->scan("SELECT 1, 'a'", FakeSqlLexerProfiles::standard());
49
50        self::assertSame(
51            [
52                SqlTokenKind::Word,
53                SqlTokenKind::Whitespace,
54                SqlTokenKind::Number,
55                SqlTokenKind::Symbol,
56                SqlTokenKind::Whitespace,
57                SqlTokenKind::String,
58            ],
59            array_map(static fn (SqlToken $t): SqlTokenKind => $t->kind, $tokens),
60        );
61    }
62
63    public function testScanCountsHowDeeplyEachLexemeIsNested(): void
64    {
65        $tokens = (new SqlTokenScanner())->scan('((1))', FakeSqlLexerProfiles::standard());
66
67        self::assertSame([0, 1, 2, 1, 0], array_map(static fn (SqlToken $t): int => $t->depth, $tokens));
68    }
69
70    public function testScanReadsAnUnterminatedStringToTheEndOfTheStatement(): void
71    {
72        $tokens = (new SqlTokenScanner())->scan("SELECT 'a", FakeSqlLexerProfiles::standard());
73
74        self::assertSame("'a", $tokens[count($tokens) - 1]->text);
75    }
76
77    public function testScanReadsAWholeCommentAsOneLexeme(): void
78    {
79        $tokens = (new SqlTokenScanner())->scan('/* a b */1', FakeSqlLexerProfiles::standard());
80
81        self::assertSame('/* a b */', $tokens[0]->text);
82    }
83
84}
85