Packages

@kama/postgres 0.1.0

Install

kama pkg add kama.json @kama/postgres --version ^0.1.0

or in kama.json: "@kama/postgres": { "version": "^0.1.0" }

Dependencies
@kama/tls ^0.1.0

Versions

VersionPublishedRevisionIntegrity
0.1.03ab354bd8089sha256-3a2c346afea6…

@kama/postgres

The PostgreSQL client for kama. It speaks the PostgreSQL v3 wire protocol directly, in kama, over std::net, the way pgx, tokio-postgres, pgjdbc and Npgsql do. There is no libpq and nothing to install. TLS comes from @kama/tls.

kama pkg add kama.json @kama/postgres --version ^0.1.0     # from the official registry, registry.kama-lang.org

Needs kama ≥ 0.9.519, declared in the manifest, so an older compiler is refused by name. Tested, debug and release, on macOS arm64 and Linux, against PostgreSQL 14–18 and 19 beta.

Status: 0.1.0, early. It connects, authenticates and runs queries, with typed parameters and typed rows, over TCP, a Unix-domain socket or TLS (every sslmode, client certificates, SCRAM-SHA-256-PLUS). Not there yet: the statement cache, helpers for transactions (plain begin/commit statements work), COPY, waiting for notifications, pipelining, cancellation and query timeouts, target_session_attrs, load_balance_hosts, and a pool. See docs/ROADMAP.md. In 0.x, a minor version may break the API.

Using it

import { core::println, std::collections::DynamicArray,
         postgres::Config, postgres::Connection, postgres::PgError, postgres::Rows, postgres::column,
         postgres::columnOpt };

fn int32 main() {
    // A libpq connection string; the service file, PG* variables and ~/.pgpass apply as they do for psql.
    Result<Config, PgError> parsed = Config.parse(text: "postgresql://[email protected]/inventory?connect_timeout=5");
    Config config = match (give parsed) { case Ok(value: c): give c; case Err(error: e): { println(s: e.message()); return 1; } };
    Result<Connection, PgError> opened = Connection.connect(config: config);
    Connection conn = match (give opened) { case Ok(value: c): give c; case Err(error: e): { println(s: e.message()); return 1; } };

    Result<DynamicArray<Rows>, PgError> r = conn.simpleQuery(sql: "select name, qty from items order by name");
    DynamicArray<Rows> results = match (give r) { case Ok(value: v): give v; case Err(error: e): { println(s: e.message()); return 1; } };
    isize i = 0;
    while (i < results[0].rowCount()) {
        // Typed access, as PQgetvalue addresses a value: the result, the row, the column.
        Result<Optional<string>, PgError> name = columnOpt::<string>(rows: results[0], row: i, index: 0);
        Result<int32, PgError> qty = column::<int32>(rows: results[0], row: i, index: 1);
        match (give name) {
            case Ok(value: n): { match (give n) { case Some(value: s): { println(s: give s); } case None: { println(s: "(null)"); } }; }
            case Err(error: e): { println(s: e.message()); }
        };
        i = i + 1;
    }
    conn.close();
    return 0;
}

Parameters go through the extended protocol, with the pg tag or a Query. A hole is always a parameter, never text in the SQL. Holes are typed: an empty Optional is NULL, and a DynamicArray<uint8> is bytea:

Optional<string> supplier = Optional::None;
Query q = pg"select name, qty from items where qty > ${least} and supplier is not distinct from ${supplier}";
Result<Rows, PgError> r = conn.query(q: q);

rowAs::<T> reads a row into a @generate(Deserializable) type by column name. prepare gives a reusable Statement. startRows/nextRows read a large result a chunk at a time, and openPortal/fetch read a cursor inside a transaction.

A server error is PgError::Server with every field PostgreSQL sends. Compare e.sqlstate() with the constants in postgres::sqlstate. Messages read as psql prints them. With no NoticeHandler set, notices go to std::log under the tag postgres.

TLS

TLS is libpq's, setting for setting: sslmode from disable to verify-full with libpq's fallbacks (prefer tries TLS and then plaintext, allow the other way round, each on a new connection after a refusal), sslnegotiation=direct (PostgreSQL 17 and later), sslrootcert (a file, or system), sslcrl and sslcrldir, sslcert/sslkey/sslpassword, sslcertmode, sslsni, ssl_min_protocol_version/ssl_max_protocol_version, channel_binding and sslkeylogfile. As in libpq, a root certificate file that exists means the server's chain is verified under any sslmode, and verify-full checks the server's name by libpq's rules (subjectAltNames, then the CN, one-label wildcards).

Result<Config, PgError> parsed = Config.parse(text: "host=db.internal dbname=inventory user=app sslmode=verify-full sslrootcert=/etc/pg/ca.crt");
// … connect as above; then:
bool encrypted = conn.sslInUse();
Optional<string> protocol = conn.sslAttribute(name: "protocol");   // "TLSv1.3"

Connection.connectWith(dialer:, config:) takes a Dialer (pgx's DialFunc) that opens each connection, through a tunnel or a proxy, and still gets libpq's host, address and encryption fallbacks. Connection.connectOver(transport:, config:) starts a session over one transport the caller opened, TLS included.

The TLS library is Mbed TLS, through @kama/tls, not OpenSSL. What that changes:

What it will cover

No GSSAPI/Kerberos/SSPI: a server that asks for them gets a clear error.

Tests

KAMA=/path/to/kama tools/test.sh               # hermetic unit tests, debug and release
KAMA=/path/to/kama tools/test-integration.sh   # live tests on PostgreSQL 14–18 and 19 beta, debug and release
tools/test-integration.sh --version 18         # one version
tools/pg.sh smoke --version 18                 # one login per auth and TLS method, with the server's own psql
tools/pg.sh down --version 18

The unit tests need no server. They include a scripted fake server that drives every startup and query path, hostile ones included, and runs Mbed TLS as the server for the TLS paths. The integration tests start each server in podman or docker, and run the whole suite a second time over TLS. Their expected results come from libpq itself: PostgreSQL's own authentication, service-file and TLS tests (its negotiation matrix and its host-name cases, replayed by tools/gen-ssl-vectors.sh), and the server container's libpq asked case by case (tools/gen-libpq-test-cases.sh).

The integration server has one role per authentication method and TLS on. Its test certificates are generated into out/ and never committed. See tests/integration/server/.

License

@kama/postgres is licensed under either of

at your option. It vendors nothing; its TLS comes from @kama/tls, whose README says what that bundles.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in @kama/postgres by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.