Chore/shift manual queries into pg meta 03 (#43951)

## Context

Shifting more dashboard queries into pg-meta so that we centralize all
manually written queries in one place
Having them in packages/pg-meta also allows us to write tests for them

## To test

Just needs a smoke test on
- Table Editor
  - Fetching entities
  - Viewing definition 
- SQL Editor
  - View ongoing queries
  - Abort queries
- Integrations
  - Queues
- Database
  - Migrations
  -Triggers (Updating)
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-03-19 18:31:46 +08:00
1 parent 3ae9f2d663
commit be26feb9ba
53 files changed
+1537 -1578

No files matched your search

@@ -1,4 +1,4 @@
import { THRESHOLD_COUNT } from '@supabase/pg-meta/src/sql/studio/get-count-estimate'
import { THRESHOLD_COUNT } from '@supabase/pg-meta'
import { keepPreviousData } from '@tanstack/react-query'
import { useParams } from 'common'
import { useTableFilter } from 'components/grid/hooks/useTableFilter'
@@ -1,4 +1,4 @@
import { THRESHOLD_COUNT } from '@supabase/pg-meta/src/query/table-row-query'
import { THRESHOLD_COUNT } from '@supabase/pg-meta'
import { keepPreviousData } from '@tanstack/react-query'
import { useParams } from 'common'
import { useTableFilter } from 'components/grid/hooks/useTableFilter'
@@ -1,5 +1,4 @@
import { OptimizedSearchColumns } from '@supabase/pg-meta'
import { THRESHOLD_COUNT } from '@supabase/pg-meta/src/sql/studio/get-count-estimate'
import { THRESHOLD_COUNT, type OptimizedSearchColumns } from '@supabase/pg-meta'
import { keepPreviousData } from '@tanstack/react-query'
import { useParams } from 'common'
import { formatEstimatedCount } from 'components/grid/components/footer/pagination/Pagination.utils'
@@ -1,10 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { QUEUES_SCHEMA } from '@supabase/pg-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
import { DocsButton } from 'components/ui/DocsButton'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import {
@@ -16,16 +12,16 @@ import { InlineLink } from 'components/ui/InlineLink'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation'
import { useQueuesExposePostgrestStatusQuery } from 'data/database-queues/database-queues-expose-postgrest-status-query'
import {
QUEUES_SCHEMA,
useDatabaseQueueToggleExposeMutation,
} from 'data/database-queues/database-queues-toggle-postgrest-mutation'
import { useDatabaseQueueToggleExposeMutation } from 'data/database-queues/database-queues-toggle-postgrest-mutation'
import { useDatabaseQueuesVersionQuery } from 'data/database-queues/database-queues-version-query'
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
import { useTablesQuery } from 'data/tables/tables-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL, IS_PLATFORM } from 'lib/constants'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Form_Shadcn_,
@@ -37,6 +33,7 @@ import {
import { Admonition } from 'ui-patterns'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { z } from 'zod'
// [Joshen] Not convinced with the UI and layout but getting the functionality out first
@@ -1,9 +1,8 @@
import { getQueuesExposePostgrestStatusSQL } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import minify from 'pg-minify'
import { executeSql } from 'data/sql/execute-sql-query'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { QUEUES_SCHEMA } from './database-queues-toggle-postgrest-mutation'
import { databaseQueuesKeys } from './keys'
export type DatabaseQueuesVariables = {
@@ -11,21 +10,17 @@ export type DatabaseQueuesVariables = {
connectionString?: string | null
}
// [Joshen] Check if all the relevant functions exist to indicate whether PGMQ has been exposed through PostgREST
const queueSqlQuery = minify(/**SQL */ `
SELECT exists (select schema_name FROM information_schema.schemata WHERE schema_name = '${QUEUES_SCHEMA}');
`)
export async function getDatabaseQueuesExposePostgrestStatus({
projectRef,
connectionString,
}: DatabaseQueuesVariables) {
if (!projectRef) throw new Error('Project ref is required')
const sql = getQueuesExposePostgrestStatusSQL()
const { result } = await executeSql({
projectRef,
connectionString,
sql: queueSqlQuery,
sql,
})
return result[0].exists as boolean
}
@@ -1,11 +1,11 @@
import { getExposeQueuesSQL, HIDE_QUEUES_FROM_POSTGREST_SQL } from '@supabase/pg-meta'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import minify from 'pg-minify'
import { toast } from 'sonner'
import { databaseKeys } from 'data/database/keys'
import { executeSql } from 'data/sql/execute-sql-query'
import { isGreaterThanOrEqual } from 'lib/semver'
import { toast } from 'sonner'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { databaseQueuesKeys } from './keys'
export type DatabaseQueueExposePostgrestVariables = {
@@ -17,221 +17,6 @@ export type DatabaseQueueExposePostgrestVariables = {
}
const CONDITIONAL_READ_SIGNATURE_PGMQ_VERSION = '1.5.0'
export const QUEUES_SCHEMA = 'pgmq_public'
const getExposeQueuesSQL = (pgmqVersion: string) => {
const conditionalJsonb = isGreaterThanOrEqual(
pgmqVersion,
CONDITIONAL_READ_SIGNATURE_PGMQ_VERSION
)
? `, conditional := '{}'::jsonb`
: ''
const jsonBArg = isGreaterThanOrEqual(pgmqVersion, CONDITIONAL_READ_SIGNATURE_PGMQ_VERSION)
? `, jsonb`
: ''
return minify(/* SQL */ `
create schema if not exists ${QUEUES_SCHEMA};
grant usage on schema ${QUEUES_SCHEMA} to postgres, anon, authenticated, service_role;
create or replace function ${QUEUES_SCHEMA}.pop(
queue_name text
)
returns setof pgmq.message_record
language plpgsql
set search_path = ''
as $$
begin
return query
select *
from pgmq.pop(
queue_name := queue_name
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.pop(queue_name text) is 'Retrieves and locks the next message from the specified queue.';
create or replace function ${QUEUES_SCHEMA}.send(
queue_name text,
message jsonb,
sleep_seconds integer default 0 -- renamed from 'delay'
)
returns setof bigint
language plpgsql
set search_path = ''
as $$
begin
return query
select *
from pgmq.send(
queue_name := queue_name,
msg := message,
delay := sleep_seconds
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.send(queue_name text, message jsonb, sleep_seconds integer) is 'Sends a message to the specified queue, optionally delaying its availability by a number of seconds.';
create or replace function ${QUEUES_SCHEMA}.send_batch(
queue_name text,
messages jsonb[],
sleep_seconds integer default 0 -- renamed from 'delay'
)
returns setof bigint
language plpgsql
set search_path = ''
as $$
begin
return query
select *
from pgmq.send_batch(
queue_name := queue_name,
msgs := messages,
delay := sleep_seconds
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.send_batch(queue_name text, messages jsonb[], sleep_seconds integer) is 'Sends a batch of messages to the specified queue, optionally delaying their availability by a number of seconds.';
create or replace function ${QUEUES_SCHEMA}.archive(
queue_name text,
message_id bigint
)
returns boolean
language plpgsql
set search_path = ''
as $$
begin
return
pgmq.archive(
queue_name := queue_name,
msg_id := message_id
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.archive(queue_name text, message_id bigint) is 'Archives a message by moving it from the queue to a permanent archive.';
create or replace function ${QUEUES_SCHEMA}.delete(
queue_name text,
message_id bigint
)
returns boolean
language plpgsql
set search_path = ''
as $$
begin
return
pgmq.delete(
queue_name := queue_name,
msg_id := message_id
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.delete(queue_name text, message_id bigint) is 'Permanently deletes a message from the specified queue.';
create or replace function ${QUEUES_SCHEMA}.read(
queue_name text,
sleep_seconds integer,
n integer
)
returns setof pgmq.message_record
language plpgsql
set search_path = ''
as $$
begin
return query
select *
from pgmq.read(
queue_name := queue_name,
vt := sleep_seconds,
qty := n ${conditionalJsonb}
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.read(queue_name text, sleep_seconds integer, n integer) is 'Reads up to "n" messages from the specified queue with an optional "sleep_seconds" (visibility timeout).';
-- Grant execute permissions on wrapper functions to roles
grant execute on function ${QUEUES_SCHEMA}.pop(text) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.pop(text) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.send(text, jsonb, integer) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.send(text, jsonb, integer) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.send_batch(text, jsonb[], integer) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.send_batch(text, jsonb[], integer) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.archive(text, bigint) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.archive(text, bigint) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.delete(text, bigint) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.delete(text, bigint) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.read(text, integer, integer) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.read(text, integer, integer ${jsonBArg}) to postgres, service_role, anon, authenticated;
-- For the service role, we want full access
-- Grant permissions on existing tables
grant all privileges on all tables in schema pgmq to postgres, service_role;
-- Ensure service_role has permissions on future tables
alter default privileges in schema pgmq grant all privileges on tables to postgres, service_role;
grant usage on schema pgmq to postgres, anon, authenticated, service_role;
/*
Grant access to sequences to API roles by default. Existing table permissions
continue to enforce insert restrictions. This is necessary to accommodate the
on-backup hook that rebuild queue table primary keys to avoid a pg_dump segfault.
This can be removed once logical backups are completely retired.
*/
grant usage, select, update
on all sequences in schema pgmq
to anon, authenticated, service_role;
alter default privileges in schema pgmq
grant usage, select, update
on sequences
to anon, authenticated, service_role;
`)
}
const HIDE_QUEUES_FROM_POSTGREST_SQL = minify(/* SQL */ `
drop function if exists
${QUEUES_SCHEMA}.pop(queue_name text),
${QUEUES_SCHEMA}.send(queue_name text, message jsonb, sleep_seconds integer),
${QUEUES_SCHEMA}.send_batch(queue_name text, message jsonb[], sleep_seconds integer),
${QUEUES_SCHEMA}.archive(queue_name text, message_id bigint),
${QUEUES_SCHEMA}.delete(queue_name text, message_id bigint),
${QUEUES_SCHEMA}.read(queue_name text, sleep integer, n integer)
;
-- Revoke execute permissions on inner pgmq functions to roles (inverse of enabling)
do $$
begin
if exists (select 1 from pg_namespace where nspname = 'pgmq') then
-- Revoke privileges on the schema itself
revoke all on schema pgmq from anon, authenticated, service_role;
-- Revoke default privileges for future objects
alter default privileges in schema pgmq revoke all on tables from anon, authenticated, service_role;
alter default privileges in schema pgmq revoke all on sequences from anon, authenticated, service_role;
alter default privileges in schema pgmq revoke all on functions from anon, authenticated, service_role;
end if;
end $$;
drop schema if exists ${QUEUES_SCHEMA};
`)
export async function toggleQueuesExposurePostgrest({
projectRef,
@@ -239,7 +24,11 @@ export async function toggleQueuesExposurePostgrest({
connectionString,
enable,
}: DatabaseQueueExposePostgrestVariables) {
const sql = enable ? getExposeQueuesSQL(pgmqVersion) : HIDE_QUEUES_FROM_POSTGREST_SQL
const isNewerPgmqversion = isGreaterThanOrEqual(
pgmqVersion,
CONDITIONAL_READ_SIGNATURE_PGMQ_VERSION
)
const sql = enable ? getExposeQueuesSQL({ isNewerPgmqversion }) : HIDE_QUEUES_FROM_POSTGREST_SQL
const { result } = await executeSql({
projectRef,
@@ -1,11 +1,11 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { getDatabaseTriggerUpdateSQL } from '@supabase/pg-meta'
import { PGTrigger, PGTriggerCreate } from '@supabase/pg-meta/src/pg-meta-triggers'
import { PostgresTrigger } from '@supabase/postgres-meta'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { executeSql } from 'data/sql/execute-sql-query'
import { quoteLiteral } from 'lib/pg-format'
import { toast } from 'sonner'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { databaseTriggerKeys } from './keys'
// [Joshen] Writing this query within FE as the PATCH endpoint from pg-meta only supports updating
@@ -19,22 +19,6 @@ export type DatabaseTriggerUpdateVariables = {
updatedTrigger: PGTriggerCreate & Pick<PGTrigger, 'enabled_mode'>
}
export function getDatabaseTriggerUpdateSQL({
originalTrigger,
updatedTrigger,
}: Pick<DatabaseTriggerUpdateVariables, 'originalTrigger' | 'updatedTrigger'>) {
const { name, activation, events, schema, table, function_schema, function_name, function_args } =
updatedTrigger
return /* SQL */ `
BEGIN;
DROP TRIGGER "${originalTrigger.name}" ON "${originalTrigger.schema}"."${originalTrigger.table}";
CREATE TRIGGER "${name}" ${activation} ${events.join(' OR ')} ON "${schema}"."${table}"
FOR EACH ROW EXECUTE FUNCTION
"${function_schema}"."${function_name}"(${function_args?.map(quoteLiteral).join(',') ?? ''});
COMMIT;
`.trim()
}
export async function updateDatabaseTrigger({
projectRef,
connectionString,
+5 -29
View File
@@ -1,7 +1,9 @@
import { getTableConstraintsSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { UseCustomQueryOptions } from 'types'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
import { UseCustomQueryOptions } from 'types'
type GetTableConstraintsVariables = {
id?: number
@@ -22,34 +24,6 @@ export enum CONSTRAINT_TYPE {
EXCLUSION_CONSTRAINT = 'x',
}
export const getTableConstraintsSql = ({ id }: GetTableConstraintsVariables) => {
const sql = /* SQL */ `
with table_info as (
select
n.nspname::text as schema,
c.relname::text as name,
to_regclass(concat('"', n.nspname, '"."', c.relname, '"')) as regclass
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.oid = ${id}
)
select
con.oid as id,
con.conname as name,
con.contype as type
from pg_catalog.pg_constraint con
inner join pg_catalog.pg_class rel
on rel.oid = con.conrelid
inner join pg_catalog.pg_namespace nsp
on nsp.oid = connamespace
inner join table_info ti
on ti.schema = nsp.nspname
and ti.name = rel.relname;
`.trim()
return sql
}
export type TableConstraintsVariables = GetTableConstraintsVariables & {
projectRef?: string
connectionString?: string | null
@@ -62,6 +36,8 @@ export async function getTableConstraints(
{ projectRef, connectionString, id }: TableConstraintsVariables,
signal?: AbortSignal
) {
if (!id) throw new Error('Table ID is required')
const sql = getTableConstraintsSql({ id })
const { result } = await executeSql(
{ projectRef, connectionString, sql, queryKey: ['table-constraints', id] },
@@ -1,15 +1,9 @@
import { getDatabaseSizeSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
import { UseCustomQueryOptions } from 'types'
export const getDatabaseSizeSql = () => {
const sql = /* SQL */ `
select sum(pg_database_size(pg_database.datname))::bigint as db_size from pg_database;
`.trim()
return sql
}
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
export type DatabaseSizeVariables = {
projectRef?: string
@@ -1,708 +0,0 @@
import minify from 'pg-minify'
// Derived from https://github.com/MichaelDBA/pg_get_tabledef
// NOTE: when updating, \n must be replaced with \\n in the SQL below
// public.x is replaced with pg_temp.x
// REMOVE (as it breaks the encoding for some reason):
// SELECT REPLACE(REPLACE(setting, '"$user"', '$user'), '$user', '"$user"') INTO search_path_old
// FROM pg_settings
// WHERE name = 'search_path';
export const CREATE_PG_GET_TABLEDEF_SQL = minify(
/* SQL */ `
DROP TYPE IF EXISTS pg_temp.tabledefs CASCADE;
CREATE TYPE pg_temp.tabledefs AS ENUM ('PKEY_INTERNAL','PKEY_EXTERNAL','FKEYS_INTERNAL', 'FKEYS_EXTERNAL', 'COMMENTS', 'FKEYS_NONE', 'INCLUDE_TRIGGERS', 'NO_TRIGGERS');
-- SELECT * FROM pg_temp.pg_get_coldef('sample','orders','id');
-- DROP FUNCTION pg_temp.pg_get_coldef(text,text,text,boolean);
CREATE OR REPLACE FUNCTION pg_temp.pg_get_coldef(
in_schema text,
in_table text,
in_column text,
oldway boolean default False
)
RETURNS text
LANGUAGE plpgsql VOLATILE
AS
$$
DECLARE
v_coldef text;
v_dt1 text;
v_dt2 text;
v_dt3 text;
v_nullable boolean;
v_position int;
v_identity text;
v_generated text;
v_hasdflt boolean;
v_dfltexpr text;
BEGIN
IF oldway THEN
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum;
-- RAISE NOTICE 'DEBUG: oldway=%',v_coldef;
ELSE
-- a.attrelid::regclass::text, a.attname
SELECT CASE WHEN a.atttypid = ANY ('{int,int8,int2}'::regtype[]) AND EXISTS (SELECT FROM pg_attrdef ad WHERE ad.adrelid = a.attrelid AND ad.adnum = a.attnum AND
pg_get_expr(ad.adbin, ad.adrelid) = 'nextval(''' || (pg_get_serial_sequence (a.attrelid::regclass::text, a.attname))::regclass || '''::regclass)') THEN CASE a.atttypid
WHEN 'int'::regtype THEN 'serial' WHEN 'int8'::regtype THEN 'bigserial' WHEN 'int2'::regtype THEN 'smallserial' END ELSE format_type(a.atttypid, a.atttypmod) END AS data_type
INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum;
-- RAISE NOTICE 'DEBUG: newway=%',v_coldef;
-- Issue#24: not implemented yet
-- might replace with this below to do more detailed parsing...
-- SELECT a.atttypid::regtype AS dt1, format_type(a.atttypid, a.atttypmod) as dt2, t.typname as dt3, CASE WHEN not(a.attnotnull) THEN True ELSE False END AS nullable,
-- a.attnum, a.attidentity, a.attgenerated, a.atthasdef, pg_get_expr(ad.adbin, ad.adrelid) dfltexpr
-- INTO v_dt1, v_dt2, v_dt3, v_nullable, v_position, v_identity, v_generated, v_hasdflt, v_dfltexpr
-- FROM pg_attribute a JOIN pg_class c ON (a.attrelid = c.oid) JOIN pg_type t ON (a.atttypid = t.oid) LEFT JOIN pg_attrdef ad ON (a.attrelid = ad.adrelid AND a.attnum = ad.adnum)
-- WHERE c.relkind in ('r','p') AND a.attnum > 0 AND NOT a.attisdropped AND c.relnamespace::regnamespace::text = in_schema AND c.relname = in_table AND a.attname = in_column;
-- RAISE NOTICE 'schema=% table=% column=% dt1=% dt2=% dt3=% nullable=% pos=% identity=% generated=% HasDefault=% DeftExpr=%', in_schema, in_table, in_column, v_dt1,v_dt2,v_dt3,v_nullable,v_position,v_identity,v_generated,v_hasdflt,v_dfltexpr;
END IF;
RETURN v_coldef;
END;
$$;
-- SELECT * FROM pg_temp.pg_get_tabledef('sample', 'address', false);
DROP FUNCTION IF EXISTS pg_temp.pg_get_tabledef(character varying,character varying,boolean,tabledefs[]);
CREATE OR REPLACE FUNCTION pg_temp.pg_get_tabledef(
in_schema varchar,
in_table varchar,
_verbose boolean,
VARIADIC arr pg_temp.tabledefs[] DEFAULT '{}':: pg_temp.tabledefs[]
)
RETURNS text
LANGUAGE plpgsql VOLATILE
AS
$$
DECLARE
v_qualified text := '';
v_table_ddl text;
v_table_oid int;
v_colrec record;
v_constraintrec record;
v_trigrec record;
v_indexrec record;
v_rec record;
v_constraint_name text;
v_constraint_def text;
v_pkey_def text := '';
v_fkey_def text := '';
v_fkey_defs text := '';
v_trigger text := '';
v_partition_key text := '';
v_partbound text;
v_parent text;
v_parent_schema text;
v_persist text;
v_temp text := '';
v_temp2 text;
v_relopts text;
v_tablespace text;
v_pgversion int;
bSerial boolean;
bPartition boolean;
bInheritance boolean;
bRelispartition boolean;
constraintarr text[] := '{}';
constraintelement text;
bSkip boolean;
bVerbose boolean := False;
v_cnt1 integer;
v_cnt2 integer;
search_path_old text := '';
search_path_new text := '';
v_partial boolean;
v_pos integer;
-- assume defaults for ENUMs at the getgo
pkcnt int := 0;
fkcnt int := 0;
trigcnt int := 0;
cmtcnt int := 0;
pktype pg_temp.tabledefs := 'PKEY_INTERNAL';
fktype pg_temp.tabledefs := 'FKEYS_INTERNAL';
trigtype pg_temp.tabledefs := 'NO_TRIGGERS';
arglen integer;
vargs text;
avarg pg_temp.tabledefs;
-- exception variables
v_ret text;
v_diag1 text;
v_diag2 text;
v_diag3 text;
v_diag4 text;
v_diag5 text;
v_diag6 text;
BEGIN
SET client_min_messages = 'notice';
IF _verbose THEN bVerbose = True; END IF;
-- v17 fix: handle case-sensitive
-- v_qualified = in_schema || '.' || in_table;
arglen := array_length($4, 1);
IF arglen IS NULL THEN
-- nothing to do, so assume defaults
NULL;
ELSE
-- loop thru args
-- IF 'NO_TRIGGERS' = ANY ($4)
-- select array_to_string($4, ',', '***') INTO vargs;
IF bVerbose THEN RAISE NOTICE 'arguments=%', $4; END IF;
FOREACH avarg IN ARRAY $4 LOOP
IF bVerbose THEN RAISE NOTICE 'arg=%', avarg; END IF;
IF avarg = 'FKEYS_INTERNAL' OR avarg = 'FKEYS_EXTERNAL' OR avarg = 'FKEYS_NONE' THEN
fkcnt = fkcnt + 1;
fktype = avarg;
ELSEIF avarg = 'INCLUDE_TRIGGERS' OR avarg = 'NO_TRIGGERS' THEN
trigcnt = trigcnt + 1;
trigtype = avarg;
ELSEIF avarg = 'PKEY_EXTERNAL' THEN
pkcnt = pkcnt + 1;
pktype = avarg;
ELSEIF avarg = 'COMMENTS' THEN
cmtcnt = cmtcnt + 1;
END IF;
END LOOP;
IF fkcnt > 1 THEN
RAISE WARNING 'Only one foreign key option can be provided. You provided %', fkcnt;
RETURN '';
ELSEIF trigcnt > 1 THEN
RAISE WARNING 'Only one trigger option can be provided. You provided %', trigcnt;
RETURN '';
ELSEIF pkcnt > 1 THEN
RAISE WARNING 'Only one pkey option can be provided. You provided %', pkcnt;
RETURN '';
ELSEIF cmtcnt > 1 THEN
RAISE WARNING 'Only one comments option can be provided. You provided %', cmtcnt;
RETURN '';
END IF;
END IF;
SELECT c.oid, (select setting from pg_settings where name = 'server_version_num') INTO v_table_oid, v_pgversion FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind in ('r','p') AND c.relname = in_table AND n.nspname = in_schema;
-- set search_path = public before we do anything to force explicit schema qualification but dont forget to set it back before exiting...
SELECT setting INTO search_path_old FROM pg_settings WHERE name = 'search_path';
-- RAISE NOTICE 'DEBUG tableddl: saving old search_path: ***%***', search_path_old;
EXECUTE 'SET search_path = "public"';
SELECT setting INTO search_path_new FROM pg_settings WHERE name = 'search_path';
-- RAISE NOTICE 'DEBUG tableddl: using new search path=***%***', search_path_new;
-- throw an error if table was not found
IF (v_table_oid IS NULL) THEN
RAISE EXCEPTION 'table does not exist';
END IF;
-- get user-defined tablespaces if applicable
SELECT tablespace INTO v_temp FROM pg_tables WHERE schemaname = in_schema and tablename = in_table and tablespace IS NOT NULL;
IF v_temp IS NULL THEN
v_tablespace := 'TABLESPACE pg_default';
ELSE
v_tablespace := 'TABLESPACE ' || v_temp;
END IF;
-- also see if there are any SET commands for this table, ie, autovacuum_enabled=off, fillfactor=70
WITH relopts AS (SELECT unnest(c.reloptions) relopts FROM pg_class c, pg_namespace n WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table)
SELECT string_agg(r.relopts, ', ') as relopts INTO v_temp from relopts r;
IF v_temp IS NULL THEN
v_relopts := '';
ELSE
v_relopts := ' WITH (' || v_temp || ')';
END IF;
-- -----------------------------------------------------------------------------------
-- Create table defs for partitions/children using inheritance or declarative methods.
-- inheritance: pg_class.relkind = 'r' pg_class.relispartition=false pg_class.relpartbound is NULL
-- declarative: pg_class.relkind = 'r' pg_class.relispartition=true pg_class.relpartbound is NOT NULL
-- -----------------------------------------------------------------------------------
v_partbound := '';
bPartition := False;
bInheritance := False;
IF v_pgversion < 100000 THEN
-- Issue#11: handle parent schema
SELECT c2.relname parent, c2.relnamespace::regnamespace INTO v_parent, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
IF (v_parent IS NOT NULL) THEN
bPartition := True;
bInheritance := True;
END IF;
ELSE
-- Issue#11: handle parent schema
SELECT c2.relname parent, c1.relispartition, pg_get_expr(c1.relpartbound, c1.oid, true), c2.relnamespace::regnamespace INTO v_parent, bRelispartition, v_partbound, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
IF (v_parent IS NOT NULL) THEN
bPartition := True;
IF bRelispartition THEN
bInheritance := False;
ELSE
bInheritance := True;
END IF;
END IF;
END IF;
IF bPartition THEN
--Issue#17 fix for case-sensitive tables
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
--Issue#19 put double-quotes around SQL keyword column names
-- Issue#121: fix keyword lookup for table name not column name that does not apply here
-- SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R';
SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = in_table AND catcode = 'R';
IF bInheritance THEN
-- inheritance-based
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '"( '|| E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || '( '|| E'\\n';
END IF;
-- Jump to constraints section to add the check constraints
ELSE
-- declarative-based
IF v_relopts <> '' THEN
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '" PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || v_relopts || ' ' || v_tablespace || '; ' || E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || ' PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || v_relopts || ' ' || v_tablespace || '; ' || E'\\n';
END IF;
ELSE
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '" PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || ' ' || v_tablespace || '; ' || E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || ' PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || ' ' || v_tablespace || '; ' || E'\\n';
END IF;
END IF;
-- Jump to constraints and index section to add the check constraints and indexes and perhaps FKeys
END IF;
END IF;
IF bVerbose THEN RAISE NOTICE '(1)tabledef so far: %', v_table_ddl; END IF;
IF NOT bPartition THEN
-- see if this is unlogged or temporary table
select c.relpersistence into v_persist from pg_class c, pg_namespace n where n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table and c.relkind = 'r';
IF v_persist = 'u' THEN
v_temp := 'UNLOGGED';
ELSIF v_persist = 't' THEN
v_temp := 'TEMPORARY';
ELSE
v_temp := '';
END IF;
END IF;
-- start the create definition for regular tables unless we are in progress creating an inheritance-based child table
IF NOT bPartition THEN
--Issue#17 fix for case-sensitive tables
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
IF v_cnt1 > 0 THEN
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '."' || in_table || '" (' || E'\\n';
ELSE
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '.' || in_table || ' (' || E'\\n';
END IF;
END IF;
-- RAISE NOTICE 'DEBUG2: tabledef so far: %', v_table_ddl;
-- define all of the columns in the table unless we are in progress creating an inheritance-based child table
IF NOT bPartition THEN
FOR v_colrec IN
SELECT c.column_name, c.data_type, c.udt_name, c.udt_schema, c.character_maximum_length, c.is_nullable, c.column_default, c.numeric_precision, c.numeric_scale, c.is_identity, c.identity_generation, c.is_generated, c.generation_expression
FROM information_schema.columns c WHERE (table_schema, table_name) = (in_schema, in_table) ORDER BY ordinal_position
LOOP
IF bVerbose THEN RAISE NOTICE '(col loop) name=% type=% udt_name=% default=% is_generated=% gen_expr=%', v_colrec.column_name, v_colrec.data_type, v_colrec.udt_name, v_colrec.column_default, v_colrec.is_generated, v_colrec.generation_expression; END IF;
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
-- SELECT CASE WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial;
SELECT CASE WHEN pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial;
IF bVerbose THEN
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
-- SELECT pg_get_serial_sequence(v_qualified, v_colrec.column_name) into v_temp;
SELECT pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) into v_temp;
IF v_temp IS NULL THEN v_temp = 'NA'; END IF;
SELECT pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name) INTO v_diag1;
RAISE NOTICE 'DEBUG table: % Column: % datatype: % Serial=% serialval=% coldef=%', v_qualified, v_colrec.column_name, v_colrec.data_type, bSerial, v_temp, v_diag1;
RAISE NOTICE 'DEBUG tabledef: %', v_table_ddl;
END IF;
--Issue#17 put double-quotes around case-sensitive column names
SELECT COUNT(*) INTO v_cnt1 FROM information_schema.columns t WHERE EXISTS (SELECT REGEXP_MATCHES(s.column_name, '([A-Z]+)','g') FROM information_schema.columns s
WHERE t.table_schema=s.table_schema and t.table_name=s.table_name and t.column_name=s.column_name AND t.table_schema = quote_ident(in_schema) AND column_name = v_colrec.column_name);
--Issue#19 put double-quotes around SQL keyword column names
SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R';
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := v_table_ddl || ' "' || v_colrec.column_name || '" ';
ELSE
v_table_ddl := v_table_ddl || ' ' || v_colrec.column_name || ' ';
END IF;
-- Issue#23: Handle autogenerated columns and rewrite as a simpler IF THEN ELSE branch instead of a much more complex embedded CASE STATEMENT
IF v_colrec.is_generated = 'ALWAYS' and v_colrec.generation_expression IS NOT NULL THEN
-- searchable tsvector GENERATED ALWAYS AS (to_tsvector('simple'::regconfig, COALESCE(translate(email, '@.-'::citext, ' '::text), ''::text)) ) STORED
v_temp = v_colrec.data_type || ' GENERATED ALWAYS AS (' || v_colrec.generation_expression || ') STORED ';
ELSEIF v_colrec.udt_name in ('geometry', 'box2d', 'box2df', 'box3d', 'geography', 'geometry_dump', 'gidx', 'spheroid', 'valid_detail') THEN
v_temp = v_colrec.udt_name;
ELSEIF v_colrec.data_type = 'USER-DEFINED' THEN
v_temp = v_colrec.udt_schema || '.' || v_colrec.udt_name;
ELSEIF v_colrec.data_type = 'ARRAY' THEN
-- Issue#6 fix: handle arrays
v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name);
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
-- WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL
ELSEIF pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN
-- Issue#8 fix: handle serial. Note: NOT NULL is implied so no need to declare it explicitly
v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name);
ELSE
v_temp = v_colrec.data_type;
END IF;
-- RAISE NOTICE 'column def1=%', v_temp;
-- handle IDENTITY columns
IF v_colrec.is_identity = 'YES' THEN
IF v_colrec.identity_generation = 'ALWAYS' THEN
v_temp = v_temp || ' GENERATED ALWAYS AS IDENTITY';
ELSE
v_temp = v_temp || ' GENERATED BY DEFAULT AS IDENTITY';
END IF;
ELSEIF v_colrec.character_maximum_length IS NOT NULL THEN
v_temp = v_temp || ('(' || v_colrec.character_maximum_length || ')');
ELSEIF v_colrec.numeric_precision > 0 AND v_colrec.numeric_scale > 0 THEN
v_temp = v_temp || '(' || v_colrec.numeric_precision || ',' || v_colrec.numeric_scale || ')';
END IF;
-- Handle NULL/NOT NULL
IF bSerial THEN
v_temp = v_temp || ' NOT NULL';
ELSEIF v_colrec.is_nullable = 'NO' THEN
v_temp = v_temp || ' NOT NULL';
ELSEIF v_colrec.is_nullable = 'YES' THEN
v_temp = v_temp || ' NULL';
END IF;
-- Handle defaults
IF v_colrec.column_default IS NOT null AND NOT bSerial THEN
-- RAISE NOTICE 'Setting default for column, %', v_colrec.column_name;
v_temp = v_temp || (' DEFAULT ' || v_colrec.column_default);
END IF;
v_temp = v_temp || ',' || E'\\n';
-- RAISE NOTICE 'column def2=%', v_temp;
v_table_ddl := v_table_ddl || v_temp;
-- RAISE NOTICE 'tabledef=%', v_table_ddl;
END LOOP;
END IF;
IF bVerbose THEN RAISE NOTICE '(2)tabledef so far: %', v_table_ddl; END IF;
-- define all the constraints: conparentid does not exist pre PGv11
IF v_pgversion < 110000 THEN
FOR v_constraintrec IN
SELECT con.conname as constraint_name, con.contype as constraint_type,
CASE
WHEN con.contype = 'p' THEN 1 -- primary key constraint
WHEN con.contype = 'u' THEN 2 -- unique constraint
WHEN con.contype = 'f' THEN 3 -- foreign key constraint
WHEN con.contype = 'c' THEN 4
ELSE 5
END as type_rank,
pg_get_constraintdef(con.oid) as constraint_definition
FROM pg_catalog.pg_constraint con JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace
WHERE nsp.nspname = in_schema AND rel.relname = in_table ORDER BY type_rank
LOOP
v_constraint_name := v_constraintrec.constraint_name;
v_constraint_def := v_constraintrec.constraint_definition;
IF v_constraintrec.type_rank = 1 THEN
IF pkcnt = 0 OR pktype = 'PKEY_INTERNAL' THEN
-- internal def
v_constraint_name := v_constraintrec.constraint_name;
v_constraint_def := v_constraintrec.constraint_definition;
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- Issue#16 handle external PG def
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table and r.conname = v_constraint_name;
END IF;
IF bPartition THEN
continue;
END IF;
ELSIF v_constraintrec.type_rank = 3 THEN
-- handle foreign key constraints
--Issue#22 fix: added FKEY_NONE check
IF fktype = 'FKEYS_NONE' THEN
-- skip
continue;
ELSIF fkcnt = 0 OR fktype = 'FKEYS_INTERNAL' THEN
-- internal def
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- external def
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table;
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
END IF;
ELSE
-- handle all other constraints besides PKEY and FKEYS as internal defs by default
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
END IF;
if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF;
constraintarr := constraintarr || v_constraintrec.constraint_name:: text;
END LOOP;
ELSE
-- handle PG versions 11 and up
-- Issue#20: Fix logic for external PKEY and FKEYS
FOR v_constraintrec IN
SELECT con.conname as constraint_name, con.contype as constraint_type,
CASE
WHEN con.contype = 'p' THEN 1 -- primary key constraint
WHEN con.contype = 'u' THEN 2 -- unique constraint
WHEN con.contype = 'f' THEN 3 -- foreign key constraint
WHEN con.contype = 'c' THEN 4
ELSE 5
END as type_rank,
pg_get_constraintdef(con.oid) as constraint_definition
FROM pg_catalog.pg_constraint con JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace
WHERE nsp.nspname = in_schema AND rel.relname = in_table
--Issue#13 added this condition:
AND con.conparentid = 0
ORDER BY type_rank
LOOP
v_constraint_name := v_constraintrec.constraint_name;
v_constraint_def := v_constraintrec.constraint_definition;
IF v_constraintrec.type_rank = 1 THEN
IF pkcnt = 0 OR pktype = 'PKEY_INTERNAL' THEN
-- internal def
v_constraint_name := v_constraintrec.constraint_name;
v_constraint_def := v_constraintrec.constraint_definition;
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- Issue#16 handle external PG def
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table;
END IF;
IF bPartition THEN
continue;
END IF;
ELSIF v_constraintrec.type_rank = 3 THEN
-- handle foreign key constraints
--Issue#22 fix: added FKEY_NONE check
IF fktype = 'FKEYS_NONE' THEN
-- skip
continue;
ELSIF fkcnt = 0 OR fktype = 'FKEYS_INTERNAL' THEN
-- internal def
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- external def
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table and
r.conname = v_constraint_name and r.conparentid = 0;
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
END IF;
ELSE
-- handle all other constraints besides PKEY and FKEYS as internal defs by default
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
END IF;
if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF;
constraintarr := constraintarr || v_constraintrec.constraint_name:: text;
END LOOP;
END IF;
-- drop the last comma before ending the create statement, which should be right before the carriage return character
-- Issue#24: make sure the comma is there before removing it
select substring(v_table_ddl, length(v_table_ddl) - 1, 1) INTO v_temp;
IF v_temp = ',' THEN
v_table_ddl = substr(v_table_ddl, 0, length(v_table_ddl) - 1) || E'\\n';
END IF;
IF bVerbose THEN RAISE NOTICE '(3)tabledef so far: %', trim(v_table_ddl); END IF;
-- ---------------------------------------------------------------------------
-- at this point we have everything up to the last table-enclosing parenthesis
-- ---------------------------------------------------------------------------
IF bVerbose THEN RAISE NOTICE '(4)tabledef so far: %', v_table_ddl; END IF;
-- See if this is an inheritance-based child table and finish up the table create.
IF bPartition and bInheritance THEN
-- Issue#11: handle parent schema
-- v_table_ddl := v_table_ddl || ') INHERITS (' || in_schema || '.' || v_parent || ') ' || E'\\n' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
IF v_parent_schema = '' OR v_parent_schema IS NULL THEN v_parent_schema = in_schema; END IF;
v_table_ddl := v_table_ddl || ') INHERITS (' || v_parent_schema || '.' || v_parent || ') ' || E'\\n' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
END IF;
IF v_pgversion >= 100000 AND NOT bPartition and NOT bInheritance THEN
-- See if this is a partitioned table (pg_class.relkind = 'p') and add the partitioned key
SELECT pg_get_partkeydef(c1.oid) as partition_key INTO v_partition_key FROM pg_class c1 JOIN pg_namespace n ON (n.oid = c1.relnamespace) LEFT JOIN pg_partitioned_table p ON (c1.oid = p.partrelid)
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.relkind = 'p';
IF v_partition_key IS NOT NULL AND v_partition_key <> '' THEN
-- add partition clause
-- NOTE: cannot specify default tablespace for partitioned relations
-- v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ' ' || v_tablespace || ';' || E'\\n';
v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ';' || E'\\n';
ELSEIF v_relopts <> '' THEN
v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
ELSE
-- end the create definition
v_table_ddl := v_table_ddl || ') ' || v_tablespace || ';' || E'\\n';
END IF;
END IF;
IF bVerbose THEN RAISE NOTICE '(5)tabledef so far: %', v_table_ddl; END IF;
-- Add closing paren for regular tables
-- IF NOT bPartition THEN
-- v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || E';\\n';
-- END IF;
-- RAISE NOTICE 'ddlsofar3: %', v_table_ddl;
-- Issue#16 create the external PKEY def if indicated
IF v_pkey_def <> '' THEN
v_table_ddl := v_table_ddl || v_pkey_def || E'\\n';
END IF;
-- Issue#20
IF v_fkey_defs <> '' THEN
v_table_ddl := v_table_ddl || v_fkey_defs || E'\\n';
END IF;
IF bVerbose THEN RAISE NOTICE '(6)tabledef so far: %', v_table_ddl; END IF;
-- create indexes
FOR v_indexrec IN
SELECT indexdef, COALESCE(tablespace, 'pg_default') as tablespace, indexname FROM pg_indexes WHERE (schemaname, tablename) = (in_schema, in_table)
LOOP
-- RAISE NOTICE 'DEBUG6: indexname=% indexdef=%', v_indexrec.indexname, v_indexrec.indexdef;
-- loop through constraints and skip ones already defined
bSkip = False;
FOREACH constraintelement IN ARRAY constraintarr
LOOP
IF constraintelement = v_indexrec.indexname THEN
-- RAISE NOTICE 'DEBUG7: skipping index, %', v_indexrec.indexname;
bSkip = True;
EXIT;
END IF;
END LOOP;
if bSkip THEN CONTINUE; END IF;
-- Add IF NOT EXISTS clause so partition index additions will not be created if declarative partition in effect and index already created on parent
v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE INDEX', 'CREATE INDEX IF NOT EXISTS');
-- Fix Issue#26: do it for unique/primary key indexes as well
v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE UNIQUE INDEX', 'CREATE UNIQUE INDEX IF NOT EXISTS');
-- RAISE NOTICE 'DEBUG8: adding index, %', v_indexrec.indexname;
-- NOTE: cannot specify default tablespace for partitioned relations
IF v_partition_key IS NOT NULL AND v_partition_key <> '' THEN
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ';' || E'\\n';
ELSE
-- Issue#25: see if partial index or not
select CASE WHEN i.indpred IS NOT NULL THEN True ELSE False END INTO v_partial
FROM pg_index i JOIN pg_class c1 ON (i.indexrelid = c1.oid) JOIN pg_class c2 ON (i.indrelid = c2.oid)
WHERE c1.relnamespace::regnamespace::text = in_schema AND c2.relnamespace::regnamespace::text = in_schema AND c2.relname = in_table AND c1.relname = v_indexrec.indexname;
IF v_partial THEN
-- Put tablespace def before WHERE CLAUSE
v_temp = v_indexrec.indexdef;
v_pos = POSITION(' WHERE ' IN v_temp);
v_temp2 = SUBSTRING(v_temp, v_pos);
v_temp = SUBSTRING(v_temp, 1, v_pos);
v_table_ddl := v_table_ddl || v_temp || ' TABLESPACE ' || v_indexrec.tablespace || v_temp2 || ';' || E'\\n';
ELSE
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ' TABLESPACE ' || v_indexrec.tablespace || ';' || E'\\n';
END IF;
END IF;
END LOOP;
IF bVerbose THEN RAISE NOTICE '(7)tabledef so far: %', v_table_ddl; END IF;
-- Issue#20: added logic for table and column comments
IF cmtcnt > 0 THEN
FOR v_rec IN
SELECT c.relname, 'COMMENT ON ' || CASE WHEN c.relkind in ('r','p') AND a.attname IS NULL THEN 'TABLE ' WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN 'COLUMN ' WHEN c.relkind = 'f' THEN 'FOREIGN TABLE '
WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW ' WHEN c.relkind = 'v' THEN 'VIEW ' WHEN c.relkind = 'i' THEN 'INDEX ' WHEN c.relkind = 'S' THEN 'SEQUENCE ' ELSE 'XX' END || n.nspname || '.' ||
CASE WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN quote_ident(c.relname) || '.' || a.attname ELSE quote_ident(c.relname) END || ' IS ' || quote_literal(d.description) || ';' as ddl
FROM pg_class c JOIN pg_namespace n ON (n.oid = c.relnamespace) LEFT JOIN pg_description d ON (c.oid = d.objoid) LEFT JOIN pg_attribute a ON (c.oid = a.attrelid AND a.attnum > 0 and a.attnum = d.objsubid)
WHERE d.description IS NOT NULL AND n.nspname = in_schema AND c.relname = in_table ORDER BY 2 desc, ddl
LOOP
--RAISE NOTICE 'comments:%', v_rec.ddl;
v_table_ddl = v_table_ddl || v_rec.ddl || E'\\n';
END LOOP;
END IF;
IF bVerbose THEN RAISE NOTICE '(8)tabledef so far: %', v_table_ddl; END IF;
IF trigtype = 'INCLUDE_TRIGGERS' THEN
-- Issue#14: handle multiple triggers for a table
FOR v_trigrec IN
select pg_get_triggerdef(t.oid, True) || ';' as triggerdef FROM pg_trigger t, pg_class c, pg_namespace n
WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table and c.relkind = 'r' and t.tgrelid = c.oid and NOT t.tgisinternal
LOOP
v_table_ddl := v_table_ddl || v_trigrec.triggerdef;
v_table_ddl := v_table_ddl || E'\\n';
IF bVerbose THEN RAISE NOTICE 'triggerdef = %', v_trigrec.triggerdef; END IF;
END LOOP;
END IF;
IF bVerbose THEN RAISE NOTICE '(9)tabledef so far: %', v_table_ddl; END IF;
-- add empty line
v_table_ddl := v_table_ddl || E'\\n';
IF bVerbose THEN RAISE NOTICE '(10)tabledef so far: %', v_table_ddl; END IF;
-- reset search_path back to what it was
IF search_path_old = '' THEN
SELECT set_config('search_path', '', false) into v_temp;
ELSE
EXECUTE 'SET search_path = ' || search_path_old;
END IF;
RETURN v_table_ddl;
EXCEPTION
WHEN others THEN
BEGIN
GET STACKED DIAGNOSTICS v_diag1 = MESSAGE_TEXT, v_diag2 = PG_EXCEPTION_DETAIL, v_diag3 = PG_EXCEPTION_HINT, v_diag4 = RETURNED_SQLSTATE, v_diag5 = PG_CONTEXT, v_diag6 = PG_EXCEPTION_CONTEXT;
-- v_ret := 'line=' || v_diag6 || '. '|| v_diag4 || '. ' || v_diag1 || ' .' || v_diag2 || ' .' || v_diag3;
v_ret := 'line=' || v_diag6 || '. '|| v_diag4 || '. ' || v_diag1;
RAISE EXCEPTION '%', v_ret;
-- put additional coding here if necessarY
RETURN '';
END;
END;
$$;`,
{ compress: true, removeAll: true }
)
@@ -1,80 +1,9 @@
import { getEntityDefinitionsSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { CREATE_PG_GET_TABLEDEF_SQL } from './database-table-definition'
import { databaseKeys } from './keys'
import { UseCustomQueryOptions } from 'types'
export const getEntityDefinitionsSql = ({
schemas,
limit = 100,
}: {
schemas: string[]
limit?: number
}) => {
const sql = /* SQL */ `
${CREATE_PG_GET_TABLEDEF_SQL}
with records as (
select
c.oid::int8 as "id",
case c.relkind
when 'r' then pg_temp.pg_get_tabledef(
concat(nc.nspname),
concat(c.relname),
false,
'FKEYS_INTERNAL',
'NO_TRIGGERS'
)
when 'v' then concat(
'create view ', concat(nc.nspname, '.', c.relname), ' as',
pg_get_viewdef(concat(nc.nspname, '.', c.relname), true)
)
when 'm' then concat(
'create materialized view ', concat(nc.nspname, '.', c.relname), ' as',
pg_get_viewdef(concat(nc.nspname, '.', c.relname), true)
)
when 'f' then concat('create foreign table ', nc.nspname, '.', c.relname, ' ( ... )')
when 'p' then pg_temp.pg_get_tabledef(
concat(nc.nspname),
concat(c.relname),
false,
'FKEYS_INTERNAL',
'NO_TRIGGERS'
)
end as "sql"
from
pg_namespace nc
join pg_class c on nc.oid = c.relnamespace
where
c.relkind in ('r', 'v', 'm', 'f', 'p')
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')
)
and nc.nspname IN (${schemas.map((schema) => `'${schema}'`).join(', ')})
order by c.relname asc
limit ${limit}
offset 0
)
select
jsonb_build_object(
'definitions', coalesce(jsonb_agg(
jsonb_build_object(
'id', r.id,
'sql', r.sql
)
), '[]'::jsonb)
) "data"
from records r;
`.trim()
return sql
}
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
export type EntityDefinitionsVariables = {
limit?: number
@@ -1,3 +1,4 @@
import { getForeignKeyConstraintsSql } from '@supabase/pg-meta'
import { QueryClient, useQuery } from '@tanstack/react-query'
import { IS_PLATFORM } from 'common'
import { UseCustomQueryOptions } from 'types'
@@ -40,61 +41,6 @@ export type ForeignKeyConstraint = {
target_columns: string[]
}
export const getForeignKeyConstraintsSql = ({ schema }: GetForeignKeyConstraintsVariables) => {
if (!schema) throw new Error('schema is required')
const sql = /* SQL */ `
SELECT
con.oid as id,
con.conname as constraint_name,
con.confdeltype as deletion_action,
con.confupdtype as update_action,
rel.oid as source_id,
nsp.nspname as source_schema,
rel.relname as source_table,
(
SELECT
array_agg(
att.attname
ORDER BY
un.ord
)
FROM
unnest(con.conkey) WITH ORDINALITY un (attnum, ord)
INNER JOIN pg_attribute att ON att.attnum = un.attnum
WHERE
att.attrelid = rel.oid
) source_columns,
frel.oid as target_id,
fnsp.nspname as target_schema,
frel.relname as target_table,
(
SELECT
array_agg(
att.attname
ORDER BY
un.ord
)
FROM
unnest(con.confkey) WITH ORDINALITY un (attnum, ord)
INNER JOIN pg_attribute att ON att.attnum = un.attnum
WHERE
att.attrelid = frel.oid
) target_columns
FROM
pg_constraint con
INNER JOIN pg_class rel ON rel.oid = con.conrelid
INNER JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
INNER JOIN pg_class frel ON frel.oid = con.confrelid
INNER JOIN pg_namespace fnsp ON fnsp.oid = frel.relnamespace
WHERE
con.contype = 'f'
AND nsp.nspname = '${schema}'
`.trim()
return sql
}
export type ForeignKeyConstraintsVariables = GetForeignKeyConstraintsVariables & {
projectRef?: string
connectionString?: string | null
@@ -104,6 +50,8 @@ export async function getForeignKeyConstraints(
{ projectRef, connectionString, schema }: ForeignKeyConstraintsVariables,
signal?: AbortSignal
) {
if (!schema) throw new Error('Schema is required')
const sql = getForeignKeyConstraintsSql({ schema })
const { result } = await executeSql(
+3 -9
View File
@@ -1,15 +1,9 @@
import { getKeywordsSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
import { UseCustomQueryOptions } from 'types'
export const getKeywordsSql = () => {
const sql = /* SQL */ `
SELECT word FROM pg_get_keywords();
`.trim()
return sql
}
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
export type KeywordsVariables = {
projectRef?: string
@@ -1,14 +1,10 @@
import { getMaxConnectionsSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { UseCustomQueryOptions } from 'types'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
export const getMaxConnectionsSql = () => {
const sql = /* SQL */ `show max_connections`
return sql
}
export type MaxConnectionsVariables = {
projectRef?: string
connectionString?: string | null
+2 -11
View File
@@ -1,5 +1,7 @@
import { getMigrationsSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { UseCustomQueryOptions } from 'types'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
@@ -9,17 +11,6 @@ export type DatabaseMigration = {
statements?: string[]
}
export const getMigrationsSql = () => {
const sql = /* SQL */ `
select
*
from supabase_migrations.schema_migrations sm
order by sm.version desc
`.trim()
return sql
}
export type MigrationsVariables = {
projectRef?: string
connectionString?: string | null
@@ -1,55 +0,0 @@
import { getCheckPrimaryKeysExistsSQL } from '@supabase/pg-meta/src/sql/studio/check-primary-keys-exists'
import { useQuery } from '@tanstack/react-query'
import { executeSql } from 'data/sql/execute-sql-query'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { databaseKeys } from './keys'
type CheckPrimaryKeysExistsVariables = {
projectRef?: string
connectionString?: string | null
tables: { name: string; schema: string }[]
}
type CheckPrimaryKeysExistResponse = {
id: string
name: string
schema: string
has_primary_key: boolean
}[]
export async function checkPrimaryKeysExists({
projectRef,
connectionString,
tables,
}: CheckPrimaryKeysExistsVariables) {
if (!projectRef) throw new Error('Project ref is required')
const { result } = await executeSql({
projectRef,
connectionString,
sql: getCheckPrimaryKeysExistsSQL(tables),
})
return {
offendingTables: (result as CheckPrimaryKeysExistResponse).filter((x) => !x.has_primary_key),
}
}
export type CheckPrimaryKeysExistsData = Awaited<ReturnType<typeof checkPrimaryKeysExists>>
export type CheckPrimaryKeysExistsError = ResponseError
export const useCheckPrimaryKeysExists = <TData = CheckPrimaryKeysExistsData>(
{ projectRef, connectionString, tables }: CheckPrimaryKeysExistsVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<CheckPrimaryKeysExistsData, CheckPrimaryKeysExistsError, TData> = {}
) =>
useQuery<CheckPrimaryKeysExistsData, CheckPrimaryKeysExistsError, TData>({
queryKey: databaseKeys.checkPrimaryKeysExists(projectRef, tables),
queryFn: () => checkPrimaryKeysExists({ projectRef, connectionString, tables }),
retry: false,
enabled: enabled && typeof projectRef !== 'undefined' && tables.length > 0,
...options,
})
@@ -1,5 +1,7 @@
import { getTableColumnsSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { UseCustomQueryOptions } from 'types'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
@@ -11,81 +13,6 @@ export type TableColumn = {
columns: any[]
}
export const getTableColumnsSql = ({ table, schema }: { table?: string; schema?: string }) => {
const conditions = []
if (table) {
conditions.push(`tablename = '${table}'`)
}
if (schema) {
conditions.push(`schemaname = '${schema}'`)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const sql = /* SQL */ `
SELECT
tbl.schemaname,
tbl.tablename,
tbl.quoted_name,
tbl.is_table,
json_agg(a) as columns
FROM
(
SELECT
n.nspname as schemaname,
c.relname as tablename,
(quote_ident(n.nspname) || '.' || quote_ident(c.relname)) as quoted_name,
true as is_table
FROM
pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE
c.relkind = 'r'
AND n.nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
AND n.nspname not like 'pg_temp_%'
AND n.nspname not like 'pg_toast_temp_%'
AND has_schema_privilege(n.oid, 'USAGE') = true
AND has_table_privilege(quote_ident(n.nspname) || '.' || quote_ident(c.relname), 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') = true
union all
SELECT
n.nspname as schemaname,
c.relname as tablename,
(quote_ident(n.nspname) || '.' || quote_ident(c.relname)) as quoted_name,
false as is_table
FROM
pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE
c.relkind in ('v', 'm')
AND n.nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
AND n.nspname not like 'pg_temp_%'
AND n.nspname not like 'pg_toast_temp_%'
AND has_schema_privilege(n.oid, 'USAGE') = true
AND has_table_privilege(quote_ident(n.nspname) || '.' || quote_ident(c.relname), 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') = true
) as tbl
LEFT JOIN (
SELECT
attrelid,
attname,
format_type(atttypid, atttypmod) as data_type,
attnum,
attisdropped
FROM
pg_attribute
) as a ON (
a.attrelid = tbl.quoted_name::regclass
AND a.attnum > 0
AND NOT a.attisdropped
AND has_column_privilege(tbl.quoted_name, a.attname, 'SELECT, INSERT, UPDATE, REFERENCES')
)
${whereClause}
GROUP BY schemaname, tablename, quoted_name, is_table;
`.trim()
return sql
}
export type TableColumnsVariables = {
projectRef?: string
connectionString?: string | null
@@ -1,39 +1,14 @@
import { getTableDefinitionSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { UseCustomQueryOptions } from 'types'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { CREATE_PG_GET_TABLEDEF_SQL } from './database-table-definition'
import { databaseKeys } from './keys'
type GetTableDefinitionArgs = {
id?: number
}
// [Joshen] Eventually move this into entity-definition-query
const getTableDefinitionSql = ({ id }: GetTableDefinitionArgs) => {
const sql = /* SQL */ `
${CREATE_PG_GET_TABLEDEF_SQL}
with table_info as (
select
n.nspname::text as schema,
c.relname::text as name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.oid = ${id}
)
select pg_temp.pg_get_tabledef (
t.schema,
t.name,
false,
'FKEYS_INTERNAL',
'INCLUDE_TRIGGERS'
) as definition
from table_info t;
`.trim()
return sql
}
export type TableDefinitionVariables = GetTableDefinitionArgs & {
projectRef?: string
connectionString?: string | null
@@ -43,9 +18,7 @@ export async function getTableDefinition(
{ projectRef, connectionString, id }: TableDefinitionVariables,
signal?: AbortSignal
) {
if (!id) {
throw new Error('id is required')
}
if (!id) throw new Error('id is required')
const sql = getTableDefinitionSql({ id })
const { result } = await executeSql(
@@ -1,10 +1,10 @@
import { getTableIndexAdvisorSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { filterProtectedSchemaIndexStatements } from 'components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils'
import { executeSql } from 'data/sql/execute-sql-query'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { databaseKeys } from './keys'
import { filterProtectedSchemaIndexStatements } from 'components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils'
import { databaseKeys } from './keys'
export type TableIndexAdvisorVariables = {
projectRef?: string
@@ -31,60 +31,6 @@ export type TableIndexAdvisorResponse = {
columnsWithSuggestions: string[]
}
//Generates SQL to find top 5 SELECT queries involving a table and run them through index_advisor
export function getTableIndexAdvisorSql(schema: string, table: string): string {
const escapedSchema = schema.replace(/'/g, "''")
const escapedTable = table.replace(/'/g, "''")
// Escape regex metacharacters so schema/table names are matched literally
const regexSchema = escapedSchema.toLowerCase().replace(/[.+*?^${}()|[\]\\]/g, '\\$&')
const regexTable = escapedTable.toLowerCase().replace(/[.+*?^${}()|[\]\\]/g, '\\$&')
return /* SQL */ `
-- Get top 5 SELECT queries involving this table and run through index_advisor
set search_path to public, extensions;
with top_queries as (
select
statements.query,
statements.calls,
statements.total_exec_time + statements.total_plan_time as total_time,
statements.mean_exec_time + statements.mean_plan_time as mean_time
from pg_stat_statements as statements
inner join pg_authid as auth on statements.userid = auth.oid
where
-- Filter for SELECT queries only (index_advisor only works with SELECT)
(lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
-- Filter for queries involving our table. Use regex word boundaries so that e.g.
-- looking for table "orders" does not match queries on "orders_items".
and (
lower(statements.query) ~ '(^|[^a-z0-9_$])${regexSchema}[.]${regexTable}($|[^a-z0-9_$])'
or lower(statements.query) ~ '(^|[^a-z0-9_$])from[[:space:]]+${regexTable}($|[^a-z0-9_$])'
or lower(statements.query) ~ '(^|[^a-z0-9_$])join[[:space:]]+${regexTable}($|[^a-z0-9_$])'
)
-- Exclude system queries
and statements.query not like '%pg_catalog%'
and statements.query not like '%information_schema%'
order by statements.calls desc
limit 5
)
select
tq.query,
tq.calls,
tq.total_time,
tq.mean_time,
coalesce(ia.index_statements, '{}') as index_statements,
coalesce((ia.startup_cost_before)::numeric, 0) as startup_cost_before,
coalesce((ia.startup_cost_after)::numeric, 0) as startup_cost_after,
coalesce((ia.total_cost_before)::numeric, 0) as total_cost_before,
coalesce((ia.total_cost_after)::numeric, 0) as total_cost_after
from top_queries tq
left join lateral (
select * from index_advisor(tq.query)
) ia on true;
`.trim()
}
// Strips ordering modifiers and outer quotes from a raw column token from an index statement.
// e.g. '"created_at" DESC NULLS LAST' -> 'created_at'
export function cleanIndexColumnName(raw: string): string {
@@ -1,35 +1,14 @@
import { getViewDefinitionSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { UseCustomQueryOptions } from 'types'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
import { UseCustomQueryOptions } from 'types'
type GetViewDefinitionArgs = {
id?: number
}
// [Joshen] Eventually move this into entity-definition-query
export const getViewDefinitionSql = ({ id }: GetViewDefinitionArgs) => {
if (!id) {
throw new Error('id is required')
}
const sql = /* SQL */ `
with table_info as (
select
n.nspname::text as schema,
c.relname::text as name,
to_regclass(concat('"', n.nspname, '"."', c.relname, '"')) as regclass
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.oid = ${id}
)
select pg_get_viewdef(t.regclass, true) as definition
from table_info t
`.trim()
return sql
}
export type ViewDefinitionVariables = GetViewDefinitionArgs & {
projectRef?: string
connectionString?: string | null
@@ -39,6 +18,8 @@ export async function getViewDefinition(
{ projectRef, connectionString, id }: ViewDefinitionVariables,
signal?: AbortSignal
) {
if (!id) throw new Error('View ID is required')
const sql = getViewDefinitionSql({ id })
const { result } = await executeSql(
{
@@ -1,6 +1,8 @@
import { getEntityTypesSQL } from '@supabase/pg-meta'
import { InfiniteData, QueryClient, useInfiniteQuery } from '@tanstack/react-query'
import { executeSql, ExecuteSqlVariables } from 'data/sql/execute-sql-query'
import type { ResponseError, UseCustomInfiniteQueryOptions } from 'types'
import { ENTITY_TYPE } from './entity-type-constants'
import { entityTypeKeys } from './keys'
@@ -43,63 +45,7 @@ export async function getEntityTypes(
}: EntityTypesVariables,
signal?: AbortSignal
) {
const innerOrderBy = sort === 'alphabetical' ? `c.relname asc` : `"type_sort" asc, c.relname asc`
const outerOrderBy = sort === 'alphabetical' ? `r.name asc` : `r.type_sort asc, r.name asc`
const sql = /* SQL */ `
with records as (
select
c.oid::int8 as "id",
nc.nspname as "schema",
c.relname as "name",
c.relkind as "type",
case c.relkind
when 'r' then 1
when 'v' then 2
when 'm' then 3
when 'f' then 4
when 'p' then 5
end as "type_sort",
obj_description(c.oid) as "comment",
count(*) over() as "count",
c.relrowsecurity as "rls_enabled"
from
pg_namespace nc
join pg_class c on nc.oid = c.relnamespace
where
c.relkind in (${filterTypes.map((x) => `'${x}'`).join(', ')})
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')
)
and nc.nspname in (${schemas.map((x) => `'${x}'`)})
${search ? `and c.relname ilike '%${search}%'` : ''}
order by ${innerOrderBy}
limit ${limit}
offset ${page * limit}
)
select
jsonb_build_object(
'entities', coalesce(jsonb_agg(
jsonb_build_object(
'id', r.id,
'schema', r.schema,
'name', r.name,
'type', r.type,
'comment', r.comment,
'rls_enabled', r.rls_enabled
)
order by ${outerOrderBy}
), '[]'::jsonb),
'count', coalesce(min(r.count), 0)
) "data"
from records r;
`
const sql = getEntityTypesSQL({ schemas, sort, filterTypes, search, limit, page })
const { result } = await executeSql(
{
@@ -1,20 +1,9 @@
import { replicationLagSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { replicaKeys } from './keys'
import { UseCustomQueryOptions } from 'types'
export const replicationLagSql = () => {
const sql = /* SQL */ `
select
case
when (select count(*) from pg_stat_wal_receiver) = 1 and pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()
then 0
else coalesce(extract(epoch from now() - pg_last_xact_replay_timestamp()),0)
end as physical_replica_lag_second
`
return sql
}
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { replicaKeys } from './keys'
export type ReplicationLagVariables = {
id: string
+4 -3
View File
@@ -1,8 +1,9 @@
import { getAbortQuerySQL } from '@supabase/pg-meta'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { executeSql } from 'data/sql/execute-sql-query'
import { toast } from 'sonner'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { sqlKeys } from './keys'
export type QueryAbortVariables = {
@@ -12,7 +13,7 @@ export type QueryAbortVariables = {
}
export async function abortQuery({ pid, projectRef, connectionString }: QueryAbortVariables) {
const sql = /* SQL */ `select pg_terminate_backend(${pid})`
const sql = getAbortQuerySQL({ pid })
const { result } = await executeSql({ projectRef, connectionString, sql })
return result
}
+4 -10
View File
@@ -1,7 +1,9 @@
import { getOngoingQueriesSql } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { UseCustomQueryOptions } from 'types'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { sqlKeys } from './keys'
import { UseCustomQueryOptions } from 'types'
type OngoingQuery = {
pid: number
@@ -9,14 +11,6 @@ type OngoingQuery = {
query_start: string
}
export const getOngoingQueriesSql = () => {
const sql = /* SQL */ `
select pid, query, query_start from pg_stat_activity where state = 'active' and datname = 'postgres';
`.trim()
return sql
}
export type OngoingQueriesVariables = {
projectRef?: string
connectionString?: string | null
@@ -26,7 +20,7 @@ export async function getOngoingQueries(
{ projectRef, connectionString }: OngoingQueriesVariables,
signal?: AbortSignal
) {
const sql = getOngoingQueriesSql().trim()
const sql = getOngoingQueriesSql()
const { result } = await executeSql(
{ projectRef, connectionString, sql, queryKey: ['ongoing-queries'] },
@@ -1,9 +1,9 @@
import { getTablesWithAnonAuthenticatedAccessSQL } from '@supabase/pg-meta/src/sql/studio/check-tables-anon-authenticated-access'
import { getTablesWithAnonAuthenticatedAccessSQL } from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { UseCustomQueryOptions } from 'types'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { tableKeys } from './keys'
import { UseCustomQueryOptions } from 'types'
type TablesRolesAccessArgs = {
schema: string
+1 -1
View File
@@ -1,3 +1,4 @@
import { QUEUES_SCHEMA } from '@supabase/pg-meta'
import {
SUPABASE_TARGET_SCHEMA_OPTION,
WRAPPERS,
@@ -11,7 +12,6 @@ import { uniq, uniqBy } from 'lodash'
import { useMemo } from 'react'
import { useSelectedProjectQuery } from './misc/useSelectedProject'
import { QUEUES_SCHEMA } from '@/data/database-queues/database-queues-toggle-postgrest-mutation'
/**
* A list of system schemas that users should not interact with
+3 -4
View File
@@ -1,14 +1,13 @@
import { getEntityDefinitionsSql } from '@supabase/pg-meta'
import { tool } from 'ai'
import { stripIndent } from 'common-tags'
import { z } from 'zod'
// import { processSql, renderSupabaseJs } from '@supabase/sql-to-rest'
import { IS_PLATFORM } from 'common'
import { stripIndent } from 'common-tags'
import { getDatabaseFunctions } from 'data/database-functions/database-functions-query'
import { getDatabasePolicies } from 'data/database-policies/database-policies-query'
import { getEntityDefinitionsSql } from 'data/database/entity-definitions-query'
import { executeSql } from 'data/sql/execute-sql-query'
import { executeQuery } from 'lib/api/self-hosted/query'
import { z } from 'zod'
export const getFallbackTools = ({
projectRef,
+12 -50
View File
@@ -18,56 +18,18 @@ import version from './pg-meta-version'
import views from './pg-meta-views'
import * as query from './query/index'
// Studio SQL queries
export {
getUserSQL,
getIndexStatusesSQL,
getUsersCountSQL,
getPaginatedUsersSQL,
getIndexWorkerStatusSQL,
USER_SEARCH_INDEXES,
type OptimizedSearchColumns,
type UsersCursor,
} from './sql/studio/auth'
export {
getLargestSizeLimitBucketsSqlUnoptimized,
LARGEST_SIZE_LIMIT_BUCKETS_COUNT,
} from './sql/studio/storage'
export {
getExposedTablesSql,
getExposedTableCountsSql,
getExposedFunctionsSql,
getExposedFunctionCountsSql,
buildTablePrivilegesSql,
buildFunctionPrivilegesSql,
buildDefaultPrivilegesSql,
getDefaultPrivilegesStateSql,
} from './sql/studio/privileges'
export {
getIndexesSQL,
getDatabaseExtensionDefaultSchemaSQL,
getCronJobsMinimalSql,
getCronJobsSql,
getJobRunDetailsPageCountSql,
getDeleteOldCronJobRunDetailsByCtidSql,
getScheduleDeleteCronJobRunDetailsSql,
getTableRowsCountSql,
} from './sql/studio/database'
export {
type ForeignKey,
FOREIGN_KEY_CASCADE_ACTION,
getAddPrimaryKeySQL,
getDropConstraintSQL,
getAddForeignKeySQL,
getRemoveForeignKeySQL,
getUpdateIdentitySequenceSQL,
getDuplicateIdentitySequenceSQL,
getDuplicateTableSQL,
getDuplicateRowsSQL,
getEnableRLSSQL,
} from './sql/studio/table-editor'
export { getLiveTupleEstimate } from './sql/studio/get-live-tuple-estimate'
/**
* Studio specific SQL queries
* [Joshen] If it gets cumbersome, we can also consider path exports for studio queries
* So consumption can look something like:
* import { ... } from '@supabase/pg-meta/table-editor'
*/
export * from './sql/studio/advisor'
export * from './sql/studio/auth'
export * from './sql/studio/storage'
export * from './sql/studio/database'
export * from './sql/studio/table-editor'
export * from './sql/studio/sql-editor'
export default {
roles,
@@ -0,0 +1,55 @@
/**
* Generates SQL to find top 5 SELECT queries involving a table and run them through index_advisor
*/
export function getTableIndexAdvisorSql(schema: string, table: string): string {
const escapedSchema = schema.replace(/'/g, "''")
const escapedTable = table.replace(/'/g, "''")
// Escape regex metacharacters so schema/table names are matched literally
const regexSchema = escapedSchema.toLowerCase().replace(/[.+*?^${}()|[\]\\]/g, '\\$&')
const regexTable = escapedTable.toLowerCase().replace(/[.+*?^${}()|[\]\\]/g, '\\$&')
return /* SQL */ `
-- Get top 5 SELECT queries involving this table and run through index_advisor
set search_path to public, extensions;
with top_queries as (
select
statements.query,
statements.calls,
statements.total_exec_time + statements.total_plan_time as total_time,
statements.mean_exec_time + statements.mean_plan_time as mean_time
from pg_stat_statements as statements
inner join pg_authid as auth on statements.userid = auth.oid
where
-- Filter for SELECT queries only (index_advisor only works with SELECT)
(lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
-- Filter for queries involving our table. Use regex word boundaries so that e.g.
-- looking for table "orders" does not match queries on "orders_items".
and (
lower(statements.query) ~ '(^|[^a-z0-9_$])${regexSchema}[.]${regexTable}($|[^a-z0-9_$])'
or lower(statements.query) ~ '(^|[^a-z0-9_$])from[[:space:]]+${regexTable}($|[^a-z0-9_$])'
or lower(statements.query) ~ '(^|[^a-z0-9_$])join[[:space:]]+${regexTable}($|[^a-z0-9_$])'
)
-- Exclude system queries
and statements.query not like '%pg_catalog%'
and statements.query not like '%information_schema%'
order by statements.calls desc
limit 5
)
select
tq.query,
tq.calls,
tq.total_time,
tq.mean_time,
coalesce(ia.index_statements, '{}') as index_statements,
coalesce((ia.startup_cost_before)::numeric, 0) as startup_cost_before,
coalesce((ia.startup_cost_after)::numeric, 0) as startup_cost_after,
coalesce((ia.total_cost_before)::numeric, 0) as total_cost_before,
coalesce((ia.total_cost_after)::numeric, 0) as total_cost_after
from top_queries tq
left join lateral (
select * from index_advisor(tq.query)
) ia on true;
`.trim()
}
@@ -0,0 +1 @@
export * from './index-advisor'
@@ -1,5 +1,5 @@
import { literal } from '../../../pg-format'
import { COUNT_ESTIMATE_SQL, THRESHOLD_COUNT } from '../get-count-estimate'
import { COUNT_ESTIMATE_SQL, THRESHOLD_COUNT } from '../database/get-count-estimate'
import { prefixToUUID, stringRange } from './get-users-common'
import type { OptimizedSearchColumns } from './get-users-types'
@@ -1,20 +0,0 @@
export const getCheckPrimaryKeysExistsSQL = (tables: { name: string; schema: string }[]) => {
const formattedTables = tables.map((table) => `'${table.schema}.${table.name}'`).join(',')
return /* SQL */ `
WITH targets(rel) AS (
SELECT unnest(ARRAY[${formattedTables}]::regclass[])
)
SELECT
c.oid AS id,
n.nspname AS schema,
c.relname AS name,
(con.conrelid IS NOT NULL) AS has_primary_key
FROM targets t
JOIN pg_class c ON c.oid = t.rel
JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_constraint con
ON con.conrelid = c.oid AND con.contype = 'p'
ORDER BY n.nspname, c.relname;
`
}
@@ -0,0 +1,74 @@
export const getTableColumnsSql = ({ table, schema }: { table?: string; schema?: string }) => {
const conditions = []
if (table) {
conditions.push(`tablename = '${table}'`)
}
if (schema) {
conditions.push(`schemaname = '${schema}'`)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const sql = /* SQL */ `
SELECT
tbl.schemaname,
tbl.tablename,
tbl.quoted_name,
tbl.is_table,
json_agg(a) as columns
FROM
(
SELECT
n.nspname as schemaname,
c.relname as tablename,
(quote_ident(n.nspname) || '.' || quote_ident(c.relname)) as quoted_name,
true as is_table
FROM
pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE
c.relkind = 'r'
AND n.nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
AND n.nspname not like 'pg_temp_%'
AND n.nspname not like 'pg_toast_temp_%'
AND has_schema_privilege(n.oid, 'USAGE') = true
AND has_table_privilege(quote_ident(n.nspname) || '.' || quote_ident(c.relname), 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') = true
union all
SELECT
n.nspname as schemaname,
c.relname as tablename,
(quote_ident(n.nspname) || '.' || quote_ident(c.relname)) as quoted_name,
false as is_table
FROM
pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE
c.relkind in ('v', 'm')
AND n.nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
AND n.nspname not like 'pg_temp_%'
AND n.nspname not like 'pg_toast_temp_%'
AND has_schema_privilege(n.oid, 'USAGE') = true
AND has_table_privilege(quote_ident(n.nspname) || '.' || quote_ident(c.relname), 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') = true
) as tbl
LEFT JOIN (
SELECT
attrelid,
attname,
format_type(atttypid, atttypmod) as data_type,
attnum,
attisdropped
FROM
pg_attribute
) as a ON (
a.attrelid = tbl.quoted_name::regclass
AND a.attnum > 0
AND NOT a.attisdropped
AND has_column_privilege(tbl.quoted_name, a.attname, 'SELECT, INSERT, UPDATE, REFERENCES')
)
${whereClause}
GROUP BY schemaname, tablename, quoted_name, is_table;
`.trim()
return sql
}
@@ -1,4 +1,14 @@
export * from './check-tables-anon-authenticated-access'
export * from './columns'
export * from './cron-jobs'
export * from './extensions'
export * from './get-count-estimate'
export * from './indexes'
export * from './migrations'
export * from './misc'
export * from './privileges'
export * from './queues'
export * from './rows'
export * from './table-definition'
export * from './triggers'
export * from './views'
@@ -0,0 +1,10 @@
export const getMigrationsSql = () => {
const sql = /* SQL */ `
select
*
from supabase_migrations.schema_migrations sm
order by sm.version desc
`.trim()
return sql
}
@@ -0,0 +1,39 @@
import { literal } from '../../../pg-format'
export const getDatabaseSizeSql = () => {
const sql = /* SQL */ `
select sum(pg_database_size(pg_database.datname))::bigint as db_size from pg_database;
`.trim()
return sql
}
export const getLiveTupleEstimate = (table: string, schema: string = 'public') => {
const sql = /* SQL */ `
SELECT n_live_tup AS live_tuple_estimate
FROM pg_stat_user_tables
WHERE schemaname = ${literal(schema)}
AND relname = ${literal(table)};
`.trim()
return sql
}
export const getMaxConnectionsSql = () => {
const sql = /* SQL */ `show max_connections`
return sql
}
export const replicationLagSql = () => {
const sql = /* SQL */ `
select
case
when (select count(*) from pg_stat_wal_receiver) = 1 and pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()
then 0
else coalesce(extract(epoch from now() - pg_last_xact_replay_timestamp()),0)
end as physical_replica_lag_second
`
return sql
}
@@ -0,0 +1,215 @@
export const QUEUES_SCHEMA = 'pgmq_public'
export const HIDE_QUEUES_FROM_POSTGREST_SQL = /* SQL */ `
drop function if exists
${QUEUES_SCHEMA}.pop(queue_name text),
${QUEUES_SCHEMA}.send(queue_name text, message jsonb, sleep_seconds integer),
${QUEUES_SCHEMA}.send_batch(queue_name text, message jsonb[], sleep_seconds integer),
${QUEUES_SCHEMA}.archive(queue_name text, message_id bigint),
${QUEUES_SCHEMA}.delete(queue_name text, message_id bigint),
${QUEUES_SCHEMA}.read(queue_name text, sleep integer, n integer)
;
-- Revoke execute permissions on inner pgmq functions to roles (inverse of enabling)
do $$
begin
if exists (select 1 from pg_namespace where nspname = 'pgmq') then
-- Revoke privileges on the schema itself
revoke all on schema pgmq from anon, authenticated, service_role;
-- Revoke default privileges for future objects
alter default privileges in schema pgmq revoke all on tables from anon, authenticated, service_role;
alter default privileges in schema pgmq revoke all on sequences from anon, authenticated, service_role;
alter default privileges in schema pgmq revoke all on functions from anon, authenticated, service_role;
end if;
end $$;
drop schema if exists ${QUEUES_SCHEMA};
`
export const getExposeQueuesSQL = ({ isNewerPgmqversion }: { isNewerPgmqversion: boolean }) => {
const conditionalJsonb = isNewerPgmqversion ? `, conditional := '{}'::jsonb` : ''
const jsonBArg = isNewerPgmqversion ? `, jsonb` : ''
return /* SQL */ `
create schema if not exists ${QUEUES_SCHEMA};
grant usage on schema ${QUEUES_SCHEMA} to postgres, anon, authenticated, service_role;
create or replace function ${QUEUES_SCHEMA}.pop(
queue_name text
)
returns setof pgmq.message_record
language plpgsql
set search_path = ''
as $$
begin
return query
select *
from pgmq.pop(
queue_name := queue_name
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.pop(queue_name text) is 'Retrieves and locks the next message from the specified queue.';
create or replace function ${QUEUES_SCHEMA}.send(
queue_name text,
message jsonb,
sleep_seconds integer default 0 -- renamed from 'delay'
)
returns setof bigint
language plpgsql
set search_path = ''
as $$
begin
return query
select *
from pgmq.send(
queue_name := queue_name,
msg := message,
delay := sleep_seconds
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.send(queue_name text, message jsonb, sleep_seconds integer) is 'Sends a message to the specified queue, optionally delaying its availability by a number of seconds.';
create or replace function ${QUEUES_SCHEMA}.send_batch(
queue_name text,
messages jsonb[],
sleep_seconds integer default 0 -- renamed from 'delay'
)
returns setof bigint
language plpgsql
set search_path = ''
as $$
begin
return query
select *
from pgmq.send_batch(
queue_name := queue_name,
msgs := messages,
delay := sleep_seconds
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.send_batch(queue_name text, messages jsonb[], sleep_seconds integer) is 'Sends a batch of messages to the specified queue, optionally delaying their availability by a number of seconds.';
create or replace function ${QUEUES_SCHEMA}.archive(
queue_name text,
message_id bigint
)
returns boolean
language plpgsql
set search_path = ''
as $$
begin
return
pgmq.archive(
queue_name := queue_name,
msg_id := message_id
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.archive(queue_name text, message_id bigint) is 'Archives a message by moving it from the queue to a permanent archive.';
create or replace function ${QUEUES_SCHEMA}.delete(
queue_name text,
message_id bigint
)
returns boolean
language plpgsql
set search_path = ''
as $$
begin
return
pgmq.delete(
queue_name := queue_name,
msg_id := message_id
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.delete(queue_name text, message_id bigint) is 'Permanently deletes a message from the specified queue.';
create or replace function ${QUEUES_SCHEMA}.read(
queue_name text,
sleep_seconds integer,
n integer
)
returns setof pgmq.message_record
language plpgsql
set search_path = ''
as $$
begin
return query
select *
from pgmq.read(
queue_name := queue_name,
vt := sleep_seconds,
qty := n ${conditionalJsonb}
);
end;
$$;
comment on function ${QUEUES_SCHEMA}.read(queue_name text, sleep_seconds integer, n integer) is 'Reads up to "n" messages from the specified queue with an optional "sleep_seconds" (visibility timeout).';
-- Grant execute permissions on wrapper functions to roles
grant execute on function ${QUEUES_SCHEMA}.pop(text) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.pop(text) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.send(text, jsonb, integer) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.send(text, jsonb, integer) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.send_batch(text, jsonb[], integer) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.send_batch(text, jsonb[], integer) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.archive(text, bigint) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.archive(text, bigint) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.delete(text, bigint) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.delete(text, bigint) to postgres, service_role, anon, authenticated;
grant execute on function ${QUEUES_SCHEMA}.read(text, integer, integer) to postgres, service_role, anon, authenticated;
grant execute on function pgmq.read(text, integer, integer ${jsonBArg}) to postgres, service_role, anon, authenticated;
-- For the service role, we want full access
-- Grant permissions on existing tables
grant all privileges on all tables in schema pgmq to postgres, service_role;
-- Ensure service_role has permissions on future tables
alter default privileges in schema pgmq grant all privileges on tables to postgres, service_role;
grant usage on schema pgmq to postgres, anon, authenticated, service_role;
/*
Grant access to sequences to API roles by default. Existing table permissions
continue to enforce insert restrictions. This is necessary to accommodate the
on-backup hook that rebuild queue table primary keys to avoid a pg_dump segfault.
This can be removed once logical backups are completely retired.
*/
grant usage, select, update
on all sequences in schema pgmq
to anon, authenticated, service_role;
alter default privileges in schema pgmq
grant usage, select, update
on sequences
to anon, authenticated, service_role;
`.trim()
}
// [Joshen] Check if all the relevant functions exist to indicate whether PGMQ has been exposed through PostgREST
export const getQueuesExposePostgrestStatusSQL = () => {
return /**SQL */ `
SELECT exists (select schema_name FROM information_schema.schemata WHERE schema_name = '${QUEUES_SCHEMA}');
`.trim()
}
@@ -1,5 +1,5 @@
import { Filter, Query } from '../../../query'
import { COUNT_ESTIMATE_SQL, THRESHOLD_COUNT } from '../get-count-estimate'
import { COUNT_ESTIMATE_SQL, THRESHOLD_COUNT } from './get-count-estimate'
/**
* [Joshen] Initially check reltuples from pg_class for an estimate of row count on the table
@@ -0,0 +1,802 @@
/**
* Derived from https://github.com/MichaelDBA/pg_get_tabledef
* NOTE: when updating, \n must be replaced with \\n in the SQL below
* public.x is replaced with pg_temp.x
* REMOVE (as it breaks the encoding for some reason):
* SELECT REPLACE(REPLACE(setting, '"$user"', '$user'), '$user', '"$user"') INTO search_path_old
* FROM pg_settings
* WHERE name = 'search_path';
*/
export const CREATE_PG_GET_TABLEDEF_SQL = /* SQL */ `
DROP TYPE IF EXISTS pg_temp.tabledefs CASCADE;
CREATE TYPE pg_temp.tabledefs AS ENUM ('PKEY_INTERNAL','PKEY_EXTERNAL','FKEYS_INTERNAL', 'FKEYS_EXTERNAL', 'COMMENTS', 'FKEYS_NONE', 'INCLUDE_TRIGGERS', 'NO_TRIGGERS');
-- SELECT * FROM pg_temp.pg_get_coldef('sample','orders','id');
-- DROP FUNCTION pg_temp.pg_get_coldef(text,text,text,boolean);
CREATE OR REPLACE FUNCTION pg_temp.pg_get_coldef(
in_schema text,
in_table text,
in_column text,
oldway boolean default False
)
RETURNS text
LANGUAGE plpgsql VOLATILE
AS
$$
DECLARE
v_coldef text;
v_dt1 text;
v_dt2 text;
v_dt3 text;
v_nullable boolean;
v_position int;
v_identity text;
v_generated text;
v_hasdflt boolean;
v_dfltexpr text;
BEGIN
IF oldway THEN
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum;
-- RAISE NOTICE 'DEBUG: oldway=%',v_coldef;
ELSE
-- a.attrelid::regclass::text, a.attname
SELECT CASE WHEN a.atttypid = ANY ('{int,int8,int2}'::regtype[]) AND EXISTS (SELECT FROM pg_attrdef ad WHERE ad.adrelid = a.attrelid AND ad.adnum = a.attnum AND
pg_get_expr(ad.adbin, ad.adrelid) = 'nextval(''' || (pg_get_serial_sequence (a.attrelid::regclass::text, a.attname))::regclass || '''::regclass)') THEN CASE a.atttypid
WHEN 'int'::regtype THEN 'serial' WHEN 'int8'::regtype THEN 'bigserial' WHEN 'int2'::regtype THEN 'smallserial' END ELSE format_type(a.atttypid, a.atttypmod) END AS data_type
INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum;
-- RAISE NOTICE 'DEBUG: newway=%',v_coldef;
-- Issue#24: not implemented yet
-- might replace with this below to do more detailed parsing...
-- SELECT a.atttypid::regtype AS dt1, format_type(a.atttypid, a.atttypmod) as dt2, t.typname as dt3, CASE WHEN not(a.attnotnull) THEN True ELSE False END AS nullable,
-- a.attnum, a.attidentity, a.attgenerated, a.atthasdef, pg_get_expr(ad.adbin, ad.adrelid) dfltexpr
-- INTO v_dt1, v_dt2, v_dt3, v_nullable, v_position, v_identity, v_generated, v_hasdflt, v_dfltexpr
-- FROM pg_attribute a JOIN pg_class c ON (a.attrelid = c.oid) JOIN pg_type t ON (a.atttypid = t.oid) LEFT JOIN pg_attrdef ad ON (a.attrelid = ad.adrelid AND a.attnum = ad.adnum)
-- WHERE c.relkind in ('r','p') AND a.attnum > 0 AND NOT a.attisdropped AND c.relnamespace::regnamespace::text = in_schema AND c.relname = in_table AND a.attname = in_column;
-- RAISE NOTICE 'schema=% table=% column=% dt1=% dt2=% dt3=% nullable=% pos=% identity=% generated=% HasDefault=% DeftExpr=%', in_schema, in_table, in_column, v_dt1,v_dt2,v_dt3,v_nullable,v_position,v_identity,v_generated,v_hasdflt,v_dfltexpr;
END IF;
RETURN v_coldef;
END;
$$;
-- SELECT * FROM pg_temp.pg_get_tabledef('sample', 'address', false);
DROP FUNCTION IF EXISTS pg_temp.pg_get_tabledef(character varying,character varying,boolean,tabledefs[]);
CREATE OR REPLACE FUNCTION pg_temp.pg_get_tabledef(
in_schema varchar,
in_table varchar,
_verbose boolean,
VARIADIC arr pg_temp.tabledefs[] DEFAULT '{}':: pg_temp.tabledefs[]
)
RETURNS text
LANGUAGE plpgsql VOLATILE
AS
$$
DECLARE
v_qualified text := '';
v_table_ddl text;
v_table_oid int;
v_colrec record;
v_constraintrec record;
v_trigrec record;
v_indexrec record;
v_rec record;
v_constraint_name text;
v_constraint_def text;
v_pkey_def text := '';
v_fkey_def text := '';
v_fkey_defs text := '';
v_trigger text := '';
v_partition_key text := '';
v_partbound text;
v_parent text;
v_parent_schema text;
v_persist text;
v_temp text := '';
v_temp2 text;
v_relopts text;
v_tablespace text;
v_pgversion int;
bSerial boolean;
bPartition boolean;
bInheritance boolean;
bRelispartition boolean;
constraintarr text[] := '{}';
constraintelement text;
bSkip boolean;
bVerbose boolean := False;
v_cnt1 integer;
v_cnt2 integer;
search_path_old text := '';
search_path_new text := '';
v_partial boolean;
v_pos integer;
-- assume defaults for ENUMs at the getgo
pkcnt int := 0;
fkcnt int := 0;
trigcnt int := 0;
cmtcnt int := 0;
pktype pg_temp.tabledefs := 'PKEY_INTERNAL';
fktype pg_temp.tabledefs := 'FKEYS_INTERNAL';
trigtype pg_temp.tabledefs := 'NO_TRIGGERS';
arglen integer;
vargs text;
avarg pg_temp.tabledefs;
-- exception variables
v_ret text;
v_diag1 text;
v_diag2 text;
v_diag3 text;
v_diag4 text;
v_diag5 text;
v_diag6 text;
BEGIN
SET client_min_messages = 'notice';
IF _verbose THEN bVerbose = True; END IF;
-- v17 fix: handle case-sensitive
-- v_qualified = in_schema || '.' || in_table;
arglen := array_length($4, 1);
IF arglen IS NULL THEN
-- nothing to do, so assume defaults
NULL;
ELSE
-- loop thru args
-- IF 'NO_TRIGGERS' = ANY ($4)
-- select array_to_string($4, ',', '***') INTO vargs;
IF bVerbose THEN RAISE NOTICE 'arguments=%', $4; END IF;
FOREACH avarg IN ARRAY $4 LOOP
IF bVerbose THEN RAISE NOTICE 'arg=%', avarg; END IF;
IF avarg = 'FKEYS_INTERNAL' OR avarg = 'FKEYS_EXTERNAL' OR avarg = 'FKEYS_NONE' THEN
fkcnt = fkcnt + 1;
fktype = avarg;
ELSEIF avarg = 'INCLUDE_TRIGGERS' OR avarg = 'NO_TRIGGERS' THEN
trigcnt = trigcnt + 1;
trigtype = avarg;
ELSEIF avarg = 'PKEY_EXTERNAL' THEN
pkcnt = pkcnt + 1;
pktype = avarg;
ELSEIF avarg = 'COMMENTS' THEN
cmtcnt = cmtcnt + 1;
END IF;
END LOOP;
IF fkcnt > 1 THEN
RAISE WARNING 'Only one foreign key option can be provided. You provided %', fkcnt;
RETURN '';
ELSEIF trigcnt > 1 THEN
RAISE WARNING 'Only one trigger option can be provided. You provided %', trigcnt;
RETURN '';
ELSEIF pkcnt > 1 THEN
RAISE WARNING 'Only one pkey option can be provided. You provided %', pkcnt;
RETURN '';
ELSEIF cmtcnt > 1 THEN
RAISE WARNING 'Only one comments option can be provided. You provided %', cmtcnt;
RETURN '';
END IF;
END IF;
SELECT c.oid, (select setting from pg_settings where name = 'server_version_num') INTO v_table_oid, v_pgversion FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind in ('r','p') AND c.relname = in_table AND n.nspname = in_schema;
-- set search_path = public before we do anything to force explicit schema qualification but dont forget to set it back before exiting...
SELECT setting INTO search_path_old FROM pg_settings WHERE name = 'search_path';
-- RAISE NOTICE 'DEBUG tableddl: saving old search_path: ***%***', search_path_old;
EXECUTE 'SET search_path = "public"';
SELECT setting INTO search_path_new FROM pg_settings WHERE name = 'search_path';
-- RAISE NOTICE 'DEBUG tableddl: using new search path=***%***', search_path_new;
-- throw an error if table was not found
IF (v_table_oid IS NULL) THEN
RAISE EXCEPTION 'table does not exist';
END IF;
-- get user-defined tablespaces if applicable
SELECT tablespace INTO v_temp FROM pg_tables WHERE schemaname = in_schema and tablename = in_table and tablespace IS NOT NULL;
IF v_temp IS NULL THEN
v_tablespace := 'TABLESPACE pg_default';
ELSE
v_tablespace := 'TABLESPACE ' || v_temp;
END IF;
-- also see if there are any SET commands for this table, ie, autovacuum_enabled=off, fillfactor=70
WITH relopts AS (SELECT unnest(c.reloptions) relopts FROM pg_class c, pg_namespace n WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table)
SELECT string_agg(r.relopts, ', ') as relopts INTO v_temp from relopts r;
IF v_temp IS NULL THEN
v_relopts := '';
ELSE
v_relopts := ' WITH (' || v_temp || ')';
END IF;
-- -----------------------------------------------------------------------------------
-- Create table defs for partitions/children using inheritance or declarative methods.
-- inheritance: pg_class.relkind = 'r' pg_class.relispartition=false pg_class.relpartbound is NULL
-- declarative: pg_class.relkind = 'r' pg_class.relispartition=true pg_class.relpartbound is NOT NULL
-- -----------------------------------------------------------------------------------
v_partbound := '';
bPartition := False;
bInheritance := False;
IF v_pgversion < 100000 THEN
-- Issue#11: handle parent schema
SELECT c2.relname parent, c2.relnamespace::regnamespace INTO v_parent, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
IF (v_parent IS NOT NULL) THEN
bPartition := True;
bInheritance := True;
END IF;
ELSE
-- Issue#11: handle parent schema
SELECT c2.relname parent, c1.relispartition, pg_get_expr(c1.relpartbound, c1.oid, true), c2.relnamespace::regnamespace INTO v_parent, bRelispartition, v_partbound, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
IF (v_parent IS NOT NULL) THEN
bPartition := True;
IF bRelispartition THEN
bInheritance := False;
ELSE
bInheritance := True;
END IF;
END IF;
END IF;
IF bPartition THEN
--Issue#17 fix for case-sensitive tables
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
--Issue#19 put double-quotes around SQL keyword column names
-- Issue#121: fix keyword lookup for table name not column name that does not apply here
-- SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R';
SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = in_table AND catcode = 'R';
IF bInheritance THEN
-- inheritance-based
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '"( '|| E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || '( '|| E'\\n';
END IF;
-- Jump to constraints section to add the check constraints
ELSE
-- declarative-based
IF v_relopts <> '' THEN
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '" PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || v_relopts || ' ' || v_tablespace || '; ' || E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || ' PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || v_relopts || ' ' || v_tablespace || '; ' || E'\\n';
END IF;
ELSE
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '" PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || ' ' || v_tablespace || '; ' || E'\\n';
ELSE
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || ' PARTITION OF ' || in_schema || '.' || v_parent || ' ' || v_partbound || ' ' || v_tablespace || '; ' || E'\\n';
END IF;
END IF;
-- Jump to constraints and index section to add the check constraints and indexes and perhaps FKeys
END IF;
END IF;
IF bVerbose THEN RAISE NOTICE '(1)tabledef so far: %', v_table_ddl; END IF;
IF NOT bPartition THEN
-- see if this is unlogged or temporary table
select c.relpersistence into v_persist from pg_class c, pg_namespace n where n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table and c.relkind = 'r';
IF v_persist = 'u' THEN
v_temp := 'UNLOGGED';
ELSIF v_persist = 't' THEN
v_temp := 'TEMPORARY';
ELSE
v_temp := '';
END IF;
END IF;
-- start the create definition for regular tables unless we are in progress creating an inheritance-based child table
IF NOT bPartition THEN
--Issue#17 fix for case-sensitive tables
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
IF v_cnt1 > 0 THEN
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '."' || in_table || '" (' || E'\\n';
ELSE
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '.' || in_table || ' (' || E'\\n';
END IF;
END IF;
-- RAISE NOTICE 'DEBUG2: tabledef so far: %', v_table_ddl;
-- define all of the columns in the table unless we are in progress creating an inheritance-based child table
IF NOT bPartition THEN
FOR v_colrec IN
SELECT c.column_name, c.data_type, c.udt_name, c.udt_schema, c.character_maximum_length, c.is_nullable, c.column_default, c.numeric_precision, c.numeric_scale, c.is_identity, c.identity_generation, c.is_generated, c.generation_expression
FROM information_schema.columns c WHERE (table_schema, table_name) = (in_schema, in_table) ORDER BY ordinal_position
LOOP
IF bVerbose THEN RAISE NOTICE '(col loop) name=% type=% udt_name=% default=% is_generated=% gen_expr=%', v_colrec.column_name, v_colrec.data_type, v_colrec.udt_name, v_colrec.column_default, v_colrec.is_generated, v_colrec.generation_expression; END IF;
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
-- SELECT CASE WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial;
SELECT CASE WHEN pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial;
IF bVerbose THEN
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
-- SELECT pg_get_serial_sequence(v_qualified, v_colrec.column_name) into v_temp;
SELECT pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) into v_temp;
IF v_temp IS NULL THEN v_temp = 'NA'; END IF;
SELECT pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name) INTO v_diag1;
RAISE NOTICE 'DEBUG table: % Column: % datatype: % Serial=% serialval=% coldef=%', v_qualified, v_colrec.column_name, v_colrec.data_type, bSerial, v_temp, v_diag1;
RAISE NOTICE 'DEBUG tabledef: %', v_table_ddl;
END IF;
--Issue#17 put double-quotes around case-sensitive column names
SELECT COUNT(*) INTO v_cnt1 FROM information_schema.columns t WHERE EXISTS (SELECT REGEXP_MATCHES(s.column_name, '([A-Z]+)','g') FROM information_schema.columns s
WHERE t.table_schema=s.table_schema and t.table_name=s.table_name and t.column_name=s.column_name AND t.table_schema = quote_ident(in_schema) AND column_name = v_colrec.column_name);
--Issue#19 put double-quotes around SQL keyword column names
SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R';
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
v_table_ddl := v_table_ddl || ' "' || v_colrec.column_name || '" ';
ELSE
v_table_ddl := v_table_ddl || ' ' || v_colrec.column_name || ' ';
END IF;
-- Issue#23: Handle autogenerated columns and rewrite as a simpler IF THEN ELSE branch instead of a much more complex embedded CASE STATEMENT
IF v_colrec.is_generated = 'ALWAYS' and v_colrec.generation_expression IS NOT NULL THEN
-- searchable tsvector GENERATED ALWAYS AS (to_tsvector('simple'::regconfig, COALESCE(translate(email, '@.-'::citext, ' '::text), ''::text)) ) STORED
v_temp = v_colrec.data_type || ' GENERATED ALWAYS AS (' || v_colrec.generation_expression || ') STORED ';
ELSEIF v_colrec.udt_name in ('geometry', 'box2d', 'box2df', 'box3d', 'geography', 'geometry_dump', 'gidx', 'spheroid', 'valid_detail') THEN
v_temp = v_colrec.udt_name;
ELSEIF v_colrec.data_type = 'USER-DEFINED' THEN
v_temp = v_colrec.udt_schema || '.' || v_colrec.udt_name;
ELSEIF v_colrec.data_type = 'ARRAY' THEN
-- Issue#6 fix: handle arrays
v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name);
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
-- WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL
ELSEIF pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN
-- Issue#8 fix: handle serial. Note: NOT NULL is implied so no need to declare it explicitly
v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name);
ELSE
v_temp = v_colrec.data_type;
END IF;
-- RAISE NOTICE 'column def1=%', v_temp;
-- handle IDENTITY columns
IF v_colrec.is_identity = 'YES' THEN
IF v_colrec.identity_generation = 'ALWAYS' THEN
v_temp = v_temp || ' GENERATED ALWAYS AS IDENTITY';
ELSE
v_temp = v_temp || ' GENERATED BY DEFAULT AS IDENTITY';
END IF;
ELSEIF v_colrec.character_maximum_length IS NOT NULL THEN
v_temp = v_temp || ('(' || v_colrec.character_maximum_length || ')');
ELSEIF v_colrec.numeric_precision > 0 AND v_colrec.numeric_scale > 0 THEN
v_temp = v_temp || '(' || v_colrec.numeric_precision || ',' || v_colrec.numeric_scale || ')';
END IF;
-- Handle NULL/NOT NULL
IF bSerial THEN
v_temp = v_temp || ' NOT NULL';
ELSEIF v_colrec.is_nullable = 'NO' THEN
v_temp = v_temp || ' NOT NULL';
ELSEIF v_colrec.is_nullable = 'YES' THEN
v_temp = v_temp || ' NULL';
END IF;
-- Handle defaults
IF v_colrec.column_default IS NOT null AND NOT bSerial THEN
-- RAISE NOTICE 'Setting default for column, %', v_colrec.column_name;
v_temp = v_temp || (' DEFAULT ' || v_colrec.column_default);
END IF;
v_temp = v_temp || ',' || E'\\n';
-- RAISE NOTICE 'column def2=%', v_temp;
v_table_ddl := v_table_ddl || v_temp;
-- RAISE NOTICE 'tabledef=%', v_table_ddl;
END LOOP;
END IF;
IF bVerbose THEN RAISE NOTICE '(2)tabledef so far: %', v_table_ddl; END IF;
-- define all the constraints: conparentid does not exist pre PGv11
IF v_pgversion < 110000 THEN
FOR v_constraintrec IN
SELECT con.conname as constraint_name, con.contype as constraint_type,
CASE
WHEN con.contype = 'p' THEN 1 -- primary key constraint
WHEN con.contype = 'u' THEN 2 -- unique constraint
WHEN con.contype = 'f' THEN 3 -- foreign key constraint
WHEN con.contype = 'c' THEN 4
ELSE 5
END as type_rank,
pg_get_constraintdef(con.oid) as constraint_definition
FROM pg_catalog.pg_constraint con JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace
WHERE nsp.nspname = in_schema AND rel.relname = in_table ORDER BY type_rank
LOOP
v_constraint_name := v_constraintrec.constraint_name;
v_constraint_def := v_constraintrec.constraint_definition;
IF v_constraintrec.type_rank = 1 THEN
IF pkcnt = 0 OR pktype = 'PKEY_INTERNAL' THEN
-- internal def
v_constraint_name := v_constraintrec.constraint_name;
v_constraint_def := v_constraintrec.constraint_definition;
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- Issue#16 handle external PG def
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table and r.conname = v_constraint_name;
END IF;
IF bPartition THEN
continue;
END IF;
ELSIF v_constraintrec.type_rank = 3 THEN
-- handle foreign key constraints
--Issue#22 fix: added FKEY_NONE check
IF fktype = 'FKEYS_NONE' THEN
-- skip
continue;
ELSIF fkcnt = 0 OR fktype = 'FKEYS_INTERNAL' THEN
-- internal def
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- external def
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table;
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
END IF;
ELSE
-- handle all other constraints besides PKEY and FKEYS as internal defs by default
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
END IF;
if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF;
constraintarr := constraintarr || v_constraintrec.constraint_name:: text;
END LOOP;
ELSE
-- handle PG versions 11 and up
-- Issue#20: Fix logic for external PKEY and FKEYS
FOR v_constraintrec IN
SELECT con.conname as constraint_name, con.contype as constraint_type,
CASE
WHEN con.contype = 'p' THEN 1 -- primary key constraint
WHEN con.contype = 'u' THEN 2 -- unique constraint
WHEN con.contype = 'f' THEN 3 -- foreign key constraint
WHEN con.contype = 'c' THEN 4
ELSE 5
END as type_rank,
pg_get_constraintdef(con.oid) as constraint_definition
FROM pg_catalog.pg_constraint con JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace
WHERE nsp.nspname = in_schema AND rel.relname = in_table
--Issue#13 added this condition:
AND con.conparentid = 0
ORDER BY type_rank
LOOP
v_constraint_name := v_constraintrec.constraint_name;
v_constraint_def := v_constraintrec.constraint_definition;
IF v_constraintrec.type_rank = 1 THEN
IF pkcnt = 0 OR pktype = 'PKEY_INTERNAL' THEN
-- internal def
v_constraint_name := v_constraintrec.constraint_name;
v_constraint_def := v_constraintrec.constraint_definition;
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- Issue#16 handle external PG def
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table;
END IF;
IF bPartition THEN
continue;
END IF;
ELSIF v_constraintrec.type_rank = 3 THEN
-- handle foreign key constraints
--Issue#22 fix: added FKEY_NONE check
IF fktype = 'FKEYS_NONE' THEN
-- skip
continue;
ELSIF fkcnt = 0 OR fktype = 'FKEYS_INTERNAL' THEN
-- internal def
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
ELSE
-- external def
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table and
r.conname = v_constraint_name and r.conparentid = 0;
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
END IF;
ELSE
-- handle all other constraints besides PKEY and FKEYS as internal defs by default
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|| 'CONSTRAINT' || ' '
|| v_constraint_name || ' '
|| v_constraint_def
|| ',' || E'\\n';
END IF;
if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF;
constraintarr := constraintarr || v_constraintrec.constraint_name:: text;
END LOOP;
END IF;
-- drop the last comma before ending the create statement, which should be right before the carriage return character
-- Issue#24: make sure the comma is there before removing it
select substring(v_table_ddl, length(v_table_ddl) - 1, 1) INTO v_temp;
IF v_temp = ',' THEN
v_table_ddl = substr(v_table_ddl, 0, length(v_table_ddl) - 1) || E'\\n';
END IF;
IF bVerbose THEN RAISE NOTICE '(3)tabledef so far: %', trim(v_table_ddl); END IF;
-- ---------------------------------------------------------------------------
-- at this point we have everything up to the last table-enclosing parenthesis
-- ---------------------------------------------------------------------------
IF bVerbose THEN RAISE NOTICE '(4)tabledef so far: %', v_table_ddl; END IF;
-- See if this is an inheritance-based child table and finish up the table create.
IF bPartition and bInheritance THEN
-- Issue#11: handle parent schema
-- v_table_ddl := v_table_ddl || ') INHERITS (' || in_schema || '.' || v_parent || ') ' || E'\\n' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
IF v_parent_schema = '' OR v_parent_schema IS NULL THEN v_parent_schema = in_schema; END IF;
v_table_ddl := v_table_ddl || ') INHERITS (' || v_parent_schema || '.' || v_parent || ') ' || E'\\n' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
END IF;
IF v_pgversion >= 100000 AND NOT bPartition and NOT bInheritance THEN
-- See if this is a partitioned table (pg_class.relkind = 'p') and add the partitioned key
SELECT pg_get_partkeydef(c1.oid) as partition_key INTO v_partition_key FROM pg_class c1 JOIN pg_namespace n ON (n.oid = c1.relnamespace) LEFT JOIN pg_partitioned_table p ON (c1.oid = p.partrelid)
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.relkind = 'p';
IF v_partition_key IS NOT NULL AND v_partition_key <> '' THEN
-- add partition clause
-- NOTE: cannot specify default tablespace for partitioned relations
-- v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ' ' || v_tablespace || ';' || E'\\n';
v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ';' || E'\\n';
ELSEIF v_relopts <> '' THEN
v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
ELSE
-- end the create definition
v_table_ddl := v_table_ddl || ') ' || v_tablespace || ';' || E'\\n';
END IF;
END IF;
IF bVerbose THEN RAISE NOTICE '(5)tabledef so far: %', v_table_ddl; END IF;
-- Add closing paren for regular tables
-- IF NOT bPartition THEN
-- v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || E';\\n';
-- END IF;
-- RAISE NOTICE 'ddlsofar3: %', v_table_ddl;
-- Issue#16 create the external PKEY def if indicated
IF v_pkey_def <> '' THEN
v_table_ddl := v_table_ddl || v_pkey_def || E'\\n';
END IF;
-- Issue#20
IF v_fkey_defs <> '' THEN
v_table_ddl := v_table_ddl || v_fkey_defs || E'\\n';
END IF;
IF bVerbose THEN RAISE NOTICE '(6)tabledef so far: %', v_table_ddl; END IF;
-- create indexes
FOR v_indexrec IN
SELECT indexdef, COALESCE(tablespace, 'pg_default') as tablespace, indexname FROM pg_indexes WHERE (schemaname, tablename) = (in_schema, in_table)
LOOP
-- RAISE NOTICE 'DEBUG6: indexname=% indexdef=%', v_indexrec.indexname, v_indexrec.indexdef;
-- loop through constraints and skip ones already defined
bSkip = False;
FOREACH constraintelement IN ARRAY constraintarr
LOOP
IF constraintelement = v_indexrec.indexname THEN
-- RAISE NOTICE 'DEBUG7: skipping index, %', v_indexrec.indexname;
bSkip = True;
EXIT;
END IF;
END LOOP;
if bSkip THEN CONTINUE; END IF;
-- Add IF NOT EXISTS clause so partition index additions will not be created if declarative partition in effect and index already created on parent
v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE INDEX', 'CREATE INDEX IF NOT EXISTS');
-- Fix Issue#26: do it for unique/primary key indexes as well
v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE UNIQUE INDEX', 'CREATE UNIQUE INDEX IF NOT EXISTS');
-- RAISE NOTICE 'DEBUG8: adding index, %', v_indexrec.indexname;
-- NOTE: cannot specify default tablespace for partitioned relations
IF v_partition_key IS NOT NULL AND v_partition_key <> '' THEN
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ';' || E'\\n';
ELSE
-- Issue#25: see if partial index or not
select CASE WHEN i.indpred IS NOT NULL THEN True ELSE False END INTO v_partial
FROM pg_index i JOIN pg_class c1 ON (i.indexrelid = c1.oid) JOIN pg_class c2 ON (i.indrelid = c2.oid)
WHERE c1.relnamespace::regnamespace::text = in_schema AND c2.relnamespace::regnamespace::text = in_schema AND c2.relname = in_table AND c1.relname = v_indexrec.indexname;
IF v_partial THEN
-- Put tablespace def before WHERE CLAUSE
v_temp = v_indexrec.indexdef;
v_pos = POSITION(' WHERE ' IN v_temp);
v_temp2 = SUBSTRING(v_temp, v_pos);
v_temp = SUBSTRING(v_temp, 1, v_pos);
v_table_ddl := v_table_ddl || v_temp || ' TABLESPACE ' || v_indexrec.tablespace || v_temp2 || ';' || E'\\n';
ELSE
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ' TABLESPACE ' || v_indexrec.tablespace || ';' || E'\\n';
END IF;
END IF;
END LOOP;
IF bVerbose THEN RAISE NOTICE '(7)tabledef so far: %', v_table_ddl; END IF;
-- Issue#20: added logic for table and column comments
IF cmtcnt > 0 THEN
FOR v_rec IN
SELECT c.relname, 'COMMENT ON ' || CASE WHEN c.relkind in ('r','p') AND a.attname IS NULL THEN 'TABLE ' WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN 'COLUMN ' WHEN c.relkind = 'f' THEN 'FOREIGN TABLE '
WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW ' WHEN c.relkind = 'v' THEN 'VIEW ' WHEN c.relkind = 'i' THEN 'INDEX ' WHEN c.relkind = 'S' THEN 'SEQUENCE ' ELSE 'XX' END || n.nspname || '.' ||
CASE WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN quote_ident(c.relname) || '.' || a.attname ELSE quote_ident(c.relname) END || ' IS ' || quote_literal(d.description) || ';' as ddl
FROM pg_class c JOIN pg_namespace n ON (n.oid = c.relnamespace) LEFT JOIN pg_description d ON (c.oid = d.objoid) LEFT JOIN pg_attribute a ON (c.oid = a.attrelid AND a.attnum > 0 and a.attnum = d.objsubid)
WHERE d.description IS NOT NULL AND n.nspname = in_schema AND c.relname = in_table ORDER BY 2 desc, ddl
LOOP
--RAISE NOTICE 'comments:%', v_rec.ddl;
v_table_ddl = v_table_ddl || v_rec.ddl || E'\\n';
END LOOP;
END IF;
IF bVerbose THEN RAISE NOTICE '(8)tabledef so far: %', v_table_ddl; END IF;
IF trigtype = 'INCLUDE_TRIGGERS' THEN
-- Issue#14: handle multiple triggers for a table
FOR v_trigrec IN
select pg_get_triggerdef(t.oid, True) || ';' as triggerdef FROM pg_trigger t, pg_class c, pg_namespace n
WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table and c.relkind = 'r' and t.tgrelid = c.oid and NOT t.tgisinternal
LOOP
v_table_ddl := v_table_ddl || v_trigrec.triggerdef;
v_table_ddl := v_table_ddl || E'\\n';
IF bVerbose THEN RAISE NOTICE 'triggerdef = %', v_trigrec.triggerdef; END IF;
END LOOP;
END IF;
IF bVerbose THEN RAISE NOTICE '(9)tabledef so far: %', v_table_ddl; END IF;
-- add empty line
v_table_ddl := v_table_ddl || E'\\n';
IF bVerbose THEN RAISE NOTICE '(10)tabledef so far: %', v_table_ddl; END IF;
-- reset search_path back to what it was
IF search_path_old = '' THEN
SELECT set_config('search_path', '', false) into v_temp;
ELSE
EXECUTE 'SET search_path = ' || search_path_old;
END IF;
RETURN v_table_ddl;
EXCEPTION
WHEN others THEN
BEGIN
GET STACKED DIAGNOSTICS v_diag1 = MESSAGE_TEXT, v_diag2 = PG_EXCEPTION_DETAIL, v_diag3 = PG_EXCEPTION_HINT, v_diag4 = RETURNED_SQLSTATE, v_diag5 = PG_CONTEXT, v_diag6 = PG_EXCEPTION_CONTEXT;
-- v_ret := 'line=' || v_diag6 || '. '|| v_diag4 || '. ' || v_diag1 || ' .' || v_diag2 || ' .' || v_diag3;
v_ret := 'line=' || v_diag6 || '. '|| v_diag4 || '. ' || v_diag1;
RAISE EXCEPTION '%', v_ret;
-- put additional coding here if necessarY
RETURN '';
END;
END;
$$;`.trim()
export const getTableDefinitionSql = ({ id }: { id: number }) => {
const sql = /* SQL */ `
${CREATE_PG_GET_TABLEDEF_SQL}
with table_info as (
select
n.nspname::text as schema,
c.relname::text as name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.oid = ${id}
)
select pg_temp.pg_get_tabledef (
t.schema,
t.name,
false,
'FKEYS_INTERNAL',
'INCLUDE_TRIGGERS'
) as definition
from table_info t;
`.trim()
return sql
}
export const getEntityDefinitionsSql = ({
schemas,
limit = 100,
}: {
schemas: string[]
limit?: number
}) => {
const sql = /* SQL */ `
${CREATE_PG_GET_TABLEDEF_SQL}
with records as (
select
c.oid::int8 as "id",
case c.relkind
when 'r' then pg_temp.pg_get_tabledef(
concat(nc.nspname),
concat(c.relname),
false,
'FKEYS_INTERNAL',
'NO_TRIGGERS'
)
when 'v' then concat(
'create view ', concat(nc.nspname, '.', c.relname), ' as',
pg_get_viewdef(concat(nc.nspname, '.', c.relname), true)
)
when 'm' then concat(
'create materialized view ', concat(nc.nspname, '.', c.relname), ' as',
pg_get_viewdef(concat(nc.nspname, '.', c.relname), true)
)
when 'f' then concat('create foreign table ', nc.nspname, '.', c.relname, ' ( ... )')
when 'p' then pg_temp.pg_get_tabledef(
concat(nc.nspname),
concat(c.relname),
false,
'FKEYS_INTERNAL',
'NO_TRIGGERS'
)
end as "sql"
from
pg_namespace nc
join pg_class c on nc.oid = c.relnamespace
where
c.relkind in ('r', 'v', 'm', 'f', 'p')
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')
)
and nc.nspname IN (${schemas.map((schema) => `'${schema}'`).join(', ')})
order by c.relname asc
limit ${limit}
offset 0
)
select
jsonb_build_object(
'definitions', coalesce(jsonb_agg(
jsonb_build_object(
'id', r.id,
'sql', r.sql
)
), '[]'::jsonb)
) "data"
from records r;
`.trim()
return sql
}
@@ -0,0 +1,38 @@
import { ident } from '../../../pg-format'
import { PGTrigger, PGTriggerCreate } from '../../../pg-meta-triggers'
// [Joshen] Writing this query within FE as the PATCH endpoint from pg-meta only supports updating
// trigger name and enabled mode. So we'll delete and create the trigger, within a single transaction
// Copying the SQL from https://github.com/supabase/postgres-meta/blob/master/src/lib/PostgresMetaTriggers.ts
export function getDatabaseTriggerUpdateSQL({
originalTrigger,
updatedTrigger,
}: {
originalTrigger: {
id: number
table_id: number
enabled_mode: 'ORIGIN' | 'REPLICA' | 'ALWAYS' | 'DISABLED'
name: string
table: string
schema: string
condition: string | null
orientation: 'ROW' | 'STATEMENT'
activation: 'BEFORE' | 'AFTER' | 'INSTEAD OF'
events: string[]
function_schema: string
function_name: string
function_args: string[]
}
updatedTrigger: PGTriggerCreate & Pick<PGTrigger, 'enabled_mode'>
}) {
const { name, activation, events, schema, table, function_schema, function_name, function_args } =
updatedTrigger
return /* SQL */ `
BEGIN;
DROP TRIGGER "${originalTrigger.name}" ON "${originalTrigger.schema}"."${originalTrigger.table}";
CREATE TRIGGER "${name}" ${activation} ${events.join(' OR ')} ON "${schema}"."${table}"
FOR EACH ROW EXECUTE FUNCTION
"${function_schema}"."${function_name}"(${function_args?.map(ident).join(',') ?? ''});
COMMIT;
`.trim()
}
@@ -0,0 +1,21 @@
export const getViewDefinitionSql = ({ id }: { id: number }) => {
if (!id) {
throw new Error('id is required')
}
const sql = /* SQL */ `
with table_info as (
select
n.nspname::text as schema,
c.relname::text as name,
to_regclass(concat('"', n.nspname, '"."', c.relname, '"')) as regclass
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.oid = ${id}
)
select pg_get_viewdef(t.regclass, true) as definition
from table_info t
`.trim()
return sql
}
@@ -1,12 +0,0 @@
import { literal } from '../../pg-format'
export const getLiveTupleEstimate = (table: string, schema: string = 'public') => {
const sql = /* SQL */ `
SELECT n_live_tup AS live_tuple_estimate
FROM pg_stat_user_tables
WHERE schemaname = ${literal(schema)}
AND relname = ${literal(table)};
`.trim()
return sql
}
@@ -0,0 +1,3 @@
export const getAbortQuerySQL = ({ pid }: { pid: number }) => {
return `select pg_terminate_backend(${pid})`.trim()
}
@@ -0,0 +1,3 @@
export * from './abort-query'
export * from './keywords'
export * from './ongoing-queries'
@@ -0,0 +1,7 @@
export const getKeywordsSql = () => {
const sql = /* SQL */ `
SELECT word FROM pg_get_keywords();
`.trim()
return sql
}
@@ -0,0 +1,7 @@
export const getOngoingQueriesSql = () => {
const sql = /* SQL */ `
select pid, query, query_start from pg_stat_activity where state = 'active' and datname = 'postgres';
`.trim()
return sql
}
@@ -1,5 +1,33 @@
import { ident } from '../../../pg-format'
export const getTableConstraintsSql = ({ id }: { id: number }) => {
const sql = /* SQL */ `
with table_info as (
select
n.nspname::text as schema,
c.relname::text as name,
to_regclass(concat('"', n.nspname, '"."', c.relname, '"')) as regclass
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.oid = ${id}
)
select
con.oid as id,
con.conname as name,
con.contype as type
from pg_catalog.pg_constraint con
inner join pg_catalog.pg_class rel
on rel.oid = con.conrelid
inner join pg_catalog.pg_namespace nsp
on nsp.oid = connamespace
inner join table_info ti
on ti.schema = nsp.nspname
and ti.name = rel.relname;
`.trim()
return sql
}
export const getDropConstraintSQL = ({
schema,
table,
@@ -0,0 +1,75 @@
export const getEntityTypesSQL = ({
schemas,
search,
sort,
filterTypes,
limit,
page,
}: {
schemas: string[]
search?: string
sort: 'alphabetical' | 'grouped-alphabetical'
filterTypes: string[]
limit: number
page: number
}) => {
const innerOrderBy = sort === 'alphabetical' ? `c.relname asc` : `"type_sort" asc, c.relname asc`
const outerOrderBy = sort === 'alphabetical' ? `r.name asc` : `r.type_sort asc, r.name asc`
const sql = /* SQL */ `
with records as (
select
c.oid::int8 as "id",
nc.nspname as "schema",
c.relname as "name",
c.relkind as "type",
case c.relkind
when 'r' then 1
when 'v' then 2
when 'm' then 3
when 'f' then 4
when 'p' then 5
end as "type_sort",
obj_description(c.oid) as "comment",
count(*) over() as "count",
c.relrowsecurity as "rls_enabled"
from
pg_namespace nc
join pg_class c on nc.oid = c.relnamespace
where
c.relkind in (${filterTypes.map((x) => `'${x}'`).join(', ')})
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')
)
and nc.nspname in (${schemas.map((x) => `'${x}'`)})
${search ? `and c.relname ilike '%${search}%'` : ''}
order by ${innerOrderBy}
limit ${limit}
offset ${page * limit}
)
select
jsonb_build_object(
'entities', coalesce(jsonb_agg(
jsonb_build_object(
'id', r.id,
'schema', r.schema,
'name', r.name,
'type', r.type,
'comment', r.comment,
'rls_enabled', r.rls_enabled
)
order by ${outerOrderBy}
), '[]'::jsonb),
'count', coalesce(min(r.count), 0)
) "data"
from records r;
`.trim()
return sql
}
@@ -64,6 +64,61 @@ DROP CONSTRAINT IF EXISTS ${ident(relation.name)}
)
}
export const getForeignKeyConstraintsSql = ({ schema }: { schema: string }) => {
if (!schema) throw new Error('schema is required')
const sql = /* SQL */ `
SELECT
con.oid as id,
con.conname as constraint_name,
con.confdeltype as deletion_action,
con.confupdtype as update_action,
rel.oid as source_id,
nsp.nspname as source_schema,
rel.relname as source_table,
(
SELECT
array_agg(
att.attname
ORDER BY
un.ord
)
FROM
unnest(con.conkey) WITH ORDINALITY un (attnum, ord)
INNER JOIN pg_attribute att ON att.attnum = un.attnum
WHERE
att.attrelid = rel.oid
) source_columns,
frel.oid as target_id,
fnsp.nspname as target_schema,
frel.relname as target_table,
(
SELECT
array_agg(
att.attname
ORDER BY
un.ord
)
FROM
unnest(con.confkey) WITH ORDINALITY un (attnum, ord)
INNER JOIN pg_attribute att ON att.attnum = un.attnum
WHERE
att.attrelid = frel.oid
) target_columns
FROM
pg_constraint con
INNER JOIN pg_class rel ON rel.oid = con.conrelid
INNER JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
INNER JOIN pg_class frel ON frel.oid = con.confrelid
INNER JOIN pg_namespace fnsp ON fnsp.oid = frel.relnamespace
WHERE
con.contype = 'f'
AND nsp.nspname = '${schema}'
`.trim()
return sql
}
export interface ForeignKey {
id?: number | string
name?: string
@@ -4,3 +4,4 @@ export * from './foreign-keys'
export * from './primary-keys'
export * from './rls'
export * from './table'
export * from './entities'