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