sqlspec 0.16.0__cp311-cp311-macosx_13_0_x86_64.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.
Files changed (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-311-darwin.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1347 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +440 -0
  52. sqlspec/builder/_column.py +324 -0
  53. sqlspec/builder/_ddl.py +1383 -0
  54. sqlspec/builder/_ddl_utils.py +104 -0
  55. sqlspec/builder/_delete.py +77 -0
  56. sqlspec/builder/_insert.py +241 -0
  57. sqlspec/builder/_merge.py +56 -0
  58. sqlspec/builder/_parsing_utils.py +140 -0
  59. sqlspec/builder/_select.py +174 -0
  60. sqlspec/builder/_update.py +186 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +195 -0
  63. sqlspec/builder/mixins/_delete_operations.py +36 -0
  64. sqlspec/builder/mixins/_insert_operations.py +152 -0
  65. sqlspec/builder/mixins/_join_operations.py +115 -0
  66. sqlspec/builder/mixins/_merge_operations.py +416 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +123 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +144 -0
  69. sqlspec/builder/mixins/_select_operations.py +599 -0
  70. sqlspec/builder/mixins/_update_operations.py +164 -0
  71. sqlspec/builder/mixins/_where_clause.py +609 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-311-darwin.so +0 -0
  76. sqlspec/core/cache.py +873 -0
  77. sqlspec/core/compiler.cpython-311-darwin.so +0 -0
  78. sqlspec/core/compiler.py +396 -0
  79. sqlspec/core/filters.cpython-311-darwin.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-311-darwin.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-311-darwin.so +0 -0
  84. sqlspec/core/parameters.py +1209 -0
  85. sqlspec/core/result.cpython-311-darwin.so +0 -0
  86. sqlspec/core/result.py +664 -0
  87. sqlspec/core/splitter.cpython-311-darwin.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-311-darwin.so +0 -0
  90. sqlspec/core/statement.py +666 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +472 -0
  93. sqlspec/driver/_common.py +612 -0
  94. sqlspec/driver/_sync.py +473 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +164 -0
  97. sqlspec/driver/mixins/_sql_translator.py +36 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-311-darwin.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +400 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-311-darwin.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-311-darwin.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-311-darwin.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-311-darwin.so +0 -0
  142. sqlspec/utils/type_guards.py +1135 -0
  143. sqlspec-0.16.0.dist-info/METADATA +365 -0
  144. sqlspec-0.16.0.dist-info/RECORD +148 -0
  145. sqlspec-0.16.0.dist-info/WHEEL +4 -0
  146. sqlspec-0.16.0.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.0.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.0.dist-info/licenses/NOTICE +29 -0
sqlspec/core/cache.py ADDED
@@ -0,0 +1,873 @@
1
+ """Caching system with unified cache management.
2
+
3
+ This module provides a caching system with LRU eviction and TTL support for
4
+ SQL statement processing, parameter processing, and expression caching.
5
+
6
+ Components:
7
+ - CacheKey: Immutable cache key with optimized hashing
8
+ - UnifiedCache: Main cache implementation with LRU eviction and TTL
9
+ - StatementCache: Specialized cache for compiled SQL statements
10
+ - ExpressionCache: Specialized cache for parsed SQLGlot expressions
11
+ - ParameterCache: Specialized cache for processed parameters
12
+
13
+ Features:
14
+ - LRU caching with configurable size and TTL
15
+ - Thread-safe cache operations for concurrent access
16
+ - Cached hash values to avoid recomputation
17
+ - O(1) cache lookup and insertion operations
18
+ """
19
+
20
+ import threading
21
+ import time
22
+ from typing import TYPE_CHECKING, Any, Generic, Optional
23
+
24
+ from mypy_extensions import mypyc_attr
25
+ from typing_extensions import TypeVar
26
+
27
+ from sqlspec.utils.logging import get_logger
28
+
29
+ if TYPE_CHECKING:
30
+ import sqlglot.expressions as exp
31
+
32
+ from sqlspec.core.statement import SQL
33
+
34
+ __all__ = (
35
+ "CacheKey",
36
+ "CacheStats",
37
+ "ExpressionCache",
38
+ "ParameterCache",
39
+ "StatementCache",
40
+ "UnifiedCache",
41
+ "get_cache_config",
42
+ "get_default_cache",
43
+ "get_expression_cache",
44
+ "get_parameter_cache",
45
+ "get_statement_cache",
46
+ "sql_cache",
47
+ )
48
+
49
+ T = TypeVar("T")
50
+ CacheValueT = TypeVar("CacheValueT")
51
+
52
+ # Cache configuration constants
53
+ DEFAULT_MAX_SIZE = 10000 # LRU cache size limit
54
+ DEFAULT_TTL_SECONDS = 3600 # 1 hour default TTL
55
+ CACHE_STATS_UPDATE_INTERVAL = 100 # Update stats every N operations
56
+
57
+ # Cache slots - optimized structure
58
+ CACHE_KEY_SLOTS = ("_hash", "_key_data")
59
+ CACHE_NODE_SLOTS = ("key", "value", "prev", "next", "timestamp", "access_count")
60
+ UNIFIED_CACHE_SLOTS = ("_cache", "_lock", "_max_size", "_ttl", "_head", "_tail", "_stats")
61
+ CACHE_STATS_SLOTS = ("hits", "misses", "evictions", "total_operations", "memory_usage")
62
+
63
+
64
+ @mypyc_attr(allow_interpreted_subclasses=True)
65
+ class CacheKey:
66
+ """Immutable cache key with optimized hashing.
67
+
68
+ This class provides an immutable cache key for consistent cache operations
69
+ across all cache types.
70
+
71
+ Features:
72
+ - Cached hash value to avoid recomputation
73
+ - Immutable design for safe sharing across threads
74
+ - Fast equality comparison with short-circuit evaluation
75
+
76
+ Args:
77
+ key_data: Tuple of hashable values that uniquely identify the cached item
78
+ """
79
+
80
+ __slots__ = ("_hash", "_key_data")
81
+
82
+ def __init__(self, key_data: tuple[Any, ...]) -> None:
83
+ """Initialize cache key.
84
+
85
+ Args:
86
+ key_data: Tuple of hashable values for the cache key
87
+ """
88
+ self._key_data = key_data
89
+ self._hash = hash(key_data)
90
+
91
+ @property
92
+ def key_data(self) -> tuple[Any, ...]:
93
+ """Get the key data tuple."""
94
+ return self._key_data
95
+
96
+ def __hash__(self) -> int:
97
+ """Return cached hash value."""
98
+ return self._hash
99
+
100
+ def __eq__(self, other: object) -> bool:
101
+ """Equality comparison with short-circuit evaluation."""
102
+ if type(other) is not CacheKey:
103
+ return False
104
+ other_key = other # type: CacheKey
105
+ if self._hash != other_key._hash:
106
+ return False
107
+ return self._key_data == other_key._key_data
108
+
109
+ def __repr__(self) -> str:
110
+ """String representation of the cache key."""
111
+ return f"CacheKey({self._key_data!r})"
112
+
113
+
114
+ @mypyc_attr(allow_interpreted_subclasses=True)
115
+ class CacheStats:
116
+ """Cache statistics tracking.
117
+
118
+ Tracks cache performance metrics including hit rates, evictions,
119
+ and memory usage.
120
+ """
121
+
122
+ __slots__ = CACHE_STATS_SLOTS
123
+
124
+ def __init__(self) -> None:
125
+ """Initialize cache statistics."""
126
+ self.hits = 0
127
+ self.misses = 0
128
+ self.evictions = 0
129
+ self.total_operations = 0
130
+ self.memory_usage = 0
131
+
132
+ @property
133
+ def hit_rate(self) -> float:
134
+ """Calculate cache hit rate as percentage."""
135
+ total = self.hits + self.misses
136
+ return (self.hits / total * 100) if total > 0 else 0.0
137
+
138
+ @property
139
+ def miss_rate(self) -> float:
140
+ """Calculate cache miss rate as percentage."""
141
+ return 100.0 - self.hit_rate
142
+
143
+ def record_hit(self) -> None:
144
+ """Record a cache hit."""
145
+ self.hits += 1
146
+ self.total_operations += 1
147
+
148
+ def record_miss(self) -> None:
149
+ """Record a cache miss."""
150
+ self.misses += 1
151
+ self.total_operations += 1
152
+
153
+ def record_eviction(self) -> None:
154
+ """Record a cache eviction."""
155
+ self.evictions += 1
156
+
157
+ def reset(self) -> None:
158
+ """Reset all statistics."""
159
+ self.hits = 0
160
+ self.misses = 0
161
+ self.evictions = 0
162
+ self.total_operations = 0
163
+ self.memory_usage = 0
164
+
165
+ def __repr__(self) -> str:
166
+ """String representation of cache statistics."""
167
+ return (
168
+ f"CacheStats(hit_rate={self.hit_rate:.1f}%, "
169
+ f"hits={self.hits}, misses={self.misses}, "
170
+ f"evictions={self.evictions}, ops={self.total_operations})"
171
+ )
172
+
173
+
174
+ @mypyc_attr(allow_interpreted_subclasses=True)
175
+ class CacheNode:
176
+ """Internal cache node for LRU linked list implementation.
177
+
178
+ This class represents a node in the doubly-linked list used for
179
+ LRU cache implementation with O(1) operations.
180
+ """
181
+
182
+ __slots__ = CACHE_NODE_SLOTS
183
+
184
+ def __init__(self, key: CacheKey, value: Any) -> None:
185
+ """Initialize cache node.
186
+
187
+ Args:
188
+ key: Cache key for this node
189
+ value: Cached value
190
+ """
191
+ self.key = key
192
+ self.value = value
193
+ self.prev: Optional[CacheNode] = None
194
+ self.next: Optional[CacheNode] = None
195
+ self.timestamp = time.time()
196
+ self.access_count = 1
197
+
198
+
199
+ @mypyc_attr(allow_interpreted_subclasses=True)
200
+ class UnifiedCache(Generic[CacheValueT]):
201
+ """Cache with LRU eviction and TTL support.
202
+
203
+ This class provides a thread-safe cache implementation with LRU eviction
204
+ and time-based expiration.
205
+
206
+ Features:
207
+ - O(1) cache lookup, insertion, and deletion operations
208
+ - LRU eviction policy with configurable size limits
209
+ - TTL-based expiration for cache entries
210
+ - Thread-safe operations
211
+ - Statistics tracking
212
+
213
+ Args:
214
+ max_size: Maximum number of items to cache (LRU eviction when exceeded)
215
+ ttl_seconds: Time-to-live in seconds (None for no expiration)
216
+ """
217
+
218
+ __slots__ = UNIFIED_CACHE_SLOTS
219
+
220
+ def __init__(self, max_size: int = DEFAULT_MAX_SIZE, ttl_seconds: Optional[int] = DEFAULT_TTL_SECONDS) -> None:
221
+ """Initialize unified cache.
222
+
223
+ Args:
224
+ max_size: Maximum number of cache entries
225
+ ttl_seconds: Time-to-live in seconds (None for no expiration)
226
+ """
227
+ self._cache: dict[CacheKey, CacheNode] = {}
228
+ self._lock = threading.RLock()
229
+ self._max_size = max_size
230
+ self._ttl = ttl_seconds
231
+ self._stats = CacheStats()
232
+
233
+ self._head = CacheNode(CacheKey(()), None)
234
+ self._tail = CacheNode(CacheKey(()), None)
235
+ self._head.next = self._tail
236
+ self._tail.prev = self._head
237
+
238
+ def get(self, key: CacheKey) -> Optional[CacheValueT]:
239
+ """Get value from cache with LRU update.
240
+
241
+ Args:
242
+ key: Cache key to lookup
243
+
244
+ Returns:
245
+ Cached value or None if not found or expired
246
+ """
247
+ with self._lock:
248
+ node: Optional[CacheNode] = self._cache.get(key)
249
+ if node is None:
250
+ self._stats.record_miss()
251
+ return None
252
+
253
+ current_time: float = time.time()
254
+ ttl: Optional[int] = self._ttl
255
+ if ttl is not None and (current_time - node.timestamp) > ttl:
256
+ self._remove_node(node)
257
+ del self._cache[key]
258
+ self._stats.record_miss()
259
+ self._stats.record_eviction()
260
+ return None
261
+
262
+ self._move_to_head(node)
263
+ node.access_count += 1
264
+ self._stats.record_hit()
265
+ return node.value # type: ignore[no-any-return]
266
+
267
+ def put(self, key: CacheKey, value: CacheValueT) -> None:
268
+ """Put value in cache with LRU management.
269
+
270
+ Args:
271
+ key: Cache key
272
+ value: Value to cache
273
+ """
274
+ with self._lock:
275
+ existing_node: Optional[CacheNode] = self._cache.get(key)
276
+ if existing_node is not None:
277
+ existing_node.value = value
278
+ existing_node.timestamp = time.time()
279
+ existing_node.access_count += 1
280
+ self._move_to_head(existing_node)
281
+ return
282
+
283
+ new_node: CacheNode = CacheNode(key, value)
284
+ self._cache[key] = new_node
285
+ self._add_to_head(new_node)
286
+
287
+ cache_size: int = len(self._cache)
288
+ max_size: int = self._max_size
289
+ if cache_size > max_size:
290
+ tail_node: Optional[CacheNode] = self._tail.prev
291
+ if tail_node is not None and tail_node is not self._head:
292
+ self._remove_node(tail_node)
293
+ del self._cache[tail_node.key]
294
+ self._stats.record_eviction()
295
+
296
+ def delete(self, key: CacheKey) -> bool:
297
+ """Delete entry from cache.
298
+
299
+ Args:
300
+ key: Cache key to delete
301
+
302
+ Returns:
303
+ True if key was found and deleted, False otherwise
304
+ """
305
+ with self._lock:
306
+ node: Optional[CacheNode] = self._cache.get(key)
307
+ if node is None:
308
+ return False
309
+
310
+ self._remove_node(node)
311
+ del self._cache[key]
312
+ return True
313
+
314
+ def clear(self) -> None:
315
+ """Clear all cache entries."""
316
+ with self._lock:
317
+ self._cache.clear()
318
+ self._head.next = self._tail
319
+ self._tail.prev = self._head
320
+ self._stats.reset()
321
+
322
+ def size(self) -> int:
323
+ """Get current cache size."""
324
+ return len(self._cache)
325
+
326
+ def is_empty(self) -> bool:
327
+ """Check if cache is empty."""
328
+ return len(self._cache) == 0
329
+
330
+ def get_stats(self) -> CacheStats:
331
+ """Get cache statistics."""
332
+ return self._stats
333
+
334
+ def _add_to_head(self, node: CacheNode) -> None:
335
+ """Add node after head."""
336
+ node.prev = self._head
337
+ head_next: Optional[CacheNode] = self._head.next
338
+ node.next = head_next
339
+ if head_next is not None:
340
+ head_next.prev = node
341
+ self._head.next = node
342
+
343
+ def _remove_node(self, node: CacheNode) -> None:
344
+ """Remove node from linked list."""
345
+ node_prev: Optional[CacheNode] = node.prev
346
+ node_next: Optional[CacheNode] = node.next
347
+ if node_prev is not None:
348
+ node_prev.next = node_next
349
+ if node_next is not None:
350
+ node_next.prev = node_prev
351
+
352
+ def _move_to_head(self, node: CacheNode) -> None:
353
+ """Move existing node to head."""
354
+ self._remove_node(node)
355
+ self._add_to_head(node)
356
+
357
+ def __len__(self) -> int:
358
+ """Get current cache size."""
359
+ return len(self._cache)
360
+
361
+ def __contains__(self, key: CacheKey) -> bool:
362
+ """Check if key exists in cache."""
363
+ with self._lock:
364
+ node: Optional[CacheNode] = self._cache.get(key)
365
+ if node is None:
366
+ return False
367
+
368
+ ttl: Optional[int] = self._ttl
369
+ if ttl is not None:
370
+ current_time: float = time.time()
371
+ if (current_time - node.timestamp) > ttl:
372
+ return False
373
+
374
+ return True
375
+
376
+
377
+ @mypyc_attr(allow_interpreted_subclasses=False)
378
+ class StatementCache:
379
+ """Specialized cache for compiled SQL statements.
380
+
381
+ Caches compiled SQL statements and their execution parameters.
382
+ """
383
+
384
+ def __init__(self, max_size: int = DEFAULT_MAX_SIZE) -> None:
385
+ """Initialize statement cache.
386
+
387
+ Args:
388
+ max_size: Maximum number of statements to cache
389
+ """
390
+ self._cache: UnifiedCache[tuple[str, Any]] = UnifiedCache(max_size)
391
+
392
+ def get_compiled(self, statement: "SQL") -> Optional[tuple[str, Any]]:
393
+ """Get compiled SQL and parameters from cache.
394
+
395
+ Args:
396
+ statement: SQL statement to lookup
397
+
398
+ Returns:
399
+ Tuple of (compiled_sql, parameters) or None if not cached
400
+ """
401
+ cache_key = self._create_statement_key(statement)
402
+ return self._cache.get(cache_key)
403
+
404
+ def put_compiled(self, statement: "SQL", compiled_sql: str, parameters: Any) -> None:
405
+ """Cache compiled SQL and parameters.
406
+
407
+ Args:
408
+ statement: Original SQL statement
409
+ compiled_sql: Compiled SQL string
410
+ parameters: Processed parameters
411
+ """
412
+ cache_key = self._create_statement_key(statement)
413
+ self._cache.put(cache_key, (compiled_sql, parameters))
414
+
415
+ def _create_statement_key(self, statement: "SQL") -> CacheKey:
416
+ """Create cache key for SQL statement.
417
+
418
+ Args:
419
+ statement: SQL statement
420
+
421
+ Returns:
422
+ Cache key for the statement
423
+ """
424
+ # Create key from SQL text, parameters, and configuration
425
+ key_data = (
426
+ "statement",
427
+ statement._raw_sql,
428
+ hash(statement), # Includes parameters and flags
429
+ str(statement.dialect) if statement.dialect else None,
430
+ statement.is_many,
431
+ statement.is_script,
432
+ )
433
+ return CacheKey(key_data)
434
+
435
+ def clear(self) -> None:
436
+ """Clear statement cache."""
437
+ self._cache.clear()
438
+
439
+ def get_stats(self) -> CacheStats:
440
+ """Get cache statistics."""
441
+ return self._cache.get_stats()
442
+
443
+
444
+ @mypyc_attr(allow_interpreted_subclasses=False)
445
+ class ExpressionCache:
446
+ """Specialized cache for parsed SQLGlot expressions.
447
+
448
+ Caches parsed SQLGlot expressions to avoid redundant parsing operations.
449
+ """
450
+
451
+ def __init__(self, max_size: int = DEFAULT_MAX_SIZE) -> None:
452
+ """Initialize expression cache.
453
+
454
+ Args:
455
+ max_size: Maximum number of expressions to cache
456
+ """
457
+ self._cache: UnifiedCache[exp.Expression] = UnifiedCache(max_size)
458
+
459
+ def get_expression(self, sql: str, dialect: Optional[str] = None) -> "Optional[exp.Expression]":
460
+ """Get parsed expression from cache.
461
+
462
+ Args:
463
+ sql: SQL string
464
+ dialect: SQL dialect
465
+
466
+ Returns:
467
+ Parsed expression or None if not cached
468
+ """
469
+ cache_key = self._create_expression_key(sql, dialect)
470
+ return self._cache.get(cache_key)
471
+
472
+ def put_expression(self, sql: str, expression: "exp.Expression", dialect: Optional[str] = None) -> None:
473
+ """Cache parsed expression.
474
+
475
+ Args:
476
+ sql: SQL string
477
+ expression: Parsed SQLGlot expression
478
+ dialect: SQL dialect
479
+ """
480
+ cache_key = self._create_expression_key(sql, dialect)
481
+ self._cache.put(cache_key, expression)
482
+
483
+ def _create_expression_key(self, sql: str, dialect: Optional[str]) -> CacheKey:
484
+ """Create cache key for expression.
485
+
486
+ Args:
487
+ sql: SQL string
488
+ dialect: SQL dialect
489
+
490
+ Returns:
491
+ Cache key for the expression
492
+ """
493
+ key_data = ("expression", sql, dialect)
494
+ return CacheKey(key_data)
495
+
496
+ def clear(self) -> None:
497
+ """Clear expression cache."""
498
+ self._cache.clear()
499
+
500
+ def get_stats(self) -> CacheStats:
501
+ """Get cache statistics."""
502
+ return self._cache.get_stats()
503
+
504
+
505
+ @mypyc_attr(allow_interpreted_subclasses=False)
506
+ class ParameterCache:
507
+ """Specialized cache for processed parameters.
508
+
509
+ Caches processed parameter transformations.
510
+ """
511
+
512
+ def __init__(self, max_size: int = DEFAULT_MAX_SIZE) -> None:
513
+ """Initialize parameter cache.
514
+
515
+ Args:
516
+ max_size: Maximum number of parameter sets to cache
517
+ """
518
+ self._cache: UnifiedCache[Any] = UnifiedCache(max_size)
519
+
520
+ def get_parameters(self, original_params: Any, config_hash: int) -> Optional[Any]:
521
+ """Get processed parameters from cache.
522
+
523
+ Args:
524
+ original_params: Original parameters
525
+ config_hash: Hash of parameter processing configuration
526
+
527
+ Returns:
528
+ Processed parameters or None if not cached
529
+ """
530
+ cache_key = self._create_parameter_key(original_params, config_hash)
531
+ return self._cache.get(cache_key)
532
+
533
+ def put_parameters(self, original_params: Any, processed_params: Any, config_hash: int) -> None:
534
+ """Cache processed parameters.
535
+
536
+ Args:
537
+ original_params: Original parameters
538
+ processed_params: Processed parameters
539
+ config_hash: Hash of parameter processing configuration
540
+ """
541
+ cache_key = self._create_parameter_key(original_params, config_hash)
542
+ self._cache.put(cache_key, processed_params)
543
+
544
+ def _create_parameter_key(self, params: Any, config_hash: int) -> CacheKey:
545
+ """Create cache key for parameters.
546
+
547
+ Args:
548
+ params: Parameters to cache
549
+ config_hash: Configuration hash
550
+
551
+ Returns:
552
+ Cache key for the parameters
553
+ """
554
+ # Create stable key from parameters and configuration
555
+ try:
556
+ if isinstance(params, dict):
557
+ param_key = tuple(sorted(params.items()))
558
+ elif isinstance(params, (list, tuple)):
559
+ param_key = tuple(params)
560
+ else:
561
+ param_key = (params,)
562
+
563
+ key_data = ("parameters", param_key, config_hash)
564
+ return CacheKey(key_data)
565
+ except (TypeError, ValueError):
566
+ param_key = (str(params), type(params).__name__) # type: ignore[assignment]
567
+ key_data = ("parameters", param_key, config_hash)
568
+ return CacheKey(key_data)
569
+
570
+ def clear(self) -> None:
571
+ """Clear parameter cache."""
572
+ self._cache.clear()
573
+
574
+ def get_stats(self) -> CacheStats:
575
+ """Get cache statistics."""
576
+ return self._cache.get_stats()
577
+
578
+
579
+ _default_cache: Optional[UnifiedCache[Any]] = None
580
+ _statement_cache: Optional[StatementCache] = None
581
+ _expression_cache: Optional[ExpressionCache] = None
582
+ _parameter_cache: Optional[ParameterCache] = None
583
+ _cache_lock = threading.Lock()
584
+
585
+
586
+ def get_default_cache() -> UnifiedCache[Any]:
587
+ """Get the default unified cache instance.
588
+
589
+ Returns:
590
+ Singleton default cache instance
591
+ """
592
+ global _default_cache
593
+ if _default_cache is None:
594
+ with _cache_lock:
595
+ if _default_cache is None:
596
+ _default_cache = UnifiedCache[Any]()
597
+ return _default_cache
598
+
599
+
600
+ def get_statement_cache() -> StatementCache:
601
+ """Get the statement cache instance.
602
+
603
+ Returns:
604
+ Singleton statement cache instance
605
+ """
606
+ global _statement_cache
607
+ if _statement_cache is None:
608
+ with _cache_lock:
609
+ if _statement_cache is None:
610
+ _statement_cache = StatementCache()
611
+ return _statement_cache
612
+
613
+
614
+ def get_expression_cache() -> ExpressionCache:
615
+ """Get the expression cache instance.
616
+
617
+ Returns:
618
+ Singleton expression cache instance
619
+ """
620
+ global _expression_cache
621
+ if _expression_cache is None:
622
+ with _cache_lock:
623
+ if _expression_cache is None:
624
+ _expression_cache = ExpressionCache()
625
+ return _expression_cache
626
+
627
+
628
+ def get_parameter_cache() -> ParameterCache:
629
+ """Get the parameter cache instance.
630
+
631
+ Returns:
632
+ Singleton parameter cache instance
633
+ """
634
+ global _parameter_cache
635
+ if _parameter_cache is None:
636
+ with _cache_lock:
637
+ if _parameter_cache is None:
638
+ _parameter_cache = ParameterCache()
639
+ return _parameter_cache
640
+
641
+
642
+ def clear_all_caches() -> None:
643
+ """Clear all cache instances."""
644
+ if _default_cache is not None:
645
+ _default_cache.clear()
646
+ if _statement_cache is not None:
647
+ _statement_cache.clear()
648
+ if _expression_cache is not None:
649
+ _expression_cache.clear()
650
+ if _parameter_cache is not None:
651
+ _parameter_cache.clear()
652
+
653
+
654
+ def get_cache_statistics() -> dict[str, CacheStats]:
655
+ """Get statistics from all cache instances.
656
+
657
+ Returns:
658
+ Dictionary mapping cache type to statistics
659
+ """
660
+ stats = {}
661
+ if _default_cache is not None:
662
+ stats["default"] = _default_cache.get_stats()
663
+ if _statement_cache is not None:
664
+ stats["statement"] = _statement_cache.get_stats()
665
+ if _expression_cache is not None:
666
+ stats["expression"] = _expression_cache.get_stats()
667
+ if _parameter_cache is not None:
668
+ stats["parameter"] = _parameter_cache.get_stats()
669
+ return stats
670
+
671
+
672
+ _global_cache_config: "Optional[CacheConfig]" = None
673
+
674
+
675
+ @mypyc_attr(allow_interpreted_subclasses=True)
676
+ class CacheConfig:
677
+ """Global cache configuration for SQLSpec.
678
+
679
+ Controls caching behavior across the SQLSpec system.
680
+ """
681
+
682
+ def __init__(
683
+ self,
684
+ *,
685
+ compiled_cache_enabled: bool = True,
686
+ sql_cache_enabled: bool = True,
687
+ fragment_cache_enabled: bool = True,
688
+ optimized_cache_enabled: bool = True,
689
+ sql_cache_size: int = 1000,
690
+ fragment_cache_size: int = 5000,
691
+ optimized_cache_size: int = 2000,
692
+ ) -> None:
693
+ """Initialize cache configuration.
694
+
695
+ Args:
696
+ compiled_cache_enabled: Enable compiled SQL caching
697
+ sql_cache_enabled: Enable SQL statement caching
698
+ fragment_cache_enabled: Enable AST fragment caching
699
+ optimized_cache_enabled: Enable optimized expression caching
700
+ sql_cache_size: Maximum SQL cache entries
701
+ fragment_cache_size: Maximum fragment cache entries
702
+ optimized_cache_size: Maximum optimized cache entries
703
+ """
704
+ self.compiled_cache_enabled = compiled_cache_enabled
705
+ self.sql_cache_enabled = sql_cache_enabled
706
+ self.fragment_cache_enabled = fragment_cache_enabled
707
+ self.optimized_cache_enabled = optimized_cache_enabled
708
+ self.sql_cache_size = sql_cache_size
709
+ self.fragment_cache_size = fragment_cache_size
710
+ self.optimized_cache_size = optimized_cache_size
711
+
712
+
713
+ def get_cache_config() -> CacheConfig:
714
+ """Get the global cache configuration.
715
+
716
+ Returns:
717
+ Current global cache configuration instance
718
+ """
719
+ global _global_cache_config
720
+ if _global_cache_config is None:
721
+ _global_cache_config = CacheConfig()
722
+ return _global_cache_config
723
+
724
+
725
+ def update_cache_config(config: CacheConfig) -> None:
726
+ """Update the global cache configuration.
727
+
728
+ This clears all existing caches when configuration changes to ensure
729
+ consistency with the new settings.
730
+
731
+ Args:
732
+ config: New cache configuration to apply globally
733
+ """
734
+ logger = get_logger("sqlspec.cache")
735
+ logger.info("Cache configuration updated: %s", config)
736
+
737
+ global _global_cache_config
738
+ _global_cache_config = config
739
+
740
+ unified_cache = get_default_cache()
741
+ unified_cache.clear()
742
+ statement_cache = get_statement_cache()
743
+ statement_cache.clear()
744
+
745
+ logger = get_logger("sqlspec.cache")
746
+ logger.info(
747
+ "Cache configuration updated - all caches cleared",
748
+ extra={
749
+ "compiled_cache_enabled": config.compiled_cache_enabled,
750
+ "sql_cache_enabled": config.sql_cache_enabled,
751
+ "fragment_cache_enabled": config.fragment_cache_enabled,
752
+ "optimized_cache_enabled": config.optimized_cache_enabled,
753
+ },
754
+ )
755
+
756
+
757
+ @mypyc_attr(allow_interpreted_subclasses=True)
758
+ class CacheStatsAggregate:
759
+ """Aggregated cache statistics from all cache instances."""
760
+
761
+ __slots__ = (
762
+ "fragment_capacity",
763
+ "fragment_hit_rate",
764
+ "fragment_hits",
765
+ "fragment_misses",
766
+ "fragment_size",
767
+ "optimized_capacity",
768
+ "optimized_hit_rate",
769
+ "optimized_hits",
770
+ "optimized_misses",
771
+ "optimized_size",
772
+ "sql_capacity",
773
+ "sql_hit_rate",
774
+ "sql_hits",
775
+ "sql_misses",
776
+ "sql_size",
777
+ )
778
+
779
+ def __init__(self) -> None:
780
+ """Initialize aggregated cache statistics."""
781
+ self.sql_hit_rate = 0.0
782
+ self.fragment_hit_rate = 0.0
783
+ self.optimized_hit_rate = 0.0
784
+ self.sql_size = 0
785
+ self.fragment_size = 0
786
+ self.optimized_size = 0
787
+ self.sql_capacity = 0
788
+ self.fragment_capacity = 0
789
+ self.optimized_capacity = 0
790
+ self.sql_hits = 0
791
+ self.sql_misses = 0
792
+ self.fragment_hits = 0
793
+ self.fragment_misses = 0
794
+ self.optimized_hits = 0
795
+ self.optimized_misses = 0
796
+
797
+
798
+ def get_cache_stats() -> CacheStatsAggregate:
799
+ """Get current cache statistics from all caches.
800
+
801
+ Returns:
802
+ Combined cache statistics object
803
+ """
804
+ stats_dict = get_cache_statistics()
805
+ stats = CacheStatsAggregate()
806
+
807
+ for cache_name, cache_stats in stats_dict.items():
808
+ hits = cache_stats.hits
809
+ misses = cache_stats.misses
810
+ size = 0
811
+
812
+ if "sql" in cache_name.lower():
813
+ stats.sql_hits += hits
814
+ stats.sql_misses += misses
815
+ stats.sql_size += size
816
+ elif "fragment" in cache_name.lower():
817
+ stats.fragment_hits += hits
818
+ stats.fragment_misses += misses
819
+ stats.fragment_size += size
820
+ elif "optimized" in cache_name.lower():
821
+ stats.optimized_hits += hits
822
+ stats.optimized_misses += misses
823
+ stats.optimized_size += size
824
+
825
+ sql_total = stats.sql_hits + stats.sql_misses
826
+ if sql_total > 0:
827
+ stats.sql_hit_rate = stats.sql_hits / sql_total
828
+
829
+ fragment_total = stats.fragment_hits + stats.fragment_misses
830
+ if fragment_total > 0:
831
+ stats.fragment_hit_rate = stats.fragment_hits / fragment_total
832
+
833
+ optimized_total = stats.optimized_hits + stats.optimized_misses
834
+ if optimized_total > 0:
835
+ stats.optimized_hit_rate = stats.optimized_hits / optimized_total
836
+
837
+ return stats
838
+
839
+
840
+ def reset_cache_stats() -> None:
841
+ """Reset all cache statistics."""
842
+ clear_all_caches()
843
+
844
+
845
+ def log_cache_stats() -> None:
846
+ """Log current cache statistics using the configured logger."""
847
+ logger = get_logger("sqlspec.cache")
848
+ stats = get_cache_stats()
849
+ logger.info("Cache Statistics: %s", stats)
850
+
851
+
852
+ @mypyc_attr(allow_interpreted_subclasses=False)
853
+ class SQLCompilationCache:
854
+ """Wrapper around StatementCache for compatibility."""
855
+
856
+ __slots__ = ("_statement_cache", "_unified_cache")
857
+
858
+ def __init__(self) -> None:
859
+ self._statement_cache = get_statement_cache()
860
+ self._unified_cache = get_default_cache()
861
+
862
+ def get(self, cache_key: str) -> Optional[tuple[str, Any]]:
863
+ """Get cached compiled SQL and parameters."""
864
+ key = CacheKey((cache_key,))
865
+ return self._unified_cache.get(key)
866
+
867
+ def set(self, cache_key: str, value: tuple[str, Any]) -> None:
868
+ """Set cached compiled SQL and parameters."""
869
+ key = CacheKey((cache_key,))
870
+ self._unified_cache.put(key, value)
871
+
872
+
873
+ sql_cache = SQLCompilationCache()