v2.2: a transaction’s basis is fixed at its first query
A transaction’s snapshot and clock time are fixed when its first query runs, as are any of its BEGIN or SET TRANSACTION options that bear on them.
Previously, BEGIN awaited the connection’s writes, captured the snapshot and fixed the clock itself.
A transaction’s characteristics can now arrive after BEGIN, through SET TRANSACTION, so the basis waits until they have all landed - as a Postgres repeatable-read transaction takes its snapshot at its first statement.
A transaction now sees a write that another connection makes between its BEGIN and its first query, and CURRENT_TIMESTAMP is the clock at that first query.
To keep a basis fixed at BEGIN, issue the transaction’s first query straight after it, or pin the basis explicitly with BEGIN READ ONLY WITH (SNAPSHOT_TOKEN = ..., CLOCK_TIME = ...).
SELECT is optional in XTDB - if not provided, it defaults to SELECT *
SELECT may be placed after GROUP BY in XTDB, so that the query clauses are written in the order that they’re executed in practice.
GROUP BY is optional in XTDB - if not provided, it defaults to all of the columns used outside of an aggregate function.
If you start your query with FROM, you may then include arbitrarily many sets of WHERE/GROUP BY/SELECT clauses, which will be evaluated in order.
Predicates can be comma-separated in XTDB, to aid with SQL generation - these are treated as conjuncts.
There may be an arbitrary number of commas at the start, between any two predicate expressions, or at the end.
XTQL queries are sent within an SQL string literal - e.g.
'(-> (from ...) ...)'.
Given the possible presence of single quotes within the query, it is recommended to use dollar-delimited strings here: XTQL $$ <xtql query> $$
Due to implementation details in some drivers (e.g. PGJDBC), it is required to additionally specify the params in standard SQL (?) following your XTQL query, so that the driver knows how many arguments to allow.
<table-function> covers both purpose-built table-valued functions (e.g. GENERATE_SERIES) and any scalar function call wrapped as a single-row table — see table functions.
FOR VALID_TIME ONLY FROM X TO Y selects rows whose valid-time period overlaps [X, Y)and clamps the projected _valid_from / _valid_to to that window.
Rows whose stored period extends outside [X, Y) are returned with their bounds tightened to X / Y; rows fully inside the window are returned unchanged.
The ONLY form is specific to VALID_TIME — there is no FOR SYSTEM_TIME ONLY.
Compare with the plain FROM/TO form, which matches the same rows but returns each row’s stored_valid_from / _valid_to:
Use ONLY when you want each row’s projected validity to reflect the intersection of the row with the query window — e.g. computing how long a row was valid within a reporting period, or feeding a downstream consumer that expects bounds within the window.
Use the plain form when you want to know the row’s full stored validity and only use [X, Y) to filter which rows come back.
ONLY also lets the engine skip pages that would only contribute to the parts of _valid_from / _valid_to that get clamped away, so it is generally faster than projecting a GREATEST / LEAST clamp on top of the plain form.
Nested sub-queries allow you to easily create tree-shaped results, using NEST_MANY and NEST_ONE:
For example, if you have a one-to-many relationship (e.g. customers → orders), you can write a query that, for each customer, returns an array of their orders as nested objects:
Queries in XTDB run against a ‘basis’, which consists of:
a ‘snapshot’ - an upper bound on the transactions that are visible to the query.
a ‘clock time’ - used for any function calls that reference the current time (e.g. CURRENT_TIMESTAMP)
These can be set either on a per-query basis, using SETTING, or for a transaction, using BEGIN or SET TRANSACTION.
A transaction’s basis is fixed when its first query runs, and every query in the transaction shares it.
Setting the default valid-time/system-time applies to any FROM clause that doesn’t have any valid-time/system-time specification explicitly set.
Setting the SNAPSHOT_TOKEN enforces an upper-bound on the transactions visible to the query - i.e. no matter what the per-table system-time clauses specify, they will not see anything newer than this snapshot-token.
If not provided, this defaults to the latest-completed transaction on the queried node.
Setting the CLOCK_TIME defines a fixed value for any functions that depend on the current time - e.g. CURRENT_TIMESTAMP.
It also defines the default valid-time selection for any tables in FROM clauses that don’t otherwise have a valid-time specification.
If not provided, it defaults to the clock-time fixed at the transaction’s first query.
A transaction may be either READ ONLY or READ WRITE.
If not specified, it will be inferred from the first statement in the transaction.
Transactions must not mix query statements and DML statements.
BEGIN’s transaction modes are those of SET TRANSACTION: BEGIN READ ONLY, CLOCK_TIME = ... opens a transaction and applies SET TRANSACTION READ ONLY, CLOCK_TIME = ... to it.
BEGIN READ ONLY WITH (...) takes the same options inside WITH (...), TIMEZONE included.
Committing/rolling back a read-only transaction has no effect in XTDB, because readers never block writers nor each other.
BEGIN, START TRANSACTION, COMMIT and ROLLBACK are SQL statements over the Postgres wire protocol only.
ADBC and Flight SQL clients open and end transactions through their own transaction API, and reject these statements as SQL; they set a transaction’s options with SET TRANSACTION.
SET TRANSACTION sets the open transaction’s access mode and options, as a comma-separated list of transaction modes.
The access mode, READ ONLY or READ WRITE, may appear anywhere in the list.
An option belonging to one access mode requires that access mode in the same statement - SET TRANSACTION CLOCK_TIME = ... on its own is rejected.
TIMEZONE and ISOLATION LEVEL need no access mode.
A later SET TRANSACTION sets only the modes it names, and keeps the rest:
<option> = DEFAULT clears an option set earlier.
Changing the access mode clears the previous access mode’s options.
The time zone is kept.
It is accepted until the transaction’s first query or first write - afterwards, it is rejected.
Outside a transaction, it is rejected under auto-commit, and opens a transaction otherwise.
ISOLATION LEVEL ... is accepted, and has no effect: XTDB transactions are always serializable.
SET TRANSACTION ISOLATION LEVEL ... on its own is accepted anywhere.
TIMEZONE sets the time zone for the duration of the transaction, affecting any time zone-aware date/time literals and functions.
If not provided, it defaults to the time-zone of the connection.
For read-only transactions:
SNAPSHOT_TOKEN and CLOCK_TIME behave the same as in SETTING.
AWAIT_TOKEN may be provided to wait for a specific transaction to be visible on the queried node before the transaction’s first query runs.
If not provided, it defaults to waiting for the latest-submitted transaction on the current connection.