summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Graham <timograham@gmail.com>2014-12-29 11:41:16 -0500
committerTim Graham <timograham@gmail.com>2015-01-31 12:33:11 -0500
commit75303b01a9cc900eebf1f27ba0bc6508334242fc (patch)
tree2a5da4f8a8036b5f833caa776ecec20dd81774d4
parent64a899dc815f1a070dc7a7c22276e8bb41e46ea6 (diff)
Fixed #24245 -- Added introspection for database defaults.
Needed for tests for migrations handling of database defaults.
-rw-r--r--django/db/backends/base/features.py3
-rw-r--r--django/db/backends/mysql/introspection.py11
-rw-r--r--django/db/backends/oracle/features.py1
-rw-r--r--django/db/backends/sqlite3/introspection.py32
-rw-r--r--docs/releases/1.9.txt9
-rw-r--r--tests/schema/tests.py50
6 files changed, 92 insertions, 14 deletions
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py
index 4b4d5c6d75..a7bc85e683 100644
--- a/django/db/backends/base/features.py
+++ b/django/db/backends/base/features.py
@@ -125,6 +125,9 @@ class BaseDatabaseFeatures(object):
# This is True for all core backends.
can_introspect_null = True
+ # Can the backend introspect the default value of a column?
+ can_introspect_default = True
+
# Confirm support for introspected foreign keys
# Every database can do this reliably, except MySQL,
# which can't do it for MyISAM tables
diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py
index dc78a00bfc..aa3c008d13 100644
--- a/django/db/backends/mysql/introspection.py
+++ b/django/db/backends/mysql/introspection.py
@@ -9,8 +9,8 @@ from django.utils.encoding import force_text
from MySQLdb.constants import FIELD_TYPE
-FieldInfo = namedtuple('FieldInfo', FieldInfo._fields + ('extra',))
-
+FieldInfo = namedtuple('FieldInfo', FieldInfo._fields + ('extra', 'default'))
+InfoLine = namedtuple('InfoLine', 'col_name data_type max_len num_prec num_scale extra column_default')
foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)")
@@ -61,9 +61,9 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
# not visible length (#5725)
# - precision and scale (for decimal fields) (#5014)
# - auto_increment is not available in cursor.description
- InfoLine = namedtuple('InfoLine', 'col_name data_type max_len num_prec num_scale extra')
cursor.execute("""
- SELECT column_name, data_type, character_maximum_length, numeric_precision, numeric_scale, extra
+ SELECT column_name, data_type, character_maximum_length, numeric_precision,
+ numeric_scale, extra, column_default
FROM information_schema.columns
WHERE table_name = %s AND table_schema = DATABASE()""", [table_name])
field_info = {line[0]: InfoLine(*line) for line in cursor.fetchall()}
@@ -80,7 +80,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
to_int(field_info[col_name].num_prec) or line[4],
to_int(field_info[col_name].num_scale) or line[5])
+ (line[6],)
- + (field_info[col_name].extra,)))
+ + (field_info[col_name].extra,)
+ + (field_info[col_name].column_default,)))
)
return fields
diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py
index 93c0beaf1e..ffa5c6474c 100644
--- a/django/db/backends/oracle/features.py
+++ b/django/db/backends/oracle/features.py
@@ -28,6 +28,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
has_bulk_insert = True
supports_tablespaces = True
supports_sequence_reset = False
+ can_introspect_default = False # Pending implementation by an interested person.
can_introspect_max_length = False
can_introspect_time_field = False
atomic_transactions = False
diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py
index 13b658bd6a..a002b79933 100644
--- a/django/db/backends/sqlite3/introspection.py
+++ b/django/db/backends/sqlite3/introspection.py
@@ -1,4 +1,5 @@
import re
+from collections import namedtuple
from django.db.backends.base.introspection import (
BaseDatabaseIntrospection, FieldInfo, TableInfo,
@@ -6,6 +7,7 @@ from django.db.backends.base.introspection import (
field_size_re = re.compile(r'^\s*(?:var)?char\s*\(\s*(\d+)\s*\)\s*$')
+FieldInfo = namedtuple('FieldInfo', FieldInfo._fields + ('default',))
def get_field_size(name):
@@ -69,8 +71,18 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
def get_table_description(self, cursor, table_name):
"Returns a description of the table, with the DB-API cursor.description interface."
- return [FieldInfo(info['name'], info['type'], None, info['size'], None, None,
- info['null_ok']) for info in self._table_info(cursor, table_name)]
+ return [
+ FieldInfo(
+ info['name'],
+ info['type'],
+ None,
+ info['size'],
+ None,
+ None,
+ info['null_ok'],
+ info['default'],
+ ) for info in self._table_info(cursor, table_name)
+ ]
def column_name_converter(self, name):
"""
@@ -211,13 +223,15 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
def _table_info(self, cursor, name):
cursor.execute('PRAGMA table_info(%s)' % self.connection.ops.quote_name(name))
- # cid, name, type, notnull, dflt_value, pk
- return [{'name': field[1],
- 'type': field[2],
- 'size': get_field_size(field[2]),
- 'null_ok': not field[3],
- 'pk': field[5] # undocumented
- } for field in cursor.fetchall()]
+ # cid, name, type, notnull, default_value, pk
+ return [{
+ 'name': field[1],
+ 'type': field[2],
+ 'size': get_field_size(field[2]),
+ 'null_ok': not field[3],
+ 'default': field[4],
+ 'pk': field[5], # undocumented
+ } for field in cursor.fetchall()]
def get_constraints(self, cursor, table_name):
"""
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index ce317ffeb7..8c7aff8fe1 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -165,6 +165,15 @@ Backwards incompatible changes in 1.9
deprecation timeline for a given feature, its removal may appear as a
backwards incompatible change.
+Database backend API
+~~~~~~~~~~~~~~~~~~~~
+
+* A couple of new tests rely on the ability of the backend to introspect column
+ defaults (returning the result as ``Field.default``). You can set the
+ ``can_introspect_default`` database feature to ``False`` if your backend
+ doesn't implement this. You may want to review the implementation on the
+ backends that Django includes for reference (:ticket:`24245`).
+
Miscellaneous
~~~~~~~~~~~~~
diff --git a/tests/schema/tests.py b/tests/schema/tests.py
index 2dd56d7248..12fac76579 100644
--- a/tests/schema/tests.py
+++ b/tests/schema/tests.py
@@ -1348,3 +1348,53 @@ class SchemaTests(TransactionTestCase):
finally:
# Cleanup model states
AuthorWithM2M._meta.local_many_to_many.remove(new_field)
+
+ def test_add_field_default_dropped(self):
+ # Create the table
+ with connection.schema_editor() as editor:
+ editor.create_model(Author)
+ # Ensure there's no surname field
+ columns = self.column_classes(Author)
+ self.assertNotIn("surname", columns)
+ # Create a row
+ Author.objects.create(name='Anonymous1')
+ # Add new CharField with a default
+ new_field = CharField(max_length=15, blank=True, default='surname default')
+ new_field.set_attributes_from_name("surname")
+ with connection.schema_editor() as editor:
+ editor.add_field(Author, new_field)
+ # Ensure field was added with the right default
+ with connection.cursor() as cursor:
+ cursor.execute("SELECT surname FROM schema_author;")
+ item = cursor.fetchall()[0]
+ self.assertEqual(item[0], 'surname default')
+ # And that the default is no longer set in the database.
+ field = next(
+ f for f in connection.introspection.get_table_description(cursor, "schema_author")
+ if f.name == "surname"
+ )
+ if connection.features.can_introspect_default:
+ self.assertIsNone(field.default)
+
+ def test_alter_field_default_dropped(self):
+ # Create the table
+ with connection.schema_editor() as editor:
+ editor.create_model(Author)
+ # Create a row
+ Author.objects.create(name='Anonymous1')
+ self.assertEqual(Author.objects.get().height, None)
+ old_field = Author._meta.get_field('height')
+ # The default from the new field is used in updating existing rows.
+ new_field = IntegerField(blank=True, default=42)
+ new_field.set_attributes_from_name('height')
+ with connection.schema_editor() as editor:
+ editor.alter_field(Author, old_field, new_field)
+ self.assertEqual(Author.objects.get().height, 42)
+ # The database default should be removed.
+ with connection.cursor() as cursor:
+ field = next(
+ f for f in connection.introspection.get_table_description(cursor, "schema_author")
+ if f.name == "height"
+ )
+ if connection.features.can_introspect_default:
+ self.assertIsNone(field.default)