summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbquinn <brendan@cluefulmedia.com>2018-01-09 16:04:56 +0000
committerTim Graham <timograham@gmail.com>2018-01-27 18:51:40 -0500
commitc2b969e124f65d375aac969f253279085b6f7078 (patch)
tree9e484478a50e0815ed8cadee122dd5d0c6914856
parent92f48680dbd2e02f2b33f6ad0e35b7d337889fb2 (diff)
Fixed #29004 -- Added inspectdb --include-views option.
-rw-r--r--AUTHORS1
-rw-r--r--django/core/management/commands/inspectdb.py17
-rw-r--r--django/db/backends/sqlite3/introspection.py28
-rw-r--r--docs/ref/django-admin.txt13
-rw-r--r--docs/releases/2.1.txt3
-rw-r--r--tests/inspectdb/tests.py34
-rw-r--r--tests/introspection/tests.py2
7 files changed, 78 insertions, 20 deletions
diff --git a/AUTHORS b/AUTHORS
index 2034ee4fcb..ef00671540 100644
--- a/AUTHORS
+++ b/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>
diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py
index c502b34b51..0834a17c19 100644
--- a/django/core/management/commands/inspectdb.py
+++ b/django/core/management/commands/inspectdb.py
@@ -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) + ',)'
diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py
index f3a33a97f2..bb78e2533e 100644
--- a/django/db/backends/sqlite3/introspection.py
+++ b/django/db/backends/sqlite3/introspection.py
@@ -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:
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 84ed59aa51..cf64fbfe0b 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -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``
------------
diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt
index 323da06fd6..03d80115d3 100644
--- a/docs/releases/2.1.txt
+++ b/docs/releases/2.1.txt
@@ -155,7 +155,8 @@ Internationalization
Management Commands
~~~~~~~~~~~~~~~~~~~
-* ...
+* The new :option:`inspectdb --include-views` option allows creating models
+ for database views.
Migrations
~~~~~~~~~~
diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py
index 88b828168b..ed55e31353 100644
--- a/tests/inspectdb/tests.py
+++ b/tests/inspectdb/tests.py
@@ -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')
diff --git a/tests/introspection/tests.py b/tests/introspection/tests.py
index a66ecaba03..ce102fcdad 100644
--- a/tests/introspection/tests.py
+++ b/tests/introspection/tests.py
@@ -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)})