Files
supabase/packages/pg-meta/test/sql/studio/fdw.test.ts
Jordi Enric f0952fdef8 fix(studio): delete only the selected foreign server FE-4462 (#50785)
## Problem

The dashboard lists one row per foreign server, but deleting a row
dropped its foreign data wrapper with CASCADE. When multiple servers
shared a wrapper, deleting one removed all of them.

## Fix

Drop the selected server and its foreign tables. Remove the underlying
wrapper and Vault secret only when no servers still use it. Edits to a
shared wrapper now stop before making changes because the existing edit
flow recreates the underlying wrapper.

## How to test

1. Configure two BigQuery foreign servers that use the same foreign data
wrapper. Delete one from the dashboard.
2. Confirm the other server and its foreign tables still exist and work.
3. Delete the remaining server. Confirm the foreign data wrapper and its
Vault secret are removed.
4. Attempt to edit one of two servers sharing a wrapper. Confirm the
edit fails without removing either server.

Focused pg-meta tests and typecheck pass.

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

* **New Features**
* Shared connections are identified in the integrations list, with
guidance for editing them in the SQL Editor. Editing is disabled when a
wrapper is shared, with an explanation shown.
* Deleting a connection removes its foreign tables and removes the
wrapper and Vault secret only when no other connection uses them.
* **Bug Fixes**
* Connection deletion verifies that the selected server still belongs to
the wrapper and reports failures using connection-focused wording.
* Attempts to edit a wrapper used by another connection are blocked with
a clear explanation.
* Connection deletion and confirmation messages now consistently refer
to deleting a connection.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-25 16:49:01 +02:00

129 lines
4.7 KiB
TypeScript

import { expect, test } from 'vitest'
import { getCreateFDWSql, getDeleteFDWSql, getUpdateFDWSql } from '../../../src'
import { literal } from '../../../src/pg-format'
const baseArgs = {
mode: 'skip' as const,
tables: [],
sourceSchema: '',
targetSchema: '',
}
// A value with both a backslash and a single quote. literal() escapes it to a
// single E'...' literal; the old code then re-escaped the quotes and embedded
// it in the outer E'...' string, decoding the backslash twice.
const trickyValue = "ab\\cd'ef"
test('unencrypted server option values are passed as format() %L arguments', () => {
const sql = getCreateFDWSql({
...baseArgs,
wrapperMeta: {
handlerName: 'wasm_fdw_handler',
validatorName: 'wasm_fdw_validator',
server: { options: [{ name: 'api_key', encrypted: false }] },
},
formState: { wrapper_name: 'my_wrapper', server_name: 'my_server', api_key: trickyValue },
})
// The option is emitted as a %L placeholder so format() escapes the value once.
expect(sql).toContain('api_key %L')
// The raw value reaches format() as a single-level literal() argument.
expect(sql).toContain(literal(trickyValue))
// Regression: the value must NOT be embedded as a double-escaped literal in the
// outer E'...' string (literal(value).replace(/'/g, "''")), which corrupted
// backslashes or aborted creation with "invalid Unicode escape".
const doubleEscaped = literal(trickyValue).replace(/'/g, "''")
expect(sql).not.toContain(doubleEscaped)
})
test('encrypted server options still resolve their value through Vault unchanged', () => {
const sql = getCreateFDWSql({
...baseArgs,
wrapperMeta: {
handlerName: 'wasm_fdw_handler',
validatorName: 'wasm_fdw_validator',
server: { options: [{ name: 'api_secret', encrypted: true }] },
},
formState: { wrapper_name: 'my_wrapper', server_name: 'my_server', api_secret: 'shh' },
})
// Encrypted options keep the ''%s'' placeholder filled by the vault secret id.
expect(sql).toContain("api_secret ''%s''")
expect(sql).toContain('vault.create_secret')
})
test('deleting a wrapper row drops its server and preserves shared wrapper dependencies', () => {
const sql = getDeleteFDWSql({
wrapper: { id: 42, name: 'bigquery_fdw', server_name: 'selected_bigquery_server' },
wrapperMeta: {
handlerName: 'big_query_fdw_handler',
validatorName: 'big_query_fdw_validator',
server: { options: [{ name: 'sa_key_id', encrypted: true }] },
},
})
expect(sql).toContain('where s.oid = 42')
expect(sql).toContain("and s.srvname = ''selected_bigquery_server''")
expect(sql).toContain("and w.fdwname = ''bigquery_fdw''")
expect(sql.indexOf('raise exception')).toBeLessThan(sql.indexOf('drop server'))
expect(sql).toContain('drop server if exists selected_bigquery_server cascade')
expect(sql).toContain("where w.fdwname = 'bigquery_fdw'")
expect(sql).toContain("execute format('drop foreign data wrapper if exists %I cascade'")
expect(sql).not.toContain('drop foreign data wrapper if exists "bigquery_fdw" cascade')
expect(sql).toContain("where fdwname = 'bigquery_fdw'")
})
test('editing a shared wrapper fails before any server is dropped', () => {
const sql = getUpdateFDWSql({
wrapper: { id: 42, name: 'bigquery_fdw', server_name: 'selected_bigquery_server' },
wrapperMeta: {
handlerName: 'big_query_fdw_handler',
validatorName: 'big_query_fdw_validator',
server: { options: [] },
},
formState: { wrapper_name: 'bigquery_fdw', server_name: 'selected_bigquery_server' },
tables: [],
})
expect(sql).toContain("s.srvname <> 'selected_bigquery_server'")
expect(sql.indexOf('raise exception')).toBeLessThan(sql.indexOf('drop server'))
})
test('editing an existing foreign table excludes catalog fields from its options', () => {
const sql = getUpdateFDWSql({
wrapper: { id: 42, name: 'bigquery_fdw', server_name: 'bigquery_server' },
wrapperMeta: {
handlerName: 'big_query_fdw_handler',
validatorName: 'big_query_fdw_validator',
server: { options: [{ name: 'project_id', encrypted: false }] },
},
formState: {
wrapper_name: 'bigquery_fdw',
server_name: 'bigquery_server',
project_id: 'example-project',
},
tables: [
{
id: 171639,
schema: 'qa_bq',
schema_name: 'qa_bq',
table_name: 'orders',
columns: [{ name: 'id', type: 'text' }],
is_new_schema: false,
index: 0,
table: 'orders',
location: 'US',
},
],
})
expect(sql).toContain('create foreign table qa_bq.orders')
expect(sql).toContain('"table" \'orders\'')
expect(sql).toContain("location 'US'")
expect(sql).not.toContain('id 171639')
expect(sql).not.toContain("schema 'qa_bq'")
})