Fixed #29004 -- Added inspectdb --include-views option.
This commit is contained in:
parent
92f48680db
commit
c2b969e124
1
AUTHORS
1
AUTHORS
|
@ -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>
|
||||
|
|
|
@ -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) + ',)'
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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``
|
||||
------------
|
||||
|
||||
|
|
|
@ -155,7 +155,8 @@ Internationalization
|
|||
Management Commands
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
* The new :option:`inspectdb --include-views` option allows creating models
|
||||
for database views.
|
||||
|
||||
Migrations
|
||||
~~~~~~~~~~
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)})
|
||||
|
||||
|
|
Loading…
Reference in New Issue