# GQL Beta
OverGraph ships **GQL Beta**, a GQL/Cypher-style query surface for graph reads and writes. Use it
when a query or mutation reads better as text than as a request object.
The authoritative public reference is the [GQL Beta section in the API docs](api-reference.md#gql-beta).
This page is the compact syntax companion.
Connector calls:
- Rust: `DatabaseEngine::execute_gql(...)` and `DatabaseEngine::explain_gql(...)`
- Node.js: `executeGql(...)`, `executeGqlAsync(...)`, `explainGql(...)`, `explainGqlAsync(...)`
- Python: `execute_gql(...)`, async `execute_gql(...)`, `explain_gql(...)`, async `explain_gql(...)`
Supported at a glance:
- `MATCH`, `OPTIONAL MATCH`, `WHERE`, `RETURN`, `ORDER BY`, `SKIP` / `OFFSET`, and `LIMIT`
- `WITH`, `WITH *`, `WITH DISTINCT`, later `MATCH` stages, and `RETURN DISTINCT`
- aggregation with `count`, `sum`, `avg`, `min`, `max`, and `collect`
- `UNION`, `UNION ALL`, `EXISTS { ... }`, and read-only `CALL { ... }`
- bounded paths, path values, path helper functions, and constrained shortest paths
- `CREATE`, keyed node `MERGE`, unique relationship `MERGE`, `ON CREATE SET`, `ON MATCH SET`,
`SET`, `REMOVE`, `DELETE r`, `DETACH DELETE n`, and mutation returns
- params, read cursors, compact rows, explain/profile, ReadOnly mode, mutation stats, and vector
opt-in for returned node values
## Read Syntax
Read clause shape:
```gql
MATCH <pattern> [, <pattern>...] [WHERE <predicate>]
OPTIONAL MATCH <pattern> [, <pattern>...] [WHERE <predicate>]
WITH [DISTINCT] <items> [WHERE <predicate>] [ORDER BY ...] [SKIP ...] [LIMIT ...]
CALL { <read clauses ending in RETURN> }
RETURN [DISTINCT] <items>
ORDER BY <order-expression> [ASC|DESC], ...
SKIP <integer-or-param>
OFFSET <integer-or-param>
LIMIT <integer-or-param>
```
`WITH` controls what names are visible to later clauses. `WITH *` keeps visible aliases,
`WITH DISTINCT` deduplicates rows, and row operations on `WITH` apply before the next clause.
```gql
MATCH (p:Person)
WITH p, lower(trim(p.email)) AS email
WHERE email ENDS WITH '@example.com'
MATCH (p)-[:WORKS_AT]->(c:Company)
RETURN DISTINCT p.name AS person, email, c.name AS company
ORDER BY person
LIMIT 20
```
Read branches can be combined with `UNION` or `UNION ALL`:
```gql
MATCH (p:Person) WHERE p.status = 'active'
RETURN p.name AS name
UNION ALL
MATCH (p:Person) WHERE p.status = 'invited'
RETURN p.name AS name
```
Pattern shapes:
- Node pattern: `MATCH (n:Person)`
- Fixed edge pattern: `MATCH (a)-[r:KNOWS]->(b)`
- Multiple fixed hops: `MATCH (a)-[r]->(b)-[s]->(c)`
- Undirected fixed edge: `MATCH (a)-[r:KNOWS]-(b)`
- Optional group: `MATCH (a) OPTIONAL MATCH (a)-[r:REPORTS_TO]->(m)`
- Bounded path: `MATCH p = (a)-[:KNOWS*1..3]->(b)`
- Zero-to-N bounded path: `MATCH p = (a)-[:KNOWS*..2]->(b)`
- Exact-length path: `MATCH p = (a)-[:KNOWS*2]->(b)`
- One-hop path with relationship alias: `MATCH p = (a)-[r:KNOWS*1..1]->(b)`
- Shortest path: `MATCH p = shortestPath((a)-[:KNOWS*1..5]->(b))`
- All equal shortest paths: `MATCH p = allShortestPaths((a)-[:KNOWS*1..5]-(b))`
Shortest path uses pre-bound endpoint aliases:
```gql
MATCH (a:Person {key: $from})
WITH a
MATCH (b:Person {key: $to})
WITH a, b
MATCH p = shortestPath((a)-[:KNOWS*1..4]->(b))
RETURN p, node_ids(p) AS node_ids, edge_ids(p) AS edge_ids, length(p) AS hops
```
Expressions include variables, `id(n)`, `id(r)`, `labels(n)`, `type(r)`, path functions, path
fields, property access, literals, params, boolean predicates, comparisons, null checks, `IN`,
arithmetic, string predicates, `CASE`, and `RETURN *`.
Scalar functions include `coalesce`, `to_string`, `to_integer`, `to_float`, `abs`, `floor`, `ceil`,
`round`, `lower`, `upper`, `trim`, `substring`, `size`, `head`, and `last`.
Aggregation example:
```gql
MATCH (n:Person)
WITH n.group AS group,
count(*) AS total,
avg(n.rank) AS avg_rank,
collect(DISTINCT n.status) AS statuses
WHERE total > 1
RETURN group, total, coalesce(avg_rank, 0.0) AS avg_rank, statuses
ORDER BY total DESC
```
Read-only subqueries:
```gql
MATCH (p:Person)
WHERE EXISTS { MATCH (p)-[:WORKS_AT]->(c:Company) RETURN c }
WITH p
CALL { MATCH (p)-[:WORKS_AT]->(c:Company) RETURN c.name AS company }
RETURN p.name AS person, company
```
## Mutation Syntax
Mutation clause shape:
```gql
MATCH <pattern> [WHERE <predicate>]
OPTIONAL MATCH <pattern> [WHERE <predicate>]
WITH [DISTINCT] <items> [WHERE <predicate>] [ORDER BY ...] [SKIP ...] [LIMIT ...]
CALL { <read clauses ending in RETURN> }
CREATE <pattern> [, <pattern>...]
MERGE (n:Label {key: expr}) [ON CREATE SET ...] [ON MATCH SET ...]
MERGE (a)-[r:TYPE]->(b) [ON CREATE SET ...] [ON MATCH SET ...]
SET <assignment>
REMOVE <target>
DELETE <edge-alias>
DETACH DELETE <node-alias>
RETURN [DISTINCT] <items>
ORDER BY <order-expression> [ASC|DESC], ...
SKIP <integer-or-param>
OFFSET <integer-or-param>
LIMIT <integer-or-param>
```
Read prefixes go before the first write clause. Create-only statements do not need a read prefix.
Mutation forms:
- `CREATE (n:Person {key: 'ada', name: 'Ada'})`
- `CREATE (a:Person {key: 'a'})-[r:KNOWS {since: 2026}]->(b:Person {key: 'b'})`
- `MERGE (n:Person {key: $key}) ON CREATE SET n.created = true ON MATCH SET n.seen = true`
- `MATCH (a:Person {key: $a}) MATCH (b:Person {key: $b}) MERGE (a)-[r:KNOWS]->(b)`
- `MATCH (n:Person) WHERE n.key = 'ada' SET n.status = 'active'`
- `MATCH (n:Person) WHERE n.key = 'ada' SET n += $props`
- `MATCH (n:Person) WHERE n.key = 'ada' SET n:Engineer`
- `MATCH (n:Person) WHERE n.key = 'ada' REMOVE n.status`
- `MATCH (n:Person) WHERE n.key = 'ada' REMOVE n:Engineer`
- `MATCH (a)-[r:KNOWS]->(b) DELETE r`
- `MATCH (n:Person) WHERE n.key = 'ada' DETACH DELETE n`
Mutation return example:
```gql
MATCH (n:Person) WHERE n.key = 'ada'
SET n.status = 'active'
RETURN DISTINCT n.key AS key, n.status AS status
ORDER BY key
LIMIT 1
```
MERGE example:
```gql
MATCH (s:Source)
WITH s.target_key AS key
MERGE (a:Account {key: key})
ON CREATE SET a.status = 'created', a.count = 1
ON MATCH SET a.status = 'matched', a.count = coalesce(a.count, 0) + 1
RETURN DISTINCT a.key AS key, a.status AS status, a.count AS count
```
## Options
Rust uses `GqlExecutionOptions`. Node and Python expose connector-native option names:
| `mode` | `mode` | `mode` | `Auto` / `"auto"` |
| `allow_full_scan` | `allowFullScan` | `allow_full_scan` | `false` |
| `max_rows` | `maxRows` | `max_rows` | `10000` |
| `cursor` | `cursor` | `cursor` | `None` / `null` |
| `max_cursor_bytes` | `maxCursorBytes` | `max_cursor_bytes` | `16384` |
| `max_mutation_rows` | `maxMutationRows` | `max_mutation_rows` | `10000` |
| `max_mutation_ops` | `maxMutationOps` | `max_mutation_ops` | `50000` |
| `max_pipeline_rows` | `maxPipelineRows` | `max_pipeline_rows` | `65536` |
| `max_groups` | `maxGroups` | `max_groups` | `65536` |
| `max_collect_items` | `maxCollectItems` | `max_collect_items` | `65536` |
| `max_union_branches` | `maxUnionBranches` | `max_union_branches` | `16` |
| `max_subquery_invocations` | `maxSubqueryInvocations` | `max_subquery_invocations` | `4096` |
| `max_subquery_depth` | `maxSubqueryDepth` | `max_subquery_depth` | `2` |
| `max_shortest_path_pairs` | `maxShortestPathPairs` | `max_shortest_path_pairs` | `4096` |
| `max_query_bytes` | `maxQueryBytes` | `max_query_bytes` | `1048576` |
| `max_param_bytes` | `maxParamBytes` | `max_param_bytes` | `1048576` |
| `max_ast_depth` | `maxAstDepth` | `max_ast_depth` | `256` |
| `max_literal_items` | `maxLiteralItems` | `max_literal_items` | `10000` |
| `max_intermediate_bindings` | `maxIntermediateBindings` | `max_intermediate_bindings` | `65536` |
| `max_frontier` | `maxFrontier` | `max_frontier` | `65536` |
| `max_path_hops` | `maxPathHops` | `max_path_hops` | `16` |
| `max_paths_per_start` | `maxPathsPerStart` | `max_paths_per_start` | `4096` |
| `max_order_materialization` | `maxOrderMaterialization` | `max_order_materialization` | `65536` |
| `max_skip` | `maxSkip` | `max_skip` | `100000` |
| `include_plan` | `includePlan` | `include_plan` | `false` |
| `profile` | `profile` | `profile` | `false` |
| `compact_rows` | `compactRows` | `compact_rows` | `false` |
| `include_vectors` | `includeVectors` | `include_vectors` | `false` |
`compactRows` / `compact_rows` switches connector row serialization from objects to arrays.
`includeVectors` / `include_vectors` includes dense and sparse vectors in returned node values.
## Results
Rust returns positional rows. Node.js and Python return object rows by default and positional arrays
when compact rows are enabled.
Node.js result shape:
```js
{
kind: 'query',
columns: ['name', 'rank'],
rows: [{ name: 'Ada', rank: 2 }],
nextCursor: null,
stats: { rowsReturned: 1, rowsMatched: 3, rowsAfterFilter: 1, ... },
mutationStats: null,
plan: null
}
```
Python result shape:
```python
{
"kind": "mutation",
"columns": ["name"],
"rows": [{"name": "Ada"}],
"next_cursor": None,
"stats": {"rows_returned": 1, "rows_matched": 1, "rows_after_filter": 1, ...},
"mutation_stats": {"nodes_created": 1, "mutation_ops": 1, ...},
"plan": None,
}
```
## Path Values
Returning a path alias yields a path value:
| Node IDs | `nodeIds` | `node_ids` | `node_ids` |
| Edge IDs | `edgeIds` | `edge_ids` | `edge_ids` |
| Hydrated nodes | `nodes` | `nodes` | `nodes` |
| Hydrated edges | `edges` | `edges` | `edges` |
`length(p)` returns hop count. `node_ids(p)` and `edge_ids(p)` return ID lists. Returning `p`
directly returns the path value shape above.
## Cursors
Read results with another page include `next_cursor` / `nextCursor`. Pass it back as `cursor` with
the same logical read statement and referenced params:
```js
const first = db.executeGql(
'MATCH (n:Person) RETURN n.name AS name ORDER BY n.name LIMIT 10'
);
const second = db.executeGql(
'MATCH (n:Person) RETURN n.name AS name ORDER BY n.name LIMIT 10',
null,
{ cursor: first.nextCursor }
);
```
## Params
Parameter values can be nulls, booleans, signed and unsigned integers where the connector can
represent them, finite floats, strings, bytes, lists, and maps with string keys.
Only referenced params are resource-validated; extra unused params are ignored.
## Explain And Profile
`explain_gql` / `explainGql` returns a unified explain payload with:
- `kind`
- `columns`
- `read` for read-plan details or mutation read-prefix details
- `mutation` for mutation operation and return-plan details
- `caps`
- `warnings`
- `notes`
When `includePlan` / `include_plan` is true on `execute_gql`, the result includes the same explain
payload in `plan`. When `profile` is true, `stats.elapsedUs` / `stats.elapsed_us` is populated.
## Examples
Node.js:
```js
db.executeGql(
`CREATE (p:Person {key: $personKey, name: $personName, status: 'active'})
-[r:WORKS_AT {role: $role, since: $since}]->
(c:Company {key: $companyKey, name: $companyName})
RETURN p.name AS person, c.name AS company, r.role AS role`,
{
personKey: 'ada',
personName: 'Ada',
companyKey: 'overgraph',
companyName: 'OverGraph',
role: 'engineer',
since: 2026,
}
);
const rows = db.executeGql(
`MATCH (p:Person)-[r:WORKS_AT]->(c:Company)
WITH c.name AS company, count(*) AS people, collect(DISTINCT p.name) AS names
RETURN company, people, names
ORDER BY people DESC`,
null,
{ includePlan: true }
);
```
Python:
```python
created = db.execute_gql(
"""
MERGE (p:Person {key: $key})
ON CREATE SET p.name = $name, p.status = 'active'
ON MATCH SET p.seen = true
RETURN p.key AS key, p.name AS name
""",
{"key": "ada", "name": "Ada"},
)
rows = db.execute_gql(
"""
MATCH (p:Person)-[r:WORKS_AT]->(c:Company)
WHERE EXISTS { MATCH (p)-[:WORKS_AT]->(c) RETURN c }
RETURN p.name AS person, c.name AS company
ORDER BY p.name
LIMIT 10
""",
include_plan=True,
)
```
Rust:
```rust
let result = engine.execute_gql(
"MATCH (a:Person {key: 'ada'}) \
WITH a \
MATCH (b:Person {key: 'ben'}) \
WITH a, b \
MATCH p = shortestPath((a)-[:KNOWS*1..4]->(b)) \
RETURN p, node_ids(p) AS ids, length(p) AS hops",
&GqlParams::new(),
&GqlExecutionOptions::default(),
)?;
```