From b1531545fbb4247d37e538e8bc1dbf6bf3df3871 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:02:02 -0400 Subject: [PATCH] security: migrate more files to safeSql (#44727) ## Summary by CodeRabbit * **Refactor** * Overhauled SQL generation across the Studio: queries and helpers now use safer, parameterized SQL construction for more robust and consistent behavior. * **Bug Fixes** * Improved trigger update flow so event values are normalized before saving, reducing errors when modifying webhooks/triggers. --- .../Database/Hooks/EditHookPanel.tsx | 7 +- ...ase-trigger-update-transaction-mutation.ts | 5 +- .../src/sql/studio/advisor/index-advisor.ts | 26 +-- .../src/sql/studio/auth/get-index-statuses.ts | 9 +- .../studio/auth/get-index-worker-status.ts | 8 +- .../pg-meta/src/sql/studio/auth/get-user.ts | 11 +- .../check-tables-anon-authenticated-access.ts | 14 +- .../database/check-tables-without-rls.ts | 12 +- .../src/sql/studio/database/columns.ts | 27 +-- .../src/sql/studio/database/cron-jobs.ts | 80 ++++----- .../src/sql/studio/database/extensions.ts | 18 +- .../sql/studio/database/get-count-estimate.ts | 6 +- .../src/sql/studio/database/indexes.ts | 12 +- .../pg-meta/src/sql/studio/database/misc.ts | 33 ++-- .../src/sql/studio/database/triggers.ts | 37 +++-- .../pg-meta/src/sql/studio/database/vault.ts | 39 ++--- .../pg-meta/src/sql/studio/database/views.ts | 20 +-- .../studio/integrations/enable-webhooks.ts | 14 +- .../src/sql/studio/sql-editor/abort-query.ts | 6 +- .../src/sql/studio/sql-editor/keywords.ts | 8 +- .../sql/studio/sql-editor/ongoing-queries.ts | 8 +- .../studio/storage/delete-bucket-prefix.ts | 9 +- .../storage/get-largest-size-limit-buckets.ts | 8 +- .../sql/studio/table-editor/constraints.ts | 21 ++- .../src/sql/studio/table-editor/entities.ts | 34 ++-- .../sql/studio/table-editor/foreign-keys.ts | 157 +++++++++--------- .../src/sql/studio/table-editor/identity.ts | 10 +- .../sql/studio/table-editor/primary-keys.ts | 10 +- .../src/sql/studio/table-editor/rls.ts | 12 +- 29 files changed, 346 insertions(+), 315 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index 11646d724e7..672447796fc 100644 --- a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -1,4 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' +import { keyword } from '@supabase/pg-meta' import { PGTriggerCreate } from '@supabase/pg-meta/src/pg-meta-triggers' import type { PostgresTrigger } from '@supabase/postgres-meta' import { useQueryClient } from '@tanstack/react-query' @@ -275,7 +276,11 @@ export const EditHookPanel = () => { projectRef: project?.ref, connectionString: project?.connectionString, originalTrigger: selectedHook, - updatedTrigger: { ...payload, enabled_mode: 'ORIGIN' }, + updatedTrigger: { + ...payload, + enabled_mode: 'ORIGIN', + events: payload.events.map(keyword), + }, }) } } catch (error) { 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 64897d5ac32..738fe4a3061 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,4 +1,4 @@ -import { getDatabaseTriggerUpdateSQL } from '@supabase/pg-meta' +import { getDatabaseTriggerUpdateSQL, type SafeSqlFragment } 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' @@ -16,7 +16,8 @@ export type DatabaseTriggerUpdateVariables = { projectRef: string connectionString?: string | null originalTrigger: PostgresTrigger - updatedTrigger: PGTriggerCreate & Pick + updatedTrigger: Omit & + Pick & { events: Array } } export async function updateDatabaseTrigger({ diff --git a/packages/pg-meta/src/sql/studio/advisor/index-advisor.ts b/packages/pg-meta/src/sql/studio/advisor/index-advisor.ts index 3fe41522920..8708b11e1d8 100644 --- a/packages/pg-meta/src/sql/studio/advisor/index-advisor.ts +++ b/packages/pg-meta/src/sql/studio/advisor/index-advisor.ts @@ -1,15 +1,18 @@ +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + /** * 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, "''") +export function getTableIndexAdvisorSql(schema: string, table: string): SafeSqlFragment { + // Escape regex metacharacters so schema/table names are matched literally in PostgreSQL regex + const regexSchema = schema.toLowerCase().replace(/[.+*?^${}()|[\]\\]/g, '\\$&') + const regexTable = table.toLowerCase().replace(/[.+*?^${}()|[\]\\]/g, '\\$&') - // Escape regex metacharacters so schema/table names are matched literally - const regexSchema = escapedSchema.toLowerCase().replace(/[.+*?^${}()|[\]\\]/g, '\\$&') - const regexTable = escapedTable.toLowerCase().replace(/[.+*?^${}()|[\]\\]/g, '\\$&') + const tablePattern = literal(`(^|[^a-z0-9_$])${regexSchema}[.]${regexTable}($|[^a-z0-9_$])`) + const fromPattern = literal(`(^|[^a-z0-9_$])from[[:space:]]+${regexTable}($|[^a-z0-9_$])`) + const joinPattern = literal(`(^|[^a-z0-9_$])join[[:space:]]+${regexTable}($|[^a-z0-9_$])`) - return /* SQL */ ` + return safeSql` -- Get top 5 SELECT queries involving this table and run through index_advisor set search_path to public, extensions; @@ -27,9 +30,9 @@ with top_queries as ( -- 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_$])' + lower(statements.query) ~ ${tablePattern} + or lower(statements.query) ~ ${fromPattern} + or lower(statements.query) ~ ${joinPattern} ) -- Exclude system queries and statements.query not like '%pg_catalog%' @@ -50,6 +53,5 @@ select from top_queries tq left join lateral ( select * from index_advisor(tq.query) -) ia on true; -`.trim() +) ia on true;` } diff --git a/packages/pg-meta/src/sql/studio/auth/get-index-statuses.ts b/packages/pg-meta/src/sql/studio/auth/get-index-statuses.ts index 685c1c809fb..97ad7e83afa 100644 --- a/packages/pg-meta/src/sql/studio/auth/get-index-statuses.ts +++ b/packages/pg-meta/src/sql/studio/auth/get-index-statuses.ts @@ -1,4 +1,4 @@ -import { literal } from '../../../pg-format' +import { joinSqlFragments, literal, safeSql, type SafeSqlFragment } from '../../../pg-format' export const USER_SEARCH_INDEXES = [ 'idx_users_email', @@ -10,11 +10,12 @@ export const USER_SEARCH_INDEXES = [ 'users_phone_key', ] -export const getIndexStatusesSQL = () => { - return `SELECT c.relname as index_name, i.indisvalid as is_valid, i.indisready as is_ready +export const getIndexStatusesSQL = (): SafeSqlFragment => { + const indexNames = joinSqlFragments(USER_SEARCH_INDEXES.map(literal), ', ') + return safeSql`SELECT c.relname as index_name, i.indisvalid as is_valid, i.indisready as is_ready FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'auth' - AND c.relname IN (${USER_SEARCH_INDEXES.map(literal).join(', ')});` + AND c.relname IN (${indexNames});` } diff --git a/packages/pg-meta/src/sql/studio/auth/get-index-worker-status.ts b/packages/pg-meta/src/sql/studio/auth/get-index-worker-status.ts index 8917fa5f849..43244c9127a 100644 --- a/packages/pg-meta/src/sql/studio/auth/get-index-worker-status.ts +++ b/packages/pg-meta/src/sql/studio/auth/get-index-worker-status.ts @@ -1,11 +1,13 @@ +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + // Checks pg_locks to determine if the index worker advisory lock is currently held const INDEX_WORKER_ADVISORY_LOCK_KEY = 'auth_index_worker' -export const getIndexWorkerStatusSQL = () => { - return `SELECT EXISTS ( +export const getIndexWorkerStatusSQL = (): SafeSqlFragment => { + return safeSql`SELECT EXISTS ( SELECT 1 FROM pg_locks WHERE locktype = 'advisory' - AND (classid::bigint << 32 | objid::bigint) = hashtext('${INDEX_WORKER_ADVISORY_LOCK_KEY}')::bigint + AND (classid::bigint << 32 | objid::bigint) = hashtext(${literal(INDEX_WORKER_ADVISORY_LOCK_KEY)})::bigint ) as is_in_progress;` } diff --git a/packages/pg-meta/src/sql/studio/auth/get-user.ts b/packages/pg-meta/src/sql/studio/auth/get-user.ts index cbb03efd0bb..9527cce33d5 100644 --- a/packages/pg-meta/src/sql/studio/auth/get-user.ts +++ b/packages/pg-meta/src/sql/studio/auth/get-user.ts @@ -1,5 +1,7 @@ -export const getUserSQL = (userId: string) => { - const sql = /* SQL */ ` +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + +export const getUserSQL = (userId: string): SafeSqlFragment => { + return safeSql` select auth.users.id, auth.users.email, @@ -28,8 +30,5 @@ select ) as providers from auth.users -where id = '${userId}'; -`.trim() - - return sql +where id = ${literal(userId)};` } diff --git a/packages/pg-meta/src/sql/studio/database/check-tables-anon-authenticated-access.ts b/packages/pg-meta/src/sql/studio/database/check-tables-anon-authenticated-access.ts index 67b80967b09..23f4fc10364 100644 --- a/packages/pg-meta/src/sql/studio/database/check-tables-anon-authenticated-access.ts +++ b/packages/pg-meta/src/sql/studio/database/check-tables-anon-authenticated-access.ts @@ -1,13 +1,19 @@ +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + /** * Given a schema name, list all the tables in that schema which have access * granted to either the "anon" or "authenticated" role */ -export const getTablesWithAnonAuthenticatedAccessSQL = ({ schema }: { schema: string }) => - /* SQL */ ` +export const getTablesWithAnonAuthenticatedAccessSQL = ({ + schema, +}: { + schema: string +}): SafeSqlFragment => + safeSql` SELECT c.relname AS table_name FROM pg_catalog.pg_class AS c JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace -WHERE n.nspname = '${schema}' +WHERE n.nspname = ${literal(schema)} AND c.relkind IN ('r','p') -- table, partitioned table AND EXISTS ( SELECT 1 @@ -16,4 +22,4 @@ WHERE n.nspname = '${schema}' WHERE r.rolname IN ('anon','authenticated') ) ; -`.trim() +` diff --git a/packages/pg-meta/src/sql/studio/database/check-tables-without-rls.ts b/packages/pg-meta/src/sql/studio/database/check-tables-without-rls.ts index 3c0aba825db..0269a72e082 100644 --- a/packages/pg-meta/src/sql/studio/database/check-tables-without-rls.ts +++ b/packages/pg-meta/src/sql/studio/database/check-tables-without-rls.ts @@ -1,4 +1,4 @@ -import { literal } from '../../../pg-format' +import { joinSqlFragments, literal, safeSql, type SafeSqlFragment } from '../../../pg-format' /** * Builds a SQL query that returns entities exposed through the Data API that @@ -12,10 +12,14 @@ import { literal } from '../../../pg-format' * Checks against the _target_ schemas rather than the currently active * PostgREST config, so it works correctly when enabling the Data API. */ -export const getUnsafeEntitiesInApiSql = ({ schemas }: { schemas: Array }) => { - const schemaList = schemas.map(literal).join(', ') +export const getUnsafeEntitiesInApiSql = ({ + schemas, +}: { + schemas: Array +}): SafeSqlFragment => { + const schemaList = joinSqlFragments(schemas.map(literal), ', ') - return /* SQL */ ` + return safeSql` select n.nspname as schema, c.relname as name, diff --git a/packages/pg-meta/src/sql/studio/database/columns.ts b/packages/pg-meta/src/sql/studio/database/columns.ts index 170071cf455..24bbd9be21e 100644 --- a/packages/pg-meta/src/sql/studio/database/columns.ts +++ b/packages/pg-meta/src/sql/studio/database/columns.ts @@ -1,16 +1,25 @@ -export const getTableColumnsSql = ({ table, schema }: { table?: string; schema?: string }) => { - const conditions = [] +import { joinSqlFragments, literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + +export const getTableColumnsSql = ({ + table, + schema, +}: { + table?: string + schema?: string +}): SafeSqlFragment => { + const conditions: Array = [] if (table) { - conditions.push(`tablename = '${table}'`) + conditions.push(safeSql`tablename = ${literal(table)}`) } if (schema) { - conditions.push(`schemaname = '${schema}'`) + conditions.push(safeSql`schemaname = ${literal(schema)}`) } - const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '' + const whereClause = + conditions.length > 0 ? safeSql`WHERE ${joinSqlFragments(conditions, ' AND ')}` : safeSql`` + + return safeSql` - const sql = /* SQL */ ` - SELECT tbl.schemaname, tbl.tablename, @@ -68,7 +77,5 @@ export const getTableColumnsSql = ({ table, schema }: { table?: string; schema?: ) ${whereClause} GROUP BY schemaname, tablename, quoted_name, is_table; -`.trim() - - return sql +` } diff --git a/packages/pg-meta/src/sql/studio/database/cron-jobs.ts b/packages/pg-meta/src/sql/studio/database/cron-jobs.ts index 43847f6df2d..cd8bb41094a 100644 --- a/packages/pg-meta/src/sql/studio/database/cron-jobs.ts +++ b/packages/pg-meta/src/sql/studio/database/cron-jobs.ts @@ -1,4 +1,4 @@ -import { literal } from '../../../pg-format' +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' export const getCronJobsMinimalSql = ({ searchTerm, @@ -8,21 +8,24 @@ export const getCronJobsMinimalSql = ({ searchTerm?: string page: number limit: number -}) => - ` -SELECT +}): SafeSqlFragment => { + const searchClause = searchTerm + ? safeSql`WHERE job.jobname ILIKE ${literal(`%${searchTerm}%`)}` + : safeSql`` + return safeSql` +SELECT job.jobid, job.jobname, job.schedule, job.command, job.active -FROM +FROM cron.job job -${!!searchTerm ? `WHERE job.jobname ILIKE ${literal(`%${searchTerm}%`)}` : ''} +${searchClause} ORDER BY job.jobid -LIMIT ${limit} -OFFSET ${page * limit}; -`.trim() +LIMIT ${literal(limit)} +OFFSET ${literal(page * limit)};` +} export const getCronJobsSql = ({ searchTerm, @@ -32,28 +35,31 @@ export const getCronJobsSql = ({ searchTerm?: string page: number limit: number -}) => - ` +}): SafeSqlFragment => { + const searchClause = searchTerm + ? safeSql`WHERE job.jobname ILIKE ${literal(`%${searchTerm}%`)}` + : safeSql`` + return safeSql` WITH latest_runs AS ( - SELECT + SELECT jobid, status, MAX(start_time) AS latest_run FROM cron.job_run_details GROUP BY jobid, status ), most_recent_runs AS ( - SELECT - jobid, - status, + SELECT + jobid, + status, latest_run FROM latest_runs lr1 WHERE latest_run = ( - SELECT MAX(latest_run) - FROM latest_runs lr2 + SELECT MAX(latest_run) + FROM latest_runs lr2 WHERE lr2.jobid = lr1.jobid ) ) -SELECT +SELECT job.jobid, job.jobname, job.schedule, @@ -61,14 +67,14 @@ SELECT job.active, mr.latest_run, mr.status -FROM +FROM cron.job job LEFT JOIN most_recent_runs mr ON job.jobid = mr.jobid -${!!searchTerm ? `WHERE job.jobname ILIKE ${literal(`%${searchTerm}%`)}` : ''} +${searchClause} ORDER BY job.jobid -LIMIT ${limit} -OFFSET ${page * limit}; -`.trim() +LIMIT ${literal(limit)} +OFFSET ${literal(page * limit)};` +} /** * Delete old cron job run details using ctid range filtering. @@ -86,13 +92,11 @@ export const getDeleteOldCronJobRunDetailsByCtidSql = ( interval: string, startPage: number, endPage: number -) => { - // After validation, these are guaranteed to be safe integers - // Using literal() on the string representation ensures proper escaping +): SafeSqlFragment => { const safeCtidStart = literal(`(${startPage},0)`) const safeCtidEnd = literal(`(${endPage},0)`) - return ` + return safeSql` WITH deleted AS ( DELETE FROM cron.job_run_details WHERE ctid >= ${safeCtidStart}::tid @@ -100,30 +104,30 @@ WITH deleted AS ( AND end_time < now() - interval ${literal(interval)} RETURNING 1 ) -SELECT count(*) as deleted_count FROM deleted; -`.trim() +SELECT count(*) as deleted_count FROM deleted;` } const CRON_CLEANUP_SCHEDULE_NAME = 'delete-job-run-details' const CRON_CLEANUP_SCHEDULE_EXPRESSION = '0 12 * * *' -export const getScheduleDeleteCronJobRunDetailsSql = (interval: string) => - ` +export const getScheduleDeleteCronJobRunDetailsSql = (interval: string): SafeSqlFragment => { + const command = safeSql`DELETE FROM cron.job_run_details WHERE end_time < now() - interval ${literal(interval)};` + + return safeSql` SELECT cron.schedule( ${literal(CRON_CLEANUP_SCHEDULE_NAME)}, ${literal(CRON_CLEANUP_SCHEDULE_EXPRESSION)}, - $$DELETE FROM cron.job_run_details WHERE end_time < now() - interval ${literal(interval)}$$ -); -`.trim() + ${literal(command)} +);` +} /** * Get the total number of pages in the job_run_details table. * This is used to iterate through the table in batches using ctid ranges. */ -export const getJobRunDetailsPageCountSql = () => - ` +export const getJobRunDetailsPageCountSql = (): SafeSqlFragment => + safeSql` SELECT pg_relation_size(oid) / current_setting('block_size')::int8 AS num_pages FROM pg_class WHERE relname = 'job_run_details' - AND relnamespace = 'cron'::regnamespace; -`.trim() + AND relnamespace = 'cron'::regnamespace;` diff --git a/packages/pg-meta/src/sql/studio/database/extensions.ts b/packages/pg-meta/src/sql/studio/database/extensions.ts index 40d8ced591d..1ec94a515ce 100644 --- a/packages/pg-meta/src/sql/studio/database/extensions.ts +++ b/packages/pg-meta/src/sql/studio/database/extensions.ts @@ -1,8 +1,8 @@ -import { ident } from '../../../pg-format' +import { ident, safeSql, type SafeSqlFragment } from '../../../pg-format' import pgMetaExtensions from '../../../pg-meta-extensions' -export const getDatabaseExtensionsSQL = () => - ` +export const getDatabaseExtensionsSQL = (): SafeSqlFragment => + safeSql` SELECT e.name, n.nspname AS schema, @@ -16,7 +16,7 @@ FROM LEFT JOIN pg_namespace n ON x.extnamespace = n.oid LEFT JOIN pg_available_extension_versions ev ON ev.name = e.name AND ev.version = e.default_version; -`.trim() +` export const getEnableDatabaseExtensionSQL = ({ schema, @@ -30,12 +30,10 @@ export const getEnableDatabaseExtensionSQL = ({ version: string cascade?: boolean createSchema?: boolean -}) => { +}): SafeSqlFragment => { const { sql } = pgMetaExtensions.create({ schema, name, version, cascade }) return createSchema - ? ` -CREATE SCHEMA IF NOT EXISTS ${ident(schema)}; -${sql} -`.trim() - : sql.trim() + ? safeSql`CREATE SCHEMA IF NOT EXISTS ${ident(schema)}; +${sql}` + : sql } diff --git a/packages/pg-meta/src/sql/studio/database/get-count-estimate.ts b/packages/pg-meta/src/sql/studio/database/get-count-estimate.ts index 52c01e36d61..6ee03d3eb81 100644 --- a/packages/pg-meta/src/sql/studio/database/get-count-estimate.ts +++ b/packages/pg-meta/src/sql/studio/database/get-count-estimate.ts @@ -1,6 +1,8 @@ +import { safeSql, type SafeSqlFragment } from '../../../pg-format' + export const THRESHOLD_COUNT = 50000 -export const COUNT_ESTIMATE_SQL = /* SQL */ ` +export const COUNT_ESTIMATE_SQL: SafeSqlFragment = safeSql` CREATE OR REPLACE FUNCTION pg_temp.count_estimate( query text ) RETURNS integer LANGUAGE plpgsql AS $$ @@ -11,4 +13,4 @@ BEGIN RETURN plan->0->'Plan'->'Plan Rows'; END; $$; -`.trim() +` diff --git a/packages/pg-meta/src/sql/studio/database/indexes.ts b/packages/pg-meta/src/sql/studio/database/indexes.ts index 69059c691c0..eb818cb8448 100644 --- a/packages/pg-meta/src/sql/studio/database/indexes.ts +++ b/packages/pg-meta/src/sql/studio/database/indexes.ts @@ -1,5 +1,7 @@ -export const getIndexesSQL = ({ schema }: { schema: string }) => { - const sql = /* SQL */ ` +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + +export const getIndexesSQL = ({ schema }: { schema: string }): SafeSqlFragment => { + return safeSql` SELECT n.nspname AS schema, t.relname AS "table", @@ -17,7 +19,7 @@ JOIN LATERAL unnest(idx.indkey) WITH ORDINALITY AS k(attnum, ord) ON TRUE LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum -WHERE n.nspname = '${schema}' +WHERE n.nspname = ${literal(schema)} GROUP BY n.nspname, t.relname, @@ -25,7 +27,5 @@ GROUP BY idx.indexrelid ORDER BY schema, "table", name; -`.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 index e98e40e734f..76e1e251a24 100644 --- a/packages/pg-meta/src/sql/studio/database/misc.ts +++ b/packages/pg-meta/src/sql/studio/database/misc.ts @@ -1,39 +1,28 @@ -import { literal } from '../../../pg-format' +import { literal, safeSql, type SafeSqlFragment } 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 getDatabaseSizeSql = (): SafeSqlFragment => { + return safeSql`select sum(pg_database_size(pg_database.datname))::bigint as db_size from pg_database;` } -export const getLiveTupleEstimate = (table: string, schema: string = 'public') => { - const sql = /* SQL */ ` +export const getLiveTupleEstimate = (table: string, schema: string = 'public'): SafeSqlFragment => { + return safeSql` SELECT n_live_tup AS live_tuple_estimate FROM pg_stat_user_tables WHERE schemaname = ${literal(schema)} -AND relname = ${literal(table)}; -`.trim() - - return sql +AND relname = ${literal(table)};` } -export const getMaxConnectionsSql = () => { - const sql = /* SQL */ `show max_connections` - - return sql +export const getMaxConnectionsSql = (): SafeSqlFragment => { + return safeSql`show max_connections` } -export const replicationLagSql = () => { - const sql = /* SQL */ ` -select +export const replicationLagSql = (): SafeSqlFragment => { + return safeSql` +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/database/triggers.ts b/packages/pg-meta/src/sql/studio/database/triggers.ts index c3c191137a3..e91bc869ce0 100644 --- a/packages/pg-meta/src/sql/studio/database/triggers.ts +++ b/packages/pg-meta/src/sql/studio/database/triggers.ts @@ -1,4 +1,11 @@ -import { literal } from '../../../pg-format' +import { + ident, + joinSqlFragments, + keyword, + literal, + safeSql, + type SafeSqlFragment, +} 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 @@ -18,21 +25,25 @@ export function getDatabaseTriggerUpdateSQL({ condition: string | null orientation: 'ROW' | 'STATEMENT' activation: 'BEFORE' | 'AFTER' | 'INSTEAD OF' - events: string[] + events: Array function_schema: string function_name: string - function_args: string[] + function_args: Array } - updatedTrigger: PGTriggerCreate & Pick -}) { + updatedTrigger: Omit & + Pick & { events: Array } +}): SafeSqlFragment { 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(literal).join(',') ?? ''}); -COMMIT; -`.trim() + const eventsList = joinSqlFragments(events, ' OR ') + const argsList = + function_args && function_args.length > 0 + ? joinSqlFragments(function_args.map(literal), ',') + : safeSql`` + return safeSql`BEGIN; +DROP TRIGGER ${ident(originalTrigger.name)} ON ${ident(originalTrigger.schema)}.${ident(originalTrigger.table)}; +CREATE TRIGGER ${ident(name)} ${keyword(activation)} ${eventsList} ON ${ident(schema)}.${ident(table)} + FOR EACH ROW EXECUTE FUNCTION + ${ident(function_schema)}.${ident(function_name)}(${argsList}); +COMMIT;` } diff --git a/packages/pg-meta/src/sql/studio/database/vault.ts b/packages/pg-meta/src/sql/studio/database/vault.ts index 9d1bb0aae0a..38a90421ed0 100644 --- a/packages/pg-meta/src/sql/studio/database/vault.ts +++ b/packages/pg-meta/src/sql/studio/database/vault.ts @@ -1,4 +1,4 @@ -import { literal } from '../../../pg-format' +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' export const getCreateVaultSecretSQL = ({ secret, @@ -8,16 +8,14 @@ export const getCreateVaultSecretSQL = ({ secret: string name?: string description?: string -}) => { - const sql = /* SQL */ ` - select vault.create_secret( - new_secret := ${literal(secret)} - ${name ? `, new_name := ${literal(name)}` : ''} - ${description ? `, new_description := ${literal(description)}` : ''} - ) - `.trim() - - return sql +}): SafeSqlFragment => { + const namePart = name ? safeSql`, new_name := ${literal(name)}` : safeSql`` + const descriptionPart = description + ? safeSql`, new_description := ${literal(description)}` + : safeSql`` + return safeSql`select vault.create_secret( + new_secret := ${literal(secret)}${namePart}${descriptionPart} + )` } export const getUpdateVaultSecretSQL = ({ @@ -30,14 +28,13 @@ export const getUpdateVaultSecretSQL = ({ secret?: string name?: string description?: string -}) => { - const sql = /* SQL */ ` -select vault.update_secret( - secret_id := ${literal(id)} - ${secret ? `, new_secret := ${literal(secret)}` : ''} - ${name ? `, new_name := ${literal(name)}` : ''} - ${description ? `, new_description := ${literal(description)}` : ''} -) -`.trim() - return sql +}): SafeSqlFragment => { + const secretPart = secret ? safeSql`, new_secret := ${literal(secret)}` : safeSql`` + const namePart = name ? safeSql`, new_name := ${literal(name)}` : safeSql`` + const descriptionPart = description + ? safeSql`, new_description := ${literal(description)}` + : safeSql`` + return safeSql`select vault.update_secret( + secret_id := ${literal(id)}${secretPart}${namePart}${descriptionPart} + )` } diff --git a/packages/pg-meta/src/sql/studio/database/views.ts b/packages/pg-meta/src/sql/studio/database/views.ts index 9a6d5ec267d..15c8a279831 100644 --- a/packages/pg-meta/src/sql/studio/database/views.ts +++ b/packages/pg-meta/src/sql/studio/database/views.ts @@ -1,16 +1,18 @@ +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + export const getViewDefinitionSql = ({ id, includeCreateStatement = false, }: { id: number includeCreateStatement?: boolean -}) => { +}): SafeSqlFragment => { if (!id) { throw new Error('id is required') } const definitionSql = includeCreateStatement - ? /* SQL */ ` + ? safeSql` concat( case t.relkind when 'm' then 'create materialized view ' @@ -27,12 +29,12 @@ export const getViewDefinitionSql = ({ E' as\n', pg_get_viewdef(t.regclass, true) ) - `.trim() - : 'pg_get_viewdef(t.regclass, true)' + ` + : safeSql`pg_get_viewdef(t.regclass, true)` - const sql = /* SQL */ ` + return safeSql` with table_info as ( - select + select c.relkind, n.nspname::text as schema, c.relname::text as name, @@ -40,11 +42,9 @@ export const getViewDefinitionSql = ({ 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} + where c.oid = ${literal(id)} ) select ${definitionSql} as definition from table_info t - `.trim() - - return sql + ` } diff --git a/packages/pg-meta/src/sql/studio/integrations/enable-webhooks.ts b/packages/pg-meta/src/sql/studio/integrations/enable-webhooks.ts index 8197ea36ce5..c31d7d759c6 100644 --- a/packages/pg-meta/src/sql/studio/integrations/enable-webhooks.ts +++ b/packages/pg-meta/src/sql/studio/integrations/enable-webhooks.ts @@ -1,17 +1,19 @@ // Copied over from Management API here: // https://github.com/supabase/infrastructure/blob/develop/api/apps/mgmt-api/src/common/projects/project-database-webhooks.service.ts#L58 -export const getCheckWebhooksEnabledSQL = () => - ` +import { safeSql, type SafeSqlFragment } from '../../../pg-format' + +export const getCheckWebhooksEnabledSQL = (): SafeSqlFragment => + safeSql` SELECT EXISTS ( SELECT 1 FROM information_schema.schemata WHERE schema_name = 'supabase_functions' ) AS schema_exists; -`.trim() +` -export const getEnableWebhooksSQL = () => - ` +export const getEnableWebhooksSQL = (): SafeSqlFragment => + safeSql` BEGIN; DO $$ @@ -258,4 +260,4 @@ BEGIN; GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role; COMMIT; -`.trim() +` 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 index 9909e25092d..483a74bc73a 100644 --- a/packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts +++ b/packages/pg-meta/src/sql/studio/sql-editor/abort-query.ts @@ -1,3 +1,5 @@ -export const getAbortQuerySQL = ({ pid }: { pid: number }) => { - return `select pg_terminate_backend(${pid})`.trim() +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + +export const getAbortQuerySQL = ({ pid }: { pid: number }): SafeSqlFragment => { + return safeSql`select pg_terminate_backend(${literal(pid)})` } diff --git a/packages/pg-meta/src/sql/studio/sql-editor/keywords.ts b/packages/pg-meta/src/sql/studio/sql-editor/keywords.ts index 56297ea1a0e..80fe1707eb1 100644 --- a/packages/pg-meta/src/sql/studio/sql-editor/keywords.ts +++ b/packages/pg-meta/src/sql/studio/sql-editor/keywords.ts @@ -1,7 +1,5 @@ -export const getKeywordsSql = () => { - const sql = /* SQL */ ` -SELECT word FROM pg_get_keywords(); -`.trim() +import { safeSql, type SafeSqlFragment } from '../../../pg-format' - return sql +export const getKeywordsSql = (): SafeSqlFragment => { + return safeSql`SELECT word FROM pg_get_keywords();` } 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 index 7fc0e22fc92..38f047caf8b 100644 --- a/packages/pg-meta/src/sql/studio/sql-editor/ongoing-queries.ts +++ b/packages/pg-meta/src/sql/studio/sql-editor/ongoing-queries.ts @@ -1,7 +1,5 @@ -export const getOngoingQueriesSql = () => { - const sql = /* SQL */ ` -select pid, query, query_start from pg_stat_activity where state = 'active' and datname = 'postgres'; -`.trim() +import { safeSql, type SafeSqlFragment } from '../../../pg-format' - return sql +export const getOngoingQueriesSql = (): SafeSqlFragment => { + return safeSql`select pid, query, query_start from pg_stat_activity where state = 'active' and datname = 'postgres';` } diff --git a/packages/pg-meta/src/sql/studio/storage/delete-bucket-prefix.ts b/packages/pg-meta/src/sql/studio/storage/delete-bucket-prefix.ts index bdb35b1331a..8626511cac0 100644 --- a/packages/pg-meta/src/sql/studio/storage/delete-bucket-prefix.ts +++ b/packages/pg-meta/src/sql/studio/storage/delete-bucket-prefix.ts @@ -1,12 +1,11 @@ +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + export const getDeleteBucketPrefixSQL = ({ bucketId, prefix, }: { bucketId: string prefix: string -}) => { - const sql = /* SQL */ ` -select storage.delete_prefix('${bucketId}', '${prefix}'); -`.trim() - return sql +}): SafeSqlFragment => { + return safeSql`select storage.delete_prefix(${literal(bucketId)}, ${literal(prefix)});` } diff --git a/packages/pg-meta/src/sql/studio/storage/get-largest-size-limit-buckets.ts b/packages/pg-meta/src/sql/studio/storage/get-largest-size-limit-buckets.ts index f3b234ad691..387c6465ca1 100644 --- a/packages/pg-meta/src/sql/studio/storage/get-largest-size-limit-buckets.ts +++ b/packages/pg-meta/src/sql/studio/storage/get-largest-size-limit-buckets.ts @@ -1,3 +1,5 @@ +import { literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + export const LARGEST_SIZE_LIMIT_BUCKETS_COUNT = 50 /** @@ -6,10 +8,10 @@ export const LARGEST_SIZE_LIMIT_BUCKETS_COUNT = 50 * This query is unoptimized and should not be automatically called because * there is no index on `file_size_limit` in the `storage.buckets` table. */ -export const getLargestSizeLimitBucketsSqlUnoptimized = /* SQL */ ` +export const getLargestSizeLimitBucketsSqlUnoptimized: SafeSqlFragment = safeSql` SELECT id, name, file_size_limit FROM storage.buckets WHERE file_size_limit IS NOT NULL ORDER BY file_size_limit DESC -LIMIT ${LARGEST_SIZE_LIMIT_BUCKETS_COUNT + 1}; -`.trim() +LIMIT ${literal(LARGEST_SIZE_LIMIT_BUCKETS_COUNT + 1)}; +` 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 98d2c8f6ef9..be1052d46d3 100644 --- a/packages/pg-meta/src/sql/studio/table-editor/constraints.ts +++ b/packages/pg-meta/src/sql/studio/table-editor/constraints.ts @@ -1,17 +1,17 @@ -import { ident } from '../../../pg-format' +import { ident, literal, safeSql, type SafeSqlFragment } from '../../../pg-format' -export const getTableConstraintsSql = ({ id }: { id: number }) => { - const sql = /* SQL */ ` +export const getTableConstraintsSql = ({ id }: { id: number }): SafeSqlFragment => { + return safeSql` with table_info as ( - select + 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} + where c.oid = ${literal(id)} ) -select +select con.oid as id, con.conname as name, con.contype as type @@ -21,11 +21,9 @@ inner join pg_catalog.pg_class rel inner join pg_catalog.pg_namespace nsp on nsp.oid = connamespace inner join table_info ti - on ti.schema = nsp.nspname + on ti.schema = nsp.nspname and ti.name = rel.relname; -`.trim() - - return sql +` } export const getDropConstraintSQL = ({ @@ -36,4 +34,5 @@ export const getDropConstraintSQL = ({ schema: string table: string name: string -}) => `ALTER TABLE ${ident(schema)}.${ident(table)} DROP CONSTRAINT ${ident(name)}` +}): SafeSqlFragment => + safeSql`ALTER TABLE ${ident(schema)}.${ident(table)} DROP CONSTRAINT ${ident(name)}` diff --git a/packages/pg-meta/src/sql/studio/table-editor/entities.ts b/packages/pg-meta/src/sql/studio/table-editor/entities.ts index 7b3dd619fd4..84cf3012a0a 100644 --- a/packages/pg-meta/src/sql/studio/table-editor/entities.ts +++ b/packages/pg-meta/src/sql/studio/table-editor/entities.ts @@ -1,3 +1,5 @@ +import { joinSqlFragments, literal, safeSql, type SafeSqlFragment } from '../../../pg-format' + export const getEntityTypesSQL = ({ schemas, search, @@ -6,17 +8,23 @@ export const getEntityTypesSQL = ({ limit, page, }: { - schemas: string[] + schemas: Array search?: string sort: 'alphabetical' | 'grouped-alphabetical' - filterTypes: string[] + filterTypes: Array 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` +}): SafeSqlFragment => { + const innerOrderBy = + sort === 'alphabetical' ? safeSql`c.relname asc` : safeSql`"type_sort" asc, c.relname asc` + const outerOrderBy = + sort === 'alphabetical' ? safeSql`r.name asc` : safeSql`r.type_sort asc, r.name asc` - const sql = /* SQL */ ` + const typeList = joinSqlFragments(filterTypes.map(literal), ', ') + const schemaList = joinSqlFragments(schemas.map(literal), ', ') + const searchClause = search ? safeSql`and c.relname ilike ${literal(`%${search}%`)}` : safeSql`` + + return safeSql` with records as ( select c.oid::int8 as "id", @@ -37,7 +45,7 @@ export const getEntityTypesSQL = ({ pg_namespace nc join pg_class c on nc.oid = c.relnamespace where - c.relkind in (${filterTypes.map((x) => `'${x}'`).join(', ')}) + c.relkind in (${typeList}) and not pg_is_other_temp_schema(nc.oid) and ( pg_has_role(c.relowner, 'USAGE') @@ -47,11 +55,11 @@ export const getEntityTypesSQL = ({ ) 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}%'` : ''} + and nc.nspname in (${schemaList}) + ${searchClause} order by ${innerOrderBy} - limit ${limit} - offset ${page * limit} + limit ${literal(limit)} + offset ${literal(page * limit)} ) select jsonb_build_object( @@ -69,7 +77,5 @@ export const getEntityTypesSQL = ({ '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 89162d11bf0..14fdf1bb3be 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 @@ -1,46 +1,46 @@ -import { ident } from '../../../pg-format' +import { ident, joinSqlFragments, literal, safeSql, type SafeSqlFragment } from '../../../pg-format' export const getAddForeignKeySQL = ({ table, foreignKeys, }: { table: { schema: string; name: string } - foreignKeys: ForeignKey[] -}) => { - const getOnDeleteSql = (action: string) => + foreignKeys: Array +}): SafeSqlFragment => { + const getOnDeleteSql = (action: string): SafeSqlFragment => action === FOREIGN_KEY_CASCADE_ACTION.CASCADE - ? 'ON DELETE CASCADE' + ? safeSql`ON DELETE CASCADE` : action === FOREIGN_KEY_CASCADE_ACTION.RESTRICT - ? 'ON DELETE RESTRICT' + ? safeSql`ON DELETE RESTRICT` : action === FOREIGN_KEY_CASCADE_ACTION.SET_DEFAULT - ? 'ON DELETE SET DEFAULT' + ? safeSql`ON DELETE SET DEFAULT` : action === FOREIGN_KEY_CASCADE_ACTION.SET_NULL - ? 'ON DELETE SET NULL' - : '' - const getOnUpdateSql = (action: string) => + ? safeSql`ON DELETE SET NULL` + : safeSql`` + + const getOnUpdateSql = (action: string): SafeSqlFragment => action === FOREIGN_KEY_CASCADE_ACTION.CASCADE - ? 'ON UPDATE CASCADE' + ? safeSql`ON UPDATE CASCADE` : action === FOREIGN_KEY_CASCADE_ACTION.RESTRICT - ? 'ON UPDATE RESTRICT' - : '' - return ( - foreignKeys - .map((relation) => { - const { deletionAction, updateAction } = relation - const onDeleteSql = getOnDeleteSql(deletionAction) - const onUpdateSql = getOnUpdateSql(updateAction) - return ` - ALTER TABLE ${ident(table.schema)}.${ident(table.name)} - ADD FOREIGN KEY (${relation.columns.map((column) => ident(column.source)).join(', ')}) - REFERENCES ${ident(relation.schema)}.${ident(relation.table)} (${relation.columns.map((column) => ident(column.target)).join(', ')}) - ${onUpdateSql} - ${onDeleteSql} - ` - .replace(/\s+/g, ' ') - .trim() - }) - .join(';') + ';' - ) + ? safeSql`ON UPDATE RESTRICT` + : safeSql`` + + const stmts = foreignKeys.map((relation) => { + const { deletionAction, updateAction } = relation + const onDeleteSql = getOnDeleteSql(deletionAction) + const onUpdateSql = getOnUpdateSql(updateAction) + const sourceCols = joinSqlFragments( + relation.columns.map((column) => ident(column.source)), + ', ' + ) + const targetCols = joinSqlFragments( + relation.columns.map((column) => ident(column.target)), + ', ' + ) + return safeSql`ALTER TABLE ${ident(table.schema)}.${ident(table.name)} ADD FOREIGN KEY (${sourceCols}) REFERENCES ${ident(relation.schema)}.${ident(relation.table)} (${targetCols}) ${onUpdateSql} ${onDeleteSql}` + }) + + return safeSql`${joinSqlFragments(stmts, ';\n')};` } export const getRemoveForeignKeySQL = ({ @@ -48,75 +48,66 @@ export const getRemoveForeignKeySQL = ({ foreignKeys, }: { table: { schema: string; name: string } - foreignKeys: ForeignKey[] -}) => { - return ( - foreignKeys - .map((relation) => - ` -ALTER TABLE IF EXISTS ${ident(table.schema)}.${ident(table.name)} -DROP CONSTRAINT IF EXISTS ${ident(relation.name)} -` - .replace(/\s+/g, ' ') - .trim() - ) - .join(';') + ';' + foreignKeys: Array +}): SafeSqlFragment => { + const stmts = foreignKeys.map( + (relation) => + safeSql`ALTER TABLE IF EXISTS ${ident(table.schema)}.${ident(table.name)} DROP CONSTRAINT IF EXISTS ${ident(relation.name)}` ) + return safeSql`${joinSqlFragments(stmts, ';\n')};` } -export const getForeignKeyConstraintsSql = ({ schema }: { schema: string }) => { +export const getForeignKeyConstraintsSql = ({ schema }: { schema: string }): SafeSqlFragment => { if (!schema) throw new Error('schema is required') - const sql = /* SQL */ ` -SELECT - con.oid as id, - con.conname as constraint_name, + return safeSql` +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, + nsp.nspname as source_schema, + rel.relname as source_table, ( - SELECT + SELECT array_agg( - att.attname - ORDER BY + 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 + ) + 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, + ) source_columns, frel.oid as target_id, - fnsp.nspname as target_schema, - frel.relname as target_table, + fnsp.nspname as target_schema, + frel.relname as target_table, ( - SELECT + SELECT array_agg( - att.attname - ORDER BY + 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 + ) + 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 + ) 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 + AND nsp.nspname = ${literal(schema)} +` } export interface ForeignKey { @@ -126,7 +117,7 @@ export interface ForeignKey { schema: string table: string - columns: { source: string; sourceType?: string; target: string; targetType?: string }[] + columns: Array<{ source: string; sourceType?: string; target: string; targetType?: string }> deletionAction: string updateAction: string toRemove?: boolean diff --git a/packages/pg-meta/src/sql/studio/table-editor/identity.ts b/packages/pg-meta/src/sql/studio/table-editor/identity.ts index c25c97efe91..cf5c6601825 100644 --- a/packages/pg-meta/src/sql/studio/table-editor/identity.ts +++ b/packages/pg-meta/src/sql/studio/table-editor/identity.ts @@ -1,4 +1,4 @@ -import { ident } from '../../../pg-format' +import { ident, literal, safeSql, type SafeSqlFragment } from '../../../pg-format' export const getUpdateIdentitySequenceSQL = ({ schema, @@ -8,8 +8,8 @@ export const getUpdateIdentitySequenceSQL = ({ schema: string table: string column: string -}) => { - return `SELECT setval('${ident(schema)}.${ident(`${table}_${column}_seq`)}', (SELECT COALESCE(MAX(${ident(column)}), 1) FROM ${ident(schema)}.${ident(table)}))` +}): SafeSqlFragment => { + return safeSql`SELECT setval(${literal(`${ident(schema)}.${ident(`${table}_${column}_seq`)}`)}::regclass, (SELECT COALESCE(MAX(${ident(column)}), 1) FROM ${ident(schema)}.${ident(table)}))` } export const getDuplicateIdentitySequenceSQL = ({ @@ -22,6 +22,6 @@ export const getDuplicateIdentitySequenceSQL = ({ duplicatedTableName: string sourceTableName: string sourceTableSchema: string -}) => { - return `SELECT setval('${ident(sourceTableSchema)}.${ident(`${duplicatedTableName}_${columnName}_seq`)}', (SELECT COALESCE(MAX(${ident(columnName)}), 1) FROM ${ident(sourceTableSchema)}.${ident(sourceTableName)}));` +}): SafeSqlFragment => { + return safeSql`SELECT setval(${literal(`${ident(sourceTableSchema)}.${ident(`${duplicatedTableName}_${columnName}_seq`)}`)}::regclass, (SELECT COALESCE(MAX(${ident(columnName)}), 1) FROM ${ident(sourceTableSchema)}.${ident(sourceTableName)}));` } diff --git a/packages/pg-meta/src/sql/studio/table-editor/primary-keys.ts b/packages/pg-meta/src/sql/studio/table-editor/primary-keys.ts index 53fc0185d05..8c3e1882b75 100644 --- a/packages/pg-meta/src/sql/studio/table-editor/primary-keys.ts +++ b/packages/pg-meta/src/sql/studio/table-editor/primary-keys.ts @@ -1,4 +1,4 @@ -import { ident } from '../../../pg-format' +import { ident, joinSqlFragments, safeSql, type SafeSqlFragment } from '../../../pg-format' /** * The functions below are basically just queries but may be supported directly @@ -11,8 +11,8 @@ export const getAddPrimaryKeySQL = ({ }: { schema: string table: string - columns: string[] -}) => { - const primaryKeyColumns = columns.map((col) => ident(col)).join(', ') - return `ALTER TABLE ${ident(schema)}.${ident(table)} ADD PRIMARY KEY (${primaryKeyColumns})` + columns: Array +}): SafeSqlFragment => { + const primaryKeyColumns = joinSqlFragments(columns.map(ident), ', ') + return safeSql`ALTER TABLE ${ident(schema)}.${ident(table)} ADD PRIMARY KEY (${primaryKeyColumns})` } diff --git a/packages/pg-meta/src/sql/studio/table-editor/rls.ts b/packages/pg-meta/src/sql/studio/table-editor/rls.ts index 1d142971025..29f4a5dbd26 100644 --- a/packages/pg-meta/src/sql/studio/table-editor/rls.ts +++ b/packages/pg-meta/src/sql/studio/table-editor/rls.ts @@ -1,5 +1,11 @@ -import { ident } from '../../../pg-format' +import { ident, safeSql, type SafeSqlFragment } from '../../../pg-format' -export const getEnableRLSSQL = ({ schema, table }: { schema: string; table: string }) => { - return `ALTER TABLE ${ident(schema)}.${ident(table)} ENABLE ROW LEVEL SECURITY` +export const getEnableRLSSQL = ({ + schema, + table, +}: { + schema: string + table: string +}): SafeSqlFragment => { + return safeSql`ALTER TABLE ${ident(schema)}.${ident(table)} ENABLE ROW LEVEL SECURITY` }