packages/ztd-query-core/tests/Unit/Sql/SqlLexerProfileTest.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\SqlLexerProfile;
12
13#[CoversClass(SqlLexerProfile::class)]
14#[UsesClass(\ZtdQuery\Sql\Profile\SqlCommentProfile::class)]
15#[UsesClass(\ZtdQuery\Sql\Profile\SqlQuoteProfile::class)]
16#[UsesClass(\ZtdQuery\Sql\Profile\SqlParameterProfile::class)]
17#[UsesClass(\ZtdQuery\Sql\Profile\SqlSymbolProfile::class)]
18#[UsesClass(\ZtdQuery\Sql\LexicalDelimiters::class)]
19#[UsesClass(\ZtdQuery\Sql\LexicalPattern::class)]
20final class SqlLexerProfileTest extends TestCase
21{
22 public function testStartsLineCommentSeesEveryPrefixTheDialectDeclares(): void
23 {
24 $profile = FakeSqlLexerProfiles::custom(
25 lineCommentPrefixes: ['--', '#'],
26 whitespaceDelimitedLineCommentPrefixes: ['//'],
27 blockCommentPairs: ['/*' => '*/'],
28 stringQuotePairs: ["'" => "'"],
29 identifierQuotePairs: ['"' => '"', '[' => ']'],
30 namedParameterSeparators: [':' => ['::'], '$' => []],
31 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
32 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
33 backslashEscapedStringPrefixes: ['E'],
34 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
35 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
36 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
37 identifierStartPattern: '/^[_A-Za-z]$/',
38 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
39 bracketPair: ['[', ']'],
40 nestingPair: ['(', ')'],
41 statementDelimiter: ';',
42 listDelimiter: ',',
43 nestedBlockComments: true,
44 backslashEscapedStrings: false,
45 );
46
47 self::assertTrue($profile->startsLineComment('-- comment', 0));
48 self::assertTrue($profile->startsLineComment('x # comment', 2));
49 self::assertTrue($profile->startsLineComment('// comment', 0));
50 self::assertFalse($profile->startsLineComment('//not-comment', 0));
51 }
52
53 public function testBlockCommentAtAnswersTheDelimitersThatOpenAndCloseOne(): void
54 {
55 $profile = FakeSqlLexerProfiles::custom(
56 lineCommentPrefixes: ['--', '#'],
57 whitespaceDelimitedLineCommentPrefixes: ['//'],
58 blockCommentPairs: ['/*' => '*/'],
59 stringQuotePairs: ["'" => "'"],
60 identifierQuotePairs: ['"' => '"', '[' => ']'],
61 namedParameterSeparators: [':' => ['::'], '$' => []],
62 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
63 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
64 backslashEscapedStringPrefixes: ['E'],
65 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
66 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
67 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
68 identifierStartPattern: '/^[_A-Za-z]$/',
69 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
70 bracketPair: ['[', ']'],
71 nestingPair: ['(', ')'],
72 statementDelimiter: ';',
73 listDelimiter: ',',
74 nestedBlockComments: true,
75 backslashEscapedStrings: false,
76 );
77
78 self::assertSame(['/*', '*/'], $profile->blockCommentAt('/* comment */', 0));
79 self::assertNull($profile->blockCommentAt('/ value', 0));
80 }
81
82 public function testStringQuoteClosingAnswersWhatClosesAStringThisOpened(): void
83 {
84 $profile = FakeSqlLexerProfiles::custom(
85 lineCommentPrefixes: ['--', '#'],
86 whitespaceDelimitedLineCommentPrefixes: ['//'],
87 blockCommentPairs: ['/*' => '*/'],
88 stringQuotePairs: ["'" => "'"],
89 identifierQuotePairs: ['"' => '"', '[' => ']'],
90 namedParameterSeparators: [':' => ['::'], '$' => []],
91 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
92 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
93 backslashEscapedStringPrefixes: ['E'],
94 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
95 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
96 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
97 identifierStartPattern: '/^[_A-Za-z]$/',
98 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
99 bracketPair: ['[', ']'],
100 nestingPair: ['(', ')'],
101 statementDelimiter: ';',
102 listDelimiter: ',',
103 nestedBlockComments: true,
104 backslashEscapedStrings: false,
105 );
106
107 self::assertSame("'", $profile->stringQuoteClosing("'"));
108 }
109
110 public function testIdentifierQuoteClosingAnswersWhatClosesAnIdentifierThisOpened(): void
111 {
112 $profile = FakeSqlLexerProfiles::custom(
113 lineCommentPrefixes: ['--', '#'],
114 whitespaceDelimitedLineCommentPrefixes: ['//'],
115 blockCommentPairs: ['/*' => '*/'],
116 stringQuotePairs: ["'" => "'"],
117 identifierQuotePairs: ['"' => '"', '[' => ']'],
118 namedParameterSeparators: [':' => ['::'], '$' => []],
119 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
120 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
121 backslashEscapedStringPrefixes: ['E'],
122 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
123 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
124 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
125 identifierStartPattern: '/^[_A-Za-z]$/',
126 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
127 bracketPair: ['[', ']'],
128 nestingPair: ['(', ')'],
129 statementDelimiter: ';',
130 listDelimiter: ',',
131 nestedBlockComments: true,
132 backslashEscapedStrings: false,
133 );
134
135 self::assertSame(']', $profile->identifierQuoteClosing('['));
136 self::assertNull($profile->identifierQuoteClosing('`'));
137 }
138
139 public function testUnquoteIdentifierReadsTheNameAQuotedIdentifierStandsFor(): void
140 {
141 $profile = FakeSqlLexerProfiles::custom(
142 lineCommentPrefixes: ['--', '#'],
143 whitespaceDelimitedLineCommentPrefixes: ['//'],
144 blockCommentPairs: ['/*' => '*/'],
145 stringQuotePairs: ["'" => "'"],
146 identifierQuotePairs: ['"' => '"', '[' => ']'],
147 namedParameterSeparators: [':' => ['::'], '$' => []],
148 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
149 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
150 backslashEscapedStringPrefixes: ['E'],
151 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
152 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
153 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
154 identifierStartPattern: '/^[_A-Za-z]$/',
155 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
156 bracketPair: ['[', ']'],
157 nestingPair: ['(', ')'],
158 statementDelimiter: ';',
159 listDelimiter: ',',
160 nestedBlockComments: true,
161 backslashEscapedStrings: false,
162 );
163
164 self::assertSame('bracket]name', $profile->unquoteIdentifier('[bracket]]name]'));
165 self::assertSame('[bracket', $profile->unquoteIdentifier('[bracket'));
166 self::assertSame('bracket]', $profile->unquoteIdentifier('bracket]'));
167 }
168
169 public function testQuotedIdentifierValueSpeaksOnlyForACompleteQuotedIdentifier(): void
170 {
171 $profile = FakeSqlLexerProfiles::custom(
172 lineCommentPrefixes: ['--', '#'],
173 whitespaceDelimitedLineCommentPrefixes: ['//'],
174 blockCommentPairs: ['/*' => '*/'],
175 stringQuotePairs: ["'" => "'"],
176 identifierQuotePairs: ['"' => '"', '[' => ']'],
177 namedParameterSeparators: [':' => ['::'], '$' => []],
178 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
179 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
180 backslashEscapedStringPrefixes: ['E'],
181 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
182 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
183 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
184 identifierStartPattern: '/^[_A-Za-z]$/',
185 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
186 bracketPair: ['[', ']'],
187 nestingPair: ['(', ')'],
188 statementDelimiter: ';',
189 listDelimiter: ',',
190 nestedBlockComments: true,
191 backslashEscapedStrings: false,
192 );
193
194 self::assertSame('bracket]name', $profile->quotedIdentifierValue('[bracket]]name]'));
195 self::assertNull($profile->quotedIdentifierValue('[bracket'));
196 self::assertNull($profile->quotedIdentifierValue('[]'));
197 self::assertNull($profile->quotedIdentifierValue('plain'));
198 }
199
200 public function testSupportsNestedBlockCommentsFollowsTheDialect(): void
201 {
202 $profile = FakeSqlLexerProfiles::custom(
203 lineCommentPrefixes: ['--', '#'],
204 whitespaceDelimitedLineCommentPrefixes: ['//'],
205 blockCommentPairs: ['/*' => '*/'],
206 stringQuotePairs: ["'" => "'"],
207 identifierQuotePairs: ['"' => '"', '[' => ']'],
208 namedParameterSeparators: [':' => ['::'], '$' => []],
209 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
210 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
211 backslashEscapedStringPrefixes: ['E'],
212 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
213 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
214 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
215 identifierStartPattern: '/^[_A-Za-z]$/',
216 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
217 bracketPair: ['[', ']'],
218 nestingPair: ['(', ')'],
219 statementDelimiter: ';',
220 listDelimiter: ',',
221 nestedBlockComments: true,
222 backslashEscapedStrings: false,
223 );
224
225 self::assertTrue($profile->supportsNestedBlockComments());
226 }
227
228 public function testDollarQuoteDelimiterAtAnswersTheDelimiterStartingThere(): void
229 {
230 $profile = FakeSqlLexerProfiles::custom(
231 lineCommentPrefixes: ['--', '#'],
232 whitespaceDelimitedLineCommentPrefixes: ['//'],
233 blockCommentPairs: ['/*' => '*/'],
234 stringQuotePairs: ["'" => "'"],
235 identifierQuotePairs: ['"' => '"', '[' => ']'],
236 namedParameterSeparators: [':' => ['::'], '$' => []],
237 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
238 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
239 backslashEscapedStringPrefixes: ['E'],
240 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
241 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
242 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
243 identifierStartPattern: '/^[_A-Za-z]$/',
244 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
245 bracketPair: ['[', ']'],
246 nestingPair: ['(', ')'],
247 statementDelimiter: ';',
248 listDelimiter: ',',
249 nestedBlockComments: true,
250 backslashEscapedStrings: false,
251 );
252
253 self::assertSame('$tag$', $profile->dollarQuoteDelimiterAt('$tag$body$tag$', 0));
254 }
255
256 public function testPositionalParameterLengthAtMeasuresTheParameterStartingThere(): void
257 {
258 $profile = FakeSqlLexerProfiles::custom(
259 lineCommentPrefixes: ['--', '#'],
260 whitespaceDelimitedLineCommentPrefixes: ['//'],
261 blockCommentPairs: ['/*' => '*/'],
262 stringQuotePairs: ["'" => "'"],
263 identifierQuotePairs: ['"' => '"', '[' => ']'],
264 namedParameterSeparators: [':' => ['::'], '$' => []],
265 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
266 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
267 backslashEscapedStringPrefixes: ['E'],
268 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
269 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
270 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
271 identifierStartPattern: '/^[_A-Za-z]$/',
272 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
273 bracketPair: ['[', ']'],
274 nestingPair: ['(', ')'],
275 statementDelimiter: ';',
276 listDelimiter: ',',
277 nestedBlockComments: true,
278 backslashEscapedStrings: false,
279 );
280
281 self::assertSame(3, $profile->positionalParameterLengthAt('$12 tail', 0));
282 self::assertSame(3, $profile->positionalParameterLengthAt('?34 tail', 0));
283 self::assertSame(0, $profile->positionalParameterLengthAt('value', 0));
284 }
285
286 public function testNamedParameterPrefixAtTellsAParameterFromAnOperatorSpelledAlike(): void
287 {
288 $profile = FakeSqlLexerProfiles::custom(
289 lineCommentPrefixes: ['--', '#'],
290 whitespaceDelimitedLineCommentPrefixes: ['//'],
291 blockCommentPairs: ['/*' => '*/'],
292 stringQuotePairs: ["'" => "'"],
293 identifierQuotePairs: ['"' => '"', '[' => ']'],
294 namedParameterSeparators: [':' => ['::'], '$' => []],
295 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
296 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
297 backslashEscapedStringPrefixes: ['E'],
298 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
299 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
300 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
301 identifierStartPattern: '/^[_A-Za-z]$/',
302 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
303 bracketPair: ['[', ']'],
304 nestingPair: ['(', ')'],
305 statementDelimiter: ';',
306 listDelimiter: ',',
307 nestedBlockComments: true,
308 backslashEscapedStrings: false,
309 );
310
311 self::assertSame(':', $profile->namedParameterPrefixAt(':name', 0));
312 self::assertSame(':', $profile->namedParameterPrefixAt('x:name', 1));
313 self::assertSame('$', $profile->namedParameterPrefixAt('$name', 0));
314 self::assertNull($profile->namedParameterPrefixAt('value::type', 6));
315 self::assertNull($profile->namedParameterPrefixAt('::name', 1));
316 }
317
318 public function testParameterNameSeparatorAtAnswersWhatSeparatesAPrefixFromItsName(): void
319 {
320 $profile = FakeSqlLexerProfiles::custom(
321 lineCommentPrefixes: ['--', '#'],
322 whitespaceDelimitedLineCommentPrefixes: ['//'],
323 blockCommentPairs: ['/*' => '*/'],
324 stringQuotePairs: ["'" => "'"],
325 identifierQuotePairs: ['"' => '"', '[' => ']'],
326 namedParameterSeparators: [':' => ['::'], '$' => []],
327 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
328 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
329 backslashEscapedStringPrefixes: ['E'],
330 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
331 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
332 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
333 identifierStartPattern: '/^[_A-Za-z]$/',
334 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
335 bracketPair: ['[', ']'],
336 nestingPair: ['(', ')'],
337 statementDelimiter: ';',
338 listDelimiter: ',',
339 nestedBlockComments: true,
340 backslashEscapedStrings: false,
341 );
342
343 self::assertSame('::', $profile->parameterNameSeparatorAt(':', 'name::part', 4));
344 self::assertNull($profile->parameterNameSeparatorAt(':', 'name.part', 4));
345 }
346
347 public function testParameterSuffixLengthMeasuresWhatFollowsAParameterName(): void
348 {
349 $profile = FakeSqlLexerProfiles::custom(
350 lineCommentPrefixes: ['--', '#'],
351 whitespaceDelimitedLineCommentPrefixes: ['//'],
352 blockCommentPairs: ['/*' => '*/'],
353 stringQuotePairs: ["'" => "'"],
354 identifierQuotePairs: ['"' => '"', '[' => ']'],
355 namedParameterSeparators: [':' => ['::'], '$' => []],
356 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
357 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
358 backslashEscapedStringPrefixes: ['E'],
359 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
360 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
361 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
362 identifierStartPattern: '/^[_A-Za-z]$/',
363 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
364 bracketPair: ['[', ']'],
365 nestingPair: ['(', ')'],
366 statementDelimiter: ';',
367 listDelimiter: ',',
368 nestedBlockComments: true,
369 backslashEscapedStrings: false,
370 );
371
372 self::assertSame(9, $profile->parameterSuffixLength('$', '(payload)', 0));
373 self::assertSame(0, $profile->parameterSuffixLength('$', 'payload', 0));
374 }
375
376 public function testStringUsesBackslashEscapesLooksAtWhatIntroducedTheString(): void
377 {
378 $profile = FakeSqlLexerProfiles::custom(
379 lineCommentPrefixes: ['--', '#'],
380 whitespaceDelimitedLineCommentPrefixes: ['//'],
381 blockCommentPairs: ['/*' => '*/'],
382 stringQuotePairs: ["'" => "'"],
383 identifierQuotePairs: ['"' => '"', '[' => ']'],
384 namedParameterSeparators: [':' => ['::'], '$' => []],
385 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
386 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
387 backslashEscapedStringPrefixes: ['E'],
388 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
389 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
390 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
391 identifierStartPattern: '/^[_A-Za-z]$/',
392 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
393 bracketPair: ['[', ']'],
394 nestingPair: ['(', ')'],
395 statementDelimiter: ';',
396 listDelimiter: ',',
397 nestedBlockComments: true,
398 backslashEscapedStrings: false,
399 );
400
401 self::assertTrue($profile->stringUsesBackslashEscapes("E'value'", 1));
402 self::assertTrue($profile->stringUsesBackslashEscapes("E'value", 1));
403 self::assertTrue($profile->stringUsesBackslashEscapes("+E'value'", 2));
404 self::assertTrue($profile->stringUsesBackslashEscapes("a+E'value'", 3));
405 self::assertFalse($profile->stringUsesBackslashEscapes("nameE'value'", 5));
406 }
407
408 public function testNumberLengthAtMeasuresTheNumberStartingThere(): void
409 {
410 $profile = FakeSqlLexerProfiles::custom(
411 lineCommentPrefixes: ['--', '#'],
412 whitespaceDelimitedLineCommentPrefixes: ['//'],
413 blockCommentPairs: ['/*' => '*/'],
414 stringQuotePairs: ["'" => "'"],
415 identifierQuotePairs: ['"' => '"', '[' => ']'],
416 namedParameterSeparators: [':' => ['::'], '$' => []],
417 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
418 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
419 backslashEscapedStringPrefixes: ['E'],
420 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
421 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
422 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
423 identifierStartPattern: '/^[_A-Za-z]$/',
424 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
425 bracketPair: ['[', ']'],
426 nestingPair: ['(', ')'],
427 statementDelimiter: ';',
428 listDelimiter: ',',
429 nestedBlockComments: true,
430 backslashEscapedStrings: false,
431 );
432
433 self::assertSame(7, $profile->numberLengthAt('0xCA_FE tail', 0));
434 self::assertSame(2, $profile->numberLengthAt('x42 tail', 1));
435 self::assertSame(0, $profile->numberLengthAt('value', 0));
436 }
437
438 public function testIsIdentifierStartAnswersForTheFirstCharacterOfAName(): void
439 {
440 $profile = FakeSqlLexerProfiles::custom(
441 lineCommentPrefixes: ['--', '#'],
442 whitespaceDelimitedLineCommentPrefixes: ['//'],
443 blockCommentPairs: ['/*' => '*/'],
444 stringQuotePairs: ["'" => "'"],
445 identifierQuotePairs: ['"' => '"', '[' => ']'],
446 namedParameterSeparators: [':' => ['::'], '$' => []],
447 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
448 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
449 backslashEscapedStringPrefixes: ['E'],
450 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
451 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
452 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
453 identifierStartPattern: '/^[_A-Za-z]$/',
454 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
455 bracketPair: ['[', ']'],
456 nestingPair: ['(', ')'],
457 statementDelimiter: ';',
458 listDelimiter: ',',
459 nestedBlockComments: true,
460 backslashEscapedStrings: false,
461 );
462
463 self::assertTrue($profile->isIdentifierStart('_'));
464 self::assertFalse($profile->isIdentifierStart('1'));
465 }
466
467 public function testIsIdentifierPartAnswersForEveryLaterCharacterOfAName(): void
468 {
469 $profile = FakeSqlLexerProfiles::custom(
470 lineCommentPrefixes: ['--', '#'],
471 whitespaceDelimitedLineCommentPrefixes: ['//'],
472 blockCommentPairs: ['/*' => '*/'],
473 stringQuotePairs: ["'" => "'"],
474 identifierQuotePairs: ['"' => '"', '[' => ']'],
475 namedParameterSeparators: [':' => ['::'], '$' => []],
476 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
477 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
478 backslashEscapedStringPrefixes: ['E'],
479 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
480 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
481 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
482 identifierStartPattern: '/^[_A-Za-z]$/',
483 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
484 bracketPair: ['[', ']'],
485 nestingPair: ['(', ')'],
486 statementDelimiter: ';',
487 listDelimiter: ',',
488 nestedBlockComments: true,
489 backslashEscapedStrings: false,
490 );
491
492 self::assertTrue($profile->isIdentifierPart('$'));
493 }
494
495 public function testIsBracketOpeningAnswersForTheCharacterThatOpensOne(): void
496 {
497 $profile = FakeSqlLexerProfiles::custom(
498 lineCommentPrefixes: ['--', '#'],
499 whitespaceDelimitedLineCommentPrefixes: ['//'],
500 blockCommentPairs: ['/*' => '*/'],
501 stringQuotePairs: ["'" => "'"],
502 identifierQuotePairs: ['"' => '"', '[' => ']'],
503 namedParameterSeparators: [':' => ['::'], '$' => []],
504 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
505 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
506 backslashEscapedStringPrefixes: ['E'],
507 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
508 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
509 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
510 identifierStartPattern: '/^[_A-Za-z]$/',
511 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
512 bracketPair: ['[', ']'],
513 nestingPair: ['(', ')'],
514 statementDelimiter: ';',
515 listDelimiter: ',',
516 nestedBlockComments: true,
517 backslashEscapedStrings: false,
518 );
519
520 self::assertTrue($profile->isBracketOpening('['));
521 self::assertFalse($profile->isBracketOpening('('));
522 }
523
524 public function testIsBracketClosingAnswersForTheCharacterThatClosesOne(): void
525 {
526 $profile = FakeSqlLexerProfiles::custom(
527 lineCommentPrefixes: ['--', '#'],
528 whitespaceDelimitedLineCommentPrefixes: ['//'],
529 blockCommentPairs: ['/*' => '*/'],
530 stringQuotePairs: ["'" => "'"],
531 identifierQuotePairs: ['"' => '"', '[' => ']'],
532 namedParameterSeparators: [':' => ['::'], '$' => []],
533 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
534 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
535 backslashEscapedStringPrefixes: ['E'],
536 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
537 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
538 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
539 identifierStartPattern: '/^[_A-Za-z]$/',
540 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
541 bracketPair: ['[', ']'],
542 nestingPair: ['(', ')'],
543 statementDelimiter: ';',
544 listDelimiter: ',',
545 nestedBlockComments: true,
546 backslashEscapedStrings: false,
547 );
548
549 self::assertTrue($profile->isBracketClosing(']'));
550 self::assertFalse($profile->isBracketClosing(')'));
551 }
552
553 public function testIsNestingOpeningAnswersForTheCharacterThatOpensOne(): void
554 {
555 $profile = FakeSqlLexerProfiles::custom(
556 lineCommentPrefixes: ['--', '#'],
557 whitespaceDelimitedLineCommentPrefixes: ['//'],
558 blockCommentPairs: ['/*' => '*/'],
559 stringQuotePairs: ["'" => "'"],
560 identifierQuotePairs: ['"' => '"', '[' => ']'],
561 namedParameterSeparators: [':' => ['::'], '$' => []],
562 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
563 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
564 backslashEscapedStringPrefixes: ['E'],
565 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
566 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
567 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
568 identifierStartPattern: '/^[_A-Za-z]$/',
569 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
570 bracketPair: ['[', ']'],
571 nestingPair: ['(', ')'],
572 statementDelimiter: ';',
573 listDelimiter: ',',
574 nestedBlockComments: true,
575 backslashEscapedStrings: false,
576 );
577
578 self::assertTrue($profile->isNestingOpening('('));
579 }
580
581 public function testIsNestingClosingAnswersForTheCharacterThatClosesOne(): void
582 {
583 $profile = FakeSqlLexerProfiles::custom(
584 lineCommentPrefixes: ['--', '#'],
585 whitespaceDelimitedLineCommentPrefixes: ['//'],
586 blockCommentPairs: ['/*' => '*/'],
587 stringQuotePairs: ["'" => "'"],
588 identifierQuotePairs: ['"' => '"', '[' => ']'],
589 namedParameterSeparators: [':' => ['::'], '$' => []],
590 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
591 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
592 backslashEscapedStringPrefixes: ['E'],
593 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
594 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
595 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
596 identifierStartPattern: '/^[_A-Za-z]$/',
597 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
598 bracketPair: ['[', ']'],
599 nestingPair: ['(', ')'],
600 statementDelimiter: ';',
601 listDelimiter: ',',
602 nestedBlockComments: true,
603 backslashEscapedStrings: false,
604 );
605
606 self::assertTrue($profile->isNestingClosing(')'));
607 }
608
609 public function testIsStatementDelimiterAnswersForTheSymbolThatEndsAStatement(): void
610 {
611 $profile = FakeSqlLexerProfiles::custom(
612 lineCommentPrefixes: ['--', '#'],
613 whitespaceDelimitedLineCommentPrefixes: ['//'],
614 blockCommentPairs: ['/*' => '*/'],
615 stringQuotePairs: ["'" => "'"],
616 identifierQuotePairs: ['"' => '"', '[' => ']'],
617 namedParameterSeparators: [':' => ['::'], '$' => []],
618 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
619 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
620 backslashEscapedStringPrefixes: ['E'],
621 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
622 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
623 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
624 identifierStartPattern: '/^[_A-Za-z]$/',
625 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
626 bracketPair: ['[', ']'],
627 nestingPair: ['(', ')'],
628 statementDelimiter: ';',
629 listDelimiter: ',',
630 nestedBlockComments: true,
631 backslashEscapedStrings: false,
632 );
633
634 self::assertTrue($profile->isStatementDelimiter(';'));
635 }
636
637 public function testListDelimiterAnswersTheSeparatorBetweenListItems(): void
638 {
639 $profile = FakeSqlLexerProfiles::custom(
640 lineCommentPrefixes: ['--', '#'],
641 whitespaceDelimitedLineCommentPrefixes: ['//'],
642 blockCommentPairs: ['/*' => '*/'],
643 stringQuotePairs: ["'" => "'"],
644 identifierQuotePairs: ['"' => '"', '[' => ']'],
645 namedParameterSeparators: [':' => ['::'], '$' => []],
646 namedParameterSuffixPatterns: [':' => '/^<[^ ]*>/', '$' => '/^\([^ ]*\)/'],
647 namedParameterForbiddenPredecessors: [':' => [':'], '$' => []],
648 backslashEscapedStringPrefixes: ['E'],
649 positionalParameterPatterns: ['/^\$[0-9]+/', '/^\?[0-9]*/'],
650 dollarQuoteDelimiterPattern: '/^\$(?:[_A-Za-z][_A-Za-z0-9]*)?\$/',
651 numericLiteralPattern: '/^(?:0[xX][0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[0-9]+(?:_[0-9]+)*)/',
652 identifierStartPattern: '/^[_A-Za-z]$/',
653 identifierPartPattern: '/^[_A-Za-z0-9$]$/',
654 bracketPair: ['[', ']'],
655 nestingPair: ['(', ')'],
656 statementDelimiter: ';',
657 listDelimiter: ',',
658 nestedBlockComments: true,
659 backslashEscapedStrings: false,
660 );
661
662 self::assertSame(',', $profile->listDelimiter());
663 }
664
665}
666