sqlspec 0.24.1__py3-none-any.whl → 0.25.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 (42) hide show
  1. sqlspec/_sql.py +11 -15
  2. sqlspec/_typing.py +2 -0
  3. sqlspec/adapters/adbc/driver.py +2 -2
  4. sqlspec/adapters/oracledb/driver.py +5 -0
  5. sqlspec/adapters/psycopg/config.py +2 -4
  6. sqlspec/base.py +3 -4
  7. sqlspec/builder/_base.py +55 -13
  8. sqlspec/builder/_column.py +9 -0
  9. sqlspec/builder/_ddl.py +7 -7
  10. sqlspec/builder/_insert.py +10 -6
  11. sqlspec/builder/_parsing_utils.py +23 -4
  12. sqlspec/builder/_update.py +1 -1
  13. sqlspec/builder/mixins/_cte_and_set_ops.py +31 -22
  14. sqlspec/builder/mixins/_delete_operations.py +12 -7
  15. sqlspec/builder/mixins/_insert_operations.py +50 -36
  16. sqlspec/builder/mixins/_join_operations.py +1 -0
  17. sqlspec/builder/mixins/_merge_operations.py +54 -28
  18. sqlspec/builder/mixins/_order_limit_operations.py +1 -0
  19. sqlspec/builder/mixins/_pivot_operations.py +1 -0
  20. sqlspec/builder/mixins/_select_operations.py +42 -14
  21. sqlspec/builder/mixins/_update_operations.py +30 -18
  22. sqlspec/builder/mixins/_where_clause.py +48 -60
  23. sqlspec/core/__init__.py +3 -2
  24. sqlspec/core/cache.py +297 -351
  25. sqlspec/core/compiler.py +5 -3
  26. sqlspec/core/filters.py +246 -213
  27. sqlspec/core/hashing.py +9 -11
  28. sqlspec/core/parameters.py +20 -7
  29. sqlspec/core/statement.py +67 -12
  30. sqlspec/driver/_async.py +2 -2
  31. sqlspec/driver/_common.py +31 -14
  32. sqlspec/driver/_sync.py +2 -2
  33. sqlspec/driver/mixins/_result_tools.py +60 -7
  34. sqlspec/loader.py +8 -9
  35. sqlspec/storage/backends/fsspec.py +1 -0
  36. sqlspec/typing.py +2 -0
  37. {sqlspec-0.24.1.dist-info → sqlspec-0.25.0.dist-info}/METADATA +1 -1
  38. {sqlspec-0.24.1.dist-info → sqlspec-0.25.0.dist-info}/RECORD +42 -42
  39. {sqlspec-0.24.1.dist-info → sqlspec-0.25.0.dist-info}/WHEEL +0 -0
  40. {sqlspec-0.24.1.dist-info → sqlspec-0.25.0.dist-info}/entry_points.txt +0 -0
  41. {sqlspec-0.24.1.dist-info → sqlspec-0.25.0.dist-info}/licenses/LICENSE +0 -0
  42. {sqlspec-0.24.1.dist-info → sqlspec-0.25.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/core/cache.py CHANGED
@@ -13,6 +13,7 @@ Components:
13
13
 
14
14
  import threading
15
15
  import time
16
+ from dataclasses import dataclass
16
17
  from typing import TYPE_CHECKING, Any, Final, Optional
17
18
 
18
19
  from mypy_extensions import mypyc_attr
@@ -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
- "ExpressionCache",
32
- "ParameterCache",
33
- "StatementCache",
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
- if _statement_cache is not None:
602
- _statement_cache.clear()
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
- if _statement_cache is not None:
619
- stats["statement"] = _statement_cache.get_stats()
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
- statement_cache = get_statement_cache()
694
- statement_cache.clear()
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
- @mypyc_attr(allow_interpreted_subclasses=False)
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
- Cache statistics object
469
+ Dictionary of cache statistics
754
470
  """
755
- stats_dict = get_cache_statistics()
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 SQLCompilationCache:
805
- """Wrapper around StatementCache for compatibility."""
487
+ class ParametersView:
488
+ """Read-only view of parameters without copying.
806
489
 
807
- __slots__ = ("_statement_cache", "_unified_cache")
490
+ Provides read-only access to parameters without making copies,
491
+ enabling zero-copy parameter access patterns.
492
+ """
808
493
 
809
- def __init__(self) -> None:
810
- self._statement_cache = get_statement_cache()
811
- self._unified_cache = get_default_cache()
494
+ __slots__ = ("_named_ref", "_positional_ref")
812
495
 
813
- def get(self, cache_key: str) -> Optional[tuple[str, Any]]:
814
- """Get cached compiled SQL and parameters."""
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
- def set(self, cache_key: str, value: tuple[str, Any]) -> None:
819
- """Set cached compiled SQL and parameters."""
820
- key = CacheKey((cache_key,))
821
- self._unified_cache.put(key, value)
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
- sql_cache = SQLCompilationCache()
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[tuple[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)