sqlspec 0.24.1__py3-none-any.whl → 0.26.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.
- sqlspec/_serialization.py +223 -21
- sqlspec/_sql.py +20 -62
- sqlspec/_typing.py +11 -0
- sqlspec/adapters/adbc/config.py +8 -1
- sqlspec/adapters/adbc/data_dictionary.py +290 -0
- sqlspec/adapters/adbc/driver.py +129 -20
- sqlspec/adapters/adbc/type_converter.py +159 -0
- sqlspec/adapters/aiosqlite/config.py +3 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
- sqlspec/adapters/aiosqlite/driver.py +17 -3
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/config.py +11 -8
- sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
- sqlspec/adapters/asyncmy/driver.py +31 -7
- sqlspec/adapters/asyncpg/config.py +3 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
- sqlspec/adapters/asyncpg/driver.py +19 -4
- sqlspec/adapters/bigquery/config.py +3 -0
- sqlspec/adapters/bigquery/data_dictionary.py +109 -0
- sqlspec/adapters/bigquery/driver.py +21 -3
- sqlspec/adapters/bigquery/type_converter.py +93 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/config.py +2 -0
- sqlspec/adapters/duckdb/data_dictionary.py +124 -0
- sqlspec/adapters/duckdb/driver.py +32 -5
- sqlspec/adapters/duckdb/pool.py +1 -1
- sqlspec/adapters/duckdb/type_converter.py +103 -0
- sqlspec/adapters/oracledb/config.py +6 -0
- sqlspec/adapters/oracledb/data_dictionary.py +442 -0
- sqlspec/adapters/oracledb/driver.py +68 -9
- sqlspec/adapters/oracledb/migrations.py +51 -67
- sqlspec/adapters/oracledb/type_converter.py +132 -0
- sqlspec/adapters/psqlpy/config.py +3 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
- sqlspec/adapters/psqlpy/driver.py +23 -179
- sqlspec/adapters/psqlpy/type_converter.py +73 -0
- sqlspec/adapters/psycopg/config.py +8 -4
- sqlspec/adapters/psycopg/data_dictionary.py +257 -0
- sqlspec/adapters/psycopg/driver.py +40 -5
- sqlspec/adapters/sqlite/config.py +3 -0
- sqlspec/adapters/sqlite/data_dictionary.py +117 -0
- sqlspec/adapters/sqlite/driver.py +18 -3
- sqlspec/adapters/sqlite/pool.py +13 -4
- sqlspec/base.py +3 -4
- sqlspec/builder/_base.py +130 -48
- sqlspec/builder/_column.py +66 -24
- sqlspec/builder/_ddl.py +91 -41
- sqlspec/builder/_insert.py +40 -58
- sqlspec/builder/_parsing_utils.py +127 -12
- sqlspec/builder/_select.py +147 -2
- sqlspec/builder/_update.py +1 -1
- sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
- sqlspec/builder/mixins/_delete_operations.py +12 -7
- sqlspec/builder/mixins/_insert_operations.py +50 -36
- sqlspec/builder/mixins/_join_operations.py +15 -30
- sqlspec/builder/mixins/_merge_operations.py +210 -78
- sqlspec/builder/mixins/_order_limit_operations.py +4 -10
- sqlspec/builder/mixins/_pivot_operations.py +1 -0
- sqlspec/builder/mixins/_select_operations.py +44 -22
- sqlspec/builder/mixins/_update_operations.py +30 -37
- sqlspec/builder/mixins/_where_clause.py +52 -70
- sqlspec/cli.py +246 -140
- sqlspec/config.py +33 -19
- sqlspec/core/__init__.py +3 -2
- sqlspec/core/cache.py +298 -352
- sqlspec/core/compiler.py +61 -4
- sqlspec/core/filters.py +246 -213
- sqlspec/core/hashing.py +9 -11
- sqlspec/core/parameters.py +27 -10
- sqlspec/core/statement.py +72 -12
- sqlspec/core/type_conversion.py +234 -0
- sqlspec/driver/__init__.py +6 -3
- sqlspec/driver/_async.py +108 -5
- sqlspec/driver/_common.py +186 -17
- sqlspec/driver/_sync.py +108 -5
- sqlspec/driver/mixins/_result_tools.py +60 -7
- sqlspec/exceptions.py +5 -0
- sqlspec/loader.py +8 -9
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +153 -14
- sqlspec/migrations/commands.py +34 -96
- sqlspec/migrations/context.py +145 -0
- sqlspec/migrations/loaders.py +25 -8
- sqlspec/migrations/runner.py +352 -82
- sqlspec/storage/backends/fsspec.py +1 -0
- sqlspec/typing.py +4 -0
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/serializers.py +50 -2
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
- sqlspec-0.26.0.dist-info/RECORD +157 -0
- sqlspec-0.24.1.dist-info/RECORD +0 -139
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/core/cache.py
CHANGED
|
@@ -13,7 +13,8 @@ Components:
|
|
|
13
13
|
|
|
14
14
|
import threading
|
|
15
15
|
import time
|
|
16
|
-
from
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Final, Optional, Union
|
|
17
18
|
|
|
18
19
|
from mypy_extensions import mypyc_attr
|
|
19
20
|
from typing_extensions import TypeVar
|
|
@@ -21,23 +22,24 @@ from typing_extensions import TypeVar
|
|
|
21
22
|
from sqlspec.utils.logging import get_logger
|
|
22
23
|
|
|
23
24
|
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
|
|
24
27
|
import sqlglot.expressions as exp
|
|
25
28
|
|
|
26
|
-
from sqlspec.core.statement import SQL
|
|
27
29
|
|
|
28
30
|
__all__ = (
|
|
29
31
|
"CacheKey",
|
|
30
32
|
"CacheStats",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
33
|
+
"CachedStatement",
|
|
34
|
+
"FiltersView",
|
|
35
|
+
"MultiLevelCache",
|
|
36
|
+
"ParametersView",
|
|
34
37
|
"UnifiedCache",
|
|
38
|
+
"canonicalize_filters",
|
|
39
|
+
"create_cache_key",
|
|
40
|
+
"get_cache",
|
|
35
41
|
"get_cache_config",
|
|
36
42
|
"get_default_cache",
|
|
37
|
-
"get_expression_cache",
|
|
38
|
-
"get_parameter_cache",
|
|
39
|
-
"get_statement_cache",
|
|
40
|
-
"sql_cache",
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
T = TypeVar("T")
|
|
@@ -339,202 +341,7 @@ class UnifiedCache:
|
|
|
339
341
|
return not (ttl is not None and time.time() - node.timestamp > ttl)
|
|
340
342
|
|
|
341
343
|
|
|
342
|
-
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
343
|
-
class StatementCache:
|
|
344
|
-
"""Cache for compiled SQL statements."""
|
|
345
|
-
|
|
346
|
-
def __init__(self, max_size: int = DEFAULT_MAX_SIZE) -> None:
|
|
347
|
-
"""Initialize statement cache.
|
|
348
|
-
|
|
349
|
-
Args:
|
|
350
|
-
max_size: Maximum number of statements to cache
|
|
351
|
-
"""
|
|
352
|
-
self._cache: UnifiedCache = UnifiedCache(max_size)
|
|
353
|
-
|
|
354
|
-
def get_compiled(self, statement: "SQL") -> Optional[tuple[str, Any]]:
|
|
355
|
-
"""Get compiled SQL and parameters from cache.
|
|
356
|
-
|
|
357
|
-
Args:
|
|
358
|
-
statement: SQL statement to lookup
|
|
359
|
-
|
|
360
|
-
Returns:
|
|
361
|
-
Tuple of (compiled_sql, parameters) or None if not found
|
|
362
|
-
"""
|
|
363
|
-
cache_key = self._create_statement_key(statement)
|
|
364
|
-
return self._cache.get(cache_key)
|
|
365
|
-
|
|
366
|
-
def put_compiled(self, statement: "SQL", compiled_sql: str, parameters: Any) -> None:
|
|
367
|
-
"""Cache compiled SQL and parameters.
|
|
368
|
-
|
|
369
|
-
Args:
|
|
370
|
-
statement: Original SQL statement
|
|
371
|
-
compiled_sql: Compiled SQL string
|
|
372
|
-
parameters: Processed parameters
|
|
373
|
-
"""
|
|
374
|
-
cache_key = self._create_statement_key(statement)
|
|
375
|
-
self._cache.put(cache_key, (compiled_sql, parameters))
|
|
376
|
-
|
|
377
|
-
def _create_statement_key(self, statement: "SQL") -> CacheKey:
|
|
378
|
-
"""Create cache key for SQL statement.
|
|
379
|
-
|
|
380
|
-
Args:
|
|
381
|
-
statement: SQL statement
|
|
382
|
-
|
|
383
|
-
Returns:
|
|
384
|
-
Cache key for the statement
|
|
385
|
-
"""
|
|
386
|
-
|
|
387
|
-
key_data = (
|
|
388
|
-
"statement",
|
|
389
|
-
statement._raw_sql,
|
|
390
|
-
hash(statement),
|
|
391
|
-
str(statement.dialect) if statement.dialect else None,
|
|
392
|
-
statement.is_many,
|
|
393
|
-
statement.is_script,
|
|
394
|
-
)
|
|
395
|
-
return CacheKey(key_data)
|
|
396
|
-
|
|
397
|
-
def clear(self) -> None:
|
|
398
|
-
"""Clear statement cache."""
|
|
399
|
-
self._cache.clear()
|
|
400
|
-
|
|
401
|
-
def get_stats(self) -> CacheStats:
|
|
402
|
-
"""Get cache statistics."""
|
|
403
|
-
return self._cache.get_stats()
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
407
|
-
class ExpressionCache:
|
|
408
|
-
"""Cache for parsed expressions."""
|
|
409
|
-
|
|
410
|
-
def __init__(self, max_size: int = DEFAULT_MAX_SIZE) -> None:
|
|
411
|
-
"""Initialize expression cache.
|
|
412
|
-
|
|
413
|
-
Args:
|
|
414
|
-
max_size: Maximum number of expressions to cache
|
|
415
|
-
"""
|
|
416
|
-
self._cache: UnifiedCache = UnifiedCache(max_size)
|
|
417
|
-
|
|
418
|
-
def get_expression(self, sql: str, dialect: Optional[str] = None) -> "Optional[exp.Expression]":
|
|
419
|
-
"""Get parsed expression from cache.
|
|
420
|
-
|
|
421
|
-
Args:
|
|
422
|
-
sql: SQL string
|
|
423
|
-
dialect: SQL dialect
|
|
424
|
-
|
|
425
|
-
Returns:
|
|
426
|
-
Parsed expression or None if not found
|
|
427
|
-
"""
|
|
428
|
-
cache_key = self._create_expression_key(sql, dialect)
|
|
429
|
-
return self._cache.get(cache_key)
|
|
430
|
-
|
|
431
|
-
def put_expression(self, sql: str, expression: "exp.Expression", dialect: Optional[str] = None) -> None:
|
|
432
|
-
"""Cache parsed expression.
|
|
433
|
-
|
|
434
|
-
Args:
|
|
435
|
-
sql: SQL string
|
|
436
|
-
expression: Parsed SQLGlot expression
|
|
437
|
-
dialect: SQL dialect
|
|
438
|
-
"""
|
|
439
|
-
cache_key = self._create_expression_key(sql, dialect)
|
|
440
|
-
self._cache.put(cache_key, expression)
|
|
441
|
-
|
|
442
|
-
def _create_expression_key(self, sql: str, dialect: Optional[str]) -> CacheKey:
|
|
443
|
-
"""Create cache key for expression.
|
|
444
|
-
|
|
445
|
-
Args:
|
|
446
|
-
sql: SQL string
|
|
447
|
-
dialect: SQL dialect
|
|
448
|
-
|
|
449
|
-
Returns:
|
|
450
|
-
Cache key for the expression
|
|
451
|
-
"""
|
|
452
|
-
key_data = ("expression", sql, dialect)
|
|
453
|
-
return CacheKey(key_data)
|
|
454
|
-
|
|
455
|
-
def clear(self) -> None:
|
|
456
|
-
"""Clear expression cache."""
|
|
457
|
-
self._cache.clear()
|
|
458
|
-
|
|
459
|
-
def get_stats(self) -> CacheStats:
|
|
460
|
-
"""Get cache statistics."""
|
|
461
|
-
return self._cache.get_stats()
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
465
|
-
class ParameterCache:
|
|
466
|
-
"""Cache for processed parameters."""
|
|
467
|
-
|
|
468
|
-
def __init__(self, max_size: int = DEFAULT_MAX_SIZE) -> None:
|
|
469
|
-
"""Initialize parameter cache.
|
|
470
|
-
|
|
471
|
-
Args:
|
|
472
|
-
max_size: Maximum number of parameter sets to cache
|
|
473
|
-
"""
|
|
474
|
-
self._cache: UnifiedCache = UnifiedCache(max_size)
|
|
475
|
-
|
|
476
|
-
def get_parameters(self, original_params: Any, config_hash: int) -> Optional[Any]:
|
|
477
|
-
"""Get processed parameters from cache.
|
|
478
|
-
|
|
479
|
-
Args:
|
|
480
|
-
original_params: Original parameters
|
|
481
|
-
config_hash: Hash of parameter processing configuration
|
|
482
|
-
|
|
483
|
-
Returns:
|
|
484
|
-
Processed parameters or None if not found
|
|
485
|
-
"""
|
|
486
|
-
cache_key = self._create_parameter_key(original_params, config_hash)
|
|
487
|
-
return self._cache.get(cache_key)
|
|
488
|
-
|
|
489
|
-
def put_parameters(self, original_params: Any, processed_params: Any, config_hash: int) -> None:
|
|
490
|
-
"""Cache processed parameters.
|
|
491
|
-
|
|
492
|
-
Args:
|
|
493
|
-
original_params: Original parameters
|
|
494
|
-
processed_params: Processed parameters
|
|
495
|
-
config_hash: Hash of parameter processing configuration
|
|
496
|
-
"""
|
|
497
|
-
cache_key = self._create_parameter_key(original_params, config_hash)
|
|
498
|
-
self._cache.put(cache_key, processed_params)
|
|
499
|
-
|
|
500
|
-
def _create_parameter_key(self, params: Any, config_hash: int) -> CacheKey:
|
|
501
|
-
"""Create cache key for parameters.
|
|
502
|
-
|
|
503
|
-
Args:
|
|
504
|
-
params: Parameters to cache
|
|
505
|
-
config_hash: Configuration hash
|
|
506
|
-
|
|
507
|
-
Returns:
|
|
508
|
-
Cache key for the parameters
|
|
509
|
-
"""
|
|
510
|
-
|
|
511
|
-
try:
|
|
512
|
-
param_key: tuple[Any, ...]
|
|
513
|
-
if isinstance(params, dict):
|
|
514
|
-
param_key = tuple(sorted(params.items()))
|
|
515
|
-
elif isinstance(params, (list, tuple)):
|
|
516
|
-
param_key = tuple(params)
|
|
517
|
-
else:
|
|
518
|
-
param_key = (params,)
|
|
519
|
-
|
|
520
|
-
return CacheKey(("parameters", param_key, config_hash))
|
|
521
|
-
except (TypeError, ValueError):
|
|
522
|
-
param_key_fallback = (str(params), type(params).__name__)
|
|
523
|
-
return CacheKey(("parameters", param_key_fallback, config_hash))
|
|
524
|
-
|
|
525
|
-
def clear(self) -> None:
|
|
526
|
-
"""Clear parameter cache."""
|
|
527
|
-
self._cache.clear()
|
|
528
|
-
|
|
529
|
-
def get_stats(self) -> CacheStats:
|
|
530
|
-
"""Get cache statistics."""
|
|
531
|
-
return self._cache.get_stats()
|
|
532
|
-
|
|
533
|
-
|
|
534
344
|
_default_cache: Optional[UnifiedCache] = None
|
|
535
|
-
_statement_cache: Optional[StatementCache] = None
|
|
536
|
-
_expression_cache: Optional[ExpressionCache] = None
|
|
537
|
-
_parameter_cache: Optional[ParameterCache] = None
|
|
538
345
|
_cache_lock = threading.Lock()
|
|
539
346
|
|
|
540
347
|
|
|
@@ -552,58 +359,12 @@ def get_default_cache() -> UnifiedCache:
|
|
|
552
359
|
return _default_cache
|
|
553
360
|
|
|
554
361
|
|
|
555
|
-
def get_statement_cache() -> StatementCache:
|
|
556
|
-
"""Get the statement cache instance.
|
|
557
|
-
|
|
558
|
-
Returns:
|
|
559
|
-
Singleton statement cache instance
|
|
560
|
-
"""
|
|
561
|
-
global _statement_cache
|
|
562
|
-
if _statement_cache is None:
|
|
563
|
-
with _cache_lock:
|
|
564
|
-
if _statement_cache is None:
|
|
565
|
-
_statement_cache = StatementCache()
|
|
566
|
-
return _statement_cache
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
def get_expression_cache() -> ExpressionCache:
|
|
570
|
-
"""Get the expression cache instance.
|
|
571
|
-
|
|
572
|
-
Returns:
|
|
573
|
-
Singleton expression cache instance
|
|
574
|
-
"""
|
|
575
|
-
global _expression_cache
|
|
576
|
-
if _expression_cache is None:
|
|
577
|
-
with _cache_lock:
|
|
578
|
-
if _expression_cache is None:
|
|
579
|
-
_expression_cache = ExpressionCache()
|
|
580
|
-
return _expression_cache
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
def get_parameter_cache() -> ParameterCache:
|
|
584
|
-
"""Get the parameter cache instance.
|
|
585
|
-
|
|
586
|
-
Returns:
|
|
587
|
-
Singleton parameter cache instance
|
|
588
|
-
"""
|
|
589
|
-
global _parameter_cache
|
|
590
|
-
if _parameter_cache is None:
|
|
591
|
-
with _cache_lock:
|
|
592
|
-
if _parameter_cache is None:
|
|
593
|
-
_parameter_cache = ParameterCache()
|
|
594
|
-
return _parameter_cache
|
|
595
|
-
|
|
596
|
-
|
|
597
362
|
def clear_all_caches() -> None:
|
|
598
363
|
"""Clear all cache instances."""
|
|
599
364
|
if _default_cache is not None:
|
|
600
365
|
_default_cache.clear()
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if _expression_cache is not None:
|
|
604
|
-
_expression_cache.clear()
|
|
605
|
-
if _parameter_cache is not None:
|
|
606
|
-
_parameter_cache.clear()
|
|
366
|
+
cache = get_cache()
|
|
367
|
+
cache.clear()
|
|
607
368
|
|
|
608
369
|
|
|
609
370
|
def get_cache_statistics() -> dict[str, CacheStats]:
|
|
@@ -615,12 +376,8 @@ def get_cache_statistics() -> dict[str, CacheStats]:
|
|
|
615
376
|
stats = {}
|
|
616
377
|
if _default_cache is not None:
|
|
617
378
|
stats["default"] = _default_cache.get_stats()
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if _expression_cache is not None:
|
|
621
|
-
stats["expression"] = _expression_cache.get_stats()
|
|
622
|
-
if _parameter_cache is not None:
|
|
623
|
-
stats["parameter"] = _parameter_cache.get_stats()
|
|
379
|
+
cache = get_cache()
|
|
380
|
+
stats["multi_level"] = cache.get_stats()
|
|
624
381
|
return stats
|
|
625
382
|
|
|
626
383
|
|
|
@@ -690,8 +447,8 @@ def update_cache_config(config: CacheConfig) -> None:
|
|
|
690
447
|
|
|
691
448
|
unified_cache = get_default_cache()
|
|
692
449
|
unified_cache.clear()
|
|
693
|
-
|
|
694
|
-
|
|
450
|
+
cache = get_cache()
|
|
451
|
+
cache.clear()
|
|
695
452
|
|
|
696
453
|
logger = get_logger("sqlspec.cache")
|
|
697
454
|
logger.info(
|
|
@@ -705,87 +462,13 @@ def update_cache_config(config: CacheConfig) -> None:
|
|
|
705
462
|
)
|
|
706
463
|
|
|
707
464
|
|
|
708
|
-
|
|
709
|
-
class CacheStatsAggregate:
|
|
710
|
-
"""Cache statistics from all cache instances."""
|
|
711
|
-
|
|
712
|
-
__slots__ = (
|
|
713
|
-
"fragment_capacity",
|
|
714
|
-
"fragment_hit_rate",
|
|
715
|
-
"fragment_hits",
|
|
716
|
-
"fragment_misses",
|
|
717
|
-
"fragment_size",
|
|
718
|
-
"optimized_capacity",
|
|
719
|
-
"optimized_hit_rate",
|
|
720
|
-
"optimized_hits",
|
|
721
|
-
"optimized_misses",
|
|
722
|
-
"optimized_size",
|
|
723
|
-
"sql_capacity",
|
|
724
|
-
"sql_hit_rate",
|
|
725
|
-
"sql_hits",
|
|
726
|
-
"sql_misses",
|
|
727
|
-
"sql_size",
|
|
728
|
-
)
|
|
729
|
-
|
|
730
|
-
def __init__(self) -> None:
|
|
731
|
-
"""Initialize cache statistics."""
|
|
732
|
-
self.sql_hit_rate = 0.0
|
|
733
|
-
self.fragment_hit_rate = 0.0
|
|
734
|
-
self.optimized_hit_rate = 0.0
|
|
735
|
-
self.sql_size = 0
|
|
736
|
-
self.fragment_size = 0
|
|
737
|
-
self.optimized_size = 0
|
|
738
|
-
self.sql_capacity = 0
|
|
739
|
-
self.fragment_capacity = 0
|
|
740
|
-
self.optimized_capacity = 0
|
|
741
|
-
self.sql_hits = 0
|
|
742
|
-
self.sql_misses = 0
|
|
743
|
-
self.fragment_hits = 0
|
|
744
|
-
self.fragment_misses = 0
|
|
745
|
-
self.optimized_hits = 0
|
|
746
|
-
self.optimized_misses = 0
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
def get_cache_stats() -> CacheStatsAggregate:
|
|
465
|
+
def get_cache_stats() -> dict[str, CacheStats]:
|
|
750
466
|
"""Get cache statistics from all caches.
|
|
751
467
|
|
|
752
468
|
Returns:
|
|
753
|
-
|
|
469
|
+
Dictionary of cache statistics
|
|
754
470
|
"""
|
|
755
|
-
|
|
756
|
-
stats = CacheStatsAggregate()
|
|
757
|
-
|
|
758
|
-
for cache_name, cache_stats in stats_dict.items():
|
|
759
|
-
hits = cache_stats.hits
|
|
760
|
-
misses = cache_stats.misses
|
|
761
|
-
size = 0
|
|
762
|
-
|
|
763
|
-
if "sql" in cache_name.lower():
|
|
764
|
-
stats.sql_hits += hits
|
|
765
|
-
stats.sql_misses += misses
|
|
766
|
-
stats.sql_size += size
|
|
767
|
-
elif "fragment" in cache_name.lower():
|
|
768
|
-
stats.fragment_hits += hits
|
|
769
|
-
stats.fragment_misses += misses
|
|
770
|
-
stats.fragment_size += size
|
|
771
|
-
elif "optimized" in cache_name.lower():
|
|
772
|
-
stats.optimized_hits += hits
|
|
773
|
-
stats.optimized_misses += misses
|
|
774
|
-
stats.optimized_size += size
|
|
775
|
-
|
|
776
|
-
sql_total = stats.sql_hits + stats.sql_misses
|
|
777
|
-
if sql_total > 0:
|
|
778
|
-
stats.sql_hit_rate = stats.sql_hits / sql_total
|
|
779
|
-
|
|
780
|
-
fragment_total = stats.fragment_hits + stats.fragment_misses
|
|
781
|
-
if fragment_total > 0:
|
|
782
|
-
stats.fragment_hit_rate = stats.fragment_hits / fragment_total
|
|
783
|
-
|
|
784
|
-
optimized_total = stats.optimized_hits + stats.optimized_misses
|
|
785
|
-
if optimized_total > 0:
|
|
786
|
-
stats.optimized_hit_rate = stats.optimized_hits / optimized_total
|
|
787
|
-
|
|
788
|
-
return stats
|
|
471
|
+
return get_cache_statistics()
|
|
789
472
|
|
|
790
473
|
|
|
791
474
|
def reset_cache_stats() -> None:
|
|
@@ -801,24 +484,287 @@ def log_cache_stats() -> None:
|
|
|
801
484
|
|
|
802
485
|
|
|
803
486
|
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
804
|
-
class
|
|
805
|
-
"""
|
|
487
|
+
class ParametersView:
|
|
488
|
+
"""Read-only view of parameters without copying.
|
|
806
489
|
|
|
807
|
-
|
|
490
|
+
Provides read-only access to parameters without making copies,
|
|
491
|
+
enabling zero-copy parameter access patterns.
|
|
492
|
+
"""
|
|
808
493
|
|
|
809
|
-
|
|
810
|
-
self._statement_cache = get_statement_cache()
|
|
811
|
-
self._unified_cache = get_default_cache()
|
|
494
|
+
__slots__ = ("_named_ref", "_positional_ref")
|
|
812
495
|
|
|
813
|
-
def
|
|
814
|
-
"""
|
|
815
|
-
key = CacheKey((cache_key,))
|
|
816
|
-
return self._unified_cache.get(key)
|
|
496
|
+
def __init__(self, positional: list[Any], named: dict[str, Any]) -> None:
|
|
497
|
+
"""Initialize parameters view.
|
|
817
498
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
499
|
+
Args:
|
|
500
|
+
positional: List of positional parameters (will be referenced, not copied)
|
|
501
|
+
named: Dictionary of named parameters (will be referenced, not copied)
|
|
502
|
+
"""
|
|
503
|
+
self._positional_ref = positional
|
|
504
|
+
self._named_ref = named
|
|
822
505
|
|
|
506
|
+
def get_positional(self, index: int) -> Any:
|
|
507
|
+
"""Get positional parameter by index.
|
|
823
508
|
|
|
824
|
-
|
|
509
|
+
Args:
|
|
510
|
+
index: Parameter index
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Parameter value
|
|
514
|
+
"""
|
|
515
|
+
return self._positional_ref[index]
|
|
516
|
+
|
|
517
|
+
def get_named(self, key: str) -> Any:
|
|
518
|
+
"""Get named parameter by key.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
key: Parameter name
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Parameter value
|
|
525
|
+
"""
|
|
526
|
+
return self._named_ref[key]
|
|
527
|
+
|
|
528
|
+
def has_named(self, key: str) -> bool:
|
|
529
|
+
"""Check if named parameter exists.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
key: Parameter name
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
True if parameter exists
|
|
536
|
+
"""
|
|
537
|
+
return key in self._named_ref
|
|
538
|
+
|
|
539
|
+
@property
|
|
540
|
+
def positional_count(self) -> int:
|
|
541
|
+
"""Number of positional parameters."""
|
|
542
|
+
return len(self._positional_ref)
|
|
543
|
+
|
|
544
|
+
@property
|
|
545
|
+
def named_count(self) -> int:
|
|
546
|
+
"""Number of named parameters."""
|
|
547
|
+
return len(self._named_ref)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
551
|
+
@dataclass(frozen=True)
|
|
552
|
+
class CachedStatement:
|
|
553
|
+
"""Immutable cached statement result.
|
|
554
|
+
|
|
555
|
+
This class stores compiled SQL and parameters in an immutable format
|
|
556
|
+
that can be safely shared between different parts of the system without
|
|
557
|
+
risk of mutation. Tuple parameters ensure no copying is needed.
|
|
558
|
+
"""
|
|
559
|
+
|
|
560
|
+
compiled_sql: str
|
|
561
|
+
parameters: Optional[Union[tuple[Any, ...], dict[str, Any]]] # None allowed for static script compilation
|
|
562
|
+
expression: Optional["exp.Expression"]
|
|
563
|
+
|
|
564
|
+
def get_parameters_view(self) -> "ParametersView":
|
|
565
|
+
"""Get read-only parameter view.
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
View object that provides read-only access to parameters
|
|
569
|
+
"""
|
|
570
|
+
if self.parameters is None:
|
|
571
|
+
return ParametersView([], {})
|
|
572
|
+
return ParametersView(list(self.parameters), {})
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def create_cache_key(level: str, key: str, dialect: Optional[str] = None) -> str:
|
|
576
|
+
"""Create optimized cache key using string concatenation.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
level: Cache level (statement, expression, parameter)
|
|
580
|
+
key: Base cache key
|
|
581
|
+
dialect: SQL dialect (optional)
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Optimized cache key string
|
|
585
|
+
"""
|
|
586
|
+
return f"{level}:{dialect or 'default'}:{key}"
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
590
|
+
class MultiLevelCache:
|
|
591
|
+
"""Single cache with namespace isolation - no connection pool complexity."""
|
|
592
|
+
|
|
593
|
+
__slots__ = ("_cache",)
|
|
594
|
+
|
|
595
|
+
def __init__(self, max_size: int = DEFAULT_MAX_SIZE, ttl_seconds: Optional[int] = DEFAULT_TTL_SECONDS) -> None:
|
|
596
|
+
"""Initialize multi-level cache.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
max_size: Maximum number of cache entries
|
|
600
|
+
ttl_seconds: Time-to-live in seconds (None for no expiration)
|
|
601
|
+
"""
|
|
602
|
+
self._cache = UnifiedCache(max_size, ttl_seconds)
|
|
603
|
+
|
|
604
|
+
def get(self, level: str, key: str, dialect: Optional[str] = None) -> Optional[Any]:
|
|
605
|
+
"""Get value from cache with level and dialect namespace.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
level: Cache level (e.g., "statement", "expression", "parameter")
|
|
609
|
+
key: Cache key
|
|
610
|
+
dialect: SQL dialect (optional)
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
Cached value or None if not found
|
|
614
|
+
"""
|
|
615
|
+
full_key = create_cache_key(level, key, dialect)
|
|
616
|
+
cache_key = CacheKey((full_key,))
|
|
617
|
+
return self._cache.get(cache_key)
|
|
618
|
+
|
|
619
|
+
def put(self, level: str, key: str, value: Any, dialect: Optional[str] = None) -> None:
|
|
620
|
+
"""Put value in cache with level and dialect namespace.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
level: Cache level (e.g., "statement", "expression", "parameter")
|
|
624
|
+
key: Cache key
|
|
625
|
+
value: Value to cache
|
|
626
|
+
dialect: SQL dialect (optional)
|
|
627
|
+
"""
|
|
628
|
+
full_key = create_cache_key(level, key, dialect)
|
|
629
|
+
cache_key = CacheKey((full_key,))
|
|
630
|
+
self._cache.put(cache_key, value)
|
|
631
|
+
|
|
632
|
+
def delete(self, level: str, key: str, dialect: Optional[str] = None) -> bool:
|
|
633
|
+
"""Delete entry from cache.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
level: Cache level
|
|
637
|
+
key: Cache key to delete
|
|
638
|
+
dialect: SQL dialect (optional)
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
True if key was found and deleted, False otherwise
|
|
642
|
+
"""
|
|
643
|
+
full_key = create_cache_key(level, key, dialect)
|
|
644
|
+
cache_key = CacheKey((full_key,))
|
|
645
|
+
return self._cache.delete(cache_key)
|
|
646
|
+
|
|
647
|
+
def clear(self) -> None:
|
|
648
|
+
"""Clear all cache entries."""
|
|
649
|
+
self._cache.clear()
|
|
650
|
+
|
|
651
|
+
def get_stats(self) -> CacheStats:
|
|
652
|
+
"""Get cache statistics."""
|
|
653
|
+
return self._cache.get_stats()
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
_multi_level_cache: Optional[MultiLevelCache] = None
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def get_cache() -> MultiLevelCache:
|
|
660
|
+
"""Get the multi-level cache instance.
|
|
661
|
+
|
|
662
|
+
Returns:
|
|
663
|
+
Singleton multi-level cache instance
|
|
664
|
+
"""
|
|
665
|
+
global _multi_level_cache
|
|
666
|
+
if _multi_level_cache is None:
|
|
667
|
+
with _cache_lock:
|
|
668
|
+
if _multi_level_cache is None:
|
|
669
|
+
_multi_level_cache = MultiLevelCache()
|
|
670
|
+
return _multi_level_cache
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@dataclass(frozen=True)
|
|
674
|
+
class Filter:
|
|
675
|
+
"""Immutable filter that can be safely shared."""
|
|
676
|
+
|
|
677
|
+
field_name: str
|
|
678
|
+
operation: str
|
|
679
|
+
value: Any
|
|
680
|
+
|
|
681
|
+
def __post_init__(self) -> None:
|
|
682
|
+
"""Validate filter parameters."""
|
|
683
|
+
if not self.field_name:
|
|
684
|
+
msg = "Field name cannot be empty"
|
|
685
|
+
raise ValueError(msg)
|
|
686
|
+
if not self.operation:
|
|
687
|
+
msg = "Operation cannot be empty"
|
|
688
|
+
raise ValueError(msg)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def canonicalize_filters(filters: "list[Filter]") -> "tuple[Filter, ...]":
|
|
692
|
+
"""Create canonical representation of filters for cache keys.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
filters: List of filters to canonicalize
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
Tuple of unique filters sorted by field_name, operation, then value
|
|
699
|
+
"""
|
|
700
|
+
if not filters:
|
|
701
|
+
return ()
|
|
702
|
+
|
|
703
|
+
# Deduplicate and sort for canonical representation
|
|
704
|
+
unique_filters = set(filters)
|
|
705
|
+
return tuple(sorted(unique_filters, key=lambda f: (f.field_name, f.operation, str(f.value))))
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
709
|
+
class FiltersView:
|
|
710
|
+
"""Read-only view of filters without copying.
|
|
711
|
+
|
|
712
|
+
Provides zero-copy access to filters with methods for querying,
|
|
713
|
+
iteration, and canonical representation generation.
|
|
714
|
+
"""
|
|
715
|
+
|
|
716
|
+
__slots__ = ("_filters_ref",)
|
|
717
|
+
|
|
718
|
+
def __init__(self, filters: "list[Any]") -> None:
|
|
719
|
+
"""Initialize filters view.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
filters: List of filters (will be referenced, not copied)
|
|
723
|
+
"""
|
|
724
|
+
self._filters_ref = filters
|
|
725
|
+
|
|
726
|
+
def __len__(self) -> int:
|
|
727
|
+
"""Get number of filters."""
|
|
728
|
+
return len(self._filters_ref)
|
|
729
|
+
|
|
730
|
+
def __iter__(self) -> "Iterator[Any]":
|
|
731
|
+
"""Iterate over filters."""
|
|
732
|
+
return iter(self._filters_ref)
|
|
733
|
+
|
|
734
|
+
def get_by_field(self, field_name: str) -> "list[Any]":
|
|
735
|
+
"""Get all filters for a specific field.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
field_name: Field name to filter by
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
List of filters matching the field name
|
|
742
|
+
"""
|
|
743
|
+
return [f for f in self._filters_ref if hasattr(f, "field_name") and f.field_name == field_name]
|
|
744
|
+
|
|
745
|
+
def has_field(self, field_name: str) -> bool:
|
|
746
|
+
"""Check if any filter exists for a field.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
field_name: Field name to check
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
True if field has filters
|
|
753
|
+
"""
|
|
754
|
+
return any(hasattr(f, "field_name") and f.field_name == field_name for f in self._filters_ref)
|
|
755
|
+
|
|
756
|
+
def to_canonical(self) -> "tuple[Any, ...]":
|
|
757
|
+
"""Create canonical representation for cache keys.
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
Canonical tuple representation of filters
|
|
761
|
+
"""
|
|
762
|
+
# Convert to Filter objects if needed, then canonicalize
|
|
763
|
+
filter_objects = []
|
|
764
|
+
for f in self._filters_ref:
|
|
765
|
+
if isinstance(f, Filter):
|
|
766
|
+
filter_objects.append(f)
|
|
767
|
+
elif hasattr(f, "field_name") and hasattr(f, "operation") and hasattr(f, "value"):
|
|
768
|
+
filter_objects.append(Filter(f.field_name, f.operation, f.value))
|
|
769
|
+
|
|
770
|
+
return canonicalize_filters(filter_objects)
|