Coverage for snekql/mariadb/runtime.py: 90%
146 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 21:13 +0300
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 21:13 +0300
1"""MariaDB adapter for the backend-neutral query runtime."""
3from __future__ import annotations
5import asyncio
6from collections.abc import Awaitable, Callable, Sequence
7from importlib import import_module
8from typing import Any, Literal, cast
10from snekql.errors import (
11 DatabaseClosedError,
12 DatabaseCloseTimeoutError,
13 DatabaseClosingError,
14 DatabaseRuntimeError,
15 PoolTimeoutError,
16)
17from snekql.mariadb.config import Config
18from snekql.mariadb.query import (
19 compile_mariadb_select_sql,
20 compile_mariadb_write_sql,
21 materialize_mariadb_select_row,
22)
23from snekql.mariadb.schema import initialize_mariadb_schema
24from snekql.model import Table
25from snekql.query import AnySelectQuery
26from snekql.storage import SchemaPolicy
27from snekql.structured_logging import ResolvedStructuredLogger
28from snekql.validation import NonNegativeFloat
31def _import_aiomysql() -> Any:
32 """Import the optional MariaDB driver at runtime initialization time."""
34 try:
35 return cast("Any", import_module("aiomysql"))
36 except ModuleNotFoundError as error:
37 if error.name == "aiomysql":
38 msg = "MariaDB runtime requires the aiomysql extra; install with snekql[aiomysql]"
39 raise DatabaseRuntimeError(msg) from error
40 raise
43class MariaDBCursorAdapter:
44 """Runtime cursor adapter backed by an aiomysql cursor."""
46 def __init__(self, cursor: object) -> None:
47 self.cursor: object = cursor
49 async def fetchone(self) -> Sequence[object] | None:
50 row = await cast("Any", self.cursor).fetchone()
51 if row is None:
52 return None
53 return cast("Sequence[object]", row)
55 async def fetchall(self) -> Sequence[Sequence[object]]:
56 rows = await cast("Any", self.cursor).fetchall()
57 return [cast("Sequence[object]", row) for row in rows]
59 async def close(self) -> None:
60 close_result = cast("Any", self.cursor).close()
61 if close_result is not None:
62 _ = await close_result
65class MariaDBConnectionAdapter:
66 """Runtime connection adapter backed by an aiomysql connection."""
68 def __init__(self, connection: object) -> None:
69 self.connection: object = connection
71 async def begin(self) -> None:
72 await cast("Any", self.connection).begin()
74 async def commit(self) -> None:
75 await cast("Any", self.connection).commit()
77 async def rollback(self) -> None:
78 await cast("Any", self.connection).rollback()
80 async def execute(
81 self,
82 sql: str,
83 params: tuple[object, ...],
84 ) -> MariaDBCursorAdapter:
85 cursor = await cast("Any", self.connection).cursor()
86 try:
87 _ = await cursor.execute(sql, params)
88 except Exception:
89 close_result = cursor.close()
90 if close_result is not None:
91 _ = await close_result
92 raise
93 return MariaDBCursorAdapter(cursor)
96class MariaDBConnectionPool:
97 """Small lifecycle wrapper around an aiomysql connection pool."""
99 def __init__(
100 self,
101 pool: object,
102 *,
103 logger: ResolvedStructuredLogger,
104 ) -> None:
105 self.closed: bool = False
106 self.closing: bool = False
107 self.logger: ResolvedStructuredLogger = logger
108 self.pool: object = pool
110 def check_accepting_work(self) -> None:
111 """Reject new work when closed or temporarily closing."""
113 if self.closed:
114 self.logger.warning(
115 "database rejected work", backend="mariadb", reason="closed"
116 )
117 msg = "database is closed"
118 raise DatabaseClosedError(msg)
119 if self.closing:
120 self.logger.warning(
121 "database rejected work", backend="mariadb", reason="closing"
122 )
123 msg = "database is closing"
124 raise DatabaseClosingError(msg)
126 async def acquire(self, acquisition_timeout: NonNegativeFloat) -> object:
127 """Acquire a MariaDB connection within the requested timeout."""
129 self.check_accepting_work()
130 self.logger.debug(
131 "connection acquisition started",
132 backend="mariadb",
133 timeout=acquisition_timeout,
134 )
135 try:
136 pool = cast("Any", self.pool)
137 acquire = cast("Callable[[], Awaitable[object]]", pool.acquire)
138 connection = await asyncio.wait_for(acquire(), timeout=acquisition_timeout)
139 except TimeoutError as error:
140 self.logger.warning(
141 "connection acquisition timed out",
142 backend="mariadb",
143 timeout=acquisition_timeout,
144 )
145 msg = "timed out acquiring database connection"
146 raise PoolTimeoutError(msg) from error
147 self.logger.debug("connection acquired", backend="mariadb")
148 return connection
150 async def release(self, connection: object) -> None:
151 """Return a connection to the underlying aiomysql pool."""
153 release = cast("Any", self.pool).release
154 _ = release(connection)
155 self.logger.debug("connection released", backend="mariadb", closed=False)
157 async def close(self, close_timeout: NonNegativeFloat) -> None:
158 """Close the underlying aiomysql pool and wait for connections."""
160 self.logger.debug("database close started", backend="mariadb")
161 if self.closed:
162 self.logger.debug("database close skipped", backend="mariadb")
163 return
164 self.closing = True
165 try:
166 pool = cast("Any", self.pool)
167 pool.close()
168 wait_closed = cast("Callable[[], Awaitable[None]]", pool.wait_closed)
169 await asyncio.wait_for(wait_closed(), timeout=close_timeout)
170 except TimeoutError as error:
171 self.closing = False
172 self.logger.warning("database close timed out", backend="mariadb")
173 msg = "timed out closing database"
174 raise DatabaseCloseTimeoutError(msg) from error
175 else:
176 self.closed = True
177 self.closing = False
178 self.logger.debug("database close completed", backend="mariadb")
181class MariaDBRuntime:
182 """MariaDB adapter satisfying the backend-neutral runtime seam."""
184 backend_family: Literal["mariadb"] = "mariadb"
186 def __init__(
187 self,
188 *,
189 logger: ResolvedStructuredLogger,
190 acquire_timeout: NonNegativeFloat,
191 connection_pool: MariaDBConnectionPool,
192 ) -> None:
193 self.acquire_timeout: NonNegativeFloat = acquire_timeout
194 self.connection_pool: MariaDBConnectionPool = connection_pool
195 self.logger: ResolvedStructuredLogger = logger
197 async def acquire(
198 self,
199 acquisition_timeout: NonNegativeFloat,
200 ) -> MariaDBConnectionAdapter:
201 connection = await self.connection_pool.acquire(acquisition_timeout)
202 return MariaDBConnectionAdapter(connection)
204 async def release(self, connection: object) -> None:
205 if not isinstance(connection, MariaDBConnectionAdapter):
206 msg = "MariaDB runtime cannot release a foreign connection"
207 raise DatabaseRuntimeError(msg)
208 await self.connection_pool.release(connection.connection)
210 async def close(self, close_timeout: NonNegativeFloat) -> None:
211 await self.connection_pool.close(close_timeout)
213 def check_accepting_work(self) -> None:
214 self.connection_pool.check_accepting_work()
216 def compile_select_sql(
217 self,
218 query: AnySelectQuery,
219 ) -> tuple[str, tuple[object, ...]]:
220 return compile_mariadb_select_sql(query)
222 def compile_write_sql(self, query: object) -> tuple[str, tuple[object, ...]]:
223 return compile_mariadb_write_sql(query)
225 def materialize_select_row(
226 self,
227 query: AnySelectQuery,
228 row: Sequence[object],
229 ) -> object:
230 return materialize_mariadb_select_row(query, row)
233async def initialize_runtime(
234 config: Config,
235 models: Sequence[type[Table[Any]]],
236 schema_policy: SchemaPolicy,
237 *,
238 logger: ResolvedStructuredLogger,
239) -> MariaDBRuntime:
240 """Initialize MariaDB connectivity, schema startup, and pool lifecycle."""
242 aiomysql = _import_aiomysql()
243 logger.debug("mariadb pool opening", host=config.host, port=config.port)
244 pool = await aiomysql.create_pool(
245 autocommit=False,
246 charset=config.charset,
247 connect_timeout=config.acquire_timeout,
248 db=config.database,
249 host=config.host,
250 maxsize=config.pool_size,
251 minsize=1,
252 password=config.password,
253 port=config.port,
254 unix_socket=str(config.unix_socket) if config.unix_socket is not None else None,
255 user=config.user,
256 )
257 connection_pool = MariaDBConnectionPool(pool, logger=logger)
258 connection = await connection_pool.acquire(config.acquire_timeout)
259 try:
260 await initialize_mariadb_schema(
261 connection,
262 models,
263 schema_policy,
264 logger=logger,
265 )
266 except Exception:
267 await connection_pool.release(connection)
268 await connection_pool.close(config.acquire_timeout)
269 raise
270 await connection_pool.release(connection)
271 return MariaDBRuntime(
272 logger=logger,
273 acquire_timeout=config.acquire_timeout,
274 connection_pool=connection_pool,
275 )
278__all__ = [
279 "MariaDBConnectionAdapter",
280 "MariaDBConnectionPool",
281 "MariaDBCursorAdapter",
282 "MariaDBRuntime",
283 "initialize_runtime",
284]