I built a dynamic query that returns
- Total count of entries in CTE
- Paginated entries from CTE
- List of column names used in CTE
- 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 |
+--------------+------------+------------------+------------+----------+
2
Answers
You can try this:
Here is an explanation.
1. The trick is here to generate a row with empty values in
cte
query. This can be achieved usingunion all
withnull
-values:As the
cte
query still generates one row, the final result will be as follows:2.
TotalCount
is 1 ascte
generates an "empty" row.3. For
'{}'
value inPaginatedEntries
column you can modify your query as follows:For null values it generates
{}
as a result.Here is the demo.
You can find column names of all existing relations in
information_schema.columns
system view. You can get them in a separate CTE, thenleft 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: democte_1
aleft join
with a single dummy value generates a row that starts with that value and keeps allcte
fields filled withnull
. 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 theis 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.