Docs
What it’s for
postgres is a PostgreSQL client over libpq: you write the SQL, bind $1-style parameters, and read typed rows. It backs the Postgres SQL catalog in iceberg, and it’s where Orca records runs so they resume where they stopped. postgres.pool adds a connection pool for a threaded service, such as one built on threads. It’s a client, not a framework: there’s no ORM and no async.
Install
A new project needs Modular’s channel, pixi’s build preview and the compiler before the tin:
pixi init app -c https://conda.modular.com/max -c conda-forge && cd app
sed -i.bak 's/^\[workspace\]/[workspace]\npreview = ["pixi-build"]/' pixi.toml
pixi add "mojo-compiler==1.2.0"
pixi shelf add postgres-mojo # brings threads-mojo, and libpq from conda-forge
libpq is loaded at run time from the environment, and conda-forge’s build links OpenSSL, so sslmode=require works. You need a PostgreSQL server to talk to; for a local one, pixi add postgresql gives you initdb and pg_ctl.
Quick start
Create a table, insert a row in a transaction, read it back and catch a duplicate by its SQLSTATE. This needs a running server; the program compiles as is.
from postgres import Connection, Params, sqlstate_of
def main() raises:
var conn = Connection("postgresql://localhost/app?connect_timeout=5")
_ = conn.execute(
"CREATE TABLE IF NOT EXISTS people ("
" id bigint PRIMARY KEY, name text NOT NULL, note text)"
)
var tx = conn.begin()
_ = tx.execute(
"INSERT INTO people VALUES ($1, $2, $3)",
Params().int64(1).text("Ada").null(),
)
tx.commit() # without this, the insert rolls back
var res = conn.query(
"SELECT id, name, note FROM people WHERE id >= $1", Params().int64(1)
)
for row in res:
var note = "NULL" if row.is_null("note") else row.text("note")
print(row.int64("id"), row.text("name"), note)
try:
_ = conn.execute(
"INSERT INTO people VALUES ($1, $2, $3)",
Params().int64(1).text("Ada").null(),
)
except e:
if sqlstate_of(e) == "23505":
print("already there")
API
Everything but the pool is exported from postgres.
Connection(conninfo: String) raises or Connection(config: ConnectionConfig) raises: one PGconn, used from one thread. The conninfo is a libpq URI or keyword string.
execute(mut self, sql: String, params: Params = Params()) raises -> Int: runs a command, discards any rows and returns the count from the command tag.query(mut self, sql: String, params: Params = Params()) raises -> Result: runs a command and returns its rows.prepare(mut self, name: String, sql: String) raises -> Statement: preparessqlon the server once; theStatementthen hasexecute(params)andquery(params).begin(mut self) raises -> Transaction: issuesBEGIN.copy_in(mut self, sql: String) raises -> CopyInandcopy_out(mut self, sql: String) raises -> CopyOut: the bulk path,COPY ... FROM STDINandCOPY ... TO STDOUT.last_error(self) -> PostgresError,is_alive(self) -> Bool,ping(mut self) -> Boolandclose(mut self).
Transaction: execute, query, savepoint(name), rollback_to(name), release(name), commit() and rollback(). A transaction that is dropped, or a with conn.begin() block left, without commit() rolls back.
Params(): a chainable builder, one method per type, each binding the matching OID: text, int16, int32, int64, float32, float64, bool, numeric (a String), bytea, date_days, time_micros, timestamp_micros, timestamptz_micros, uuid, json, jsonb, and null(oid: UInt32 = 0).
Result: iterable over Row, with num_rows(), num_cols(), row(row: Int) raises -> Row, column_name, column_oid and affected_rows(). Row is a snapshot that outlives its Result. Its accessors take a column index or a name: is_null, opt_text, text, bool, int16, int32, int64, float32, float64, numeric, bytea, date_days, time_micros, timestamp_micros, timestamptz_micros, uuid and json.
CopyIn: write(data), write_rows(mut encoder: CopyEncoder), finish() raises -> Int (the row count) and abort(reason). CopyOut: next(mut chunk), read_all() and rows() raises -> List[String]. CopyEncoder builds COPY rows (field, null, end_row, row) and decode_row splits one back into Optional[String] cells.
ConnectionConfig(host: String = "localhost", port: Int = 5432, dbname: String = "", user: String = "", password: String = "", application_name: String = "postgres.mojo", sslmode: String = "prefer", connect_timeout: Int = 10): typed fields instead of a URI. set(key, value) adds any other libpq keyword.
postgres.pool (import it by name): ConnectionPool(conninfo: String, config: PoolConfig = PoolConfig()) raises, with lease() raises -> Lease for with pool.lease() as lease: lease.connection().query(...), plus ref(), stats(), reap() and close(). PoolConfig(max_size: Int = 10, min_idle: Int = 0, acquire_timeout_ms: Int = 5000, max_lifetime_ms: Int = 1_800_000, max_idle_ms: Int = 600_000, on_return: Int = ON_RETURN_ROLLBACK, validate_on_checkout: Bool = True).
Errors and types
- Every server error raises an
Errorshapedpostgres [SQLSTATE 23505] <message>, followed byDETAIL:,HINT:andSQL:lines when there are any.sqlstate_of(e)reads the code back.last_error()keeps the structuredPostgresErrorwith predicates such asis_unique_violation(),is_deadlock()andis_retryable(). - Two codes are made on the client:
08001when a connection never came up,08006once it’s closed or lost. Committing a transaction block that already failed raises25P02(after rolling it back) instead of silently rolling back. - Typed accessors parse the cell’s text as the type you ask for; they don’t check the OID. A NULL cell raises instead of reading as zero.
numericstays aString, so no digits are lost. Dates and times are epoch integers,timestamptzis normalized to UTC, and aninfinityor BC date raises: read it withtext(). - A
Statement,Transaction,CopyInorCopyOutshares its connection and keeps it open after theConnectionvalue is gone.close()ends it for all of them; they raise08006after that. - A pool lease returns its connection with
ROLLBACKif a block is still open. A handle that outlives its lease gets the connection closed instead of pooled, so two threads never share aPGconn. A checkout that waits longer thanacquire_timeout_msraises53300. Nothing reaps idle connections in the background: callreap()on your own timer. - Results and parameters use text format; binary format,
LISTEN/NOTIFYand async aren’t supported. Server notices go to stderr; addoptions=-c client_min_messages=warningto the conninfo to quiet them.
Tests
pixi run unit needs no server: config rendering, SQLSTATE parsing, the text codec, Params, the COPY codec, and loading libpq with and without CONDA_PREFIX. pixi run server and pixi run pool each start a throwaway cluster from the conda postgresql package on a free port, with no Docker. They cover every type, transactions, COPY, every error path and the pool under twelve threads (on the stable toolchain only, since the nightly can’t read the threads package), with a negative control that proves the escape check can fail. A cross-check writes an eighteen-column fixture with this tin and reads it cell by cell with psycopg 3, then the other way round, and compares both with psql. CI runs pixi run test on Linux and macOS, on the stable and nightly toolchains.
The full reference is the README on GitHub.