This commit is contained in:
@ -255,11 +255,15 @@ function qdb_analytics_set_followup_note(PDO $pdo, array $tokenRec, string $clie
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO client_followup_note (clientCode, note, updatedByUserID, updatedAt)
|
||||
VALUES (:cc, :n, :uid, :ts)
|
||||
ON CONFLICT(clientCode) DO UPDATE SET
|
||||
note = excluded.note,
|
||||
updatedByUserID = excluded.updatedByUserID,
|
||||
updatedAt = excluded.updatedAt'
|
||||
VALUES (:cc, :n, :uid, :ts) '
|
||||
. qdb_upsert_update(
|
||||
['clientCode'],
|
||||
[
|
||||
'note' => 'excluded.note',
|
||||
'updatedByUserID' => 'excluded.updatedByUserID',
|
||||
'updatedAt' => 'excluded.updatedAt',
|
||||
]
|
||||
)
|
||||
)->execute([
|
||||
':cc' => $clientCode,
|
||||
':n' => $note,
|
||||
|
||||
@ -575,12 +575,16 @@ function qdb_dev_persist_submission(
|
||||
$answeredAt = $completedAt;
|
||||
$answerInsert = $pdo->prepare(
|
||||
'INSERT INTO client_answer (clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
|
||||
VALUES (:cc, :qid, :aoid, :ftv, :nv, :at)
|
||||
ON CONFLICT(clientCode, questionID) DO UPDATE SET
|
||||
answerOptionID = excluded.answerOptionID,
|
||||
freeTextValue = excluded.freeTextValue,
|
||||
numericValue = excluded.numericValue,
|
||||
answeredAt = excluded.answeredAt'
|
||||
VALUES (:cc, :qid, :aoid, :ftv, :nv, :at) '
|
||||
. qdb_upsert_update(
|
||||
['clientCode', 'questionID'],
|
||||
[
|
||||
'answerOptionID' => 'excluded.answerOptionID',
|
||||
'freeTextValue' => 'excluded.freeTextValue',
|
||||
'numericValue' => 'excluded.numericValue',
|
||||
'answeredAt' => 'excluded.answeredAt',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($answers as $a) {
|
||||
@ -645,13 +649,17 @@ function qdb_dev_persist_submission(
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO completed_questionnaire (clientCode, questionnaireID, assignedByCoach, status, startedAt, completedAt, sumPoints)
|
||||
VALUES (:cc, :qn, :abc, \'completed\', :sa, :ca, :sp)
|
||||
ON CONFLICT(clientCode, questionnaireID) DO UPDATE SET
|
||||
assignedByCoach = excluded.assignedByCoach,
|
||||
status = \'completed\',
|
||||
startedAt = COALESCE(excluded.startedAt, startedAt),
|
||||
completedAt = excluded.completedAt,
|
||||
sumPoints = excluded.sumPoints'
|
||||
VALUES (:cc, :qn, :abc, \'completed\', :sa, :ca, :sp) '
|
||||
. qdb_upsert_update(
|
||||
['clientCode', 'questionnaireID'],
|
||||
[
|
||||
'assignedByCoach' => 'excluded.assignedByCoach',
|
||||
'status' => "'completed'",
|
||||
'startedAt' => 'COALESCE(excluded.startedAt, startedAt)',
|
||||
'completedAt' => 'excluded.completedAt',
|
||||
'sumPoints' => 'excluded.sumPoints',
|
||||
]
|
||||
)
|
||||
)->execute([
|
||||
':cc' => $clientCode,
|
||||
':qn' => $qnID,
|
||||
@ -863,7 +871,7 @@ function qdb_wipe_all_data_except_admins(PDO $pdo): array
|
||||
try {
|
||||
// Interview data references clients/coaches; coach references supervisor.
|
||||
// Disable FK checks for this bulk wipe (same pattern as questionnaire delete).
|
||||
$pdo->exec('PRAGMA foreign_keys = OFF');
|
||||
qdb_set_foreign_keys($pdo, false);
|
||||
|
||||
$deleted['client_answers'] = (int)$pdo->exec('DELETE FROM client_answer');
|
||||
if (qdb_table_exists($pdo, 'client_answer_submission')) {
|
||||
@ -891,14 +899,14 @@ function qdb_wipe_all_data_except_admins(PDO $pdo): array
|
||||
$users->execute();
|
||||
$deleted['users'] = $users->rowCount();
|
||||
|
||||
$pdo->exec('PRAGMA foreign_keys = ON');
|
||||
qdb_set_foreign_keys($pdo, true);
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
try {
|
||||
$pdo->exec('PRAGMA foreign_keys = ON');
|
||||
qdb_set_foreign_keys($pdo, true);
|
||||
} catch (Throwable $ignored) {
|
||||
}
|
||||
throw $e;
|
||||
|
||||
@ -104,10 +104,14 @@ function qdb_save_structure_snapshot(PDO $pdo, string $qnID, int $revision, arra
|
||||
}
|
||||
$pdo->prepare(
|
||||
'INSERT INTO questionnaire_structure_snapshot (questionnaireID, structureRevision, createdAt, manifestJson)
|
||||
VALUES (:qn, :rev, :ts, :mj)
|
||||
ON CONFLICT(questionnaireID, structureRevision) DO UPDATE SET
|
||||
manifestJson = excluded.manifestJson,
|
||||
createdAt = excluded.createdAt'
|
||||
VALUES (:qn, :rev, :ts, :mj) '
|
||||
. qdb_upsert_update(
|
||||
['questionnaireID', 'structureRevision'],
|
||||
[
|
||||
'manifestJson' => 'excluded.manifestJson',
|
||||
'createdAt' => 'excluded.createdAt',
|
||||
]
|
||||
)
|
||||
)->execute([
|
||||
':qn' => $qnID,
|
||||
':rev' => $revision,
|
||||
@ -168,10 +172,7 @@ function qdb_option_has_client_data(PDO $pdo, string $answerOptionID): bool {
|
||||
if ($stmt->fetchColumn()) {
|
||||
return true;
|
||||
}
|
||||
$chk = $pdo->query(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'client_answer_submission' LIMIT 1"
|
||||
);
|
||||
if (!$chk || !$chk->fetchColumn()) {
|
||||
if (!qdb_table_exists($pdo, 'client_answer_submission')) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
@ -187,10 +188,7 @@ function qdb_question_has_client_data(PDO $pdo, string $questionID): bool {
|
||||
if ($stmt->fetchColumn()) {
|
||||
return true;
|
||||
}
|
||||
$chk = $pdo->query(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'client_answer_submission' LIMIT 1"
|
||||
);
|
||||
if (!$chk || !$chk->fetchColumn()) {
|
||||
if (!qdb_table_exists($pdo, 'client_answer_submission')) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
|
||||
@ -12,6 +12,10 @@ define('QDB_READ_GENERATION_FILE', QDB_UPLOADS_DIR . '/.qdb_read_generation');
|
||||
* @return array{0: PDO, 1: string, 2: null}
|
||||
*/
|
||||
function qdb_read_db_open(): array {
|
||||
if (qdb_is_mysql()) {
|
||||
return qdb_mysql_open();
|
||||
}
|
||||
|
||||
$signature = qdb_read_cache_signature();
|
||||
$cached = qdb_read_cache_get($signature);
|
||||
if ($cached !== null) {
|
||||
|
||||
@ -383,12 +383,16 @@ function qdb_recompute_profile_scores_for_client(PDO $pdo, string $clientCode, ?
|
||||
$upsert = $pdo->prepare(
|
||||
'INSERT INTO client_scoring_profile_result
|
||||
(clientCode, profileID, weightedTotal, band, computedAt, questionnaireSnapshot)
|
||||
VALUES (:cc, :pid, :wt, :band, :at, :snap)
|
||||
ON CONFLICT(clientCode, profileID) DO UPDATE SET
|
||||
weightedTotal = excluded.weightedTotal,
|
||||
band = excluded.band,
|
||||
computedAt = excluded.computedAt,
|
||||
questionnaireSnapshot = excluded.questionnaireSnapshot'
|
||||
VALUES (:cc, :pid, :wt, :band, :at, :snap) '
|
||||
. qdb_upsert_update(
|
||||
['clientCode', 'profileID'],
|
||||
[
|
||||
'weightedTotal' => 'excluded.weightedTotal',
|
||||
'band' => 'excluded.band',
|
||||
'computedAt' => 'excluded.computedAt',
|
||||
'questionnaireSnapshot' => 'excluded.questionnaireSnapshot',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($profiles as $profile) {
|
||||
|
||||
@ -97,8 +97,8 @@ function qdb_settings_save_on_pdo(PDO $pdo, array $settings): void
|
||||
qdb_ensure_system_setting_table($pdo);
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO system_setting (settingKey, settingValue)
|
||||
VALUES (:k, :v)
|
||||
ON CONFLICT(settingKey) DO UPDATE SET settingValue = excluded.settingValue'
|
||||
VALUES (:k, :v) '
|
||||
. qdb_upsert_update(['settingKey'], ['settingValue' => 'excluded.settingValue'])
|
||||
);
|
||||
foreach (qdb_settings_keys() as $key) {
|
||||
$stmt->execute([':k' => $key, ':v' => (string)(int)($settings[$key] ?? 0)]);
|
||||
|
||||
226
lib/sql_dialect.php
Normal file
226
lib/sql_dialect.php
Normal file
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
/** Database driver helpers (SQLite legacy + MySQL). */
|
||||
|
||||
function qdb_driver(): string {
|
||||
if (isset($GLOBALS['qdb_driver_cache'])) {
|
||||
return (string)$GLOBALS['qdb_driver_cache'];
|
||||
}
|
||||
$GLOBALS['qdb_driver_cache'] = qdb_driver_resolve();
|
||||
return (string)$GLOBALS['qdb_driver_cache'];
|
||||
}
|
||||
|
||||
function qdb_reset_driver_cache(): void {
|
||||
unset($GLOBALS['qdb_driver_cache']);
|
||||
}
|
||||
|
||||
function qdb_driver_resolve(): string {
|
||||
$configured = strtolower(trim((string)(qdb_env_get('QDB_DRIVER') ?? '')));
|
||||
if ($configured === 'mysql' || $configured === 'sqlite') {
|
||||
$driver = $configured;
|
||||
return $driver;
|
||||
}
|
||||
if (qdb_env_get('QDB_MYSQL_HOST') || qdb_env_get('QDB_MYSQL_DATABASE')) {
|
||||
$driver = 'mysql';
|
||||
return $driver;
|
||||
}
|
||||
$driver = 'sqlite';
|
||||
return $driver;
|
||||
}
|
||||
|
||||
function qdb_is_mysql(): bool {
|
||||
return qdb_driver() === 'mysql';
|
||||
}
|
||||
|
||||
function qdb_is_sqlite(): bool {
|
||||
return qdb_driver() === 'sqlite';
|
||||
}
|
||||
|
||||
function qdb_schema_file(): string {
|
||||
return qdb_is_mysql()
|
||||
? __DIR__ . '/../schema.mysql.sql'
|
||||
: QDB_SCHEMA;
|
||||
}
|
||||
|
||||
function qdb_mysql_dsn(): string {
|
||||
$host = qdb_env_get('QDB_MYSQL_HOST') ?: '127.0.0.1';
|
||||
$port = qdb_env_get('QDB_MYSQL_PORT') ?: '3306';
|
||||
$db = qdb_env_get('QDB_MYSQL_DATABASE') ?: 'nat-as-db';
|
||||
return "mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4";
|
||||
}
|
||||
|
||||
function qdb_mysql_credentials(): array {
|
||||
return [
|
||||
qdb_env_get('QDB_MYSQL_USER') ?: 'nat_as_db',
|
||||
qdb_env_get('QDB_MYSQL_PASSWORD') ?: '',
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{0: PDO, 1: string, 2: null} */
|
||||
function qdb_mysql_open(): array {
|
||||
static $pdo = null;
|
||||
if ($pdo === null) {
|
||||
[$user, $pass] = qdb_mysql_credentials();
|
||||
$pdo = new PDO(qdb_mysql_dsn(), $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
}
|
||||
return [$pdo, '', null];
|
||||
}
|
||||
|
||||
function qdb_set_foreign_keys(PDO $pdo, bool $enabled): void {
|
||||
if (qdb_is_mysql()) {
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS = ' . ($enabled ? '1' : '0'));
|
||||
return;
|
||||
}
|
||||
$pdo->exec('PRAGMA foreign_keys = ' . ($enabled ? 'ON' : 'OFF') . ';');
|
||||
}
|
||||
|
||||
function qdb_table_exists(PDO $pdo, string $table): bool {
|
||||
if (qdb_is_mysql()) {
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = :t LIMIT 1'
|
||||
);
|
||||
$stmt->execute([':t' => $table]);
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = :t LIMIT 1"
|
||||
);
|
||||
$stmt->execute([':t' => $table]);
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
function qdb_column_exists(PDO $pdo, string $table, string $column): bool {
|
||||
if (qdb_is_mysql()) {
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = :t AND column_name = :c LIMIT 1'
|
||||
);
|
||||
$stmt->execute([':t' => $table, ':c' => $column]);
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
$stmt = $pdo->query('PRAGMA table_info(' . $table . ')');
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $col) {
|
||||
if (($col['name'] ?? '') === $column) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function qdb_schema_version(PDO $pdo): int {
|
||||
if (qdb_is_mysql()) {
|
||||
if (!qdb_table_exists($pdo, 'schema_version')) {
|
||||
return 0;
|
||||
}
|
||||
return (int)$pdo->query('SELECT version FROM schema_version WHERE id = 1')->fetchColumn();
|
||||
}
|
||||
return (int)$pdo->query('PRAGMA user_version')->fetchColumn();
|
||||
}
|
||||
|
||||
function qdb_set_schema_version(PDO $pdo, int $version): void {
|
||||
if (qdb_is_mysql()) {
|
||||
if (!qdb_table_exists($pdo, 'schema_version')) {
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS schema_version (
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
version INT NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
}
|
||||
$pdo->prepare(
|
||||
'INSERT INTO schema_version (id, version) VALUES (1, :v)
|
||||
ON DUPLICATE KEY UPDATE version = VALUES(version)'
|
||||
)->execute([':v' => $version]);
|
||||
return;
|
||||
}
|
||||
$pdo->exec('PRAGMA user_version = ' . (int)$version . ';');
|
||||
}
|
||||
|
||||
function qdb_stmt_affected_rows(PDO $pdo, PDOStatement $stmt): int {
|
||||
if (qdb_is_mysql()) {
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
return (int)$pdo->query('SELECT changes()')->fetchColumn();
|
||||
}
|
||||
|
||||
function qdb_create_index_if_not_exists(PDO $pdo, string $name, string $table, string $columnsSql): void {
|
||||
if (qdb_is_mysql()) {
|
||||
if (!qdb_table_exists($pdo, $table)) {
|
||||
return;
|
||||
}
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = :t AND index_name = :i LIMIT 1'
|
||||
);
|
||||
$stmt->execute([':t' => $table, ':i' => $name]);
|
||||
if ($stmt->fetchColumn()) {
|
||||
return;
|
||||
}
|
||||
$pdo->exec("CREATE INDEX `{$name}` ON `{$table}` ({$columnsSql})");
|
||||
return;
|
||||
}
|
||||
$pdo->exec("CREATE INDEX IF NOT EXISTS {$name} ON {$table}({$columnsSql})");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $conflictColumns
|
||||
*/
|
||||
function qdb_upsert_do_nothing(array $conflictColumns): string {
|
||||
if (qdb_is_mysql()) {
|
||||
$first = $conflictColumns[0];
|
||||
return "ON DUPLICATE KEY UPDATE `{$first}` = `{$first}`";
|
||||
}
|
||||
return 'ON CONFLICT(' . implode(', ', $conflictColumns) . ') DO NOTHING';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $conflictColumns
|
||||
* @param array<string, string> $assignments column => expression (use excluded.col on SQLite)
|
||||
*/
|
||||
function qdb_upsert_update(array $conflictColumns, array $assignments): string {
|
||||
if (qdb_is_mysql()) {
|
||||
$parts = [];
|
||||
foreach ($assignments as $column => $expression) {
|
||||
$mysqlExpr = preg_replace('/\bexcluded\.(\w+)\b/', 'VALUES($1)', $expression) ?? $expression;
|
||||
$parts[] = "`{$column}` = {$mysqlExpr}";
|
||||
}
|
||||
return 'ON DUPLICATE KEY UPDATE ' . implode(', ', $parts);
|
||||
}
|
||||
$parts = [];
|
||||
foreach ($assignments as $column => $expression) {
|
||||
$parts[] = "{$column} = {$expression}";
|
||||
}
|
||||
return 'ON CONFLICT(' . implode(', ', $conflictColumns) . ') DO UPDATE SET ' . implode(', ', $parts);
|
||||
}
|
||||
|
||||
function qdb_exec_schema_file(PDO $pdo, string $schemaPath): void {
|
||||
$sql = file_get_contents($schemaPath);
|
||||
if ($sql === false) {
|
||||
throw new RuntimeException("Could not read schema: {$schemaPath}");
|
||||
}
|
||||
qdb_exec_schema_sql($pdo, $sql);
|
||||
}
|
||||
|
||||
function qdb_exec_schema_sql(PDO $pdo, string $sql): void {
|
||||
if (qdb_is_mysql()) {
|
||||
foreach (qdb_split_sql_statements($sql) as $statement) {
|
||||
$trimmed = trim($statement);
|
||||
if ($trimmed === '' || str_starts_with($trimmed, '--')) {
|
||||
continue;
|
||||
}
|
||||
$pdo->exec($trimmed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$pdo->exec($sql);
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
function qdb_split_sql_statements(string $sql): array {
|
||||
$parts = preg_split('/;\s*\n/', $sql) ?: [];
|
||||
return array_values(array_filter(array_map('trim', $parts), fn($s) => $s !== ''));
|
||||
}
|
||||
Reference in New Issue
Block a user