sqlspec 0.13.1__py3-none-any.whl → 0.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (110) hide show
  1. sqlspec/__init__.py +39 -1
  2. sqlspec/adapters/adbc/config.py +4 -40
  3. sqlspec/adapters/adbc/driver.py +29 -16
  4. sqlspec/adapters/aiosqlite/config.py +2 -20
  5. sqlspec/adapters/aiosqlite/driver.py +36 -18
  6. sqlspec/adapters/asyncmy/config.py +2 -33
  7. sqlspec/adapters/asyncmy/driver.py +23 -16
  8. sqlspec/adapters/asyncpg/config.py +5 -39
  9. sqlspec/adapters/asyncpg/driver.py +41 -18
  10. sqlspec/adapters/bigquery/config.py +2 -43
  11. sqlspec/adapters/bigquery/driver.py +26 -14
  12. sqlspec/adapters/duckdb/config.py +2 -49
  13. sqlspec/adapters/duckdb/driver.py +35 -16
  14. sqlspec/adapters/oracledb/config.py +4 -83
  15. sqlspec/adapters/oracledb/driver.py +54 -27
  16. sqlspec/adapters/psqlpy/config.py +2 -55
  17. sqlspec/adapters/psqlpy/driver.py +28 -8
  18. sqlspec/adapters/psycopg/config.py +4 -73
  19. sqlspec/adapters/psycopg/driver.py +69 -24
  20. sqlspec/adapters/sqlite/config.py +3 -21
  21. sqlspec/adapters/sqlite/driver.py +50 -26
  22. sqlspec/cli.py +248 -0
  23. sqlspec/config.py +18 -20
  24. sqlspec/driver/_async.py +28 -10
  25. sqlspec/driver/_common.py +5 -4
  26. sqlspec/driver/_sync.py +28 -10
  27. sqlspec/driver/mixins/__init__.py +6 -0
  28. sqlspec/driver/mixins/_cache.py +114 -0
  29. sqlspec/driver/mixins/_pipeline.py +0 -4
  30. sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
  31. sqlspec/driver/mixins/_result_utils.py +0 -2
  32. sqlspec/driver/mixins/_sql_translator.py +0 -2
  33. sqlspec/driver/mixins/_storage.py +4 -18
  34. sqlspec/driver/mixins/_type_coercion.py +0 -2
  35. sqlspec/driver/parameters.py +4 -4
  36. sqlspec/extensions/aiosql/adapter.py +4 -4
  37. sqlspec/extensions/litestar/__init__.py +2 -1
  38. sqlspec/extensions/litestar/cli.py +48 -0
  39. sqlspec/extensions/litestar/plugin.py +3 -0
  40. sqlspec/loader.py +1 -1
  41. sqlspec/migrations/__init__.py +23 -0
  42. sqlspec/migrations/base.py +390 -0
  43. sqlspec/migrations/commands.py +525 -0
  44. sqlspec/migrations/runner.py +215 -0
  45. sqlspec/migrations/tracker.py +153 -0
  46. sqlspec/migrations/utils.py +89 -0
  47. sqlspec/protocols.py +37 -3
  48. sqlspec/statement/builder/__init__.py +8 -8
  49. sqlspec/statement/builder/{column.py → _column.py} +82 -52
  50. sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
  51. sqlspec/statement/builder/_ddl_utils.py +1 -1
  52. sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
  53. sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
  54. sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
  55. sqlspec/statement/builder/_parsing_utils.py +5 -3
  56. sqlspec/statement/builder/{select.py → _select.py} +59 -61
  57. sqlspec/statement/builder/{update.py → _update.py} +2 -2
  58. sqlspec/statement/builder/mixins/__init__.py +24 -30
  59. sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
  60. sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
  61. sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
  62. sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
  63. sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
  64. sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
  65. sqlspec/statement/builder/mixins/_select_operations.py +612 -0
  66. sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
  67. sqlspec/statement/builder/mixins/_where_clause.py +536 -0
  68. sqlspec/statement/cache.py +50 -0
  69. sqlspec/statement/filters.py +37 -8
  70. sqlspec/statement/parameters.py +154 -25
  71. sqlspec/statement/pipelines/__init__.py +1 -1
  72. sqlspec/statement/pipelines/context.py +4 -4
  73. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
  74. sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
  75. sqlspec/statement/pipelines/validators/_performance.py +1 -5
  76. sqlspec/statement/sql.py +246 -176
  77. sqlspec/utils/__init__.py +2 -1
  78. sqlspec/utils/statement_hashing.py +203 -0
  79. sqlspec/utils/type_guards.py +32 -0
  80. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/METADATA +1 -1
  81. sqlspec-0.14.0.dist-info/RECORD +143 -0
  82. sqlspec-0.14.0.dist-info/entry_points.txt +2 -0
  83. sqlspec/service/__init__.py +0 -4
  84. sqlspec/service/_util.py +0 -147
  85. sqlspec/service/pagination.py +0 -26
  86. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  87. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  88. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  89. sqlspec/statement/builder/mixins/_from.py +0 -63
  90. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  91. sqlspec/statement/builder/mixins/_having.py +0 -35
  92. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  93. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  94. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  95. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  96. sqlspec/statement/builder/mixins/_returning.py +0 -37
  97. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  98. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  99. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  100. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  101. sqlspec/statement/builder/mixins/_where.py +0 -401
  102. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  103. sqlspec/statement/parameter_manager.py +0 -220
  104. sqlspec/statement/sql_compiler.py +0 -140
  105. sqlspec-0.13.1.dist-info/RECORD +0 -150
  106. /sqlspec/statement/builder/{base.py → _base.py} +0 -0
  107. /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
  108. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
  109. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
  110. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/config.py CHANGED
@@ -47,10 +47,6 @@ logger = get_logger("config")
47
47
  class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
48
48
  """Protocol defining the interface for database configurations."""
49
49
 
50
- # Note: __slots__ cannot be used with dataclass fields in Python < 3.10
51
- # Concrete subclasses can still use __slots__ for any additional attributes
52
- __slots__ = ()
53
-
54
50
  is_async: "ClassVar[bool]" = field(init=False, default=False)
55
51
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=False)
56
52
  supports_native_arrow_import: "ClassVar[bool]" = field(init=False, default=False)
@@ -61,12 +57,24 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
61
57
  driver_type: "type[DriverT]" = field(init=False, repr=False, hash=False, compare=False)
62
58
  pool_instance: "Optional[PoolT]" = field(default=None)
63
59
  default_row_type: "type[Any]" = field(init=False)
60
+ migration_config: "dict[str, Any]" = field(default_factory=dict)
61
+ """Migration configuration settings."""
64
62
  _dialect: "DialectType" = field(default=None, init=False, repr=False, hash=False, compare=False)
65
63
 
64
+ # Adapter-level cache configuration
65
+ enable_adapter_cache: bool = field(default=True)
66
+ """Enable adapter-level SQL compilation caching."""
67
+ adapter_cache_size: int = field(default=500)
68
+ """Maximum number of compiled SQL statements to cache per adapter."""
69
+ enable_prepared_statements: bool = field(default=False)
70
+ """Enable prepared statement pooling for supported databases."""
71
+ prepared_statement_cache_size: int = field(default=100)
72
+ """Maximum number of prepared statements to maintain."""
73
+
66
74
  supported_parameter_styles: "ClassVar[tuple[str, ...]]" = ()
67
75
  """Parameter styles supported by this database adapter (e.g., ('qmark', 'named_colon'))."""
68
76
 
69
- preferred_parameter_style: "ClassVar[str]" = "none"
77
+ default_parameter_style: "ClassVar[str]" = "none"
70
78
  """The preferred/native parameter style for this database."""
71
79
 
72
80
  def __hash__(self) -> int:
@@ -85,7 +93,7 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
85
93
  The SQL dialect type for this database.
86
94
  """
87
95
  if self._dialect is None:
88
- self._dialect = self._get_dialect() # type: ignore[misc]
96
+ self._dialect = self._get_dialect()
89
97
  return self._dialect
90
98
 
91
99
  def _get_dialect(self) -> "DialectType":
@@ -177,8 +185,6 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
177
185
  class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
178
186
  """Base class for a sync database configurations that do not implement a pool."""
179
187
 
180
- __slots__ = ()
181
-
182
188
  is_async: "ClassVar[bool]" = field(init=False, default=False)
183
189
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=False)
184
190
  pool_instance: None = None
@@ -209,8 +215,6 @@ class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
209
215
  class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
210
216
  """Base class for an async database configurations that do not implement a pool."""
211
217
 
212
- __slots__ = ()
213
-
214
218
  is_async: "ClassVar[bool]" = field(init=False, default=True)
215
219
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=False)
216
220
  pool_instance: None = None
@@ -241,15 +245,11 @@ class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
241
245
  class GenericPoolConfig:
242
246
  """Generic Database Pool Configuration."""
243
247
 
244
- __slots__ = ()
245
-
246
248
 
247
249
  @dataclass
248
250
  class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
249
251
  """Generic Sync Database Configuration."""
250
252
 
251
- __slots__ = ()
252
-
253
253
  is_async: "ClassVar[bool]" = field(init=False, default=False)
254
254
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=True)
255
255
 
@@ -261,7 +261,7 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
261
261
  """
262
262
  if self.pool_instance is not None:
263
263
  return self.pool_instance
264
- self.pool_instance = self._create_pool() # type: ignore[misc]
264
+ self.pool_instance = self._create_pool()
265
265
  return self.pool_instance
266
266
 
267
267
  def close_pool(self) -> None:
@@ -271,7 +271,7 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
271
271
  def provide_pool(self, *args: Any, **kwargs: Any) -> PoolT:
272
272
  """Provide pool instance."""
273
273
  if self.pool_instance is None:
274
- self.pool_instance = self.create_pool() # type: ignore[misc]
274
+ self.pool_instance = self.create_pool()
275
275
  return self.pool_instance
276
276
 
277
277
  def create_connection(self) -> ConnectionT:
@@ -301,8 +301,6 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
301
301
  class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
302
302
  """Generic Async Database Configuration."""
303
303
 
304
- __slots__ = ()
305
-
306
304
  is_async: "ClassVar[bool]" = field(init=False, default=True)
307
305
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=True)
308
306
 
@@ -314,7 +312,7 @@ class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
314
312
  """
315
313
  if self.pool_instance is not None:
316
314
  return self.pool_instance
317
- self.pool_instance = await self._create_pool() # type: ignore[misc]
315
+ self.pool_instance = await self._create_pool()
318
316
  return self.pool_instance
319
317
 
320
318
  async def close_pool(self) -> None:
@@ -324,7 +322,7 @@ class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
324
322
  async def provide_pool(self, *args: Any, **kwargs: Any) -> PoolT:
325
323
  """Provide pool instance."""
326
324
  if self.pool_instance is None:
327
- self.pool_instance = await self.create_pool() # type: ignore[misc]
325
+ self.pool_instance = await self.create_pool()
328
326
  return self.pool_instance
329
327
 
330
328
  async def create_connection(self) -> ConnectionT:
sqlspec/driver/_async.py CHANGED
@@ -207,17 +207,23 @@ class AsyncDriverAdapterProtocol(CommonDriverAttributesMixin[ConnectionT, RowT],
207
207
  _config: "Optional[SQLConfig]" = None,
208
208
  **kwargs: Any,
209
209
  ) -> "SQLResult[RowT]":
210
- _filters, param_sequence = process_execute_many_parameters(parameters)
210
+ """Execute statement multiple times with different parameters.
211
211
 
212
- # For execute_many, disable transformations to prevent literal extraction
213
- # since the SQL already has placeholders for bulk operations
214
- many_config = _config or self.config
215
- if many_config.enable_transformations:
216
- from dataclasses import replace
212
+ Now passes first parameter set through pipeline to enable
213
+ literal extraction and consistent parameter processing.
214
+ """
215
+ filters, param_sequence = process_execute_many_parameters(parameters)
216
+
217
+ # Process first parameter set through pipeline for literal extraction
218
+ first_params = param_sequence[0] if param_sequence else None
217
219
 
218
- many_config = replace(many_config, enable_transformations=False)
220
+ # Build statement with first params to trigger pipeline processing
221
+ sql_statement = self._build_statement(
222
+ statement, first_params, *filters, _config=_config or self.config, **kwargs
223
+ )
219
224
 
220
- sql_statement = self._build_statement(statement, _config=many_config, **kwargs).as_many(param_sequence)
225
+ # Mark as many with full sequence
226
+ sql_statement = sql_statement.as_many(param_sequence)
221
227
 
222
228
  return await self._execute_statement(
223
229
  statement=sql_statement, connection=self._connection(_connection), **kwargs
@@ -230,14 +236,26 @@ class AsyncDriverAdapterProtocol(CommonDriverAttributesMixin[ConnectionT, RowT],
230
236
  *parameters: "Union[StatementParameters, StatementFilter]",
231
237
  _connection: "Optional[ConnectionT]" = None,
232
238
  _config: "Optional[SQLConfig]" = None,
239
+ _suppress_warnings: bool = False, # New parameter for migrations
233
240
  **kwargs: Any,
234
241
  ) -> "SQLResult[RowT]":
242
+ """Execute a multi-statement script.
243
+
244
+ By default, validates each statement and logs warnings for dangerous
245
+ operations. Use _suppress_warnings=True for migrations and admin scripts.
246
+ """
235
247
  script_config = _config or self.config
236
- if script_config.enable_validation:
237
- script_config = replace(script_config, enable_validation=False, strict_mode=False)
248
+
249
+ # Keep validation enabled by default
250
+ # Validators will log warnings for dangerous operations
238
251
 
239
252
  sql_statement = self._build_statement(statement, *parameters, _config=script_config, **kwargs)
240
253
  sql_statement = sql_statement.as_script()
254
+
255
+ # Pass suppress warnings flag to execution
256
+ if _suppress_warnings:
257
+ kwargs["_suppress_warnings"] = True
258
+
241
259
  return await self._execute_statement(
242
260
  statement=sql_statement, connection=self._connection(_connection), **kwargs
243
261
  )
sqlspec/driver/_common.py CHANGED
@@ -9,7 +9,7 @@ import sqlglot
9
9
  from sqlglot import exp
10
10
  from sqlglot.tokens import TokenType
11
11
 
12
- from sqlspec.driver.parameters import normalize_parameter_sequence
12
+ from sqlspec.driver.parameters import convert_parameter_sequence
13
13
  from sqlspec.exceptions import NotFoundError
14
14
  from sqlspec.statement import SQLConfig
15
15
  from sqlspec.statement.parameters import ParameterStyle, ParameterValidator, TypedParameter
@@ -60,6 +60,7 @@ class CommonDriverAttributesMixin(ABC, Generic[ConnectionT, RowT]):
60
60
  config: SQL statement configuration
61
61
  default_row_type: Default row type for results (DictRow, TupleRow, etc.)
62
62
  """
63
+ super().__init__()
63
64
  self.connection = connection
64
65
  self.config = config or SQLConfig()
65
66
  self.default_row_type = default_row_type or dict[str, Any]
@@ -335,11 +336,11 @@ class CommonDriverAttributesMixin(ABC, Generic[ConnectionT, RowT]):
335
336
  Parameters with TypedParameter objects unwrapped to primitive values
336
337
  """
337
338
 
338
- normalized = normalize_parameter_sequence(parameters)
339
- if not normalized:
339
+ converted = convert_parameter_sequence(parameters)
340
+ if not converted:
340
341
  return []
341
342
 
342
- return [self._coerce_parameter(p) if isinstance(p, TypedParameter) else p for p in normalized]
343
+ return [self._coerce_parameter(p) if isinstance(p, TypedParameter) else p for p in converted]
343
344
 
344
345
  def _prepare_driver_parameters_many(self, parameters: Any) -> "list[Any]":
345
346
  """Prepare parameter sequences for executemany operations.
sqlspec/driver/_sync.py CHANGED
@@ -205,17 +205,23 @@ class SyncDriverAdapterProtocol(CommonDriverAttributesMixin[ConnectionT, RowT],
205
205
  _config: "Optional[SQLConfig]" = None,
206
206
  **kwargs: Any,
207
207
  ) -> "SQLResult[RowT]":
208
- _filters, param_sequence = process_execute_many_parameters(parameters)
208
+ """Execute statement multiple times with different parameters.
209
209
 
210
- # For execute_many, disable transformations to prevent literal extraction
211
- # since the SQL already has placeholders for bulk operations
212
- many_config = _config or self.config
213
- if many_config.enable_transformations:
214
- from dataclasses import replace
210
+ Now passes first parameter set through pipeline to enable
211
+ literal extraction and consistent parameter processing.
212
+ """
213
+ filters, param_sequence = process_execute_many_parameters(parameters)
214
+
215
+ # Process first parameter set through pipeline for literal extraction
216
+ first_params = param_sequence[0] if param_sequence else None
215
217
 
216
- many_config = replace(many_config, enable_transformations=False)
218
+ # Build statement with first params to trigger pipeline processing
219
+ sql_statement = self._build_statement(
220
+ statement, first_params, *filters, _config=_config or self.config, **kwargs
221
+ )
217
222
 
218
- sql_statement = self._build_statement(statement, _config=many_config, **kwargs).as_many(param_sequence)
223
+ # Mark as many with full sequence
224
+ sql_statement = sql_statement.as_many(param_sequence)
219
225
 
220
226
  return self._execute_statement(statement=sql_statement, connection=self._connection(_connection), **kwargs)
221
227
 
@@ -226,12 +232,24 @@ class SyncDriverAdapterProtocol(CommonDriverAttributesMixin[ConnectionT, RowT],
226
232
  *parameters: "Union[StatementParameters, StatementFilter]",
227
233
  _connection: "Optional[ConnectionT]" = None,
228
234
  _config: "Optional[SQLConfig]" = None,
235
+ _suppress_warnings: bool = False, # New parameter for migrations
229
236
  **kwargs: Any,
230
237
  ) -> "SQLResult[RowT]":
238
+ """Execute a multi-statement script.
239
+
240
+ By default, validates each statement and logs warnings for dangerous
241
+ operations. Use _suppress_warnings=True for migrations and admin scripts.
242
+ """
231
243
  script_config = _config or self.config
232
- if script_config.enable_validation:
233
- script_config = replace(script_config, enable_validation=False, strict_mode=False)
244
+
245
+ # Keep validation enabled by default
246
+ # Validators will log warnings for dangerous operations
234
247
 
235
248
  sql_statement = self._build_statement(statement, *parameters, _config=script_config, **kwargs)
236
249
  sql_statement = sql_statement.as_script()
250
+
251
+ # Pass suppress warnings flag to execution
252
+ if _suppress_warnings:
253
+ kwargs["_suppress_warnings"] = True
254
+
237
255
  return self._execute_statement(statement=sql_statement, connection=self._connection(_connection), **kwargs)
@@ -1,16 +1,22 @@
1
1
  """Driver mixins for instrumentation, storage, and utilities."""
2
2
 
3
+ from sqlspec.driver.mixins._cache import AsyncAdapterCacheMixin, SyncAdapterCacheMixin
3
4
  from sqlspec.driver.mixins._pipeline import AsyncPipelinedExecutionMixin, SyncPipelinedExecutionMixin
5
+ from sqlspec.driver.mixins._query_tools import AsyncQueryMixin, SyncQueryMixin
4
6
  from sqlspec.driver.mixins._result_utils import ToSchemaMixin
5
7
  from sqlspec.driver.mixins._sql_translator import SQLTranslatorMixin
6
8
  from sqlspec.driver.mixins._storage import AsyncStorageMixin, SyncStorageMixin
7
9
  from sqlspec.driver.mixins._type_coercion import TypeCoercionMixin
8
10
 
9
11
  __all__ = (
12
+ "AsyncAdapterCacheMixin",
10
13
  "AsyncPipelinedExecutionMixin",
14
+ "AsyncQueryMixin",
11
15
  "AsyncStorageMixin",
12
16
  "SQLTranslatorMixin",
17
+ "SyncAdapterCacheMixin",
13
18
  "SyncPipelinedExecutionMixin",
19
+ "SyncQueryMixin",
14
20
  "SyncStorageMixin",
15
21
  "ToSchemaMixin",
16
22
  "TypeCoercionMixin",
@@ -0,0 +1,114 @@
1
+ """Adapter-level caching mixin for compiled SQL and prepared statements."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Optional
4
+
5
+ from sqlspec.statement.cache import SQLCache
6
+ from sqlspec.statement.parameters import ParameterStyle
7
+
8
+ if TYPE_CHECKING:
9
+ from sqlspec.statement.sql import SQL
10
+
11
+ __all__ = ("AsyncAdapterCacheMixin", "SyncAdapterCacheMixin")
12
+
13
+
14
+ class SyncAdapterCacheMixin:
15
+ """Mixin for adapter-level SQL compilation caching.
16
+
17
+ This mixin provides:
18
+ - Compiled SQL caching to avoid repeated compilation
19
+ - Parameter style conversion caching
20
+ - Prepared statement name management (for supported databases)
21
+
22
+ Integrates transparently with existing adapter execution flow.
23
+ """
24
+
25
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
26
+ """Initialize adapter with caching support."""
27
+ super().__init__(*args, **kwargs)
28
+
29
+ # Get cache configuration from config or use defaults
30
+ config = getattr(self, "config", None)
31
+ cache_size = getattr(config, "adapter_cache_size", 500) if config else 500
32
+ enable_cache = getattr(config, "enable_adapter_cache", True) if config else True
33
+
34
+ # Initialize caches
35
+ self._compiled_cache: Optional[SQLCache] = SQLCache(max_size=cache_size) if enable_cache else None
36
+ self._prepared_statements: dict[str, str] = {}
37
+ self._prepared_counter = 0
38
+
39
+ def _get_compiled_sql(self, statement: "SQL", target_style: ParameterStyle) -> tuple[str, Any]:
40
+ """Get compiled SQL with caching.
41
+
42
+ Args:
43
+ statement: SQL statement to compile
44
+ target_style: Target parameter style for compilation
45
+
46
+ Returns:
47
+ Tuple of (compiled_sql, parameters)
48
+ """
49
+ if self._compiled_cache is None:
50
+ # Caching disabled
51
+ return statement.compile(placeholder_style=target_style)
52
+
53
+ # Generate cache key
54
+ cache_key = self._adapter_cache_key(statement, target_style)
55
+
56
+ # Check cache
57
+ cached = self._compiled_cache.get(cache_key)
58
+ if cached is not None:
59
+ return cached # type: ignore[no-any-return]
60
+
61
+ # Compile and cache
62
+ result = statement.compile(placeholder_style=target_style)
63
+ self._compiled_cache.set(cache_key, result)
64
+ return result
65
+
66
+ def _adapter_cache_key(self, statement: "SQL", style: ParameterStyle) -> str:
67
+ """Generate adapter-specific cache key.
68
+
69
+ Args:
70
+ statement: SQL statement
71
+ style: Parameter style
72
+
73
+ Returns:
74
+ Cache key string
75
+ """
76
+ # Use statement's internal cache key which includes SQL hash, params, and dialect
77
+ base_key = statement._cache_key()
78
+ # Add adapter-specific context
79
+ return f"{self.__class__.__name__}:{style.value}:{base_key}"
80
+
81
+ def _get_or_create_prepared_statement_name(self, sql_hash: str) -> str:
82
+ """Get or create a prepared statement name for the given SQL.
83
+
84
+ Used by PostgreSQL and other databases that support prepared statements.
85
+
86
+ Args:
87
+ sql_hash: Hash of the SQL statement
88
+
89
+ Returns:
90
+ Prepared statement name
91
+ """
92
+ if sql_hash in self._prepared_statements:
93
+ return self._prepared_statements[sql_hash]
94
+
95
+ # Create new prepared statement name
96
+ self._prepared_counter += 1
97
+ stmt_name = f"sqlspec_ps_{self._prepared_counter}"
98
+ self._prepared_statements[sql_hash] = stmt_name
99
+ return stmt_name
100
+
101
+ def _clear_adapter_cache(self) -> None:
102
+ """Clear all adapter-level caches."""
103
+ if self._compiled_cache is not None:
104
+ self._compiled_cache.clear()
105
+ self._prepared_statements.clear()
106
+ self._prepared_counter = 0
107
+
108
+
109
+ class AsyncAdapterCacheMixin(SyncAdapterCacheMixin):
110
+ """Async version of AdapterCacheMixin.
111
+
112
+ Identical to AdapterCacheMixin since caching operations are synchronous.
113
+ Provided for naming consistency with async adapters.
114
+ """
@@ -54,8 +54,6 @@ class PipelineOperation:
54
54
  class SyncPipelinedExecutionMixin:
55
55
  """Mixin providing pipeline execution for sync drivers."""
56
56
 
57
- __slots__ = ()
58
-
59
57
  def pipeline(
60
58
  self,
61
59
  *,
@@ -87,8 +85,6 @@ class SyncPipelinedExecutionMixin:
87
85
  class AsyncPipelinedExecutionMixin:
88
86
  """Async version of pipeline execution mixin."""
89
87
 
90
- __slots__ = ()
91
-
92
88
  def pipeline(
93
89
  self,
94
90
  *,