Adam Ierymenko 0e5651f353
1.12.0 merge to main (#2104)
* add note about forceTcpRelay

* Create a sample systemd unit for tcp proxy

* set gitattributes for rust & cargo so hashes dont conflict on Windows

* Revert "set gitattributes for rust & cargo so hashes dont conflict on Windows"

This reverts commit 032dc5c108195f6bbc2e224f00da5b785df4b7f9.

* Turn off autocrlf for rust source

Doesn't appear to play nice well when it comes to git and vendored cargo package hashes

* Fix #1883 (#1886)

Still unknown as to why, but the call to `nc->GetProperties()` can fail
when setting a friendly name on the Windows virtual ethernet adapter.
Ensure that `ncp` is not null before continuing and accessing the device
GUID.

* Don't vendor packages for zeroidc (#1885)

* Added docker environment way to join networks (#1871)

* add StringUtils

* fix headers
use recommended headers and remove unused headers

* move extern "C"
only JNI functions need to be exported

* cleanup

* fix ANDROID-50: RESULT_ERROR_BAD_PARAMETER typo

* fix typo in log message

* fix typos in JNI method signatures

* fix typo

* fix ANDROID-51: fieldName is uninitialized

* fix ANDROID-35: memory leak

* fix missing DeleteLocalRef in loops

* update to use unique error codes

* add GETENV macro

* add LOG_TAG defines

* ANDROID-48: add ZT_jnicache.cpp

* ANDROID-48: use ZT_jnicache.cpp and remove ZT_jnilookup.cpp and ZT_jniarray.cpp

* add Event.fromInt

* add PeerRole.fromInt

* add ResultCode.fromInt

* fix ANDROID-36: issues with ResultCode

* add VirtualNetworkConfigOperation.fromInt

* fix ANDROID-40: VirtualNetworkConfigOperation out-of-sync with ZT_VirtualNetworkConfigOperation enum

* add VirtualNetworkStatus.fromInt

* fix ANDROID-37: VirtualNetworkStatus out-of-sync with ZT_VirtualNetworkStatus enum

* add VirtualNetworkType.fromInt

* make NodeStatus a plain data class

* fix ANDROID-52: synchronization bug with nodeMap

* Node init work: separate Node construction and init

* add Node.toString

* make PeerPhysicalPath a plain data class

* remove unused PeerPhysicalPath.fixed

* add array functions

* make Peer a plain data class

* make Version a plain data class

* fix ANDROID-42: copy/paste error

* fix ANDROID-49: VirtualNetworkConfig.equals is wrong

* reimplement VirtualNetworkConfig.equals

* reimplement VirtualNetworkConfig.compareTo

* add VirtualNetworkConfig.hashCode

* make VirtualNetworkConfig a plain data class

* remove unused VirtualNetworkConfig.enabled

* reimplement VirtualNetworkDNS.equals

* add VirtualNetworkDNS.hashCode

* make VirtualNetworkDNS a plain data class

* reimplement VirtualNetworkRoute.equals

* reimplement VirtualNetworkRoute.compareTo

* reimplement VirtualNetworkRoute.toString

* add VirtualNetworkRoute.hashCode

* make VirtualNetworkRoute a plain data class

* add isSocketAddressEmpty

* add addressPort

* add fromSocketAddressObject

* invert logic in a couple of places and return early

* newInetAddress and newInetSocketAddress work
allow newInetSocketAddress to return NULL if given empty address

* fix ANDROID-38: stack corruption in onSendPacketRequested

* use GETENV macro

* JniRef work
JniRef does not use callbacks struct, so remove
fix NewGlobalRef / DeleteGlobalRef mismatch

* use PRId64 macros

* switch statement work

* comments and logging

* Modifier 'public' is redundant for interface members

* NodeException can be made a checked Exception

* 'NodeException' does not define a 'serialVersionUID' field

* 'finalize()' should not be overridden
this is fine to do because ZeroTierOneService calls close() when it is done

* error handling, error reporting, asserts, logging

* simplify loadLibrary

* rename Node.networks -> Node.networkConfigs

* Windows file permissions fix (#1887)

* Allow macOS interfaces to use multiple IP addresses (#1879)

Co-authored-by: Sean OMeara <someara@users.noreply.github.com>
Co-authored-by: Grant Limberg <glimberg@users.noreply.github.com>

* Fix condition where full HELLOs might not be sent when necessary (#1877)

Co-authored-by: Grant Limberg <glimberg@users.noreply.github.com>

* 1.10.4 version bumps

* Add security policy to repo (#1889)

* [+] add e2k64 arch (#1890)

* temp fix for ANDROID-56: crash inside newNetworkConfig from too many args

* 1.10.4 release notes

* Windows 1.10.4 Advanced Installer bump

* Revert "temp fix for ANDROID-56: crash inside newNetworkConfig from too many args"

This reverts commit dd627cd7f44ad623a110bb14f72d0bea72a09e30.

* actual fix for ANDROID-56: crash inside newNetworkConfig
cast all arguments to varargs functions as good style

* Fix addIp being called with applied ips (#1897)

This was getting called outside of the check for existing ips
Because of the added ifdef and a brace getting moved to the
wrong place.

```
if (! n.tap()->addIp(*ip)) {
	fprintf(stderr, "ERROR: unable to add ip address %s" ZT_EOL_S, ip->toString(ipbuf));
}
WinFWHelper::newICMPRule(*ip, n.config().nwid);

```

* 1.10.5 (#1905)

* 1.10.5 bump

* 1.10.5 for Windows

* 1.10.5

* Prevent path-learning loops (#1914)

* Prevent path-learning loops

* Only allow new overwrite if not bonded

* fix binding temporary ipv6 addresses on macos (#1910)

The check code wasn't running.

I don't know why !defined(TARGET_OS_IOS) would exclude code on
desktop macOS. I did a quick search and changed it to defined(TARGET_OS_MAC).
Not 100% sure what the most correct solution there is.

You can verify the old and new versions with

`ifconfig | grep temporary`

plus

`zerotier-cli info -j` -> listeningOn

* 1.10.6 (#1929)

* 1.10.5 bump

* 1.10.6

* 1.10.6 AIP for Windows.

* Release notes for 1.10.6 (#1931)

* Minor tweak to Synology Docker image script (#1936)

* Change if_def again so ios can build (#1937)

All apple's variables are "defined"
but sometimes they are defined as "0"

* move begin/commit into try/catch block (#1932)

Thread was exiting in some cases

* Bump openssl from 0.10.45 to 0.10.48 in /zeroidc (#1938)

Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.45 to 0.10.48.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.45...openssl-v0.10.48)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* new drone bits

* Fix multiple network join from environment entrypoint.sh.release (#1961)

* _bond_m guards _bond, not _paths_m (#1965)

* Fix: warning: mutex '_aqm_m' is not held on every path through here [-Wthread-safety-analysis] (#1964)

* Bump h2 from 0.3.16 to 0.3.17 in /zeroidc (#1963)

Bumps [h2](https://github.com/hyperium/h2) from 0.3.16 to 0.3.17.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.16...v0.3.17)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Grant Limberg <glimberg@users.noreply.github.com>

* Add note that binutils is required on FreeBSD (#1968)

* Add prometheus metrics for Central controllers (#1969)

* add header-only prometheus lib to ext

* rename folder

* Undo rename directory

* prometheus simpleapi included on mac & linux

* wip

* wire up some controller stats

* Get windows building with prometheus

* bsd build flags for prometheus

* Fix multiple network join from environment entrypoint.sh.release (#1961)

* _bond_m guards _bond, not _paths_m (#1965)

* Fix: warning: mutex '_aqm_m' is not held on every path through here [-Wthread-safety-analysis] (#1964)

* Serve prom metrics from /metrics endpoint

* Add prom metrics for Central controller specific things

* reorganize metric initialization

* testing out a labled gauge on Networks

* increment error counter on throw

* Consolidate metrics definitions

Put all metric definitions into node/Metrics.hpp.  Accessed as needed
from there.

* Revert "testing out a labled gauge on Networks"

This reverts commit 499ed6d95e11452019cdf48e32ed4cd878c2705b.

* still blows up but adding to the record for completeness right now

* Fix runtime issues with metrics

* Add metrics files to visual studio project

* Missed an "extern"

* add copyright headers to new files

* Add metrics for sent/received bytes (total)

* put /metrics endpoint behind auth

* sendto returns int on Win32

---------

Co-authored-by: Leonardo Amaral <leleobhz@users.noreply.github.com>
Co-authored-by: Brenton Bostick <bostick@gmail.com>

* Central startup update (#1973)

* allow specifying authtoken in central startup

* set allowManagedFrom

* move redis_mem_notification to the correct place

* add node checkins metric

* wire up min/max connection pool size metrics

* x86_64-unknown-linux-gnu on ubuntu runner (#1975)

* adding incoming zt packet type metrics (#1976)

* use cpp-httplib for HTTP control plane (#1979)

refactored the old control plane code to use [cpp-httplib](https://github.com/yhirose/cpp-httplib) instead of a hand rolled HTTP server.  Makes the control plane code much more legible.  Also no longer randomly stops responding.

* Outgoing Packet Metrics (#1980)

add tx/rx labels to packet counters and add metrics for outgoing packets

* Add short-term validation test workflow (#1974)

Add short-term validation test workflow

* Brenton/curly braces (#1971)

* fix formatting

* properly adjust various lines
breakup multiple statements onto multiple lines

* insert {} around if, for, etc.

* Fix rust dependency caching (#1983)

* fun with rust caching

* kick

* comment out invalid yaml keys for now

* Caching should now work

* re-add/rename key directives

* bump

* bump

* bump

* Don't force rebuild on Windows build GH Action (#1985)

Switching `/t:ZeroTierOne:Rebuild` to just `/t:ZeroTierOne` allows the Windows build to use the rust cache.  `/t:ZeroTierOne:Rebuild` cleared the cache before building.

* More packet metrics (#1982)

* found path negotation sends that weren't accounted for

* Fix histogram so it will actually compile

* Found more places for packet metrics

* separate the bind & listen calls on the http backplane (#1988)

* fix memory leak (#1992)

* fix a couple of metrics (#1989)

* More aggressive CLI spamming (#1993)

* fix type signatures (#1991)

* Network-metrics (#1994)

* Add a couple quick functions for converting a uint64_t network ID/node ID into std::string

* Network metrics

* Peer metrics (#1995)

* Adding peer metrics

still need to be wired up for use

* per peer packet metrics

* Fix crash from bad instantiation of histogram

* separate alive & dead path counts

* Add peer metric update block

* add peer latency values in doPingAndKeepalive

* prevent deadlock

* peer latency histogram actually works now

* cleanup

* capture counts of packets to specific peers

---------

Co-authored-by: Joseph Henry <joseph.henry@zerotier.com>

* Metrics consolidation (#1997)

* Rename zt_packet_incoming -> zt_packet

Also consolidate zt_peer_packets into a single metric with tx and rx labels.  Same for ztc_tcp_data and ztc_udp_data

* Further collapse tcp & udp into metric labels for zt_data

* Fix zt_data metric description

* zt_peer_packets description fix

* Consolidate incoming/outgoing network packets to a single metric

* zt_incoming_packet_error -> zt_packet_error

* Disable peer metrics for central controllers

Can change in the future if needed, but given the traffic our controllers serve, that's going to be a *lot* of data

* Disable peer metrics for controllers pt 2

* Update readme files for metrics (#2000)

* Controller Metrics & Network Config Request Fix (#2003)

* add new metrics for network config request queue size and sso expirations
* move sso expiration to its own thread in the controller
* fix potential undefined behavior when modifying a set

* Enable RTTI in Windows build

The new prometheus histogram stuff needs it.

Access violation - no RTTI data!INVALID packet 636ebd9ee8cac6c0 from cafe9efeb9(2605:9880:200:1200:30:571:e34:51/9993) (unexpected exception in tryDecode())

* Don't re-apply routes on BSD

See issue #1986

* Capture setContent by-value instead of by-reference (#2006)

Co-authored-by: Grant Limberg <glimberg@users.noreply.github.com>

* fix typos (#2010)

* central controller metrics & request path updates (#2012)

* internal db metrics

* use shared mutexes for read/write locks

* remove this lock. only used for a metric

* more metrics

* remove exploratory metrics

place controller request benchmarks behind ifdef

* Improve validation test (#2013)

* fix init order for EmbeddedNetworkController (#2014)

* add constant for getifaddrs cache time

* cache getifaddrs - mac

* cache getifaddrs - linux

* cache getifaddrs - bsd

* cache getifaddrs - windows

* Fix oidc client lookup query

join condition referenced the wrong table.  Worked fine unless there were multiple identical client IDs

* Fix udp sent metric

was only incrementing by 1 for each packet sent

* Allow sending all surface addresses to peer in low-bandwidth mode

* allow enabling of low bandwidth mode on controllers

* don't unborrow bad connections

pool will clean them up later

* Multi-arch controller container (#2037)

create arm64 & amd64 images for central controller

* Update README.md

issue #2009

* docker tags change

* fix oidc auth url memory leak (#2031)

getAuthURL() was not calling zeroidc::free_cstr(url);

the only place authAuthURL is called, the url can be retrieved
from the network config instead.

You could alternatively copy the string and call free_cstr in getAuthURL.
If that's better we can change the PR.

Since now there are no callers of getAuthURL I deleted it.

Co-authored-by: Grant Limberg <glimberg@users.noreply.github.com>

* Bump openssl from 0.10.48 to 0.10.55 in /zeroidc (#2034)

Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.48 to 0.10.55.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.48...openssl-v0.10.55)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Grant Limberg <glimberg@users.noreply.github.com>

* zeroidc cargo warnings (#2029)

* fix unused struct member cargo warning

* fix unused import cargo warning

* fix unused return value cargo warning

---------

Co-authored-by: Grant Limberg <glimberg@users.noreply.github.com>

* fix memory leak in macos ipv6/dns helper (#2030)

Co-authored-by: Grant Limberg <glimberg@users.noreply.github.com>

* Consider ZEROTIER_JOIN_NETWORKS in healthcheck (#1978)

* Add a 2nd auth token only for access to /metrics (#2043)

* Add a 2nd auth token for /metrics

Allows administrators to distribute a token that only has access to read
metrics and nothing else.

Also added support for using bearer auth tokens for both types of tokens

Separate endpoint for metrics #2041

* Update readme

* fix a couple of cases of writing the wrong token

* Add warning to cli for allow default on FreeBSD

It doesn't work.
Not possible to fix with deficient network
stack and APIs.

ZeroTierOne-freebsd # zerotier-cli set 9bee8941b5xxxxxx allowDefault=1
400 set Allow Default does not work properly on FreeBSD. See #580
root@freebsd13-a:~/ZeroTierOne-freebsd # zerotier-cli get 9bee8941b5xxxxxx allowDefault
1

* ARM64 Support for TapDriver6 (#1949)

* Release memory previously allocated by UPNP_GetValidIGD

* Fix ifdef that breaks libzt on iOS (#2050)

* less drone (#2060)

* Exit if loading an invalid identity from disk (#2058)

* Exit if loading an invalid identity from disk

Previously, if an invalid identity was loaded from disk, ZeroTier would
generate a new identity & chug along and generate a brand new identity
as if nothing happened.  When running in containers, this introduces the
possibility for key matter loss; especially when running in containers
where the identity files are mounted in the container read only.  In
this case, ZT will continue chugging along with a brand new identity
with no possibility of recovering the private key.

ZeroTier should exit upon loading of invalid identity.public/identity.secret #2056

* add validation test for #2056

* tcp-proxy: fix build

* Adjust tcp-proxy makefile to support metrics

There's no way to get the metrics yet. Someone will
have to add the http service.

* remove ZT_NO_METRIC ifdef

* Implement recvmmsg() for Linux to reduce syscalls. (#2046)

Between 5% and 40% speed improvement on Linux, depending on system configuration and load.

* suppress warnings: comparison of integers of different signs: 'int64_t' (aka 'long') and 'uint64_t' (aka 'unsigned long') [-Wsign-compare] (#2063)

* fix warning: 'OS_STRING' macro redefined [-Wmacro-redefined] (#2064)

Even though this is in ext, these particular chunks of code were added
by us, so are ok to modify.

* Apply default route a different way - macOS

The original way we applied default route, by forking
0.0.0.0/0 into 0/1 and 128/1 works, but if mac os has any networking
hiccups -if you change SSIDs or sleep/wake- macos erases the system default route.
And then all networking on the computer is broken.

to summarize the new way:
allowDefault=1
```
sudo route delete default 192.168.82.1
sudo route add default 10.2.0.2
sudo route add -ifscope en1 default 192.168.82.1
```

gives us this routing table
```
Destination        Gateway            RT_IFA             Flags        Refs      Use    Mtu          Netif Expire    rtt(ms) rttvar(ms)
default            10.2.0.2           10.2.0.18          UGScg          90        1   2800       feth4823
default            192.168.82.1       192.168.82.217     UGScIg
```

allowDefault=0
```
sudo route delete default
sudo route delete -ifscope en1 default
sudo route add default 192.168.82.1
```

Notice the I flag, for -ifscope, on the physical default route.

route change does not seem to work reliably.

* fix docker tag for controllers (#2066)

* Update build.sh (#2068)

fix mkwork compilation errors

* Fix network DNS on macOS

It stopped working for ipv4 only networks in Monterey.
See #1696

We add some config like so to System Configuration

```
scutil
show State:/Network/Service/9bee8941b5xxxxxx/IPv4
<dictionary> {
  Addresses : <array> {
    0 : 10.2.1.36
  }
  InterfaceName : feth4823
  Router : 10.2.1.36
  ServerAddress : 127.0.0.1
}

```

* Add search domain to macos dns configuration

Stumbled upon this while debugging something else.
If we add search domain to our system configuration for
network DNS, then search domains work:

```
ping server1                                                                                                                                                                                    ~
PING server1.my.domain (10.123.3.1): 56 data bytes
64 bytes from 10.123.3.1
```

* Fix reporting of secondaryPort and tertiaryPort See: #2039

* Fix typos (#2075)

* Disable executable stacks on assembly objects (#2071)

Add `--noexecstack` to the assembler flags so the resulting binary
will link with a non-executable stack.

Fixes zerotier/ZeroTierOne#1179

Co-authored-by: Joseph Henry <joseph.henry@zerotier.com>

* Test that starting zerotier before internet works

* Don't skip hellos when there are no paths available

working on #2082

* Update validate-1m-linux.sh

* Save zt node log files on abort

* Separate test and summary step in validator script

* Don't apply default route until zerotier is "online"

I was running into issues with restarting the zerotier service while
"full tunnel" mode is enabled.
When zerotier first boots, it gets network state from the cache
on disk. So it immediately applies all the routes it knew about
before it shutdown.
The network config may have change in this time.
If it has, then your default route is via a route
you are blocked from talking on. So you  can't get the current
network config, so your internet does not work.

Other options include
- don't use cached network state on boot
- find a better criteria than "online"

* Fix node time-to-online counter in validator script

* Export variables so that they are accessible by exit function

* Fix PortMapper issue on ZeroTier startup

See issue #2082

We use a call to libnatpmp::ininatpp to make sure the computer
has working network sockets before we go into the main
nat-pmp/upnp logic.

With basic exponenetial delay up to 30 seconds.

* testing

* Comment out PortMapper debug

this got left turned on in a confusing merge previously

* fix macos default route again

see commit fb6af1971 * Fix network DNS on macOS
adding that stuff to System Config causes this extra route to be added
which breaks ipv4 default route.
We figured out a weird System Coniguration setting
that works.

--- old
couldn't figure out how to fix it in SystemConfiguration
so here we are# Please enter the commit message for your changes. Lines starting

We also moved the dns setter to before the syncIps stuff
to help with a race condition. It didn't always work when
you re-joined a network with default route enabled.

* Catch all conditions in switch statement, remove trailing whitespaces

* Add setmtu command, fix bond lifetime issue

* Basic cleanups

* Check if null is passed to VirtualNetworkConfig.equals and name fixes

* ANDROID-96: Simplify and use return code from node_init directly

* Windows arm64 (#2099)

* ARM64 changes for 1.12

* 1.12 Windows advanced installer updates and updates for ARM64

* 1.12.0

* Linux build fixes for old distros.

* release notes

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: travis laduke <travisladuke@gmail.com>
Co-authored-by: Grant Limberg <grant.limberg@zerotier.com>
Co-authored-by: Grant Limberg <glimberg@users.noreply.github.com>
Co-authored-by: Leonardo Amaral <leleobhz@users.noreply.github.com>
Co-authored-by: Brenton Bostick <bostick@gmail.com>
Co-authored-by: Sean OMeara <someara@users.noreply.github.com>
Co-authored-by: Joseph Henry <joseph-henry@users.noreply.github.com>
Co-authored-by: Roman Peshkichev <roman.peshkichev@gmail.com>
Co-authored-by: Joseph Henry <joseph.henry@zerotier.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Stavros Kois <47820033+stavros-k@users.noreply.github.com>
Co-authored-by: Jake Vis <jakevis@outlook.com>
Co-authored-by: Jörg Thalheim <joerg@thalheim.io>
Co-authored-by: lison <imlison@foxmail.com>
Co-authored-by: Kenny MacDermid <kenny@macdermid.ca>
2023-08-23 14:24:21 -04:00

811 lines
30 KiB
C++

/* Common code and definitions for the transaction classes.
*
* pqxx::transaction_base defines the interface for any abstract class that
* represents a database transaction.
*
* DO NOT INCLUDE THIS FILE DIRECTLY; include pqxx/transaction_base instead.
*
* Copyright (c) 2000-2022, Jeroen T. Vermeulen.
*
* See COPYING for copyright license. If you did not receive a file called
* COPYING with this source code, please notify the distributor of this
* mistake, or contact the author.
*/
#ifndef PQXX_H_TRANSACTION_BASE
#define PQXX_H_TRANSACTION_BASE
#if !defined(PQXX_HEADER_PRE)
# error "Include libpqxx headers as <pqxx/header>, not <pqxx/header.hxx>."
#endif
#include <string_view>
#include <utility>
/* End-user programs need not include this file, unless they define their own
* transaction classes. This is not something the typical program should want
* to do.
*
* However, reading this file is worthwhile because it defines the public
* interface for the available transaction classes such as transaction and
* nontransaction.
*/
#include "pqxx/connection.hxx"
#include "pqxx/internal/concat.hxx"
#include "pqxx/internal/encoding_group.hxx"
#include "pqxx/isolation.hxx"
#include "pqxx/result.hxx"
#include "pqxx/row.hxx"
#include "pqxx/stream_from.hxx"
#include "pqxx/util.hxx"
namespace pqxx::internal::gate
{
class transaction_subtransaction;
class transaction_sql_cursor;
class transaction_stream_to;
class transaction_transaction_focus;
} // namespace pqxx::internal::gate
namespace pqxx
{
using namespace std::literals;
class transaction_focus;
/**
* @defgroup transactions Transaction classes
*
* All database access goes through instances of these classes.
* However, not all implementations of this interface need to provide full
* transactional integrity.
*
* Several implementations of this interface are shipped with libpqxx,
* including the plain transaction class, the entirely unprotected
* nontransaction, and the more cautious robusttransaction.
*/
/// Interface definition (and common code) for "transaction" classes.
/**
* @ingroup transactions
*
* Abstract base class for all transaction types.
*/
class PQXX_LIBEXPORT PQXX_NOVTABLE transaction_base
{
public:
transaction_base() = delete;
transaction_base(transaction_base const &) = delete;
transaction_base(transaction_base &&) = delete;
transaction_base &operator=(transaction_base const &) = delete;
transaction_base &operator=(transaction_base &&) = delete;
virtual ~transaction_base() = 0;
/// Commit the transaction.
/** Make the effects of this transaction definite. If you destroy a
* transaction without invoking its @ref commit() first, that will implicitly
* abort it. (For the @ref nontransaction class though, "commit" and "abort"
* really don't do anything, hence its name.)
*
* There is, however, a minute risk that you might lose your connection to
* the database at just the wrong moment here. In that case, libpqxx may be
* unable to determine whether the database was able to complete the
* transaction, or had to roll it back. In that scenario, @ref commit() will
* throw an in_doubt_error. There is a different transaction class called
* @ref robusttransaction which takes some special precautions to reduce this
* risk.
*/
void commit();
/// Abort the transaction.
/** No special effort is required to call this function; it will be called
* implicitly when the transaction is destructed.
*/
void abort();
/**
* @ingroup escaping-functions
*
* Use these when writing SQL queries that incorporate C++ values as SQL
* constants.
*
* The functions you see here are just convenience shortcuts to the same
* functions on the connection object.
*/
//@{
/// Escape string for use as SQL string literal in this transaction.
template<typename... ARGS> [[nodiscard]] auto esc(ARGS &&...args) const
{
return conn().esc(std::forward<ARGS>(args)...);
}
/// Escape binary data for use as SQL string literal in this transaction.
/** Raw, binary data is treated differently from regular strings. Binary
* strings are never interpreted as text, so they may safely include byte
* values or byte sequences that don't happen to represent valid characters
* in the character encoding being used.
*
* The binary string does not stop at the first zero byte, as is the case
* with textual strings. Instead, it may contain zero bytes anywhere. If
* it happens to contain bytes that look like quote characters, or other
* things that can disrupt their use in SQL queries, they will be replaced
* with special escape sequences.
*/
template<typename... ARGS> [[nodiscard]] auto esc_raw(ARGS &&...args) const
{
return conn().esc_raw(std::forward<ARGS>(args)...);
}
/// Unescape binary data, e.g. from a table field or notification payload.
/** Takes a binary string as escaped by PostgreSQL, and returns a restored
* copy of the original binary data.
*/
[[nodiscard, deprecated("Use unesc_bin() instead.")]] std::string
unesc_raw(zview text) const
{
#include "pqxx/internal/ignore-deprecated-pre.hxx"
return conn().unesc_raw(text);
#include "pqxx/internal/ignore-deprecated-post.hxx"
}
/// Unescape binary data, e.g. from a table field or notification payload.
/** Takes a binary string as escaped by PostgreSQL, and returns a restored
* copy of the original binary data.
*/
[[nodiscard]] std::basic_string<std::byte> unesc_bin(zview text)
{
return conn().unesc_bin(text);
}
/// Unescape binary data, e.g. from a table field or notification payload.
/** Takes a binary string as escaped by PostgreSQL, and returns a restored
* copy of the original binary data.
*/
[[nodiscard, deprecated("Use unesc_bin() instead.")]] std::string
unesc_raw(char const *text) const
{
#include "pqxx/internal/ignore-deprecated-pre.hxx"
return conn().unesc_raw(text);
#include "pqxx/internal/ignore-deprecated-post.hxx"
}
/// Unescape binary data, e.g. from a table field or notification payload.
/** Takes a binary string as escaped by PostgreSQL, and returns a restored
* copy of the original binary data.
*/
[[nodiscard]] std::basic_string<std::byte> unesc_bin(char const text[])
{
return conn().unesc_bin(text);
}
/// Represent object as SQL string, including quoting & escaping.
/** Nulls are recognized and represented as SQL nulls. */
template<typename T> [[nodiscard]] std::string quote(T const &t) const
{
return conn().quote(t);
}
[[deprecated(
"Use std::basic_string<std::byte> instead of binarystring.")]] std::string
quote(binarystring const &t) const
{
return conn().quote(t.bytes_view());
}
/// Binary-escape and quote a binary string for use as an SQL constant.
[[deprecated("Use quote(std::basic_string_view<std::byte>).")]] std::string
quote_raw(unsigned char const bin[], std::size_t len) const
{
return quote(binary_cast(bin, len));
}
/// Binary-escape and quote a binary string for use as an SQL constant.
[[deprecated("Use quote(std::basic_string_view<std::byte>).")]] std::string
quote_raw(zview bin) const;
#if defined(PQXX_HAVE_CONCEPTS)
/// Binary-escape and quote a binary string for use as an SQL constant.
/** For binary data you can also just use @ref quote(data). */
template<binary DATA>
[[nodiscard]] std::string quote_raw(DATA const &data) const
{
return conn().quote_raw(data);
}
#endif
/// Escape an SQL identifier for use in a query.
[[nodiscard]] std::string quote_name(std::string_view identifier) const
{
return conn().quote_name(identifier);
}
/// Escape string for literal LIKE match.
[[nodiscard]] std::string
esc_like(std::string_view bin, char escape_char = '\\') const
{
return conn().esc_like(bin, escape_char);
}
//@}
/**
* @name Command execution
*
* There are many functions for executing (or "performing") a command (or
* "query"). This is the most fundamental thing you can do with the library,
* and you always do it from a transaction class.
*
* Command execution can throw many types of exception, including sql_error,
* broken_connection, and many sql_error subtypes such as
* feature_not_supported or insufficient_privilege. But any exception thrown
* by the C++ standard library may also occur here. All exceptions you will
* see libpqxx throw are derived from std::exception.
*
* One unusual feature in libpqxx is that you can give your query a name or
* description. This does not mean anything to the database, but sometimes
* it can help libpqxx produce more helpful error messages, making problems
* in your code easier to debug.
*
* Many of the execution functions used to accept a `desc` argument, a
* human-readable description of the statement for use in error messages.
* This could make failures easier to debug. Future versions will use
* C++20's `std::source_location` to identify the failing statement.
*/
//@{
/// Execute a command.
/**
* @param query Query or command to execute.
* @param desc Optional identifier for query, to help pinpoint SQL errors.
* @return A result set describing the query's or command's result.
*/
[[deprecated("The desc parameter is going away.")]] result
exec(std::string_view query, std::string_view desc);
/// Execute a command.
/**
* @param query Query or command to execute.
* @return A result set describing the query's or command's result.
*/
result exec(std::string_view query)
{
#include "pqxx/internal/ignore-deprecated-pre.hxx"
return exec(query, std::string_view{});
#include "pqxx/internal/ignore-deprecated-post.hxx"
}
/// Execute a command.
/**
* @param query Query or command to execute.
* @param desc Optional identifier for query, to help pinpoint SQL errors.
* @return A result set describing the query's or command's result.
*/
[[deprecated(
"Pass your query as a std::string_view, not stringstream.")]] result
exec(std::stringstream const &query, std::string_view desc)
{
#include "pqxx/internal/ignore-deprecated-pre.hxx"
return exec(query.str(), desc);
#include "pqxx/internal/ignore-deprecated-post.hxx"
}
/// Execute command, which should return zero rows of data.
/** Works like @ref exec, but fails if the result contains data. It still
* returns a result, however, which may contain useful metadata.
*
* @throw unexpected_rows If the query returned the wrong number of rows.
*/
[[deprecated("The desc parameter is going away.")]] result
exec0(zview query, std::string_view desc)
{
#include "pqxx/internal/ignore-deprecated-pre.hxx"
return exec_n(0, query, desc);
#include "pqxx/internal/ignore-deprecated-post.hxx"
}
/// Execute command, which should return zero rows of data.
/** Works like @ref exec, but fails if the result contains data. It still
* returns a result, however, which may contain useful metadata.
*
* @throw unexpected_rows If the query returned the wrong number of rows.
*/
result exec0(zview query) { return exec_n(0, query); }
/// Execute command returning a single row of data.
/** Works like @ref exec, but requires the result to contain exactly one row.
* The row can be addressed directly, without the need to find the first row
* in a result set.
*
* @throw unexpected_rows If the query returned the wrong number of rows.
*/
[[deprecated("The desc parameter is going away.")]] row
exec1(zview query, std::string_view desc)
{
#include "pqxx/internal/ignore-deprecated-pre.hxx"
return exec_n(1, query, desc).front();
#include "pqxx/internal/ignore-deprecated-post.hxx"
}
/// Execute command returning a single row of data.
/** Works like @ref exec, but requires the result to contain exactly one row.
* The row can be addressed directly, without the need to find the first row
* in a result set.
*
* @throw unexpected_rows If the query returned the wrong number of rows.
*/
row exec1(zview query) { return exec_n(1, query).front(); }
/// Execute command, expect given number of rows.
/** Works like @ref exec, but checks that the result has exactly the expected
* number of rows.
*
* @throw unexpected_rows If the query returned the wrong number of rows.
*/
[[deprecated("The desc parameter is going away.")]] result
exec_n(result::size_type rows, zview query, std::string_view desc);
/// Execute command, expect given number of rows.
/** Works like @ref exec, but checks that the result has exactly the expected
* number of rows.
*
* @throw unexpected_rows If the query returned the wrong number of rows.
*/
result exec_n(result::size_type rows, zview query)
{
#include "pqxx/internal/ignore-deprecated-pre.hxx"
return exec_n(rows, query, std::string_view{});
#include "pqxx/internal/ignore-deprecated-post.hxx"
}
/// Perform query, expecting exactly 1 row with 1 field, and convert it.
/** This is convenience shorthand for querying exactly one value from the
* database. It returns that value, converted to the type you specify.
*/
template<typename TYPE>
[[deprecated("The desc parameter is going away.")]] TYPE
query_value(zview query, std::string_view desc)
{
#include "pqxx/internal/ignore-deprecated-pre.hxx"
row const r{exec1(query, desc)};
#include "pqxx/internal/ignore-deprecated-post.hxx"
if (std::size(r) != 1)
throw usage_error{internal::concat(
"Queried single value from result with ", std::size(r), " columns.")};
return r[0].as<TYPE>();
}
/// Perform query, expecting exactly 1 row with 1 field, and convert it.
/** This is convenience shorthand for querying exactly one value from the
* database. It returns that value, converted to the type you specify.
*/
template<typename TYPE> TYPE query_value(zview query)
{
row const r{exec1(query)};
if (std::size(r) != 1)
throw usage_error{internal::concat(
"Queried single value from result with ", std::size(r), " columns.")};
return r[0].as<TYPE>();
}
/// Execute a query, and loop over the results row by row.
/** Converts the rows to `std::tuple`, of the column types you specify.
*
* Use this with a range-based "for" loop. It executes the query, and
* directly maps the resulting rows onto a `std::tuple` of the types you
* specify. It starts before all the data from the server is in, so if your
* network connection to the server breaks while you're iterating, you'll get
* an exception partway through.
*
* The stream lives entirely within the lifetime of the transaction. Make
* sure you destroy the stream before you destroy the transaction. Either
* iterate the stream all the way to the end, or destroy first the stream
* and then the transaction without touching either in any other way. Until
* the stream has finished, the transaction is in a special state where it
* cannot execute queries.
*
* As a special case, tuple may contain `std::string_view` fields, but the
* strings to which they point will only remain valid until you extract the
* next row. After that, the memory holding the string may be overwritten or
* deallocated.
*
* If any of the columns can be null, and the C++ type to which it translates
* does not have a null value, wrap the type in `std::optional` (or if
* you prefer, `std::shared_ptr` or `std::unique_ptr)`. These templates
* do recognise null values, and libpqxx will know how to convert to them.
*
* The connection is in a special state until the iteration finishes. So if
* it does not finish due to a `break` or a `return` or an exception, then
* the entire connection becomes effectively unusable.
*
* Querying in this way is faster than the `exec()` methods for larger
* results (but slower for small ones). You can start processing rows before
* the full result is in. Also, `stream()` scales better in terms of memory
* usage. Where @ref exec() reads the entire result into memory at once,
* `stream()` will read and process one row at at a time.
*
* Your query executes as part of a COPY command, not as a stand-alone query,
* so there are limitations to what you can do in the query. It can be
* either a SELECT or VALUES query; or an INSERT, UPDATE, or DELETE with a
* RETURNING clause. See the documentation for PostgreSQL's COPY command for
* the details:
*
* https://www.postgresql.org/docs/current/sql-copy.html
*
* Iterating in this way does require each of the field types you pass to be
* default-constructible, copy-constructible, and assignable. These
* requirements may be loosened once libpqxx moves on to C++20.
*/
template<typename... TYPE>
[[nodiscard]] auto stream(std::string_view query) &
{
// Tricky: std::make_unique() supports constructors but not RVO functions.
return pqxx::internal::owning_stream_input_iteration<TYPE...>{
std::unique_ptr<stream_from>{
new stream_from{stream_from::query(*this, query)}}};
}
// C++20: Concept like std::invocable, but without specifying param types.
/// Perform a streaming query, and for each result row, call `func`.
/** Here, `func` can be a function, a `std::function`, a lambda, or an
* object that supports the function call operator. Of course `func` must
* have an unambiguous signature; it can't be overloaded or generic.
*
* The `for_each` function executes `query` in a stream using
* @ref pqxx::stream_from. Every time a row of data comes in from the
* server, it converts the row's fields to the types of `func`'s respective
* parameters, and calls `func` with those values.
*
* This will not work for all queries, but straightforward `SELECT` and
* `UPDATE ... RETURNING` queries should work. Consult the documentation for
* @ref pqxx::stream_from and PostgreSQL's underlying `COPY` command for the
* full details.
*
* Streaming a query like this is likely to be slower than the @ref exec()
* functions for small result sets, but faster for large result sets. So if
* performance matters, you'll want to use `for_each` if you query large
* amounts of data, but not if you do lots of queries with small outputs.
*/
template<typename CALLABLE>
inline auto for_each(std::string_view query, CALLABLE &&func)
{
using param_types =
pqxx::internal::strip_types_t<pqxx::internal::args_t<CALLABLE>>;
param_types const *const sample{nullptr};
auto data_stream{stream_like(query, sample)};
for (auto const &fields : data_stream) std::apply(func, fields);
}
/**
* @name Parameterized statements
*
* You'll often need parameters in the queries you execute: "select the
* car with this licence plate." If the parameter is a string, you need to
* quote it and escape any special characters inside it, or it may become a
* target for an SQL injection attack. If it's an integer (for example),
* you need to convert it to a string, but in the database's format, without
* locale-specific niceties like "," separators between the thousands.
*
* Parameterised statements are an easier and safer way to do this. They're
* like prepared statements, but for a single use. You don't need to name
* them, and you don't need to prepare them first.
*
* Your query will include placeholders like `$1` and `$2` etc. in the places
* where you want the arguments to go. Then, you pass the argument values
* and the actual query is constructed for you.
*
* Pass the exact right number of parameters, and in the right order. The
* parameters in the query don't have to be neatly ordered from `$1` to
* `$2` to `$3` - but you must pass the argument for `$1` first, the one
* for `$2` second, etc.
*
* @warning Beware of "nul" bytes. Any string you pass as a parameter will
* end at the first char with value zero. If you pass a string that contains
* a zero byte, the last byte in the value will be the one just before the
* zero.
*/
//@{
/// Execute an SQL statement with parameters.
template<typename... Args> result exec_params(zview query, Args &&...args)
{
params pp(args...);
return internal_exec_params(query, pp.make_c_params());
}
// Execute parameterised statement, expect a single-row result.
/** @throw unexpected_rows if the result does not consist of exactly one row.
*/
template<typename... Args> row exec_params1(zview query, Args &&...args)
{
return exec_params_n(1, query, std::forward<Args>(args)...).front();
}
// Execute parameterised statement, expect a result with zero rows.
/** @throw unexpected_rows if the result contains rows.
*/
template<typename... Args> result exec_params0(zview query, Args &&...args)
{
return exec_params_n(0, query, std::forward<Args>(args)...);
}
// Execute parameterised statement, expect exactly a given number of rows.
/** @throw unexpected_rows if the result contains the wrong number of rows.
*/
template<typename... Args>
result exec_params_n(std::size_t rows, zview query, Args &&...args)
{
auto const r{exec_params(query, std::forward<Args>(args)...)};
check_rowcount_params(rows, std::size(r));
return r;
}
//@}
/**
* @name Prepared statements
*
* These are very similar to parameterised statements. The difference is
* that you prepare them in advance, giving them identifying names. You can
* then call them by these names, passing in the argument values appropriate
* for that call.
*
* You prepare a statement on the connection, using
* @ref pqxx::connection::prepare(). But you then call the statement in a
* transaction, using the functions you see here.
*
* Never try to prepare, execute, or unprepare a prepared statement manually
* using direct SQL queries when you also use the libpqxx equivalents. For
* any given statement, either prepare, manage, and execute it through the
* dedicated libpqxx functions; or do it all directly in SQL. Don't mix the
* two, or the code may get confused.
*
* See \ref prepared for a full discussion.
*
* @warning Beware of "nul" bytes. Any string you pass as a parameter will
* end at the first char with value zero. If you pass a string that contains
* a zero byte, the last byte in the value will be the one just before the
* zero. If you need a zero byte, you're dealing with binary strings, not
* regular strings. Represent binary strings on the SQL side as `BYTEA`
* (or as large objects). On the C++ side, use types like
* `std::basic_string<std::byte>` or `std::basic_string_view<std::byte>`
* or (in C++20) `std::vector<std::byte>`. Also, consider large objects on
* the SQL side and @ref blob on the C++ side.
*/
//@{
/// Execute a prepared statement, with optional arguments.
template<typename... Args>
result exec_prepared(zview statement, Args &&...args)
{
params pp(args...);
return internal_exec_prepared(statement, pp.make_c_params());
}
/// Execute a prepared statement, and expect a single-row result.
/** @throw pqxx::unexpected_rows if the result was not exactly 1 row.
*/
template<typename... Args>
row exec_prepared1(zview statement, Args &&...args)
{
return exec_prepared_n(1, statement, std::forward<Args>(args)...).front();
}
/// Execute a prepared statement, and expect a result with zero rows.
/** @throw pqxx::unexpected_rows if the result contained rows.
*/
template<typename... Args>
result exec_prepared0(zview statement, Args &&...args)
{
return exec_prepared_n(0, statement, std::forward<Args>(args)...);
}
/// Execute a prepared statement, expect a result with given number of rows.
/** @throw pqxx::unexpected_rows if the result did not contain exactly the
* given number of rows.
*/
template<typename... Args>
result
exec_prepared_n(result::size_type rows, zview statement, Args &&...args)
{
auto const r{exec_prepared(statement, std::forward<Args>(args)...)};
check_rowcount_prepared(statement, rows, std::size(r));
return r;
}
//@}
/**
* @name Error/warning output
*/
//@{
/// Have connection process a warning message.
void process_notice(char const msg[]) const { m_conn.process_notice(msg); }
/// Have connection process a warning message.
void process_notice(zview msg) const { m_conn.process_notice(msg); }
//@}
/// The connection in which this transaction lives.
[[nodiscard]] constexpr connection &conn() const noexcept { return m_conn; }
/// Set session variable using SQL "SET" command.
/** @deprecated To set a transaction-local variable, execute an SQL `SET`
* command. To set a session variable, use the connection's
* @ref set_session_var function.
*
* @warning When setting a string value, you must make sure that the string
* is "safe." If you call @ref quote() on the string, it will return a
* safely escaped and quoted version for use as an SQL literal.
*
* @warning This function executes SQL. Do not try to set or get variables
* while a pipeline or table stream is active.
*
* @param var The variable to set.
* @param value The new value to store in the variable. This can be any SQL
* expression.
*/
[[deprecated(
"Set transaction-local variables using SQL SET statements.")]] void
set_variable(std::string_view var, std::string_view value);
/// Read session variable using SQL "SHOW" command.
/** @warning This executes SQL. Do not try to set or get variables while a
* pipeline or table stream is active.
*/
[[deprecated("Read variables using SQL SHOW statements.")]] std::string
get_variable(std::string_view);
// C++20: constexpr.
/// Transaction name, if you passed one to the constructor; or empty string.
[[nodiscard]] std::string_view name() const &noexcept { return m_name; }
protected:
/// Create a transaction (to be called by implementation classes only).
/** The name, if nonempty, must begin with a letter and may contain letters
* and digits only.
*/
transaction_base(
connection &c, std::string_view tname,
std::shared_ptr<std::string> rollback_cmd) :
m_conn{c}, m_name{tname}, m_rollback_cmd{rollback_cmd}
{}
/// Create a transaction (to be called by implementation classes only).
/** Its rollback command will be "ROLLBACK".
*
* The name, if nonempty, must begin with a letter and may contain letters
* and digits only.
*/
transaction_base(connection &c, std::string_view tname);
/// Create a transaction (to be called by implementation classes only).
explicit transaction_base(connection &c);
/// Register this transaction with the connection.
void register_transaction();
/// End transaction. To be called by implementing class' destructor.
void close() noexcept;
/// To be implemented by derived implementation class: commit transaction.
virtual void do_commit() = 0;
/// Transaction type-specific way of aborting a transaction.
/** @warning This will become "final", since this function can be called
* from the implementing class destructor.
*/
virtual void do_abort();
/// Set the rollback command.
void set_rollback_cmd(std::shared_ptr<std::string> cmd)
{
m_rollback_cmd = cmd;
}
/// Execute query on connection directly.
result direct_exec(std::string_view, std::string_view desc = ""sv);
result
direct_exec(std::shared_ptr<std::string>, std::string_view desc = ""sv);
private:
enum class status
{
active,
aborted,
committed,
in_doubt
};
PQXX_PRIVATE void check_pending_error();
result
internal_exec_prepared(zview statement, internal::c_params const &args);
result internal_exec_params(zview query, internal::c_params const &args);
/// Throw unexpected_rows if prepared statement returned wrong no. of rows.
void check_rowcount_prepared(
zview statement, result::size_type expected_rows,
result::size_type actual_rows);
/// Throw unexpected_rows if wrong row count from parameterised statement.
void
check_rowcount_params(std::size_t expected_rows, std::size_t actual_rows);
/// Describe this transaction to humans, e.g. "transaction 'foo'".
[[nodiscard]] std::string description() const;
friend class pqxx::internal::gate::transaction_transaction_focus;
PQXX_PRIVATE void register_focus(transaction_focus *);
PQXX_PRIVATE void unregister_focus(transaction_focus *) noexcept;
PQXX_PRIVATE void register_pending_error(zview) noexcept;
PQXX_PRIVATE void register_pending_error(std::string &&) noexcept;
/// Like @ref stream(), but takes a tuple rather than a parameter pack.
template<typename... ARGS>
auto stream_like(std::string_view query, std::tuple<ARGS...> const *)
{
return stream<ARGS...>(query);
}
connection &m_conn;
/// Current "focus": a pipeline, a nested transaction, a stream...
/** This pointer is used for only one purpose: sanity checks against mistakes
* such as opening one while another is still active.
*/
transaction_focus const *m_focus = nullptr;
status m_status = status::active;
bool m_registered = false;
std::string m_name;
std::string m_pending_error;
/// SQL command for aborting this type of transaction.
std::shared_ptr<std::string> m_rollback_cmd;
static constexpr std::string_view s_type_name{"transaction"sv};
};
// C++20: Can borrowed_range help?
/// Forbidden specialisation: underlying buffer immediately goes out of scope.
template<>
std::string_view transaction_base::query_value<std::string_view>(
zview query, std::string_view desc) = delete;
/// Forbidden specialisation: underlying buffer immediately goes out of scope.
template<>
zview transaction_base::query_value<zview>(
zview query, std::string_view desc) = delete;
} // namespace pqxx
namespace pqxx::internal
{
/// The SQL command for starting a given type of transaction.
template<pqxx::isolation_level isolation, pqxx::write_policy rw>
extern const zview begin_cmd;
// These are not static members, so "constexpr" does not imply "inline".
template<>
inline constexpr zview begin_cmd<read_committed, write_policy::read_write>{
"BEGIN"_zv};
template<>
inline constexpr zview begin_cmd<read_committed, write_policy::read_only>{
"BEGIN READ ONLY"_zv};
template<>
inline constexpr zview begin_cmd<repeatable_read, write_policy::read_write>{
"BEGIN ISOLATION LEVEL REPEATABLE READ"_zv};
template<>
inline constexpr zview begin_cmd<repeatable_read, write_policy::read_only>{
"BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY"_zv};
template<>
inline constexpr zview begin_cmd<serializable, write_policy::read_write>{
"BEGIN ISOLATION LEVEL SERIALIZABLE"_zv};
template<>
inline constexpr zview begin_cmd<serializable, write_policy::read_only>{
"BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY"_zv};
} // namespace pqxx::internal
#endif