@@ -132,33 +132,30 @@ def _init(self):
132132 """
133133 # The local initialization for the specific cursor.
134134
135- def __init__ (self , cursor_id ):
135+ def __init__ (self , cursor_id , wref = weakref . ref , ID = ID ):
136136 self .cursor_id = cursor_id
137137 if self .statement is not None :
138138 self ._output = self .statement ._output
139139 self ._output_io = self .statement ._output_io
140140 self ._output_formats = self .statement ._output_formats or ()
141141 self ._output_attmap = self .statement ._output_attmap
142142
143- if self .cursor_id == ID (self ):
144- addgarbage = self .database .pq .garbage_cursors .append
145- typio = self .database .typio
143+ self ._pq_cursor_id = self .database .typio .encode (cursor_id )
144+ # If the cursor's id was generated, it should be garbage collected.
145+ if cursor_id == ID (self ):
146+ garbage = self .database .pq .garbage_cursors .append
147+ cid = self ._pq_cursor_id
146148 # Callback for closing the cursor on remote end.
147- self ._del = weakref .ref (
148- self , lambda x : addgarbage (typio .encode (cursor_id ))
149- )
150- self ._quoted_cursor_id = '"' + self .cursor_id .replace ('"' , '""' ) + '"'
151- self ._pq_cursor_id = self .database .typio .encode (self .cursor_id )
149+ self ._del = wref (self , lambda x : garbage (cid ))
150+ self ._quoted_cursor_id = '"' + cursor_id .replace ('"' , '""' ) + '"'
152151 self ._init ()
153152
154153 def __iter__ (self ):
155154 return self
156155
157156 def close (self ):
158157 if self .closed is False :
159- self .database .pq .garbage_cursors .append (
160- self .database .typio .encode (self .cursor_id )
161- )
158+ self .database .pq .garbage_cursors .append (self ._pq_cursor_id )
162159 self .closed = True
163160 # Don't need the weakref anymore.
164161 if hasattr (self , '_del' ):
@@ -271,12 +268,11 @@ def _process_tuple_chunk_Row(self, x,
271268 proc = process_chunk ,
272269 from_seq = Row .from_sequence ,
273270 ):
274- return proc (self ._output_io , x , self ._raise_column_tuple_error )
275- #attmap = self._output_attmap
276- #return [
277- # from_seq(attmap, y)
278- # for y in proc(self._output_io, x, self._raise_column_tuple_error)
279- #]
271+ attmap = self ._output_attmap
272+ return [
273+ from_seq (attmap , y )
274+ for y in proc (self ._output_io , x , self ._raise_column_tuple_error )
275+ ]
280276
281277 # Process the elemnt.Tuple messages in `x` for chunks()
282278 def _process_tuple_chunk (self , x , proc = process_chunk ):
@@ -355,8 +351,12 @@ class Chunks(Output, pg_api.Chunks):
355351 pass
356352
357353##
358- # FetchAll is a Chunks cursor that gets all the information
359- # in the cursor.
354+ # FetchAll - A Chunks cursor that gets *all* the records in the cursor.
355+ #
356+ # It has added complexity over other variants as in order to stream results,
357+ # chunks have to be removed from the protocol transaction's received messages.
358+ # If this wasn't done, the entire result set would be fully buffered prior
359+ # to processing.
360360class FetchAll (Chunks ):
361361 _e_factors = ('statement' , 'parameters' ,)
362362 def _e_metas (self ):
@@ -379,14 +379,17 @@ def _init(self,
379379 self ._pq_xp_fetchall () + (element .SynchronizeMessage ,)
380380 )
381381 self .database ._pq_push (self ._xact , self )
382+
383+ # Get more messages until the first Tuple is seen.
382384 STEP = self .database ._pq_step
383385 while self ._xact .state != xact .Complete :
384386 STEP ()
385387 for x in self ._xact .messages_received ():
386388 if x .__class__ is tuple or expect == x .type :
387- # no need to step once this is seen
389+ # No need to step anymore once this is seen.
388390 return
389391 elif x .type == null :
392+ # The protocol transaction is going to be complete..
390393 self .database ._pq_complete ()
391394 self ._xact = None
392395 return
@@ -395,12 +398,15 @@ def _init(self,
395398 self .database ._pq_complete ()
396399 # If this was a select/copy cursor,
397400 # the data messages would have caused an earlier
398- # return.
401+ # return. It's empty.
399402 self ._xact = None
400403 return
401404 elif x .type in (bindcomplete , parsecomplete ):
405+ # Noise.
402406 pass
403407 else :
408+ # This should have been caught by the protocol transaction.
409+ # "Can't happen".
404410 self .database ._pq_complete ()
405411 if self ._xact .fatal is None :
406412 self ._xact .fatal = False
@@ -412,15 +418,16 @@ def _init(self,
412418 self .database ._raise_pq_error (self ._xact , controller = self )
413419 return
414420
415- def __next__ (self ):
421+ def __next__ (self , data_types = ( tuple , bytes ) ):
416422 x = self ._xact
417423 # self._xact = None; means that the cursor has been exhausted.
418424 if x is None :
419425 raise StopIteration
420426
421427 # Finish the protocol transaction.
428+ STEP = self .database ._pq_step
422429 while x .state is not xact .Complete and not x .completed :
423- self . database . _pq_step ()
430+ STEP ()
424431
425432 # fatal is None == no error
426433 # fatal is True == dead connection
@@ -432,10 +439,14 @@ def __next__(self):
432439 if not x .completed :
433440 # Transaction has been cleaned out of completed? iterator is done.
434441 self ._xact = None
442+ self .close ()
435443 raise StopIteration
436444
437445 # Get the chunk to be processed.
438- chunk = x .completed [0 ][1 ]
446+ chunk = [
447+ y for y in x .completed [0 ][1 ]
448+ if y .__class__ in data_types
449+ ]
439450 r = self ._process_chunk (chunk )
440451 # Remove it, it's been processed.
441452 del x .completed [0 ]
@@ -447,21 +458,15 @@ class SingleXactCopy(FetchAll):
447458
448459class SingleXactFetch (FetchAll ):
449460 _expect = element .Tuple .type
450- _process_chunk_ = FetchAll ._process_tuple_chunk_Row
451-
452- def _process_chunk (self , x , tuple_type = tuple ):
453- return self ._process_chunk_ ((
454- y for y in x if y .__class__ is tuple
455- ))
456461
457462class MultiXactStream (Chunks ):
458- chunksize = 1024 * 3
463+ chunksize = 1024 * 4
459464 # only tuple streams
460- _process_chunk = Output ._process_tuple_chunk_Row
465+ _process_chunk = Output ._process_tuple_chunk
461466
462467 def _e_metas (self ):
463468 yield ('chunksize' , self .chunksize )
464- yield ('type' , type ( self ) .__name__ )
469+ yield ('type' , self . __class__ .__name__ )
465470
466471 def __init__ (self , statement , parameters , cursor_id ):
467472 self .statement = statement
@@ -505,6 +510,7 @@ def __next__(self, tuple_type = tuple):
505510 else :
506511 # it's done.
507512 self ._xact = None
513+ self .close ()
508514 if not chunk :
509515 # chunk is empty, it's done *right* now.
510516 raise StopIteration
@@ -740,38 +746,38 @@ def _e_metas(self):
740746 yield ('sql_column_types' , ct )
741747
742748 def clone (self ):
743- ps = type ( self ) (self .database , None , self .string )
749+ ps = self . __class__ (self .database , None , self .string )
744750 ps ._init ()
745751 ps ._fini ()
746752 return ps
747753
748- def __init__ (self , database , statement_id , string ):
754+ def __init__ (self ,
755+ database , statement_id , string ,
756+ wref = weakref .ref
757+ ):
749758 self .database = database
750- self .statement_id = statement_id or ID (self )
751759 self .string = string
760+ self .statement_id = statement_id or ID (self )
752761 self ._xact = None
753- self ._pq_statement_id = None
754762 self .closed = None
763+ self ._pq_statement_id = database .typio ._encode (self .statement_id )[0 ]
755764
756765 if not statement_id :
757- addgarbage = database .pq .garbage_statements .append
758- typio = database .typio
759- sid = self .statement_id
766+ garbage = database .pq .garbage_statements .append
767+ sid = self ._pq_statement_id
760768 # Callback for closing the statement on remote end.
761- self ._del = weakref .ref (
762- self , lambda x : addgarbage (typio .encode (sid ))
763- )
769+ self ._del = wref (self , lambda x : garbage (sid ))
764770
765771 def __repr__ (self ):
766772 return '<{mod}.{name}[{ci}] {state}>' .format (
767- mod = type ( self ) .__module__ ,
768- name = type ( self ) .__name__ ,
773+ mod = self . __class__ .__module__ ,
774+ name = self . __class__ .__name__ ,
769775 ci = self .database .connector ._pq_iri ,
770776 state = self .state ,
771777 )
772778
773- def _pq_parameters (self , parameters ):
774- return process_tuple (
779+ def _pq_parameters (self , parameters , proc = process_tuple ):
780+ return proc (
775781 self ._input_io , parameters ,
776782 self ._raise_parameter_tuple_error
777783 )
@@ -915,9 +921,6 @@ def _init(self):
915921 the return as there may be things that can be done while waiting
916922 for the return. Use the _fini() to complete.
917923 """
918- self ._pq_statement_id = self .database .typio ._encode (
919- self .statement_id
920- )[0 ]
921924 if self .string is not None :
922925 q = self .database .typio ._encode (str (self .string ))[0 ]
923926 cmd = [
@@ -993,23 +996,27 @@ def __call__(self, *parameters):
993996 raise TypeError ("statement requires %d parameters, given %d" % (
994997 len (self ._input ), len (parameters )
995998 ))
999+ ##
9961000 # get em' all!
9971001 if self ._output is None :
9981002 # might be a copy.
9991003 c = SingleXactCopy (self , parameters )
10001004 else :
10011005 c = SingleXactFetch (self , parameters )
1006+ c ._process_chunk = c ._process_tuple_chunk_Row
10021007
10031008 # iff output is None, it's not a tuple returning query.
10041009 # however, if it's a copy, detect that fact by SingleXactCopy's
10051010 # immediate return after finding the copy begin message(no complete).
1006- if self ._output is None and c .command () is not None :
1007- return (c .command (), c .count ())
1008- else :
1009- r = []
1010- for x in c :
1011- r .extend (x )
1012- return r
1011+ if self ._output is None :
1012+ cmd = c .command ()
1013+ if cmd is not None :
1014+ return (cmd , c .count ())
1015+ # Returns rows, accumulate in a list.
1016+ r = []
1017+ for x in c :
1018+ r .extend (x )
1019+ return r
10131020
10141021 def declare (self , * parameters ):
10151022 if self .closed is None :
@@ -1022,7 +1029,10 @@ def declare(self, *parameters):
10221029 return Cursor (self , parameters , self .database , None )
10231030
10241031 def rows (self , * parameters , ** kw ):
1025- return chain .from_iterable (self .chunks (* parameters , ** kw ))
1032+ chunks = self .chunks (* parameters , ** kw )
1033+ if chunks ._output_io :
1034+ chunks ._process_chunk = chunks ._process_tuple_chunk_Row
1035+ return chain .from_iterable (chunks )
10261036 __iter__ = rows
10271037
10281038 def chunks (self , * parameters ):
@@ -1033,10 +1043,12 @@ def chunks(self, *parameters):
10331043 raise TypeError ("statement requires %d parameters, given %d" % (
10341044 len (self ._input ), len (parameters )
10351045 ))
1046+
10361047 if self ._output is None :
1048+ # It's *probably* a COPY.
10371049 return SingleXactCopy (self , parameters )
10381050 if self .database .pq .state == b'I' :
1039- # Currently, *not* in a transaction block, so
1051+ # Currently, *not* in a Transaction block, so
10401052 # DECLARE the statement WITH HOLD in order to allow
10411053 # access across transactions.
10421054 if self .string is not None :
@@ -1047,6 +1059,7 @@ def chunks(self, *parameters):
10471059 # This happens when statement_from_id is used.
10481060 return SingleXactFetch (self , parameters )
10491061 else :
1062+ # Likely, the best possible case. It gets to use Execute messages.
10501063 return MultiXactInsideBlock (self , parameters , None )
10511064
10521065 def column (self , * parameters , ** kw ):
@@ -1056,12 +1069,15 @@ def column(self, *parameters, **kw):
10561069
10571070 def first (self , * parameters ):
10581071 if self .closed is None :
1072+ # Not fully initialized; assume interrupted.
10591073 self ._fini ()
10601074 if self ._input is not None :
1075+ # Use a regular TypeError.
10611076 if len (parameters ) != len (self ._input ):
10621077 raise TypeError ("statement requires %d parameters, given %d" % (
10631078 len (self ._input ), len (parameters )
10641079 ))
1080+
10651081 # Parameters? Build em'.
10661082 db = self .database
10671083
@@ -1084,10 +1100,12 @@ def first(self, *parameters):
10841100 ),
10851101 # Get all
10861102 element .Execute (b'' , 0xFFFFFFFF ),
1103+ element .ClosePortal (b'' ),
10871104 element .SynchronizeMessage
10881105 ),
10891106 asynchook = db ._receive_async
10901107 )
1108+ # Push and complete protocol transaction.
10911109 db ._pq_push (x , self )
10921110 db ._pq_complete ()
10931111
@@ -1102,6 +1120,7 @@ def first(self, *parameters):
11021120 return None
11031121
11041122 if len (self ._output_io ) > 1 :
1123+ # Multiple columns, return a Row.
11051124 return Row .from_sequence (
11061125 self ._output_attmap ,
11071126 process_tuple (
@@ -1110,6 +1129,7 @@ def first(self, *parameters):
11101129 ),
11111130 )
11121131 else :
1132+ # Single column output.
11131133 if xt [0 ] is None :
11141134 return None
11151135 io = self ._output_io [0 ] or self .database .typio .decode
0 commit comments