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