4

I'm trying to return a set of rows after doing UPDATE.

Something like this.

UPDATE Notis new_noti SET notis = '{}'::noti_record_type[] 
FROM (SELECT * FROM Notis WHERE user_id = 2 FOR UPDATE) old_noti 
WHERE old_noti.user_id = new_noti.user_id RETURNING unnest(old_noti.notis);

but postgres complains, rightly so:

set-valued function called in context that cannot accept a set

How am I supposed to go about implementing this?

That is, RETURNING a set of rows from SELECTed array after UPDATE?

I'm aware that a function can achieve this using RETURNS SETOF but rather prefer not to if possible.

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
Daniel Shin
  • 5,086
  • 2
  • 30
  • 53

3 Answers3

4

Use WITH statement:

WITH upd AS (
    UPDATE Notis new_noti SET notis = '{}'::noti_record_type[] 
    FROM (SELECT * FROM Notis WHERE user_id = 2 FOR UPDATE) old_noti 
    WHERE old_noti.user_id = new_noti.user_id RETURNING old_noti.notis
    )
SELECT unnest(notis) FROM upd;
klin
  • 112,967
  • 15
  • 204
  • 232
  • Aha! Thank you so much. Just one more thing. If the noti_record_type is custom type, how can I unwrap that to proper columns by each type key? Currently it returns with single column named `unnest`. – Daniel Shin Aug 28 '15 at 15:15
  • `select (unnest(notis)).col1, (unnest(notis)).col2 from upd` – klin Aug 28 '15 at 15:20
3

Use a data-modifying CTE.
You can use a set-returning function in the SELECT list, but it is cleaner to move it to the FROM list with a LATERAL subquery since Postgres 9.3. Especially if you need to extract multiple columns (from a row type like you commented). It would also be inefficient to call unnest() multiple times.

WITH upd AS (
   UPDATE notis n
   SET    notis = '{}'::noti_record_type[]  -- explicit cast optional
   FROM  (
      SELECT user_id, notis
      FROM   notis
      WHERE  user_id = 2
      FOR    UPDATE
      ) old_n
    WHERE old_n.user_id = n.user_id
    RETURNING old_n.notis
    )
SELECT n.*
FROM   upd u, unnest(u.notis) n;  -- implicit CROSS JOIN LATERAL

If the array can be empty and you want to preserve empty / NULL results use LEFT JOIN LATERAL ... ON true. See:

Also, multiple set-returning functions in the same SELECT can exhibit surprising behavior. Avoid that. This has been sanitized with Postgres 10. See:

Alternative to unnest multiple arrays in parallel before and after Postgres 10:

Related:

Behavior of composite / row values

Postgres has an oddity when assigning a row type (or composite or record type) from a set-returning function to a column list. One might expect that the row-type field is treated as one column and assigned to the respective column, but that is not so. It is decomposed automatically (one row-layer only!) and assigned element-by-element.

So this does not work as expected:

SELECT (my_row).*
FROM   upd u, unnest(u.notis) n(my_row);

But this does (like @klin commented):

SELECT (my_row).*
FROM   upd u, unnest(u.notis) my_row;

Or the simpler version I ended up using:

SELECT n.*
FROM   upd u, unnest(u.notis) n;

Another oddity: A composite (or row) type with a single field is decomposed automatically. Thus, table alias and column alias end up doing the same in the outer SELECT list:

SELECT n FROM unnest(ARRAY[1,2,3]) n;
SELECT n FROM unnest(ARRAY[1,2,3]) n(n);
SELECT n FROM unnest(ARRAY[1,2,3]) t(n);
SELECT t FROM unnest(ARRAY[1,2,3]) t(n);  -- except output column name is "t"

For more than one field, the row-wrapper is preserved:

SELECT t FROM unnest(ARRAY[1,2,3]) WITH ORDINALITY t(n);  -- requires 9.4+

Confused? There is more. For composite types (the case at hand) like:

CREATE TYPE my_type AS (id int, txt text);

While this works as expected:

SELECT n FROM unnest(ARRAY[(1, 'foo')::my_type, (2, 'bar')::my_type]) n;

You are in for a surprise here:

SELECT n FROM unnest(ARRAY[(1, 'foo')::my_type, (2, 'bar')::my_type]) n(n);

And that's the error I had: When providing a column list, Postgres decomposes the row and assigns provided names one-by-one. Referring to n in the SELECT list does not return the composite type, but only the (renamed) first element. I had mistakenly expected the row type and tried to decompose with (my_row).* - which only returns the first element nonetheless.

Then again:

SELECT t FROM unnest(ARRAY[(1, 'foo')::my_type, (2, 'bar')::my_type]) t(n);

(Be aware that the first element has been renamed to "n"!)

With the new form of unnest() taking multiple array arguments (Postgres 9.4+):

SELECT *
FROM   unnest(ARRAY[(1, 'foo')::my_type, (2, 'bar')::my_type]
            , ARRAY[(3, 'baz')::my_type, (4, 'bak')::my_type]) n;

Column aliases only for the first two output columns:

SELECT *
FROM   unnest(ARRAY[(1, 'foo')::my_type, (2, 'bar')::my_type]
            , ARRAY[(3, 'baz')::my_type, (4, 'bak')::my_type]) n(a, b);

Column aliases for all output columns:

SELECT *
FROM   unnest(ARRAY[(1,'foo')::my_type, (2,'bar')::my_type]
            , ARRAY[(3,'baz')::my_type, (4,'bak')::my_type]) n(a,b,c,d);

db<>fiddle here
Old sqlfiddle

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • 2
    Your query will raise an error. Should be `select (my_row).* from upd u, unnest(u.notis) my_row` – klin Aug 29 '15 at 09:13
  • @klin: You are right, sorry I did not test. I am using an even simpler variant now and added an explanation. – Erwin Brandstetter Aug 29 '15 at 17:31
  • @ErwinBrandstetter In your last example. since unnest will generate 4 columns. You only provide 2 column alias/name, then the other two columns will use the default composite type's name. Like `select * from unnest(ARRAY[1,2], ARRAY['foo','bar','baz']) as x(a);` the returned result first column is **a**, the second column is **unnest**. You should add another example: `select * from unnest( array[(1,'foo')::my_type, (2,'bar')::my_type], array[(3,'baz')::my_type,(4,'bak')::my_type] ) n(a,b,c,d);` – jian Mar 21 '22 at 14:56
  • 1
    @Mark: I added your last example for completeness.Updated some details while being at it. The answer is from 2015, originally! – Erwin Brandstetter Mar 22 '22 at 03:23
0

Probably For:

SELECT *
FROM   unnest (ARRAY[(1, 'foo')::my_type, (2, 'bar')::my_type]
             , ARRAY[(3, 'baz')::my_type, (4, 'bak')::my_type]) n(a, b);

Use:

SELECT *
FROM   unnest (ARRAY[(1, 'foo')::text, (2, 'bar')::text]
             , ARRAY[(3, 'baz')::text, (4, 'bak')::text]) WITH ORDINALITY AS t(first_col, second_col);
IncredibleHat
  • 4,000
  • 4
  • 15
  • 27