Types
PgPool = ref object of PgPoolOwner
- Connection pool that manages a set of PostgreSQL connections.
PoolConfig = object connConfig*: ConnConfig minSize*: int ## Minimum connections, idle or checked out, as `size()` counts them ## (default 1) maxSize*: int ## Maximum connections, as `size()` counts them (default 10) idleTimeout*: Duration ## Close idle connections after this duration (default 10min, ZeroDuration=disabled) maxLifetime*: Duration ## Max connection lifetime (default 1hr, ZeroDuration=disabled), measured ## from connect completion. A connection past it is closed, not reused: the ## idle scan of `acquire` and the maintenance sweep evict it, and `release` ## closes it rather than parking it or handing it to a waiter. A borrow ## already in flight is not interrupted, and a conn a `connect` has just ## produced is handed out once, however long the dial took. maintenanceInterval*: Duration ## Maintenance loop interval (default 30s) healthCheckTimeout*: Duration ## Ping idle connections older than this before returning (default 5s, ZeroDuration=disabled). ## Applies to plaintext connections. For TLS connections, see `tlsHealthCheckTimeout`. ## An idle connection that received bytes since it parked (a NOTIFY, or ## what a closing backend sends ahead of its FIN) is pinged regardless, ## by `acquire` and by the maintenance sweep, if the liveness probe sees ## them. It misses bytes a lower layer has already read off the socket: ## the TLS layer, or under chronos on Windows its posted read, which ## takes them as they arrive. tlsHealthCheckTimeout*: Duration ## Same as `healthCheckTimeout` but for TLS connections (default 500ms, ## ZeroDuration=disabled). ## The liveness probe cannot see bytes the TLS layer has already read off ## the socket, so TLS pools keep a much shorter idle window than plaintext. pingTimeout*: Duration ## Max time to wait for a health check ping response (default 5s, ZeroDuration=no timeout) acquireTimeout*: Duration ## Deadline for the entire `acquire` call (default 30s, ZeroDuration=no ## timeout). Idle health-check pings, a caller-driven connect, and the ## wait for a released connection all draw from this one budget, so ## acquire latency is bounded by ~`acquireTimeout` rather than ## `pingTimeout*N + connectTimeout + acquireTimeout`. maxWaiters*: int = -1 ## Max queued acquire waiters, and when `pipelined` is true also the max ## depth of `pendingOps` before `pool.exec`/`pool.query` enqueue ## (default -1=unlimited, 0=reject immediately). Rejects with ## `PgPoolError(pekQueueFull)` when full. resetQuery*: string ## SQL to execute when returning a connection to the pool (default ""=disabled). ## Disabled, the pool clears only typed-API advisory locks and a changed ## `TimeZone`: `SET` / `SET ROLE` / `search_path`, prepared statements, ## temporary tables and `LISTEN` reach the next borrower. Runs from ## `resetSession` (the `with*` macros and the pool's query procs); a raw ## `release` skips it. ## Common values: "DISCARD ALL" (full reset, recommended for PgBouncer), ## "DEALLOCATE ALL" (clear prepared statements only), ## "RESET ALL" (reset session parameters only; `SET ROLE` survives it, ## so use "RESET ALL; RESET ROLE" when borrowers switch roles). ## `DISCARD ALL` and `RESET ROLE` return to a role sent at startup ## (`extraParams`), not to the login role. The statement cache is ## kept unless it drops the prepared statements (DISCARD/DEALLOCATE ALL). ## On failure, the connection is discarded. A `TimeZone` it leaves off ## the connect value is set back afterwards, at the cost of a round trip; ## behind PgBouncer, starting its server connections in the same zone ## (`timezone=` on the `[databases]` entry) avoids that. ## Treat as trusted operator config β do not build from untrusted input ## (executed via the simple query protocol, which allows multi-statement). resetQueryTimeout*: Duration ## Deadline for each server round-trip in `resetSession` β covers ## `pg_advisory_unlock_all` (when session locks are dirty), `resetQuery` ## and the `TimeZone` restore (default 5s, ZeroDuration=no timeout). A ## hung server would otherwise stall the release path and starve the ## pool; on timeout the connection is closed and the release proceeds. tracer*: PgTracer ## Optional tracer for pool-level hooks (acquire/release) pipelined*: bool ## Enable implicit query batching for pool.exec/query (default false). ## When enabled, concurrent calls within the same event loop tick are ## batched into a single TCP write per connection using per-query SYNC ## for error isolation. One dispatch uses at most `maxSize div 2` ## connections (at least 1, or 2 when its ops mix finite and unlimited ## timeouts). A dispatch from a later tick can overlap a running one and ## takes its own share, so this does not bound the pool's total. A ## dispatch queues only for its first connection (one per timeout ## class) and takes the rest while an idle one or room under `maxSize` ## is free, so on a busy pool it runs on fewer. The ops on one ## connection run as one pipeline and settle when it ends, so each op's ## `timeout` covers the whole pipeline, and one that expires fails its ## op even if the server ran it. `maxPipelineSize` caps the ops one ## dispatch takes. maxPipelineSize*: int ## Max operations per pipeline batch per connection (default 0=unlimited). ## Only used when `pipelined` is true. connectBackoffInitial*: Duration ## First backoff after a maintenance-loop connect failure (default 1s, ## ZeroDuration=disabled, falls back to fixed `maintenanceInterval` retries). connectBackoffMax*: Duration ## Cap for exponential backoff growth (default 60s). Doubles each failure ## until reaching this value.
- Configuration for the connection pool. Create via initPoolConfig.
PooledConnHandle = ref object conn*: PgConnection pool*: PgPool
-
A pool-borrowed connection paired with the pool it came from.
Returned by PgPool.acquireHandle and PgPoolCluster.readConnection / writeConnection. The handle must be released with release(h) to return the connection to the pool β typically via defer: h.release(). Forgetting to release leaks the connection until the pool is closed.
No session reset: unlike withConnection / withReadConnection / withWriteConnection, release(h) does not call resetSession, so a configured resetQuery will not run. A connection that still holds session-level advisory locks acquired through the typed API, or whose TimeZone changed since connect, is closed instead of returning to the pool. Use the with*Connection templates when you want automatic session cleanup, or call pool.resetSession(h.conn) yourself before release(h).
pool is the pool the connection was actually borrowed from. For PgPoolCluster.readConnection with fallbackPrimary, this can be either the replica or the primary depending on which served the acquire.
PoolMetrics = object acquireCount*: int64 ## Total successful acquires acquireDuration*: Duration ## Total time spent waiting in acquire timeoutCount*: int64 ## Number of acquire timeouts createCount*: int64 ## Number of new connections created closeCount*: int64 ## Number of connections closed/discarded connectFailureCount*: int64 ## Number of connect attempts that failed once `newPool` returned: a ## refusal, a config fault or a failure that may clear. A dial cut short ## by an `acquire`'s budget counts in `timeoutCount` instead, and one a ## chronos cancel cuts short (the caller's, or `close()`'s) counts nowhere; ## asyncdispatch lets the dial end, and counts it. refusalCount*: int64 ## Number of connect attempts the server refused; what tells a pool held ## below `minSize` by a refusal from an idle one.
- Cumulative pool statistics.
Procs
proc acquire(pool: PgPool): Future[PgConnection] {....stackTrace: false, raises: [ Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
-
Acquire a connection from the pool. Tries idle connections first (with health checks), creates a new one if under maxSize, or waits for a release. Raises PgPoolError on every failure mode: acquire timeout, pool closed, waiter queue full, or a failed connect attempt β for connect failures the underlying error (e.g. PgConnectionError) is preserved as the parent of the raised PgPoolError. Use the kind field (PoolErrorKind) to distinguish the failure mode programmatically. A connect that raises PgConfigError is reported as pekConfigFault, after which the pool never dials again (the config cannot change). A connect the server refuses (see isLastingRefusal) is reported as pekRefused, as are the waiters no borrower is left to serve; connectRefusal keeps the refusal until a connect succeeds, but a waiter queued after it gets a dial of its own. retryAdvice returns raStop for either kind.
Timeout types differ by phase: this proc reports its own deadline as PgPoolError(pekAcquireTimeout), while a pipelined pool.exec / pool.query wait and the pool withTransactionDeadline / withTransactionRetryDeadline deadline raise PgTimeoutError instead β even when the deadline variant expires during its internal acquire. An except PgPoolError clause alone therefore misses those operation timeouts; catch PgTimeoutError first:
try: discard await pool.query(sql"SELECT 1", timeout = seconds(5)) except PgTimeoutError: echo "operation timed out" except PgPoolError as e: echo "acquire failed: ", e.kind
retryAdvice returns raRetry for both timeout types, so a retry loop can treat them alike. proc acquireHandle(pool: PgPool): Future[PooledConnHandle] {....stackTrace: false, raises: [ Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
-
Acquire a connection wrapped in a PooledConnHandle. Equivalent to acquire, but the returned handle pairs the connection with its owning pool and provides an idempotent release(h).
The caller is responsible for releasing β typically via defer: h.release(). Forgetting to release leaks the connection. release(h) does not run resetSession; prefer withConnection when automatic session cleanup is desired.
proc activeCount(pool: PgPool): int {....raises: [], tags: [], forbids: [].}
- Number of slots in use outside the idle set: connections checked out, plus slots held by an acquire still running (granted, dialing or checking a connection; under asyncdispatch, even after a caller's wait() gave up on it), by a minSize replenish dial, and by the maintenance sweep while it pings an idle connection. Not a count of borrowers: it can be nonzero with none.
proc buildReleaseAndReraise(releaseCall, bodyErrSym, bodyDefectSym: NimNode): NimNode {. ...raises: [], tags: [], forbids: [].}
- Build the release-and-re-raise block shared by the pooled with* macros and the cluster's withReadConnection / withWriteConnection. The caller captures the body's error into bodyErrSym / bodyDefectSym, then splices this block to run releaseCall (an awaited resetSessionAndRelease) outside a finally, so a failing release can't mask the body error. A release failure is re-raised only when the body succeeded β the body error verbatim, its Defect wrapped in PgPoolError (the Defect is parent). A release-path CancelledError is always re-raised: the caller is cancelling the whole macro.
proc close(pool: PgPool; timeout = ZeroDuration): Future[void] {. ...stackTrace: false, raises: [Exception, ValueError], tags: [RootEffect, TimeEffect], forbids: [].}
-
Close the pool: stop the maintenance loop, fail queued acquires and pipelined operations with pekClosed, and close the idle connections. An acquire already dialing or pinging fails once that dial or ping ends; a checked-out connection is closed when released.
When timeout > ZeroDuration, timeout is a single deadline shared by both the drain of activeCount() and the background-task drain. Pending background closes still running when the deadline elapses are cancelled so close() returns promptly. The idle connections, closed between the two drains, are not cut short: closing one sends Terminate and waits for no reply. Without a timeout (or ZeroDuration), close() does not wait for activeCount() and the background drain waits unbounded.
asyncdispatch cannot cancel a future: there the maintenance loop, and background tasks still running at the deadline, outlive close() until their current sleep, connect, ping or close ends. A connection they open or ping after close() is closed, not kept.
proc connectRefusal(pool: PgPool): ref CatchableError {....raises: [], tags: [], forbids: [].}
- The latest refusal a connect reported (a wrong password, a missing database) since one last succeeded, or nil. It is how a pool held below minSize by a refusal surfaces. A failure of another kind, a config fault and a close leave it; the next successful connect clears it: an acquire's, or the maintenance loop's every connectBackoffMax while the pool is below minSize. The loop dials only while below minSize (a waiter dials into its slot as an acquire), so a pool already at minSize keeps it longer. A config fault itself is not recorded here; it surfaces as pekConfigFault.
proc exec(pool: PgPool; sql: string; params: seq[PgParam] = @[]; timeout: Duration = ZeroDuration): Future[CommandResult] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgTimeoutError, CancelledError, PgError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
-
Execute a statement with typed parameters using a pooled connection. When pipelined is enabled, the operation is batched with other concurrent calls and sent in a single TCP write.
In pipelined mode a batch runs under a single timeout, so a finite timeout may be widened to the largest finite timeout among the ops it is batched with. An op with no timeout (ZeroDuration) is batched separately and stays unlimited. A finite timeout is measured from enqueue (queue dwell included); when maxWaiters >= 0, enqueue itself rejects with pekQueueFull once pendingOps reaches that depth.
A pipelined wait that exceeds timeout raises PgTimeoutError, not PgPoolError, so except PgPoolError alone misses it β catch PgTimeoutError first (see acquire).
proc exec(pool: PgPool; sql: string; params: seq[PgParamInline]; timeout: Duration = ZeroDuration): Future[CommandResult] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgTimeoutError, CancelledError, PgError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a statement with heap-alloc-free inline parameters using a pooled connection. Batches through the pipelined path when pipelined is enabled; see the seq[PgParam] overload for the batch timeout and maxWaiters semantics.
proc execInTransaction(pool: PgPool; sql: string; params: seq[PgParam] = @[]; opts: TransactionOptions; timeout: Duration = ZeroDuration): Future[CommandResult] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a statement inside a pipelined transaction with options (isolation / access mode / deferrable) applied to the BEGIN. Returned rows (e.g. RETURNING) are discarded undecoded; read them with queryInTransaction.
proc execInTransaction(pool: PgPool; sql: string; params: seq[PgParam] = @[]; timeout: Duration = ZeroDuration): Future[CommandResult] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a statement inside a pipelined transaction with typed parameters. Returned rows (e.g. RETURNING) are discarded undecoded; read them with queryInTransaction.
proc initPoolConfig(connConfig: ConnConfig; minSize = 1; maxSize = 10; idleTimeout = minutes(10); maxLifetime = hours(1); maintenanceInterval = seconds(30); healthCheckTimeout = seconds(5); tlsHealthCheckTimeout = milliseconds(500); pingTimeout = seconds(5); acquireTimeout = seconds(30); maxWaiters = -1; resetQuery = ""; resetQueryTimeout = seconds(5); pipelined = false; maxPipelineSize = 0; connectBackoffInitial = seconds(1); connectBackoffMax = seconds(60)): PoolConfig {. ...raises: [ValueError], tags: [], forbids: [].}
-
Create a pool configuration with sensible defaults. Maintenance replenishes size() toward minSize, checked-out connections included; acquires dial up to maxSize. Set resetQuery to clean session state on release (e.g. "DISCARD ALL" for PgBouncer). Set pipelined to true to enable implicit query batching for pool.exec/pool.query. When the maintenance loop fails to open a connection, subsequent retries use exponential backoff starting at connectBackoffInitial, doubling up to connectBackoffMax. Set connectBackoffInitial = ZeroDuration to disable backoff and fall back to fixed-interval retries. A refusal (see connectRefusal) skips the ramp to connectBackoffMax and replenishes one connection at a time, but a waiter still gets a dial: one refused fails the waiters no borrower can serve with PgPoolError(pekRefused).
Raises ValueError if parameters are invalid.
proc metrics(pool: PgPool): PoolMetrics {....raises: [], tags: [], forbids: [].}
- Cumulative pool metrics.
proc newPool(config: PoolConfig): Future[PgPool] {....stackTrace: false, raises: [ Exception, ValueError, CatchableError, PgQueryError, PgStateError, PgConnectionError, PgUnavailableError, CancelledError, PgTypeError, PgProtocolError, PgTimeoutError, AsyncTimeoutError, PgConfigError], tags: [RootEffect, WriteIOEffect, TimeEffect], forbids: [].}
- Create a new connection pool and establish minSize initial connections. Raises if any initial connection fails (all opened connections are closed on error). Cancelled (chronos), it stops the dials under way and closes the ones that came up. asyncdispatch cannot cancel it: a wait that gives up leaves it running, and the pool it returns is for that wait's onOrphan to close. Raises ValueError if config is invalid; re-checked here so a directly constructed PoolConfig cannot bypass initPoolConfig's validation.
proc notify(pool: PgPool; channel: string; payload: string = ""; timeout: Duration = ZeroDuration): Future[void] {....stackTrace: false, raises: [ Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Send a NOTIFY on channel with optional payload using a pooled connection.
proc pendingAcquires(pool: PgPool): int {....raises: [], tags: [], forbids: [].}
- Number of non-cancelled waiters queued for a connection.
proc poolConfig(pool: PgPool): PoolConfig {....raises: [], tags: [], forbids: [].}
- The configuration in effect, as a copy: editing it does not reconfigure the pool. A zero maintenanceInterval reads back as the 30 s default.
proc query(pool: PgPool; sql: string; params: seq[PgParam] = @[]; resultFormat: ResultFormat = rfAuto; timeout: Duration = ZeroDuration): Future[ QueryResult] {....stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgTimeoutError, CancelledError, PgError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
-
Execute a query with typed parameters using a pooled connection. When pipelined is enabled, the operation is batched with other concurrent calls and sent in a single TCP write.
In pipelined mode a batch runs under a single timeout, so a finite timeout may be widened to the largest finite timeout among the ops it is batched with. An op with no timeout (ZeroDuration) is batched separately and stays unlimited. A finite timeout is measured from enqueue (queue dwell included); when maxWaiters >= 0, enqueue itself rejects with pekQueueFull once pendingOps reaches that depth.
A pipelined wait that exceeds timeout raises PgTimeoutError, not PgPoolError, so except PgPoolError alone misses it β catch PgTimeoutError first (see acquire).
proc query(pool: PgPool; sql: string; params: seq[PgParamInline]; resultFormat: ResultFormat = rfAuto; timeout: Duration = ZeroDuration): Future[ QueryResult] {....stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgTimeoutError, CancelledError, PgError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query with heap-alloc-free inline parameters using a pooled connection. Batches through the pipelined path when pipelined is enabled; see the seq[PgParam] overload for the batch timeout and maxWaiters semantics.
proc queryColumn(pool: PgPool; sql: string; params: seq[PgParam] = @[]; timeout: Duration = ZeroDuration): Future[seq[string]] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query and return the first column of all rows as strings. Raises PgNullError if any value is NULL.
proc queryEach(pool: PgPool; sql: string; params: seq[PgParam] = @[]; callback: RowCallback; resultFormat: ResultFormat = rfAuto; timeout: Duration = ZeroDuration): Future[int64] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
-
Execute a query with typed parameters using a pooled connection, invoking callback once per row.
Row lifetime: the Row passed to callback is only valid for the duration of that single invocation. To retain a row beyond the callback, call row.clone() to get a detached copy.
proc queryExists(pool: PgPool; sql: string; params: seq[PgParam] = @[]; timeout: Duration = ZeroDuration): Future[bool] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query and return whether any rows exist.
proc queryInTransaction(pool: PgPool; sql: string; params: seq[PgParam] = @[]; opts: TransactionOptions; resultFormat: ResultFormat = rfAuto; timeout: Duration = ZeroDuration): Future[QueryResult] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query inside a pipelined transaction with options (isolation / access mode / deferrable) applied to the BEGIN.
proc queryInTransaction(pool: PgPool; sql: string; params: seq[PgParam] = @[]; resultFormat: ResultFormat = rfAuto; timeout: Duration = ZeroDuration): Future[QueryResult] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query inside a pipelined transaction with typed parameters.
proc queryRow(pool: PgPool; sql: string; params: seq[PgParam] = @[]; resultFormat: ResultFormat = rfAuto; timeout: Duration = ZeroDuration): Future[Row] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query and return the first row. Raises PgNoRowsError if no rows are returned.
proc queryRowOpt(pool: PgPool; sql: string; params: seq[PgParam] = @[]; resultFormat: ResultFormat = rfAuto; timeout: Duration = ZeroDuration): Future[Option[Row]] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query and return the first row, or none if no rows.
proc queryValue(pool: PgPool; sql: string; params: seq[PgParam] = @[]; timeout: Duration = ZeroDuration): Future[string] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query and return the first column of the first row as a string. Raises PgNoRowsError if no rows are returned, or PgNullError if the value is NULL.
proc queryValue[T](pool: PgPool; _: typedesc[T]; sql: string; params: seq[PgParam] = @[]; timeout: Duration = ZeroDuration): Future[ T] {....stackTrace: false.}
- Execute a query and return the first column of the first row as T. Raises PgNoRowsError if no rows are returned, or PgNullError if the value is NULL.
proc queryValueOpt(pool: PgPool; sql: string; params: seq[PgParam] = @[]; timeout: Duration = ZeroDuration): Future[Option[string]] {. ...stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query and return the first column of the first row as a string. Returns none if no rows or the value is NULL.
proc queryValueOpt[T](pool: PgPool; _: typedesc[T]; sql: string; params: seq[PgParam] = @[]; timeout: Duration = ZeroDuration): Future[Option[T]] {. ...stackTrace: false.}
- Execute a query and return the first column of the first row as T. Returns none if no rows or the value is NULL.
proc queryValueOrDefault(pool: PgPool; sql: string; params: seq[PgParam] = @[]; default: string = ""; timeout: Duration = ZeroDuration): Future[ string] {....stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a query and return the first column of the first row as a string. Returns default if no rows or the value is NULL.
proc queryValueOrDefault[T](pool: PgPool; _: typedesc[T]; sql: string; params: seq[PgParam] = @[]; default: T; timeout: Duration = ZeroDuration): Future[T] {. ...stackTrace: false.}
- Execute a query and return the first column of the first row as T. Returns default if no rows or the value is NULL.
proc queryValueOrDefault[T](pool: PgPool; sql: string; params: seq[PgParam] = @[]; default: T; timeout: Duration = ZeroDuration): Future[T] {. ...stackTrace: false.}
- Execute a query and return the first column of the first row as T, inferring T from default. Returns default if no rows or the value is NULL.
proc release(conn: PgConnection) {....raises: [PgError, ValueError, Exception], tags: [RootEffect, TimeEffect], forbids: [].}
-
Return a connection to its owning pool. If the connection is broken, in a transaction, or past maxLifetime, it is closed instead; if waiters are queued, it is handed directly to the next waiter.
No session reset: resetSession does not run, so resetQuery is skipped and session state (SET, SET ROLE, prepared statements, β¦) reaches the next borrower; a connection holding typed-API advisory locks or a changed TimeZone is closed instead. Use resetSessionAndRelease or the with* macros to clean up.
The owning pool is tracked on conn.ownerPool, set automatically when the connection is acquired from a PgPool (including pools inside a PgPoolCluster). For standalone connections created with connect this field is nil and calling release raises PgError β use conn.close() instead.
Double-release: releasing an already-idle connection is a no-op (tracer onPoolDoubleRelease), but after a FIFO handoff a second raw release(conn) can still re-route the waiter's connection β this API carries no per-borrow token. Prefer PooledConnHandle / with*.
withConnection, withReadConnection, withWriteConnection, withPipeline, and withTransaction call this automatically; direct callers only need it when they manage acquire/release manually.
proc release(h: PooledConnHandle) {....raises: [PgError, ValueError, Exception], tags: [RootEffect, TimeEffect], forbids: [].}
-
Return the borrowed connection to its pool. Idempotent β safe to call twice (e.g. once explicitly and once via defer).
Does not run `resetSession`. Session state (SET, SET ROLE, prepared statements, etc.) on the connection is not cleared before it returns to the pool, so subsequent borrowers may observe it. A connection holding advisory locks acquired via the typed API, or whose TimeZone changed since connect, is closed instead, at the cost of a reconnect. If that matters, use withConnection / withReadConnection / withWriteConnection instead, or call await h.pool.resetSession(h.conn) yourself before release(h).
proc releaseReclaimed(conn: PgConnection) {. ...raises: [PgError, ValueError, Exception], tags: [RootEffect, TimeEffect], forbids: [].}
- Internal (exported for pg_pool_cluster): return a connection the pool reclaimed β an acquire abandoned by a timeout whose late future still delivered one. The application never took delivery, so the borrow is demoted before the release and a close taken during shutdown stays a PgConnectionError.
proc resetSession(pool: PgPool; conn: PgConnection): owned(Future[void]) {. ...stackTrace: false, raises: [Exception, CancelledError, PgPoolError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
-
Reset session-affecting state on a connection before returning it to the pool. Releases any session-level advisory locks acquired through the typed API, runs the configured resetQuery (if any), then sets back a TimeZone changed since connect (with TimeZone=DEFAULT, RESETs it to the server's current one). On failure, closes the connection so that release() will discard it.
Always safe to call: returns immediately when the connection is unusable (broken / mid-transaction), has nothing to clean up (no resetQuery, no advisory locks held, TimeZone unchanged), or is bound for discard (TimeZone moved, but the server reported none at connect to set back). Callers don't need to gate on the pool config.
Swallows CatchableError (invoked from finally, so a raised reset error would mask the body's original exception) but re-raises CancelledError β chronos requires cancellation to propagate. A Defect is re-raised as PgPoolError (the Defect as parent) after marking csClosed (the reset did not complete). Callers must chain release() under try/finally (or use resetSessionAndRelease) to keep pool accounting balanced on cancel.
proc resetSessionAndRelease(pool: PgPool; conn: PgConnection): owned( Future[void]) {....stackTrace: false, raises: [Exception, CancelledError, PgPoolError, ValueError, PgError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
-
resetSession + release, wrapped so release() still runs when the reset propagates CancelledError (else a pool slot leaks).
Correct for both application-held and pool-internal borrows. A conn past maxLifetime skips the reset: release closes it, so the round trip would be spent on a conn the pool throws away.
proc simpleExec(pool: PgPool; sql: string; timeout: Duration = ZeroDuration): Future[ CommandResult] {....stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute a side-effect SQL command via the simple query protocol using a pooled connection. See PgConnection.simpleExec for semantics β no parameters, no plan cache, last command tag returned.
proc simpleQuery(pool: PgPool; sql: string; timeout: Duration = ZeroDuration): Future[ seq[QueryResult]] {....stackTrace: false, raises: [Exception, ValueError, PgPoolError, PgError, CancelledError, CatchableError], tags: [RootEffect, TimeEffect, WriteIOEffect], forbids: [].}
- Execute one or more SQL statements via the simple query protocol using a pooled connection. See PgConnection.simpleQuery for semantics β multi-statement, no parameters, no plan cache.
Macros
macro withConnection(pool: PgPool; conn, body: untyped): untyped
-
Acquire a connection, execute body, then release it back to the pool. The connection is available as conn inside the body. resetSession runs before release, so a configured resetQuery is applied and any session-level advisory locks acquired through the typed API are released via pg_advisory_unlock_all. Without a resetQuery, other session state (SET, SET ROLE, prepared statements, β¦) reaches the next borrower.
Release runs outside finally (a failing await in an asyncdispatch finally masks the body error), so return / break / continue escaping the body are rejected at compile time.
macro withPipeline(pool: PgPool; pipeline, body: untyped): untyped
-
Acquire a connection, create a Pipeline, execute body, then release. pipeline and the acquired conn are available in the body.
Body return / break / continue escaping to an enclosing loop are rejected at compile time (see withConnection).
macro withTransaction(pool: PgPool; args: varargs[untyped]): untyped
-
Execute body inside a BEGIN/COMMIT transaction using a pooled connection. On exception, ROLLBACK is issued automatically. A COMMIT the server rolled back raises PgQueryError (25P02), as in the PgConnection overload. Using return inside the body is a compile-time error.
Usage: pool.withTransaction(conn): conn.exec(...) pool.withTransaction(conn, seconds(5)): conn.exec(...) pool.withTransaction(conn, TransactionOptions(isolation: ilSerializable)): conn.exec(...) pool.withTransaction(conn, opts, seconds(5)): conn.exec(...)
Warning: Inside the body, use conn.exec(...) / conn.query(...) directly β not pool.exec(...) / pool.query(...). Pool methods acquire a separate connection, so those statements would run outside this transaction. Do not issue transaction-control SQL (COMMIT, ROLLBACK, ...) inside the body either: as in the PgConnection overload, ending the transaction yourself is not detected.
Timeout semantics: The timeout argument applies per-call to BEGIN, COMMIT, and ROLLBACK only β it does not bound body operations or pool.acquire(). Worst-case wall-clock = acquire(unbounded) + BEGIN(β€timeout) + body(unbounded) + COMMIT(β€timeout) [+ ROLLBACK(β€timeout) on failure]. Use withTransactionDeadline for a single wall-clock deadline covering acquire, BEGIN, body, and COMMIT.
macro withTransactionDeadline(pool: PgPool; args: varargs[untyped]): untyped
-
Execute body inside a BEGIN/COMMIT transaction bounded by a single wall-clock deadline that covers pool.acquire(), BEGIN, the body, and COMMIT together. A COMMIT the server rolled back raises PgQueryError (25P02), as in withTransaction.
Usage: pool.withTransactionDeadline(conn, seconds(5)): await conn.exec(...) pool.withTransactionDeadline(conn, TransactionOptions(...), seconds(5)): await conn.exec(...)
On deadline exceeded: PgTimeoutError is raised. Under chronos the body future is cancelled: a connection with a request in flight is invalidated (server-side CancelRequest, dropped on release), while one that unwinds cleanly (grace ROLLBACK succeeded) returns to the pool healthy. The grace ROLLBACK only runs in that second shape β the deadline expiring between statements; a cancellation inside a statement is caught there and invalidates the connection, so the cleanup skips it. Under asyncdispatch the still-running body keeps the connection, which is retired csClosed without a ROLLBACK and dropped on its eventual release. Whenever ROLLBACK is skipped, dropping the connection is what ends the server-side transaction. If the timeout fires while still waiting for acquire(), chronos cancels that acquire. Under asyncdispatch it stays queued, counting toward maxWaiters, until it gets a connection or its own acquireTimeout expires. A connection it still gets goes straight back to the pool: the body never runs, so a caller that retries does not run it twice. A deadline that expires after COMMIT, during the pool's session reset, raises nothing: the transaction committed. Under chronos the reset is cancelled and the connection dropped. Under asyncdispatch the reset runs on, bounded only by resetQueryTimeout, as on any release.
This holds for the acquire phase too: a deadline that expires while waiting for acquire() raises PgTimeoutError, not PgPoolError(pekAcquireTimeout) as a bare acquire() would. An except PgPoolError clause alone therefore misses it β catch PgTimeoutError first (see acquire). retryAdvice returns raRetry for both timeout types.
Edge case β acquire-completion race: under asyncdispatch the only preemption point is await, but the outer wait may still fire its timeout on the same tick the body finishes. To avoid a false-positive PgTimeoutError in that window, the timeout handler checks bodyFut.completed() (success only) and, when true, returns normally instead of reporting a timeout. A still-running or failed body falls through to the standard invalidate-and-raise path. This narrows but does not eliminate the race β a PgTimeoutError from this macro still does not guarantee the transaction was rolled back if the body was mid-flight when the timer won; it only guarantees the caller gave up waiting.
On other body exceptions: ROLLBACK runs with rollbackGrace. A body Defect is re-raised wrapped in PgPoolError (Defect as parent).
Warning: Inside the body, use conn.exec(...) / conn.query(...) directly β not pool.exec(...) / pool.query(...). Pool methods acquire a separate connection, so those statements would run outside this transaction.
macro withTransactionRetry(pool: PgPool; retryOpts: RetryOptions; args: varargs[untyped]): untyped
-
Execute body inside a BEGIN/COMMIT transaction on a pooled connection, re-running the whole transaction when it fails with a retryable error (by default the serialization_failure / deadlock_detected SQLSTATEs β see RetryOptions). The pooled connection is acquired once and reused across attempts; a ROLLBACK between attempts returns it to a clean tsIdle state. On a non-retryable error, or once maxAttempts is exhausted, the last exception propagates. A COMMIT the server rolled back raises PgQueryError (25P02), retried as in the PgConnection overload. Using return inside the body is a compile-time error.
Usage: pool.withTransactionRetry(RetryOptions(maxAttempts: 3), conn): await conn.exec(...) pool.withTransactionRetry(RetryOptions(...), conn, seconds(5)): await conn.exec(...) pool.withTransactionRetry(RetryOptions(...), conn, TransactionOptions(isolation: ilSerializable)): await conn.exec(...) pool.withTransactionRetry(RetryOptions(...), conn, opts, seconds(5)): await conn.exec(...)
Idempotency: body runs once per attempt, so it must be safe to re-run; non-database side effects are repeated on every retry. See withTransaction for the timeout semantics and the in-body conn.exec(...) warning.
macro withTransactionRetryDeadline(pool: PgPool; retryOpts: RetryOptions; args: varargs[untyped]): untyped
-
Execute body inside a BEGIN/COMMIT transaction on a pooled connection, bounded by a single wall-clock deadline that is shared across all retry attempts (covering acquire(), BEGIN, body, and COMMIT of every attempt), re-running the whole transaction on a retryable error while budget remains.
Usage: pool.withTransactionRetryDeadline(RetryOptions(maxAttempts: 3), conn, seconds(5)): await conn.exec(...) pool.withTransactionRetryDeadline(RetryOptions(...), conn, TransactionOptions(...), seconds(5)): await conn.exec(...)
Each attempt acquires a fresh connection (the previous one is released by the per-attempt finally), so a failed/poisoned connection is dropped by the pool's health check rather than reused. Worst-case wall-clock is deadline, not maxAttempts * deadline.
On deadline exceeded: PgTimeoutError is raised β never retried. This holds for the acquire phase too: a deadline that expires while waiting for acquire() raises PgTimeoutError, not PgPoolError(pekAcquireTimeout) as a bare acquire() would. An except PgPoolError clause alone therefore misses it β catch PgTimeoutError first (see acquire). retryAdvice returns raRetry for both timeout types. The in-flight connection, and a deadline that expires after COMMIT, are handled as in withTransactionDeadline. On a retryable error: ROLLBACK runs with rollbackGrace and the transaction is retried if budget remains. A COMMIT the server rolled back (25P02) is retried as in withTransactionRetry. See withTransactionDeadline for the acquire-race / completed() rationale and the in-body conn.exec(...) warning. Idempotency: body runs once per attempt; non-database side effects repeat. Using return inside the body is a compile-time error.
On a `Defect` raised by the body: re-raised wrapped in PgPoolError (parent = Defect), never retried; see withTransactionDeadline.
Templates
template runAndRelease[T](pool: PgPool; conn: PgConnection; body: Future[T]): Future[ T]
- acquire β body β resetSessionAndRelease. body is evaluated lazily inside a try so a synchronous raise from its async prelude (parameter encoding, guards, β¦) still releases conn; see runAndReleaseImpl for the error-handling semantics.