Skip to content

Commit 7a7d755

Browse files
cofinprovinzkrautsourcery-ai[bot]
authored
fix: change upsert_many behavior (#90)
Co-authored-by: Janek Nouvertné <provinzkraut@posteo.de> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
1 parent c53b2ea commit 7a7d755

3 files changed

Lines changed: 138 additions & 50 deletions

File tree

advanced_alchemy/repository/_async.py

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -526,10 +526,7 @@ async def get_or_upsert(
526526
When using match_fields and actual model values differ from ``kwargs``, the
527527
model value will be updated.
528528
"""
529-
match_fields = match_fields or self.match_fields
530-
if isinstance(match_fields, str):
531-
match_fields = [match_fields]
532-
if match_fields:
529+
if match_fields := self._get_match_fields(match_fields=match_fields):
533530
match_filter = {
534531
field_name: kwargs.get(field_name, None)
535532
for field_name in match_fields
@@ -601,10 +598,7 @@ async def get_and_update(
601598
Raises:
602599
NotFoundError: If no instance found identified by `item_id`.
603600
"""
604-
match_fields = match_fields or self.match_fields
605-
if isinstance(match_fields, str):
606-
match_fields = [match_fields]
607-
if match_fields:
601+
if match_fields := self._get_match_fields(match_fields=match_fields):
608602
match_filter = {
609603
field_name: kwargs.get(field_name, None)
610604
for field_name in match_fields
@@ -932,10 +926,7 @@ async def upsert(
932926
Raises:
933927
NotFoundError: If no instance found with same identifier as `data`.
934928
"""
935-
match_fields = match_fields or self.match_fields
936-
if isinstance(match_fields, str):
937-
match_fields = [match_fields]
938-
if match_fields:
929+
if match_fields := self._get_match_fields(match_fields=match_fields):
939930
match_filter = {
940931
field_name: getattr(data, field_name, None)
941932
for field_name in match_fields
@@ -1013,29 +1004,37 @@ async def upsert_many(
10131004
instances: list[ModelT] = []
10141005
data_to_update: list[ModelT] = []
10151006
data_to_insert: list[ModelT] = []
1016-
match_fields = match_fields or self.match_fields
1017-
if isinstance(match_fields, str):
1018-
match_fields = [match_fields]
1019-
match_filter: list[FilterTypes | ColumnElement[bool]] = [
1020-
CollectionFilter(
1021-
field_name=self.id_attribute,
1022-
values=[getattr(datum, self.id_attribute) for datum in data if datum is not None] if data else None,
1023-
),
1024-
]
1007+
match_fields = self._get_match_fields(match_fields=match_fields)
1008+
if match_fields is None:
1009+
match_fields = [self.id_attribute]
1010+
match_filter: list[FilterTypes | ColumnElement[bool]] = []
10251011
if match_fields:
10261012
for field_name in match_fields:
10271013
field = get_instrumented_attr(self.model_type, field_name)
1028-
matched_values = [getattr(datum, field_name) for datum in data if datum is not None]
1014+
matched_values = [
1015+
field_data for datum in data if (field_data := getattr(datum, field_name)) is not None
1016+
]
10291017
if self._prefer_any:
10301018
match_filter.append(any_(matched_values) == field) # type: ignore[arg-type]
10311019
else:
10321020
match_filter.append(field.in_(matched_values))
10331021

10341022
with wrap_sqlalchemy_exception():
1035-
existing_objs = await self.list(*match_filter, auto_expunge=False)
1036-
existing_ids = [getattr(datum, self.id_attribute) for datum in existing_objs if datum is not None]
1023+
existing_objs = await self.list(
1024+
*match_filter,
1025+
auto_expunge=False,
1026+
)
1027+
for field_name in match_fields:
1028+
field = get_instrumented_attr(self.model_type, field_name)
1029+
matched_values = [getattr(datum, field_name) for datum in existing_objs if datum is not None]
1030+
if self._prefer_any:
1031+
match_filter.append(any_(matched_values) == field) # type: ignore[arg-type]
1032+
else:
1033+
match_filter.append(field.in_(matched_values))
1034+
existing_ids = self._get_object_ids(existing_objs=existing_objs)
1035+
data = self._merge_on_match_fields(data, existing_objs, match_fields)
10371036
for datum in data:
1038-
if getattr(datum, self.id_attribute) is not None in existing_ids:
1037+
if getattr(datum, self.id_attribute, None) in existing_ids:
10391038
data_to_update.append(datum)
10401039
else:
10411040
data_to_insert.append(datum)
@@ -1052,6 +1051,38 @@ async def upsert_many(
10521051
self._expunge(instance, auto_expunge=auto_expunge)
10531052
return instances
10541053

1054+
def _get_object_ids(self, existing_objs: list[ModelT]) -> list[Any]:
1055+
return [obj_id for datum in existing_objs if (obj_id := getattr(datum, self.id_attribute)) is not None]
1056+
1057+
def _get_match_fields(
1058+
self,
1059+
match_fields: list[str] | str | None = None,
1060+
id_attribute: str | None = None,
1061+
) -> list[str] | None:
1062+
id_attribute = id_attribute or self.id_attribute
1063+
match_fields = match_fields or self.match_fields
1064+
if isinstance(match_fields, str):
1065+
match_fields = [match_fields]
1066+
return match_fields
1067+
1068+
def _merge_on_match_fields(
1069+
self,
1070+
data: list[ModelT],
1071+
existing_data: list[ModelT],
1072+
match_fields: list[str] | str | None = None,
1073+
) -> list[ModelT]:
1074+
match_fields = self._get_match_fields(match_fields=match_fields)
1075+
if match_fields is None:
1076+
match_fields = [self.id_attribute]
1077+
for existing_datum in existing_data:
1078+
for row_id, datum in enumerate(data):
1079+
match = all(
1080+
getattr(datum, field_name) == getattr(existing_datum, field_name) for field_name in match_fields
1081+
)
1082+
if match and getattr(existing_datum, self.id_attribute) is not None:
1083+
setattr(data[row_id], self.id_attribute, getattr(existing_datum, self.id_attribute))
1084+
return data
1085+
10551086
async def list(
10561087
self,
10571088
*filters: FilterTypes | ColumnElement[bool],

advanced_alchemy/repository/_sync.py

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -527,10 +527,7 @@ def get_or_upsert(
527527
When using match_fields and actual model values differ from ``kwargs``, the
528528
model value will be updated.
529529
"""
530-
match_fields = match_fields or self.match_fields
531-
if isinstance(match_fields, str):
532-
match_fields = [match_fields]
533-
if match_fields:
530+
if match_fields := self._get_match_fields(match_fields=match_fields):
534531
match_filter = {
535532
field_name: kwargs.get(field_name, None)
536533
for field_name in match_fields
@@ -602,10 +599,7 @@ def get_and_update(
602599
Raises:
603600
NotFoundError: If no instance found identified by `item_id`.
604601
"""
605-
match_fields = match_fields or self.match_fields
606-
if isinstance(match_fields, str):
607-
match_fields = [match_fields]
608-
if match_fields:
602+
if match_fields := self._get_match_fields(match_fields=match_fields):
609603
match_filter = {
610604
field_name: kwargs.get(field_name, None)
611605
for field_name in match_fields
@@ -933,10 +927,7 @@ def upsert(
933927
Raises:
934928
NotFoundError: If no instance found with same identifier as `data`.
935929
"""
936-
match_fields = match_fields or self.match_fields
937-
if isinstance(match_fields, str):
938-
match_fields = [match_fields]
939-
if match_fields:
930+
if match_fields := self._get_match_fields(match_fields=match_fields):
940931
match_filter = {
941932
field_name: getattr(data, field_name, None)
942933
for field_name in match_fields
@@ -1014,29 +1005,37 @@ def upsert_many(
10141005
instances: list[ModelT] = []
10151006
data_to_update: list[ModelT] = []
10161007
data_to_insert: list[ModelT] = []
1017-
match_fields = match_fields or self.match_fields
1018-
if isinstance(match_fields, str):
1019-
match_fields = [match_fields]
1020-
match_filter: list[FilterTypes | ColumnElement[bool]] = [
1021-
CollectionFilter(
1022-
field_name=self.id_attribute,
1023-
values=[getattr(datum, self.id_attribute) for datum in data if datum is not None] if data else None,
1024-
),
1025-
]
1008+
match_fields = self._get_match_fields(match_fields=match_fields)
1009+
if match_fields is None:
1010+
match_fields = [self.id_attribute]
1011+
match_filter: list[FilterTypes | ColumnElement[bool]] = []
10261012
if match_fields:
10271013
for field_name in match_fields:
10281014
field = get_instrumented_attr(self.model_type, field_name)
1029-
matched_values = [getattr(datum, field_name) for datum in data if datum is not None]
1015+
matched_values = [
1016+
field_data for datum in data if (field_data := getattr(datum, field_name)) is not None
1017+
]
10301018
if self._prefer_any:
10311019
match_filter.append(any_(matched_values) == field) # type: ignore[arg-type]
10321020
else:
10331021
match_filter.append(field.in_(matched_values))
10341022

10351023
with wrap_sqlalchemy_exception():
1036-
existing_objs = self.list(*match_filter, auto_expunge=False)
1037-
existing_ids = [getattr(datum, self.id_attribute) for datum in existing_objs if datum is not None]
1024+
existing_objs = self.list(
1025+
*match_filter,
1026+
auto_expunge=False,
1027+
)
1028+
for field_name in match_fields:
1029+
field = get_instrumented_attr(self.model_type, field_name)
1030+
matched_values = [getattr(datum, field_name) for datum in existing_objs if datum is not None]
1031+
if self._prefer_any:
1032+
match_filter.append(any_(matched_values) == field) # type: ignore[arg-type]
1033+
else:
1034+
match_filter.append(field.in_(matched_values))
1035+
existing_ids = self._get_object_ids(existing_objs=existing_objs)
1036+
data = self._merge_on_match_fields(data, existing_objs, match_fields)
10381037
for datum in data:
1039-
if getattr(datum, self.id_attribute) is not None in existing_ids:
1038+
if getattr(datum, self.id_attribute, None) in existing_ids:
10401039
data_to_update.append(datum)
10411040
else:
10421041
data_to_insert.append(datum)
@@ -1053,6 +1052,38 @@ def upsert_many(
10531052
self._expunge(instance, auto_expunge=auto_expunge)
10541053
return instances
10551054

1055+
def _get_object_ids(self, existing_objs: list[ModelT]) -> list[Any]:
1056+
return [obj_id for datum in existing_objs if (obj_id := getattr(datum, self.id_attribute)) is not None]
1057+
1058+
def _get_match_fields(
1059+
self,
1060+
match_fields: list[str] | str | None = None,
1061+
id_attribute: str | None = None,
1062+
) -> list[str] | None:
1063+
id_attribute = id_attribute or self.id_attribute
1064+
match_fields = match_fields or self.match_fields
1065+
if isinstance(match_fields, str):
1066+
match_fields = [match_fields]
1067+
return match_fields
1068+
1069+
def _merge_on_match_fields(
1070+
self,
1071+
data: list[ModelT],
1072+
existing_data: list[ModelT],
1073+
match_fields: list[str] | str | None = None,
1074+
) -> list[ModelT]:
1075+
match_fields = self._get_match_fields(match_fields=match_fields)
1076+
if match_fields is None:
1077+
match_fields = [self.id_attribute]
1078+
for existing_datum in existing_data:
1079+
for row_id, datum in enumerate(data):
1080+
match = all(
1081+
getattr(datum, field_name) == getattr(existing_datum, field_name) for field_name in match_fields
1082+
)
1083+
if match and getattr(existing_datum, self.id_attribute) is not None:
1084+
setattr(data[row_id], self.id_attribute, getattr(existing_datum, self.id_attribute))
1085+
return data
1086+
10561087
def list(
10571088
self,
10581089
*filters: FilterTypes | ColumnElement[bool],

tests/integration/test_repository.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,32 @@ async def test_repo_upsert_many_method_match_non_id(
10881088
assert existing_count_now > existing_count
10891089

10901090

1091+
async def test_repo_upsert_many_method_match_not_on_input(
1092+
author_repo: AuthorRepository,
1093+
author_model: AuthorModel,
1094+
) -> None:
1095+
if author_repo._dialect.name.startswith("spanner") and os.environ.get("SPANNER_EMULATOR_HOST"):
1096+
pytest.skip(
1097+
"Skipped on emulator. See the following: https://github.com/GoogleCloudPlatform/cloud-spanner-emulator/issues/73",
1098+
)
1099+
existing_count = await maybe_async(author_repo.count())
1100+
existing_obj = await maybe_async(author_repo.get_one(name="Agatha Christie"))
1101+
existing_obj.name = "Agatha C."
1102+
_ = await maybe_async(
1103+
author_repo.upsert_many(
1104+
data=[
1105+
existing_obj,
1106+
author_model(name="Inserted Author"),
1107+
author_model(name="Custom Author"),
1108+
],
1109+
match_fields=["id"],
1110+
),
1111+
)
1112+
existing_count_now = await maybe_async(author_repo.count())
1113+
1114+
assert existing_count_now > existing_count
1115+
1116+
10911117
async def test_repo_filter_before_after(author_repo: AuthorRepository) -> None:
10921118
before_filter = BeforeAfter(
10931119
field_name="created_at",

0 commit comments

Comments
 (0)