This repository has been archived on 2024-09-05. You can view files and clone it, but cannot push or open issues or pull requests.
shimmie2/core/database.php

456 lines
13 KiB
PHP
Raw Normal View History

<?php
abstract class DatabaseDriver
{
public const MYSQL = "mysql";
public const PGSQL = "pgsql";
public const SQLITE = "sqlite";
}
/**
* A class for controlled database access
*/
class Database
{
2019-06-14 18:17:03 +00:00
/**
* The PDO database connection object, for anyone who wants direct access.
* @var null|PDO
*/
private $db = null;
/**
* @var float
*/
public $dbtime = 0.0;
/**
* Meta info about the database engine.
* @var DBEngine|null
*/
private $engine = null;
/**
* A boolean flag to track if we already have an active transaction.
* (ie: True if beginTransaction() already called)
*
* @var bool
*/
public $transaction = false;
/**
* How many queries this DB object has run
*/
public $query_count = 0;
2019-05-29 17:23:29 +00:00
private function connect_db(): void
{
# FIXME: detect ADODB URI, automatically translate PDO DSN
/*
* Why does the abstraction layer act differently depending on the
* back-end? Because PHP is deliberately retarded.
*
* http://stackoverflow.com/questions/237367
*/
$matches = [];
$db_user=null;
$db_pass=null;
if (preg_match("/user=([^;]*)/", DATABASE_DSN, $matches)) {
$db_user=$matches[1];
}
if (preg_match("/password=([^;]*)/", DATABASE_DSN, $matches)) {
$db_pass=$matches[1];
}
$db_params = [
2019-10-01 10:07:38 +00:00
PDO::ATTR_PERSISTENT => DATABASE_KA,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
$this->db = new PDO(DATABASE_DSN, $db_user, $db_pass, $db_params);
$this->connect_engine();
$this->engine->init($this->db);
$this->beginTransaction();
}
2019-05-29 17:23:29 +00:00
private function connect_engine(): void
{
if (preg_match("/^([^:]*)/", DATABASE_DSN, $matches)) {
$db_proto=$matches[1];
} else {
throw new SCoreException("Can't figure out database engine");
}
if ($db_proto === DatabaseDriver::MYSQL) {
$this->engine = new MySQL();
} elseif ($db_proto === DatabaseDriver::PGSQL) {
$this->engine = new PostgreSQL();
} elseif ($db_proto === DatabaseDriver::SQLITE) {
$this->engine = new SQLite();
} else {
die('Unknown PDO driver: '.$db_proto);
}
}
2019-05-29 17:23:29 +00:00
public function beginTransaction(): void
{
if ($this->transaction === false) {
$this->db->beginTransaction();
$this->transaction = true;
}
}
public function commit(): bool
{
if (!is_null($this->db) && $this->transaction === true) {
$this->transaction = false;
return $this->db->commit();
} else {
throw new SCoreException("Unable to call commit() as there is no transaction currently open.");
}
}
public function rollback(): bool
{
if (!is_null($this->db) && $this->transaction === true) {
$this->transaction = false;
return $this->db->rollback();
} else {
throw new SCoreException("Unable to call rollback() as there is no transaction currently open.");
}
}
public function escape(string $input): string
{
if (is_null($this->db)) {
$this->connect_db();
}
return $this->db->Quote($input);
}
public function scoreql_to_sql(string $input): string
{
if (is_null($this->engine)) {
$this->connect_engine();
}
return $this->engine->scoreql_to_sql($input);
}
public function scoresql_value_prepare($input)
{
if (is_null($this->engine)) {
$this->connect_engine();
}
2019-09-29 13:30:55 +00:00
if ($input===true) {
return $this->engine->BOOL_Y;
2019-09-29 13:30:55 +00:00
} elseif ($input===false) {
return $this->engine->BOOL_N;
}
return $input;
}
public function get_driver_name(): string
{
if (is_null($this->engine)) {
$this->connect_engine();
}
return $this->engine->name;
}
private function count_time(string $method, float $start, string $query, ?array $args): void
{
2019-09-29 13:30:55 +00:00
global $_tracer, $tracer_enabled;
$dur = microtime(true) - $start;
if ($tracer_enabled) {
$query = trim(preg_replace('/^[\t ]+/m', '', $query)); // trim leading whitespace
$_tracer->complete($start * 1000000, $dur * 1000000, "DB Query", ["query"=>$query, "args"=>$args, "method"=>$method]);
}
2019-09-29 13:30:55 +00:00
$this->query_count++;
2019-07-06 22:01:22 +00:00
$this->dbtime += $dur;
}
public function set_timeout(int $time): void
{
$this->engine->set_timeout($this->db, $time);
}
public function execute(string $query, array $args=[], bool $scoreql = false): PDOStatement
{
try {
2019-11-02 19:57:34 +00:00
if ($scoreql===true) {
$query = $this->scoreql_to_sql($query);
}
if (is_null($this->db)) {
$this->connect_db();
}
$stmt = $this->db->prepare(
"-- " . str_replace("%2F", "/", urlencode(@$_GET['q'])). "\n" .
$query
2019-10-02 08:05:48 +00:00
);
assert(!is_bool($stmt));
// $stmt = $this->db->prepare($query);
if (!array_key_exists(0, $args)) {
foreach ($args as $name=>$value) {
if (is_int($value)) {
$stmt->bindValue(':'.$name, $value, PDO::PARAM_INT);
} else {
$stmt->bindValue(':'.$name, $value, PDO::PARAM_STR);
}
}
$stmt->execute();
} else {
$stmt->execute($args);
}
return $stmt;
} catch (PDOException $pdoe) {
throw new SCoreException($pdoe->getMessage(), $query);
2019-09-29 13:30:55 +00:00
}
}
/**
* Execute an SQL query and return a 2D array.
*/
public function get_all(string $query, array $args=[], bool $scoreql = false): array
{
2019-11-02 19:57:34 +00:00
if ($scoreql===true) {
$query = $this->scoreql_to_sql($query);
}
$_start = microtime(true);
$data = $this->execute($query, $args)->fetchAll();
$this->count_time("get_all", $_start, $query, $args);
return $data;
}
/**
* Execute an SQL query and return a iterable object for use with generators.
*/
public function get_all_iterable(string $query, array $args=[], bool $scoreql = false): PDOStatement
{
2019-11-02 19:57:34 +00:00
if ($scoreql===true) {
$query = $this->scoreql_to_sql($query);
}
$_start = microtime(true);
$data = $this->execute($query, $args);
$this->count_time("get_all_iterable", $_start, $query, $args);
return $data;
}
/**
* Execute an SQL query and return a single row.
*/
public function get_row(string $query, array $args=[], bool $scoreql = false): ?array
{
2019-11-02 19:57:34 +00:00
if ($scoreql===true) {
$query = $this->scoreql_to_sql($query);
}
$_start = microtime(true);
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_row", $_start, $query, $args);
return $row ? $row : null;
}
/**
* Execute an SQL query and return a boolean based on whether it returns a result
*/
public function exists(string $query, array $args=[], bool $scoreql = false): bool
{
2019-11-02 19:57:34 +00:00
if ($scoreql===true) {
$query = $this->scoreql_to_sql($query);
}
$_start = microtime(true);
$result = $this->execute($query, $args);
$this->count_time("exists", $_start, $query, $args);
return $result->rowCount()>0;
}
/**
* Execute an SQL query and return the first column of each row.
*/
public function get_col(string $query, array $args=[], bool $scoreql = false): array
{
2019-11-02 19:57:34 +00:00
if ($scoreql===true) {
$query = $this->scoreql_to_sql($query);
}
$_start = microtime(true);
2019-07-07 14:42:19 +00:00
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_COLUMN);
$this->count_time("get_col", $_start, $query, $args);
return $res;
}
/**
* Execute an SQL query and return the first column of each row as a single iterable object.
*/
public function get_col_iterable(string $query, array $args=[], bool $scoreql = false): Generator
{
2019-11-02 19:57:34 +00:00
if ($scoreql===true) {
$query = $this->scoreql_to_sql($query);
}
$_start = microtime(true);
$stmt = $this->execute($query, $args);
$this->count_time("get_col_iterable", $_start, $query, $args);
foreach ($stmt as $row) {
yield $row[0];
}
}
/**
* Execute an SQL query and return the the first column => the second column.
*/
public function get_pairs(string $query, array $args=[], bool $scoreql = false): array
{
2019-11-02 19:57:34 +00:00
if ($scoreql===true) {
$query = $this->scoreql_to_sql($query);
}
$_start = microtime(true);
2019-07-07 14:42:19 +00:00
$res = $this->execute($query, $args)->fetchAll(PDO::FETCH_KEY_PAIR);
$this->count_time("get_pairs", $_start, $query, $args);
return $res;
}
/**
* Execute an SQL query and return a single value.
*/
public function get_one(string $query, array $args=[], bool $scoreql = false)
{
2019-11-02 19:57:34 +00:00
if ($scoreql===true) {
$query = $this->scoreql_to_sql($query);
}
$_start = microtime(true);
$row = $this->execute($query, $args)->fetch();
$this->count_time("get_one", $_start, $query, $args);
return $row[0];
}
/**
* Get the ID of the last inserted row.
*/
public function get_last_insert_id(string $seq): int
{
if ($this->engine->name == DatabaseDriver::PGSQL) {
2019-10-02 08:03:14 +00:00
$id = $this->db->lastInsertId($seq);
} else {
2019-10-02 08:03:14 +00:00
$id = $this->db->lastInsertId();
}
2019-10-02 08:05:48 +00:00
assert(is_numeric($id));
return (int)$id;
}
/**
* Create a table from pseudo-SQL.
*/
public function create_table(string $name, string $data): void
{
if (is_null($this->engine)) {
$this->connect_engine();
}
$data = trim($data, ", \t\n\r\0\x0B"); // mysql doesn't like trailing commas
$this->execute($this->engine->create_table_sql($name, $data));
}
/**
* Returns the number of tables present in the current database.
*
* @throws SCoreException
*/
public function count_tables(): int
{
if (is_null($this->db) || is_null($this->engine)) {
$this->connect_db();
}
if ($this->engine->name === DatabaseDriver::MYSQL) {
return count(
$this->get_all("SHOW TABLES")
);
} elseif ($this->engine->name === DatabaseDriver::PGSQL) {
return count(
$this->get_all("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
);
} elseif ($this->engine->name === DatabaseDriver::SQLITE) {
return count(
$this->get_all("SELECT name FROM sqlite_master WHERE type = 'table'")
);
} else {
throw new SCoreException("Can't count tables for database type {$this->engine->name}");
}
}
}
class MockDatabase extends Database
{
/** @var int */
private $query_id = 0;
/** @var array */
private $responses = [];
public function __construct(array $responses = [])
{
$this->responses = $responses;
}
public function execute(string $query, array $params=[], bool $scoreql = false): PDOStatement
{
log_debug(
"mock-database",
"QUERY: " . $query .
"\nARGS: " . var_export($params, true) .
"\nRETURN: " . var_export($this->responses[$this->query_id], true)
);
return $this->responses[$this->query_id++];
}
2019-05-28 18:00:23 +00:00
public function _execute(string $query, array $params=[])
{
log_debug(
"mock-database",
"QUERY: " . $query .
"\nARGS: " . var_export($params, true) .
"\nRETURN: " . var_export($this->responses[$this->query_id], true)
);
return $this->responses[$this->query_id++];
}
public function get_all(string $query, array $args=[], bool $scoreql = false): array
{
return $this->_execute($query, $args);
}
public function get_row(string $query, array $args=[], bool $scoreql = false): ?array
{
return $this->_execute($query, $args);
}
public function get_col(string $query, array $args=[], bool $scoreql = false): array
{
return $this->_execute($query, $args);
}
public function get_pairs(string $query, array $args=[], bool $scoreql = false): array
{
return $this->_execute($query, $args);
}
public function get_one(string $query, array $args=[], bool $scoreql = false)
{
return $this->_execute($query, $args);
}
public function get_last_insert_id(string $seq): int
{
return $this->query_id;
}
public function scoreql_to_sql(string $sql): string
{
return $sql;
}
2019-05-28 18:00:23 +00:00
public function create_table(string $name, string $def): void
{
}
2019-05-28 18:00:23 +00:00
2019-05-29 17:23:29 +00:00
public function connect_engine(): void
{
}
}