Files
supabase/apps/studio/data/table-editor/table-editor-query-sql.ts
Charis df63ce3658 fix(mssql foreign tables): disallow sort on columns filtered for equality (#40137)
There is an edge case interaction between the Postgres query parser and
MSSQL foreign tables, where the query parser may drop sort clauses that
are redundant with applied filters. This leads to invalid MSSQL syntax,
because the resulting query has a `limit` but no `sort`, and the user
sees a confusing error message.

This PR detects this edge case on MSSQL foreign tables. There are three
cases:
1. The user filters by a column, but there are still other columns
available for sorting. The default search for a sorting column will
leave out the filtered column.
2. The user filters by a column/several columns, and there are no more
columns that can be used for sorting. We stop the query and show an
admonition.
3. The user filters by a column, then tries to sort by the same column.
We stop the query and show an admonition.
2025-11-06 13:14:30 -05:00

274 lines
11 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
)) as primary_keys
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))
join pg_namespace n on c.relnamespace = n.oid
where i.indisprimary
group by i.indrelid
),
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),
'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),
'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 columns c on b.id = c.table_id;
`)
}