Skip to content

Commit 03b2f27

Browse files
authored
Merge pull request piccolo-orm#109 from piccolo-orm/frozen_queries
Frozen queries
2 parents c6a7048 + be83849 commit 03b2f27

File tree

20 files changed

+285
-39
lines changed

20 files changed

+285
-39
lines changed

docs/src/piccolo/query_clauses/batch.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ There's currently no synchronous version. However, it's easy enough to achieve:
3333
async for _batch in batch:
3434
print(_batch)
3535
36-
import asyncio
37-
asyncio.run(get_batch())
36+
from piccolo.utils.sync import run_sync
37+
run_sync(get_batch())
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.. _freeze:
2+
3+
freeze
4+
======
5+
6+
You can use the ``freeze`` clause with any query type.
7+
8+
.. currentmodule:: piccolo.query.base
9+
10+
.. automethod:: Query.freeze

docs/src/piccolo/query_clauses/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ by modifying the return values.
1616
./order_by
1717
./where
1818
./batch
19+
./freeze

piccolo/apps/user/tables.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ class BaseUser(Table, tablename="piccolo_user"):
4141
)
4242

4343
def __init__(self, **kwargs):
44-
"""
45-
Generating passwords upfront is expensive, so might need reworking.
46-
"""
44+
# Generating passwords upfront is expensive, so might need reworking.
4745
password = kwargs.get("password", None)
4846
if password:
4947
kwargs["password"] = self.__class__.hash_password(password)

piccolo/query/base.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ def __exit__(self, exception_type, exception, traceback):
2222

2323
class Query:
2424

25-
__slots__ = ("table",)
25+
__slots__ = ("table", "_frozen_querystrings")
2626

27-
def __init__(self, table: t.Type[Table]):
27+
def __init__(
28+
self,
29+
table: t.Type[Table],
30+
frozen_querystrings: t.Optional[t.Sequence[QueryString]] = None,
31+
):
2832
self.table = table
33+
self._frozen_querystrings = frozen_querystrings
2934

3035
@property
3136
def engine_type(self) -> str:
@@ -156,6 +161,9 @@ def querystrings(self) -> t.Sequence[QueryString]:
156161
"""
157162
Calls the correct underlying method, depending on the current engine.
158163
"""
164+
if self._frozen_querystrings is not None:
165+
return self._frozen_querystrings
166+
159167
engine_type = self.engine_type
160168
if engine_type == "postgres":
161169
try:
@@ -174,5 +182,86 @@ def querystrings(self) -> t.Sequence[QueryString]:
174182

175183
###########################################################################
176184

185+
def freeze(self) -> FrozenQuery:
186+
"""
187+
This is a performance optimisation when the same query is run
188+
repeatedly. For example:
189+
190+
.. code-block:: python
191+
192+
TOP_BANDS = Band.select(
193+
Band.name
194+
).order_by(
195+
Band.popularity,
196+
ascending=False
197+
).limit(
198+
10
199+
).output(
200+
as_json=True
201+
).freeze()
202+
203+
# In the corresponding view/endpoint of whichever web framework
204+
# you're using:
205+
async def top_bands(self, request):
206+
return await TOP_BANDS.run()
207+
208+
It means that Piccolo doesn't have to work as hard each time the query
209+
is run to generate the corresponding SQL - some of it is cached. If the
210+
query is defined within the view/endpoint, it has to generate the SQL
211+
from scratch each time.
212+
213+
Once a query is frozen, you can't apply any more clauses to it
214+
(``where``, ``limit``, ``output`` etc).
215+
216+
Even though ``freeze`` helps with performance, there are limits to
217+
how much it can help, as most of the time is still spent waiting for a
218+
response from the database. However, for high throughput apps and data
219+
science scripts, it's a worthwhile optimisation.
220+
221+
"""
222+
querystrings = self.querystrings
223+
for querystring in querystrings:
224+
querystring.freeze(engine_type=self.engine_type)
225+
226+
# Copy the query, so we don't store any references to the original.
227+
query = self.__class__(
228+
table=self.table, frozen_querystrings=self.querystrings
229+
)
230+
231+
if hasattr(self, "limit_delegate"):
232+
# Needed for `response_handler`
233+
query.limit_delegate = self.limit_delegate.copy() # type: ignore
234+
235+
if hasattr(self, "output_delegate"):
236+
# Needed for `_process_results`
237+
query.output_delegate = self.output_delegate.copy() # type: ignore
238+
239+
return FrozenQuery(query=query)
240+
241+
###########################################################################
242+
177243
def __str__(self) -> str:
178244
return "; ".join([i.__str__() for i in self.querystrings])
245+
246+
247+
class FrozenQuery:
248+
def __init__(self, query: Query):
249+
self.query = query
250+
251+
async def run(self, *args, **kwargs):
252+
return await self.query.run(*args, **kwargs)
253+
254+
def run_sync(self, *args, **kwargs):
255+
return self.query.run_sync(*args, **kwargs)
256+
257+
def __getattr__(self, name: str):
258+
if hasattr(self.query, name):
259+
raise AttributeError(
260+
f"This query is frozen - {name} is only available on "
261+
"unfrozen queries."
262+
)
263+
else:
264+
raise AttributeError("Unrecognised attribute name.")
265+
266+
def __str__(self) -> str:
267+
return self.query.__str__()

piccolo/query/methods/alter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,8 @@ class Alter(Query):
293293
"_set_unique",
294294
)
295295

296-
def __init__(self, table: t.Type[Table]):
297-
super().__init__(table)
296+
def __init__(self, table: t.Type[Table], **kwargs):
297+
super().__init__(table, **kwargs)
298298
self._add_foreign_key_constraint: t.List[AddForeignKeyConstraint] = []
299299
self._add: t.List[AddColumn] = []
300300
self._drop_contraint: t.List[DropConstraint] = []
@@ -506,7 +506,7 @@ def set_digits(
506506
return self
507507

508508
@property
509-
def querystrings(self) -> t.Sequence[QueryString]:
509+
def default_querystrings(self) -> t.Sequence[QueryString]:
510510
if self._drop_table is not None:
511511
return [self._drop_table.querystring]
512512

piccolo/query/methods/count.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
class Count(Query):
1616
__slots__ = ("where_delegate",)
1717

18-
def __init__(self, table: t.Type[Table]):
19-
super().__init__(table)
18+
def __init__(self, table: t.Type[Table], **kwargs):
19+
super().__init__(table, **kwargs)
2020
self.where_delegate = WhereDelegate()
2121

2222
def where(self, where: Combinable) -> Count:
@@ -27,7 +27,7 @@ async def response_handler(self, response) -> bool:
2727
return response[0]["count"]
2828

2929
@property
30-
def querystrings(self) -> t.Sequence[QueryString]:
30+
def default_querystrings(self) -> t.Sequence[QueryString]:
3131
select = Select(self.table)
3232
select.where_delegate._where = self.where_delegate._where
3333
return [

piccolo/query/methods/create.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ def __init__(
2121
table: t.Type[Table],
2222
if_not_exists: bool = False,
2323
only_default_columns: bool = False,
24+
**kwargs,
2425
):
25-
super().__init__(table)
26+
super().__init__(table, **kwargs)
2627
self.if_not_exists = if_not_exists
2728
self.only_default_columns = only_default_columns
2829

2930
@property
30-
def querystrings(self) -> t.Sequence[QueryString]:
31+
def default_querystrings(self) -> t.Sequence[QueryString]:
3132
prefix = "CREATE TABLE"
3233
if self.if_not_exists:
3334
prefix += " IF NOT EXISTS"

piccolo/query/methods/create_index.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ def __init__(
1717
columns: t.List[t.Union[Column, str]],
1818
method: IndexMethod = IndexMethod.btree,
1919
if_not_exists: bool = False,
20+
**kwargs,
2021
):
2122
self.columns = columns
2223
self.method = method
2324
self.if_not_exists = if_not_exists
24-
super().__init__(table)
25+
super().__init__(table, **kwargs)
2526

2627
@property
2728
def column_names(self) -> t.List[str]:

piccolo/query/methods/delete.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ class Delete(Query):
1818

1919
__slots__ = ("force", "where_delegate")
2020

21-
def __init__(self, table: t.Type[Table], force: bool = False):
22-
super().__init__(table)
21+
def __init__(self, table: t.Type[Table], force: bool = False, **kwargs):
22+
super().__init__(table, **kwargs)
2323
self.force = force
2424
self.where_delegate = WhereDelegate()
2525

@@ -40,7 +40,7 @@ def _validate(self):
4040
)
4141

4242
@property
43-
def querystrings(self) -> t.Sequence[QueryString]:
43+
def default_querystrings(self) -> t.Sequence[QueryString]:
4444
query = f"DELETE FROM {self.table._meta.tablename}"
4545
if self.where_delegate._where:
4646
query += " WHERE {}"

0 commit comments

Comments
 (0)