mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
convert fdw sql utilities to safesql (#45069)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved SQL query handling for vector bucket creation and Foreign Data Wrapper operations <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
485402640f
commit
bff92df845
5 files changed
+139
-96
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { literal, safeSql } from '@supabase/pg-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
@@ -171,7 +172,7 @@ export const CreateVectorTableSheet = ({ bucketName }: CreateVectorTableSheetPro
|
||||
serverName: wrapperInstance.server_name,
|
||||
sourceSchema: schema,
|
||||
targetSchema: schema,
|
||||
schemaOptions: [`bucket_name '${bucketName}'`],
|
||||
schemaOptions: [safeSql`bucket_name ${literal(bucketName)}`],
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
+4
-1
@@ -1,4 +1,5 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { literal, safeSql } from '@supabase/pg-meta'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useParams } from 'common'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
@@ -115,7 +116,9 @@ export const InitializeForeignSchemaDialog = () => {
|
||||
serverName: wrapperInstance.server_name,
|
||||
sourceSchema: updatedImportForeignSchemaSyntax ? bucketId : values.schema,
|
||||
targetSchema: values.schema,
|
||||
schemaOptions: updatedImportForeignSchemaSyntax ? undefined : [`bucket_name '${bucketId}'`],
|
||||
schemaOptions: updatedImportForeignSchemaSyntax
|
||||
? undefined
|
||||
: [safeSql`bucket_name ${literal(bucketId)}`],
|
||||
})
|
||||
|
||||
toast.success(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getCreateFDWSql } from '@supabase/pg-meta'
|
||||
import { getCreateFDWSql, type SafeSqlFragment } from '@supabase/pg-meta'
|
||||
import { wrapWithTransaction } from '@supabase/pg-meta/src/query'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
@@ -23,7 +23,7 @@ export type FDWCreateVariables = {
|
||||
tables: any[]
|
||||
sourceSchema: string
|
||||
targetSchema: string
|
||||
schemaOptions?: string[]
|
||||
schemaOptions?: SafeSqlFragment[]
|
||||
}
|
||||
|
||||
export async function createFDW({ projectRef, connectionString, ...rest }: FDWCreateVariables) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getImportForeignSchemaSql } from '@supabase/pg-meta'
|
||||
import { getImportForeignSchemaSql, type SafeSqlFragment } from '@supabase/pg-meta'
|
||||
import { wrapWithTransaction } from '@supabase/pg-meta/src/query'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
@@ -16,7 +16,7 @@ export type FDWImportForeignSchemaVariables = {
|
||||
serverName: string
|
||||
sourceSchema: string
|
||||
targetSchema: string
|
||||
schemaOptions?: string[]
|
||||
schemaOptions?: SafeSqlFragment[]
|
||||
}
|
||||
|
||||
export async function importForeignSchema({
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import {
|
||||
ident,
|
||||
joinSqlFragments,
|
||||
keyword,
|
||||
literal,
|
||||
safeSql,
|
||||
type SafeSqlFragment,
|
||||
} from '../../../pg-format'
|
||||
|
||||
type SimplifiedWrapperMeta = {
|
||||
handlerName: string
|
||||
validatorName: string
|
||||
server: { options: { name: string; encrypted: boolean }[] }
|
||||
}
|
||||
|
||||
export const getFDWsSql = () => {
|
||||
const sql = /* SQL */ `
|
||||
export const getFDWsSql = (): SafeSqlFragment => {
|
||||
const sql = safeSql`
|
||||
select
|
||||
s.oid as "id",
|
||||
w.fdwname as "name",
|
||||
@@ -58,20 +67,27 @@ export function getCreateFDWSql({
|
||||
}
|
||||
// If mode is skip, the wrapper will skip the last step, binding the schema/tables to foreign data. This could be done later.
|
||||
mode: 'tables' | 'schema' | 'skip'
|
||||
tables: any[]
|
||||
tables: {
|
||||
is_new_schema: boolean
|
||||
schema_name: string
|
||||
table_name: string
|
||||
columns: { name: string; type: string }[]
|
||||
}[]
|
||||
sourceSchema: string
|
||||
targetSchema: string
|
||||
schemaOptions?: string[]
|
||||
}) {
|
||||
const newSchemasSql = tables
|
||||
.filter((table) => table.is_new_schema)
|
||||
.map((table) => /* SQL */ `create schema if not exists ${table.schema_name};`)
|
||||
.join('\n')
|
||||
schemaOptions?: SafeSqlFragment[]
|
||||
}): SafeSqlFragment {
|
||||
const newSchemasSql = joinSqlFragments(
|
||||
tables
|
||||
.filter((table) => table.is_new_schema)
|
||||
.map((table) => safeSql`create schema if not exists ${ident(table.schema_name)};`),
|
||||
'\n'
|
||||
)
|
||||
|
||||
const createWrapperSql = /* SQL */ `
|
||||
create foreign data wrapper "${formState.wrapper_name}"
|
||||
handler "${wrapperMeta.handlerName}"
|
||||
validator "${wrapperMeta.validatorName}";
|
||||
const createWrapperSql = safeSql`
|
||||
create foreign data wrapper ${ident(formState.wrapper_name)}
|
||||
handler ${ident(wrapperMeta.handlerName)}
|
||||
validator ${ident(wrapperMeta.validatorName)};
|
||||
`
|
||||
|
||||
const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted)
|
||||
@@ -79,10 +95,9 @@ export function getCreateFDWSql({
|
||||
|
||||
const createEncryptedKeysSqlArray = encryptedOptions.map((option) => {
|
||||
const key = `${formState.wrapper_name}_${option.name}`
|
||||
// Escape single quotes in postgresql by doubling them up
|
||||
const value = (formState[option.name] || '').replace(/'/g, "''")
|
||||
const quotedValue = literal(formState[option.name] || '')
|
||||
|
||||
return /* SQL */ `
|
||||
return safeSql`
|
||||
do $$
|
||||
begin
|
||||
-- Old wrappers has an implicit dependency on pgsodium. For new wrappers
|
||||
@@ -118,43 +133,53 @@ export function getCreateFDWSql({
|
||||
create extension if not exists pgsodium;
|
||||
|
||||
perform pgsodium.create_key(
|
||||
name := '${key}'
|
||||
name := ${literal(key)}
|
||||
);
|
||||
|
||||
perform vault.create_secret(
|
||||
new_secret := '${value}',
|
||||
new_name := '${key}',
|
||||
new_key_id := (select id from pgsodium.valid_key where name = '${key}')
|
||||
new_secret := ${quotedValue},
|
||||
new_name := ${literal(key)},
|
||||
new_key_id := (select id from pgsodium.valid_key where name = ${literal(key)})
|
||||
);
|
||||
else
|
||||
perform vault.create_secret(
|
||||
new_secret := '${value}',
|
||||
new_name := '${key}'
|
||||
new_secret := ${quotedValue},
|
||||
new_name := ${literal(key)}
|
||||
);
|
||||
end if;
|
||||
end $$;
|
||||
`
|
||||
})
|
||||
|
||||
const createEncryptedKeysSql = createEncryptedKeysSqlArray.join('\n')
|
||||
const createEncryptedKeysSql = joinSqlFragments(createEncryptedKeysSqlArray, '\n')
|
||||
|
||||
const encryptedOptionsSqlArray = encryptedOptions
|
||||
.filter((option) => formState[option.name])
|
||||
.map((option) => `${option.name} ''%s''`)
|
||||
.map((option) => safeSql`${ident(option.name)} ''%s''`)
|
||||
const unencryptedOptionsSqlArray = unencryptedOptions
|
||||
.filter((option) => formState[option.name])
|
||||
// wrap all option names in double quotes to handle dots
|
||||
// wrap all options values in single quotes, replace single quotes with 4 single quotes to escape them in SQL past the execute format
|
||||
.map((option) => `"${option.name}" ''${formState[option.name].replace(/'/g, `''''`)}''`)
|
||||
const optionsSqlArray = [...encryptedOptionsSqlArray, ...unencryptedOptionsSqlArray].join(',')
|
||||
.map((option) => {
|
||||
// literal() returns 'value' with single quotes. Escape those quotes for
|
||||
// the surrounding E'...' string context ('' represents one ')
|
||||
const escapedValue = literal(formState[option.name]).replace(/'/g, "''") as SafeSqlFragment
|
||||
|
||||
const createServerSql = /* SQL */ `
|
||||
return safeSql`${ident(option.name)} ${escapedValue}`
|
||||
})
|
||||
const optionsSqlArray = joinSqlFragments(
|
||||
[...encryptedOptionsSqlArray, ...unencryptedOptionsSqlArray],
|
||||
','
|
||||
)
|
||||
|
||||
const createServerSql = safeSql`
|
||||
do $$
|
||||
declare
|
||||
-- Old wrappers has an implicit dependency on pgsodium. For new wrappers
|
||||
-- we use Vault directly.
|
||||
is_using_old_wrappers bool;
|
||||
${encryptedOptions.map((option) => `v_${option.name} text;`).join('\n')}
|
||||
${joinSqlFragments(
|
||||
encryptedOptions.map((option) => safeSql`${ident(`v_${option.name}`)} text;`),
|
||||
'\n'
|
||||
)}
|
||||
begin
|
||||
is_using_old_wrappers := (select extversion from pg_extension where extname = 'wrappers') in (
|
||||
'0.1.0',
|
||||
@@ -184,65 +209,73 @@ export function getCreateFDWSql({
|
||||
'0.4.4',
|
||||
'0.4.5'
|
||||
);
|
||||
${encryptedOptions
|
||||
.map(
|
||||
(option) => /* SQL */ `
|
||||
${joinSqlFragments(
|
||||
encryptedOptions.map(
|
||||
(option) => safeSql`
|
||||
if is_using_old_wrappers then
|
||||
select id into v_${option.name} from pgsodium.valid_key where name = '${formState.wrapper_name}_${option.name}' limit 1;
|
||||
select id into ${ident(`v_${option.name}`)} from pgsodium.valid_key where name = ${literal(`${formState.wrapper_name}_${option.name}`)} limit 1;
|
||||
else
|
||||
select id into v_${option.name} from vault.secrets where name = '${formState.wrapper_name}_${option.name}' limit 1;
|
||||
select id into ${ident(`v_${option.name}`)} from vault.secrets where name = ${literal(`${formState.wrapper_name}_${option.name}`)} limit 1;
|
||||
end if;
|
||||
`
|
||||
)
|
||||
.join('\n')}
|
||||
),
|
||||
'\n'
|
||||
)}
|
||||
|
||||
execute format(
|
||||
E'create server "${formState.server_name}" foreign data wrapper "${formState.wrapper_name}" options (${optionsSqlArray});',
|
||||
${encryptedOptions
|
||||
.filter((option) => formState[option.name])
|
||||
.map((option) => `v_${option.name}`)
|
||||
.join(',\n')}
|
||||
E'create server ${ident(formState.server_name)} foreign data wrapper ${ident(formState.wrapper_name)} options (${optionsSqlArray});',
|
||||
${joinSqlFragments(
|
||||
encryptedOptions
|
||||
.filter((option) => formState[option.name])
|
||||
.map((option) => ident(`v_${option.name}`)),
|
||||
','
|
||||
)}
|
||||
);
|
||||
end $$;
|
||||
`
|
||||
|
||||
const createTablesSql = tables
|
||||
.map((newTable) => {
|
||||
const columns = newTable.columns as {
|
||||
name: string
|
||||
type: string
|
||||
}[]
|
||||
const createTablesSql = joinSqlFragments(
|
||||
tables.map((newTable) => {
|
||||
const columns = newTable.columns
|
||||
|
||||
return /* SQL */ `
|
||||
create foreign table "${newTable.schema_name}"."${newTable.table_name}" (
|
||||
${columns.map((column) => `"${column.name}" ${column.type}`).join(',\n ')}
|
||||
return safeSql`
|
||||
create foreign table ${ident(newTable.schema_name)}.${ident(newTable.table_name)} (
|
||||
${joinSqlFragments(
|
||||
columns.map((column) => safeSql`${ident(column.name)} ${keyword(column.type)}`),
|
||||
','
|
||||
)}
|
||||
)
|
||||
server ${formState.server_name}
|
||||
server ${ident(formState.server_name)}
|
||||
options (
|
||||
${Object.entries(newTable)
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
key !== 'table_name' &&
|
||||
key !== 'schema_name' &&
|
||||
key !== 'columns' &&
|
||||
key !== 'index' &&
|
||||
key !== 'is_new_schema' &&
|
||||
Boolean(value)
|
||||
)
|
||||
.map(([key, value]) => `${key} '${value}'`)
|
||||
.join(',\n ')}
|
||||
${joinSqlFragments(
|
||||
Object.entries(newTable)
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
key !== 'table_name' &&
|
||||
key !== 'schema_name' &&
|
||||
key !== 'columns' &&
|
||||
key !== 'index' &&
|
||||
key !== 'is_new_schema' &&
|
||||
Boolean(value)
|
||||
)
|
||||
.map(([key, value]) => safeSql`${ident(key)} ${literal(value)}`),
|
||||
','
|
||||
)}
|
||||
);
|
||||
`
|
||||
})
|
||||
.join('\n\n')
|
||||
}),
|
||||
'\n\n'
|
||||
)
|
||||
|
||||
const options = [...schemaOptions, "strict 'true'"].join(', ')
|
||||
const options = joinSqlFragments([...schemaOptions, safeSql`strict 'true'`], ', ')
|
||||
|
||||
const importForeignSchemaSql = /* SQL */ `
|
||||
import foreign schema "${sourceSchema}" from server ${formState.server_name} into ${targetSchema} options (${options});
|
||||
function createImportForeignSchemaSql(): SafeSqlFragment {
|
||||
return safeSql`
|
||||
import foreign schema ${ident(sourceSchema)} from server ${ident(formState.server_name)} into ${ident(targetSchema)} options (${options});
|
||||
`
|
||||
}
|
||||
|
||||
const sql = /* SQL */ `
|
||||
const sql = safeSql`
|
||||
${newSchemasSql}
|
||||
|
||||
${createWrapperSql}
|
||||
@@ -251,9 +284,9 @@ export function getCreateFDWSql({
|
||||
|
||||
${createServerSql}
|
||||
|
||||
${mode === 'tables' ? createTablesSql : ''}
|
||||
${mode === 'tables' ? createTablesSql : safeSql``}
|
||||
|
||||
${mode === 'schema' ? importForeignSchemaSql : ''}
|
||||
${mode === 'schema' ? createImportForeignSchemaSql() : safeSql``}
|
||||
`
|
||||
|
||||
return sql
|
||||
@@ -265,13 +298,13 @@ export const getDeleteFDWSql = ({
|
||||
}: {
|
||||
wrapper: { name: string }
|
||||
wrapperMeta: SimplifiedWrapperMeta
|
||||
}) => {
|
||||
}): SafeSqlFragment => {
|
||||
const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted)
|
||||
|
||||
const deleteEncryptedSecretsSqlArray = encryptedOptions.map((option) => {
|
||||
const key = `${wrapper.name}_${option.name}`
|
||||
|
||||
return /* SQL */ `
|
||||
return safeSql`
|
||||
do $$
|
||||
begin
|
||||
-- Old wrappers has an implicit dependency on pgsodium. For new wrappers
|
||||
@@ -304,20 +337,20 @@ export const getDeleteFDWSql = ({
|
||||
'0.4.4',
|
||||
'0.4.5'
|
||||
) then
|
||||
delete from vault.secrets where key_id = (select id from pgsodium.valid_key where name = '${key}');
|
||||
delete from vault.secrets where key_id = (select id from pgsodium.valid_key where name = ${literal(key)});
|
||||
|
||||
delete from pgsodium.key where name = '${key}';
|
||||
delete from pgsodium.key where name = ${literal(key)};
|
||||
else
|
||||
delete from vault.secrets where name = '${key}';
|
||||
delete from vault.secrets where name = ${literal(key)};
|
||||
end if;
|
||||
end $$;
|
||||
`
|
||||
})
|
||||
|
||||
const deleteEncryptedSecretsSql = deleteEncryptedSecretsSqlArray.join('\n')
|
||||
const deleteEncryptedSecretsSql = joinSqlFragments(deleteEncryptedSecretsSqlArray, '\n')
|
||||
|
||||
const sql = /* SQL */ `
|
||||
drop foreign data wrapper if exists "${wrapper.name}" cascade;
|
||||
const sql = safeSql`
|
||||
drop foreign data wrapper if exists ${ident(wrapper.name)} cascade;
|
||||
|
||||
${deleteEncryptedSecretsSql}
|
||||
`
|
||||
@@ -335,7 +368,7 @@ export const getUpdateFDWSql = ({
|
||||
wrapperMeta: SimplifiedWrapperMeta
|
||||
formState: { [k: string]: string }
|
||||
tables: any[]
|
||||
}) => {
|
||||
}): SafeSqlFragment => {
|
||||
const deleteWrapperSql = getDeleteFDWSql({ wrapper, wrapperMeta })
|
||||
const createWrapperSql = getCreateFDWSql({
|
||||
wrapperMeta,
|
||||
@@ -346,7 +379,7 @@ export const getUpdateFDWSql = ({
|
||||
targetSchema: '',
|
||||
})
|
||||
|
||||
const sql = /* SQL */ `
|
||||
const sql = safeSql`
|
||||
${deleteWrapperSql}
|
||||
|
||||
${createWrapperSql}
|
||||
@@ -364,20 +397,26 @@ export function getImportForeignSchemaSql({
|
||||
serverName: string
|
||||
sourceSchema: string
|
||||
targetSchema: string
|
||||
schemaOptions?: string[]
|
||||
}) {
|
||||
const options = [...schemaOptions, "strict 'true'"].join(', ')
|
||||
schemaOptions?: SafeSqlFragment[]
|
||||
}): SafeSqlFragment {
|
||||
const options = joinSqlFragments([...schemaOptions, safeSql`strict 'true'`], ', ')
|
||||
|
||||
const sql = /* SQL */ `
|
||||
import foreign schema "${sourceSchema}" from server ${serverName} into ${targetSchema} options (${options});
|
||||
const sql = safeSql`
|
||||
import foreign schema ${ident(sourceSchema)} from server ${ident(serverName)} into ${ident(targetSchema)} options (${options});
|
||||
`
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export function getDropForeignTableSql({ schema, table }: { schema: string; table: string }) {
|
||||
const sql = /* SQL */ `
|
||||
drop foreign table if exists "${schema}"."${table}";
|
||||
export function getDropForeignTableSql({
|
||||
schema,
|
||||
table,
|
||||
}: {
|
||||
schema: string
|
||||
table: string
|
||||
}): SafeSqlFragment {
|
||||
const sql = safeSql`
|
||||
drop foreign table if exists ${ident(schema)}.${ident(table)};
|
||||
`
|
||||
|
||||
return sql
|
||||
|
||||
Reference in new issue
Block a user