12

How can we pass an array of (an unlimited amount of) rows (ie, a constant table) as the parameter/argument of a PostgreSQL function?

Here's an idea:

CREATE TYPE foo AS (
    x bigint,
    y smallint,
    z varchar(64)
);

CREATE OR REPLACE FUNCTION bar(bigint, foo[]) RETURNS TABLE(a bigint, x bigint, y smallint, z varchar(64)) AS
$$
    SELECT $1, x, y, z FROM unnest($2);
$$
LANGUAGE SQL;

The below function call works, but is there a way to make it shorter?

SELECT * FROM bar(1, ARRAY[(1,2,'body1'),(2,1,'body2')]::foo[]);

For example, we can't remove the ::foo[] cast, but is there a way to rewrite things so that we can omit it?

Should we be using a variatic argument?

3 Answers 3

11

My Google searches kept leading me here, so I'm going to post an answer that may not match exactly the needs of the OP, but might be helpful to others who see the title How to pass multiple rows to PostgreSQL function?

The OPs original request was for a type:

CREATE TYPE foo AS (
    x bigint,
    y smallint,
    z varchar(64)
);

If you are like me, you may want to pass in the results of a standard SELECT query to a function. So imagine I have a table (rather than a type) created as:

CREATE TABLE foo AS (
    x bigint,
    y smallint,
    z varchar(64)
);

I want to pass to a function the results of:

SELECT * from foo WHERE x = 12345;

The results may be zero or many rows.

According to the postgres docs at https://www.postgresql.org/docs/9.5/static/rowtypes.html creating a table also leads to the creation of a composite type with the same name. Which is helpful, since this automatically handles the CREATE TYPE foo in the original question, which I can now pass in to a function as an array.

Now I can create a function that accepts an array of foo typed values (simplified to focus on what is passed in, and how the records are used, rather than what is returned):

CREATE OR REPLACE FUNCTION bar(someint bigint, foos foo[]) RETURNS ...
LANGUAGE plpgsql
AS $$
DECLARE
    foo_record record;
begin

-- We are going to loop through each composite type value in the array
-- The elements of the composite value are referenced just like 
-- the columns in the original table row
FOREACH foo_record IN ARRAY foos LOOP
  -- do something, maybe like:
  INSERT INTO new_foo (
    x, y, z
  )
  VALUES (
    foo_record.x,
    foo_record.y,
    foo_record.z
  );

END LOOP;

RETURN...
END;
$$;

This function bar(bigint, foo[]) can then be called quite simply with:

SELECT bar(4126521, ARRAY(SELECT * from foo WHERE x = 12345));

which passes in all the rows of a query on the foo table as a foo typed array. The function as we have seen then performs some action against each of those rows.

Although the example is contrived, and perhaps not exactly what the OP was asking, it fits the title of the question and might save others from having to search more to find what they need.

EDIT naming the function arguments makes things easier

Sign up to request clarification or add additional context in comments.

1 Comment

Failed with following error : ERROR: subquery must return only one column LINE 2: SELECT bar(4126521, ARRAY(SELECT * from foo WHERE x = 12345)); ^ SQL state: 42601 Character: 22
3

PostgreSQL doesn't have table-valued variables (yet), so nothing's going to be pretty. Passing arrays is inefficient but will work for reasonable-sized inputs.

For bigger inputs, what often works is to pass a refcursor. It's clumsy, but can be practical for larger data sets, sometimes combined with temp tables.

e.g.

CREATE OR REPLACE FUNCTION bar(i bigint, c refcursor) RETURNS TABLE(a bigint, x bigint, y smallint, z varchar(64)) AS
$$
DECLARE
    cursrow foo;
BEGIN
    LOOP
        FETCH NEXT FROM c INTO cursrow;
        a := i;
        x := cursrow.x;
        y := cursrow.y;
        z := cursrow.z;
        RETURN NEXT;
        IF NOT FOUND THEN
            EXIT;
        END IF;
    END LOOP;
    RETURN;
END;
$$;

usage:

demo=> BEGIN;
BEGIN
demo=> DECLARE "curs1" CURSOR FOR VALUES (1,2,'body1'), (2,1,'body2');
DECLARE CURSOR
craig=> SELECT bar(1, 'curs1');
      bar      
---------------
 (1,1,2,body1)
 (1,2,1,body2)
 (1,,,)
(3 rows)

demo=> COMMIT;
COMMIT

Not beautiful. But then, plpgsql never is. It's a pity it doesn't have row-valued lvalues, as being able to write something like (x, y, z) := cursrow or ROW(x, y, z) := cursrow would make it a bit less ugly.

RETURN NEXT works, but only if you return record not named out parameters or TABLE.

And sadly, you can't use SQL (not plpgsql) FETCH ALL in a subexpression so you cannot write

RETURN QUERY NEXT i, cursrow.* FROM (FETCH ALL FROM c) AS cursrow;

Comments

2

It seems that one of the problems is the using of smallint type which can not be converted implicitly from an int constants. And consider the following:

-- drop function if exists bar(bigint, variadic foo[]);
-- drop type if exists foo;

CREATE TYPE foo AS (
    x bigint,
    y int, -- change type to integer
    z varchar(64)
);

CREATE OR REPLACE FUNCTION bar(bigint, variadic foo[]) RETURNS TABLE(
  a bigint,
  x bigint,
  y int, -- and here
  z varchar(64)) AS
$$
    SELECT $1, x, y, z FROM unnest($2);
$$
LANGUAGE SQL;

-- Voila! It is even simpler then the using of the ARRAY constructor
SELECT * FROM bar(1, (1,2,'body1'), (2,1,'body2'), (3,4,'taddy bear'));

dbfiddle

About variadic parameters

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.