Files
DBClient/app/drivers/base.py
T
2026-05-21 15:46:41 -04:00

210 lines
6.0 KiB
Python

"""
Abstract base driver interface.
All DB-specific drivers must implement this interface so the UI is fully DB-agnostic.
"""
import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class ColumnInfo:
name: str
data_type: str
nullable: bool
default: Optional[str]
is_primary_key: bool
is_foreign_key: bool
extra: str = ""
@dataclass
class IndexInfo:
name: str
columns: list
is_unique: bool
index_type: str = ""
@dataclass
class ForeignKeyInfo:
name: str
column: str
ref_table: str
ref_column: str
on_update: str = ""
on_delete: str = ""
@dataclass
class TableInfo:
name: str
schema: str
row_count: int = 0
size_bytes: int = 0
engine: str = ""
comment: str = ""
class BaseDriver(ABC):
"""Abstract base class for all database drivers.
Thread-safety
-------------
A single driver instance is shared across multiple ``SchemaWorker`` threads
(columns, indexes, FK, DDL all fire in parallel when a table is opened).
The ``_lock`` ``threading.RLock`` ensures that each subclass's ``_cur()``
context manager holds the lock for the *entire* execute → fetch sequence,
serialising concurrent access on the underlying (non-thread-safe) connection.
"""
def __init__(self, config: dict):
self.config = config
self._connection = None
self.db_type = ""
# Reentrant lock shared by all _cur() calls; prevents concurrent
# threads from interleaving reads/writes on the same socket.
self._lock = threading.RLock()
@abstractmethod
def connect(self) -> None:
"""Establish database connection."""
pass
@abstractmethod
def disconnect(self) -> None:
"""Close database connection."""
pass
@abstractmethod
def test_connection(self) -> tuple:
"""Test connection. Returns (bool success, str message)."""
pass
@abstractmethod
def get_databases(self) -> list:
"""Returns list of database name strings."""
pass
@abstractmethod
def get_tables(self, database: str) -> list:
"""Returns list of TableInfo for the given database."""
pass
@abstractmethod
def get_views(self, database: str) -> list:
"""Returns list of view name strings."""
pass
@abstractmethod
def get_columns(self, database: str, table: str) -> list:
"""Returns list of ColumnInfo for a table."""
pass
@abstractmethod
def get_indexes(self, database: str, table: str) -> list:
"""Returns list of IndexInfo for a table."""
pass
@abstractmethod
def get_foreign_keys(self, database: str, table: str) -> list:
"""Returns list of ForeignKeyInfo for a table."""
pass
@abstractmethod
def get_table_ddl(self, database: str, table: str) -> str:
"""Returns DDL CREATE TABLE statement."""
pass
@abstractmethod
def execute_query(self, sql: str, params: Optional[tuple] = None) -> tuple:
"""Execute a query. Returns (list[str] columns, list[tuple] rows, int rowcount)."""
pass
@abstractmethod
def execute_script(self, sql: str) -> list:
"""Execute multiple statements. Returns list of (columns, rows, rowcount, message) tuples."""
pass
@abstractmethod
def get_table_data(self, database: str, table: str,
where: str = "", order_by: str = "",
limit: int = 1000, offset: int = 0) -> tuple:
"""Returns (columns, rows, total_count) for paginated table data."""
pass
@abstractmethod
def get_table_row_count(self, database: str, table: str, where: str = "") -> int:
"""Returns total row count for a table."""
pass
@abstractmethod
def insert_row(self, database: str, table: str, data: dict) -> bool:
pass
@abstractmethod
def update_row(self, database: str, table: str, data: dict, where: dict) -> bool:
pass
@abstractmethod
def delete_row(self, database: str, table: str, where: dict) -> bool:
pass
@abstractmethod
def get_functions(self, database: str) -> list:
pass
@abstractmethod
def get_stored_procedures(self, database: str) -> list:
pass
@abstractmethod
def get_triggers(self, database: str, table: str = "") -> list:
pass
@abstractmethod
def explain_query(self, sql: str) -> tuple:
"""Returns (columns, rows) for EXPLAIN output."""
pass
@abstractmethod
def get_process_list(self) -> tuple:
"""Returns (columns, rows) of running processes."""
pass
@abstractmethod
def kill_process(self, process_id: int) -> bool:
pass
# ── Table designer ────────────────────────────────────────────────────────
@abstractmethod
def add_column(self, database: str, table: str, col_name: str,
col_type: str, nullable: bool = True,
default: Optional[str] = None) -> bool:
"""Add a column to an existing table via ALTER TABLE."""
pass
@abstractmethod
def drop_column(self, database: str, table: str, col_name: str) -> bool:
"""Drop a column from a table via ALTER TABLE."""
pass
@abstractmethod
def rename_column(self, database: str, table: str,
old_name: str, new_name: str) -> bool:
"""Rename a column via ALTER TABLE."""
pass
@property
def is_connected(self) -> bool:
return self._connection is not None
def get_connection_info(self) -> str:
"""Return human-readable connection info string."""
cfg = self.config
if self.db_type == "sqlite":
return cfg.get("database", "")
return f"{cfg.get('user', '')}@{cfg.get('host', '')}:{cfg.get('port', '')}"