From a438febb1e43ee6868173135899dfd95dc345e67 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 12 Nov 2025 11:21:43 +0100 Subject: [PATCH] Move exclusion constraints to Meta.constraints. --- .../qfieldcloud/subscription/functions.py | 7 +++ .../migrations/0004_auto_20220923_1602.py | 47 +++++++++++++++---- docker-app/qfieldcloud/subscription/models.py | 35 +++++++++++++- .../qfieldcloud/subscription/sql_config.py | 30 ------------ 4 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 docker-app/qfieldcloud/subscription/functions.py diff --git a/docker-app/qfieldcloud/subscription/functions.py b/docker-app/qfieldcloud/subscription/functions.py new file mode 100644 index 000000000..4e0a5a261 --- /dev/null +++ b/docker-app/qfieldcloud/subscription/functions.py @@ -0,0 +1,7 @@ +from django.contrib.postgres.fields import DateTimeRangeField +from django.db.models import Func + + +class TsTzRange(Func): + function = "TSTZRANGE" + output_field = DateTimeRangeField() diff --git a/docker-app/qfieldcloud/subscription/migrations/0004_auto_20220923_1602.py b/docker-app/qfieldcloud/subscription/migrations/0004_auto_20220923_1602.py index 11eb29806..69bd5a253 100644 --- a/docker-app/qfieldcloud/subscription/migrations/0004_auto_20220923_1602.py +++ b/docker-app/qfieldcloud/subscription/migrations/0004_auto_20220923_1602.py @@ -2,15 +2,18 @@ import uuid +import django.contrib.postgres.constraints +import django.contrib.postgres.fields.ranges import django.db.migrations.state import django.db.models.deletion -import migrate_sql.operations from django.conf import settings from django.contrib.postgres.operations import BtreeGistExtension from django.db import migrations, models from django.db.models import Q from django.utils import timezone +import qfieldcloud.subscription.functions + now = timezone.now() @@ -193,10 +196,23 @@ class Migration(migrations.Migration): ], ), migrations.RunPython(populate_subscriptions_model, populate_account_plan_field), - migrate_sql.operations.CreateSQL( - name="subscription_subscription_prevent_overlaps_idx", - sql="\n ALTER TABLE subscription_subscription\n ADD CONSTRAINT subscription_subscription_prevent_overlaps\n EXCLUDE USING gist (\n account_id WITH =,\n tstzrange(active_since, active_until) WITH &&\n )\n WHERE (active_since IS NOT NULL)\n ", - reverse_sql="\n ALTER TABLE subscription_subscription DROP CONSTRAINT subscription_subscription_prevent_overlaps\n ", + migrations.AddConstraint( + model_name="subscription", + constraint=django.contrib.postgres.constraints.ExclusionConstraint( + condition=models.Q(("active_since__isnull", False)), + expressions=[ + ("account_id", "="), + ( + qfieldcloud.subscription.functions.TsTzRange( + "active_since", + "active_until", + django.contrib.postgres.fields.ranges.RangeBoundary(), + ), + "&&", + ), + ], + name="subscription_subscription_prevent_overlaps", + ), ), ################# # Packages @@ -277,10 +293,23 @@ class Migration(migrations.Migration): null=False, ), ), - migrate_sql.operations.CreateSQL( - name="subscription_package_prevent_overlaps_idx", - sql="\n ALTER TABLE subscription_package\n ADD CONSTRAINT subscription_package_prevent_overlaps\n EXCLUDE USING gist (\n subscription_id WITH =,\n tstzrange(active_since, active_until) WITH &&\n )\n WHERE (active_since IS NOT NULL)\n ", - reverse_sql="\n ALTER TABLE subscription_package DROP CONSTRAINT subscription_package_prevent_overlaps\n ", + migrations.AddConstraint( + model_name="package", + constraint=django.contrib.postgres.constraints.ExclusionConstraint( + condition=models.Q(("active_since__isnull", False)), + expressions=[ + ("subscription_id", "="), + ( + qfieldcloud.subscription.functions.TsTzRange( + "active_since", + "active_until", + django.contrib.postgres.fields.ranges.RangeBoundary(), + ), + "&&", + ), + ], + name="subscription_package_prevent_overlaps", + ), ), #################### # Add auditing fields to plans and packages diff --git a/docker-app/qfieldcloud/subscription/models.py b/docker-app/qfieldcloud/subscription/models.py index 256444835..3805c0b83 100644 --- a/docker-app/qfieldcloud/subscription/models.py +++ b/docker-app/qfieldcloud/subscription/models.py @@ -8,6 +8,8 @@ from deprecated import deprecated from django.apps import apps from django.conf import settings +from django.contrib.postgres.constraints import ExclusionConstraint +from django.contrib.postgres.fields import RangeBoundary, RangeOperators from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models, transaction @@ -20,6 +22,7 @@ from qfieldcloud.core.models import Organization, Person, User, UserAccount from .exceptions import NotPremiumPlanException +from .functions import TsTzRange logger = logging.getLogger(__name__) @@ -374,6 +377,22 @@ class Package(models.Model): ), ) + class Meta: + constraints = [ + ExclusionConstraint( + name="subscription_package_prevent_overlaps", + index_type="GIST", + expressions=[ + ("subscription_id", RangeOperators.EQUAL), + ( + TsTzRange("active_since", "active_until", RangeBoundary()), + RangeOperators.OVERLAPS, + ), + ], + condition=Q(active_since__isnull=False), + ), + ] + # TODO add check constraint makes sure there are no two active additional packages at the same time, # because we assume that once you change your quantity, the old Package instance has an end_date @@ -953,7 +972,21 @@ def __str__(self): class Subscription(AbstractSubscription): - pass + class Meta: + constraints = [ + ExclusionConstraint( + name="subscription_subscription_prevent_overlaps", + index_type="GIST", + expressions=[ + ("account_id", RangeOperators.EQUAL), + ( + TsTzRange("active_since", "active_until", RangeBoundary()), + RangeOperators.OVERLAPS, + ), + ], + condition=Q(active_since__isnull=False), + ), + ] class CurrentSubscription(AbstractSubscription): diff --git a/docker-app/qfieldcloud/subscription/sql_config.py b/docker-app/qfieldcloud/subscription/sql_config.py index f9e23aa50..b0e4146ee 100644 --- a/docker-app/qfieldcloud/subscription/sql_config.py +++ b/docker-app/qfieldcloud/subscription/sql_config.py @@ -1,36 +1,6 @@ from migrate_sql.config import SQLItem sql_items = [ - SQLItem( - "subscription_subscription_prevent_overlaps_idx", - r""" - ALTER TABLE subscription_subscription - ADD CONSTRAINT subscription_subscription_prevent_overlaps - EXCLUDE USING gist ( - account_id WITH =, - tstzrange(active_since, active_until) WITH && - ) - WHERE (active_since IS NOT NULL) - """, - r""" - ALTER TABLE subscription_subscription DROP CONSTRAINT subscription_subscription_prevent_overlaps - """, - ), - SQLItem( - "subscription_package_prevent_overlaps_idx", - r""" - ALTER TABLE subscription_package - ADD CONSTRAINT subscription_package_prevent_overlaps - EXCLUDE USING gist ( - subscription_id WITH =, - tstzrange(active_since, active_until) WITH && - ) - WHERE (active_since IS NOT NULL) - """, - r""" - ALTER TABLE subscription_package DROP CONSTRAINT subscription_package_prevent_overlaps - """, - ), SQLItem( "current_subscriptions_vw", r"""