Skip to content

Concepts

Tracing queries

Available since: v4.0.0

Tracing queries

A trace reports the graph that a query walked. When a query returns ->tagged->tag or author.name, the result holds the values but not the edges and links that led to them. Call .trace() on a query to ask SurrealDB for those edges and links as well, so you can draw the real graph of a result instead of guessing which records connect.

A trace needs SurrealDB 4.0.0 or later. It only describes what your session was allowed to read.

Call .trace() on a query and name the kinds of trace you want. Awaiting the query and .collect() return the rows as before, so read the trace from .responses() or .statements(), or from .stream().

import { dedupeTrace } from 'surrealdb';

const [response] = await db
    .query('SELECT title, author.name, ->tagged->tag AS tags FROM post')
    .trace('relations', 'links')
    .responses();

if (response.success) {
    const trace = dedupeTrace(response.trace ?? {});

    for (const { in: from, edge, out } of trace.relations ?? []) {
        console.log(`${from} -[${edge}]-> ${out}`);
    }
}
Output
post:1 -[tagged:a]-> tag:rust
post:1 -[tagged:b]-> tag:db
post:2 -[tagged:c]-> tag:rust

The order of entries in a trace is not defined. With no arguments, .trace() asks for every kind.

KindEntryReported when
relations{ in, edge, out }A graph lookup, such as ->tagged->tag, in a field the query returns
references{ referrer, referenced }A <~ reference lookup in a field the query returns
links{ record, field, target }A returned field reads through a record link, such as author in author.name
variables{ name: value }A LET statement at the top level of the query
subscriptionsUuid[]A LIVE SELECT statement starts a live query

For relations, in and out are the records at the two ends of the edge, and edge is the record of the edge table. For references, the referrer is the record that holds the field, and the referenced record is the one it points at.

A lookup in a WHERE clause is not traced.

A trace is a plain object with one array per kind that has entries. A kind with no entries is left out, so check for undefined before you read it.

interface QueryTrace {
    relations?: { in?: RecordId; edge: RecordId; out?: RecordId }[];
    references?: { referrer: RecordId; referenced: RecordId }[];
    links?: { record: RecordId; field: string; target: RecordId }[];
    variables?: Record<string, unknown>;
    subscriptions?: Uuid[];
    partial?: true;
    truncated?: TraceKind[];
}

Two more properties describe how complete the trace is:

  • partial: Set to true when a lookup fed the result through a function, operator or aggregate. The trace is correct, but it may not show everything that contributed to the value.

  • truncated: Lists the kinds for which the server stopped recording at a limit.

A trace holds only what your session can read. An edge table that your session cannot read adds no entries. If your session cannot read the field that names the record at one end of an edge, that end is undefined.

.responses() and .statements() collect the trace for each statement, so you receive one trace per statement.

const [people] = await db
    .query('SELECT ->knows->person AS friends FROM person')
    .trace('relations')
    .responses();

if (people.success) {
    people.trace?.relations;
}
for await (const statement of db.query('SELECT ->knows->person FROM person').trace('relations').statements()) {
    statement.index;
    statement.trace?.relations;
}

A statement that fails has no trace, and a statement that reports nothing has no trace property.

.stream() passes on the trace as the server sends it. The server attaches a trace to a frame that it already sends, so a trace does not add frames of its own.

for await (const frame of db.query('SELECT ->tagged->tag FROM post').trace('relations').stream()) {
    if (frame.isValue()) {
        frame.value;
        frame.trace;
    } else if (frame.isDone()) {
        frame.trace;
    } else if (frame.isError()) {
        frame.error;
    }
}

A frame holds its trace in these places:

  • A batch of rows: The trace is on the first value frame of the batch. The other value frames of the batch have no trace.

  • A single value: The trace is on the value frame, for a statement such as SELECT ... FROM ONLY that returns one value.

  • The end of a statement: The trace is on the done frame. It holds the parts that belong to the statement as a whole, such as variables, and any entries that no earlier frame carried.

A trace belongs to its statement and not to the rows of the frame it arrived with. It is also provisional, as the rows are: when a statement fails, its error frame cancels every value and trace that you received for that statement.

The server does not remove repeated entries across frames, and .responses() and .statements() join the frames of a statement as they are. The same edge can therefore appear more than once, for example when two returned records share a tag.

Use dedupeTrace() to keep the first copy of each entry. It identifies entries by their record ids, and a link also by its field.

import { dedupeTrace } from 'surrealdb';

const unique = dedupeTrace(response.trace ?? {});

When you stream, keep your own set of the entries that you have seen, or call dedupeTrace() on the trace that you gather.

SituationResult
The connected server is older than SurrealDB 4.0.0The call fails with an UnavailableFeatureError before it is sent
A kind that does not exist, such as trace('edges').trace() throws a TypeError
A query in a dialect other than SurrealQLThe call fails with a TypeError
  • Edges that a RELATE statement writes. The result of that statement already holds them.

  • Links that FETCH resolves.

Was this page helpful?