Initial Codes

This commit is contained in:
2026-05-21 15:46:41 -04:00
commit b01ad5ea40
40 changed files with 9102 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
"""
Driver factory — returns an instantiated driver for the given DB type.
"""
from app.drivers.base import BaseDriver
def get_driver(db_type: str, config: dict) -> BaseDriver:
"""Instantiate and return the correct driver for db_type."""
db_type = db_type.lower()
if db_type == "mysql":
from app.drivers.mysql_driver import MySQLDriver
return MySQLDriver(config)
elif db_type in ("postgresql", "postgres"):
from app.drivers.postgres_driver import PostgreSQLDriver
return PostgreSQLDriver(config)
elif db_type == "sqlite":
from app.drivers.sqlite_driver import SQLiteDriver
return SQLiteDriver(config)
elif db_type == "mssql":
from app.drivers.mssql_driver import MSSQLDriver
return MSSQLDriver(config)
else:
raise ValueError(f"Unsupported database type: {db_type!r}")
+209
View File
@@ -0,0 +1,209 @@
"""
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', '')}"
+333
View File
@@ -0,0 +1,333 @@
"""Microsoft SQL Server driver using pyodbc."""
from typing import Optional
from app.drivers.base import (
BaseDriver, ColumnInfo, IndexInfo, ForeignKeyInfo, TableInfo
)
try:
import pyodbc
PYODBC_AVAILABLE = True
except ImportError:
PYODBC_AVAILABLE = False
class MSSQLDriver(BaseDriver):
"""SQL Server driver via pyodbc (ODBC Driver 17/18 for SQL Server required)."""
def __init__(self, config: dict):
super().__init__(config)
self.db_type = "mssql"
def _conn_str(self) -> str:
host = self.config.get("host", "localhost")
port = int(self.config.get("port", 1433))
db = self.config.get("database", "master")
user = self.config.get("user", "")
pwd = self.config.get("password", "")
# Try drivers in order of preference
for driver in [
"ODBC Driver 18 for SQL Server",
"ODBC Driver 17 for SQL Server",
"SQL Server",
]:
return (
f"DRIVER={{{driver}}};"
f"SERVER={host},{port};"
f"DATABASE={db};"
f"UID={user};PWD={pwd};"
f"TrustServerCertificate=yes;"
f"Connection Timeout={self.config.get('connection_timeout', 30)};"
)
def connect(self) -> None:
if not PYODBC_AVAILABLE:
raise ImportError("pyodbc is not installed. Run: pip install pyodbc")
self._connection = pyodbc.connect(self._conn_str(), autocommit=True)
def disconnect(self) -> None:
if self._connection:
try:
self._connection.close()
except Exception:
pass
finally:
self._connection = None
def test_connection(self) -> tuple:
if not PYODBC_AVAILABLE:
return False, "pyodbc is not installed. Run: pip install pyodbc"
try:
conn = pyodbc.connect(self._conn_str(), autocommit=True)
conn.close()
return True, "Connection successful"
except Exception as e:
return False, str(e)
def _cur(self):
return self._connection.cursor()
# ── Schema introspection ──────────────────────────────────────────────────
def get_databases(self) -> list:
c = self._cur()
c.execute("SELECT name FROM sys.databases ORDER BY name")
return [r[0] for r in c.fetchall()]
def get_tables(self, database: str) -> list:
c = self._cur()
c.execute(f"""
SELECT t.name, s.name,
COALESCE(p.rows, 0), 0, '', ''
FROM [{database}].sys.tables t
JOIN [{database}].sys.schemas s ON t.schema_id = s.schema_id
LEFT JOIN (
SELECT object_id, SUM(rows) AS rows
FROM [{database}].sys.partitions WHERE index_id IN (0,1)
GROUP BY object_id
) p ON p.object_id = t.object_id
ORDER BY t.name
""")
return [TableInfo(name=r[0], schema=r[1], row_count=r[2],
size_bytes=r[3], engine=r[4], comment=r[5])
for r in c.fetchall()]
def get_views(self, database: str) -> list:
c = self._cur()
c.execute(f"SELECT name FROM [{database}].sys.views ORDER BY name")
return [r[0] for r in c.fetchall()]
def get_columns(self, database: str, table: str) -> list:
c = self._cur()
c.execute(f"""
SELECT c.name, tp.name, c.is_nullable, dc.definition,
CASE WHEN pk.column_id IS NOT NULL THEN 1 ELSE 0 END,
CASE WHEN fk.parent_column_id IS NOT NULL THEN 1 ELSE 0 END,
CASE WHEN c.is_identity = 1 THEN 'auto_increment' ELSE '' END
FROM [{database}].sys.columns c
JOIN [{database}].sys.types tp ON tp.user_type_id = c.user_type_id
JOIN [{database}].sys.tables t ON t.object_id = c.object_id
LEFT JOIN [{database}].sys.default_constraints dc
ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id
LEFT JOIN (
SELECT ic.column_id, ic.object_id
FROM [{database}].sys.index_columns ic
JOIN [{database}].sys.indexes i ON i.object_id = ic.object_id AND i.index_id = ic.index_id
WHERE i.is_primary_key = 1
) pk ON pk.object_id = c.object_id AND pk.column_id = c.column_id
LEFT JOIN (
SELECT fkc.parent_column_id, fkc.parent_object_id
FROM [{database}].sys.foreign_key_columns fkc
) fk ON fk.parent_object_id = c.object_id AND fk.parent_column_id = c.column_id
WHERE t.name = ?
ORDER BY c.column_id
""", (table,))
return [ColumnInfo(name=r[0], data_type=r[1], nullable=bool(r[2]),
default=r[3], is_primary_key=bool(r[4]),
is_foreign_key=bool(r[5]), extra=r[6] or "")
for r in c.fetchall()]
def get_indexes(self, database: str, table: str) -> list:
c = self._cur()
c.execute(f"""
SELECT i.name, i.is_unique, STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY ic.key_ordinal)
FROM [{database}].sys.indexes i
JOIN [{database}].sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
JOIN [{database}].sys.columns c ON c.object_id = i.object_id AND c.column_id = ic.column_id
JOIN [{database}].sys.tables t ON t.object_id = i.object_id
WHERE t.name = ?
GROUP BY i.name, i.is_unique
""", (table,))
return [IndexInfo(name=r[0], columns=r[2].split(','),
is_unique=bool(r[1]))
for r in c.fetchall()]
def get_foreign_keys(self, database: str, table: str) -> list:
c = self._cur()
c.execute(f"""
SELECT fk.name, pc.name, rt.name, rc.name,
fk.update_referential_action_desc,
fk.delete_referential_action_desc
FROM [{database}].sys.foreign_keys fk
JOIN [{database}].sys.tables pt ON pt.object_id = fk.parent_object_id
JOIN [{database}].sys.tables rt ON rt.object_id = fk.referenced_object_id
JOIN [{database}].sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
JOIN [{database}].sys.columns pc ON pc.object_id = fkc.parent_object_id AND pc.column_id = fkc.parent_column_id
JOIN [{database}].sys.columns rc ON rc.object_id = fkc.referenced_object_id AND rc.column_id = fkc.referenced_column_id
WHERE pt.name = ?
""", (table,))
return [ForeignKeyInfo(name=r[0], column=r[1],
ref_table=r[2], ref_column=r[3],
on_update=r[4] or "", on_delete=r[5] or "")
for r in c.fetchall()]
def get_table_ddl(self, database: str, table: str) -> str:
cols = self.get_columns(database, table)
lines = [f"CREATE TABLE [{table}] ("]
col_defs = []
for col in cols:
d = f" [{col.name}] {col.data_type}"
if not col.nullable: d += " NOT NULL"
if col.default: d += f" DEFAULT {col.default}"
col_defs.append(d)
lines.append(",\n".join(col_defs))
lines.append(");")
return "\n".join(lines)
def get_functions(self, database: str) -> list:
c = self._cur()
c.execute(f"""
SELECT name FROM [{database}].sys.objects
WHERE type IN ('FN','IF','TF') ORDER BY name
""")
return [r[0] for r in c.fetchall()]
def get_stored_procedures(self, database: str) -> list:
c = self._cur()
c.execute(f"""
SELECT name FROM [{database}].sys.procedures ORDER BY name
""")
return [r[0] for r in c.fetchall()]
def get_triggers(self, database: str, table: str = "") -> list:
c = self._cur()
if table:
c.execute(f"""
SELECT t.name FROM [{database}].sys.triggers t
JOIN [{database}].sys.tables tb ON tb.object_id = t.parent_id
WHERE tb.name = ? ORDER BY t.name
""", (table,))
else:
c.execute(f"SELECT name FROM [{database}].sys.triggers ORDER BY name")
return [r[0] for r in c.fetchall()]
# ── Query execution ───────────────────────────────────────────────────────
def execute_query(self, sql: str, params: Optional[tuple] = None) -> tuple:
c = self._cur()
c.execute(sql, params or ())
if c.description:
cols = [d[0] for d in c.description]
rows = c.fetchall()
return cols, [tuple(r) for r in rows], len(rows)
return [], [], c.rowcount
def execute_script(self, sql: str) -> list:
results = []
stmts = [s.strip() for s in sql.split(';') if s.strip()]
for stmt in stmts:
try:
c = self._cur()
c.execute(stmt)
if c.description:
cols = [d[0] for d in c.description]
rows = [tuple(r) for r in c.fetchall()]
results.append((cols, rows, len(rows), ""))
else:
results.append(([], [], c.rowcount,
f"{c.rowcount} row(s) affected"))
except Exception as e:
results.append(([], [], 0, f"Error: {e}"))
return results
# ── Table data CRUD ───────────────────────────────────────────────────────
def get_table_data(self, database: str, table: str,
where: str = "", order_by: str = "",
limit: int = 1000, offset: int = 0) -> tuple:
sql = f"SELECT * FROM [{database}].[dbo].[{table}]"
if where: sql += f" WHERE {where}"
if order_by: sql += f" ORDER BY {order_by}"
sql += f" OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY"
return self.execute_query(sql)
def get_table_row_count(self, database: str, table: str, where: str = "") -> int:
sql = f"SELECT COUNT(*) FROM [{database}].[dbo].[{table}]"
if where: sql += f" WHERE {where}"
c = self._cur()
c.execute(sql)
return c.fetchone()[0]
def insert_row(self, database: str, table: str, data: dict) -> bool:
cols = ", ".join(f"[{c}]" for c in data)
ph = ", ".join(["?"] * len(data))
c = self._cur()
c.execute(f"INSERT INTO [{database}].[dbo].[{table}] ({cols}) VALUES ({ph})",
tuple(data.values()))
return True
@staticmethod
def _where(where: dict) -> tuple:
parts, params = [], []
for col, val in where.items():
if val is None:
parts.append(f"[{col}] IS NULL")
else:
parts.append(f"[{col}] = ?")
params.append(val)
return " AND ".join(parts), params
def update_row(self, database: str, table: str, data: dict, where: dict) -> bool:
set_cl = ", ".join(f"[{c}] = ?" for c in data)
where_cl, where_params = self._where(where)
c = self._cur()
c.execute(f"UPDATE [{database}].[dbo].[{table}] SET {set_cl} WHERE {where_cl}",
tuple(data.values()) + tuple(where_params))
return True
def delete_row(self, database: str, table: str, where: dict) -> bool:
where_cl, where_params = self._where(where)
c = self._cur()
c.execute(f"DELETE FROM [{database}].[dbo].[{table}] WHERE {where_cl}",
tuple(where_params))
return True
# ── Server tools ──────────────────────────────────────────────────────────
def explain_query(self, sql: str) -> tuple:
c = self._cur()
c.execute(f"SET SHOWPLAN_TEXT ON; {sql}; SET SHOWPLAN_TEXT OFF")
return ["Plan"], c.fetchall()
def get_process_list(self) -> tuple:
c = self._cur()
c.execute("""
SELECT session_id, login_name, status, host_name,
program_name, cpu_time, text
FROM sys.dm_exec_sessions s
CROSS APPLY sys.dm_exec_sql_text(s.most_recent_sql_handle) t
WHERE s.is_user_process = 1
""")
cols = [d[0] for d in c.description]
return cols, [tuple(r) for r in c.fetchall()]
def kill_process(self, process_id: int) -> bool:
c = self._cur()
c.execute(f"KILL {process_id}")
return True
# ── Table designer ────────────────────────────────────────────────────────
def add_column(self, database: str, table: str, col_name: str,
col_type: str, nullable: bool = True,
default=None) -> bool:
null_clause = "NULL" if nullable else "NOT NULL"
default_clause = f" DEFAULT {default}" if default is not None and default != "" else ""
sql = (f"ALTER TABLE [{database}].[dbo].[{table}] "
f"ADD [{col_name}] {col_type} {null_clause}{default_clause}")
c = self._cur()
c.execute(sql)
return True
def drop_column(self, database: str, table: str, col_name: str) -> bool:
sql = f"ALTER TABLE [{database}].[dbo].[{table}] DROP COLUMN [{col_name}]"
c = self._cur()
c.execute(sql)
return True
def rename_column(self, database: str, table: str,
old_name: str, new_name: str) -> bool:
# sp_rename is the standard way in MSSQL
sql = f"EXEC sp_rename '[{database}].[dbo].[{table}].[{old_name}]', '{new_name}', 'COLUMN'"
c = self._cur()
c.execute(sql)
return True
+377
View File
@@ -0,0 +1,377 @@
"""MySQL driver implementation using pymysql."""
import pymysql
import pymysql.cursors
from contextlib import contextmanager
from typing import Optional
from app.drivers.base import (
BaseDriver, ColumnInfo, IndexInfo, ForeignKeyInfo, TableInfo
)
from app.utils.logger import get_logger
_log = get_logger(__name__)
class MySQLDriver(BaseDriver):
"""MySQL / MariaDB database driver."""
def __init__(self, config: dict):
super().__init__(config)
self.db_type = "mysql"
def _connect_kwargs(self) -> dict:
kw = {
"host": self.config.get("host", "localhost"),
"port": int(self.config.get("port", 3306)),
"user": self.config.get("user", ""),
"password": self.config.get("password", ""),
"connect_timeout": int(self.config.get("connection_timeout", 30)),
"autocommit": True,
"charset": "utf8mb4",
}
db = self.config.get("database", "")
if db:
kw["database"] = db
return kw
def connect(self) -> None:
host = self.config.get("host", "localhost")
port = self.config.get("port", 3306)
user = self.config.get("user", "")
_log.info("MySQL connecting host=%s:%s user=%s", host, port, user)
try:
self._connection = pymysql.connect(**self._connect_kwargs())
_log.info("MySQL connected host=%s:%s user=%s", host, port, user)
except Exception:
_log.error("MySQL connection failed host=%s:%s user=%s",
host, port, user, exc_info=True)
raise
def disconnect(self) -> None:
if self._connection:
try:
self._connection.close()
except Exception:
pass
finally:
self._connection = None
def test_connection(self) -> tuple:
try:
conn = pymysql.connect(**self._connect_kwargs())
conn.close()
return True, "Connection successful"
except Exception as e:
return False, str(e)
def _ensure_alive(self) -> None:
"""Ping the server and silently reconnect if the connection has gone away."""
if self._connection is None:
_log.warning("MySQL connection is None — connecting now")
self.connect()
return
try:
self._connection.ping(reconnect=True)
except Exception:
_log.warning("MySQL ping failed — attempting full reconnect", exc_info=True)
try:
self.connect()
_log.info("MySQL reconnected successfully")
except Exception:
_log.error("MySQL reconnect failed", exc_info=True)
raise
@contextmanager
def _cur(self):
"""Yield a cursor while holding the driver lock.
Using a contextmanager means the lock is held for the *entire*
``with self._cur() as c: c.execute(); c.fetchall()`` block, which
prevents concurrent SchemaWorker threads from interleaving on the same
TCP socket (pymysql is not thread-safe).
"""
with self._lock:
self._ensure_alive()
cursor = self._connection.cursor(pymysql.cursors.Cursor)
try:
yield cursor
finally:
try:
cursor.close()
except Exception:
pass
@staticmethod
def _fmt_err(e: Exception) -> str:
"""Return a readable string for a pymysql exception.
pymysql errors carry (error_code, message) as args, so ``str(e)``
prints something like ``(0, '')``. This helper unwraps that.
"""
args = getattr(e, "args", ())
if args and isinstance(args[0], int):
code, msg = args[0], args[1] if len(args) > 1 else ""
if msg:
return f"MySQL error {code}: {msg}"
if code == 0:
return "Lost connection to MySQL server (connection timed out or was reset)."
return f"MySQL error {code}"
return str(e)
# ── Schema introspection ──────────────────────────────────────────────────
def get_databases(self) -> list:
with self._cur() as c:
c.execute("SHOW DATABASES")
return [r[0] for r in c.fetchall()]
def get_tables(self, database: str) -> list:
with self._cur() as c:
c.execute("""
SELECT TABLE_NAME, TABLE_SCHEMA,
COALESCE(TABLE_ROWS, 0),
COALESCE(DATA_LENGTH + INDEX_LENGTH, 0),
COALESCE(ENGINE, ''),
COALESCE(TABLE_COMMENT, '')
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = %s AND TABLE_TYPE = 'BASE TABLE'
ORDER BY TABLE_NAME
""", (database,))
return [TableInfo(name=r[0], schema=r[1], row_count=r[2],
size_bytes=r[3], engine=r[4], comment=r[5])
for r in c.fetchall()]
def get_views(self, database: str) -> list:
with self._cur() as c:
c.execute("""
SELECT TABLE_NAME FROM information_schema.VIEWS
WHERE TABLE_SCHEMA = %s ORDER BY TABLE_NAME
""", (database,))
return [r[0] for r in c.fetchall()]
def get_columns(self, database: str, table: str) -> list:
with self._cur() as c:
c.execute("""
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE,
COLUMN_DEFAULT, COLUMN_KEY, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
ORDER BY ORDINAL_POSITION
""", (database, table))
return [ColumnInfo(name=r[0], data_type=r[1],
nullable=(r[2] == "YES"), default=r[3],
is_primary_key=(r[4] == "PRI"),
is_foreign_key=(r[4] == "MUL"),
extra=r[5] or "")
for r in c.fetchall()]
def get_indexes(self, database: str, table: str) -> list:
with self._cur() as c:
c.execute(f"SHOW INDEX FROM `{database}`.`{table}`")
idx_map = {}
for r in c.fetchall():
name, non_unique, col, idx_type = r[2], r[1], r[4], r[10]
if name not in idx_map:
idx_map[name] = IndexInfo(name=name, columns=[col],
is_unique=(non_unique == 0),
index_type=idx_type)
else:
idx_map[name].columns.append(col)
return list(idx_map.values())
def get_foreign_keys(self, database: str, table: str) -> list:
with self._cur() as c:
c.execute("""
SELECT kcu.CONSTRAINT_NAME, kcu.COLUMN_NAME,
kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME,
rc.UPDATE_RULE, rc.DELETE_RULE
FROM information_schema.KEY_COLUMN_USAGE kcu
JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
ON rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
AND rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
WHERE kcu.TABLE_SCHEMA = %s AND kcu.TABLE_NAME = %s
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
""", (database, table))
return [ForeignKeyInfo(name=r[0], column=r[1],
ref_table=r[2], ref_column=r[3],
on_update=r[4] or "", on_delete=r[5] or "")
for r in c.fetchall()]
def get_table_ddl(self, database: str, table: str) -> str:
with self._cur() as c:
c.execute(f"SHOW CREATE TABLE `{database}`.`{table}`")
row = c.fetchone()
return row[1] if row else ""
def get_functions(self, database: str) -> list:
with self._cur() as c:
c.execute("""
SELECT ROUTINE_NAME FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = %s AND ROUTINE_TYPE = 'FUNCTION'
ORDER BY ROUTINE_NAME
""", (database,))
return [r[0] for r in c.fetchall()]
def get_stored_procedures(self, database: str) -> list:
with self._cur() as c:
c.execute("""
SELECT ROUTINE_NAME FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = %s AND ROUTINE_TYPE = 'PROCEDURE'
ORDER BY ROUTINE_NAME
""", (database,))
return [r[0] for r in c.fetchall()]
def get_triggers(self, database: str, table: str = "") -> list:
with self._cur() as c:
if table:
c.execute("""
SELECT TRIGGER_NAME FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = %s AND EVENT_OBJECT_TABLE = %s
ORDER BY TRIGGER_NAME
""", (database, table))
else:
c.execute("""
SELECT TRIGGER_NAME FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = %s ORDER BY TRIGGER_NAME
""", (database,))
return [r[0] for r in c.fetchall()]
# ── Query execution ───────────────────────────────────────────────────────
def execute_query(self, sql: str, params: Optional[tuple] = None) -> tuple:
with self._cur() as c:
c.execute(sql, params)
if c.description:
cols = [d[0] for d in c.description]
rows = c.fetchall()
return cols, rows, len(rows)
return [], [], c.rowcount
def execute_script(self, sql: str) -> list:
results = []
stmts = [s.strip() for s in sql.split(';') if s.strip()]
with self._cur() as c:
for stmt in stmts:
try:
c.execute(stmt)
if c.description:
cols = [d[0] for d in c.description]
rows = c.fetchall()
results.append((cols, rows, len(rows), ""))
else:
results.append(([], [], c.rowcount,
f"{c.rowcount} row(s) affected"))
except Exception as e:
results.append(([], [], 0, f"Error: {e}"))
return results
# ── Table data CRUD ───────────────────────────────────────────────────────
def get_table_data(self, database: str, table: str,
where: str = "", order_by: str = "",
limit: int = 1000, offset: int = 0) -> tuple:
sql = f"SELECT * FROM `{database}`.`{table}`"
if where: sql += f" WHERE {where}"
if order_by: sql += f" ORDER BY {order_by}"
sql += f" LIMIT {limit} OFFSET {offset}"
return self.execute_query(sql)
def get_table_row_count(self, database: str, table: str, where: str = "") -> int:
sql = f"SELECT COUNT(*) FROM `{database}`.`{table}`"
if where: sql += f" WHERE {where}"
with self._cur() as c:
c.execute(sql)
return c.fetchone()[0]
def insert_row(self, database: str, table: str, data: dict) -> bool:
cols = ", ".join(f"`{c}`" for c in data)
ph = ", ".join(["%s"] * len(data))
with self._cur() as c:
c.execute(f"INSERT INTO `{database}`.`{table}` ({cols}) VALUES ({ph})",
tuple(data.values()))
return True
@staticmethod
def _where(where: dict) -> tuple:
parts, params = [], []
for col, val in where.items():
if val is None:
parts.append(f"`{col}` IS NULL")
else:
parts.append(f"`{col}` = %s")
params.append(val)
return " AND ".join(parts), params
def update_row(self, database: str, table: str, data: dict, where: dict) -> bool:
set_cl = ", ".join(f"`{c}` = %s" for c in data)
where_cl, where_params = self._where(where)
with self._cur() as c:
c.execute(f"UPDATE `{database}`.`{table}` SET {set_cl} WHERE {where_cl}",
tuple(data.values()) + tuple(where_params))
return True
def delete_row(self, database: str, table: str, where: dict) -> bool:
where_cl, where_params = self._where(where)
with self._cur() as c:
c.execute(f"DELETE FROM `{database}`.`{table}` WHERE {where_cl}",
tuple(where_params))
return True
# ── Server tools ──────────────────────────────────────────────────────────
def explain_query(self, sql: str) -> tuple:
with self._cur() as c:
c.execute(f"EXPLAIN {sql}")
return [d[0] for d in c.description], c.fetchall()
def get_process_list(self) -> tuple:
with self._cur() as c:
c.execute("SHOW FULL PROCESSLIST")
return [d[0] for d in c.description], c.fetchall()
def kill_process(self, process_id: int) -> bool:
# MUST use a dedicated connection, not self._cur().
#
# If a QueryWorker is running a long query it holds self._lock via
# _cur(). kill_process is called from a *different* SchemaWorker
# thread; using self._cur() here would block waiting for that lock,
# so the KILL command would never reach MySQL and the server would
# eventually raise error 1317 ("Query execution was interrupted") on
# its own. A fresh connection bypasses the lock entirely, which is
# exactly how MySQL's KILL is intended to work.
conn = pymysql.connect(**self._connect_kwargs())
try:
with conn.cursor() as c:
c.execute(f"KILL {process_id}")
finally:
try:
conn.close()
except Exception:
pass
return True
# ── Table designer ────────────────────────────────────────────────────────
def add_column(self, database: str, table: str, col_name: str,
col_type: str, nullable: bool = True,
default=None) -> bool:
null_clause = "NULL" if nullable else "NOT NULL"
default_clause = f" DEFAULT {default}" if default is not None and default != "" else ""
sql = (f"ALTER TABLE `{database}`.`{table}` "
f"ADD COLUMN `{col_name}` {col_type} {null_clause}{default_clause}")
with self._cur() as c:
c.execute(sql)
return True
def drop_column(self, database: str, table: str, col_name: str) -> bool:
sql = f"ALTER TABLE `{database}`.`{table}` DROP COLUMN `{col_name}`"
with self._cur() as c:
c.execute(sql)
return True
def rename_column(self, database: str, table: str,
old_name: str, new_name: str) -> bool:
sql = (f"ALTER TABLE `{database}`.`{table}` "
f"RENAME COLUMN `{old_name}` TO `{new_name}`")
with self._cur() as c:
c.execute(sql)
return True
+343
View File
@@ -0,0 +1,343 @@
"""PostgreSQL driver implementation using psycopg2."""
import psycopg2
import psycopg2.extras
from contextlib import contextmanager
from typing import Optional
from app.drivers.base import (
BaseDriver, ColumnInfo, IndexInfo, ForeignKeyInfo, TableInfo
)
class PostgreSQLDriver(BaseDriver):
"""PostgreSQL database driver."""
def __init__(self, config: dict):
super().__init__(config)
self.db_type = "postgresql"
def _dsn(self) -> dict:
kw = {
"host": self.config.get("host", "localhost"),
"port": int(self.config.get("port", 5432)),
"user": self.config.get("user", ""),
"password": self.config.get("password", ""),
"connect_timeout": int(self.config.get("connection_timeout", 30)),
}
db = self.config.get("database", "")
if db:
kw["dbname"] = db
return kw
def connect(self) -> None:
self._connection = psycopg2.connect(**self._dsn())
self._connection.autocommit = True
def disconnect(self) -> None:
if self._connection:
try:
self._connection.close()
except Exception:
pass
finally:
self._connection = None
def test_connection(self) -> tuple:
try:
conn = psycopg2.connect(**self._dsn())
conn.close()
return True, "Connection successful"
except Exception as e:
return False, str(e)
@contextmanager
def _cur(self):
"""Yield a cursor while holding the driver lock (thread-safe execute→fetch)."""
with self._lock:
cursor = self._connection.cursor()
try:
yield cursor
finally:
try:
cursor.close()
except Exception:
pass
# ── Schema introspection ──────────────────────────────────────────────────
def get_databases(self) -> list:
with self._cur() as c:
c.execute(
"SELECT datname FROM pg_database "
"WHERE datistemplate = false ORDER BY datname"
)
return [r[0] for r in c.fetchall()]
def get_tables(self, database: str) -> list:
with self._cur() as c:
c.execute("""
SELECT t.table_name, t.table_schema,
COALESCE(s.n_live_tup, 0),
0,
'',
COALESCE(obj_description(
(quote_ident(t.table_schema)||'.'||quote_ident(t.table_name))::regclass,
'pg_class'), '')
FROM information_schema.tables t
LEFT JOIN pg_stat_user_tables s
ON s.schemaname = t.table_schema AND s.relname = t.table_name
WHERE t.table_schema NOT IN ('pg_catalog','information_schema')
AND t.table_type = 'BASE TABLE'
ORDER BY t.table_name
""")
return [TableInfo(name=r[0], schema=r[1], row_count=r[2],
size_bytes=r[3], engine=r[4], comment=r[5])
for r in c.fetchall()]
def get_views(self, database: str) -> list:
with self._cur() as c:
c.execute("""
SELECT table_name FROM information_schema.views
WHERE table_schema NOT IN ('pg_catalog','information_schema')
ORDER BY table_name
""")
return [r[0] for r in c.fetchall()]
def get_columns(self, database: str, table: str) -> list:
with self._cur() as c:
c.execute("""
SELECT c.column_name, c.data_type, c.is_nullable,
c.column_default,
(SELECT true FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'PRIMARY KEY'
AND kcu.table_name = c.table_name
AND kcu.column_name = c.column_name
LIMIT 1) IS NOT NULL,
false
FROM information_schema.columns c
WHERE c.table_name = %s
ORDER BY c.ordinal_position
""", (table,))
return [ColumnInfo(name=r[0], data_type=r[1],
nullable=(r[2] == "YES"), default=r[3],
is_primary_key=bool(r[4]),
is_foreign_key=bool(r[5]))
for r in c.fetchall()]
def get_indexes(self, database: str, table: str) -> list:
with self._cur() as c:
c.execute("""
SELECT i.relname, ix.indisunique,
array_agg(a.attname ORDER BY k.n) AS cols
FROM pg_class t
JOIN pg_index ix ON t.oid = ix.indrelid
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n)
ON TRUE
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
WHERE t.relname = %s
GROUP BY i.relname, ix.indisunique
ORDER BY i.relname
""", (table,))
return [IndexInfo(name=r[0], columns=list(r[2]),
is_unique=bool(r[1]))
for r in c.fetchall()]
def get_foreign_keys(self, database: str, table: str) -> list:
with self._cur() as c:
c.execute("""
SELECT tc.constraint_name, kcu.column_name,
ccu.table_name, ccu.column_name,
rc.update_rule, rc.delete_rule
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
JOIN information_schema.referential_constraints rc
ON rc.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = %s
""", (table,))
return [ForeignKeyInfo(name=r[0], column=r[1],
ref_table=r[2], ref_column=r[3],
on_update=r[4] or "", on_delete=r[5] or "")
for r in c.fetchall()]
def get_table_ddl(self, database: str, table: str) -> str:
cols = self.get_columns(database, table)
lines = [f"CREATE TABLE {table} ("]
col_defs = []
for c in cols:
d = f" {c.name} {c.data_type}"
if not c.nullable: d += " NOT NULL"
if c.default: d += f" DEFAULT {c.default}"
col_defs.append(d)
lines.append(",\n".join(col_defs))
lines.append(");")
return "\n".join(lines)
def get_functions(self, database: str) -> list:
with self._cur() as c:
c.execute("""
SELECT routine_name FROM information_schema.routines
WHERE routine_type = 'FUNCTION'
AND routine_schema NOT IN ('pg_catalog','information_schema')
ORDER BY routine_name
""")
return [r[0] for r in c.fetchall()]
def get_stored_procedures(self, database: str) -> list:
with self._cur() as c:
c.execute("""
SELECT routine_name FROM information_schema.routines
WHERE routine_type = 'PROCEDURE'
AND routine_schema NOT IN ('pg_catalog','information_schema')
ORDER BY routine_name
""")
return [r[0] for r in c.fetchall()]
def get_triggers(self, database: str, table: str = "") -> list:
with self._cur() as c:
if table:
c.execute("""
SELECT trigger_name FROM information_schema.triggers
WHERE event_object_table = %s ORDER BY trigger_name
""", (table,))
else:
c.execute(
"SELECT trigger_name FROM information_schema.triggers "
"ORDER BY trigger_name"
)
return [r[0] for r in c.fetchall()]
# ── Query execution ───────────────────────────────────────────────────────
def execute_query(self, sql: str, params: Optional[tuple] = None) -> tuple:
with self._cur() as c:
c.execute(sql, params)
if c.description:
cols = [d[0] for d in c.description]
rows = c.fetchall()
return cols, rows, len(rows)
return [], [], c.rowcount
def execute_script(self, sql: str) -> list:
results = []
stmts = [s.strip() for s in sql.split(';') if s.strip()]
with self._cur() as c:
for stmt in stmts:
try:
c.execute(stmt)
if c.description:
cols = [d[0] for d in c.description]
rows = c.fetchall()
results.append((cols, rows, len(rows), ""))
else:
results.append(([], [], c.rowcount,
f"{c.rowcount} row(s) affected"))
except Exception as e:
results.append(([], [], 0, f"Error: {e}"))
return results
# ── Table data CRUD ───────────────────────────────────────────────────────
def get_table_data(self, database: str, table: str,
where: str = "", order_by: str = "",
limit: int = 1000, offset: int = 0) -> tuple:
sql = f'SELECT * FROM "{table}"'
if where: sql += f" WHERE {where}"
if order_by: sql += f" ORDER BY {order_by}"
sql += f" LIMIT {limit} OFFSET {offset}"
return self.execute_query(sql)
def get_table_row_count(self, database: str, table: str, where: str = "") -> int:
sql = f'SELECT COUNT(*) FROM "{table}"'
if where: sql += f" WHERE {where}"
with self._cur() as c:
c.execute(sql)
return c.fetchone()[0]
def insert_row(self, database: str, table: str, data: dict) -> bool:
cols = ", ".join(f'"{c}"' for c in data)
ph = ", ".join(["%s"] * len(data))
with self._cur() as c:
c.execute(f'INSERT INTO "{table}" ({cols}) VALUES ({ph})',
tuple(data.values()))
return True
@staticmethod
def _where(where: dict) -> tuple:
parts, params = [], []
for col, val in where.items():
if val is None:
parts.append(f'"{col}" IS NULL')
else:
parts.append(f'"{col}" = %s')
params.append(val)
return " AND ".join(parts), params
def update_row(self, database: str, table: str, data: dict, where: dict) -> bool:
set_cl = ", ".join(f'"{c}" = %s' for c in data)
where_cl, where_params = self._where(where)
with self._cur() as c:
c.execute(f'UPDATE "{table}" SET {set_cl} WHERE {where_cl}',
tuple(data.values()) + tuple(where_params))
return True
def delete_row(self, database: str, table: str, where: dict) -> bool:
where_cl, where_params = self._where(where)
with self._cur() as c:
c.execute(f'DELETE FROM "{table}" WHERE {where_cl}',
tuple(where_params))
return True
# ── Server tools ──────────────────────────────────────────────────────────
def explain_query(self, sql: str) -> tuple:
with self._cur() as c:
c.execute(f"EXPLAIN ANALYZE {sql}")
return ["Plan"], c.fetchall()
def get_process_list(self) -> tuple:
with self._cur() as c:
c.execute("""
SELECT pid, usename, application_name, client_addr,
state, query, query_start
FROM pg_stat_activity WHERE state IS NOT NULL
ORDER BY query_start DESC NULLS LAST
""")
cols = [d[0] for d in c.description]
return cols, c.fetchall()
def kill_process(self, process_id: int) -> bool:
with self._cur() as c:
c.execute("SELECT pg_terminate_backend(%s)", (process_id,))
return True
# ── Table designer ────────────────────────────────────────────────────────
def add_column(self, _database: str, table: str, col_name: str,
col_type: str, nullable: bool = True,
default=None) -> bool:
default_clause = f" DEFAULT {default}" if default is not None and default != "" else ""
null_clause = "" if nullable else " NOT NULL"
sql = (f'ALTER TABLE "{table}" '
f'ADD COLUMN "{col_name}" {col_type}{default_clause}{null_clause}')
with self._cur() as c:
c.execute(sql)
return True
def drop_column(self, _database: str, table: str, col_name: str) -> bool:
sql = f'ALTER TABLE "{table}" DROP COLUMN "{col_name}"'
with self._cur() as c:
c.execute(sql)
return True
def rename_column(self, _database: str, table: str,
old_name: str, new_name: str) -> bool:
sql = f'ALTER TABLE "{table}" RENAME COLUMN "{old_name}" TO "{new_name}"'
with self._cur() as c:
c.execute(sql)
return True
+280
View File
@@ -0,0 +1,280 @@
"""SQLite driver implementation using stdlib sqlite3."""
import sqlite3
from contextlib import contextmanager
from typing import Optional
from app.drivers.base import (
BaseDriver, ColumnInfo, IndexInfo, ForeignKeyInfo, TableInfo
)
class SQLiteDriver(BaseDriver):
"""SQLite database driver (uses stdlib sqlite3)."""
def __init__(self, config: dict):
super().__init__(config)
self.db_type = "sqlite"
def connect(self) -> None:
db_path = self.config.get("database", ":memory:")
self._connection = sqlite3.connect(
db_path,
check_same_thread=False,
timeout=int(self.config.get("connection_timeout", 30)),
)
self._connection.execute("PRAGMA journal_mode=WAL")
self._connection.execute("PRAGMA foreign_keys=ON")
def disconnect(self) -> None:
if self._connection:
try:
self._connection.close()
except Exception:
pass
finally:
self._connection = None
def test_connection(self) -> tuple:
try:
db_path = self.config.get("database", "")
conn = sqlite3.connect(db_path, timeout=5)
conn.execute("SELECT 1")
conn.close()
return True, "Connection successful"
except Exception as e:
return False, str(e)
@contextmanager
def _cur(self):
"""Yield a cursor while holding the driver lock (thread-safe execute→fetch)."""
with self._lock:
cursor = self._connection.cursor()
try:
yield cursor
finally:
try:
cursor.close()
except Exception:
pass
# ── Schema introspection ──────────────────────────────────────────────────
def get_databases(self) -> list:
return [self.config.get("database", "main")]
def get_tables(self, database: str = "") -> list:
with self._cur() as c:
c.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
""")
names = [r[0] for r in c.fetchall()]
tables = []
for name in names:
try:
with self._cur() as rc:
rc.execute(f'SELECT COUNT(*) FROM "{name}"')
row_count = rc.fetchone()[0]
except Exception:
row_count = 0
tables.append(TableInfo(name=name, schema="main", row_count=row_count))
return tables
def get_views(self, database: str = "") -> list:
with self._cur() as c:
c.execute("SELECT name FROM sqlite_master WHERE type='view' ORDER BY name")
return [r[0] for r in c.fetchall()]
def get_columns(self, database: str, table: str) -> list:
with self._cur() as c:
c.execute(f'PRAGMA table_info("{table}")')
cols = []
for r in c.fetchall():
# cid, name, type, notnull, dflt_value, pk
cols.append(ColumnInfo(
name=r[1],
data_type=r[2] or "TEXT",
nullable=not bool(r[3]),
default=str(r[4]) if r[4] is not None else None,
is_primary_key=bool(r[5]),
is_foreign_key=False,
))
return cols
def get_indexes(self, database: str, table: str) -> list:
with self._cur() as c:
c.execute(f'PRAGMA index_list("{table}")')
index_rows = c.fetchall()
indexes = []
for r in index_rows:
idx_name = r[1]
is_unique = bool(r[2])
with self._cur() as cc:
cc.execute(f'PRAGMA index_info("{idx_name}")')
cols = [row[2] for row in cc.fetchall()]
indexes.append(IndexInfo(name=idx_name, columns=cols, is_unique=is_unique))
return indexes
def get_foreign_keys(self, database: str, table: str) -> list:
with self._cur() as c:
c.execute(f'PRAGMA foreign_key_list("{table}")')
return [ForeignKeyInfo(
name=f"fk_{r[3]}",
column=r[3], ref_table=r[2], ref_column=r[4],
on_update=r[5] or "", on_delete=r[6] or "",
) for r in c.fetchall()]
def get_table_ddl(self, database: str, table: str) -> str:
with self._cur() as c:
c.execute(
"SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'",
(table,)
)
row = c.fetchone()
return row[0] if row else ""
def get_functions(self, database: str) -> list:
return []
def get_stored_procedures(self, database: str) -> list:
return []
def get_triggers(self, database: str, table: str = "") -> list:
with self._cur() as c:
if table:
c.execute(
"SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name=?",
(table,)
)
else:
c.execute("SELECT name FROM sqlite_master WHERE type='trigger' ORDER BY name")
return [r[0] for r in c.fetchall()]
# ── Query execution ───────────────────────────────────────────────────────
def execute_query(self, sql: str, params: Optional[tuple] = None) -> tuple:
with self._cur() as c:
c.execute(sql, params or ())
if c.description:
cols = [d[0] for d in c.description]
rows = c.fetchall()
return cols, rows, len(rows)
self._connection.commit()
return [], [], c.rowcount
def execute_script(self, sql: str) -> list:
results = []
stmts = [s.strip() for s in sql.split(';') if s.strip()]
for stmt in stmts:
try:
with self._cur() as c:
c.execute(stmt)
if c.description:
cols = [d[0] for d in c.description]
rows = c.fetchall()
results.append((cols, rows, len(rows), ""))
else:
self._connection.commit()
results.append(([], [], c.rowcount,
f"{c.rowcount} row(s) affected"))
except Exception as e:
results.append(([], [], 0, f"Error: {e}"))
return results
# ── Table data CRUD ───────────────────────────────────────────────────────
def get_table_data(self, database: str, table: str,
where: str = "", order_by: str = "",
limit: int = 1000, offset: int = 0) -> tuple:
sql = f'SELECT * FROM "{table}"'
if where: sql += f" WHERE {where}"
if order_by: sql += f" ORDER BY {order_by}"
sql += f" LIMIT {limit} OFFSET {offset}"
return self.execute_query(sql)
def get_table_row_count(self, database: str, table: str, where: str = "") -> int:
sql = f'SELECT COUNT(*) FROM "{table}"'
if where: sql += f" WHERE {where}"
with self._cur() as c:
c.execute(sql)
return c.fetchone()[0]
def insert_row(self, database: str, table: str, data: dict) -> bool:
cols = ", ".join(f'"{c}"' for c in data)
ph = ", ".join(["?"] * len(data))
with self._cur() as c:
c.execute(f'INSERT INTO "{table}" ({cols}) VALUES ({ph})',
tuple(data.values()))
self._connection.commit()
return True
@staticmethod
def _where(where: dict) -> tuple:
parts, params = [], []
for col, val in where.items():
if val is None:
parts.append(f'"{col}" IS NULL')
else:
parts.append(f'"{col}" = ?')
params.append(val)
return " AND ".join(parts), params
def update_row(self, database: str, table: str, data: dict, where: dict) -> bool:
set_cl = ", ".join(f'"{c}" = ?' for c in data)
where_cl, where_params = self._where(where)
with self._cur() as c:
c.execute(f'UPDATE "{table}" SET {set_cl} WHERE {where_cl}',
tuple(data.values()) + tuple(where_params))
self._connection.commit()
return True
def delete_row(self, database: str, table: str, where: dict) -> bool:
where_cl, where_params = self._where(where)
with self._cur() as c:
c.execute(f'DELETE FROM "{table}" WHERE {where_cl}', tuple(where_params))
self._connection.commit()
return True
# ── Server tools ──────────────────────────────────────────────────────────
def explain_query(self, sql: str) -> tuple:
with self._cur() as c:
c.execute(f"EXPLAIN QUERY PLAN {sql}")
cols = [d[0] for d in c.description]
return cols, c.fetchall()
def get_process_list(self) -> tuple:
return ["Info"], [("SQLite does not support process listing.",)]
def kill_process(self, process_id: int) -> bool:
return False
# ── Table designer ────────────────────────────────────────────────────────
def add_column(self, _database: str, table: str, col_name: str,
col_type: str, nullable: bool = True,
default=None) -> bool:
null_clause = "" if nullable else " NOT NULL"
default_clause = f" DEFAULT {default}" if default is not None and default != "" else ""
sql = (f'ALTER TABLE "{table}" '
f'ADD COLUMN "{col_name}" {col_type}{default_clause}{null_clause}')
with self._cur() as c:
c.execute(sql)
self._connection.commit()
return True
def drop_column(self, _database: str, table: str, col_name: str) -> bool:
# Requires SQLite 3.35.0+
with self._cur() as c:
c.execute(f'ALTER TABLE "{table}" DROP COLUMN "{col_name}"')
self._connection.commit()
return True
def rename_column(self, _database: str, table: str,
old_name: str, new_name: str) -> bool:
# Requires SQLite 3.25.0+
with self._cur() as c:
c.execute(f'ALTER TABLE "{table}" RENAME COLUMN "{old_name}" TO "{new_name}"')
self._connection.commit()
return True