Skip to content

Commit 54435ab

Browse files
committed
closer...
1 parent f614041 commit 54435ab

File tree

130 files changed

+7429
-8081
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

130 files changed

+7429
-8081
lines changed

docs/INTERNAL_REFERENCE.md

Lines changed: 176 additions & 1430 deletions
Large diffs are not rendered by default.

docs/REFACTORING_DRIVERS.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Refactoring Guide: SQLSpec Driver Adapters (`_execute_impl`)
2+
3+
This guide outlines the steps to refactor existing SQLSpec driver adapters to align with the changes to the `_execute_impl` method signature and the handling of batch (`is_many`) and script (`is_script`) execution modes.
4+
5+
**Core Principle**: The `_execute_impl` method in driver adapters now has a simplified signature. Information about whether an operation is a batch execution (`is_many`) or a script execution (`is_script`) is now part of the `SQL` object itself (accessible via `statement.is_many` and `statement.is_script`). The `parameters` and `config` are also primarily sourced from the `SQL` object.
6+
7+
## General Steps for Refactoring `_execute_impl`
8+
9+
1. **Update `_execute_impl` Signature**:
10+
* The new signature for both synchronous and asynchronous drivers is:
11+
12+
```python
13+
# For async drivers
14+
async def _execute_impl(
15+
self,
16+
statement: SQL, # The fully prepared SQL object
17+
connection: Optional[YourDriverConnectionType] = None,
18+
**kwargs: Any, # For any remaining driver-specific execution options
19+
) -> Any: # Raw result from the database driver
20+
```
21+
22+
```python
23+
# For sync drivers
24+
def _execute_impl(
25+
self,
26+
statement: SQL, # The fully prepared SQL object
27+
connection: Optional[YourDriverConnectionType] = None,
28+
**kwargs: Any, # For any remaining driver-specific execution options
29+
) -> Any: # Raw result from the database driver
30+
```
31+
32+
* Remove `parameters`, `config`, `is_many`, and `is_script` from the method parameters.
33+
34+
2. **Access Execution Mode from `SQL` Object**:
35+
* Inside `_execute_impl`, determine the execution mode using the `SQL` object's properties:
36+
* `if statement.is_script:`
37+
* `if statement.is_many:`
38+
39+
3. **Access SQL String and Parameters from `SQL` Object**:
40+
* **SQL String**: Get the final SQL string to execute using `statement.to_sql(placeholder_style=self._get_placeholder_style())`.
41+
* For scripts (`statement.is_script`), use `statement.to_sql(placeholder_style=ParameterStyle.STATIC)` to get the raw SQL as scripts usually don't use dynamic placeholders in the same way.
42+
* **Parameters**: Access the processed and merged parameters directly from `statement.parameters` or `statement._merged_parameters` (the latter is often more reliable for the final list/dict after processing).
43+
* For `is_many=True`, `statement.parameters` (or `statement._merged_parameters`) will typically be a sequence of parameter sets (e.g., a list of tuples or list of dicts).
44+
* For single execution, it will be a single parameter set (e.g., a tuple or dict) or `None`.
45+
46+
4. **Handle `is_script` Logic**:
47+
* If `statement.is_script` is true:
48+
* Generate SQL using `ParameterStyle.STATIC`.
49+
* Use the database driver's method for executing scripts (e.g., `cursor.executescript()` for SQLite, `connection.execute()` for asyncpg for simple scripts).
50+
* Parameters are usually not passed separately for scripts; they should be embedded if the script syntax supports it, or `ParameterStyle.STATIC` should have rendered them in.
51+
52+
5. **Handle `is_many` Logic (Batch Execution)**:
53+
* If `statement.is_many` is true:
54+
* Ensure `statement.parameters` is treated as a sequence of parameter sets.
55+
* Use the database driver's batch execution method (e.g., `cursor.executemany()`).
56+
57+
6. **Handle Single Execution Logic**:
58+
* If not `is_script` and not `is_many`:
59+
* Use the database driver's standard single statement execution method (e.g., `cursor.execute()`, `connection.execute()`, `connection.fetch()`).
60+
* Pass the `statement.parameters` (appropriately formatted as a tuple or dict if needed by the driver API) along with the SQL string.
61+
62+
7. **Configuration**: The `SQL` object's `statement.config` is available if any specific configuration is needed at this level, but generally, the SQL string and parameters from the `statement` object should be pre-configured.
63+
64+
8. **Remove `config` Parameter Usage**: If `_execute_impl` was previously using the `config` parameter to re-copy or re-configure the statement, this logic should now be unnecessary as the input `statement: SQL` object is assumed to be fully prepared by the base driver protocol before `_execute_impl` is called.
65+
66+
9. **Update Calls in `select_to_arrow` (if applicable)**:
67+
* If your driver implements an Arrow-specific method like `select_to_arrow` that internally calls helper methods which construct SQL or use parameters, ensure those helpers also now rely on the `SQL` object for parameters rather than taking them as separate arguments if they were refactored similarly.
68+
* Typically, `select_to_arrow` would construct its own `SQL` object or receive one, and then pass the necessary SQL string and parameters to the underlying DB-API call. Ensure it uses `stmt_obj.to_sql(...)` and `stmt_obj.parameters` correctly.
69+
70+
## Example: Refactoring `_execute_impl` for an Async Driver
71+
72+
**Old Signature Example**:
73+
74+
```python
75+
# async def _execute_impl(
76+
# self,
77+
# statement: SQL,
78+
# parameters: Optional[SQLParameterType] = None,
79+
# connection: Optional[YourConnectionType] = None,
80+
# config: Optional[SQLConfig] = None, # Old
81+
# is_many: bool = False, # Old
82+
# is_script: bool = False, # Old
83+
# **kwargs: Any,
84+
# ) -> Any:
85+
# conn = self._connection(connection)
86+
# final_sql = statement.to_sql(placeholder_style=self._get_placeholder_style())
87+
# if is_script:
88+
# # ... script logic with final_sql ...
89+
# elif is_many:
90+
# # ... executemany logic with final_sql and parameters ...
91+
# else:
92+
# # ... execute logic with final_sql and parameters ...
93+
```
94+
95+
**New Signature and Logic Example**:
96+
97+
```python
98+
from sqlspec.statement.sql import SQL
99+
from sqlspec.statement.parameters import ParameterStyle # For ParameterStyle.STATIC
100+
from typing import Optional, Any
101+
102+
# Assuming YourConnectionType is defined
103+
104+
async def _execute_impl(
105+
self,
106+
statement: SQL, # SQL object now carries all necessary info
107+
connection: Optional[YourConnectionType] = None,
108+
**kwargs: Any,
109+
) -> Any:
110+
conn = self._connection(connection)
111+
112+
if statement.is_script:
113+
final_sql = statement.to_sql(placeholder_style=ParameterStyle.STATIC)
114+
# Example: return await conn.execute_script_raw(final_sql) # Hypothetical driver method
115+
# For asyncpg, it might just be: return await conn.execute(final_sql)
116+
# Ensure no separate parameters are passed if the script is self-contained.
117+
return await conn.execute(final_sql) # Example for asyncpg
118+
119+
final_sql = statement.to_sql(placeholder_style=self._get_placeholder_style())
120+
params_to_execute = statement.parameters # This should be the final list/dict
121+
122+
if statement.is_many:
123+
# Ensure params_to_execute is a sequence of sequences/dicts
124+
# Example: return await conn.executemany(final_sql, params_to_execute)
125+
pass # Replace with actual driver call
126+
else:
127+
# Ensure params_to_execute is a single sequence/dict or None
128+
# Example: return await conn.execute(final_sql, *params_to_execute) # If driver expects *args
129+
# Example: return await conn.execute(final_sql, params_to_execute) # If driver expects a list/tuple or dict
130+
pass # Replace with actual driver call
131+
132+
# Remember to handle return values appropriate for your driver (cursors, status strings, etc.)
133+
```
134+
135+
**Key Points**:
136+
137+
* The `SQL` object passed to `_execute_impl` is the single source of truth for the SQL string, its parameters, and execution mode (script/many).
138+
* The base driver adapter (`SyncDriverAdapterProtocol` / `AsyncDriverAdapterProtocol`) methods (`execute`, `execute_many`, `execute_script`) are responsible for preparing the `SQL` object correctly (e.g., calling `sql_obj.as_many()` or `sql_obj.as_script()`) before calling `_execute_impl`.
139+
* Individual drivers no longer need to interpret `is_many` or `is_script` bools passed as parameters.
140+
141+
This refactor simplifies the `_execute_impl` interface and centralizes query information within the `SQL` object, leading to cleaner and more maintainable driver adapters.

docs/REFACTORING_PROCESSORS.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Refactoring Guide: SQLSpec Statement Processors
2+
3+
## ✅ MIGRATION COMPLETE
4+
5+
The refactoring of SQLSpec statement processors has been **successfully completed**. All processor classes now use the unified `process(context: SQLProcessingContext)` interface.
6+
7+
## What Was Accomplished
8+
9+
### ✅ Unified Interface Implementation
10+
11+
All processor classes now implement the `ProcessorProtocol` interface with a single `process(context)` method:
12+
13+
- **Validators**: `CartesianProductDetector`, `InjectionDetector`, `TautologyDetector`, etc.
14+
- **Transformers**: `ParameterizeLiterals`, `RemoveComments`, `RemoveHints`
15+
- **Analyzers**: `StatementAnalyzer`
16+
17+
### ✅ Legacy Code Removal
18+
19+
- Removed old abstract base classes: `SQLValidation`, `SQLTransformation`, `SQLAnalysis`
20+
- Removed the `UnifiedProcessor` class that was using the old interfaces
21+
- Cleaned up dead code and legacy compatibility bridges
22+
- Updated all tests to use the new interfaces
23+
24+
### ✅ Bug Fixes
25+
26+
- Fixed critical bug in `SQLValidator.process()` where unsafe validation results were ignored due to incorrect boolean evaluation
27+
- All validation results are now properly aggregated regardless of their safety status
28+
29+
### ✅ Test Coverage
30+
31+
- All 168+ pipeline tests passing
32+
- Tests updated to use new `SQLProcessingContext` interface
33+
- Removed tests for deprecated abstract base classes
34+
35+
## Current Architecture
36+
37+
### ProcessorProtocol Interface
38+
39+
```python
40+
class ProcessorProtocol(ABC, Generic[ExpressionT]):
41+
@abstractmethod
42+
def process(self, context: SQLProcessingContext) -> tuple[ExpressionT, Optional[ValidationResult]]:
43+
"""Process an SQL expression using the provided context."""
44+
```
45+
46+
### SQLProcessingContext
47+
48+
The unified context object that carries:
49+
50+
- Current SQL expression
51+
- Configuration settings
52+
- Dialect information
53+
- Parameter information
54+
- Extracted parameters from transformers
55+
- Analysis results
56+
57+
### StatementPipeline
58+
59+
Orchestrates the execution of processors in sequence:
60+
61+
1. **Transformation Stage**: Modifies SQL expressions
62+
2. **Validation Stage**: Checks for security and quality issues
63+
3. **Analysis Stage**: Extracts metadata and insights
64+
65+
## Benefits Achieved
66+
67+
1. **Unified Interface**: Single `process(context)` method for all processors
68+
2. **Better Context Sharing**: Rich context object enables better communication between processors
69+
3. **Improved Performance**: Reduced redundant parsing and processing
70+
4. **Enhanced Maintainability**: Cleaner, more consistent codebase
71+
5. **Better Testing**: More focused and reliable test coverage
72+
73+
## Migration Notes
74+
75+
- **No Backwards Compatibility**: Old abstract base classes have been completely removed
76+
- **All Processors Updated**: Every processor class now uses the new interface
77+
- **Tests Modernized**: All tests use the new `SQLProcessingContext` approach
78+
- **Documentation Updated**: This guide reflects the completed state
79+
80+
The SQLSpec statement processing pipeline is now fully modernized and ready for future enhancements!

docs/examples/demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -994,7 +994,7 @@ def main() -> None:
994994
# 🎉 Conclusion
995995
demo_conclusion()
996996

997-
except Exception as e: # noqa: BLE001
997+
except Exception as e:
998998
console.print(f"[red]❌ Demo error: {e}[/red]")
999999
console.print_exception()
10001000
finally:
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
## [REF-001] Instrumentation Architecture: Context Managers vs Decorators
2+
3+
**DECISION**: Migrated from decorator-based to context manager-based instrumentation.
4+
5+
**IMPLEMENTATION**:
6+
7+
- **Protocol Layer**: Public methods (`execute`, `execute_many`, `execute_script`) use context managers
8+
- **Driver Layer**: Private methods (`_execute_impl`, `_wrap_select_result`) use context managers
9+
- **Context Managers**: `instrument_operation()` (sync) and `instrument_operation_async()` (async)
10+
11+
**USER BENEFIT**:
12+
13+
- Clean type signatures (no decorator interference)
14+
- Multi-level telemetry (API + driver level)
15+
- Comprehensive tracking of database operations
16+
17+
**CODE EXAMPLES**:
18+
19+
```python
20+
# User calls this
21+
result = driver.execute("SELECT * FROM users")
22+
23+
# Results in telemetry hierarchy:
24+
# 1. High-level: "execute" operation (API usage)
25+
# 2. Low-level: "psycopg_execute" operation (database access)
26+
# 3. Low-level: "psycopg_wrap_select" operation (result processing)
27+
```
28+
29+
**TELEMETRY COVERAGE**:
30+
31+
- OpenTelemetry spans with proper attributes
32+
- Prometheus metrics (counters, histograms, gauges)
33+
- Structured logging with context
34+
- Error tracking and latency monitoring
35+
36+
---
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## [REF-002] Psycopg Driver: ModelDTOT and Schema Type Patterns
2+
3+
**DECISION**: Preserve exact `ModelDTOT` and `schema_type` behavior from main branch.
4+
5+
**IMPLEMENTATION**:
6+
7+
- `SelectResult.rows` always contains `dict[str, Any]` objects
8+
- Schema conversion handled by type system and result converter patterns
9+
- `_wrap_select_result` uses conditional return types based on `schema_type` parameter
10+
11+
**USER BENEFIT**:
12+
13+
- Type-safe result conversion with intelligent typing
14+
- Seamless integration with DTO patterns
15+
- Backwards compatibility with existing code
16+
17+
**CODE EXAMPLES**:
18+
19+
```python
20+
# With schema type - gets SelectResult[User]
21+
users = driver.execute("SELECT * FROM users", schema_type=User)
22+
23+
# Without schema type - gets SelectResult[dict[str, Any]]
24+
raw_data = driver.execute("SELECT * FROM users")
25+
26+
# Both work, but typing provides safety
27+
user_name = users.rows[0].name # ✅ Type-safe
28+
user_name = raw_data.rows[0]["name"] # ✅ Dict access
29+
```
30+
31+
**OVERLOAD PATTERNS**:
32+
33+
```python
34+
@overload
35+
def execute(statement: SelectBuilder, *, schema_type: type[ModelDTOT]) -> SelectResult[ModelDTOT]: ...
36+
37+
@overload
38+
def execute(statement: SelectBuilder, *, schema_type: None = None) -> SelectResult[dict[str, Any]]: ...
39+
```
40+
41+
---
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
## [REF-003] Connection Pool Lifecycle and Instrumentation
2+
3+
**DECISION**: Instrument pool operations for observability into connection management.
4+
5+
**IMPLEMENTATION**:
6+
7+
- Pool creation: `_create_pool_impl()` with timing and logging
8+
- Pool closure: `_close_pool_impl()` with cleanup tracking
9+
- Connection provision: Context managers for connection lifecycle
10+
- Session provision: Context managers for driver instances
11+
12+
**USER BENEFIT**:
13+
14+
- Visibility into pool health and performance
15+
- Connection leak detection capabilities
16+
- Pool sizing optimization data
17+
18+
**CONFIG PATTERNS**:
19+
20+
```python
21+
# TypedDict approach for clean configuration
22+
config = PsycopgAsyncConfig(
23+
pool_config={
24+
"min_size": 5,
25+
"max_size": 20,
26+
"max_lifetime": 3600,
27+
},
28+
instrumentation=InstrumentationConfig(
29+
log_pool_operations=True,
30+
enable_prometheus=True,
31+
)
32+
)
33+
34+
# Usage
35+
async with config.provide_session() as driver:
36+
result = await driver.execute("SELECT 1")
37+
```
38+
39+
**INSTRUMENTATION POINTS**:
40+
41+
- Pool create/destroy operations
42+
- Connection acquire/release timing
43+
- Pool size and utilization metrics
44+
- Connection error rates and types
45+
46+
---

0 commit comments

Comments
 (0)