mirror of
https://github.com/maxpozdeev/mytinytodo.git
synced 2026-03-11 08:55:27 +00:00
+ can setup postgresql database
This commit is contained in:
parent
ca3e332a2f
commit
772305dbed
4 changed files with 378 additions and 28 deletions
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
// Rename it to config.php before using in docker.
|
||||
|
||||
if (getenv('MTT_DB_TYPE') == 'mysql') {
|
||||
define("MTT_DB_TYPE", "mysql");
|
||||
define("MTT_DB_HOST", getenv('MTT_DB_HOST'));
|
||||
define("MTT_DB_NAME", getenv('MTT_DB_NAME'));
|
||||
define("MTT_DB_USER", getenv('MTT_DB_USER'));
|
||||
define("MTT_DB_PASSWORD", getenv('MTT_DB_PASSWORD'));
|
||||
define("MTT_DB_PREFIX", getenv('MTT_DB_PREFIX'));
|
||||
define("MTT_DB_DRIVER", getenv('MTT_DB_DRIVER'));
|
||||
if (getenv('MTT_DB_TYPE') == 'mysql' || getenv('MTT_DB_TYPE') == 'postgres') {
|
||||
define("MTT_DB_TYPE", getenv('MTT_DB_TYPE'));
|
||||
define("MTT_DB_HOST", getenv('MTT_DB_HOST') ?: "undefined_host");
|
||||
define("MTT_DB_NAME", getenv('MTT_DB_NAME') ?: "undefined_db");
|
||||
define("MTT_DB_USER", getenv('MTT_DB_USER') ?: "undefined_user");
|
||||
define("MTT_DB_PASSWORD", getenv('MTT_DB_PASSWORD') ?: "");
|
||||
define("MTT_DB_PREFIX", getenv('MTT_DB_PREFIX') ?: "mtt_");
|
||||
define("MTT_DB_DRIVER", getenv('MTT_DB_DRIVER') ?: "");
|
||||
}
|
||||
else if (getenv('MTT_DB_TYPE') == 'sqlite') {
|
||||
define("MTT_DB_TYPE", "sqlite");
|
||||
|
|
|
|||
208
src/includes/class.db.postgres.php
Normal file
208
src/includes/class.db.postgres.php
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
This file is a part of myTinyTodo.
|
||||
(C) Copyright 2023 Max Pozdeev <maxpozdeev@gmail.com>
|
||||
Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------- //
|
||||
class DatabaseResult_Postgres extends DatabaseResult_Abstract
|
||||
{
|
||||
/** @var PDOStatement */
|
||||
protected $q;
|
||||
|
||||
/** @var int */
|
||||
protected $affected;
|
||||
|
||||
/** @var string const */
|
||||
protected $schema = 'public';
|
||||
|
||||
function __construct(PDO $dbh, string $query, bool $resultless = false)
|
||||
{
|
||||
// use with DELETE, INSERT, UPDATE
|
||||
if ($resultless)
|
||||
{
|
||||
$this->affected = (int) $dbh->exec($query); //throws PDOException
|
||||
}
|
||||
// SELECT
|
||||
else
|
||||
{
|
||||
$this->q = $dbh->query($query); //throws PDOException
|
||||
$this->affected = $this->q->rowCount();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchRow(): ?array
|
||||
{
|
||||
$res = $this->q->fetch(PDO::FETCH_NUM);
|
||||
if ($res === false || !is_array($res)) {
|
||||
return null;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
function fetchAssoc(): ?array
|
||||
{
|
||||
$res = $this->q->fetch(PDO::FETCH_ASSOC);
|
||||
if ($res === false || !is_array($res)) {
|
||||
return null;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
function rowsAffected(): int
|
||||
{
|
||||
return $this->affected;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------- //
|
||||
class Database_Postgres extends Database_Abstract
|
||||
{
|
||||
const DBTYPE = 'postgres';
|
||||
|
||||
/** @var PDO */
|
||||
protected $dbh;
|
||||
|
||||
/** @var int */
|
||||
protected $affected = 0;
|
||||
|
||||
protected $dbname;
|
||||
|
||||
function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
function connect(array $params): void
|
||||
{
|
||||
$host = $params['host'];
|
||||
$user = $params['user'];
|
||||
$pass = $params['password'];
|
||||
$db = $params['db'];
|
||||
$options = array(
|
||||
PDO::PGSQL_ATTR_DISABLE_PREPARES => 1,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
);
|
||||
$this->dbname = $db;
|
||||
$this->dbh = new PDO("pgsql:host=$host;dbname=$db", $user, $pass, $options);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Returns single row of SELECT query as indexed array (FETCH_NUM).
|
||||
Returns single field value if resulting array has only one field.
|
||||
*/
|
||||
function sq(string $query, ?array $values = null)
|
||||
{
|
||||
$q = $this->_dq($query, $values);
|
||||
|
||||
$res = $q->fetchRow();
|
||||
if ($res === false || !is_array($res)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sizeof($res) > 1) return $res;
|
||||
else return $res[0];
|
||||
}
|
||||
|
||||
/*
|
||||
Returns single row of SELECT query as dictionary array (FETCH_ASSOC).
|
||||
*/
|
||||
function sqa(string $query, ?array $values = null): ?array
|
||||
{
|
||||
$q = $this->_dq($query, $values);
|
||||
$res = $q->fetchAssoc();
|
||||
if ($res === false || !is_array($res)){
|
||||
return null;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
function dq(string $query, ?array $values = null) : DatabaseResult_Abstract
|
||||
{
|
||||
return $this->_dq($query, $values);
|
||||
}
|
||||
|
||||
/*
|
||||
for resultless queries like INSERT,UPDATE,DELETE
|
||||
*/
|
||||
function ex(string $query, ?array $values = null): void
|
||||
{
|
||||
$this->_dq($query, $values, true);
|
||||
}
|
||||
|
||||
private function _dq(string $query, ?array $values = null, bool $resultless = false) : DatabaseResult_Abstract
|
||||
{
|
||||
if (null !== $values && sizeof($values) > 0)
|
||||
{
|
||||
$m = explode('?', $query);
|
||||
if (sizeof($m) < sizeof($values)+1) {
|
||||
throw new Exception("params to set MORE than query params");
|
||||
}
|
||||
if (sizeof($m) > sizeof($values)+1) {
|
||||
throw new Exception("params to set LESS than query params");
|
||||
}
|
||||
$query = "";
|
||||
for ($i=0; $i<sizeof($m)-1; $i++) {
|
||||
$query .= $m[$i]. $this->quote($values[$i]);
|
||||
}
|
||||
$query .= $m[$i];
|
||||
}
|
||||
$this->lastQuery = $query;
|
||||
$dbr = new DatabaseResult_Postgres($this->dbh, $query, $resultless);
|
||||
$this->affected = $dbr->rowsAffected();
|
||||
return $dbr;
|
||||
}
|
||||
|
||||
function affected(): int
|
||||
{
|
||||
return $this->affected;
|
||||
}
|
||||
|
||||
function quote($value): string
|
||||
{
|
||||
if (null === $value) {
|
||||
return 'null';
|
||||
}
|
||||
return $this->dbh->quote((string) $value);
|
||||
}
|
||||
|
||||
function quoteForLike(string $format, string $string): string
|
||||
{
|
||||
$string = str_replace(array('\\','%','_'), array('\\\\','\%','\_'), $string);
|
||||
return $this->dbh->quote(sprintf($format, $string)). " ESCAPE '\'";
|
||||
}
|
||||
|
||||
function like(string $column, string $format, string $string): string
|
||||
{
|
||||
$column = str_replace('"', '""', $column);
|
||||
return '"'. $column. '" ILIKE '. $this->quoteForLike($format, $string);
|
||||
}
|
||||
|
||||
function lastInsertId(?string $name = null): ?string
|
||||
{
|
||||
$ret = $this->dbh->lastInsertId();
|
||||
if (false === $ret) {
|
||||
return null;
|
||||
}
|
||||
return (string) $ret;
|
||||
}
|
||||
|
||||
function tableExists(string $table): bool
|
||||
{
|
||||
$r = $this->sq("SELECT 1 FROM information_schema.tables WHERE table_catalog = ? AND table_name = ?",
|
||||
array($this->dbname, $table) );
|
||||
if ($r === false || $r === null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function tableFieldExists(string $table, string $field): bool
|
||||
{
|
||||
$r = $this->sq("SELECT 1 FROM information_schema.columns WHERE table_name = ? AND column_name = ? AND table_schema = ?",
|
||||
array($table, $field, $this->schema) );
|
||||
if ($r === false || $r === null) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
25
src/init.php
25
src/init.php
|
|
@ -116,6 +116,31 @@ function configureDbConnection()
|
|||
$db->dq("SET NAMES utf8mb4");
|
||||
}
|
||||
|
||||
# PostgreSQL Database
|
||||
else if (MTT_DB_TYPE == 'postgres')
|
||||
{
|
||||
require_once(MTTINC. 'class.db.postgres.php');
|
||||
$db = DBConnection::init(new Database_Postgres());
|
||||
try {
|
||||
$db->connect([
|
||||
'host' => MTT_DB_HOST,
|
||||
'user' => MTT_DB_USER,
|
||||
'password' => MTT_DB_PASSWORD,
|
||||
'db' => MTT_DB_NAME,
|
||||
]);
|
||||
}
|
||||
catch(Exception $e) {
|
||||
$errlog = "Failed to connect to PostgreSQL database: ". $e->getMessage();
|
||||
if (MTT_DEBUG) {
|
||||
logAndDie($errlog);
|
||||
}
|
||||
else {
|
||||
logAndDie("Failed to connect to database", $errlog);
|
||||
}
|
||||
}
|
||||
$db->dq("SET NAMES 'utf8'");
|
||||
}
|
||||
|
||||
# SQLite3 Database
|
||||
elseif (MTT_DB_TYPE == 'sqlite')
|
||||
{
|
||||
|
|
|
|||
157
src/setup.php
157
src/setup.php
|
|
@ -77,22 +77,28 @@ if ($configExists)
|
|||
// Determine current installed db version
|
||||
$ver = databaseVersion($db);
|
||||
|
||||
if ($ver == '1.4') {
|
||||
if ($ver == '') {
|
||||
// clean install
|
||||
// will not load settings from database in init.php
|
||||
Config::$noDatabase = true;
|
||||
}
|
||||
else if (version_compare($ver, '1.4') < 0) {
|
||||
// Very old or previously failed while install
|
||||
exitMessage(htmlspecialchars("Can not update. Unsupported database version ($ver)."));
|
||||
}
|
||||
else if ($ver == '1.4') {
|
||||
// Need to upgrade. Do not ask for old password
|
||||
require_once(MTTPATH. 'db/config.php');
|
||||
Config::loadConfigV14($config);
|
||||
unset($config);
|
||||
DBConnection::init($db);
|
||||
}
|
||||
else {
|
||||
if ($ver == '') {
|
||||
Config::$noDatabase = true; //will not load settings from database in init.php
|
||||
}
|
||||
require_once('./init.php');
|
||||
if ( !is_logged() ) {
|
||||
die("Access denied!<br> Disable password protection or Log in.");
|
||||
}
|
||||
|
||||
require_once('./init.php');
|
||||
if ( !is_logged() ) {
|
||||
die("Access denied!<br> Disable password protection or Log in.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($ver == '')
|
||||
|
|
@ -102,7 +108,7 @@ if ($ver == '')
|
|||
if ($install == '' && $db !== null)
|
||||
{
|
||||
# We already have settings file and need to create tables.
|
||||
exitMessage("<form method=post>Click next to create tables in '". htmlspecialchars($dbtype). "' database.<br><br>
|
||||
exitMessage("<form method=post>Click next to create tables in ". htmlspecialchars(databaseTypeName($db)). " database.<br><br>
|
||||
<input type=hidden name=stoken value='$setupToken'>
|
||||
<input type=hidden name=install value=create>
|
||||
<input type=submit value=' Next '></form>");
|
||||
|
|
@ -114,9 +120,10 @@ if ($ver == '')
|
|||
<form method=post>Select database type to use:<br><br>
|
||||
<input type=hidden name=install value=config>
|
||||
<input type=hidden name=stoken value='$setupToken'>
|
||||
<label><input type=radio name=db_type value=sqlite checked=checked onclick=\"document.getElementById('mysqlsettings').style.display='none'\">SQLite</label><br><br>
|
||||
<label><input type=radio name=db_type value=mysql onclick=\"document.getElementById('mysqlsettings').style.display=''\">MySQL</label><br>
|
||||
<div id='mysqlsettings' style='display:none; margin-left:30px;'><br><table>
|
||||
<label><input type=radio name=db_type value=sqlite checked=checked onclick=\"document.getElementById('dbsettings').style.display='none'\"> SQLite</label><br><br>
|
||||
<label><input type=radio name=db_type value=mysql onclick=\"document.getElementById('dbsettings').style.display=''\"> MySQL</label><br><br>
|
||||
<label><input type=radio name=db_type value=postgres onclick=\"document.getElementById('dbsettings').style.display=''\"> PostgreSQL (beta)</label><br>
|
||||
<div id='dbsettings' style='display:none; margin-left:30px;'><br><table>
|
||||
<tr><td>Host:</td><td><input name=db_host value=localhost></td></tr>
|
||||
<tr><td>Database:</td><td><input name=db_name value=mytinytodo></td></tr>
|
||||
<tr><td>User:</td><td><input name=db_user value=mtt></td></tr>
|
||||
|
|
@ -129,9 +136,12 @@ if ($ver == '')
|
|||
{
|
||||
check_post_stoken();
|
||||
# Save configuration
|
||||
$dbtype = ($_POST['db_type'] == 'mysql') ? 'mysql' : 'sqlite';
|
||||
$dbtype = $_POST['db_type'] ?? '';
|
||||
if ($dbtype != 'mysql' && $dbtype != 'postgres' && $dbtype != 'sqlite') {
|
||||
exitMessage("Unknown database type $dbtype");
|
||||
}
|
||||
Config::set('db.type', $dbtype);
|
||||
if ($dbtype == 'mysql') {
|
||||
if ($dbtype == 'mysql' || $dbtype == 'postgres') {
|
||||
Config::set('db.host', _post('db_host'));
|
||||
Config::set('db.name', _post('db_name'));
|
||||
Config::set('db.user', _post('db_user'));
|
||||
|
|
@ -240,12 +250,15 @@ function createAllTables($db, $dbtype)
|
|||
if ($dbtype == 'mysql') {
|
||||
createMysqlTables($db);
|
||||
}
|
||||
else if ($dbtype == 'postgres') {
|
||||
createPostgresTables($db);
|
||||
}
|
||||
else {
|
||||
createSqliteTables($db);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ===== mysql ========================================================= */
|
||||
function createMysqlTables($db)
|
||||
{
|
||||
$db->ex(
|
||||
|
|
@ -324,6 +337,78 @@ function createMysqlTables($db)
|
|||
}
|
||||
|
||||
|
||||
/* ===== postgres =============================================== */
|
||||
function createPostgresTables($db)
|
||||
{
|
||||
$db->ex(
|
||||
"CREATE TABLE {$db->prefix}lists (
|
||||
id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
uuid CHAR(36) NOT NULL default '',
|
||||
ow INTEGER NOT NULL default 0,
|
||||
name VARCHAR(250) NOT NULL default '',
|
||||
d_created INTEGER NOT NULL default 0,
|
||||
d_edited INTEGER NOT NULL default 0,
|
||||
sorting SMALLINT NOT NULL default 0,
|
||||
published SMALLINT NOT NULL default 0,
|
||||
taskview INTEGER NOT NULL default 0,
|
||||
extra TEXT
|
||||
) ");
|
||||
$db->ex("CREATE UNIQUE INDEX {$db->prefix}lists_uuid ON {$db->prefix}lists (uuid)");
|
||||
|
||||
$db->ex(
|
||||
"CREATE TABLE {$db->prefix}todolist (
|
||||
id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
uuid CHAR(36) NOT NULL default '',
|
||||
list_id INTEGER NOT NULL default 0,
|
||||
d_created INTEGER NOT NULL default 0,
|
||||
d_completed INTEGER NOT NULL default 0,
|
||||
d_edited INTEGER NOT NULL default 0,
|
||||
compl SMALLINT NOT NULL default 0,
|
||||
title VARCHAR(250) NOT NULL default '',
|
||||
note TEXT default NULL,
|
||||
prio SMALLINT NOT NULL default 0,
|
||||
ow INTEGER NOT NULL default 0,
|
||||
duedate DATE default NULL
|
||||
) ");
|
||||
$db->ex("CREATE INDEX {$db->prefix}todo_list_id ON {$db->prefix}todolist (list_id)");
|
||||
$db->ex("CREATE UNIQUE INDEX {$db->prefix}todo_uuid ON {$db->prefix}todolist (uuid)");
|
||||
|
||||
$db->ex(
|
||||
"CREATE TABLE {$db->prefix}tags (
|
||||
id INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(250) NOT NULL DEFAULT ''
|
||||
) ");
|
||||
$db->ex("CREATE UNIQUE INDEX {$db->prefix}tags_name ON {$db->prefix}tags (name)");
|
||||
|
||||
$db->ex(
|
||||
"CREATE TABLE {$db->prefix}tag2task (
|
||||
tag_id INTEGER NOT NULL,
|
||||
task_id INTEGER NOT NULL,
|
||||
list_id INTEGER NOT NULL
|
||||
) ");
|
||||
$db->ex("CREATE INDEX {$db->prefix}tag2task_tag_id ON {$db->prefix}tag2task (tag_id)");
|
||||
$db->ex("CREATE INDEX {$db->prefix}tag2task_task_id ON {$db->prefix}tag2task (task_id)");
|
||||
$db->ex("CREATE INDEX {$db->prefix}tag2task_list_id ON {$db->prefix}tag2task (list_id)");
|
||||
|
||||
$db->ex(
|
||||
"CREATE TABLE {$db->prefix}settings (
|
||||
param_key VARCHAR(250) NOT NULL default '',
|
||||
param_value TEXT
|
||||
) ");
|
||||
$db->ex("CREATE UNIQUE INDEX {$db->prefix}settings_key ON {$db->prefix}settings (param_key)");
|
||||
|
||||
$db->ex(
|
||||
"CREATE TABLE {$db->prefix}sessions (
|
||||
id VARCHAR(64) NOT NULL default '',
|
||||
data TEXT,
|
||||
last_access INTEGER NOT NULL default 0,
|
||||
expires INTEGER NOT NULL default 0
|
||||
) ");
|
||||
$db->ex("CREATE UNIQUE INDEX {$db->prefix}sessions_id ON {$db->prefix}sessions (id)");
|
||||
}
|
||||
|
||||
|
||||
/* ===== sqlite =============================================== */
|
||||
function createSqliteTables($db)
|
||||
{
|
||||
$db->ex(
|
||||
|
|
@ -364,7 +449,7 @@ function createSqliteTables($db)
|
|||
$db->ex(
|
||||
"CREATE TABLE {$db->prefix}tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(50) NOT NULL DEFAULT '' COLLATE UTF8CI
|
||||
name VARCHAR(250) NOT NULL DEFAULT '' COLLATE UTF8CI
|
||||
) ");
|
||||
$db->ex("CREATE INDEX tags_name ON {$db->prefix}tags (name)"); //NB: unique in mysql
|
||||
|
||||
|
|
@ -515,12 +600,31 @@ function testConnect(&$error)
|
|||
if (!defined($c)) throw new Exception("$c is not defined");
|
||||
}
|
||||
|
||||
$db->connect( array(
|
||||
$db->connect([
|
||||
'host' => MTT_DB_HOST,
|
||||
'user' => MTT_DB_USER,
|
||||
'password' => MTT_DB_PASSWORD,
|
||||
'db' => MTT_DB_NAME
|
||||
));
|
||||
]);
|
||||
}
|
||||
else if (MTT_DB_TYPE == 'postgres')
|
||||
{
|
||||
if (!defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')) {
|
||||
throw new Exception("Required PHP extension 'PDO_PostgreSQL' is not installed.");
|
||||
}
|
||||
require_once(MTTINC. 'class.db.postgres.php');
|
||||
|
||||
foreach (['MTT_DB_HOST', 'MTT_DB_USER', 'MTT_DB_PASSWORD', 'MTT_DB_NAME', 'MTT_DB_PREFIX'] as $c) {
|
||||
if (!defined($c)) throw new Exception("$c is not defined");
|
||||
}
|
||||
|
||||
$db = new Database_Postgres;
|
||||
$db->connect([
|
||||
'host' => MTT_DB_HOST,
|
||||
'user' => MTT_DB_USER,
|
||||
'password' => MTT_DB_PASSWORD,
|
||||
'db' => MTT_DB_NAME
|
||||
]);
|
||||
}
|
||||
else if (MTT_DB_TYPE == 'sqlite')
|
||||
{
|
||||
|
|
@ -535,7 +639,9 @@ function testConnect(&$error)
|
|||
}
|
||||
require_once(MTTINC. 'class.db.sqlite3.php');
|
||||
$db = new Database_Sqlite3;
|
||||
$db->connect( array( 'filename' => MTTPATH. 'db/todolist.db' ) );
|
||||
$db->connect([
|
||||
'filename' => MTTPATH. 'db/todolist.db'
|
||||
]);
|
||||
}
|
||||
else {
|
||||
throw new Exception("Unsupported database type: ". MTT_DB_TYPE);
|
||||
|
|
@ -566,6 +672,17 @@ function myExceptionHandler($e)
|
|||
exit;
|
||||
}
|
||||
|
||||
function databaseTypeName(Database_Abstract $db)
|
||||
{
|
||||
$s = get_class($db);
|
||||
switch ($s) {
|
||||
case "Database_Mysql": return "MySQL";
|
||||
case "Database_Postgres": return "PostgreSQL";
|
||||
case "Database_Sqlite3": return "SQLite";
|
||||
default: return $s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
### update v1.4 to v1.7 ##########
|
||||
function update_14_17(Database_Abstract $db, $dbtype)
|
||||
|
|
|
|||
Loading…
Reference in a new issue