From ebd270627c3350101959fac59650259f2d33efcf Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Wed, 12 Sep 2018 21:53:24 +0100 Subject: [PATCH] Refs #29722 -- Added introspection of partitions for PostgreSQL. --- django/core/management/commands/inspectdb.py | 28 ++++++++++----- django/db/backends/postgresql/features.py | 1 + .../db/backends/postgresql/introspection.py | 15 ++++---- docs/ref/django-admin.txt | 13 ++++++- docs/releases/2.2.txt | 4 +++ tests/inspectdb/tests.py | 34 +++++++++++++++++++ 6 files changed, 77 insertions(+), 18 deletions(-) diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index cbebd6f60c..5cdd52fccf 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -22,6 +22,9 @@ class Command(BaseCommand): '--database', default=DEFAULT_DB_ALIAS, help='Nominates a database to introspect. Defaults to using the "default" database.', ) + parser.add_argument( + '--include-partitions', action='store_true', help='Also output models for partition tables.', + ) parser.add_argument( '--include-views', action='store_true', help='Also output models for database views.', ) @@ -55,12 +58,15 @@ class Command(BaseCommand): yield 'from %s import models' % self.db_module known_models = [] table_info = connection.introspection.get_table_list(cursor) - tables_to_introspect = ( - options['table'] or - sorted(info.name for info in table_info if options['include_views'] or info.type == 't') - ) - for table_name in tables_to_introspect: + # Determine types of tables and/or views to be introspected. + types = {'t'} + if options['include_partitions']: + types.add('p') + if options['include_views']: + types.add('v') + + for table_name in (options['table'] or sorted(info.name for info in table_info if info.type in types)): if table_name_filter is not None and callable(table_name_filter): if not table_name_filter(table_name): continue @@ -160,7 +166,8 @@ class Command(BaseCommand): field_desc += ' # ' + ' '.join(comment_notes) yield ' %s' % field_desc is_view = any(info.name == table_name and info.type == 'v' for info in table_info) - for meta_line in self.get_meta(table_name, constraints, column_to_field_name, is_view): + is_partition = any(info.name == table_name and info.type == 'p' for info in table_info) + for meta_line in self.get_meta(table_name, constraints, column_to_field_name, is_view, is_partition): yield meta_line def normalize_col_name(self, col_name, used_column_names, is_relation): @@ -257,7 +264,7 @@ class Command(BaseCommand): return field_type, field_params, field_notes - def get_meta(self, table_name, constraints, column_to_field_name, is_view): + def get_meta(self, table_name, constraints, column_to_field_name, is_view, is_partition): """ Return a sequence comprising the lines of code necessary to construct the inner Meta class for the model corresponding @@ -273,7 +280,12 @@ class Command(BaseCommand): columns = [x for x in columns if x is not None] if len(columns) > 1: unique_together.append(str(tuple(column_to_field_name[c] for c in columns))) - managed_comment = " # Created from a view. Don't remove." if is_view else "" + if is_view: + managed_comment = " # Created from a view. Don't remove." + elif is_partition: + managed_comment = " # Created from a partition. Don't remove." + else: + managed_comment = '' meta = [''] if has_unsupported_constraint: meta.append(' # A unique constraint could not be introspected.') diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index 3f43abf9d5..89ff06fdf5 100644 --- a/django/db/backends/postgresql/features.py +++ b/django/db/backends/postgresql/features.py @@ -73,3 +73,4 @@ class DatabaseFeatures(BaseDatabaseFeatures): has_gin_pending_list_limit = property(operator.attrgetter('is_postgresql_9_5')) supports_ignore_conflicts = property(operator.attrgetter('is_postgresql_9_5')) has_phraseto_tsquery = property(operator.attrgetter('is_postgresql_9_6')) + supports_table_partitions = property(operator.attrgetter('is_postgresql_10')) diff --git a/django/db/backends/postgresql/introspection.py b/django/db/backends/postgresql/introspection.py index 85538262cb..3ce88ccfbf 100644 --- a/django/db/backends/postgresql/introspection.py +++ b/django/db/backends/postgresql/introspection.py @@ -42,18 +42,15 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_table_list(self, cursor): """Return a list of table and view names in the current database.""" cursor.execute(""" - SELECT c.relname, c.relkind + SELECT c.relname, + CASE WHEN {} THEN 'p' WHEN c.relkind IN ('m', 'v') THEN 'v' ELSE 't' END FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('f', 'm', 'r', 'v') + WHERE c.relkind IN ('f', 'm', 'p', 'r', 'v') AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid) - """) - 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 - ] + """.format('c.relispartition' if self.connection.features.supports_table_partitions else 'FALSE')) + return [TableInfo(*row) for row in cursor.fetchall() if row[0] not in self.ignored_tables] def get_table_description(self, cursor, table_name): """ @@ -73,7 +70,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): 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') + WHERE c.relkind IN ('f', 'm', 'p', 'r', 'v') AND c.relname = %s AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid) diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index c1844b8e64..6a3294788a 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -352,7 +352,8 @@ file) to standard output. You may choose what tables or views to inspect by passing their names as arguments. If no arguments are provided, models are created for views only if -the :option:`--include-views` option is used. +the :option:`--include-views` option is used. Models for partition tables are +created on PostgreSQL if the :option:`--include-partitions` option is used. Use this if you have a legacy database with which you'd like to use Django. The script will inspect the database and create a model for each table within @@ -404,6 +405,8 @@ PostgreSQL * Models are created for foreign tables. * Models are created for materialized views if :option:`--include-views` is used. +* Models are created for partition tables if + :option:`--include-partitions` is used. .. versionchanged:: 2.2 @@ -413,6 +416,14 @@ PostgreSQL Specifies the database to introspect. Defaults to ``default``. +.. django-admin-option:: --include-partitions + +.. versionadded:: 2.2 + +If this option is provided, models are also created for partitions. + +Only support for PostgreSQL is implemented. + .. django-admin-option:: --include-views .. versionadded:: 2.1 diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index 02d3fd8682..01eb2b7d47 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -183,6 +183,10 @@ Management Commands * :option:`inspectdb --include-views` now creates models for materialized views on PostgreSQL. +* The new :option:`inspectdb --include-partitions` option allows creating + models for partition tables on PostgreSQL. In older versions, models are + created child tables instead the parent. + * :djadmin:`inspectdb` now introspects :class:`~django.db.models.DurationField` for Oracle and PostgreSQL. diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index 90a8c5f5e2..b578dbf3df 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -334,6 +334,40 @@ class InspectDBTransactionalTests(TransactionTestCase): with connection.cursor() as cursor: cursor.execute('DROP MATERIALIZED VIEW IF EXISTS inspectdb_people_materialized_view') + @skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific SQL') + @skipUnlessDBFeature('supports_table_partitions') + def test_include_partitions(self): + """inspectdb --include-partitions creates models for partitions.""" + with connection.cursor() as cursor: + cursor.execute('''\ + CREATE TABLE inspectdb_partition_parent (name text not null) + PARTITION BY LIST (left(upper(name), 1)) + ''') + cursor.execute('''\ + CREATE TABLE inspectdb_partition_child + PARTITION OF inspectdb_partition_parent + FOR VALUES IN ('A', 'B', 'C') + ''') + out = StringIO() + partition_model_parent = 'class InspectdbPartitionParent(models.Model):' + partition_model_child = 'class InspectdbPartitionChild(models.Model):' + partition_managed = 'managed = False # Created from a partition.' + try: + call_command('inspectdb', table_name_filter=inspectdb_tables_only, stdout=out) + no_partitions_output = out.getvalue() + self.assertIn(partition_model_parent, no_partitions_output) + self.assertNotIn(partition_model_child, no_partitions_output) + self.assertNotIn(partition_managed, no_partitions_output) + call_command('inspectdb', table_name_filter=inspectdb_tables_only, include_partitions=True, stdout=out) + with_partitions_output = out.getvalue() + self.assertIn(partition_model_parent, with_partitions_output) + self.assertIn(partition_model_child, with_partitions_output) + self.assertIn(partition_managed, with_partitions_output) + finally: + with connection.cursor() as cursor: + cursor.execute('DROP TABLE IF EXISTS inspectdb_partition_child') + cursor.execute('DROP TABLE IF EXISTS inspectdb_partition_parent') + @skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific SQL') def test_foreign_data_wrapper(self): with connection.cursor() as cursor: