1
0
mirror of https://github.com/django/django.git synced 2025-06-06 12:09:11 +00:00

[5.0.x] Fixed #35018 -- Fixed migrations crash on GeneratedField with BooleanField as output_field on Oracle < 23c.

Thanks Václav Řehák for the report.

Regression in f333e3513e8bdf5ffeb6eeb63021c230082e6f95.

Backport of fcf95e592774a6ededec35481a2061474d467a2b from main.
This commit is contained in:
Mariusz Felisiak 2023-12-12 05:39:11 +01:00
parent 03af8fbd0f
commit 5f89da0837
8 changed files with 75 additions and 18 deletions

View File

@ -300,7 +300,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
@async_unsafe @async_unsafe
def create_cursor(self, name=None): def create_cursor(self, name=None):
return FormatStylePlaceholderCursor(self.connection) return FormatStylePlaceholderCursor(self.connection, self)
def _commit(self): def _commit(self):
if self.connection is not None: if self.connection is not None:
@ -365,7 +365,11 @@ class OracleParam:
param = Oracle_datetime.from_datetime(param) param = Oracle_datetime.from_datetime(param)
string_size = 0 string_size = 0
# Oracle doesn't recognize True and False correctly. has_boolean_data_type = (
cursor.database.features.supports_boolean_expr_in_select_clause
)
if not has_boolean_data_type:
# Oracle < 23c doesn't recognize True and False correctly.
if param is True: if param is True:
param = 1 param = 1
elif param is False: elif param is False:
@ -389,6 +393,8 @@ class OracleParam:
self.input_size = Database.CLOB self.input_size = Database.CLOB
elif isinstance(param, datetime.datetime): elif isinstance(param, datetime.datetime):
self.input_size = Database.TIMESTAMP self.input_size = Database.TIMESTAMP
elif has_boolean_data_type and isinstance(param, bool):
self.input_size = Database.DB_TYPE_BOOLEAN
else: else:
self.input_size = None self.input_size = None
@ -426,9 +432,10 @@ class FormatStylePlaceholderCursor:
charset = "utf-8" charset = "utf-8"
def __init__(self, connection): def __init__(self, connection, database):
self.cursor = connection.cursor() self.cursor = connection.cursor()
self.cursor.outputtypehandler = self._output_type_handler self.cursor.outputtypehandler = self._output_type_handler
self.database = database
@staticmethod @staticmethod
def _output_number_converter(value): def _output_number_converter(value):
@ -528,14 +535,24 @@ class FormatStylePlaceholderCursor:
# values. It can be used only in single query execute() because # values. It can be used only in single query execute() because
# executemany() shares the formatted query with each of the params # executemany() shares the formatted query with each of the params
# list. e.g. for input params = [0.75, 2, 0.75, 'sth', 0.75] # list. e.g. for input params = [0.75, 2, 0.75, 'sth', 0.75]
# params_dict = {0.75: ':arg0', 2: ':arg1', 'sth': ':arg2'} # params_dict = {
# (float, 0.75): ':arg0',
# (int, 2): ':arg1',
# (str, 'sth'): ':arg2',
# }
# args = [':arg0', ':arg1', ':arg0', ':arg2', ':arg0'] # args = [':arg0', ':arg1', ':arg0', ':arg2', ':arg0']
# params = {':arg0': 0.75, ':arg1': 2, ':arg2': 'sth'} # params = {':arg0': 0.75, ':arg1': 2, ':arg2': 'sth'}
# The type of parameters in param_types keys is necessary to avoid
# unifying 0/1 with False/True.
param_types = [(type(param), param) for param in params]
params_dict = { params_dict = {
param: ":arg%d" % i for i, param in enumerate(dict.fromkeys(params)) param_type: ":arg%d" % i
for i, param_type in enumerate(dict.fromkeys(param_types))
}
args = [params_dict[param_type] for param_type in param_types]
params = {
placeholder: param for (_, param), placeholder in params_dict.items()
} }
args = [params_dict[param] for param in params]
params = {value: key for key, value in params_dict.items()}
query %= tuple(args) query %= tuple(args)
else: else:
# Handle params as sequence # Handle params as sequence

View File

@ -76,7 +76,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_slicing_ordering_in_compound = True supports_slicing_ordering_in_compound = True
requires_compound_order_by_subquery = True requires_compound_order_by_subquery = True
allows_multiple_constraints_on_same_fields = False allows_multiple_constraints_on_same_fields = False
supports_boolean_expr_in_select_clause = False
supports_comparing_boolean_expr = False supports_comparing_boolean_expr = False
supports_json_field_contains = False supports_json_field_contains = False
supports_collation_on_textfield = False supports_collation_on_textfield = False
@ -119,6 +118,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"Oracle doesn't support comparing NCLOB to NUMBER.": { "Oracle doesn't support comparing NCLOB to NUMBER.": {
"generic_relations_regress.tests.GenericRelationTests.test_textlink_filter", "generic_relations_regress.tests.GenericRelationTests.test_textlink_filter",
}, },
"Oracle doesn't support casting filters to NUMBER.": {
"lookup.tests.LookupQueryingTests.test_aggregate_combined_lookup",
},
} }
django_test_expected_failures = { django_test_expected_failures = {
# A bug in Django/oracledb with respect to string handling (#23843). # A bug in Django/oracledb with respect to string handling (#23843).
@ -166,3 +168,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
@cached_property @cached_property
def supports_primitives_in_json_field(self): def supports_primitives_in_json_field(self):
return self.connection.oracle_version >= (21,) return self.connection.oracle_version >= (21,)
@cached_property
def supports_boolean_expr_in_select_clause(self):
return self.connection.oracle_version >= (23,)

View File

@ -21,6 +21,7 @@ class InsertVar:
"PositiveBigIntegerField": int, "PositiveBigIntegerField": int,
"PositiveSmallIntegerField": int, "PositiveSmallIntegerField": int,
"PositiveIntegerField": int, "PositiveIntegerField": int,
"BooleanField": int,
"FloatField": Database.NATIVE_FLOAT, "FloatField": Database.NATIVE_FLOAT,
"DateTimeField": Database.TIMESTAMP, "DateTimeField": Database.TIMESTAMP,
"DateField": Database.Date, "DateField": Database.Date,

View File

@ -1702,10 +1702,13 @@ class OrderBy(Expression):
return (template % placeholders).rstrip(), params return (template % placeholders).rstrip(), params
def as_oracle(self, compiler, connection): def as_oracle(self, compiler, connection):
# Oracle doesn't allow ORDER BY EXISTS() or filters unless it's wrapped # Oracle < 23c doesn't allow ORDER BY EXISTS() or filters unless it's
# in a CASE WHEN. # wrapped in a CASE WHEN.
if connection.ops.conditional_expression_supported_in_where_clause( if (
not connection.features.supports_boolean_expr_in_select_clause
and connection.ops.conditional_expression_supported_in_where_clause(
self.expression self.expression
)
): ):
copy = self.copy() copy = self.copy()
copy.expression = Case( copy.expression = Case(

View File

@ -58,7 +58,13 @@ class GeneratedField(Field):
resolved_expression = self.expression.resolve_expression( resolved_expression = self.expression.resolve_expression(
self._query, allow_joins=False self._query, allow_joins=False
) )
return compiler.compile(resolved_expression) sql, params = compiler.compile(resolved_expression)
if (
getattr(self.expression, "conditional", False)
and not connection.features.supports_boolean_expr_in_select_clause
):
sql = f"CASE WHEN {sql} THEN 1 ELSE 0 END"
return sql, params
def check(self, **kwargs): def check(self, **kwargs):
databases = kwargs.get("databases") or [] databases = kwargs.get("databases") or []

View File

@ -20,3 +20,7 @@ Bugfixes
* Fixed a regression in Django 5.0 that caused a crash of ``Model.save()`` for * Fixed a regression in Django 5.0 that caused a crash of ``Model.save()`` for
models with both ``GeneratedField`` and ``ForeignKey`` fields models with both ``GeneratedField`` and ``ForeignKey`` fields
(:ticket:`35019`). (:ticket:`35019`).
* Fixed a bug in Django 5.0 that caused a migration crash on Oracle < 23c when
adding a ``GeneratedField`` with ``output_field=BooleanField``
(:ticket:`35018`).

View File

@ -1522,7 +1522,6 @@ class LookupQueryingTests(TestCase):
qs = Season.objects.order_by(LessThan(F("year"), 1910), F("year")) qs = Season.objects.order_by(LessThan(F("year"), 1910), F("year"))
self.assertSequenceEqual(qs, [self.s1, self.s3, self.s2]) self.assertSequenceEqual(qs, [self.s1, self.s3, self.s2])
@skipUnlessDBFeature("supports_boolean_expr_in_select_clause")
def test_aggregate_combined_lookup(self): def test_aggregate_combined_lookup(self):
expression = Cast(GreaterThan(F("year"), 1900), models.IntegerField()) expression = Cast(GreaterThan(F("year"), 1900), models.IntegerField())
qs = Season.objects.aggregate(modern=models.Sum(expression)) qs = Season.objects.aggregate(modern=models.Sum(expression))

View File

@ -852,6 +852,27 @@ class SchemaTests(TransactionTestCase):
False, False,
) )
@isolate_apps("schema")
@skipUnlessDBFeature("supports_virtual_generated_columns")
def test_add_generated_boolean_field(self):
class GeneratedBooleanFieldModel(Model):
value = IntegerField(null=True)
has_value = GeneratedField(
expression=Q(value__isnull=False),
output_field=BooleanField(),
db_persist=False,
)
class Meta:
app_label = "schema"
with connection.schema_editor() as editor:
editor.create_model(GeneratedBooleanFieldModel)
obj = GeneratedBooleanFieldModel.objects.create()
self.assertIs(obj.has_value, False)
obj = GeneratedBooleanFieldModel.objects.create(value=1)
self.assertIs(obj.has_value, True)
@isolate_apps("schema") @isolate_apps("schema")
@skipUnlessDBFeature("supports_stored_generated_columns") @skipUnlessDBFeature("supports_stored_generated_columns")
def test_add_generated_field(self): def test_add_generated_field(self):