1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00

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> Brandon Chinn <http://brandonchinn178.github.io>
Brant Harris Brant Harris
Brendan Hayward <brendanhayward85@gmail.com> Brendan Hayward <brendanhayward85@gmail.com>
Brendan Quinn <brendan@cluefulmedia.com>
Brenton Simpson <http://theillustratedlife.com> Brenton Simpson <http://theillustratedlife.com>
Brett Cannon <brett@python.org> Brett Cannon <brett@python.org>
Brett Hoerner <bretthoerner@bretthoerner.com> Brett Hoerner <bretthoerner@bretthoerner.com>

View File

@ -22,6 +22,9 @@ class Command(BaseCommand):
'--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, '--database', action='store', dest='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-views', action='store_true', help='Also output models for database views.',
)
def handle(self, **options): def handle(self, **options):
try: 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 "# Feel free to rename the models, but don't rename db_table values or field names."
yield 'from %s import models' % self.db_module yield 'from %s import models' % self.db_module
known_models = [] 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: for table_name in tables_to_introspect:
if table_name_filter is not None and callable(table_name_filter): if table_name_filter is not None and callable(table_name_filter):
@ -160,7 +167,8 @@ class Command(BaseCommand):
if comment_notes: if comment_notes:
field_desc += ' # ' + ' '.join(comment_notes) field_desc += ' # ' + ' '.join(comment_notes)
yield ' %s' % field_desc 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 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 +265,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): def get_meta(self, table_name, constraints, column_to_field_name, is_view):
""" """
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
@ -272,9 +280,10 @@ class Command(BaseCommand):
# so we build the string rather than interpolate the tuple # so we build the string rather than interpolate the tuple
tup = '(' + ', '.join("'%s'" % column_to_field_name[c] for c in columns) + ')' tup = '(' + ', '.join("'%s'" % column_to_field_name[c] for c in columns) + ')'
unique_together.append(tup) unique_together.append(tup)
managed_comment = " # Created from a view. Don't remove." if is_view else ""
meta = ["", meta = ["",
" class Meta:", " class Meta:",
" managed = False", " managed = False%s" % managed_comment,
" db_table = '%s'" % table_name] " db_table = '%s'" % table_name]
if unique_together: if unique_together:
tup = '(' + ', '.join(unique_together) + ',)' tup = '(' + ', '.join(unique_together) + ',)'

View File

@ -96,13 +96,16 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
relations = {} relations = {}
# Schema for this table # Schema for this table
cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"]) cursor.execute(
try: "SELECT sql, type FROM sqlite_master "
results = cursor.fetchone()[0].strip() "WHERE tbl_name = %s AND type IN ('table', 'view')",
except TypeError: [table_name]
)
create_sql, table_type = cursor.fetchone()
if table_type == 'view':
# It might be a view, then no results will be returned # It might be a view, then no results will be returned
return relations 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 # Walk through and look for references to other tables. SQLite doesn't
# really have enforced references, but since it echoes out the SQL used # 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): def get_primary_key_column(self, cursor, table_name):
"""Return the column name of the primary key for the given table.""" """Return the column name of the primary key for the given table."""
# Don't use PRAGMA because that causes issues with some transactions # 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() row = cursor.fetchone()
if row is None: if row is None:
raise ValueError("Table %s does not exist" % table_name) raise ValueError("Table %s does not exist" % table_name)
results = row[0].strip() create_sql, table_type = row
results = results[results.index('(') + 1:results.rindex(')')] if table_type == 'view':
for field_desc in results.split(','): # 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() field_desc = field_desc.strip()
m = re.search('"(.*)".*PRIMARY KEY( AUTOINCREMENT)?', field_desc) m = re.search('"(.*)".*PRIMARY KEY( AUTOINCREMENT)?', field_desc)
if m: 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 Introspects the database tables in the database pointed-to by the
:setting:`NAME` setting and outputs a Django model module (a ``models.py`` :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 file) to standard output.
their names as arguments.
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. 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
@ -405,6 +408,12 @@ it because ``True`` is its default value).
Specifies the database to introspect. Defaults to ``default``. 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`` ``loaddata``
------------ ------------

View File

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

View File

@ -4,7 +4,8 @@ from unittest import mock, skipUnless
from django.core.management import call_command from django.core.management import call_command
from django.db import connection 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 from .models import ColumnTypes
@ -260,10 +261,37 @@ class InspectDBTestCase(TestCase):
be visible in the output. be visible in the output.
""" """
out = StringIO() out = StringIO()
with mock.patch('django.db.backends.base.introspection.BaseDatabaseIntrospection.table_names', with mock.patch('django.db.connection.introspection.get_table_list',
return_value=['nonexistent']): return_value=[TableInfo(name='nonexistent', type='t')]):
call_command('inspectdb', stdout=out) call_command('inspectdb', stdout=out)
output = out.getvalue() output = out.getvalue()
self.assertIn("# Unable to inspect table 'nonexistent'", output) self.assertIn("# Unable to inspect table 'nonexistent'", output)
# The error message depends on the backend # The error message depends on the backend
self.assertIn("# The error was:", output) 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: for statement in create_table_statements:
with connection.cursor() as cursor: 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') relations = connection.introspection.get_relations(cursor, 'mocked_table')
self.assertEqual(relations, {'art_id': ('id', Article._meta.db_table)}) self.assertEqual(relations, {'art_id': ('id', Article._meta.db_table)})