Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: upsert with no identity field populated #169

Merged
merged 13 commits into from
May 3, 2024
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ repos:
- id: unasyncd
additional_dependencies: ["ruff"]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: "v0.3.5"
rev: "v0.4.2"
hooks:
- id: ruff
args: ["--fix"]
Expand All @@ -36,12 +36,12 @@ repos:
additional_dependencies:
- tomli
- repo: https://github.com/psf/black
rev: 24.3.0
rev: 24.4.2
hooks:
- id: black
args: [--config=./pyproject.toml]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.9.0"
rev: "v1.10.0"
hooks:
- id: mypy
exclude: "docs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ def on_cli_init(self, cli: Group) -> None:
from advanced_alchemy.extensions.litestar.cli import database_group

cli.add_command(database_group)
return super().on_cli_init(cli)

def on_app_init(self, app_config: AppConfig) -> AppConfig:
"""Configure application for use with SQLAlchemy.
Expand Down
8 changes: 6 additions & 2 deletions advanced_alchemy/repository/_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,12 +962,16 @@ async def upsert(
elif getattr(data, self.id_attribute, None) is not None:
match_filter = {self.id_attribute: getattr(data, self.id_attribute, None)}
else:
match_filter = data.to_dict()
match_filter = data.to_dict(exclude={self.id_attribute})
existing = await self.get_one_or_none(**match_filter)
if not existing:
return await self.add(data, auto_commit=auto_commit, auto_expunge=auto_expunge, auto_refresh=auto_refresh)
with wrap_sqlalchemy_exception():
instance = await self._attach_to_session(data, strategy="merge")
for field_name, new_field_value in data.to_dict(exclude={self.id_attribute}).items():
field = getattr(existing, field_name, MISSING)
if field is not MISSING and field != new_field_value:
setattr(existing, field_name, new_field_value)
instance = await self._attach_to_session(existing, strategy="merge")
await self._flush_or_commit(auto_commit=auto_commit)
await self._refresh(
instance,
Expand Down
8 changes: 6 additions & 2 deletions advanced_alchemy/repository/_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -963,12 +963,16 @@ def upsert(
elif getattr(data, self.id_attribute, None) is not None:
match_filter = {self.id_attribute: getattr(data, self.id_attribute, None)}
else:
match_filter = data.to_dict()
match_filter = data.to_dict(exclude={self.id_attribute})
existing = self.get_one_or_none(**match_filter)
if not existing:
return self.add(data, auto_commit=auto_commit, auto_expunge=auto_expunge, auto_refresh=auto_refresh)
with wrap_sqlalchemy_exception():
instance = self._attach_to_session(data, strategy="merge")
for field_name, new_field_value in data.to_dict(exclude={self.id_attribute}).items():
field = getattr(existing, field_name, MISSING)
if field is not MISSING and field != new_field_value:
setattr(existing, field_name, new_field_value)
instance = self._attach_to_session(existing, strategy="merge")
self._flush_or_commit(auto_commit=auto_commit)
self._refresh(
instance,
Expand Down
181 changes: 88 additions & 93 deletions pdm.lock

Large diffs are not rendered by default.

40 changes: 20 additions & 20 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ description = "Ready-to-go SQLAlchemy concoctions."
keywords = ["sqlalchemy", "alembic", "litestar", "sanic", "fastapi", "flask"]
license = { text = "MIT" }
maintainers = [
{ name = "Litestar Developers", email = "hello@litestar.dev" },
{ name = "Cody Fincher", email = "cody@litestar.dev" },
{ name = "Jacob Coffee", email = "jacob@litestar.dev" },
{ name = "Janek Nouvertné", email = "provinzkraut@litestar.dev" },
{ name = "Peter Schutt", email = "peter@litestar.dev" },
{ name = "Visakh Unnikrishnan", email = "guacs@litestar.dev" },
{ name = "Alc", email = "alc@litestar.dev" }
{ name = "Litestar Developers", email = "hello@litestar.dev" },
{ name = "Cody Fincher", email = "cody@litestar.dev" },
{ name = "Jacob Coffee", email = "jacob@litestar.dev" },
{ name = "Janek Nouvertné", email = "provinzkraut@litestar.dev" },
{ name = "Peter Schutt", email = "peter@litestar.dev" },
{ name = "Visakh Unnikrishnan", email = "guacs@litestar.dev" },
{ name = "Alc", email = "alc@litestar.dev" },
]
name = "advanced_alchemy"
readme = "README.md"
Expand Down Expand Up @@ -100,18 +100,18 @@ dev = [
"aioodbc>=0.5.0",
]
docs = [
"sphinx>=7.1.2",
"sphinx-autobuild>=2021.3.14",
"sphinx-copybutton>=0.5.2",
"sphinx-click>=5.0.1",
"sphinx-toolbox>=3.5.0",
"blacken-docs>=1.16.0",
"sphinx-design>=0.5.0",
"sphinx-paramlinks>=0.6.0",
"sphinx-togglebutton>=0.3.2",
# "litestar-sphinx-theme @ {root:uri}/../litestar-sphinx-theme", # only needed when working on the theme
"litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git@v3",
"auto-pytabs[sphinx]>=0.4.0",
"sphinx>=7.1.2",
"sphinx-autobuild>=2021.3.14",
"sphinx-copybutton>=0.5.2",
"sphinx-click>=5.0.1",
"sphinx-toolbox>=3.5.0",
"blacken-docs>=1.16.0",
"sphinx-design>=0.5.0",
"sphinx-paramlinks>=0.6.0",
"sphinx-togglebutton>=0.3.2",
# "litestar-sphinx-theme @ {root:uri}/../litestar-sphinx-theme", # only needed when working on the theme
"litestar-sphinx-theme @ git+https://github.com/litestar-org/litestar-sphinx-theme.git@v3",
"auto-pytabs[sphinx]>=0.4.0",
]
extensions = [
"litestar[cli]>=2.0.0",
Expand Down Expand Up @@ -182,7 +182,7 @@ markers = [
testpaths = ["tests"]

[tool.coverage.run]
concurrency = ["multiprocessing", "thread"]
concurrency = ["multiprocessing"]
omit = [
"*/tests/*",
"advanced_alchemy/alembic/templates/asyncio/env.py",
Expand Down
14 changes: 13 additions & 1 deletion tests/integration/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,18 @@ async def test_repo_upsert_method(
upsert2_insert_obj = await maybe_async(author_repo.upsert(author_model(id=new_pk_id, name="Another Author")))
assert upsert2_insert_obj.id is not None
assert upsert2_insert_obj.name == "Another Author"
_ = await maybe_async(author_repo.get_one(name="Leo Tolstoy"))
# ensures that it still works even if the ID isn't set on an existing key
new_dob = datetime.strptime("2028-09-09", "%Y-%m-%d").date()
upsert3_update_obj = await maybe_async(
author_repo.upsert(
author_model(name="Leo Tolstoy", dob=new_dob),
match_fields=["name"],
),
)
assert upsert3_update_obj.id in {UUID("5ef29f3c-3560-4d15-ba6b-a2e5c721e4d2"), 2024}
assert upsert3_update_obj.name == "Leo Tolstoy"
assert upsert3_update_obj.dob == new_dob


async def test_repo_upsert_many_method(
Expand Down Expand Up @@ -2178,7 +2190,7 @@ async def test_service_upsert_method_match(
upsert_update_obj = await maybe_async(
author_service.upsert(data=existing_obj.to_dict(exclude={"id"}), match_fields=["name"]),
)
assert str(upsert_update_obj.id) != str(first_author_id)
assert str(upsert_update_obj.id) == str(first_author_id)
assert upsert_update_obj.name == "Agatha C."

upsert_insert_obj = await maybe_async(
Expand Down
Loading