forked from Public/pics
Add SQLite support as alternative database backend
Support config-driven choice between MySQL and SQLite via DB_DRIVER constant, defaulting to MySQL for backward compatibility. All SQL adaptation lives in Database.php (UDFs + query rewriting), so model files need no changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> SQLite: remove FK constraints, revert 0→null sentinel changes The SQLite schema had FOREIGN KEY constraints that don't exist in the MySQL schema. These forced a cascade of 0→null changes to satisfy FK enforcement. Removing them keeps the two backends behaviorally consistent and minimises the diff. Real SQLite compat fixes (UDFs, query rewriting, rowCount→count, Router fixes, EditAlbum guard) are preserved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,18 +9,35 @@
|
||||
class Database
|
||||
{
|
||||
private $connection;
|
||||
private $driver;
|
||||
private $query_count = 0;
|
||||
private $logged_queries = [];
|
||||
|
||||
public function __construct($host, $user, $password, $name)
|
||||
public function __construct($driver, array $options)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
|
||||
try
|
||||
{
|
||||
$this->connection = new PDO("mysql:host=$host;dbname=$name;charset=utf8mb4", $user, $password, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
if ($driver === 'sqlite')
|
||||
{
|
||||
$this->connection = new PDO("sqlite:" . $options['file'], null, null, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
$this->connection->exec('PRAGMA journal_mode=WAL');
|
||||
$this->registerSQLiteFunctions();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->connection = new PDO(
|
||||
"mysql:host={$options['host']};dbname={$options['name']};charset=utf8mb4",
|
||||
$options['user'], $options['password'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
// Give up if we have a connection error.
|
||||
catch (PDOException $e)
|
||||
@@ -31,6 +48,72 @@ class Database
|
||||
}
|
||||
}
|
||||
|
||||
private function registerSQLiteFunctions()
|
||||
{
|
||||
$pdo = $this->connection;
|
||||
|
||||
$pdo->sqliteCreateFunction('CONCAT', function () {
|
||||
return implode('', func_get_args());
|
||||
}, -1);
|
||||
|
||||
$pdo->sqliteCreateFunction('IF', function ($cond, $t, $f) {
|
||||
return $cond ? $t : $f;
|
||||
}, 3);
|
||||
|
||||
$pdo->sqliteCreateFunction('FROM_UNIXTIME', function ($ts) {
|
||||
return date('Y-m-d H:i:s', $ts);
|
||||
}, 1);
|
||||
|
||||
$pdo->sqliteCreateFunction('UNIX_TIMESTAMP', function () {
|
||||
return time();
|
||||
}, 0);
|
||||
|
||||
$pdo->sqliteCreateFunction('CURRENT_TIMESTAMP', function () {
|
||||
return date('Y-m-d H:i:s');
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private function rewriteForSQLite($sql)
|
||||
{
|
||||
// REPLACE INTO → INSERT OR REPLACE INTO
|
||||
$sql = preg_replace('/\bREPLACE\s+INTO\b/i', 'INSERT OR REPLACE INTO', $sql);
|
||||
|
||||
// ON DUPLICATE KEY UPDATE → ON CONFLICT(slug) DO UPDATE SET
|
||||
// When present, strip INSERT IGNORE down to INSERT (OR IGNORE conflicts with ON CONFLICT).
|
||||
if (preg_match('/\bON\s+DUPLICATE\s+KEY\s+UPDATE\b/i', $sql))
|
||||
{
|
||||
$sql = preg_replace('/\bINSERT\s+IGNORE\s+INTO\b/i', 'INSERT INTO', $sql);
|
||||
$sql = preg_replace(
|
||||
'/\bON\s+DUPLICATE\s+KEY\s+UPDATE\s+(.+)/i',
|
||||
'ON CONFLICT(slug) DO UPDATE SET $1',
|
||||
$sql
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// INSERT IGNORE INTO → INSERT OR IGNORE INTO
|
||||
$sql = preg_replace('/\bINSERT\s+IGNORE\s+INTO\b/i', 'INSERT OR IGNORE INTO', $sql);
|
||||
}
|
||||
|
||||
// UPDATE table AS alias SET ... → UPDATE table SET ... (with alias replaced by table name)
|
||||
if (preg_match('/\bUPDATE\s+(\w+)\s+AS\s+(\w+)\s+SET\b/i', $sql, $m))
|
||||
{
|
||||
$table = $m[1];
|
||||
$alias = $m[2];
|
||||
$sql = preg_replace('/\bUPDATE\s+\w+\s+AS\s+\w+\s+SET\b/i', "UPDATE $table SET", $sql);
|
||||
$sql = preg_replace('/\b' . preg_quote($alias, '/') . '\.(\w+)\b/', "$table.$1", $sql);
|
||||
}
|
||||
|
||||
// LIMIT :offset, :limit → LIMIT :limit OFFSET :offset
|
||||
$sql = preg_replace(
|
||||
'/\bLIMIT\s+:offset\s*,\s*:limit\b/i',
|
||||
'LIMIT :limit OFFSET :offset',
|
||||
$sql
|
||||
);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public function getQueryCount()
|
||||
{
|
||||
return $this->query_count;
|
||||
@@ -180,6 +263,10 @@ class Database
|
||||
// Preprocessing/checks: prepare any arrays for binding
|
||||
$db_string = $this->expandPlaceholders($db_string, $db_values);
|
||||
|
||||
// SQLite query rewriting
|
||||
if ($this->driver === 'sqlite')
|
||||
$db_string = $this->rewriteForSQLite($db_string);
|
||||
|
||||
// Prepare query for execution
|
||||
$statement = $this->connection->prepare($db_string);
|
||||
|
||||
@@ -223,13 +310,10 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->rowCount($res) === 0)
|
||||
return null;
|
||||
|
||||
$object = $this->fetchObject($res, $class);
|
||||
$this->free($res);
|
||||
|
||||
return $object;
|
||||
return $object ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,9 +323,6 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($object = $this->fetchObject($res, $class))
|
||||
$rows[] = $object;
|
||||
@@ -258,13 +339,10 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$row = $this->fetchNum($res);
|
||||
$this->free($res);
|
||||
|
||||
return $row;
|
||||
return $row ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,9 +352,6 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchNum($res))
|
||||
$rows[] = $row;
|
||||
@@ -293,9 +368,6 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchNum($res))
|
||||
$rows[$row[0]] = $row[1];
|
||||
@@ -312,9 +384,6 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchAssoc($res))
|
||||
{
|
||||
@@ -334,13 +403,10 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$row = $this->fetchAssoc($res);
|
||||
$this->free($res);
|
||||
|
||||
return $row;
|
||||
return $row ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,9 +416,6 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchAssoc($res))
|
||||
$rows[] = $row;
|
||||
@@ -369,14 +432,13 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
// If this happens, you're doing it wrong.
|
||||
if ($this->rowCount($res) === 0)
|
||||
return null;
|
||||
|
||||
list($value) = $this->fetchNum($res);
|
||||
$row = $this->fetchNum($res);
|
||||
$this->free($res);
|
||||
|
||||
return $value;
|
||||
if (!$row)
|
||||
return null;
|
||||
|
||||
return $row[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -386,9 +448,6 @@ class Database
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if ($this->rowCount($res) === 0)
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
while ($row = $this->fetchNum($res))
|
||||
$rows[] = $row[0];
|
||||
@@ -412,7 +471,10 @@ class Database
|
||||
$data = [$data];
|
||||
|
||||
// Determine the method of insertion.
|
||||
$method = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
|
||||
if ($this->driver === 'sqlite')
|
||||
$method = $method == 'replace' ? 'INSERT OR REPLACE' : ($method == 'ignore' ? 'INSERT OR IGNORE' : 'INSERT');
|
||||
else
|
||||
$method = $method == 'replace' ? 'REPLACE' : ($method == 'ignore' ? 'INSERT IGNORE' : 'INSERT');
|
||||
|
||||
// What columns are we inserting?
|
||||
$columns = array_keys($data[0]);
|
||||
|
||||
Reference in New Issue
Block a user