diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index e07adb970d..56f7d4fb6a 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -151,6 +151,9 @@ class BaseDatabaseFeatures: # Can the backend introspect the column order (ASC/DESC) for indexes? supports_index_column_ordering = True + # Does the backend support introspection of materialized views? + can_introspect_materialized_views = False + # Support for the DISTINCT ON clause can_distinct_on_fields = False diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index 98ea186dcb..741a03ae0e 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -241,11 +241,14 @@ class DatabaseCreation(BaseDatabaseCreation): if not success and self._test_settings_get('PASSWORD') is None: set_password = 'ALTER USER %(user)s IDENTIFIED BY "%(password)s"' self._execute_statements(cursor, [set_password], parameters, verbosity) - # Most test-suites can be run without the create-view privilege. But some need it. - extra = "GRANT CREATE VIEW TO %(user)s" - success = self._execute_allow_fail_statements(cursor, [extra], parameters, verbosity, 'ORA-01031') - if not success and verbosity >= 2: - self.log('Failed to grant CREATE VIEW permission to test user. This may be ok.') + # Most test suites can be run without "create view" and + # "create materialized view" privileges. But some need it. + for object_type in ('VIEW', 'MATERIALIZED VIEW'): + extra = 'GRANT CREATE %(object_type)s TO %(user)s' + parameters['object_type'] = object_type + success = self._execute_allow_fail_statements(cursor, [extra], parameters, verbosity, 'ORA-01031') + if not success and verbosity >= 2: + self.log('Failed to grant CREATE %s permission to test user. This may be ok.' % object_type) def _execute_test_db_destruction(self, cursor, parameters, verbosity): if verbosity >= 2: diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index 7e288e9840..7c72831c25 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -21,6 +21,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): truncates_names = True supports_tablespaces = True supports_sequence_reset = False + can_introspect_materialized_views = True can_introspect_time_field = False atomic_transactions = False supports_combined_alters = False diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py index 51566365bc..29193f0507 100644 --- a/django/db/backends/oracle/introspection.py +++ b/django/db/backends/oracle/introspection.py @@ -48,8 +48,20 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_table_list(self, cursor): """Return a list of table and view names in the current database.""" - cursor.execute("SELECT TABLE_NAME, 't' FROM USER_TABLES UNION ALL " - "SELECT VIEW_NAME, 'v' FROM USER_VIEWS") + cursor.execute(""" + SELECT table_name, 't' + FROM user_tables + WHERE + NOT EXISTS ( + SELECT 1 + FROM user_mviews + WHERE user_mviews.mview_name = user_tables.table_name + ) + UNION ALL + SELECT view_name, 'v' FROM user_views + UNION ALL + SELECT mview_name, 'v' FROM user_mviews + """) return [TableInfo(self.identifier_converter(row[0]), row[1]) for row in cursor.fetchall()] def get_table_description(self, cursor, table_name): diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index 89ff06fdf5..5c8701c396 100644 --- a/django/db/backends/postgresql/features.py +++ b/django/db/backends/postgresql/features.py @@ -21,6 +21,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_transactions = True can_introspect_autofield = True can_introspect_ip_address_field = True + can_introspect_materialized_views = True can_introspect_small_integer_field = True can_distinct_on_fields = True can_rollback_ddl = True diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 39b3fb9898..98f14629bb 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -764,9 +764,10 @@ and a user granted ``RESOURCE WITH ADMIN OPTION`` can grant ``RESOURCE``, such a user cannot grant the individual privileges (e.g. ``CREATE TABLE``), and thus ``RESOURCE WITH ADMIN OPTION`` is not usually sufficient for running tests. -Some test suites also create views; to run these, the user also needs -the ``CREATE VIEW WITH ADMIN OPTION`` privilege. In particular, this is needed -for Django's own test suite. +Some test suites also create views or materialized views; to run these, the +user also needs ``CREATE VIEW WITH ADMIN OPTION`` and +``CREATE MATERIALIZED VIEW WITH ADMIN OPTION`` privileges. In particular, this +is needed for Django's own test suite. All of these privileges are included in the DBA role, which is appropriate for use on a private developer's database. diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 6a3294788a..19f1d046fe 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -399,6 +399,12 @@ it because ``True`` is its default value). Database-specific notes ~~~~~~~~~~~~~~~~~~~~~~~ +Oracle +^^^^^^ + +* Models are created for materialized views if :option:`--include-views` is + used. + PostgreSQL ^^^^^^^^^^ diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index 8a15262ff9..7cabba3ad6 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -182,7 +182,7 @@ Management Commands * :djadmin:`inspectdb` now creates models for foreign tables on PostgreSQL. * :option:`inspectdb --include-views` now creates models for materialized views - on PostgreSQL. + on Oracle and PostgreSQL. * The new :option:`inspectdb --include-partitions` option allows creating models for partition tables on PostgreSQL. In older versions, models are diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index 6452002c3e..c69eb8e49b 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -310,16 +310,16 @@ class InspectDBTransactionalTests(TransactionTestCase): with connection.cursor() as cursor: cursor.execute('DROP VIEW inspectdb_people_view') - @skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific SQL') + @skipUnlessDBFeature('can_introspect_materialized_views') def test_include_materialized_views(self): - """inspectdb --include-views creates models for database materialized views.""" + """inspectdb --include-views creates models for materialized views.""" with connection.cursor() as cursor: cursor.execute( - 'CREATE MATERIALIZED VIEW inspectdb_people_materialized_view AS ' + 'CREATE MATERIALIZED VIEW inspectdb_people_materialized AS ' 'SELECT id, name FROM inspectdb_people' ) out = StringIO() - view_model = 'class InspectdbPeopleMaterializedView(models.Model):' + view_model = 'class InspectdbPeopleMaterialized(models.Model):' view_managed = 'managed = False # Created from a view.' try: call_command('inspectdb', table_name_filter=inspectdb_tables_only, stdout=out) @@ -332,7 +332,7 @@ class InspectDBTransactionalTests(TransactionTestCase): self.assertIn(view_managed, with_views_output) finally: with connection.cursor() as cursor: - cursor.execute('DROP MATERIALIZED VIEW IF EXISTS inspectdb_people_materialized_view') + cursor.execute('DROP MATERIALIZED VIEW inspectdb_people_materialized') @skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific SQL') @skipUnlessDBFeature('supports_table_partitions')