Refs #29722 -- Added introspection of partitions for PostgreSQL.

This commit is contained in:
Nick Pope 2018-09-12 21:53:24 +01:00 committed by Tim Graham
parent 0607699902
commit ebd270627c
6 changed files with 77 additions and 18 deletions

View File

@ -22,6 +22,9 @@ class Command(BaseCommand):
'--database', default=DEFAULT_DB_ALIAS, '--database', default=DEFAULT_DB_ALIAS,
help='Nominates a database to introspect. Defaults to using the "default" database.', 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( parser.add_argument(
'--include-views', action='store_true', help='Also output models for database views.', '--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 yield 'from %s import models' % self.db_module
known_models = [] known_models = []
table_info = connection.introspection.get_table_list(cursor) 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 table_name_filter is not None and callable(table_name_filter):
if not table_name_filter(table_name): if not table_name_filter(table_name):
continue continue
@ -160,7 +166,8 @@ class Command(BaseCommand):
field_desc += ' # ' + ' '.join(comment_notes) field_desc += ' # ' + ' '.join(comment_notes)
yield ' %s' % field_desc yield ' %s' % field_desc
is_view = any(info.name == table_name and info.type == 'v' for info in table_info) 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 yield meta_line
def normalize_col_name(self, col_name, used_column_names, is_relation): 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 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 Return a sequence comprising the lines of code necessary
to construct the inner Meta class for the model corresponding 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] columns = [x for x in columns if x is not None]
if len(columns) > 1: if len(columns) > 1:
unique_together.append(str(tuple(column_to_field_name[c] for c in columns))) 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 = [''] meta = ['']
if has_unsupported_constraint: if has_unsupported_constraint:
meta.append(' # A unique constraint could not be introspected.') meta.append(' # A unique constraint could not be introspected.')

View File

@ -73,3 +73,4 @@ class DatabaseFeatures(BaseDatabaseFeatures):
has_gin_pending_list_limit = property(operator.attrgetter('is_postgresql_9_5')) has_gin_pending_list_limit = property(operator.attrgetter('is_postgresql_9_5'))
supports_ignore_conflicts = 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')) has_phraseto_tsquery = property(operator.attrgetter('is_postgresql_9_6'))
supports_table_partitions = property(operator.attrgetter('is_postgresql_10'))

View File

@ -42,18 +42,15 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
def get_table_list(self, cursor): def get_table_list(self, cursor):
"""Return a list of table and view names in the current database.""" """Return a list of table and view names in the current database."""
cursor.execute(""" 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 FROM pg_catalog.pg_class c
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace 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 n.nspname NOT IN ('pg_catalog', 'pg_toast')
AND pg_catalog.pg_table_is_visible(c.oid) AND pg_catalog.pg_table_is_visible(c.oid)
""") """.format('c.relispartition' if self.connection.features.supports_table_partitions else 'FALSE'))
mapping = {'f': 't', 'm': 'v', 'r': 't', 'v': 'v'} return [TableInfo(*row) for row in cursor.fetchall() if row[0] not in self.ignored_tables]
return [
TableInfo(row[0], mapping[row[1]])
for row in cursor.fetchall() if row[0] not in self.ignored_tables
]
def get_table_description(self, cursor, table_name): 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_type t ON a.atttypid = t.oid
JOIN pg_class c ON a.attrelid = c.oid JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.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 c.relname = %s
AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
AND pg_catalog.pg_table_is_visible(c.oid) AND pg_catalog.pg_table_is_visible(c.oid)

View File

@ -352,7 +352,8 @@ file) to standard output.
You may choose what tables or views to inspect by passing their names as 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 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. 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 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 foreign tables.
* Models are created for materialized views if * Models are created for materialized views if
:option:`--include-views` is used. :option:`--include-views` is used.
* Models are created for partition tables if
:option:`--include-partitions` is used.
.. versionchanged:: 2.2 .. versionchanged:: 2.2
@ -413,6 +416,14 @@ PostgreSQL
Specifies the database to introspect. Defaults to ``default``. 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 .. django-admin-option:: --include-views
.. versionadded:: 2.1 .. versionadded:: 2.1

View File

@ -183,6 +183,10 @@ Management Commands
* :option:`inspectdb --include-views` now creates models for materialized views * :option:`inspectdb --include-views` now creates models for materialized views
on PostgreSQL. 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` * :djadmin:`inspectdb` now introspects :class:`~django.db.models.DurationField`
for Oracle and PostgreSQL. for Oracle and PostgreSQL.

View File

@ -334,6 +334,40 @@ class InspectDBTransactionalTests(TransactionTestCase):
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute('DROP MATERIALIZED VIEW IF EXISTS inspectdb_people_materialized_view') 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') @skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific SQL')
def test_foreign_data_wrapper(self): def test_foreign_data_wrapper(self):
with connection.cursor() as cursor: with connection.cursor() as cursor: