Skip to content

Commit 584e2c0

Browse files
committed
Prevent Oracle from changing field.null to True
Fixed django#17957 -- when using Oracle and character fields, the fields were set null = True to ease the handling of empty strings. This caused problems when using multiple databases from different vendors, or when the character field happened to be also a primary key. The handling was changed so that NOT NULL is not emitted on Oracle even if field.null = False, and field.null is not touched otherwise. Thanks to bhuztez for the report, ramiro for triaging & comments, ikelly for the patch and alex for reviewing.
1 parent e75bd7e commit 584e2c0

File tree

6 files changed

+57
-16
lines changed

6 files changed

+57
-16
lines changed

django/db/backends/creation.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,13 @@ def sql_create_model(self, model, style, known_models=set()):
5050
# Make the definition (e.g. 'foo VARCHAR(30)') for this field.
5151
field_output = [style.SQL_FIELD(qn(f.column)),
5252
style.SQL_COLTYPE(col_type)]
53-
if not f.null:
53+
# Oracle treats the empty string ('') as null, so coerce the null
54+
# option whenever '' is a possible value.
55+
null = f.null
56+
if (f.empty_strings_allowed and not f.primary_key and
57+
self.connection.features.interprets_empty_strings_as_nulls):
58+
null = True
59+
if not null:
5460
field_output.append(style.SQL_KEYWORD('NOT NULL'))
5561
if f.primary_key:
5662
field_output.append(style.SQL_KEYWORD('PRIMARY KEY'))

django/db/models/fields/__init__.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,6 @@ def __init__(self, verbose_name=None, name=None, primary_key=False,
8585
self.primary_key = primary_key
8686
self.max_length, self._unique = max_length, unique
8787
self.blank, self.null = blank, null
88-
# Oracle treats the empty string ('') as null, so coerce the null
89-
# option whenever '' is a possible value.
90-
if (self.empty_strings_allowed and
91-
connection.features.interprets_empty_strings_as_nulls):
92-
self.null = True
9388
self.rel = rel
9489
self.default = default
9590
self.editable = editable

django/db/models/sql/query.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,7 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
11931193
entry.negate()
11941194
self.where.add(entry, AND)
11951195
break
1196-
if field.null:
1196+
if self.is_nullable(field):
11971197
# In SQL NULL = anyvalue returns unknown, and NOT unknown
11981198
# is still unknown. However, in Python None = anyvalue is False
11991199
# (and not False is True...), and we want to return this Python's
@@ -1396,7 +1396,8 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
13961396
opts, target)
13971397

13981398
alias = self.join((alias, table, from_col, to_col),
1399-
exclusions=exclusions, nullable=field.null)
1399+
exclusions=exclusions,
1400+
nullable=self.is_nullable(field))
14001401
joins.append(alias)
14011402
else:
14021403
# Non-relation fields.
@@ -1946,6 +1947,24 @@ def set_start(self, start):
19461947
self.select = [(select_alias, select_col)]
19471948
self.remove_inherited_models()
19481949

1950+
def is_nullable(self, field):
1951+
"""
1952+
A helper to check if the given field should be treated as nullable.
1953+
1954+
Some backends treat '' as null and Django treats such fields as
1955+
nullable for those backends. In such situations field.null can be
1956+
False even if we should treat the field as nullable.
1957+
"""
1958+
# We need to use DEFAULT_DB_ALIAS here, as QuerySet does not have
1959+
# (nor should it have) knowledge of which connection is going to be
1960+
# used. The proper fix would be to defer all decisions where
1961+
# is_nullable() is needed to the compiler stage, but that is not easy
1962+
# to do currently.
1963+
if ((connections[DEFAULT_DB_ALIAS].features.interprets_empty_strings_as_nulls)
1964+
and field.empty_strings_allowed):
1965+
return True
1966+
else:
1967+
return field.null
19491968

19501969
def get_order_dir(field, default='ASC'):
19511970
"""

docs/ref/databases.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -685,11 +685,11 @@ NULL and empty strings
685685

686686
Django generally prefers to use the empty string ('') rather than
687687
NULL, but Oracle treats both identically. To get around this, the
688-
Oracle backend coerces the ``null=True`` option on fields that have
689-
the empty string as a possible value. When fetching from the database,
690-
it is assumed that a NULL value in one of these fields really means
691-
the empty string, and the data is silently converted to reflect this
692-
assumption.
688+
Oracle backend ignores an explicit ``null`` option on fields that
689+
have the empty string as a possible value and generates DDL as if
690+
``null=True``. When fetching from the database, it is assumed that
691+
a ``NULL`` value in one of these fields really means the empty
692+
string, and the data is silently converted to reflect this assumption.
693693

694694
``TextField`` limitations
695695
-------------------------

docs/ref/models/fields.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,8 @@ string, not ``NULL``.
5555

5656
.. note::
5757

58-
When using the Oracle database backend, the ``null=True`` option will be
59-
coerced for string-based fields that have the empty string as a possible
60-
value, and the value ``NULL`` will be stored to denote the empty string.
58+
When using the Oracle database backend, the value ``NULL`` will be stored to
59+
denote the empty string regardless of this attribute.
6160

6261
If you want to accept :attr:`~Field.null` values with :class:`BooleanField`,
6362
use :class:`NullBooleanField` instead.

tests/regressiontests/queries/tests.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,3 +1930,25 @@ def test_col_not_in_list_containing_null(self):
19301930
self.assertQuerysetEqual(
19311931
NullableName.objects.exclude(name__in=[None]),
19321932
['i1'], attrgetter('name'))
1933+
1934+
class EmptyStringsAsNullTest(TestCase):
1935+
"""
1936+
Test that filtering on non-null character fields works as expected.
1937+
The reason for these tests is that Oracle treats '' as NULL, and this
1938+
can cause problems in query construction. Refs #17957.
1939+
"""
1940+
1941+
def setUp(self):
1942+
self.nc = NamedCategory.objects.create(name='')
1943+
1944+
def test_direct_exclude(self):
1945+
self.assertQuerysetEqual(
1946+
NamedCategory.objects.exclude(name__in=['nonexisting']),
1947+
[self.nc.pk], attrgetter('pk')
1948+
)
1949+
1950+
def test_joined_exclude(self):
1951+
self.assertQuerysetEqual(
1952+
DumbCategory.objects.exclude(namedcategory__name__in=['nonexisting']),
1953+
[self.nc.pk], attrgetter('pk')
1954+
)

0 commit comments

Comments
 (0)