Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return column names from CTE, even if it's empty

I built a dynamic query that returns

  1. Total count of entries in CTE
  2. Paginated entries from CTE
  3. List of column names used in CTE
  4. Requested page size and page number

The beauty of in returning total count + pagination in a single roundtrip. And you can insert any CTE in it (in my case CTEs are built dynamically on backend)

My issue is that when CTE has 0 rows (e.g. where condition is not satisfied) an outer query is empty as well and I can't extract column names. How can I fix it?

with "cte" as (
select...)
select
    (
    select
        array_agg(column_name)
    from
        (
        select
            jsonb_object_keys(to_jsonb(cte)) as column_name) as column_names) as "ColumnNames",
    COUNT(*) over() as "TotalCount",
    (
    select
        array_agg(cte_subquery)
    from
        (
        select
            *
        from
            cte offset 30
        limit 15) as cte_subquery) as "PaginatedEntries",
    3 as "PageNumber",
    15 as "PageSize"
from
    "cte"
limit 1

Fiddle: https://sqlize.online/sql/psql15/e5cd43dff492f9143e725c97cc6e0199/

Expected result

+-------------+------------+------------------+------------+----------+
|  ColumnNames | TotalCount | PaginatedEntries | PageNumber | PageSize |
+--------------+------------+------------------+------------+----------+
| {id,age,name,email} | 0   |         {}       |          3 |       15 |
+--------------+------------+------------------+------------+----------+
like image 380
Nick Farsi Avatar asked Feb 03 '26 11:02

Nick Farsi


1 Answers

You can find column names of all existing relations in information_schema.columns system view. You can get them in a separate CTE, then left join...on true with that (demo).

Since you aim to inject the query and you're interested in whatever columns it turns out to have - which might not correspond to any specific table - you can do a similar thing the same with your current jsonb_object_keys() approach: demo

with 
 cte   as (select * from dummy_table where true)
,cte_1 as (select * from (values (1)) as a(" ") left join cte on true limit 1)  
,cte_column_names("ColumnNames") as 
( select array_agg(column_name)
  from (select jsonb_object_keys(to_jsonb(cte_1)) as column_name
        from cte_1 offset 1) a)
,cte_paginated_entries("PaginatedEntries") as 
(select jsonb_agg(cte_subquery)
     from (select * from cte offset 0 limit 15) as cte_subquery)
select
    cn."ColumnNames"
  , COUNT(*) over() -(case when pe."PaginatedEntries" is null then 1 else 0 end) as "TotalCount" 
  , coalesce(pe."PaginatedEntries",'[]'::jsonb)
  , 3 as "PageNumber"
  , 15 as "PageSize"
from cte_column_names cn 
  left join cte on true 
  left join cte_paginated_entries pe on true
limit 1
  1. In cte_1 a left join with a single dummy value generates a row that starts with that value and keeps all cte fields filled with null. This solves your problem of not having a row at all to read column names from.
  2. That lets you then pass it to your field name aggregation, where offset 1 skips the previously added dummy field.
  3. This leaves your count(*) over () trick that wants to claim there's a non-zero total count, still seeing the row with the column names whenever there were no actual rows.
  4. A case can correct that: even if there was a single column with a null, in that scenario "PaginatedEntries" will not be null but a an array with a null in it, which doesn't match the is null case.
  5. Added coalesce to match your expected output.

You clarified you actually do make sure there's order by that keeps paging order consistent. Note that you still need the state to also stay consistent: you can hold on to the table snapshot by issuing subsequent page queries within the same transaction.

This isn't very pretty but it works the way you intended. Still, you might want to consider using cursors and cursor-returning functions as well as session-specific temp tables, all of which could get you all that info in a single round-trip a bit more cleanly. At the moment, your pagination keeps re-evaluating things it otherwise wouldn't need to.

like image 113
Zegarek Avatar answered Feb 05 '26 05:02

Zegarek



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!