Skip to content

Commit e7c5d72

Browse files
authored
Merge pull request piccolo-orm#122 from piccolo-orm/json_improvements
Json improvements
2 parents a3e8be9 + 2c9d798 commit e7c5d72

File tree

25 files changed

+583
-91
lines changed

25 files changed

+583
-91
lines changed

docs/src/piccolo/query_clauses/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ by modifying the return values.
1414
./limit
1515
./offset
1616
./order_by
17+
./output
1718
./where
1819
./batch
1920
./freeze
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.. _output:
2+
3+
output
4+
======
5+
6+
You can use ``output`` clauses with the following queries:
7+
8+
* :ref:`Select`
9+
* :ref:`Objects`
10+
11+
-------------------------------------------------------------------------------
12+
13+
Select queries only
14+
-------------------
15+
16+
as_json
17+
~~~~~~~
18+
19+
To return the data as a JSON string:
20+
21+
.. code-block:: python
22+
23+
>>> Band.select().output(as_json=True).run_sync()
24+
'[{"name":"Pythonistas","manager":1,"popularity":1000,"id":1},{"name":"Rustaceans","manager":2,"popularity":500,"id":2}]'
25+
26+
Piccolo can use `orjson <https://github.com/ijl/orjson>`_ for JSON serialisation,
27+
which is blazing fast, and can handle most Python types, including dates,
28+
datetimes, and UUIDs. To install Piccolo with orjson support use
29+
``pip install piccolo[orjson]``.
30+
31+
as_list
32+
~~~~~~~
33+
34+
If you're just querying a single column from a database table, you can use
35+
``as_list`` to flatten the results into a single list.
36+
37+
.. code-block:: python
38+
39+
>>> Band.select(Band.id).output(as_list=True).run_sync()
40+
[1, 2]
41+
42+
-------------------------------------------------------------------------------
43+
44+
Select and Objects queries
45+
--------------------------
46+
47+
load_json
48+
~~~~~~~~~
49+
50+
If querying JSON or JSONB columns, you can tell Piccolo to deserialise the JSON
51+
values automatically.
52+
53+
.. code-block:: python
54+
55+
>>> RecordingStudio.select().output(load_json=True).run_sync()
56+
[{'id': 1, 'name': 'Abbey Road', 'facilities': {'restaurant': True, 'mixing_desk': True}}]
57+
58+
>>> studio = RecordingStudio.objects().first().output(load_json=True).run_sync()
59+
>>> studio.facilities
60+
{'restaurant': True, 'mixing_desk': True}

docs/src/piccolo/query_types/objects.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ order_by
117117

118118
See  :ref:`order_by`.
119119

120+
output
121+
~~~~~~
122+
123+
See  :ref:`output`.
124+
120125
where
121126
~~~~~
122127

docs/src/piccolo/query_types/select.rst

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -147,21 +147,7 @@ See  :ref:`order_by`.
147147
output
148148
~~~~~~
149149

150-
By default, the data is returned as a list of dictionaries (where each
151-
dictionary represents a row). This can be altered using the ``output`` method.
152-
153-
To return the data as a JSON string:
154-
155-
.. code-block:: python
156-
157-
>>> b = Band
158-
>>> b.select().output(as_json=True).run_sync()
159-
'[{"name":"Pythonistas","manager":1,"popularity":1000,"id":1},{"name":"Rustaceans","manager":2,"popularity":500,"id":2}]'
160-
161-
Piccolo can use `orjson <https://github.com/ijl/orjson>`_ for JSON serialisation,
162-
which is blazing fast, and can handle most Python types, including dates,
163-
datetimes, and UUIDs. To install Piccolo with orjson support use
164-
``pip install piccolo[orjson]``.
150+
See :ref:`output`.
165151

166152
where
167153
~~~~~

piccolo/apps/playground/commands/run.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ForeignKey,
1515
Integer,
1616
Interval,
17+
JSON,
1718
Numeric,
1819
Timestamp,
1920
UUID,
@@ -55,7 +56,12 @@ class DiscountCode(Table):
5556
active = Boolean(default=True, null=True)
5657

5758

58-
TABLES = (Manager, Band, Venue, Concert, Ticket, DiscountCode)
59+
class RecordingStudio(Table):
60+
name = Varchar(length=100)
61+
facilities = JSON()
62+
63+
64+
TABLES = (Manager, Band, Venue, Concert, Ticket, DiscountCode, RecordingStudio)
5965

6066

6167
def populate():
@@ -105,6 +111,11 @@ def populate():
105111
discount_code = DiscountCode(code=uuid.uuid4())
106112
discount_code.save().run_sync()
107113

114+
recording_studio = RecordingStudio(
115+
name="Abbey Road", facilities={"restaurant": True, "mixing_desk": True}
116+
)
117+
recording_studio.save().run_sync()
118+
108119

109120
def run(
110121
engine: str = "sqlite",

piccolo/apps/user/commands/change_permissions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async def change_permissions(
3434
)
3535
return
3636

37-
params: t.Dict[Column, bool] = {}
37+
params: t.Dict[t.Union[Column, str], bool] = {}
3838

3939
if admin is not None:
4040
params[BaseUser.admin] = admin

piccolo/columns/combination.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from __future__ import annotations
2-
from enum import Enum
32
import typing as t
43

54
from piccolo.columns.operators.comparison import ComparisonOperator
65
from piccolo.custom_types import Combinable, Iterable
76
from piccolo.querystring import QueryString
7+
from piccolo.utils.sql_values import convert_to_sql_value
88

99
if t.TYPE_CHECKING:
1010
from piccolo.columns.base import Column
@@ -121,20 +121,17 @@ def clean_value(self, value: t.Any) -> t.Any:
121121
# it should still work.
122122
Band.select().where(Band.manager == guido).run_sync()
123123
124-
"""
125-
from piccolo.table import Table
124+
Also, convert Enums to their underlying values, and serialise any JSON.
126125
127-
if isinstance(value, Table):
128-
return value.id
129-
else:
130-
return value
126+
"""
127+
return convert_to_sql_value(value=value, column=self.column)
131128

132129
@property
133130
def values_querystring(self) -> QueryString:
134-
if isinstance(self.values, Undefined):
135-
raise ValueError("values is undefined")
131+
values = self.values
136132

137-
values = [i.value if isinstance(i, Enum) else i for i in self.values]
133+
if isinstance(values, Undefined):
134+
raise ValueError("values is undefined")
138135

139136
template = ", ".join(["{}" for _ in values])
140137
return QueryString(template, *values)
@@ -143,12 +140,8 @@ def values_querystring(self) -> QueryString:
143140
def querystring(self) -> QueryString:
144141
args: t.List[t.Any] = []
145142
if self.value != UNDEFINED:
146-
value = (
147-
self.value.value
148-
if isinstance(self.value, Enum)
149-
else self.value
150-
)
151-
args.append(value)
143+
args.append(self.value)
144+
152145
if self.values != UNDEFINED:
153146
args.append(self.values_querystring)
154147

piccolo/columns/operators/comparison.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .base import Operator
1+
from piccolo.columns.operators.base import Operator
22

33

44
class ComparisonOperator(Operator):

piccolo/query/base.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
from time import time
44
import typing as t
55

6+
from piccolo.columns.column_types import JSON, JSONB
7+
from piccolo.query.mixins import ColumnsDelegate
68
from piccolo.querystring import QueryString
79
from piccolo.utils.sync import run_sync
8-
from piccolo.utils.encoding import dump_json
10+
from piccolo.utils.encoding import dump_json, load_json
911

1012
if t.TYPE_CHECKING: # pragma: no cover
1113
from piccolo.table import Table # noqa
14+
from piccolo.query.mixins import OutputDelegate
1215

1316

1417
class Timer:
@@ -51,9 +54,54 @@ async def _process_results(self, results):
5154
if hasattr(self, "run_callback"):
5255
self.run_callback(raw)
5356

54-
raw = await self.response_handler(raw)
57+
output: t.Optional[OutputDelegate] = getattr(
58+
self, "output_delegate", None
59+
)
60+
61+
#######################################################################
62+
63+
if output and output._output.load_json:
64+
columns_delegate: t.Optional[ColumnsDelegate] = getattr(
65+
self, "columns_delegate", None
66+
)
67+
68+
if columns_delegate is not None:
69+
json_columns = [
70+
i
71+
for i in columns_delegate.selected_columns
72+
if isinstance(i, (JSON, JSONB))
73+
]
74+
else:
75+
json_columns = self.table._meta.json_columns
76+
77+
json_column_names = []
78+
for column in json_columns:
79+
if column.alias is not None:
80+
json_column_names.append(column.alias)
81+
elif len(column._meta.call_chain) > 0:
82+
json_column_names.append(
83+
column.get_select_string(
84+
engine_type=column._meta.engine_type
85+
)
86+
)
87+
else:
88+
json_column_names.append(column._meta.name)
89+
90+
processed_raw = []
91+
92+
for row in raw:
93+
new_row = {**row}
94+
for json_column_name in json_column_names:
95+
value = new_row.get(json_column_name)
96+
if value is not None:
97+
new_row[json_column_name] = load_json(value)
98+
processed_raw.append(new_row)
5599

56-
output = getattr(self, "output_delegate", None)
100+
raw = processed_raw
101+
102+
#######################################################################
103+
104+
raw = await self.response_handler(raw)
57105

58106
if output:
59107
if output._output.as_objects:

piccolo/query/methods/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .alter import Alter # noqa: F401: F401
1+
from .alter import Alter # noqa: F401
22
from .count import Count # noqa: F401
33
from .create import Create # noqa: F401
44
from .create_index import CreateIndex # noqa: F401

0 commit comments

Comments
 (0)