From be26feb9bac28905cd77c8f9c096ecd9eabac222 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 19 Mar 2026 18:31:46 +0800 Subject: [PATCH] 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) --- .../footer/pagination/Pagination.tsx | 2 +- .../header/sort/SortPopoverPrimitive.tsx | 2 +- .../interfaces/Auth/Users/UsersFooter.tsx | 3 +- .../Integrations/Queues/QueuesSettings.tsx | 15 +- ...se-queues-expose-postgrest-status-query.ts | 13 +- ...tabase-queues-toggle-postgrest-mutation.ts | 227 +---- ...ase-trigger-update-transaction-mutation.ts | 24 +- .../studio/data/database/constraints-query.ts | 34 +- .../data/database/database-size-query.ts | 12 +- .../database/database-table-definition.ts | 708 ---------------- .../data/database/entity-definitions-query.ts | 77 +- .../database/foreign-key-constraints-query.ts | 58 +- apps/studio/data/database/keywords-query.ts | 12 +- .../data/database/max-connections-query.ts | 8 +- apps/studio/data/database/migrations-query.ts | 13 +- .../database/primary-keys-exists-query.ts | 55 -- .../data/database/table-columns-query.ts | 77 +- .../data/database/table-definition-query.ts | 33 +- .../database/table-index-advisor-query.ts | 60 +- .../data/database/view-definition-query.ts | 29 +- .../entity-types-infinite-query.ts | 60 +- .../data/read-replicas/replica-lag-query.ts | 17 +- apps/studio/data/sql/abort-query-mutation.ts | 7 +- apps/studio/data/sql/ongoing-queries-query.ts | 14 +- .../data/tables/tables-roles-access-query.ts | 4 +- apps/studio/hooks/useProtectedSchemas.ts | 2 +- apps/studio/lib/ai/tools/fallback-tools.ts | 7 +- packages/pg-meta/src/index.ts | 62 +- .../src/sql/studio/advisor/index-advisor.ts | 55 ++ .../pg-meta/src/sql/studio/advisor/index.ts | 1 + .../src/sql/studio/auth/get-users-count.ts | 2 +- .../sql/studio/check-primary-keys-exists.ts | 20 - .../check-tables-anon-authenticated-access.ts | 0 .../src/sql/studio/database/columns.ts | 74 ++ .../{ => database}/get-count-estimate.ts | 0 .../pg-meta/src/sql/studio/database/index.ts | 10 + .../src/sql/studio/database/migrations.ts | 10 + .../pg-meta/src/sql/studio/database/misc.ts | 39 + .../sql/studio/{ => database}/privileges.ts | 0 .../pg-meta/src/sql/studio/database/queues.ts | 215 +++++ .../pg-meta/src/sql/studio/database/rows.ts | 2 +- .../sql/studio/database/table-definition.ts | 802 ++++++++++++++++++ .../src/sql/studio/database/triggers.ts | 38 + .../pg-meta/src/sql/studio/database/views.ts | 21 + .../src/sql/studio/get-live-tuple-estimate.ts | 12 - .../src/sql/studio/sql-editor/abort-query.ts | 3 + .../src/sql/studio/sql-editor/index.ts | 3 + .../src/sql/studio/sql-editor/keywords.ts | 7 + .../sql/studio/sql-editor/ongoing-queries.ts | 7 + .../sql/studio/table-editor/constraints.ts | 28 + .../src/sql/studio/table-editor/entities.ts | 75 ++ .../sql/studio/table-editor/foreign-keys.ts | 55 ++ .../src/sql/studio/table-editor/index.ts | 1 + 53 files changed, 1537 insertions(+), 1578 deletions(-) delete mode 100644 apps/studio/data/database/database-table-definition.ts delete mode 100644 apps/studio/data/database/primary-keys-exists-query.ts create mode 100644 packages/pg-meta/src/sql/studio/advisor/index-advisor.ts create mode 100644 packages/pg-meta/src/sql/studio/advisor/index.ts delete mode 100644 packages/pg-meta/src/sql/studio/check-primary-keys-exists.ts rename packages/pg-meta/src/sql/studio/{ => database}/check-tables-anon-authenticated-access.ts (100%) create mode 100644 packages/pg-meta/src/sql/studio/database/columns.ts rename packages/pg-meta/src/sql/studio/{ => database}/get-count-estimate.ts (100%) create mode 100644 packages/pg-meta/src/sql/studio/database/migrations.ts create mode 100644 packages/pg-meta/src/sql/studio/database/misc.ts rename packages/pg-meta/src/sql/studio/{ => database}/privileges.ts (100%) create mode 100644 packages/pg-meta/src/sql/studio/database/queues.ts create mode 100644 packages/pg-meta/src/sql/studio/database/table-definition.ts create mode 100644 packages/pg-meta/src/sql/studio/database/triggers.ts create mode 100644 packages/pg-meta/src/sql/studio/database/views.ts delete mode 100644 packages/pg-meta/src/sql/studio/get-live-tuple-estimate.ts create mode 100644 packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts create mode 100644 packages/pg-meta/src/sql/studio/sql-editor/index.ts create mode 100644 packages/pg-meta/src/sql/studio/sql-editor/keywords.ts create mode 100644 packages/pg-meta/src/sql/studio/sql-editor/ongoing-queries.ts create mode 100644 packages/pg-meta/src/sql/studio/table-editor/entities.ts diff --git a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx index 4a4b2914742..4a1cade3260 100644 --- a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx +++ b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx @@ -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' diff --git a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx index ee26db7e751..963e981f129 100644 --- a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx +++ b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx @@ -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' diff --git a/apps/studio/components/interfaces/Auth/Users/UsersFooter.tsx b/apps/studio/components/interfaces/Auth/Users/UsersFooter.tsx index 1edf69ca47d..38295c01e47 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersFooter.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersFooter.tsx @@ -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' diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx index e39d4506f02..65ae0420e1a 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx @@ -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 diff --git a/apps/studio/data/database-queues/database-queues-expose-postgrest-status-query.ts b/apps/studio/data/database-queues/database-queues-expose-postgrest-status-query.ts index 56515efef60..af1a4af7300 100644 --- a/apps/studio/data/database-queues/database-queues-expose-postgrest-status-query.ts +++ b/apps/studio/data/database-queues/database-queues-expose-postgrest-status-query.ts @@ -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 } diff --git a/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts b/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts index bd637945550..049c3494e6c 100644 --- a/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts +++ b/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts @@ -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, diff --git a/apps/studio/data/database-triggers/database-trigger-update-transaction-mutation.ts b/apps/studio/data/database-triggers/database-trigger-update-transaction-mutation.ts index a71b0425932..4786808fc8e 100644 --- a/apps/studio/data/database-triggers/database-trigger-update-transaction-mutation.ts +++ b/apps/studio/data/database-triggers/database-trigger-update-transaction-mutation.ts @@ -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 } -export function getDatabaseTriggerUpdateSQL({ - originalTrigger, - updatedTrigger, -}: Pick) { - 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, diff --git a/apps/studio/data/database/constraints-query.ts b/apps/studio/data/database/constraints-query.ts index 22fc7c249ec..2be0ade1e49 100644 --- a/apps/studio/data/database/constraints-query.ts +++ b/apps/studio/data/database/constraints-query.ts @@ -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] }, diff --git a/apps/studio/data/database/database-size-query.ts b/apps/studio/data/database/database-size-query.ts index e2ac985f56a..9601c6ed8e3 100644 --- a/apps/studio/data/database/database-size-query.ts +++ b/apps/studio/data/database/database-size-query.ts @@ -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 diff --git a/apps/studio/data/database/database-table-definition.ts b/apps/studio/data/database/database-table-definition.ts deleted file mode 100644 index 59baf815eee..00000000000 --- a/apps/studio/data/database/database-table-definition.ts +++ /dev/null @@ -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 } -) diff --git a/apps/studio/data/database/entity-definitions-query.ts b/apps/studio/data/database/entity-definitions-query.ts index f32f87e3c6b..1a9dd14ba94 100644 --- a/apps/studio/data/database/entity-definitions-query.ts +++ b/apps/studio/data/database/entity-definitions-query.ts @@ -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 diff --git a/apps/studio/data/database/foreign-key-constraints-query.ts b/apps/studio/data/database/foreign-key-constraints-query.ts index 5b6782a51b9..e68c5660e73 100644 --- a/apps/studio/data/database/foreign-key-constraints-query.ts +++ b/apps/studio/data/database/foreign-key-constraints-query.ts @@ -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( diff --git a/apps/studio/data/database/keywords-query.ts b/apps/studio/data/database/keywords-query.ts index b2cb3e68af6..b1f2c3667c8 100644 --- a/apps/studio/data/database/keywords-query.ts +++ b/apps/studio/data/database/keywords-query.ts @@ -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 diff --git a/apps/studio/data/database/max-connections-query.ts b/apps/studio/data/database/max-connections-query.ts index bf3a183fce7..1575cc4fe3e 100644 --- a/apps/studio/data/database/max-connections-query.ts +++ b/apps/studio/data/database/max-connections-query.ts @@ -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 diff --git a/apps/studio/data/database/migrations-query.ts b/apps/studio/data/database/migrations-query.ts index f15f44e8108..ae1b7209930 100644 --- a/apps/studio/data/database/migrations-query.ts +++ b/apps/studio/data/database/migrations-query.ts @@ -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 diff --git a/apps/studio/data/database/primary-keys-exists-query.ts b/apps/studio/data/database/primary-keys-exists-query.ts deleted file mode 100644 index fb3091d0a76..00000000000 --- a/apps/studio/data/database/primary-keys-exists-query.ts +++ /dev/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> -export type CheckPrimaryKeysExistsError = ResponseError - -export const useCheckPrimaryKeysExists = ( - { projectRef, connectionString, tables }: CheckPrimaryKeysExistsVariables, - { - enabled = true, - ...options - }: UseCustomQueryOptions = {} -) => - useQuery({ - queryKey: databaseKeys.checkPrimaryKeysExists(projectRef, tables), - queryFn: () => checkPrimaryKeysExists({ projectRef, connectionString, tables }), - retry: false, - enabled: enabled && typeof projectRef !== 'undefined' && tables.length > 0, - ...options, - }) diff --git a/apps/studio/data/database/table-columns-query.ts b/apps/studio/data/database/table-columns-query.ts index fbd23edcded..55d614a7fb5 100644 --- a/apps/studio/data/database/table-columns-query.ts +++ b/apps/studio/data/database/table-columns-query.ts @@ -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 diff --git a/apps/studio/data/database/table-definition-query.ts b/apps/studio/data/database/table-definition-query.ts index 4d0aaa6ab9d..0d472a75a98 100644 --- a/apps/studio/data/database/table-definition-query.ts +++ b/apps/studio/data/database/table-definition-query.ts @@ -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( diff --git a/apps/studio/data/database/table-index-advisor-query.ts b/apps/studio/data/database/table-index-advisor-query.ts index c682f42b090..c57d683cdd7 100644 --- a/apps/studio/data/database/table-index-advisor-query.ts +++ b/apps/studio/data/database/table-index-advisor-query.ts @@ -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 { diff --git a/apps/studio/data/database/view-definition-query.ts b/apps/studio/data/database/view-definition-query.ts index 411a7204825..4838e1d0a2c 100644 --- a/apps/studio/data/database/view-definition-query.ts +++ b/apps/studio/data/database/view-definition-query.ts @@ -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( { diff --git a/apps/studio/data/entity-types/entity-types-infinite-query.ts b/apps/studio/data/entity-types/entity-types-infinite-query.ts index 21a7306cb00..fbec33185cd 100644 --- a/apps/studio/data/entity-types/entity-types-infinite-query.ts +++ b/apps/studio/data/entity-types/entity-types-infinite-query.ts @@ -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( { diff --git a/apps/studio/data/read-replicas/replica-lag-query.ts b/apps/studio/data/read-replicas/replica-lag-query.ts index dc026bbfb35..b4ce270d565 100644 --- a/apps/studio/data/read-replicas/replica-lag-query.ts +++ b/apps/studio/data/read-replicas/replica-lag-query.ts @@ -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 diff --git a/apps/studio/data/sql/abort-query-mutation.ts b/apps/studio/data/sql/abort-query-mutation.ts index bbfc5b7d9e4..13f8a7e87de 100644 --- a/apps/studio/data/sql/abort-query-mutation.ts +++ b/apps/studio/data/sql/abort-query-mutation.ts @@ -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 } diff --git a/apps/studio/data/sql/ongoing-queries-query.ts b/apps/studio/data/sql/ongoing-queries-query.ts index 9c258aba9b7..9ca249d37e3 100644 --- a/apps/studio/data/sql/ongoing-queries-query.ts +++ b/apps/studio/data/sql/ongoing-queries-query.ts @@ -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'] }, diff --git a/apps/studio/data/tables/tables-roles-access-query.ts b/apps/studio/data/tables/tables-roles-access-query.ts index 408fef212e3..dad21c73699 100644 --- a/apps/studio/data/tables/tables-roles-access-query.ts +++ b/apps/studio/data/tables/tables-roles-access-query.ts @@ -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 diff --git a/apps/studio/hooks/useProtectedSchemas.ts b/apps/studio/hooks/useProtectedSchemas.ts index 6597028711b..4d72705d697 100644 --- a/apps/studio/hooks/useProtectedSchemas.ts +++ b/apps/studio/hooks/useProtectedSchemas.ts @@ -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 diff --git a/apps/studio/lib/ai/tools/fallback-tools.ts b/apps/studio/lib/ai/tools/fallback-tools.ts index 5c0b2b1c2fe..1c90cdfffb9 100644 --- a/apps/studio/lib/ai/tools/fallback-tools.ts +++ b/apps/studio/lib/ai/tools/fallback-tools.ts @@ -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, diff --git a/packages/pg-meta/src/index.ts b/packages/pg-meta/src/index.ts index c5692818611..6035d5789da 100644 --- a/packages/pg-meta/src/index.ts +++ b/packages/pg-meta/src/index.ts @@ -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, diff --git a/packages/pg-meta/src/sql/studio/advisor/index-advisor.ts b/packages/pg-meta/src/sql/studio/advisor/index-advisor.ts new file mode 100644 index 00000000000..3fe41522920 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/advisor/index-advisor.ts @@ -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() +} diff --git a/packages/pg-meta/src/sql/studio/advisor/index.ts b/packages/pg-meta/src/sql/studio/advisor/index.ts new file mode 100644 index 00000000000..1bc3b58c74e --- /dev/null +++ b/packages/pg-meta/src/sql/studio/advisor/index.ts @@ -0,0 +1 @@ +export * from './index-advisor' diff --git a/packages/pg-meta/src/sql/studio/auth/get-users-count.ts b/packages/pg-meta/src/sql/studio/auth/get-users-count.ts index 530664402ff..864dd2807c9 100644 --- a/packages/pg-meta/src/sql/studio/auth/get-users-count.ts +++ b/packages/pg-meta/src/sql/studio/auth/get-users-count.ts @@ -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' diff --git a/packages/pg-meta/src/sql/studio/check-primary-keys-exists.ts b/packages/pg-meta/src/sql/studio/check-primary-keys-exists.ts deleted file mode 100644 index 203e72bf423..00000000000 --- a/packages/pg-meta/src/sql/studio/check-primary-keys-exists.ts +++ /dev/null @@ -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; -` -} diff --git a/packages/pg-meta/src/sql/studio/check-tables-anon-authenticated-access.ts b/packages/pg-meta/src/sql/studio/database/check-tables-anon-authenticated-access.ts similarity index 100% rename from packages/pg-meta/src/sql/studio/check-tables-anon-authenticated-access.ts rename to packages/pg-meta/src/sql/studio/database/check-tables-anon-authenticated-access.ts diff --git a/packages/pg-meta/src/sql/studio/database/columns.ts b/packages/pg-meta/src/sql/studio/database/columns.ts new file mode 100644 index 00000000000..170071cf455 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/database/columns.ts @@ -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 +} diff --git a/packages/pg-meta/src/sql/studio/get-count-estimate.ts b/packages/pg-meta/src/sql/studio/database/get-count-estimate.ts similarity index 100% rename from packages/pg-meta/src/sql/studio/get-count-estimate.ts rename to packages/pg-meta/src/sql/studio/database/get-count-estimate.ts diff --git a/packages/pg-meta/src/sql/studio/database/index.ts b/packages/pg-meta/src/sql/studio/database/index.ts index ef3bfe8ad23..293e332cdff 100644 --- a/packages/pg-meta/src/sql/studio/database/index.ts +++ b/packages/pg-meta/src/sql/studio/database/index.ts @@ -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' diff --git a/packages/pg-meta/src/sql/studio/database/migrations.ts b/packages/pg-meta/src/sql/studio/database/migrations.ts new file mode 100644 index 00000000000..2d0148f289d --- /dev/null +++ b/packages/pg-meta/src/sql/studio/database/migrations.ts @@ -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 +} diff --git a/packages/pg-meta/src/sql/studio/database/misc.ts b/packages/pg-meta/src/sql/studio/database/misc.ts new file mode 100644 index 00000000000..e98e40e734f --- /dev/null +++ b/packages/pg-meta/src/sql/studio/database/misc.ts @@ -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 +} diff --git a/packages/pg-meta/src/sql/studio/privileges.ts b/packages/pg-meta/src/sql/studio/database/privileges.ts similarity index 100% rename from packages/pg-meta/src/sql/studio/privileges.ts rename to packages/pg-meta/src/sql/studio/database/privileges.ts diff --git a/packages/pg-meta/src/sql/studio/database/queues.ts b/packages/pg-meta/src/sql/studio/database/queues.ts new file mode 100644 index 00000000000..a0aa3674ba2 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/database/queues.ts @@ -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() +} diff --git a/packages/pg-meta/src/sql/studio/database/rows.ts b/packages/pg-meta/src/sql/studio/database/rows.ts index fd475b5ef7f..6de45ce52ec 100644 --- a/packages/pg-meta/src/sql/studio/database/rows.ts +++ b/packages/pg-meta/src/sql/studio/database/rows.ts @@ -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 diff --git a/packages/pg-meta/src/sql/studio/database/table-definition.ts b/packages/pg-meta/src/sql/studio/database/table-definition.ts new file mode 100644 index 00000000000..78e47afe139 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/database/table-definition.ts @@ -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 +} diff --git a/packages/pg-meta/src/sql/studio/database/triggers.ts b/packages/pg-meta/src/sql/studio/database/triggers.ts new file mode 100644 index 00000000000..7fba812d058 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/database/triggers.ts @@ -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 +}) { + 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() +} diff --git a/packages/pg-meta/src/sql/studio/database/views.ts b/packages/pg-meta/src/sql/studio/database/views.ts new file mode 100644 index 00000000000..0594e73fd04 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/database/views.ts @@ -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 +} diff --git a/packages/pg-meta/src/sql/studio/get-live-tuple-estimate.ts b/packages/pg-meta/src/sql/studio/get-live-tuple-estimate.ts deleted file mode 100644 index cf8dcb40638..00000000000 --- a/packages/pg-meta/src/sql/studio/get-live-tuple-estimate.ts +++ /dev/null @@ -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 -} diff --git a/packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts b/packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts new file mode 100644 index 00000000000..9909e25092d --- /dev/null +++ b/packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts @@ -0,0 +1,3 @@ +export const getAbortQuerySQL = ({ pid }: { pid: number }) => { + return `select pg_terminate_backend(${pid})`.trim() +} diff --git a/packages/pg-meta/src/sql/studio/sql-editor/index.ts b/packages/pg-meta/src/sql/studio/sql-editor/index.ts new file mode 100644 index 00000000000..7f744430299 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/sql-editor/index.ts @@ -0,0 +1,3 @@ +export * from './abort-query' +export * from './keywords' +export * from './ongoing-queries' diff --git a/packages/pg-meta/src/sql/studio/sql-editor/keywords.ts b/packages/pg-meta/src/sql/studio/sql-editor/keywords.ts new file mode 100644 index 00000000000..56297ea1a0e --- /dev/null +++ b/packages/pg-meta/src/sql/studio/sql-editor/keywords.ts @@ -0,0 +1,7 @@ +export const getKeywordsSql = () => { + const sql = /* SQL */ ` +SELECT word FROM pg_get_keywords(); +`.trim() + + return sql +} diff --git a/packages/pg-meta/src/sql/studio/sql-editor/ongoing-queries.ts b/packages/pg-meta/src/sql/studio/sql-editor/ongoing-queries.ts new file mode 100644 index 00000000000..7fc0e22fc92 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/sql-editor/ongoing-queries.ts @@ -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 +} diff --git a/packages/pg-meta/src/sql/studio/table-editor/constraints.ts b/packages/pg-meta/src/sql/studio/table-editor/constraints.ts index 82354ee2474..98d2c8f6ef9 100644 --- a/packages/pg-meta/src/sql/studio/table-editor/constraints.ts +++ b/packages/pg-meta/src/sql/studio/table-editor/constraints.ts @@ -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, diff --git a/packages/pg-meta/src/sql/studio/table-editor/entities.ts b/packages/pg-meta/src/sql/studio/table-editor/entities.ts new file mode 100644 index 00000000000..7b3dd619fd4 --- /dev/null +++ b/packages/pg-meta/src/sql/studio/table-editor/entities.ts @@ -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 +} diff --git a/packages/pg-meta/src/sql/studio/table-editor/foreign-keys.ts b/packages/pg-meta/src/sql/studio/table-editor/foreign-keys.ts index 617d0cde241..89162d11bf0 100644 --- a/packages/pg-meta/src/sql/studio/table-editor/foreign-keys.ts +++ b/packages/pg-meta/src/sql/studio/table-editor/foreign-keys.ts @@ -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 diff --git a/packages/pg-meta/src/sql/studio/table-editor/index.ts b/packages/pg-meta/src/sql/studio/table-editor/index.ts index 25e6f5d6c98..3fb5ea4a313 100644 --- a/packages/pg-meta/src/sql/studio/table-editor/index.ts +++ b/packages/pg-meta/src/sql/studio/table-editor/index.ts @@ -4,3 +4,4 @@ export * from './foreign-keys' export * from './primary-keys' export * from './rls' export * from './table' +export * from './entities'