|
21 | 21 | from sqlspec.core.parameters import ( |
22 | 22 | ParameterProcessor, |
23 | 23 | ParameterProfile, |
24 | | - fingerprint_parameters, |
| 24 | + structural_fingerprint, |
25 | 25 | validate_parameter_alignment, |
| 26 | + value_fingerprint, |
26 | 27 | ) |
27 | 28 | from sqlspec.utils.logging import get_logger, log_with_context |
28 | 29 | from sqlspec.utils.type_guards import get_value_attribute |
@@ -349,11 +350,26 @@ def compile( |
349 | 350 | cache_key = self._make_cache_key(sql, parameters, is_many) |
350 | 351 |
|
351 | 352 | if cache_key in self._cache: |
352 | | - result = self._cache[cache_key] |
| 353 | + cached_result = self._cache[cache_key] |
353 | 354 | del self._cache[cache_key] |
354 | | - self._cache[cache_key] = result |
| 355 | + self._cache[cache_key] = cached_result |
355 | 356 | self._cache_hits += 1 |
356 | | - return result |
| 357 | + # Structural fingerprinting means same SQL structure = same cache entry, |
| 358 | + # but we must still process the caller's actual parameter values |
| 359 | + dialect_str = str(self._config.dialect) if self._config.dialect else None |
| 360 | + _, processed_params, _, _ = self._prepare_parameters(sql, parameters, is_many, dialect_str) |
| 361 | + # Return cached compilation metadata with NEW parameters |
| 362 | + return CompiledSQL( |
| 363 | + compiled_sql=cached_result.compiled_sql, |
| 364 | + execution_parameters=processed_params, |
| 365 | + operation_type=cached_result.operation_type, |
| 366 | + expression=cached_result.expression, |
| 367 | + parameter_style=cached_result.parameter_style, |
| 368 | + supports_many=cached_result.supports_many, |
| 369 | + parameter_casts=cached_result.parameter_casts, |
| 370 | + parameter_profile=cached_result.parameter_profile, |
| 371 | + operation_profile=cached_result.operation_profile, |
| 372 | + ) |
357 | 373 |
|
358 | 374 | self._cache_misses += 1 |
359 | 375 | result = self._compile_uncached(sql, parameters, is_many, expression) |
@@ -602,13 +618,15 @@ def _finalize_compilation( |
602 | 618 | if self._config.parameter_config.needs_static_script_compilation and processed_params is None: |
603 | 619 | return processed_sql, processed_params, parameter_profile |
604 | 620 | if ast_was_transformed and expression is not None: |
| 621 | + # Pass the transformed expression through the pipeline to avoid re-parsing |
605 | 622 | transformed_result = self._parameter_processor.process_for_execution( |
606 | 623 | sql=expression.sql(dialect=dialect_str), |
607 | 624 | parameters=parameters, |
608 | 625 | config=self._config.parameter_config, |
609 | 626 | dialect=dialect_str, |
610 | 627 | is_many=is_many, |
611 | 628 | wrap_types=self._config.enable_parameter_type_wrapping, |
| 629 | + parsed_expression=expression, |
612 | 630 | ) |
613 | 631 | final_sql = transformed_result.sql |
614 | 632 | final_params = transformed_result.parameters |
@@ -762,21 +780,28 @@ def _make_cache_key(self, sql: str, parameters: Any, is_many: bool = False) -> s |
762 | 780 | Returns: |
763 | 781 | Cache key string |
764 | 782 | """ |
765 | | - |
766 | | - param_fingerprint = fingerprint_parameters(parameters) |
| 783 | + # For static script compilation, parameter VALUES are embedded in the SQL string, |
| 784 | + # so different values produce different compiled SQL. Must use value_fingerprint |
| 785 | + # to avoid returning cached SQL with stale embedded values. |
| 786 | + if self._config.parameter_config.needs_static_script_compilation: |
| 787 | + param_fingerprint = value_fingerprint(parameters) |
| 788 | + else: |
| 789 | + # Use structural fingerprint (keys + types, not values) for better cache hit rates |
| 790 | + param_fingerprint = structural_fingerprint(parameters, is_many) |
767 | 791 | dialect_str = str(self._config.dialect) if self._config.dialect else None |
768 | | - param_style = self._config.parameter_config.default_parameter_style.value |
769 | | - |
770 | | - hash_data = ( |
771 | | - sql, |
772 | | - param_fingerprint, |
773 | | - param_style, |
774 | | - dialect_str, |
775 | | - self._config.enable_parsing, |
776 | | - self._config.enable_transformations, |
777 | | - is_many, |
| 792 | + # Include both input and execution parameter styles to avoid cache collisions |
| 793 | + # (e.g., MySQL asyncmy uses ? for input but %s for execution) |
| 794 | + input_style = self._config.parameter_config.default_parameter_style.value |
| 795 | + exec_style = ( |
| 796 | + self._config.parameter_config.default_execution_parameter_style.value |
| 797 | + if self._config.parameter_config.default_execution_parameter_style |
| 798 | + else input_style |
778 | 799 | ) |
779 | 800 |
|
| 801 | + # Exclude enable_parsing and enable_transformations from hash_data as they are |
| 802 | + # per-config static flags, not per-statement - they belong in pipeline key only |
| 803 | + hash_data = (sql, param_fingerprint, input_style, exec_style, dialect_str, is_many) |
| 804 | + |
780 | 805 | hash_str = hashlib.blake2b(repr(hash_data).encode("utf-8"), digest_size=8).hexdigest() |
781 | 806 | return f"sql_{hash_str}" |
782 | 807 |
|
@@ -924,7 +949,8 @@ def clear_cache(self) -> None: |
924 | 949 |
|
925 | 950 | def _make_parse_cache_key(self, sql: str, dialect: "str | None") -> str: |
926 | 951 | dialect_marker = dialect or "default" |
927 | | - hash_str = hashlib.sha256(f"{dialect_marker}:{sql}".encode()).hexdigest()[:16] |
| 952 | + # Use blake2b instead of sha256 for faster hashing (~50% faster) |
| 953 | + hash_str = hashlib.blake2b(f"{dialect_marker}:{sql}".encode(), digest_size=8).hexdigest() |
928 | 954 | return f"parse_{hash_str}" |
929 | 955 |
|
930 | 956 | @property |
|
0 commit comments