diff --git a/piccolo/query/operators/json.py b/piccolo/query/operators/json.py index be7529135..c119b6965 100644 --- a/piccolo/query/operators/json.py +++ b/piccolo/query/operators/json.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union from piccolo.querystring import QueryString -from piccolo.utils.encoding import dump_json +from piccolo.utils.encoding import dump_json, load_json if TYPE_CHECKING: from piccolo.columns.column_types import JSON @@ -12,9 +12,17 @@ class JSONQueryString(QueryString): def clean_value(self, value: Any): - if not isinstance(value, (str, QueryString)): - value = dump_json(value) - return value + if isinstance(value, QueryString): + return value + + if isinstance(value, str): + try: + load_json(value) + except Exception: + value = dump_json(value) + return value + + return dump_json(value) def __eq__(self, value) -> QueryString: # type: ignore[override] value = self.clean_value(value) diff --git a/tests/query/operators/test_json.py b/tests/query/operators/test_json.py index d7840ef9b..c91dac3cc 100644 --- a/tests/query/operators/test_json.py +++ b/tests/query/operators/test_json.py @@ -1,6 +1,7 @@ from unittest import TestCase from piccolo.columns import JSONB +from piccolo.querystring import QueryString from piccolo.query.operators.json import GetChildElement, GetElementFromPath from piccolo.table import Table from tests.base import engines_skip @@ -50,3 +51,76 @@ def test_query(self): ) self.assertListEqual(query_args, [["a", "b"]]) + + +class TestJSONQueryString(TestCase): + + def test_clean_value(self): + element = GetChildElement(RecordingStudio.facilities, "name") + + # Plain strings should be encoded as JSON + self.assertEqual(element.clean_value("hello world"), '"hello world"') + self.assertEqual(element.clean_value("foo"), '"foo"') + + # Already valid JSON strings should be preserved + self.assertEqual( + element.clean_value('{"message": "hello world"}'), + '{"message": "hello world"}', + ) + self.assertEqual(element.clean_value('["a", "b"]'), '["a", "b"]') + self.assertEqual( + element.clean_value('"already quoted"'), + '"already quoted"', + ) + + # Other python types should be dumped to JSON + self.assertEqual( + element.clean_value({"message": "hello world"}), + '{"message": "hello world"}', + ) + self.assertEqual(element.clean_value(["a", "b"]), '["a", "b"]') + self.assertEqual(element.clean_value(123), "123") + self.assertEqual(element.clean_value(True), "true") + self.assertEqual(element.clean_value(None), "null") + + # QueryString should be preserved + qs = QueryString("SELECT 1") + self.assertEqual(element.clean_value(qs), qs) + + def test_eq_plain_string(self): + querystring = ( + GetChildElement(RecordingStudio.facilities, "name") + == "hello world" + ) + sql, query_args = querystring.compile_string(engine_type="postgres") + self.assertEqual( + sql, + '"recording_studio"."facilities" -> $1 = $2', + ) + self.assertListEqual(query_args, ["name", '"hello world"']) + + def test_ne_plain_string(self): + querystring = ( + GetChildElement(RecordingStudio.facilities, "name") + != "hello world" + ) + sql, query_args = querystring.compile_string(engine_type="postgres") + self.assertEqual( + sql, + '"recording_studio"."facilities" -> $1 != $2', + ) + self.assertListEqual(query_args, ["name", '"hello world"']) + + def test_eq_valid_json_string(self): + querystring = ( + GetChildElement(RecordingStudio.facilities, "info") + == '{"message": "hello world"}' + ) + sql, query_args = querystring.compile_string(engine_type="postgres") + self.assertEqual( + sql, + '"recording_studio"."facilities" -> $1 = $2', + ) + self.assertListEqual( + query_args, ["info", '{"message": "hello world"}'] + )