From ed41c32ad88a91ba84f04b76528e3ab436f83e82 Mon Sep 17 00:00:00 2001 From: Daniel Dolezal Date: Mon, 6 Apr 2026 17:22:27 +0200 Subject: [PATCH] don't crash server when sqlite-zstd extension can't be loaded --- server/database/GameplayDatabase.py | 50 +++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/server/database/GameplayDatabase.py b/server/database/GameplayDatabase.py index 2075c09..2cdd813 100644 --- a/server/database/GameplayDatabase.py +++ b/server/database/GameplayDatabase.py @@ -1,13 +1,21 @@ from datetime import datetime, timezone -import asyncio, sqlite3, json, os +import asyncio, sqlite3, json, os, logging, sys from pathlib import Path +logger = logging.getLogger(__name__) +if not logger.handlers: + _handler = logging.StreamHandler(stream=sys.stdout) + _handler.setFormatter(logging.Formatter(fmt="%(levelname)s %(module)s: %(message)s")) + logger.addHandler(_handler) + logger.propagate = False + _ZSTD_EXT = Path(os.environ.get("SQLITE_ZSTD_EXT", "/usr/local/lib/libsqlite_zstd.so")).expanduser().resolve() class GameplayDatabase: def __init__(self, db_path:str, busy_timeout_ms:int=5000): self.db_path = db_path self.busy_timeout_ms = max(1000, int(busy_timeout_ms)) + self._zstd_available = False self._initialize_database() def _connect(self) -> sqlite3.Connection: @@ -16,13 +24,18 @@ class GameplayDatabase: timeout=max(1, self.busy_timeout_ms // 1000), isolation_level=None, ) - - if Path(_ZSTD_EXT).exists(): - connection.enable_load_extension(True) - connection.load_extension(str(_ZSTD_EXT)) - connection.enable_load_extension(False) - connection.row_factory = sqlite3.Row + + if _ZSTD_EXT.exists(): + try: + connection.enable_load_extension(True) + connection.load_extension(str(_ZSTD_EXT)) + self._zstd_available = True + except sqlite3.OperationalError as e: + logger.warning(f"sqlite-zstd extension skipped: {e}") + finally: + connection.enable_load_extension(False) + connection.execute("PRAGMA foreign_keys = ON") connection.execute("PRAGMA journal_mode = WAL") connection.execute("PRAGMA synchronous = NORMAL") @@ -102,7 +115,8 @@ class GameplayDatabase: self._ensure_column_exists(connection, "games", "your_snake_version", "TEXT") self._ensure_column_exists(connection, "games", "game_type", "TEXT") self._ensure_column_exists(connection, "snake_turns", "latency", "TEXT") - self._enable_zstd_compression(connection) + if self._zstd_available: + self._enable_zstd_compression(connection) connection.execute("PRAGMA optimize") def _create_indexes_if_tables(self, connection: sqlite3.Connection) -> None: @@ -121,11 +135,27 @@ class GameplayDatabase: connection.execute(f"CREATE INDEX IF NOT EXISTS {idx_name} ON {table}({cols})") def _ensure_column_exists(self, connection:sqlite3.Connection, table_name:str, column_name:str, column_type:str) -> None: - existing = connection.execute(f"PRAGMA table_info({table_name})").fetchall() + obj = connection.execute( + "SELECT type FROM sqlite_master WHERE name = ?", (table_name,) + ).fetchone() + + if obj and obj["type"] == "view": + # zstd replaced this table with a view — operate on the underlying compressed table + underlying = f"_{table_name}_zstd" + exists = connection.execute( + "SELECT 1 FROM sqlite_master WHERE name = ? AND type = 'table'", (underlying,) + ).fetchone() + if not exists: + return # nothing we can do without the extension + actual_table = underlying + else: + actual_table = table_name + + existing = connection.execute(f"PRAGMA table_info({actual_table})").fetchall() if any(row["name"] == column_name for row in existing): return - connection.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}") + connection.execute(f"ALTER TABLE {actual_table} ADD COLUMN {column_name} {column_type}") def _enable_zstd_compression(self, connection: sqlite3.Connection) -> None: compressed_columns = [