db = $db; } /** * Выполняет действие * @param Action $action * @param string $db_file * @throws Exception */ public function executeAction(array $action, $db_file = ""): void { switch ($action["type"]) { case "dropTable": $this->dropTableQuery($action["table_name"], true); break; case "createTable": $constraints = $action["constraints"] ?? null; $this->createTableQuery($action["table_name"], $action["fields"], $constraints); break; case "addColumn": $this->addColumn($action["table_name"], $action["column_name"], $action["field"]); break; case "insert": $this->db->insertQuery($action["table_name"], $action["values"]); break; case "alterReference": $this->alterReference($action["table"], $action["column"], $action["refTable"], $action["refColumn"]); break; case "renameColumn": $this->renameColumn($action["table"], $action["old_name"], $action["new_name"]); break; case "createView": $this->recreateView($action["view"], $action["select"]); break; case "executeFile": if ($this->db->isPostgres() && isset($action["pgsql"])) { $file = $action["pgsql"]; } else { $file = $action["source"]; } $stmtList = SQLStatementExtractor::extractFile(Path::join(dirname($db_file), $file)); foreach ($stmtList as $stmt) { $this->db->executeQuery($stmt); } break; default: throw new Exception("unknown action " . $action["type"] . PHP_EOL); } } /** * Дропает и создаёт SQL VIEW * @param string $viewName * @param string $selectStatement */ public function recreateView($viewName, $selectStatement): void { $this->db->query("DROP VIEW " . $viewName); $this->db->query("CREATE VIEW " . $viewName . " AS " . $selectStatement); } /** * Дропает таблицу * @param string $table * @param bool $cascade */ public function dropTableQuery($table, $cascade = false): void { $statement = "DROP TABLE IF EXISTS " . $table; if ($this->db->isPostgres() && $cascade) { $statement .= " CASCADE"; } $this->db->query($statement); } /** * Добавляет ссылку на другую таблицу * @param string $table * @param string $column * @param string $refTable * @param string $refColumn */ public function alterReference($table, $column, $refTable, $refColumn): void { $this->db->query("ALTER TABLE " . $table . " ADD CONSTRAINT " . $table . "_" . $column . "fk" . " FOREIGN KEY (" . $column . ") REFERENCES " . $refTable . " (" . $refColumn . ") ON DELETE CASCADE ON UPDATE CASCADE"); } /** * Извлечение информации о полях таблицы * @param string $table * @return array{type:string,not_null:bool,constraint:?string}[]|null */ public function tableInfo($table) { $pg = $this->db->isPostgres(); if ($pg) { throw new Exception("Not implemented for postgres"); } else { $results = $this->db->fetchAllArray("PRAGMA table_info(" . $table . ");"); if (empty($results)) { return null; } $fields = []; foreach ($results as $result) { $fields[$result["name"]] = [ "type" => $result["type"], "not_null" => boolval($result["notnull"]), "constraint" => ((bool) $result["pk"]) ? "PRIMARY KEY" : null ]; } return $fields; } } /** * Переименование столбца в таблице * @param string $table * @param string $old_name * @param string $new_name */ public function renameColumn($table, $old_name, $new_name): void { $pg = $this->db->isPostgres(); if ($pg) { $this->db->query("ALTER TABLE " . $table . " RENAME COLUMN " . $old_name . " TO " . $new_name); } else { $tmp_table = "tmp_" . $table; $this->dropTableQuery($tmp_table); $table_info = $this->tableInfo($table); if (isset($table_info[$new_name])) { return; } $data = $this->dumpTable($table); $this->db->query("ALTER TABLE " . $table . " RENAME TO " . $tmp_table . ";"); $table_info[$new_name] = $table_info[$old_name]; unset($table_info[$old_name]); $this->createTableQuery($table, $table_info, null); foreach ($data as $row) { $values = $row['values']; $values[$new_name] = $values[$old_name]; unset($values[$old_name]); $this->db->insertQuery($table, $values); } $this->dropTableQuery($tmp_table); } } /** * Обновление ключа serial после ручной вставки * @param string $table * @param string $column */ public function updateSerial($table, $column): void { $this->db->query("SELECT setval(pg_get_serial_sequence('" . $table . "', '" . $column . "'), coalesce(max(" . $column . "),0) + 1, false) FROM " . $table); } /** * Возвращает определение столбца * @param string $name * @param ColumnProps $data * @param bool $pg * @return string */ public function columnDefinition($name, $data, $pg) { $constraint = isset($data['constraint']) ? " " . $data['constraint'] : ""; $references = ""; if (isset($data['references'])) { $references = " REFERENCES " . $data['references']['refTable'] . '(' . $data['references']['refColumn'] . ')'; } if (isset($data["not_null"]) && $data["not_null"]) { $constraint .= " NOT NULL"; } $type = $data['type']; if (!$pg) { if (strtolower($type) == "serial") { $type = "integer"; } } return $name . " " . $type . $references . $constraint; } /** * Добавляет столбец в таблицу * @param string $table_name * @param string $column_name * @param array $field */ public function addColumn($table_name, $column_name, $field): void { $pg = $this->db->isPostgres(); $q = "ALTER TABLE " . $table_name . " ADD COLUMN " . $this->columnDefinition($column_name, $field, $pg); $this->db->query($q); } /** * Возвращает определение ограничения * @param array{fields: string[], type: string} $c * @return string */ public function getConstraintDef(array $c) { if ($c['type'] == 'unique') { return "UNIQUE(" . implode(", ", $c['fields']) . ")"; } return ""; } /** * Создает таблицу * @example createTableQuery('users',['id'=>['type'=>'integer','constraint'=>'PRIMARY KEY']]) * @param string $table * @param array $fields * @param array|string|null $constraints */ public function createTableQuery($table, $fields, $constraints): void { $pg = $this->db->isPostgres(); if ($constraints) { if (is_array($constraints)) { $constraints = $this->getConstraintDef($constraints); } $constraints = ", " . $constraints; } $statement = "CREATE TABLE $table (" . implode( ",", array_map(function ($name, $data) use ($pg) { return $this->columnDefinition($name, $data, $pg); }, array_keys($fields), array_values($fields)) ) . " " . $constraints . ")"; $this->db->query($statement); } /** * Возвращает дамп таблицы * @param string $table_name * @return array */ public function dumpTable($table_name) { $pg = $this->db->isPostgres(); $result = []; $data = $this->db->fetchAllArray("SELECT * FROM " . $table_name . ";"); if (!$pg) { $table_fields = $this->tableInfo($table_name); foreach ($table_fields as $name => $value) { $type = strtolower($value['type']); if ($type == "boolean") { foreach ($data as &$row) { if (isset($row[$name])) { $row[$name] = boolval($row[$name]); } } } } } foreach ($data as $r) { $result[] = [ "type" => "insert", "table_name" => $table_name, "values" => $r ]; } return $result; } /** * Возвращает все имена таблиц * @return list */ public function getAllTableNames() { $result = []; if ($this->db->isPostgres()) { $query = "SELECT table_name as name FROM information_schema.tables WHERE table_schema='public'"; } else { $query = "SELECT * FROM sqlite_master WHERE type='table'"; } $tables = $this->db->fetchAllArray($query); foreach ($tables as $table) { $result[] = $table['name']; } return $result; } /** * Возвращает дамп всех таблиц * @return array */ public function dumpInserts() { $table_names = $this->getAllTableNames(); $result = []; foreach ($table_names as $table_name) { $result = array_merge($result, $this->dumpTable($table_name)); } return $result; } }