Skip to content

Commit ddd49d0

Browse files
authored
1399 load_json and array values with prefetch (#1408)
* add test * wip * break up join tests * fix bug * add test assertions * simplify * add comment
1 parent 4204f20 commit ddd49d0

4 files changed

Lines changed: 157 additions & 93 deletions

File tree

piccolo/query/base.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -105,28 +105,27 @@ async def _process_results(self, results) -> QueryResponseType:
105105

106106
raw = await self.response_handler(raw)
107107

108-
if output:
109-
if output._output.as_objects:
110-
if output._output.nested:
111-
return cast(
112-
QueryResponseType,
113-
[
114-
make_nested_object(
115-
row,
116-
self.table,
117-
load_json=output._output.load_json,
118-
)
119-
for row in raw
120-
],
121-
)
122-
else:
123-
return cast(
124-
QueryResponseType,
125-
[
126-
self.table(**columns, _exists_in_db=True)
127-
for columns in raw
128-
],
129-
)
108+
if output and output._output.as_objects:
109+
if output._output.nested:
110+
return cast(
111+
QueryResponseType,
112+
[
113+
make_nested_object(
114+
row,
115+
self.table,
116+
load_json=output._output.load_json,
117+
)
118+
for row in raw
119+
],
120+
)
121+
else:
122+
return cast(
123+
QueryResponseType,
124+
[
125+
self.table(**columns, _exists_in_db=True)
126+
for columns in raw
127+
],
128+
)
130129

131130
return cast(QueryResponseType, raw)
132131

piccolo/utils/encoding.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class JSONDict(dict):
6363
...
6464

6565

66-
def load_json(data: str) -> Any:
66+
def load_json(data: str | bytes | bytearray) -> Any:
6767
response = (
6868
orjson.loads(data) if ORJSON else json.loads(data) # type: ignore
6969
)

piccolo/utils/objects.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,15 @@ def make_nested_object(
6969
else:
7070
# The value doesn't belong to a foreign key, so just append it.
7171
table_params[key] = value
72-
elif load_json and key in json_column_names and value is not None:
72+
elif (
73+
load_json
74+
and key in json_column_names
75+
# The top level might already have the JSON loaded, so check for
76+
# a string, rather than it being non-null.
77+
# https://github.com/piccolo-orm/piccolo/issues/1399
78+
# Ideally we want a cleaner solution than this.
79+
and isinstance(value, (str, bytes, bytearray))
80+
):
7381
table_params[key] = encoding.load_json(value)
7482
else:
7583
table_params[key] = value

tests/table/test_join.py

Lines changed: 126 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import decimal
2+
from unittest import TestCase
23

3-
from piccolo.testing.test_case import TableTest
4+
from piccolo.columns.column_types import JSON, ForeignKey, Serial, Varchar
5+
from piccolo.table import Table
6+
from piccolo.testing.test_case import AsyncTableTest, TableTest
47
from tests.base import engine_is
58
from tests.example_apps.music.tables import (
69
Band,
@@ -18,6 +21,9 @@
1821

1922
class TestCreateJoin:
2023
def test_create_join(self):
24+
"""
25+
A simple test to make sure tables with foreign keys can be created.
26+
"""
2127
for table in TABLES:
2228
table.create_table().run_sync()
2329

@@ -74,9 +80,10 @@ def setUp(self):
7480
)
7581
instrument.save().run_sync()
7682

77-
###########################################################################
7883

79-
def test_join(self):
84+
class TestSelectJoin(TestJoin):
85+
86+
def test_select_join(self):
8087
select_query = Concert.select(
8188
Concert.band_1.name,
8289
Concert.band_2.name,
@@ -211,25 +218,6 @@ def test_select_all_columns_deep(self):
211218
},
212219
)
213220

214-
def test_proxy_columns(self):
215-
"""
216-
Make sure that ``proxy_columns`` are set correctly.
217-
218-
There used to be a bug which meant queries got slower over time:
219-
220-
https://github.com/piccolo-orm/piccolo/issues/691
221-
222-
"""
223-
# We call it multiple times to make sure it doesn't change with time.
224-
for _ in range(2):
225-
self.assertEqual(
226-
len(Concert.band_1._.manager._foreign_key_meta.proxy_columns),
227-
2,
228-
)
229-
self.assertEqual(
230-
len(Concert.band_1._foreign_key_meta.proxy_columns), 4
231-
)
232-
233221
def test_select_all_columns_root(self):
234222
"""
235223
Make sure that using ``all_columns`` at the root doesn't interfere
@@ -343,7 +331,29 @@ def test_select_all_columns_exclude(self):
343331
},
344332
)
345333

346-
###########################################################################
334+
335+
class TestProxyColumns(TestCase):
336+
def test_proxy_columns(self):
337+
"""
338+
Make sure that ``proxy_columns`` are set correctly.
339+
340+
There used to be a bug which meant queries got slower over time:
341+
342+
https://github.com/piccolo-orm/piccolo/issues/691
343+
344+
"""
345+
# We call it multiple times to make sure it doesn't change with time.
346+
for _ in range(2):
347+
self.assertEqual(
348+
len(Concert.band_1._.manager._foreign_key_meta.proxy_columns),
349+
2,
350+
)
351+
self.assertEqual(
352+
len(Concert.band_1._foreign_key_meta.proxy_columns), 4
353+
)
354+
355+
356+
class TestObjectsJoin(TestJoin):
347357

348358
def test_objects_nested(self):
349359
"""
@@ -413,52 +423,6 @@ def test_objects__all_related__deep(self):
413423
self.assertIsInstance(ticket.concert.band_1.manager, Manager)
414424
self.assertIsInstance(ticket.concert.band_2.manager, Manager)
415425

416-
def test_objects_nested_with_load_json(self):
417-
"""
418-
Make sure that nested objects works alongside ``load_json`` (i.e. the
419-
JSON on nested objects gets loaded).
420-
421-
https://github.com/piccolo-orm/piccolo/issues/1383
422-
423-
"""
424-
instrument = (
425-
Instrument.objects(Instrument.recording_studio)
426-
.output(load_json=True)
427-
.first()
428-
.run_sync()
429-
)
430-
assert instrument is not None
431-
self.assertDictEqual(
432-
instrument.recording_studio.facilities,
433-
{"restaurant": True},
434-
)
435-
436-
def test_objects_nested_with_load_json_null(self):
437-
"""
438-
Make sure that nested objects works alongside ``load_json``, when
439-
the nested object has a null value for a JSON column.
440-
441-
https://github.com/piccolo-orm/piccolo/issues/1391
442-
443-
"""
444-
RecordingStudio.update(
445-
{
446-
RecordingStudio.facilities: None,
447-
},
448-
force=True,
449-
).run_sync()
450-
451-
instrument = (
452-
Instrument.objects(Instrument.recording_studio)
453-
.output(load_json=True)
454-
.first()
455-
.run_sync()
456-
)
457-
assert instrument is not None
458-
self.assertIsNone(
459-
instrument.recording_studio.facilities,
460-
)
461-
462426
def test_objects_prefetch_clause(self):
463427
"""
464428
Make sure that ``prefetch`` clause works correctly.
@@ -562,3 +526,96 @@ def test_objects_prefetch_db_column_name(self):
562526
signing = Signing.objects().prefetch(Signing.with_).first().run_sync()
563527
assert signing is not None
564528
self.assertIsInstance(signing.with_, Band)
529+
530+
531+
class TestObjectsNestedLoadJSON(TestJoin):
532+
def test_objects_nested_with_load_json(self):
533+
"""
534+
Make sure that nested objects works alongside ``load_json`` (i.e. the
535+
JSON on nested objects gets loaded).
536+
537+
https://github.com/piccolo-orm/piccolo/issues/1383
538+
539+
"""
540+
instrument = (
541+
Instrument.objects(Instrument.recording_studio)
542+
.output(load_json=True)
543+
.first()
544+
.run_sync()
545+
)
546+
assert instrument is not None
547+
self.assertDictEqual(
548+
instrument.recording_studio.facilities,
549+
{"restaurant": True},
550+
)
551+
552+
def test_objects_nested_with_load_json_null(self):
553+
"""
554+
Make sure that nested objects works alongside ``load_json``, when
555+
the nested object has a null value for a JSON column.
556+
557+
https://github.com/piccolo-orm/piccolo/issues/1391
558+
559+
"""
560+
RecordingStudio.update(
561+
{
562+
RecordingStudio.facilities: None,
563+
},
564+
force=True,
565+
).run_sync()
566+
567+
instrument = (
568+
Instrument.objects(Instrument.recording_studio)
569+
.output(load_json=True)
570+
.first()
571+
.run_sync()
572+
)
573+
assert instrument is not None
574+
self.assertIsNone(
575+
instrument.recording_studio.facilities,
576+
)
577+
578+
579+
class TableA(Table):
580+
id = Serial(primary_key=True)
581+
name = Varchar()
582+
583+
584+
class TableB(Table):
585+
data = JSON()
586+
table_a = ForeignKey(TableA, null=False)
587+
588+
589+
class TestObjectsNestedLoadJSONArray(AsyncTableTest):
590+
tables = [TableA, TableB]
591+
592+
async def test_objects_nested_with_load_json_array(self):
593+
"""
594+
Make sure that nested objects works alongside ``load_json``, when
595+
the parent object has a JSON column, and it contains an array value.
596+
597+
https://github.com/piccolo-orm/piccolo/issues/1399#issuecomment-4864444701
598+
599+
The problem was because we already loaded the JSON string on the top
600+
level row, and then were trying to load it again in
601+
`make_nested_object`.
602+
603+
""" # noqa: E501
604+
table_a = TableA()
605+
await table_a.save()
606+
607+
data = ["a", "b", "c"]
608+
table_b = TableB(data=data, table_a=table_a.id)
609+
await table_b.save()
610+
611+
# Without prefetch:
612+
row = await TableB.objects().output(load_json=True).first()
613+
assert row
614+
assert row.data == data
615+
616+
# With prefetch:
617+
row = (
618+
await TableB.objects(TableB.table_a).output(load_json=True).first()
619+
)
620+
assert row
621+
assert row.data == data

0 commit comments

Comments
 (0)