summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2025-12-13 16:38:04 +0100
committerGitHub <noreply@github.com>2025-12-13 16:38:04 +0100
commit0174a85770356fd12e4c8daa42a4f1c752ae00e6 (patch)
tree716dee5c8e9e604b5f3889080ab66e860efde23c
parente95468ed97b1c250939da316218c39dad577ba32 (diff)
Fixed #36765 -- Added support for stored GeneratedFields on Oracle 23ai/26ai (23.7+).
Thanks Jacob Walls for the review.
-rw-r--r--django/db/backends/base/features.py2
-rw-r--r--django/db/backends/base/schema.py15
-rw-r--r--django/db/backends/oracle/features.py11
-rw-r--r--django/db/backends/oracle/schema.py3
-rw-r--r--docs/ref/models/fields.txt9
-rw-r--r--docs/releases/6.1.txt4
-rw-r--r--tests/migrations/test_operations.py19
-rw-r--r--tests/model_fields/models.py6
-rw-r--r--tests/model_fields/test_generatedfield.py2
-rw-r--r--tests/schema/tests.py4
10 files changed, 61 insertions, 14 deletions
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py
index a28d622bac..2031beaf8a 100644
--- a/django/db/backends/base/features.py
+++ b/django/db/backends/base/features.py
@@ -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
diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py
index 1f27d6a0d4..4a59a6fe67 100644
--- a/django/db/backends/base/schema.py
+++ b/django/db/backends/base/schema.py
@@ -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} "
diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py
index 78dc97a2ec..ea484e336e 100644
--- a/django/db/backends/oracle/features.py
+++ b/django/db/backends/oracle/features.py
@@ -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)
diff --git a/django/db/backends/oracle/schema.py b/django/db/backends/oracle/schema.py
index 13fa7220ce..2f79489631 100644
--- a/django/db/backends/oracle/schema.py
+++ b/django/db/backends/oracle/schema.py
@@ -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"
diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index 492335504c..a87d7261a7 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -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
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index 8ab9ef46a8..a392b3d755 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -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
~~~~~~~~~~
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index ec4b772c13..00c76538e0 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -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(
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index 816e5a16ba..800a9ff5aa 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -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,
)
diff --git a/tests/model_fields/test_generatedfield.py b/tests/model_fields/test_generatedfield.py
index f0ac6eecb5..161084ecef 100644
--- a/tests/model_fields/test_generatedfield.py
+++ b/tests/model_fields/test_generatedfield.py
@@ -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")
diff --git a/tests/schema/tests.py b/tests/schema/tests.py
index ab8b07e9d3..12c14563e0 100644
--- a/tests/schema/tests.py
+++ b/tests/schema/tests.py
@@ -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(),