diff --git a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx
index ebb2a327ee135..2cb2d1d1516f5 100644
--- a/apps/studio/components/interfaces/Auth/Policies/Policies.tsx
+++ b/apps/studio/components/interfaces/Auth/Policies/Policies.tsx
@@ -10,7 +10,7 @@ import {
PolicyTableRow,
PolicyTableRowProps,
} from 'components/interfaces/Auth/Policies/PolicyTableRow'
-import ProtectedSchemaWarning from 'components/interfaces/Database/ProtectedSchemaWarning'
+import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
diff --git a/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx
index c09796c1e3ab0..53b3c1f65ff47 100644
--- a/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx
+++ b/apps/studio/components/interfaces/Database/EnumeratedTypes/EnumeratedTypes.tsx
@@ -13,7 +13,7 @@ import {
useEnumeratedTypesQuery,
} from 'data/enumerated-types/enumerated-types-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import {
Button,
DropdownMenu,
@@ -22,7 +22,7 @@ import {
DropdownMenuTrigger,
Input,
} from 'ui'
-import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
+import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import CreateEnumeratedTypeSidePanel from './CreateEnumeratedTypeSidePanel'
import DeleteEnumeratedTypeModal from './DeleteEnumeratedTypeModal'
import EditEnumeratedTypeSidePanel from './EditEnumeratedTypeSidePanel'
@@ -52,11 +52,7 @@ const EnumeratedTypes = () => {
)
: enumeratedTypes.filter((x) => x.schema === selectedSchema)
- const protectedSchemas = (schemas ?? []).filter((schema) =>
- PROTECTED_SCHEMAS.includes(schema?.name ?? '')
- )
- const schema = schemas?.find((schema) => schema.name === selectedSchema)
- const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
return (
@@ -81,7 +77,7 @@ const EnumeratedTypes = () => {
- {!isLocked && (
+ {!isSchemaLocked && (
{
- {isLocked && }
+ {isSchemaLocked && (
+
+ )}
{isLoading && }
@@ -140,7 +138,7 @@ const EnumeratedTypes = () => {
{type.name}
{type.enums.join(', ')}
- {!isLocked && (
+ {!isSchemaLocked && (
diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx
index fd966494bdfc6..d481d68ea2707 100644
--- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx
+++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx
@@ -13,7 +13,7 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex
import { useDatabaseFunctionCreateMutation } from 'data/database-functions/database-functions-create-mutation'
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
import { useDatabaseFunctionUpdateMutation } from 'data/database-functions/database-functions-update-mutation'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import type { FormSchema } from 'types'
import {
Button,
@@ -149,6 +149,8 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
}
}, [visible, func])
+ const { data: protectedSchemas } = useProtectedSchemas()
+
return (
isClosingSidePanel()}>
{
s.name)}
size="small"
onSelectSchema={(name) => field.onChange(name)}
/>
diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx
index 079776aec576c..22427a0581b80 100644
--- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx
+++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx
@@ -1,6 +1,6 @@
import type { PostgresFunction } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { noop, partition } from 'lodash'
+import { noop } from 'lodash'
import { Search } from 'lucide-react'
import { useRouter } from 'next/router'
@@ -16,10 +16,10 @@ import { useDatabaseFunctionsQuery } from 'data/database-functions/database-func
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Input } from 'ui'
-import ProtectedSchemaWarning from '../../ProtectedSchemaWarning'
+import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
import FunctionList from './FunctionList'
interface FunctionsListProps {
@@ -60,11 +60,8 @@ const FunctionsList = ({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
- const [protectedSchemas] = partition(schemas ?? [], (schema) =>
- PROTECTED_SCHEMAS.includes(schema?.name ?? '')
- )
- const foundSchema = schemas?.find((schema) => schema.name === selectedSchema)
- const isLocked = protectedSchemas.some((s) => s.id === foundSchema?.id)
+
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const {
data: functions,
@@ -126,7 +123,7 @@ const FunctionsList = ({
- {!isLocked && (
+ {!isSchemaLocked && (
<>
- {isLocked && }
+ {isSchemaLocked && }
diff --git a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx
index 263d20bfd4d5d..742a04ace3b1b 100644
--- a/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx
+++ b/apps/studio/components/interfaces/Database/Indexes/Indexes.tsx
@@ -1,4 +1,4 @@
-import { partition, sortBy } from 'lodash'
+import { sortBy } from 'lodash'
import { AlertCircle, Search, Trash } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
@@ -14,10 +14,10 @@ import { useDatabaseIndexDeleteMutation } from 'data/database-indexes/index-dele
import { DatabaseIndex, useIndexesQuery } from 'data/database-indexes/indexes-query'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { Button, Input, SidePanel } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
-import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
+import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import CreateIndexSidePanel from './CreateIndexSidePanel'
const Indexes = () => {
@@ -58,11 +58,7 @@ const Indexes = () => {
},
})
- const [protectedSchemas] = partition(schemas ?? [], (schema) =>
- PROTECTED_SCHEMAS.includes(schema?.name ?? '')
- )
- const schema = schemas?.find((schema) => schema.name === selectedSchema)
- const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const sortedIndexes = sortBy(allIndexes ?? [], (index) => index.name.toLocaleLowerCase())
const indexes =
@@ -122,7 +118,7 @@ const Indexes = () => {
icon={ }
/>
- {!isLocked && (
+ {!isSchemaLocked && (
{
)}
- {isLocked && }
+ {isSchemaLocked && }
{isLoadingIndexes && }
@@ -190,7 +186,7 @@ const Indexes = () => {
setSelectedIndex(index)}>
View definition
- {!isLocked && (
+ {!isSchemaLocked && (
void
-}) => {
+import { INTERNAL_SCHEMAS, useIsProtectedSchema } from 'hooks/useProtectedSchemas'
+import { Admonition } from 'ui-patterns'
+
+export const ProtectedSchemaDialog = ({ onClose }: { onClose: () => void }) => {
return (
-
- onClose()}>
- Understood
-
-
- }
- onCancel={() => onClose()}
- >
-
+ <>
+
+ Schemas managed by Supabase
+
+
+
The following schemas are managed by Supabase and are currently protected from write
access through the dashboard.
- {PROTECTED_SCHEMAS.map((schema) => (
+ {INTERNAL_SCHEMAS.map((schema) => (
{schema}
@@ -45,32 +43,67 @@ export const ProtectedSchemaModal = ({
You can, however, still interact with those schemas through the SQL Editor although we
advise you only do so if you know what you are doing.
-
-
+
+
+
+
+ Understood
+
+
+
+ >
)
}
-const ProtectedSchemaWarning = ({ schema, entity }: { schema: string; entity: string }) => {
+export const ProtectedSchemaWarning = ({
+ size = 'md',
+ schema,
+ entity,
+}: {
+ size?: 'sm' | 'md'
+ schema: string
+ entity: string
+}) => {
const [showModal, setShowModal] = useState(false)
+ const { isSchemaLocked, reason } = useIsProtectedSchema({ schema })
+
+ if (!isSchemaLocked) return null
return (
- <>
-
-
- Currently viewing {entity} from a protected schema
-
+ div>p]:prose [&>div>p]:max-w-full [&>div>p]:!leading-normal',
+ size === 'sm' ? '[&>div>p]:text-xs' : '[&>div>p]:text-sm'
+ )}
+ >
+ {reason === 'fdw' ? (
+
+ The {schema} schema is used by Supabase to connect to
+ analytics buckets and is read-only through the dashboard.
+
+ ) : (
+ <>
The {schema} schema is managed by Supabase and is
read-only through the dashboard.
- setShowModal(true)}>
- Learn more
-
-
-
-
setShowModal(false)} />
- >
+
+
+ setShowModal(true)}>
+ Learn more
+
+
+
+ setShowModal(false)} />
+
+
+ >
+ )}
+
)
}
-
-export default ProtectedSchemaWarning
diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx
index fe0f306660f1f..a67ff1ea1312e 100644
--- a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx
+++ b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx
@@ -1,6 +1,6 @@
import type { PostgresPublication } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useState } from 'react'
+import { useMemo, useState } from 'react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
@@ -10,10 +10,10 @@ import InformationBox from 'components/ui/InformationBox'
import { Loading } from 'components/ui/Loading'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
+import { AlertCircle, ChevronLeft, Search } from 'lucide-react'
import { Button, Input } from 'ui'
import PublicationsTableItem from './PublicationsTableItem'
-import { ChevronLeft, Search, AlertCircle } from 'lucide-react'
interface PublicationsTablesProps {
selectedPublication: PostgresPublication
@@ -29,27 +29,26 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
'publications'
)
+ const { data: protectedSchemas } = useProtectedSchemas()
+
const {
- data: tables,
+ data: tablesData,
isLoading,
isSuccess,
isError,
error,
- } = useTablesQuery(
- {
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- },
- {
- select(tables) {
- return tables.filter((table) =>
- filterString.length === 0
- ? !PROTECTED_SCHEMAS.includes(table.schema)
- : !PROTECTED_SCHEMAS.includes(table.schema) && table.name.includes(filterString)
- )
- },
- }
- )
+ } = useTablesQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+ const tables = useMemo(() => {
+ return (tablesData || []).filter((table) =>
+ filterString.length === 0
+ ? !protectedSchemas.find((s) => s.name === table.schema)
+ : !protectedSchemas.find((s) => s.name === table.schema) &&
+ table.name.includes(filterString)
+ )
+ }, [tablesData, protectedSchemas, filterString])
return (
<>
diff --git a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx
index c77fee04b4ea2..e3830b6680ea9 100644
--- a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx
+++ b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx
@@ -14,7 +14,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { isTableLike } from 'data/table-editor/table-editor-types'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import {
Button,
DropdownMenu,
@@ -26,7 +26,7 @@ import {
TooltipContent,
TooltipTrigger,
} from 'ui'
-import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
+import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
interface ColumnListProps {
onAddColumn: () => void
@@ -63,7 +63,7 @@ const ColumnList = ({
? selectedTable?.columns ?? []
: selectedTable?.columns?.filter((column) => column.name.includes(filterString))) ?? []
- const isLocked = PROTECTED_SCHEMAS.includes(selectedTable?.schema ?? '')
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
const canUpdateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
return (
@@ -81,7 +81,7 @@ const ColumnList = ({
icon={ }
/>
- {!isLocked && isTableEntity && (
+ {!isSchemaLocked && isTableEntity && (
}
disabled={!canUpdateColumns}
@@ -100,7 +100,9 @@ const ColumnList = ({
)}
- {isLocked && }
+ {isSchemaLocked && (
+
+ )}
{isLoading && }
@@ -156,7 +158,7 @@ const ColumnList = ({
)}
- {!isLocked && isTableEntity && (
+ {!isSchemaLocked && isTableEntity && (
} />
@@ -183,7 +185,7 @@ const ColumnList = ({
onDeleteColumn(x)}
className="space-x-2"
>
diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx
index ab2bc61582b04..500bc001642e0 100644
--- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx
+++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx
@@ -35,7 +35,7 @@ import { useTablesQuery } from 'data/tables/tables-query'
import { useViewsQuery } from 'data/views/views-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import {
Button,
Checkbox_Shadcn_,
@@ -54,7 +54,7 @@ import {
TooltipTrigger,
cn,
} from 'ui'
-import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
+import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import { formatAllEntities } from './Tables.utils'
interface TableListProps {
@@ -184,7 +184,7 @@ const TableList = ({
(x) => visibleTypes.includes(x.type)
)
- const isLocked = PROTECTED_SCHEMAS.includes(selectedSchema)
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const error = tablesError || viewsError || materializedViewsError || foreignTablesError
const isError = isErrorTables || isErrorViews || isErrorMaterializedViews || isErrorForeignTables
@@ -269,7 +269,7 @@ const TableList = ({
icon={ }
/>
- {!isLocked && (
+ {!isSchemaLocked && (
}
@@ -290,7 +290,7 @@ const TableList = ({
- {isLocked && }
+ {isSchemaLocked && }
{isLoading && }
@@ -462,7 +462,7 @@ const TableList = ({
- {!isLocked && (
+ {!isSchemaLocked && (
} />
@@ -519,10 +519,10 @@ const TableList = ({
{
- if (canUpdateTables && !isLocked) {
+ if (canUpdateTables && !isSchemaLocked) {
onDeleteTable({
...x,
schema: selectedSchema,
diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx
index a4e5987ba159e..5d381c0058849 100644
--- a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx
+++ b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx
@@ -11,7 +11,7 @@ import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/databas
import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-mutation'
import { useTablesQuery } from 'data/tables/tables-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import {
Button,
Checkbox_Shadcn_,
@@ -107,14 +107,16 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
}
)
- const { data = [], isSuccess } = useTablesQuery({
+ const { data = [], isSuccess: isSuccessTables } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
+ const { data: protectedSchemas, isSuccess: isSuccessProtectedSchemas } = useProtectedSchemas()
+ const isSuccess = isSuccessTables && isSuccessProtectedSchemas
const tables = data
.sort((a, b) => a.schema.localeCompare(b.schema))
- .filter((a) => !PROTECTED_SCHEMAS.includes(a.schema))
+ .filter((a) => !protectedSchemas.find((s) => s.name === a.schema))
const isEditing = !!selectedTrigger
const form = useForm>({
diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx
index 03cf7348cd35c..df5058fe2dc80 100644
--- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx
+++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx
@@ -1,6 +1,6 @@
import { PostgresTrigger } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { noop, partition } from 'lodash'
+import { noop } from 'lodash'
import { Plus, Search } from 'lucide-react'
import { useState } from 'react'
@@ -13,14 +13,13 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import SchemaSelector from 'components/ui/SchemaSelector'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query'
-import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema, useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Input } from 'ui'
-import ProtectedSchemaWarning from '../../ProtectedSchemaWarning'
+import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
import TriggerList from './TriggerList'
interface TriggersListProps {
@@ -39,21 +38,15 @@ const TriggersList = ({
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const [filterString, setFilterString] = useState('')
- const { data: schemas } = useSchemasQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const [protectedSchemas] = partition(schemas ?? [], (schema) =>
- PROTECTED_SCHEMAS.includes(schema?.name ?? '')
- )
- const schema = schemas?.find((schema) => schema.name === selectedSchema)
- const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
+ const { data: protectedSchemas } = useProtectedSchemas()
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const { data = [], isSuccess } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
- const hasTables = data.filter((a) => !PROTECTED_SCHEMAS.includes(a.schema)).length > 0
+ const hasTables =
+ data.filter((a) => !protectedSchemas.find((s) => s.name === a.schema)).length > 0
const {
data: triggers,
@@ -115,7 +108,7 @@ const TriggersList = ({
onChange={(e) => setFilterString(e.target.value)}
/>
- {!isLocked && (
+ {!isSchemaLocked && (
- {isLocked && }
+ {isSchemaLocked && }
diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/CreateIcebergWrapperSheet.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/CreateIcebergWrapperSheet.tsx
index 25c9659027e20..9d22737a437ad 100644
--- a/apps/studio/components/interfaces/Integrations/Wrappers/CreateIcebergWrapperSheet.tsx
+++ b/apps/studio/components/interfaces/Integrations/Wrappers/CreateIcebergWrapperSheet.tsx
@@ -337,7 +337,7 @@ export const CreateIcebergWrapperSheet = ({
/>
A new schema will be created. For security purposes, the wrapper tables
- from the foreign schema cannot be created within an existing schema
+ from the foreign schema cannot be created within an existing schema.
diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx
index 73b2ce54d26c7..d388404874029 100644
--- a/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx
+++ b/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx
@@ -440,7 +440,7 @@ export const CreateWrapperSheet = ({
/>
A new schema will be created. For security purposes, the wrapper tables
- from the foreign schema cannot be created within an existing schema
+ from the foreign schema cannot be created within an existing schema.
diff --git a/apps/studio/components/interfaces/Storage/ImportForeignSchemaDialog.tsx b/apps/studio/components/interfaces/Storage/ImportForeignSchemaDialog.tsx
index c54d1e3bd45d6..d7371c08591c3 100644
--- a/apps/studio/components/interfaces/Storage/ImportForeignSchemaDialog.tsx
+++ b/apps/studio/components/interfaces/Storage/ImportForeignSchemaDialog.tsx
@@ -107,7 +107,7 @@ export const ImportForeignSchemaDialog = ({
const serverOptions = await getDecryptedParameters({
ref: project?.ref,
connectionString: project?.connectionString ?? undefined,
- serverName,
+ wrapper,
})
const formValues: Record = {
@@ -158,11 +158,7 @@ export const ImportForeignSchemaDialog = ({
hideFooter
visible={visible}
size="medium"
- header={
-
- Connect namespace {namespace}
-
- }
+ header={Connect namespace "{namespace}" }
onCancel={() => onClose()}
>
@@ -173,9 +169,9 @@ export const ImportForeignSchemaDialog = ({
name="targetSchema"
render={({ field }) => (
diff --git a/apps/studio/components/interfaces/Storage/ImportForeignSchemaDialog.utils.ts b/apps/studio/components/interfaces/Storage/ImportForeignSchemaDialog.utils.ts
index 923e41ae733dc..1b1f6d5172097 100644
--- a/apps/studio/components/interfaces/Storage/ImportForeignSchemaDialog.utils.ts
+++ b/apps/studio/components/interfaces/Storage/ImportForeignSchemaDialog.utils.ts
@@ -1,4 +1,4 @@
-import { getFDWs } from 'data/fdw/fdws-query'
+import { type FDW } from 'data/fdw/fdws-query'
import { getDecryptedValues } from 'data/vault/vault-secret-decrypted-value-query'
import { INTEGRATIONS } from '../Integrations/Landing/Integrations.constants'
import { WrapperMeta } from '../Integrations/Wrappers/Wrappers.types'
@@ -7,18 +7,16 @@ import { convertKVStringArrayToJson } from '../Integrations/Wrappers/Wrappers.ut
export const getDecryptedParameters = async ({
ref,
connectionString,
- serverName,
+ wrapper,
}: {
ref?: string
connectionString?: string
- serverName: string
+ wrapper: FDW
}) => {
const integration = INTEGRATIONS.find((i) => i.id === 'iceberg_wrapper' && i.type === 'wrapper')
const wrapperMeta = (integration?.type === 'wrapper' && integration.meta) as WrapperMeta
const wrapperServerOptions = wrapperMeta.server.options
- const FDWs = await getFDWs({ projectRef: ref, connectionString: connectionString })
- const wrapper = FDWs.find((fdw) => fdw.server_name === serverName)
const serverOptions = convertKVStringArrayToJson(wrapper?.server_options ?? [])
const paramsToBeDecrypted = Object.fromEntries(
diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
index a14889e5736e6..f32a66bb37a61 100644
--- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
@@ -25,7 +25,7 @@ import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import {
Button,
@@ -60,7 +60,7 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
const isMaterializedView = isTableLikeMaterializedView(table)
const realtimeEnabled = useIsFeatureEnabled('realtime:all')
- const isLocked = PROTECTED_SCHEMAS.includes(table.schema)
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: table.schema })
const { mutate: updateTable } = useTableUpdateMutation({
onError: (error) => {
@@ -200,10 +200,10 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
)}
- {isTable && !isLocked ? (
+ {isTable && !isSchemaLocked ? (
table.rls_enabled ? (
<>
- {policies.length < 1 && !isLocked ? (
+ {policies.length < 1 && !isSchemaLocked ? (
{
) : (
0 ? (
+ isSchemaLocked || policies.length > 0 ? (
{
With RLS enabled, anonymous users will not be able to read/write data in the
table.
- {!isLocked && (
+ {!isSchemaLocked && (
!PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
+ (type) => !protectedSchemas.find((s) => s.name === type.schema)
)
const { data: constraints } = useTableConstraintsQuery({
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
index 4a78fa7af4623..50e60b293d11c 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
@@ -16,10 +16,12 @@ import {
useForeignKeyConstraintsQuery,
} from 'data/database/foreign-key-constraints-query'
import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query'
+import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
+import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useUrlState } from 'hooks/ui/useUrlState'
-import { PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS } from 'lib/constants/schemas'
+import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Badge, Checkbox, Input, SidePanel } from 'ui'
import { Admonition } from 'ui-patterns'
@@ -41,8 +43,6 @@ import {
generateTableFieldFromPostgresTable,
validateFields,
} from './TableEditor.utils'
-import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
-import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
export interface TableEditorProps {
table?: PostgresTable
@@ -100,8 +100,9 @@ const TableEditor = ({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
+ const { data: protectedSchemas } = useProtectedSchemas({ excludeSchemas: ['extensions'] })
const enumTypes = (types ?? []).filter(
- (type) => !PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
+ (type) => !protectedSchemas.find((s) => s.name === type.schema)
)
const { data: publications } = useDatabasePublicationsQuery({
diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx
index fe294b3616908..d1bff23294c1b 100644
--- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx
@@ -14,7 +14,7 @@ import {
} from 'data/table-editor/table-editor-types'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useUrlState } from 'hooks/ui/useUrlState'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useAppStateSnapshot } from 'state/app-state'
import { TableEditorTableStateContextProvider } from 'state/table-editor-table'
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
@@ -72,6 +72,8 @@ export const TableGridEditor = ({
}
}, [onClearDashboardHistory, router, selectedTable, tabs])
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
+
// NOTE: DO NOT PUT HOOKS AFTER THIS LINE
if (isLoadingSelectedTable || !projectRef) {
return (
@@ -138,8 +140,8 @@ export const TableGridEditor = ({
const isViewSelected = isView(selectedTable) || isMaterializedView(selectedTable)
const isTableSelected = isTableLike(selectedTable)
- const isLocked = PROTECTED_SCHEMAS.includes(selectedTable?.schema ?? '')
- const canEditViaTableEditor = isTableSelected && !isLocked
+
+ const canEditViaTableEditor = isTableSelected && !isSchemaLocked
const editable = !isReadOnly && canEditViaTableEditor
const gridKey = `${selectedTable.schema}_${selectedTable.name}`
diff --git a/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx
index 42c1ad27e8d22..efd0981dd29f0 100644
--- a/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx
+++ b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx
@@ -1,14 +1,17 @@
import { type PostgresColumn } from '@supabase/postgres-meta'
import { AlertTriangle, Code, Loader2, Table2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
+import { useEffect, useMemo, useRef } from 'react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering'
-import { type SqlSnippet, useSqlSnippetsQuery } from 'data/content/sql-snippets-query'
+import { useSqlSnippetsQuery, type SqlSnippet } from 'data/content/sql-snippets-query'
+import { usePrefetchTables, useTablesQuery, type TablesData } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
+import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { useProfile } from 'lib/profile'
import {
cn,
@@ -34,9 +37,6 @@ import {
useSetCommandMenuSize,
useSetPage,
} from 'ui-patterns/CommandMenu'
-import { usePrefetchTables, useTablesQuery, type TablesData } from 'data/tables/tables-query'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
-import { useEffect, useRef } from 'react'
export function useSqlEditorGotoCommands(options?: CommandOptions) {
let { ref } = useParams()
@@ -295,19 +295,20 @@ export function useQueryTableCommands(options?: CommandOptions) {
function TableSelector() {
const router = useRouter()
const project = useSelectedProject()
+ const { data: protectedSchemas } = useProtectedSchemas()
const {
- data: tables,
+ data: tablesData,
isLoading,
isError,
isSuccess,
- } = useTablesQuery(
- {
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- includeColumns: true,
- },
- { select: excludeSupabaseControlledSchemas }
- )
+ } = useTablesQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ includeColumns: true,
+ })
+ const tables = useMemo(() => {
+ return tablesData?.filter((table) => !protectedSchemas.find((s) => s.name === table.schema))
+ }, [tablesData, protectedSchemas])
return (
@@ -360,10 +361,6 @@ from ${formatTableIdentifier(table)}
`.trim()
}
-function excludeSupabaseControlledSchemas(tables: TablesData) {
- return tables.filter((table) => !PROTECTED_SCHEMAS.includes(table.schema))
-}
-
// Not a perfectly spec-compliant regex , since Postgres also allows non-Latin
// letters and letters with diacritical marks, but quoting them defensively
// is easier than writing the regex. ¯\_(ツ)_/¯
diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
index 3262273143e00..27ea4e9ea551d 100644
--- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
+++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
@@ -1,30 +1,25 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { partition } from 'lodash'
import { Filter, Plus } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'common'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import { ExportDialog } from 'components/grid/components/header/ExportDialog'
-import { ProtectedSchemaModal } from 'components/interfaces/Database/ProtectedSchemaWarning'
+import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import InfiniteList from 'components/ui/InfiniteList'
import SchemaSelector from 'components/ui/SchemaSelector'
-import { useSchemasQuery } from 'data/database/schemas-query'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
- AlertDescription_Shadcn_,
- AlertTitle_Shadcn_,
- Alert_Shadcn_,
Button,
Checkbox_Shadcn_,
Label_Shadcn_,
@@ -44,14 +39,13 @@ import { useTableEditorTabsCleanUp } from '../Tabs/Tabs.utils'
import EntityListItem from './EntityListItem'
import { TableMenuEmptyState } from './TableMenuEmptyState'
-const TableEditorMenu = () => {
+export const TableEditorMenu = () => {
const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const snap = useTableEditorStateSnapshot()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const isMobile = useBreakpoint()
- const [showModal, setShowModal] = useState(false)
const [searchText, setSearchText] = useState('')
const [tableToExport, setTableToExport] = useState<{ name: string; schema: string }>()
const [visibleTypes, setVisibleTypes] = useState(Object.values(ENTITY_TYPE))
@@ -89,19 +83,9 @@ const TableEditorMenu = () => {
[data?.pages]
)
- const { data: schemas } = useSchemasQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
-
- const schema = schemas?.find((schema) => schema.name === selectedSchema)
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
- const [protectedSchemas] = partition(
- (schemas ?? []).sort((a, b) => a.name.localeCompare(b.name)),
- (schema) => PROTECTED_SCHEMAS.includes(schema?.name ?? '')
- )
- const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
+ const { isSchemaLocked, reason } = useIsProtectedSchema({ schema: selectedSchema })
const { data: selectedTable } = useTableEditorQuery({
projectRef: project?.ref,
@@ -138,7 +122,7 @@ const TableEditorMenu = () => {
/>
- {!isLocked ? (
+ {!isSchemaLocked ? (
{
New table
) : (
-
-
- Viewing protected schema
-
-
-
- This schema is managed by Supabase and is read-only through the table editor
-
- setShowModal(true)}>
- Learn more
-
-
-
+
)}
@@ -282,7 +254,7 @@ const TableEditorMenu = () => {
itemProps={{
projectRef: project?.ref!,
id: Number(id),
- isLocked,
+ isSchemaLocked,
onExportCLI: () => {
const entity = entityTypes?.find((x) => x.id === id)
if (!entity) return
@@ -308,7 +280,6 @@ const TableEditorMenu = () => {
if (!open) setTableToExport(undefined)
}}
/>
-
setShowModal(false)} />
>
)
}
diff --git a/apps/studio/components/layouts/Tabs/SortableTab.tsx b/apps/studio/components/layouts/Tabs/SortableTab.tsx
index 4f03c326c8d71..11cab9c709578 100644
--- a/apps/studio/components/layouts/Tabs/SortableTab.tsx
+++ b/apps/studio/components/layouts/Tabs/SortableTab.tsx
@@ -2,10 +2,10 @@ import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { AnimatePresence, motion } from 'framer-motion'
import { X } from 'lucide-react'
-import { useRouter } from 'next/router'
import { useMemo } from 'react'
import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
+import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useTabsStateSnapshot, type Tab } from 'state/tabs'
import { cn, TabsTrigger_Shadcn_ } from 'ui'
@@ -28,8 +28,7 @@ export const SortableTab = ({
openTabs: Tab[]
onClose: (id: string) => void
}) => {
- const router = useRouter()
- const currentSchema = (router.query.schema as string) || 'public'
+ const { selectedSchema: currentSchema } = useQuerySchemaState()
const tabs = useTabsStateSnapshot()
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: tab.id,
@@ -45,16 +44,8 @@ export const SortableTab = ({
const shouldShowSchema = useMemo(() => {
// For both table and schema tabs, show schema if:
// Any tab has a different schema than the current schema parameter
- if (tab.type === 'r') {
- const anyTabHasDifferentSchema = openTabs
- .filter((t) => t.type === 'r')
- .some((t) => t.metadata?.schema !== currentSchema)
-
- return anyTabHasDifferentSchema
- }
-
- return false
- }, [openTabs, currentSchema, tab.type])
+ return openTabs.some((t) => t.metadata?.schema !== currentSchema)
+ }, [openTabs, currentSchema])
// Create a motion version of TabsTrigger while preserving all functionality
// const MotionTabsTrigger = motion(TabsTrigger_Shadcn_)
diff --git a/apps/studio/data/fdw/fdw-delete-mutation.ts b/apps/studio/data/fdw/fdw-delete-mutation.ts
index 19e2778c8f1cb..37cd3030c467c 100644
--- a/apps/studio/data/fdw/fdw-delete-mutation.ts
+++ b/apps/studio/data/fdw/fdw-delete-mutation.ts
@@ -73,7 +73,7 @@ export const getDeleteFDWSql = ({
const deleteEncryptedSecretsSql = deleteEncryptedSecretsSqlArray.join('\n')
const sql = /* SQL */ `
- drop foreign data wrapper if exists ${wrapper.name} cascade;
+ drop foreign data wrapper if exists "${wrapper.name}" cascade;
${deleteEncryptedSecretsSql}
`
diff --git a/apps/studio/hooks/misc/useSchemaQueryState.ts b/apps/studio/hooks/misc/useSchemaQueryState.ts
index f981ac4495189..12ab49493aa93 100644
--- a/apps/studio/hooks/misc/useSchemaQueryState.ts
+++ b/apps/studio/hooks/misc/useSchemaQueryState.ts
@@ -12,7 +12,12 @@ const useIsomorphicUseQueryState = (defaultSchema: string) => {
return [defaultSchema, () => {}] as const
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks
- return useQueryState('schema', parseAsString.withDefault(defaultSchema))
+ return useQueryState(
+ 'schema',
+ parseAsString.withDefault(defaultSchema).withOptions({
+ clearOnDefault: false,
+ })
+ )
}
}
diff --git a/apps/studio/hooks/useProtectedSchemas.ts b/apps/studio/hooks/useProtectedSchemas.ts
new file mode 100644
index 0000000000000..92dfcf9625d38
--- /dev/null
+++ b/apps/studio/hooks/useProtectedSchemas.ts
@@ -0,0 +1,112 @@
+import { uniq } from 'lodash'
+import { useMemo } from 'react'
+
+import { WRAPPER_HANDLERS } from 'components/interfaces/Integrations/Wrappers/Wrappers.constants'
+import {
+ convertKVStringArrayToJson,
+ wrapperMetaComparator,
+} from 'components/interfaces/Integrations/Wrappers/Wrappers.utils'
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+import { QUEUES_SCHEMA } from 'data/database-queues/database-queues-toggle-postgrest-mutation'
+import { useFDWsQuery } from 'data/fdw/fdws-query'
+
+/**
+ * A list of system schemas that users should not interact with
+ */
+export const INTERNAL_SCHEMAS = [
+ 'auth',
+ 'cron',
+ 'extensions',
+ 'information_schema',
+ 'net',
+ 'pgsodium',
+ 'pgsodium_masks',
+ 'pgbouncer',
+ 'pgtle',
+ 'realtime',
+ 'storage',
+ 'supabase_functions',
+ 'supabase_migrations',
+ 'vault',
+ 'graphql',
+ 'graphql_public',
+ QUEUES_SCHEMA,
+]
+
+/**
+ * Get the list of schemas used by IcebergFDWs
+ */
+const useIcebergFdwSchemasQuery = () => {
+ const { project } = useProjectContext()
+ const result = useFDWsQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+
+ const schemas = useMemo(() => {
+ const icebergFDWs = result.data?.filter((wrapper) =>
+ wrapperMetaComparator(
+ { handlerName: WRAPPER_HANDLERS.ICEBERG, server: { options: [] } },
+ wrapper
+ )
+ )
+
+ const fdwSchemas = icebergFDWs
+ ?.map((fdw) => convertKVStringArrayToJson(fdw.server_options))
+ .map((options) => options['supabase_target_schema'])
+ .flatMap((s) => s?.split(','))
+ .filter(Boolean)
+
+ return uniq(fdwSchemas)
+ }, [result.data])
+
+ return { ...result, data: schemas }
+}
+
+/**
+ * Returns a list of schemas that are protected by Supabase (internal schemas or schemas used by Iceberg FDWs).
+ */
+export const useProtectedSchemas = ({
+ excludeSchemas = [],
+}: { excludeSchemas?: string[] } = {}) => {
+ // Stabilize the excludeSchemas array to prevent unnecessary re-computations
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const stableExcludeSchemas = useMemo(() => excludeSchemas, [JSON.stringify(excludeSchemas)])
+
+ const result = useIcebergFdwSchemasQuery()
+
+ const schemas = useMemo<{ name: string; type: 'fdw' | 'internal' }[]>(() => {
+ const internalSchemas = INTERNAL_SCHEMAS.map((s) => ({ name: s, type: 'internal' as const }))
+ const icebergFdwSchemas = result.data?.map((s) => ({ name: s, type: 'fdw' as const }))
+
+ const schemas = uniq([...internalSchemas, ...icebergFdwSchemas])
+ return schemas.filter((schema) => !stableExcludeSchemas.includes(schema.name))
+ }, [result.data, stableExcludeSchemas])
+
+ return { ...result, data: schemas }
+}
+
+/**
+ * Returns whether a given schema is protected by Supabase (internal schema or schema used by Iceberg FDWs).
+ */
+export const useIsProtectedSchema = ({
+ schema,
+ excludedSchemas = [],
+}: {
+ schema: string
+ excludedSchemas?: string[]
+}):
+ | { isSchemaLocked: false; reason: undefined }
+ | { isSchemaLocked: true; reason: 'fdw' | 'internal' } => {
+ const { data: schemas } = useProtectedSchemas({ excludeSchemas: excludedSchemas })
+
+ const foundSchema = schemas.find((s) => s.name === schema)
+
+ if (foundSchema) {
+ return {
+ isSchemaLocked: true,
+ reason: foundSchema.type,
+ }
+ }
+ return { isSchemaLocked: false, reason: undefined }
+}
diff --git a/apps/studio/lib/constants/schemas.ts b/apps/studio/lib/constants/schemas.ts
deleted file mode 100644
index 0d15f8ff19a39..0000000000000
--- a/apps/studio/lib/constants/schemas.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { QUEUES_SCHEMA } from 'data/database-queues/database-queues-toggle-postgrest-mutation'
-
-/**
- * A list of system schemas that users should not interact with
- */
-export const PROTECTED_SCHEMAS = [
- 'auth',
- 'cron',
- 'extensions',
- 'information_schema',
- 'net',
- 'pgsodium',
- 'pgsodium_masks',
- 'pgbouncer',
- 'pgtle',
- 'realtime',
- 'storage',
- 'supabase_functions',
- 'supabase_migrations',
- 'vault',
- 'graphql',
- 'graphql_public',
- QUEUES_SCHEMA,
-]
-
-export const PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS = PROTECTED_SCHEMAS.filter(
- (x) => x !== 'extensions'
-)
diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx
index 8cf5cd173a6af..0b086d6c89605 100644
--- a/apps/studio/pages/project/[ref]/auth/policies.tsx
+++ b/apps/studio/pages/project/[ref]/auth/policies.tsx
@@ -1,6 +1,5 @@
import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { partition } from 'lodash'
import { Search } from 'lucide-react'
import { useState } from 'react'
@@ -18,11 +17,10 @@ import NoPermission from 'components/ui/NoPermission'
import SchemaSelector from 'components/ui/SchemaSelector'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
-import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { useUrlState } from 'hooks/ui/useUrlState'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
import { Input } from 'ui'
@@ -76,16 +74,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false)
const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState()
- const { data: schemas } = useSchemasQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const [protectedSchemas] = partition(
- schemas,
- (schema) => schema?.name !== 'realtime' && PROTECTED_SCHEMAS.includes(schema?.name ?? '')
- )
- const selectedSchema = schemas?.find((s) => s.name === schema)
- const isLocked = protectedSchemas.some((s) => s.id === selectedSchema?.id)
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: schema, excludedSchemas: ['realtime'] })
const { data: policies } = useDatabasePoliciesQuery({
projectRef: project?.ref,
@@ -151,7 +140,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
schema={schema}
tables={filteredTables}
hasTables={tables.length > 0}
- isLocked={isLocked}
+ isLocked={isSchemaLocked}
onSelectCreatePolicy={(table: string) => {
if (isInlineEditorEnabled) {
setEditorPanel({
diff --git a/apps/studio/pages/project/[ref]/database/column-privileges.tsx b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
index abbd13827b36c..0d78688c571f7 100644
--- a/apps/studio/pages/project/[ref]/database/column-privileges.tsx
+++ b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
@@ -16,7 +16,7 @@ import {
} from 'components/interfaces/Database/Privileges/Privileges.utils'
import PrivilegesHead from 'components/interfaces/Database/Privileges/PrivilegesHead'
import PrivilegesTable from 'components/interfaces/Database/Privileges/PrivilegesTable'
-import ProtectedSchemaWarning from 'components/interfaces/Database/ProtectedSchemaWarning'
+import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -30,7 +30,7 @@ import { useTablePrivilegesQuery } from 'data/privileges/table-privileges-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
-import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
+import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import type { NextPageWithLayout } from 'types'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
@@ -132,7 +132,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
const table = tableList?.find(
(table) => table.schema === selectedSchema && table.name === selectedTable
)
- const isLocked = PROTECTED_SCHEMAS.includes(selectedSchema)
+ const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const {
tableCheckedStates,
@@ -286,7 +286,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
)}
{
hasChanges={hasChanges}
isApplyingChanges={isApplyingChanges}
/>
- {isLocked && (
+ {isSchemaLocked && (
)}
{isLoading ? (
@@ -310,7 +310,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
) : table && tablePrivilege ? (