From bf8b625a3bb6c2cb5f1be3713f3bafe2c1050366 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Wed, 12 Sep 2018 01:23:35 +0100 Subject: [PATCH] Refs #29722 -- Added introspection of materialized views for PostgreSQL. --- .../db/backends/postgresql/introspection.py | 31 ++++++++++++------- docs/ref/django-admin.txt | 4 ++- docs/releases/2.2.txt | 3 ++ tests/inspectdb/tests.py | 24 ++++++++++++++ 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/django/db/backends/postgresql/introspection.py b/django/db/backends/postgresql/introspection.py index c20d7b659e..77db6d5cbf 100644 --- a/django/db/backends/postgresql/introspection.py +++ b/django/db/backends/postgresql/introspection.py @@ -44,11 +44,11 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): SELECT c.relname, c.relkind FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('f', 'r', 'v') + WHERE c.relkind IN ('f', 'm', 'r', 'v') AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid) """) - mapping = {'f': 't', 'r': 't', 'v': 'v'} + mapping = {'f': 't', 'm': 'v', 'r': 't', 'v': 'v'} return [ TableInfo(row[0], mapping[row[1]]) for row in cursor.fetchall() if row[0] not in self.ignored_tables @@ -59,18 +59,27 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): Return a description of the table with the DB-API cursor.description interface. """ - # As cursor.description does not return reliably the nullable property, - # we have to query the information_schema (#7783) + # Query the pg_catalog tables as cursor.description does not reliably + # return the nullable property and information_schema.columns does not + # contain details of materialized views. cursor.execute(""" - SELECT column_name, is_nullable, column_default - FROM information_schema.columns - WHERE table_name = %s""", [table_name]) + SELECT + a.attname AS column_name, + NOT (a.attnotnull OR (t.typtype = 'd' AND t.typnotnull)) AS is_nullable, + pg_get_expr(ad.adbin, ad.adrelid) AS column_default + FROM pg_attribute a + LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum + JOIN pg_type t ON a.atttypid = t.oid + JOIN pg_class c ON a.attrelid = c.oid + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE c.relkind IN ('f', 'm', 'r', 'v') + AND c.relname = %s + AND n.nspname NOT IN ('pg_catalog', 'pg_toast') + AND pg_catalog.pg_table_is_visible(c.oid) + """, [table_name]) field_map = {line[0]: line[1:] for line in cursor.fetchall()} cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name)) - return [ - FieldInfo(*line[0:6], field_map[line.name][0] == 'YES', field_map[line.name][1]) - for line in cursor.description - ] + return [FieldInfo(*line[0:6], *field_map[line.name]) for line in cursor.description] def get_sequences(self, cursor, table_name, table_fields=()): cursor.execute(""" diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index b6a2319341..1a3baecfda 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -402,10 +402,12 @@ PostgreSQL ^^^^^^^^^^ * Models are created for foreign tables. +* Models are created for materialized views if + :option:`--include-views` is used. .. versionchanged:: 2.2 - Support for foreign tables was added. + Support for foreign tables and materialized views was added. .. django-admin-option:: --database DATABASE diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index 95aded1920..901868cebf 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -179,6 +179,9 @@ Management Commands * :djadmin:`inspectdb` now creates models for foreign tables on PostgreSQL. +* :option:`inspectdb --include-views` now creates models for materialized views + on PostgreSQL. + Migrations ~~~~~~~~~~ diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index 83c49eb7e3..849773ef9e 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -310,6 +310,30 @@ class InspectDBTransactionalTests(TransactionTestCase): with connection.cursor() as cursor: cursor.execute('DROP VIEW inspectdb_people_view') + @skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific SQL') + def test_include_materialized_views(self): + """inspectdb --include-views creates models for database materialized views.""" + with connection.cursor() as cursor: + cursor.execute( + 'CREATE MATERIALIZED VIEW inspectdb_people_materialized_view AS ' + 'SELECT id, name FROM inspectdb_people' + ) + out = StringIO() + view_model = 'class InspectdbPeopleMaterializedView(models.Model):' + view_managed = 'managed = False # Created from a view.' + try: + call_command('inspectdb', table_name_filter=inspectdb_tables_only, stdout=out) + no_views_output = out.getvalue() + self.assertNotIn(view_model, no_views_output) + self.assertNotIn(view_managed, no_views_output) + call_command('inspectdb', table_name_filter=inspectdb_tables_only, include_views=True, stdout=out) + with_views_output = out.getvalue() + self.assertIn(view_model, with_views_output) + self.assertIn(view_managed, with_views_output) + finally: + with connection.cursor() as cursor: + cursor.execute('DROP MATERIALIZED VIEW IF EXISTS inspectdb_people_materialized_view') + @skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific SQL') def test_foreign_data_wrapper(self): with connection.cursor() as cursor: