master
ブランチ master
LittleBeast / src / Std / Lib / Db / Sqlite / Parser.php
48.5 KB、1351行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, and/or sell copies of the Software, subject to the following conditions:
10
11 1. **Origin Attribution**:
12 - You must not misrepresent the origin of the Software; you must not claim
13 you created the original Software.
14 - If the Software is used in a product, you must either:
15 a. Provide clear attribution in the product's documentation, user interface,
16 or other visible areas, **OR**
17 b. Pay the original developers a fee they specify in writing.
18 2. **Usage Restriction**:
19 - The Software, or any derivative works, dependencies, or libraries
20 incorporating it, must not be used for censorship or to suppress freedom of
21 speech, expression, or creativity. Prohibited uses include, but are not
22 limited to:
23 - Censorship of so-called "hate speech", visuals, non-mainstream opinions,
24 ideas, or objective reality.
25 - Tools or systems designed to restrict access to information or
26 artistic works.
27 3. **Notice Preservation**:
28 - This license and the above copyright notice must remain intact in all copies
29 of the source code.
30
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Lib\Db\Sqlite;
39
40use Std\Lib\Db\DbException;
41
42/**
43 * SQLiteのSQLの構文解析(字句解析も)
44 *
45 * 結果は配列の構文木。式は['t' => 種類, …]。
46 */
47final class Parser {
48 // 別名(AS無し)や名前に使えない言葉
49 private const RESERVED = [
50 'ADD', 'ALL', 'ALTER', 'AND', 'AS', 'BETWEEN', 'BY', 'CASE', 'CHECK', 'COLLATE', 'COMMIT', 'CONSTRAINT', 'CREATE', 'CROSS',
51 'DEFAULT', 'DELETE', 'DISTINCT', 'DROP', 'ELSE', 'END', 'ESCAPE', 'EXCEPT', 'EXISTS', 'FOREIGN', 'FROM', 'FULL', 'GLOB',
52 'GROUP', 'HAVING', 'IN', 'INDEX', 'INNER', 'INSERT', 'INTERSECT', 'INTO', 'IS', 'ISNULL', 'JOIN', 'LEFT', 'LIKE', 'LIMIT',
53 'NATURAL', 'NOT', 'NOTNULL', 'NULL', 'OFFSET', 'ON', 'OR', 'ORDER', 'OUTER', 'PRIMARY', 'REFERENCES', 'RETURNING',
54 'RIGHT', 'SELECT', 'SET', 'TABLE', 'THEN', 'TO', 'UNION', 'UNIQUE', 'UPDATE', 'USING', 'VALUES', 'WHEN', 'WHERE',
55 'WINDOW', 'WITH', 'REGEXP', 'MATCH',
56 ];
57
58 private array $tok = [];
59 private int $p = 0;
60 private string $sql = '';
61 private int $nParam = 0;
62 /** 名前付きのパラメータ(「:name」等) => 番号 */
63 private array $names = [];
64 private int $lastNameStart = 0;
65
66 /**
67 * SQLを文に分ける
68 *
69 * @param string $sql
70 * @return array 文の構文木の配列(各文の'sql'に元の文字列)
71 * @throws DbException
72 */
73 public function parseAll(string $sql): array {
74 $this->sql = $sql;
75 $this->tok = $this->lex($sql);
76 $this->p = 0;
77 $out = [];
78 while (true) {
79 while ($this->isOp(';')) $this->p++;
80 if ($this->cur()['t'] === 'eof') break;
81 $this->nParam = 0;
82 $start = $this->cur()['s'];
83 $this->names = [];
84 $st = $this->statement();
85 $end = $this->tok[$this->p - 1]['e'];
86 $st['sql'] = substr($sql, $start, $end - $start);
87 $st['start'] = $start;
88 $st['nParam'] = $this->nParam;
89 $st['names'] = $this->names;
90 $out[] = $st;
91 if (!$this->isOp(';') && $this->cur()['t'] !== 'eof') $this->error();
92 }
93 return $out;
94 }
95
96 /** 式だけ(DEFAULT・CHECK等の文字列から) */
97 public function parseExpr(string $sql): array {
98 $this->sql = $sql;
99 $this->tok = $this->lex($sql);
100 $this->p = 0;
101 $e = $this->expr();
102 if ($this->cur()['t'] !== 'eof') $this->error();
103 return $e;
104 }
105
106 ///////////////
107 // 字句解析
108
109 private function lex(string $s): array {
110 $out = [];
111 $len = strlen($s);
112 $i = 0;
113 while ($i < $len) {
114 $c = $s[$i];
115 if (ctype_space($c)) {
116 $i++;
117 continue;
118 }
119 if ($c === '-' && ($s[$i + 1] ?? '') === '-') {
120 $j = strpos($s, "\n", $i);
121 $i = $j === false ? $len : $j + 1;
122 continue;
123 }
124 if ($c === '/' && ($s[$i + 1] ?? '') === '*') {
125 $j = strpos($s, '*/', $i + 2);
126 $i = $j === false ? $len : $j + 2;
127 continue;
128 }
129 $st = $i;
130 if ($c === "'") {
131 $v = '';
132 $i++;
133 while (true) {
134 $j = strpos($s, "'", $i);
135 if ($j === false) throw new DbException('閉じられていない文字列', 1, 'HY000');
136 $v .= substr($s, $i, $j - $i);
137 if (($s[$j + 1] ?? '') === "'") {
138 $v .= "'";
139 $i = $j + 2;
140 continue;
141 }
142 $i = $j + 1;
143 break;
144 }
145 $out[] = ['t' => 'str', 'v' => $v, 's' => $st, 'e' => $i];
146 continue;
147 }
148 if ($c === '"' || $c === '`' || $c === '[') {
149 $close = $c === '[' ? ']' : $c;
150 $v = '';
151 $i++;
152 while (true) {
153 $j = strpos($s, $close, $i);
154 if ($j === false) throw new DbException('閉じられていない識別子', 1, 'HY000');
155 $v .= substr($s, $i, $j - $i);
156 if ($close !== ']' && ($s[$j + 1] ?? '') === $close) {
157 $v .= $close;
158 $i = $j + 2;
159 continue;
160 }
161 $i = $j + 1;
162 break;
163 }
164 $out[] = ['t' => 'id', 'v' => $v, 'u' => '', 'q' => true, 's' => $st, 'e' => $i];
165 continue;
166 }
167 if (($c === 'x' || $c === 'X') && ($s[$i + 1] ?? '') === "'") {
168 $j = strpos($s, "'", $i + 2);
169 if ($j === false) throw new DbException('閉じられていないBLOB', 1, 'HY000');
170 $hex = substr($s, $i + 2, $j - $i - 2);
171 if (strlen($hex) % 2 || !ctype_xdigit($hex) && $hex !== '') throw new DbException("不正なBLOB:{$hex}", 1, 'HY000');
172 $i = $j + 1;
173 $out[] = ['t' => 'blob', 'v' => (string)hex2bin($hex), 's' => $st, 'e' => $i];
174 continue;
175 }
176 if (ctype_digit($c) || ($c === '.' && ctype_digit($s[$i + 1] ?? ''))) {
177 if ($c === '0' && ($s[$i + 1] ?? '') !== '' && strtolower($s[$i + 1]) === 'x' && preg_match('/\G0[xX]([0-9a-fA-F]+)/', $s, $m, 0, $i)) {
178 $i += strlen($m[0]);
179 $hex = str_pad($m[1], 16, '0', STR_PAD_LEFT);
180 if (strlen($hex) > 16) throw new DbException("16進数が大き過ぎます:{$m[0]}", 1, 'HY000');
181 $out[] = ['t' => 'num', 'v' => unpack('J', hex2bin($hex))[1], 's' => $st, 'e' => $i];
182 continue;
183 }
184 preg_match('/\G(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/', $s, $m, 0, $i);
185 $i += strlen($m[0]);
186 if (preg_match('/\G[A-Za-z_]/', $s, $mm, 0, $i)) throw new DbException("認識できないトークン:{$m[0]}".substr($s, $i, 1), 1, 'HY000');
187 $txt = $m[0];
188 if (ctype_digit($txt)) {
189 $t2 = ltrim($txt, '0');
190 $v = $t2 === '' ? 0 : ((string)(int)$t2 === $t2 ? (int)$t2 : (float)$txt);
191 } else {
192 $v = (float)$txt;
193 }
194 $out[] = ['t' => 'num', 'v' => $v, 's' => $st, 'e' => $i];
195 continue;
196 }
197 if (ctype_alpha($c) || $c === '_' || ord($c) >= 0x80) {
198 preg_match('/\G[A-Za-z0-9_$\x80-\xFF]+/', $s, $m, 0, $i);
199 $i += strlen($m[0]);
200 $out[] = ['t' => 'id', 'v' => $m[0], 'u' => strtoupper($m[0]), 'q' => false, 's' => $st, 'e' => $i];
201 continue;
202 }
203 if ($c === '?') {
204 preg_match('/\G\?(\d*)/', $s, $m, 0, $i);
205 $i += strlen($m[0]);
206 $out[] = ['t' => 'param', 'v' => $m[1] === '' ? null : (int)$m[1], 's' => $st, 'e' => $i];
207 continue;
208 }
209 if (($c === ':' || $c === '@' || $c === '$') && preg_match('/\G[:@$][A-Za-z0-9_\x80-\xFF]+/', $s, $m, 0, $i)) {
210 $i += strlen($m[0]);
211 $out[] = ['t' => 'param', 'v' => $m[0], 's' => $st, 'e' => $i];
212 continue;
213 }
214 foreach (['->>', '||', '<<', '>>', '<=', '>=', '==', '!=', '<>', '->'] as $op) {
215 if (substr($s, $i, strlen($op)) === $op) {
216 $i += strlen($op);
217 $out[] = ['t' => 'op', 'v' => $op, 's' => $st, 'e' => $i];
218 continue 2;
219 }
220 }
221 if (str_contains('(),;.=<>+-*/%&|~', $c)) {
222 $i++;
223 $out[] = ['t' => 'op', 'v' => $c, 's' => $st, 'e' => $i];
224 continue;
225 }
226 throw new DbException("認識できない文字:{$c}", 1, 'HY000');
227 }
228 $out[] = ['t' => 'eof', 'v' => '', 's' => $len, 'e' => $len];
229 return $out;
230 }
231
232 ///////////////
233 // 補助
234
235 private function cur(): array {
236 return $this->tok[$this->p];
237 }
238
239 private function peek(int $n = 1): array {
240 return $this->tok[min($this->p + $n, count($this->tok) - 1)];
241 }
242
243 private function isKw(string ...$kw): bool {
244 $t = $this->tok[$this->p];
245 return $t['t'] === 'id' && !$t['q'] && in_array($t['u'], $kw, true);
246 }
247
248 private function acceptKw(string ...$kw): ?string {
249 if (!$this->isKw(...$kw)) return null;
250 return $this->tok[$this->p++]['u'];
251 }
252
253 private function expectKw(string $kw): void {
254 if (!$this->acceptKw($kw)) $this->error($kw);
255 }
256
257 private function isOp(string $op): bool {
258 $t = $this->tok[$this->p];
259 return $t['t'] === 'op' && $t['v'] === $op;
260 }
261
262 private function acceptOp(string $op): bool {
263 if (!$this->isOp($op)) return false;
264 $this->p++;
265 return true;
266 }
267
268 private function expectOp(string $op): void {
269 if (!$this->acceptOp($op)) $this->error($op);
270 }
271
272 /** @return never */
273 private function error(string $expected = ''): never {
274 $t = $this->cur();
275 $near = $t['t'] === 'eof' ? '最後' : '「'.substr($this->sql, $t['s'], max(1, min(30, $t['e'] - $t['s']))).'」';
276 throw new DbException("SQLの構文エラー:{$near}の近く".($expected !== '' ? "({$expected}が必要)" : ''), 1, '42000');
277 }
278
279 /** 名前(識別子、予約語以外の言葉、文字列も可) */
280 private function name(bool $allowReserved = false): string {
281 $t = $this->cur();
282 if ($t['t'] === 'id' && ($t['q'] || $allowReserved || !in_array($t['u'], self::RESERVED, true))) {
283 $this->p++;
284 return $t['v'];
285 }
286 if ($t['t'] === 'str') {
287 $this->p++;
288 return $t['v'];
289 }
290 $this->error('名前');
291 }
292
293 private function isName(): bool {
294 $t = $this->cur();
295 return ($t['t'] === 'id' && ($t['q'] || !in_array($t['u'], self::RESERVED, true))) || $t['t'] === 'str';
296 }
297
298 /** 「schema.name」(schemaはmain・tempのみ) */
299 private function qualifiedName(): string {
300 $this->lastNameStart = $this->cur()['s'];
301 $n = $this->name();
302 if ($this->isOp('.') && $this->peek()['t'] === 'id') {
303 if (!in_array(strtolower($n), ['main', 'temp'], true)) throw new DbException("不明なデータベース:{$n}", 1, 'HY000');
304 $this->p++;
305 $this->lastNameStart = $this->cur()['s'];
306 $n = $this->name();
307 }
308 return $n;
309 }
310
311 /** 字句解析だけ(スキーマのSQLの書き換え用) */
312 public function tokens(string $sql): array {
313 return $this->lex($sql);
314 }
315
316 private function ifNotExists(): bool {
317 if ($this->isKw('IF') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'NOT') {
318 $this->p += 2;
319 $this->expectKw('EXISTS');
320 return true;
321 }
322 return false;
323 }
324
325 private function ifExists(): bool {
326 if ($this->isKw('IF') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'EXISTS') {
327 $this->p += 2;
328 return true;
329 }
330 return false;
331 }
332
333 ///////////////
334 // 文
335
336 private function statement(): array {
337 if ($this->acceptKw('EXPLAIN')) throw new DbException('EXPLAINは未対応です', 1, 'HY000');
338 if ($this->isKw('SELECT', 'VALUES', 'WITH')) {
339 $with = $this->isKw('WITH') ? $this->with() : [];
340 if ($this->isKw('INSERT', 'REPLACE')) return $this->insert() + ['with' => $with];
341 if ($this->isKw('UPDATE')) return $this->update() + ['with' => $with];
342 if ($this->isKw('DELETE')) return $this->delete() + ['with' => $with];
343 $s = $this->select(false);
344 $s['with'] = array_merge($with, $s['with']);
345 return $s;
346 }
347 if ($this->isOp('(')) return $this->select();
348 if ($this->isKw('INSERT', 'REPLACE')) return $this->insert();
349 if ($this->isKw('UPDATE')) return $this->update();
350 if ($this->isKw('DELETE')) return $this->delete();
351 if ($this->acceptKw('CREATE')) return $this->create();
352 if ($this->acceptKw('DROP')) {
353 $kind = $this->acceptKw('TABLE', 'INDEX', 'VIEW', 'TRIGGER') ?? $this->error('TABLE');
354 $if = $this->ifExists();
355 return ['t' => 'drop', 'kind' => strtolower($kind), 'name' => $this->qualifiedName(), 'ifExists' => $if];
356 }
357 if ($this->acceptKw('ALTER')) return $this->alter();
358 if ($this->acceptKw('BEGIN')) {
359 $mode = $this->acceptKw('DEFERRED', 'IMMEDIATE', 'EXCLUSIVE') ?? 'DEFERRED';
360 if ($this->acceptKw('TRANSACTION') && $this->isName()) $this->name();
361 return ['t' => 'begin', 'mode' => $mode];
362 }
363 if ($this->acceptKw('COMMIT', 'END')) {
364 $this->acceptKw('TRANSACTION');
365 return ['t' => 'commit'];
366 }
367 if ($this->acceptKw('ROLLBACK')) {
368 $this->acceptKw('TRANSACTION');
369 if ($this->acceptKw('TO')) {
370 $this->acceptKw('SAVEPOINT');
371 return ['t' => 'rollbackTo', 'name' => $this->name()];
372 }
373 return ['t' => 'rollback'];
374 }
375 if ($this->acceptKw('SAVEPOINT')) return ['t' => 'savepoint', 'name' => $this->name()];
376 if ($this->acceptKw('RELEASE')) {
377 $this->acceptKw('SAVEPOINT');
378 return ['t' => 'release', 'name' => $this->name()];
379 }
380 if ($this->acceptKw('PRAGMA')) return $this->pragma();
381 if ($this->acceptKw('VACUUM', 'ANALYZE', 'REINDEX')) {
382 while (!$this->isOp(';') && $this->cur()['t'] !== 'eof') $this->p++;
383 return ['t' => 'noop'];
384 }
385 if ($this->isKw('ATTACH', 'DETACH')) throw new DbException('ATTACH・DETACHは未対応です', 1, 'HY000');
386 $this->error();
387 }
388
389 private function pragma(): array {
390 $name = strtolower($this->qualifiedName());
391 $arg = null;
392 if ($this->acceptOp('=')) {
393 $arg = $this->pragmaValue();
394 } else if ($this->acceptOp('(')) {
395 $arg = $this->pragmaValue();
396 $this->expectOp(')');
397 }
398 return ['t' => 'pragma', 'name' => $name, 'arg' => $arg];
399 }
400
401 private function pragmaValue(): mixed {
402 $t = $this->cur();
403 $neg = $this->acceptOp('-');
404 $t = $this->cur();
405 $this->p++;
406 if ($t['t'] === 'num') return $neg ? -$t['v'] : $t['v'];
407 if ($t['t'] === 'str' || $t['t'] === 'id') return $t['v'];
408 $this->p--;
409 $this->error('値');
410 }
411
412 /** WITH名前(列) AS (SELECT…), … */
413 private function with(): array {
414 $this->expectKw('WITH');
415 if ($this->acceptKw('RECURSIVE')) $recursive = true;
416 $out = [];
417 do {
418 $name = $this->name();
419 $cols = null;
420 if ($this->acceptOp('(')) {
421 $cols = [];
422 do $cols[] = $this->name(); while ($this->acceptOp(','));
423 $this->expectOp(')');
424 }
425 $this->expectKw('AS');
426 if ($this->acceptKw('NOT')) $this->expectKw('MATERIALIZED');
427 else $this->acceptKw('MATERIALIZED');
428 $this->expectOp('(');
429 $sel = $this->select();
430 $this->expectOp(')');
431 $out[strtolower($name)] = ['sel' => $sel, 'cols' => $cols, 'name' => $name, 'recursive' => !empty($recursive)];
432 } while ($this->acceptOp(','));
433 return $out;
434 }
435
436 /**
437 * SELECT(複合も)
438 *
439 * @return array t、with、cores、ops、order、limit、offset
440 */
441 private function select(bool $allowWith = true): array {
442 $with = $allowWith && $this->isKw('WITH') ? $this->with() : [];
443 $cores = [$this->selectCore()];
444 $ops = [];
445 while ($op = $this->acceptKw('UNION', 'INTERSECT', 'EXCEPT')) {
446 if ($op === 'UNION' && $this->acceptKw('ALL')) $op = 'UNION ALL';
447 $ops[] = $op;
448 $cores[] = $this->selectCore();
449 }
450 $order = [];
451 if ($this->acceptKw('ORDER')) {
452 $this->expectKw('BY');
453 $order = $this->orderList();
454 }
455 $limit = $offset = null;
456 if ($this->acceptKw('LIMIT')) {
457 $limit = $this->expr();
458 if ($this->acceptKw('OFFSET')) {
459 $offset = $this->expr();
460 } else if ($this->acceptOp(',')) {
461 $offset = $limit;
462 $limit = $this->expr();
463 }
464 }
465 return ['t' => 'select', 'with' => $with, 'cores' => $cores, 'ops' => $ops, 'order' => $order, 'limit' => $limit, 'offset' => $offset];
466 }
467
468 private function orderList(): array {
469 $out = [];
470 do {
471 $e = $this->expr();
472 $desc = $this->acceptKw('DESC') !== null;
473 if (!$desc) $this->acceptKw('ASC');
474 $nulls = null;
475 if ($this->acceptKw('NULLS')) $nulls = $this->acceptKw('FIRST', 'LAST') ?? $this->error('FIRST');
476 $out[] = ['e' => $e, 'desc' => $desc, 'nulls' => $nulls];
477 } while ($this->acceptOp(','));
478 return $out;
479 }
480
481 private function selectCore(): array {
482 if ($this->acceptOp('(')) {
483 // 括弧の中のSELECT(複合の一部)
484 $s = $this->select();
485 $this->expectOp(')');
486 return ['sub' => $s];
487 }
488 if ($this->acceptKw('VALUES')) {
489 $rows = [];
490 do {
491 $this->expectOp('(');
492 $row = [];
493 do $row[] = $this->expr(); while ($this->acceptOp(','));
494 $this->expectOp(')');
495 $rows[] = $row;
496 } while ($this->acceptOp(','));
497 return ['values' => $rows];
498 }
499 $this->expectKw('SELECT');
500 $distinct = $this->acceptKw('DISTINCT') !== null;
501 if (!$distinct) $this->acceptKw('ALL');
502 $cols = [];
503 do {
504 if ($this->acceptOp('*')) {
505 $cols[] = ['star' => null];
506 continue;
507 }
508 if ($this->cur()['t'] === 'id' && $this->peek()['t'] === 'op' && $this->peek()['v'] === '.'
509 && $this->peek(2)['t'] === 'op' && $this->peek(2)['v'] === '*') {
510 $tbl = $this->cur()['v'];
511 $this->p += 3;
512 $cols[] = ['star' => $tbl];
513 continue;
514 }
515 $s = $this->cur()['s'];
516 $e = $this->expr();
517 $text = substr($this->sql, $s, $this->tok[$this->p - 1]['e'] - $s);
518 $as = null;
519 if ($this->acceptKw('AS')) $as = $this->name(true);
520 else if ($this->isName()) $as = $this->name();
521 $cols[] = ['e' => $e, 'as' => $as, 'text' => $text];
522 } while ($this->acceptOp(','));
523 $from = $this->acceptKw('FROM') ? $this->from() : [];
524 $where = $this->acceptKw('WHERE') ? $this->expr() : null;
525 $group = [];
526 $having = null;
527 if ($this->acceptKw('GROUP')) {
528 $this->expectKw('BY');
529 do $group[] = $this->expr(); while ($this->acceptOp(','));
530 }
531 if ($this->acceptKw('HAVING')) $having = $this->expr();
532 if ($this->isKw('WINDOW')) throw new DbException('ウィンドウ関数は未対応です', 1, 'HY000');
533 return ['distinct' => $distinct, 'cols' => $cols, 'from' => $from, 'where' => $where, 'group' => $group, 'having' => $having];
534 }
535
536 /** FROM:テーブル・サブクエリと結合の配列 */
537 private function from(): array {
538 $out = [$this->fromItem('inner')];
539 while (true) {
540 if ($this->acceptOp(',')) {
541 $out[] = $this->fromItem('inner');
542 continue;
543 }
544 $natural = $this->acceptKw('NATURAL') !== null;
545 $kind = null;
546 if ($this->acceptKw('LEFT')) {
547 $this->acceptKw('OUTER');
548 $kind = 'left';
549 } else if ($this->acceptKw('INNER')) {
550 $kind = 'inner';
551 } else if ($this->acceptKw('CROSS')) {
552 $kind = 'inner';
553 } else if ($this->isKw('RIGHT', 'FULL')) {
554 throw new DbException('RIGHT JOIN・FULL JOINは未対応です', 1, 'HY000');
555 }
556 if ($kind === null && !$this->isKw('JOIN')) {
557 if ($natural) $this->error('JOIN');
558 break;
559 }
560 $this->expectKw('JOIN');
561 $item = $this->fromItem($kind ?? 'inner');
562 $item['natural'] = $natural;
563 if ($this->acceptKw('ON')) {
564 $item['on'] = $this->expr();
565 } else if ($this->acceptKw('USING')) {
566 $this->expectOp('(');
567 $item['using'] = [];
568 do $item['using'][] = $this->name(); while ($this->acceptOp(','));
569 $this->expectOp(')');
570 }
571 $out[] = $item;
572 }
573 return $out;
574 }
575
576 private function fromItem(string $join): array {
577 $item = ['join' => $join, 'on' => null, 'using' => null, 'natural' => false];
578 if ($this->acceptOp('(')) {
579 if ($this->isKw('SELECT', 'VALUES', 'WITH')) {
580 $item['sel'] = $this->select();
581 $this->expectOp(')');
582 } else {
583 // (a JOIN b):サブクエリにする
584 $from = $this->from();
585 $this->expectOp(')');
586 $item['sel'] = ['t' => 'select', 'with' => [], 'cores' => [['distinct' => false, 'cols' => [['star' => null]], 'from' => $from,
587 'where' => null, 'group' => [], 'having' => null]], 'ops' => [], 'order' => [], 'limit' => null, 'offset' => null];
588 }
589 } else {
590 $name = $this->qualifiedName();
591 if ($this->acceptOp('(')) throw new DbException("テーブル値関数は未対応です:{$name}", 1, 'HY000');
592 $item['table'] = $name;
593 }
594 $item['as'] = null;
595 if ($this->acceptKw('AS')) $item['as'] = $this->name();
596 else if ($this->isName() && !$this->isKw('INDEXED')) $item['as'] = $this->name();
597 if ($this->acceptKw('INDEXED')) {
598 $this->expectKw('BY');
599 $this->name();
600 } else if ($this->isKw('NOT') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'INDEXED') {
601 $this->p += 2;
602 }
603 return $item;
604 }
605
606 private function conflictClause(): ?string {
607 if ($this->acceptKw('OR')) return $this->acceptKw('ROLLBACK', 'ABORT', 'REPLACE', 'FAIL', 'IGNORE') ?? $this->error('REPLACE');
608 return null;
609 }
610
611 private function returning(): ?array {
612 if (!$this->acceptKw('RETURNING')) return null;
613 $cols = [];
614 do {
615 if ($this->acceptOp('*')) {
616 $cols[] = ['star' => null];
617 continue;
618 }
619 $s = $this->cur()['s'];
620 $e = $this->expr();
621 $text = substr($this->sql, $s, $this->tok[$this->p - 1]['e'] - $s);
622 $as = null;
623 if ($this->acceptKw('AS')) $as = $this->name(true);
624 else if ($this->isName()) $as = $this->name();
625 $cols[] = ['e' => $e, 'as' => $as, 'text' => $text];
626 } while ($this->acceptOp(','));
627 return $cols;
628 }
629
630 private function insert(): array {
631 $or = null;
632 if ($this->acceptKw('REPLACE')) {
633 $or = 'REPLACE';
634 } else {
635 $this->expectKw('INSERT');
636 $or = $this->conflictClause();
637 }
638 $this->expectKw('INTO');
639 $table = $this->qualifiedName();
640 $alias = null;
641 if ($this->acceptKw('AS')) $alias = $this->name();
642 $cols = null;
643 if ($this->acceptOp('(')) {
644 $cols = [];
645 do $cols[] = $this->name(); while ($this->acceptOp(','));
646 $this->expectOp(')');
647 }
648 $sel = null;
649 $default = false;
650 if ($this->acceptKw('DEFAULT')) {
651 $this->expectKw('VALUES');
652 $default = true;
653 } else {
654 $sel = $this->select();
655 }
656 $upsert = [];
657 while ($this->isKw('ON') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'CONFLICT') {
658 $this->p += 2;
659 $target = null;
660 $twhere = null;
661 if ($this->acceptOp('(')) {
662 $target = [];
663 do {
664 $target[] = $this->name();
665 if ($this->acceptKw('COLLATE')) $this->name();
666 $this->acceptKw('ASC', 'DESC');
667 } while ($this->acceptOp(','));
668 $this->expectOp(')');
669 if ($this->acceptKw('WHERE')) $twhere = $this->expr();
670 }
671 $this->expectKw('DO');
672 if ($this->acceptKw('NOTHING')) {
673 $upsert[] = ['target' => $target, 'nothing' => true];
674 continue;
675 }
676 $this->expectKw('UPDATE');
677 $this->expectKw('SET');
678 $set = $this->setList();
679 $where = $this->acceptKw('WHERE') ? $this->expr() : null;
680 $upsert[] = ['target' => $target, 'nothing' => false, 'set' => $set, 'where' => $where];
681 }
682 return ['t' => 'insert', 'or' => $or, 'table' => $table, 'alias' => $alias, 'cols' => $cols, 'sel' => $sel,
683 'default' => $default, 'upsert' => $upsert, 'returning' => $this->returning(), 'with' => []];
684 }
685
686 private function setList(): array {
687 $set = [];
688 do {
689 if ($this->acceptOp('(')) {
690 $names = [];
691 do $names[] = $this->name(); while ($this->acceptOp(','));
692 $this->expectOp(')');
693 $this->expectOp('=');
694 $this->expectOp('(');
695 $vals = [];
696 do $vals[] = $this->expr(); while ($this->acceptOp(','));
697 $this->expectOp(')');
698 if (count($names) !== count($vals)) throw new DbException('SETの列と値の数が違います', 1, 'HY000');
699 foreach ($names as $i => $n) $set[] = [$n, $vals[$i]];
700 continue;
701 }
702 $n = $this->name();
703 $this->expectOp('=');
704 $set[] = [$n, $this->expr()];
705 } while ($this->acceptOp(','));
706 return $set;
707 }
708
709 private function update(): array {
710 $this->expectKw('UPDATE');
711 $or = $this->conflictClause();
712 $table = $this->qualifiedName();
713 $alias = null;
714 if ($this->acceptKw('AS')) $alias = $this->name();
715 else if ($this->isName() && !$this->isKw('SET', 'INDEXED')) $alias = $this->name();
716 if ($this->acceptKw('INDEXED')) {
717 $this->expectKw('BY');
718 $this->name();
719 }
720 $this->expectKw('SET');
721 $set = $this->setList();
722 $from = $this->acceptKw('FROM') ? $this->from() : [];
723 $where = $this->acceptKw('WHERE') ? $this->expr() : null;
724 return ['t' => 'update', 'or' => $or, 'table' => $table, 'alias' => $alias, 'set' => $set, 'from' => $from, 'where' => $where,
725 'returning' => $this->returning(), 'with' => []];
726 }
727
728 private function delete(): array {
729 $this->expectKw('DELETE');
730 $this->expectKw('FROM');
731 $table = $this->qualifiedName();
732 $alias = null;
733 if ($this->acceptKw('AS')) $alias = $this->name();
734 else if ($this->isName() && !$this->isKw('INDEXED')) $alias = $this->name();
735 if ($this->acceptKw('INDEXED')) {
736 $this->expectKw('BY');
737 $this->name();
738 }
739 $where = $this->acceptKw('WHERE') ? $this->expr() : null;
740 return ['t' => 'delete', 'table' => $table, 'alias' => $alias, 'where' => $where, 'returning' => $this->returning(), 'with' => []];
741 }
742
743 ///////////////
744 // CREATE・ALTER
745
746 private function create(): array {
747 $temp = $this->acceptKw('TEMP', 'TEMPORARY') !== null;
748 $unique = $this->acceptKw('UNIQUE') !== null;
749 if ($this->acceptKw('TABLE')) {
750 if ($temp) throw new DbException('一時テーブルは未対応です', 1, 'HY000');
751 return $this->createTable();
752 }
753 if ($this->acceptKw('INDEX')) return $this->createIndex($unique);
754 if ($this->acceptKw('VIEW')) {
755 $if = $this->ifNotExists();
756 $name = $this->qualifiedName();
757 $nameTok = ['s' => $this->lastNameStart];
758 $cols = null;
759 if ($this->acceptOp('(')) {
760 $cols = [];
761 do $cols[] = $this->name(); while ($this->acceptOp(','));
762 $this->expectOp(')');
763 }
764 $this->expectKw('AS');
765 $sel = $this->select();
766 return ['t' => 'createView', 'name' => $name, 'ifNotExists' => $if, 'cols' => $cols, 'sel' => $sel, 'nameStart' => $nameTok['s']];
767 }
768 if ($this->acceptKw('TRIGGER')) throw new DbException('トリガーは未対応です', 1, 'HY000');
769 if ($this->acceptKw('VIRTUAL')) throw new DbException('仮想テーブルは未対応です', 1, 'HY000');
770 $this->error('TABLE');
771 }
772
773 private function createTable(): array {
774 $if = $this->ifNotExists();
775 $name = $this->qualifiedName();
776 $nameTok = ['s' => $this->lastNameStart];
777 if ($this->acceptKw('AS')) {
778 return ['t' => 'createTable', 'name' => $name, 'ifNotExists' => $if, 'as' => $this->select(), 'nameStart' => $nameTok['s']];
779 }
780 $def = $this->tableBody();
781 return ['t' => 'createTable', 'name' => $name, 'ifNotExists' => $if, 'def' => $def, 'nameStart' => $nameTok['s']];
782 }
783
784 /**
785 * テーブルの定義(括弧から最後まで)
786 *
787 * @return array cols(name、type、notNull、default、pk、autoinc、unique、collate、check、pkDesc)、pk、uniques、checks、withoutRowid、strict
788 */
789 public function tableBody(): array {
790 $this->expectOp('(');
791 $cols = [];
792 $pk = null;
793 $uniques = [];
794 $checks = [];
795 $pkAuto = false;
796 // 自動のインデックスの順番(sqlite_autoindex_テーブル_番号)
797 $order = [];
798 while (true) {
799 if ($this->isKw('CONSTRAINT', 'PRIMARY', 'UNIQUE', 'CHECK', 'FOREIGN')) {
800 if ($this->acceptKw('CONSTRAINT')) $this->name();
801 if ($this->acceptKw('PRIMARY')) {
802 $this->expectKw('KEY');
803 $list = $this->indexedColumns();
804 if ($this->acceptKw('AUTOINCREMENT')) $pkAuto = true;
805 $this->onConflict();
806 if ($pk !== null) throw new DbException("テーブルに主キーが複数有ります", 1, 'HY000');
807 $pk = $list;
808 $order[] = ['pk', $list];
809 } else if ($this->acceptKw('UNIQUE')) {
810 $list = $this->indexedColumns();
811 $uniques[] = $list;
812 $order[] = ['unique', $list];
813 $this->onConflict();
814 } else if ($this->acceptKw('CHECK')) {
815 $s = $this->cur()['s'];
816 $this->expectOp('(');
817 $checks[] = $this->expr();
818 $this->expectOp(')');
819 } else if ($this->acceptKw('FOREIGN')) {
820 $this->expectKw('KEY');
821 $this->expectOp('(');
822 do $this->name(); while ($this->acceptOp(','));
823 $this->expectOp(')');
824 $this->references();
825 } else {
826 $this->error();
827 }
828 } else {
829 $c = $this->columnDef();
830 foreach ($c['cons'] as $k) $order[] = [$k, [['name' => $c['name'], 'desc' => $k === 'pk' && $c['pkDesc'], 'collate' => null]]];
831 $cols[] = $c;
832 }
833 if ($this->acceptOp(',')) continue;
834 $this->expectOp(')');
835 break;
836 }
837 $withoutRowid = false;
838 $strict = false;
839 while (true) {
840 if ($this->acceptKw('WITHOUT')) {
841 if (!$this->isKw('ROWID')) $this->error('ROWID');
842 $this->p++;
843 $withoutRowid = true;
844 } else if ($this->acceptKw('STRICT')) {
845 $strict = true;
846 } else {
847 break;
848 }
849 if (!$this->acceptOp(',')) break;
850 }
851 foreach ($cols as $c) {
852 if ($c['pk']) {
853 if ($pk !== null) throw new DbException("テーブルに主キーが複数有ります", 1, 'HY000');
854 $pk = [['name' => $c['name'], 'desc' => $c['pkDesc'], 'collate' => null]];
855 $pkAuto = $c['autoinc'];
856 }
857 }
858 return ['cols' => $cols, 'pk' => $pk, 'pkAuto' => $pkAuto, 'uniques' => $uniques, 'checks' => $checks,
859 'withoutRowid' => $withoutRowid, 'strict' => $strict, 'order' => $order];
860 }
861
862 private function onConflict(): void {
863 if ($this->isKw('ON') && $this->peek()['t'] === 'id' && $this->peek()['u'] === 'CONFLICT') {
864 $this->p += 2;
865 $this->acceptKw('ROLLBACK', 'ABORT', 'FAIL', 'IGNORE', 'REPLACE') ?? $this->error('ABORT');
866 }
867 }
868
869 private function references(): void {
870 $this->expectKw('REFERENCES');
871 $this->name();
872 if ($this->acceptOp('(')) {
873 do $this->name(); while ($this->acceptOp(','));
874 $this->expectOp(')');
875 }
876 while (true) {
877 if ($this->acceptKw('ON')) {
878 $this->acceptKw('DELETE', 'UPDATE') ?? $this->error('DELETE');
879 if ($this->acceptKw('SET')) $this->acceptKw('NULL', 'DEFAULT') ?? $this->error('NULL');
880 else if ($this->acceptKw('NO')) $this->expectKw('ACTION');
881 else $this->acceptKw('CASCADE', 'RESTRICT') ?? $this->error('CASCADE');
882 } else if ($this->acceptKw('MATCH')) {
883 $this->name();
884 } else if ($this->isKw('NOT') && $this->peek()['u'] === 'DEFERRABLE' || $this->isKw('DEFERRABLE')) {
885 $this->acceptKw('NOT');
886 $this->expectKw('DEFERRABLE');
887 if ($this->acceptKw('INITIALLY')) $this->acceptKw('DEFERRED', 'IMMEDIATE');
888 } else {
889 break;
890 }
891 }
892 }
893
894 /** (列 [COLLATE x] [ASC|DESC], …) */
895 private function indexedColumns(): array {
896 $this->expectOp('(');
897 $out = [];
898 do {
899 $t = $this->cur();
900 $e = $this->expr();
901 $coll = null;
902 if ($e['t'] === 'collate') {
903 $coll = strtoupper($e['coll']);
904 $e = $e['e'];
905 }
906 if ($e['t'] !== 'col' || $e['tbl'] !== null) throw new DbException('式のインデックスは未対応です', 1, 'HY000');
907 $desc = $this->acceptKw('DESC') !== null;
908 if (!$desc) $this->acceptKw('ASC');
909 $out[] = ['name' => $e['name'], 'desc' => $desc, 'collate' => $coll];
910 } while ($this->acceptOp(','));
911 $this->expectOp(')');
912 return $out;
913 }
914
915 private function columnDef(): array {
916 $c = ['name' => $this->name(), 'type' => '', 'notNull' => false, 'default' => null, 'pk' => false, 'pkDesc' => false,
917 'autoinc' => false, 'unique' => false, 'collate' => null, 'checks' => [], 'defaultText' => null, 'cons' => []];
918 // 型の名前
919 $words = [];
920 while ($this->cur()['t'] === 'id' && !$this->cur()['q'] && !$this->isKw('CONSTRAINT', 'PRIMARY', 'NOT', 'NULL', 'UNIQUE',
921 'CHECK', 'DEFAULT', 'COLLATE', 'REFERENCES', 'GENERATED', 'AS')) {
922 $words[] = $this->cur()['v'];
923 $this->p++;
924 }
925 if (empty($words) && $this->cur()['t'] === 'id' && $this->cur()['q']) {
926 $words[] = $this->cur()['v'];
927 $this->p++;
928 }
929 $type = implode(' ', $words);
930 if ($this->acceptOp('(')) {
931 $args = [];
932 do {
933 $neg = $this->acceptOp('-') ? '-' : ($this->acceptOp('+') ? '' : '');
934 $t = $this->cur();
935 if ($t['t'] !== 'num') $this->error('数');
936 $this->p++;
937 $args[] = $neg.substr($this->sql, $t['s'], $t['e'] - $t['s']);
938 } while ($this->acceptOp(','));
939 $this->expectOp(')');
940 $type .= '('.implode(',', $args).')';
941 }
942 $c['type'] = $type;
943 while (true) {
944 if ($this->acceptKw('CONSTRAINT')) $this->name();
945 if ($this->acceptKw('PRIMARY')) {
946 $this->expectKw('KEY');
947 $c['pk'] = true;
948 $c['cons'][] = 'pk';
949 $c['pkDesc'] = $this->acceptKw('DESC') !== null;
950 if (!$c['pkDesc']) $this->acceptKw('ASC');
951 $this->onConflict();
952 if ($this->acceptKw('AUTOINCREMENT')) $c['autoinc'] = true;
953 } else if ($this->acceptKw('NOT')) {
954 $this->expectKw('NULL');
955 $c['notNull'] = true;
956 $this->onConflict();
957 } else if ($this->acceptKw('NULL')) {
958 $this->onConflict();
959 } else if ($this->acceptKw('UNIQUE')) {
960 $c['unique'] = true;
961 $c['cons'][] = 'unique';
962 $this->onConflict();
963 } else if ($this->acceptKw('CHECK')) {
964 $this->expectOp('(');
965 $c['checks'][] = $this->expr();
966 $this->expectOp(')');
967 } else if ($this->acceptKw('DEFAULT')) {
968 $ds = $this->cur()['s'];
969 if ($this->acceptOp('(')) {
970 $c['default'] = $this->expr();
971 $this->expectOp(')');
972 } else {
973 $neg = $this->acceptOp('-');
974 if (!$neg) $this->acceptOp('+');
975 $t = $this->cur();
976 if ($t['t'] === 'id' && !$t['q'] && in_array($t['u'], ['CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'TRUE', 'FALSE', 'NULL'], true)) {
977 $c['default'] = $this->primary();
978 } else if ($t['t'] === 'id') {
979 // 識別子は文字列として
980 $this->p++;
981 $c['default'] = ['t' => 'lit', 'v' => $t['v']];
982 } else {
983 $c['default'] = $this->primary();
984 }
985 if ($neg) $c['default'] = ['t' => 'un', 'op' => '-', 'e' => $c['default']];
986 }
987 $c['defaultText'] = substr($this->sql, $ds, $this->tok[$this->p - 1]['e'] - $ds);
988 } else if ($this->acceptKw('COLLATE')) {
989 $c['collate'] = strtoupper($this->name());
990 } else if ($this->isKw('REFERENCES')) {
991 $this->references();
992 } else if ($this->isKw('GENERATED', 'AS')) {
993 throw new DbException('生成列は未対応です', 1, 'HY000');
994 } else {
995 break;
996 }
997 }
998 return $c;
999 }
1000
1001 private function createIndex(bool $unique): array {
1002 $if = $this->ifNotExists();
1003 $name = $this->qualifiedName();
1004 $nameTok = ['s' => $this->lastNameStart];
1005 $this->expectKw('ON');
1006 $table = $this->name();
1007 $cols = $this->indexedColumns();
1008 if ($this->acceptKw('WHERE')) throw new DbException('部分インデックスは未対応です', 1, 'HY000');
1009 return ['t' => 'createIndex', 'name' => $name, 'table' => $table, 'unique' => $unique, 'cols' => $cols, 'ifNotExists' => $if,
1010 'nameStart' => $nameTok['s']];
1011 }
1012
1013 private function alter(): array {
1014 $this->expectKw('TABLE');
1015 $table = $this->qualifiedName();
1016 if ($this->acceptKw('RENAME')) {
1017 if ($this->acceptKw('TO')) return ['t' => 'alter', 'op' => 'rename', 'table' => $table, 'to' => $this->name()];
1018 $this->acceptKw('COLUMN');
1019 $from = $this->name();
1020 $this->expectKw('TO');
1021 return ['t' => 'alter', 'op' => 'renameColumn', 'table' => $table, 'from' => $from, 'to' => $this->name()];
1022 }
1023 if ($this->acceptKw('ADD')) {
1024 $this->acceptKw('COLUMN');
1025 $s = $this->cur()['s'];
1026 $col = $this->columnDef();
1027 $text = substr($this->sql, $s, $this->tok[$this->p - 1]['e'] - $s);
1028 return ['t' => 'alter', 'op' => 'addColumn', 'table' => $table, 'col' => $col, 'text' => $text];
1029 }
1030 if ($this->acceptKw('DROP')) {
1031 $this->acceptKw('COLUMN');
1032 return ['t' => 'alter', 'op' => 'dropColumn', 'table' => $table, 'col' => $this->name()];
1033 }
1034 $this->error('RENAME');
1035 }
1036
1037 ///////////////
1038 // 式
1039
1040 public function expr(): array {
1041 $l = $this->andExpr();
1042 while ($this->acceptKw('OR')) $l = ['t' => 'bin', 'op' => 'OR', 'l' => $l, 'r' => $this->andExpr()];
1043 return $l;
1044 }
1045
1046 private function andExpr(): array {
1047 $l = $this->notExpr();
1048 while ($this->acceptKw('AND')) $l = ['t' => 'bin', 'op' => 'AND', 'l' => $l, 'r' => $this->notExpr()];
1049 return $l;
1050 }
1051
1052 private function notExpr(): array {
1053 if ($this->acceptKw('NOT')) return ['t' => 'un', 'op' => 'NOT', 'e' => $this->notExpr()];
1054 return $this->equality();
1055 }
1056
1057 private function equality(): array {
1058 $l = $this->comparison();
1059 while (true) {
1060 if ($this->acceptOp('=') || $this->acceptOp('==')) {
1061 $l = ['t' => 'bin', 'op' => '=', 'l' => $l, 'r' => $this->comparison()];
1062 } else if ($this->acceptOp('!=') || $this->acceptOp('<>')) {
1063 $l = ['t' => 'bin', 'op' => '!=', 'l' => $l, 'r' => $this->comparison()];
1064 } else if ($this->acceptKw('IS')) {
1065 $not = $this->acceptKw('NOT') !== null;
1066 if ($this->acceptKw('DISTINCT')) {
1067 $this->expectKw('FROM');
1068 $not = !$not;
1069 }
1070 $l = ['t' => 'is', 'not' => $not, 'l' => $l, 'r' => $this->comparison()];
1071 } else if ($this->acceptKw('ISNULL')) {
1072 $l = ['t' => 'isnull', 'not' => false, 'e' => $l];
1073 } else if ($this->acceptKw('NOTNULL')) {
1074 $l = ['t' => 'isnull', 'not' => true, 'e' => $l];
1075 } else if ($this->isKw('NOT') && $this->peek()['t'] === 'id' && in_array($this->peek()['u'], ['NULL', 'IN', 'LIKE', 'GLOB', 'BETWEEN', 'REGEXP', 'MATCH'], true)) {
1076 $this->p++;
1077 if ($this->acceptKw('NULL')) {
1078 $l = ['t' => 'isnull', 'not' => true, 'e' => $l];
1079 } else {
1080 $l = $this->postfixOp($l, true);
1081 }
1082 } else if ($this->isKw('IN', 'LIKE', 'GLOB', 'BETWEEN', 'REGEXP', 'MATCH')) {
1083 $l = $this->postfixOp($l, false);
1084 } else {
1085 return $l;
1086 }
1087 }
1088 }
1089
1090 private function postfixOp(array $l, bool $not): array {
1091 if ($this->acceptKw('IN')) {
1092 if ($this->acceptOp('(')) {
1093 if ($this->isKw('SELECT', 'VALUES', 'WITH')) {
1094 $sel = $this->select();
1095 $this->expectOp(')');
1096 return ['t' => 'in', 'not' => $not, 'e' => $l, 'list' => null, 'sel' => $sel];
1097 }
1098 $list = [];
1099 if (!$this->isOp(')')) {
1100 do $list[] = $this->expr(); while ($this->acceptOp(','));
1101 }
1102 $this->expectOp(')');
1103 return ['t' => 'in', 'not' => $not, 'e' => $l, 'list' => $list, 'sel' => null];
1104 }
1105 $name = $this->qualifiedName();
1106 return ['t' => 'in', 'not' => $not, 'e' => $l, 'list' => null, 'sel' => null, 'table' => $name];
1107 }
1108 if ($this->acceptKw('BETWEEN')) {
1109 $lo = $this->comparison();
1110 $this->expectKw('AND');
1111 $hi = $this->comparison();
1112 return ['t' => 'between', 'not' => $not, 'e' => $l, 'lo' => $lo, 'hi' => $hi];
1113 }
1114 $op = $this->acceptKw('LIKE', 'GLOB', 'REGEXP', 'MATCH');
1115 if ($op === 'REGEXP' || $op === 'MATCH') throw new DbException("{$op}は未対応です", 1, 'HY000');
1116 $r = $this->comparison();
1117 $esc = $this->acceptKw('ESCAPE') ? $this->comparison() : null;
1118 return ['t' => 'like', 'op' => $op, 'not' => $not, 'l' => $l, 'r' => $r, 'esc' => $esc];
1119 }
1120
1121 private function comparison(): array {
1122 $l = $this->bitwise();
1123 while (true) {
1124 foreach (['<=', '>=', '<', '>'] as $op) {
1125 if ($this->acceptOp($op)) {
1126 $l = ['t' => 'bin', 'op' => $op, 'l' => $l, 'r' => $this->bitwise()];
1127 continue 2;
1128 }
1129 }
1130 return $l;
1131 }
1132 }
1133
1134 private function bitwise(): array {
1135 $l = $this->additive();
1136 while (true) {
1137 foreach (['<<', '>>', '&', '|'] as $op) {
1138 if ($this->acceptOp($op)) {
1139 $l = ['t' => 'bin', 'op' => $op, 'l' => $l, 'r' => $this->additive()];
1140 continue 2;
1141 }
1142 }
1143 return $l;
1144 }
1145 }
1146
1147 private function additive(): array {
1148 $l = $this->multiplicative();
1149 while (true) {
1150 if ($this->acceptOp('+')) $l = ['t' => 'bin', 'op' => '+', 'l' => $l, 'r' => $this->multiplicative()];
1151 else if ($this->acceptOp('-')) $l = ['t' => 'bin', 'op' => '-', 'l' => $l, 'r' => $this->multiplicative()];
1152 else return $l;
1153 }
1154 }
1155
1156 private function multiplicative(): array {
1157 $l = $this->concat();
1158 while (true) {
1159 foreach (['*', '/', '%'] as $op) {
1160 if ($this->acceptOp($op)) {
1161 $l = ['t' => 'bin', 'op' => $op, 'l' => $l, 'r' => $this->concat()];
1162 continue 2;
1163 }
1164 }
1165 return $l;
1166 }
1167 }
1168
1169 private function concat(): array {
1170 $l = $this->unary();
1171 while (true) {
1172 if ($this->acceptOp('||')) $l = ['t' => 'bin', 'op' => '||', 'l' => $l, 'r' => $this->unary()];
1173 else if ($this->acceptOp('->>')) $l = ['t' => 'fn', 'name' => 'json_extract_text', 'args' => [$l, $this->unary()], 'distinct' => false, 'star' => false];
1174 else if ($this->acceptOp('->')) $l = ['t' => 'fn', 'name' => 'json_extract_json', 'args' => [$l, $this->unary()], 'distinct' => false, 'star' => false];
1175 else return $l;
1176 }
1177 }
1178
1179 private function unary(): array {
1180 if ($this->acceptOp('-')) {
1181 $e = $this->unary();
1182 if ($e['t'] === 'lit' && (is_int($e['v']) || is_float($e['v']))) {
1183 // -9223372036854775808
1184 if (is_float($e['v']) && $e['v'] === 9.2233720368547758e18) return ['t' => 'lit', 'v' => PHP_INT_MIN];
1185 return ['t' => 'lit', 'v' => -$e['v']];
1186 }
1187 return ['t' => 'un', 'op' => '-', 'e' => $e];
1188 }
1189 if ($this->acceptOp('+')) return ['t' => 'un', 'op' => '+', 'e' => $this->unary()];
1190 if ($this->acceptOp('~')) return ['t' => 'un', 'op' => '~', 'e' => $this->unary()];
1191 $e = $this->primary();
1192 while ($this->acceptKw('COLLATE')) $e = ['t' => 'collate', 'e' => $e, 'coll' => strtoupper($this->name())];
1193 return $e;
1194 }
1195
1196 private function primary(): array {
1197 $t = $this->cur();
1198 switch ($t['t']) {
1199 case 'num':
1200 $this->p++;
1201 return ['t' => 'lit', 'v' => $t['v']];
1202 case 'str':
1203 $this->p++;
1204 return ['t' => 'lit', 'v' => $t['v']];
1205 case 'blob':
1206 $this->p++;
1207 return ['t' => 'lit', 'v' => new Blob($t['v'])];
1208 case 'param':
1209 $this->p++;
1210 if ($t['v'] === null) {
1211 $i = ++$this->nParam;
1212 } else if (is_int($t['v'])) {
1213 $i = $t['v'];
1214 $this->nParam = max($this->nParam, $i);
1215 } else {
1216 // 名前付き:最初に出た順番で番号を付ける(SQLiteと同じ)
1217 $i = $this->names[$t['v']] ??= ++$this->nParam;
1218 }
1219 return ['t' => 'param', 'i' => $i];
1220 case 'op':
1221 if ($t['v'] === '(') {
1222 $this->p++;
1223 if ($this->isKw('SELECT', 'VALUES', 'WITH')) {
1224 $sel = $this->select();
1225 $this->expectOp(')');
1226 return ['t' => 'sub', 'sel' => $sel];
1227 }
1228 $e = $this->expr();
1229 if ($this->isOp(',')) {
1230 $list = [$e];
1231 while ($this->acceptOp(',')) $list[] = $this->expr();
1232 $this->expectOp(')');
1233 return ['t' => 'vector', 'list' => $list];
1234 }
1235 $this->expectOp(')');
1236 return $e;
1237 }
1238 $this->error();
1239 case 'id':
1240 if (!$t['q']) {
1241 switch ($t['u']) {
1242 case 'NULL':
1243 $this->p++;
1244 return ['t' => 'lit', 'v' => null];
1245 case 'TRUE':
1246 $this->p++;
1247 return ['t' => 'lit', 'v' => 1];
1248 case 'FALSE':
1249 $this->p++;
1250 return ['t' => 'lit', 'v' => 0];
1251 case 'CURRENT_TIMESTAMP':
1252 case 'CURRENT_DATE':
1253 case 'CURRENT_TIME':
1254 $this->p++;
1255 return ['t' => 'fn', 'name' => strtolower($t['u']), 'args' => [], 'distinct' => false, 'star' => false];
1256 case 'CASE':
1257 return $this->caseExpr();
1258 case 'CAST':
1259 $this->p++;
1260 $this->expectOp('(');
1261 $e = $this->expr();
1262 $this->expectKw('AS');
1263 $words = [];
1264 while ($this->cur()['t'] === 'id') {
1265 $words[] = $this->cur()['v'];
1266 $this->p++;
1267 }
1268 if ($this->acceptOp('(')) {
1269 while (!$this->acceptOp(')')) $this->p++;
1270 }
1271 $this->expectOp(')');
1272 return ['t' => 'cast', 'e' => $e, 'type' => implode(' ', $words)];
1273 case 'EXISTS':
1274 $this->p++;
1275 $this->expectOp('(');
1276 $sel = $this->select();
1277 $this->expectOp(')');
1278 return ['t' => 'exists', 'sel' => $sel];
1279 case 'NOT':
1280 $this->error();
1281 case 'RAISE':
1282 throw new DbException('RAISEは未対応です', 1, 'HY000');
1283 }
1284 }
1285 $this->p++;
1286 // 関数
1287 if ($this->isOp('(') && !$t['q']) {
1288 $this->p++;
1289 $name = strtolower($t['v']);
1290 $distinct = false;
1291 $star = false;
1292 $args = [];
1293 if ($this->acceptOp('*')) {
1294 $star = true;
1295 } else if (!$this->isOp(')')) {
1296 $distinct = $this->acceptKw('DISTINCT') !== null;
1297 do $args[] = $this->expr(); while ($this->acceptOp(','));
1298 if ($this->acceptKw('ORDER')) throw new DbException('集約関数のORDER BYは未対応です', 1, 'HY000');
1299 }
1300 $this->expectOp(')');
1301 $filter = null;
1302 if ($this->acceptKw('FILTER')) {
1303 $this->expectOp('(');
1304 $this->expectKw('WHERE');
1305 $filter = $this->expr();
1306 $this->expectOp(')');
1307 }
1308 if ($this->isKw('OVER')) throw new DbException('ウィンドウ関数は未対応です', 1, 'HY000');
1309 return ['t' => 'fn', 'name' => $name, 'args' => $args, 'distinct' => $distinct, 'star' => $star, 'filter' => $filter];
1310 }
1311 // 列(table.column)
1312 if ($this->isOp('.')) {
1313 $this->p++;
1314 $c = $this->cur();
1315 if ($c['t'] !== 'id') $this->error('列');
1316 $this->p++;
1317 if ($this->isOp('.')) {
1318 // schema.table.column
1319 $this->p++;
1320 $c2 = $this->cur();
1321 if ($c2['t'] !== 'id') $this->error('列');
1322 $this->p++;
1323 return ['t' => 'col', 'tbl' => $c['v'], 'name' => $c2['v']];
1324 }
1325 return ['t' => 'col', 'tbl' => $t['v'], 'name' => $c['v']];
1326 }
1327 if (!$t['q'] && in_array($t['u'], self::RESERVED, true)) {
1328 $this->p--;
1329 $this->error();
1330 }
1331 // 「"文字列"」は列が無ければ文字列(SQLiteの互換)
1332 return ['t' => 'col', 'tbl' => null, 'name' => $t['v'], 'dq' => $t['q'] && ($this->sql[$t['s']] ?? '') === '"'];
1333 }
1334 $this->error();
1335 }
1336
1337 private function caseExpr(): array {
1338 $this->expectKw('CASE');
1339 $base = $this->isKw('WHEN') ? null : $this->expr();
1340 $when = [];
1341 while ($this->acceptKw('WHEN')) {
1342 $c = $this->expr();
1343 $this->expectKw('THEN');
1344 $when[] = [$c, $this->expr()];
1345 }
1346 if (empty($when)) $this->error('WHEN');
1347 $else = $this->acceptKw('ELSE') ? $this->expr() : null;
1348 $this->expectKw('END');
1349 return ['t' => 'case', 'base' => $base, 'when' => $when, 'else' => $else];
1350 }
1351}