Files
Alaister Young 29493e02d0 [FE-4010] feat(studio): add read-only replica connection option for HA projects (#49485)
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>
2026-08-28 10:43:55 +01:00

154 lines
3.6 KiB
TypeScript

export type Example = {
installCommands?: string[]
files?: {
name: string
content: string
}[]
}
const examples = {
nodejs: {
installCommands: ['npm install postgres'],
files: [
{
name: 'db.js',
content: `import postgres from 'postgres'
const connectionString = process.env.DATABASE_URL
const sql = postgres(connectionString)
export default sql`,
},
],
},
golang: {
installCommands: ['go get github.com/jackc/pgx/v5'],
files: [
{
name: 'main.go',
content: `package main
import (
"context"
"log"
"os"
"github.com/jackc/pgx/v5"
)
func main() {
conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("Failed to connect to the database: %v", err)
}
defer conn.Close(context.Background())
// Example query to test connection
var version string
if err := conn.QueryRow(context.Background(), "SELECT version()").Scan(&version); err != nil {
log.Fatalf("Query failed: %v", err)
}
log.Println("Connected to:", version)
}`,
},
],
},
dotnet: {
installCommands: [
// SSL Negotiation=Direct in the generated connection string requires Npgsql 9.0+.
// Concrete version: quoting a floating version breaks on Windows cmd, and
// floating versions fail under Central Package Management (NU1011).
'dotnet add package Npgsql --version 9.0.5',
'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION',
],
},
python: {
installCommands: ['pip install python-dotenv psycopg2'],
files: [
{
name: 'main.py',
content: `import psycopg2
from dotenv import load_dotenv
import os
# Load environment variables from .env
load_dotenv()
# Fetch variables
USER = os.getenv("user")
PASSWORD = os.getenv("password")
HOST = os.getenv("host")
PORT = os.getenv("port")
DBNAME = os.getenv("dbname")
# Connect to the database
try:
connection = psycopg2.connect(
user=USER,
password=PASSWORD,
host=HOST,
port=PORT,
dbname=DBNAME
)
print("Connection successful!")
# Create a cursor to execute SQL queries
cursor = connection.cursor()
# Example query
cursor.execute("SELECT NOW();")
result = cursor.fetchone()
print("Current Time:", result)
# Close the cursor and connection
cursor.close()
connection.close()
print("Connection closed.")
except Exception as e:
print(f"Failed to connect: {e}")`,
},
],
},
sqlalchemy: {
installCommands: ['pip install python-dotenv sqlalchemy psycopg2'],
files: [
{
name: 'main.py',
content: `from sqlalchemy import create_engine
# from sqlalchemy.pool import NullPool
from dotenv import load_dotenv
import os
# Load environment variables from .env
load_dotenv()
# Fetch variables
USER = os.getenv("user")
PASSWORD = os.getenv("password")
HOST = os.getenv("host")
PORT = os.getenv("port")
DBNAME = os.getenv("dbname")
# Construct the SQLAlchemy connection string
DATABASE_URL = f"postgresql+psycopg2://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}?sslmode=require"
# Create the SQLAlchemy engine
engine = create_engine(DATABASE_URL)
# If using Transaction Pooler or Session Pooler, we want to ensure we disable SQLAlchemy client side pooling -
# https://docs.sqlalchemy.org/en/20/core/pooling.html#switching-pool-implementations
# engine = create_engine(DATABASE_URL, poolclass=NullPool)
# Test the connection
try:
with engine.connect() as connection:
print("Connection successful!")
except Exception as e:
print(f"Failed to connect: {e}")`,
},
],
},
}
export default examples