Concourse automatically tracks every change to every piece of data,
creating a complete version history. This enables you to query and
read data as it existed at any point in the past.
A temporal query evaluates its selection criteria based on the
version of data at a previous timestamp. The query returns the
records that would have matched the criteria at that point in
time.
12345
// Java// Find records where age > 30 as of last monthSet<Long>records=concourse.find("age > 30",Timestamp.fromString("last month"));
A temporal read retrieves the version of the data that existed
at a previous timestamp. The read returns what the data looked
like at that point in time.
1234
// Java// Get the name that was stored in record 1 last monthObjectname=concourse.get("name",1,Timestamp.fromString("last month"));
You can combine temporal queries and temporal reads in a single
operation:
123456
// Java// Select data from records matching criteria, both// evaluated at the same historical timestampMap<Long,Map<String,Set<Object>>>data=concourse.select("age > 30",Timestamp.fromString("last month"));
The trailing at / as of clause shown above pins every read
in an operation to a single timestamp. For mixed-time questions
— “give me a customer’s current tier alongside their
lifetime spend as of last quarter” — you can attach a
per-key timestamp directly to the key with bracket annotations.
Append [<timestamp>] to any key to pin that key’s read to a
specific moment:
123
select [tier, lifetime_spend["end of last quarter"]] from1--tierreadsatthepresentmoment--lifetime_spendreadsatthecloseoflastquarter
Bracket annotations work wherever a read or query accepts a key:
selection keys, leaf keys in where conditions, navigation stops,
and order by keys. The bracket value can be a numeric microsecond
timestamp or any natural-language expression that
Timestamp.fromString accepts:
1234
select name[1672531200000000] from 1
select name["yesterday"] from 1
select name["January 1, 2024"] from 1
select name["Q3 2024"] from 1
When both a bracket and a trailing at/as of are present, the
bracket always wins for the key it sits on. Unbracketed keys
default to the trailing at clause when one is supplied, or to the
present moment otherwise:
12345
--Bracketswin; the trailing clause fills in for unbracketed keysselect [tier["last week"], lifetime_spend] from1at"end of last quarter"--tierreadsatlastweek(bracketwins)--lifetime_spendreadsatendoflastquarter(default-fill)
This rule applies uniformly to selection keys, navigation stops, and
the leaf keys in where conditions.
Order keys behave differently across commands
order by keys have a known wrinkle today that may be reconciled
in a future release. On find and other condition-only commands,
an unbracketed sort key reads at the present moment regardless of
the trailing at. On select, get, and other commands that
materialize a result set, an unbracketed sort key inherits the
trailing at so the sort stays consistent with the materialized
values. To pin a sort read explicitly and uniformly across both
families, bracket the key directly
(order by name["last week"]) or pin the order clause
(order by name asc at "last week").
Conditions in a where clause evaluate each leaf at its own pinned
moment, so a single query can match records by their state at
multiple points in time:
1234
findtier="PLATINUM"andlifetime_spend["end of last quarter"] >=100000--evaluatestieragainstthepresentmoment--evaluateslifetime_spendatthecloseoflastquarter
Each stop of a navigation path can carry its own bracket
independently. The same precedence applies per stop:
123456789
-- Pin only the link traversal; read each destination's current
-- salary
select manager["last week"].salary from 1
-- Pin both the link traversal and the destination read
select manager["last week"].salary["last week"] from 1
-- Same effect as above, expressed via the legacy trailing clause
select manager.salary from 1 at "last week"
The audit method returns a complete revision history for a
record or a specific field. Each entry maps a commit timestamp to
a list of human-readable descriptions of the changes made.
The chronicle method returns a time series of the values stored
for a key in a record after every change. Unlike audit, which
describes what changed, chronicle shows the state of the
field after each change.
The diff method returns the net changes between two points
in time. Unlike audit, which records every individual change,
diff computes the minimal set of additions and removals needed
to transition from the start state to the end state.
// JavaMap<Object,Map<Diff,Set<Long>>>changes=concourse.diff("status",Timestamp.fromString("last week"));// Shows which values were added/removed and in which records
The revert method atomically restores a key in a record to its
state at a previous timestamp. Concourse computes the necessary
adds and removes to recreate the historical state, then applies
them as new revisions. The original history is preserved.
Reverting does not delete history. Instead, it creates new
revisions that undo the changes made since the target
timestamp. The full audit trail, including the revert
operations themselves, is preserved.