4

In continuing from a previous case that was assisted by @Erwin Brandstetter and @Craig Ringer, I have fixed my code to become as follows. Note, that my function myresult() outputs now text, and not a table (as indeed, as was pointed out in the former case, there is no point in outputting a table object, since we would need to define all its columns ahead, which basically defies the entire purpose):

CREATE OR REPLACE FUNCTION myresult(mytable text, myprefix text)
RETURNS text AS 
$func$
DECLARE
   myoneliner text;
BEGIN
   SELECT INTO myoneliner  
          'SELECT '
        || string_agg(quote_ident(column_name::text), ',' ORDER BY column_name)
        || ' FROM ' || quote_ident(mytable)
   FROM   information_schema.columns
   WHERE  table_name = mytable
   AND    column_name LIKE myprefix||'%'
   AND    table_schema = 'public';  -- schema name; might be another param

   RAISE NOTICE 'My additional text: %', myoneliner;
   RETURN myoneliner;
END
$func$ LANGUAGE plpgsql;

Call:

select myresult('dkj_p_k27ac','enri');   

Upon running the above procedure I get a text string, which is basically a query. I'll refer to it up next as 'oneliner-output', just for simplicity.
The 'oneline-output' looks as follows (I just copy/paste it from the one output cell that I've got into here):

"SELECT enrich_d_dkj_p_k27ac,enrich_lr_dkj_p_k27ac,enrich_r_dkj_p_k27ac FROM dkj_p_k27ac"
  • Note that the double quotes from both sides of the statement were part of the myresult() output. I didn't add them.

I understand much better now the problematic idea of thinking to construct a single function that would both create the 'oneliner-output' AND execute it. I am able to copy/paste the 'oneliner-output' into a new Postgres query window and execute it as a normal query just fine, receiving the desired columns and rows in my Data Output window.
I would like, however, to automate this step, so to avoid the copy/paste step. Is there a way in Postgres to use the text output (the 'oneliner-output') that I receive from myresult() function, and execute it? Can a second function be created that would receive the output of myresult() and use it for executing a query?

Along these lines, while I know that the following scripting (here below) works and actually outputs exactly the desired columns and rows:

-- DEALLOCATE stmt1; -- use this line after the first time 'stmt1' was created
prepare stmt1 as SELECT enrich_d_dkj_p_k27ac,enrich_lr_dkj_p_k27ac,enrich_r_dkj_p_k27ac FROM dkj_p_k27ac;
execute stmt1;
  • I was thinking maybe something like the following scripting could potentially work, after doing the correct tweaking? Not sure how though.

    prepare stmt1 as THE_OUTPUT_OF_myresult();
    execute stmt1;
    

Attempt with a refcursor

CREATE OR REPLACE FUNCTION show_mytable(ref refcursor) RETURNS refcursor AS $$
BEGIN
   OPEN ref FOR SELECT enrich_d_dkj_p_k27ac,enrich_lr_dkj_p_k27ac,enrich_r_dkj_p_k27ac FROM dkj_p_k27ac;   -- Open a cursor 
   RETURN ref;    -- Return the cursor to the caller
END;
$$ LANGUAGE plpgsql;

Call:

BEGIN;
SELECT show_mytable('roy');
FETCH ALL IN "roy"; 

This procedure, actually works and spits out the desired columns and rows, and yet again, I have to provide the exact SELECT statement.

I basically would like to be able and provide it instead as the output of my myresult() function. Something like this:

CREATE OR REPLACE FUNCTION show_mytable(ref refcursor) RETURNS refcursor AS $$
BEGIN
   OPEN ref FOR myresult();   -- Open a cursor 
   RETURN ref;    -- Return the cursor to the caller
END;
$$ LANGUAGE plpgsql;

Call:

BEGIN;
SELECT show_mytable('roy');
FETCH ALL IN "roy"; 
Community
  • 1
  • 1
Roy
  • 723
  • 2
  • 8
  • 21
  • 1
    Also posted to pgsql-general: http://www.postgresql.org/message-id/CAH4KVCCsbXffLQaznYnEKD9WTenhZtwhaE+D9YPTFNZnVDsxTA@mail.gmail.com – Craig Ringer Jan 08 '15 at 00:08

2 Answers2

2

The trick with PREPARE doesn't work, since it does not take a * text string* (a value) like CREATE FUNCTION does, but a valid statement (code).

To convert data into executable code you need to use dynamic SQL, i.e. EXECUTE in a plpgsql function or DO statement. This works without problem as long as the return type does not depend on the outcome of the first function myresult(). Else you are back to catch 22 as outlined in my previous answer:

The crucial part is to declare the return type (row type in this case) somehow. You can create a TABLE, TEMP TABLE or TYPE for the purpose. Or you can use a prepared statement or a refcursor.

Solution with prepared statement

You have been very close. The missing piece of the puzzle is to prepare the generated query with dynamic SQL.

Function to prepare statement dynamically

Create this function once. It's a optimized and safe version of your function myresult():

CREATE OR REPLACE FUNCTION f_prep_query (_tbl regclass, _prefix text)
  RETURNS void AS 
$func$
BEGIN
   IF EXISTS (SELECT 1 FROM pg_prepared_statements WHERE name = 'stmt_dyn') THEN
      DEALLOCATE stmt_dyn;
   END IF;                 -- you my or may not need this safety check 

   EXECUTE (
     SELECT 'PREPARE stmt_dyn AS SELECT '
         || string_agg(quote_ident(attname), ',' ORDER BY attname)
         || ' FROM ' || _tbl
      FROM   pg_catalog.pg_attribute
      WHERE  attrelid = _tbl
      AND    attname LIKE _prefix || '%'
      AND    attnum > 0
      AND    NOT attisdropped
     );
END
$func$  LANGUAGE plpgsql;

I use regclass for the table name parameter _tbl to make it unambiguous and safe against SQLi. Details:

The information schema does not include the oid column of system catalogs, so I switched to pg_catalog.pg_attribute instead of information_schema.columns. That's faster, too. There are pros and cons for this:

If a prepared statement with the name stmt_dyn already existed, PREPARE would raise an exception. If that is acceptable, remove the check on the system view pg_prepared_statements and the following DEALLOCATE.
More sophisticated algorithms are possible to manage multiple prepared statements per session, or take the name of the prepared statement as additional parameter, or even use an MD5 hash of the query string as name, but that's beyond the scope of this question.

Be aware that PREPARE operates outside the scope of transactions, once PREPARE succeeds, the prepared statement exists for the lifetime of the session. If the wrapping transaction is aborted, PREPARE is unaffected. ROLLBACK cannot remove prepared statements.

Dynamic query execution

Two queries, but only one call to the server. And very efficient, too.

SELECT f_prep_query('tbl'::regclass, 'pre'::text);
EXECUTE stmt_dyn;

Simpler and much more efficient for most simple use cases than creating a temp table or a cursor and selecting / fetching from that (which would be other options).

SQL Fiddle.

Community
  • 1
  • 1
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • Hi @Erwin Brandstetter, thanks a lot for your reply again! As you know by now my supposed task - the entire issue is that I DO NOT want to put any constrains on the Table format, as the SQL SELECT command that should be the outcome of myresult() function is supposed to be interchangeable (with different number of columns and different types) every time. I am wondering however if there could be any solution to my problem in Postgres???... I would surely hope that there may be a way to automate the procedure I am after...(!!!) If not, then this is a great disadvantage of the software :( Tnx! – Roy Jan 07 '15 at 22:41
  • Great, I surely hope so.. My last attempt was as indicated in above under "Attempt2", but I'm afraid that this is yet, not the RIGHT way to perform the task, as I can't really replace the SELECT statement by a TEXT string as I wish :( – Roy Jan 07 '15 at 23:44
  • Yeah, or return a refcursor and FETCH it. – Craig Ringer Jan 08 '15 at 00:08
  • @Craig: Yes, like we discussed before. But I like a prepared statement better than a cursor or a temp table. Those need to save a snapshot of the result, which occupies RAM (or worse: disk space), while the prepared statement just saves the prepared statement (for possible reuse). Should perform better in most cases, and allows query parameters, too. Think of `WHERE` conditions ... – Erwin Brandstetter Jan 08 '15 at 00:15
  • 1
    @Roy: What you saw was the effect of your client: pgAdmin (for example) only displays the results from the last query. Other clients display all results. I removed the transaction wrapper altogether, since `PREPARE` doesn't care about transactions anyway. Updated the outdated statement name (among other things) and improved the function a bit more. – Erwin Brandstetter Jan 08 '15 at 21:30
  • @Erwin Brandstetter: Thanks a lot for this explanation, and improvements. I have raised a new question regarding how would it be possible to store the result of this procedure into a new table in the db instead of receiving it at the DataOutput. Would you be able please so advise? Thanks a lot! (http://stackoverflow.com/questions/27845824/saving-the-output-of-a-dynamic-query-that-uses-prepare-statement-into-a-table) – Roy Jan 09 '15 at 13:44
1

I think I found a solution too, using a refcursor.
I would be very glad if you could go through it, check and tell me if you think it is 'Kosher'. Frankly, I am not too sure what I've came up with here, as I am not that familiar with the syntax. But I was rather able to synthesize this using different examples I found on the web. It seems to work for me. I would be very glad if you could articulate this solution for me and for other users - and tell what do you think of it.

First lets create the function that constructs the dynamic SELECT statement:

CREATE OR REPLACE FUNCTION myresult2()
  RETURNS text AS 
$func$
DECLARE
   myoneliner text;
   mytable    text := 'dkj_p_k27ac';
   myprefix   text := 'enri';
BEGIN
   SELECT INTO myoneliner  
          'SELECT '
        || string_agg(quote_ident(column_name::text), ',' ORDER BY column_name)
        || ' FROM ' || quote_ident(mytable)
   FROM   information_schema.columns
   WHERE  table_name = mytable
   AND    column_name LIKE myprefix||'%'
   AND    table_schema = 'public';  -- schema name; might be another param

   -- RAISE NOTICE 'My additional text: %', myoneliner; -- for debugging
   RETURN myoneliner;
END
$func$ LANGUAGE plpgsql;

Now, lets create a second function that can execute the string TEXT output of the first function myresult2():

CREATE OR REPLACE FUNCTION show_mytable(ref refcursor)
  RETURNS refcursor AS
$func$
DECLARE
   mydynamicstatment text := myresult2();
BEGIN       
   OPEN ref FOR EXECUTE mydynamicstatment;
   RETURN ref;  -- return cursor to the caller
END;
$func$ LANGUAGE plpgsql;

Call:

BEGIN;
SELECT show_mytable('roy');
FETCH ALL IN "roy";
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
Roy
  • 723
  • 2
  • 8
  • 21
  • 1
    It's basically a valid implementation of a refcursor solution like Craig suggested. I took the liberty to trim some noise and simplify a bit. One might collapse both functions into one unless the first function is used independently for something else, too. – Erwin Brandstetter Jan 08 '15 at 00:56
  • Thanks @Erwin Brandstetter, could you suggest how to save the output of this SELECT into a table? I tried: BEGIN; SELECT show_mytable('roy') ; FETCH ALL IN "roy" into mydaughtertable; but failed... :( Thanks! – Roy Jan 08 '15 at 01:28
  • 1
    If you want to save the output into a table, everything becomes much simpler, since you can execute that in a single dynamic statement. You don't need a refcursor or prepared statements. Just `EXECUTE 'CREATE TABLE AS ';`. – Erwin Brandstetter Jan 08 '15 at 01:32
  • @Erwin Brandstetter: Indeed, this solution works like a charm: CREATE TABLE mydaughtertable AS SELECT enrich_d_dkj_p_k27ac,enrich_lr_dkj_p_k27ac,enrich_r_dkj_p_k27ac FROM dkj_p_k27ac BUT please remember - I want to use here a dynamic SELECT statement. That means that I can't just use use the hardcore SELECT expression as I did here above, but rather something like: CREATE TABLE mydaughtertable AS myresult('dkj_p_k27ac','enri') and this scripting doesn't work. ERROR: syntax error at or near "myresult" LINE 1: CREATE TABLE mydaughtertable AS myresult('dkj_p_k27ac','enri... – Roy Jan 08 '15 at 01:45
  • 2
    Ask a new question. Comments are not the place. :) – Erwin Brandstetter Jan 08 '15 at 01:47
  • Is that what your first question is all about. Only saw that one just now, you did not get the best possible answer: http://stackoverflow.com/questions/27727672/in-postgres-select-columns-with-the-same-prefix Is that your ultimate objective? – Erwin Brandstetter Jan 08 '15 at 01:56
  • @Erwin Brandstetter: Well, these are two separate questions that I had. The first was to be able and delete from existing table a few columns that share the same prefix. The second question (which we solved today) - was about creating a daughter table that HAVE only selected columns. These indeed may be 'reciprocal' tasks, but it is good to know how to perform each of them, as they in some events it'll be easier to refer to the 'wanted' columns rather than referring to the 'unwanted' columns. – Roy Jan 08 '15 at 02:02
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/68380/discussion-between-roy-and-erwin-brandstetter). – Roy Jan 08 '15 at 02:08
  • continuing question is [here] (http://stackoverflow.com/questions/27831796/saving-the-output-of-a-dynamic-query-into-a-table) – Roy Jan 08 '15 at 02:23
  • Hi @ErwinBrandstetter, I tried implementing your solution. I think that your code, at the execution part (where you SELECT f_prep_query()) should be slightly change for the code to work: It should be: BEGIN; -- optional SELECT f_prep_query('dkj_p_k27ac'::regclass, 'enri'::text); EXECUTE stmt_dyn; --so basically to things to change - 'stmt1' should be 'stmt_dyn'. And remove the COMMIT line. With it, instead of a table output you get: query result with 1 row discarded. query result with 15425 rows discarded. Query returned successfully with no result in 332 ms. Maybe change code accordingly? – Roy Jan 08 '15 at 15:50
  • continuing question regarding how to save the output of a dynamic query that uses prepare statement into a table, is here (http://stackoverflow.com/questions/27845824/saving-the-output-of-a-dynamic-query-that-uses-prepare-statement-into-a-table) – Roy Jan 08 '15 at 17:19