connection
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Async MySQL connection manager with connection pool.
|
||||
|
||||
Design mirrors DorisConnectionManager: a single shared pool, acquired via
|
||||
an async context manager, with an explicit ``close()`` for clean shutdown.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import AsyncGenerator, AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
import aiomysql
|
||||
from aiomysql import Pool
|
||||
|
||||
from .config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Only these SQL keywords are permitted when write access is disabled
|
||||
_READ_ONLY_KEYWORDS = frozenset(
|
||||
{"SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "WITH"}
|
||||
)
|
||||
_FIRST_KEYWORD_RE = re.compile(r"--[^\n]*|/\*.*?\*/", re.DOTALL)
|
||||
|
||||
|
||||
def _first_sql_keyword(sql: str) -> str:
|
||||
stripped = _FIRST_KEYWORD_RE.sub("", sql).strip()
|
||||
return stripped.split(None, 1)[0].upper() if stripped else ""
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""Holds the aiomysql connection pool and enforces write policy."""
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
self.config = config
|
||||
self._pool: Pool | None = None
|
||||
|
||||
async def start(self) -> None:
|
||||
db = self.config.database
|
||||
self._pool = await aiomysql.create_pool(
|
||||
host=db.host,
|
||||
port=db.port,
|
||||
user=db.user,
|
||||
password=db.password,
|
||||
db=db.database or None,
|
||||
minsize=db.pool_min,
|
||||
maxsize=db.pool_max,
|
||||
connect_timeout=db.connect_timeout,
|
||||
autocommit=True,
|
||||
charset="utf8mb4",
|
||||
)
|
||||
logger.info(
|
||||
"Connection pool started: %s:%d db=%s",
|
||||
db.host,
|
||||
db.port,
|
||||
db.database or "(none)",
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
if self._pool is not None:
|
||||
self._pool.close()
|
||||
await self._pool.wait_closed()
|
||||
self._pool = None
|
||||
logger.info("Connection pool closed")
|
||||
|
||||
def _assert_write_allowed(self, sql: str) -> None:
|
||||
keyword = _first_sql_keyword(sql)
|
||||
if not self.config.query.allow_write and keyword not in _READ_ONLY_KEYWORDS:
|
||||
raise PermissionError(
|
||||
f"Write operations are disabled (first keyword: {keyword!r}). "
|
||||
"Set QUERY_ALLOW_WRITE=true to enable DML/DDL."
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def acquire(self) -> AsyncIterator[aiomysql.Connection]:
|
||||
if self._pool is None:
|
||||
raise RuntimeError("Connection pool is not started")
|
||||
async with self._pool.acquire() as conn:
|
||||
yield conn
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
sql: str,
|
||||
args: tuple[Any, ...] | None = None,
|
||||
*,
|
||||
database: str | None = None,
|
||||
max_rows: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute *sql* and return a structured result dict."""
|
||||
self._assert_write_allowed(sql)
|
||||
limit = max_rows if max_rows is not None else self.config.query.max_rows
|
||||
async with self.acquire() as conn:
|
||||
if database:
|
||||
await conn.select_db(database)
|
||||
async with conn.cursor(aiomysql.DictCursor) as cur:
|
||||
await cur.execute(sql, args)
|
||||
rows = await cur.fetchmany(limit + 1)
|
||||
truncated = len(rows) > limit
|
||||
if truncated:
|
||||
rows = rows[:limit]
|
||||
return {
|
||||
"columns": [d[0] for d in cur.description] if cur.description else [],
|
||||
"rows": [dict(r) for r in rows],
|
||||
"row_count": len(rows),
|
||||
"truncated": truncated,
|
||||
"max_rows": limit,
|
||||
}
|
||||
Reference in New Issue
Block a user