### Install triggers on a specific database Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/upgrading.md When using multi-database setups, use the --database argument with management commands to install triggers on one database at a time. ```bash python manage.py migrate --database=my_database ``` -------------------------------- ### Installation and Management Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/module.md Functions for installing, uninstalling, enabling, disabling, and pruning triggers. ```APIDOC ## function pgtrigger.install ## function pgtrigger.uninstall ## function pgtrigger.enable ## function pgtrigger.disable ## function pgtrigger.prunable ## function pgtrigger.prune ``` -------------------------------- ### List Trigger Installation Status for Schemas Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_db.md Shows the installation status of triggers relative to specified schemas. Use this to verify trigger deployment in multi-schema environments. ```bash python manage.py pgtrigger ls -s my_schema -s public ``` -------------------------------- ### Manual Trigger Installation Command Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md Manually install all triggers defined in the codebase using the `pgtrigger install` management command. ```bash python manage.py pgtrigger install ``` -------------------------------- ### Configure settings to disable migrations and enable installation Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/upgrading.md Set PGTRIGGER_MIGRATIONS to False to disable trigger migrations and PGTRIGGER_INSTALL_ON_MIGRATE to True to ensure triggers are always installed after 'migrate'. ```python settings.PGTRIGGER_MIGRATIONS = False settings.PGTRIGGER_INSTALL_ON_MIGRATE = True ``` -------------------------------- ### Install Trigger on Third-Party Model Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md Install triggers on third-party models by declaring them on a proxy model. This example protects Django's User model from deletion. ```python class UserProxy(User): class Meta: proxy = True triggers = [ pgtrigger.Protect(name='protect_deletes', operation=pgtrigger.Delete) ] ``` -------------------------------- ### Setup Conda Environment Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/CONTRIBUTING.md Set up a development environment using Conda. Dependent services like databases must be managed manually. ```bash make conda-setup ``` -------------------------------- ### Install Trigger on Many-to-Many Through Model Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md Install triggers on default many-to-many 'through' models using a proxy model. This example protects Django User group relationships from deletion. ```python class UserGroupTriggers(User.groups.through): class Meta: proxy = True triggers = [ pgtrigger.Protect(name='protect_deletes', operation=pgtrigger.Delete) ] ``` -------------------------------- ### Clone Repository and Setup Docker Environment Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/CONTRIBUTING.md Clone the project repository and set up the development environment using Docker. Ensure Docker is running before executing. ```bash git clone git@github.com:AmbitionEng/django-pgtrigger.git cd django-pgtrigger make docker-setup ``` -------------------------------- ### Manage Trigger Lifecycle Programmatically with pgtrigger Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Use functions like `install`, `uninstall`, `enable`, `disable`, `prune`, and `prunable` to manage trigger installation state in code, mirroring management commands. Triggers can be installed globally or for specific databases. ```python import pgtrigger # Install all triggers on the default database pgtrigger.install() ``` ```python # Install specific triggers on a named database pgtrigger.install( "myapp.Post:lock_published", database="secondary", ) ``` ```python # Uninstall a specific trigger pgtrigger.uninstall("myapp.AuditLog:append_only") ``` ```python # Disable triggers temporarily (globally — use pgtrigger.ignore for per-thread) pgtrigger.disable("myapp.Post:no_redundant_updates") # ... bulk import ... pgtrigger.enable("myapp.Post:no_redundant_updates") ``` ```python # Check for and remove orphaned triggers (no longer in codebase) orphans = pgtrigger.prunable() for table, trigger_id, is_enabled, db in orphans: print(f"Orphan: {trigger_id} on {table} ({db})") pgtrigger.prune() ``` -------------------------------- ### Show Trigger Installation Status Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md View the installation status of individual or all triggers using the `pgtrigger ls` management command. ```bash python manage.py pgtrigger ls ``` -------------------------------- ### Install Triggers on a Specific Database Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_db.md Installs all defined triggers on a specified database using the management command. Useful when managing triggers across multiple databases. ```bash python manage.py pgtrigger install --database other ``` -------------------------------- ### Generate and apply migrations Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Run Django management commands to create trigger migrations and install them in the PostgreSQL database. ```bash python manage.py makemigrations # Creates trigger migration python manage.py migrate # Installs trigger in Postgres ``` -------------------------------- ### Install django-pgtrigger with pip Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/installation.md Use pip to install the django-pgtrigger package. This command should be run in your project's virtual environment. ```bash pip3 install django-pgtrigger ``` -------------------------------- ### pgtrigger.install, pgtrigger.uninstall, pgtrigger.enable, pgtrigger.disable, pgtrigger.prune, pgtrigger.prunable Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Functions for managing trigger installation state in code, mirroring the management commands. ```APIDOC ## `pgtrigger.install`, `pgtrigger.uninstall`, `pgtrigger.enable`, `pgtrigger.disable`, `pgtrigger.prune`, `pgtrigger.prunable` — Programmatic Lifecycle Management Functions for managing trigger installation state in code, mirroring the management commands. ```python import pgtrigger # Install all triggers on the default database pgtrigger.install() # Install specific triggers on a named database pgtrigger.install( "myapp.Post:lock_published", database="secondary", ) # Uninstall a specific trigger pgtrigger.uninstall("myapp.AuditLog:append_only") # Disable triggers temporarily (globally — use pgtrigger.ignore for per-thread) pgtrigger.disable("myapp.Post:no_redundant_updates") # ... bulk import ... pgtrigger.enable("myapp.Post:no_redundant_updates") # Check for and remove orphaned triggers (no longer in codebase) orphans = pgtrigger.prunable() for table, trigger_id, is_enabled, db in orphans: print(f"Orphan: {trigger_id} on {table} ({db})") pgtrigger.prune() ``` ``` -------------------------------- ### Enable Trigger Installation on Migrate Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md Enable automatic trigger installation after every `python manage.py migrate` by setting `settings.PGTRIGGER_INSTALL_ON_MIGRATE` to `True`. Note that reversing migrations can cause issues. ```python settings.PGTRIGGER_INSTALL_ON_MIGRATE = True ``` -------------------------------- ### Illustrating Statement-Level Trigger Behavior Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md Example code demonstrating how to perform bulk operations on 'TrackedModel' and then query 'HistoryModel' to observe the trigger's logging effect. ```python TrackedModel.objects.bulk_create([TrackedModel(field='old1'), TrackedModel(field='old2')]) # Update all fields to "new" TrackedModel.objects.update(field='new') # The trigger should have tracked these updates print(HistoryModel.values('old_field', 'new_field')) >>> [{ 'old_field': 'old1', 'new_field': 'new' }, { 'old_field': 'old2', 'new_field': 'new' }] ``` -------------------------------- ### Manual Trigger Enable Command Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md Manually enable all installed triggers using the `pgtrigger enable` management command. ```bash python manage.py pgtrigger enable ``` -------------------------------- ### Define a Protect Trigger Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/basics.md Example of defining a trigger that prevents deletion of a model instance. Triggers are installed via Django's migration system. ```python from django.db import models import pgtrigger class CannotDelete(models.Model): class Meta: triggers = [ pgtrigger.Protect(name='protect_deletes', operation=pgtrigger.Delete) ] ``` -------------------------------- ### Add pgtrigger to Django INSTALLED_APPS Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/installation.md After installation, add 'pgtrigger' to the INSTALLED_APPS setting in your Django project's settings.py file. ```python INSTALLED_APPS = [ ... 'pgtrigger', ] ``` -------------------------------- ### History Tracking with Statement-Level Trigger Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/statement.md This example demonstrates a statement-level trigger that tracks history by bulk inserting old and new field values from transition tables. It utilizes pgtrigger.Composer for simplified declaration. ```python pgtrigger.Composer( name="track_history", level=pgtrigger.Statement, when=pgtrigger.After, operation=pgtrigger.Update, func=f""" INSERT INTO {HistoryModel._meta.db_table}(old_field, new_field) SELECT old_values.field AS old_field, new_values.field AS new_field FROM old_values JOIN new_values ON old_values.id = new_values.id; RETURN NULL; """ ) ``` -------------------------------- ### Define a Model for Triggers Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/conditional.md This is a sample Django model used in the following examples to demonstrate trigger conditions. ```python class MyModel(models.Model): int_field = models.IntegerField() char_field = models.CharField(null=True) dt_field = models.DateTimeField(auto_now=True) ``` -------------------------------- ### Statement-Level Trigger with Composer Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Use Composer for statement-level triggers, automatically setting up transition table references and providing template variables for conditional logic. This example tracks value history on bulk updates. ```python import pgtrigger from django.db import models class FieldHistory(models.Model): old_value = models.TextField() new_value = models.TextField() changed_at = models.DateTimeField(auto_now_add=True) class TrackedModel(models.Model): value = models.TextField() class Meta: triggers = [ # Statement-level history trigger (one INSERT per bulk UPDATE) pgtrigger.Composer( name="track_value_history", level=pgtrigger.Statement, when=pgtrigger.After, operation=pgtrigger.Update, func=f""" INSERT INTO {FieldHistory._meta.db_table}(old_value, new_value) SELECT old_values.value, new_values.value FROM old_values JOIN new_values ON old_values.id = new_values.id WHERE old_values.value IS DISTINCT FROM new_values.value; RETURN NULL; """, ), # Conditional statement-level trigger using template variables pgtrigger.Composer( name="alert_on_large_values", level=pgtrigger.Statement, when=pgtrigger.After, operation=pgtrigger.Update, declare=[("val", "RECORD")], func=pgtrigger.Func( """ FOR val IN SELECT new_values.* FROM {cond_new_values} LOOP RAISE NOTICE 'Large value updated: %', val.value; END LOOP; RETURN NULL; """ ), condition=pgtrigger.Q(new__value__gt="m"), ), ] # Bulk update triggers a single INSERT into FieldHistory TrackedModel.objects.bulk_create([TrackedModel(value="a"), TrackedModel(value="b")]) TrackedModel.objects.update(value="z") # One statement-level trigger fires; two rows written to FieldHistory ``` -------------------------------- ### Manage Triggers with Django Management Commands Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Utilize the `pgtrigger` management command for lifecycle control, including listing, installing, uninstalling, enabling, disabling, and pruning triggers. Options for specifying databases and schemas are available. ```bash # List all triggers and their status python manage.py pgtrigger ls # Output format: [ENABLED|DISABLED] # Statuses: INSTALLED, OUTDATED, UNINSTALLED, PRUNE, UNALLOWED ``` ```bash # List triggers for a specific database python manage.py pgtrigger ls --database secondary ``` ```bash # List triggers in a specific schema (multi-tenant) python manage.py pgtrigger ls -s tenant_abc -s public ``` ```bash # Install all triggers (also prunes orphans) python manage.py pgtrigger install ``` ```bash # Install a specific trigger python manage.py pgtrigger install myapp.Post:lock_published ``` ```bash # Uninstall all triggers python manage.py pgtrigger uninstall ``` ```bash # Enable / disable specific triggers globally python manage.py pgtrigger enable myapp.Post:lock_published python manage.py pgtrigger disable myapp.Post:lock_published ``` ```bash # Remove orphaned pgtrigger-managed triggers no longer in code python manage.py pgtrigger prune ``` -------------------------------- ### Q Objects: Combined Conditions Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/conditional.md Q objects can be negated, AND-ed, and OR-ed, similar to Django's Q objects. This example fires if 'char_field' is not distinct from its old version OR if 'int_field' in the new row is 0. ```python pgtrigger.Q(old__char_field__ndf=pgtrigger.F("new__char_field")) | pgtrigger.Q(new__int_field=0) ``` -------------------------------- ### Define Raw SQL Condition Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/conditional.md Use `pgtrigger.Condition` to express triggers using raw SQL. This is useful for complex conditions not covered by built-in utilities. Note that this specific example is equivalent to `pgtrigger.AnyChange()`. ```python pgtrigger.Condition("OLD.* IS DISTINCT FROM NEW.*") ``` -------------------------------- ### Define custom triggers with pgtrigger.Trigger Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Use the base Trigger class to define custom database triggers. Override get_func() for dynamic SQL or supply a func string directly. Example shows auto-incrementing version and mirroring content. ```python import pgtrigger from django.db import models class VersionedModel(models.Model): version = models.IntegerField(default=0) content = models.TextField() class Meta: triggers = [ # Increment version on every update where something actually changed pgtrigger.Trigger( name="auto_increment_version", when=pgtrigger.Before, operation=pgtrigger.Update, condition=pgtrigger.AnyChange(), func="NEW.version = NEW.version + 1; RETURN NEW;", ), # Keep two fields in sync on insert or update pgtrigger.Trigger( name="mirror_content", when=pgtrigger.Before, operation=pgtrigger.Update | pgtrigger.Insert, func="NEW.content = UPPER(NEW.content); RETURN NEW;", ), ] # After python manage.py makemigrations && migrate: obj = VersionedModel.objects.create(content="hello") # obj.version == 0 (no update yet) obj.content = "world" obj.save() # obj.version == 1 ``` -------------------------------- ### Declare Triggers in Abstract Base Model Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md Declare triggers in an abstract model for inheritance. This example provides a base model for soft-delete functionality. ```python class BaseSoftDelete(models.Model): is_active = models.BooleanField(default=True) class Meta: abstract = True triggers = [pgtrigger.SoftDelete(name="soft_delete", field="is_active")] ``` -------------------------------- ### Manual Trigger Disable Command Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md Manually disable all installed triggers using the `pgtrigger disable` management command. ```bash python manage.py pgtrigger disable ``` -------------------------------- ### Soft Delete Model with Custom Manager Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md Implement soft deletion by excluding inactive objects from default QuerySets. This setup ensures that `objects.all()` only returns active items, while `all_objects` can still access deleted ones. ```python class NotDeletedManager(models.Manager): """Automatically filters out soft deleted objects from QuerySets""" def get_queryset(self): return super().get_queryset().exclude(is_active=False) class SoftDeleteModel(models.Model): # This field is set to false when the model is deleted is_active = models.BooleanField(default=True) all_objects = models.ModelManager() # access deleted objects too objects = NotDeletedManager() # filter out soft deleted objects class Meta: triggers = [ pgtrigger.SoftDelete(name="soft_delete", field="is_active") ] # Return both active/deleted data via Django Admin, dumpdata, etc. default_manager_name = "all_objects" ``` -------------------------------- ### Statement-Level Protect Trigger Example Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/statement.md Use `pgtrigger.Protect` with `level=pgtrigger.Statement` to prevent updates on a model at the statement level. This is more performant for bulk operations than row-level triggers. ```python class MyModel(models.Model): class Meta: triggers = [ pgtrigger.Protect( name="protect_updates", level=pgtrigger.Statement, operation=pgtrigger.Update ) ] ``` -------------------------------- ### Protect rows with pgtrigger.Protect Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Use the Protect trigger to raise a Postgres exception for specified operations. Works at row and statement levels. Example shows append-only logs and protecting published posts. ```python import pgtrigger from django.db import models class AuditLog(models.Model): message = models.TextField() created_at = models.DateTimeField(auto_now_add=True) class Meta: triggers = [ # Append-only: block updates and deletes at row level pgtrigger.Protect( name="append_only", operation=pgtrigger.Update | pgtrigger.Delete, ), ] class Post(models.Model): status = models.CharField(default="draft") body = models.TextField() class Meta: triggers = [ # Protect published posts from being edited (conditional) pgtrigger.Protect( name="lock_published", operation=pgtrigger.Update, condition=pgtrigger.Q(old__status="published") & ~pgtrigger.Q(new__status="archived"), ), # Statement-level protection for bulk operations pgtrigger.Protect( name="protect_bulk_delete", level=pgtrigger.Statement, operation=pgtrigger.Delete, ), ] # AuditLog.objects.filter(pk=1).delete() # => raises Exception: pgtrigger: Cannot delete rows from auditlog table ``` -------------------------------- ### Conditionally Protect Deletions on Active Objects Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/README.md This example extends the Protect trigger to only prevent deletion if the object is currently active. It uses pgtrigger.Q to specify the condition based on the OLD row data. ```python import pgtrigger class ProtectedModel(models.Model): """Active object cannot be deleted!""" is_active = models.BooleanField(default=True) class Meta: triggers = [ pgtrigger.Protect( name="protect_deletes", operation=pgtrigger.Delete, condition=pgtrigger.Q(old__is_active=True) ) ] ``` -------------------------------- ### Serve Documentation Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/CONTRIBUTING.md Serve the MkDocs Material documentation locally for preview. ```bash make docs-serve ``` -------------------------------- ### Build Documentation Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/CONTRIBUTING.md Build the MkDocs Material documentation. A shortcut for serving the documentation is 'make docs-serve'. ```bash make docs ``` -------------------------------- ### Transactionally Create User and Profile Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/deferrable.md Demonstrates the correct way to create a user and their profile within a transaction to satisfy the deferrable trigger. An attempt outside a transaction will fail. ```python # This will succeed since the user has a profile when # the transaction completes with transaction.atomic(): user = User.objects.create() Profile.objects.create(user=user) # This will fail since it is not in a transaction user = User.objects.create() Profile.objects.create(user=user) ``` -------------------------------- ### Run Full Test Suite Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/CONTRIBUTING.md Execute the complete test suite against all supported Python versions. ```bash make full-test-suite ``` -------------------------------- ### Run Tests Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/CONTRIBUTING.md Execute the test suite on a single Python version. For the full suite across all supported Python versions, use 'make full-test-suite'. ```bash make test ``` -------------------------------- ### Inspect Registered Triggers with pgtrigger.registered Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Retrieve a list of registered triggers for all models or specific URIs. The `get_installation_status` method can be used to check the status and enabled state of a trigger. ```python import pgtrigger # Get all registered triggers for model, trigger in pgtrigger.registered(): print(f"{trigger.get_uri(model)} → {model._meta.db_table}") ``` ```python # Get specific triggers by URI pairs = pgtrigger.registered( "myapp.Post:lock_published", "myapp.AuditLog:append_only", ) for model, trigger in pairs: status, enabled = trigger.get_installation_status(model) print(f"{trigger.name}: {status}, enabled={enabled}") ``` -------------------------------- ### Dynamic Search Path with pgtrigger.schema Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt pgtrigger.schema sets the Postgres search_path for the current thread, which is essential for multi-tenant applications. All database operations within the context manager will use the specified schema. ```python import pgtrigger # Single schema context with pgtrigger.schema("tenant_abc"): # All DB operations use search_path = "tenant_abc" MyModel.objects.all() ``` -------------------------------- ### Conditional Statement-Level Update Trigger Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/statement.md Use `pgtrigger.Composer` with statement-level triggers to conditionally access transition tables. This example uses `cond_new_values` which automatically joins old and new tables when the condition spans both. ```python pgtrigger.Composer( name="composer_protect", level=pgtrigger.Statement, when=pgtrigger.After, operation=pgtrigger.Update, declare=[("val", "RECORD")], func=pgtrigger.Func( """ FOR val IN SELECT new_values.* FROM {cond_new_values} LOOP RAISE EXCEPTION 'uh oh'; END LOOP; RETURN NULL; """ ), condition=pgtrigger.Q(new__int_field__gt=0, old__int_field__lt=100), ) ``` ```sql FOR val IN SELECT new_values.* FROM old_values JOIN new_values ON old_values.id = new_values.id WHERE new_values.int_field > 0 AND old_values.int_field < 100 LOOP RAISE EXCEPTION 'uh oh'; END LOOP; RETURN NULL; ``` -------------------------------- ### Runtime Execution Utilities Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/module.md Functions for managing trigger execution context, including accessing constraints, ignoring triggers, and checking ignore status. ```APIDOC ## attribute pgtrigger.constraints ## function pgtrigger.ignore ## function pgtrigger.is_ignored ## attribute pgtrigger.schema ``` -------------------------------- ### Force Deferrable Trigger to Fire Immediately Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/deferrable.md Demonstrates how to use `pgtrigger.constraints` to change a deferrable trigger's timing to `pgtrigger.Immediate` within a transaction. This will cause the trigger to execute and potentially raise an error if conditions are not met. ```python with transaction.atomic(): user = User.objects.create(...) # Make the deferrable trigger fire immediately. This will cause an exception # because a profile has not yet been created for the user pgtrigger.constraints(pgtrigger.Immediate, "auth.User:profile_for_every_user") ``` -------------------------------- ### Configure django-pgtrigger settings Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Optional settings for django-pgtrigger, showing default values. These control migration integration, auto-installation, pruning, schema, and model meta usage. ```python # settings.py INSTALLED_APPS = [ ... "pgtrigger", ] # Optional settings (showing defaults) PGTRIGGER_MIGRATIONS = True # Integrate triggers into makemigrations PGTRIGGER_INSTALL_ON_MIGRATE = False # Auto-install after migrate (legacy) PGTRIGGER_PRUNE_ON_INSTALL = True # Prune orphaned triggers on install/uninstall PGTRIGGER_SCHEMA = "public" # Schema for the ignore helper function PGTRIGGER_SCHEMA_EDITOR = True # Patch schema editor for column type changes PGTRIGGER_MODEL_META = True # Allow triggers in model Meta ``` -------------------------------- ### Trigger Conditions with Q and F Objects Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Use Django-style Q and F objects for building trigger conditions that reference OLD and NEW rows. Use `old__` and `new__` prefixes to reference columns. Custom lookups `__df` (IS DISTINCT FROM) and `__ndf` (IS NOT DISTINCT FROM) are registered. ```python import pgtrigger from django.db import models class Account(models.Model): balance = models.DecimalField(max_digits=12, decimal_places=2) owner = models.ForeignKey("auth.User", on_delete=models.CASCADE) is_frozen = models.BooleanField(default=False) class Meta: triggers = [ # Block withdrawals when account is frozen pgtrigger.Protect( name="no_withdraw_when_frozen", operation=pgtrigger.Update, condition=( pgtrigger.Q(old__is_frozen=True) & pgtrigger.Q(old__balance__gt=pgtrigger.F("new__balance")) ), ), # Fire only when owner actually changes (handles NULLs correctly) pgtrigger.Trigger( name="log_owner_change", when=pgtrigger.After, operation=pgtrigger.Update, condition=pgtrigger.Q(old__owner_id__df=pgtrigger.F("new__owner_id")), func="RAISE NOTICE 'Owner changed on account %', NEW.id; RETURN NULL;", ), # Fire when balance changes AND account is not frozen pgtrigger.Trigger( name="balance_change_audit", when=pgtrigger.After, operation=pgtrigger.Update, condition=( pgtrigger.Q(old__balance__df=pgtrigger.F("new__balance")) & pgtrigger.Q(new__is_frozen=False) ), func="RAISE NOTICE 'Balance changed: %', NEW.id; RETURN NULL;", ), ] ``` -------------------------------- ### Programmatically Set Search Path for Triggers Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_db.md Dynamically sets the PostgreSQL search_path for trigger operations within a context manager. Nested calls append to the path. ```python with pgtrigger.schema("myschema", "public"): # seach_path is set to "myschema,public". Any nested invocations of # pgtrigger.schema will append to the path if not currently # present ``` -------------------------------- ### Define a Deferrable Trigger for Profile Creation Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/deferrable.md This trigger ensures a `Profile` model exists for every `User` upon creation. It must be executed within a transaction. ```python class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) class UserProxy(User): class Meta: proxy = True triggers = [ pgtrigger.Trigger( name="profile_for_every_user", when=pgtrigger.After, operation=pgtrigger.Insert, timing=pgtrigger.Deferred, func=f""" IF NOT EXISTS (SELECT FROM {Profile._meta.db_table} WHERE user_id = NEW.id) THEN RAISE EXCEPTION 'Profile does not exist for user %', NEW.id; END IF; RETURN NULL; """ ) ] ``` -------------------------------- ### Management Commands Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt The `pgtrigger` management command provides subcommands for full lifecycle control of triggers. ```APIDOC ## Management Commands `django-pgtrigger` ships a `pgtrigger` management command with subcommands for full lifecycle control. All subcommands accept `-d`/`--database` and `-s`/`--schema` options. ```bash # List all triggers and their status python manage.py pgtrigger ls # Output format: [ENABLED|DISABLED] # Statuses: INSTALLED, OUTDATED, UNINSTALLED, PRUNE, UNALLOWED # List triggers for a specific database python manage.py pgtrigger ls --database secondary # List triggers in a specific schema (multi-tenant) python manage.py pgtrigger ls -s tenant_abc -s public # Install all triggers (also prunes orphans) python manage.py pgtrigger install # Install a specific trigger python manage.py pgtrigger install myapp.Post:lock_published # Uninstall all triggers python manage.py pgtrigger uninstall # Enable / disable specific triggers globally python manage.py pgtrigger enable myapp.Post:lock_published python manage.py pgtrigger disable myapp.Post:lock_published # Remove orphaned pgtrigger-managed triggers no longer in code python manage.py pgtrigger prune ``` ``` -------------------------------- ### Correctly Ignore a Deferrable Trigger Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/deferrable.md Shows the proper method for ignoring a deferrable trigger by wrapping the transaction with `pgtrigger.ignore`. Using `durable=True` is recommended to prevent issues with parent transactions. ```python with pgtrigger.ignore("my_app.UserProxy:profile_for_every_user"): # Use durable=True, otherwise we may be wrapped in a parent # transaction with transaction.atomic(durable=True): # We no longer need a profile for a user... User.objects.create(...) ``` -------------------------------- ### Runtime Deferrable Trigger Timing with pgtrigger.constraints Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt pgtrigger.constraints allows overriding the timing of deferrable triggers within a transaction, similar to Postgres' SET CONSTRAINTS. Use it to change triggers from Deferred to Immediate timing at runtime. ```python import pgtrigger from django.db import transaction, models from myapp.models import Profile class UserProxy(User): class Meta: proxy = True triggers = [ pgtrigger.Trigger( name="require_profile", when=pgtrigger.After, operation=pgtrigger.Insert, timing=pgtrigger.Deferred, # runs at end of transaction func=f""" IF NOT EXISTS ( SELECT 1 FROM {Profile._meta.db_table} WHERE user_id = NEW.id ) THEN RAISE EXCEPTION 'Profile required for user %', NEW.id; END IF; RETURN NULL; """, ) ] # Normal deferred usage — both must be created in same transaction with transaction.atomic(): user = User.objects.create(username="alice") Profile.objects.create(user=user) # Trigger fires here at end of transaction — passes # Change to IMMEDIATE at runtime — trigger fires right after INSERT with transaction.atomic(): pgtrigger.constraints(pgtrigger.Immediate, "myapp.UserProxy:require_profile") user = User.objects.create(username="bob") # => Exception raised immediately because profile doesn't exist yet ``` -------------------------------- ### Statement-Level Triggers with Transition Tables Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Use `pgtrigger.Referencing` to declare `OLD TABLE` and `NEW TABLE` transition tables for statement-level triggers. This provides access to all rows affected by the statement. ```python import pgtrigger from django.db import models class PriceHistory(models.Model): product_id = models.IntegerField() old_price = models.DecimalField(max_digits=10, decimal_places=2) new_price = models.DecimalField(max_digits=10, decimal_places=2) class Product(models.Model): price = models.DecimalField(max_digits=10, decimal_places=2) class Meta: triggers = [ pgtrigger.Trigger( name="log_price_changes", level=pgtrigger.Statement, when=pgtrigger.After, operation=pgtrigger.Update, referencing=pgtrigger.Referencing(old="old_prices", new="new_prices"), func=f""" INSERT INTO {PriceHistory._meta.db_table}(product_id, old_price, new_price) SELECT new_prices.id, old_prices.price, new_prices.price FROM old_prices JOIN new_prices ON old_prices.id = new_prices.id WHERE old_prices.price IS DISTINCT FROM new_prices.price; RETURN NULL; """, ) ] # Bulk price update → single trigger execution, batch insert into PriceHistory Product.objects.filter(price__lt=10).update(price=10) ``` -------------------------------- ### Registry Functions Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/module.md Functions for registering and managing triggers within the database. ```APIDOC ## function pgtrigger.register ## attribute pgtrigger.registered ``` -------------------------------- ### Test Failure-Based Trigger with pytest Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/faq.md Demonstrates how to test a failure-based trigger like pgtrigger.Protect using pytest. It shows how to catch the expected InternalError and handle transaction states. ```python from djagno.db import transaction from django.db.utils import InternalError import pytest @pytest.mark.django_db def test_protection_trigger(): with pytest.raises(InternalError, match="Cannot delete"), transaction.atomic(): # Try to delete protected model # Since the above assertion is wrapped in transaction.atomic, we will still # have a valid transaction in our test case here ``` -------------------------------- ### Accessing All Objects with Soft Delete Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md Retrieve all objects, including soft-deleted ones, by explicitly using the `all_objects` manager. This is useful when you need to access data that has been marked as inactive. ```python MyModelName.all_objects.all() ``` -------------------------------- ### Official Interface for User Creation Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md Enforce the use of a specific function (`create_user`) for creating user objects by protecting direct inserts on the `User` model. This ensures that all user creations go through a controlled interface. ```python @pgtrigger.ignore("my_app.User:protect_inserts") def create_user(**kwargs): return User.objects.create(**kwargs) class User(models.Model): class Meta: triggers = [ pgtrigger.Protect(name="protect_inserts", operation=pgtrigger.Insert) ] ``` -------------------------------- ### Q and F Objects: Compare Old and New Fields Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/conditional.md This condition fires only when the 'int_field' of the old row is greater than the 'int_field' of the new row. F objects are used for comparing fields. ```python pgtrigger.Q(old__int_field__gt=pgtrigger.F("new__int_field")) ``` -------------------------------- ### Model-Aware Function Templates with pgtrigger.Func Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Use pgtrigger.Func to create trigger functions that can reference model metadata like table and field names using Python format-string syntax. This is useful for dynamic SQL generation within triggers. ```python import pgtrigger from django.db import models class Snapshot(models.Model): source_table = models.CharField(max_length=100) row_data = models.JSONField() class TrackedItem(models.Model): name = models.CharField(max_length=200) quantity = models.IntegerField() class Meta: triggers = [ pgtrigger.Trigger( name="snapshot_on_delete", when=pgtrigger.Before, operation=pgtrigger.Delete, declare=[("snapshot_data", "JSONB")], func=pgtrigger.Func( """ snapshot_data := row_to_json(OLD)::jsonb; INSERT INTO {meta.db_table}(source_table, row_data) VALUES ('{meta.db_table}', snapshot_data); RETURN OLD; """ ), ) ] ``` -------------------------------- ### Inherit Meta Class with Triggers Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md When a child model defines its own Meta class, ensure it inherits from the parent's Meta class to include inherited triggers. ```python class ChildModel(BaseSoftDelete): class Meta(BaseSoftDelete.Meta): ordering = ["is_active"] ``` -------------------------------- ### Statement-Level Trigger for History Tracking Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md Implement a statement-level trigger to log changes to a 'TrackedModel' into a 'HistoryModel'. This approach is efficient for bulk operations as it fires once per statement. ```python class HistoryModel(models.Model): old_field = models.CharField(max_length=32) new_field = models.CharField(max_length=32) class TrackedModel(models.Model): field = models.CharField(max_length=32) class Meta: triggers = [ pgtrigger.Trigger( name="track_history", level=pgtrigger.Statement, when=pgtrigger.After, operation=pgtrigger.Update, referencing=pgtrigger.Referencing(old="old_values", new="new_values"), func=f""" INSERT INTO {HistoryModel._meta.db_table}(old_field, new_field) SELECT old_values.field AS old_field, new_values.field AS new_field FROM old_values JOIN new_values ON old_values.id = new_values.id; RETURN NULL; "", ) ] ``` -------------------------------- ### Allow Deactivation of Published Models Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md Extend the protection to allow updates only when a published model is being transitioned to an 'inactive' status. This prevents general edits but allows for a specific deactivation flow. ```python class Post(models.Model): status = models.CharField(default="unpublished") content = models.TextField() class Meta: triggers = [ pgtrigger.Protect( name="freeze_published_model_allow_deactivation", operation=pgtrigger.Update, condition=( pgtrigger.Q(old__status="published") & ~pgtrigger.Q(new__status="inactive") ) ] ``` -------------------------------- ### Manual Trigger Uninstallation Command Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md Manually uninstall all triggers managed by django-pgtrigger using the `pgtrigger uninstall` management command. ```bash python manage.py pgtrigger uninstall ``` -------------------------------- ### Run Python command to prune old triggers Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/upgrading.md Use this operation within a migration to remove orphaned triggers after migrating third-party or many-to-many models. ```python from django.core.management import call_command # ... inside a migration file migrations.RunPython(lambda: call_command("pgtrigger", "prune")) ``` -------------------------------- ### Condition Definitions Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/module.md Defines various ways to specify conditions for trigger execution, including specific changes, distinctness, and conditional logic. ```APIDOC ## class pgtrigger.Condition ## class pgtrigger.AnyChange ## class pgtrigger.AnyDontChange ## class pgtrigger.AllChange ## class pgtrigger.AllDontChange ## class pgtrigger.Q ## class pgtrigger.F ## class pgtrigger.IsDistinctFrom ## class pgtrigger.IsNotDistinctFrom ``` -------------------------------- ### Rename pgtrigger.get to pgtrigger.registered Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/upgrading.md In version 4, pgtrigger.get has been renamed to pgtrigger.registered. Update your code accordingly if you are using this function. ```python pgtrigger.registered ``` -------------------------------- ### Implement Versioned Models with Triggers Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md Automatically increment a version field on model updates and protect direct edits to the version field. This uses two triggers: one for protection and one for incrementing. ```python class Versioned(models.Model): """ This model is versioned. The "version" field is incremented on every update, and users cannot directly update the "version" field. """ version = models.IntegerField(default=0) char_field = models.CharField(max_length=32) class Meta: triggers = [ # Protect anyone editing the version field directly pgtrigger.Protect( name="protect_updates", operation=pgtrigger.Update, condition=pgtrigger.AnyChange("version") ), # Increment the version field on changes pgtrigger.Trigger( name="versioning", when=pgtrigger.Before, operation=pgtrigger.Update, func="NEW.version = NEW.version + 1; RETURN NEW;", # Don't increment version on redundant updates. condition=pgtrigger.AnyChange() ) ] ``` -------------------------------- ### Enforce State Transitions with pgtrigger.FSM Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt This trigger enforces that a CharField can only transition between explicitly allowed states. It's useful for managing workflows and ensuring data integrity. ```python import pgtrigger from django.db import models class Ticket(models.Model): STATUS_OPEN = "open" STATUS_IN_PROGRESS = "in_progress" STATUS_RESOLVED = "resolved" STATUS_CLOSED = "closed" status = models.CharField(max_length=32, default=STATUS_OPEN) title = models.CharField(max_length=200) class Meta: triggers = [ pgtrigger.FSM( name="ticket_status_fsm", field="status", transitions=[ ("open", "in_progress"), ("in_progress", "resolved"), ("resolved", "closed"), ("resolved", "open"), # allow re-opening ], ) ] ticket = Ticket.objects.create(title="Bug report") # ticket.status == "open" ticket.status = "in_progress" ticket.save() # OK ticket.status = "closed" # ticket.save() # => raises Exception: pgtrigger: Invalid transition of field "status" # from "in_progress" to "closed" on table ticket ``` -------------------------------- ### Bypass Deletion Protection with pgtrigger.ignore Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/ignoring_triggers.md Use the pgtrigger.ignore context manager to bypass specific triggers during a single thread of execution. Provide the trigger URI in the format '{app_label}.{model_name}:{trigger_name}'. ```python class CannotDelete(models.Model): class Meta: triggers = [ pgtrigger.Protect(name="protect_deletes", operation=pgtrigger.Delete) ] # Bypass deletion protection with pgtrigger.ignore("my_app.CannotDelete:protect_deletes"): CannotDelete.objects.all().delete() ``` -------------------------------- ### pgtrigger.registered Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Returns a list of (model, trigger) tuples for all registered triggers or for specific URIs. ```APIDOC ## `pgtrigger.registered` — Inspect Registered Triggers Returns a list of `(model, trigger)` tuples for all registered triggers or for specific URIs. ```python import pgtrigger # Get all registered triggers for model, trigger in pgtrigger.registered(): print(f"{trigger.get_uri(model)} → {model._meta.db_table}") # Get specific triggers by URI pairs = pgtrigger.registered( "myapp.Post:lock_published", "myapp.AuditLog:append_only", ) for model, trigger in pairs: status, enabled = trigger.get_installation_status(model) print(f"{trigger.name}: {status}, enabled={enabled}") ``` ``` -------------------------------- ### Handling Transactions with pgtrigger.ignore Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/ignoring_triggers.md When using pgtrigger.ignore within a transaction that might encounter errors, wrap the outer transaction in `with pgtrigger.ignore.session():` or the inner `try/except` block in `with transaction.atomic():` to prevent errors caused by flushing temporary Postgres variables. ```python with transaction.atomic(): with ptrigger.ignore("app.Model:protect_inserts"): try: # Create an object that raises an integrity error app.Model.objects.create(unique_key="duplicate") except IntegrityError: # Ignore the integrity error pass # When we exit the context manager here, it will try to flush # a local Postgres variable. This causes an error because the transaction # is in an errored state. ``` -------------------------------- ### Trigger Clauses Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/module.md Components used to define the conditions and actions of a trigger. ```APIDOC ## class pgtrigger.Row ## class pgtrigger.Statement ## class pgtrigger.After ## class pgtrigger.Before ## class pgtrigger.InsteadOf ## class pgtrigger.Insert ## class pgtrigger.Update ## class pgtrigger.Delete ## class pgtrigger.Truncate ## class pgtrigger.UpdateOf ## class pgtrigger.Referencing ## class pgtrigger.Immediate ## class pgtrigger.Deferred ## class pgtrigger.Func ``` -------------------------------- ### Append-Only Model Protection Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md Create an append-only model that prevents updates and deletes using `pgtrigger.Protect`. This ensures data integrity by disallowing modifications after creation. ```python class AppendOnlyModel(models.Model): my_field = models.IntField() class Meta: triggers = [ pgtrigger.Protect( name="append_only", operation=(pgtrigger.Update | pgtrigger.Delete) ) ] ``` -------------------------------- ### Raw SQL Condition for Triggers Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Use `pgtrigger.Condition` to supply a raw SQL fragment for the trigger's `WHEN` clause. This is useful for complex conditions not covered by other condition classes. ```python import pgtrigger from django.db import models class Ledger(models.Model): amount = models.IntegerField() note = models.TextField(blank=True) class Meta: triggers = [ # Raw SQL condition: fire when any field changes pgtrigger.Protect( name="protect_any_change", operation=pgtrigger.Update, condition=pgtrigger.Condition("OLD.* IS DISTINCT FROM NEW.*"), ), # Only allow positive amounts pgtrigger.Protect( name="enforce_positive_amount", operation=pgtrigger.Insert | pgtrigger.Update, condition=pgtrigger.Condition("NEW.amount < 0"), ), ] ``` -------------------------------- ### Enforce Field Transitions with pgtrigger.FSM Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md This trigger enforces valid state transitions for a model field, similar to a finite state machine. It only works for non-null CharField fields and can optionally use a condition to limit when transitions are enforced. ```python class MyModel(models.Model): """Enforce valid transitions of the "status" field""" status = models.CharField(max_length=32, default="unpublished") class Meta: triggers = [ pgtrigger.FSM( name="status_fsm", field="status", transitions=[ ("unpublished", "published"), ("published", "inactive"), ] ) ] ``` -------------------------------- ### pgtrigger.register Source: https://context7.com/ambitioneng/django-pgtrigger/llms.txt Decorator or function for attaching triggers to models outside of the Meta class, primarily for third-party model targets. ```APIDOC ## `pgtrigger.register` — Programmatic Trigger Registration Decorator or function for attaching triggers to models outside of the `Meta` class, primarily for third-party model targets. ```python import pgtrigger from django.contrib.auth.models import User # Protect Django's built-in User model using a proxy @pgtrigger.register( pgtrigger.Protect(name="protect_deletes", operation=pgtrigger.Delete) ) class UserProxy(User): class Meta: proxy = True # Functional form pgtrigger.register( pgtrigger.SoftDelete(name="soft_delete", field="is_active") )(MyModel) # Abstract base model with inherited trigger class AuditBase(models.Model): is_active = models.BooleanField(default=True) class Meta: abstract = True triggers = [pgtrigger.SoftDelete(name="soft_delete", field="is_active")] class Article(AuditBase): title = models.CharField(max_length=200) class Meta(AuditBase.Meta): ordering = ["title"] # inherits triggers = [SoftDelete(...)] ``` ``` -------------------------------- ### Condition: Any Field Change (Excluding Auto Fields) Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/conditional.md This condition fires on changes to any fields except 'auto_now' and 'auto_now_add' datetime fields. Included and excluded fields can both be supplied. ```python # Fires on changes to any fields except auto_now and auto_now_add fields pgtrigger.AnyChange(exclude_auto=True) ``` -------------------------------- ### Programmatically Register Trigger Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/advanced_installation.md Register triggers programmatically using `pgtrigger.register`. This can be used as a decorator or called directly on a model. ```python # Register a protection trigger for a model pgtrigger.register(pgtrigger.Protect(...))(MyModel) ``` -------------------------------- ### Conditional Deletion Protection Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/cookbook.md Implement dynamic deletion protection where models can only be deleted if they have a specific flag (`is_deletable`) set to true. This prevents accidental deletion of critical data. ```python class DynamicDeletionModel(models.Model): is_deletable = models.BooleanField(default=False) class Meta: triggers = [ pgtrigger.Protect( name="protect_deletes", operation=pgtrigger.Delete, condition=pgtrigger.Q(old__is_deletable=False) ) ] ``` -------------------------------- ### Trigger Definition Source: https://github.com/ambitioneng/django-pgtrigger/blob/main/docs/module.md Defines a database trigger with various clauses for behavior customization. ```APIDOC ## class pgtrigger.Trigger Represents a database trigger. Allows definition of operations, timing, referencing, and conditions. ```