Skip to content

Commit b6d6fe6

Browse files
authored
add nested output option (piccolo-orm#204)
* add `nested` output option * fix typo in comment * fix a typo in docstring * updated docs
1 parent ceb12d9 commit b6d6fe6

File tree

7 files changed

+156
-18
lines changed

7 files changed

+156
-18
lines changed

docs/src/piccolo/query_clauses/output.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ If you're just querying a single column from a database table, you can use
3939
>>> Band.select(Band.id).output(as_list=True).run_sync()
4040
[1, 2]
4141
42+
nested
43+
~~~~~~
44+
45+
Output any data from related tables in nested dictionaries.
46+
47+
.. code-block:: python
48+
49+
>>> Band.select(Band.name, Band.manager.name).first().output(nested=True).run_sync()
50+
{'name': 'Pythonistas', 'manager': {'name': 'Guido'}}
51+
4252
-------------------------------------------------------------------------------
4353

4454
Select and Objects queries

docs/src/piccolo/query_types/select.rst

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ To get certain columns:
2020
>>> Band.select(Band.name).run_sync()
2121
[{'name': 'Rustaceans'}, {'name': 'Pythonistas'}]
2222
23-
Or making an alias to make it shorter:
23+
Or use an alias to make it shorter:
2424

2525
.. code-block:: python
2626
@@ -46,24 +46,41 @@ This is equivalent to ``SELECT name AS title FROM band`` in SQL.
4646
Joins
4747
-----
4848

49-
One of the most powerful things about select is it's support for joins.
49+
One of the most powerful things about ``select`` is it's support for joins.
5050

5151
.. code-block:: python
5252
53-
>>> b = Band
54-
>>> b.select(b.name, b.manager.name).run_sync()
53+
>>> Band.select(Band.name, Band.manager.name).run_sync()
5554
[{'name': 'Pythonistas', 'manager.name': 'Guido'}, {'name': 'Rustaceans', 'manager.name': 'Graydon'}]
5655
5756
5857
The joins can go several layers deep.
5958

6059
.. code-block:: python
6160
62-
c = Concert
63-
c.select(
64-
c.id,
65-
c.band_1.manager.name
66-
).run_sync()
61+
>>> Concert.select(Concert.id, Concert.band_1.manager.name).run_sync()
62+
[{'id': 1, 'band_1.manager.name': 'Guido'}]
63+
64+
If you want all of the columns from a related table, there's a useful shortcut
65+
which saves you from typing them all out:
66+
67+
.. code-block:: python
68+
69+
>>> Band.select(Band.name, *Band.manager.all_columns()).run_sync()
70+
[
71+
{'name': 'Pythonistas', 'manager.id': 1, 'manager.name': 'Guido'},
72+
{'name': 'Rustaceans', 'manager.id': 2, 'manager.name': 'Graydon'}
73+
]
74+
75+
You can also get the response as nested dictionaries, which can be very useful:
76+
77+
.. code-block:: python
78+
79+
>>> Band.select(Band.name, *Band.manager.all_columns()).output(nested=True).run_sync()
80+
[
81+
{'name': 'Pythonistas', 'manager': {'id': 1, 'name': 'Guido'}},
82+
{'name': 'Rustaceans', 'manager': {'id': 2, 'manager.name': 'Graydon'}}
83+
]
6784
6885
String syntax
6986
-------------
@@ -183,29 +200,29 @@ By default all columns are returned from the queried table.
183200

184201
.. code-block:: python
185202
186-
b = Band
187203
# Equivalent to SELECT * from band
188-
b.select().run_sync()
204+
Band.select().run_sync()
189205
190206
To restrict the returned columns, either pass in the columns into the
191207
``select`` method, or use the ``columns`` method.
192208

193209
.. code-block:: python
194210
195-
b = Band
196211
# Equivalent to SELECT name from band
197-
b.select().columns(b.name).run_sync()
212+
Band.select(Band.name).run_sync()
213+
214+
# Or alternatively:
215+
Band.select().columns(Band.name).run_sync()
198216
199217
The ``columns`` method is additive, meaning you can chain it to add additional
200218
columns.
201219

202220
.. code-block:: python
203221
204-
b = Band
205-
b.select().columns(b.name).columns(b.manager).run_sync()
222+
Band.select().columns(Band.name).columns(Band.manager).run_sync()
206223
207224
# Or just define it one go:
208-
b.select().columns(b.name, b.manager).run_sync()
225+
Band.select().columns(Band.name, Band.manager).run_sync()
209226
210227
211228
first

piccolo/query/methods/select.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
WhereDelegate,
2020
)
2121
from piccolo.querystring import QueryString
22+
from piccolo.utils.dictionary import make_nested
2223

2324
if t.TYPE_CHECKING: # pragma: no cover
2425
from piccolo.custom_types import Combinable
@@ -203,13 +204,23 @@ def offset(self, number: int) -> Select:
203204
return self
204205

205206
async def response_handler(self, response):
207+
# If no columns were specified, it's a select *, so we know that
208+
# no columns were selected from related tables.
209+
was_select_star = len(self.columns_delegate.selected_columns) == 0
210+
206211
if self.limit_delegate._first:
207212
if len(response) == 0:
208213
return None
214+
215+
if self.output_delegate._output.nested and not was_select_star:
216+
return make_nested(response[0])
209217
else:
210218
return response[0]
211219
else:
212-
return response
220+
if self.output_delegate._output.nested and not was_select_star:
221+
return [make_nested(i) for i in response]
222+
else:
223+
return response
213224

214225
def order_by(self, *columns: Column, ascending=True) -> Select:
215226
_columns: t.List[Column] = [
@@ -225,9 +236,13 @@ def output(
225236
as_list: bool = False,
226237
as_json: bool = False,
227238
load_json: bool = False,
239+
nested: bool = False,
228240
) -> Select:
229241
self.output_delegate.output(
230-
as_list=as_list, as_json=as_json, load_json=load_json
242+
as_list=as_list,
243+
as_json=as_json,
244+
load_json=load_json,
245+
nested=nested,
231246
)
232247
return self
233248

piccolo/query/mixins.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,15 @@ class Output:
7878
as_list: bool = False
7979
as_objects: bool = False
8080
load_json: bool = False
81+
nested: bool = False
8182

8283
def copy(self) -> Output:
8384
return self.__class__(
8485
as_json=self.as_json,
8586
as_list=self.as_list,
8687
as_objects=self.as_objects,
8788
load_json=self.load_json,
89+
nested=self.nested,
8890
)
8991

9092

@@ -195,6 +197,7 @@ def output(
195197
as_list: t.Optional[bool] = None,
196198
as_json: t.Optional[bool] = None,
197199
load_json: t.Optional[bool] = None,
200+
nested: t.Optional[bool] = None,
198201
):
199202
"""
200203
:param as_list:
@@ -216,6 +219,9 @@ def output(
216219
if load_json is not None:
217220
self._output.load_json = bool(load_json)
218221

222+
if nested is not None:
223+
self._output.nested = bool(nested)
224+
219225
def copy(self) -> OutputDelegate:
220226
_output = self._output.copy() if self._output is not None else None
221227
return self.__class__(_output=_output)

piccolo/utils/dictionary.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
import typing as t
4+
5+
6+
def make_nested(dictionary: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
7+
"""
8+
Rows are returned from the database as a flat dictionary, with keys such
9+
as ``'manager.name'`` if the column belongs to a related table.
10+
11+
This function puts any values from a related table into a sub dictionary.
12+
13+
.. code-block::
14+
15+
response = Band.select(Band.name, Band.manager.name).run_sync()
16+
>>> print(response)
17+
[{'name': 'Pythonistas', 'band.name': 'Guido'}]
18+
19+
>>> make_nested(response[0])
20+
{'name': 'Pythonistas', 'band': {'name': 'Guido'}}
21+
22+
"""
23+
output: t.Dict[str, t.Any] = {}
24+
25+
for key, value in dictionary.items():
26+
path = key.split(".")
27+
if len(path) == 1:
28+
output[path[0]] = value
29+
else:
30+
dictionary = output.setdefault(path[0], {})
31+
for path_element in path[1:-1]:
32+
dictionary = dictionary.setdefault(path_element, {})
33+
dictionary[path[-1]] = value
34+
35+
return output

tests/table/test_output.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,30 @@ def test_objects(self):
5959

6060
self.assertEqual(results[0].facilities, json)
6161
self.assertEqual(results[0].facilities_b, json)
62+
63+
64+
class TestOutputNested(DBTestCase):
65+
def test_output_nested(self):
66+
self.insert_row()
67+
68+
response = (
69+
Band.select(Band.name, Band.manager.name)
70+
.output(nested=True)
71+
.run_sync()
72+
)
73+
self.assertEqual(
74+
response, [{"name": "Pythonistas", "manager": {"name": "Guido"}}]
75+
)
76+
77+
def test_output_nested_with_first(self):
78+
self.insert_row()
79+
80+
response = (
81+
Band.select(Band.name, Band.manager.name)
82+
.first()
83+
.output(nested=True)
84+
.run_sync()
85+
)
86+
self.assertEqual(
87+
response, {"name": "Pythonistas", "manager": {"name": "Guido"}}
88+
)

tests/utils/test_dictionary.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from unittest import TestCase
2+
3+
from piccolo.utils.dictionary import make_nested
4+
5+
6+
class TestMakeNested(TestCase):
7+
def test_nesting(self):
8+
response = make_nested(
9+
{
10+
"id": 1,
11+
"name": "Pythonistas",
12+
"manager.id": 1,
13+
"manager.name": "Guido",
14+
"manager.car.colour": "green",
15+
}
16+
)
17+
self.assertEqual(
18+
response,
19+
{
20+
"id": 1,
21+
"name": "Pythonistas",
22+
"manager": {
23+
"id": 1,
24+
"name": "Guido",
25+
"car": {"colour": "green"},
26+
},
27+
},
28+
)

0 commit comments

Comments
 (0)