warnings)); } private function out(string $s): void { $n = str_ends_with(strtolower($this->path), '.gz') ? gzwrite($this->fh, $s) : fwrite($this->fh, $s); if ($n === false) throw new DbException("ダンプのファイルに書けません:{$this->path}", 0, 'HY000'); } public function begin(bool $drop): void { $this->drop = $drop; $gz = str_ends_with(strtolower($this->path), '.gz'); if ($gz && !function_exists('gzopen')) throw new DbException("圧縮したダンプにはzlibが必要です:{$this->path}", 0, 'HY000'); $fh = $gz ? @gzopen($this->path, 'wb6') : @fopen($this->path, 'wb'); if ($fh === false) throw new DbException("ダンプのファイルを作れません:{$this->path}", 0, 'HY000'); $this->fh = $fh; $this->out("-- LittleBeastのデータベースの変換({$this->dialect})\n-- ".gmdate('Y-m-d H:i:s')." UTC\n"); if ($this->dialect === 'mysql') { $this->out("SET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\nSET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';\nSET time_zone = '+00:00';\n"); } else if ($this->dialect === 'pgsql') { $this->out("SET client_encoding = 'UTF8';\nSET standard_conforming_strings = on;\nSET TimeZone = 'UTC';\n"); } $this->out("BEGIN;\n\n"); } public function createTable(array $t): void { $q = fn(string $n) => Convert::quote($this->dialect, $n); if ($this->drop) $this->out('DROP TABLE IF EXISTS '.$q($t['name']).($this->dialect === 'pgsql' ? ' CASCADE' : '').";\n"); $this->out(Convert::createTable($this->dialect, $t, $this->warnings).";\n\n"); } public function insertRows(array $t, array $rows): void { $q = fn(string $n) => Convert::quote($this->dialect, $n); $head = 'INSERT INTO '.$q($t['name']).' ('.implode(', ', array_map(fn($c) => $q($c['name']), $t['columns'])).') VALUES'; foreach (array_chunk($rows, $this->perInsert) as $chunk) { $vals = []; foreach ($chunk as $r) { $parts = []; foreach ($t['columns'] as $i => $c) $parts[] = $this->value($r[$i], $c, $t); $vals[] = '('.implode(', ', $parts).')'; } $this->out($head."\n".implode(",\n", $vals).";\n"); } } private function value(mixed $v, array $c, array $t): string { if ($v === null) return 'NULL'; if (is_string($v) && $this->dialect === 'pgsql' && str_contains($v, "\0")) { $this->warnings[] = "PostgreSQLの文字列に0のバイトは入れられないので、消しました:{$t['name']}.{$c['name']}"; $v = str_replace("\0", '', $v); } if (is_float($v) && $this->dialect !== 'pgsql' && (is_nan($v) || is_infinite($v))) { $this->warnings[] = "NaN・無限はNULLにしました:{$t['name']}.{$c['name']}"; } return Convert::literal($this->dialect, $v, $c); } public function finishTable(array $t): void { $q = fn(string $n) => Convert::quote($this->dialect, $n); foreach ($t['indexes'] as $idx) $this->out(Convert::createIndex($this->dialect, $t, $idx).";\n"); // 自動の番号の続き foreach ($t['columns'] as $c) { if (!$c['autoinc'] || count($t['pk']) !== 1 || strcasecmp($t['pk'][0], $c['name']) !== 0) continue; if ($this->dialect === 'pgsql') { $this->out("SELECT setval(pg_get_serial_sequence('".str_replace("'", "''", $q($t['name']))."', '".str_replace("'", "''", $c['name'])."'), " ."COALESCE(MAX({$q($c['name'])}), 1), MAX({$q($c['name'])}) IS NOT NULL) FROM {$q($t['name'])};\n"); } } $this->out("\n"); } public function abort(): void { if ($this->fh) { str_ends_with(strtolower($this->path), '.gz') ? @gzclose($this->fh) : @fclose($this->fh); $this->fh = null; @unlink($this->path); } } public function end(): void { $this->out("COMMIT;\n"); if ($this->dialect === 'mysql') $this->out("SET FOREIGN_KEY_CHECKS = 1;\n"); str_ends_with(strtolower($this->path), '.gz') ? gzclose($this->fh) : fclose($this->fh); $this->fh = null; } }