Fixed #36765 -- Added support for stored GeneratedFields on Oracle 23ai/26ai (23.7+).

Thanks Jacob Walls for the review.
This commit is contained in:
Mariusz Felisiak
2025-12-13 16:38:04 +01:00
committed by GitHub
parent e95468ed97
commit 0174a85770
10 changed files with 61 additions and 14 deletions

View File

@@ -373,6 +373,8 @@ class BaseDatabaseFeatures:
supports_stored_generated_columns = False
# Does the backend support virtual generated columns?
supports_virtual_generated_columns = False
# Does the backend support altering data types of generated columns?
supports_alter_generated_column_data_type = True
# Does the backend support the logical XOR operator?
supports_logical_xor = False

View File

@@ -452,10 +452,14 @@ class BaseDatabaseSchemaEditor:
params = []
return sql % default_sql, params
def _column_generated_persistency_sql(self, field):
"""Return the SQL to define the persistency of generated fields."""
return "STORED" if field.db_persist else "VIRTUAL"
def _column_generated_sql(self, field):
"""Return the SQL to use in a GENERATED ALWAYS clause."""
expression_sql, params = field.generated_sql(self.connection)
persistency_sql = "STORED" if field.db_persist else "VIRTUAL"
persistency_sql = self._column_generated_persistency_sql(field)
if self.connection.features.requires_literal_defaults:
expression_sql = expression_sql % tuple(self.quote_value(p) for p in params)
params = ()
@@ -906,6 +910,15 @@ class BaseDatabaseSchemaEditor:
else:
new_field_sql = new_field.generated_sql(self.connection)
modifying_generated_field = old_field_sql != new_field_sql
db_features = self.connection.features
# Some databases (e.g. Oracle) don't allow altering a data type
# for generated columns.
if (
not modifying_generated_field
and old_type != new_type
and not db_features.supports_alter_generated_column_data_type
):
modifying_generated_field = True
if modifying_generated_field:
raise ValueError(
f"Modifying GeneratedFields is not supported - the field {new_field} "

View File

@@ -69,8 +69,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
supports_ignore_conflicts = False
max_query_params = 2**16 - 1
supports_partial_indexes = False
supports_stored_generated_columns = False
supports_virtual_generated_columns = True
supports_alter_generated_column_data_type = False
can_rename_index = True
supports_slicing_ordering_in_compound = True
requires_compound_order_by_subquery = True
@@ -131,6 +131,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"Oracle doesn't support casting filters to NUMBER.": {
"lookup.tests.LookupQueryingTests.test_aggregate_combined_lookup",
},
"Oracle doesn't support some data types (e.g. BOOLEAN, BLOB) in "
"GeneratedField expressions (ORA-54003).": {
"schema.tests.SchemaTests.test_add_generated_field_contains",
"schema.tests.SchemaTests.test_add_generated_field_with_kt_model",
},
}
if self.connection.oracle_version < (23,):
skips.update(
@@ -228,3 +233,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
@cached_property
def supports_uuid4_function(self):
return self.connection.oracle_version >= (23, 9)
@cached_property
def supports_stored_generated_columns(self):
return self.connection.oracle_version >= (23, 7)

View File

@@ -251,3 +251,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
if collation is None and old_collation is not None:
collation = self._get_default_collation(table_name)
return super()._collate_sql(collation, old_collation, table_name)
def _column_generated_persistency_sql(self, field):
return "MATERIALIZED" if field.db_persist else "VIRTUAL"

View File

@@ -1346,8 +1346,13 @@ materialized view.
real column. If ``False``, the column acts as a virtual column and does
not occupy database storage space.
PostgreSQL < 18 only supports persisted columns. Oracle only supports
virtual columns.
PostgreSQL < 18 only supports persisted columns. Oracle < 23ai/26ai (23.7)
only supports virtual columns.
.. versionchanged:: 6.1
Support for stored ``GeneratedField``\s was added on Oracle 23ai/26ai
(23.7+).
.. admonition:: Database limitations

View File

@@ -266,6 +266,10 @@ Models
* The new :class:`~django.db.models.functions.UUID4` and
:class:`~django.db.models.functions.UUID7` database functions were added.
* :class:`~django.db.models.GeneratedField` now supports stored columns
(:attr:`~django.db.models.GeneratedField.db_persist` set to ``True``) on
Oracle 23ai/26ai (23.7+).
Pagination
~~~~~~~~~~

View File

@@ -1491,7 +1491,7 @@ class OperationTests(OperationTestBase):
"name_and_id",
models.GeneratedField(
expression=Concat(("name"), ("rider_id")),
output_field=models.TextField(),
output_field=models.CharField(max_length=60),
db_persist=True,
),
),
@@ -6363,6 +6363,15 @@ class OperationTests(OperationTestBase):
("test_igfc_2", generated_1, regular),
("test_igfc_3", generated_1, generated_2),
]
if not connection.features.supports_alter_generated_column_data_type:
generated_3 = models.GeneratedField(
expression=F("pink") + F("pink"),
output_field=models.DecimalField(decimal_places=2, max_digits=16),
db_persist=db_persist,
)
tests.append(
("test_igfc_4", generated_1, generated_3),
)
for app_label, add_field, alter_field in tests:
project_state = self.set_up_test_model(app_label)
operations = [
@@ -6441,7 +6450,7 @@ class OperationTests(OperationTestBase):
"Pony",
"modified_pink",
models.GeneratedField(
expression=F("pink"),
expression=F("pink") + 2,
output_field=models.IntegerField(),
db_persist=True,
),
@@ -6450,7 +6459,7 @@ class OperationTests(OperationTestBase):
"Pony",
"modified_pink",
models.GeneratedField(
expression=F("pink"),
expression=F("pink") + 2,
output_field=models.IntegerField(),
db_persist=False,
),
@@ -6489,7 +6498,9 @@ class OperationTests(OperationTestBase):
operation.database_backwards(app_label, editor, new_state, project_state)
self.assertColumnNotExists(f"{app_label}_pony", "modified_pink")
@skipUnlessDBFeature("supports_stored_generated_columns")
@skipUnlessDBFeature(
"supports_stored_generated_columns", "supports_alter_generated_column_data_type"
)
def test_generated_field_changes_output_field(self):
app_label = "test_gfcof"
operation = migrations.AddField(

View File

@@ -9,7 +9,7 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.db import connection, models
from django.db.models import F, Value
from django.db.models.fields.files import ImageFieldFile
from django.db.models.functions import Lower
from django.db.models.functions import Cast, Lower
from django.utils.functional import SimpleLazyObject
from django.utils.translation import gettext_lazy as _
@@ -534,7 +534,7 @@ class UUIDGrandchild(UUIDChild):
class GeneratedModelFieldWithConverters(models.Model):
field = models.UUIDField()
field_copy = models.GeneratedField(
expression=F("field"),
expression=Cast("field", models.UUIDField()),
output_field=models.UUIDField(),
db_persist=True,
)
@@ -561,7 +561,7 @@ class GeneratedModelNonAutoPk(models.Model):
id = models.IntegerField(primary_key=True)
a = models.IntegerField()
b = models.GeneratedField(
expression=F("a"),
expression=F("a") + 1,
output_field=models.IntegerField(),
db_persist=True,
)

View File

@@ -413,7 +413,7 @@ class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
obj = GeneratedModelNonAutoPk.objects.create(id=1, a=2)
self.assertEqual(obj.id, 1)
self.assertEqual(obj.a, 2)
self.assertEqual(obj.b, 2)
self.assertEqual(obj.b, 3)
@skipUnlessDBFeature("supports_virtual_generated_columns")

View File

@@ -1029,7 +1029,7 @@ class SchemaTests(TransactionTestCase):
class GeneratedFieldIndexedModel(Model):
number = IntegerField(default=1)
generated = GeneratedField(
expression=F("number"),
expression=F("number") + 1,
db_persist=True,
output_field=IntegerField(),
)
@@ -1042,7 +1042,7 @@ class SchemaTests(TransactionTestCase):
old_field = GeneratedFieldIndexedModel._meta.get_field("generated")
new_field = GeneratedField(
expression=F("number"),
expression=F("number") + 1,
db_persist=True,
db_index=True,
output_field=IntegerField(),