-
Notifications
You must be signed in to change notification settings - Fork 46
PoC: Add an Exclusion Constraint for pagetranslation's slug uniqueness #3826
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
base: develop
Are you sure you want to change the base?
Conversation
Summary of options for SolutionOption 1: Database trigger with JOIN
Option 2: Denormalize region and Database trigger
Option 3: Denormalize region & Exclusion constraint (implemented in the PoC)
Option 4: Denormalize region & Create is_current field & Unique Constraint (with partial index) only for is_current version
Pseudo-Option: Materialized View (instead of column denormalization) & Database Trigger:This is (imo) not an option, because we would not guarantee absolute syncronicity of the Materialized View Table |
|
In Django 5.1 we get The generated field on pageTranslation model: and the exclusionConstraint: |
I believe that GeneratedField does not support this use case. In the expression for the generated field, you can only reference fields of the same model, so Apart from that, I think the trigger solution from Option 1 might be the cleanest, because we don't risk other data inconsistencies just to solve another data inconstency :) Also, I would assume, that the join does not have a huge performance impact since it will probably use the index on cms_page.id. For bulk operations, this might be a different story, but it's probably still feasible (?). Edit: On the other hand, I am not sure whether Option 1 would be safe with regard to concurrency (as opposed to the ExclusionConstraint). We might need to acquire table locks (e.g. |
Short description
This is part 2 of this PR #3784. It shows one way we could set a Constraint that guarantees a
slugs uniqueness (here only done for pagetranslation). In part I have implemented application level safeguards, but as mentioned there, they are not really sufficient to guarantee a slugs uniqueness. Using anExclusionConstraintis only one way though, there are other options which I will explain:Proposed changes
Why is a simple DB constraint not implementable as
UniqueConstraint?A unique constraint basically creates an Index (can be partial when used with condition). But we need the condition "where page_id differs from other page_id". Meaning we need to be comparing two rows, which is not doable neither in Django nor SQL.
But there are ways to ensure uniqueness of slugs on the database level especially with PostgreSQL. The different tools we need are
A database trigger alone would allow us to before-hook into every
INSERTorUPDATEon for example the cms_pagetranslation table and then call a function that checks for the uniqueness of the slug against the other pagetranslation rows and throws an exception if its not unique. I have great doubts though that this would be a sufficient solution, since on every Insert/Update-Trigger we would have to do a join on every single row, since we need to access the region field on the foreign key pageof every row we compare against. I have great doubts that this would not impact our performance noticably.the trigger (using the pgtrigger library):
Triggers can either be directly defined inside a
migrations.RunSQL(not the best way) or we can use the django-pgtrigger package, which allows us to define the trigger inside the META class of the model itself.Another option than this elaborate trigger described above would be to use the PostgresQL specific ExclusionConstraint that is actually a constraint that does exactly what we want
BUT it cannot cope with joins, so
(F('page__region', RangeOperators.Equal)wont work!=> this basically forces us into the position to consider denormalization of the region_id column into pagetranslation
WHAT DO I MEAN BY THAT?
Create a field region on pagetranslation (or rather on AbstractContentTranslation) like this
region = models.ForeignKey("cms.Region", null=True, editable=False, on_delete=models.CASCADE)Both rare cases, but we need to be safe
Once we would have region denormalized on pagetranslation, we then can either chose to implement the trigger from above without the
JOINor use theExclusionConstraintHow to test on the DB Level inside the shell
should throw an IntegrityError
Pull Request Review Guidelines
Side effects
Faithfulness to issue description and design
There are no intended deviations from the issue and design.
Resolved issues
Fixes: #3060
Blocked by: #3837
Related to: #3917
Pull Request Review Guidelines