Skip to content

Commit 523e48b

Browse files
authored
feat(result): add schema_type parameter to SQLResult helper methods (#106)
Adds optional `schema_type` parameter to `SQLResult` data retrieval methods, enabling direct type-safe data transformation across the entire result API. Also refactors `to_schema()` functionality into a standalone utility module to eliminate circular imports.
1 parent 94c482f commit 523e48b

File tree

19 files changed

+632
-359
lines changed

19 files changed

+632
-359
lines changed

docs/examples/litestar_asyncpg.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ async def get_version(db_session: AsyncpgDriver) -> dict[str, Any]:
4747
"""Get PostgreSQL version information."""
4848
result = await db_session.execute(SQL("SELECT version() as version"))
4949

50-
if result.data:
51-
return {"database": "PostgreSQL", "version": result.data[0]["version"][:50] + "..."}
50+
version_row = result.one_or_none()
51+
if version_row:
52+
return {"database": "PostgreSQL", "version": version_row["version"][:50] + "..."}
5253
return {"error": "Could not retrieve version"}
5354

5455

@@ -62,8 +63,9 @@ async def list_tables(db_session: AsyncpgDriver) -> dict[str, Any]:
6263
ORDER BY table_name
6364
""")
6465

65-
if result.data:
66-
tables = [row["table_name"] for row in result.data]
66+
tables_data = result.all()
67+
if tables_data:
68+
tables = [row["table_name"] for row in tables_data]
6769
return {"tables": tables, "count": len(tables)}
6870
return {"tables": [], "count": 0}
6971

docs/examples/litestar_duckllm.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ class ChatMessage(Struct):
3333

3434
@post("/chat", sync_to_thread=True)
3535
def duckllm_chat(db_session: DuckDBDriver, data: ChatMessage) -> ChatMessage:
36-
results = db_session.execute("SELECT open_prompt(?)", data.message).get_first()
37-
return db_session.to_schema(results or {"message": "No response from DuckLLM"}, schema_type=ChatMessage)
36+
result = db_session.execute("SELECT open_prompt(?)", data.message)
37+
messages = result.get_data(schema_type=ChatMessage)
38+
return messages[0] if messages else ChatMessage(message="No response from DuckLLM")
3839

3940

4041
spec = SQLSpec()

docs/examples/standalone_demo.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -659,16 +659,17 @@ def interactive() -> None:
659659
spec_temp.add_config(db_config)
660660
with spec_temp.provide_session(type(db_config)) as driver:
661661
result = driver.execute(sql_obj)
662-
if result.data:
663-
console.print(f"[green]Returned {len(result.data)} rows[/green]")
664-
if len(result.data) <= MAX_ROWS_TO_DISPLAY:
665-
for row in result.data:
662+
data = result.all()
663+
if data:
664+
console.print(f"[green]Returned {len(data)} rows[/green]")
665+
if len(data) <= MAX_ROWS_TO_DISPLAY:
666+
for row in data:
666667
console.print(f" {row}")
667668
else:
668669
console.print(" First 3 rows:")
669-
for row in result.data[:3]:
670+
for row in data[:3]:
670671
console.print(f" {row}")
671-
console.print(f" ... and {len(result.data) - 3} more")
672+
console.print(f" ... and {len(data) - 3} more")
672673
except Exception as e:
673674
console.print(f"[yellow]Query built successfully but execution failed: {e}[/yellow]")
674675

docs/extensions/litestar/dependency_injection.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Injects the SQLSpec driver instance with full query capabilities (recommended).
6969
@get("/users")
7070
async def get_users(db_session: AsyncpgDriver) -> dict:
7171
result = await db_session.execute("SELECT * FROM users")
72-
return {"users": result.data}
72+
return {"users": result.all()}
7373
7474
**When to use**: All standard database operations (recommended).
7575

@@ -92,7 +92,7 @@ Dependencies are resolved by type annotation:
9292
async def handler(db_session: AsyncpgDriver) -> dict:
9393
# SQLSpec injects AsyncpgDriver instance
9494
result = await db_session.execute("SELECT * FROM users")
95-
return {"users": result.data}
95+
return {"users": result.all()}
9696
9797
By Dependency Key
9898
-----------------
@@ -234,7 +234,7 @@ Use specific driver types for better type checking:
234234
# IDE knows exact driver types
235235
pg_result = await postgres.execute("SELECT * FROM users")
236236
duck_result = await duckdb.execute("SELECT * FROM events")
237-
return {"pg": pg_result.data, "duck": duck_result.data}
237+
return {"pg": pg_result.all(), "duck": duck_result.all()}
238238
239239
Best Practices
240240
==============
@@ -252,7 +252,7 @@ Prefer ``db_session`` for standard database operations:
252252
@get("/users")
253253
async def get_users(db_session: AsyncpgDriver) -> dict:
254254
result = await db_session.execute("SELECT * FROM users")
255-
return {"users": result.data}
255+
return {"users": result.all()}
256256
257257
# Advanced: Use connection only when needed
258258
@get("/bulk-import")

docs/extensions/litestar/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Here's a simple example of creating a Litestar application with SQLSpec integrat
6666
@get("/users")
6767
async def list_users(db_session: AsyncpgDriver) -> dict:
6868
result = await db_session.execute("SELECT * FROM users LIMIT 10")
69-
return {"users": result.data}
69+
return {"users": result.all()}
7070
7171
@post("/users")
7272
async def create_user(

docs/extensions/litestar/quickstart.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Define route handlers that use dependency injection to access the database:
8989
@get("/users")
9090
async def list_users(db_session: AsyncpgDriver) -> dict:
9191
result = await db_session.execute("SELECT * FROM users LIMIT 10")
92-
return {"users": result.data}
92+
return {"users": result.all()}
9393
9494
@get("/users/{user_id:int}")
9595
async def get_user(
@@ -184,7 +184,7 @@ Here's a complete working example:
184184
result = await db_session.execute(
185185
"SELECT id, name, email FROM users ORDER BY id LIMIT 10"
186186
)
187-
return {"users": result.data}
187+
return {"users": result.all()}
188188
189189
@get("/users/{user_id:int}")
190190
async def get_user(

docs/reference/core.rst

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,36 +137,48 @@ Result Processing
137137

138138
**Attributes:**
139139

140-
- ``data`` - List of result rows (dicts)
140+
- ``data`` - List of result rows (dicts) - prefer using helper methods instead
141141
- ``rows_affected`` - Number of rows affected by INSERT/UPDATE/DELETE
142142
- ``columns`` - Column names
143143
- ``metadata`` - Query metadata
144144

145-
**Methods:**
145+
**Recommended Helper Methods:**
146146

147147
.. code-block:: python
148148
149149
result = await session.execute("SELECT * FROM users")
150150
151-
# Access data
152-
all_rows = result.data
151+
# Get all rows (replaces result.data)
152+
all_rows = result.all()
153+
154+
# Get exactly one row (raises if not exactly one)
155+
user = result.one()
156+
157+
# Get one row or None (raises if multiple)
158+
user = result.one_or_none()
159+
160+
# Get first row without validation
153161
first_row = result.get_first()
154-
first_row_or_none = result.get_first_or_none()
155162
156-
# Map to types
163+
# Get scalar value (first column of first row)
164+
count = result.scalar()
165+
166+
# Map to typed models
157167
from pydantic import BaseModel
158168
159169
class User(BaseModel):
160170
id: int
161171
name: str
162172
email: str
163173
164-
users = result.as_type(User) # List[User]
165-
user = result.as_type_one(User) # User
166-
user_or_none = result.as_type_one_or_none(User) # User | None
174+
# Get all rows as typed models
175+
users: list[User] = result.all(schema_type=User)
176+
177+
# Get exactly one row as typed model
178+
user: User = result.one(schema_type=User)
167179
168-
# Get scalar value
169-
count = result.scalar() # First column of first row
180+
# Get one or none as typed model
181+
user: User | None = result.one_or_none(schema_type=User)
170182
171183
**Type Mapping:**
172184

docs/reference/extensions.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ Plugin
156156
@get("/users")
157157
async def get_users(db: AsyncpgDriver) -> list[dict]:
158158
result = await db.select("SELECT * FROM users")
159-
return result.data
159+
return result.all()
160160
161161
app = Litestar(route_handlers=[get_users], plugins=[plugin])
162162

docs/reference/index.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,10 @@ Common Workflows
139139
**Processing Results:**
140140

141141
1. Execute query: ``result = await session.execute(sql)``
142-
2. Access data: ``result.data`` (list of dicts)
143-
3. Get first row: ``result.get_first()``
144-
4. Map to models: ``result.as_type(User)``
142+
2. Get all rows: ``result.all()`` (list of dicts)
143+
3. Get one row: ``result.one()`` (raises if not exactly one)
144+
4. Get first row: ``result.get_first()`` (returns first or None)
145+
5. Map to models: ``result.all(schema_type=User)`` or ``result.one(schema_type=User)``
145146

146147
See Also
147148
========

docs/usage/data_flow.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,15 @@ SQLSpec can automatically map results to typed objects:
269269
email: str
270270
is_active: Optional[bool] = True
271271
272-
# Execute with schema type
273-
result = session.execute(
274-
"SELECT id, name, email, is_active FROM users",
275-
schema_type=User
276-
)
272+
# Execute query
273+
result = session.execute("SELECT id, name, email, is_active FROM users")
274+
275+
# Map results to typed User instances
276+
users: list[User] = result.all(schema_type=User)
277277
278-
# Results are typed User instances
279-
users: list[User] = result.to_schema()
280-
user: User = result.one() # Type-safe!
278+
# Or get single typed user
279+
single_result = session.execute("SELECT id, name, email, is_active FROM users WHERE id = ?", 1)
280+
user: User = single_result.one(schema_type=User) # Type-safe!
281281
282282
**Supported Schema Types**
283283

0 commit comments

Comments
 (0)