diff options
| author | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2025-12-13 16:38:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-13 16:38:04 +0100 |
| commit | 0174a85770356fd12e4c8daa42a4f1c752ae00e6 (patch) | |
| tree | 716dee5c8e9e604b5f3889080ab66e860efde23c | |
| parent | e95468ed97b1c250939da316218c39dad577ba32 (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.py | 2 | ||||
| -rw-r--r-- | django/db/backends/base/schema.py | 15 | ||||
| -rw-r--r-- | django/db/backends/oracle/features.py | 11 | ||||
| -rw-r--r-- | django/db/backends/oracle/schema.py | 3 | ||||
| -rw-r--r-- | docs/ref/models/fields.txt | 9 | ||||
| -rw-r--r-- | docs/releases/6.1.txt | 4 | ||||
| -rw-r--r-- | tests/migrations/test_operations.py | 19 | ||||
| -rw-r--r-- | tests/model_fields/models.py | 6 | ||||
| -rw-r--r-- | tests/model_fields/test_generatedfield.py | 2 | ||||
| -rw-r--r-- | tests/schema/tests.py | 4 |
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(), |
