227 lines
7.2 KiB
PHP
227 lines
7.2 KiB
PHP
<?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 !== ''));
|
|
}
|