mirror of
https://github.com/django/django.git
synced 2025-03-27 17:50:46 +00:00
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.
|
# This is True for all core backends.
|
||||||
can_introspect_null = True
|
can_introspect_null = True
|
||||||
|
|
||||||
|
# Can the backend introspect the default value of a column?
|
||||||
|
can_introspect_default = True
|
||||||
|
|
||||||
# Confirm support for introspected foreign keys
|
# Confirm support for introspected foreign keys
|
||||||
# Every database can do this reliably, except MySQL,
|
# Every database can do this reliably, except MySQL,
|
||||||
# which can't do it for MyISAM tables
|
# 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
|
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 `([^`]*)` \(`([^`]*)`\)")
|
foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)")
|
||||||
|
|
||||||
|
|
||||||
@ -61,9 +61,9 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||||||
# not visible length (#5725)
|
# not visible length (#5725)
|
||||||
# - precision and scale (for decimal fields) (#5014)
|
# - precision and scale (for decimal fields) (#5014)
|
||||||
# - auto_increment is not available in cursor.description
|
# - auto_increment is not available in cursor.description
|
||||||
InfoLine = namedtuple('InfoLine', 'col_name data_type max_len num_prec num_scale extra')
|
|
||||||
cursor.execute("""
|
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
|
FROM information_schema.columns
|
||||||
WHERE table_name = %s AND table_schema = DATABASE()""", [table_name])
|
WHERE table_name = %s AND table_schema = DATABASE()""", [table_name])
|
||||||
field_info = {line[0]: InfoLine(*line) for line in cursor.fetchall()}
|
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_prec) or line[4],
|
||||||
to_int(field_info[col_name].num_scale) or line[5])
|
to_int(field_info[col_name].num_scale) or line[5])
|
||||||
+ (line[6],)
|
+ (line[6],)
|
||||||
+ (field_info[col_name].extra,)))
|
+ (field_info[col_name].extra,)
|
||||||
|
+ (field_info[col_name].column_default,)))
|
||||||
)
|
)
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
has_bulk_insert = True
|
has_bulk_insert = True
|
||||||
supports_tablespaces = True
|
supports_tablespaces = True
|
||||||
supports_sequence_reset = False
|
supports_sequence_reset = False
|
||||||
|
can_introspect_default = False # Pending implementation by an interested person.
|
||||||
can_introspect_max_length = False
|
can_introspect_max_length = False
|
||||||
can_introspect_time_field = False
|
can_introspect_time_field = False
|
||||||
atomic_transactions = False
|
atomic_transactions = False
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
from django.db.backends.base.introspection import (
|
from django.db.backends.base.introspection import (
|
||||||
BaseDatabaseIntrospection, FieldInfo, TableInfo,
|
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*$')
|
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):
|
def get_field_size(name):
|
||||||
@ -69,8 +71,18 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||||||
|
|
||||||
def get_table_description(self, cursor, table_name):
|
def get_table_description(self, cursor, table_name):
|
||||||
"Returns a description of the table, with the DB-API cursor.description interface."
|
"Returns a description of the table, with the DB-API cursor.description interface."
|
||||||
return [FieldInfo(info['name'], info['type'], None, info['size'], None, None,
|
return [
|
||||||
info['null_ok']) for info in self._table_info(cursor, table_name)]
|
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):
|
def column_name_converter(self, name):
|
||||||
"""
|
"""
|
||||||
@ -211,13 +223,15 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||||||
|
|
||||||
def _table_info(self, cursor, name):
|
def _table_info(self, cursor, name):
|
||||||
cursor.execute('PRAGMA table_info(%s)' % self.connection.ops.quote_name(name))
|
cursor.execute('PRAGMA table_info(%s)' % self.connection.ops.quote_name(name))
|
||||||
# cid, name, type, notnull, dflt_value, pk
|
# cid, name, type, notnull, default_value, pk
|
||||||
return [{'name': field[1],
|
return [{
|
||||||
'type': field[2],
|
'name': field[1],
|
||||||
'size': get_field_size(field[2]),
|
'type': field[2],
|
||||||
'null_ok': not field[3],
|
'size': get_field_size(field[2]),
|
||||||
'pk': field[5] # undocumented
|
'null_ok': not field[3],
|
||||||
} for field in cursor.fetchall()]
|
'default': field[4],
|
||||||
|
'pk': field[5], # undocumented
|
||||||
|
} for field in cursor.fetchall()]
|
||||||
|
|
||||||
def get_constraints(self, cursor, table_name):
|
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
|
deprecation timeline for a given feature, its removal may appear as a
|
||||||
backwards incompatible change.
|
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
|
Miscellaneous
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -1348,3 +1348,53 @@ class SchemaTests(TransactionTestCase):
|
|||||||
finally:
|
finally:
|
||||||
# Cleanup model states
|
# Cleanup model states
|
||||||
AuthorWithM2M._meta.local_many_to_many.remove(new_field)
|
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…
x
Reference in New Issue
Block a user