Skip to content

Commit 02f5827

Browse files
committed
frozen queries initial concept
1 parent c6a7048 commit 02f5827

File tree

11 files changed

+83
-14
lines changed

11 files changed

+83
-14
lines changed

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: 73 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,68 @@ 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 endpoint, it has to generate the SQL from
211+
scratch each time.
212+
213+
Once a query is frozen, you can't apply any more clauses to it (where,
214+
limit, output etc).
215+
216+
"""
217+
# Copy the query, so we don't store any references to the original.
218+
query = self.__class__(
219+
table=self.table, frozen_querystrings=self.querystrings
220+
)
221+
return FrozenQuery(query=query)
222+
223+
###########################################################################
224+
177225
def __str__(self) -> str:
178226
return "; ".join([i.__str__() for i in self.querystrings])
227+
228+
229+
class FrozenQuery:
230+
def __init__(self, query: Query):
231+
self.query = query
232+
233+
async def run(self, *args, **kwargs):
234+
return await self.query.run(*args, **kwargs)
235+
236+
def run_sync(self, *args, **kwargs):
237+
return self.query.run_sync(*args, **kwargs)
238+
239+
def __getattr__(self, name: str):
240+
if hasattr(self.query, name):
241+
raise AttributeError(
242+
f"This query is frozen - {name} is only available on "
243+
"unfrozen queries."
244+
)
245+
else:
246+
raise AttributeError("Unrecognised attribute name.")
247+
248+
def __str__(self) -> str:
249+
return self.query.__str__()

piccolo/query/methods/alter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(
2727
self.only_default_columns = only_default_columns
2828

2929
@property
30-
def querystrings(self) -> t.Sequence[QueryString]:
30+
def default_querystrings(self) -> t.Sequence[QueryString]:
3131
prefix = "CREATE TABLE"
3232
if self.if_not_exists:
3333
prefix += " IF NOT EXISTS"

piccolo/query/methods/delete.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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 {}"

piccolo/query/methods/drop_index.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def column_names(self) -> t.List[str]:
2727
]
2828

2929
@property
30-
def querystrings(self) -> t.Sequence[QueryString]:
30+
def default_querystrings(self) -> t.Sequence[QueryString]:
3131
column_names = self.column_names
3232
index_name = self.table._get_index_name(column_names)
3333
query = "DROP INDEX"

piccolo/query/methods/exists.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def response_handler(self, response) -> bool:
2929
return bool(response[0]["exists"])
3030

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

piccolo/query/methods/objects.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ async def response_handler(self, response):
8181
return response
8282

8383
@property
84-
def querystrings(self) -> t.Sequence[QueryString]:
84+
def default_querystrings(self) -> t.Sequence[QueryString]:
8585
select = Select(table=self.table)
8686

8787
for attr in (

piccolo/query/methods/raw.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ def __init__(
2020
self.querystring = querystring
2121

2222
@property
23-
def querystrings(self) -> t.Sequence[QueryString]:
23+
def default_querystrings(self) -> t.Sequence[QueryString]:
2424
return [self.querystring]

0 commit comments

Comments
 (0)