phplibrary/src/Database/Manager.php

357 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace ctiso\Database;
use ctiso\Database;
use ctiso\Tools\SQLStatementExtractor;
use ctiso\Path;
use Exception;
/**
* @phpstan-type Action array{
* type:string,
* table_name:string,
* table:string,
* fields:array,
* field: ColumnProps,
* constraints:?array,
* references:?array,
* source:string,
* pgsql?:string,
* old_name?:string,
* new_name?:string,
* column?:string,
* column_name?:string,
* refTable?:string,
* refColumn?:string,
* values:array,
* view:string,
* select:string
* }
*
* @phpstan-type ColumnProps array{
* name:string,
* type:string,
* not_null:bool,
* default:?string,
* references:?array{refTable:string,refColumn:string}
* }
*/
class Manager
{
/** @var Database */
public $db;
public function __construct(Database $db)
{
$this->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<string>
*/
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;
}
}