Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ dist
/.eslintcache
.vscode/
manually-test-on-heroku.js
.history
7 changes: 4 additions & 3 deletions docs/pages/apis/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Every field of the `config` object is entirely optional. A `Client` instance wil
type Config = {
user?: string, // default process.env.PGUSER || process.env.USER
password?: string or function, //default process.env.PGPASSWORD
host?: string, // default process.env.PGHOST
port?: number, // default process.env.PGPORT
host?: string | string[], // default process.env.PGHOST; array enables multi-host failover
port?: number | number[], // default process.env.PGPORT; one value or one per host
database?: string, // default process.env.PGDATABASE || user
connectionString?: string, // e.g. postgres://user:password@host:5432/database
ssl?: any, // passed directly to node.TLSSocket, supports all tls.connect options
Expand All @@ -29,7 +29,8 @@ type Config = {
idle_in_transaction_session_timeout?: number, // number of milliseconds before terminating any session with an open idle transaction, default is no timeout
client_encoding?: string, // specifies the character set encoding that the database uses for sending data to the client
fallback_application_name?: string, // provide an application name to use if application_name is not set
options?: string // command-line options to be sent to the server
options?: string, // command-line options to be sent to the server
targetSessionAttrs?: 'any' | 'read-write' | 'read-only' | 'primary' | 'standby' | 'prefer-standby', // default 'any'; requires host to be an array
}
```
Expand Down
56 changes: 56 additions & 0 deletions docs/pages/features/connecting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,62 @@ client = new Client({
})
```

## Multiple hosts

node-postgres supports connecting to multiple PostgreSQL hosts. Pass arrays to `host` and `port` to enable automatic failover — the client tries each host in order and uses the first one it can reach.

```js
import { Client } from 'pg'

const client = new Client({
host: ['primary.db.com', 'replica1.db.com', 'replica2.db.com'],
port: 5432, // single port reused for all hosts
database: 'mydb',
user: 'dbuser',
password: 'secretpassword',
})

await client.connect() // tries hosts left to right until one succeeds
```

You can also specify a different port for each host:

```js
const client = new Client({
host: ['host-a.db.com', 'host-b.db.com'],
port: [5432, 5433],
database: 'mydb',
})
```

Port rules (same as libpq):
- **one port** — reused for every host
- **one port per host** — each port is paired with the corresponding host by index
- any other combination throws at construction time

### target_session_attrs

Use `targetSessionAttrs` to control which host is accepted based on its role. This mirrors the [libpq `target_session_attrs`](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS) option.

```js
const client = new Client({
host: ['primary.db.com', 'replica.db.com'],
port: 5432,
targetSessionAttrs: 'read-write', // only connect to a writable primary
})
```

| Value | Accepted server |
|---|---|
| `any` (default) | any server |
| `read-write` | server where `transaction_read_only = off` |
| `read-only` | server where `transaction_read_only = on` |
| `primary` | server that is not in hot standby |
| `standby` | server that is in hot standby |
| `prefer-standby` | standby if available, otherwise any |

When all hosts are exhausted without finding a matching server, the client emits an error.

## Connection URI

You can initialize both a pool and a client with a connection string URI as well. This is common in environments like Heroku where the database connection string is supplied to your application dyno through an environment variable. Connection string parsing brought to you by [pg-connection-string](https://github.com/brianc/node-postgres/tree/master/packages/pg-connection-string).
Expand Down
6 changes: 4 additions & 2 deletions packages/pg/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class Client extends EventEmitter {
keepAlive: c.keepAlive || false,
keepAliveInitialDelayMillis: c.keepAliveInitialDelayMillis || 0,
encoding: this.connectionParameters.client_encoding || 'utf8',
targetSessionAttrs: c.targetSessionAttrs || this.connectionParameters.targetSessionAttrs || null,
trustParameterStatus: c.trustParameterStatus || false,
})
this._queryQueue = []
this.binary = c.binary || defaults.binary
Expand Down Expand Up @@ -155,7 +157,7 @@ class Client extends EventEmitter {
}
}

if (this.host && this.host.indexOf('/') === 0) {
if (!Array.isArray(this.host) && this.host && this.host.indexOf('/') === 0) {
con.connect(this.host + '/.s.PGSQL.' + this.port)
} else {
con.connect(this.port, this.host)
Expand Down Expand Up @@ -542,7 +544,7 @@ class Client extends EventEmitter {
if (client.activeQuery === query) {
const con = this.connection

if (this.host && this.host.indexOf('/') === 0) {
if (!Array.isArray(this.host) && this.host && this.host.indexOf('/') === 0) {
con.connect(this.host + '/.s.PGSQL.' + this.port)
} else {
con.connect(this.port, this.host)
Expand Down
20 changes: 19 additions & 1 deletion packages/pg/lib/connection-parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,16 @@ class ConnectionParameters {
this.database = this.user
}

this.port = parseInt(val('port', config), 10)
const rawPort = val('port', config)
this.port = Array.isArray(rawPort) ? rawPort.map((p) => parseInt(p, 10)) : parseInt(rawPort, 10)
this.host = val('host', config)

const hosts = Array.isArray(this.host) ? this.host : [this.host]
const ports = Array.isArray(this.port) ? this.port : [this.port]
if (ports.length !== 1 && ports.length !== hosts.length) {
throw new Error(`ports must have either 1 entry or the same number of entries as hosts (${hosts.length})`)
}

// "hiding" the password so it doesn't show up in stack traces
// or if the client is console.logged
Object.defineProperty(this, 'password', {
Expand Down Expand Up @@ -111,6 +118,17 @@ class ConnectionParameters {
this.idle_in_transaction_session_timeout = val('idle_in_transaction_session_timeout', config, false)
this.query_timeout = val('query_timeout', config, false)

this.targetSessionAttrs = val('targetSessionAttrs', config)

const validTargetSessionAttrs = ['any', 'read-write', 'read-only', 'primary', 'standby', 'prefer-standby']
if (this.targetSessionAttrs && !validTargetSessionAttrs.includes(this.targetSessionAttrs)) {
throw new Error(
`invalid targetSessionAttrs value: "${this.targetSessionAttrs}". Must be one of: ${validTargetSessionAttrs.join(
', '
)}`
)
}

if (config.connectionTimeoutMillis === undefined) {
this.connect_timeout = process.env.PGCONNECT_TIMEOUT || 0
} else {
Expand Down
Loading
Loading