1
0
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:
2026-02-14 12:04:42 +01:00
parent b0ee3081a6
commit a71b8c9717
9 changed files with 286 additions and 55 deletions

View File

@@ -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]);