postgresqlpostgresql-16

Is the order of result_types in a Postgres prepared statement guaranteed to match the column order?


Basically as the title says, I'm trying to figure out if the order of elements in the array in pg_prepared_statements.result_types is guaranteed to match the column order of the results when you do EXECUTE <prepared statement name>;


Solution

  • Yes, it is. Postgres keeps the result structure fixed and constant. There's an adequate comment in src/backend/commands /prepare.c responsible for this:

    /*
     * Given a prepared statement, determine the result tupledesc it will
     * produce.  Returns NULL if the execution will not return tuples.
     *
     * Note: the result is created or copied into current memory context.
     */
    TupleDesc
    FetchPreparedStatementResultDesc(PreparedStatement *stmt)
    {
        /*
         * Since we don't allow prepared statements' result tupdescs to change,
         * there's no need to worry about revalidating the cached plan here.
         */
    

    Further:

        /* Shouldn't find a non-fixed-result cached plan */
        if (!entry->plansource->fixed_result)
            elog(ERROR, "EXECUTE does not support variable-result cached plans");
    

    The pg_prepared_statements view is based on pg_prepared_statement() in that file that pulls out the result types directly from the prepared statements.

    Unlike routines, prepared statements are not subject to overloading and their names have to be unique within a session:

    name
    An arbitrary name given to this particular prepared statement. It must be unique within a single session and is subsequently used to execute or deallocate a previously prepared statement.

    If you're debugging some strange behaviour that feels like your output structure keeps changing, make sure you're really running the prepared statement you're expecting to run.
    One way statements can seemingly share a name is through mixed-case identifiers (they are case-sensitive only when double-quoted):
    demo at db<>fiddle

    prepare "a_statement"(float) as 
    select 'I accept a float and return (text,boolean)' as txt
      , true as b;
    
    prepare "A_statement"(int) as 
    select 'I accept an int and return (text,bigint,json)' as txt
      , 1e11::bigint
      ,'{"a":0}'::json;
    
    execute A_STATEMENT(1);
    execute a_StaTEmEnT(1);
    execute a_statement(1);
    
    txt b
    I accept a float and return (text,boolean) t
    txt b
    I accept a float and return (text,boolean) t
    txt b
    I accept a float and return (text,boolean) t
    execute "A_statement"(1);
    
    txt int8 json
    I accept an integer and return (text,bigint,json) 100000000000 {"a":0}

    If you're trying to use the statement in PL/pgSQL, you might want to also make sure you're running it with

    execute 'execute a_statement($1,$2)' using p1,p2;
    

    Because in that context execute is for dynamic statements, and only once you're in it, you can use the plain SQL execute that's for prepared statements. The doc has a note on that:

    Note
    The PL/pgSQL EXECUTE statement is not related to the EXECUTE SQL statement supported by the PostgreSQL server. The server's EXECUTE statement cannot be used directly within PL/pgSQL functions (and is not needed).

    It is technically possible to accidentally call an overloaded routine that yields a text that happens to be a valid SQL statement that PL/pgSQL execute then executes, yielding something else again. Extremely unlikely and bizarre, but doable:
    demo at db<>fiddle

    prepare"a_statement"(float)as select 'prep statement(float)->(text,boolean)' txt,true b;
    create function a_statement(int)returns text return $$select 1,true,'abc';$$;
    
    do $d$
      declare rec record;
    begin
     execute a_statement(1) into rec;
     create table t1 as select rec::text;
     execute 'execute a_statement(1)' into rec;
     create table t2 as select rec::text;
     create table t3 as execute a_statement(1);
    end $d$;
    
    table t1;
    
    (1,t,abc)
    table t2;
    
    ("prep statement(float)->(text,boolean)",t)
    table t3;
    
    txt b
    prep statement(float)->(text,boolean) t