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);
}
}