diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 97e42d8f50f..63f8f9d973b 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -518,6 +518,8 @@ class BaseDatabaseFeatures(object): supports_subqueries_in_group_by = True supports_bitwise_or = True + supports_boolean_type = True + supports_binary_field = True # Do time/datetime fields have microsecond precision? @@ -562,6 +564,9 @@ class BaseDatabaseFeatures(object): # Does the backend reset sequences between tests? 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 # Every database can do this reliably, except MySQL, # 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_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 can_distinct_on_fields = False @@ -616,6 +639,8 @@ class BaseDatabaseFeatures(object): # Suffix for backends that don't support "SELECT xxx;" queries. bare_select_suffix = '' + lowercases_column_names = False + def __init__(self, connection): self.connection = connection diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 70d9a29d566..3c71c5792c2 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -172,11 +172,13 @@ class DatabaseFeatures(BaseDatabaseFeatures): has_select_for_update_nowait = False supports_forward_references = False supports_long_model_names = False + supports_boolean_type = False # XXX MySQL DB-API drivers currently fail on binary data on Python 3. supports_binary_field = six.PY2 supports_microsecond_precision = False supports_regex_backreferencing = False supports_date_lookup_using_string = False + can_introspect_binary_field = False supports_timezones = False requires_explicit_null_ordering_when_grouping = True allows_auto_pk_0 = False diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index e53c8c9fb67..312eb7e40b0 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -111,6 +111,8 @@ class DatabaseFeatures(BaseDatabaseFeatures): has_bulk_insert = True supports_tablespaces = True supports_sequence_reset = False + can_introspect_max_length = False + can_introspect_time_field = False atomic_transactions = False supports_combined_alters = False nulls_order_largest = True @@ -118,6 +120,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): connection_persists_old_columns = True closed_cursor_error_class = InterfaceError bare_select_suffix = " FROM DUAL" + lowercases_column_names = True class DatabaseOperations(BaseDatabaseOperations): diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index df883e3ebb5..7b2cdc0aa20 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -52,6 +52,8 @@ class DatabaseFeatures(BaseDatabaseFeatures): uses_savepoints = True supports_tablespaces = True supports_transactions = True + can_introspect_ip_address_field = True + can_introspect_small_integer_field = True can_distinct_on_fields = True can_rollback_ddl = True supports_combined_alters = True diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 4dcdb1109d9..419b3944299 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -105,6 +105,8 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_foreign_keys = False supports_check_constraints = False autocommits_when_autocommit_is_off = True + can_introspect_positive_integer_field = True + can_introspect_small_integer_field = True supports_transactions = True atomic_transactions = False can_rollback_ddl = True diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py index 6c5ede7c61a..856e63ce4a6 100644 --- a/tests/inspectdb/tests.py +++ b/tests/inspectdb/tests.py @@ -2,18 +2,13 @@ from __future__ import unicode_literals import re -from unittest import expectedFailure, skipUnless +from unittest import skipUnless from django.core.management import call_command from django.db import connection from django.test import TestCase, skipUnlessDBFeature from django.utils.six import PY3, StringIO -if connection.vendor == 'oracle': - expectedFailureOnOracle = expectedFailure -else: - expectedFailureOnOracle = lambda f: f - class InspectDBTestCase(TestCase): @@ -44,30 +39,41 @@ class InspectDBTestCase(TestCase): return assertFieldType - # Inspecting Oracle DB doesn't produce correct results, see #19884 - @expectedFailureOnOracle def test_field_types(self): """Test introspection of various Django field types""" assertFieldType = self.make_field_type_asserter() - assertFieldType('char_field', "models.CharField(max_length=10)") - assertFieldType('comma_separated_int_field', "models.CharField(max_length=99)") + # Inspecting Oracle DB doesn't produce correct results (#19884): + # - 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_time_field', "models.DateTimeField()") - assertFieldType('email_field', "models.CharField(max_length=75)") - assertFieldType('file_field', "models.CharField(max_length=100)") - assertFieldType('file_path_field', "models.CharField(max_length=100)") - if connection.vendor == 'postgresql': - # Only PostgreSQL has a specific type + if (connection.features.can_introspect_max_length and + not connection.features.interprets_empty_strings_as_nulls): + assertFieldType('email_field', "models.CharField(max_length=75)") + assertFieldType('file_field', "models.CharField(max_length=100)") + assertFieldType('file_path_field', "models.CharField(max_length=100)") + if connection.features.can_introspect_ip_address_field: assertFieldType('ip_address_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('gen_ip_adress_field', "models.CharField(max_length=39)") - assertFieldType('slug_field', "models.CharField(max_length=50)") - assertFieldType('text_field', "models.TextField()") - assertFieldType('time_field', "models.TimeField()") - assertFieldType('url_field', "models.CharField(max_length=200)") + if (connection.features.can_introspect_max_length and + not connection.features.interprets_empty_strings_as_nulls): + assertFieldType('slug_field', "models.CharField(max_length=50)") + 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): """Test introspection of various Django field types""" @@ -75,34 +81,48 @@ class InspectDBTestCase(TestCase): if not connection.features.can_introspect_autofield: assertFieldType('id', "models.IntegerField(primary_key=True) # AutoField?") - assertFieldType('big_int_field', "models.BigIntegerField()") - if connection.vendor == 'mysql': - # No native boolean type on MySQL - assertFieldType('bool_field', "models.IntegerField()") - assertFieldType('null_bool_field', "models.IntegerField(blank=True, null=True)") + + if connection.features.can_introspect_big_integer_field: + assertFieldType('big_int_field', "models.BigIntegerField()") else: + assertFieldType('big_int_field', "models.IntegerField()") + + if connection.features.supports_boolean_type: assertFieldType('bool_field', "models.BooleanField()") 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': - # Guessed arguments, see #5014 + # Guessed arguments on SQLite, see #5014 assertFieldType('decimal_field', "models.DecimalField(max_digits=10, decimal_places=5) " "# max_digits and decimal_places have been guessed, " "as this database handles decimal fields as float") else: assertFieldType('decimal_field', "models.DecimalField(max_digits=6, decimal_places=1)") + assertFieldType('float_field', "models.FloatField()") + 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_small_int_field', "models.PositiveSmallIntegerField()") else: - # 'unsigned' property undetected on other backends 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()") else: 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()") else: assertFieldType('small_int_field', "models.IntegerField()") @@ -156,7 +176,7 @@ class InspectDBTestCase(TestCase): out = StringIO() call_command('inspectdb', stdout=out) 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_field = models.IntegerField(db_column='%s_')" % base_name, output) self.assertIn("field_field_0 = models.IntegerField(db_column='%s__')" % base_name, output) diff --git a/tests/introspection/tests.py b/tests/introspection/tests.py index 0c339bc8ead..fe7c3cefba5 100644 --- a/tests/introspection/tests.py +++ b/tests/introspection/tests.py @@ -1,17 +1,10 @@ from __future__ import unicode_literals -import unittest - from django.db import connection from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature from .models import Reporter, Article -if connection.vendor == 'oracle': - expectedFailureOnOracle = unittest.expectedFailure -else: - expectedFailureOnOracle = lambda f: f - class IntrospectionTests(TestCase): def test_table_names(self): @@ -61,19 +54,16 @@ class IntrospectionTests(TestCase): def test_get_table_description_types(self): with connection.cursor() as cursor: 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( [datatype(r[1], r) for r in desc], ['AutoField' if connection.features.can_introspect_autofield else 'IntegerField', '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 # inspect the length of character columns). - @expectedFailureOnOracle + @skipUnlessDBFeature('can_introspect_max_length') def test_get_table_description_col_lengths(self): with connection.cursor() as cursor: desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table)