packages/sql-parser/src/Sqlite/Lexer/QuotedScanner.php
1<?php
2
3declare(strict_types=1);
4
5namespace SqlParser\Sqlite\Lexer;
6
7use SqlParser\Lexer\Lexeme;
8use SqlParser\Lexer\LexicalException;
9
10/**
11 * Reads SQLite's quoted tokens: strings, quoted identifiers and blob literals.
12 *
13 * Single quotes delimit strings; double quotes, backticks and brackets
14 * delimit identifiers. A quote inside doubles itself, except in brackets.
15 *
16 * @visibility root
17 */
18final class QuotedScanner
19{
20 /**
21 * Reads the quoted token at the cursor, if one starts there.
22 *
23 * @param Scan $scan The tokenization in progress
24 *
25 * @return Lexeme|null The lexeme, or null when no quoted token starts here
26 *
27 * @throws LexicalException When the token never closes or a blob is malformed
28 */
29 public function scan(Scan $scan): ?Lexeme
30 {
31 $cursor = $scan->cursor;
32 $start = $cursor->offset();
33 $character = $cursor->peek();
34 if ($character === "'" || $character === '"' || $character === '`') {
35 if ($cursor->takeQuoted($character) === null) {
36 throw LexicalException::unterminated($character === "'" ? 'string' : 'quoted identifier', $cursor->source, $start);
37 }
38
39 return $scan->lexeme($character === "'" ? 'STRING' : 'ID', $start);
40 }
41 if ($character === '[') {
42 if (!$cursor->skipPastOrEnd(']')) {
43 throw LexicalException::unterminated('bracketed identifier', $cursor->source, $start);
44 }
45
46 return $scan->lexeme('ID', $start);
47 }
48 if (($character === 'x' || $character === 'X') && $cursor->peek(1) === "'") {
49 if ($cursor->match("[xX]'(?:[0-9A-Fa-f]{2})*'") === null) {
50 throw LexicalException::unexpectedCharacter($cursor->source, $start);
51 }
52
53 return $scan->lexeme('BLOB', $start);
54 }
55
56 return null;
57 }
58}
59