Refs #29722 -- Added introspection of partitions for PostgreSQL.
This commit is contained in:
parent
0607699902
commit
ebd270627c
|
@ -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.')
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue