Skip to content

Commit c809ae1

Browse files
committed
Add tests for explicit schema handling
1 parent 236ad91 commit c809ae1

File tree

7 files changed

+235
-20
lines changed

7 files changed

+235
-20
lines changed

tests/test_project/multidbtest/tests.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@
33
from django.core.management import call_command
44
from django.db import DEFAULT_DB_ALIAS, connections
55
from django.dispatch import receiver
6-
from django.test import TestCase, override_settings
6+
from django.test import TestCase
77

88
from django_pgviews.signals import view_synced
99

1010
from ..viewtest.models import RelatedView
1111
from .models import MonthlyObservation, Observation
12-
from .routers import WeatherPinnedRouter
1312

1413

15-
@override_settings(DATABASE_ROUTERS=[WeatherPinnedRouter()])
1614
class WeatherPinnedViewConnectionTest(TestCase):
1715
"""Weather views should only return weather_db when pinned."""
1816

@@ -29,17 +27,6 @@ def test_other_app_view_using_default_db(self):
2927
self.assertEqual(RelatedView.get_view_connection(using=DEFAULT_DB_ALIAS), connections["default"])
3028

3129

32-
class DefaultRouterViewConnectionTest(TestCase):
33-
"""All views should should use default alias by default."""
34-
35-
def test_weather_view_default(self):
36-
self.assertEqual(MonthlyObservation.get_view_connection(using=DEFAULT_DB_ALIAS), connections["default"])
37-
38-
def test_other_app_view_default(self):
39-
self.assertEqual(RelatedView.get_view_connection(using=DEFAULT_DB_ALIAS), connections["default"])
40-
41-
42-
@override_settings(DATABASE_ROUTERS=[WeatherPinnedRouter()])
4330
class WeatherPinnedRefreshViewTest(TestCase):
4431
"""View.refresh() should automatically select the appropriate database."""
4532

@@ -57,7 +44,6 @@ def test_refresh(self):
5744
self.assertEqual(MonthlyObservation.objects.count(), 1)
5845

5946

60-
@override_settings(DATABASE_ROUTERS=[WeatherPinnedRouter()])
6147
class WeatherPinnedMigrateTest(TestCase):
6248
"""Ensure views are only sync'd against the correct database on migrate."""
6349

@@ -86,7 +72,6 @@ def on_view_synced(sender, **kwargs):
8672
self.assertNotIn(RelatedView, synced_views)
8773

8874

89-
@override_settings(DATABASE_ROUTERS=[WeatherPinnedRouter()])
9075
class WeatherPinnedSyncPGViewsTest(TestCase):
9176
"""Ensure views are only sync'd against the correct database with sync_pgviews."""
9277

@@ -115,7 +100,6 @@ def on_view_synced(sender, **kwargs):
115100
self.assertNotIn(RelatedView, synced_views)
116101

117102

118-
@override_settings(DATABASE_ROUTERS=[WeatherPinnedRouter()])
119103
class WeatherPinnedRefreshPGViewsTest(TestCase):
120104
"""Ensure views are only refreshed on each database using refresh_pgviews"""
121105

tests/test_project/multidbtest/routers.py renamed to tests/test_project/routers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
class WeatherPinnedRouter:
1+
class DBRouter:
22
"""
33
A router to control all database operations on models and views in the
44
multidbtest application.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from django.db import connections, models
2+
from django.db.models import signals
3+
from django.dispatch import receiver
4+
5+
from django_pgviews import view
6+
7+
8+
class SchemaObservation(models.Model):
9+
date = models.DateField()
10+
temperature = models.IntegerField()
11+
12+
13+
VIEW_SQL = """
14+
WITH summary AS (
15+
SELECT
16+
date_trunc('month', date) AS date,
17+
count(*)
18+
FROM schemadbtest_schemaobservation
19+
GROUP BY 1
20+
ORDER BY date
21+
) SELECT
22+
ROW_NUMBER() OVER () AS id,
23+
date,
24+
count
25+
FROM summary;
26+
"""
27+
28+
29+
class SchemaMonthlyObservationView(view.View):
30+
sql = VIEW_SQL
31+
date = models.DateField()
32+
count = models.IntegerField()
33+
34+
35+
class SchemaMonthlyObservationMaterializedView(view.MaterializedView):
36+
sql = VIEW_SQL
37+
date = models.DateField()
38+
count = models.IntegerField()
39+
40+
class Meta:
41+
managed = False
42+
indexes = [models.Index(fields=["date"])]
43+
44+
45+
@receiver(signals.pre_migrate)
46+
def create_test_schema(sender, app_config, using, **kwargs):
47+
command = "CREATE SCHEMA IF NOT EXISTS {};".format("other")
48+
with connections[using].cursor() as cursor:
49+
cursor.execute(command)
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import datetime as dt
2+
from contextlib import closing
3+
4+
from django.core.management import call_command
5+
from django.db import DEFAULT_DB_ALIAS, connections
6+
from django.dispatch import receiver
7+
from django.test import TestCase
8+
9+
from django_pgviews.signals import view_synced
10+
11+
from ..viewtest.models import RelatedView
12+
from ..viewtest.tests import get_list_of_indexes
13+
from .models import SchemaMonthlyObservationMaterializedView, SchemaMonthlyObservationView, SchemaObservation
14+
15+
16+
class WeatherPinnedViewConnectionTest(TestCase):
17+
"""Weather views should only return schema_db when pinned."""
18+
19+
def test_schema_view_using_schema_db(self):
20+
self.assertEqual(SchemaMonthlyObservationView.get_view_connection(using="schema_db"), connections["schema_db"])
21+
22+
def test_schema_view_using_default_db(self):
23+
self.assertIsNone(SchemaMonthlyObservationView.get_view_connection(using=DEFAULT_DB_ALIAS))
24+
25+
def test_schema_materialized_view_using_schema_db(self):
26+
self.assertEqual(
27+
SchemaMonthlyObservationMaterializedView.get_view_connection(using="schema_db"), connections["schema_db"]
28+
)
29+
30+
def test_schema_materialized_view_using_default_db(self):
31+
self.assertIsNone(SchemaMonthlyObservationMaterializedView.get_view_connection(using=DEFAULT_DB_ALIAS))
32+
33+
def test_other_app_view_using_schema_db(self):
34+
self.assertIsNone(RelatedView.get_view_connection(using="schema_db"))
35+
36+
def test_other_app_view_using_default_db(self):
37+
self.assertEqual(RelatedView.get_view_connection(using=DEFAULT_DB_ALIAS), connections["default"])
38+
39+
40+
class SchemaTest(TestCase):
41+
"""View.refresh() should automatically select the appropriate schema."""
42+
43+
databases = {DEFAULT_DB_ALIAS, "schema_db"}
44+
45+
def test_schemas(self):
46+
with closing(connections["schema_db"].cursor()) as cur:
47+
cur.execute("""SELECT schemaname FROM pg_tables WHERE tablename LIKE 'schemadbtest_schemaobservation';""")
48+
49+
res = cur.fetchone()
50+
self.assertIsNotNone(res, "Can't find table schemadbtest_schemaobservation;")
51+
52+
(schemaname,) = res
53+
self.assertEqual(schemaname, "other")
54+
55+
cur.execute(
56+
"""SELECT schemaname FROM pg_views WHERE viewname LIKE 'schemadbtest_schemamonthlyobservationview';"""
57+
)
58+
59+
res = cur.fetchone()
60+
self.assertIsNotNone(res, "Can't find schemadbtest_schemamonthlyobservationview;")
61+
62+
(schemaname,) = res
63+
self.assertEqual(schemaname, "other")
64+
65+
cur.execute(
66+
"""SELECT schemaname FROM pg_matviews WHERE matviewname LIKE 'schemadbtest_schemamonthlyobservationmaterializedview';"""
67+
)
68+
69+
res = cur.fetchone()
70+
self.assertIsNotNone(res, "Can't find schemadbtest_schemamonthlyobservationmaterializedview.")
71+
72+
(schemaname,) = res
73+
self.assertEqual(schemaname, "other")
74+
75+
indexes = get_list_of_indexes(cur, SchemaMonthlyObservationMaterializedView)
76+
self.assertEqual(indexes, {"schemadbtes_date_9985f7_idx"})
77+
78+
def test_view(self):
79+
SchemaObservation.objects.create(date=dt.date(2022, 1, 1), temperature=10)
80+
SchemaObservation.objects.create(date=dt.date(2022, 1, 3), temperature=20)
81+
self.assertEqual(SchemaMonthlyObservationView.objects.count(), 1)
82+
83+
def test_mat_view_pre_refresh(self):
84+
SchemaObservation.objects.create(date=dt.date(2022, 1, 1), temperature=10)
85+
SchemaObservation.objects.create(date=dt.date(2022, 1, 3), temperature=20)
86+
self.assertEqual(SchemaMonthlyObservationMaterializedView.objects.count(), 0)
87+
88+
def test_mat_view_refresh(self):
89+
SchemaObservation.objects.create(date=dt.date(2022, 1, 1), temperature=10)
90+
SchemaObservation.objects.create(date=dt.date(2022, 1, 3), temperature=20)
91+
SchemaMonthlyObservationMaterializedView.refresh()
92+
self.assertEqual(SchemaMonthlyObservationMaterializedView.objects.count(), 1)
93+
94+
def test_view_exists_on_sync(self):
95+
synced = []
96+
97+
@receiver(view_synced)
98+
def on_view_synced(sender, **kwargs):
99+
synced.append(sender)
100+
if sender == SchemaMonthlyObservationView:
101+
self.assertEqual(
102+
dict(
103+
{"status": "EXISTS", "has_changed": False},
104+
update=False,
105+
force=False,
106+
signal=view_synced,
107+
using="schema_db",
108+
),
109+
kwargs,
110+
)
111+
if sender == SchemaMonthlyObservationMaterializedView:
112+
self.assertEqual(
113+
dict(
114+
{"status": "UPDATED", "has_changed": True},
115+
update=False,
116+
force=False,
117+
signal=view_synced,
118+
using="schema_db",
119+
),
120+
kwargs,
121+
)
122+
123+
call_command("sync_pgviews", database="schema_db", update=False)
124+
125+
self.assertIn(SchemaMonthlyObservationView, synced)
126+
self.assertIn(SchemaMonthlyObservationMaterializedView, synced)
127+
128+
def test_sync_pgviews_materialized_views_check_sql_changed(self):
129+
self.assertEqual(SchemaObservation.objects.count(), 0, "Test started with non-empty SchemaObservation")
130+
self.assertEqual(
131+
SchemaMonthlyObservationMaterializedView.objects.count(), 0, "Test started with non-empty mat view"
132+
)
133+
134+
SchemaObservation.objects.create(date=dt.date(2022, 1, 1), temperature=10)
135+
136+
# test regular behaviour, the mat view got recreated
137+
call_command("sync_pgviews", database="schema_db", update=False) # uses default django setting, False
138+
self.assertEqual(SchemaMonthlyObservationMaterializedView.objects.count(), 1)
139+
140+
# the mat view did not get recreated because the model hasn't changed
141+
SchemaObservation.objects.create(date=dt.date(2022, 2, 3), temperature=20)
142+
call_command("sync_pgviews", database="schema_db", update=False, materialized_views_check_sql_changed=True)
143+
self.assertEqual(SchemaMonthlyObservationMaterializedView.objects.count(), 1)
144+
145+
# the mat view got recreated because the mat view SQL has changed
146+
147+
# let's pretend the mat view in the DB is ordered by name, while the defined on models isn't
148+
with connections["schema_db"].cursor() as cursor:
149+
cursor.execute("DROP MATERIALIZED VIEW schemadbtest_schemamonthlyobservationmaterializedview CASCADE;")
150+
cursor.execute(
151+
"""
152+
CREATE MATERIALIZED VIEW schemadbtest_schemamonthlyobservationmaterializedview as
153+
WITH summary AS (
154+
SELECT
155+
date_trunc('day', date) AS date,
156+
count(*)
157+
FROM schemadbtest_schemaobservation
158+
GROUP BY 1
159+
ORDER BY date
160+
) SELECT
161+
ROW_NUMBER() OVER () AS id,
162+
date,
163+
count
164+
FROM summary;
165+
"""
166+
)
167+
168+
call_command("sync_pgviews", update=False, materialized_views_check_sql_changed=True)
169+
self.assertEqual(SchemaMonthlyObservationMaterializedView.objects.count(), 2)
170+
171+
def test_migrate_materialized_views_check_sql_changed_default(self):
172+
self.assertEqual(SchemaObservation.objects.count(), 0, "Test started with non-empty SchemaObservation")
173+
self.assertEqual(
174+
SchemaMonthlyObservationMaterializedView.objects.count(), 0, "Test started with non-empty mat view"
175+
)
176+
177+
SchemaObservation.objects.create(date=dt.date(2022, 1, 1), temperature=10)
178+
179+
call_command("migrate", database="schema_db")
180+
181+
self.assertEqual(SchemaMonthlyObservationMaterializedView.objects.count(), 1)

tests/test_project/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"PORT": "",
3939
},
4040
}
41+
DATABASE_ROUTERS = ["test_project.routers.DBRouter"]
4142

4243

4344
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

tests/test_project/viewtest/tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,9 @@ def on_all_views_synced(sender, **kwargs):
240240
call_command("sync_pgviews", update=False)
241241

242242
# All views went through syncing
243-
self.assertEqual(len(synced_views), 13)
244243
self.assertEqual(all_views_were_synced[0], True)
245244
self.assertFalse(expected)
245+
self.assertEqual(len(synced_views), 12)
246246

247247
def test_get_sql(self):
248248
User.objects.create(username="old", is_superuser=True, date_joined=timezone.now() - timedelta(days=10))

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ deps=
2222
dj42: https://github.com/django/django/archive/stable/4.2.x.tar.gz#egg=django
2323
dj50: https://github.com/django/django/archive/stable/5.0.x.tar.gz#egg=django
2424
commands=
25-
python manage.py test {posargs:test_project.viewtest test_project.multidbtest}
25+
python manage.py test {posargs:test_project.viewtest test_project.multidbtest test_project.schemadbtest} -v2
2626
passenv =
2727
DB_NAME
2828
DB_USER

0 commit comments

Comments
 (0)