357 lines
11 KiB
PHP
357 lines
11 KiB
PHP
<?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;
|
||
}
|
||
}
|