I built a dynamic query that returns
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 |
+--------------+------------+------------------+------------+----------+
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
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.offset 1 skips the previously added dummy field.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.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.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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With