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); require_once __DIR__ . '/lib/migrate_v4.php'; if ($currentVersion < 4) { qdb_migrate_v4($pdo); } $pdo->exec("PRAGMA user_version = " . QDB_VERSION . ";"); $pdo->exec("PRAGMA foreign_keys = ON;"); // Persist upgrade even when opened read-only (otherwise migration is lost on discard) if (!$writable) { qdb_save($tmpDb, $lockFp); return qdb_open(false); } } 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); } }