Jooq ERROR: missing FROM-clause entry for table for nested query(sum and group by)

Viewed 1754

I have the following jooq statement executed on Postgres 9.6 :

DSLContext context = DSL.using(connection, POSTGRES_10);
testSafesFundsAllocationRecord record = 
context.select()
       .from(
          select(
            TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID,       
            sum(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT)
               .as(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT))
         .from(TEST_SAFES_FUNDS_ALLOCATION)
         .groupBy(TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID))
       .where(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT.greaterOrEqual(amount))
       .and(TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID.notIn(excludedLedgers))
       .orderBy(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT.asc())
       .limit(1)
       .fetchOne()
       .into(TEST_SAFES_FUNDS_ALLOCATION);

It is translated to the following:

SQL [
select "alias_88420990"."ledger_id", "alias_88420990"."amount" from (
select "public"."test_safes_funds_allocation"."ledger_id", 
sum("public"."test_safes_funds_allocation"."amount") as "amount" from "public"."test_safes_funds_allocation" group by "public"."test_safes_funds_allocation"."ledger_id") as "alias_88420990"
 where ("public"."test_safes_funds_allocation"."amount" >= ? and 1 = 1) order by "public"."test_safes_funds_allocation"."amount" asc limit ?]; 

excludedLedgers -> is an empty string array

and the result is:

org.postgresql.util.PSQLException: ERROR: missing FROM-clause entry for table "test_safes_funds_allocation"
  Position: 334

Can someone tell me what is the problem in the query ? it is nested one as you can see... but I can't seem to understand the problem. The Query does the following: sum all the amount rows of the same ledger id(group by) and then from the output return the minimal row that has amount greater than variable amount , we can exclude specific ledger_id via the array(it is empty in this example).

any help will be greatly appreciated,

Regards

1 Answers

Unnamed derived tables are supported only in a few SQL dialects (e.g. Oracle), not in all (e.g. not in PostgreSQL). This is why jOOQ generates an alias for you for every derived table. Now that your derived table is aliased, you can no longer access its columns using the original table names, but you have to use the alias instead. You can see where this went wrong in your generated SQL query:

select 
  "alias_88420990"."ledger_id", -- These are correctly referenced, because you used
  "alias_88420990"."amount"     -- select(), so jOOQ did the dereferencing for your
from (
  select 
    "public"."test_safes_funds_allocation"."ledger_id", 
    sum("public"."test_safes_funds_allocation"."amount") as "amount"
  from "public"."test_safes_funds_allocation" 
  group by "public"."test_safes_funds_allocation"."ledger_id"
) as "alias_88420990" -- This alias is generated by jOOQ

-- In these predicates, you're referencing the original column name with full qualification
-- when you should be referncing the column from alias_88420990 instead
where ("public"."test_safes_funds_allocation"."amount" >= ? and 1 = 1) 
order by "public"."test_safes_funds_allocation"."amount" asc 
limit ?

Canonical fix

So your jOOQ query could be rewritten as such in order to get the table names right:

// Create a local variable to contain your subquery. Ideally, provide an explicit alias
Table<?> t = table(
    select(
        TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID,       
        sum(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT)
           .as(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT))
   .from(TEST_SAFES_FUNDS_ALLOCATION)
   .groupBy(TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID)).as("t");

// Now, use t everywhere, instead of TEST_SAFES_FUNDS_ALLOCATION
context.select()
       .from(t)
       .where(t.field(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT).greaterOrEqual(amount))
       .and(t.field(TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID).notIn(excludedLedgers))
       .orderBy(t.field(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT).asc())
       .limit(1)
       .fetchOne();

I'm using Table.field(Field) to extract a field from an aliased table, without losing type information.

Improved fix, leveraging existing table types

Given that your derived has two columns whose name also appear in the original table, you could use a "trick"to get more type safety out of the jOOQ API, and thus a more convenient way to dereference the columns. Alias the table first:

// This t reference now has all the column references like the original table
TestSafesFundsAllocation t = TEST_SAFES_FUNDS_ALLOCATION.as("t");

// The subquery is also named "t", but has a different definition
Table<?> subquery = table(
    select(
        TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID,       
        sum(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT)
           .as(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT))
   .from(TEST_SAFES_FUNDS_ALLOCATION)
   .groupBy(TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID)).as(t);

// Now, select again from the subquery, but dereference columns from the aliased table t
context.select()
       .from(subquery)
       .where(t.AMOUNT.greaterOrEqual(amount))
       .and(t.LEDGER_ID.notIn(excludedLedgers))
       .orderBy(t.AMOUNT.asc())
       .limit(1)
       .fetchOne();

Remember, this is a trick, which only works as long as the derived table's columns have the same names and types as the original table which the derived table selects from.

Rewrite your SQL

Derived tables (and common table expressions) are an area where jOOQ's DSL isn't just as powerful as native SQL because jOOQ cannot easily type check a derived table in the usual way. This is why local variables and type unsafe dereferencing has to be used.

Often, this caveat is a good enough reason to avoid derived tables entirely, if this is an option. In your case, it is, you don't need a derived table. A better query (even better in native SQL) would be:

context.select(
          TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID, 
          sum(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT).as(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT))
       .from(TEST_SAFES_FUNDS_ALLOCATION)
       .where(TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID.notIn(excludedLedgers))
       .groupBy(TEST_SAFES_FUNDS_ALLOCATION.LEDGER_ID)
       .having(sum(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT).greaterOrEqual(amount))
       .orderBy(sum(TEST_SAFES_FUNDS_ALLOCATION.AMOUNT).asc())
       .limit(1)
       .fetchOne()
Related