Skip to content

Commit dda8e6a

Browse files
committed
When a materialized view doesn't need to be recreated, ensure indexes are created correctly
1 parent 21f274c commit dda8e6a

File tree

2 files changed

+74
-22
lines changed

2 files changed

+74
-22
lines changed

django_pgviews/view.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from django.core import exceptions
1212
from django.db import connection, transaction
1313
from django.db import models
14-
from django.db.models import Index
1514
from django.db.models.query import QuerySet
1615

1716
from django_pgviews.db import get_fields_by_name
@@ -100,6 +99,55 @@ def _concurrent_index_name(view_name, concurrent_index):
10099
return view_name + "_" + "_".join([s.strip() for s in concurrent_index.split(",")]) + "_index"
101100

102101

102+
def _create_concurrent_index(cursor, view_name, concurrent_index):
103+
cursor.execute(
104+
"CREATE UNIQUE INDEX {index_name} ON {view_name} ({concurrent_index})".format(
105+
view_name=view_name,
106+
index_name=_concurrent_index_name(view_name, concurrent_index),
107+
concurrent_index=concurrent_index,
108+
)
109+
)
110+
111+
112+
def _ensure_indexes(connection, cursor, view_cls, schema_name_log):
113+
"""
114+
This function gets called when a materialized view is deemed not needing a re-create. That is however only a part
115+
of the story, since that checks just the SQL of the view itself. The second part is the indexes.
116+
This function gets the current indexes on the materialized view and reconciles them with the indexes that
117+
should be in the view, dropping extra ones and creating new ones.
118+
"""
119+
view_name = view_cls._meta.db_table
120+
concurrent_index = view_cls._concurrent_index
121+
indexes = view_cls._meta.indexes
122+
vschema, vname = _schema_and_name(connection, view_name)
123+
124+
cursor.execute("SELECT indexname FROM pg_indexes WHERE tablename = %s AND schemaname = %s", [vname, vschema])
125+
126+
existing_indexes = set(x[0] for x in cursor.fetchall())
127+
required_indexes = set(x.name for x in indexes)
128+
129+
if view_cls._concurrent_index is not None:
130+
concurrent_index_name = _concurrent_index_name(view_name, concurrent_index)
131+
required_indexes.add(concurrent_index_name)
132+
else:
133+
concurrent_index_name = None
134+
135+
for index_name in existing_indexes - required_indexes:
136+
cursor.execute(f"DROP INDEX {index_name}")
137+
log.info("pgview dropped index %s on view %s (%s)", index_name, view_name, schema_name_log)
138+
139+
for index_name in required_indexes - existing_indexes:
140+
if index_name == concurrent_index_name:
141+
_create_concurrent_index(cursor, view_name, concurrent_index)
142+
log.info("pgview created concurrent index on view %s (%s)", view_name, schema_name_log)
143+
else:
144+
for index in indexes:
145+
if index.name == index_name:
146+
connection.schema_editor().add_index(view_cls, index)
147+
log.info("pgview created index %s on view %s (%s)", index.name, view_name, schema_name_log)
148+
break
149+
150+
103151
@transaction.atomic()
104152
def create_materialized_view(connection, view_cls, check_sql_changed=False):
105153
"""
@@ -155,6 +203,7 @@ def create_materialized_view(connection, view_cls, check_sql_changed=False):
155203
_drop_mat_view(cursor, temp_viewname)
156204

157205
if definitions[0] == definitions[1]:
206+
_ensure_indexes(connection, cursor, view_cls, schema_name_log)
158207
return "EXISTS"
159208

160209
if view_exists:
@@ -165,13 +214,7 @@ def create_materialized_view(connection, view_cls, check_sql_changed=False):
165214
log.info("pgview created materialized view %s (%s)", view_name, schema_name_log)
166215

167216
if concurrent_index is not None:
168-
cursor.execute(
169-
"CREATE UNIQUE INDEX {index_name} ON {view_name} ({concurrent_index})".format(
170-
view_name=view_name,
171-
index_name=_concurrent_index_name(view_name, concurrent_index),
172-
concurrent_index=concurrent_index,
173-
)
174-
)
217+
_create_concurrent_index(cursor, view_name, concurrent_index)
175218
log.info("pgview created concurrent index on view %s (%s)", view_name, schema_name_log)
176219

177220
if view_cls._meta.indexes:

tests/test_project/test_project/viewtest/tests.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ def create_test_schema(sender, app_config, **kwargs):
2727
cursor.execute(command)
2828

2929

30+
def get_list_of_indexes(cursor, cls):
31+
cursor.execute("SELECT indexname FROM pg_indexes WHERE tablename = %s", [cls._meta.db_table])
32+
return set(x[0] for x in cursor.fetchall())
33+
34+
3035
class ViewTestCase(TestCase):
3136
"""
3237
Run the tests to ensure the post_migrate hooks were called.
@@ -133,26 +138,30 @@ def test_materialized_view(self):
133138
)
134139

135140
def test_materialized_view_indexes(self):
136-
args = [
137-
models.MaterializedRelatedViewWithIndex._meta.db_table,
138-
"viewtest_materializedrelatedviewwithindex_id_index",
139-
]
140-
141141
with connection.cursor() as cursor:
142+
orig_indexes = get_list_of_indexes(cursor, models.MaterializedRelatedViewWithIndex)
143+
144+
self.assertIn("viewtest_materializedrelatedviewwithindex_id_index", orig_indexes)
145+
self.assertEqual(len(orig_indexes), 2)
146+
147+
for index_name in orig_indexes:
148+
cursor.execute(f"DROP INDEX {index_name}")
149+
142150
cursor.execute(
143-
"SELECT COUNT(*) FROM pg_indexes WHERE tablename = %s AND indexname = %s",
144-
args,
151+
"CREATE UNIQUE INDEX viewtest_materializedrelatedviewwithindex_concurrent_idx "
152+
"ON viewtest_materializedrelatedviewwithindex (id)"
145153
)
146-
id_index_count = cursor.fetchone()[0]
147-
148154
cursor.execute(
149-
"SELECT COUNT(*) FROM pg_indexes WHERE tablename = %s AND indexname != %s",
150-
args,
155+
"CREATE INDEX viewtest_materializedrelatedviewwithindex_some_idx "
156+
"ON viewtest_materializedrelatedviewwithindex (model_id)"
151157
)
152-
other_index_count = cursor.fetchone()[0]
153158

154-
self.assertEqual(id_index_count, 1)
155-
self.assertEqual(other_index_count, 1)
159+
call_command("sync_pgviews", materialized_views_check_sql_changed=True)
160+
161+
with connection.cursor() as cursor:
162+
new_indexes = get_list_of_indexes(cursor, models.MaterializedRelatedViewWithIndex)
163+
164+
self.assertEqual(new_indexes, orig_indexes)
156165

157166
def test_materialized_view_with_no_data(self):
158167
"""

0 commit comments

Comments
 (0)