Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions claude-skill.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ The skill is structured so Claude only loads the reference it needs:
| Module | Covers |
| --- | --- |
| `startup` | Installation, starting the server, connecting (HTTP, authentication, embedded), loading a graph |
| `querying` | `MATCH`, `OPTIONAL MATCH`, paths, `WITH`, aggregation, `UNWIND`, subqueries, built-in functions, `LOAD CSV` |
| `writing` | `CREATE`, `MERGE`, `SET`, `DELETE`, the change and commit workflow |
| `querying` | `MATCH`, `OPTIONAL MATCH`, label expressions, paths, `WITH`, `NEXT`, `WHEN`, aggregation, `UNWIND`, maps, subqueries, built-in functions, parameters, `LOAD CSV` |
| `writing` | `CREATE`, `MERGE`, `SET`, `DELETE`, `FOREACH`, bulk edges by node ID, the change and commit workflow |
| `importing` | CSV, Parquet, JSONL, GML and Neo4j imports |
| `algorithms` | Shortest path, vector search, embeddings, GNN sampling |
| `introspection` | Schema exploration, versioning and time travel, Python SDK reference |
Expand Down
21 changes: 14 additions & 7 deletions commands.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "CLI Commands"
sidebarTitle: "Commands"
description: "Reference for TuringDB CLI commands: start, UI, and stop."
description: "Reference for TuringDB CLI commands (start, UI and stop), the interactive shell, environment variables and the HTTP API."
---

## Start
Expand All @@ -26,7 +26,9 @@ turingdb start [options]
| `-ui` | Launch the built-in visualizer proxy | off |
| `-ui-port <port>` | Visualizer proxy port | `8080` |
| `-auth-on` | Require a bearer token on every request. The token is read from the `TURINGDB_AUTH_TOKEN` environment variable, and must be set when you use this flag | off |
| `-start-timeout <ms>` | Time to wait for daemon readiness | `500` |
| `-no-vector-mmap` | Read HNSW [vector indexes](/vector-search) into memory at startup instead of memory-mapping their files | off |

With `-demon`, `turingdb start` returns once the server is ready. If the server cannot start, it exits with an error that says why, for example `Could not start the server on 127.0.0.1:6666: bind: Address already in use`, or `TuringDB is already running` when another instance uses the same `-turing-dir`.

### Examples

Expand Down Expand Up @@ -79,6 +81,8 @@ Without `-demon`, `turingdb start` serves HTTP **and** opens an interactive Cyph
| `cd <graph>` | Switch the active graph |
| `checkout change-<id>` | Work on a change. `checkout <commit>` opens a past commit, and `checkout` returns to the latest commit of main |
| `read <file>` | Run the queries in a file |
| `:param <name> => <value>` | Set a [query parameter](/query/cypher_subset#query-parameters), with the value written as JSON: `:param label => "Person"`. `:param clear` removes them all |
| `SHOW PARAMETERS` | List the parameters set in the shell |
| `help` | List the available commands |
| `quit` / `exit` | Leave the shell |

Expand Down Expand Up @@ -160,14 +164,17 @@ turingdb stop -turing-dir /data/myturing

## HTTP API

The Python SDK is a thin client over a single endpoint, so you can query a server from any language. With the default (JSON) server:
The Python SDK is a thin client over a single endpoint, so you can query a server from any language. With the default (JSON) server, send the query as a JSON object:

```bash
curl -s -X POST 'http://localhost:6666/query?graph=mygraph' -d "MATCH (n) RETURN count(n)"
curl -s -X POST 'http://localhost:6666/query?graph=mygraph' \
-d '{"query": "MATCH (n:$label) RETURN count(n)", "parameters": {"label": "Person"}}'
```

- The request body is the query text. The optional query-string parameters `change=<hex change id>` and `commit=<hash>` target a change or a past commit.
- The request body is a JSON object with a `query` string and an optional `parameters` object (see [query parameters](/query/cypher_subset#query-parameters)).
- The optional query-string parameters `change=<change id>` and `commit=<hash>` target a change or a past commit. The change ID is the decimal number that `CHANGE NEW` returns.
- With `-auth-on`, send `Authorization: Bearer <token>`. A missing or incorrect token returns HTTP `401` with an empty body.
- A successful response is `{"header": {"column_names": [...], "column_types": [...]}, "data": [chunk, ...], "time": <ms>}`, where each chunk is a list of columns.
- An error response is `{"error": "PARSE_ERROR", "error_details": "...", "time": <ms>}`. The code is one of `PARSE_ERROR`, `ANALYZE_ERROR`, `PLAN_ERROR`, `EXEC_ERROR`, `CHANGE_NOT_FOUND` or `COMMIT_NOT_FOUND`.
- A request, headers included, can be up to 1 MiB. A larger one returns HTTP `413` with the code `REQUEST_TOO_BIG`.
- A successful response is `{"header": {"column_names": [...], "column_types": [...]}, "data": [chunk, ...], "time": <ms>}`, where each chunk is a list of columns. Doubles are written with full precision.
- An error response is `{"error": "PARSE_ERROR", "error_details": "...", "time": <ms>}`. The code is one of `PARSE_ERROR`, `ANALYZE_ERROR`, `PLAN_ERROR`, `EXEC_ERROR`, `GRAPH_NOT_FOUND`, `CHANGE_NOT_FOUND`, `COMMIT_NOT_FOUND` or `REQUEST_TOO_BIG`, and the details name the graph, change or commit that was not found.
- Queries on a non-HEAD commit need `LOAD COMMIT '<hash>'` first when you talk to the API directly. The CLI and the SDK do this for you.
4 changes: 2 additions & 2 deletions graph_dev/create_nodes_edges.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ description: "Once you have a valid change, you can start adding new nodes and e

## Merge, delete and bulk loading

- Use `MERGE` instead of `CREATE` to avoid duplicates: `MERGE (n:Person {name: 'Jane'})`. Run `COMMIT` first when `MERGE` should find nodes created earlier in the same change.
- Use `MERGE` instead of `CREATE` to avoid duplicates: `MERGE (n:Person {name: 'Jane'})`. `MERGE` also finds nodes created earlier in the same change.
- Remove data with `DELETE` (edges, or nodes without edges) and `DETACH DELETE` (a node and all its edges).
- Every node needs at least one label, and every edge exactly one edge type.
- For more than a few thousand nodes or edges, build the graph with [`LOAD PARQUET`](/import_data/parquet) instead of Cypher writes.
- For more than a few thousand nodes or edges, build the graph with [`LOAD PARQUET`](/import_data/parquet) instead of Cypher writes. To add many edges between existing nodes, [`UNWIND` pairs of node IDs](/query/writing#edges-between-existing-nodes).

See [Writing data](/query/writing) for the full reference.
10 changes: 5 additions & 5 deletions import_data/csv.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ Read a row's fields as `row.col` or `row[<literal index>]`. Without `WITH HEADER

## Combining with the rest of a query

`LOAD CSV` composes with `MATCH`, `WITH` / `WHERE`, aggregates, `ORDER BY` and `UNWIND`. For example, join the rows with nodes that are already in the graph:
`LOAD CSV` composes with `MATCH`, `OPTIONAL MATCH`, `WITH` / `WHERE`, aggregates, `ORDER BY`, `UNWIND` and subqueries, which can all read `row.x`. For example, join the rows with nodes that are already in the graph:

```jsx
LOAD CSV "people.csv" WITH HEADERS AS row
MATCH (p:Person {name: row.name})
RETURN p.name, row.city
```

<Note>
To use `row.x` inside an `OPTIONAL MATCH` pattern that directly follows `LOAD CSV`, project the field first: `... AS row WITH row.name AS nm OPTIONAL MATCH (p:Person {name: nm}) ...`.
</Note>
LOAD CSV "people.csv" WITH HEADERS AS row
OPTIONAL MATCH (p:Person {name: row.name})
RETURN row.name, p.age
```

## Building a graph from CSV

Expand Down
2 changes: 1 addition & 1 deletion import_data/jsonl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ LOAD JSONL 'mydata.jsonl' AS mygraph WITH EMBEDDINGS [{"embedding", 384}, {"summ
Each spec is `{"<property name>", <dimension>}`. The dimension must be **greater than 1**, and every array under that property must have exactly that many numeric elements. This works for both node and relationship properties. Array properties not listed in `WITH EMBEDDINGS` are stored as Lists.

<Note>
When an import fails, additional details are written to `<turing-dir>/logs/turingdb.log`.
When an import fails, the error names the line of the file that could not be read, for example `JSONL error 'JSONL parse' at line 2: ...`. More details are written to `<turing-dir>/logs/turingdb.log`.
</Note>

<Note>
Expand Down
24 changes: 13 additions & 11 deletions pythonsdk/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ or using `pip`:
pip install turingdb
```

The SDK targets TuringDB v3: pin `turingdb>=3.0` in your project (older projects may pin `1.x`, which uses the previous engine and a much smaller Cypher subset). The wheel supports Python 3.10 to 3.14 on Linux (x86_64, aarch64) and macOS 15 or later (arm64). It also contains the `turingdb` command-line tool, the `turing-parquet` import tool, and the in-process engine.
The SDK targets TuringDB v3: pin `turingdb>=3.0.2` in your project (older projects may pin `1.x`, which uses the previous engine and a much smaller Cypher subset). Upgrade the SDK and the server together: from 3.0.2 the JSON client sends each query as a JSON body, which is what a 3.0.2 server expects. The wheel supports Python 3.10 to 3.14 on Linux (x86_64, aarch64) and macOS 15 or later (arm64). It also contains the `turingdb` command-line tool, the `turing-parquet` import tool, and the in-process engine.

## Getting Started

Expand Down Expand Up @@ -87,6 +87,7 @@ A server in binary mode serves the binary protocol only, so `curl`, the JSON cli

- `host` accepts `"localhost"` together with `port=`, or a full URL such as `"http://localhost:6666"`. Use one of these two forms.
- The query API is identical, and results come back as the same DataFrames. Embeddings are returned as numpy `float32` arrays instead of Python lists.
- A [`shortestPath`](/shortest-path) `path` cannot be read over the native backend yet. Run those queries through the JSON backend.
- To list the graphs on disk, use the JSON client. On the native backend, `list_loaded_graphs()` lists the loaded graphs.
- `token`, `reconnect()` and the S3 helpers work as on the JSON backend.

Expand Down Expand Up @@ -159,17 +160,18 @@ client.load_graph("mygraph")

If `raise_if_loaded=False`, it will silently continue if the graph is already loaded.

### `query(query: str) → pandas.DataFrame`
### `query(query: str, query_parameters: dict | None = None) → pandas.DataFrame`

Runs a Cypher query on the current graph.

```python
df = client.query("MATCH (n:Person) RETURN n.name, n.age")
df = client.query("MATCH (n:$label) RETURN n.name", {"label": "Person"})
```

Returns results as a `pandas.DataFrame`, with automatic typing and parsing.
Returns results as a `pandas.DataFrame`, with automatic typing and parsing. `query_parameters` fills `$name` [parameters](/query/cypher_subset#query-parameters), which stand for labels and edge types.

### `query_raw(query: str) → dict`
### `query_raw(query: str, query_parameters: dict | None = None) → dict`

Like `query`, but returns the raw server response as a `dict` instead of a DataFrame. Useful for inspecting result metadata or column dtypes directly.

Expand Down Expand Up @@ -223,7 +225,7 @@ client.set_commit("haax42")

### `set_change(change: int | str)`

Manually set the change ID to use (accepts int or hex string).
Manually set the change ID to use (accepts the int that `new_change()` returns, or the same number as a string).

```python
client.set_change(3)
Expand Down Expand Up @@ -292,15 +294,15 @@ All queries return a `pandas.DataFrame`, typed according to the schema:
| `List`, `Map` | object (Python list, dict) |
| `Embedding` | object (list over HTTP, numpy `float32` array on the native and embedded backends) |

Nodes and edges (`RETURN n`) come back as their internal integer IDs (`UInt64`), so project the properties you need. A named path comes back as a list of `{'type': 'node' | 'edge', 'id': ...}` dictionaries. See [Functions and data types](/query/functions#how-results-come-back) for the full table.
Nodes and edges (`RETURN n`) come back as their internal integer IDs (`UInt64`), so project the properties you need, or return `properties(n)` as a dictionary. A named path comes back as a list of `{'type': 'node' | 'edge', 'id': ...}` dictionaries. Doubles keep their full precision on every backend. See [Functions and data types](/query/functions#how-results-come-back) for the full table.

<Note>
Over the JSON backend, Doubles are serialised with 6 decimal places. Use the native or embedded backend if you need full precision.
</Note>
<Tip>
Integer columns hold numpy integers, which an f-string prints as `np.int64(5)`. Convert IDs with `.tolist()` or `int()` before writing them into a query.
</Tip>

## Error Handling

Query errors raise `TuringDBException`. The message starts with the error code (`PARSE_ERROR`, `ANALYZE_ERROR`, `PLAN_ERROR`, `EXEC_ERROR`, `CHANGE_NOT_FOUND`, `COMMIT_NOT_FOUND`, ...) and points at the offending part of the query.
Query errors raise `TuringDBException`. The message starts with the error code (`PARSE_ERROR`, `ANALYZE_ERROR`, `PLAN_ERROR`, `EXEC_ERROR`, `GRAPH_NOT_FOUND`, `CHANGE_NOT_FOUND`, `COMMIT_NOT_FOUND`, `REQUEST_TOO_BIG`, ...) and points at the offending part of the query, or names the graph, change or commit that was not found.

```python
from turingdb import TuringDB, TuringDBException
Expand All @@ -311,7 +313,7 @@ except TuringDBException as e:
print("Query failed:", e)
```

On the JSON backend, transport failures (the server is down, or HTTP 401 for a bad or missing token) surface as `httpx` exceptions instead.
On the JSON backend, transport failures (the server is down, or HTTP 401 for a bad or missing token) surface as `httpx` exceptions instead. HTTP errors that the server explains, such as `REQUEST_TOO_BIG`, still raise `TuringDBException`.

## Example Workflow

Expand Down
60 changes: 54 additions & 6 deletions query/aggregation.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Results, Aggregation and Lists"
sidebarTitle: "Results and Aggregation"
description: "Shape results with WITH, DISTINCT, ORDER BY, SKIP, LIMIT and UNION, then aggregate, unwind and transform lists."
description: "Shape results with WITH, DISTINCT, ORDER BY, SKIP, LIMIT and UNION, chain queries with NEXT and WHEN, then aggregate, unwind and transform lists."
---

These examples use the [sample graph](/query/matching) from the matching page.
Expand Down Expand Up @@ -97,6 +97,49 @@ RETURN c.name AS name

`UNION` removes duplicates and `UNION ALL` keeps them. All branches must return the same column names, and each column must keep one value type across the branches.

## NEXT

`NEXT` chains queries in one request: the columns returned by the query before it become the incoming rows of the query after it.

```cypher
MATCH (n:Person)
RETURN n.name AS nm, n.age AS age
NEXT
RETURN nm
ORDER BY nm LIMIT 2

MATCH (n:Person)
RETURN n.city AS city, count(*) AS people
NEXT
RETURN max(people) AS biggestCity
```

- Every expression in a `RETURN` followed by `NEXT` needs an alias (`AS`).
- A later query sees what an earlier one wrote: `CREATE (x:Tag {v: 1}) RETURN x NEXT MATCH (t:Tag) RETURN count(t)`.

## Conditional queries with WHEN

`WHEN <condition> THEN <query> [WHEN ... THEN ...] [ELSE <query>]` runs the first branch whose condition holds. Branches return the same columns, and they can also write.

```cypher
WHEN true THEN RETURN 1 AS x ELSE RETURN 2 AS x

MATCH (n:Person)
RETURN n.name AS nm, n.age AS age
NEXT
WHEN age > 30 THEN RETURN nm, 'senior' AS band
ELSE RETURN nm, 'junior' AS band

MATCH (n:Person)
CALL (n) {
WHEN n.age > 30 THEN RETURN 'senior' AS band
ELSE RETURN 'junior' AS band
}
RETURN n.name, band
```

A `WHEN` can start a query, follow `NEXT`, or form the body of a [`CALL` subquery](/query/subqueries#call-subqueries), where it runs once per row. Several `WHEN` queries combine with `UNION`: `{ WHEN c1 THEN RETURN 1 AS x } UNION { WHEN c2 THEN RETURN 2 AS x }`.

## Aggregation

Grouping is implicit, as in Neo4j: the non-aggregated items in the `RETURN` are the grouping keys.
Expand Down Expand Up @@ -130,6 +173,9 @@ ORDER BY c DESC
MATCH (n)
RETURN labels(n) AS l, count(*) AS c
ORDER BY c DESC

MATCH (n:Person)
RETURN stDev(n.age) AS sd, percentileCont(n.age, 0.5) AS median, percentileDisc(n.age, 0.9) AS p90
```

| Aggregate | Notes |
Expand All @@ -138,9 +184,9 @@ ORDER BY c DESC
| `collect(x)`, `collect(DISTINCT x)` | Nulls are dropped |
| `sum(x)` | |
| `avg(x)` | Always a Double |
| `min(x)`, `max(x)` | Numbers, strings, booleans, `DateTime` and `Duration` |

Statistical aggregates such as `stdev` and `percentileCont` are not yet available. Compute them in your application, for example in pandas.
| `min(x)`, `max(x)` | Numbers, strings, booleans, `DateTime`, `Duration`, lists and values of mixed types |
| `stDev(x)`, `stDevP(x)` | Sample and population standard deviation |
| `percentileCont(x, p)`, `percentileDisc(x, p)` | Interpolated and nearest-value percentile; `p` is a fraction, such as `0.5` for the median |

**Rules:**

Expand Down Expand Up @@ -187,7 +233,7 @@ RETURN t, count(*)
`range(start, end[, step])` includes both bounds. Unwound nodes stay nodes, so `p.name` works after `UNWIND ns AS p`. You can also `UNWIND` a list of maps and read their keys: `UNWIND [{a: 1}, {a: 2}] AS m RETURN m.a`.

<Note>
Write the list into the query text, for example `f"UNWIND {ids} AS x ..."` in Python. Query parameters such as `UNWIND $list` are not yet supported.
Write the list into the query text, for example `f"UNWIND {ids} AS x ..."` in Python. Value parameters such as `UNWIND $list` are not yet supported.
</Note>

## Lists
Expand All @@ -200,7 +246,7 @@ RETURN size([1, 2]), head([1, 2]), last([1, 2]), tail([1, 2]), reverse([1, 2, 3]
MATCH (n:Person {name: 'Alice'})
RETURN n.tags[0], size(n.tags)

RETURN [1, 2] + [3], 'a' || 'b'
RETURN [1, 2] + [3], [1, 2] + 3, 'a' || 'b'
```

- Indexing and slicing work on literals, list properties and function results (`labels(n)[0]`). Negative indexes count from the end, and an out-of-range index returns `null`.
Expand Down Expand Up @@ -237,3 +283,5 @@ RETURN n.name,
```

The simple form (`CASE n.city WHEN 'London' THEN ...`) and the searched form (`CASE WHEN condition THEN ...`) both work. TuringDB also accepts comparison tests in the simple form, such as `CASE n.age WHEN > 30 THEN 'old' WHEN IS NULL THEN '?' END`.

Branches can return lists, maps, `DateTime` and `Duration` values, and they may mix types: `CASE WHEN n.age > 30 THEN n.tags ELSE [] END` and `coalesce(n.nickname, n.age)` both work.
Loading
Loading