Fixed #24245 -- Added introspection for database defaults.
Needed for tests for migrations handling of database defaults.
This commit is contained in:
parent
64a899dc81
commit
75303b01a9
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,12 +223,14 @@ 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],
|
||||
# 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],
|
||||
'pk': field[5] # undocumented
|
||||
'default': field[4],
|
||||
'pk': field[5], # undocumented
|
||||
} for field in cursor.fetchall()]
|
||||
|
||||
def get_constraints(self, cursor, table_name):
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue