1
0
mirror of https://github.com/django/django.git synced 2025-01-18 22:33:44 +00:00

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.
This commit is contained in:
Mariusz Felisiak 2023-12-12 05:39:11 +01:00 committed by GitHub
parent b287af5dc9
commit fcf95e5927
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 75 additions and 18 deletions

View File

@ -300,7 +300,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
@async_unsafe
def create_cursor(self, name=None):
return FormatStylePlaceholderCursor(self.connection)
return FormatStylePlaceholderCursor(self.connection, self)
def _commit(self):
if self.connection is not None:
@ -365,11 +365,15 @@ class OracleParam:
param = Oracle_datetime.from_datetime(param)
string_size = 0
# Oracle doesn't recognize True and False correctly.
if param is True:
param = 1
elif param is False:
param = 0
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:
param = 1
elif param is False:
param = 0
if hasattr(param, "bind_parameter"):
self.force_bytes = param.bind_parameter(cursor)
elif isinstance(param, (Database.Binary, datetime.timedelta)):
@ -389,6 +393,8 @@ class OracleParam:
self.input_size = Database.DB_TYPE_CLOB
elif isinstance(param, datetime.datetime):
self.input_size = Database.DB_TYPE_TIMESTAMP
elif has_boolean_data_type and isinstance(param, bool):
self.input_size = Database.DB_TYPE_BOOLEAN
else:
self.input_size = None
@ -426,9 +432,10 @@ class FormatStylePlaceholderCursor:
charset = "utf-8"
def __init__(self, connection):
def __init__(self, connection, database):
self.cursor = connection.cursor()
self.cursor.outputtypehandler = self._output_type_handler
self.database = database
@staticmethod
def _output_number_converter(value):
@ -528,14 +535,24 @@ class FormatStylePlaceholderCursor:
# values. It can be used only in single query execute() because
# executemany() shares the formatted query with each of the params
# 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']
# 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 = {
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)
else:
# Handle params as sequence

View File

@ -76,7 +76,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_slicing_ordering_in_compound = True
requires_compound_order_by_subquery = True
allows_multiple_constraints_on_same_fields = False
supports_boolean_expr_in_select_clause = False
supports_comparing_boolean_expr = False
supports_json_field_contains = False
supports_collation_on_textfield = False
@ -119,6 +118,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"Oracle doesn't support comparing NCLOB to NUMBER.": {
"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 = {
# A bug in Django/oracledb with respect to string handling (#23843).
@ -170,3 +172,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
@cached_property
def supports_frame_exclusion(self):
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,
"PositiveSmallIntegerField": int,
"PositiveIntegerField": int,
"BooleanField": int,
"FloatField": Database.DB_TYPE_BINARY_DOUBLE,
"DateTimeField": Database.DB_TYPE_TIMESTAMP,
"DateField": Database.Date,

View File

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

View File

@ -58,7 +58,13 @@ class GeneratedField(Field):
resolved_expression = self.expression.resolve_expression(
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):
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
models with both ``GeneratedField`` and ``ForeignKey`` fields
(: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"))
self.assertSequenceEqual(qs, [self.s1, self.s3, self.s2])
@skipUnlessDBFeature("supports_boolean_expr_in_select_clause")
def test_aggregate_combined_lookup(self):
expression = Cast(GreaterThan(F("year"), 1900), models.IntegerField())
qs = Season.objects.aggregate(modern=models.Sum(expression))

View File

@ -846,6 +846,27 @@ class SchemaTests(TransactionTestCase):
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")
@skipUnlessDBFeature("supports_stored_generated_columns")
def test_add_generated_field(self):