packages/ztd-query-core/src/Sql/Profile/SqlSymbolProfile.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Sql\Profile;
6
7use ZtdQuery\Exception\InvalidDefinitionException;
8use ZtdQuery\Sql\LexicalPattern;
9
10/**
11 * How one dialect spells what is written plainly.
12 *
13 * Which characters may spell an identifier, how far a number runs, what
14 * nests, what brackets, and what ends a statement or separates a list are
15 * all asked here, because they are the shapes a scanner tells apart without
16 * knowing what any of them mean.
17 */
18final class SqlSymbolProfile
19{
20 /** @var array{non-empty-string, non-empty-string}|null */
21 private readonly ?array $bracketPair;
22
23 /** @var array{non-empty-string, non-empty-string} */
24 private readonly array $nestingPair;
25
26 /**
27 * @param string $numericLiteralPattern Pattern a number is written as
28 * @param string $identifierStartPattern Pattern the first character of an identifier matches
29 * @param string $identifierPartPattern Pattern every later character of an identifier matches
30 * @param array{string, string}|null $bracketPair Opening and closing bracket, or null where the dialect has none
31 * @param array{string, string} $nestingPair Opening and closing delimiter that nest
32 * @param string $statementDelimiter Single character that ends a statement
33 * @param string $listDelimiter Single character that separates list items
34 * @param LexicalPattern $patterns Reads a regular expression against a position
35 *
36 * @throws InvalidDefinitionException When a pattern is unreadable, a delimiter is empty, or a single-character delimiter is not one character
37 */
38 public function __construct(
39 private readonly string $numericLiteralPattern,
40 private readonly string $identifierStartPattern,
41 private readonly string $identifierPartPattern,
42 ?array $bracketPair,
43 array $nestingPair,
44 private readonly string $statementDelimiter,
45 private readonly string $listDelimiter,
46 private readonly LexicalPattern $patterns = new LexicalPattern(),
47 ) {
48 $this->patterns->assertValid($this->numericLiteralPattern);
49 $this->patterns->assertValid($this->identifierStartPattern);
50 $this->patterns->assertValid($this->identifierPartPattern);
51 if ($bracketPair !== null && ($bracketPair[0] === '' || $bracketPair[1] === '')) {
52 throw new InvalidDefinitionException('Bracket delimiters must not be empty.');
53 }
54 /** @var array{non-empty-string, non-empty-string}|null $bracketPair */
55 $this->bracketPair = $bracketPair;
56 if ($nestingPair[0] === '' || $nestingPair[1] === '') {
57 throw new InvalidDefinitionException('Nesting delimiters must not be empty.');
58 }
59 /** @var array{non-empty-string, non-empty-string} $nestingPair */
60 $this->nestingPair = $nestingPair;
61 if (strlen($this->statementDelimiter) !== 1 || strlen($this->listDelimiter) !== 1) {
62 throw new InvalidDefinitionException('Statement and list delimiters must be single characters.');
63 }
64 }
65
66 /**
67 * Answers how long the number starting here is.
68 *
69 * @param string $sql Statement being scanned
70 * @param int $offset Position to look at
71 *
72 * @return int Its length, or zero when no number starts there
73 */
74 public function numberLengthAt(string $sql, int $offset): int
75 {
76 $match = $this->patterns->matchAt($this->numericLiteralPattern, $sql, $offset);
77
78 return $match === null ? 0 : strlen($match);
79 }
80
81 /**
82 * Reports whether an identifier may begin with this character.
83 *
84 * @param string $character Character to test
85 *
86 * @return bool True when it may
87 */
88 public function isIdentifierStart(string $character): bool
89 {
90 return $this->patterns->matchesCharacter($this->identifierStartPattern, $character);
91 }
92
93 /**
94 * Reports whether an identifier may continue with this character.
95 *
96 * @param string $character Character to test
97 *
98 * @return bool True when it may
99 */
100 public function isIdentifierPart(string $character): bool
101 {
102 return $this->patterns->matchesCharacter($this->identifierPartPattern, $character);
103 }
104
105 /**
106 * Reports whether this character opens a bracket.
107 *
108 * @param string $character Character to test
109 *
110 * @return bool True when it does, and false where the dialect brackets nothing
111 */
112 public function isBracketOpening(string $character): bool
113 {
114 return $this->bracketPair !== null && $character === $this->bracketPair[0];
115 }
116
117 /**
118 * Reports whether this character closes a bracket.
119 *
120 * @param string $character Character to test
121 *
122 * @return bool True when it does, and false where the dialect brackets nothing
123 */
124 public function isBracketClosing(string $character): bool
125 {
126 return $this->bracketPair !== null && $character === $this->bracketPair[1];
127 }
128
129 /**
130 * Reports whether this character opens a nesting.
131 *
132 * @param string $character Character to test
133 *
134 * @return bool True when it does
135 */
136 public function isNestingOpening(string $character): bool
137 {
138 return $character === $this->nestingPair[0];
139 }
140
141 /**
142 * Reports whether this character closes a nesting.
143 *
144 * @param string $character Character to test
145 *
146 * @return bool True when it does
147 */
148 public function isNestingClosing(string $character): bool
149 {
150 return $character === $this->nestingPair[1];
151 }
152
153 /**
154 * Reports whether this symbol ends a statement.
155 *
156 * @param string $symbol Symbol to test
157 *
158 * @return bool True when it does
159 */
160 public function isStatementDelimiter(string $symbol): bool
161 {
162 return $symbol === $this->statementDelimiter;
163 }
164
165 /**
166 * Answers the character that separates list items.
167 *
168 * @return string The separator
169 */
170 public function listDelimiter(): string
171 {
172 return $this->listDelimiter;
173 }
174}
175