mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
29493e02d0
For Multigres (HA) projects you can't connect to read replicas directly — reads go through a read-only load balancer on the primary's host at port 5433. Since #44695 stripped the pooler UI, HA projects showed no source option at all in the Connect dialog and still prompted for the IPv4 add-on. This surfaces it as a first-class, clearly-labeled read-only source. In the UI it's labeled `Replica (read-only)` rather than "load balancer" — the primary goes through the same gateway, so "load balancer" would be confusing from a product perspective (internally the `load-balancer` source identifier and `HIGH_AVAILABILITY_LOAD_BALANCER_PORT` constant keep their names). <img width="883" height="342" alt="Screenshot 2026-08-24 at 11 32 26 PM" src="https://github.com/user-attachments/assets/3716f6dd-0325-4b9d-adbc-9ece9244de62" /> **Added:** - Source select for HA projects in the Direct tab: `Primary database` + `Replica (read-only)` (individual replica rows are filtered out — they're only reachable via the load balancer) - Replica (load balancer) connection strings on all 9 connection types: primary host, port `5433`, with the Multigres-required `sslmode=require&sslnegotiation=direct` params (JDBC gets the `sslNegotiation` spelling, .NET gets `SSL Negotiation=Direct`) - `Read-only` badge on the connection code block + note pointing writes at the primary - Programmatic labels for the ConnectSheet select/switch/multi-select fields (the Source combobox previously had no accessible name) **Changed:** - The generated-file step (Node.js/Golang/.NET/Python/SQLAlchemy) is now source-aware — it previously ignored the Source selection entirely (also affected read replicas on normal projects) and silently rendered the primary's connection info - .NET template now emits `Port=` (Npgsql defaults to 5432 when omitted) and the install step actually installs Npgsql (pinned 9.0.5 — `SSL Negotiation` requires 9+) - SQLAlchemy `DATABASE_URL` merges `sslmode=require` into the string's existing query params instead of a hardcoded suffix that could drop TLS - Source option labels normalized to sentence case (`Primary database`, `Read replica (…)`) - `MultipleCodeBlock` (ui-patterns) accepts an optional `className` - HA coercion in `useConnectState` extended: a stale replica `connectionSource` restored from URL/localStorage falls back to the primary **Removed:** - IPv4 add-on admonition for HA projects (the forced-direct method was tripping it; the add-on doesn't apply to Multigres) Out of scope (needs platform work): SQL editor / Data API / other `DatabaseSelector` surfaces — executing against the load balancer requires a platform-issued connection string, and the load-balancers API only returns a REST endpoint today. The `5433` port is a client-side constant (`HIGH_AVAILABILITY_LOAD_BALANCER_PORT`) until the API exposes it. ## To test On an HA (Multigres) project: - Open Connect → Direct: Source shows exactly `Primary database` and `Replica (read-only)`; selecting the replica shows `…@<primary-host>:5433/postgres?sslmode=require&sslnegotiation=direct`, a `Read-only` badge, and the read-only note - Cycle all 9 connection types with the replica selected — every snippet carries port 5433 (`.NET` includes `Port=5433;…;SSL Negotiation=Direct`), badge/note persist - No "Enable IPv4 add-on" admonition anywhere in the Direct tab - Switch tabs / hard-reload: source resets to primary with no stale badge/string combos On a normal project: - Direct tab unchanged: no `Replica (read-only)` option, pooler badges and IPv4 admonitions behave as before, `.NET` now shows `Port=5432` and no `SSL Negotiation` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added read-only load-balancer connection options for high-availability projects. - Added .NET and SQLAlchemy connection examples with required SSL settings. - Added clear read-only labels and notices explaining write restrictions. - **Bug Fixes** - Suppressed IPv4 add-on notices for high-availability connections. - Improved connection-source selection and restored-setting handling. - Improved connection form identification and accessibility. - **Style** - Added customizable styling support for multi-code-block displays. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
155 lines
5.0 KiB
TypeScript
155 lines
5.0 KiB
TypeScript
import type { ConnectionStringMethod } from './Connect.constants'
|
|
import type { ConnectionStringPooler } from './Connect.types'
|
|
|
|
export const DEFAULT_PORT = '5432'
|
|
export const PASSWORD_PLACEHOLDER = '[YOUR-PASSWORD]'
|
|
|
|
/** Appends query params to a connection string, joining with `?` or `&` as needed */
|
|
export const appendConnectionStringParams = (uri: string, params: string) =>
|
|
!uri || !params ? uri : `${uri}${uri.includes('?') ? '&' : '?'}${params}`
|
|
|
|
export type ConnectionParams = {
|
|
host: string
|
|
port: string
|
|
user: string
|
|
database: string
|
|
/** Raw query string including the leading `?`, or '' when the URI has none */
|
|
search: string
|
|
}
|
|
|
|
export const resolveConnectionString = ({
|
|
connectionMethod,
|
|
useSharedPooler,
|
|
connectionStringPooler,
|
|
}: {
|
|
connectionMethod: ConnectionStringMethod
|
|
useSharedPooler: boolean
|
|
connectionStringPooler: ConnectionStringPooler | undefined
|
|
}) => {
|
|
if (!connectionStringPooler) return ''
|
|
|
|
if (connectionMethod === 'direct') {
|
|
return connectionStringPooler.direct ?? ''
|
|
}
|
|
|
|
if (connectionMethod === 'session') {
|
|
return connectionStringPooler.sessionShared ?? ''
|
|
}
|
|
|
|
if (useSharedPooler || !connectionStringPooler.transactionDedicated) {
|
|
return connectionStringPooler.transactionShared ?? ''
|
|
}
|
|
|
|
return connectionStringPooler.transactionDedicated ?? ''
|
|
}
|
|
|
|
export const parseConnectionParams = (connectionString: string): ConnectionParams => {
|
|
if (!connectionString) {
|
|
return {
|
|
host: 'hidden',
|
|
port: DEFAULT_PORT,
|
|
user: 'hidden',
|
|
database: 'hidden',
|
|
search: '',
|
|
}
|
|
}
|
|
|
|
try {
|
|
const parsed = new URL(connectionString)
|
|
// The URL parser percent-encodes characters that aren't valid in user-info
|
|
// (e.g. brackets in the self-hosted `postgres.[POOLER_TENANT_ID]` placeholder).
|
|
// Decode so the displayed string matches the literal we wrote.
|
|
const decode = (value: string) => {
|
|
try {
|
|
return decodeURIComponent(value)
|
|
} catch {
|
|
return value
|
|
}
|
|
}
|
|
return {
|
|
host: parsed.hostname || 'hidden',
|
|
port: parsed.port || DEFAULT_PORT,
|
|
user: parsed.username ? decode(parsed.username) : 'hidden',
|
|
database: parsed.pathname?.replace(/^\//, '') || 'hidden',
|
|
search: parsed.search,
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
host: 'hidden',
|
|
port: DEFAULT_PORT,
|
|
user: 'hidden',
|
|
database: 'hidden',
|
|
search: '',
|
|
}
|
|
}
|
|
}
|
|
|
|
export const buildSafeConnectionString = (
|
|
connectionString: string,
|
|
params: ConnectionParams
|
|
): string => {
|
|
if (!connectionString) return ''
|
|
|
|
return `postgresql://${params.user}:${PASSWORD_PLACEHOLDER}@${params.host}:${params.port}/${params.database}${params.search}`
|
|
}
|
|
|
|
export const buildPsqlCommand = (params: ConnectionParams) =>
|
|
params.search
|
|
? // Query params (e.g. sslmode) can't be expressed as psql flags, so fall
|
|
// back to the URI form — psql prompts for the password.
|
|
`psql "postgresql://${params.user}@${params.host}:${params.port}/${params.database}${params.search}"`
|
|
: `psql -h ${params.host} -p ${params.port} -d ${params.database} -U ${params.user}`
|
|
|
|
export const buildJdbcString = (params: ConnectionParams) => {
|
|
// pgJDBC (42.7.4+) spells libpq's `sslnegotiation` as `sslNegotiation`
|
|
const extraParams = params.search
|
|
? `&${params.search.slice(1).replace('sslnegotiation=', 'sslNegotiation=')}`
|
|
: ''
|
|
return `jdbc:postgresql://${params.host}:${params.port}/${params.database}?user=${params.user}&password=${PASSWORD_PLACEHOLDER}${extraParams}`
|
|
}
|
|
|
|
/**
|
|
* Ensures a connection string's query params carry `sslmode=require` without
|
|
* dropping params the URI already has (e.g. `options=reference%3D...` or
|
|
* `sslnegotiation=direct`).
|
|
*/
|
|
export const withRequiredSslmode = (search: string) => {
|
|
if (!search) return '?sslmode=require'
|
|
if (search.includes('sslmode=')) return search
|
|
return `${search}&sslmode=require`
|
|
}
|
|
|
|
export const buildDotnetConnectionString = (params: ConnectionParams) => {
|
|
// Multigres only accepts direct SSL negotiation; Npgsql (9+) spells it
|
|
// `SSL Negotiation=Direct` and throws on the parameter in older versions,
|
|
// so only emit it when the resolved URI carries the param.
|
|
const sslNegotiation = params.search.includes('sslnegotiation=direct')
|
|
? ';SSL Negotiation=Direct'
|
|
: ''
|
|
return `Host=${params.host};Port=${params.port};Database=${params.database};Username=${params.user};Password=${PASSWORD_PLACEHOLDER};SSL Mode=Require;Trust Server Certificate=true${sslNegotiation}`
|
|
}
|
|
|
|
export const buildConnectionStringWithPassword = (
|
|
connectionString: string,
|
|
password: string
|
|
): string => {
|
|
if (!connectionString || !password) return connectionString
|
|
|
|
const encodedPassword = (() => {
|
|
try {
|
|
return encodeURIComponent(password)
|
|
} catch {
|
|
return password
|
|
}
|
|
})()
|
|
|
|
return connectionString.split(PASSWORD_PLACEHOLDER).join(encodedPassword)
|
|
}
|
|
|
|
export const buildConnectionParameters = (params: ConnectionParams) => [
|
|
{ key: 'host', value: params.host },
|
|
{ key: 'port', value: params.port },
|
|
{ key: 'database', value: params.database },
|
|
{ key: 'user', value: params.user },
|
|
]
|