summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Pope <nick.pope@flightdataservices.com>2018-09-12 01:23:35 +0100
committerTim Graham <timograham@gmail.com>2018-10-02 14:02:04 -0400
commitbf8b625a3bb6c2cb5f1be3713f3bafe2c1050366 (patch)
treebc5b93f5358016b15e7dd49ba2cb1f9002dea9d2
parent45ef3df7d07489ee0b76479cc799faa92e443a69 (diff)
Refs #29722 -- Added introspection of materialized views for PostgreSQL.
-rw-r--r--django/db/backends/postgresql/introspection.py31
-rw-r--r--docs/ref/django-admin.txt4
-rw-r--r--docs/releases/2.2.txt3
-rw-r--r--tests/inspectdb/tests.py24
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
@@ -311,6 +311,30 @@ class InspectDBTransactionalTests(TransactionTestCase):
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:
cursor.execute('CREATE EXTENSION IF NOT EXISTS file_fdw')