|
| 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. |
0 commit comments