Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 89e850e

Browse files
committed
plpython: Add SPI cursor support
Add a function plpy.cursor that is similar to plpy.execute but uses an SPI cursor to avoid fetching the entire result set into memory. Jan Urbański, reviewed by Steve Singer
1 parent e6d9e21 commit 89e850e

9 files changed

+1251
-3
lines changed

doc/src/sgml/plpython.sgml

+81
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,15 @@ $$ LANGUAGE plpythonu;
891891
can be modified.
892892
</para>
893893

894+
<para>
895+
Note that calling <literal>plpy.execute</literal> will cause the entire
896+
result set to be read into memory. Only use that function when you are sure
897+
that the result set will be relatively small. If you don't want to risk
898+
excessive memory usage when fetching large results,
899+
use <literal>plpy.cursor</literal> rather
900+
than <literal>plpy.execute</literal>.
901+
</para>
902+
894903
<para>
895904
For example:
896905
<programlisting>
@@ -958,6 +967,78 @@ $$ LANGUAGE plpythonu;
958967

959968
</sect2>
960969

970+
<sect2>
971+
<title>Accessing Data with Cursors</title>
972+
973+
<para>
974+
The <literal>plpy.cursor</literal> function accepts the same arguments
975+
as <literal>plpy.execute</literal> (except for <literal>limit</literal>)
976+
and returns a cursor object, which allows you to process large result sets
977+
in smaller chunks. As with <literal>plpy.execute</literal>, either a query
978+
string or a plan object along with a list of arguments can be used. The
979+
cursor object provides a <literal>fetch</literal> method that accepts an
980+
integer parameter and returns a result object. Each time you
981+
call <literal>fetch</literal>, the returned object will contain the next
982+
batch of rows, never larger than the parameter value. Once all rows are
983+
exhausted, <literal>fetch</literal> starts returning an empty result
984+
object. Cursor objects also provide an
985+
<ulink url="http://docs.python.org/library/stdtypes.html#iterator-types">iterator
986+
interface</ulink>, yielding one row at a time until all rows are exhausted.
987+
Data fetched that way is not returned as result objects, but rather as
988+
dictionaries, each dictionary corresponding to a single result row.
989+
</para>
990+
991+
<para>
992+
Cursors are automatically disposed of. But if you want to explicitly
993+
release all resources held by a cursor, use the <literal>close</literal>
994+
method. Once closed, a cursor cannot be fetched from anymore.
995+
</para>
996+
997+
<tip>
998+
<para>
999+
Do not confuse objects created by <literal>plpy.cursor</literal> with
1000+
DB-API cursors as defined by
1001+
the <ulink url="http://www.python.org/dev/peps/pep-0249/">Python Database
1002+
API specification</ulink>. They don't have anything in common except for
1003+
the name.
1004+
</para>
1005+
</tip>
1006+
1007+
<para>
1008+
An example of two ways of processing data from a large table is:
1009+
<programlisting>
1010+
CREATE FUNCTION count_odd_iterator() RETURNS integer AS $$
1011+
odd = 0
1012+
for row in plpy.cursor("select num from largetable"):
1013+
if row['num'] % 2:
1014+
odd += 1
1015+
return odd
1016+
$$ LANGUAGE plpythonu;
1017+
1018+
CREATE FUNCTION count_odd_fetch(batch_size integer) RETURNS integer AS $$
1019+
odd = 0
1020+
cursor = plpy.cursor("select num from largetable")
1021+
while True:
1022+
rows = cursor.fetch(batch_size)
1023+
if not rows:
1024+
break
1025+
for row in rows:
1026+
if row['num'] % 2:
1027+
odd += 1
1028+
return odd
1029+
$$ LANGUAGE plpythonu;
1030+
1031+
CREATE FUNCTION count_odd_prepared() RETURNS integer AS $$
1032+
odd = 0
1033+
plan = plpy.prepare("select num from largetable where num % $1 &lt;&gt; 0", ["integer"])
1034+
rows = list(plpy.cursor(plan, [2]))
1035+
1036+
return len(rows)
1037+
$$ LANGUAGE plpythonu;
1038+
</programlisting>
1039+
</para>
1040+
</sect2>
1041+
9611042
<sect2 id="plpython-trapping">
9621043
<title>Trapping Errors</title>
9631044

src/pl/plpython/expected/plpython_spi.out

+151
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,154 @@ CONTEXT: PL/Python function "result_nrows_test"
133133
2
134134
(1 row)
135135

136+
-- cursor objects
137+
CREATE FUNCTION simple_cursor_test() RETURNS int AS $$
138+
res = plpy.cursor("select fname, lname from users")
139+
does = 0
140+
for row in res:
141+
if row['lname'] == 'doe':
142+
does += 1
143+
return does
144+
$$ LANGUAGE plpythonu;
145+
CREATE FUNCTION double_cursor_close() RETURNS int AS $$
146+
res = plpy.cursor("select fname, lname from users")
147+
res.close()
148+
res.close()
149+
$$ LANGUAGE plpythonu;
150+
CREATE FUNCTION cursor_fetch() RETURNS int AS $$
151+
res = plpy.cursor("select fname, lname from users")
152+
assert len(res.fetch(3)) == 3
153+
assert len(res.fetch(3)) == 1
154+
assert len(res.fetch(3)) == 0
155+
assert len(res.fetch(3)) == 0
156+
try:
157+
# use next() or __next__(), the method name changed in
158+
# http://www.python.org/dev/peps/pep-3114/
159+
try:
160+
res.next()
161+
except AttributeError:
162+
res.__next__()
163+
except StopIteration:
164+
pass
165+
else:
166+
assert False, "StopIteration not raised"
167+
$$ LANGUAGE plpythonu;
168+
CREATE FUNCTION cursor_mix_next_and_fetch() RETURNS int AS $$
169+
res = plpy.cursor("select fname, lname from users order by fname")
170+
assert len(res.fetch(2)) == 2
171+
172+
item = None
173+
try:
174+
item = res.next()
175+
except AttributeError:
176+
item = res.__next__()
177+
assert item['fname'] == 'rick'
178+
179+
assert len(res.fetch(2)) == 1
180+
$$ LANGUAGE plpythonu;
181+
CREATE FUNCTION fetch_after_close() RETURNS int AS $$
182+
res = plpy.cursor("select fname, lname from users")
183+
res.close()
184+
try:
185+
res.fetch(1)
186+
except ValueError:
187+
pass
188+
else:
189+
assert False, "ValueError not raised"
190+
$$ LANGUAGE plpythonu;
191+
CREATE FUNCTION next_after_close() RETURNS int AS $$
192+
res = plpy.cursor("select fname, lname from users")
193+
res.close()
194+
try:
195+
try:
196+
res.next()
197+
except AttributeError:
198+
res.__next__()
199+
except ValueError:
200+
pass
201+
else:
202+
assert False, "ValueError not raised"
203+
$$ LANGUAGE plpythonu;
204+
CREATE FUNCTION cursor_fetch_next_empty() RETURNS int AS $$
205+
res = plpy.cursor("select fname, lname from users where false")
206+
assert len(res.fetch(1)) == 0
207+
try:
208+
try:
209+
res.next()
210+
except AttributeError:
211+
res.__next__()
212+
except StopIteration:
213+
pass
214+
else:
215+
assert False, "StopIteration not raised"
216+
$$ LANGUAGE plpythonu;
217+
CREATE FUNCTION cursor_plan() RETURNS SETOF text AS $$
218+
plan = plpy.prepare(
219+
"select fname, lname from users where fname like $1 || '%' order by fname",
220+
["text"])
221+
for row in plpy.cursor(plan, ["w"]):
222+
yield row['fname']
223+
for row in plpy.cursor(plan, ["j"]):
224+
yield row['fname']
225+
$$ LANGUAGE plpythonu;
226+
CREATE FUNCTION cursor_plan_wrong_args() RETURNS SETOF text AS $$
227+
plan = plpy.prepare("select fname, lname from users where fname like $1 || '%'",
228+
["text"])
229+
c = plpy.cursor(plan, ["a", "b"])
230+
$$ LANGUAGE plpythonu;
231+
SELECT simple_cursor_test();
232+
simple_cursor_test
233+
--------------------
234+
3
235+
(1 row)
236+
237+
SELECT double_cursor_close();
238+
double_cursor_close
239+
---------------------
240+
241+
(1 row)
242+
243+
SELECT cursor_fetch();
244+
cursor_fetch
245+
--------------
246+
247+
(1 row)
248+
249+
SELECT cursor_mix_next_and_fetch();
250+
cursor_mix_next_and_fetch
251+
---------------------------
252+
253+
(1 row)
254+
255+
SELECT fetch_after_close();
256+
fetch_after_close
257+
-------------------
258+
259+
(1 row)
260+
261+
SELECT next_after_close();
262+
next_after_close
263+
------------------
264+
265+
(1 row)
266+
267+
SELECT cursor_fetch_next_empty();
268+
cursor_fetch_next_empty
269+
-------------------------
270+
271+
(1 row)
272+
273+
SELECT cursor_plan();
274+
cursor_plan
275+
-------------
276+
willem
277+
jane
278+
john
279+
(3 rows)
280+
281+
SELECT cursor_plan_wrong_args();
282+
ERROR: TypeError: Expected sequence of 1 argument, got 2: ['a', 'b']
283+
CONTEXT: Traceback (most recent call last):
284+
PL/Python function "cursor_plan_wrong_args", line 4, in <module>
285+
c = plpy.cursor(plan, ["a", "b"])
286+
PL/Python function "cursor_plan_wrong_args"

src/pl/plpython/expected/plpython_subtransaction.out

+66
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,69 @@ SELECT * FROM subtransaction_tbl;
409409
(1 row)
410410

411411
DROP TABLE subtransaction_tbl;
412+
-- cursor/subtransactions interactions
413+
CREATE FUNCTION cursor_in_subxact() RETURNS int AS $$
414+
with plpy.subtransaction():
415+
cur = plpy.cursor("select * from generate_series(1, 20) as gen(i)")
416+
cur.fetch(10)
417+
fetched = cur.fetch(10);
418+
return int(fetched[5]["i"])
419+
$$ LANGUAGE plpythonu;
420+
CREATE FUNCTION cursor_aborted_subxact() RETURNS int AS $$
421+
try:
422+
with plpy.subtransaction():
423+
cur = plpy.cursor("select * from generate_series(1, 20) as gen(i)")
424+
cur.fetch(10);
425+
plpy.execute("select no_such_function()")
426+
except plpy.SPIError:
427+
fetched = cur.fetch(10)
428+
return int(fetched[5]["i"])
429+
return 0 # not reached
430+
$$ LANGUAGE plpythonu;
431+
CREATE FUNCTION cursor_plan_aborted_subxact() RETURNS int AS $$
432+
try:
433+
with plpy.subtransaction():
434+
plpy.execute('create temporary table tmp(i) '
435+
'as select generate_series(1, 10)')
436+
plan = plpy.prepare("select i from tmp")
437+
cur = plpy.cursor(plan)
438+
plpy.execute("select no_such_function()")
439+
except plpy.SPIError:
440+
fetched = cur.fetch(5)
441+
return fetched[2]["i"]
442+
return 0 # not reached
443+
$$ LANGUAGE plpythonu;
444+
CREATE FUNCTION cursor_close_aborted_subxact() RETURNS boolean AS $$
445+
try:
446+
with plpy.subtransaction():
447+
cur = plpy.cursor('select 1')
448+
plpy.execute("select no_such_function()")
449+
except plpy.SPIError:
450+
cur.close()
451+
return True
452+
return False # not reached
453+
$$ LANGUAGE plpythonu;
454+
SELECT cursor_in_subxact();
455+
cursor_in_subxact
456+
-------------------
457+
16
458+
(1 row)
459+
460+
SELECT cursor_aborted_subxact();
461+
ERROR: ValueError: iterating a cursor in an aborted subtransaction
462+
CONTEXT: Traceback (most recent call last):
463+
PL/Python function "cursor_aborted_subxact", line 8, in <module>
464+
fetched = cur.fetch(10)
465+
PL/Python function "cursor_aborted_subxact"
466+
SELECT cursor_plan_aborted_subxact();
467+
ERROR: ValueError: iterating a cursor in an aborted subtransaction
468+
CONTEXT: Traceback (most recent call last):
469+
PL/Python function "cursor_plan_aborted_subxact", line 10, in <module>
470+
fetched = cur.fetch(5)
471+
PL/Python function "cursor_plan_aborted_subxact"
472+
SELECT cursor_close_aborted_subxact();
473+
ERROR: ValueError: closing a cursor in an aborted subtransaction
474+
CONTEXT: Traceback (most recent call last):
475+
PL/Python function "cursor_close_aborted_subxact", line 7, in <module>
476+
cur.close()
477+
PL/Python function "cursor_close_aborted_subxact"

src/pl/plpython/expected/plpython_subtransaction_0.out

+70
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,73 @@ SELECT * FROM subtransaction_tbl;
382382
(0 rows)
383383

384384
DROP TABLE subtransaction_tbl;
385+
-- cursor/subtransactions interactions
386+
CREATE FUNCTION cursor_in_subxact() RETURNS int AS $$
387+
with plpy.subtransaction():
388+
cur = plpy.cursor("select * from generate_series(1, 20) as gen(i)")
389+
cur.fetch(10)
390+
fetched = cur.fetch(10);
391+
return int(fetched[5]["i"])
392+
$$ LANGUAGE plpythonu;
393+
ERROR: could not compile PL/Python function "cursor_in_subxact"
394+
DETAIL: SyntaxError: invalid syntax (line 3)
395+
CREATE FUNCTION cursor_aborted_subxact() RETURNS int AS $$
396+
try:
397+
with plpy.subtransaction():
398+
cur = plpy.cursor("select * from generate_series(1, 20) as gen(i)")
399+
cur.fetch(10);
400+
plpy.execute("select no_such_function()")
401+
except plpy.SPIError:
402+
fetched = cur.fetch(10)
403+
return int(fetched[5]["i"])
404+
return 0 # not reached
405+
$$ LANGUAGE plpythonu;
406+
ERROR: could not compile PL/Python function "cursor_aborted_subxact"
407+
DETAIL: SyntaxError: invalid syntax (line 4)
408+
CREATE FUNCTION cursor_plan_aborted_subxact() RETURNS int AS $$
409+
try:
410+
with plpy.subtransaction():
411+
plpy.execute('create temporary table tmp(i) '
412+
'as select generate_series(1, 10)')
413+
plan = plpy.prepare("select i from tmp")
414+
cur = plpy.cursor(plan)
415+
plpy.execute("select no_such_function()")
416+
except plpy.SPIError:
417+
fetched = cur.fetch(5)
418+
return fetched[2]["i"]
419+
return 0 # not reached
420+
$$ LANGUAGE plpythonu;
421+
ERROR: could not compile PL/Python function "cursor_plan_aborted_subxact"
422+
DETAIL: SyntaxError: invalid syntax (line 4)
423+
CREATE FUNCTION cursor_close_aborted_subxact() RETURNS boolean AS $$
424+
try:
425+
with plpy.subtransaction():
426+
cur = plpy.cursor('select 1')
427+
plpy.execute("select no_such_function()")
428+
except plpy.SPIError:
429+
cur.close()
430+
return True
431+
return False # not reached
432+
$$ LANGUAGE plpythonu;
433+
ERROR: could not compile PL/Python function "cursor_close_aborted_subxact"
434+
DETAIL: SyntaxError: invalid syntax (line 4)
435+
SELECT cursor_in_subxact();
436+
ERROR: function cursor_in_subxact() does not exist
437+
LINE 1: SELECT cursor_in_subxact();
438+
^
439+
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
440+
SELECT cursor_aborted_subxact();
441+
ERROR: function cursor_aborted_subxact() does not exist
442+
LINE 1: SELECT cursor_aborted_subxact();
443+
^
444+
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
445+
SELECT cursor_plan_aborted_subxact();
446+
ERROR: function cursor_plan_aborted_subxact() does not exist
447+
LINE 1: SELECT cursor_plan_aborted_subxact();
448+
^
449+
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
450+
SELECT cursor_close_aborted_subxact();
451+
ERROR: function cursor_close_aborted_subxact() does not exist
452+
LINE 1: SELECT cursor_close_aborted_subxact();
453+
^
454+
HINT: No function matches the given name and argument types. You might need to add explicit type casts.

0 commit comments

Comments
 (0)