Added feature flags for introspection capabilities.

This commit is contained in:
Aymeric Augustin 2014-05-07 22:14:39 +02:00
parent cff59bedc2
commit 99d9fa329a
7 changed files with 88 additions and 44 deletions

View File

@ -518,6 +518,8 @@ class BaseDatabaseFeatures(object):
supports_subqueries_in_group_by = True supports_subqueries_in_group_by = True
supports_bitwise_or = True supports_bitwise_or = True
supports_boolean_type = True
supports_binary_field = True supports_binary_field = True
# Do time/datetime fields have microsecond precision? # Do time/datetime fields have microsecond precision?
@ -562,6 +564,9 @@ class BaseDatabaseFeatures(object):
# Does the backend reset sequences between tests? # Does the backend reset sequences between tests?
supports_sequence_reset = True supports_sequence_reset = True
# Can the backend determine reliably the length of a CharField?
can_introspect_max_length = 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
@ -570,6 +575,24 @@ class BaseDatabaseFeatures(object):
# Can the backend introspect an AutoField, instead of an IntegerField? # Can the backend introspect an AutoField, instead of an IntegerField?
can_introspect_autofield = False can_introspect_autofield = False
# Can the backend introspect a BigIntegerField, instead of an IntegerField?
can_introspect_big_integer_field = True
# Can the backend introspect an BinaryField, instead of an TextField?
can_introspect_binary_field = True
# Can the backend introspect an IPAddressField, instead of an CharField?
can_introspect_ip_address_field = False
# Can the backend introspect a PositiveIntegerField, instead of an IntegerField?
can_introspect_positive_integer_field = False
# Can the backend introspect a SmallIntegerField, instead of an IntegerField?
can_introspect_small_integer_field = False
# Can the backend introspect a TimeField, instead of a DateTimeField?
can_introspect_time_field = True
# Support for the DISTINCT ON clause # Support for the DISTINCT ON clause
can_distinct_on_fields = False can_distinct_on_fields = False
@ -616,6 +639,8 @@ class BaseDatabaseFeatures(object):
# Suffix for backends that don't support "SELECT xxx;" queries. # Suffix for backends that don't support "SELECT xxx;" queries.
bare_select_suffix = '' bare_select_suffix = ''
lowercases_column_names = False
def __init__(self, connection): def __init__(self, connection):
self.connection = connection self.connection = connection

View File

@ -172,11 +172,13 @@ class DatabaseFeatures(BaseDatabaseFeatures):
has_select_for_update_nowait = False has_select_for_update_nowait = False
supports_forward_references = False supports_forward_references = False
supports_long_model_names = False supports_long_model_names = False
supports_boolean_type = False
# XXX MySQL DB-API drivers currently fail on binary data on Python 3. # XXX MySQL DB-API drivers currently fail on binary data on Python 3.
supports_binary_field = six.PY2 supports_binary_field = six.PY2
supports_microsecond_precision = False supports_microsecond_precision = False
supports_regex_backreferencing = False supports_regex_backreferencing = False
supports_date_lookup_using_string = False supports_date_lookup_using_string = False
can_introspect_binary_field = False
supports_timezones = False supports_timezones = False
requires_explicit_null_ordering_when_grouping = True requires_explicit_null_ordering_when_grouping = True
allows_auto_pk_0 = False allows_auto_pk_0 = False

View File

@ -111,6 +111,8 @@ 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_max_length = False
can_introspect_time_field = False
atomic_transactions = False atomic_transactions = False
supports_combined_alters = False supports_combined_alters = False
nulls_order_largest = True nulls_order_largest = True
@ -118,6 +120,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
connection_persists_old_columns = True connection_persists_old_columns = True
closed_cursor_error_class = InterfaceError closed_cursor_error_class = InterfaceError
bare_select_suffix = " FROM DUAL" bare_select_suffix = " FROM DUAL"
lowercases_column_names = True
class DatabaseOperations(BaseDatabaseOperations): class DatabaseOperations(BaseDatabaseOperations):

View File

@ -52,6 +52,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
uses_savepoints = True uses_savepoints = True
supports_tablespaces = True supports_tablespaces = True
supports_transactions = True supports_transactions = True
can_introspect_ip_address_field = True
can_introspect_small_integer_field = True
can_distinct_on_fields = True can_distinct_on_fields = True
can_rollback_ddl = True can_rollback_ddl = True
supports_combined_alters = True supports_combined_alters = True

View File

@ -105,6 +105,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_foreign_keys = False supports_foreign_keys = False
supports_check_constraints = False supports_check_constraints = False
autocommits_when_autocommit_is_off = True autocommits_when_autocommit_is_off = True
can_introspect_positive_integer_field = True
can_introspect_small_integer_field = True
supports_transactions = True supports_transactions = True
atomic_transactions = False atomic_transactions = False
can_rollback_ddl = True can_rollback_ddl = True

View File

@ -2,18 +2,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re import re
from unittest import expectedFailure, skipUnless from unittest import skipUnless
from django.core.management import call_command from django.core.management import call_command
from django.db import connection from django.db import connection
from django.test import TestCase, skipUnlessDBFeature from django.test import TestCase, skipUnlessDBFeature
from django.utils.six import PY3, StringIO from django.utils.six import PY3, StringIO
if connection.vendor == 'oracle':
expectedFailureOnOracle = expectedFailure
else:
expectedFailureOnOracle = lambda f: f
class InspectDBTestCase(TestCase): class InspectDBTestCase(TestCase):
@ -44,30 +39,41 @@ class InspectDBTestCase(TestCase):
return assertFieldType return assertFieldType
# Inspecting Oracle DB doesn't produce correct results, see #19884
@expectedFailureOnOracle
def test_field_types(self): def test_field_types(self):
"""Test introspection of various Django field types""" """Test introspection of various Django field types"""
assertFieldType = self.make_field_type_asserter() assertFieldType = self.make_field_type_asserter()
assertFieldType('char_field', "models.CharField(max_length=10)") # Inspecting Oracle DB doesn't produce correct results (#19884):
assertFieldType('comma_separated_int_field', "models.CharField(max_length=99)") # - it gets max_length wrong: it returns a number of bytes.
# - it reports fields as blank=True when they aren't.
if (connection.features.can_introspect_max_length and
not connection.features.interprets_empty_strings_as_nulls):
assertFieldType('char_field', "models.CharField(max_length=10)")
assertFieldType('comma_separated_int_field', "models.CharField(max_length=99)")
assertFieldType('date_field', "models.DateField()") assertFieldType('date_field', "models.DateField()")
assertFieldType('date_time_field', "models.DateTimeField()") assertFieldType('date_time_field', "models.DateTimeField()")
assertFieldType('email_field', "models.CharField(max_length=75)") if (connection.features.can_introspect_max_length and
assertFieldType('file_field', "models.CharField(max_length=100)") not connection.features.interprets_empty_strings_as_nulls):
assertFieldType('file_path_field', "models.CharField(max_length=100)") assertFieldType('email_field', "models.CharField(max_length=75)")
if connection.vendor == 'postgresql': assertFieldType('file_field', "models.CharField(max_length=100)")
# Only PostgreSQL has a specific type assertFieldType('file_path_field', "models.CharField(max_length=100)")
if connection.features.can_introspect_ip_address_field:
assertFieldType('ip_address_field', "models.GenericIPAddressField()") assertFieldType('ip_address_field', "models.GenericIPAddressField()")
assertFieldType('gen_ip_adress_field', "models.GenericIPAddressField()") assertFieldType('gen_ip_adress_field', "models.GenericIPAddressField()")
else: elif (connection.features.can_introspect_max_length and
not connection.features.interprets_empty_strings_as_nulls):
assertFieldType('ip_address_field', "models.CharField(max_length=15)") assertFieldType('ip_address_field', "models.CharField(max_length=15)")
assertFieldType('gen_ip_adress_field', "models.CharField(max_length=39)") assertFieldType('gen_ip_adress_field', "models.CharField(max_length=39)")
assertFieldType('slug_field', "models.CharField(max_length=50)") if (connection.features.can_introspect_max_length and
assertFieldType('text_field', "models.TextField()") not connection.features.interprets_empty_strings_as_nulls):
assertFieldType('time_field', "models.TimeField()") assertFieldType('slug_field', "models.CharField(max_length=50)")
assertFieldType('url_field', "models.CharField(max_length=200)") if not connection.features.interprets_empty_strings_as_nulls:
assertFieldType('text_field', "models.TextField()")
if connection.features.can_introspect_time_field:
assertFieldType('time_field', "models.TimeField()")
if (connection.features.can_introspect_max_length and
not connection.features.interprets_empty_strings_as_nulls):
assertFieldType('url_field', "models.CharField(max_length=200)")
def test_number_field_types(self): def test_number_field_types(self):
"""Test introspection of various Django field types""" """Test introspection of various Django field types"""
@ -75,34 +81,48 @@ class InspectDBTestCase(TestCase):
if not connection.features.can_introspect_autofield: if not connection.features.can_introspect_autofield:
assertFieldType('id', "models.IntegerField(primary_key=True) # AutoField?") assertFieldType('id', "models.IntegerField(primary_key=True) # AutoField?")
assertFieldType('big_int_field', "models.BigIntegerField()")
if connection.vendor == 'mysql': if connection.features.can_introspect_big_integer_field:
# No native boolean type on MySQL assertFieldType('big_int_field', "models.BigIntegerField()")
assertFieldType('bool_field', "models.IntegerField()")
assertFieldType('null_bool_field', "models.IntegerField(blank=True, null=True)")
else: else:
assertFieldType('big_int_field', "models.IntegerField()")
if connection.features.supports_boolean_type:
assertFieldType('bool_field', "models.BooleanField()") assertFieldType('bool_field', "models.BooleanField()")
assertFieldType('null_bool_field', "models.NullBooleanField()") assertFieldType('null_bool_field', "models.NullBooleanField()")
else:
assertFieldType('bool_field', "models.IntegerField()")
assertFieldType('null_bool_field', "models.IntegerField(blank=True, null=True)")
if connection.vendor == 'sqlite': if connection.vendor == 'sqlite':
# Guessed arguments, see #5014 # Guessed arguments on SQLite, see #5014
assertFieldType('decimal_field', "models.DecimalField(max_digits=10, decimal_places=5) " assertFieldType('decimal_field', "models.DecimalField(max_digits=10, decimal_places=5) "
"# max_digits and decimal_places have been guessed, " "# max_digits and decimal_places have been guessed, "
"as this database handles decimal fields as float") "as this database handles decimal fields as float")
else: else:
assertFieldType('decimal_field', "models.DecimalField(max_digits=6, decimal_places=1)") assertFieldType('decimal_field', "models.DecimalField(max_digits=6, decimal_places=1)")
assertFieldType('float_field', "models.FloatField()") assertFieldType('float_field', "models.FloatField()")
assertFieldType('int_field', "models.IntegerField()") assertFieldType('int_field', "models.IntegerField()")
if connection.vendor == 'sqlite':
if connection.features.can_introspect_positive_integer_field:
assertFieldType('pos_int_field', "models.PositiveIntegerField()") assertFieldType('pos_int_field', "models.PositiveIntegerField()")
assertFieldType('pos_small_int_field', "models.PositiveSmallIntegerField()")
else: else:
# 'unsigned' property undetected on other backends
assertFieldType('pos_int_field', "models.IntegerField()") assertFieldType('pos_int_field', "models.IntegerField()")
if connection.vendor == 'postgresql':
if connection.features.can_introspect_positive_integer_field:
if connection.features.can_introspect_small_integer_field:
assertFieldType('pos_small_int_field', "models.PositiveSmallIntegerField()")
else:
assertFieldType('pos_small_int_field', "models.PositiveIntegerField()")
else:
if connection.features.can_introspect_small_integer_field:
assertFieldType('pos_small_int_field', "models.SmallIntegerField()") assertFieldType('pos_small_int_field', "models.SmallIntegerField()")
else: else:
assertFieldType('pos_small_int_field', "models.IntegerField()") assertFieldType('pos_small_int_field', "models.IntegerField()")
if connection.vendor in ('sqlite', 'postgresql'):
if connection.features.can_introspect_small_integer_field:
assertFieldType('small_int_field', "models.SmallIntegerField()") assertFieldType('small_int_field', "models.SmallIntegerField()")
else: else:
assertFieldType('small_int_field', "models.IntegerField()") assertFieldType('small_int_field', "models.IntegerField()")
@ -156,7 +176,7 @@ class InspectDBTestCase(TestCase):
out = StringIO() out = StringIO()
call_command('inspectdb', stdout=out) call_command('inspectdb', stdout=out)
output = out.getvalue() output = out.getvalue()
base_name = 'Field' if connection.vendor != 'oracle' else 'field' base_name = 'field' if connection.features.lowercases_column_names else 'Field'
self.assertIn("field = models.IntegerField()", output) self.assertIn("field = models.IntegerField()", output)
self.assertIn("field_field = models.IntegerField(db_column='%s_')" % base_name, output) self.assertIn("field_field = models.IntegerField(db_column='%s_')" % base_name, output)
self.assertIn("field_field_0 = models.IntegerField(db_column='%s__')" % base_name, output) self.assertIn("field_field_0 = models.IntegerField(db_column='%s__')" % base_name, output)

View File

@ -1,17 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
from django.db import connection from django.db import connection
from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature
from .models import Reporter, Article from .models import Reporter, Article
if connection.vendor == 'oracle':
expectedFailureOnOracle = unittest.expectedFailure
else:
expectedFailureOnOracle = lambda f: f
class IntrospectionTests(TestCase): class IntrospectionTests(TestCase):
def test_table_names(self): def test_table_names(self):
@ -61,19 +54,16 @@ class IntrospectionTests(TestCase):
def test_get_table_description_types(self): def test_get_table_description_types(self):
with connection.cursor() as cursor: with connection.cursor() as cursor:
desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table) desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table)
# The MySQL exception is due to the cursor.description returning the same constant for
# text and blob columns. TODO: use information_schema database to retrieve the proper
# field type on MySQL
self.assertEqual( self.assertEqual(
[datatype(r[1], r) for r in desc], [datatype(r[1], r) for r in desc],
['AutoField' if connection.features.can_introspect_autofield else 'IntegerField', ['AutoField' if connection.features.can_introspect_autofield else 'IntegerField',
'CharField', 'CharField', 'CharField', 'BigIntegerField', 'CharField', 'CharField', 'CharField', 'BigIntegerField',
'BinaryField' if connection.vendor != 'mysql' else 'TextField'] 'BinaryField' if connection.features.can_introspect_binary_field else 'TextField']
) )
# The following test fails on Oracle due to #17202 (can't correctly # The following test fails on Oracle due to #17202 (can't correctly
# inspect the length of character columns). # inspect the length of character columns).
@expectedFailureOnOracle @skipUnlessDBFeature('can_introspect_max_length')
def test_get_table_description_col_lengths(self): def test_get_table_description_col_lengths(self):
with connection.cursor() as cursor: with connection.cursor() as cursor:
desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table) desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table)