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:
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>
|
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>
|
||||||
|
@ -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) + ',)'
|
||||||
|
@ -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:
|
||||||
|
@ -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``
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -155,7 +155,8 @@ Internationalization
|
|||||||
Management Commands
|
Management Commands
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* The new :option:`inspectdb --include-views` option allows creating models
|
||||||
|
for database views.
|
||||||
|
|
||||||
Migrations
|
Migrations
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
@ -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')
|
||||||
|
@ -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)})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user