1
1
# Do not edit this file directly. It has been autogenerated from
2
2
# advanced_alchemy/repository/_async.py
3
+ import datetime
4
+ import decimal
3
5
import random
4
6
import string
5
7
from collections .abc import Iterable , Sequence
56
58
57
59
DEFAULT_INSERTMANYVALUES_MAX_PARAMETERS : Final = 950
58
60
POSTGRES_VERSION_SUPPORTING_MERGE : Final = 15
61
+ DEFAULT_SAFE_TYPES : Final [set [type [Any ]]] = {
62
+ int ,
63
+ float ,
64
+ str ,
65
+ bool ,
66
+ bytes ,
67
+ decimal .Decimal ,
68
+ datetime .date ,
69
+ datetime .datetime ,
70
+ datetime .time ,
71
+ datetime .timedelta ,
72
+ }
59
73
60
74
61
75
@runtime_checkable
@@ -498,6 +512,65 @@ def _get_uniquify(self, uniquify: Optional[bool] = None) -> bool:
498
512
"""
499
513
return bool (uniquify ) if uniquify is not None else self ._uniquify
500
514
515
+ def _type_must_use_in_instead_of_any (self , matched_values : "list[Any]" , field_type : "Any" = None ) -> bool :
516
+ """Determine if field.in_() should be used instead of any_() for compatibility.
517
+
518
+ Uses SQLAlchemy's type introspection to detect types that may have DBAPI
519
+ serialization issues with the ANY() operator. Checks if actual values match
520
+ the column's expected python_type - mismatches indicate complex types that
521
+ need the safer IN() operator. Falls back to Python type checking when
522
+ SQLAlchemy type information is unavailable.
523
+
524
+ Args:
525
+ matched_values: Values to be used in the filter
526
+ field_type: Optional SQLAlchemy TypeEngine from the column
527
+
528
+ Returns:
529
+ bool: True if field.in_() should be used instead of any_()
530
+ """
531
+ if not matched_values :
532
+ return False
533
+
534
+ if field_type is not None :
535
+ try :
536
+ expected_python_type = getattr (field_type , "python_type" , None )
537
+ if expected_python_type is not None :
538
+ for value in matched_values :
539
+ if value is not None and not isinstance (value , expected_python_type ):
540
+ return True
541
+ except (AttributeError , NotImplementedError ):
542
+ return True
543
+
544
+ return any (value is not None and type (value ) not in DEFAULT_SAFE_TYPES for value in matched_values )
545
+
546
+ def _get_unique_values (self , values : "list[Any]" ) -> "list[Any]" :
547
+ """Get unique values from a list, handling unhashable types safely.
548
+
549
+ Args:
550
+ values: List of values to deduplicate
551
+
552
+ Returns:
553
+ list[Any]: List of unique values preserving order
554
+ """
555
+ if not values :
556
+ return []
557
+
558
+ try :
559
+ # Fast path for hashable types
560
+ seen : set [Any ] = set ()
561
+ unique_values : list [Any ] = []
562
+ for value in values :
563
+ if value not in seen :
564
+ unique_values .append (value )
565
+ seen .add (value )
566
+ except TypeError :
567
+ # Fallback for unhashable types (e.g., dicts from JSONB)
568
+ unique_values = []
569
+ for value in values :
570
+ if value not in unique_values :
571
+ unique_values .append (value )
572
+ return unique_values
573
+
501
574
@staticmethod
502
575
def _get_error_messages (
503
576
error_messages : Optional [Union [ErrorMessages , EmptyType ]] = Empty ,
@@ -975,9 +1048,11 @@ def _get_delete_many_statement(
975
1048
statement = statement .execution_options (** execution_options )
976
1049
if supports_returning and statement_type != "select" :
977
1050
statement = cast ("ReturningDelete[tuple[ModelT]]" , statement .returning (model_type )) # type: ignore[union-attr,assignment] # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType,reportAttributeAccessIssue,reportUnknownVariableType]
978
- if self ._prefer_any :
979
- return statement .where (any_ (id_chunk ) == id_attribute ) # type: ignore[arg-type]
980
- return statement .where (id_attribute .in_ (id_chunk )) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
1051
+ # Use field.in_() if types are incompatible with ANY() or if dialect doesn't prefer ANY()
1052
+ use_in = not self ._prefer_any or self ._type_must_use_in_instead_of_any (id_chunk , id_attribute .type )
1053
+ if use_in :
1054
+ return statement .where (id_attribute .in_ (id_chunk )) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
1055
+ return statement .where (any_ (id_chunk ) == id_attribute ) # type: ignore[arg-type]
981
1056
982
1057
def get (
983
1058
self ,
@@ -1869,7 +1944,9 @@ def upsert_many(
1869
1944
matched_values = [
1870
1945
field_data for datum in data if (field_data := getattr (datum , field_name )) is not None
1871
1946
]
1872
- match_filter .append (any_ (matched_values ) == field if self ._prefer_any else field .in_ (matched_values )) # type: ignore[arg-type]
1947
+ # Use field.in_() if types are incompatible with ANY() or if dialect doesn't prefer ANY()
1948
+ use_in = not self ._prefer_any or self ._type_must_use_in_instead_of_any (matched_values , field .type )
1949
+ match_filter .append (field .in_ (matched_values ) if use_in else any_ (matched_values ) == field ) # type: ignore[arg-type]
1873
1950
1874
1951
with wrap_sqlalchemy_exception (
1875
1952
error_messages = error_messages , dialect_name = self ._dialect .name , wrap_exceptions = self .wrap_exceptions
@@ -1882,10 +1959,12 @@ def upsert_many(
1882
1959
)
1883
1960
for field_name in match_fields :
1884
1961
field = get_instrumented_attr (self .model_type , field_name )
1885
- matched_values = list (
1886
- {getattr (datum , field_name ) for datum in existing_objs if datum }, # ensure the list is unique
1887
- )
1888
- match_filter .append (any_ (matched_values ) == field if self ._prefer_any else field .in_ (matched_values )) # type: ignore[arg-type]
1962
+ # Safe deduplication that handles unhashable types (e.g., JSONB dicts)
1963
+ all_values = [getattr (datum , field_name ) for datum in existing_objs if datum ]
1964
+ matched_values = self ._get_unique_values (all_values )
1965
+ # Use field.in_() if types are incompatible with ANY() or if dialect doesn't prefer ANY()
1966
+ use_in = not self ._prefer_any or self ._type_must_use_in_instead_of_any (matched_values , field .type )
1967
+ match_filter .append (field .in_ (matched_values ) if use_in else any_ (matched_values ) == field ) # type: ignore[arg-type]
1889
1968
existing_ids = self ._get_object_ids (existing_objs = existing_objs )
1890
1969
data = self ._merge_on_match_fields (data , existing_objs , match_fields )
1891
1970
for datum in data :
0 commit comments