libpqxx 8 is ready for you!

Libpqxx 8 is the library's biggest release ever.

It's been a long trek. On the one hand it was a parallel journey to the adoption of C++20. We're not relying on all the new C++20 features, but concepts, std::source_location, spans, and ranges are integral parts of the design. Also I was hit by a health problem that reduced the amount of work I could do, and often made it hard for me to trust my own judgment. Luckily that happened after most of the important design work was done, or you would not see this release today.

On the other hand, I've also had help: community members reported bugs and submitted patches, some of very high quality. And people helped each other out, which is awesome to see. A Copilot subscription on GitHub helped with an extra layer of code reviews. It found a lot of typos in comments, but also some genuine and sometimes subtle mistakes. (I didn't let it write actual code though; it gets too many things wrong, some of which I would probably not have caught in review. Kernighan's Law applies.) Other large language models, in particular Claude, helped a lot in solving configuration problems.

Perhaps most of all, my wonderful former employer Eonics brought together a Mastermind group where a bunch of us discuss our hobby projects, learn, and share questions and ideas. The group often came up with unexpected and helpful approaches for my problems. If you have a hobby project, I wholeheartedly recommend looking for a mastermind group near you! And if you speak Dutch, have a look at the Eonics website. You'll find podcasts, hack nights, interviews, and much else to make IT work fun.

Thank you

Several people helped. I'm probably missing somebody, but I wanted to thank...

  • @tt4g for helping lots of people with their questions, often before I even see them.
  • Klaus Silveira @klaussilveira), our first donor-level sponsor.
  • Hendrik M. @Tosenaeus, who also sponsors us.
  • Eonics, CoffeeSprout, ComPosiX, and the whole Eonics Mastermind team.
  • Kirit Sælensminde, for discussing features, ideas, and C++ language changes.
  • J.H.M. "Ray" Dassen, much missed, who did a lot to help get libpqxx off the ground, all the way back.
  • All those who submitted patches, reported problems, or asked for improvements.

What's changed?

Just about everything has changed at least a little bit.

349 files changed, 28826 insertions(+), 24686 deletions(-)

Most "standard" code using libpqxx will still work without changes, but often you'll find that there are now better ways to do things. Functions and classes that were deprecated three years ago are now gone. Some other ones are now deprecated, and will be gone a few years from now.

The API has become better at helping you keep your code memory-safe and verifiable by static code analysis. There are fewer raw, C-style pointers. At the same time some things have become more efficient through the use of views, especially string_view, and internal simplifications. Ranges and spans have made things clearer and simpler. Concepts have made some things more flexible.

More details follow. See the NEWS file for a more complete list with less detail, or UPGRADING.md for advice on how to deal with breaking changes.

Things that can break with old code

If you're just using 8.0 as a drop-in replacement for an older version in an existing application, this may be the only useful section for you.

C++20 is now the minimum. As per the above it's fine if some C++20 features don't work for you yet, but concepts definitely need to work, and you'll need a bunch of the new C++20 standard library features.

PostgreSQL 11 is now the absolute minimum. You won't be able to connect to older servers. The exact version is a fairly arbitrary choice. The oldest version supported upstream is 14, so hopefully 11 is liberal enough.

Exceptions have changed. The whole class hierarchy is different, so if you do any detailed catching you'll have to review that. On the bright side, libpqxx exceptions are now all derived from pqxx::failure so you may not need as many catch blocks as before. Your handler can figure out much of what it needs by calling the exception's member functions, rather than by knowing its type.

Rows and fields have changed. The old row and field classes used to keep a result object alive in memory, even if you destroyed the result object. Now, most of the time you'll get a row_ref or field_ref which does not do that. Be careful with lifetimes, but it rarely becomes an issue, and the new classes are simpler and more efficient.

Result & row iterators have changed. Indexing an iterator now finally does what the C++ standard says, even though it's probably less intuitive or useful. If you previously did i[n], you'd now do (*i)[n].

Comparing results, rows, or fields now compares identity, not contents. I doubt anyone ever used what these == operators did.

pqxx::bytes is now an alias for std::vector<std::byte>, not a std::basic_string<std::byte>. It's not clear that the fine print in the standard ever quite allowed the string definition. Some code will require small changes at compile time. If you ever used the c_str() member function, now you'll use data(). More appropriate for binary data.

The string conversion API has changed. This is the extensible API that converts between C++-side objects and their SQL text formats. If you wrote your own to support additional types, those should still work; but the newer API is generally easier, safer, and more efficient if you want to optimise.

A lot of strings no longer have or need a terminating zero. This includes the string conversion API.

In some situations, a params will require text encoding information. In those situations, pass your connection, transaction, or encoding group as the first argument to the params constructor.

No longer supports std::filesystem. Nor does the build try to figure out whether it needs to link any additional libraries to make it work.

A zview can no longer contain a null pointer. This used to be valid for empty strings, but it no longer is.

More classes are now final.

The scripts in tools/ have been revised. Shell scripts now have a ".sh" suffix to their name. Some old tools are gone: rmlo,pqxxthreadsafety, test_all.py.

The JOHAB encoding is not supported. As far as we can tell, there is no single standard for how this encoding was supposed to work, and support on the server side never fully worked anyway. It will be removed from PostgreSQL.

Some deprecated items are now gone:

  • binarystring (use blob instead).
  • connection_base (use just connection, it's the same thing).
  • encrypt_password() (use the equivalents in connection).
  • dynamic_params (use params instead).
  • transaction_base::unesc_raw() (use unesc_bin() instead).
  • transaction_base::quote_raw() (use quote(bytes_view) instead).
  • Some field constructors.
  • Row slicing. Has anyone ever used this?

Querying continues to evolve

Querying data has been growing simpler and more regular for a while now. But I'll summarise how the 8.0 way of querying differs from the old ways.

Passing statement parameters: You no longer call separate exec(), exec_params(), or exec_prepared(). Instead, you just call exec(), and in addition to the query, pass a params object if the statement needs parameters. If the query is a prepared statement rather than SQL text, you use its name, wrapped in a pqxx::prepped object:

tx.exec(
    pqxx::prepped{"my_query"},
    pqxx::params{1, 23, "param");

Executing, querying, streaming: These are the three models for executing a query, and where possible they look similar. "Executing" a query is the classic exec() function, and it returns a pqxx::result. "Querying" works very similar, but you pass template arguments to state which types of result columns you expect, and then you iterate over the return value. "Streaming" looks a lot like querying but is optimised for large data sets: it's slower for small numbers of rows but faster for large numbers. (Sadly a streaming query can not take parameters.)

Expected result sizes: If you want to make sure that a query returns a specific number of rows, you no longer use exec0(), exec1(), or exec_n(); you just use exec() and then call a member function on the result such as no_rows(), one_row(), or expected_rows(). There are similar functions for the number of columns.

Various new features

This is not a complete list! There's more than I can fit in here. You'll have to explore the documentation or the code to find them all.

Here are some major ones:

Example code. There is now an examples/ directory containing source for various toy applications. There will be more in the future. Unlike the tests, these examples are entirely meant for you, to illustrate what you can do with libpqxx. But unlike the documentation, they get compiled and run along with the test, so they won't fall out of date.

Source locations & stack traces: A libpqxx exception can now tell you the associated std::source_location and, if your compiler supports it, a full std::stacktrace.

Easier SQL arrays: Single-dimensional SQL arrays are now super easy, barely an inconvenience. You can now pass a std::vector<double> as a statement parameter and libpqxx will convert it to an SQL array. You can stream a std::array<std::string> directly out of an SQL array in a query, read a result field of SQL array type as a std::inplace_vector<int>, and so on. Multi-dimensional arrays are a bit harder, but not much: the pqxx::array class does all the parsing for you.

Generic binary data: Where you need to pass binary data to a libpqxx function, you can now generally pass any contiguous block of std::byte.

Result & row iterators are now proper random access iterators. This may allow some algorithms to work more efficiently.

Encoding groups. Sometimes when dealing with text, libpqxx needs to know its encoding. It doesn't need to know the exact encoding, but it does need enough information to be able to tell, for example, a closing quote in an SQL string from a byte in a multibyte character that happens to have the exact same value. There is an enum to describe the different basic encoding schemes, and it is now part of the public API.

Combine connection strings and connection parameters. You could already create a connection based on a connection string, or on key/value parameters. Now you can pass both.

Nicer connection::port_number() to replace connection::port(). The old function returned a C string, thanks to the underlying C API. The useful information is "either a number, or nothing." The new function returns std::optional<int> as you'd expect.

If available, uses C++ Reflection to obtain type names. This replaces a bunch of string constants that triggered false-positive alerts in some verification tools such as Valgrind.

Build improvements

The build has also seen some improvement:

Minimum C++ version. If you're building as C++20, you no longer need to pass an explicit option like -std=c++20 on the compiler command line. Both the CMake build and the configure build now default to C++20 if nothing is specified.

Unity builds. This style of build combines multiple C++ translation units into a single huge C++ file. This enables some cross-module optimisation, but (under the right circumstances) can also speed up the build. This is now supported in the CMake build.

More parallelism in configure build. A new, "flattened" hierarchy of "make" targets reduces unused CPU time while compiling. The tests are now in a single directory.

Various MSVC fixes. There were some compilation problems, and lots of warnings, with Microsoft Visual Studio in particular. There was even an error (apparently due to a compiler bug) in the 2026 edition. Fixed.

Test against ASCII-only databases. The regular libpqxx CI tests run against databases that support full Unicode. But some users test against ASCII-only databases, which broke the more advanced tests. The tests will now skip those checks if the database does not support Unicode.

Handy function to render a std::source_location as text. Called source_loc(), this produces a more or less standard format that IDEs seem to parse well, but which humans can also read.

Only one generated config header. When you configured a build, it used to generate several config-*.h headers in your build tree's include/pqxx/ directory. Now it's only config-compiler.h. And it contains only relevant macros, prefixed with "PQXX", even in the CMake build.

If you like it

Libpqxx represents decades of FTE work, mostly done in my spare time. If you would like to support this, please consider sponsoring by a small amount that you can spare:

  • Through GitHub: https://github.com/sponsors/jtv
  • Through Buy Me A Coffee: https://buymeacoffee.com/pqxx

This helps me pay for the costs of maintaining and publishing the library, and if enough people contribute, I'd like to do something nice for those who contribute their time, insights, and effort by supporting fellow users or submitting patches.