Initial Codes
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user