Fixed #24245 -- Added introspection for database defaults.

Needed for tests for migrations handling of database defaults.
This commit is contained in:
Tim Graham 2014-12-29 11:41:16 -05:00
parent 64a899dc81
commit 75303b01a9
6 changed files with 92 additions and 14 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):
"""

View File

@ -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
~~~~~~~~~~~~~

View File

@ -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)