feat: switch from MySQL to SQLite for zero-dependency deployment

- Rewrite db.php to use SQLite via PDO (no external DB server needed)
- Rewrite schema.sql for SQLite (AUTOINCREMENT, CHECK constraints,
  triggers for updated_at, ON CONFLICT instead of ON DUPLICATE KEY)
- Fix config.php upsert to use ON CONFLICT syntax
- Fix index.php: tag filter via json_each, vote upsert via ON CONFLICT,
  and fix protocol create route (was returning 405 for POST /api/protocols)
- Fix yaml.php seed import: replace invalid false callbacks with fn() => null,
  fix yaml_parse pos argument (-1 -> 0)
- Update Dockerfile: drop pdo_mysql/mysqli (pdo_sqlite is built in)
- Update CloudronManifest.json: remove mysql addon, bump to v0.3.0
- Update README for SQLite deployment
This commit is contained in:
Protocolbot
2026-07-06 14:42:45 -06:00
parent 7379d0c540
commit 3386ac6542
8 changed files with 155 additions and 132 deletions
Executable → Regular
+22 -24
View File
@@ -1,25 +1,35 @@
<?php
/**
* Database connection and helpers.
* Uses MySQL/MariaDB via PDO (Cloudron-compatible).
* Uses SQLite via PDO — no external database server required.
*
* The database file path is configurable via the DATABASE_PATH env var.
* Defaults to data/protocol_droid.db relative to the project root.
*/
function db_path(): string {
$configured = getenv('DATABASE_PATH');
if ($configured) return $configured;
return __DIR__ . '/../data/protocol_droid.db';
}
function db(): PDO {
static $pdo = null;
if ($pdo === null) {
$host = getenv('MYSQL_HOST') ?: 'localhost';
$port = getenv('MYSQL_PORT') ?: '3306';
$name = getenv('MYSQL_DATABASE') ?: 'protocol_droid';
$user = getenv('MYSQL_USER') ?: 'root';
$pass = getenv('MYSQL_PASSWORD') ?: '';
$dsn = "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4";
$path = db_path();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
$dsn = "sqlite:{$path}";
try {
$pdo = new PDO($dsn, $user, $pass, [
$pdo = new PDO($dsn, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
// Enable foreign key enforcement (off by default in SQLite)
$pdo->exec('PRAGMA foreign_keys = ON');
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['error' => 'Database connection failed']);
@@ -32,19 +42,7 @@ function db(): PDO {
function db_init(): void {
$sql = file_get_contents(__DIR__ . '/../schema.sql');
if (!$sql) return;
// Split on semicolons at end of statements (simple split — schema has no complex strings with semicolons)
$statements = array_filter(array_map('trim', preg_split('/;\s*(?=\n|$)/', $sql)));
foreach ($statements as $stmt) {
if (empty($stmt)) continue;
try {
db()->exec($stmt);
} catch (PDOException $e) {
// Ignore "already exists" errors
if (strpos($e->getMessage(), 'already exists') === false) {
error_log("Schema init: " . $e->getMessage());
}
}
}
db()->exec($sql);
}
function db_query(string $sql, array $params = []): PDOStatement {
@@ -75,8 +73,8 @@ function db_insert(string $table, array $data): int {
function db_update(string $table, array $data, string $where, array $whereParams): int {
$set = implode(', ', array_map(fn($c) => "`$c` = ?", array_keys($data)));
$sql = "UPDATE {$table} SET {$set} WHERE {$where}";
db_query($sql, array_merge(array_values($data), $whereParams));
return db_query("SELECT ROW_COUNT()")->fetchColumn();
$stmt = db_query($sql, array_merge(array_values($data), $whereParams));
return $stmt->rowCount();
}
function slugify(string $text): string {