-- cursor-streaming SPI: spi_query / spi_fetchrow / spi_cursor_close / -- spi_query_prepared -- basic streaming loop: constant-memory iteration over a result set create function test_cursor_basic(int) returns int language plphp as $$ $cursor = spi_query("select generate_series(1, $args[0]) as g"); $sum = 0; while ($row = spi_fetchrow($cursor)) $sum += $row['g']; return $sum; $$; select test_cursor_basic(1); select test_cursor_basic(5); select test_cursor_basic(1000); -- the cursor is closed automatically at exhaustion: further fetches -- just return false create function test_cursor_exhausted() returns text language plphp as $$ $cursor = spi_query("select 1 as a"); $row = spi_fetchrow($cursor); pg_raise('notice', "first fetch: " . $row['a']); $row = spi_fetchrow($cursor); pg_raise('notice', "second fetch is " . ($row === false ? "false" : "a row")); $row = spi_fetchrow($cursor); pg_raise('notice', "third fetch is " . ($row === false ? "false" : "a row")); return "ok"; $$; select test_cursor_exhausted(); -- early close: abandon a cursor before exhaustion; fetching afterwards -- returns false, and closing again is harmless create function test_cursor_close() returns text language plphp as $$ $cursor = spi_query("select generate_series(1, 1000000) as g"); $row = spi_fetchrow($cursor); pg_raise('notice', "got first row: " . $row['g']); spi_cursor_close($cursor); $row = spi_fetchrow($cursor); pg_raise('notice', "fetch after close is " . ($row === false ? "false" : "a row")); spi_cursor_close($cursor); return "ok"; $$; select test_cursor_close(); -- fetching from a cursor that never existed returns false create function test_cursor_bogus() returns text language plphp as $$ $row = spi_fetchrow("no such cursor"); return $row === false ? "false" : "a row"; $$; select test_cursor_bogus(); -- streaming from a prepared plan with arguments create function test_cursor_prepared(int, int) returns int language plphp as $$ $plan = spi_prepare("select generate_series($1, $2) as g", "int4", "int4"); $cursor = spi_query_prepared($plan, $args[0], $args[1]); $sum = 0; while ($row = spi_fetchrow($cursor)) $sum += $row['g']; spi_freeplan($plan); return $sum; $$; -- 3 + 4 + 5 = 12 select test_cursor_prepared(3, 5); -- two cursors interleaved create function test_cursor_interleave() returns int language plphp as $$ $c1 = spi_query("select 1 as a union all select 2"); $c2 = spi_query("select 30 as b union all select 40"); $ret = 0; while (($r1 = spi_fetchrow($c1)) && ($r2 = spi_fetchrow($c2))) { pg_raise('notice', sprintf("got values %d and %d", $r1['a'], $r2['b'])); $ret += $r1['a'] + $r2['b']; } return $ret; $$; -- 1 + 30 + 2 + 40 = 73 select test_cursor_interleave(); -- an error in the query is reported and the function can run again create function test_cursor_error(int) returns int language plphp as $$ $cursor = spi_query("select 1 / $args[0] as q"); $row = spi_fetchrow($cursor); return $row['q']; $$; select test_cursor_error(1); select test_cursor_error(0); select test_cursor_error(1); -- spi_each: run a callback once per row, streaming over a cursor create function test_each() returns int language plphp as $$ $sum = 0; $n = spi_each("select generate_series(1, 5) as g", function ($row) use (&$sum) { $sum += $row['g']; }); return $n * 1000 + $sum; $$; -- 5 rows, sum 15 select test_each(); -- returning false from the callback stops the iteration early create function test_each_stop() returns int language plphp as $$ return spi_each("select generate_series(1, 1000000) as g", function ($row) { if ($row['g'] >= 3) return false; }); $$; select test_each_stop(); -- an exception thrown by the callback propagates (and the cursor is closed) create function test_each_throw() returns text language plphp as $$ try { spi_each("select generate_series(1, 10) as g", function ($row) { if ($row['g'] == 2) throw new Exception("stop at 2"); }); } catch (Exception $e) { return "caught: " . $e->getMessage(); } return "not reached"; $$; select test_each_throw();