mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
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:
1 parent
3864f25336
commit
116faefcda
36 files changed
+173
-101
No files matched your search
+2
-2
@@ -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
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
Reference in new issue
Block a user