Browse Source

Refs #29722 -- Added introspection of materialized views for PostgreSQL.

Nick Pope 6 years ago
parent
commit
bf8b625a3b

+ 20 - 11
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("""

+ 3 - 1
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
 

+ 3 - 0
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
 ~~~~~~~~~~
 

+ 24 - 0
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: