テーブル */ public array $tables = []; /** 小文字の名前 => インデックス */ public array $indexes = []; /** 小文字の名前 => ビュー(name、sel、cols) */ public array $views = []; /** トリガーの有るテーブル(小文字) */ public array $triggers = []; public int $maxRowid = 0; /** * sqlite_schemaを読む */ public static function load(Btree $bt, int $pages): self { $s = new self(); $s->tables['sqlite_schema'] = self::schemaTable(); if ($pages === 0) return $s; $parser = new Parser(); $autos = []; foreach ($bt->scan(1) as $rowid => $rec) { $s->maxRowid = max($s->maxRowid, $rowid); [$type, $name, $tbl, $root, $sql] = Record::decode($rec) + [null, null, null, null, null]; $type = (string)$type; $lname = strtolower((string)$name); try { if ($type === 'table') { $st = $parser->parseAll((string)$sql)[0] ?? null; if (($st['t'] ?? '') !== 'createTable' || !isset($st['def'])) throw new DbException("不正なテーブルのSQL:{$name}", 11, 'HY000'); $s->tables[$lname] = self::table((string)$name, (int)$root, (string)$sql, $st['def']) + ['schemaRowid' => $rowid]; } else if ($type === 'index') { if ($sql === null) { $autos[] = [(string)$name, strtolower((string)$tbl), (int)$root, $rowid]; continue; } $st = $parser->parseAll((string)$sql)[0] ?? null; if (($st['t'] ?? '') !== 'createIndex') throw new DbException("不正なインデックスのSQL:{$name}", 11, 'HY000'); $s->indexes[$lname] = ['name' => (string)$name, 'root' => (int)$root, 'table' => strtolower((string)$tbl), 'unique' => $st['unique'], 'def' => $st['cols'], 'origin' => 'c', 'sql' => (string)$sql, 'schemaRowid' => $rowid]; } else if ($type === 'view') { $st = $parser->parseAll((string)$sql)[0] ?? null; if (($st['t'] ?? '') !== 'createView') throw new DbException("不正なビューのSQL:{$name}", 11, 'HY000'); $s->views[$lname] = ['name' => (string)$name, 'sel' => $st['sel'], 'cols' => $st['cols'], 'sql' => (string)$sql, 'schemaRowid' => $rowid]; } else if ($type === 'trigger') { $s->triggers[strtolower((string)$tbl)] = true; $s->triggers['@'.$lname] = $rowid; } } catch (DbException $e) { if ($type === 'table' && str_contains($e->getMessage(), '未対応')) { // WITHOUT ROWID等:読めないテーブルとして $s->tables[$lname] = ['name' => (string)$name, 'unsupported' => $e->getMessage(), 'root' => (int)$root, 'cols' => [], 'map' => [], 'indexes' => [], 'schemaRowid' => $rowid, 'sql' => (string)$sql]; continue; } throw $e; } } // 自動のインデックス(UNIQUE・PRIMARY KEYの制約) foreach ($autos as [$name, $tbl, $root, $rowid]) { $t = $s->tables[$tbl] ?? null; if (!$t || !preg_match('/_(\d+)$/', $name, $m)) continue; $c = $t['constraints'][(int)$m[1] - 1] ?? null; if ($c === null) throw new DbException("データベースが壊れています({$name})", 11, 'HY000'); $s->indexes[strtolower($name)] = ['name' => $name, 'root' => $root, 'table' => $tbl, 'unique' => true, 'def' => $c[1], 'origin' => $c[0] === 'pk' ? 'pk' : 'u', 'sql' => null, 'schemaRowid' => $rowid]; } foreach ($s->indexes as $k => $idx) { if (!isset($s->tables[$idx['table']])) continue; $s->indexes[$k] = self::indexInfo($idx, $s->tables[$idx['table']]); $s->tables[$idx['table']]['indexes'][] = $k; } return $s; } /** sqlite_schemaのテーブル */ private static function schemaTable(): array { $cols = []; foreach (['type' => 'text', 'name' => 'text', 'tbl_name' => 'text', 'rootpage' => 'int', 'sql' => 'text'] as $n => $ty) { $cols[] = ['name' => $n, 'type' => $ty, 'aff' => Value::affinity($ty), 'notNull' => false, 'default' => null, 'defaultText' => null, 'collate' => 'BINARY', 'pk' => 0]; } return ['name' => 'sqlite_schema', 'root' => 1, 'sql' => '', 'cols' => $cols, 'map' => ['type' => 0, 'name' => 1, 'tbl_name' => 2, 'rootpage' => 3, 'sql' => 4], 'ipk' => null, 'autoinc' => false, 'checks' => [], 'indexes' => [], 'constraints' => [], 'system' => true]; } /** * テーブルの定義を作る * * @param array $def Parser::tableBody()の結果 */ public static function table(string $name, int $root, string $sql, array $def): array { if ($def['withoutRowid']) throw new DbException("WITHOUT ROWIDのテーブルは未対応です:{$name}", 1, 'HY000'); $cols = []; $map = []; foreach ($def['cols'] as $i => $c) { $lc = strtolower($c['name']); if (isset($map[$lc])) throw new DbException("重複した列:{$c['name']}", 1, 'HY000'); $map[$lc] = $i; $cols[] = ['name' => $c['name'], 'type' => $c['type'], 'aff' => Value::affinity($c['type']), 'notNull' => $c['notNull'], 'default' => $c['default'], 'defaultText' => $c['defaultText'], 'collate' => $c['collate'] ?? 'BINARY', 'pk' => 0, 'checks' => $c['checks']]; } $ipk = null; $pkCols = []; if ($def['pk'] !== null) { foreach ($def['pk'] as $k => $p) { $ci = $map[strtolower($p['name'])] ?? null; if ($ci === null) throw new DbException("主キーの列がありません:{$p['name']}", 1, 'HY000'); $cols[$ci]['pk'] = $k + 1; $pkCols[] = $ci; } if (count($pkCols) === 1 && strtoupper($cols[$pkCols[0]]['type']) === 'INTEGER') { // 列の「INTEGER PRIMARY KEY DESC」だけはrowidの別名にならない $colDesc = false; foreach ($def['cols'] as $c) if ($c['pk'] && $c['pkDesc']) $colDesc = true; if (!$colDesc) $ipk = $pkCols[0]; } } if ($def['pkAuto'] && $ipk === null) throw new DbException('AUTOINCREMENTはINTEGER PRIMARY KEYだけに使えます', 1, 'HY000'); // 自動のインデックスの元になる制約(重複は1つ) $constraints = []; $seen = []; foreach ($def['order'] as [$kind, $list]) { if ($kind === 'pk' && $ipk !== null) continue; $key = implode(',', array_map(fn($p) => strtolower($p['name']), $list)); if (isset($seen[$key])) continue; $seen[$key] = true; $constraints[] = [$kind, $list]; } $checks = $def['checks']; foreach ($cols as $c) foreach ($c['checks'] as $ch) $checks[] = $ch; return ['name' => $name, 'root' => $root, 'sql' => $sql, 'cols' => $cols, 'map' => $map, 'ipk' => $ipk, 'autoinc' => $def['pkAuto'], 'checks' => $checks, 'indexes' => [], 'constraints' => $constraints]; } /** インデックスの列・照合順序・降順 */ public static function indexInfo(array $idx, array $t): array { $cols = $colls = $desc = []; foreach ($idx['def'] as $d) { $ci = $t['map'][strtolower($d['name'])] ?? null; if ($ci === null) { if (in_array(strtolower($d['name']), ['rowid', 'oid', '_rowid_'], true)) throw new DbException('rowidのインデックスは未対応です', 1, 'HY000'); throw new DbException("インデックスの列がありません:{$d['name']}", 1, 'HY000'); } $cols[] = $ci; $colls[] = $d['collate'] ?? $t['cols'][$ci]['collate']; $desc[] = $d['desc']; } $colls[] = 'BINARY'; $desc[] = false; return $idx + ['cols' => $cols, 'colls' => $colls, 'desc' => $desc]; } /** テーブル(無ければnull) */ public function table_(string $name): ?array { $l = strtolower($name); if ($l === 'sqlite_master' || $l === 'sqlite_temp_master' || $l === 'sqlite_temp_schema') $l = 'sqlite_schema'; return $this->tables[$l] ?? null; } /** 名前が使われているか */ public function exists(string $name): bool { $l = strtolower($name); return isset($this->tables[$l]) || isset($this->indexes[$l]) || isset($this->views[$l]) || isset($this->triggers['@'.$l]) || $l === 'sqlite_master'; } }