This directory contains a set of examples demonstrating the usage of the qbm-pgsql module, a C++20 asynchronous
PostgreSQL client integrated with the QB Actor Framework. Most lessons use the coroutine API;
09-callbacks-and-await teaches the callback chain and its synchronous await() drain.
- Overview
- Prerequisites
- Connection String
- Building the Examples
- Running the Examples
- Example Descriptions
These examples are designed to illustrate core functionalities of the qbm-pgsql library, including:
- Establishing connections to a PostgreSQL server.
- Executing simple and prepared SQL statements.
- Managing database transactions.
- Handling various PostgreSQL data types.
- Implementing error handling strategies.
Only 05-errors.cpp is an actor application (ErrorHandlingActor, driven by qb::Main,
05-errors.cpp:79, :313-321). Examples 1–4 use no actors at all: they are standalone qb-io
coroutines, scaffolded as qb::io::async::init() + coro_scheduler().spawn(...) + run_until(...)
(01-connect-and-query.cpp:90-96).
qb::pg::tcp::database db;
if (!co_await db.connect(PG_CONNECTION_STRING)) // yields bool
co_return;
auto reply = co_await db.execute("SELECT version();"); // Reply<resultset>
if (reply.ok()) {
const auto &rs = reply.result();
if (!rs.empty())
std::cout << rs[0][0].as<std::string>();
} else {
std::cerr << reply.error().what(); // qb::pg::error::db_error
}
// Prepared statements
co_await db.prepare(NAME, "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id;",
qb::pg::type_oid_sequence{qb::pg::oid::text, qb::pg::oid::text});
auto ins = co_await db.execute(NAME, qb::pg::params{name, email}); // Reply<resultset>
// Transactions — explicit and manual; nothing commits or rolls back for you
if (!(co_await db.begin()).ok())
co_return;
// ... statements; on the first !ok() call `co_await db.rollback();` yourself ...
co_await db.commit();The coroutine examples use linear error handling; 09-callbacks-and-await shows the separate
.then() / .success() / .error() callback chain. db_error carries what(),
severity, code (SQLSTATE string), detail, a structured sqlstate enum and, for a server error, every other field
the server sent (constraint_name(), table_name(), hint(), position(), ...). Client-side conversion failures are
still thrown: qb::pg::error::value_is_null and qb::pg::error::field_type_mismatch, caught with try/catch
(05-errors.cpp:271, :282).
- PostgreSQL Server: A running PostgreSQL server instance (version 9.6 or higher recommended).
- Database and User: A PostgreSQL database and a user with privileges to connect, create tables, and perform
DML/DDL operations on that database. The examples default to a database named
testand a usertestwith passwordtest. - QB Framework: The QB Actor Framework, including
qb-core,qb-io, andqbm-pgsqlmodules, must be built. - CMake: 3.22 is the floor declared by
examples/CMakeLists.txt:25, but the examples are built from the superproject, whose own floor is 3.24 (CMakeLists.txt:2,qb/CMakeLists.txt:31). Configuring through aCMakePresets.jsonpreset needs 3.25 (the file is schema v6). - C++20 Compiler: the examples are coroutines; C++17 will not compile them.
Every lesson reads QB_EXAMPLE_PG_URI at startup, falling back to:
tcp://test:test@localhost:5432[test]
Set the variable to match your PostgreSQL server's host, port, database name, username, and password.
For example: QB_EXAMPLE_PG_URI='tcp://test:test@127.0.0.1:55432[test]' ./qb-example-modules-pgsql-parameters.
Use a disposable database when running a whole corpus. Lessons 02, 04–07, 09 and 10 create
connection-local temporary tables, so same-named permanent tables remain untouched. Lesson 03
uses a random table name because a READ ONLY transaction is allowed to update a temporary table;
its read-only demonstration needs a permanent table. It drops that table only after successful
creation. Lesson 07 also puts its trigger function in pg_temp and derives its channel from the
publisher's backend PID, isolating simultaneous runs.
The callback lesson qualifies every table reference with pg_temp and stops after failed
temporary-table setup, so a public table cannot answer its later queries. The transaction
lesson checks rollback and DROP; cleanup failure exits nonzero and names the table for repair.
The ownership regression check creates its own PostgreSQL cluster on a free loopback port, seeds same-named public tables, runs each affected lesson twice concurrently, and checks that only those sentinels remain. Run it after building the PostgreSQL targets:
python3 examples/06-modules/pgsql/test-ownership.py build/presets/release --strictThe format is schema://[user[:password]@]host[:port][database_name].
schema: Can betcpfor plain TCP connections orsslif you are usingqb::pg::tcp::ssl::databaseand your server supports SSL.[...]: Optional parts. If database name is omitted, it might try to connect to a database with the same name as the user.
Build from the superproject root, which force-enables QB_BUILD_EXAMPLES:
cmake --preset release
cmake --build --preset release --target qb-example-modules-pgsql-connect-and-queryThere is no standalone build. This directory's CMakeLists.txt has no cmake_minimum_required
and calls qb_example(), which only qb defines — cmake -S examples/06-modules/pgsql fails before it
compiles anything. The same is true of the whole isndev/qb-examples repository; see
examples/README.md.
The executables land in build/presets/<preset>/examples/06-modules/pgsql/.
Once built, run each example from the build output directory:
./build/presets/release/examples/06-modules/pgsql/qb-example-modules-pgsql-connect-and-queryMake sure your PostgreSQL server is running and accessible with the connection string you've configured in the source
file. All eleven need a live server: dev/agent/run-examples.py records needs = postgres for every
target here and reports a SKIP, never a pass, when nothing answers.
- Purpose: Demonstrates the most basic interaction with a PostgreSQL database: establishing a connection and executing a simple query.
- Key Features:
- Standalone
qb-ioscaffolding — no actor, noqb::Main:init()+coro_scheduler().spawn()+run_until()(01-connect-and-query.cpp:90-96). - Creating a
qb::pg::tcp::databaseclient (:53). co_await db.connect(uri)— yieldsbool; on failuredb.error().what()says why (:56-59).co_await db.execute("SELECT version();")— yieldsReply<resultset>; no explicit transaction is opened (:65).reply.ok()/reply.result()/reply.error(), thenrs[0][0].as<std::string>()to read one field (:66-73).- A scope guard flips the
runningflag on every exit path sorun_until()stops (:46-51).
- Standalone
- Database Operations:
SELECT version();
- Purpose: Illustrates the use of prepared statements for enhanced performance and security.
- Key Features:
- Creating the temporary
userstable with a plainco_await db.execute(sql)(02-parameters.cpp:125) — DDL is not prepared. - Preparing the INSERT and SELECT with
co_await db.prepare(name, sql, qb::pg::type_oid_sequence{...})→Reply<PreparedQuery>(:138,:148), specifying parameter type OIDs (qb::pg::oid::text,qb::pg::oid::int4). - Executing them by name with packed parameters:
co_await db.execute(PREPARE_INSERT_USER, qb::pg::params{name, email})(:62)co_await db.execute(PREPARE_SELECT_USER_BY_ID, qb::pg::params{new_id})(:79)
- Retrieving a generated id from the
RETURNING idresult set. std::optionalfor nullable columns —qb::pg::paramsacceptsstd::nullopttransparently (:158).- Detecting a unique-constraint violation by SQLSTATE:
std::string(err.code) == "23505"(:64-67). - A miss: selecting id
999and finding an empty result set (:163-173). - Cleanup is an ordinary awaited statement at the end of the coroutine, not a destructor and not a blocking
.await()(:175-185).
- Creating the temporary
- Database Operations:
CREATE TEMP TABLE users (...)INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id;SELECT id, name, email FROM users WHERE id = $1;DROP TABLE IF EXISTS users;
Its @brief promised savepoints and its body contained none (measured: zero calls to savepoint,
rollback_savepoint or release_savepoint); it had no with_transaction; and it ended by
checking the balances of three accounts it never created. It now covers all five shapes, each with
a gated verdict: manual begin/commit/rollback, qb::pg::with_transaction and its exact
exception contract (transaction_abort → rollback + failed Reply; anything else → rollback then
rethrow; nesting refused before a BEGIN is sent), SAVEPOINTs for partial rollback,
transaction_mode for isolation and READ ONLY, and set_timeout for a statement_timeout that
fails with SQLSTATE 57014. Note the spellings: rollback_savepoint and release_savepoint, not
rollback_to and release.
- Purpose: every shape the word "transaction" covers in this client, each proved against a live server rather than described. The worked scenario is still a fund transfer, but between two rows of a table the program creates and drops itself.
- Key Features:
- Manual control —
co_await db.begin(), the statements, thencommit()orrollback()yourself, on the branch you decide. Ordinary control flow: checkreply.ok()after eachco_await. The example prints the trap that makes this shape dangerous — aBEGINwhoseUPDATEfailed may leave a PostgreSQL block aborted, in which case a laterCOMMITrolls back the whole block. Local failures that sent no SQL and successful updates affecting zero rows need an explicit application decision; check everyReply. qb::pg::with_transactionand its exact exception contract:transaction_abort→ rollback plus a failedReply; any other exception → rollback then rethrow; a nested call is refused before aBEGINis even sent.- SAVEPOINT for partial rollback — note the spellings,
rollback_savepointandrelease_savepoint, notrollback_toandrelease. transaction_modefor isolation level and READ ONLY.set_timeoutfor astatement_timeoutthat fails with SQLSTATE 57014.
- Manual control —
- Database Operations — one table, created and dropped by the program:
CREATE TABLE qb_tx_accounts_<run-id> (...)(a fresh random name)INSERT,UPDATE, andSELECTagainst that run's tableDROP TABLE qb_tx_accounts_<run-id>;
- Run:
./build/presets/release/examples/06-modules/pgsql/qb-example-modules-pgsql-transactions
- Purpose: Demonstrates inserting and retrieving various PostgreSQL data types, showcasing the mapping between C++
types and PostgreSQL types supported by
qbm-pgsql. - Key Features:
- Creating a table (
data_types_test) with columns of many different PostgreSQL types (integers, text, boolean, numeric/decimal, float, date/time, UUID, bytea, JSON/JSONB, arrays). - Preparing an
INSERTwith placeholders for all supported types and specifying their OIDs, viaco_await db.prepare(name, sql, qb::pg::type_oid_sequence{...}). - Preparing a
SELECTstatement to retrieve all columns. - Inserting sample data using C++ types like
int,short,long long,std::string,bool,float,double,qb::wall_time,qb::uuid,std::vector<char>(for bytea),qb::json,std::vector<int>,std::vector<std::string>, andstd::optional<std::string>. - Retrieving data using
row[column_name].as<ExpectedCppType>(). - Displaying the retrieved data, including formatting for timestamps and byte arrays.
- Creating a table (
- Database Operations:
CREATE TEMP TABLE data_types_test (...)(with numerous data types)INSERT INTO data_types_test (...) VALUES ($1, $2, ..., $21) RETURNING id;SELECT * FROM data_types_test WHERE id = $1;DROP TABLE IF EXISTS data_types_test;
- Purpose: Focuses on demonstrating how
qbm-pgsqlreports various database and client-side errors. The only actor-based example here —ErrorHandlingActor : public qb::Actor(:70) withqb::io::async::task<bool> onInit()thatco_await db.connect(...), added to aqb::Mainengine (:277-284). - Key Features:
- A helper
printDbErrorto display detailed information from aqb::pg::error::db_error(severity, SQLSTATE, message, detail) —:130. - Scenario 1: Syntax Error: Executing an intentionally malformed SQL query to trigger a
42601 (syntax_error). - Scenario 2: Unique Constraint Violation: Attempting to insert a duplicate value into a column with a
UNIQUEconstraint, triggering a23505 (unique_violation). - Scenario 3: Check Constraint Violation: Attempting to insert data that violates a
CHECKconstraint (e.g., negative quantity), triggering a23514 (check_violation). - Scenario 4: Client-Side Result Processing Errors:
- Attempting to retrieve a
NULLdatabase value using.as<std::string>()(non-optional type), expecting aqb::pg::error::value_is_nullexception. - Attempting to retrieve an integer column using
.as<std::string>()(type mismatch), expecting aqb::pg::error::field_type_mismatchexception.
- Attempting to retrieve a
- Two different mechanisms, deliberately: server-side failures arrive as data —
if (!reply.ok()) printDbError(..., reply.error())— while client-side conversion failures are thrown and caught withtry/catch(:237-254). - Driving an SQL-issuing coroutine from a synchronous actor handler with
spawn(...).
- A helper
- Database Operations:
CREATE TEMP TABLE error_test_items (...)(withUNIQUEandCHECKconstraints)INSERT INTO error_test_items (name, quantity, description) VALUES ($1, $2, $3);SELECT name, quantity, description FROM error_test_items WHERE name = $1;- Deliberately malformed
SELEC * FRM non_existent_table; DROP TABLE IF EXISTS error_test_items;
These examples provide a solid foundation for understanding and utilizing the qbm-pgsql module in your C++
applications. Remember to adapt the connection settings and SQL schemas to your specific needs.
- Purpose: reading a result set without writing a loop, and the two runtime failures the by-name
loop invites (a misspelled column name is a string that compiles;
as<T>()on a NULL throws unlessTis astd::optional). Both move into ONE place — the tuple you declare. - API:
row::as<std::tuple<...>>(),resultset::one<Ts...>()(the single-row query, as anoptional<tuple>),all<Ts...>()(an eager vector you own) versusrows<Ts...>()(a lazy view that BORROWS the result set and creates a tuple on each dereference; string fields can allocate),field::text()/field::view()for a read with no copy, andresultset::json()for the whole set at once. - Names: the architecture document called these
first_asandviews<Ts...>. Neither exists; the shipped names areone/all/rows(qbm/pgsql/src/qbm/pgsql/resultset.h:214/230/248). - Two things measured while writing it: the tuple is POSITIONAL over the SELECT list with no name
matching, so a mismatch throws at RUN time; and
resultset::json()renders every scalar as a JSON STRING (only NULL is typed), because it emits the text-format wire bytes rather than re-typing them. - Run:
./build/presets/release/examples/06-modules/pgsql/qb-example-modules-pgsql-typed-rows
- Purpose: PostgreSQL as an event bus — the replacement for a
SELECT ... WHERE updated_at > $1poll. Anotify_co_consumeryouco_await, and a TRIGGER that callspg_notify()so the writer does not have to know a listener exists. - API:
qb::pg::tcp::notify_co_consumer,listen/unlisten/unlisten_all,notify,on_notify+on_notify_dropped,receive(),notify_channel_capacity(),enable_auto_reconnect,on_connected,listened_channels(),notification::kind. - The four rules it measures: delivery is TRANSACTIONAL (a NOTIFY inside a transaction arrives at COMMIT, never after a rollback); it is NOT a queue (one sent while nobody listens is gone); the payload is capped at 8000 bytes, so send an identifier and let the reader fetch the row; and the subscription belongs to the SESSION — which the client keeps for you.
- A reconnection replays the LISTEN and marks the hole (Huly QB-111): the link is dropped from the
server (
pg_terminate_backendfrom the publisher — what a failover or an admin does), the armed reconnection run makes a new session and replays whatlisten()left, and the SAMEreceive()loop goes on: its next element is a gap marker (kind == notification_kind::gap, reasonreconnect) — what was published while no session listened is gone, so that is where to reconcile against the table, on the same connection, which is ready — then the next notification. The example checks the replay on the server itself (pg_listening_channels()). Until 3.3 a reconnected consumer listened on nothing until the program issued LISTEN again. disconnect()ends the session: terminal (down, reasonuser, no run), the stream ends withnullopt, the channels are forgotten, and the nextconnect()is a fresh session. It completes the teardown inside the call and is safe from a coroutine (Huly QB-253).- Run:
./build/presets/release/examples/06-modules/pgsql/qb-example-modules-pgsql-listen-notify
- Purpose: PostgreSQL has NO TLS port. A client connects in cleartext, sends an 8-byte SSLRequest
and reads one byte back; only then is the socket upgraded. So the DSN stays
tcp://and the only change is the client TYPE —qb::pg::tcp::ssl::database, which selects thestcptransport and hands it tostarttls_connectwithpostgres_ssl_negotiator. - API:
qb::pg::connection_options+ssl_verify_mode,connect(options),application_name,set_timeout/get_timeout,qb::pg::sqlstate::query_canceled. - What it says out loud: the default
ssl_verify_mode::noneis ENCRYPTED BUT UNAUTHENTICATED (libpq'ssslmode=require) — it defeats a passive listener and nothing else. Andset_timeoutis TRANSACTION-scoped: it rides with the nextBEGINasSET LOCAL statement_timeout, so it must be called BEFOREbegin(), and a query it cancels is SQLSTATE 57014 rather than a dropped connection. - It MEASURES the server rather than asserting about it: whether TLS is available is the
operator's decision (
ssl = on), so sections 1 and 2 print what they found in either direction. The proof ispg_stat_ssl— the server's own view of the connection, not the client's opinion. - Run:
./build/presets/release/examples/06-modules/pgsql/qb-example-modules-pgsql-tls-and-limits
- Purpose:
qbm-pgsqlhas TWO complete APIs over one connection — every operation is declared twice, once returningpg_reply_awaiter<T>and once taking callbacks — and only theco_awaithalf had ever been demonstrated. This file is the other half, and it contains noco_awaitat all: it is what you write from amain(), a migration tool, a test fixture, or any thread that is not running an event loop. - API:
execute(sql, on_success, on_error),.then()/.success()/.error(),qb::pg::discard_query/discard_error/discard_prepare,prepare(),prepare_file(),execute(name, qb::pg::params{...}, ...),Transaction::await()and its free-function formqb::pg::await(db), plus the one bridge synchronous code needs —qb::io::async::run_sync(db.connect(...)), becauseconnectis the single operation with no callback overload. - The chain is a QUEUE:
.then()appends a node that fires only if the prior step succeeded,.error()one for failure. A callback command can start sending as soon as it is enqueued on a ready connection;await()drives the loop until the queue drains. Statements still take ordered request/ReadyForQuery turns. The program reports whether any handler had fired before itsawait()call; that observation is not a start contract. - Why there are THREE discards and not one: the callback overloads take the handler by value and
call it unconditionally, with no per-reply branch, so a handler is never absent — and
prepare()wants(Transaction&, PreparedQuery const&)whereexecute()wants(Transaction&, results). Reaching fornullptris a compile error, which is the correct outcome. prepare_file()readsresources/sql/top-scores.sql— staged next to the binary byqb_stage_example_resources()— so the statement is reviewable, diffable and lintable AS SQL instead of surviving as an escaped C++ literal. The parameter OIDs still come from the call site.- Run:
./build/presets/release/examples/06-modules/pgsql/qb-example-modules-pgsql-callbacks-and-await
- Purpose: the result set that does not fit.
co_await db.query("SELECT * FROM huge")reads everyDataRowinto memory before resuming you;query_stream()declares a server-sideCURSORandFETCHes one batch at a time, so client memory isbatch_sizerows regardless of how many the query matches.LIMIT/OFFSETpaging is not the alternative —OFFSET nmakes the server walk and discard n rows every time, so a paged scan is quadratic. - API:
query_stream(sql, batch_size, on_row)yieldingqb::pg::Reply<void>, besidequery(),begin()/commit()/in_transaction()andqb::io::async::when_all. - The row is a VIEW: it points INTO the batch buffer, which the next
FETCHoverwrites. It is valid for the duration of the call and not one instruction longer — copy out a value (as<T>copies), never keep the row, afield, or astring_viewtaken from one. - Cursors need a transaction, and PostgreSQL allows one per session. Inside a transaction you
opened,
query_streamdeclares the cursor there and closes only the cursor; outside one it opens its ownBEGIN…COMMIT. Two streams overlapping on one connection therefore SHARE that block — the first opens it, the last to finish ends it, and it is rolled back if either failed. Each gets its own cursor NAME, which is what stops them from closing each other's; the program runs two at once throughwhen_alland checks both answers, because without that they would both read zero. - Failure is a Reply, not an exception — the exception path belongs to YOUR callback, and one that throws closes the cursor, rethrows, and leaves the connection usable.
- Run:
./build/presets/release/examples/06-modules/pgsql/qb-example-modules-pgsql-streaming-results
- Purpose: COPY FROM STDIN streams CSV through a source callback and COPY TO STDOUT streams it back through a sink. The program checks three imported rows and compares exported bytes to the expected CSV. The table is temporary and dropped on completion.
- Session evidence:
set_startup_option("application_name", ...)is called before connect;current_settingconfirms it reached the server.parameter_status("server_version")andserver_version()read its reported version.scram_authenticated()reports whether this particular server selected SCRAM and verified its signature; the lesson does not assume that authentication method. - CancelRequest evidence: a second session checks
pg_stat_activityuntil the first is actively executingpg_sleep(5), then callscancel_async()on the first client. The first query must return SQLSTATE 57014 and the same connection must still answerSELECT 1. This checks cancellation delivery and recovery, not event-loop latency.cancel()sends the same request synchronously and can block the caller for two seconds; call it from another thread, not an event-loop core. - Run:
./build/presets/release/examples/06-modules/pgsql/qb-example-modules-pgsql-copy-and-cancel