mirror of
https://github.com/django/django.git
synced 2026-02-09 02:49:25 +08:00
Fixed #36765 -- Added support for stored GeneratedFields on Oracle 23ai/26ai (23.7+).
Thanks Jacob Walls for the review.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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} "
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user