Browse Source

#844: added tests and migrations

Rafał Pitoń 8 years ago
parent
commit
f0f2566fc9

+ 18 - 15
misago/core/pgutils.py

@@ -13,7 +13,7 @@ class PgPartialIndex(Index):
             raise ValueError('partial index requires WHERE clause')
             raise ValueError('partial index requires WHERE clause')
         self.where = where
         self.where = where
 
 
-        super(PartialIndex, self).__init__(fields, name)
+        super(PgPartialIndex, self).__init__(fields, name)
 
 
     def set_name_with_model(self, model):
     def set_name_with_model(self, model):
         table_name = model._meta.db_table
         table_name = model._meta.db_table
@@ -25,7 +25,7 @@ class PgPartialIndex(Index):
 
 
         # The length of the parts of the name is based on the default max
         # The length of the parts of the name is based on the default max
         # length of 30 characters.
         # length of 30 characters.
-        hash_data = [table_name] + where_items + [self.suffix]
+        hash_data = [table_name] + self.fields + where_items + [self.suffix]
         self.name = '%s_%s_%s' % (
         self.name = '%s_%s_%s' % (
             table_name[:11],
             table_name[:11],
             column_names[0][:7],
             column_names[0][:7],
@@ -52,45 +52,48 @@ class PgPartialIndex(Index):
                 'where': "'{}'".format(', '.join(where_items)),
                 'where': "'{}'".format(', '.join(where_items)),
             }
             }
         else:
         else:
-            return super(PartialIndex, self).__repr__()
+            return super(PgPartialIndex, self).__repr__()
 
 
     def deconstruct(self):
     def deconstruct(self):
-        path, args, kwargs = super(PartialIndex, self).deconstruct()
+        path, args, kwargs = super(PgPartialIndex, self).deconstruct()
         kwargs['where'] = self.where
         kwargs['where'] = self.where
         return path, args, kwargs
         return path, args, kwargs
 
 
     def get_sql_create_template_values(self, model, schema_editor, using):
     def get_sql_create_template_values(self, model, schema_editor, using):
-        parameters = super(PartialIndex, self).get_sql_create_template_values(model, schema_editor, '')
-        parameters['extra'] = self.get_sql_extra(schema_editor)
+        parameters = super(PgPartialIndex, self).get_sql_create_template_values(
+            model, schema_editor, '')
+        parameters['extra'] = self.get_sql_extra(model, schema_editor)
         return parameters
         return parameters
 
 
-    def get_sql_extra(self, schema_editor):
+    def get_sql_extra(self, model, schema_editor):
         quote_name = schema_editor.quote_name
         quote_name = schema_editor.quote_name
         quote_value = schema_editor.quote_value
         quote_value = schema_editor.quote_value
 
 
         clauses = []
         clauses = []
         for field, condition in self.where.items():
         for field, condition in self.where.items():
-            fieldname = None
+            field_name = None
             compr = None
             compr = None
             if field.endswith('__lt'):
             if field.endswith('__lt'):
-                fieldname = field[:-4]
+                field_name = field[:-4]
                 compr = '<'
                 compr = '<'
             elif field.endswith('__gt'):
             elif field.endswith('__gt'):
-                fieldname = field[:-4]
+                field_name = field[:-4]
                 compr = '>'
                 compr = '>'
             elif field.endswith('__lte'):
             elif field.endswith('__lte'):
-                fieldname = field[:-5]
+                field_name = field[:-5]
                 compr = '<='
                 compr = '<='
             elif field.endswith('__gte'):
             elif field.endswith('__gte'):
-                fieldname = field[:-5]
+                field_name = field[:-5]
                 compr = '>='
                 compr = '>='
             else:
             else:
-                fieldname = field
+                field_name = field
                 compr = '='
                 compr = '='
 
 
+            column = model._meta.get_field(field_name).column
             clauses.append('{} {} {}'.format(
             clauses.append('{} {} {}'.format(
-                quote_name(fieldname), compr, quote_value(condition)))
-        return ' WHERE {}'.format(' AND '.join(clauses))
+                quote_name(column), compr, quote_value(condition)))
+        # sort clauses for their order to be determined and testable
+        return ' WHERE {}'.format(' AND '.join(sorted(clauses)))
 
 
 
 
 class CreatePartialIndex(RunSQL):
 class CreatePartialIndex(RunSQL):

+ 138 - 0
misago/core/tests/test_pgpartialindex.py

@@ -0,0 +1,138 @@
+from django.db import connection
+from django.test import TestCase
+
+from misago.threads.models import Thread
+
+from misago.core.pgutils import PgPartialIndex
+
+
+class PgPartialIndexTests(TestCase):
+    def test_multiple_fields(self):
+        """multiple fields are supported"""
+        with connection.schema_editor() as editor:
+            sql = PgPartialIndex(
+                fields=['has_events', 'is_hidden'],
+                name='test_partial',
+                where={'has_events': True},
+            ).create_sql(Thread, editor)
+
+            self.assertIn('CREATE INDEX "test_partial" ON "misago_threads_thread"', sql)
+            self.assertIn('ON "misago_threads_thread" ("has_events", "is_hidden")', sql)
+
+    def test_where_clauses(self):
+        """where clauses generate correctly"""
+        with connection.schema_editor() as editor:
+            sql = PgPartialIndex(
+                fields=['has_events'],
+                name='test_partial',
+                where={'has_events': True},
+            ).create_sql(Thread, editor)
+
+            self.assertTrue(sql.endswith('WHERE "has_events" = true'))
+
+            sql = PgPartialIndex(
+                fields=['has_events'],
+                name='test_partial',
+                where={'has_events': False},
+            ).create_sql(Thread, editor)
+            self.assertTrue(sql.endswith('WHERE "has_events" = false'))
+
+            sql = PgPartialIndex(
+                fields=['has_events'],
+                name='test_partial',
+                where={'has_events': 42},
+            ).create_sql(Thread, editor)
+            self.assertTrue(sql.endswith('WHERE "has_events" = 42'))
+
+            sql = PgPartialIndex(
+                fields=['has_events'],
+                name='test_partial',
+                where={'has_events__lt': 42},
+            ).create_sql(Thread, editor)
+            self.assertTrue(sql.endswith('WHERE "has_events" < 42'))
+
+            sql = PgPartialIndex(
+                fields=['has_events'],
+                name='test_partial',
+                where={'has_events__gt': 42},
+            ).create_sql(Thread, editor)
+            self.assertTrue(sql.endswith('WHERE "has_events" > 42'))
+
+            sql = PgPartialIndex(
+                fields=['has_events'],
+                name='test_partial',
+                where={'has_events__lte': 42},
+            ).create_sql(Thread, editor)
+            self.assertTrue(sql.endswith('WHERE "has_events" <= 42'))
+
+            sql = PgPartialIndex(
+                fields=['has_events'],
+                name='test_partial',
+                where={'has_events__gte': 42},
+            ).create_sql(Thread, editor)
+            self.assertTrue(sql.endswith('WHERE "has_events" >= 42'))
+
+    def test_multiple_where_clauses(self):
+        """where clause with multiple conditions generates correctly"""
+        with connection.schema_editor() as editor:
+            sql = PgPartialIndex(
+                fields=['has_events'],
+                name='test_partial',
+                where={
+                    'has_events__gte': 42,
+                    'is_hidden': True,
+                },
+            ).create_sql(Thread, editor)
+            self.assertTrue(sql.endswith('WHERE "has_events" >= 42 AND "is_hidden" = true'))
+
+    def test_set_name_with_model(self):
+        """valid index name is autogenerated"""
+        index = PgPartialIndex(
+            fields=['has_events', 'is_hidden'],
+            where={'has_events': True},
+        )
+        index.set_name_with_model(Thread)
+        self.assertEqual(index.name, 'misago_thre_has_eve_1b05b8_part')
+
+        index = PgPartialIndex(
+            fields=['has_events', 'is_hidden', 'is_closed'],
+            where={'has_events': True},
+        )
+        index.set_name_with_model(Thread)
+        self.assertEqual(index.name, 'misago_thre_has_eve_eaab5e_part')
+
+        index = PgPartialIndex(
+            fields=['has_events', 'is_hidden', 'is_closed'],
+            where={
+                'has_events': True,
+                'is_closed': False,
+            },
+        )
+        index.set_name_with_model(Thread)
+        self.assertEqual(index.name, 'misago_thre_has_eve_e738fe_part')
+
+    def test_index_repr(self):
+        """index creates descriptive representation string"""
+        index = PgPartialIndex(
+            fields=['has_events'],
+            where={'has_events': True},
+        )
+        self.assertEqual(repr(index), "<PgPartialIndex: fields='has_events', where='has_events=True'>")
+
+        index = PgPartialIndex(
+            fields=['has_events', 'is_hidden'],
+            where={'has_events': True},
+        )
+        self.assertIn("fields='has_events, is_hidden',", repr(index))
+        self.assertIn(", where='has_events=True'", repr(index))
+
+        index = PgPartialIndex(
+            fields=['has_events', 'is_hidden', 'is_closed'],
+            where={
+                'has_events': True,
+                'is_closed': False,
+                'replies__gte': 5,
+            },
+        )
+        self.assertIn("fields='has_events, is_hidden, is_closed',", repr(index))
+        self.assertIn(", where='has_events=True, is_closed=False, replies__gte=5'", repr(index))

+ 72 - 0
misago/threads/migrations/0006_redo_partial_indexes.py

@@ -24,6 +24,78 @@ class Migration(migrations.Migration):
                 WHERE has_open_reports = TRUE
                 WHERE has_open_reports = TRUE
             """,
             """,
         ),
         ),
+        migrations.RunSQL(
+            "DROP INDEX misago_post_is_hidden_partial",
+            """
+                CREATE INDEX misago_post_is_hidden_partial
+                ON misago_threads_post(is_hidden)
+                WHERE is_hidden = FALSE
+            """,
+        ),
+        migrations.RunSQL(
+            "DROP INDEX misago_thread_is_global",
+            """
+                CREATE INDEX misago_thread_is_global
+                ON misago_threads_thread(weight)
+                WHERE weight = 2
+            """,
+        ),
+        migrations.RunSQL(
+            "DROP INDEX misago_thread_is_local",
+            """
+                CREATE INDEX misago_thread_is_local
+                ON misago_threads_thread(weight)
+                WHERE weight < 2
+            """,
+        ),
+        migrations.RunSQL(
+            "DROP INDEX misago_thread_has_reported_posts_partial",
+            """
+                CREATE INDEX misago_thread_has_reported_posts_partial
+                ON misago_threads_thread(has_reported_posts)
+                WHERE has_reported_posts = TRUE
+            """,
+        ),
+        migrations.RunSQL(
+            "DROP INDEX misago_thread_has_unapproved_posts_partial",
+            """
+                CREATE INDEX misago_thread_has_unapproved_posts_partial
+                ON misago_threads_thread(has_unapproved_posts)
+                WHERE has_unapproved_posts = TRUE
+            """,
+        ),
+        migrations.RunSQL(
+            "DROP INDEX misago_thread_is_hidden_partial",
+            """
+                CREATE INDEX misago_thread_is_hidden_partial
+                ON misago_threads_thread(is_hidden)
+                WHERE is_hidden = FALSE
+            """,
+        ),
+        migrations.RunSQL(
+            "DROP INDEX misago_thread_is_pinned_globally_partial",
+            """
+                CREATE INDEX misago_thread_is_pinned_globally_partial
+                ON misago_threads_thread(weight)
+                WHERE weight = 2
+            """,
+        ),
+        migrations.RunSQL(
+            "DROP INDEX misago_thread_is_pinned_locally_partial",
+            """
+                CREATE INDEX misago_thread_is_pinned_locally_partial
+                ON misago_threads_thread(weight)
+                WHERE weight = 1
+            """,
+        ),
+        migrations.RunSQL(
+            "DROP INDEX misago_thread_is_unpinned_partial",
+            """
+                CREATE INDEX misago_thread_is_unpinned_partial
+                ON misago_threads_thread(weight)
+                WHERE weight = 0
+            """,
+        ),
 
 
         migrations.AddIndex(
         migrations.AddIndex(
             model_name='post',
             model_name='post',

+ 41 - 0
misago/users/migrations/0009_redo_partial_indexes.py

@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.1 on 2017-05-26 21:56
+from __future__ import unicode_literals
+
+from django.db import migrations
+import misago.core.pgutils
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_users', '0008_ban_registration_only'),
+    ]
+
+    operations = [
+        migrations.RunSQL(
+            "DROP INDEX misago_users_user_is_staff_partial",
+            """
+                CREATE INDEX misago_users_user_is_staff_partial
+                ON misago_users_user(is_staff)
+                WHERE is_staff = TRUE
+            """,
+        ),
+        migrations.RunSQL(
+            "DROP INDEX misago_users_user_requires_activation_partial",
+            """
+                CREATE INDEX misago_users_user_requires_activation_partial
+                ON misago_users_user(requires_activation)
+                WHERE requires_activation > 0
+            """,
+        ),
+
+        migrations.AddIndex(
+            model_name='user',
+            index=misago.core.pgutils.PgPartialIndex(fields=[b'is_staff'], name='misago_user_is_staf_f5a0fa_part', where={b'is_staff': True}),
+        ),
+        migrations.AddIndex(
+            model_name='user',
+            index=misago.core.pgutils.PgPartialIndex(fields=[b'requires_activation'], name='misago_user_require_50b161_part', where={b'requires_activation__gt': 0}),
+        ),
+    ]

+ 13 - 0
misago/users/models/user.py

@@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _
 from misago.acl import get_user_acl
 from misago.acl import get_user_acl
 from misago.acl.models import Role
 from misago.acl.models import Role
 from misago.conf import settings
 from misago.conf import settings
+from misago.core.pgutils import PgPartialIndex
 from misago.core.utils import slugify
 from misago.core.utils import slugify
 from misago.users import avatars
 from misago.users import avatars
 from misago.users.signatures import is_user_signature_valid
 from misago.users.signatures import is_user_signature_valid
@@ -268,6 +269,18 @@ class User(AbstractBaseUser, PermissionsMixin):
 
 
     objects = UserManager()
     objects = UserManager()
 
 
+    class Meta:
+        indexes = [
+            PgPartialIndex(
+                fields=['is_staff'],
+                where={'is_staff': True},
+            ),
+            PgPartialIndex(
+                fields=['requires_activation'],
+                where={'requires_activation__gt': 0},
+            ),
+        ]
+
     def clean(self):
     def clean(self):
         self.username = self.normalize_username(self.username)
         self.username = self.normalize_username(self.username)
         self.email = UserManager.normalize_email(self.email)
         self.email = UserManager.normalize_email(self.email)