packages/sql-faker/tests/Unit/Sqlite/Generation/Tokenization/SqliteTokenizerTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\SqlFaker\Sqlite\Generation\Tokenization;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFaker\Generation\Exception\LexicalException;
12use SqlFaker\Sqlite\Generation\Tokenization\SqliteTokenizer;
13
14#[CoversClass(SqliteTokenizer::class)]
15#[UsesClass(LexicalException::class)]
16final class SqliteTokenizerTest extends TestCase
17{
18    /**
19     * @param list<string> $expected
20     */
21    #[DataProvider('providerSql')]
22    public function testTokenizeReadsTextAsTheServerWould(string $sql, array $expected): void
23    {
24        self::assertSame($expected, (new SqliteTokenizer(['SELECT' => 'SELECT']))->tokenize($sql));
25    }
26
27    /**
28     * @return iterable<string, array{string, list<string>}>
29     */
30    public static function providerSql(): iterable
31    {
32        yield 'nothing' => ['', []];
33        yield 'keyword' => ['SELECT', ['SELECT']];
34        yield 'keyword regardless of case' => ['select', ['SELECT']];
35        yield 'identifier' => ['users', ['ID']];
36        yield 'double quoted identifier' => ['"users"', ['ID']];
37        yield 'backtick quoted identifier' => ['`users`', ['ID']];
38        yield 'bracket quoted identifier' => ['[users]', ['ID']];
39        yield 'string literal' => ["'text'", ['STRING']];
40        yield 'blob literal' => ["X'FF'", ['BLOB']];
41        yield 'anonymous parameter' => ['?', ['VARIABLE']];
42        yield 'numbered parameter' => ['?1', ['VARIABLE']];
43        yield 'named parameter' => [':name', ['VARIABLE']];
44        yield 'at parameter' => ['@name', ['VARIABLE']];
45        yield 'dollar parameter' => ['$name', ['VARIABLE']];
46        yield 'integer' => ['42', ['INTEGER']];
47        yield 'hex integer' => ['0xFF', ['INTEGER']];
48        yield 'separated integer' => ['1_0', ['QNUMBER']];
49        yield 'float' => ['1.5', ['FLOAT']];
50        yield 'exponent float' => ['1e3', ['FLOAT']];
51        yield 'pointer' => ['->', ['PTR']];
52        yield 'double pointer' => ['->>', ['PTR']];
53        yield 'concatenation' => ['||', ['CONCAT']];
54        yield 'double equals is equality' => ['==', ['EQ']];
55        yield 'punctuation' => ['(', ['LP']];
56        yield 'line comment is trivia' => ["SELECT -- note\nusers", ['SELECT', 'ID']];
57        yield 'block comment is trivia' => ['SELECT /* note */ users', ['SELECT', 'ID']];
58    }
59
60    public function testTokenizeReportsTextItCannotRead(): void
61    {
62        $tokenizer = new SqliteTokenizer([]);
63
64        $this->expectException(LexicalException::class);
65        $this->expectExceptionMessage('Unsupported SQLite lexical input at offset 0:');
66
67        $tokenizer->tokenize('\\');
68    }
69
70    public function testTokenizeReportsAnUnterminatedBracketIdentifier(): void
71    {
72        $tokenizer = new SqliteTokenizer([]);
73
74        $this->expectException(LexicalException::class);
75        $this->expectExceptionMessage('Unterminated SQLite bracket identifier.');
76
77        $tokenizer->tokenize('[users');
78    }
79
80    public function testQuotedTokenAtLeavesAnythingThatOpensNoQuote(): void
81    {
82        $offset = 0;
83
84        self::assertNull((new SqliteTokenizer([]))->quotedTokenAt('users', $offset));
85        self::assertSame(0, $offset);
86    }
87
88    public function testLiteralTokenAtLeavesTextThatIsNoLiteral(): void
89    {
90        $offset = 0;
91
92        self::assertNull((new SqliteTokenizer([]))->literalTokenAt('users', $offset));
93    }
94
95    public function testWordTokenAtLeavesTextThatIsNoWord(): void
96    {
97        $offset = 0;
98
99        self::assertNull((new SqliteTokenizer([]))->wordTokenAt('42', $offset));
100    }
101
102    public function testOperatorTokenAtLeavesTextThatIsNoOperator(): void
103    {
104        $offset = 0;
105
106        self::assertNull((new SqliteTokenizer([]))->operatorTokenAt('users', $offset));
107    }
108
109    public function testTokenAtReadsTheFirstTokenAndAdvancesPastIt(): void
110    {
111        $tokenizer = new SqliteTokenizer(['SELECT' => 'SELECT']);
112        $offset = 0;
113
114        self::assertSame('SELECT', $tokenizer->tokenAt('SELECT users', $offset));
115        self::assertSame(6, $offset);
116    }
117
118    public function testSkipTriviaReportsWhenNothingWasSkipped(): void
119    {
120        $offset = 0;
121
122        self::assertFalse((new SqliteTokenizer([]))->skipTrivia('SELECT', $offset));
123    }
124
125    public function testSkipTriviaReportsAnUnterminatedBlockComment(): void
126    {
127        $offset = 0;
128
129        $this->expectException(LexicalException::class);
130        $this->expectExceptionMessage('Unterminated SQLite block comment.');
131
132        (new SqliteTokenizer([]))->skipTrivia('/* never closed', $offset);
133    }
134
135    public function testSkipQuotedTreatsADoubledQuoteAsAnEscape(): void
136    {
137        $offset = 0;
138
139        (new SqliteTokenizer([]))->skipQuoted("'a''b' rest", $offset, "'");
140
141        self::assertSame(6, $offset);
142    }
143
144    public function testSkipQuotedReportsARunThatNeverCloses(): void
145    {
146        $offset = 0;
147
148        $this->expectException(LexicalException::class);
149        $this->expectExceptionMessage('Unterminated SQLite quoted token');
150
151        (new SqliteTokenizer([]))->skipQuoted("'abc", $offset, "'");
152    }
153
154    public function testOperatorAtPrefersTheLongestOperator(): void
155    {
156        $tokenizer = new SqliteTokenizer([]);
157
158        self::assertSame(['->>', 'PTR'], $tokenizer->operatorAt('->>', 0));
159        self::assertSame(['->', 'PTR'], $tokenizer->operatorAt('->x', 0));
160        self::assertSame(['-', 'MINUS'], $tokenizer->operatorAt('-x', 0));
161    }
162
163    public function testOperatorAtReportsNothingForAWord(): void
164    {
165        self::assertNull((new SqliteTokenizer([]))->operatorAt('users', 0));
166    }
167
168    public function testNormalizedSourceTokensStripsThePrefixAndTheSpacing(): void
169    {
170        self::assertSame(
171            ['SELECT', 'ID'],
172            (new SqliteTokenizer([]))->normalizedSourceTokens(['TK_SELECT', 'TK_SPACE', 'TK_ID']),
173        );
174    }
175
176    public function testNormalizedSourceTokensLeavesUnprefixedNamesAlone(): void
177    {
178        self::assertSame(['SELECT'], (new SqliteTokenizer([]))->normalizedSourceTokens(['SELECT']));
179    }
180}
181