Skip to content

Commit 14c5aba

Browse files
committed
add association proxy support
1 parent ecd3997 commit 14c5aba

File tree

3 files changed

+34
-8
lines changed

3 files changed

+34
-8
lines changed

sqlmodel/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
from .main import Field as Field
118118
from .main import Relationship as Relationship
119119
from .main import SQLModel as SQLModel
120+
from .main import SQLModelConfig as SQLModelConfig
120121
from .orm.session import Session as Session
121122
from .sql.expression import all_ as all_
122123
from .sql.expression import and_ as and_

sqlmodel/_compat.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,9 @@ def sqlmodel_table_construct(
243243
# End SQLModel override
244244

245245
fields_values: Dict[str, Any] = {}
246-
defaults: Dict[
247-
str, Any
248-
] = {} # keeping this separate from `fields_values` helps us compute `_fields_set`
246+
defaults: Dict[str, Any] = (
247+
{}
248+
) # keeping this separate from `fields_values` helps us compute `_fields_set`
249249
for name, field in cls.model_fields.items():
250250
if field.alias and field.alias in values:
251251
fields_values[name] = values.pop(field.alias)
@@ -289,6 +289,10 @@ def sqlmodel_table_construct(
289289
value = values.get(key, Undefined)
290290
if value is not Undefined:
291291
setattr(self_instance, key, value)
292+
for key in self_instance.__sqlalchemy_association_proxies__:
293+
value = values.get(key, Undefined)
294+
if value is not Undefined:
295+
setattr(self_instance, key, value)
292296
# End SQLModel override
293297
return self_instance
294298

sqlmodel/main.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@
110110
hybrid_method,
111111
ColumnProperty,
112112
declared_attr,
113-
AssociationProxy,
114113
]
115114

116115

@@ -498,6 +497,7 @@ def Relationship(
498497
class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
499498
__sqlmodel_relationships__: Dict[str, RelationshipInfo]
500499
__sqlalchemy_constructs__: Dict[str, SQLAlchemyConstruct]
500+
__sqlalchemy_association_proxies__: Dict[str, AssociationProxy]
501501
model_config: SQLModelConfig
502502
model_fields: Dict[str, FieldInfo]
503503
__config__: Type[SQLModelConfig]
@@ -527,11 +527,14 @@ def __new__(
527527
) -> Any:
528528
relationships: Dict[str, RelationshipInfo] = {}
529529
sqlalchemy_constructs: Dict[str, SQLAlchemyConstruct] = {}
530+
sqlalchemy_association_proxies: Dict[str, AssociationProxy] = {}
530531
dict_for_pydantic = {}
531532
original_annotations = get_annotations(class_dict)
532533
pydantic_annotations = {}
533534
relationship_annotations = {}
534535
for k, v in class_dict.items():
536+
if isinstance(v, AssociationProxy):
537+
sqlalchemy_association_proxies[k] = v
535538
if isinstance(v, RelationshipInfo):
536539
relationships[k] = v
537540
elif isinstance(
@@ -558,6 +561,7 @@ def __new__(
558561
"__sqlmodel_relationships__": relationships,
559562
"__annotations__": pydantic_annotations,
560563
"__sqlalchemy_constructs__": sqlalchemy_constructs,
564+
"__sqlalchemy_association_proxies__": sqlalchemy_association_proxies,
561565
}
562566
# Duplicate logic from Pydantic to filter config kwargs because if they are
563567
# passed directly including the registry Pydantic will pass them over to the
@@ -584,6 +588,9 @@ def __new__(
584588
for k, v in sqlalchemy_constructs.items():
585589
setattr(new_cls, k, v)
586590

591+
for k, v in sqlalchemy_association_proxies.items():
592+
setattr(new_cls, k, v)
593+
587594
def get_config(name: str) -> Any:
588595
config_class_value = get_config_value(
589596
model=new_cls, parameter=name, default=Undefined
@@ -639,10 +646,13 @@ def __init__(
639646
# triggers an error
640647
base_is_table = any(is_table_model_class(base) for base in bases)
641648
if is_table_model_class(cls) and not base_is_table:
649+
for (
650+
association_proxy_name,
651+
association_proxy,
652+
) in cls.__sqlalchemy_association_proxies__.items():
653+
setattr(cls, association_proxy_name, association_proxy)
654+
642655
for rel_name, rel_info in cls.__sqlmodel_relationships__.items():
643-
if rel_name in cls.__sqlalchemy_constructs__:
644-
# Skip hybrid properties
645-
continue
646656
if rel_info.sa_relationship:
647657
# There's a SQLAlchemy relationship declared, that takes precedence
648658
# over anything else, use that and continue with the next attribute
@@ -860,6 +870,7 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
860870
__slots__ = ("__weakref__",)
861871
__tablename__: ClassVar[Union[str, Callable[..., str]]]
862872
__sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty[Any]]]
873+
__sqlalchemy_association_proxies__: ClassVar[Dict[str, AssociationProxy]]
863874
__name__: ClassVar[str]
864875
metadata: ClassVar[MetaData]
865876
__allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six
@@ -909,9 +920,19 @@ def __setattr__(self, name: str, value: Any) -> None:
909920
# Set in SQLAlchemy, before Pydantic to trigger events and updates
910921
if is_table_model_class(self.__class__) and is_instrumented(self, name): # type: ignore[no-untyped-call]
911922
set_attribute(self, name, value)
923+
# Set in SQLAlchemy association proxies
924+
if (
925+
is_table_model_class(self.__class__)
926+
and name in self.__sqlalchemy_association_proxies__
927+
):
928+
association_proxy = self.__sqlalchemy_association_proxies__[name]
929+
association_proxy.__set__(self, value)
912930
# Set in Pydantic model to trigger possible validation changes, only for
913931
# non relationship values
914-
if name not in self.__sqlmodel_relationships__:
932+
if (
933+
name not in self.__sqlmodel_relationships__
934+
and name not in self.__sqlalchemy_association_proxies__
935+
):
915936
super().__setattr__(name, value)
916937

917938
def __repr_args__(self) -> Sequence[Tuple[Optional[str], Any]]:

0 commit comments

Comments
 (0)