#37376 closed Bug (fixed)
UUID4() function on Oracle persists data in uppercase hex but queries in lowercase hex
| Reported by: | Jacob Walls | Owned by: | Jacob Walls |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 6.1 |
| Severity: | Release blocker | Keywords: | UUID |
| Cc: | Lily, Mariusz Felisiak, Loïc LEFEVRE | Triage Stage: | Ready for checkin |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
field_defaults.tests.DefaultTests.test_foreign_key_to_parent_with_expression_pk(), fails on Oracle 26ai (i.e. 23.26.2.0):
django.db.utils.IntegrityError: ORA-02291: integrity constraint (DJANGO_TESTS_DEFAULT_22.FIELD_DEF_PARENT_ID_42B58311_F) violated - parent key not found Help: https://docs.oracle.com/error-help/db/ora-02291/
The queries in this test are:
1. INSERT INTO "FIELD_DEFAULTS_DBDEFAULTSF0214" ("UUID") VALUES (UUID()) RETURNING "FIELD_DEFAULTS_DBDEFAULTSF0214"."UUID" INTO <django.db.backends.oracle.utils.BoundVar object at 0xffff7d1da360> 2. INSERT INTO "FIELD_DEFAULTS_DBDEFAULTSF22F2" ("PARENT_ID") VALUES (b41dd9331e764f988fc47e139ef0251d) RETURNING "FIELD_DEFAULTS_DBDEFAULTSF22F2"."ID" INTO <django.db.backends.oracle.utils.BoundVar object at 0xffff7d1da360>
The UUID value returned by query 1 differs in case from the value bound in query 2. Check by skipping Django's deserialization and reading it from a raw cursor:
def test_foreign_key_to_parent_with_expression_pk(self): parent = DBDefaultsFunctionPK(pk=UUID4()) obj = DBDefaultsFunctionFK(parent=parent) parent.save() with connection.cursor() as cursor: cursor.execute('SELECT "UUID" FROM "FIELD_DEFAULTS_DBDEFAULTSF0214"') print(cursor.fetchall()) obj.save() self.assertEqual(obj.parent_id, parent.pk)
[('B41DD9331E764F988FC47E139EF0251D',)]
The Oracle docs show the uppercase form being persisted. My understanding is that the FK fails integrity when a lowercase value is used in the second query, via Django/Python-side normalization, and fails a case-sensitive check on the varchar column in the db.
Bug in accceec9493d08e19d59fa1a59f69c0fdf23bb13.
The test passes after adjusting UUID4.as_oracle() to lowercase like this. (RAWTOHEX makes explicit the implicit conversion done today when inserting raw UUID into varchar columns.)
-
django/db/models/functions/uuid.py
diff --git a/django/db/models/functions/uuid.py b/django/db/models/functions/uuid.py index 7059798ff3..17754a48c1 100644
a b class UUID4(Func): 32 32 raise NotSupportedError( 33 33 "UUID4 requires Oracle version 23ai/26ai (23.9) or later." 34 34 ) 35 return self.as_sql(compiler, connection, function="UUID", **extra_context) 35 return self.as_sql( 36 compiler, 37 connection, 38 function="UUID", 39 template=f"LOWER(RAWTOHEX({self.template}))", 40 **extra_context, 41 ) 36 42 37 43 38 44 class UUID7(Func):
I presume if we go through with this we will need to write a release note advising Oracle users already on Django 6.1 to migrate their data created via UUID4 to lowercase (including foreign keys).
Change History (8)
comment:1 by , 3 weeks ago
| Triage Stage: | Unreviewed → Accepted |
|---|
follow-up: 4 comment:2 by , 3 weeks ago
comment:3 by , 3 weeks ago
| Cc: | added |
|---|
That's a much better approach. I'll explore the various get_db_prep_* methods to make sure I understand the differences.
Loïc, this is the remaining failure on Oracle 26ai on Test Pilot.
comment:4 by , 2 weeks ago
Loïc wrote in an email:
My understanding is that when we deal with UUIDs in Oracle, RAW(16) is usually the data type we target.
...
Now are you saying the Python driver at some point converts it into a varchar2 which makes it lowercase? (just to understand fully the issue).
Django prefers the VARCHAR2(32) format for UUIDField() (docs), which creates unfriendliness for RAW(16)'s created by Oracle either via UUID4() (recent) or SYS_GUID(), discussed in #24012. So it's not an issue in the driver; it's an issue with an asymmetry between Django's default=uuid.uuid4, which will persist lowercase, and Django's db_default=UUID4(), which will persist uppercase.
Replying to Mariusz Felisiak:
In
UUIDField.get_db_prep_value()we callto_python()which normalizes values usinguuid.UUID(). We could follow you suggestion or returnvalue.hex.upper()for Oracle inUUIDField.get_db_prep_value(). The second should be "more" backward compatible.
My concern about returning value.hex.upper() is that it will break projects using Python-generated values, i.e. default=uuid.uuid4, which persist lowercase.
I feel like the more compatible option might be to lowercase inside UUID4() like originally suggested. How does that sound?
comment:6 by , 2 weeks ago
| Triage Stage: | Accepted → Ready for checkin |
|---|
In
UUIDField.get_db_prep_value()we callto_python()which normalizes values usinguuid.UUID(). We could follow you suggestion or returnvalue.hex.upper()for Oracle inUUIDField.get_db_prep_value(). The second should be "more" backward compatible.