mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
security: migrate more files to safeSql (#44727)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
f415fb9f5e
commit
b1531545fb
29 files changed
+346
-315
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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<PGTrigger, 'enabled_mode'>
|
||||
updatedTrigger: Omit<PGTriggerCreate, 'events'> &
|
||||
Pick<PGTrigger, 'enabled_mode'> & { events: Array<SafeSqlFragment> }
|
||||
}
|
||||
|
||||
export async function updateDatabaseTrigger({
|
||||
|
||||
@@ -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;`
|
||||
}
|
||||
@@ -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});`
|
||||
}
|
||||
@@ -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;`
|
||||
}
|
||||
@@ -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)};`
|
||||
}
|
||||
@@ -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()
|
||||
`
|
||||
@@ -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<string> }) => {
|
||||
const schemaList = schemas.map(literal).join(', ')
|
||||
export const getUnsafeEntitiesInApiSql = ({
|
||||
schemas,
|
||||
}: {
|
||||
schemas: Array<string>
|
||||
}): SafeSqlFragment => {
|
||||
const schemaList = joinSqlFragments(schemas.map(literal), ', ')
|
||||
|
||||
return /* SQL */ `
|
||||
return safeSql`
|
||||
select
|
||||
n.nspname as schema,
|
||||
c.relname as name,
|
||||
|
||||
@@ -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<SafeSqlFragment> = []
|
||||
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
|
||||
`
|
||||
}
|
||||
@@ -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;`
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
`
|
||||
@@ -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
|
||||
`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<string>
|
||||
function_schema: string
|
||||
function_name: string
|
||||
function_args: string[]
|
||||
function_args: Array<string>
|
||||
}
|
||||
updatedTrigger: PGTriggerCreate & Pick<PGTrigger, 'enabled_mode'>
|
||||
}) {
|
||||
updatedTrigger: Omit<PGTriggerCreate, 'events'> &
|
||||
Pick<PGTrigger, 'enabled_mode'> & { events: Array<SafeSqlFragment> }
|
||||
}): 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;`
|
||||
}
|
||||
@@ -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}
|
||||
)`
|
||||
}
|
||||
@@ -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
|
||||
`
|
||||
}
|
||||
@@ -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()
|
||||
`
|
||||
@@ -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)})`
|
||||
}
|
||||
@@ -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();`
|
||||
}
|
||||
@@ -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';`
|
||||
}
|
||||
@@ -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)});`
|
||||
}
|
||||
@@ -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)};
|
||||
`
|
||||
@@ -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)}`
|
||||
@@ -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<string>
|
||||
search?: string
|
||||
sort: 'alphabetical' | 'grouped-alphabetical'
|
||||
filterTypes: string[]
|
||||
filterTypes: Array<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`
|
||||
}): 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
|
||||
`
|
||||
}
|
||||
@@ -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<ForeignKey>
|
||||
}): 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<ForeignKey>
|
||||
}): 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
|
||||
|
||||
@@ -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)}));`
|
||||
}
|
||||
@@ -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<string>
|
||||
}): SafeSqlFragment => {
|
||||
const primaryKeyColumns = joinSqlFragments(columns.map(ident), ', ')
|
||||
return safeSql`ALTER TABLE ${ident(schema)}.${ident(table)} ADD PRIMARY KEY (${primaryKeyColumns})`
|
||||
}
|
||||
@@ -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`
|
||||
}
|
||||
Reference in new issue
Block a user