Skip to content

Commit ab0fd3e

Browse files
committed
feat: add debug mode for query logging
- Added debug property to SurranticConfig - Added query logging for all database operations - Updated documentation with debug mode examples - Bumped version to 0.1.5
1 parent 8a4c3c1 commit ab0fd3e

File tree

3 files changed

+148
-141
lines changed

3 files changed

+148
-141
lines changed

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,21 @@ You can also override these settings directly in your code using `SurranticConfi
127127
```python
128128
from surrantic import SurranticConfig
129129

130-
# Override all or some of the connection settings
131130
SurranticConfig.configure(
132-
address="ws://mydb:8000",
133-
user="myuser",
134-
password="mypass",
135-
namespace="myns",
136-
database="mydb"
131+
address="ws://localhost:8000",
132+
user="root",
133+
password="root",
134+
namespace="test",
135+
database="test",
136+
debug=True # Enable query logging
137137
)
138+
```
138139

139-
# Your models will now use the new configuration
140-
user = User(name="John", email="john@example.com")
141-
await user.asave() # Uses the custom configuration
140+
When debug mode is enabled, all queries and their results will be logged:
141+
142+
```
143+
DEBUG:surrantic.base:Query: SELECT * FROM user ORDER BY created DESC
144+
DEBUG:surrantic.base:Result: [{"id": "user:123", "name": "John Doe", "email": "john@example.com"}]
142145
```
143146

144147
### Logging

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "surrantic"
3-
version = "0.1.4"
3+
version = "0.1.5"
44
description = "A simple Pydantic ORM implementation for SurrealDB"
55
readme = "README.md"
66
authors = [

src/surrantic/base.py

Lines changed: 135 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import os
44
from contextlib import asynccontextmanager, contextmanager
55
from datetime import datetime, timezone
6-
from typing import (Any, AsyncGenerator, ClassVar, Generator, List, Optional,
7-
Type, TypeVar, Union)
6+
from typing import Any, AsyncGenerator, ClassVar, Generator, List, Optional, Type, TypeVar, Union
87

98
from dotenv import load_dotenv
109
from pydantic import BaseModel, field_serializer
@@ -45,6 +44,14 @@ def _prepare_data(obj: BaseModel) -> str:
4544
items.append(f"{field_name}: {_prepare_value(value)}")
4645
return "{ " + ", ".join(items) + " }"
4746

47+
def _log_query(query: str, result: Any = None) -> None:
48+
"""Log query and result if debug is enabled"""
49+
config = SurranticConfig.get_instance()
50+
if config.debug:
51+
logger.debug("Query: %s", query)
52+
if result is not None:
53+
logger.debug("Result: %s", result)
54+
4855
class SurranticConfig:
4956
"""Configuration class for Surrantic database connection.
5057
@@ -59,6 +66,7 @@ def __init__(self):
5966
self.password = SURREAL_PASS
6067
self.namespace = SURREAL_NAMESPACE
6168
self.database = SURREAL_DATABASE
69+
self.debug = False
6270

6371
@classmethod
6472
def get_instance(cls) -> 'SurranticConfig':
@@ -73,7 +81,8 @@ def configure(cls,
7381
user: Optional[str] = None,
7482
password: Optional[str] = None,
7583
namespace: Optional[str] = None,
76-
database: Optional[str] = None) -> None:
84+
database: Optional[str] = None,
85+
debug: Optional[bool] = None) -> None:
7786
"""Configure the database connection parameters.
7887
7988
Args:
@@ -82,6 +91,7 @@ def configure(cls,
8291
password: The password for authentication
8392
namespace: The namespace to use
8493
database: The database to use
94+
debug: If True, all queries and results will be logged
8595
"""
8696
config = cls.get_instance()
8797
if address is not None:
@@ -94,6 +104,8 @@ def configure(cls,
94104
config.namespace = namespace
95105
if database is not None:
96106
config.database = database
107+
if debug is not None:
108+
config.debug = debug
97109

98110
class ObjectModel(BaseModel):
99111
"""Base model class for SurrealDB objects with CRUD operations.
@@ -170,60 +182,6 @@ def _get_sync_db(cls) -> Generator[SurrealDB, None, None]:
170182
db.close()
171183
logger.debug("Database connection closed")
172184

173-
async def asave(self) -> None:
174-
"""Asynchronously save or update the record in SurrealDB.
175-
176-
Updates the created and updated timestamps automatically.
177-
Creates a new record if id is None, otherwise updates existing record.
178-
179-
Raises:
180-
Exception: If table_name is not defined
181-
RuntimeError: If the database operation fails
182-
"""
183-
if not self.created:
184-
self.created = datetime.now(timezone.utc) # Make created timezone-aware
185-
self.updated = datetime.now(timezone.utc) # Make updated timezone-aware
186-
187-
if type(self).table_name:
188-
table_name = type(self).table_name
189-
else:
190-
raise Exception("No table_name defined")
191-
192-
data = _prepare_data(self)
193-
logger.debug("Prepared data for save: %s", data)
194-
195-
async with self._get_db() as db:
196-
result = await db.query(f"UPSERT {table_name} CONTENT {data}")
197-
self.id = result[0]["result"][0]["id"]
198-
logger.info("Successfully saved record with ID: %s", self.id)
199-
200-
def save(self) -> None:
201-
"""Synchronously save or update the record in SurrealDB.
202-
203-
Updates the created and updated timestamps automatically.
204-
Creates a new record if id is None, otherwise updates existing record.
205-
206-
Raises:
207-
Exception: If table_name is not defined
208-
RuntimeError: If the database operation fails
209-
"""
210-
if not self.created:
211-
self.created = datetime.now(timezone.utc)
212-
self.updated = datetime.now(timezone.utc)
213-
214-
if type(self).table_name:
215-
table_name = type(self).table_name
216-
else:
217-
raise Exception("No table_name defined")
218-
219-
data = _prepare_data(self)
220-
logger.debug("Prepared data for save: %s", data)
221-
222-
with self._get_sync_db() as db:
223-
result = db.query(f"UPSERT {table_name} CONTENT {data}")
224-
self.id = result[0]["result"][0]["id"]
225-
logger.info("Successfully saved record with ID: %s", self.id)
226-
227185
@classmethod
228186
async def aget_all(cls: Type[T], order_by: Optional[str] = None, order_direction: Optional[str] = None) -> List[T]:
229187
"""Asynchronously retrieve all records from the table.
@@ -239,23 +197,19 @@ async def aget_all(cls: Type[T], order_by: Optional[str] = None, order_direction
239197
ValueError: If table_name is not set
240198
RuntimeError: If the database operation fails
241199
"""
242-
try:
243-
if cls.table_name:
244-
target_class = cls
245-
table_name = cls.table_name
246-
else:
247-
raise ValueError("table_name not set in model class")
248-
249-
query = f"SELECT * FROM {table_name}"
250-
if order_by:
251-
query += f" ORDER BY {order_by} {order_direction}"
252-
253-
async with cls._get_db() as db:
254-
results = await db.query(query)
255-
return [target_class(**item) for item in results[0]["result"]]
256-
except Exception as e:
257-
logger.error("Failed to fetch records: %s", str(e), exc_info=True)
258-
raise RuntimeError(f"Failed to fetch records: {str(e)}")
200+
if not cls.table_name:
201+
raise ValueError("table_name must be set")
202+
203+
query = f"SELECT * FROM {cls.table_name}"
204+
if order_by:
205+
direction = order_direction or "ASC"
206+
query += f" ORDER BY {order_by} {direction}"
207+
208+
_log_query(query)
209+
async with cls._get_db() as db:
210+
result = await db.query(query)
211+
_log_query(query, result)
212+
return [cls(**item) for item in result[0]["result"]]
259213

260214
@classmethod
261215
def get_all(cls: Type[T], order_by: Optional[str] = None, order_direction: Optional[str] = None) -> List[T]:
@@ -272,23 +226,19 @@ def get_all(cls: Type[T], order_by: Optional[str] = None, order_direction: Optio
272226
ValueError: If table_name is not set
273227
RuntimeError: If the database operation fails
274228
"""
275-
try:
276-
if cls.table_name:
277-
target_class = cls
278-
table_name = cls.table_name
279-
else:
280-
raise ValueError("table_name not set in model class")
281-
282-
query = f"SELECT * FROM {table_name}"
283-
if order_by:
284-
query += f" ORDER BY {order_by} {order_direction}"
285-
286-
with cls._get_sync_db() as db:
287-
results = db.query(query)
288-
return [target_class(**item) for item in results[0]["result"]]
289-
except Exception as e:
290-
logger.error("Failed to fetch records: %s", str(e), exc_info=True)
291-
raise RuntimeError(f"Failed to fetch records: {str(e)}")
229+
if not cls.table_name:
230+
raise ValueError("table_name must be set")
231+
232+
query = f"SELECT * FROM {cls.table_name}"
233+
if order_by:
234+
direction = order_direction or "ASC"
235+
query += f" ORDER BY {order_by} {direction}"
236+
237+
_log_query(query)
238+
with cls._get_sync_db() as db:
239+
result = db.query(query)
240+
_log_query(query, result)
241+
return [cls(**item) for item in result[0]["result"]]
292242

293243
@classmethod
294244
async def aget(cls: Type[T], id: Union[str, RecordID]) -> Optional[T]:
@@ -303,16 +253,14 @@ async def aget(cls: Type[T], id: Union[str, RecordID]) -> Optional[T]:
303253
Raises:
304254
RuntimeError: If the database operation fails
305255
"""
306-
try:
307-
async with cls._get_db() as db:
308-
results = await db.select(id)
309-
if results is None:
310-
logger.info(f"No record found with ID: {id}")
311-
return None
312-
return cls(**results)
313-
except Exception as e:
314-
logger.error("Failed to fetch record: %s", str(e), exc_info=True)
315-
raise RuntimeError(f"Failed to fetch record: {str(e)}")
256+
query = f"SELECT * FROM {id}"
257+
_log_query(query)
258+
async with cls._get_db() as db:
259+
result = await db.query(query)
260+
_log_query(query, result)
261+
if result and result[0]:
262+
return cls(**result[0][0])
263+
return None
316264

317265
@classmethod
318266
def get(cls: Type[T], id: Union[str, RecordID]) -> Optional[T]:
@@ -327,34 +275,92 @@ def get(cls: Type[T], id: Union[str, RecordID]) -> Optional[T]:
327275
Raises:
328276
RuntimeError: If the database operation fails
329277
"""
330-
try:
331-
with cls._get_sync_db() as db:
332-
results = db.select(id)
333-
if results is None:
334-
logger.info(f"No record found with ID: {id}")
335-
return None
336-
return cls(**results)
337-
except Exception as e:
338-
logger.error("Failed to fetch record: %s", str(e), exc_info=True)
339-
raise RuntimeError(f"Failed to fetch record: {str(e)}")
340-
278+
query = f"SELECT * FROM {id}"
279+
_log_query(query)
280+
with cls._get_sync_db() as db:
281+
result = db.query(query)
282+
_log_query(query, result)
283+
if result and result[0]:
284+
return cls(**result[0][0])
285+
return None
286+
287+
async def asave(self) -> None:
288+
"""Asynchronously save or update the record in SurrealDB.
289+
290+
Updates the created and updated timestamps automatically.
291+
Creates a new record if id is None, otherwise updates existing record.
292+
293+
Raises:
294+
Exception: If table_name is not defined
295+
RuntimeError: If the database operation fails
296+
"""
297+
if not self.table_name:
298+
raise ValueError("table_name must be set")
299+
300+
now = datetime.now(timezone.utc)
301+
if not self.created:
302+
self.created = now
303+
self.updated = now
304+
305+
data = _prepare_data(self)
306+
if self.id:
307+
query = f"UPDATE {self.id} SET {data}"
308+
else:
309+
query = f"CREATE {self.table_name} SET {data}"
310+
311+
_log_query(query)
312+
async with self._get_db() as db:
313+
result = await db.query(query)
314+
_log_query(query, result)
315+
if result and result[0]:
316+
self.id = RecordID.from_string(result[0][0]["id"])
317+
318+
def save(self) -> None:
319+
"""Synchronously save or update the record in SurrealDB.
320+
321+
Updates the created and updated timestamps automatically.
322+
Creates a new record if id is None, otherwise updates existing record.
323+
324+
Raises:
325+
Exception: If table_name is not defined
326+
RuntimeError: If the database operation fails
327+
"""
328+
if not self.table_name:
329+
raise ValueError("table_name must be set")
330+
331+
now = datetime.now(timezone.utc)
332+
if not self.created:
333+
self.created = now
334+
self.updated = now
335+
336+
data = _prepare_data(self)
337+
if self.id:
338+
query = f"UPDATE {self.id} SET {data}"
339+
else:
340+
query = f"CREATE {self.table_name} SET {data}"
341+
342+
_log_query(query)
343+
with self._get_sync_db() as db:
344+
result = db.query(query)
345+
_log_query(query, result)
346+
if result and result[0]:
347+
self.id = RecordID.from_string(result[0][0]["id"])
348+
341349
async def adelete(self) -> None:
342350
"""Asynchronously delete the record from the database.
343351
344352
Raises:
345353
ValueError: If the record has no ID
346354
RuntimeError: If the database operation fails
347355
"""
348-
try:
349-
if not self.id:
350-
raise ValueError("Cannot delete record without id")
356+
if not self.id:
357+
raise ValueError("Cannot delete record without ID")
351358

352-
async with self._get_db() as db:
353-
await db.delete(self.id)
354-
logger.info("Successfully deleted record with ID: %s", self.id)
355-
except Exception as e:
356-
logger.error("Failed to delete record: %s", str(e), exc_info=True)
357-
raise RuntimeError(f"Failed to delete record: {str(e)}")
359+
query = f"DELETE {self.id}"
360+
_log_query(query)
361+
async with self._get_db() as db:
362+
result = await db.query(query)
363+
_log_query(query, result)
358364

359365
def delete(self) -> None:
360366
"""Synchronously delete the record from the database.
@@ -363,13 +369,11 @@ def delete(self) -> None:
363369
ValueError: If the record has no ID
364370
RuntimeError: If the database operation fails
365371
"""
366-
try:
367-
if not self.id:
368-
raise ValueError("Cannot delete record without id")
369-
370-
with self._get_sync_db() as db:
371-
db.delete(self.id)
372-
logger.info("Successfully deleted record with ID: %s", self.id)
373-
except Exception as e:
374-
logger.error("Failed to delete record: %s", str(e), exc_info=True)
375-
raise RuntimeError(f"Failed to delete record: {str(e)}")
372+
if not self.id:
373+
raise ValueError("Cannot delete record without ID")
374+
375+
query = f"DELETE {self.id}"
376+
_log_query(query)
377+
with self._get_sync_db() as db:
378+
result = db.query(query)
379+
_log_query(query, result)

0 commit comments

Comments
 (0)