@@ -22,10 +22,15 @@ def __exit__(self, exception_type, exception, traceback):
2222
2323class 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__ ()
0 commit comments