module PG

pg

A PostgreSQL client for Quarz, written in Quarz: it speaks the server's wire protocol (version 3) over Quarz's sockets, so a query suspends only the task that waits for it.

# Cog.toml of an application
[dependencies]
PG = { git = "https://github.com/quarz-lang/pg", tag = "v0.1.0" }

The API reference of the main branch is at https://pg.api.quarz.dev.

Work in progress. What exists:

conn = PG.connect("postgres://ann:secret@localhost/shop")
conn.query("SELECT id, name FROM users WHERE id = $1", [42]).each do |row|
  puts "#{row.int("id")} #{row.string("name")}"
end
conn.close

Errors: PG::Error is the root of every error the cog raises. PG::ServerError is what the server reported (SQLSTATE code, detail, hint, position; a refused login included, 28P01 for a wrong password); PG::ConnectionError (with PG::AuthenticationError and PG::ProtocolError under it) means the connection failed or cannot be used any further; PG::TransactionError and PG::NullError are misuses.

PG::Pool.open("postgres://app@localhost/shop", size: 8) do |pool|
  Task.scope do |s|
    100.times { |i| s.spawn { pool.query("INSERT INTO hits VALUES ($1)", [i]) } }
  end
end
notes = Channel[PG::Notification].new(100)
Task.scope do |s|
  s.spawn do
    conn = PG.connect(url)
    begin
      conn.listen("jobs")
      loop do
        if (n = conn.wait_for_notify)
          notes.send(n)
        end
      end
    ensure
      conn.close
    end
  end
  s.spawn do
    while (n = notes.receive?)
      puts n.payload
    end
  end
end

Listen with listen, not exec("LISTEN ..."), which is not tracked. PG::Pool sends UNLISTEN * on a connection given back that listening?, and drops what it received: use a dedicated PG.connect for a listener.

Not yet: COPY, cancel requests, client certificates, channel binding.

Differences from libpq

For now, to be revisited:

Development

quarz test runs the tests, quarz test --doc the documentation's examples. The integration tests use PG::Testing: PG_TEST_URL when it is set; otherwise, where PostgreSQL's server programs are installed (/usr/lib/postgresql/*/bin), a throwaway cluster in a temporary directory (SCRAM-SHA-256, a random port on 127.0.0.1), removed afterwards; with neither, they are omitted. The TLS tests (test/tls_test.qz) always make clusters of their own, with the certificates of test/fixtures/tls/, and are omitted without initdb. Licensed under MIT No Attribution (LICENSE).

To develop another cog against a local checkout of pg, write Cog.local.toml beside that cog's Cog.toml (git should ignore it; quarz cog init lists it in .gitignore):

[patch]
PG = { path = "../quarz-pg" }

That cog's lock ignores the file: it keeps pg's git source.

Functions

def parse_url(url: String) -> PG::Config

Reads a connection URL, as libpq does: postgres://user:password@host:port/database?param=value (postgresql:// too). The parts are percent-decoded; host may be a socket directory (%2Fvar%2Frun%2Fpostgresql, or ?host=/var/run/...); the parameters host, port, user, password, dbname, application_name, connect_timeout (seconds), max_scram_iterations, statement_cache_size, sslmode (disable, allow, prefer, the default, require, verify-ca or verify-full: see PG.connect) and sslrootcert (a PEM file's path, or system, which makes sslmode verify-full unless given, and refuses a weaker one) are read. channel_binding may be disable or prefer (no channel binding either way) and sslnegotiation postgres. Client certificates (sslcert, sslkey, sslpassword), sslcrl and channel_binding=require are not supported: they raise ArgumentError, as does a malformed URL or an unknown parameter.

c = PG.parse_url("postgres://ann:s%40cret@db.example:6543/shop?application_name=web")
[c.host, c.port, c.user, c.password, c.database, c.application_name]
# => ["db.example", 6543, "ann", "s@cret", "shop", "web"]
PG.parse_url("postgres://ann@%2Ftmp/shop").host   # => "/tmp"
PG.parse_url("postgres://h/d?sslmode=verify-full").sslmode   # => :verify_full
PG.parse_url("postgres://h/d").sslmode                       # => :prefer
PG.parse_url("postgres://h/d?sslrootcert=system").sslmode    # => :verify_full
PG.parse_url("postgres://h/d?sslrootcert=system&sslmode=require")  # raises ArgumentError
PG.parse_url("postgres://h/d?sslcert=client.pem")  # raises ArgumentError

src/config.qz:60

def connect(url: String) -> PG::Connection

Connects to the server url names (PG.parse_url reads it), and authenticates. Raises PG::ServerError when the server refuses (a wrong password, a missing database), PG::AuthenticationError when this client cannot authenticate as the server asks, and PG::ConnectionError when the server cannot be reached or does not answer in time.

src/connection.qz:6

def connect(*, host: String = …, port: Int = …, user: String = …, password: String? = …, database: String? = …, application_name: String? = …, allow_cleartext: Bool = …, connect_timeout: Duration? = …, max_scram_iterations: Int = …, statement_cache_size: Int = …, sslmode: Symbol? = …, sslrootcert: String? = …) -> PG::Connection

Connects as PG.connect(url) does, from keywords: host is a host name or address, or a directory holding the server's unix socket (a path starting with /). A password is sent in clear only over a unix socket, or when allow_cleartext says so. max_scram_iterations is the most SCRAM iterations the server may ask for, statement_cache_size how many prepared statements PG::Connection#query keeps.

sslmode says whether the session goes over TLS (PG::Connection#tls?), as libpq's does:

  • :disable: never.
  • :allow: not at first; when the server refuses the session (a pg_hba.conf that takes only hostssl), once more over TLS.
  • :prefer (the default, unless sslrootcert is "system"): over TLS when the server speaks it, else in plain.
  • :require: over TLS, else PG::ConnectionError.
  • :verify_ca, :verify_full: over TLS, the server's certificate verified against the roots sslrootcert names, its name too (the host as given, a name or an address). :verify_ca is :verify_full here: the name is always verified, which is stricter than libpq.

sslrootcert is a PEM file's path, or "system" for the system's store; nil means ~/.postgresql/root.crt, read only when it exists (:verify_ca and :verify_full raise PG::ConnectionError when it does not). With "system", sslmode is :verify_full unless given, and a weaker one (:disable to :require) raises ArgumentError, as in libpq.

With :allow, :prefer and :require, the server is verified (as with :verify_full) when there are roots: sslrootcert, or an existing ~/.postgresql/root.crt, as libpq does; without them the session is encrypted but the server not authenticated. So a stale root.crt makes :require fail. A unix socket (host starting with /) never uses TLS, whatever sslmode says. A TLS failure (a certificate refused, a handshake that fails) is a PG::ConnectionError saying why.

src/connection.qz:43

Classes and modules

Extensions

def self.default_user -> String

in extension PG::Config

The user's name from the environment (PGUSER, then USER), as libpq takes it when a URL names none.

src/config.qz:133

def self.unescape(text: String) -> String

in extension PG::Config

text with its %XX escapes decoded. Raises ArgumentError on a malformed escape or bytes that are not UTF-8.

src/config.qz:137

def self.sslmode(text: String) -> Symbol

in extension PG::Config

The sslmode a URL spells (verify-full), as a Symbol (:verify_full). Raises ArgumentError for another value.

src/config.qz:159

def self.sslmode(mode: Symbol?, rootcert: String?) -> Symbol

in extension PG::Config

The sslmode of a connection given mode (nil: none given) and rootcert: :prefer by default, :verify_full with the system's roots, with which a weaker mode raises ArgumentError (libpq 16).

src/config.qz:168

def self.sslmode?(mode: Symbol) -> Bool

in extension PG::Config

Whether mode is a value of sslmode: :disable, :allow, :prefer, :require, :verify_ca or :verify_full.

src/config.qz:179

def self.port(text: String) -> Int

in extension PG::Config

A port number, 1 to 65535.

src/config.qz:184