Fixed #29004 -- Added inspectdb --include-views option.

This commit is contained in:
bquinn 2018-01-09 16:04:56 +00:00 committed by Tim Graham
parent 92f48680db
commit c2b969e124
7 changed files with 78 additions and 20 deletions

View File

@ -120,6 +120,7 @@ answer newbie questions, and generally made Django that much better:
Brandon Chinn <http://brandonchinn178.github.io>
Brant Harris
Brendan Hayward <brendanhayward85@gmail.com>
Brendan Quinn <brendan@cluefulmedia.com>
Brenton Simpson <http://theillustratedlife.com>
Brett Cannon <brett@python.org>
Brett Hoerner <bretthoerner@bretthoerner.com>

View File

@ -22,6 +22,9 @@ class Command(BaseCommand):
'--database', action='store', dest='database', default=DEFAULT_DB_ALIAS,
help='Nominates a database to introspect. Defaults to using the "default" database.',
)
parser.add_argument(
'--include-views', action='store_true', help='Also output models for database views.',
)
def handle(self, **options):
try:
@ -54,7 +57,11 @@ class Command(BaseCommand):
yield "# Feel free to rename the models, but don't rename db_table values or field names."
yield 'from %s import models' % self.db_module
known_models = []
tables_to_introspect = options['table'] or connection.introspection.table_names(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:
if table_name_filter is not None and callable(table_name_filter):
@ -160,7 +167,8 @@ class Command(BaseCommand):
if comment_notes:
field_desc += ' # ' + ' '.join(comment_notes)
yield ' %s' % field_desc
for meta_line in self.get_meta(table_name, constraints, column_to_field_name):
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):
yield meta_line
def normalize_col_name(self, col_name, used_column_names, is_relation):
@ -257,7 +265,7 @@ class Command(BaseCommand):
return field_type, field_params, field_notes
def get_meta(self, table_name, constraints, column_to_field_name):
def get_meta(self, table_name, constraints, column_to_field_name, is_view):
"""
Return a sequence comprising the lines of code necessary
to construct the inner Meta class for the model corresponding
@ -272,9 +280,10 @@ class Command(BaseCommand):
# so we build the string rather than interpolate the tuple
tup = '(' + ', '.join("'%s'" % column_to_field_name[c] for c in columns) + ')'
unique_together.append(tup)
managed_comment = " # Created from a view. Don't remove." if is_view else ""
meta = ["",
" class Meta:",
" managed = False",
" managed = False%s" % managed_comment,
" db_table = '%s'" % table_name]
if unique_together:
tup = '(' + ', '.join(unique_together) + ',)'

View File

@ -96,13 +96,16 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
relations = {}
# Schema for this table
cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"])
try:
results = cursor.fetchone()[0].strip()
except TypeError:
cursor.execute(
"SELECT sql, type FROM sqlite_master "
"WHERE tbl_name = %s AND type IN ('table', 'view')",
[table_name]
)
create_sql, table_type = cursor.fetchone()
if table_type == 'view':
# It might be a view, then no results will be returned
return relations
results = results[results.index('(') + 1:results.rindex(')')]
results = create_sql[create_sql.index('(') + 1:create_sql.rindex(')')]
# Walk through and look for references to other tables. SQLite doesn't
# really have enforced references, but since it echoes out the SQL used
@ -174,13 +177,20 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
def get_primary_key_column(self, cursor, table_name):
"""Return the column name of the primary key for the given table."""
# Don't use PRAGMA because that causes issues with some transactions
cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"])
cursor.execute(
"SELECT sql, type FROM sqlite_master "
"WHERE tbl_name = %s AND type IN ('table', 'view')",
[table_name]
)
row = cursor.fetchone()
if row is None:
raise ValueError("Table %s does not exist" % table_name)
results = row[0].strip()
results = results[results.index('(') + 1:results.rindex(')')]
for field_desc in results.split(','):
create_sql, table_type = row
if table_type == 'view':
# Views don't have a primary key.
return None
fields_sql = create_sql[create_sql.index('(') + 1:create_sql.rindex(')')]
for field_desc in fields_sql.split(','):
field_desc = field_desc.strip()
m = re.search('"(.*)".*PRIMARY KEY( AUTOINCREMENT)?', field_desc)
if m:

View File

@ -350,8 +350,11 @@ Specifies the database to flush. Defaults to ``default``.
Introspects the database tables in the database pointed-to by the
:setting:`NAME` setting and outputs a Django model module (a ``models.py``
file) to standard output. You may choose what tables to inspect by passing
their names as arguments.
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.
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
@ -405,6 +408,12 @@ it because ``True`` is its default value).
Specifies the database to introspect. Defaults to ``default``.
.. django-admin-option:: --include-views
.. versionadded:: 2.1
If this option is provided, models are also created for database views.
``loaddata``
------------

View File

@ -155,7 +155,8 @@ Internationalization
Management Commands
~~~~~~~~~~~~~~~~~~~
* ...
* The new :option:`inspectdb --include-views` option allows creating models
for database views.
Migrations
~~~~~~~~~~

View File

@ -4,7 +4,8 @@ from unittest import mock, skipUnless
from django.core.management import call_command
from django.db import connection
from django.test import TestCase, skipUnlessDBFeature
from django.db.backends.base.introspection import TableInfo
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
from .models import ColumnTypes
@ -260,10 +261,37 @@ class InspectDBTestCase(TestCase):
be visible in the output.
"""
out = StringIO()
with mock.patch('django.db.backends.base.introspection.BaseDatabaseIntrospection.table_names',
return_value=['nonexistent']):
with mock.patch('django.db.connection.introspection.get_table_list',
return_value=[TableInfo(name='nonexistent', type='t')]):
call_command('inspectdb', stdout=out)
output = out.getvalue()
self.assertIn("# Unable to inspect table 'nonexistent'", output)
# The error message depends on the backend
self.assertIn("# The error was:", output)
class InspectDBTransactionalTests(TransactionTestCase):
available_apps = None
def test_include_views(self):
"""inspectdb --include-views creates models for database views."""
with connection.cursor() as cursor:
cursor.execute(
'CREATE VIEW inspectdb_people_view AS '
'SELECT id, name FROM inspectdb_people'
)
out = StringIO()
view_model = 'class InspectdbPeopleView(models.Model):'
view_managed = 'managed = False # Created from a view.'
try:
call_command('inspectdb', stdout=out)
no_views_output = out.getvalue()
self.assertNotIn(view_model, no_views_output)
self.assertNotIn(view_managed, no_views_output)
call_command('inspectdb', 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 VIEW inspectdb_people_view')

View File

@ -151,7 +151,7 @@ class IntrospectionTests(TransactionTestCase):
]
for statement in create_table_statements:
with connection.cursor() as cursor:
cursor.fetchone = mock.Mock(return_value=[statement.format(Article._meta.db_table)])
cursor.fetchone = mock.Mock(return_value=[statement.format(Article._meta.db_table), 'table'])
relations = connection.introspection.get_relations(cursor, 'mocked_table')
self.assertEqual(relations, {'art_id': ('id', Article._meta.db_table)})