Files
supabase/packages/pg-meta/test/sql/studio/fdw.test.ts
oniani1 99db104cf5 fix(pg-meta): escape unencrypted FDW server options via format() %L (#47014)
Closes #47012

## What kind of change does this PR introduce?

Bug fix.

## What is the current behavior?

Unencrypted FDW server option values are pre-escaped with
`literal(value).replace(/'/g, "''")` and embedded inside the outer
`create server` `E'...'` string built by `format()`. That nests the
value inside two `E'...'` literals, so backslashes are decoded twice. A
value like `domain\user` aborts wrapper creation with `invalid Unicode
escape`, and `p@ss\w0rd` is silently stored as `p@ssw0rd`.

## What is the new behavior?

Unencrypted option values are passed as `format()` `%L` arguments, the
same way the encrypted options already supply their secret id, so
Postgres escapes each value exactly once.

Before and after, on postgres:16:

| Value | Before | After |
| --- | --- | --- |
| `domain\user` | aborts (invalid Unicode escape) | stored `domain\user`
|
| `p@ss\w0rd` | stored `p@ssw0rd` | stored `p@ss\w0rd` |
| `a\b` | stored `a`+backspace | stored `a\b` |

## Additional context

Added a unit test in `packages/pg-meta/test/sql/studio/fdw.test.ts`
asserting unencrypted options use a `%L` placeholder with the value as a
`format()` argument, and that the old double-escaped form is gone. The
encrypted-option path is unchanged.


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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of special characters in foreign data wrapper server
option values.

* **Tests**
* Added test coverage for foreign data wrapper configuration, including
edge cases with special characters.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-22 15:45:56 +08:00

57 lines
2.0 KiB
TypeScript

import { expect, test } from 'vitest'
import { getCreateFDWSql } 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')
})