| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, 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 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Lib\Db\Sqlite; |
| 39 | |
| 40 | use Std\Lib\Db\DbException; |
| 41 | |
| 42 | /** |
| 43 | * SQLiteのSQLの構文解析(字句解析も) |
| 44 | * |
| 45 | * 結果は配列の構文木。式は['t' => 種類, …]。 |
| 46 | */ |
| 47 | final 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 | } |