Skip to main content
nanograph

Query Language Reference

NanoGraph queries are defined in .gq files. The query language uses typed Datalog semantics with GraphQL-shaped syntax — named, parameterized queries validated against the schema at compile time.

Query structure

query friends_of($name: String) {
    match {
        $p: Person { name: $name }
        $p knows $f
        $f.age > 25
    }
    return { $f.name, $f.age }
    order { $f.name asc }
    limit 10
}

Every query has:

  • Name — identifier for --name when running
  • Parameters — typed inputs passed via --param key=value
  • Metadata — optional @description("...") and @instruction("...") annotations for human/agent context
  • match — conjunctive clauses that define what to find (required)
  • return — projections and aggregations (required for read queries)
  • order — sort order (optional)
  • limit — max rows (optional, required with nearest() or rrf() ordering)

Query metadata

Queries can carry optional human/agent-facing metadata:

query semantic_search($q: String)
    @description("Find the closest matching documents by semantic similarity.")
    @instruction("Use for conceptual search. Prefer keyword_search for exact terms.")
{
    match {
        $d: Doc
    }
    return { $d.slug, $d.title, nearest($d.embedding, $q) as score }
    order { nearest($d.embedding, $q) }
    limit 5
}

nanograph run prints this metadata before results in the default table output. It is advisory only and does not change planning or execution semantics.

Aliases from Project Config compose cleanly with query metadata. For example:

nanograph run search "father and son conflict"

In table mode this prints the query name plus any @description(...) / @instruction(...) text before the result rows. Machine-oriented formats like json, jsonl, and csv keep their payloads unchanged.

Parameters

Parameters are typed and prefixed with $:

query find($name: String, $min_age: I32, $since: DateTime) { ... }

Supported parameter types: all scalar types (String, I32, I64, U64, F32, F64, Bool, Date, DateTime) plus Vector(dim). For exact 64-bit integer values from JS/TS SDKs, pass decimal strings when above Number.MAX_SAFE_INTEGER.

Pass values at runtime:

nanograph run --db my.nano --query q.gq --name find \
  --param name="Alice" --param min_age=25

Match clause

The match block contains clauses that are implicitly conjoined (AND). All clauses must hold simultaneously — this is standard Datalog.

Bindings

Bind a variable to a node type:

$p: Person
$p: Person { name: "Alice" }
$p: Person { name: $n, age: 30 }

Property matches in braces filter at bind time. Variables ($n) capture values for use elsewhere.

Edge traversal

Traverse edges using Datalog predicate syntax:

$p knows $f

Direction is inferred from the schema's edge endpoint types — no arrows needed.

Edge naming

Schema defines edges in PascalCase, but queries use camelCase (lowercase first letter). The query parser requires edge identifiers to start with a lowercase letter.

Schema definitionQuery syntax
edge Knows: Person -> Person$p knows $f
edge WorksAt: Person -> Company$p worksAt $c
edge HasMentor: Character -> Character$s hasMentor $m
edge AffiliatedWith: Character -> Faction$c affiliatedWith $f
edge DebutsIn: Character -> Film$c debutsIn $f
edge ClientOwnsRecord: Client -> Record$c clientOwnsRecord $r

Using PascalCase in queries (e.g. $s HasMentor $m) produces a parse error.

Bounded expansion

Multi-hop traversal without recursion:

$a knows{1,3} $b

Compiles to a finite union of 1-hop, 2-hop, and 3-hop traversals. Bounds must satisfy: min >= 1, max >= min, max is finite.

Filters

Boolean expressions over bound variables:

$f.age > 25
$p.name != "Bob"
$p.createdAt >= datetime("2026-01-01T00:00:00Z")
$o.amount >= 10000.0

Comparison operators: =, !=, >, <, >=, <=.

Negation

not {
    $p worksAt $_
}

At least one variable in the negated block must be bound outside it. $_ is an anonymous wildcard. Semantics: no matching tuples exist for the inner clauses.

maybe { ... } (left join) and or { ... } (disjunction) are not part of the current query grammar.

Text search predicates

Filter predicates for text matching (see search.md for full details):

PredicateDescription
search($c.bio, $q)Token-based keyword match (all query tokens must be present)
fuzzy($c.bio, $q)Approximate match (tolerates typos)
match_text($c.bio, $q)Contiguous token match (phrase matching)

These go in the match block and act as filters — a row either matches or doesn't.

Return clause

Projections define what columns appear in the output:

return {
    $f.name
    $f.age
    $c.name as company
    count($f) as num_friends
}

Use as to alias columns.

Aggregation functions

FunctionDescriptionType requirement
count($var)Count of bound variableAny bound variable
sum($var.prop)SumNumeric type
avg($var.prop)AverageNumeric type
min($var.prop)MinimumNumeric or Date/DateTime
max($var.prop)MaximumNumeric or Date/DateTime

Scoring functions in return

bm25(), nearest(), and rrf() can be projected to inspect scores:

return {
    $c.slug,
    bm25($c.bio, $q) as lexical_score,
    nearest($c.embedding, $q) as semantic_distance,
    rrf(nearest($c.embedding, $q), bm25($c.bio, $q)) as hybrid_score
}

Order clause

order { $f.age desc }
order { $p.name asc, $p.age desc }

Default direction is ascending. Ordering expressions:

ExpressionDirectionMeaning
$p.name ascAscendingAlphabetical / numeric ascending
$p.age descDescendingNumeric descending
nearest($c.embedding, $q)Ascending (lower = closer)Cosine distance ordering
bm25($c.bio, $q) descDescending (higher = better)BM25 relevance ordering
rrf(..., ...) descDescending (higher = better)Hybrid fusion ordering

nearest() ordering requires a limit clause. See search.md for score interpretation.

Limit clause

limit 10

Required when using nearest() or rrf() in the order clause.

Literals

LiteralExample
String"Alice"
Integer42
Float3.14
Booleantrue, false
Datedate("2026-01-15")
DateTimedatetime("2026-01-15T10:00:00Z")
List[1, 2, 3], ["a", "b"]

Mutations

Mutation queries modify graph data. They use the same query wrapper but contain insert, update, or delete instead of match/return.

Insert

Append a new node:

query add_person($name: String, $age: I32) {
    insert Person { name: $name, age: $age }
}

Insert an edge (endpoints resolved by @key):

query add_edge($from: String, $to: String) {
    insert Knows { from: $from, to: $to }
}

Update

Update by @key (merge semantics — requires the node type to have a @key property):

query advance_stage() {
    update Opportunity set {
        stage: "won"
        closedAt: datetime("2026-02-14T00:00:00Z")
    } where slug = "opp-stripe-migration"
}

Delete

Delete nodes matching a predicate. Edges where the deleted node is an endpoint are automatically cascaded:

query remove_cancelled() {
    delete ActionItem where slug = "ai-draft-proposal"
}

Comments

// Line comment

/* Block comment */

Reserved words

The parser treats many terms as contextual keywords. Avoid using them as identifiers in queries.

CategoryKeywords
Query structurequery, match, return, order, limit
Clausesnot, as
Mutationsinsert, update, delete, set, where
Search / rankingsearch, fuzzy, match_text, bm25, rrf
Orderingasc, desc
Aggregationcount, sum, avg, min, max
Date literalsdate, datetime

true and false are literal keywords.

Type rules

The compiler enforces these rules during nanograph check:

RuleWhat it checks
T1Binding type must exist in schema
T2Property match fields must exist on the bound type
T3Property match values must match declared types
T4Traversal edge must exist in schema
T5Traversal endpoints must match edge declaration
T6Property access must reference a property on the bound variable's type
T7Comparison operands must have compatible types
T8Aggregations must wrap valid expressions (e.g. sum requires numeric)
T9not requires at least one externally-bound variable
T10Mutation shape errors (unknown mutation target, missing assignments)
T11Mutation property references must exist on the target type
T12Insert mutations must provide required fields/endpoints (@embed targets may be omitted when source is provided)
T13Duplicate mutation assignments are rejected
T14Mutation variable values must be declared query parameters
T15Traversal bounds validation and nearest() type/dimension validation
T16update for edge types is not supported
T17nearest() ordering requires a limit clause
T18Alias-based ordering is restricted when standalone nearest() ordering is present
T19search() / fuzzy() args must be String
T20match_text() / bm25() args must be String
T21rrf() args must be nearest() or bm25(); optional k must be integer > 0; requires limit