Graph#
Concourse’s document-graph data model lets you store relationships between records and traverse them efficiently. Relationships are represented as Links — directional pointers from one record to another.
Linking Records#
Use the link method to create a directed relationship from a
source record to a destination record via a named key.
1 2 3 | |
1 2 | |
Links are directional. The example above creates a relationship from record 1 to record 100, but record 100 has no automatic back-reference to record 1.
Link to Multiple Destinations#
1 2 3 | |
Removing Links#
Use unlink to remove a relationship:
1 2 | |
1 2 | |
Link Queries#
You can query for records that link to a specific destination
using the LINKS_TO operator:
1 | |
1 2 3 | |
This returns all records whose employer key contains a link to
record 100.
Navigation#
Navigation lets you traverse links and read data from linked records using dot-separated key paths called navigation keys.
Navigation Keys#
A navigation key is a dot-separated path where each segment before the last is a key containing links, and the final segment is the key to read from the destination record.
1 | |
This means: follow the employer link, then read the name key
from the linked record.
Using navigate()#
The navigate method traverses navigation keys and returns data
from the destination records.
1 2 3 4 | |
1 2 | |
Multi-Hop Navigation#
Navigation keys can span multiple hops:
1 | |
This traverses two links: first employer, then ceo, and
finally reads name from the destination.
1 2 3 | |
Navigate Multiple Keys#
1 2 3 4 5 6 | |
Navigate from Multiple Records#
1 2 3 4 | |
Navigate with Criteria#
Navigate from all records matching a query:
1 2 3 4 | |
Historical Navigation#
1 2 3 4 | |
Per-Stop Brackets#
Each stop of a navigation path can carry its own
[<timestamp>] bracket independently. The bracket pins that
stop’s read at the specified moment, while unbracketed stops
default to the operation-level timestamp (or the present
moment if none is supplied):
1 2 3 4 5 6 | |
Per-stop brackets are useful when the graph shape and the destination values belong to different points in time — for example, “who reported to me last week, and what do they earn today?” See Per-Key Timestamps for the full precedence rule.
Navigation Keys in Queries#
Navigation keys can be used directly in CCL conditions to filter records based on data in linked records:
1 | |
1 2 3 | |
This finds all records whose linked employer record has a
name of "Cinchapi". Navigation keys work with all CCL
operators:
1 2 3 | |
Transitive Navigation#
When a field contains self-referential links — for example,
a children key whose links point to more records that also have
children — the depth of the graph is data-dependent. You
can follow those links recursively by appending a * suffix to
any stop in a navigation path.
1 2 3 | |
The * tells the server to expand that stop with a
breadth-first search: start from the links at the current stop,
follow them to their destinations, and if any destination record
also has outgoing links on the same field, follow those too,
until no new records are discovered. The entire traversal runs
server-side in a single RPC.
Cycles and Termination#
Transitive traversal automatically deduplicates records as the BFS frontier expands, so cyclic graphs (e.g., a record that eventually links back to an ancestor) terminate cleanly without infinite loops. The result includes each reachable record at most once.
Examples#
1 2 3 4 5 6 7 8 9 | |
1 2 3 4 5 | |
1 2 3 | |
Mixed Stops in the Same Key#
A navigation path can combine transitive and non-transitive
stops. Each * expands independently:
1 | |
This follows parent once (single hop), then expands the
children field transitively on the destination record, then
reads name at every descendant.
Multiple transitive stops in the same key are also supported:
1 | |
Each * expands via its own BFS; the traversals compose in
order along the path.
Supported Operations#
Transitive keys work anywhere a navigation key works:
| Operation | Transitive keys |
|---|---|
select |
✓ |
get |
✓ |
navigate |
✓ |
browse |
✓ |
find |
✓ |
Non-Link Fields#
If the * modifier is applied to a stop whose field does not
contain links (for example, a plain string field), the BFS finds
no outgoing edges to follow and the traversal terminates
immediately. The result is the same as the non-transitive
variant of the same key — transitive navigation degrades
gracefully rather than raising an error.
Scoped Navigation#
When a record has a multi-valued link — for example,
assignments pointing at several tickets, or members
pointing at several users — a query that combines two
conditions over the same navigation prefix can silently match
records that should not qualify.
Consider a source record R with two assignments links:
1 2 3 | |
A query that intends to find records with a high-priority open assignment often looks like this:
1 | |
Plain . navigation evaluates each condition independently and
intersects the resulting record sets. The first condition
matches R (via Ticket 1, which has priority = "high") and
the second also matches R (via Ticket 2, which has
status = "open"), so R is returned — even though no
single assignment has both properties.
Scoped navigation makes that “same-assignment” intent explicit
with a prefix.(...) group:
1 | |
Read this as: “find records that have an assignments link
to a record where both priority = "high" and
status = "open" hold.” The group pins every inner condition
to the same destination reached via assignments, eliminating
the cross-link false positive. R no longer matches, because
no single linked ticket satisfies both leaves.
When to Reach for It#
Use scoped navigation whenever you combine two or more
conditions (with AND) that share a navigation prefix and the
prefix can be multi-valued. If the prefix only ever points at
one record, the scoped and non-scoped forms return the same
result, and you can safely continue using plain .
navigation.
Loose (non-scoped) navigation retains its historical behavior, so existing queries are unaffected. Scoped navigation is a tightening you opt in to when you need it.
Inner Keys are Relative to the Pivot#
Inside a scope, keys are resolved from the record reached via the prefix — the pivot. A plain key references a field on the pivot:
1 | |
Here priority and status both live on the assignment
record.
An inner key can itself be a navigation key, which re-enters navigation from the pivot:
1 | |
Here owner.team follows the assignment’s owner link to
another record and reads team there. The whole scope
semantically reads as: “find records with an assignment whose
priority is high and whose owner is on the platform team.”
Multi-Segment and Transitive Prefixes#
The prefix is whatever you write before the .(. Multi-segment
prefixes pivot at a deeper record:
1 | |
The pivot here is the department record reached via
company.departments. The inner conditions must both hold at
the same department.
Transitive prefixes pivot at every record in the transitive closure:
1 | |
Any single record reachable via the transitive
descendants expansion that has both name = "Acme" and
status = "active" causes the source to match.
Nested Scopes#
Scopes can nest, and each nested scope rebases the pivot at its own prefix — resolved from the enclosing pivot, not from the original source:
1 2 3 4 | |
This reads: “find records with a team link to a record named
Platform that itself has at least one member who is both a
lead and currently active.”
Composing With AND and OR#
A scoped group is a first-class condition, so it composes with ordinary leaves and with other scopes:
1 2 | |
1 2 | |
Java Driver (Criteria.scope)#
Every scope expressible in CCL is also available through the
Criteria builder, mirroring the existing Criteria.group
method:
1 2 3 4 5 6 7 8 9 10 11 | |
Scopes from the builder mix freely with group, and, and
or:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Empty or Non-Link Prefixes#
A scoped query follows a link. If the scope prefix has no link values at the traversal time — because the key has never been a link, because it carries only non-link values, or because its links have been removed — no record is reachable as a pivot and the result is empty. Writing a scope over a non-link or unused prefix is safe; it will not raise an error and it will not silently fall back to evaluating the inner conditions against the source record.
Timestamps in Scoped Queries#
A scoped query involves two logically distinct reads: the prefix traversal that determines which records count as pivots, and the leaf reads that evaluate each inner condition at each pivot. The two are pinned independently:
- A bracket on the scope prefix pins prefix traversal.
A[t].(...)traversesAatt. This is the only way to change the prefix-traversal time. - Per-leaf brackets pin their own leaf reads. They have no effect on prefix traversal.
- Both default to the present moment when no bracket is attached. Multi-stop prefixes and nested scopes apply the rule independently at each level.
The contract over the canonical cases is:
- No timestamps.
1 2 | |
- Manual uniform per-leaf timestamps.
1 2 | |
Per-leaf brackets bind only their own reads. Stamping every
inner leaf at the same t does not pin the prefix —
use A[t].(...) (case 6) for that.
- Mixed per-leaf timestamps.
1 2 3 | |
- Partially stamped leaves.
1 2 3 | |
- Bracket-pinned prefix.
1 2 | |
- Bracket-pinned prefix with per-leaf brackets.
1 2 | |
Each bracket binds independently. To pin the entire scope at one moment, repeat the timestamp on the prefix and on every unbracketed leaf:
1 2 | |
Criteria#at(Timestamp) and Scoped Criteria#
Applying Criteria#at(Timestamp) to a built Criteria
clobbers every inner leaf’s timestamp to the supplied value.
It does not bracket the scope prefix. So a scoped criteria
constructed via the builder and bound with at(t) reads each
leaf at t but traverses the prefix at present:
1 2 3 4 5 6 7 8 | |
To pin both the prefix traversal and the leaf reads at the
same moment, embed the timestamp directly in the prefix
string passed to scope(...):
1 2 3 4 5 6 7 8 | |
Tracing References#
The trace method returns all incoming links to a record —
it answers the question “which records link to this one?”
1 2 3 4 | |
1 2 | |
The result maps each key name to the set of records that contain a link to the traced record via that key.
Trace Multiple Records#
1 2 3 | |
Historical Trace#
1 2 3 4 | |
Consolidating Records#
The consolidate method atomically merges data from one or more
source records into a target record. Every field from each
source is added to the target, the sources are cleared, and
every incoming link in the database that referenced a source is
rewritten to point at the target. The operation is atomic
— either every source is fully consolidated and every
incoming link is rewritten, or nothing changes.
1 2 3 | |
1 2 | |
Consolidation is useful when resolving duplicate records or normalizing references across a graph. Because it rewrites inbound links in place, callers that held a link to a source record continue to work — their link now resolves to the consolidated target.
Traversal Optimization#
Concourse automatically selects the most efficient traversal strategy for navigation queries. Depending on the shape of the data, it may use:
-
Forward traversal: Start from the source records and follow links forward to find destination data. This is more efficient when there are fewer source records than destination records.
-
Reverse traversal: Start from potential destination records and trace links backward to find matching sources. This is more efficient when there are fewer destination records than source records.
The optimizer chooses the strategy automatically based on data characteristics. No manual tuning is required.
For transitive keys, the same optimizer considers both strategies but biases toward reverse traversal when the first stop in the path is transitive, since a forward-expansion starting point would need to enumerate an unbounded subgraph before doing any filtering.