Passed
Branch master (e82d75)
by Wilmer
07:17 queued 02:54
created

SqlTokenizer   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 305
Duplicated Lines 0 %

Test Coverage

Coverage 98.48%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 17
eloc 190
c 2
b 0
f 0
dl 0
loc 305
ccs 195
cts 198
cp 0.9848
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A isStringLiteral() 0 19 4
A isIdentifier() 0 26 6
A isWhitespace() 0 6 1
A isComment() 0 13 3
A isOperator() 0 30 1
B isKeyword() 0 138 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite;
6
7
use function mb_strtoupper;
8
use function strtr;
9
10
/**
11
 * SqlTokenizer splits SQLite queries into individual SQL tokens.
12
 *
13
 * It's used to obtain a `CHECK` constraint information from a `CREATE TABLE` SQL code.
14
 *
15
 * {@see http://www.sqlite.org/draft/tokenreq.html}
16
 * {@see https://sqlite.org/lang.html}
17
 */
18
final class SqlTokenizer extends BaseTokenizer
19
{
20
    /**
21
     * Returns whether there's a whitespace at the current offset.
22
     *
23
     * If this method returns `true`, it has to set the `$length` parameter to the length of the matched string.
24
     *
25
     * @param int $length length of the matched string.
26
     *
27
     * @return bool whether there's a whitespace at the current offset.
28
     */
29 21
    protected function isWhitespace(int &$length): bool
30
    {
31 21
        $whitespaces = ["\f" => true, "\n" => true, "\r" => true, "\t" => true, ' ' => true];
32 21
        $length = 1;
33
34 21
        return isset($whitespaces[$this->substring($length)]);
35
    }
36
37
    /**
38
     * Returns whether there's a commentary at the current offset.
39
     *
40
     * If these methods returns `true`, it has to set the `$length` parameter to the length of the matched string.
41
     *
42
     * @param int $length length of the matched string.
43
     *
44
     * @return bool whether there's a commentary at the current offset.
45
     */
46 21
    protected function isComment(int &$length): bool
47
    {
48 21
        $comments = ['--' => true, '/*' => true];
49 21
        $length = 2;
50
51 21
        if (!isset($comments[$this->substring($length)])) {
52 21
            return false;
53
        }
54
55
        $char = $this->substring($length) === '--' ? "\n" : '*/';
56
        $length = $this->indexAfter($char) - $this->offset;
57
58
        return true;
59
    }
60
61
    /**
62
     * Returns whether there's an operator at the current offset.
63
     *
64
     * If these methods returns `true`, it has to set the `$length` parameter to the length of the matched string. It may
65
     * also set `$content` to a string that will be used as a token content.
66
     *
67
     * @param int $length  length of the matched string.
68
     * @param string|null $content optional content instead of the matched string.
69
     *
70
     * @return bool whether there's an operator at the current offset.
71
     */
72 21
    protected function isOperator(int &$length, string|null &$content): bool
73
    {
74 21
        $operators = [
75 21
            '!=',
76 21
            '%',
77 21
            '&',
78 21
            '(',
79 21
            ')',
80 21
            '*',
81 21
            '+',
82 21
            ',',
83 21
            '-',
84 21
            '.',
85 21
            '/',
86 21
            ';',
87 21
            '<',
88 21
            '<<',
89 21
            '<=',
90 21
            '<>',
91 21
            '=',
92 21
            '==',
93 21
            '>',
94 21
            '>=',
95 21
            '>>',
96 21
            '|',
97 21
            '||',
98 21
            '~',
99 21
        ];
100
101 21
        return $this->startsWithAnyLongest($operators, true, $length);
102
    }
103
104
    /**
105
     * Returns whether there's an identifier at the current offset.
106
     *
107
     * If this method returns `true`, it has to set the `$length` parameter to the length of the matched string. It may
108
     * also set `$content` to a string that will be used as a token content.
109
     *
110
     * @param int $length length of the matched string.
111
     * @param string|null $content optional content instead of the matched string.
112
     *
113
     * @return bool whether there's an identifier at the current offset.
114
     */
115 21
    protected function isIdentifier(int &$length, string|null &$content): bool
116
    {
117 21
        $identifierDelimiters = ['"' => '"', '[' => ']', '`' => '`'];
118
119 21
        if (!isset($identifierDelimiters[$this->substring(1)])) {
120 21
            return false;
121
        }
122
123 21
        $delimiter = $identifierDelimiters[$this->substring(1)];
124 21
        $offset = $this->offset;
125
126 21
        while (true) {
127 21
            $offset = $this->indexAfter($delimiter, $offset + 1);
128 21
            if ($delimiter === ']' || $this->substring(1, true, $offset) !== $delimiter) {
129 21
                break;
130
            }
131
        }
132
133 21
        $length = $offset - $this->offset;
134 21
        $content = $this->substring($length - 2, true, $this->offset + 1);
135
136 21
        if ($delimiter !== ']') {
137 21
            $content = strtr($content, ["$delimiter$delimiter" => $delimiter]);
138
        }
139
140 21
        return true;
141
    }
142
143
    /**
144
     * Returns whether there's a string literal at the current offset.
145
     *
146
     * If this method returns `true`, it has to set the `$length` parameter to the length of the matched string. It may
147
     * also set `$content` to a string that will be used as a token content.
148
     *
149
     * @param int $length  length of the matched string.
150
     * @param string|null $content optional content instead of the matched string.
151
     *
152
     * @return bool whether there's a string literal at the current offset.
153
     */
154 21
    protected function isStringLiteral(int &$length, string|null &$content): bool
155
    {
156 21
        if ($this->substring(1) !== "'") {
157 21
            return false;
158
        }
159
160 4
        $offset = $this->offset;
161
162 4
        while (true) {
163 4
            $offset = $this->indexAfter("'", $offset + 1);
164 4
            if ($this->substring(1, true, $offset) !== "'") {
165 4
                break;
166
            }
167
        }
168
169 4
        $length = $offset - $this->offset;
170 4
        $content = strtr($this->substring($length - 2, true, $this->offset + 1), ["''" => "'"]);
171
172 4
        return true;
173
    }
174
175
    /**
176
     * Returns whether the given string is a keyword.
177
     *
178
     * The method may set `$content` to a string that will be used as a token content.
179
     *
180
     * @param string $string  string to be matched.
181
     * @param string|null $content optional content instead of the matched string.
182
     *
183
     * @return bool whether the given string is a keyword.
184
     */
185 21
    protected function isKeyword(string $string, string|null &$content): bool
186
    {
187 21
        $keywords = [
188 21
            'ABORT' => true,
189 21
            'ACTION' => true,
190 21
            'ADD' => true,
191 21
            'AFTER' => true,
192 21
            'ALL' => true,
193 21
            'ALTER' => true,
194 21
            'ANALYZE' => true,
195 21
            'AND' => true,
196 21
            'AS' => true,
197 21
            'ASC' => true,
198 21
            'ATTACH' => true,
199 21
            'AUTOINCREMENT' => true,
200 21
            'BEFORE' => true,
201 21
            'BEGIN' => true,
202 21
            'BETWEEN' => true,
203 21
            'BY' => true,
204 21
            'CASCADE' => true,
205 21
            'CASE' => true,
206 21
            'CAST' => true,
207 21
            'CHECK' => true,
208 21
            'COLLATE' => true,
209 21
            'COLUMN' => true,
210 21
            'COMMIT' => true,
211 21
            'CONFLICT' => true,
212 21
            'CONSTRAINT' => true,
213 21
            'CREATE' => true,
214 21
            'CROSS' => true,
215 21
            'CURRENT_DATE' => true,
216 21
            'CURRENT_TIME' => true,
217 21
            'CURRENT_TIMESTAMP' => true,
218 21
            'DATABASE' => true,
219 21
            'DEFAULT' => true,
220 21
            'DEFERRABLE' => true,
221 21
            'DEFERRED' => true,
222 21
            'DELETE' => true,
223 21
            'DESC' => true,
224 21
            'DETACH' => true,
225 21
            'DISTINCT' => true,
226 21
            'DROP' => true,
227 21
            'EACH' => true,
228 21
            'ELSE' => true,
229 21
            'END' => true,
230 21
            'ESCAPE' => true,
231 21
            'EXCEPT' => true,
232 21
            'EXCLUSIVE' => true,
233 21
            'EXISTS' => true,
234 21
            'EXPLAIN' => true,
235 21
            'FAIL' => true,
236 21
            'FOR' => true,
237 21
            'FOREIGN' => true,
238 21
            'FROM' => true,
239 21
            'FULL' => true,
240 21
            'GLOB' => true,
241 21
            'GROUP' => true,
242 21
            'HAVING' => true,
243 21
            'IF' => true,
244 21
            'IGNORE' => true,
245 21
            'IMMEDIATE' => true,
246 21
            'IN' => true,
247 21
            'INDEX' => true,
248 21
            'INDEXED' => true,
249 21
            'INITIALLY' => true,
250 21
            'INNER' => true,
251 21
            'INSERT' => true,
252 21
            'INSTEAD' => true,
253 21
            'INTERSECT' => true,
254 21
            'INTO' => true,
255 21
            'IS' => true,
256 21
            'ISNULL' => true,
257 21
            'JOIN' => true,
258 21
            'KEY' => true,
259 21
            'LEFT' => true,
260 21
            'LIKE' => true,
261 21
            'LIMIT' => true,
262 21
            'MATCH' => true,
263 21
            'NATURAL' => true,
264 21
            'NO' => true,
265 21
            'NOT' => true,
266 21
            'NOTNULL' => true,
267 21
            'NULL' => true,
268 21
            'OF' => true,
269 21
            'OFFSET' => true,
270 21
            'ON' => true,
271 21
            'OR' => true,
272 21
            'ORDER' => true,
273 21
            'OUTER' => true,
274 21
            'PLAN' => true,
275 21
            'PRAGMA' => true,
276 21
            'PRIMARY' => true,
277 21
            'QUERY' => true,
278 21
            'RAISE' => true,
279 21
            'RECURSIVE' => true,
280 21
            'REFERENCES' => true,
281 21
            'REGEXP' => true,
282 21
            'REINDEX' => true,
283 21
            'RELEASE' => true,
284 21
            'RENAME' => true,
285 21
            'REPLACE' => true,
286 21
            'RESTRICT' => true,
287 21
            'RIGHT' => true,
288 21
            'ROLLBACK' => true,
289 21
            'ROW' => true,
290 21
            'SAVEPOINT' => true,
291 21
            'SELECT' => true,
292 21
            'SET' => true,
293 21
            'TABLE' => true,
294 21
            'TEMP' => true,
295 21
            'TEMPORARY' => true,
296 21
            'THEN' => true,
297 21
            'TO' => true,
298 21
            'TRANSACTION' => true,
299 21
            'TRIGGER' => true,
300 21
            'UNION' => true,
301 21
            'UNIQUE' => true,
302 21
            'UPDATE' => true,
303 21
            'USING' => true,
304 21
            'VACUUM' => true,
305 21
            'VALUES' => true,
306 21
            'VIEW' => true,
307 21
            'VIRTUAL' => true,
308 21
            'WHEN' => true,
309 21
            'WHERE' => true,
310 21
            'WITH' => true,
311 21
            'WITHOUT' => true,
312 21
        ];
313
314 21
        $string = mb_strtoupper($string, 'UTF-8');
315
316 21
        if (!isset($keywords[$string])) {
317 20
            return false;
318
        }
319
320 21
        $content = $string;
321
322 21
        return true;
323
    }
324
}
325