From 416a6caf2d4124b884fd1a04841908e572d92c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adeodato=20Sim=C3=B3?= Date: Fri, 24 Nov 2023 22:26:13 -0300 Subject: [PATCH] Define `author_search_vector_trigger` via Author.Meta.triggers Previously, triggers lived only in a particular migration file. With this change, code for the triggers resides in the model, and their lifecycle is managed through normal Django migrations. --- ...grate_search_vec_triggers_to_pgtriggers.py | 50 +++++++++++++++++++ bookwyrm/models/author.py | 29 +++++++++++ bookwyrm/settings.py | 1 + bookwyrm/utils/db.py | 22 ++++++++ requirements.txt | 1 + 5 files changed, 103 insertions(+) create mode 100644 bookwyrm/migrations/0191_migrate_search_vec_triggers_to_pgtriggers.py create mode 100644 bookwyrm/utils/db.py diff --git a/bookwyrm/migrations/0191_migrate_search_vec_triggers_to_pgtriggers.py b/bookwyrm/migrations/0191_migrate_search_vec_triggers_to_pgtriggers.py new file mode 100644 index 000000000..10354fa67 --- /dev/null +++ b/bookwyrm/migrations/0191_migrate_search_vec_triggers_to_pgtriggers.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.20 on 2023-11-25 00:47 + +from importlib import import_module +import re + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + +trigger_migration = import_module("bookwyrm.migrations.0077_auto_20210623_2155") + +# it's _very_ convenient for development that this migration be reversible +search_vector_trigger = trigger_migration.Migration.operations[4] +author_search_vector_trigger = trigger_migration.Migration.operations[5] + + +assert re.search(r"\bCREATE TRIGGER search_vector_trigger\b", search_vector_trigger.sql) +assert re.search( + r"\bCREATE TRIGGER author_search_vector_trigger\b", + author_search_vector_trigger.sql, +) + + +class Migration(migrations.Migration): + dependencies = [ + ("bookwyrm", "0190_book_search_updates"), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name="author", + trigger=pgtrigger.compiler.Trigger( + name="reset_search_vector_on_author_edit", + sql=pgtrigger.compiler.UpsertTriggerSql( + func="WITH book AS (SELECT bookwyrm_book.id AS row_id FROM bookwyrm_author LEFT OUTER JOIN bookwyrm_book_authors ON bookwyrm_book_authors.id = new.id LEFT OUTER JOIN bookwyrm_book ON bookwyrm_book.id = bookwyrm_book_authors.book_id) UPDATE bookwyrm_book SET search_vector = '' FROM book WHERE id = book.row_id;RETURN NEW;", + hash="9c0a472e2bf60e63d593cce49f47972c7b227a00", + operation='UPDATE OF "name"', + pgid="pgtrigger_reset_search_vector_on_author_edit_a447c", + table="bookwyrm_author", + when="AFTER", + ), + ), + ), + migrations.RunSQL( + sql="""DROP TRIGGER IF EXISTS author_search_vector_trigger ON bookwyrm_author; + DROP FUNCTION IF EXISTS author_trigger; + """, + reverse_sql=author_search_vector_trigger.sql, + ), + ] diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py index b4488d46a..9b4f3c1bd 100644 --- a/bookwyrm/models/author.py +++ b/bookwyrm/models/author.py @@ -3,9 +3,11 @@ import re from typing import Tuple, Any from django.db import models +import pgtrigger from bookwyrm import activitypub from bookwyrm.settings import DOMAIN +from bookwyrm.utils.db import format_trigger from .book import BookDataModel from . import fields @@ -66,4 +68,31 @@ class Author(BookDataModel): """editions and works both use "book" instead of model_name""" return f"https://{DOMAIN}/author/{self.id}" + class Meta: + """sets up indexes and triggers""" + + triggers = [ + pgtrigger.Trigger( + name="reset_search_vector_on_author_edit", + when=pgtrigger.After, + operation=pgtrigger.UpdateOf("name"), + func=format_trigger( + """WITH book AS ( + SELECT bookwyrm_book.id AS row_id + FROM bookwyrm_author + LEFT OUTER JOIN bookwyrm_book_authors + ON bookwyrm_book_authors.id = new.id + LEFT OUTER JOIN bookwyrm_book + ON bookwyrm_book.id = bookwyrm_book_authors.book_id + ) + UPDATE bookwyrm_book + SET search_vector = '' + FROM book + WHERE id = book.row_id; + RETURN new; + """ + ), + ) + ] + activity_serializer = activitypub.Author diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 4cecc4df6..aaa50c56d 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -104,6 +104,7 @@ INSTALLED_APPS = [ "celery", "django_celery_beat", "imagekit", + "pgtrigger", "storages", ] diff --git a/bookwyrm/utils/db.py b/bookwyrm/utils/db.py new file mode 100644 index 000000000..8b74d9bf5 --- /dev/null +++ b/bookwyrm/utils/db.py @@ -0,0 +1,22 @@ +""" Database utilities """ + +from typing import cast +import sqlparse # type: ignore + + +def format_trigger(sql: str) -> str: + """format SQL trigger before storing + + we remove whitespace and use consistent casing so as to avoid migrations + due to formatting changes. + """ + return cast( + str, + sqlparse.format( + sql, + strip_comments=True, + strip_whitespace=True, + keyword_case="upper", + identifier_case="lower", + ), + ) diff --git a/requirements.txt b/requirements.txt index 36192f148..05fd9d2b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ django-celery-beat==2.4.0 django-compressor==4.3.1 django-imagekit==4.1.0 django-model-utils==4.3.1 +django-pgtrigger==4.10.0 django-sass-processor==1.2.2 django-csp==3.7 environs==9.5.0