This commit is contained in:
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