PostgreSQL Client Applications
Use the native PostgreSQL client to execute parameterized SQL, convert results to typed Codon values, and write computed results back. The API also provides explicit transactions, server cursors, and COPY for bulk transfer.
The example below performs the full read–compute–write cycle using temporary tables, so the server needs no permanent schema changes.
Before you begin: install the all-SDK distribution and have access to a PostgreSQL development database. The example uses temporary tables and does not require a Codon server extension or a database superuser.
Read, Compute, and Write
Save the example as client.codon. It inserts readings into a temporary table,
computes their total in Codon, and writes the summary back. The temporary tables
are removed when the connection closes.
import exaloop.databases.postgres as postgres
with postgres.connect(dbname="postgres") as connection:
connection.execute("CREATE TEMP TABLE readings (id bigint PRIMARY KEY, value double precision)")
connection.executemany("INSERT INTO readings VALUES ($1, $2)", [(1, 1.5), (2, 2.5)])
with connection.query("SELECT id, value FROM readings WHERE value >= $1 ORDER BY id", (1.0,)) as result:
readings = result.fetchall(Tuple[int, float])
total = sum(value for identity, value in readings)
connection.execute("CREATE TEMP TABLE summary (total double precision)")
connection.execute("INSERT INTO summary VALUES ($1)", (total,))
print(total)
Run with an installed SDK and your usual libpq connection configuration:
exaloop --with postgres run -release client.codon
4.0
Use an individual SDK without the CLI
codon run -release -plugin /path/to/sdk/postgres client.codon
The SQL placeholders keep values separate from the
query, while Tuple[int, float] describes the rows used in the calculation.
For another server, pass connection settings to postgres.connect() or use
your existing libpq environment configuration.
Connections and Security
For remote connections, use sslmode="verify-full" and provide a trusted CA
through sslrootcert. The libpq default, prefer, may fall back to plaintext;
it is not a substitute for server verification. Keep passwords in private
configuration or a permission-restricted password file.
Each concurrent worker needs its own connection, with a with block providing
deterministic cleanup. A connection loss during a write leaves its outcome
uncertain: establish whether the operation is safe to repeat before retrying.
See the connection options for timeouts, connection strings, and client certificates.
Parameters and Results
Use $1, $2, and so on for parameter values, with a separate tuple such as
(identity,). Parameters cannot stand in for table or column names; use
connection.quote_identifier() when a name must be chosen dynamically.
The main query and fetching methods are:
| API | Contract |
|---|---|
query(sql, params=()) | Owned, fully buffered Result, including for INSERT ... RETURNING |
execute(sql, params=()) | Discard result rows and return affected count, or -1 when unavailable |
executemany(sql, parameters, atomic=True) | Execute each parameter set; atomic transaction/savepoint by default |
result.columns, result.types | Column names and PostgreSQL type OIDs |
result.command, result.rowcount | Command tag and affected/selected count |
result.fetchone(Row) | Optional[Row]; absent when exhausted |
result.fetchmany(Row, size=2048) | Up to size owned rows |
result.rows(Row), result.batches(Row, size=2048) | Consume rows or batches |
result.fetchall(Row) | Materialize all remaining rows |
result.close() | Idempotently release native result storage |
Choose a row type with one field per column and use Optional[T] for nullable
fields. Values copied into Codon remain usable after the result closes.
For database-specific types, reading text is often the simplest way to preserve
the value. This is especially useful for exact numeric values and timestamps
with time zones. The type reference explains when to
use text, native dates, or JSON.
Transactions
Outside an explicit transaction, statements commit individually.
A transaction context rolls back unless you explicitly call commit().
Put the commit at the end of the successful path:
import exaloop.databases.postgres as postgres
with postgres.connect(dbname="postgres") as connection:
connection.execute("CREATE TEMP TABLE accounts (id bigint PRIMARY KEY, balance bigint)")
connection.executemany("INSERT INTO accounts VALUES ($1, $2)", [(1, 100), (2, 0)])
with connection.transaction() as transaction:
connection.execute("UPDATE accounts SET balance=balance-$1 WHERE id=$2", (25, 1))
connection.execute("UPDATE accounts SET balance=balance+$1 WHERE id=$2", (25, 2))
transaction.commit()
If either update fails, execution leaves the block before commit() and the
transfer is rolled back. For a callback-based alternative,
connection.run_transaction(operation, *args) commits when the operation returns
and rolls back if it raises.
Nested scopes use savepoints. Keep transaction control in one style rather than
mixing these scopes with raw BEGIN and COMMIT statements. For bulk inserts,
executemany() makes the batch atomic by default; use COPY when throughput is
more important than issuing individual statements.
Large Queries
query() buffers the complete PostgreSQL result. Batching that result bounds
Codon conversion allocations, not libpq's result buffer. Use a server cursor for
bounded fetching:
import exaloop.databases.postgres as postgres
with postgres.connect(dbname="postgres") as connection:
with connection.cursor("SELECT generate_series(1, $1)::bigint", (10003,), batch_size=1024) as cursor:
total = 0
for batch in cursor.batches(Tuple[int], size=2048):
total += sum(row[0] for row in batch)
assert total == 10003 * 10004 // 2
A cursor keeps a server transaction open, so finish consuming it promptly and
close it before ending the transaction. The with blocks handle cleanup here.
Fetching in batches limits client memory, but PostgreSQL may still need working
memory for a sort or join.
Bulk Transfer with COPY
copy_from(sql, chunks) accepts bytes or text chunks for COPY ... FROM STDIN.
copy_to(sql, destination) writes bytes to a binary file-like object whose
write() returns the byte count. Both return the copied row count. PostgreSQL
handles CSV, text, or binary framing according to the supplied COPY options:
import exaloop.databases.postgres as postgres
with postgres.connect(dbname="postgres") as connection:
connection.execute("CREATE TEMP TABLE imported (id bigint, name text)")
assert connection.copy_from("COPY imported FROM STDIN WITH (FORMAT csv)",
[b'1,"hello, world"\n', b'2,last\n']) == 2
with open("export.csv", "wb") as output:
assert connection.copy_to("COPY (SELECT * FROM imported ORDER BY id) TO STDOUT WITH (FORMAT csv)", output) == 2
The example imports two CSV rows and writes them to export.csv. For a large
file, supply a generator of byte chunks instead of holding the file in memory.
Use the dedicated COPY methods and reserve the connection for the transfer
until it finishes.
Handling Errors
Catch DatabaseError to handle a rejected query and inspect sqlstate when you
need to distinguish causes, such as a duplicate key. Inside a transaction, roll
back before continuing. A ConnectionError means you need a new connection.
This client is synchronous and does not provide a connection pool or automatic retries. See the compatibility reference when planning a deployment.
Next Steps
- Package the client with its native dependencies.
- Create a server function when a calculation belongs inside a SQL query.
- Check SQL value conversions for exact decimals and dates.