setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->exec($sql); $pdo->exec("PRAGMA user_version = " . QDB_VERSION . ";"); $pdo = null; } $pdo = new PDO("sqlite:$tmpDb"); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->exec("PRAGMA foreign_keys = ON;"); // Schema upgrade: apply CREATE TABLE IF NOT EXISTS for any missing tables $currentVersion = (int)$pdo->query("PRAGMA user_version")->fetchColumn(); if ($currentVersion < QDB_VERSION) { $pdo->exec("PRAGMA foreign_keys = OFF;"); $pdo->exec($sql); if ($currentVersion < 4) { $cols = $pdo->query("PRAGMA table_info(questionnaire)")->fetchAll(PDO::FETCH_ASSOC); $hasCategory = false; foreach ($cols as $col) { if (($col['name'] ?? '') === 'categoryKey') { $hasCategory = true; break; } } if (!$hasCategory) { $pdo->exec("ALTER TABLE questionnaire ADD COLUMN categoryKey TEXT NOT NULL DEFAULT ''"); } } $pdo->exec("PRAGMA user_version = " . QDB_VERSION . ";"); $pdo->exec("PRAGMA foreign_keys = ON;"); } return [$pdo, $tmpDb, $lockFp]; } /** * Encrypt the temp DB and atomically write it back to the master path. * Releases the lock. */ function qdb_save(string $tmpDb, $lockFp): void { $masterKey = get_master_key_bytes(); $plainDb = file_get_contents($tmpDb); if ($plainDb === false) throw new Exception("Could not read temp DB for save"); $enc = aes256_cbc_encrypt_bytes($plainDb, $masterKey); $dbPath = QDB_PATH; $tmpEncrypted = tempnam(dirname($dbPath), 'enc_qdb_'); if (file_put_contents($tmpEncrypted, $enc) === false) { throw new Exception("Could not write encrypted DB"); } if (!@rename($tmpEncrypted, $dbPath)) { if (!@copy($tmpEncrypted, $dbPath) || !@unlink($tmpEncrypted)) { throw new Exception("Could not save encrypted DB (rename/copy failed)"); } } @chmod($dbPath, 0644); @unlink($tmpDb); if ($lockFp) { flock($lockFp, LOCK_UN); fclose($lockFp); } } /** * Discard changes -- clean up temp file and release lock without saving. */ function qdb_discard(string $tmpDb, $lockFp): void { @unlink($tmpDb); if ($lockFp) { @flock($lockFp, LOCK_UN); @fclose($lockFp); } }