Skip to content

Commit dd19eba

Browse files
authored
Merge pull request #77 from themrinalsinha/feat/disable-sync-view-on-migrate
Resolves #76 Decouple post_migrate signal from refreshing the views
2 parents 3a71ce2 + e21b498 commit dd19eba

File tree

4 files changed

+85
-12
lines changed

4 files changed

+85
-12
lines changed

README.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ This will take all fields on `myapp.Customer` and apply them to
137137

138138
## Features
139139

140+
### Configuration
141+
`MATERIALIZED_VIEWS_DISABLE_SYNC_ON_MIGRATE`
142+
143+
When set to True, it skips running `sync_pgview` during migrations, which can be useful if you want to control the synchronization manually or avoid potential overhead during migrations. (default: False)
144+
```
145+
MATERIALIZED_VIEWS_DISABLE_SYNC_ON_MIGRATE = True
146+
```
147+
140148
### Updating Views
141149

142150
Sometimes your models change and you need your Database Views to reflect the new
@@ -215,9 +223,9 @@ def customer_saved(sender, action=None, instance=None, **kwargs):
215223

216224
Postgres 9.4 and up allow materialized views to be refreshed concurrently, without blocking reads, as long as a
217225
unique index exists on the materialized view. To enable concurrent refresh, specify the name of a column that can be
218-
used as a unique index on the materialized view. Unique index can be defined on more than one column of a materialized
219-
view. Once enabled, passing `concurrently=True` to the model's refresh method will result in postgres performing the
220-
refresh concurrently. (Note that the refresh method itself blocks until the refresh is complete; concurrent refresh is
226+
used as a unique index on the materialized view. Unique index can be defined on more than one column of a materialized
227+
view. Once enabled, passing `concurrently=True` to the model's refresh method will result in postgres performing the
228+
refresh concurrently. (Note that the refresh method itself blocks until the refresh is complete; concurrent refresh is
221229
most useful when materialized views are updated in another process or thread.)
222230

223231
Example:
@@ -245,7 +253,7 @@ def customer_saved(sender, action=None, instance=None, **kwargs):
245253

246254
#### Indexes
247255

248-
As the materialized view isn't defined through the usual Django model fields, any indexes defined there won't be
256+
As the materialized view isn't defined through the usual Django model fields, any indexes defined there won't be
249257
created on the materialized view. Luckily Django provides a Meta option called `indexes` which can be used to add custom
250258
indexes to models. `pg_views` supports defining indexes on materialized views using this option.
251259

@@ -265,7 +273,7 @@ class PreferredCustomer(pg.MaterializedView):
265273

266274
name = models.CharField(max_length=100)
267275
post_code = models.CharField(max_length=20, db_index=True)
268-
276+
269277
class Meta:
270278
managed = False # don't forget this, otherwise Django will think it's a regular model
271279
indexes = [
@@ -277,7 +285,7 @@ class PreferredCustomer(pg.MaterializedView):
277285

278286
Materialized views can be created either with or without data. By default, they are created with data, however
279287
`pg_views` supports creating materialized views without data, by defining `with_data = False` for the
280-
`pg.MaterializedView` class. Such views then do not support querying until the first
288+
`pg.MaterializedView` class. Such views then do not support querying until the first
281289
refresh (raising `django.db.utils.OperationalError`).
282290

283291
Example:
@@ -304,7 +312,7 @@ checks existing materialized view definition in the database (if the mat. view e
304312
definition with the one currently defined in your `pg.MaterializedView` subclass. If the definition matches
305313
exactly, the re-create of materialized view is skipped.
306314

307-
This feature is enabled by setting the `MATERIALIZED_VIEWS_CHECK_SQL_CHANGED` in your Django settings to `True`,
315+
This feature is enabled by setting the `MATERIALIZED_VIEWS_CHECK_SQL_CHANGED` in your Django settings to `True`,
308316
which enables the feature when running `migrate`. The command `sync_pgviews` uses this setting as well,
309317
however it also has switches `--enable-materialized-views-check-sql-changed` and
310318
`--disable-materialized-views-check-sql-changed` which override this setting for that command.
@@ -316,14 +324,14 @@ on change of the content but not the name.
316324

317325
### Schemas
318326

319-
By default, the views will get created in the schema of the database, this is usually `public`.
320-
The package supports the database defining the schema in the settings by using
327+
By default, the views will get created in the schema of the database, this is usually `public`.
328+
The package supports the database defining the schema in the settings by using
321329
options (`"OPTIONS": {"options": "-c search_path=custom_schema"}`).
322330

323331
The package `django-tenants` is supported as well, if used.
324332

325333
It is possible to define the schema explicitly for a view, if different from the default schema of the database, like
326-
this:
334+
this:
327335

328336
```python
329337
from django_pgviews import view as pg

django_pgviews/apps.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,9 @@ def ready(self):
4444
"""
4545
Find and setup the apps to set the post_migrate hooks for.
4646
"""
47-
signals.post_migrate.connect(self.sync_pgviews)
47+
from django.conf import settings
48+
49+
sync_enabled = getattr(settings, "MATERIALIZED_VIEWS_DISABLE_SYNC_ON_MIGRATE", False) is False
50+
51+
if sync_enabled:
52+
signals.post_migrate.connect(self.sync_pgviews)

tests/test_project/settings/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,5 @@
174174
"django_pgviews": {"level": "INFO", "handlers": ["console"]},
175175
},
176176
}
177+
178+
MATERIALIZED_VIEWS_SYNC_DISABLED = False

tests/test_project/viewtest/tests.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
from contextlib import closing
44
from datetime import timedelta
55

6+
from django.apps import apps
67
from django.conf import settings
78
from django.contrib import auth
89
from django.contrib.auth.models import User
910
from django.core.management import call_command
1011
from django.db import DEFAULT_DB_ALIAS, connection
12+
from django.db.models.signals import post_migrate
1113
from django.db.utils import DatabaseError, OperationalError
1214
from django.dispatch import receiver
13-
from django.test import TestCase
15+
from django.test import TestCase, override_settings
1416
from django.utils import timezone
1517

1618
from django_pgviews.signals import all_views_synced, view_synced
@@ -438,3 +440,59 @@ def test_no_schema_list(self):
438440
where_fragment, params = _make_where(schemaname=None, tablename=["test_tablename1", "test_tablename2"])
439441
self.assertEqual(where_fragment, "tablename IN (%s, %s)")
440442
self.assertEqual(params, ["test_tablename1", "test_tablename2"])
443+
444+
445+
class TestMaterializedViewSyncDisabledSettings(TestCase):
446+
def setUp(self):
447+
"""
448+
NOTE: By default, Django runs and registers signals with default values during
449+
test execution. To address this, we store the original receivers and settings,
450+
then restore them in tearDown to avoid affecting other tests.
451+
"""
452+
453+
# Store original receivers and settings
454+
self._original_receivers = list(post_migrate.receivers)
455+
self._original_config = apps.get_app_config("django_pgviews").counter
456+
457+
# Clear existing signal receivers
458+
post_migrate.receivers.clear()
459+
460+
# Get the app config and reset counter
461+
config = apps.get_app_config("django_pgviews")
462+
config.counter = 0
463+
464+
# Reload app config with new settings
465+
with override_settings(MATERIALIZED_VIEWS_DISABLE_SYNC_ON_MIGRATE=True):
466+
config.ready()
467+
468+
# Drop the view if it exists
469+
with connection.cursor() as cursor:
470+
cursor.execute("DROP MATERIALIZED VIEW IF EXISTS viewtest_materializedrelatedview CASCADE;")
471+
472+
def tearDown(self):
473+
"""Restore original signal receivers and app config state"""
474+
475+
post_migrate.receivers.clear()
476+
post_migrate.receivers.extend(self._original_receivers)
477+
apps.get_app_config("django_pgviews").counter = self._original_config
478+
479+
def test_migrate_materialized_views_sync_disabled(self):
480+
self.assertEqual(models.TestModel.objects.count(), 0)
481+
482+
models.TestModel.objects.create(name="Test")
483+
484+
call_command("migrate") # migrate is not running sync_pgviews
485+
with connection.cursor() as cursor:
486+
cursor.execute(
487+
"SELECT EXISTS (SELECT 1 FROM pg_matviews WHERE matviewname = 'viewtest_materializedrelatedview');"
488+
)
489+
exists = cursor.fetchone()[0]
490+
self.assertFalse(exists, "Materialized view viewtest_materializedrelatedview should not exist.")
491+
492+
call_command("sync_pgviews") # explicitly run sync_pgviews
493+
with connection.cursor() as cursor:
494+
cursor.execute(
495+
"SELECT EXISTS (SELECT 1 FROM pg_matviews WHERE matviewname = 'viewtest_materializedrelatedview');"
496+
)
497+
exists = cursor.fetchone()[0]
498+
self.assertTrue(exists, "Materialized view viewtest_materializedrelatedview should exist.")

0 commit comments

Comments
 (0)