RETURNING causes error: missing FROM-clause entry for table

5k Views Asked by At

I am getting the users data from UUID WHERE empl_user_pub_uuid = 'e2bb39f1f28011eab66c63cb4d9c7a34'.

Since I don't want to make an additional query to fetch additional user data I'm trying to sneak them through the INSERT.

WITH _u AS (
    SELECT
        eu.empl_user_pvt_uuid,
        ee.email,
        ep.name_first
    FROM employees.users eu
    LEFT JOIN (
        SELECT DISTINCT ON (ee.empl_user_pvt_uuid)
            ee.empl_user_pvt_uuid,
            ee.email
        FROM employees.emails ee
        ORDER BY ee.empl_user_pvt_uuid, ee.t DESC
    ) ee ON eu.empl_user_pvt_uuid = ee.empl_user_pvt_uuid
    LEFT JOIN (
        SELECT DISTINCT ON (ep.empl_user_pvt_uuid)
            ep.empl_user_pvt_uuid,
            ep.name_first
        FROM employees.profiles ep
    ) ep ON eu.empl_user_pvt_uuid = ep.empl_user_pvt_uuid
    WHERE empl_user_pub_uuid = 'e2bb39f1f28011eab66c63cb4d9c7a34'
)
INSERT INTO employees.password_resets (empl_pwd_reset_uuid, empl_user_pvt_uuid, t_valid, for_empl_user_pvt_uuid, token)
SELECT 'f70a0346-a077-11eb-bd1a-aaaaaaaaaaaa', '6efc2b7a-f27e-11ea-b66c-de1c405de048', '2021-04-18 19:57:47.111365', _u.empl_user_pvt_uuid, '19d65aea-7c4a-41bc-b580-9d047f1503e6'
FROM _u
RETURNING _u.empl_user_pvt_uuid, _u.email, _u.name_first;

However I get:

[42P01] ERROR: missing FROM-clause entry for table "_u" 
Position: 994

What am I doing wrong?

2

There are 2 best solutions below

0
On BEST ANSWER

It's true, as has been noted, that the RETURNING clause of an INSERT only sees the inserted row. More specifically, quoting the manual here:

The optional RETURNING clause causes INSERT to compute and return value(s) based on each row actually inserted (or updated, if an ON CONFLICT DO UPDATE clause was used). This is primarily useful for obtaining values that were supplied by defaults, such as a serial sequence number. However, any expression using the table's columns is allowed. The syntax of the RETURNING list is identical to that of the output list of SELECT. Only rows that were successfully inserted or updated will be returned. [...]

Bold emphasis mine.
So nothing keeps you from adding a correlated subquery to the RETURNING list:

INSERT INTO employees.password_resets AS ep
       (empl_pwd_reset_uuid                  , empl_user_pvt_uuid                    , t_valid                     , for_empl_user_pvt_uuid, token)
SELECT 'f70a0346-a077-11eb-bd1a-aaaaaaaaaaaa', '6efc2b7a-f27e-11ea-b66c-de1c405de048', '2021-04-18 19:57:47.111365', eu.empl_user_pvt_uuid , '19d65aea-7c4a-41bc-b580-9d047f1503e6'
FROM   employees.users eu
WHERE  empl_user_pub_uuid = 'e2bb39f1f28011eab66c63cb4d9c7a34'
RETURNING for_empl_user_pvt_uuid AS empl_user_pvt_uuid  -- alias to meet your org. query
        , (SELECT email
           FROM   employees.emails
           WHERE  empl_user_pvt_uuid = ep.empl_user_pvt_uuid
           ORDER  BY t DESC  -- NULLS LAST ?
           LIMIT  1
          ) AS email
        , (SELECT name_first
           FROM   employees.profiles
           WHERE  empl_user_pvt_uuid = ep.empl_user_pvt_uuid
           -- ORDER  BY ???
           LIMIT  1
          ) AS name_first;

This is also much more efficient than the query you had (or what was proposed) for multiple reasons.

Plus, possibly most important, this is correct. We use data from the row that has actually been inserted - after inserting it. (See quote at the top!) After possible default values, triggers or rules have been applied. We can be certain that what we see is what's actually in the database (currently).

You have no ORDER BY for profiles.name_first. That's not right. Either there is only one qualifying row, then we need no DISTINCT nor LIMIT 1. Or there can be multiple, then we also need a deterministic ORDER BY to get a deterministic result.

And if emails.t can be NULL, you'll want to add NULLS LAST in the ORDER BY clause. See:

Indexes

Ideally, you have these multicolumn indexes (with columns in this order):

  • users (empl_user_pub_uuid, empl_user_pvt_uuid)
  • emails (empl_user_pvt_uuid, email)
  • profiles (empl_user_pvt_uuid, name_first)

Then, if the tables are vacuumed enough, you get three index-only scans and the whole operation is lightening fast.

Get pre-INSERT values?

If you really want that (which I don't think you do), consider:

0
On

According Postgres Docs about 6.4. Returning Data From Modified Rows :

In an INSERT, the data available to RETURNING is the row as it was inserted.

But here you are trying to return columns from source table instead of destination. Returning will not be able to return columns form _u table rather only from employees.password_resets table's inserted row. But you can write nested cte for insertion and can select data from source table as well. Please try below approach.

WITH _u AS (
    SELECT
        eu.empl_user_pvt_uuid,
        ee.email,
        ep.name_first
    FROM employees.users eu
    LEFT JOIN (
        SELECT DISTINCT ON (ee.empl_user_pvt_uuid)
            ee.empl_user_pvt_uuid,
            ee.email
        FROM employees.emails ee
        ORDER BY ee.empl_user_pvt_uuid, ee.t DESC
    ) ee ON eu.empl_user_pvt_uuid = ee.empl_user_pvt_uuid
    LEFT JOIN (
        SELECT DISTINCT ON (ep.empl_user_pvt_uuid)
            ep.empl_user_pvt_uuid,
            ep.name_first
        FROM employees.profiles ep
    ) ep ON eu.empl_user_pvt_uuid = ep.empl_user_pvt_uuid
    WHERE empl_user_pub_uuid = 'e2bb39f1f28011eab66c63cb4d9c7a34'
), I as

(
    INSERT INTO employees.password_resets (empl_pwd_reset_uuid, empl_user_pvt_uuid, t_valid, for_empl_user_pvt_uuid, token)
    SELECT 'f70a0346-a077-11eb-bd1a-aaaaaaaaaaaa', '6efc2b7a-f27e-11ea-b66c-de1c405de048', '2021-04-18 19:57:47.111365', _u.empl_user_pvt_uuid, '19d65aea-7c4a-41bc-b580-9d047f1503e6'
    FROM _u
)
select _u.empl_user_pvt_uuid, _u.email, _u.name_first from _u