diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
index 54680d64291..aa5065a4f98 100644
--- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
+++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx
@@ -133,11 +133,6 @@ export const useIsSqlEditorManualSaveEnabled = () => {
return sqlEditorManualSaveEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]
}
-export const useIsRLSTesterEnabled = () => {
- const { flags } = useFeaturePreviewContext()
- return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]
-}
-
export const useIsMarketplaceEnabled = () => {
const { flags } = useFeaturePreviewContext()
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx
index 49ef243658d..7a03d13ea76 100644
--- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx
+++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx
@@ -34,12 +34,9 @@ import { IntegrationsLayoutPreview } from './IntegrationsLayoutPreview'
import { JitDbAccessPreview } from './JitDbAccessPreview'
import { PgDeltaDiffPreview } from './PgDeltaDiffPreview'
import { PlatformWebhooksPreview } from './PlatformWebhooksPreview'
-import { RLSTesterPreview } from './RLSTesterPreview'
import { SqlEditorManualSavePreview } from './SqlEditorManualSavePreview'
import { UnifiedLogsPreview } from './UnifiedLogsPreview'
import { FeaturePreview, useFeaturePreviews } from './useFeaturePreviews'
-import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
-import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { IS_PLATFORM } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
@@ -52,7 +49,6 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: {
[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]: ,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: ,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: ,
- [LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]: ,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]: ,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: ,
}
@@ -70,12 +66,6 @@ export const FeaturePreviewModal = () => {
const featurePreviewContext = useFeaturePreviewContext()
const track = useTrack()
- const { dismissBanner } = useBannerStack()
- const [, setIsDismissedRlsTesterBanner] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(ref ?? ''),
- false
- )
-
const { flags, onUpdateFlag } = featurePreviewContext
const allFeaturePreviews = (
IS_PLATFORM ? featurePreviews : featurePreviews.filter((x) => !x.isPlatformOnly)
@@ -94,11 +84,6 @@ export const FeaturePreviewModal = () => {
const isEnabling = !isSelectedFeatureEnabled
- if (selectedFeature.key === LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER) {
- dismissBanner('rls-tester-banner')
- setIsDismissedRlsTesterBanner(true)
- }
-
onUpdateFlag(selectedFeature.key, isEnabling)
track(isEnabling ? 'feature_preview_enabled' : 'feature_preview_disabled', {
feature: selectedFeature.key,
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx
deleted file mode 100644
index 01ea7cffdc5..00000000000
--- a/apps/studio/components/interfaces/App/FeaturePreview/RLSTesterPreview.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { useParams } from 'common'
-import Image from 'next/image'
-
-import { InlineLink } from '@/components/ui/InlineLink'
-import { BASE_PATH } from '@/lib/constants'
-
-export const RLSTesterPreview = () => {
- const { ref } = useParams()
-
- return (
-
-
- Verify if your RLS policies have been set up properly by running queries as a specific user.
- While role impersonation isn't a new feature on the dashboard, we've built a dedicated UI
- for this which will also show what policies are evaluated for the query.
-
-
-
-
Enabling this preview will:
-
-
- Show the "Test" button on the{' '}
-
- Database Policies page
-
-
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts
index c256c2ffac2..dd425cc52f5 100644
--- a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts
+++ b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts
@@ -30,16 +30,6 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
return useMemo(
() =>
[
- {
- key: LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER,
- name: 'RLS Tester',
- discussionsUrl: 'https://github.com/orgs/supabase/discussions/45233',
- enabled: true,
- isNew: true,
- isPlatformOnly: false,
- isDefaultOptIn: false,
- getRoute: (ref?: string) => `/project/${ref}/database/policies`,
- },
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS,
name: 'Updated Logs interface',
diff --git a/apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx b/apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx
deleted file mode 100644
index ecdaf4b6b80..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/InferredSQLViewer.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { UntrustedSqlFragment } from '@supabase/pg-meta'
-import { Loader2 } from 'lucide-react'
-import { Badge, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
-
-import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
-
-export const InferredSQLViewer = ({
- sql,
- isLoading = false,
-}: {
- sql: UntrustedSqlFragment | undefined
- isLoading?: boolean
-}) => {
- return (
- <>
-
-
-
Inferred SQL:
- {isLoading &&
}
-
-
-
-
- Generated
-
-
- This query is inferred from client library code with the help of the Assistant and may
- not guarantee correctness.
-
-
-
-
-
- {isLoading && !sql ? (
-
-
-
- ) : (
-
- )}
-
- >
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx
deleted file mode 100644
index 724f8b57cf2..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTableCard.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import { Check, ChevronDown, Edit, X } from 'lucide-react'
-import { useMemo } from 'react'
-import { cn, Collapsible, CollapsibleContent, CollapsibleTrigger, WarningIcon } from 'ui'
-
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
-import { type ParseSQLQueryOperations } from '@/data/misc/parse-query-mutation'
-
-interface RLSTableCardProps {
- table: { schema: string; name: string; isRLSEnabled: boolean }
- operation: ParseSQLQueryOperations
- role?: string
- policies: Policy[]
- hasError: boolean
- handleSelectEditPolicy: (policy: Policy) => void
-}
-
-export const RLSTableCard = ({
- table,
- operation,
- role,
- policies,
- hasError,
- handleSelectEditPolicy,
-}: RLSTableCardProps) => {
- const { schema, name, isRLSEnabled } = table
- const trueOnlyPolicy = policies.find((x) => x.definition === 'true')
- const falseOnlyPolicy = policies.find((x) => x.definition === 'false')
- const noPolicies = isRLSEnabled && policies.length === 0
-
- const tableAccessDescription = useMemo(() => {
- if (!isRLSEnabled) {
- return (
-
- RLS is disabled and all data is publicly accessible. We highly recommend enabling RLS and
- adding policies to restrict access.
-
- )
- }
-
- if (noPolicies) {
- return (
-
- RLS is enabled but no policies exist for the{' '}
- {role} role on this table -{' '}
- {operation === 'SELECT'
- ? 'no data will be returned'
- : `no data will be ${operation?.toLowerCase()}${operation?.toLowerCase().endsWith('e') ? 'd' : 'ed'}`}
- .
-
- )
- }
-
- if (trueOnlyPolicy) {
- return (
- <>
-
- The policy "{trueOnlyPolicy.name}" for the{' '}
- {role} role on this table evaluates to{' '}
- true, so all data from this query is
- accessible to this user.
-
-
- >
- )
- }
-
- if (falseOnlyPolicy) {
- return (
- <>
-
- The policy "{falseOnlyPolicy.name}" for the{' '}
- {role} role on this table evaluates to{' '}
- false, so no data from this query is
- accessible to this user.
-
-
- >
- )
- }
-
- return (
- <>
-
- {policies.length} {policies.length > 1 ? 'policies apply' : 'policy applies'} for the{' '}
- {role} role on this table.{' '}
- {operation === 'SELECT'
- ? `Only rows that match ${policies.length > 1 ? 'these conditions' : 'this condition'} are returned.`
- : `The ${operation} operation will only be successful if the conditions are matched.`}
-
-
- >
- )
- }, [
- isRLSEnabled,
- noPolicies,
- trueOnlyPolicy,
- falseOnlyPolicy,
- policies,
- role,
- operation,
- handleSelectEditPolicy,
- ])
-
- return (
-
-
-
-
- {!isRLSEnabled ? (
-
- ) : (hasError && operation === 'INSERT') || noPolicies || falseOnlyPolicy ? (
-
- ) : (
-
- )}
-
- {schema}.{name}
-
-
-
-
- {operation === 'SELECT' && (
-
- {noPolicies || falseOnlyPolicy
- ? 'Returns no rows'
- : !isRLSEnabled || !!trueOnlyPolicy
- ? 'Returns all rows'
- : null}
-
- )}
-
-
-
-
- {tableAccessDescription}
-
-
- )
-}
-
-const TableAccessPolicySummary = ({
- policies,
- operation,
- handleSelectEditPolicy,
-}: {
- policies: Policy[]
- operation: ParseSQLQueryOperations
- handleSelectEditPolicy: (policy: Policy) => void
-}) => {
- return (
-
-
- {policies.length} {policies.length > 1 ? 'policies' : 'policy'} applied
-
-
- {policies.map((policy) => (
-
-
-
{policy.name}
-
- {operation === 'SELECT' ? 'Show rows' : `Allow ${operation?.toLocaleLowerCase()}s`}{' '}
- where:{' '}
-
- {policy.definition ?? policy.check}
-
-
-
- }
- className="w-7"
- tooltip={{ content: { side: 'bottom', text: 'Edit policy' } }}
- onClick={() => {
- handleSelectEditPolicy(policy)
- }}
- />
-
- ))}
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts b/apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts
deleted file mode 100644
index 2948c48f477..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTester.types.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { type User } from '@/data/auth/users-infinite-query'
-import { type ParseSQLQueryResponse } from '@/data/misc/parse-query-mutation'
-
-export type ParseQueryResults = {
- tables: {
- schema: string
- table: string
- tablePolicies: Array
- isRLSEnabled: boolean
- }[]
- operation: ParseSQLQueryResponse['operation']
- role?: string
- user?: User
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx
deleted file mode 100644
index d6eecd60beb..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterEmptyState.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { ListTodo } from 'lucide-react'
-
-export const RLSTesterEmptyState = () => {
- return (
-
-
-
Test summary and results will be shown here
-
- Verify that the results match what your RLS policies allow
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx
deleted file mode 100644
index 5391c6318f8..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import { Badge, cn, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui'
-import { Admonition } from 'ui-patterns/admonition'
-
-import { Results } from '../../SQLEditor/UtilityPanel/Results'
-import { RLSTableCard } from './RLSTableCard'
-import { ParseQueryResults } from './RLSTester.types'
-import { deriveRLSTestState } from './RLSTesterResults.utils'
-import { useTestQueryRLS } from './useTestQueryRLS'
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { type QueryResponseError } from '@/data/sql/execute-sql-mutation'
-
-interface RLSTesterResultsProps {
- results: Object[]
- autoLimit: boolean
- parseQueryResults: ParseQueryResults
- executeSqlError: Error | QueryResponseError | null | undefined
- handleSelectEditPolicy: (policy: Policy) => void
-}
-
-export const RLSTesterResults = ({
- results,
- autoLimit,
- parseQueryResults,
- executeSqlError,
- handleSelectEditPolicy,
-}: RLSTesterResultsProps) => {
- const { limit } = useTestQueryRLS()
-
- const {
- isServiceRole,
- tableWithRLSEnabledButNoPolicies,
- tableWithRLSEnabledWithPolicyFalse,
- tableWithRLSEnabledWithPoliciesDontApply,
- noAccessToData,
- } = deriveRLSTestState(parseQueryResults)
-
- const { operation, role } = parseQueryResults
- const rlsBlockInsert = executeSqlError && operation === 'INSERT'
- const noAccess = noAccessToData || rlsBlockInsert
-
- return (
-
-
-
Summary
- {noAccess ? (
-
No access
- ) : (
-
{results.length > 0 ? 'Can access' : 'Has access'}
- )}
-
-
-
-
-
- Policies applied
-
-
- Data preview
-
-
-
- {!!parseQueryResults && (
-
-
-
Ran as
- {!parseQueryResults.role ? (
-
postgres
- ) : parseQueryResults.user ? (
-
{parseQueryResults.user.email}
- ) : parseQueryResults.role === 'anon' ? (
-
an Anonymous user
- ) : null}
-
-
- {parseQueryResults.role === 'anon' && (
-
Not logged in user
- )}
- {!!parseQueryResults.user && (
-
ID: {parseQueryResults.user.id}
- )}
-
- )}
-
-
- {!isServiceRole &&
- (!!tableWithRLSEnabledButNoPolicies ? (
-
-
- This user{' '}
- {operation === 'SELECT'
- ? 'has no access to any rows'
- : `is unable to ${operation?.toLowerCase()} any rows`}{' '}
- from this query
-
-
- The table{' '}
-
- {tableWithRLSEnabledButNoPolicies.schema}.
- {tableWithRLSEnabledButNoPolicies.table}
- {' '}
- has RLS enabled but no policies set up for the{' '}
- {parseQueryResults.role}{' '}
- role.
-
-
- ) : tableWithRLSEnabledWithPolicyFalse ? (
-
-
- This user has no access to any rows from this query
-
-
- The table{' '}
-
- {tableWithRLSEnabledWithPolicyFalse.schema}.
- {tableWithRLSEnabledWithPolicyFalse.table}
- {' '}
- has a policy that evaluates to
- false for the{' '}
- {parseQueryResults.role}{' '}
- role.
-
-
- ) : rlsBlockInsert &&
- parseQueryResults.user &&
- tableWithRLSEnabledWithPoliciesDontApply ? (
-
-
- This user is unable to {operation?.toLowerCase()} any rows from this query
-
-
- The table{' '}
-
- {tableWithRLSEnabledWithPoliciesDontApply.schema}.
- {tableWithRLSEnabledWithPoliciesDontApply.table}
- {' '}
- has a policy for the{' '}
- {parseQueryResults.role}{' '}
- role, but its condition wasn't satisfied for this specific request.
-
-
- ) : null)}
-
- {isServiceRole && (
-
-
- The postgres role has access to all rows
- for this query
-
-
- The postgres role has admin privileges and
- bypasses all RLS policies.
-
-
- )}
-
-
-
Table access
- {!isServiceRole && (
-
- {parseQueryResults?.tables.map((x) => {
- const { schema, table, tablePolicies, isRLSEnabled } = x
- return (
-
- )
- })}
-
- )}
-
-
-
-
-
-
- {results.length > 0 && (
-
- {results.length} row{results.length > 1 ? 's' : ''}
- {autoLimit && results.length >= limit && ` (Limited to only ${limit} rows)`}
-
- )}
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts
deleted file mode 100644
index 5f0e43025c6..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterResults.utils.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { ParseQueryResults } from './RLSTester.types'
-
-export function deriveRLSTestState(parseQueryResults: ParseQueryResults | undefined) {
- const isServiceRole = parseQueryResults?.role === undefined
- const tableWithRLSEnabledButNoPolicies = parseQueryResults?.tables.find(
- (x) => x.isRLSEnabled && x.tablePolicies.length === 0
- )
- const tableWithRLSEnabledWithPolicyFalse = parseQueryResults?.tables.find(
- (x) => x.isRLSEnabled && x.tablePolicies.some((y) => y.definition === 'false')
- )
- const tableWithRLSEnabledWithPoliciesDontApply = parseQueryResults?.tables.find(
- (x) => x.isRLSEnabled && x.tablePolicies.length !== 0
- )
-
- const noAccessToData =
- !isServiceRole && (!!tableWithRLSEnabledButNoPolicies || !!tableWithRLSEnabledWithPolicyFalse)
-
- return {
- isServiceRole,
- tableWithRLSEnabledButNoPolicies,
- tableWithRLSEnabledWithPolicyFalse,
- tableWithRLSEnabledWithPoliciesDontApply,
- noAccessToData,
- }
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx b/apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx
deleted file mode 100644
index d5bcc5ed0b7..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RLSTesterSheet.tsx
+++ /dev/null
@@ -1,387 +0,0 @@
-import {
- acceptUntrustedSql,
- safeSql,
- type SafeSqlFragment,
- type UntrustedSqlFragment,
-} from '@supabase/pg-meta'
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from '@ui/components/shadcn/ui/select'
-import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
-import { Code, ExternalLink } from 'lucide-react'
-import { useEffect, useRef, useState } from 'react'
-import {
- Button,
- DialogSectionSeparator,
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetSection,
- SheetTitle,
- SheetTrigger,
-} from 'ui'
-import { Admonition } from 'ui-patterns/admonition'
-import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
-import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
-
-import { InferredSQLViewer } from './InferredSQLViewer'
-import { type ParseQueryResults } from './RLSTester.types'
-import { RLSTesterEmptyState } from './RLSTesterEmptyState'
-import { RLSTesterResults } from './RLSTesterResults'
-import { RoleSelector } from './RoleSelector'
-import { SandboxManagement } from './SandboxManagement'
-import { UserSelector } from './UserSelector'
-import { UserSqlEditor } from './UserSqlEditor'
-import { useTestQueryRLS, type TestQueryBlockedReason } from './useTestQueryRLS'
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
-import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
-import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge'
-import { useTrack } from '@/lib/telemetry/track'
-import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
-import { PostgresSandboxProvider, usePostgresSandbox } from '@/state/postgres-sandbox/sandbox'
-import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
-import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
-
-interface RLSTesterSheetProps {
- handleSelectEditPolicy: (policy: Policy) => void
-}
-
-export const RLSTesterSheet = (props: RLSTesterSheetProps) => {
- return (
-
-
-
- )
-}
-
-const RLSTesterSheetContents = ({ handleSelectEditPolicy }: RLSTesterSheetProps) => {
- const track = useTrack()
- const aiSnap = useAiAssistantStateSnapshot()
- const { openSidebar } = useSidebarManagerSnapshot()
- const { setRole } = useRoleImpersonationStateSnapshot()
- const { startSandbox, status, isSyncing } = usePostgresSandbox()
-
- const sandboxEnabled = useFlag('rlsTesterSandbox')
- const sandboxIsStarting = status === 'loading'
-
- const [open, setOpen] = useState(false)
- const [selectedOption, setSelectedOption] = useState<'anon' | 'authenticated'>('anon')
- const [blockedReason, setBlockedReason] = useState()
-
- const [format, setFormat] = useState<'sql' | 'lib'>('sql')
- const [inferredSQL, setInferredSQL] = useState()
-
- const [value, setValue] = useState(safeSql``)
- const [results, setResults] = useState(null)
- const [autoLimit, setAutoLimit] = useState(false)
- const [parseQueryResults, setParseQueryResults] = useState()
-
- const {
- testQuery,
- inferSQLFromLib,
- isLoading,
- isInferring,
- executeSqlError,
- parseQueryError,
- parseClientCodeError,
- } = useTestQueryRLS()
- const isErrorDueToRLS =
- executeSqlError?.message.includes('violates row-level security policy') ?? false
- const mutationOperation = blockedReason?.type === 'mutation' ? blockedReason.operation : undefined
-
- const debounceRef = useRef | null>(null)
-
- const handleValueChange = (sql: SafeSqlFragment) => {
- setValue(sql)
- if (format !== 'lib') return
-
- if (debounceRef.current !== null) clearTimeout(debounceRef.current)
- if (!sql) return
-
- debounceRef.current = setTimeout(() => inferSQLFromLib(sql, setInferredSQL), 1500)
- }
-
- const executionCallbacks = {
- option: selectedOption,
- acknowledgeMutation: blockedReason?.type === 'mutation',
- onExecuteSQL: ({ result, isAutoLimit }: { result: Object[] | null; isAutoLimit: boolean }) => {
- setResults(result)
- setAutoLimit(isAutoLimit)
- },
- onParseQuery: setParseQueryResults,
- onValidationBlocked: setBlockedReason,
- }
-
- const onRunQuery = async () => {
- setBlockedReason(undefined)
-
- if (format === 'lib') {
- if (!inferredSQL) return
- const blocked = await testQuery({
- value: acceptUntrustedSql(inferredSQL),
- ...executionCallbacks,
- })
- if (!blocked) track('rls_tester_run_query_clicked', { type: 'inferred' })
- } else {
- const blocked = await testQuery({ value, ...executionCallbacks })
- if (!blocked) track('rls_tester_run_query_clicked', { type: 'raw' })
- }
- }
-
- const assistantSql = format === 'lib' && inferredSQL ? acceptUntrustedSql(inferredSQL) : value
-
- const getDebugPrompt = ({ includeSql = false }: { includeSql?: boolean } = {}) => {
- const prompt = `Help me fix my RLS policy based on the attached SQL snippet that gave the following error: \n\n${executeSqlError?.message}\n\nEvaluate if the problem might be query first, before checking my RLS policies.`
-
- return includeSql ? `${prompt}\n\nSQL Query:\n\`\`\`sql\n${assistantSql}\n\`\`\`` : prompt
- }
-
- const onDebugWithAssistant = () => {
- const prompt = getDebugPrompt()
- openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
- aiSnap.newChat({
- name: 'Debug RLS policies',
- sqlSnippets: [assistantSql],
- initialInput: prompt,
- })
- setOpen(false)
- }
-
- useEffect(() => {
- setRole({ type: 'postgrest', role: 'anon' })
- return () => {
- // Flip back to service role
- setRole(undefined)
- }
- // [Joshen] Intentional - to only reset back to service role when navigating away
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- return (
- <>
-
-
- }>
- Test
-
-
-
-
-
-
- What data can my users access?
-
-
-
- See what data a user is allowed to read or modify based on your RLS policies
-
-
-
-
- {sandboxEnabled &&
}
-
-
-
-
- {selectedOption === 'authenticated' && }
-
-
-
-
-
-
Query
-
- {
- const newFormat = x as 'sql' | 'lib'
- setFormat(newFormat)
- if (newFormat !== 'lib') {
- setInferredSQL(undefined)
- if (debounceRef.current !== null) clearTimeout(debounceRef.current)
- }
- }}
- >
-
-
-
-
-
- Query format
- SQL
- Client library
-
-
-
-
-
-
-
- {
- if (!isInferring && !isLoading) onRunQuery()
- },
- },
- }}
- />
-
-
-
- {format === 'lib' && (
-
-
-
-
- )}
-
-
-
- {blockedReason?.type === 'multiple-statements' ? (
-
- ) : blockedReason?.type === 'unsupported-operation' ? (
-
- ) : parseQueryError ? (
-
- ) : parseClientCodeError ? (
-
- ) : executeSqlError && !isErrorDueToRLS ? (
-
-
getDebugPrompt({ includeSql: true })}
- onOpenAssistant={onDebugWithAssistant}
- />,
- ]}
- />
-
- ) : isLoading ? (
-
-
-
- ) : results === null && !isErrorDueToRLS ? (
-
- ) : !!parseQueryResults ? (
-
- ) : null}
-
-
-
- }>
-
- Give feedback
-
-
-
- setOpen(false)}>
- Cancel
-
-
- Run query
-
-
-
-
-
-
- setBlockedReason(undefined)}
- alert={{
- title: `This ${mutationOperation} query will run against your actual database`,
- description: 'Your database may be directly modified as a result. Are you sure?',
- }}
- >
- {sandboxEnabled && (
- <>
-
- We highly recommend using the sandbox to set up an ephemeral database environment for
- testing insert, update, or delete queries.
-
- {
- startSandbox()
- setBlockedReason(undefined)
- }}
- >
- Set up sandbox
-
- >
- )}
-
- >
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx b/apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx
deleted file mode 100644
index 0d23453b36b..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/RoleSelector.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { RadioGroupStacked, RadioGroupStackedItem } from 'ui'
-import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
-
-import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
-
-interface RoleSelectorProps {
- onSelectRole: (value: 'anon' | 'authenticated') => void
-}
-
-export const RoleSelector = ({ onSelectRole }: RoleSelectorProps) => {
- const { role, setRole } = useRoleImpersonationStateSnapshot()
-
- return (
-
-
- {
- onSelectRole('anon')
- setRole({ type: 'postgrest', role: 'anon' })
- }}
- />
- {
- onSelectRole('authenticated')
- }}
- />
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx b/apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx
deleted file mode 100644
index 1200d4d361e..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/SandboxManagement.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Box, Loader2, LogOut, RefreshCw } from 'lucide-react'
-import { Badge, Button } from 'ui'
-import { Admonition } from 'ui-patterns/admonition'
-
-import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
-import { usePostgresSandbox } from '@/state/postgres-sandbox/sandbox'
-
-export const SandboxManagement = () => {
- const { status, error, isSyncing, startSandbox, destroySandbox, syncSandbox } =
- usePostgresSandbox()
-
- if (status === 'idle') {
- return (
- startSandbox()}>
- Set up sandbox
- ,
- ]}
- >
-
-
Run queries in a sandbox
-
Recommended
-
-
- Ensure that queries do not affect your actual database
-
-
- )
- }
-
- if (status === 'loading') {
- return (
-
-
-
-
-
-
Setting up sandbox
-
-
- )
- }
-
- if (status === 'error') {
- return (
- startSandbox()}>
- Retry set up
- ,
- ]}
- />
- )
- }
-
- return (
- }
- className="w-7"
- disabled={isSyncing}
- tooltip={{ content: { side: 'bottom', text: 'Exit sandbox' } }}
- onClick={() => destroySandbox()}
- />,
- }
- className="w-7"
- loading={isSyncing}
- tooltip={{ content: { side: 'bottom', text: 'Refresh schema' } }}
- onClick={() => syncSandbox()}
- />,
- ]}
- >
-
-
-
-
-
Sandbox active
-
Your database is never modified
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx b/apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx
deleted file mode 100644
index c949c2f3fe0..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/UserSelector.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import { keepPreviousData } from '@tanstack/react-query'
-import { useDebounce } from '@uidotdev/usehooks'
-import { Check, ChevronsUpDown } from 'lucide-react'
-import { useMemo, useState } from 'react'
-import { toast } from 'sonner'
-import {
- Button,
- cn,
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- copyToClipboard,
- Popover,
- PopoverContent,
- PopoverTrigger,
- ScrollArea,
-} from 'ui'
-import { Admonition } from 'ui-patterns/admonition'
-import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
-import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
-
-import { User, useUsersInfiniteQuery } from '@/data/auth/users-infinite-query'
-import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
-import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
-import { ResponseError } from '@/types'
-
-export const UserSelector = () => {
- const { data: project } = useSelectedProjectQuery()
- const state = useRoleImpersonationStateSnapshot()
-
- const [open, setOpen] = useState(false)
- const [searchText, setSearchText] = useState('')
-
- const debouncedSearchText = useDebounce(searchText, 300)
-
- const { data, error, isSuccess, isPending, isError } = useUsersInfiniteQuery(
- {
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- keywords: debouncedSearchText.trim().toLocaleLowerCase(),
- },
- { placeholderData: keepPreviousData }
- )
- const users = useMemo(() => data?.pages.flatMap((page) => page.result) ?? [], [data?.pages])
-
- const impersonatingUser =
- state.role?.type === 'postgrest' &&
- state.role.role === 'authenticated' &&
- state.role.userType === 'native'
- ? state.role.user
- : undefined
-
- const onSelectUser = async (user: User) => {
- try {
- await state.setRole({
- type: 'postgrest',
- role: 'authenticated',
- userType: 'native',
- user,
- aal: 'aal1',
- })
- } catch (error) {
- toast.error(`Failed to impersonate user: ${(error as ResponseError).message}`)
- }
- }
-
- return (
-
- ID:{' '}
- {
- copyToClipboard(impersonatingUser?.id ?? '')
- toast('Copied ID to clipboard')
- }}
- >
- {impersonatingUser.id}
-
-
- ) : undefined
- }
- >
-
-
- }
- >
- {impersonatingUser?.email ?? 'Select a user'}
-
-
-
-
-
-
- {isError ? (
-
- Failed to fetch users: {error.message}
-
- ) : (
- No user found
- )}
-
-
- {isPending && (
-
-
-
- )}
-
- {isSuccess && (
-
- 7 ? 'h-full md:h-[210px]' : ''}>
- {users.map((user) => {
- return (
- {
- onSelectUser(user)
- setOpen(false)
- }}
- >
-
-
- {user.email}
-
- {user.id?.slice(0, 8)}
-
-
- {impersonatingUser?.id === user.id &&
}
-
-
- )
- })}
-
-
- )}
-
-
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx b/apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx
deleted file mode 100644
index f920731f49d..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/UserSqlEditor.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { rawSql, type SafeSqlFragment } from '@supabase/pg-meta'
-import type { ComponentProps } from 'react'
-
-import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
-
-interface UserSqlEditorProps {
- id: string
- value: SafeSqlFragment
- placeholder?: SafeSqlFragment
- actions?: ComponentProps['actions']
- onChange: (sql: SafeSqlFragment) => void
-}
-
-/**
- * Wraps CodeEditor for user-authored SQL. The rawSql boundary lives here — any
- * text the user types is immediately promoted to SafeSqlFragment so callers
- * never handle plain strings.
- */
-export const UserSqlEditor = ({ value, onChange, ...props }: UserSqlEditorProps) => {
- return (
- onChange(rawSql(val ?? ''))}
- {...props}
- />
- )
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx b/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx
deleted file mode 100644
index 8ee7cda9bc8..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.test.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-import type { SafeSqlFragment } from '@supabase/pg-meta'
-import { screen } from '@testing-library/react'
-import { describe, expect, it, vi } from 'vitest'
-
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import type { ParseQueryResults } from '@/components/interfaces/Database/RLSTester/RLSTester.types'
-import { RLSTesterResults } from '@/components/interfaces/Database/RLSTester/RLSTesterResults'
-import { render } from '@/tests/helpers'
-
-vi.mock('@/components/interfaces/Database/RLSTester/useTestQueryRLS', () => ({
- useTestQueryRLS: () => ({ limit: 100 }),
-}))
-
-vi.mock('@/components/interfaces/Database/RLSTester/RLSTableCard', () => ({
- RLSTableCard: () =>
,
-}))
-
-vi.mock('@/components/interfaces/SQLEditor/UtilityPanel/Results', () => ({
- Results: () =>
,
-}))
-
-const sql = (s: string) => s as unknown as SafeSqlFragment
-
-const makePolicy = (definition: string | null = null): Policy =>
- ({ definition: definition !== null ? sql(definition) : null }) as Policy
-
-const makeTable = (
- overrides?: Partial
-): ParseQueryResults['tables'][number] => ({
- schema: 'public',
- table: 'items',
- isRLSEnabled: true,
- tablePolicies: [],
- ...overrides,
-})
-
-const defaultProps = {
- results: [],
- autoLimit: false,
- executeSqlError: undefined,
- handleSelectEditPolicy: vi.fn(),
-}
-
-describe('RLSTesterResults', () => {
- describe('access badge', () => {
- it('shows "No access" badge when table has RLS enabled but no policies', () => {
- render(
-
- )
- expect(screen.getByText('No access')).toBeInTheDocument()
- })
-
- it('shows "No access" badge when a policy definition is false', () => {
- render(
-
- )
- expect(screen.getByText('No access')).toBeInTheDocument()
- })
-
- it('shows "Has access" badge when results are empty and user has access', () => {
- render(
-
- )
- expect(screen.getByText('Has access')).toBeInTheDocument()
- })
-
- it('shows "Can access" badge when results are returned', () => {
- render(
-
- )
- expect(screen.getByText('Can access')).toBeInTheDocument()
- })
- })
-
- describe('policy admonitions', () => {
- it('shows service role admonition for postgres role', () => {
- render(
-
- )
- expect(screen.getByText(/bypasses all RLS policies/)).toBeInTheDocument()
- })
-
- it('shows "no policies" admonition when RLS is enabled but no policies exist', () => {
- render(
-
- )
- expect(screen.getByText(/no policies set up/)).toBeInTheDocument()
- expect(screen.getByText(/public.profiles/)).toBeInTheDocument()
- })
-
- it('shows "policy false" admonition when a policy evaluates to false', () => {
- render(
-
- )
- expect(screen.getByText(/evaluates to/)).toBeInTheDocument()
- expect(screen.getByText(/public.secrets/)).toBeInTheDocument()
- })
- })
-
- describe('"Ran as" section', () => {
- it('shows postgres for service role', () => {
- render(
-
- )
- expect(screen.getAllByText('postgres').length).toBeGreaterThan(0)
- })
-
- it('shows "an Anonymous user" for anon role', () => {
- render(
-
- )
- expect(screen.getByText('an Anonymous user')).toBeInTheDocument()
- expect(screen.getByText('Not logged in user')).toBeInTheDocument()
- })
-
- it('shows user email and ID when a user is present', () => {
- render(
-
- )
- expect(screen.getByText('alice@example.com')).toBeInTheDocument()
- expect(screen.getByText('ID: user-123')).toBeInTheDocument()
- })
- })
-})
diff --git a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts b/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts
deleted file mode 100644
index 3dff5da045f..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/__tests__/RLSTesterResults.utils.test.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-import type { SafeSqlFragment } from '@supabase/pg-meta'
-import { describe, expect, it } from 'vitest'
-
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import type { ParseQueryResults } from '@/components/interfaces/Database/RLSTester/RLSTester.types'
-import { deriveRLSTestState } from '@/components/interfaces/Database/RLSTester/RLSTesterResults.utils'
-
-const sql = (s: string) => s as unknown as SafeSqlFragment
-
-const makePolicy = (definition: string | null = null): Policy =>
- ({ definition: definition !== null ? sql(definition) : null }) as Policy
-
-const makeTable = (
- overrides?: Partial
-): ParseQueryResults['tables'][number] => ({
- schema: 'public',
- table: 'items',
- isRLSEnabled: true,
- tablePolicies: [],
- ...overrides,
-})
-
-const makeResults = (overrides?: Partial): ParseQueryResults => ({
- tables: [],
- operation: 'SELECT',
- role: 'anon',
- ...overrides,
-})
-
-describe('deriveRLSTestState', () => {
- describe('isServiceRole', () => {
- it('is true when parseQueryResults is undefined', () => {
- const { isServiceRole } = deriveRLSTestState(undefined)
- expect(isServiceRole).toBe(true)
- })
-
- it('is true when role is undefined (postgres / service role)', () => {
- const { isServiceRole } = deriveRLSTestState(makeResults({ role: undefined }))
- expect(isServiceRole).toBe(true)
- })
-
- it('is false when role is anon', () => {
- const { isServiceRole } = deriveRLSTestState(makeResults({ role: 'anon' }))
- expect(isServiceRole).toBe(false)
- })
-
- it('is false when role is authenticated', () => {
- const { isServiceRole } = deriveRLSTestState(makeResults({ role: 'authenticated' }))
- expect(isServiceRole).toBe(false)
- })
- })
-
- describe('noAccessToData', () => {
- it('is false when parseQueryResults is undefined', () => {
- const { noAccessToData } = deriveRLSTestState(undefined)
- expect(noAccessToData).toBe(false)
- })
-
- it('is false for service role even when tables have no policies', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({ role: undefined, tables: [makeTable()] })
- )
- expect(noAccessToData).toBe(false)
- })
-
- it('is false when RLS is disabled on table', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ isRLSEnabled: false, tablePolicies: [] })] })
- )
- expect(noAccessToData).toBe(false)
- })
-
- it('is true when RLS is enabled and table has no policies', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ isRLSEnabled: true, tablePolicies: [] })] })
- )
- expect(noAccessToData).toBe(true)
- })
-
- it('is true when RLS is enabled and a policy definition is false', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({
- tables: [makeTable({ tablePolicies: [makePolicy('false')] })],
- })
- )
- expect(noAccessToData).toBe(true)
- })
-
- it('is false when RLS is enabled and policies are valid (not false)', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({
- tables: [makeTable({ tablePolicies: [makePolicy('auth.uid() = user_id')] })],
- })
- )
- expect(noAccessToData).toBe(false)
- })
-
- it('is false when all tables have RLS disabled regardless of policy state', () => {
- const { noAccessToData } = deriveRLSTestState(
- makeResults({
- tables: [
- makeTable({ isRLSEnabled: false, tablePolicies: [] }),
- makeTable({
- table: 'other',
- isRLSEnabled: false,
- tablePolicies: [makePolicy('false')],
- }),
- ],
- })
- )
- expect(noAccessToData).toBe(false)
- })
- })
-
- describe('tableWithRLSEnabledButNoPolicies', () => {
- it('is undefined when no tables', () => {
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(makeResults({ tables: [] }))
- expect(tableWithRLSEnabledButNoPolicies).toBeUndefined()
- })
-
- it('is undefined when RLS disabled', () => {
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ isRLSEnabled: false })] })
- )
- expect(tableWithRLSEnabledButNoPolicies).toBeUndefined()
- })
-
- it('is undefined when table has policies', () => {
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ tablePolicies: [makePolicy('true')] })] })
- )
- expect(tableWithRLSEnabledButNoPolicies).toBeUndefined()
- })
-
- it('returns the matching table when RLS enabled with no policies', () => {
- const table = makeTable({ table: 'profiles', tablePolicies: [] })
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(
- makeResults({ tables: [table] })
- )
- expect(tableWithRLSEnabledButNoPolicies).toEqual(table)
- })
-
- it('returns the first matching table among multiple', () => {
- const first = makeTable({ table: 'profiles', tablePolicies: [] })
- const second = makeTable({ table: 'posts', tablePolicies: [] })
- const { tableWithRLSEnabledButNoPolicies } = deriveRLSTestState(
- makeResults({ tables: [first, second] })
- )
- expect(tableWithRLSEnabledButNoPolicies).toEqual(first)
- })
- })
-
- describe('tableWithRLSEnabledWithPolicyFalse', () => {
- it('is undefined when no tables', () => {
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(makeResults({ tables: [] }))
- expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined()
- })
-
- it('is undefined when RLS disabled even with false policy', () => {
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(
- makeResults({
- tables: [makeTable({ isRLSEnabled: false, tablePolicies: [makePolicy('false')] })],
- })
- )
- expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined()
- })
-
- it('is undefined when no policy has definition of false', () => {
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(
- makeResults({
- tables: [makeTable({ tablePolicies: [makePolicy('auth.uid() = user_id')] })],
- })
- )
- expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined()
- })
-
- it('returns the table when a policy definition is exactly "false"', () => {
- const table = makeTable({ table: 'secrets', tablePolicies: [makePolicy('false')] })
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(
- makeResults({ tables: [table] })
- )
- expect(tableWithRLSEnabledWithPolicyFalse).toEqual(table)
- })
-
- it('is undefined when policy definition is null (no definition)', () => {
- const { tableWithRLSEnabledWithPolicyFalse } = deriveRLSTestState(
- makeResults({ tables: [makeTable({ tablePolicies: [makePolicy(null)] })] })
- )
- expect(tableWithRLSEnabledWithPolicyFalse).toBeUndefined()
- })
- })
-})
diff --git a/apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts b/apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts
deleted file mode 100644
index a5ccadf1c6d..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/__tests__/useTestQueryRLS.utils.test.ts
+++ /dev/null
@@ -1,265 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import {
- filterTablePolicies,
- getTestQueryBlockedReason,
-} from '@/components/interfaces/Database/RLSTester/useTestQueryRLS.utils'
-
-const makePolicy = (overrides: Partial): Policy =>
- ({
- schema: 'public',
- table: 'items',
- roles: ['anon'],
- command: 'SELECT',
- ...overrides,
- }) as Policy
-
-const base = {
- policies: [] as Policy[],
- schema: 'public',
- table: 'items',
- role: 'anon',
- operation: 'SELECT' as const,
-}
-
-describe('filterTablePolicies', () => {
- describe('schema / table matching', () => {
- it('excludes policies from a different schema', () => {
- const policy = makePolicy({ schema: 'private', table: 'items' })
- expect(filterTablePolicies({ ...base, policies: [policy] })).toHaveLength(0)
- })
-
- it('excludes policies from a different table', () => {
- const policy = makePolicy({ schema: 'public', table: 'other' })
- expect(filterTablePolicies({ ...base, policies: [policy] })).toHaveLength(0)
- })
-
- it('includes a policy matching schema and table', () => {
- const policy = makePolicy({ schema: 'public', table: 'items' })
- expect(filterTablePolicies({ ...base, policies: [policy] })).toHaveLength(1)
- })
- })
-
- describe('role matching', () => {
- it('includes policy when role is in the policy roles array', () => {
- const policy = makePolicy({ roles: ['anon', 'authenticated'] })
- expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(1)
- })
-
- it('excludes policy when role is not in the policy roles array', () => {
- const policy = makePolicy({ roles: ['authenticated'] })
- expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(0)
- })
-
- it('includes policy when the only role is "public" (applies to all roles)', () => {
- const policy = makePolicy({ roles: ['public'] })
- expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(1)
- })
-
- it('excludes "public" role shortcut when policy has multiple roles including public', () => {
- const policy = makePolicy({ roles: ['public', 'authenticated'] })
- expect(filterTablePolicies({ ...base, role: 'anon', policies: [policy] })).toHaveLength(0)
- })
-
- it('handles undefined role (service role) — matches nothing unless public', () => {
- const rolePolicy = makePolicy({ roles: ['anon'] })
- const publicPolicy = makePolicy({ roles: ['public'] })
- const result = filterTablePolicies({
- ...base,
- role: undefined,
- policies: [rolePolicy, publicPolicy],
- })
- expect(result).toHaveLength(1)
- expect(result[0]).toBe(publicPolicy)
- })
- })
-
- describe('command matching', () => {
- it('includes policy when command matches the operation', () => {
- const policy = makePolicy({ command: 'SELECT' })
- expect(
- filterTablePolicies({ ...base, operation: 'SELECT', policies: [policy] })
- ).toHaveLength(1)
- })
-
- it('excludes policy when command does not match the operation', () => {
- const policy = makePolicy({ command: 'INSERT' })
- expect(
- filterTablePolicies({ ...base, operation: 'SELECT', policies: [policy] })
- ).toHaveLength(0)
- })
-
- it('includes policy with command ALL regardless of operation', () => {
- const policy = makePolicy({ command: 'ALL' })
- expect(
- filterTablePolicies({ ...base, operation: 'SELECT', policies: [policy] })
- ).toHaveLength(1)
- })
-
- it('includes ALL command policy for non-SELECT operations too', () => {
- const policy = makePolicy({ command: 'ALL' })
- expect(
- filterTablePolicies({ ...base, operation: 'INSERT', policies: [policy] })
- ).toHaveLength(1)
- })
-
- it('does not include a SELECT-only policy when operation is INSERT', () => {
- const policy = makePolicy({ command: 'SELECT' })
- expect(
- filterTablePolicies({ ...base, operation: 'INSERT', policies: [policy] })
- ).toHaveLength(0)
- })
- })
-
- describe('combined filters', () => {
- it('returns only policies that satisfy all conditions', () => {
- const match = makePolicy({
- schema: 'public',
- table: 'items',
- roles: ['anon'],
- command: 'ALL',
- })
- const wrongSchema = makePolicy({
- schema: 'private',
- table: 'items',
- roles: ['anon'],
- command: 'ALL',
- })
- const wrongRole = makePolicy({
- schema: 'public',
- table: 'items',
- roles: ['authenticated'],
- command: 'ALL',
- })
- const wrongCommand = makePolicy({
- schema: 'public',
- table: 'items',
- roles: ['anon'],
- command: 'INSERT',
- })
-
- const result = filterTablePolicies({
- ...base,
- policies: [match, wrongSchema, wrongRole, wrongCommand],
- })
- expect(result).toHaveLength(1)
- expect(result[0]).toBe(match)
- })
- })
-})
-
-describe('getTestQueryBlockedReason', () => {
- const blockedBase = {
- statementCount: 1,
- operation: 'SELECT' as const,
- hasSandbox: false,
- acknowledgeMutation: false,
- }
-
- describe('multiple statements', () => {
- it('blocks when statementCount is greater than 1', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, statementCount: 2 })).toStrictEqual({
- type: 'multiple-statements',
- })
- })
-
- it('takes priority over an unsupported operation', () => {
- expect(
- getTestQueryBlockedReason({ ...blockedBase, statementCount: 2, operation: 'DELETE' })
- ).toStrictEqual({ type: 'multiple-statements' })
- })
-
- it('takes priority over an unacknowledged mutation', () => {
- expect(
- getTestQueryBlockedReason({ ...blockedBase, statementCount: 2, operation: 'INSERT' })
- ).toStrictEqual({ type: 'multiple-statements' })
- })
-
- it('does not block when statementCount is exactly 1', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, statementCount: 1 })).toBeUndefined()
- })
-
- it('does not block when statementCount is 0', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, statementCount: 0 })).toBeUndefined()
- })
- })
-
- describe('unsupported operations', () => {
- it('blocks UPDATE', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'UPDATE' })).toStrictEqual({
- type: 'unsupported-operation',
- operation: 'UPDATE',
- })
- })
-
- it('blocks DELETE', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'DELETE' })).toStrictEqual({
- type: 'unsupported-operation',
- operation: 'DELETE',
- })
- })
-
- it('blocks UPDATE even with a sandbox available', () => {
- expect(
- getTestQueryBlockedReason({ ...blockedBase, operation: 'UPDATE', hasSandbox: true })
- ).toStrictEqual({ type: 'unsupported-operation', operation: 'UPDATE' })
- })
-
- it('blocks DELETE even when already acknowledged', () => {
- expect(
- getTestQueryBlockedReason({
- ...blockedBase,
- operation: 'DELETE',
- acknowledgeMutation: true,
- })
- ).toStrictEqual({ type: 'unsupported-operation', operation: 'DELETE' })
- })
- })
-
- describe('INSERT mutation warning', () => {
- it('blocks an unacknowledged INSERT with no sandbox', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'INSERT' })).toStrictEqual({
- type: 'mutation',
- operation: 'INSERT',
- })
- })
-
- it('does not block an INSERT when a sandbox is available', () => {
- expect(
- getTestQueryBlockedReason({ ...blockedBase, operation: 'INSERT', hasSandbox: true })
- ).toBeUndefined()
- })
-
- it('does not block an INSERT once acknowledged', () => {
- expect(
- getTestQueryBlockedReason({
- ...blockedBase,
- operation: 'INSERT',
- acknowledgeMutation: true,
- })
- ).toBeUndefined()
- })
-
- it('does not require acknowledgement when a sandbox is available', () => {
- expect(
- getTestQueryBlockedReason({
- ...blockedBase,
- operation: 'INSERT',
- hasSandbox: true,
- acknowledgeMutation: false,
- })
- ).toBeUndefined()
- })
- })
-
- describe('unblocked operations', () => {
- it('does not block SELECT', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: 'SELECT' })).toBeUndefined()
- })
-
- it('does not block when operation is undefined', () => {
- expect(getTestQueryBlockedReason({ ...blockedBase, operation: undefined })).toBeUndefined()
- })
- })
-})
diff --git a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts b/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts
deleted file mode 100644
index c94328db294..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.ts
+++ /dev/null
@@ -1,261 +0,0 @@
-import { safeSql, type SafeSqlFragment, type UntrustedSqlFragment } from '@supabase/pg-meta'
-import { useState } from 'react'
-import { toast } from 'sonner'
-
-import { checkIfAppendLimitRequired, suffixWithLimit } from '../../SQLEditor/SQLEditor.utils'
-import { type ParseQueryResults } from './RLSTester.types'
-import {
- filterTablePolicies,
- getTestQueryBlockedReason,
- type TestQueryBlockedReason,
-} from './useTestQueryRLS.utils'
-import { useParseClientCodeMutation } from '@/data/ai/parse-client-code-mutation'
-import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
-import { useCheckTableRLSStatusMutation } from '@/data/database/table-check-rls-mutation'
-import {
- useParseSQLQueryMutation,
- type ParseSQLQueryOperations,
-} from '@/data/misc/parse-query-mutation'
-import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
-import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
-import { wrapWithRoleImpersonation } from '@/lib/role-impersonation'
-import { usePostgresSandbox } from '@/state/postgres-sandbox/sandbox'
-import {
- isRoleImpersonationEnabled,
- useGetImpersonatedRoleState,
- useImpersonatedUser,
- useRoleImpersonationStateSnapshot,
-} from '@/state/role-impersonation-state'
-import { type ResponseError } from '@/types'
-
-const limit = 100
-
-export type { TestQueryBlockedReason }
-
-// [Joshen] Pre-requisite work for identifying UPDATE / DELETE failures due to RLS - not yet wired
-// in since those operations are currently blocked (see TestQueryBlockedReason's 'unsupported-
-// operation'). Exported so it isn't flagged as unused until the follow-up PR wires it back in.
-export const wrapReturnRowsAffected = (sql: SafeSqlFragment) => {
- return safeSql`
- DO $$
-DECLARE
- row_count integer;
-BEGIN
- ${sql}${(sql.endsWith(';') ? '' : ';') as SafeSqlFragment}
- GET DIAGNOSTICS row_count = ROW_COUNT;
- -- store it somewhere you can read back
- PERFORM set_config('rls_tester.rows_affected', row_count::text, true);
-END $$;
-
-SELECT current_setting('rls_tester.rows_affected', true);
-`
-}
-
-/**
- * [Joshen] Testing a SQL query for its RLS access involves 3 async steps
- * 0. (Optional) Inferring client library code to SQL query via the AI Assistant
- * 1. Parsing the provided SQL query to retrieve its operation type + tables involved
- * 2. Checking for tables involved if they've got RLS enabled
- * 3. Actually running the query to retrieve the results
- *
- * Errors should all be handled as part of the UI instead of toasts, hence the empty onError
- * handlers to mute the default error handlers within the react query mutationhooks
- */
-export const useTestQueryRLS = () => {
- const { data: project } = useSelectedProjectQuery()
- const { role } = useRoleImpersonationStateSnapshot()
-
- const { sandbox } = usePostgresSandbox()
- const getImpersonatedRoleState = useGetImpersonatedRoleState()
- const impersonatedRoleState = getImpersonatedRoleState()
- const user = useImpersonatedUser()
-
- const [isLoading, setIsLoading] = useState(false)
- const [sandboxError, setSandboxError] = useState()
-
- const { data: policies = [] } = useDatabasePoliciesQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
-
- const { mutateAsync: executeSql, error: executeSqlMutationError } = useExecuteSqlMutation({
- onError: () => {},
- })
- const executeSqlError = sandbox ? sandboxError : executeSqlMutationError
-
- const {
- mutateAsync: parseClientCode,
- isPending: isInferring,
- error: parseClientCodeError,
- } = useParseClientCodeMutation({
- onError: () => {},
- })
-
- const inferSQLFromLib = async (
- value: string,
- onInferSQL: (unchecked_sql: UntrustedSqlFragment) => void
- ) => {
- const { unchecked_sql, valid } = await parseClientCode({ code: value })
- if (valid && unchecked_sql != null) {
- onInferSQL(unchecked_sql)
- } else {
- toast.error('Client library code provided is not valid')
- }
- }
-
- const { mutateAsync: parseQuery, error: parseQueryError } = useParseSQLQueryMutation({
- onError: () => {},
- })
-
- const { mutateAsync: getTableRLSStatus, error: getTableRLSStatusError } =
- useCheckTableRLSStatusMutation({
- onError: () => {},
- })
-
- /**
- * Returns true if the query was blocked (multiple statements, or an unacknowledged mutation)
- * and did not run, false if it ran (successfully or not)
- */
- const testQuery = async ({
- value,
- option,
- acknowledgeMutation = false,
- onExecuteSQL,
- onParseQuery,
- onValidationBlocked,
- }: {
- value: SafeSqlFragment
- option: 'anon' | 'authenticated'
- acknowledgeMutation?: boolean
- onExecuteSQL: ({
- result,
- operation,
- isAutoLimit,
- }: {
- result: Object[] | null
- operation: ParseSQLQueryOperations
- isAutoLimit: boolean
- }) => void
- onParseQuery: (results?: ParseQueryResults) => void
- onValidationBlocked: (reason: TestQueryBlockedReason) => void
- }): Promise => {
- if (!project) {
- console.error('Project is required')
- return true
- }
-
- if (option === 'authenticated' && !user) {
- toast('Select which user to test as before running the query')
- return true
- }
-
- try {
- setIsLoading(true)
- setSandboxError(undefined)
-
- const { appendAutoLimit } = checkIfAppendLimitRequired(value, limit)
- const formattedSql = suffixWithLimit(value, limit)
- const data = await parseQuery({ sql: formattedSql })
-
- const blockedReason = getTestQueryBlockedReason({
- statementCount: data.statementCount,
- operation: data.operation,
- hasSandbox: !!sandbox,
- acknowledgeMutation,
- })
- if (blockedReason) {
- onValidationBlocked(blockedReason)
- return true
- }
-
- const formattedTables = data.tables.map((x) => {
- const [schema, table] = x.includes('.') ? x.split('.') : ['public', x]
- return { schema, table }
- })
- const response = await getTableRLSStatus({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- tables: formattedTables,
- })
-
- const tables = response
- .map(({ table, schema, rls_enabled }) => {
- const tablePolicies = filterTablePolicies({
- policies,
- schema,
- table,
- role: role?.role,
- operation: data.operation,
- })
- return {
- table,
- schema,
- isRLSEnabled: rls_enabled,
- tablePolicies,
- }
- })
- .sort((a, b) => {
- const aFirst = a.isRLSEnabled && a.tablePolicies.length === 0
- const bFirst = b.isRLSEnabled && b.tablePolicies.length === 0
- return Number(bFirst) - Number(aFirst)
- })
-
- const autoLimit = appendAutoLimit ? limit : undefined
- // UPDATE/DELETE are blocked above, so wrapReturnRowsAffected isn't wired in here yet -
- // it's kept for the follow-up PR that adds proper UPDATE/DELETE support
- const sql = wrapWithRoleImpersonation(formattedSql, impersonatedRoleState)
-
- try {
- const { result } = sandbox
- ? await sandbox.run({ sql }).catch((e) => {
- setSandboxError(e instanceof Error ? e : new Error(String(e)))
- throw e
- })
- : await executeSql({
- sql,
- autoLimit,
- projectRef: project.ref,
- connectionString: project.connectionString,
- isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRoleState.role),
- isStatementTimeoutDisabled: true,
- handleError: (e) => {
- throw e
- },
- queryKey: ['rls-tester'],
- })
-
- onExecuteSQL({ result, operation: data.operation, isAutoLimit: !!autoLimit })
- onParseQuery({ tables, operation: data.operation, role: role?.role, user })
- } catch (error) {
- const isRLSInsertError = Boolean(
- (error as ResponseError)?.message?.includes('new row violates row-level security policy')
- )
- onExecuteSQL({ result: null, operation: data.operation, isAutoLimit: false })
- if (isRLSInsertError) {
- onParseQuery({ tables, operation: data.operation, role: role?.role, user })
- } else {
- onParseQuery(undefined)
- }
- }
- } catch (error) {
- onExecuteSQL({ result: null, operation: undefined, isAutoLimit: false })
- onParseQuery(undefined)
- } finally {
- setIsLoading(false)
- }
-
- return false
- }
-
- return {
- limit,
- testQuery,
- inferSQLFromLib,
- isLoading,
- isInferring,
- executeSqlError,
- parseQueryError,
- parseClientCodeError,
- getTableRLSStatusError,
- }
-}
diff --git a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts b/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts
deleted file mode 100644
index b0e698dc43b..00000000000
--- a/apps/studio/components/interfaces/Database/RLSTester/useTestQueryRLS.utils.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { Policy } from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import type { ParseSQLQueryResponse } from '@/data/misc/parse-query-mutation'
-
-export type TestQueryBlockedReason =
- | { type: 'multiple-statements' }
- | { type: 'unsupported-operation'; operation: 'UPDATE' | 'DELETE' }
- | { type: 'mutation'; operation: 'INSERT' }
-
-/**
- * Decides whether a query should be blocked from running, and why. Checked in this order:
- * multiple statements first (regardless of operation), then unsupported operations (UPDATE/
- * DELETE aren't testable yet - RLS blocks them silently instead of raising an error), then
- * INSERT mutations against the real database that haven't been acknowledged yet.
- */
-export function getTestQueryBlockedReason({
- statementCount,
- operation,
- hasSandbox,
- acknowledgeMutation,
-}: {
- statementCount: number
- operation: ParseSQLQueryResponse['operation']
- hasSandbox: boolean
- acknowledgeMutation: boolean
-}): TestQueryBlockedReason | undefined {
- if (statementCount > 1) return { type: 'multiple-statements' }
- if (operation === 'UPDATE' || operation === 'DELETE') {
- return { type: 'unsupported-operation', operation }
- }
- if (operation === 'INSERT' && !hasSandbox && !acknowledgeMutation) {
- return { type: 'mutation', operation }
- }
- return undefined
-}
-
-export function filterTablePolicies({
- policies,
- schema,
- table,
- role,
- operation,
-}: {
- policies: Policy[]
- schema: string
- table: string
- role: string | undefined
- operation: ParseSQLQueryResponse['operation']
-}): Policy[] {
- return policies.filter(
- (x) =>
- x.schema === schema &&
- x.table === table &&
- (x.roles.includes(role ?? '') || (x.roles.length === 1 && x.roles[0] === 'public')) &&
- (x.command === 'ALL' || x.command === operation)
- )
-}
diff --git a/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx b/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx
index b8aa2e24b7a..75a441a86e9 100644
--- a/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx
+++ b/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx
@@ -5,7 +5,6 @@ export const BANNER_ID = {
INDEX_ADVISOR: 'index-advisor-banner',
TABLE_EDITOR_QUEUE_OPERATIONS: 'table-editor-queue-operations-banner',
RLS_EVENT_TRIGGER: 'rls-event-trigger-banner',
- RLS_TESTER: 'rls-tester-banner',
FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner',
TOS_UPDATE: 'tos-update-banner',
UNIFIED_LOGS: 'unified-logs-banner',
diff --git a/apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx b/apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx
deleted file mode 100644
index 61b1dbca19d..00000000000
--- a/apps/studio/components/ui/BannerStack/Banners/BannerRlsTester.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { LOCAL_STORAGE_KEYS } from 'common'
-import { useParams } from 'common/hooks'
-import { AnimatePresence, motion } from 'framer-motion'
-import { Check, Loader2, Terminal } from 'lucide-react'
-import { useEffect, useState } from 'react'
-import { Badge, Button, cn } from 'ui'
-
-import { BannerCard } from '../BannerCard'
-import { useBannerStack } from '../BannerStackProvider'
-import { useFeaturePreviewModal } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
-import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
-
-const text = 'select * from colors'
-
-export const BannerRlsTester = () => {
- const { ref } = useParams()
- const { selectFeaturePreview } = useFeaturePreviewModal()
-
- const [runQueryAnimate, setRunQueryAnimate] = useState(false)
- const [showSummary, setShowSummary] = useState(false)
-
- const { dismissBanner } = useBannerStack()
- const [, setIsDismissed] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(ref ?? ''),
- false
- )
-
- useEffect(() => {
- setTimeout(() => setRunQueryAnimate(true), 2400)
- }, [])
-
- useEffect(() => {
- if (runQueryAnimate) {
- setTimeout(() => setShowSummary(true), 1700)
- }
- }, [runQueryAnimate])
-
- return (
- {
- setIsDismissed(true)
- dismissBanner('rls-tester-banner')
- }}
- >
-
-
-
- Preview
-
-
-
-
- {runQueryAnimate && !showSummary ? (
-
- ) : (
-
- )}
-
- {text}
-
-
-
-
- {showSummary && (
-
-
-
-
- Can access public.colors
-
-
-
-
- )}
-
-
-
-
-
Row Level Security (RLS) Tester
-
- Verify your RLS policies are correct by running queries as a specific user
-
-
-
selectFeaturePreview(LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER)}
- >
- Enable feature preview
-
-
-
- )
-}
diff --git a/apps/studio/data/rls-tester/get-schema-ddl.ts b/apps/studio/data/rls-tester/get-schema-ddl.ts
deleted file mode 100644
index 815d1d51ab4..00000000000
--- a/apps/studio/data/rls-tester/get-schema-ddl.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import pgMeta, {
- getEntityDefinitionsSql,
- joinSqlFragments,
- literal,
- safeSql,
- type PGPolicy,
-} from '@supabase/pg-meta'
-import { z } from 'zod'
-
-import { executeSql } from '@/data/sql/execute-sql-mutation'
-import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
-
-export interface RlsTableStatus {
- schema: string
- table: string
- rls_enabled: boolean
- rls_forced: boolean
-}
-
-export interface CustomRole {
- name: string
-}
-
-export interface DatabaseSchemaDDL {
- schemas: string[]
- typeDefinitions: string[]
- entityDefinitions: string[]
- functionDefinitions: string[]
- policies: PGPolicy[]
- rlsStatuses: RlsTableStatus[]
- customRoles: CustomRole[]
-}
-
-const pgMetaRolesList = pgMeta.roles.list()
-const pgMetaFunctionsZod = pgMeta.functions.list().zod
-const pgMetaPoliciesZod = pgMeta.policies.list().zod
-const pgMetaTablesZod = pgMeta.tables.list().zod
-
-// Extension-owned / platform-specific schemas whose DDL depends on C extensions,
-// custom operators, and platform functions that PGlite cannot replicate.
-// We skip entity/function/type DDL for these but still fetch their policies —
-// those may reference user tables we do load.
-const SUPABASE_INTERNAL_SCHEMAS = new Set([...INTERNAL_SCHEMAS, '_realtime'])
-
-const SYSTEM_ROLES = new Set([
- 'postgres',
- 'anon',
- 'authenticated',
- 'service_role',
- 'supabase_admin',
- 'supabase_auth_admin',
- 'supabase_storage_admin',
- 'supabase_replication_admin',
- 'supabase_read_only_user',
- 'pg_monitor',
- 'pg_read_all_settings',
- 'pg_read_all_stats',
- 'pg_stat_scan_tables',
- 'pg_read_server_files',
- 'pg_write_server_files',
- 'pg_execute_server_program',
- 'pg_signal_backend',
- 'dashboard_user',
- 'pgbouncer',
-])
-
-function getTypeDefinitionsSql(schemas: string[]) {
- return safeSql`
- SELECT
- CASE t.typtype
- WHEN 'e' THEN
- 'CREATE TYPE ' || quote_ident(n.nspname) || '.' || quote_ident(t.typname) ||
- ' AS ENUM (' ||
- (SELECT string_agg(quote_literal(e.enumlabel), ', ' ORDER BY e.enumsortorder)
- FROM pg_enum e WHERE e.enumtypid = t.oid) ||
- ')'
- WHEN 'c' THEN
- 'CREATE TYPE ' || quote_ident(n.nspname) || '.' || quote_ident(t.typname) ||
- ' AS (' ||
- (SELECT string_agg(quote_ident(a.attname) || ' ' || pg_catalog.format_type(a.atttypid, a.atttypmod), ', ' ORDER BY a.attnum)
- FROM pg_attribute a WHERE a.attrelid = t.typrelid AND a.attnum > 0 AND NOT a.attisdropped) ||
- ')'
- WHEN 'd' THEN
- 'CREATE DOMAIN ' || quote_ident(n.nspname) || '.' || quote_ident(t.typname) ||
- ' AS ' || pg_catalog.format_type(t.typbasetype, t.typtypmod)
- END AS definition
- FROM pg_type t
- JOIN pg_namespace n ON n.oid = t.typnamespace
- LEFT JOIN pg_class c ON c.oid = t.typrelid
- LEFT JOIN pg_depend d ON d.objid = t.oid AND d.deptype = 'e'
- WHERE n.nspname IN (${joinSqlFragments(schemas.map(literal), ', ')})
- AND t.typtype IN ('e', 'c', 'd')
- AND d.objid IS NULL
- AND (t.typtype != 'c' OR c.relkind = 'c')
- ORDER BY t.typtype, n.nspname, t.typname
- `
-}
-
-type Variables = {
- projectRef?: string
- connectionString?: string | null
- schemas: string[]
-}
-
-export async function getDatabaseSchemaDDL(
- { projectRef, connectionString, schemas }: Variables,
- signal?: AbortSignal
-): Promise {
- const userSchemas = schemas.filter((s) => !SUPABASE_INTERNAL_SCHEMAS.has(s))
-
- const entitySql = getEntityDefinitionsSql({ schemas: userSchemas })
- const functionsSql = pgMeta.functions.list({ includedSchemas: userSchemas }).sql
- const policiesSql = pgMeta.policies.list({ includedSchemas: schemas }).sql
- const tablesSql = pgMeta.tables.list({ includedSchemas: userSchemas }).sql
-
- const [entityResult, policiesResult, rlsResult, rolesResult, functionsResult, typesResult] =
- await Promise.all([
- executeSql(
- { projectRef, connectionString, sql: entitySql, queryKey: ['rls-sandbox-ddl'] },
- signal
- ),
- executeSql(
- { projectRef, connectionString, sql: policiesSql, queryKey: ['rls-sandbox-policies'] },
- signal
- ),
- executeSql(
- { projectRef, connectionString, sql: tablesSql, queryKey: ['rls-sandbox-rls'] },
- signal
- ),
- executeSql(
- { projectRef, connectionString, sql: pgMetaRolesList.sql, queryKey: ['rls-sandbox-roles'] },
- signal
- ),
- executeSql(
- {
- projectRef,
- connectionString,
- sql: functionsSql,
- queryKey: ['rls-sandbox-functions'],
- },
- signal
- ),
- executeSql(
- {
- projectRef,
- connectionString,
- sql: getTypeDefinitionsSql(userSchemas),
- queryKey: ['rls-sandbox-types'],
- },
- signal
- ),
- ])
-
- const roles = (rolesResult.result as z.infer).filter(
- (r) => !SYSTEM_ROLES.has(r.name) && !r.name.startsWith('pg_') && !r.name.startsWith('supabase_')
- )
-
- const functions = (functionsResult.result as z.infer).filter(
- (f) => (f.language === 'sql' || f.language === 'plpgsql') && f.return_type !== 'trigger'
- )
-
- return {
- schemas: userSchemas,
- typeDefinitions: (typesResult.result as { definition: string }[]).map((r) => r.definition),
- entityDefinitions: (entityResult.result[0]?.data?.definitions ?? []).map(
- (d: { sql: string }) => d.sql
- ),
- functionDefinitions: functions.map((f) => f.complete_statement),
- policies: policiesResult.result as z.infer as PGPolicy[],
- rlsStatuses: (rlsResult.result as z.infer).map((t) => ({
- schema: t.schema,
- table: t.name,
- rls_enabled: t.rls_enabled,
- rls_forced: t.rls_forced,
- })),
- customRoles: roles,
- }
-}
-
-export type DatabaseSchemaDDLData = Awaited>
diff --git a/apps/studio/data/rls-tester/get-seed-data.ts b/apps/studio/data/rls-tester/get-seed-data.ts
deleted file mode 100644
index 1d4dd3318df..00000000000
--- a/apps/studio/data/rls-tester/get-seed-data.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { ident, joinSqlFragments, literal, safeSql } from '@supabase/pg-meta'
-
-import { RlsTableStatus } from './get-schema-ddl'
-import { executeSql } from '@/data/sql/execute-sql-mutation'
-
-export interface TableSeedData {
- schema: string
- table: string
- rows: Record[]
-}
-
-// Each entry can optionally restrict which columns are fetched. Used by the
-// sandbox to avoid pulling secrets (e.g. auth.users encrypted_password / tokens)
-// into the browser-side PGlite instance.
-export type SeedTableEntry = RlsTableStatus & { columns?: readonly string[] }
-
-type Variables = {
- projectRef?: string
- connectionString?: string | null
- tables: SeedTableEntry[]
- rowLimit: number
-}
-
-async function fetchTableSeed(
- {
- projectRef,
- connectionString,
- schema,
- table,
- columns,
- rowLimit,
- }: Omit & {
- schema: string
- table: string
- columns?: readonly string[]
- },
- signal?: AbortSignal
-): Promise {
- try {
- const projection =
- columns && columns.length > 0 ? joinSqlFragments(columns.map(ident), ', ') : safeSql`*`
- const { result } = await executeSql(
- {
- projectRef,
- connectionString,
- sql: safeSql`SELECT ${projection} FROM ${ident(schema)}.${ident(table)} LIMIT ${literal(Number(rowLimit))}`,
- queryKey: ['rls-sandbox-seed', schema, table],
- },
- signal
- )
- return { schema, table, rows: (result ?? []) as Record[] }
- } catch {
- return { schema, table, rows: [] }
- }
-}
-
-const SEED_CONCURRENCY = 8
-
-export async function getProjectSeedData(
- { projectRef, connectionString, tables, rowLimit }: Variables,
- signal?: AbortSignal
-): Promise {
- const results: TableSeedData[] = []
- const queue = tables.slice()
- const workers = Array.from({ length: Math.min(SEED_CONCURRENCY, queue.length) }, async () => {
- while (queue.length > 0) {
- const entry = queue.shift()
- if (!entry) break
- results.push(
- await fetchTableSeed(
- {
- projectRef,
- connectionString,
- schema: entry.schema,
- table: entry.table,
- columns: entry.columns,
- rowLimit,
- },
- signal
- )
- )
- }
- })
- await Promise.all(workers)
- return results.filter((t) => t.rows.length > 0)
-}
-
-export type ProjectSeedDataData = TableSeedData[]
diff --git a/apps/studio/package.json b/apps/studio/package.json
index 442d76c48d2..547bc0a3c54 100644
--- a/apps/studio/package.json
+++ b/apps/studio/package.json
@@ -51,8 +51,6 @@
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
- "@electric-sql/pglite": "0.4.5",
- "@electric-sql/pglite-tools": "^0.3.4",
"@graphiql/react": "^0.37.3",
"@graphiql/toolkit": "^0.11.3",
"@hcaptcha/react-hcaptcha": "^1.12.0",
diff --git a/apps/studio/pages/project/[ref]/database/policies.tsx b/apps/studio/pages/project/[ref]/database/policies.tsx
index 766d3ac6afa..34823209990 100644
--- a/apps/studio/pages/project/[ref]/database/policies.tsx
+++ b/apps/studio/pages/project/[ref]/database/policies.tsx
@@ -1,5 +1,5 @@
-import { ident, safeSql } from '@supabase/pg-meta'
import type { PGTable } from '@supabase/pg-meta'
+import { ident, safeSql } from '@supabase/pg-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { Search, X } from 'lucide-react'
@@ -21,7 +21,6 @@ import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { useIsInlineEditorEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings'
-import { useIsRLSTesterEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { Policies } from '@/components/interfaces/Database/Policies/Policies'
import { getGeneralPolicyTemplates } from '@/components/interfaces/Database/Policies/Policies.constants'
import { PoliciesDataProvider } from '@/components/interfaces/Database/Policies/PoliciesDataContext'
@@ -30,15 +29,12 @@ import {
generatePolicyUpdateSQL,
type Policy,
} from '@/components/interfaces/Database/Policies/PolicyTableRow/PolicyTableRow.utils'
-import { RLSTesterSheet } from '@/components/interfaces/Database/RLSTester/RLSTesterSheet'
import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { getExposedSchemas } from '@/components/layouts/ProjectNeedsSecuring/ProjectNeedsSecuring.utils'
import { AlertError } from '@/components/ui/AlertError'
import { AutoEnableRLSNotice } from '@/components/ui/AutoEnableRLSNotice'
-import { BannerRlsTester } from '@/components/ui/BannerStack/Banners/BannerRlsTester'
-import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
import { DocsButton } from '@/components/ui/DocsButton'
import { NoPermission } from '@/components/ui/NoPermission'
import { SchemaSelector } from '@/components/ui/SchemaSelector'
@@ -121,7 +117,6 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
)
const isInlineEditorEnabled = useIsInlineEditorEnabled()
- const rlsTesterEnabled = useIsRLSTesterEnabled()
const { openSidebar } = useSidebarManagerSnapshot()
const {
@@ -150,18 +145,12 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => setSearchString(''))
const { isSchemaLocked } = useIsProtectedSchema({ schema: schema, excludedSchemas: ['realtime'] })
- const { addBanner, dismissBanner } = useBannerStack()
const [isAutoEnableRLSMinimized] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.RLS_EVENT_TRIGGER_BANNER_DISMISSED(projectRef ?? ''),
false
)
- const [isRlsTesterBannerDismissed] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.RLS_TESTER_BANNER_DISMISSED(projectRef ?? ''),
- false
- )
-
const {
data: policies = [],
isPending: isLoadingPolicies,
@@ -249,25 +238,6 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
const handleResetSearch = useCallback(() => setSearchString(''), [setSearchString])
- useEffect(() => {
- if (rlsTesterEnabled) return
-
- if (!isRlsTesterBannerDismissed) {
- addBanner({
- id: 'rls-tester-banner',
- isDismissed: false,
- content: ,
- priority: 3,
- })
- } else {
- dismissBanner('rls-tester-banner')
- }
-
- return () => {
- dismissBanner('rls-tester-banner')
- }
- }, [addBanner, dismissBanner, isRlsTesterBannerDismissed, rlsTesterEnabled])
-
useEffect(() => {
if (selectedIdToEdit && isPoliciesSuccess && !selectedPolicyToEdit) {
toast(`Policy ID ${selectedIdToEdit} cannot be found`)
@@ -294,7 +264,6 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
{isAutoEnableRLSMinimized && }
- {rlsTesterEnabled && }
@@ -343,6 +312,7 @@ const DatabasePoliciesPage: NextPageWithLayout = () => {
}
onClick={() => setSearchString('')}
diff --git a/apps/studio/public/img/previews/rls-tester-preview.png b/apps/studio/public/img/previews/rls-tester-preview.png
deleted file mode 100644
index c21ed334276..00000000000
Binary files a/apps/studio/public/img/previews/rls-tester-preview.png and /dev/null differ
diff --git a/apps/studio/state/postgres-sandbox/pglite.worker.ts b/apps/studio/state/postgres-sandbox/pglite.worker.ts
deleted file mode 100644
index ab08903f83d..00000000000
--- a/apps/studio/state/postgres-sandbox/pglite.worker.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { PGlite } from '@electric-sql/pglite'
-import { pgcrypto } from '@electric-sql/pglite/contrib/pgcrypto'
-import { uuid_ossp } from '@electric-sql/pglite/contrib/uuid_ossp'
-import { worker } from '@electric-sql/pglite/worker'
-
-worker({
- async init() {
- return new PGlite({
- dataDir: 'memory://',
- extensions: { pgcrypto, uuid_ossp },
- })
- },
-})
diff --git a/apps/studio/state/postgres-sandbox/sandbox.constants.ts b/apps/studio/state/postgres-sandbox/sandbox.constants.ts
deleted file mode 100644
index 91e9f667507..00000000000
--- a/apps/studio/state/postgres-sandbox/sandbox.constants.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-// ALTER ROLE postgres SUPERUSER succeeds because the bootstrap connection owns the cluster.
-// Each statement is individual so a single failure cannot abort the rest.
-export const SANDBOX_SETUP_STATEMENTS = [
- `ALTER ROLE postgres SUPERUSER`,
- `DO $$ BEGIN
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anon') THEN
- CREATE ROLE anon NOLOGIN;
- END IF;
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticated') THEN
- CREATE ROLE authenticated NOLOGIN;
- END IF;
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'service_role') THEN
- CREATE ROLE service_role NOLOGIN;
- END IF;
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticator') THEN
- CREATE ROLE authenticator NOLOGIN;
- END IF;
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'dashboard_user') THEN
- CREATE ROLE dashboard_user NOLOGIN;
- END IF;
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgbouncer') THEN
- CREATE ROLE pgbouncer NOLOGIN;
- END IF;
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'supabase_admin') THEN
- CREATE ROLE supabase_admin NOLOGIN;
- END IF;
- END $$`,
- `ALTER ROLE service_role BYPASSRLS`,
- `GRANT anon TO postgres WITH ADMIN OPTION`,
- `GRANT authenticated TO postgres WITH ADMIN OPTION`,
- `GRANT service_role TO postgres WITH ADMIN OPTION`,
- `GRANT CONNECT ON DATABASE postgres TO anon, authenticated, service_role`,
- `CREATE SCHEMA IF NOT EXISTS auth`,
- `GRANT USAGE ON SCHEMA auth TO anon, authenticated, service_role`,
- `GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role`,
- // Read both the per-claim setting (request.jwt.claim.) and the JSON blob
- // (request.jwt.claims) so these work whether the caller uses Studio's role
- // impersonation (sets the JSON blob) or PostgREST-style per-claim settings.
- // Mirrors how the real Supabase auth.* helpers are defined.
- `CREATE OR REPLACE FUNCTION auth.uid() RETURNS uuid LANGUAGE sql STABLE AS
- $fn$ SELECT COALESCE(
- NULLIF(current_setting('request.jwt.claim.sub', true), ''),
- (NULLIF(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
- )::uuid $fn$`,
- `CREATE OR REPLACE FUNCTION auth.role() RETURNS text LANGUAGE sql STABLE AS
- $fn$ SELECT COALESCE(
- NULLIF(current_setting('request.jwt.claim.role', true), ''),
- (NULLIF(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role'),
- 'anon'
- ) $fn$`,
- `CREATE OR REPLACE FUNCTION auth.email() RETURNS text LANGUAGE sql STABLE AS
- $fn$ SELECT COALESCE(
- NULLIF(current_setting('request.jwt.claim.email', true), ''),
- (NULLIF(current_setting('request.jwt.claims', true), '')::jsonb ->> 'email')
- ) $fn$`,
- `GRANT EXECUTE ON FUNCTION auth.uid() TO anon, authenticated, service_role`,
- `GRANT EXECUTE ON FUNCTION auth.role() TO anon, authenticated, service_role`,
- `GRANT EXECUTE ON FUNCTION auth.email() TO anon, authenticated, service_role`,
- // Minimal auth table stubs — enough for FK references and policy expressions.
- // Projects commonly have FKs to auth.users from public schema tables (e.g. profiles),
- // so without this stub those tables fail to create and their policies can't be tested.
- `CREATE TABLE IF NOT EXISTS auth.users (
- instance_id uuid,
- id uuid NOT NULL PRIMARY KEY,
- aud varchar(255),
- role varchar(255),
- email varchar(255),
- encrypted_password varchar(255),
- email_confirmed_at timestamptz,
- invited_at timestamptz,
- confirmation_token varchar(255),
- confirmation_sent_at timestamptz,
- recovery_token varchar(255),
- recovery_sent_at timestamptz,
- email_change_token_new varchar(255),
- email_change varchar(255),
- email_change_sent_at timestamptz,
- last_sign_in_at timestamptz,
- raw_app_meta_data jsonb,
- raw_user_meta_data jsonb,
- is_super_admin boolean,
- created_at timestamptz,
- updated_at timestamptz,
- phone text DEFAULT NULL,
- phone_confirmed_at timestamptz,
- phone_change text DEFAULT '',
- phone_change_token varchar(255) DEFAULT '',
- phone_change_sent_at timestamptz,
- confirmed_at timestamptz,
- email_change_token_current varchar(255) DEFAULT '',
- email_change_confirm_status smallint DEFAULT 0,
- banned_until timestamptz,
- reauthentication_token varchar(255) DEFAULT '',
- reauthentication_sent_at timestamptz,
- is_sso_user boolean NOT NULL DEFAULT false,
- deleted_at timestamptz,
- is_anonymous boolean NOT NULL DEFAULT false
- )`,
- `CREATE TABLE IF NOT EXISTS auth.sessions (
- id uuid NOT NULL PRIMARY KEY,
- user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
- created_at timestamptz,
- updated_at timestamptz,
- factor_id uuid,
- aal text,
- not_after timestamptz,
- refreshed_at timestamp,
- user_agent text,
- ip inet,
- tag text
- )`,
- `CREATE TABLE IF NOT EXISTS auth.mfa_factors (
- id uuid NOT NULL PRIMARY KEY,
- user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
- friendly_name text,
- factor_type text NOT NULL,
- status text NOT NULL,
- created_at timestamptz NOT NULL,
- updated_at timestamptz NOT NULL,
- secret text,
- phone text,
- last_challenged_at timestamptz,
- web_authn_credential jsonb,
- web_authn_aaguid uuid
- )`,
- `GRANT SELECT, INSERT, UPDATE, DELETE ON auth.users, auth.sessions, auth.mfa_factors TO anon, authenticated, service_role`,
-]
-
-// Seeded alongside public tables so FK references from public → auth.users
-// resolve to real rows. rls flags are ignored here — auth.users is set up by
-// SANDBOX_SETUP_STATEMENTS, this entry is only used by the seed step.
-//
-// Columns are an explicit allow-list: enough to evaluate realistic RLS
-// policies (id for FK matching, role/email/metadata for claim-style checks)
-// while keeping secrets out of the browser-side PGlite instance — no
-// encrypted_password, no *_token columns.
-export const AUTH_USERS_SEED_TABLE = {
- schema: 'auth',
- table: 'users',
- rls_enabled: false,
- rls_forced: false,
- columns: [
- 'id',
- 'aud',
- 'role',
- 'email',
- 'phone',
- 'email_confirmed_at',
- 'phone_confirmed_at',
- 'last_sign_in_at',
- 'confirmed_at',
- 'raw_app_meta_data',
- 'raw_user_meta_data',
- 'is_super_admin',
- 'is_sso_user',
- 'is_anonymous',
- 'banned_until',
- 'deleted_at',
- 'created_at',
- 'updated_at',
- ],
-} as const
diff --git a/apps/studio/state/postgres-sandbox/sandbox.core.ts b/apps/studio/state/postgres-sandbox/sandbox.core.ts
deleted file mode 100644
index 08c24b598b8..00000000000
--- a/apps/studio/state/postgres-sandbox/sandbox.core.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { PGliteWorker } from '@electric-sql/pglite/worker'
-
-import { SANDBOX_SETUP_STATEMENTS } from './sandbox.constants'
-import { applySchema, applySeed } from './sandbox.utils'
-import { type DatabaseSchemaDDLData } from '@/data/rls-tester/get-schema-ddl'
-import { type TableSeedData } from '@/data/rls-tester/get-seed-data'
-import { getErrorMessage } from '@/lib/get-error-message'
-
-type RLSTestResult = Record[]
-
-export interface SandboxCore {
- setSchema(data: DatabaseSchemaDDLData): Promise
- setSeed(tables: TableSeedData[]): Promise
- destroy(): Promise
- run: (props: { sql: string }) => Promise<{ result: RLSTestResult }>
-}
-
-let instance: SandboxCore | null = null
-let initPromise: Promise | null = null
-
-export const getSandboxCore = async () => {
- if (instance) return instance
- if (!initPromise) {
- initPromise = boot().finally(() => {
- initPromise = null
- })
- }
- return initPromise
-}
-
-const boot = async (): Promise => {
- const webWorker = new Worker(new URL('./pglite.worker.ts', import.meta.url), { type: 'module' })
- const pg = await PGliteWorker.create(webWorker)
-
- for (const sql of SANDBOX_SETUP_STATEMENTS) {
- try {
- await pg.exec(sql)
- } catch (err) {
- console.warn('[Postgres sandbox] setup:', (err as Error).message, `— ${sql.slice(0, 60)}`)
- }
- }
-
- function makeExecutor() {
- return { execSql: (sql: string) => pg.exec(sql).then(() => undefined as void) }
- }
-
- async function setSchema(data: DatabaseSchemaDDLData): Promise {
- await applySchema(makeExecutor(), data)
- }
-
- async function setSeed(tables: TableSeedData[]): Promise {
- await applySeed(makeExecutor(), tables)
- }
-
- const run = async ({ sql }: { sql: string }) => {
- try {
- const results = await pg.exec(sql)
- return { result: results.at(-1)?.rows ?? [] }
- } catch (error) {
- await pg.exec('ROLLBACK').catch(() => {})
- throw error instanceof Error ? error : new Error(getErrorMessage(error) ?? String(error))
- }
- }
-
- const destroy = async () => {
- webWorker.terminate()
- instance = null
- }
-
- instance = { run, destroy, setSchema, setSeed }
- return instance
-}
diff --git a/apps/studio/state/postgres-sandbox/sandbox.tsx b/apps/studio/state/postgres-sandbox/sandbox.tsx
deleted file mode 100644
index f6a7e1b447a..00000000000
--- a/apps/studio/state/postgres-sandbox/sandbox.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { noop } from 'lodash'
-import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
-import { toast } from 'sonner'
-
-import { AUTH_USERS_SEED_TABLE } from './sandbox.constants'
-import { getSandboxCore, type SandboxCore } from './sandbox.core'
-import { getDatabaseSchemaDDL } from '@/data/rls-tester/get-schema-ddl'
-import { getProjectSeedData, TableSeedData } from '@/data/rls-tester/get-seed-data'
-import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
-import { getErrorMessage } from '@/lib/get-error-message'
-
-type SandboxStatus = 'idle' | 'loading' | 'ready' | 'error'
-
-const SandboxContext = createContext<{
- status: SandboxStatus
- error?: string
- sandbox: SandboxCore | null
- isSyncing: boolean
- startSandbox: () => void
- destroySandbox: () => Promise
- syncSandbox: () => Promise
-}>({
- status: 'idle',
- error: undefined,
- sandbox: null,
- isSyncing: false,
- startSandbox: noop,
- destroySandbox: async () => {},
- syncSandbox: async () => {},
-})
-
-export const PostgresSandboxProvider = ({ children }: PropsWithChildren) => {
- const { data: project } = useSelectedProjectQuery()
-
- const [start, setStart] = useState(false)
- const [error, setError] = useState()
- const [sandbox, setSandbox] = useState(null)
-
- const [status, setStatus] = useState('idle')
- const [isSyncing, setIsSyncing] = useState(false)
-
- const destroySandbox = async () => {
- if (isSyncing) return
- if (!sandbox) return console.error('Sandbox is not set up')
-
- await sandbox.destroy()
- setSandbox(null)
- setStatus('idle')
- setError(undefined)
- setStart(false)
- }
-
- // Internal — takes the target explicitly so the boot path can pass the
- // freshly booted core before React state has caught up. Callers outside
- // the provider use `syncSandbox()` which sources the target from state.
- const applyToCore = async (target: SandboxCore) => {
- setIsSyncing(true)
-
- try {
- const schemaDDL = await getDatabaseSchemaDDL({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- schemas: ['public'],
- })
-
- const seedData: TableSeedData[] = await getProjectSeedData({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- tables: [AUTH_USERS_SEED_TABLE, ...(schemaDDL.rlsStatuses ?? [])],
- rowLimit: 100,
- })
-
- await target.setSchema(schemaDDL)
- await target.setSeed(seedData)
- } catch (e) {
- const message = getErrorMessage(e) ?? String(e)
- if (sandbox) {
- // Refresh path — sandbox is still usable with the previous schema/data.
- toast.error(`Failed to refresh sandbox: ${message}`)
- } else {
- // Boot path — propagate so the outer .catch sets status='error' and
- // the SandboxManagement error branch renders.
- throw e
- }
- } finally {
- setIsSyncing(false)
- }
- }
-
- const syncSandbox = async () => {
- if (isSyncing) return
- if (!sandbox) return console.error('Sandbox has not been loaded')
- await applyToCore(sandbox)
- }
-
- useEffect(() => {
- if (!start) return
-
- let cancelled = false
- setStatus('loading')
-
- getSandboxCore()
- .then(async (core) => {
- if (cancelled) return
-
- await applyToCore(core)
- setSandbox(core)
- setStatus('ready')
- })
- .catch((error) => {
- if (cancelled) return
-
- setError(getErrorMessage(error) ?? '')
- setStatus('error')
- setStart(false)
- })
-
- return () => {
- cancelled = true
- }
- // applyToCore intentionally omitted: this effect should fire once when
- // `start` flips, not every time the helper identity changes.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [start])
-
- return (
- setStart(true),
- destroySandbox,
- syncSandbox,
- }}
- >
- {children}
-
- )
-}
-
-export const usePostgresSandbox = () => useContext(SandboxContext)
diff --git a/apps/studio/state/postgres-sandbox/sandbox.utils.ts b/apps/studio/state/postgres-sandbox/sandbox.utils.ts
deleted file mode 100644
index d92b4a0cb8c..00000000000
--- a/apps/studio/state/postgres-sandbox/sandbox.utils.ts
+++ /dev/null
@@ -1,210 +0,0 @@
-import { ident, literal, type PGPolicy } from '@supabase/pg-meta'
-
-import { DatabaseSchemaDDL } from '@/data/rls-tester/get-schema-ddl'
-import { TableSeedData } from '@/data/rls-tester/get-seed-data'
-import { getErrorMessage } from '@/lib/get-error-message'
-
-interface Executor {
- execSql(sql: string): Promise
-}
-
-function buildPolicySQL(policy: PGPolicy): string {
- const name = ident(policy.name)
- const target = `${ident(policy.schema)}.${ident(policy.table)}`
- const permissiveness = policy.action === 'RESTRICTIVE' ? 'AS RESTRICTIVE' : ''
- const command = policy.command === 'ALL' ? '' : `FOR ${policy.command}`
- const roles = policy.roles?.length ? `TO ${policy.roles.map(ident).join(', ')}` : ''
- const using = policy.definition ? `USING (${policy.definition})` : ''
- const withCheck = policy.check ? `WITH CHECK (${policy.check})` : ''
-
- const drop = `DROP POLICY IF EXISTS ${name} ON ${target}`
- const create = [
- `CREATE POLICY ${name}`,
- `ON ${target}`,
- permissiveness,
- command,
- roles,
- using,
- withCheck,
- ]
- .filter(Boolean)
- .join(' ')
- return `${drop}; ${create}`
-}
-
-async function tryExec(sandbox: Executor, sql: string, label: string): Promise {
- try {
- await sandbox.execSql(sql)
- } catch (err) {
- console.warn(`[rls-sandbox] skipped ${label}:`, getErrorMessage(err) ?? err)
- }
-}
-
-// Retry items until no further progress can be made — handles ordering
-// dependencies (e.g. table A references type B that hasn't been created yet).
-// Each pass attempts every pending item; survivors carry forward. When a full
-// pass makes zero progress, surviving items are reported as unresolved.
-async function runUntilFixpoint(
- items: T[],
- attempt: (item: T) => Promise,
- onUnresolved: (item: T, error: unknown) => void
-): Promise {
- let pending = items.slice()
- while (pending.length > 0) {
- const failed: Array<{ item: T; error: unknown }> = []
- for (const item of pending) {
- try {
- await attempt(item)
- } catch (error) {
- failed.push({ item, error })
- }
- }
- if (failed.length === pending.length) {
- for (const { item, error } of failed) onUnresolved(item, error)
- break
- }
- pending = failed.map((f) => f.item)
- }
-}
-
-async function applyDDLWithRetries(sandbox: Executor, ddlStatements: string[]): Promise {
- await runUntilFixpoint(
- ddlStatements,
- (ddl) => sandbox.execSql(ddl),
- (ddl, error) =>
- console.warn(
- `[rls-sandbox] skipped DDL: ${ddl.slice(0, 80).replace(/\s+/g, ' ')} — ${getErrorMessage(error) ?? String(error)}`
- )
- )
-}
-
-export async function applySchema(
- sandbox: Executor,
- {
- schemas,
- typeDefinitions,
- entityDefinitions,
- functionDefinitions,
- policies,
- rlsStatuses,
- customRoles,
- }: DatabaseSchemaDDL
-): Promise {
- // Reset each user schema so re-syncs pick up renames/drops/column changes and
- // CREATE statements don't collide with the previous run's objects.
- for (const schema of schemas) {
- const schemaId = ident(schema)
- await tryExec(sandbox, `DROP SCHEMA IF EXISTS ${schemaId} CASCADE`, `drop schema ${schema}`)
- await tryExec(sandbox, `CREATE SCHEMA ${schemaId}`, `create schema ${schema}`)
- await tryExec(
- sandbox,
- `GRANT USAGE ON SCHEMA ${schemaId} TO anon, authenticated, service_role`,
- `grant schema ${schema}`
- )
- }
-
- if (customRoles.length > 0) {
- const checks = customRoles
- .map(
- ({ name }) =>
- `IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${literal(name)}) THEN CREATE ROLE ${ident(name)} NOLOGIN; END IF;`
- )
- .join('\n')
- await tryExec(sandbox, `DO $$ BEGIN\n${checks}\nEND $$`, 'custom roles')
- }
-
- await applyDDLWithRetries(sandbox, typeDefinitions)
- await applyDDLWithRetries(sandbox, entityDefinitions)
-
- for (const schema of [...new Set(rlsStatuses.map((t) => t.schema))]) {
- await tryExec(
- sandbox,
- `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA ${ident(schema)} TO anon, authenticated, service_role`,
- `grant tables in schema ${schema}`
- )
- }
-
- for (const { schema, table, rls_enabled, rls_forced } of rlsStatuses) {
- const actions: string[] = []
- if (rls_enabled) actions.push('ENABLE ROW LEVEL SECURITY')
- if (rls_forced) actions.push('FORCE ROW LEVEL SECURITY')
- if (actions.length === 0) continue
- await tryExec(
- sandbox,
- `ALTER TABLE ${ident(schema)}.${ident(table)} ${actions.join(', ')}`,
- `RLS on ${schema}.${table}`
- )
- }
-
- // Disable check_function_bodies so functions referencing not-yet-created objects don't abort.
- // Postgres resolves policy→function references at query time, not at CREATE POLICY time.
- await tryExec(sandbox, `SET check_function_bodies = off`, 'set check_function_bodies')
- for (const fn of functionDefinitions) {
- await tryExec(sandbox, fn, `function ${fn.slice(0, 60).replace(/\s+/g, ' ')}`)
- }
- await tryExec(sandbox, `RESET check_function_bodies`, 'reset check_function_bodies')
-
- for (const policy of policies) {
- await tryExec(
- sandbox,
- buildPolicySQL(policy),
- `policy ${policy.schema}.${policy.table} "${policy.name}"`
- )
- }
-}
-
-function serializeValue(val: unknown): string {
- if (val === null || val === undefined) return 'NULL'
- if (typeof val === 'boolean') return val ? 'TRUE' : 'FALSE'
- if (typeof val === 'number') return String(val)
- if (val instanceof Date) return `'${val.toISOString()}'`
- if (Array.isArray(val)) return `ARRAY[${val.map(serializeValue).join(', ')}]`
- if (typeof val === 'object') return `'${JSON.stringify(val).replace(/'/g, "''")}'::jsonb`
- return `'${String(val).replace(/'/g, "''")}'`
-}
-
-function buildInsertSQL(schema: string, table: string, rows: Record[]): string {
- if (rows.length === 0) throw new Error(`buildInsertSQL requires at least one row`)
- const columns = Object.keys(rows[0])
- const colList = columns.map((c) => ident(c)).join(', ')
- const valuesList = rows
- .map((row) => `(${columns.map((c) => serializeValue(row[c])).join(', ')})`)
- .join(',\n ')
- return `INSERT INTO ${ident(schema)}.${ident(table)} (${colList}) VALUES\n ${valuesList};`
-}
-
-export async function applySeed(sandbox: Executor, tables: TableSeedData[]): Promise {
- // Disable FK triggers so we can delete and re-insert in any order.
- // Requires superuser (ALTER ROLE postgres SUPERUSER in SANDBOX_SETUP_STATEMENTS).
- // Falls back gracefully if the privilege is not available.
- let triggersDisabled = false
- try {
- await sandbox.execSql(`SET session_replication_role = replica`)
- triggersDisabled = true
- } catch {
- // postgres not yet a superuser in this PGlite build — proceed without it
- }
-
- try {
- // Always clear before inserting so re-seed reflects the latest data.
- for (const { schema, table } of tables) {
- try {
- await sandbox.execSql(`DELETE FROM ${ident(schema)}.${ident(table)}`)
- } catch {
- // table may not exist yet — ignore
- }
- }
-
- // Retry loop handles any remaining FK ordering constraints.
- await runUntilFixpoint(
- tables.filter((t) => t.rows.length > 0),
- (entry) => sandbox.execSql(buildInsertSQL(entry.schema, entry.table, entry.rows)),
- (entry) =>
- console.warn(`[rls-sandbox] seed skipped ${entry.schema}.${entry.table}: unresolved FK`)
- )
- } finally {
- if (triggersDisabled) {
- await sandbox.execSql(`SET session_replication_role = DEFAULT`)
- }
- }
-}
diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts
index 0968d6892df..04168861a23 100644
--- a/packages/common/constants/local-storage.ts
+++ b/packages/common/constants/local-storage.ts
@@ -25,7 +25,6 @@ export const LOCAL_STORAGE_KEYS = {
UI_PREVIEW_PG_DELTA_DIFF: 'supabase-ui-pg-delta-diff',
UI_PREVIEW_PLATFORM_WEBHOOKS: 'supabase-ui-platform-webhooks',
UI_PREVIEW_JIT_DB_ACCESS: 'supabase-ui-jit-db-access',
- UI_PREVIEW_RLS_TESTER: 'supabase-ui-rls-tester',
UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE: 'supabase-ui-sql-editor-manual-save',
UI_PREVIEW_MARKETPLACE: 'supabase-ui-marketplace',
@@ -103,8 +102,6 @@ export const LOCAL_STORAGE_KEYS = {
PROJECT_SECURITY_DISMISSED_AT: (ref: string) => `project-security-dismissed-at-${ref}`,
- RLS_TESTER_BANNER_DISMISSED: (ref: string) => `rls-tester-banner-dismissed-${ref}`,
-
// Observability banner dismissed
OBSERVABILITY_BANNER_DISMISSED: (ref: string) => `observability-banner-dismissed-${ref}`,
ORGANIZATION_MARKETPLACE_BANNER_DISMISSED: (orgSlug: string, managedBy: string) =>
diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts
index e7e463d84da..5d56aa66b3c 100644
--- a/packages/common/telemetry-constants.ts
+++ b/packages/common/telemetry-constants.ts
@@ -2817,7 +2817,6 @@ export type AiAssistantSource =
| 'log_explorer'
| 'error_code'
| 'advisor_signal_detail'
- | 'rls_tester'
/**
* User copied an AI prompt to clipboard instead of using the built-in assistant.
@@ -3546,18 +3545,6 @@ export interface HeaderLocalVersionPopoverOpenedEvent {
groups: Partial
}
-/**
- * User ran a query in the RLS tester feature preview.
- *
- * @group Events
- * @source studio
- */
-export interface RlsTesterRunQueryClickedEvent {
- action: 'rls_tester_run_query_clicked'
- properties: { type: 'raw' | 'inferred' }
- groups: Partial
-}
-
/**
* @hidden
*/
@@ -3759,4 +3746,3 @@ export type TelemetryEvent =
| HeaderUserDropdownOpenedEvent
| HeaderLocalDropdownOpenedEvent
| HeaderLocalVersionPopoverOpenedEvent
- | RlsTesterRunQueryClickedEvent
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index efce7ed4197..ada553aa572 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -921,12 +921,6 @@ importers:
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@19.2.6)
- '@electric-sql/pglite':
- specifier: 0.4.5
- version: 0.4.5
- '@electric-sql/pglite-tools':
- specifier: ^0.3.4
- version: 0.3.5(@electric-sql/pglite@0.4.5)
'@graphiql/react':
specifier: ^0.37.3
version: 0.37.3(@emotion/is-prop-valid@1.4.0)(@types/node@22.13.14)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql-ws@5.14.1(graphql@16.11.0))(graphql@16.11.0)(immer@10.1.1)(react-compiler-runtime@19.1.0-rc.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6))
@@ -3578,11 +3572,6 @@ packages:
'@effect-ts/system@0.57.5':
resolution: {integrity: sha512-/crHGujo0xnuHIYNc1VgP0HGJGFSoSqq88JFXe6FmFyXPpWt8Xu39LyLg7rchsxfXFeEdA9CrIZvLV5eswXV5g==}
- '@electric-sql/pglite-tools@0.3.5':
- resolution: {integrity: sha512-4him0RnIyqrSqk0zzeBJKJ++VVFk6bFCwKSVV7DP3T8fOQgRSVZShlrHfAChLG2uA/T4JdQ8b/15CxBn+E34TQ==}
- peerDependencies:
- '@electric-sql/pglite': 0.4.5
-
'@electric-sql/pglite@0.4.5':
resolution: {integrity: sha512-aGG2zGEyZzGWKy8P+9ZoNUV0jxt1+hgbeTf+bVAYyxVZZLXg3/9aFlfLxb08AYZVAfAkQlQIysmWjhc5hwDG8g==}
@@ -19004,11 +18993,8 @@ snapshots:
'@effect-ts/system@0.57.5': {}
- '@electric-sql/pglite-tools@0.3.5(@electric-sql/pglite@0.4.5)':
- dependencies:
- '@electric-sql/pglite': 0.4.5
-
- '@electric-sql/pglite@0.4.5': {}
+ '@electric-sql/pglite@0.4.5':
+ optional: true
'@emnapi/core@1.10.0':
dependencies: