diff --git a/src/Connection/HttpConnection.php b/src/Connection/HttpRequest.php similarity index 100% rename from src/Connection/HttpConnection.php rename to src/Connection/HttpRequest.php diff --git a/src/Connection/HttpConnectionResponse.php b/src/Connection/HttpResponse.php similarity index 100% rename from src/Connection/HttpConnectionResponse.php rename to src/Connection/HttpResponse.php diff --git a/src/Database.php b/src/Database.php index 15ac15e..c4cd264 100644 --- a/src/Database.php +++ b/src/Database.php @@ -1,23 +1,24 @@ +require_once "database/pdostatement.php"; /** - * @package system.db * Класс оболочка для PDO для замены Creole */ class Database extends PDO { - public function __construct($dsn, $username = false, $password = false) + + public $dsn; + public function __construct($dsn, $username = null, $password = null) { parent::__construct($dsn, $username, $password); $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDODatabaseStatement', array())); + $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('Database_PDOStatement', array())); } public function getDSN() { return $this->dsn; } - public function isPostgres(){ return ($this->dsn["phptype"] == "pgsql"); } @@ -26,30 +27,43 @@ class Database extends PDO */ static function getConnection(array $dsn) { + if ($dsn['phptype'] == 'pgsql' || $dsn['phptype'] == 'mysql') { $port = (isset($dsn['port'])) ? "port={$dsn['port']};" : ""; - $connection = new Database("{$dsn['phptype']}:host={$dsn['hostspec']}; $port dbname={$dsn['database']}", $dsn['username'], $dsn['password']); - $connection->query('SET client_encoding = "UTF-8"'); + /*.Database.*/$connection = new static("{$dsn['phptype']}:host={$dsn['hostspec']}; $port dbname={$dsn['database']}", $dsn['username'], $dsn['password']); + if ($dsn['phptype'] == 'pgsql') { + $connection->query('SET client_encoding="UTF-8"'); + } } if ($dsn['phptype'] == 'sqlite') { - $connection = new Database("{$dsn['phptype']}:{$dsn['database']}"); + /*.Database.*/$connection = new static("{$dsn['phptype']}:{$dsn['database']}"); + $connection->setAttribute(PDO::ATTR_TIMEOUT, 5); + $mode = defined('SQLITE_JOURNAL_MODE') ? SQLITE_JOURNAL_MODE : 'WAL'; + $connection->query("PRAGMA journal_mode=$mode"); + + if(!function_exists('sqliteLower')){ + function sqliteLower($str) { + return mb_strtolower($str, 'UTF-8'); + } + $connection->sqliteCreateFunction('LOWER', 'sqliteLower', 1); + } } $connection->dsn = $dsn; return $connection; } - public function executeQuery($query) + public function executeQuery($query, $values=null) { - $stmt = $this->prepare($query); - $stmt->setFetchMode(PDO::FETCH_ASSOC); - $stmt->execute(); - $stmt->cache = $stmt->fetchAll(); - return $stmt;//$sth->fetchAll(); + /*.Database_PDOStatement.*/$stmt = $this->prepare($query); + + $stmt->execute($values); + $stmt->cache = $stmt->fetchAll(PDO::FETCH_ASSOC); + return $stmt; } public function prepareStatement($query) { - return new DatabaseStatement($query, $this); + return new Database_Statement($query, $this); } // Для совместимости со старым представлением баз данных CIS @@ -58,7 +72,7 @@ class Database extends PDO */ public function fetchAllArray($query,$values=null) { - $sth = $this->prepare($query); + /*.Database_PDOStatement.*/$sth = $this->prepare($query); $prep = $this->prepareValues($values); $sth->execute($prep); return $sth->fetchAll(PDO::FETCH_ASSOC); @@ -69,13 +83,13 @@ class Database extends PDO */ public function fetchOneArray($query,$values=null) { - $sth = $this->prepare($query); + /*.Database_PDOStatement.*/$sth = $this->prepare($query); $prep = $this->prepareValues($values); $sth->execute($prep); return $sth->fetch(PDO::FETCH_ASSOC); } - private function assignQuote($x, $y) + private static function assignQuote($x, $y) { return $x . "=" . $this->quote($y); } @@ -136,12 +150,17 @@ class Database extends PDO */ function updateQuery($table, array $values, $cond) { - return $this->query("UPDATE $table SET " . implode(",", - array_map(array($this, 'assignQuote'), array_keys($values), array_values($values))) . " WHERE $cond"); + $prep = $this->prepareValues($values); + $sql = "UPDATE $table SET " . implode(",", + array_map(function($k,$v){return $k."=".$v;}, array_keys($values), array_keys($prep))) . " WHERE $cond"; + + $stmt = $this->prepare($sql); + $stmt->setFetchMode(PDO::FETCH_ASSOC); + $stmt->execute($prep); } function getIdGenerator() { - return new IdGenerator($this); + return new Database_IdGenerator($this); } /** @@ -149,8 +168,7 @@ class Database extends PDO * @param string $seq Имя последовательности для ключа таблицы * @return int Идентефикатор следующей записи */ - function getNextId($seq) - { + function getNextId($seq) { $result = $this->fetchOneArray("SELECT nextval('$seq')"); return $result['nextval']; } @@ -160,249 +178,3 @@ class Database extends PDO return null; } } - -class IdGenerator { - private $db; - - function __construct($db) { - $this->db = $db; - } - - function isBeforeInsert() { - return false; - } - - function isAfterInsert() { - return true; - } - - function getId($seq) { - $result = $this->db->fetchOneArray("SELECT nextval('$seq')"); - return $result['nextval']; -// $result = $this->db->fetchOneArray("SELECT last_insert_rowid() AS nextval"); -// return $result['nextval']; - } -} - -class PDODatabaseStatementIterator implements Iterator -{ - - private $result; - private $pos = 0; - private $fetchmode; - private $row_count; - private $rs; - - - /** - * Construct the iterator. - * @param PgSQLResultSet $rs - */ - public function __construct($rs) - { - $this->result = $rs; - $this->row_count = $rs->getRecordCount(); - } - - function rewind() - { - $this->pos = 0; - } - - function valid() - { - return ($this->pos < $this->row_count); - } - - function key() - { - return $this->pos; - } - - function current() - { - if (!isset($this->result->cache[$this->pos])) { - $this->result->cache[$this->pos] = $this->result->fetch(PDO::FETCH_ASSOC); - } - return $this->result->cache[$this->pos]; - } - - function next() - { - $this->pos++; - } - - function seek ( $index ) - { - $this->pos = $index; - } - - function count ( ) { - return $this->row_count; - } -} - -class PDODatabaseStatement extends PDOStatement implements IteratorAggregate -{ - protected $cursorPos = 0; - public $cache = array(); - public $fields; - - function getIterator() - { - return new PDODatabaseStatementIterator($this); - } - - protected function __construct() { - } - - function rewind() - { - $this->cursorPos = 0; - } - - public function seek($rownum) - { - if ($rownum < 0) { - return false; - } - - // PostgreSQL rows start w/ 0, but this works, because we are - // looking to move the position _before_ the next desired position - $this->cursorPos = $rownum; - return true; - } - - function valid() - { - return ( true ); - } - - - public function first() - { - if($this->cursorPos !== 0) { $this->seek(0); } - return $this->next(); - } - - function next() - { - if ($this->getRecordCount() > $this->cursorPos) { - if (!isset($this->cache[$this->cursorPos])) { - $this->cache[$this->cursorPos] = $this->fetch(PDO::FETCH_ASSOC); - } - $this->fields = $this->cache[$this->cursorPos]; - - $this->cursorPos++; - return true; - } else { - $this->fields = null; - return false; - } - } - - function key() { - return $this->cursorPos; - } - - function current() - { - return $this->result->fetch(PDO::FETCH_ASSOC); - } - - function getRow() - { - return $this->fields; - } - - function getInt($name) - { - return intval($this->fields[$name]); - } - - function getBlob($name) - { - return $this->fields[$name]; - } - - function getString($name) - { - return $this->fields[$name]; - } - - function getBoolean($name) - { - return (bool)$this->fields[$name]; - } - - function get($name) - { - return $this->fields[$name]; - } - - function getRecordCount() - { - return count($this->cache); - } -} - - -/** - * Класс оболочка для PDOStatement для замены Creole - */ -class DatabaseStatement -{ - protected $limit = null; - protected $offset = null; - protected $statement = null; - protected $binds = array(); - protected $conn; - protected $query; - - function __construct($query, $conn) { - $this->query = $query; - $this->conn = $conn; - } - - function setInt($n, $value) - { - $this->binds [] = array($n, $value, PDO::PARAM_INT); - } - - function setString($n, $value) - { - $this->binds [] = array($n, $value, PDO::PARAM_STR); - } - - function setBlob($n, $value) - { - $this->binds [] = array($n, $value, PDO::PARAM_LOB); - } - - function setLimit($limit) - { - $this->limit = $limit; - } - - function setOffset($offset) - { - $this->offset = $offset; - } - - function executeQuery() - { - if ($this->limit) { - $this->query .= " LIMIT {$this->limit} OFFSET {$this->offset}"; - } - $stmt = $this->conn->prepare($this->query); - foreach ($this->binds as $bind) { - list($n, $value, $type) = $bind; - $stmt->bindValue($n, $value, $type); - } - $stmt->setFetchMode(PDO::FETCH_ASSOC); - $stmt->execute(); - $stmt->cache = $stmt->fetchAll(); - - return $stmt; - } -} diff --git a/src/Database/IdGenerator.php b/src/Database/IdGenerator.php new file mode 100644 index 0000000..f7ebfee --- /dev/null +++ b/src/Database/IdGenerator.php @@ -0,0 +1,26 @@ +db = $db; + } + + function isBeforeInsert() { + return $this->db->isPostgres(); + } + + function isAfterInsert() { + return !$this->db->isPostgres(); + } + + function getId($seq) { + if ($this->db->isPostgres()) { + $result = $this->db->fetchOneArray("SELECT nextval('$seq') AS nextval"); + } else { + $result = $this->db->fetchOneArray("SELECT last_insert_rowid() AS nextval"); + } + return intval($result['nextval']); + } +} diff --git a/src/Database/JsonInstall.php b/src/Database/JsonInstall.php new file mode 100644 index 0000000..ee62cf3 --- /dev/null +++ b/src/Database/JsonInstall.php @@ -0,0 +1,143 @@ +db_manager = $db_manager; + } + + function install($dbinit_path, $dbfill_path = null) { + $dbinit_file = file_get_contents($dbinit_path); + if (is_string($dbinit_file)) { + $initActions = json_decode($dbinit_file, true); + if (!$initActions) { + echo "Invalid dbinit.json ".$dbinit_file; + return 0; + } + } else { + echo "No dbinit.json"; + return 0; + } + + $this->initDataBase($initActions, $dbinit_path); + if ($dbfill_path) { + $this->fillDataBase($dbfill_path); + } + $this->makeConstraints($initActions); + } + + function missingTables($tables) { + $actual_tables = $this->db_manager->GetAllTableNames(); + $missingTables = []; + foreach ($tables as $table) { + if (!in_array($table, $actual_tables)) + $missingTables[] = $table; + } + return $missingTables; + } + + //Создать таблицы + function initDataBase(/*.array.*/$initActions, $dbinit_path) { + $pg = $this->db_manager->db->isPostgres(); + if (!$pg) { + $refs = []; + //В sqlite нет alter reference. Референсы надо создавать при создании таблицы. + foreach ($initActions as $action) { + if ($action["type"] == "alterReference") { + if (!isset($refs[$action["table"]])) + $refs[$action["table"]] = []; + $refs[$action["table"]][]=$action;//добавить к списку референсов для таблицы + } + } + } + + foreach ($initActions as $action) { + if (!$pg) { + if ($action["type"] == "createTable") { + $table_name = $action["table_name"]; + if (isset($refs[$table_name])) { + foreach ($refs[$table_name] as $value) { + $action['fields'][$value['column']]['references'] = + $value['refTable']."(".$value['refColumn'].")"; + } + } + + } + } + if ($action["type"] != "alterReference") { + $this->db_manager->ExecuteAction($action, $dbinit_path); + } + } + + //Запомнить все колонки serial + $this->serialColumns = []; + if ($pg) { + foreach ($initActions as $action) { + if ($action["type"] == "createTable") { + foreach ($action["fields"] as $name => $field) { + if ($field["type"]=="serial") { + $this->serialColumns[] = [ + "table"=>$action["table_name"], + "column"=>$name + ]; + } + } + } + } + } + } + + //Заполнить данными + function fillDataBase($dbfill_file_path) { + $dbfill_file = file_get_contents($dbfill_file_path); + if (is_string($dbfill_file)) { + $actions = json_decode($dbfill_file,true); + if ($actions) { + + //Проверка что упоминаемые в списке действий таблицы уже есть в базе + $affected_tables = []; + foreach ($actions as $action) { + if ($action["table_name"]) { + $affected_tables[$action["table_name"]] = 1; + } + } + + $missing = $this->missingTables(array_keys($affected_tables)); + if (!empty($missing)) { + echo "dbfill error. Missing tables: ".implode(" ", $missing); + return; + } + + //Выполнение действий + foreach ($actions as $action) { + $this->db_manager->ExecuteAction($action, $dbfill_file_path); + } + } else { + echo "Invalid dbfill.json"; + } + } else { + echo "No dbfill.json"; + } + } + + //Обновить ключи serial и создать ограничения + function makeConstraints($initActions) { + $pg = $this->db_manager->db->isPostgres(); + if ($pg) { + foreach ($this->serialColumns as $serialColumn) { + $this->db_manager->UpdateSerial($serialColumn["table"], $serialColumn["column"]); + } + + + foreach ($initActions as $action) { + if ($action["type"] == "alterReference") { + $this->db_manager->ExecuteAction($action); + } + } + } + } + +} \ No newline at end of file diff --git a/src/Database/Manager.php b/src/Database/Manager.php new file mode 100644 index 0000000..11b8dbc --- /dev/null +++ b/src/Database/Manager.php @@ -0,0 +1,211 @@ +db = $db; + } + + public function ExecuteAction(/*.array.*/$action, $db_file = "") { + switch($action["type"]) { + case "dropTable": + $this->DropTableQuery($action["table_name"], true); + break; + case "createTable": + $constraints = isset($action["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 "executeFile": + if ($this->db->isPostgres() && isset($action["pgsql"])) { + $file = $action["pgsql"]; + } else { + $file = $action["source"]; + } + + $stmtList = Tools_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); + } + } + + public function DropTableQuery($table, $cascade=false) { + $statement = "DROP TABLE IF EXISTS ".$table; + if ($this->db->isPostgres()&&$cascade) { + $statement = $statement." CASCADE"; + } + $this->db->query($statement); + } + + public function AlterReference($table,$column,$refTable,$refColumn) { + $this->db->query("ALTER TABLE ".$table." ADD CONSTRAINT ".$table."_".$column."fk"." FOREIGN KEY (".$column.") REFERENCES ".$refTable." (".$refColumn.")"); + } + + //Извлечение информации о полях таблицы + 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"=> ((boolean) $result["pk"]) ? "PRIMARY KEY" : null + ]; + } + return $fields; + } + } + + public function RenameColumn($table, $old_name, $new_name) { + $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; + } + + /*.array.*/$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 после ручной вставки + public function UpdateSerial($table,$column) { + $this->db->query("SELECT setval(pg_get_serial_sequence('".$table."', '".$column."'), coalesce(max(".$column."),0) + 1, false) FROM ".$table); + } + + public function Column_Definition($name,$data,$pg){ + $constraint = isset($data['constraint'])?" ".$data['constraint']:""; + $references = ""; + if (isset($data['references'])) { + $references = " REFERENCES ".$data['references']; + } + if (isset($data["not_null"])&&$data["not_null"]) + $constraint .=" NOT NULL"; + $type = $data['type']; + if (!$pg) { + if (strtolower($type)=="serial") + $type = "integer"; + //if (strtolower($type)=="boolean") + // $type = "integer"; + } + return $name." ".$type.$references.$constraint; + } + + public function AddColumn($table_name,$column_name,$field){ + $pg = $this->db->isPostgres(); + $q = "ALTER TABLE ".$table_name." ADD COLUMN ". + $this->Column_Definition($column_name, $field, $pg); + $this->db->query($q); + } + + //CreateTableQuery('users',['id'=>['type'=>'integer','constraint'=>'PRIMARY KEY']]) + public function CreateTableQuery($table, $fields, $constraints) { + $pg = $this->db->isPostgres(); + if ($constraints) { + $constraints = ", " . $constraints; + } + + $statement = "CREATE TABLE $table (" . implode(",", + array_map(function($name,$data) use ($pg) { + return $this->Column_Definition($name,$data,$pg); + }, array_keys($fields), array_values($fields)) + ) . " " . $constraints . ")"; + $this->db->query($statement); + } + + public function DumpTable($table_name) { + $pg = $this->db->isPostgres(); + + /*.array.*/$result = array(); + /*.array.*/$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) { + /*.array.*/$row = $row; + if (isset($row[$name])) { + $row[$name] = boolval($row[$name]); + } + } + } + } + } + foreach ($data as $r) { + $result[] = array( + "type" => "insert", + "table_name" => $table_name, + "values" => $r + ); + } + return $result; + } + + 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; + } + + public function DumpInserts() { + $table_names = $this->GetAllTableNames(); + $result = array(); + foreach ($table_names as $table_name) { + $result = array_merge($result, $this->DumpTable($table_name)); + } + return $result; + } +} \ No newline at end of file diff --git a/src/Database/PDOStatement.php b/src/Database/PDOStatement.php new file mode 100644 index 0000000..971afb8 --- /dev/null +++ b/src/Database/PDOStatement.php @@ -0,0 +1,97 @@ +cursorPos = 0; + } + + public function seek($rownum) { + if ($rownum < 0) { + return false; + } + + // PostgreSQL rows start w/ 0, but this works, because we are + // looking to move the position _before_ the next desired position + $this->cursorPos = $rownum; + return true; + } + + function valid() { + return true; + } + + + public function first() { + if($this->cursorPos !== 0) { $this->seek(0); } + return $this->next(); + } + + function next() { + if ($this->getRecordCount() > $this->cursorPos) { + if (!isset($this->cache[$this->cursorPos])) { + $this->cache[$this->cursorPos] = $this->fetch(PDO::FETCH_ASSOC); + } + $this->fields = $this->cache[$this->cursorPos]; + + $this->cursorPos++; + return true; + } else { + $this->fields = null; + return false; + } + } + + function key() { + return $this->cursorPos; + } + + function current() { + return $this->cache[$this->cursorPos]; + } + + function getRow() { + return $this->fields; + } + + function getInt($name) { + return intval($this->fields[$name]); + } + + function getBlob($name) { + return $this->fields[$name]; + } + + function getString($name) { + return $this->fields[$name]; + } + + function getBoolean($name) { + return (bool)$this->fields[$name]; + } + + function get($name) { + return $this->fields[$name]; + } + + function getArray($name) { + return strToArray($this->fields[$name]); + } + + function getRecordCount() { + return count($this->cache); + } +} diff --git a/src/Database/Statement.php b/src/Database/Statement.php new file mode 100644 index 0000000..80b77da --- /dev/null +++ b/src/Database/Statement.php @@ -0,0 +1,61 @@ +query = $query; + $this->conn = $conn; + } + + function setInt($n, $value) + { + $this->binds [] = array($n, $value, PDO::PARAM_INT); + } + + function setString($n, $value) + { + $this->binds [] = array($n, $value, PDO::PARAM_STR); + } + + function setBlob($n, $value) + { + $this->binds [] = array($n, $value, PDO::PARAM_LOB); + } + + function setLimit($limit) + { + $this->limit = $limit; + } + + function setOffset($offset) + { + $this->offset = $offset; + } + + function executeQuery() + { + if ($this->limit) { + $this->query .= " LIMIT {$this->limit} OFFSET {$this->offset}"; + } + /*.Database_PDOStatement.*/$stmt = $this->conn->prepare($this->query); + foreach ($this->binds as $bind) { + list($n, $value, $type) = $bind; + $stmt->bindValue($n, $value, (int) $type); + } + + $stmt->execute(); + $stmt->cache = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return $stmt; + } +} diff --git a/src/Database/StatementIterator.php b/src/Database/StatementIterator.php new file mode 100644 index 0000000..cc2cc05 --- /dev/null +++ b/src/Database/StatementIterator.php @@ -0,0 +1,46 @@ +result = $rs; + $this->row_count = $rs->getRecordCount(); + } + + function rewind() { + $this->pos = 0; + } + + function valid() { + return ($this->pos < $this->row_count); + } + + function key() { + return $this->pos; + } + + function current() { + if (!isset($this->result->cache[$this->pos])) { + $this->result->cache[$this->pos] = $this->result->fetch(PDO::FETCH_ASSOC); + } + return $this->result->cache[$this->pos]; + } + + function next() { + $this->pos++; + } + + function seek($index) { + $this->pos = $index; + } + + function count() { + return $this->row_count; + } +} diff --git a/src/Validator/Rule/Notnull.php b/src/Validator/Rule/Notnull.php index a3f27f2..2b0c3cb 100644 --- a/src/Validator/Rule/Notnull.php +++ b/src/Validator/Rule/Notnull.php @@ -1,6 +1,6 @@