studio: convert more executeSql callers to SafeSqlFragment (#45645)

## Summary

- Converts ~27 `executeSql` call sites in `apps/studio/data/**` to build
SQL through `safeSql` / `ident` / `literal` / `keyword` /
`joinSqlFragments` instead of raw template-string interpolation.
- Tightens the `useDatabaseCronJobCreateMutation` and
`useDatabaseEventTriggerCreateMutation` `sql`/`query` parameter types
from `string` to `SafeSqlFragment` (callers already produce one).
- Updates `getDeleteEnumeratedTypeSQL` in `packages/pg-meta` to return
`SafeSqlFragment`.
- Fixes a bug noticed while testing where Queues integration does not
correctly handle queues with uppercase names.

## Pages to manually test

- Integrations > Cron Jobs
- Integrations > Queues
- Database > Triggers > Event Triggers
- Database > Indexes
- Reports > Query Performance
- Storage

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
  * Queue lookups now correctly handle case-insensitive queue names.
* Queue table references are now properly managed and consistently
applied throughout the queue management interface.
  * Improved queue name display normalization in the user interface.

* **Chores**
* Enhanced SQL query safety across the database layer through
parameterized query construction and safer templating approaches.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Charis authored and GitHub committed 2026-05-06 12:21:48 -04:00
1 parent 3864f25336
commit 116faefcda
36 files changed
+173 -101

No files matched your search

@@ -1,6 +1,6 @@
import z from 'zod'
import { QueryNameSchema } from '../Queues.utils'
import { QueueNameSchema } from '../Queues.utils'
const normalQueueSchema = z.object({
type: z.literal('basic'),
@@ -17,7 +17,7 @@ const unloggedQueueSchema = z.object({
})
export const FormSchema = z.object({
name: QueryNameSchema,
name: QueueNameSchema,
enableRls: z.boolean(),
values: z.discriminatedUnion('type', [
normalQueueSchema,
@@ -1,6 +1,7 @@
import dayjs from 'dayjs'
import { Check, Loader2, X } from 'lucide-react'
import { pgmqQueueTable } from './Queues.utils'
import { useQueuesMetricsQuery } from '@/data/database-queues/database-queues-metrics-query'
import { PostgresQueue } from '@/data/database-queues/database-queues-query'
import { useTablesQuery } from '@/data/tables/tables-query'
@@ -43,7 +44,7 @@ export const QueueRLSCell = ({ queue }: QueueCellProps) => {
schema: 'pgmq',
})
const queueTable = queueTables?.find((x) => x.name === `q_${queue.queue_name}`)
const queueTable = queueTables?.find((x) => x.name === pgmqQueueTable(queue.queue_name))
const isRlsEnabled = !!queueTable?.rls_enabled
return (
@@ -34,7 +34,10 @@ export const QueuePage = () => {
connectionString: project?.connectionString,
})
const currentQueue = queues?.find((queue) => queue.queue_name === childId)
// pgmq is case-insensitive when storing queue names — compare lowercased to be safe
const currentQueue = queues?.find(
(queue) => queue.queue_name.toLowerCase() === childId?.toLowerCase()
)
const pageTitle = childLabel || childId || 'Queue'
@@ -16,6 +16,7 @@ import {
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { pgmqQueueTable } from './Queues.utils'
import { DeleteQueue } from '@/components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue'
import { PurgeQueue } from '@/components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue'
import { QUEUE_MESSAGE_TYPE } from '@/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils'
@@ -51,7 +52,8 @@ export const QueueTab = () => {
connectionString: project?.connectionString,
schema: 'pgmq',
})
const queueTable = tables?.find((x) => x.name === `q_${queueName}`)
const queueRelname = queueName ? pgmqQueueTable(queueName) : undefined
const queueTable = tables?.find((x) => x.name === queueRelname)
const isRlsEnabled = queueTable?.rls_enabled ?? false
const { data: policies } = useDatabasePoliciesQuery({
@@ -59,7 +61,7 @@ export const QueueTab = () => {
connectionString: project?.connectionString,
schema: 'pgmq',
})
const queuePolicies = (policies ?? []).filter((policy) => policy.table === `q_${queueName}`)
const queuePolicies = (policies ?? []).filter((policy) => policy.table === queueRelname)
const { data: isExposed } = useQueuesExposePostgrestStatusQuery({
projectRef: project?.ref,
@@ -109,7 +109,11 @@ export const prepareQueuesForDataGrid = (queues: PostgresQueue[]): QueueWithMetr
}))
}
export const QueryNameSchema = z
// pgmq stores queue names as-provided in pgmq.meta and lowercases them only when
// building the underlying q_/a_ relations, so uppercase queue names are fine —
// the user-facing name is preserved and table-name lookups go through
// pgmqQueueTable / pgmqArchiveTable which lowercase to match pgmq's behavior.
export const QueueNameSchema = z
.string()
.trim()
.min(1, 'Please provide a name for your queue')
@@ -119,9 +123,13 @@ export const QueryNameSchema = z
'Name must contain only alphanumeric characters, underscores, and hyphens'
)
/**
* Checks if the queue name is valid. Returns a boolean.
*/
export const isQueueNameValid = (queueName: string) => {
return QueryNameSchema.safeParse(queueName).success
return QueueNameSchema.safeParse(queueName).success
}
// pgmq.format_table_name() lowercases its input, so the actual relations in the
// pgmq schema are always q_/a_ followed by the lowercased queue name — even when
// pgmq.meta stores the original casing. Use these helpers anywhere a queue name
// is mapped to a relname.
export const pgmqQueueTable = (queueName: string) => `q_${queueName.toLowerCase()}`
export const pgmqArchiveTable = (queueName: string) => `a_${queueName.toLowerCase()}`
@@ -3,6 +3,7 @@ import { includes, sortBy } from 'lodash'
import { Check, ChevronRight, Loader2, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { pgmqQueueTable } from './Queues.utils'
import Table from '@/components/to-be-cleaned/Table'
import { useQueuesMetricsQuery } from '@/data/database-queues/database-queues-metrics-query'
import { PostgresQueue } from '@/data/database-queues/database-queues-query'
@@ -24,7 +25,7 @@ const QueueRow = ({ queue }: { queue: PostgresQueue }) => {
connectionString: selectedProject?.connectionString,
schema: 'pgmq',
})
const queueTable = queueTables?.find((x) => x.name === `q_${queue.queue_name}`)
const queueTable = queueTables?.find((x) => x.name === pgmqQueueTable(queue.queue_name))
const isRlsEnabled = !!queueTable?.rls_enabled
const { data: metrics, isPending: isLoading } = useQueuesMetricsQuery(
@@ -21,6 +21,7 @@ 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 { useQueuesQuery } from '@/data/database-queues/database-queues-query'
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'
@@ -56,6 +57,18 @@ export const QueuesSettings = () => {
const tablesWithoutRLS =
queueTables?.filter((x) => x.name.startsWith('q_') && !x.rls_enabled) ?? []
// pgmq lowercases queue names when building q_/a_ relations, but pgmq.meta keeps
// the original casing. Look up each relname in list_queues() so we render the
// user-provided name rather than the lowercased relname slice.
const { data: queues } = useQueuesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const queueDisplayName = (relname: string) => {
const stripped = relname.slice(2)
return queues?.find((q) => q.queue_name.toLowerCase() === stripped)?.queue_name ?? stripped
}
const { data: config, error: configError } = useProjectPostgrestConfigQuery({
projectRef: project?.ref,
})
@@ -259,7 +272,9 @@ export const QueuesSettings = () => {
{tablesWithoutRLS.map((x) => {
return (
<li key={x.name}>
<code className="text-code-inline">{x.name.slice(2)}</code>
<code className="text-code-inline">
{queueDisplayName(x.name)}
</code>
</li>
)
})}
@@ -272,7 +287,7 @@ export const QueuesSettings = () => {
>
Enable RLS on{' '}
{tablesWithoutRLS.length === 1
? tablesWithoutRLS[0].name.slice(2)
? queueDisplayName(tablesWithoutRLS[0].name)
: `${tablesWithoutRLS.length} queues`}
</Button>
</Admonition>
@@ -354,7 +369,7 @@ export const QueuesSettings = () => {
{tablesWithoutRLS.map((x) => {
return (
<li key={x.id}>
<code className="text-code-inline">{x.name.slice(2)}</code>
<code className="text-code-inline">{queueDisplayName(x.name)}</code>
</li>
)
})}
@@ -28,6 +28,7 @@ import {
import { Admonition } from 'ui-patterns/admonition'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { pgmqArchiveTable, pgmqQueueTable } from '../Queues.utils'
import { getQueueFunctionsMapping } from './Queue.utils'
import AlertError from '@/components/ui/AlertError'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
@@ -83,15 +84,17 @@ export const QueueSettings = ({}: QueueSettingsProps) => {
connectionString: project?.connectionString,
schema: 'pgmq',
})
const queueTable = queueTables?.find((x) => x.name === `q_${name}`)
const archiveTable = queueTables?.find((x) => x.name === `a_${name}`)
const queueRelname = name ? pgmqQueueTable(name) : undefined
const archiveRelname = name ? pgmqArchiveTable(name) : undefined
const queueTable = queueTables?.find((x) => x.name === queueRelname)
const archiveTable = queueTables?.find((x) => x.name === archiveRelname)
const { data: allTablePrivileges, isSuccess: isSuccessPrivileges } = useTablePrivilegesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const queuePrivileges = allTablePrivileges?.find(
(x) => x.schema === 'pgmq' && x.name === `q_${name}`
(x) => x.schema === 'pgmq' && x.name === queueRelname
)
const { mutateAsync: grantPrivilege } = useTablePrivilegesGrantMutation()
@@ -1,3 +1,4 @@
import { safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { configKeys } from './keys'
@@ -25,7 +26,7 @@ export async function getDiskBreakdown(
{
projectRef,
connectionString,
sql: `
sql: safeSql`
SELECT
(
SELECT
@@ -1,3 +1,4 @@
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { CronJob } from './database-cron-jobs-infinite-query'
@@ -24,8 +25,8 @@ export async function getDatabaseCronJob({
projectRef,
connectionString,
sql: !!id
? `SELECT * FROM cron.job where jobid = ${id};`
: `SELECT * FROM cron.job where jobname = '${name}';`,
? safeSql`SELECT * FROM cron.job where jobid = ${literal(id)};`
: safeSql`SELECT * FROM cron.job where jobname = ${literal(name)};`,
queryKey: ['cron-job', id],
})
@@ -1,3 +1,4 @@
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -22,15 +23,13 @@ export async function runDatabaseCronJobCommand({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `
DO $$
sql: safeSql`DO $$
DECLARE
job_command text;
BEGIN
select command into job_command from cron.job where jobid = ${jobId};
select command into job_command from cron.job where jobid = ${literal(jobId)};
EXECUTE job_command;
END $$;
`.trim(),
END $$;`,
queryKey: databaseCronJobsKeys.create(),
})
@@ -1,3 +1,4 @@
import { safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { databaseCronJobsKeys } from './keys'
@@ -9,7 +10,7 @@ type DatabaseCronJobsCountVariables = {
connectionString?: string | null
}
const cronJobCountSql = `select count(jobid) from cron.job;`.trim()
const cronJobCountSql = safeSql`select count(jobid) from cron.job;`
export async function getDatabaseCronJobsCount({
projectRef,
@@ -1,3 +1,4 @@
import type { SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -8,7 +9,7 @@ import type { ResponseError, UseCustomMutationOptions } from '@/types'
export type DatabaseCronJobCreateVariables = {
projectRef: string
connectionString?: string | null
query: string
query: SafeSqlFragment
searchTerm?: string
identifier?: string | number
}
@@ -1,3 +1,4 @@
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -20,7 +21,7 @@ export async function deleteDatabaseCronJob({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `SELECT cron.unschedule(${jobId});`,
sql: safeSql`SELECT cron.unschedule(${literal(jobId)});`,
queryKey: databaseCronJobsKeys.delete(),
})
@@ -1,3 +1,4 @@
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
import { last } from 'lodash'
@@ -37,13 +38,15 @@ export async function getDatabaseCronJobRuns({
// Use runid for ordering and pagination since it's the primary key (indexed)
// and preserves chronological order (auto-incrementing)
let query = `
const afterRunIdClause =
typeof afterRunId === 'number' ? safeSql`AND runid < ${literal(afterRunId)}` : safeSql``
const query = safeSql`
SELECT * FROM cron.job_run_details
WHERE
jobid = '${jobId}'
${typeof afterRunId === 'number' ? `AND runid < '${afterRunId}'` : ''}
jobid = ${literal(jobId)}
${afterRunIdClause}
ORDER BY runid DESC
LIMIT ${CRON_JOB_RUNS_PAGE_SIZE}`
LIMIT ${literal(CRON_JOB_RUNS_PAGE_SIZE)}`
const { result } = await executeSql({
projectRef,
@@ -1,3 +1,4 @@
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -22,7 +23,7 @@ export async function toggleDatabaseCronJob({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `select cron.alter_job(job_id := ${jobId}, active := ${active});`,
sql: safeSql`select cron.alter_job(job_id := ${literal(jobId)}, active := ${literal(active)});`,
queryKey: databaseCronJobsKeys.alter(),
})
@@ -1,3 +1,4 @@
import { safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { databaseCronJobsKeys } from './keys'
@@ -18,7 +19,7 @@ export async function getDatabaseCronTimezone({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `select setting from pg_settings where name = 'cron.timezone';`,
sql: safeSql`select setting from pg_settings where name = 'cron.timezone';`,
})
return result[0].setting
}
@@ -1,3 +1,4 @@
import type { SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -8,7 +9,7 @@ import type { ResponseError, UseCustomMutationOptions } from '@/types'
export type DatabaseEventTriggerCreateVariables = {
projectRef: string
connectionString?: string | null
sql: string
sql: SafeSqlFragment
}
export async function createDatabaseEventTrigger({
@@ -1,3 +1,4 @@
import { safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { databaseEventTriggerKeys } from './keys'
@@ -22,7 +23,7 @@ export type DatabaseEventTrigger = {
function_definition: string | null
}
const EVENT_TRIGGERS_SQL = `
const EVENT_TRIGGERS_SQL = safeSql`
select
evt.oid,
evt.evtname as name,
@@ -1,4 +1,4 @@
import { ident } from '@supabase/pg-meta/src/pg-format'
import { ident, joinSqlFragments, keyword, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -24,11 +24,11 @@ export async function createDatabaseIndex({
}: DatabaseIndexCreateVariables) {
const { schema, entity, type, columns } = payload
const sql = `
CREATE INDEX ON ${ident(schema)}.${ident(entity)} USING ${type} (${columns
.map((column) => ident(column))
.join(', ')});
`.trim()
const columnList = joinSqlFragments(
columns.map((column) => ident(column)),
', '
)
const sql = safeSql`CREATE INDEX ON ${ident(schema)}.${ident(entity)} USING ${keyword(type)} (${columnList});`
const { result } = await executeSql({
projectRef,
@@ -1,4 +1,4 @@
import { ident } from '@supabase/pg-meta/src/pg-format'
import { ident, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -19,7 +19,7 @@ export async function deleteDatabaseIndex({
name,
schema,
}: DatabaseIndexDeleteVariables) {
const sql = `drop index if exists ${ident(schema)}.${ident(name)}`
const sql = safeSql`drop index if exists ${ident(schema)}.${ident(name)}`
const { result } = await executeSql({
projectRef,
@@ -1,3 +1,4 @@
import { safeSql } from '@supabase/pg-meta/src/pg-format'
import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'
import { z } from 'zod'
@@ -30,7 +31,7 @@ export async function getStripeSyncState(
{
projectRef,
connectionString,
sql: `
sql: safeSql`
SELECT started_at, closed_at, status FROM stripe.sync_runs WHERE status != 'pending' ORDER BY started_at DESC LIMIT 1;
`,
queryKey: stripeSyncKeys.syncState(projectRef),
@@ -1,4 +1,4 @@
import { literal } from '@supabase/pg-meta/src/pg-format'
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -28,7 +28,7 @@ export async function archiveDatabaseQueueMessage({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `SELECT * FROM pgmq.archive(${literal(queueName)}, ${literal(messageId)})`,
sql: safeSql`SELECT * FROM pgmq.archive(${literal(queueName)}, ${literal(messageId)})`,
queryKey: databaseQueuesKeys.create(),
})
@@ -1,4 +1,4 @@
import { literal } from '@supabase/pg-meta/src/pg-format'
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -29,7 +29,7 @@ export async function deleteDatabaseQueueMessage({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `SELECT * FROM pgmq.delete(${literal(queueName)}, ${literal(messageId)})`,
sql: safeSql`SELECT * FROM pgmq.delete(${literal(queueName)}, ${literal(messageId)})`,
queryKey: databaseQueuesKeys.create(),
})
@@ -1,10 +1,14 @@
import { literal } from '@supabase/pg-meta/src/pg-format'
import { ident, literal, safeSql, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { last } from 'lodash'
import { databaseQueuesKeys } from './keys'
import { isQueueNameValid } from '@/components/interfaces/Integrations/Queues/Queues.utils'
import {
isQueueNameValid,
pgmqArchiveTable,
pgmqQueueTable,
} from '@/components/interfaces/Integrations/Queues/Queues.utils'
import { QUEUE_MESSAGE_TYPE } from '@/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils'
import { executeSql } from '@/data/sql/execute-sql-query'
import { DATE_FORMAT } from '@/lib/constants'
@@ -47,34 +51,46 @@ export async function getDatabaseQueue({
}
// handles when scheduled and available are deselected
let queueQuery = ``
const queueTable = safeSql`${ident('pgmq')}.${ident(pgmqQueueTable(queueName))}`
const archivedTable = safeSql`${ident('pgmq')}.${ident(pgmqArchiveTable(queueName))}`
const nowLiteral = literal(dayjs(new Date()).format(DATE_FORMAT))
let queueQuery: SafeSqlFragment | null = null
if (status.includes('available') && status.includes('scheduled')) {
queueQuery = `SELECT msg_id, enqueued_at, read_ct, vt, message, NULL as archived_at FROM "pgmq"."q_${queueName}"`
queueQuery = safeSql`SELECT msg_id, enqueued_at, read_ct, vt, message, NULL as archived_at FROM ${queueTable}`
} else if (status.includes('available') && !status.includes('scheduled')) {
queueQuery = `SELECT msg_id, enqueued_at, read_ct, vt, message, NULL as archived_at FROM "pgmq"."q_${queueName}" WHERE vt < '${dayjs(new Date()).format(DATE_FORMAT)}'`
queueQuery = safeSql`SELECT msg_id, enqueued_at, read_ct, vt, message, NULL as archived_at FROM ${queueTable} WHERE vt < ${nowLiteral}`
} else if (!status.includes('available') && status.includes('scheduled')) {
queueQuery = `SELECT msg_id, enqueued_at, read_ct, vt, message, NULL as archived_at FROM "pgmq"."q_${queueName}" WHERE vt > '${dayjs(new Date()).format(DATE_FORMAT)}'`
queueQuery = safeSql`SELECT msg_id, enqueued_at, read_ct, vt, message, NULL as archived_at FROM ${queueTable} WHERE vt > ${nowLiteral}`
}
let archivedQuery = ``
if (status.includes('archived')) {
archivedQuery = `SELECT msg_id, enqueued_at, read_ct, vt, message, archived_at FROM "pgmq"."a_${queueName}"`
}
const archivedQuery = status.includes('archived')
? safeSql`SELECT msg_id, enqueued_at, read_ct, vt, message, archived_at FROM ${archivedTable}`
: null
let query = `SELECT
const unionParts = [queueQuery, archivedQuery].filter(
(part): part is SafeSqlFragment => part !== null
)
const unionFragment = unionParts.reduce(
(acc, part, index) => (index === 0 ? part : safeSql`${acc} UNION ALL ${part}`),
safeSql``
)
const whereClause = afterTimestamp
? safeSql` WHERE enqueued_at > ${literal(afterTimestamp)}`
: safeSql``
const sql = safeSql`SELECT
*
FROM
(
${[queueQuery, archivedQuery].filter(Boolean).join(' UNION ALL ')}
) AS combined`
if (afterTimestamp) {
query += ` WHERE enqueued_at > ${literal(afterTimestamp)}`
}
${unionFragment}
) AS combined${whereClause} order by enqueued_at LIMIT ${literal(QUEUE_MESSAGES_PAGE_SIZE)}`
const { result } = await executeSql({
projectRef,
connectionString,
sql: `${query} order by enqueued_at LIMIT ${QUEUE_MESSAGES_PAGE_SIZE}`,
sql,
})
return result as DatabaseQueueData
}
@@ -1,4 +1,4 @@
import { literal } from '@supabase/pg-meta/src/pg-format'
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -31,7 +31,7 @@ export async function readDatabaseQueueMessage({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `select * from pgmq.set_vt(${literal(queueName)}, ${literal(messageId)}, ${literal(duration)})`,
sql: safeSql`select * from pgmq.set_vt(${literal(queueName)}, ${literal(messageId)}, ${literal(duration)})`,
queryKey: databaseQueuesKeys.create(),
})
@@ -1,4 +1,4 @@
import { literal } from '@supabase/pg-meta/src/pg-format'
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -31,7 +31,7 @@ export async function sendDatabaseQueueMessage({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `select * from pgmq.send(${literal(queueName)}, ${literal(payload)}, ${literal(delay)})`,
sql: safeSql`select * from pgmq.send(${literal(queueName)}, ${literal(payload)}, ${literal(delay)})`,
queryKey: databaseQueuesKeys.create(),
})
@@ -1,9 +1,12 @@
import { ident, literal } from '@supabase/pg-meta/src/pg-format'
import { ident, literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { databaseQueuesKeys } from './keys'
import { isQueueNameValid } from '@/components/interfaces/Integrations/Queues/Queues.utils'
import {
isQueueNameValid,
pgmqQueueTable,
} from '@/components/interfaces/Integrations/Queues/Queues.utils'
import { executeSql } from '@/data/sql/execute-sql-query'
import { tableKeys } from '@/data/tables/keys'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
@@ -36,17 +39,21 @@ export async function createDatabaseQueue({
const { partitionInterval, retentionInterval } = configuration ?? {}
const query =
const createFragment =
type === 'partitioned'
? `select from pgmq.create_partitioned(${literal(name)}, ${literal(partitionInterval)}, ${literal(retentionInterval)});`
? safeSql`select from pgmq.create_partitioned(${literal(name)}, ${literal(partitionInterval)}, ${literal(retentionInterval)});`
: type === 'unlogged'
? `SELECT pgmq.create_unlogged(${literal(name)});`
: `SELECT pgmq.create(${literal(name)});`
? safeSql`SELECT pgmq.create_unlogged(${literal(name)});`
: safeSql`SELECT pgmq.create(${literal(name)});`
const rlsFragment = enableRls
? safeSql` alter table ${ident('pgmq')}.${ident(pgmqQueueTable(name))} enable row level security;`
: safeSql``
const { result } = await executeSql({
projectRef,
connectionString,
sql: `${query} ${enableRls ? `alter table pgmq.${ident('q_' + name)} enable row level security;` : ''}`.trim(),
sql: safeSql`${createFragment}${rlsFragment}`,
queryKey: databaseQueuesKeys.create(),
})
@@ -1,4 +1,4 @@
import { literal } from '@supabase/pg-meta/src/pg-format'
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -27,7 +27,7 @@ export async function deleteDatabaseQueue({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `select * from pgmq.drop_queue(${literal(queueName)});`,
sql: safeSql`select * from pgmq.drop_queue(${literal(queueName)});`,
queryKey: databaseQueuesKeys.delete(queueName),
})
@@ -1,8 +1,11 @@
import { ident, literal } from '@supabase/pg-meta/src/pg-format'
import { ident, literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { databaseQueuesKeys } from './keys'
import { isQueueNameValid } from '@/components/interfaces/Integrations/Queues/Queues.utils'
import {
isQueueNameValid,
pgmqQueueTable,
} from '@/components/interfaces/Integrations/Queues/Queues.utils'
import { executeSql } from '@/data/sql/execute-sql-query'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
@@ -18,27 +21,25 @@ export type PostgresQueueMetric = {
method: 'estimated' | 'precise'
}
const preciseMetricsSqlQuery = (queueName: string) => {
return `
const preciseMetricsSqlQuery = (queueName: string) =>
safeSql`
set local statement_timeout = '1s';
SELECT
COUNT(*) AS row_count
FROM
"pgmq".${ident('q_' + queueName)};
${ident('pgmq')}.${ident(pgmqQueueTable(queueName))};
`
}
const estimateMetricsSqlQuery = (queueName: string) => {
return `
const estimateMetricsSqlQuery = (queueName: string) =>
safeSql`
select
reltuples::bigint as estimated_rows
from
pg_class
where
relname = ${literal('q_' + queueName)}
relname = ${literal(pgmqQueueTable(queueName))}
and relnamespace = 'pgmq'::regnamespace;
`
}
export async function getDatabaseQueuesMetrics({
projectRef,
@@ -1,4 +1,4 @@
import { literal } from '@supabase/pg-meta/src/pg-format'
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -27,7 +27,7 @@ export async function purgeDatabaseQueue({
const { result } = await executeSql({
projectRef,
connectionString,
sql: `select * from pgmq.purge_queue(${literal(queueName)});`,
sql: safeSql`select * from pgmq.purge_queue(${literal(queueName)});`,
queryKey: databaseQueuesKeys.purge(queueName),
})
@@ -1,3 +1,4 @@
import { joinSqlFragments, literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { databaseKeys } from './keys'
@@ -31,7 +32,7 @@ export async function getInvolvedIndexesInSelectQuery({
projectRef,
connectionString,
queryKey: ['involved-indexes-explain-query'],
sql: /* sql */ `
sql: safeSql`
create or replace function pg_temp.explain_query(query text) returns jsonb
language plpgsql
as $$
@@ -101,7 +102,7 @@ export async function getInvolvedIndexesInSelectQuery({
end;
$$;
select pg_temp.explain_query('${query}') as plans;
select pg_temp.explain_query(${literal(query)}) as plans;
`,
})
@@ -113,7 +114,7 @@ export async function getInvolvedIndexesInSelectQuery({
projectRef,
connectionString,
queryKey: ['involved-indexes-names'],
sql: `select schemaname as schema, tablename as table, indexname as name from pg_indexes where indexname in (${involvedIndexes.map((name) => `'${name}'`).join(', ')});`,
sql: safeSql`select schemaname as schema, tablename as table, indexname as name from pg_indexes where indexname in (${joinSqlFragments(involvedIndexes.map(literal), ', ')});`,
})
return indexResult as GetInvolvedIndexesFromSelectQueryResponse[]
@@ -1,3 +1,4 @@
import { safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { databaseKeys } from './keys'
@@ -18,7 +19,7 @@ export async function getSupamonitorEnabled({
const { result } = await executeSql<{ libraries: string }[]>({
projectRef,
connectionString,
sql: `SELECT current_setting('shared_preload_libraries', true) AS libraries`,
sql: safeSql`SELECT current_setting('shared_preload_libraries', true) AS libraries`,
})
const libraries = result[0]?.libraries ?? ''
@@ -1,4 +1,4 @@
import { literal } from '@supabase/pg-meta/src/pg-format'
import { literal, safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { storageKeys } from './keys'
@@ -36,7 +36,7 @@ async function getPublicBucketsWithSelectPolicies({
const { result } = await executeSql<PublicBucketSelectPolicy[]>({
projectRef,
connectionString,
sql: `
sql: safeSql`
SELECT b.id AS bucket_id, b.name AS bucket_name, p.policyname
FROM storage.buckets b
JOIN pg_policies p
+3 -2
View File
@@ -1,3 +1,4 @@
import { safeSql } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
@@ -15,7 +16,7 @@ export type TableNamesVariables = {
connectionString?: string | null
}
const TABLE_NAMES_SQL = /* sql */ `
const TABLE_NAMES_SQL = safeSql`
select
c.oid::int8 as id,
nc.nspname as schema,
@@ -31,7 +32,7 @@ where c.relkind in ('r', 'p')
or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')
)
order by nc.nspname, c.relname
`.trim()
`
export async function getTableNames(
{ projectRef, connectionString }: TableNamesVariables,
@@ -22,7 +22,7 @@ export const getCreateEnumeratedTypeSQL = ({
}
export const getDeleteEnumeratedTypeSQL = ({ schema, name }: { schema: string; name: string }) => {
return `drop type if exists ${ident(schema)}.${ident(name)}`
return safeSql`drop type if exists ${ident(schema)}.${ident(name)}`
}
export const getUpdateEnumeratedTypeSQL = ({