implemented incremental schema migration for questionnaire, added categoryKey column if missing, and improved error handling in questionnaire retrieval
This commit is contained in:
80
db_init.php
80
db_init.php
@ -8,6 +8,54 @@ define('QDB_LOCK', __DIR__ . '/uploads/.qdb_lock');
|
|||||||
define('QDB_SCHEMA', __DIR__ . '/schema.sql');
|
define('QDB_SCHEMA', __DIR__ . '/schema.sql');
|
||||||
define('QDB_VERSION', 4);
|
define('QDB_VERSION', 4);
|
||||||
|
|
||||||
|
function qdb_column_exists(PDO $pdo, string $table, string $column): bool {
|
||||||
|
$stmt = $pdo->query('PRAGMA table_info(' . $table . ')');
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $col) {
|
||||||
|
if (($col['name'] ?? '') === $column) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply incremental schema fixes. Returns true if the temp DB was modified.
|
||||||
|
*/
|
||||||
|
function qdb_apply_migrations(PDO $pdo, string $schemaSql): bool {
|
||||||
|
$changed = false;
|
||||||
|
|
||||||
|
if (!qdb_column_exists($pdo, 'questionnaire', 'categoryKey')) {
|
||||||
|
$pdo->exec("ALTER TABLE questionnaire ADD COLUMN categoryKey TEXT NOT NULL DEFAULT ''");
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentVersion = (int)$pdo->query('PRAGMA user_version')->fetchColumn();
|
||||||
|
if ($currentVersion < QDB_VERSION) {
|
||||||
|
$pdo->exec('PRAGMA foreign_keys = OFF;');
|
||||||
|
$pdo->exec($schemaSql);
|
||||||
|
if (!qdb_column_exists($pdo, 'questionnaire', 'categoryKey')) {
|
||||||
|
$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;');
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function qdb_acquire_lock() {
|
||||||
|
$lockFp = fopen(QDB_LOCK, 'c');
|
||||||
|
if ($lockFp === false) {
|
||||||
|
throw new Exception('Could not open lock file');
|
||||||
|
}
|
||||||
|
if (!flock($lockFp, LOCK_EX)) {
|
||||||
|
fclose($lockFp);
|
||||||
|
throw new Exception('Could not acquire lock');
|
||||||
|
}
|
||||||
|
return $lockFp;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt the master DB file into a temp SQLite file, or create a fresh one
|
* Decrypt the master DB file into a temp SQLite file, or create a fresh one
|
||||||
* from schema.sql if no DB exists yet.
|
* from schema.sql if no DB exists yet.
|
||||||
@ -67,26 +115,20 @@ function qdb_open(bool $writable = false): array {
|
|||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
$pdo->exec("PRAGMA foreign_keys = ON;");
|
$pdo->exec("PRAGMA foreign_keys = ON;");
|
||||||
|
|
||||||
// Schema upgrade: apply CREATE TABLE IF NOT EXISTS for any missing tables
|
$schemaChanged = qdb_apply_migrations($pdo, $sql);
|
||||||
$currentVersion = (int)$pdo->query("PRAGMA user_version")->fetchColumn();
|
|
||||||
if ($currentVersion < QDB_VERSION) {
|
// Read-only opens discard the temp file; persist one-off migrations to the master DB.
|
||||||
$pdo->exec("PRAGMA foreign_keys = OFF;");
|
if ($schemaChanged && !$writable) {
|
||||||
$pdo->exec($sql);
|
$migrateLock = qdb_acquire_lock();
|
||||||
if ($currentVersion < 4) {
|
try {
|
||||||
$cols = $pdo->query("PRAGMA table_info(questionnaire)")->fetchAll(PDO::FETCH_ASSOC);
|
qdb_save($tmpDb, $migrateLock);
|
||||||
$hasCategory = false;
|
} catch (Throwable $e) {
|
||||||
foreach ($cols as $col) {
|
@unlink($tmpDb);
|
||||||
if (($col['name'] ?? '') === 'categoryKey') {
|
flock($migrateLock, LOCK_UN);
|
||||||
$hasCategory = true;
|
fclose($migrateLock);
|
||||||
break;
|
throw $e;
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$hasCategory) {
|
|
||||||
$pdo->exec("ALTER TABLE questionnaire ADD COLUMN categoryKey TEXT NOT NULL DEFAULT ''");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$pdo->exec("PRAGMA user_version = " . QDB_VERSION . ";");
|
return qdb_open(false);
|
||||||
$pdo->exec("PRAGMA foreign_keys = ON;");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$pdo, $tmpDb, $lockFp];
|
return [$pdo, $tmpDb, $lockFp];
|
||||||
|
|||||||
@ -6,23 +6,29 @@ switch ($method) {
|
|||||||
|
|
||||||
case 'GET':
|
case 'GET':
|
||||||
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||||
$rows = $pdo->query("
|
try {
|
||||||
SELECT q.questionnaireID, q.name, q.version, q.state,
|
$rows = $pdo->query("
|
||||||
q.orderIndex, q.showPoints, q.conditionJson, q.categoryKey,
|
SELECT q.questionnaireID, q.name, q.version, q.state,
|
||||||
COUNT(qu.questionID) AS questionCount
|
q.orderIndex, q.showPoints, q.conditionJson, q.categoryKey,
|
||||||
FROM questionnaire q
|
COUNT(qu.questionID) AS questionCount
|
||||||
LEFT JOIN question qu ON qu.questionnaireID = q.questionnaireID
|
FROM questionnaire q
|
||||||
GROUP BY q.questionnaireID
|
LEFT JOIN question qu ON qu.questionnaireID = q.questionnaireID
|
||||||
ORDER BY q.orderIndex, q.name
|
GROUP BY q.questionnaireID
|
||||||
")->fetchAll(PDO::FETCH_ASSOC);
|
ORDER BY q.orderIndex, q.name
|
||||||
foreach ($rows as &$r) {
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$r['orderIndex'] = (int)$r['orderIndex'];
|
foreach ($rows as &$r) {
|
||||||
$r['showPoints'] = (int)$r['showPoints'];
|
$r['orderIndex'] = (int)$r['orderIndex'];
|
||||||
$r['conditionJson'] = $r['conditionJson'] ?: '{}';
|
$r['showPoints'] = (int)$r['showPoints'];
|
||||||
|
$r['conditionJson'] = $r['conditionJson'] ?: '{}';
|
||||||
|
}
|
||||||
|
unset($r);
|
||||||
|
qdb_discard($tmpDb, $lockFp);
|
||||||
|
json_success(['questionnaires' => $rows]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
qdb_discard($tmpDb, $lockFp);
|
||||||
|
error_log('questionnaires GET: ' . $e->getMessage());
|
||||||
|
json_error('SERVER_ERROR', 'Server error', 500);
|
||||||
}
|
}
|
||||||
unset($r);
|
|
||||||
qdb_discard($tmpDb, $lockFp);
|
|
||||||
json_success(['questionnaires' => $rows]);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'POST':
|
case 'POST':
|
||||||
|
|||||||
Reference in New Issue
Block a user