From 65b4688ab304cb35a013ef54170f3d4dc1070da8 Mon Sep 17 00:00:00 2001 From: Sebastian Willing Date: Sun, 19 Nov 2023 21:20:07 +0100 Subject: [PATCH 1/3] Stop updating on conflict if `update_condition` is False but not None Some users of this library set `update_condition=0` on `upsert` for not updating anything on conflict. The `upsert` documentation says: > update_condition: > Only update if this SQL expression evaluates to true. A value evaluating to Python `False` is ignored while the documentation says no update will be done. [#186513018] --- psqlextra/query.py | 4 +++- tests/test_upsert.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/psqlextra/query.py b/psqlextra/query.py index 2d24b5a..65a20c5 100644 --- a/psqlextra/query.py +++ b/psqlextra/query.py @@ -327,7 +327,9 @@ def upsert( self.on_conflict( conflict_target, - ConflictAction.UPDATE, + ConflictAction.UPDATE + if (update_condition or update_condition is None) + else ConflictAction.NOTHING, index_predicate=index_predicate, update_condition=update_condition, update_values=update_values, diff --git a/tests/test_upsert.py b/tests/test_upsert.py index 3aa6207..a9e567b 100644 --- a/tests/test_upsert.py +++ b/tests/test_upsert.py @@ -4,6 +4,7 @@ from django.db import connection, models from django.db.models import F, Q from django.db.models.expressions import CombinedExpression, Value +from django.test.utils import CaptureQueriesContext from psqlextra.expressions import ExcludedCol from psqlextra.fields import HStoreField @@ -144,6 +145,35 @@ def test_upsert_with_update_condition(): assert obj1.active +@pytest.mark.parametrize("update_condition_value", [0, False]) +def test_upsert_with_update_condition_false(update_condition_value): + """Tests that an expression can be used as an upsert update condition.""" + + model = get_fake_model( + { + "name": models.TextField(unique=True), + "priority": models.IntegerField(), + "active": models.BooleanField(), + } + ) + + obj1 = model.objects.create(name="joe", priority=1, active=False) + + with CaptureQueriesContext(connection) as ctx: + upsert_result = model.objects.upsert( + conflict_target=["name"], + update_condition=update_condition_value, + fields=dict(name="joe", priority=2, active=True), + ) + assert upsert_result is None + assert len(ctx) == 1 + assert 'ON CONFLICT ("name") DO NOTHING' in ctx[0]["sql"] + + obj1.refresh_from_db() + assert obj1.priority == 1 + assert not obj1.active + + def test_upsert_with_update_values(): """Tests that the default update values can be overriden with custom expressions.""" From c4aab405823331e17f4683e144e12e64e7541630 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Tue, 6 Feb 2024 16:23:26 +0100 Subject: [PATCH 2/3] Prepare for Django 5.x support --- README.md | 2 +- psqlextra/sql.py | 11 +++++++++-- setup.py | 2 +- tox.ini | 3 ++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8127b25..17037d8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ | :memo: | **License** | [![License](https://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org) | | :package: | **PyPi** | [![PyPi](https://badge.fury.io/py/django-postgres-extra.svg)](https://pypi.python.org/pypi/django-postgres-extra) | | :four_leaf_clover: | **Code coverage** | [![Coverage Status](https://coveralls.io/repos/github/SectorLabs/django-postgres-extra/badge.svg?branch=coveralls)](https://coveralls.io/github/SectorLabs/django-postgres-extra?branch=master) | -| | **Django Versions** | 2.0, 2.1, 2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2 | +| | **Django Versions** | 2.0, 2.1, 2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0 | | | **Python Versions** | 3.6, 3.7, 3.8, 3.9, 3.10, 3.11 | | | **Psycopg Versions** | 2, 3 | | :book: | **Documentation** | [Read The Docs](https://django-postgres-extra.readthedocs.io/en/master/) | diff --git a/psqlextra/sql.py b/psqlextra/sql.py index 3ceb596..b265508 100644 --- a/psqlextra/sql.py +++ b/psqlextra/sql.py @@ -64,8 +64,15 @@ def rename_annotations(self, annotations) -> None: new_annotations[new_name or old_name] = annotation if new_name and self.annotation_select_mask: - self.annotation_select_mask.discard(old_name) - self.annotation_select_mask.add(new_name) + # It's a set in all versions prior to Django 5.x + # and a list in Django 5.x and newer. + # https://github.com/django/django/commit/d6b6e5d0fd4e6b6d0183b4cf6e4bd4f9afc7bf67 + if isinstance(self.annotation_select_mask, set): + self.annotation_select_mask.discard(old_name) + self.annotation_select_mask.add(new_name) + elif isinstance(self.annotation_select_mask, list): + self.annotation_select_mask.remove(old_name) + self.annotation_select_mask.append(new_name) self.annotations.clear() self.annotations.update(new_annotations) diff --git a/setup.py b/setup.py index b3217fb..c3431e2 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,7 @@ def run(self): ], python_requires=">=3.6", install_requires=[ - "Django>=2.0,<5.0", + "Django>=2.0,<6.0", "python-dateutil>=2.8.0,<=3.0.0", ], extras_require={ diff --git a/tox.ini b/tox.ini index 3e229d0..70a0e8c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = {py36,py37}-dj{20,21,22,30,31,32}-psycopg{28,29} {py38,py39,py310}-dj{21,22,30,31,32,40}-psycopg{28,29} {py38,py39,py310,py311}-dj{41}-psycopg{28,29} - {py38,py39,py310,py311}-dj{42}-psycopg{28,29,31} + {py310,py311}-dj{42,50}-psycopg{28,29,31} [testenv] deps = @@ -16,6 +16,7 @@ deps = dj40: Django~=4.0.0 dj41: Django~=4.1.0 dj42: Django~=4.2.0 + dj50: Django~=5.0.1 psycopg28: psycopg2[binary]~=2.8 psycopg29: psycopg2[binary]~=2.9 psycopg31: psycopg[binary]~=3.1 From 43a6f222b3ab85661a15ddf783c7171b76769fc8 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Wed, 7 Feb 2024 09:07:21 +0100 Subject: [PATCH 3/3] Switch CircleCI to PyPi API token for publishing --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 64e60c3..92d9093 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -142,8 +142,8 @@ jobs: name: Publish package command: > python -m twine upload - --username "${PYPI_REPO_USERNAME}" - --password "${PYPI_REPO_PASSWORD}" + --username "__token__" + --password "${PYPI_API_TOKEN}" --verbose --non-interactive --disable-progress-bar