mirror of
https://github.com/django/django.git
synced 2025-12-23 09:19:27 +00:00
Fixed #36765 -- Added support for stored GeneratedFields on Oracle 23ai/26ai (23.7+).
Some checks failed
Docs / spelling (push) Has been cancelled
Docs / blacken-docs (push) Has been cancelled
Docs / lint-docs (push) Has been cancelled
Linters / flake8 (push) Has been cancelled
Linters / isort (push) Has been cancelled
Linters / black (push) Has been cancelled
Linters / zizmor (push) Has been cancelled
Tests / Windows, SQLite, Python 3.14 (push) Has been cancelled
Tests / JavaScript tests (push) Has been cancelled
Some checks failed
Docs / spelling (push) Has been cancelled
Docs / blacken-docs (push) Has been cancelled
Docs / lint-docs (push) Has been cancelled
Linters / flake8 (push) Has been cancelled
Linters / isort (push) Has been cancelled
Linters / black (push) Has been cancelled
Linters / zizmor (push) Has been cancelled
Tests / Windows, SQLite, Python 3.14 (push) Has been cancelled
Tests / JavaScript tests (push) Has been cancelled
Thanks Jacob Walls for the review.
This commit is contained in:
parent
e95468ed97
commit
0174a85770
10 changed files with 61 additions and 14 deletions
|
|
@ -373,6 +373,8 @@ class BaseDatabaseFeatures:
|
||||||
supports_stored_generated_columns = False
|
supports_stored_generated_columns = False
|
||||||
# Does the backend support virtual generated columns?
|
# Does the backend support virtual generated columns?
|
||||||
supports_virtual_generated_columns = False
|
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?
|
# Does the backend support the logical XOR operator?
|
||||||
supports_logical_xor = False
|
supports_logical_xor = False
|
||||||
|
|
|
||||||
|
|
@ -452,10 +452,14 @@ class BaseDatabaseSchemaEditor:
|
||||||
params = []
|
params = []
|
||||||
return sql % default_sql, 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):
|
def _column_generated_sql(self, field):
|
||||||
"""Return the SQL to use in a GENERATED ALWAYS clause."""
|
"""Return the SQL to use in a GENERATED ALWAYS clause."""
|
||||||
expression_sql, params = field.generated_sql(self.connection)
|
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:
|
if self.connection.features.requires_literal_defaults:
|
||||||
expression_sql = expression_sql % tuple(self.quote_value(p) for p in params)
|
expression_sql = expression_sql % tuple(self.quote_value(p) for p in params)
|
||||||
params = ()
|
params = ()
|
||||||
|
|
@ -906,6 +910,15 @@ class BaseDatabaseSchemaEditor:
|
||||||
else:
|
else:
|
||||||
new_field_sql = new_field.generated_sql(self.connection)
|
new_field_sql = new_field.generated_sql(self.connection)
|
||||||
modifying_generated_field = old_field_sql != new_field_sql
|
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:
|
if modifying_generated_field:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Modifying GeneratedFields is not supported - the field {new_field} "
|
f"Modifying GeneratedFields is not supported - the field {new_field} "
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
supports_ignore_conflicts = False
|
supports_ignore_conflicts = False
|
||||||
max_query_params = 2**16 - 1
|
max_query_params = 2**16 - 1
|
||||||
supports_partial_indexes = False
|
supports_partial_indexes = False
|
||||||
supports_stored_generated_columns = False
|
|
||||||
supports_virtual_generated_columns = True
|
supports_virtual_generated_columns = True
|
||||||
|
supports_alter_generated_column_data_type = False
|
||||||
can_rename_index = True
|
can_rename_index = True
|
||||||
supports_slicing_ordering_in_compound = True
|
supports_slicing_ordering_in_compound = True
|
||||||
requires_compound_order_by_subquery = True
|
requires_compound_order_by_subquery = True
|
||||||
|
|
@ -131,6 +131,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
"Oracle doesn't support casting filters to NUMBER.": {
|
"Oracle doesn't support casting filters to NUMBER.": {
|
||||||
"lookup.tests.LookupQueryingTests.test_aggregate_combined_lookup",
|
"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,):
|
if self.connection.oracle_version < (23,):
|
||||||
skips.update(
|
skips.update(
|
||||||
|
|
@ -228,3 +233,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
@cached_property
|
@cached_property
|
||||||
def supports_uuid4_function(self):
|
def supports_uuid4_function(self):
|
||||||
return self.connection.oracle_version >= (23, 9)
|
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:
|
if collation is None and old_collation is not None:
|
||||||
collation = self._get_default_collation(table_name)
|
collation = self._get_default_collation(table_name)
|
||||||
return super()._collate_sql(collation, old_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
|
real column. If ``False``, the column acts as a virtual column and does
|
||||||
not occupy database storage space.
|
not occupy database storage space.
|
||||||
|
|
||||||
PostgreSQL < 18 only supports persisted columns. Oracle only supports
|
PostgreSQL < 18 only supports persisted columns. Oracle < 23ai/26ai (23.7)
|
||||||
virtual columns.
|
only supports virtual columns.
|
||||||
|
|
||||||
|
.. versionchanged:: 6.1
|
||||||
|
|
||||||
|
Support for stored ``GeneratedField``\s was added on Oracle 23ai/26ai
|
||||||
|
(23.7+).
|
||||||
|
|
||||||
.. admonition:: Database limitations
|
.. admonition:: Database limitations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,10 @@ Models
|
||||||
* The new :class:`~django.db.models.functions.UUID4` and
|
* The new :class:`~django.db.models.functions.UUID4` and
|
||||||
:class:`~django.db.models.functions.UUID7` database functions were added.
|
: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
|
Pagination
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1491,7 +1491,7 @@ class OperationTests(OperationTestBase):
|
||||||
"name_and_id",
|
"name_and_id",
|
||||||
models.GeneratedField(
|
models.GeneratedField(
|
||||||
expression=Concat(("name"), ("rider_id")),
|
expression=Concat(("name"), ("rider_id")),
|
||||||
output_field=models.TextField(),
|
output_field=models.CharField(max_length=60),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -6363,6 +6363,15 @@ class OperationTests(OperationTestBase):
|
||||||
("test_igfc_2", generated_1, regular),
|
("test_igfc_2", generated_1, regular),
|
||||||
("test_igfc_3", generated_1, generated_2),
|
("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:
|
for app_label, add_field, alter_field in tests:
|
||||||
project_state = self.set_up_test_model(app_label)
|
project_state = self.set_up_test_model(app_label)
|
||||||
operations = [
|
operations = [
|
||||||
|
|
@ -6441,7 +6450,7 @@ class OperationTests(OperationTestBase):
|
||||||
"Pony",
|
"Pony",
|
||||||
"modified_pink",
|
"modified_pink",
|
||||||
models.GeneratedField(
|
models.GeneratedField(
|
||||||
expression=F("pink"),
|
expression=F("pink") + 2,
|
||||||
output_field=models.IntegerField(),
|
output_field=models.IntegerField(),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
),
|
),
|
||||||
|
|
@ -6450,7 +6459,7 @@ class OperationTests(OperationTestBase):
|
||||||
"Pony",
|
"Pony",
|
||||||
"modified_pink",
|
"modified_pink",
|
||||||
models.GeneratedField(
|
models.GeneratedField(
|
||||||
expression=F("pink"),
|
expression=F("pink") + 2,
|
||||||
output_field=models.IntegerField(),
|
output_field=models.IntegerField(),
|
||||||
db_persist=False,
|
db_persist=False,
|
||||||
),
|
),
|
||||||
|
|
@ -6489,7 +6498,9 @@ class OperationTests(OperationTestBase):
|
||||||
operation.database_backwards(app_label, editor, new_state, project_state)
|
operation.database_backwards(app_label, editor, new_state, project_state)
|
||||||
self.assertColumnNotExists(f"{app_label}_pony", "modified_pink")
|
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):
|
def test_generated_field_changes_output_field(self):
|
||||||
app_label = "test_gfcof"
|
app_label = "test_gfcof"
|
||||||
operation = migrations.AddField(
|
operation = migrations.AddField(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db import connection, models
|
from django.db import connection, models
|
||||||
from django.db.models import F, Value
|
from django.db.models import F, Value
|
||||||
from django.db.models.fields.files import ImageFieldFile
|
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.functional import SimpleLazyObject
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
@ -534,7 +534,7 @@ class UUIDGrandchild(UUIDChild):
|
||||||
class GeneratedModelFieldWithConverters(models.Model):
|
class GeneratedModelFieldWithConverters(models.Model):
|
||||||
field = models.UUIDField()
|
field = models.UUIDField()
|
||||||
field_copy = models.GeneratedField(
|
field_copy = models.GeneratedField(
|
||||||
expression=F("field"),
|
expression=Cast("field", models.UUIDField()),
|
||||||
output_field=models.UUIDField(),
|
output_field=models.UUIDField(),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
)
|
)
|
||||||
|
|
@ -561,7 +561,7 @@ class GeneratedModelNonAutoPk(models.Model):
|
||||||
id = models.IntegerField(primary_key=True)
|
id = models.IntegerField(primary_key=True)
|
||||||
a = models.IntegerField()
|
a = models.IntegerField()
|
||||||
b = models.GeneratedField(
|
b = models.GeneratedField(
|
||||||
expression=F("a"),
|
expression=F("a") + 1,
|
||||||
output_field=models.IntegerField(),
|
output_field=models.IntegerField(),
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -413,7 +413,7 @@ class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
|
||||||
obj = GeneratedModelNonAutoPk.objects.create(id=1, a=2)
|
obj = GeneratedModelNonAutoPk.objects.create(id=1, a=2)
|
||||||
self.assertEqual(obj.id, 1)
|
self.assertEqual(obj.id, 1)
|
||||||
self.assertEqual(obj.a, 2)
|
self.assertEqual(obj.a, 2)
|
||||||
self.assertEqual(obj.b, 2)
|
self.assertEqual(obj.b, 3)
|
||||||
|
|
||||||
|
|
||||||
@skipUnlessDBFeature("supports_virtual_generated_columns")
|
@skipUnlessDBFeature("supports_virtual_generated_columns")
|
||||||
|
|
|
||||||
|
|
@ -1029,7 +1029,7 @@ class SchemaTests(TransactionTestCase):
|
||||||
class GeneratedFieldIndexedModel(Model):
|
class GeneratedFieldIndexedModel(Model):
|
||||||
number = IntegerField(default=1)
|
number = IntegerField(default=1)
|
||||||
generated = GeneratedField(
|
generated = GeneratedField(
|
||||||
expression=F("number"),
|
expression=F("number") + 1,
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
)
|
)
|
||||||
|
|
@ -1042,7 +1042,7 @@ class SchemaTests(TransactionTestCase):
|
||||||
|
|
||||||
old_field = GeneratedFieldIndexedModel._meta.get_field("generated")
|
old_field = GeneratedFieldIndexedModel._meta.get_field("generated")
|
||||||
new_field = GeneratedField(
|
new_field = GeneratedField(
|
||||||
expression=F("number"),
|
expression=F("number") + 1,
|
||||||
db_persist=True,
|
db_persist=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue