master
ブランチ master
LittleBeast / src / Std / Lib / Db / Convert / Sql.php
25 KB、631行 生 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\Convert;
39
40use Std\Lib\Db\Convert;
41use Std\Lib\Db\DbException;
42use Std\Lib\Db\Sqlite\Blob;
43
44/**
45 * 変換用のSQLの解析(MySQL・PostgreSQL・SQLiteの方言、ダンプの文の分け方、CREATE TABLE、INSERTの値)
46 */
47final class Sql {
48 // 列の定義で型の後に来る言葉
49 private const COLUMN_WORDS = ['NOT', 'NULL', 'DEFAULT', 'PRIMARY', 'UNIQUE', 'AUTO_INCREMENT', 'AUTOINCREMENT', 'COLLATE', 'COMMENT',
50 'REFERENCES', 'CHECK', 'CONSTRAINT', 'GENERATED', 'ON', 'CHARSET', 'STORAGE', 'COLUMN_FORMAT', 'INVISIBLE', 'VISIBLE', 'AS',
51 'IDENTITY', 'KEY', 'SRID', 'ENCODE', 'COMPRESSION'];
52
53 ///////////////
54 // 字句解析
55
56 /**
57 * 1つの文をトークンにする
58 *
59 * @param string $sql
60 * @param string $dialect mysql、pgsql、sqlite
61 * @return array t(id、qid、str、num、blob、op、param)、v(値)、u(大文字、idのみ)
62 */
63 public static function tokens(string $sql, string $dialect): array {
64 $out = [];
65 $len = strlen($sql);
66 $i = 0;
67 while ($i < $len) {
68 $c = $sql[$i];
69 if (ctype_space($c)) {
70 $i++;
71 continue;
72 }
73 if ($c === '-' && ($sql[$i + 1] ?? '') === '-' || ($c === '#' && $dialect === 'mysql')) {
74 $j = strpos($sql, "\n", $i);
75 $i = $j === false ? $len : $j + 1;
76 continue;
77 }
78 if ($c === '/' && ($sql[$i + 1] ?? '') === '*') {
79 $j = strpos($sql, '*/', $i + 2);
80 $i = $j === false ? $len : $j + 2;
81 continue;
82 }
83 // E'…'、X'…'、B'…'、N'…'、_binary'…'
84 if (($c === "'" || (ctype_alpha($c) && ($sql[$i + 1] ?? '') === "'"))) {
85 $prefix = $c === "'" ? '' : strtoupper($c);
86 if ($prefix !== '') $i++;
87 $backslash = $dialect === 'mysql' || $prefix === 'E';
88 [$v, $i] = self::readQuoted($sql, $i, "'", $backslash);
89 if ($prefix === 'X') {
90 $out[] = ['t' => 'blob', 'v' => (string)hex2bin(strlen($v) % 2 ? '0'.$v : $v)];
91 } else if ($prefix === 'B') {
92 $out[] = ['t' => 'num', 'v' => bindec($v)];
93 } else {
94 $out[] = ['t' => 'str', 'v' => $v];
95 }
96 continue;
97 }
98 if ($c === '"') {
99 [$v, $i] = self::readQuoted($sql, $i, '"', $dialect === 'mysql');
100 // MySQLの「"」は文字列(ANSI_QUOTESでない場合)
101 $out[] = $dialect === 'mysql' ? ['t' => 'str', 'v' => $v] : ['t' => 'qid', 'v' => $v];
102 continue;
103 }
104 if ($c === '`') {
105 [$v, $i] = self::readQuoted($sql, $i, '`', false);
106 $out[] = ['t' => 'qid', 'v' => $v];
107 continue;
108 }
109 if ($c === '[' && $dialect === 'sqlite') {
110 $j = strpos($sql, ']', $i);
111 $out[] = ['t' => 'qid', 'v' => substr($sql, $i + 1, ($j === false ? $len : $j) - $i - 1)];
112 $i = $j === false ? $len : $j + 1;
113 continue;
114 }
115 if ($c === '$' && $dialect === 'pgsql' && preg_match('/\G\$([A-Za-z_][A-Za-z0-9_]*)?\$/', $sql, $m, 0, $i)) {
116 $end = strpos($sql, $m[0], $i + strlen($m[0]));
117 $body = $end === false ? substr($sql, $i + strlen($m[0])) : substr($sql, $i + strlen($m[0]), $end - $i - strlen($m[0]));
118 $out[] = ['t' => 'str', 'v' => $body];
119 $i = $end === false ? $len : $end + strlen($m[0]);
120 continue;
121 }
122 if ($c === '0' && ($sql[$i + 1] ?? '') !== '' && strtolower($sql[$i + 1]) === 'x' && preg_match('/\G0[xX]([0-9a-fA-F]*)/', $sql, $m, 0, $i)) {
123 $i += strlen($m[0]);
124 $hex = $m[1];
125 $out[] = ['t' => 'blob', 'v' => (string)hex2bin(strlen($hex) % 2 ? '0'.$hex : $hex)];
126 continue;
127 }
128 if (ctype_digit($c) || ($c === '.' && ctype_digit($sql[$i + 1] ?? ''))) {
129 preg_match('/\G(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/', $sql, $m, 0, $i);
130 $i += strlen($m[0]);
131 $t = $m[0];
132 $v = ctype_digit($t) ? ((string)(int)ltrim($t, '0') === ltrim($t, '0') || ltrim($t, '0') === '' ? (int)$t : $t) : (float)$t;
133 $out[] = ['t' => 'num', 'v' => $v, 'raw' => $t];
134 continue;
135 }
136 if (ctype_alpha($c) || $c === '_' || ord($c) >= 0x80) {
137 preg_match('/\G[A-Za-z0-9_$\x80-\xFF]+/', $sql, $m, 0, $i);
138 $i += strlen($m[0]);
139 // MySQLの文字セットの前置き(_binary 'x'、_utf8mb4'x')
140 if ($m[0][0] === '_' && $dialect === 'mysql' && preg_match('/\G\s*\'/', $sql, $mm, 0, $i)) {
141 $i += strlen($mm[0]) - 1;
142 [$v, $i] = self::readQuoted($sql, $i, "'", true);
143 $out[] = strtolower($m[0]) === '_binary' ? ['t' => 'blob', 'v' => $v] : ['t' => 'str', 'v' => $v];
144 continue;
145 }
146 $out[] = ['t' => 'id', 'v' => $m[0], 'u' => strtoupper($m[0])];
147 continue;
148 }
149 foreach (['::', '<>', '!=', '<=', '>=', '||', '->>', '->'] as $op) {
150 if (substr($sql, $i, strlen($op)) === $op) {
151 $out[] = ['t' => 'op', 'v' => $op];
152 $i += strlen($op);
153 continue 2;
154 }
155 }
156 $out[] = ['t' => 'op', 'v' => $c];
157 $i++;
158 }
159 return $out;
160 }
161
162 /** 引用符の中($iは開く引用符の位置) */
163 private static function readQuoted(string $s, int $i, string $q, bool $backslash): array {
164 $len = strlen($s);
165 $v = '';
166 $i++;
167 while ($i < $len) {
168 $c = $s[$i];
169 if ($backslash && $c === '\\') {
170 $n = $s[$i + 1] ?? '';
171 $v .= match ($n) { '0' => "\0", 'n' => "\n", 'r' => "\r", 't' => "\t", 'b' => "\x08", 'Z' => "\x1A", default => $n };
172 $i += 2;
173 continue;
174 }
175 if ($c === $q) {
176 if (($s[$i + 1] ?? '') === $q) {
177 $v .= $q;
178 $i += 2;
179 continue;
180 }
181 return [$v, $i + 1];
182 }
183 $j = strcspn($s, $q.($backslash ? '\\' : ''), $i);
184 $v .= substr($s, $i, $j);
185 $i += $j;
186 }
187 return [$v, $len];
188 }
189
190 ///////////////
191 // 解析
192
193 /** トークンの名前(スキーマ付きは最後の部分) */
194 public static function name(array $tok, int &$p): string {
195 $t = $tok[$p] ?? null;
196 if ($t === null || !in_array($t['t'], ['id', 'qid', 'str'], true)) throw new DbException('SQLの解析:名前が必要です', 1, 'HY000');
197 $p++;
198 $n = $t['v'];
199 while (($tok[$p]['t'] ?? '') === 'op' && $tok[$p]['v'] === '.' && in_array($tok[$p + 1]['t'] ?? '', ['id', 'qid'], true)) {
200 $n = $tok[$p + 1]['v'];
201 $p += 2;
202 }
203 return $n;
204 }
205
206 private static function is(array $tok, int $p, string ...$words): bool {
207 return ($tok[$p]['t'] ?? '') === 'id' && in_array($tok[$p]['u'], $words, true);
208 }
209
210 private static function isOp(array $tok, int $p, string $op): bool {
211 return ($tok[$p]['t'] ?? '') === 'op' && $tok[$p]['v'] === $op;
212 }
213
214 /** 対応する閉じ括弧まで($pは開き括弧) */
215 private static function skipParens(array $tok, int $p): int {
216 $depth = 0;
217 for ($n = count($tok); $p < $n; $p++) {
218 if (self::isOp($tok, $p, '(')) $depth++;
219 else if (self::isOp($tok, $p, ')') && --$depth === 0) return $p + 1;
220 }
221 return $p;
222 }
223
224 /** トークンを元のSQLに近い形に戻す(DEFAULTの式・型用) */
225 public static function text(array $toks): string {
226 $out = '';
227 foreach ($toks as $t) {
228 $s = match ($t['t']) {
229 'str' => "'".str_replace("'", "''", $t['v'])."'",
230 'qid' => '"'.$t['v'].'"',
231 'blob' => "X'".bin2hex($t['v'])."'",
232 'num' => (string)($t['raw'] ?? $t['v']),
233 default => $t['v'],
234 };
235 if ($out !== '' && !in_array($s, ['(', ')', ',', '::', '[', ']'], true) && !str_ends_with($out, '(') && !str_ends_with($out, '::')) $out .= ' ';
236 $out .= $s;
237 }
238 return $out;
239 }
240
241 /** (列, 列(長さ) ASC, …) の列の名前 */
242 public static function columnList(array $tok, int &$p): array {
243 if (!self::isOp($tok, $p, '(')) throw new DbException('SQLの解析:(が必要です', 1, 'HY000');
244 $end = self::skipParens($tok, $p);
245 // 要素毎のトークン
246 $items = [[]];
247 $depth = 0;
248 for ($p++; $p < $end - 1; $p++) {
249 if (self::isOp($tok, $p, '(')) $depth++;
250 else if (self::isOp($tok, $p, ')')) $depth--;
251 else if ($depth === 0 && self::isOp($tok, $p, ',')) {
252 $items[] = [];
253 continue;
254 }
255 $items[count($items) - 1][] = $tok[$p];
256 }
257 $cols = [];
258 foreach ($items as $it) {
259 // 列の名前の後は、MySQLの(長さ)、ASC・DESC・COLLATE・演算子クラス等だけなら列、他は式(null)
260 $ok = !empty($it) && in_array($it[0]['t'], ['id', 'qid'], true);
261 if ($ok && isset($it[1])) {
262 if ($it[1]['t'] === 'op' && $it[1]['v'] === '(') $ok = ($it[2]['t'] ?? '') === 'num' && ($it[3]['v'] ?? '') === ')';
263 else $ok = in_array($it[1]['t'], ['id', 'qid'], true);
264 }
265 $cols[] = $ok ? $it[0]['v'] : null;
266 }
267 $p = $end;
268 return $cols;
269 }
270
271 /**
272 * CREATE TABLEを解析する
273 *
274 * @return array|null テーブル(Convertの形)、CREATE TABLEでなければnull
275 */
276 public static function createTable(string $sql, string $dialect, array &$warnings = []): ?array {
277 $tok = self::tokens($sql, $dialect);
278 $p = 0;
279 if (!self::is($tok, $p++, 'CREATE')) return null;
280 while (self::is($tok, $p, 'TEMP', 'TEMPORARY', 'UNLOGGED', 'GLOBAL', 'LOCAL', 'OR', 'REPLACE')) $p++;
281 if (!self::is($tok, $p++, 'TABLE')) return null;
282 if (self::is($tok, $p, 'IF')) $p += 3;
283 $name = self::name($tok, $p);
284 if (!self::isOp($tok, $p, '(')) return null;
285 $end = self::skipParens($tok, $p);
286 $t = ['name' => $name, 'columns' => [], 'pk' => [], 'indexes' => [], 'collate' => null];
287 // 項目に分ける
288 $items = [];
289 $cur = [];
290 $depth = 0;
291 for ($k = $p + 1; $k < $end - 1; $k++) {
292 if (self::isOp($tok, $k, '(')) $depth++;
293 if (self::isOp($tok, $k, ')')) $depth--;
294 if ($depth === 0 && self::isOp($tok, $k, ',')) {
295 $items[] = $cur;
296 $cur = [];
297 continue;
298 }
299 $cur[] = $tok[$k];
300 }
301 if ($cur) $items[] = $cur;
302 foreach ($items as $it) self::tableItem($t, $it, $dialect, $warnings);
303 // テーブルのオプション(MySQL)
304 for ($k = $end; $k < count($tok); $k++) {
305 if (self::is($tok, $k, 'COLLATE') && isset($tok[$k + 1])) {
306 $j = self::isOp($tok, $k + 1, '=') ? $k + 2 : $k + 1;
307 $t['collate'] = $tok[$j]['v'] ?? null;
308 }
309 if (self::is($tok, $k, 'WITHOUT')) $warnings[] = "WITHOUT ROWIDは普通のテーブルにします:{$name}";
310 }
311 if ($dialect !== 'mysql') unset($t['collate']);
312 return $t;
313 }
314
315 /** テーブルの定義の1つの項目(列又は制約) */
316 private static function tableItem(array &$t, array $it, string $dialect, array &$warnings): void {
317 $p = 0;
318 if (self::is($it, 0, 'CONSTRAINT')) $p = 2;
319 $cname = self::is($it, 0, 'CONSTRAINT') ? (string)($it[1]['v'] ?? '') : null;
320 if (self::is($it, $p, 'PRIMARY') && self::is($it, $p + 1, 'KEY')) {
321 $p += 2;
322 while (!self::isOp($it, $p, '(') && $p < count($it)) $p++;
323 $t['pk'] = array_values(array_filter(self::columnList($it, $p)));
324 return;
325 }
326 if (self::is($it, $p, 'UNIQUE', 'KEY', 'INDEX') && !self::isOp($it, $p + 1, '.')) {
327 $unique = self::is($it, $p, 'UNIQUE');
328 $p++;
329 if (self::is($it, $p, 'KEY', 'INDEX')) $p++;
330 $iname = $cname;
331 if (!self::isOp($it, $p, '(') && $p < count($it)) $iname = (string)$it[$p++]['v'];
332 while (!self::isOp($it, $p, '(') && $p < count($it)) $p++;
333 $cols = self::columnList($it, $p);
334 if (in_array(null, $cols, true)) {
335 $warnings[] = "式のインデックスは変換しません:{$t['name']}";
336 return;
337 }
338 $t['indexes'][] = ['name' => $iname ?? '', 'columns' => $cols, 'unique' => $unique];
339 return;
340 }
341 if (self::is($it, $p, 'FULLTEXT', 'SPATIAL')) {
342 $warnings[] = "全文・空間のインデックスは変換しません:{$t['name']}";
343 return;
344 }
345 if (self::is($it, $p, 'FOREIGN', 'CHECK', 'EXCLUDE') || ($cname !== null)) {
346 if (self::is($it, $p, 'FOREIGN')) $warnings[] = "外部キーは変換しません:{$t['name']}";
347 return;
348 }
349 if (self::is($it, $p, 'PERIOD', 'LIKE')) return;
350 $col = self::columnDef($it, $dialect, $t['name'], $warnings);
351 if ($col === null) return;
352 if (!empty($col['pk'])) $t['pk'] = [$col['name']];
353 if (!empty($col['unique'])) $t['indexes'][] = ['name' => '', 'columns' => [$col['name']], 'unique' => true];
354 unset($col['pk'], $col['unique']);
355 $t['columns'][] = $col;
356 }
357
358 /**
359 * 列の定義
360 *
361 * @return array|null Convertの列(pk・uniqueも)
362 */
363 public static function columnDef(array $it, string $dialect, string $table, array &$warnings): ?array {
364 $p = 0;
365 if (!in_array($it[0]['t'] ?? '', ['id', 'qid', 'str'], true)) return null;
366 $name = $it[$p++]['v'];
367 $typeToks = [];
368 $n = count($it);
369 while ($p < $n) {
370 $tk = $it[$p];
371 if ($tk['t'] === 'id' && in_array($tk['u'], self::COLUMN_WORDS, true)) break;
372 if ($tk['t'] === 'id' && $tk['u'] === 'CHARACTER' && self::is($it, $p + 1, 'SET')) break;
373 if (self::isOp($it, $p, '(')) {
374 $e = self::skipParens($it, $p);
375 $typeToks = array_merge($typeToks, array_slice($it, $p, $e - $p));
376 $p = $e;
377 continue;
378 }
379 $typeToks[] = $tk;
380 $p++;
381 }
382 $typeText = self::text($typeToks);
383 $col = Convert::parseType($typeText, $dialect) + ['name' => $name, 'notNull' => false, 'default' => null, 'autoinc' => false, 'collate' => null];
384 if (in_array(strtolower(preg_replace('/\(.*$/', '', $typeText)), ['serial', 'bigserial', 'smallserial', 'serial4', 'serial8', 'serial2'], true)) {
385 $col['autoinc'] = true;
386 $col['notNull'] = true;
387 }
388 while ($p < $n) {
389 if (self::is($it, $p, 'NOT') && self::is($it, $p + 1, 'NULL')) {
390 $col['notNull'] = true;
391 $p += 2;
392 } else if (self::is($it, $p, 'NULL')) {
393 $p++;
394 } else if (self::is($it, $p, 'PRIMARY')) {
395 $col['pk'] = true;
396 $p += 2;
397 if (self::is($it, $p, 'ASC', 'DESC')) $p++;
398 } else if (self::is($it, $p, 'UNIQUE')) {
399 $col['unique'] = true;
400 $p++;
401 if (self::is($it, $p, 'KEY')) $p++;
402 } else if (self::is($it, $p, 'AUTO_INCREMENT', 'AUTOINCREMENT')) {
403 $col['autoinc'] = true;
404 $p++;
405 } else if (self::is($it, $p, 'DEFAULT')) {
406 $p++;
407 $s = $p;
408 if (self::isOp($it, $p, '(')) {
409 $p = self::skipParens($it, $p);
410 } else {
411 while ($p < $n && !($it[$p]['t'] === 'id' && in_array($it[$p]['u'], ['NOT', 'NULL', 'PRIMARY', 'UNIQUE', 'AUTO_INCREMENT', 'COLLATE', 'COMMENT', 'REFERENCES', 'CHECK', 'CONSTRAINT', 'ON', 'GENERATED', 'CHARACTER'], true) && $p > $s)) {
412 if (self::isOp($it, $p, '(')) {
413 $p = self::skipParens($it, $p);
414 continue;
415 }
416 $p++;
417 }
418 }
419 $expr = self::text(array_slice($it, $s, $p - $s));
420 $d = Convert::parseDefault($expr);
421 if ($d !== null && isset($d['seq'])) {
422 $col['autoinc'] = true;
423 } else if ($d === null) {
424 $warnings[] = "DEFAULTを変換できません:{$table}.{$name}({$expr})";
425 } else {
426 $col['default'] = $d;
427 }
428 } else if (self::is($it, $p, 'GENERATED')) {
429 // GENERATED … AS IDENTITY(自動の番号)、GENERATED ALWAYS AS (式)(生成列)
430 $k = $p;
431 while ($k < $n && !self::is($it, $k, 'IDENTITY') && !self::isOp($it, $k, '(')) $k++;
432 if (self::is($it, $k, 'IDENTITY')) {
433 $col['autoinc'] = true;
434 $col['notNull'] = true;
435 $p = $k + 1;
436 if (self::isOp($it, $p, '(')) $p = self::skipParens($it, $p);
437 } else {
438 $warnings[] = "生成列は普通の列にします:{$table}.{$name}";
439 $p = $k < $n ? self::skipParens($it, $k) : $n;
440 }
441 } else if (self::is($it, $p, 'AS') && self::isOp($it, $p + 1, '(')) {
442 $warnings[] = "生成列は普通の列にします:{$table}.{$name}";
443 $p = self::skipParens($it, $p + 1);
444 } else if (self::is($it, $p, 'COLLATE')) {
445 $col['collate'] = $dialect === 'mysql' ? (string)($it[$p + 1]['v'] ?? '') : null;
446 $p += 2;
447 } else if (self::is($it, $p, 'CHARACTER') && self::is($it, $p + 1, 'SET')) {
448 $p += 3;
449 } else if (self::is($it, $p, 'CHARSET', 'COMMENT', 'STORAGE', 'COLUMN_FORMAT', 'SRID', 'ENCODE', 'COMPRESSION')) {
450 $p += 2;
451 } else if (self::is($it, $p, 'ON') && self::is($it, $p + 1, 'UPDATE')) {
452 $p += 3;
453 if (self::isOp($it, $p, '(')) $p = self::skipParens($it, $p);
454 } else if (self::is($it, $p, 'REFERENCES')) {
455 $p += 2;
456 if (self::isOp($it, $p, '(')) $p = self::skipParens($it, $p);
457 while ($p < $n && self::is($it, $p, 'ON', 'MATCH', 'DEFERRABLE', 'INITIALLY', 'NOT', 'CASCADE', 'RESTRICT', 'SET', 'NO', 'ACTION', 'DELETE', 'UPDATE', 'NULL', 'FULL', 'SIMPLE', 'PARTIAL', 'DEFERRED', 'IMMEDIATE')) $p++;
458 } else if (self::is($it, $p, 'CHECK')) {
459 $p = self::skipParens($it, $p + 1);
460 } else if (self::is($it, $p, 'CONSTRAINT')) {
461 $p += 2;
462 } else {
463 $p++;
464 }
465 }
466 return $col;
467 }
468
469 ///////////////
470 // 値
471
472 /**
473 * INSERT INTO t [(列)] VALUES (…), (…)
474 *
475 * @return array|null table、columns(null=全部)、rows(PHPの値の配列の配列)
476 */
477 public static function insert(string $sql, string $dialect): ?array {
478 $tok = self::tokens($sql, $dialect);
479 $p = 0;
480 if (self::is($tok, 0, 'REPLACE')) $p = 1;
481 else if (self::is($tok, 0, 'INSERT')) {
482 $p = 1;
483 while (self::is($tok, $p, 'IGNORE', 'OR', 'REPLACE', 'LOW_PRIORITY', 'DELAYED', 'HIGH_PRIORITY', 'ABORT', 'FAIL', 'ROLLBACK')) $p++;
484 } else {
485 return null;
486 }
487 if (!self::is($tok, $p++, 'INTO')) return null;
488 $table = self::name($tok, $p);
489 $cols = null;
490 if (self::isOp($tok, $p, '(')) $cols = self::columnList($tok, $p);
491 if (!self::is($tok, $p, 'VALUES', 'VALUE')) return ['table' => $table, 'columns' => $cols, 'rows' => [], 'select' => true];
492 $p++;
493 $rows = [];
494 $n = count($tok);
495 while ($p < $n && self::isOp($tok, $p, '(')) {
496 $end = self::skipParens($tok, $p);
497 $row = [];
498 $val = [];
499 $depth = 0;
500 for ($k = $p + 1; $k < $end - 1; $k++) {
501 if (self::isOp($tok, $k, '(')) $depth++;
502 if (self::isOp($tok, $k, ')')) $depth--;
503 if ($depth === 0 && self::isOp($tok, $k, ',')) {
504 $row[] = self::value($val);
505 $val = [];
506 continue;
507 }
508 $val[] = $tok[$k];
509 }
510 if ($val || $end - $p > 2) $row[] = self::value($val);
511 $rows[] = $row;
512 $p = $end;
513 if (self::isOp($tok, $p, ',')) $p++;
514 }
515 return ['table' => $table, 'columns' => $cols, 'rows' => $rows];
516 }
517
518 /** 値のトークン(リテラル、負の数、キャスト、一部の関数) */
519 public static function value(array $v): mixed {
520 // キャスト('x'::bytea、CAST(… AS …))を除く
521 $cast = null;
522 $k = count($v);
523 while ($k >= 3 && $v[$k - 2]['t'] === 'op' && $v[$k - 2]['v'] === '::') {
524 $cast = strtolower((string)$v[$k - 1]['v']);
525 $v = array_slice($v, 0, $k - 2);
526 $k = count($v);
527 }
528 if ($k >= 3 && $v[$k - 1]['t'] === 'op' && $v[$k - 1]['v'] === ']' && $v[$k - 2]['t'] === 'op' && $v[$k - 2]['v'] === '[') {
529 $v = array_slice($v, 0, $k - 2);
530 $k -= 2;
531 }
532 if ($k === 0) return null;
533 if ($k === 1) {
534 $t = $v[0];
535 switch ($t['t']) {
536 case 'num':
537 return $t['v'];
538 case 'blob':
539 return new Blob($t['v']);
540 case 'str':
541 if ($cast === 'bytea' && str_starts_with($t['v'], '\\x')) return new Blob((string)hex2bin(substr($t['v'], 2)));
542 return $t['v'];
543 case 'id':
544 return match ($t['u']) { 'NULL' => null, 'TRUE' => true, 'FALSE' => false, 'DEFAULT' => null, default => $t['v'] };
545 }
546 }
547 if ($k === 2 && $v[0]['t'] === 'op' && ($v[0]['v'] === '-' || $v[0]['v'] === '+') && $v[1]['t'] === 'num') {
548 if ($v[0]['v'] === '+') return $v[1]['v'];
549 $n = $v[1]['v'];
550 if (is_string($n)) return $n === '9223372036854775808' ? PHP_INT_MIN : '-'.$n;
551 return -$n;
552 }
553 // CAST(x AS type)
554 if ($v[0]['t'] === 'id' && $v[0]['u'] === 'CAST' && ($v[1]['v'] ?? '') === '(') {
555 $inner = [];
556 for ($i = 2; $i < $k && !($v[$i]['t'] === 'id' && $v[$i]['u'] === 'AS'); $i++) $inner[] = $v[$i];
557 return self::value($inner);
558 }
559 // sqlite3の.dumpの文字列(replace('…','\n',char(10))、unistr('…')、||)
560 $parts = self::split($v, '||');
561 if (count($parts) > 1) {
562 $out = '';
563 foreach ($parts as $part) {
564 $x = self::value($part);
565 if (!is_string($x) && !is_int($x)) return self::text($v);
566 $out .= $x;
567 }
568 return $out;
569 }
570 if ($v[0]['t'] === 'id' && ($v[1]['v'] ?? '') === '(' && $v[$k - 1]['v'] === ')' && in_array($v[0]['u'], ['REPLACE', 'CHAR', 'UNISTR'], true)) {
571 $args = array_map([self::class, 'value'], self::split(array_slice($v, 2, $k - 3), ','));
572 foreach ($args as $a) if (!is_string($a) && !is_int($a)) return self::text($v);
573 if ($v[0]['u'] === 'REPLACE' && count($args) === 3) return str_replace((string)$args[1], (string)$args[2], (string)$args[0]);
574 if ($v[0]['u'] === 'CHAR') return implode('', array_map(fn($c) => (string)mb_chr((int)$c, 'UTF-8'), $args));
575 if ($v[0]['u'] === 'UNISTR' && count($args) === 1) {
576 return (string)preg_replace_callback('/\\\\\\\\|\\\\(?:u?([0-9A-Fa-f]{4})|\+([0-9A-Fa-f]{6})|U([0-9A-Fa-f]{8}))/',
577 fn($m) => $m[0] === '\\\\' ? '\\' : (string)mb_chr((int)hexdec($m[1] ?: ($m[2] ?? '') ?: ($m[3] ?? '')), 'UTF-8'), (string)$args[0]);
578 }
579 }
580 // 他の関数(now()等)は文字列のまま
581 return self::text($v);
582 }
583
584 /** トークンを括弧の外の演算子で分ける */
585 private static function split(array $v, string $op): array {
586 $out = [[]];
587 $depth = 0;
588 foreach ($v as $t) {
589 if ($t['t'] === 'op' && $t['v'] === '(') $depth++;
590 else if ($t['t'] === 'op' && $t['v'] === ')') $depth--;
591 else if ($depth === 0 && $t['t'] === 'op' && $t['v'] === $op) {
592 $out[] = [];
593 continue;
594 }
595 $out[count($out) - 1][] = $t;
596 }
597 return $out;
598 }
599
600 /**
601 * COPYのデータの1行(タブ区切り、\N=NULL、\のエスケープ)
602 *
603 * @return array 文字列又はnull
604 */
605 public static function copyLine(string $line): array {
606 $out = [];
607 foreach (explode("\t", $line) as $f) {
608 if ($f === '\\N') {
609 $out[] = null;
610 continue;
611 }
612 if (!str_contains($f, '\\')) {
613 $out[] = $f;
614 continue;
615 }
616 $out[] = preg_replace_callback('/\\\\(x[0-9a-fA-F]{1,2}|[0-7]{1,3}|.)/', function(array $m): string {
617 $c = $m[1];
618 if ($c[0] === 'x' && strlen($c) > 1) return chr(hexdec(substr($c, 1)));
619 if (ctype_digit($c)) return chr(octdec($c) & 0xFF);
620 return match ($c) { 'b' => "\x08", 'f' => "\f", 'n' => "\n", 'r' => "\r", 't' => "\t", 'v' => "\v", default => $c };
621 }, $f);
622 }
623 return $out;
624 }
625
626 /** COPYのデータの1行を作る */
627 public static function copyField(mixed $v): string {
628 if ($v === null) return '\\N';
629 return strtr((string)$v, ["\\" => "\\\\", "\t" => "\\t", "\n" => "\\n", "\r" => "\\r"]);
630 }
631}