Files
supabase/apps/studio/data/table-editor/table-editor-query-sql.ts
Charis 8e705ecdbc fix(export all rows): use cursor pagination if possible (#40536)
Exporting all rows (in CSV, SQL, or JSON format) currently uses offset pagination, which can cause performance problems if the table is large. There is also a correctness problem if the table is being actively updated as the export happens, because the relative row offsets could shift between queries.

Now that composite filters are available in postgres-meta, we can change to using cursor pagination on the primary key (or any non-null unique keys) wherever possible. Where this is not possible, the user will be shown a confirmation dialog explaining the possible performance impact.

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2025-12-08 13:39:10 -05:00

312 lines
13 KiB
TypeScript

import minify from 'pg-minify'
export function getTableEditorSql(id?: number) {
if (!id) return ''
return minify(/* SQL */ `
with base_table_info as (
select
c.oid::int8 as id,
nc.nspname as schema,
c.relname as name,
c.relkind,
c.relrowsecurity as rls_enabled,
c.relforcerowsecurity as rls_forced,
c.relreplident,
c.relowner,
obj_description(c.oid) as comment,
fs.srvname as foreign_server_name,
fdw.fdwname as foreign_data_wrapper_name,
fdw_handler.proname as foreign_data_wrapper_handler
from pg_class c
join pg_namespace nc on nc.oid = c.relnamespace
left join pg_foreign_table ft on ft.ftrelid = c.oid
left join pg_foreign_server fs on fs.oid = ft.ftserver
left join pg_foreign_data_wrapper fdw on fdw.oid = fs.srvfdw
left join pg_proc fdw_handler on fdw.fdwhandler = fdw_handler.oid
where c.oid = ${id}
and not pg_is_other_temp_schema(nc.oid)
and (
pg_has_role(c.relowner, 'USAGE')
or has_table_privilege(
c.oid,
'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'
)
or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')
)
),
table_stats as (
select
b.id,
case
when b.relreplident = 'd' then 'DEFAULT'
when b.relreplident = 'i' then 'INDEX'
when b.relreplident = 'f' then 'FULL'
else 'NOTHING'
end as replica_identity,
pg_total_relation_size(format('%I.%I', b.schema, b.name))::int8 as bytes,
pg_size_pretty(pg_total_relation_size(format('%I.%I', b.schema, b.name))) as size,
pg_stat_get_live_tuples(b.id) as live_rows_estimate,
pg_stat_get_dead_tuples(b.id) as dead_rows_estimate
from base_table_info b
where b.relkind in ('r', 'p')
),
primary_keys as (
select
i.indrelid as table_id,
jsonb_agg(
jsonb_build_object(
'schema', n.nspname,
'table_name', c.relname,
'table_id', i.indrelid::int8,
'name', a.attname
)
order by array_position(i.indkey, a.attnum)
) as primary_keys
from pg_index i
join pg_class c on i.indrelid = c.oid
join pg_namespace n on c.relnamespace = n.oid
join pg_attribute a on a.attrelid = c.oid and a.attnum = any(i.indkey)
where i.indisprimary
group by i.indrelid
),
index_cols as (
select
i.indrelid as table_id,
i.indkey,
array_agg(
a.attname
order by array_position(i.indkey, a.attnum)
) as columns
from pg_index i
join pg_class c on i.indrelid = c.oid
join pg_attribute a on a.attrelid = c.oid
and a.attnum = any(i.indkey)
where i.indisunique
and i.indisprimary = false
group by i.indrelid, i.indkey
),
unique_indexes as (
select
ic.table_id,
jsonb_agg(
jsonb_build_object(
'schema', n.nspname,
'table_name', c.relname,
'table_id', ic.table_id::int8,
'columns', ic.columns
)
) as unique_indexes
from index_cols ic
join pg_class c on c.oid = ic.table_id
join pg_namespace n on n.oid = c.relnamespace
group by ic.table_id
),
relationships as (
select
c.conrelid as source_id,
c.confrelid as target_id,
jsonb_build_object(
'id', c.oid::int8,
'constraint_name', c.conname,
'deletion_action', c.confdeltype,
'update_action', c.confupdtype,
'source_schema', nsa.nspname,
'source_table_name', csa.relname,
'source_column_name', sa.attname,
'target_table_schema', nta.nspname,
'target_table_name', cta.relname,
'target_column_name', ta.attname
) as rel_info
from pg_constraint c
join pg_class csa on c.conrelid = csa.oid
join pg_namespace nsa on csa.relnamespace = nsa.oid
join pg_attribute sa on (sa.attrelid = c.conrelid and sa.attnum = any(c.conkey))
join pg_class cta on c.confrelid = cta.oid
join pg_namespace nta on cta.relnamespace = nta.oid
join pg_attribute ta on (ta.attrelid = c.confrelid and ta.attnum = any(c.confkey))
where c.contype = 'f'
),
columns as (
select
a.attrelid as table_id,
jsonb_agg(jsonb_build_object(
'id', (a.attrelid || '.' || a.attnum),
'table_id', c.oid::int8,
'schema', nc.nspname,
'table', c.relname,
'ordinal_position', a.attnum,
'name', a.attname,
'default_value', case
when a.atthasdef then pg_get_expr(ad.adbin, ad.adrelid)
else null
end,
'data_type', case
when t.typtype = 'd' then
case
when bt.typelem <> 0::oid and bt.typlen = -1 then 'ARRAY'
when nbt.nspname = 'pg_catalog' then format_type(t.typbasetype, null)
else 'USER-DEFINED'
end
else
case
when t.typelem <> 0::oid and t.typlen = -1 then 'ARRAY'
when nt.nspname = 'pg_catalog' then format_type(a.atttypid, null)
else 'USER-DEFINED'
end
end,
'format', case
when t.typtype = 'e' then
case
when nt.nspname <> 'public' then concat(nt.nspname, '.', coalesce(bt.typname, t.typname))
else coalesce(bt.typname, t.typname)
end
else
coalesce(bt.typname, t.typname)
end,
'is_identity', a.attidentity in ('a', 'd'),
'identity_generation', case a.attidentity
when 'a' then 'ALWAYS'
when 'd' then 'BY DEFAULT'
else null
end,
'is_generated', a.attgenerated in ('s'),
'is_nullable', not (a.attnotnull or t.typtype = 'd' and t.typnotnull),
'is_updatable', (
b.relkind in ('r', 'p') or
(b.relkind in ('v', 'f') and pg_column_is_updatable(b.id, a.attnum, false))
),
'is_unique', uniques.table_id is not null,
'check', check_constraints.definition,
'comment', col_description(c.oid, a.attnum),
'enums', coalesce(
(
select jsonb_agg(e.enumlabel order by e.enumsortorder)
from pg_catalog.pg_enum e
where e.enumtypid = coalesce(bt.oid, t.oid)
or e.enumtypid = coalesce(bt.typelem, t.typelem)
),
'[]'::jsonb
)
) order by a.attnum) as columns
from pg_attribute a
join base_table_info b on a.attrelid = b.id
join pg_class c on a.attrelid = c.oid
join pg_namespace nc on c.relnamespace = nc.oid
left join pg_attrdef ad on (a.attrelid = ad.adrelid and a.attnum = ad.adnum)
join pg_type t on a.atttypid = t.oid
join pg_namespace nt on t.typnamespace = nt.oid
left join pg_type bt on (t.typtype = 'd' and t.typbasetype = bt.oid)
left join pg_namespace nbt on bt.typnamespace = nbt.oid
left join (
select
conrelid as table_id,
conkey[1] as ordinal_position
from pg_catalog.pg_constraint
where contype = 'u' and cardinality(conkey) = 1
group by conrelid, conkey[1]
) as uniques on uniques.table_id = a.attrelid and uniques.ordinal_position = a.attnum
left join (
select distinct on (conrelid, conkey[1])
conrelid as table_id,
conkey[1] as ordinal_position,
substring(
pg_get_constraintdef(oid, true),
8,
length(pg_get_constraintdef(oid, true)) - 8
) as definition
from pg_constraint
where contype = 'c' and cardinality(conkey) = 1
order by conrelid, conkey[1], oid asc
) as check_constraints on check_constraints.table_id = a.attrelid
and check_constraints.ordinal_position = a.attnum
where a.attnum > 0
and not a.attisdropped
group by a.attrelid
)
select
case b.relkind
when 'r' then jsonb_build_object(
'entity_type', b.relkind,
'id', b.id,
'schema', b.schema,
'name', b.name,
'rls_enabled', b.rls_enabled,
'rls_forced', b.rls_forced,
'replica_identity', ts.replica_identity,
'bytes', ts.bytes,
'size', ts.size,
'live_rows_estimate', ts.live_rows_estimate,
'dead_rows_estimate', ts.dead_rows_estimate,
'comment', b.comment,
'primary_keys', coalesce(pk.primary_keys, '[]'::jsonb),
'unique_indexes', coalesce(ui.unique_indexes, '[]'::jsonb),
'relationships', coalesce(
(select jsonb_agg(r.rel_info)
from relationships r
where r.source_id = b.id or r.target_id = b.id),
'[]'::jsonb
),
'columns', coalesce(c.columns, '[]'::jsonb)
)
when 'p' then jsonb_build_object(
'entity_type', b.relkind,
'id', b.id,
'schema', b.schema,
'name', b.name,
'rls_enabled', b.rls_enabled,
'rls_forced', b.rls_forced,
'replica_identity', ts.replica_identity,
'bytes', ts.bytes,
'size', ts.size,
'live_rows_estimate', ts.live_rows_estimate,
'dead_rows_estimate', ts.dead_rows_estimate,
'comment', b.comment,
'primary_keys', coalesce(pk.primary_keys, '[]'::jsonb),
'unique_indexes', coalesce(ui.unique_indexes, '[]'::jsonb),
'relationships', coalesce(
(select jsonb_agg(r.rel_info)
from relationships r
where r.source_id = b.id or r.target_id = b.id),
'[]'::jsonb
),
'columns', coalesce(c.columns, '[]'::jsonb)
)
when 'v' then jsonb_build_object(
'entity_type', b.relkind,
'id', b.id,
'schema', b.schema,
'name', b.name,
'is_updatable', (pg_relation_is_updatable(b.id, false) & 20) = 20,
'comment', b.comment,
'columns', coalesce(c.columns, '[]'::jsonb)
)
when 'm' then jsonb_build_object(
'entity_type', b.relkind,
'id', b.id,
'schema', b.schema,
'name', b.name,
'is_populated', true,
'comment', b.comment,
'columns', coalesce(c.columns, '[]'::jsonb)
)
when 'f' then jsonb_build_object(
'entity_type', b.relkind,
'id', b.id,
'schema', b.schema,
'name', b.name,
'comment', b.comment,
'foreign_server_name', b.foreign_server_name,
'foreign_data_wrapper_name', b.foreign_data_wrapper_name,
'foreign_data_wrapper_handler', b.foreign_data_wrapper_handler,
'columns', coalesce(c.columns, '[]'::jsonb)
)
end as entity
from base_table_info b
left join table_stats ts on b.id = ts.id
left join primary_keys pk on b.id = pk.table_id
left join unique_indexes ui on b.id = ui.table_id
left join columns c on b.id = c.table_id;
`)
}