I've hit a snag with the following PL/pgSQL function I wrote:
CREATE OR REPLACE FUNCTION swipl_rpc(IN json_in JSONB, OUT json_out JSONB)
LANGUAGE plpgsql
AS $code_block$
BEGIN
CREATE TEMPORARY TABLE tmp (json_tmp JSONB);
EXECUTE format($bash$ COPY tmp FROM PROGRAM 'ggp-json ''%s''' $bash$, json_in);
SELECT json_tmp FROM tmp INTO json_out;
DROP TABLE tmp;
END;
$code_block$;
It works fine if the returned JSON is a couple of thousand characters long. But a 28618 byte JSON return string resulted in:
ERROR: could not open relation with OID 2948131
It seems to be related to the input string size, but I'm a bit in the dark here.
Not related to the length of the input string. The problem was with saved query plans. Not for the COPY
command but for the subsequent SELECT
& DELETE
.
PL/pgSQL can save query plans for nested SQL statements. Since the function body of a PL/pgSQL function is stored as plain string, no dependencies are registered. If you drop and recreate a table used in a saved plan, that can lead to the error message you reported. See:
CREATE OR REPLACE FUNCTION pg_temp.swipl_rpc(IN json_in jsonb, OUT json_out jsonb)
LANGUAGE plpgsql STRICT AS
$func$
BEGIN
CREATE TEMPORARY TABLE IF NOT EXISTS swipl_rpc_tmp (json_tmp jsonb);
EXECUTE format('COPY pg_temp.swipl_rpc_tmp FROM PROGRAM %L;
SELECT json_tmp FROM pg_temp.swipl_rpc_tmp;'
, 'ggp-json ' || quote_literal(json_in))
INTO json_out;
EXECUTE 'DELETE FROM pg_temp.swipl_rpc_tmp';
END
$func$;
A temporary table is cheaper and more appropriate for your purpose.
Plus, your current solution is open to SQL injection.
The core problem in your question is solved by executing commands involving the (temporary) table dynamically (with EXECUTE
), which never uses saved query plans. So dropping and re-creating the table can't invalidate saved plans.
EXECUTE
can take multiple SQL statements at once. Put the SELECT
command right after COPY
. The INTO
clause moves from SELECT
to the EXECUTE
command, SELECT
must be the final command.
(Since I don't drop the temp table, the reported error should not normally happen any more anyway.)
Use a more specific table name, so that conflicts are unlikely. And schema-qualify explicitly, so it can't be resolved to another table of the same name in the search path by accident if the temp table is missing (pg_temp.swipl_rpc_tmp
). Both good practice.
The temp table only ever has a single row, so use DELETE
without WHERE
. Cheaper.
There are 4 levels of nested quoting in this piece of code:
The literal representing the JSON value json_in
must be quoted like any string. (Escaping any nested single quotes!)
Said string literal is argument to the program ggp-json
, and the two are nested in single quotes within the COPY
command.
The COPY
command, plus the subsequent SELECT
form a string as argument to EXECUTE
.
The EXECUTE
command is nested in the function body of the PL/pgSQL function - another quoted string.
Your solution misses level 1 and pastes json_in
with the format specifier %s
. Any nested single quotes are not escaped and trigger an error (in the best case) or can be abused for SQL injection (in the worst case).
I fixed that by first quoting json_in
with quote_literal()
. Then concatenate with your program name ggp-json
and quote for the next level by using the format specifier %L
to format()
.
See:
(Ab-)using COPY
to execute an external program is only a workaround. A proper C function would be (much) more efficient and could return directly without a (temp) staging table.
Or a function with PL/Perl or PL/Python? (If you have one of those installed.) See: