Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/django-google-spanner/django_spanner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,27 @@ def get_prep_value(self, value):
return value


old_jsonfield_from_db_value = getattr(JSONField, "from_db_value", None)


def jsonfield_from_db_value(self, value, expression, connection):
if isinstance(value, JsonObject):
if value._is_null:
return None
if value._is_array:
return value._array_value
if value._is_scalar_value:
return value._simple_value
return dict(value)
if isinstance(value, (dict, list)):
return value
Comment on lines +99 to +109

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The JsonObject class is used in the isinstance check but is not imported in this file, which will raise a NameError at runtime when from_db_value is called. Import JsonObject from google.cloud.spanner_v1 inside the function to avoid circular dependencies and ensure runtime correctness.

Suggested change
def jsonfield_from_db_value(self, value, expression, connection):
if isinstance(value, (dict, list, JsonObject)):
return value
def jsonfield_from_db_value(self, value, expression, connection):
from google.cloud.spanner_v1 import JsonObject
if isinstance(value, (dict, list, JsonObject)):
return value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonObject is already imported at the module level in django_spanner/__init__.py (line 30). Also updated jsonfield_from_db_value to unwrap JsonObject null, array, and scalar values into their native Python equivalents.

if old_jsonfield_from_db_value:
return old_jsonfield_from_db_value(self, value, expression, connection)
return value


JSONField.get_prep_value = get_prep_value
JSONField.from_db_value = jsonfield_from_db_value

old_datetimewithnanoseconds_eq = getattr(DatetimeWithNanoseconds, "__eq__", None)

Expand Down
10 changes: 10 additions & 0 deletions packages/django-google-spanner/django_spanner/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
TimestampStr,
escape_name,
)
from google.cloud.spanner_v1 import JsonObject


class DatabaseOperations(BaseDatabaseOperations):
Expand Down Expand Up @@ -770,3 +771,12 @@ def savepoint_rollback_sql(self, sid):
Return the SQL for rolling back to a savepoint.
"""
return "SELECT 1"

def adapt_json_value(self, value, encoder):
if value is None:
return None
if isinstance(value, JsonObject):
if value._is_null or value._is_array or value._is_scalar_value:
return value
value = dict(value)
return JsonObject.from_str(super().adapt_json_value(value, encoder))
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,37 @@ def test_datetime_cast_sql(self):
"TIMESTAMP(FORMAT_TIMESTAMP('%Y-%m-%d %R:%E9S %Z', field, 'UTC'))",
)
self.assertIsNone(params)

def test_adapt_json_value(self):
import uuid

from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import JSONField
from google.cloud.spanner_v1 import JsonObject

self.assertIsNone(self.db_operations.adapt_json_value(None, None))
obj = JsonObject({"a": 1})
self.assertEqual(self.db_operations.adapt_json_value(obj, None), {"a": 1})
adapted_dict = self.db_operations.adapt_json_value({"a": 1}, None)
self.assertIsInstance(adapted_dict, JsonObject)
self.assertEqual(adapted_dict, {"a": 1})
adapted_list = self.db_operations.adapt_json_value([1, 2], None)
self.assertIsInstance(adapted_list, JsonObject)
self.assertEqual(adapted_list, [1, 2])
adapted_str = self.db_operations.adapt_json_value("str", None)
self.assertIsInstance(adapted_str, JsonObject)
self.assertEqual(adapted_str, "str")

u = uuid.UUID("12345678-1234-5678-1234-567812345678")
adapted_uuid = self.db_operations.adapt_json_value({"u": u}, DjangoJSONEncoder)
self.assertIsInstance(adapted_uuid, JsonObject)
self.assertEqual(adapted_uuid, {"u": "12345678-1234-5678-1234-567812345678"})

field = JSONField()
self.assertEqual(field.from_db_value(obj, None, None), {"a": 1})
self.assertEqual(field.from_db_value(JsonObject([1, 2]), None, None), [1, 2])
self.assertEqual(field.from_db_value(JsonObject("str"), None, None), "str")
self.assertIsNone(field.from_db_value(JsonObject(), None, None))
self.assertEqual(field.from_db_value({"a": 1}, None, None), {"a": 1})
self.assertEqual(field.from_db_value('{"a": 1}', None, None), {"a": 1})
self.assertIsNone(field.from_db_value(None, None, None))
Comment on lines +351 to +383

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the unit tests to verify that primitive JSON values are correctly wrapped in JsonObject and to cover custom encoder serialization (e.g., using uuid.UUID and DjangoJSONEncoder).

    def test_adapt_json_value(self):
        import uuid
        from django.core.serializers.json import DjangoJSONEncoder
        from django.db.models import JSONField
        from google.cloud.spanner_v1 import JsonObject

        self.assertIsNone(self.db_operations.adapt_json_value(None, None))
        obj = JsonObject({"a": 1})
        self.assertIs(self.db_operations.adapt_json_value(obj, None), obj)
        adapted_dict = self.db_operations.adapt_json_value({"a": 1}, None)
        self.assertIsInstance(adapted_dict, JsonObject)
        self.assertEqual(adapted_dict.value, {"a": 1})
        adapted_list = self.db_operations.adapt_json_value([1, 2], None)
        self.assertIsInstance(adapted_list, JsonObject)
        self.assertEqual(adapted_list.value, [1, 2])
        adapted_str = self.db_operations.adapt_json_value("str", None)
        self.assertIsInstance(adapted_str, JsonObject)
        self.assertEqual(adapted_str.value, "str")

        # Test custom encoder serialization (e.g., UUID)
        u = uuid.UUID("12345678-1234-5678-1234-567812345678")
        adapted_uuid = self.db_operations.adapt_json_value({"u": u}, DjangoJSONEncoder)
        self.assertIsInstance(adapted_uuid, JsonObject)
        self.assertEqual(adapted_uuid.value, {"u": "12345678-1234-5678-1234-567812345678"})

        field = JSONField()
        self.assertIs(field.from_db_value(obj, None, None), obj)
        self.assertEqual(field.from_db_value({"a": 1}, None, None), {"a": 1})
        self.assertEqual(field.from_db_value('{"a": 1}', None, None), {"a": 1})
        self.assertIsNone(field.from_db_value(None, None, None))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated test_adapt_json_value to cover primitive JSON values, custom encoder serialization (UUID with DjangoJSONEncoder), and JsonObject unwrapping in from_db_value.

Loading