> ## Documentation Index
> Fetch the complete documentation index at: https://docs.helix-db.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Query HelixDB with SQL

> Run SQL statements against the graph, see how tables, columns, and joins map to nodes and relationships, and which SQL HelixDB translates

Guide

HelixDB accepts SQL statements alongside native queries and Cypher. It translates
each statement into its [Cypher profile](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md)
and runs it on the same graph, transactions, and indexes, so data written in one
language is visible to the others. A table is a node label, a column is a
property, and a join is a relationship. The translation reads no schema: it
follows fixed naming rules, which you can adjust with mappings.

The translation is a port of the SQL-to-Cypher translator in the
[Neo4j JDBC driver](https://github.com/neo4j/neo4j-jdbc). It covers `SELECT`,
`INSERT`, `UPDATE`, `DELETE`, and `TRUNCATE`. Everything in
[What is supported](#what-is-supported) runs, and everything it cannot translate
faithfully is rejected with a specific error before the statement reads or writes
anything.

> [!NOTE]
>
> SQL is served by Helix Cloud gateways, and by local servers, embedded databases,
> and SDKs built from the [repository checkout](https://github.com/HelixDB/helix-db).
> The CLI does not expose SQL yet.

## Run a statement

Send one statement, with optional parameters, mappings, and a query name, to
`POST /v2/sql`. The server translates it and works out whether it reads or
writes. Parameters given as a list bind each `?` in order.

```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
let result = client
    .sql(
        "SELECT p.name AS name FROM Person p WHERE p.age >= ? ORDER BY name",
        vec![serde_json::json!(30)],
        &helix_db::SqlMappings::default(),
        Some("adults"),
    )
    .await?;
// result.columns == ["name"]; result.rows == [["Ada"], ...]
```

```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
const result = await client.sql(
  "SELECT p.name AS name FROM Person p WHERE p.age >= ? ORDER BY name",
  [30],
  {},
  "adults",
);
// { columns: ["name"], rows: [["Ada"], ...] }
```

```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
result, err := client.SQL(ctx, helix.SQLRequest{
	Query:     "SELECT p.name AS name FROM Person p WHERE p.age >= ? ORDER BY name",
	Args:      []any{30},
	QueryName: "adults",
})
// result.Columns == ["name"]; result.Rows == [["Ada"], ...]
```

```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
result = client.sql(
    "SELECT p.name AS name FROM Person p WHERE p.age >= ? ORDER BY name",
    [30],
    query_name="adults",
)
# {"columns": ["name"], "rows": [["Ada"], ...]}
```

```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "query": "SELECT p.name AS name FROM Person p WHERE p.age >= ? ORDER BY name",
  "parameters": [30],
  "query_name": "adults"
}
```

HelixDB runs the statement as this Cypher:

```cypher theme={"languages":{"custom":["languages/helixql.json"]}}
MATCH (p:Person)
WHERE p.age >= $1
RETURN p.name AS name
ORDER BY name
```

A successful response has ordered `columns` and rectangular `rows`, with one
value per column in column order, as for
[Cypher](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#run-a-statement):

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{ "columns": ["name"], "rows": [["Ada"], ["Grace"]] }
```

`query` is required; `parameters`, `query_name`, and `mappings` are optional. A
result column keeps its SQL name: an unqualified column is named after the
column, an alias after the alias, and any other entry after its text as written,
such as `p.name` or `COUNT(*)`. Repeated names are numbered: `SELECT 1, 1`
returns the columns `1` and `1_2`. Duplicate rows are kept unless the statement
uses `DISTINCT` or `GROUP BY`, row order is only defined by `ORDER BY`, and a
write without `RETURNING` succeeds with empty `columns` and `rows`.

### Over HTTP

A local server has no authentication:

```sh theme={"languages":{"custom":["languages/helixql.json"]}}
curl http://localhost:6969/v2/sql \
  -H 'Content-Type: application/json' \
  --data '{"query":"SELECT p.name AS name FROM Person p WHERE p.age >= ?","parameters":[30]}'
```

A Helix Cloud gateway takes the same body. Authenticate as for
[Cypher](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#over-http), with the cluster API
key and, on a GA shared gateway, the database ID:

```sh theme={"languages":{"custom":["languages/helixql.json"]}}
curl "$HELIX_ENDPOINT/v2/sql" \
  -H "Authorization: Bearer $HELIX_API_KEY" \
  -H "X-Helix-Database-Id: $HELIX_DATABASE_ID" \
  -H 'Content-Type: application/json' \
  --data '{"query":"SELECT count(*) AS nodes FROM Person p"}'
```

The gateway translates each statement to route it by what it does: reads go to
readers, and writes go to the elected writer. A read-only API key can read and
explain, but receives `403` for a statement that writes. The
[HTTP API reference](https://docs.helix-db.com/database/helix-db/query-guides/http-api.md#sql) describes the
request, status codes, and errors in full. A local server's gRPC API serves the
same bodies through `ExecuteSql` and `ExplainSql`.

### Request options

The native request options apply to SQL too:

| Option | Rust | TypeScript | Go | Python | HTTP header |
| - | - | - | - | - | - |
| Run on the writer | `.writer_only()` | `.writerOnly()` | `helix.WriterOnly()` | `writer_only=True` | `X-Helix-Require-Writer: true` |
| Warm caches only (reads) | `.warm_only()` | `.warmOnly()` | `helix.WarmOnly()` | `warm_only=True` | `X-Helix-Warm: true` |
| Wait for durability (writes) | `.should_await_durability(true)` | `.shouldAwaitDurability(true)` | `helix.AwaitDurability(true)` | `await_durability=True` | `X-Helix-Await-Durable: true` |

Rust and TypeScript set options on `request_builder()` / `requestBuilder()` before
`.sql(...)`; Go and Python pass them to `SQL` and `sql`. Embedded clients reject
request options.

## Explain a statement

`POST /v2/sql/explain` takes the same body, translates the statement, and plans
the translated Cypher without running it, exactly like
[explaining Cypher](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#explain-a-statement).
Explaining a write changes nothing. Planning checks parameters, so supply every
parameter the statement uses.

```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
let plan = client
    .explain_sql(
        "SELECT p.name AS name FROM Person p WHERE p.email = :email",
        std::collections::BTreeMap::from([(
            "email".to_string(),
            serde_json::json!("ada@example.com"),
        )]),
        &helix_db::SqlMappings::default(),
        None,
    )
    .await?;
// plan["effect"] == "Read"
```

```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
const plan = await client.explainSql(
  "SELECT p.name AS name FROM Person p WHERE p.email = :email",
  { email: "ada@example.com" },
);
// plan.effect === "Read"
```

```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
plan, err := client.ExplainSQL(ctx, helix.SQLRequest{
	Query:      "SELECT p.name AS name FROM Person p WHERE p.email = :email",
	Parameters: map[string]any{"email": "ada@example.com"},
})
// plan["effect"] == "Read"
```

```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
plan = client.explain_sql(
    "SELECT p.name AS name FROM Person p WHERE p.email = :email",
    {"email": "ada@example.com"},
)
# plan["effect"] == "Read"
```

```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "query": "SELECT p.name AS name FROM Person p WHERE p.email = :email",
  "parameters": { "email": "ada@example.com" }
}
```

## What is supported

| Area | Supported |
| - | - |
| Statements | `SELECT`; `INSERT .. VALUES` with one or more rows; `UPDATE .. SET`; `DELETE`; `TRUNCATE TABLE`; `RETURNING` on `INSERT` and `UPDATE` |
| Tables | Tables as node labels; `Start_TYPE_End` relationship tables; aliases; schema prefixes; [mappings](#mappings) |
| Joins | `JOIN .. ON` one column equality, `USING`, `NATURAL JOIN`, chains through a relationship table, comma joins and `CROSS JOIN` |
| Row clauses | `WHERE`, `DISTINCT`, `GROUP BY`, `HAVING`, `ORDER BY` (including positions), `LIMIT`, `OFFSET`, `FETCH FIRST`, `TOP` |
| Expressions | Parameters; literals; arithmetic; comparisons; `AND`, `OR`, `XOR`, `NOT`; `IS [NOT] NULL`, `IS [NOT] TRUE`, `IS [NOT] DISTINCT FROM`; `IN` lists; `BETWEEN`; `LIKE` and `ILIKE` with a leading or trailing `%`; `\|\|`; simple and searched `CASE`; `CAST`; row values such as `(a, b) < (1, 2)` |
| Functions | `lower`, `upper`, `length`, `trim`, `ltrim`, `rtrim`, `substring`, `concat`, `coalesce`, `nvl`, `nullif`, `nvl2`, `abs`, `square`, `power`, `mod`, and the [Cypher functions](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#what-is-supported) under their own names, such as `id(p)` |
| Aggregation | `count` (including `count(*)`), `sum`, `avg`, `min`, `max`, `array_agg`, each with `DISTINCT` arguments |
| Values | The [Cypher values](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#values-and-parameters), with the same lossless JSON encoding |

Create and read a small graph:

```sql theme={"languages":{"custom":["languages/helixql.json"]}}
INSERT INTO Person (name, age) VALUES ('Ada', 30), ('Bob', 40)
```

```sql theme={"languages":{"custom":["languages/helixql.json"]}}
INSERT INTO Person_KNOWS_Person (_start.name, _end.name, since)
VALUES ('Ada', 'Bob', 2020)
```

```sql theme={"languages":{"custom":["languages/helixql.json"]}}
SELECT a.name AS person, array_agg(b.name) AS friends
FROM Person a
NATURAL JOIN KNOWS
NATURAL JOIN Person b
WHERE a.age >= :minimumAge
GROUP BY a.name
ORDER BY person
```

## Tables and columns

| SQL | Cypher |
| - | - |
| `FROM Person p` | `MATCH (p:Person)`; the variable is the lowercased alias |
| `FROM Person` | `MATCH (person:Person)`; the variable is the lowercased table name |
| `SELECT p.name`, or `SELECT name` with one table | `p.name` |
| `SELECT p` | the node itself |
| `SELECT p.*` | `properties(p)`, a map of the node's properties |
| `SELECT *` | every table variable |
| `SELECT v$id` or `SELECT id(p)` | `id(p)` |

Labels and properties are case-sensitive and keep the case you write; table
names, aliases, and mapping keys are not. Schema prefixes such as
`public.Person` are ignored. With more than one table, every column needs a
table qualifier. Every table is a label, so a node without a label has no SQL
form.

A table named `Start_TYPE_End`, such as `Person_ACTED_IN_Movie`, is a
**relationship table**. On its own it matches
`(_start:Person)-[person_acted_in_movie:ACTED_IN]->(_end:Movie)`. Its
unqualified columns are relationship properties, `Person.name` or `_start.name`
is a property of the start node, `Movie.title` or `_end.title` of the end node,
and `person_name` is shorthand for `_start.name` when only the start label is
`Person`, in reads, `UPDATE`, and `INSERT` alike; qualify a relationship property
whose name looks like shorthand with the type, as in `ACTED_IN.person_count`.
The start and end each need a
lowercase letter, so upper-case table names such as `ORDER_LINE_ITEMS` stay node
labels. When a `FROM` names several relationship tables, each one's nodes are
`<alias>_start` and `<alias>_end` instead.

## Joins

Joins become relationship patterns by these rules:

| SQL | Cypher |
| - | - |
| `Person p JOIN Movie m ON m.id = p.directed` | `(p:Person)-[directed:DIRECTED]->(m:Movie)` |
| `Customer c JOIN Order o ON c.id = o.customer_id` | `(c:Customer)<-[customer_id:CUSTOMER_ID]-(o:Order)` |
| `Person p JOIN Movie m USING (ACTED_IN)` | `(p:Person)-[acted_in:ACTED_IN]->(m:Movie)` |
| `Person p NATURAL JOIN Movie m` | `(p:Person)-->(m:Movie)` |
| `Person p NATURAL JOIN ACTED_IN r NATURAL JOIN Movie m` | `(p:Person)-[r:ACTED_IN]->(m:Movie)` |
| `Person p JOIN ACTED_IN r ON .. JOIN Movie m ON ..` | `(p:Person)-[r:ACTED_IN]->(m:Movie)` |
| `Person p, Movie m` or `CROSS JOIN` | `(p:Person), (m:Movie)` |

* In a direct join, the second column of the `=` names the relationship type,
  uppercased, and the relationship points away from the table it belongs to. Its
  variable is the lowercased type, numbered when a table already uses it, and
  columns can read it, as in `directed.since`.
* In a chain of three tables, the middle one is the relationship; longer chains
  continue in pairs, and the last `ON` of each pair sets the direction.
* Every `ON` must compare two columns with `=`. Put other conditions in `WHERE`.
  The compared columns only name the relationship and its direction; they are
  not read.

### Mappings

`mappings` renames tables and join columns. Keys match case-insensitively:

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "query": "SELECT p.name, m.title FROM people p JOIN movies m ON m.id = p.movie_id",
  "mappings": {
    "tables": { "people": "Person", "movies": "Movie" },
    "join_columns": { "people.movie_id": "DIRECTED" }
  }
}
```

This matches `(p:Person)-[directed:DIRECTED]->(m:Movie)`. A `join_columns` key
names the join's left table and the column that names the type.

The SDKs take the same mappings: `helix_db::SqlMappings` in Rust,
`{ tables, joinColumns }` in TypeScript, `helix.SQLMappings` in Go, and the
`mappings=` dictionary in Python.

## Statements

| SQL | Cypher |
| - | - |
| `SELECT .. FROM .. WHERE ..` | `MATCH .. WHERE .. RETURN ..` |
| `DISTINCT`, `ORDER BY`, `LIMIT`, `OFFSET`, `FETCH FIRST`, `TOP` | `DISTINCT`, `ORDER BY`, `LIMIT`, `SKIP` |
| `GROUP BY`, `HAVING` | implicit grouping, or `WITH .. WHERE ..` when SQL groups differently |
| `INSERT INTO Person (name, born) VALUES (..)` | `CREATE (person:Person {name: .., born: ..})` |
| `INSERT .. VALUES (..), (..)` | `UNWIND [{..}, {..}] AS properties CREATE .. SET person = properties` |
| `UPDATE Person SET born = born + 1 WHERE ..` | `MATCH (person:Person) WHERE .. SET person.born = person.born + 1` |
| `DELETE FROM Person WHERE ..` | `MATCH (person:Person) WHERE .. DELETE person` |
| `TRUNCATE TABLE Person` | `MATCH (person:Person) DELETE person` |
| `TRUNCATE TABLE Person CASCADE` | `MATCH (person:Person) DETACH DELETE person` |
| `RETURNING ..` on `INSERT` or `UPDATE` | `RETURN ..` |

Grouping follows SQL: a selected column must be grouped or aggregated, `GROUP BY`
without an aggregate returns each group once, an aggregate without `GROUP BY`
returns one row even for an empty table, `SUM` of no values is null, and
`ORDER BY 1` or `GROUP BY 1` refer to select-list positions. An alias in
`GROUP BY`, `HAVING`, or `ORDER BY` names its select-list entry.

```sql theme={"languages":{"custom":["languages/helixql.json"]}}
SELECT c.name AS city, count(*) AS people, avg(p.age) AS averageAge
FROM Person p
NATURAL JOIN LIVES_IN
NATURAL JOIN City c
GROUP BY c.name
ORDER BY people DESC
LIMIT 10
```

An `INSERT` into a relationship table connects existing nodes. Columns of the
start and end nodes find them, and the other columns become relationship
properties:

```sql theme={"languages":{"custom":["languages/helixql.json"]}}
INSERT INTO Person_ACTED_IN_Movie (Person.name, Movie.title, role)
VALUES ('Ada', 'Hidden', 'lead')
```

which runs as:

```cypher theme={"languages":{"custom":["languages/helixql.json"]}}
MATCH (_start:Person {name: 'Ada'}), (_end:Movie {title: 'Hidden'})
CREATE (_start)-[person_acted_in_movie:ACTED_IN {role: 'lead'}]->(_end)
```

It connects every pair of nodes the columns match, and creates nothing when
either node is missing. To connect nodes by id, use `_start.v$id` and
`_end.v$id`, or `v$person_id` when the label tells the ends apart.

## Expressions and functions

Comparisons, `AND`, `OR`, `NOT`, `IS [NOT] NULL`, `IS [NOT] TRUE`,
`IS [NOT] DISTINCT FROM`, `IN` lists, `BETWEEN`, arithmetic, `CASE`, `CAST`, and
row values such as `(a, b) < (1, 2)` translate with SQL semantics. `||` and
`CONCAT` join their operands as strings; `||` returns null when an operand is
null, and `CONCAT` skips nulls, as in PostgreSQL. `CAST` uses Cypher's
conversions: to an integer type it truncates toward zero, and text that does not
convert becomes null rather than an error.
`LIKE` and `ILIKE` translate when the pattern is a literal whose only wildcards
are a leading or trailing `%`: `'%x%'` becomes `CONTAINS`, `'x%'` `STARTS WITH`,
and `'%x'` `ENDS WITH`.

Functions with a Cypher equivalent are renamed: `lower` and `upper` become
`toLower` and `toUpper`, `length` and `char_length` become `size`, `nvl` and
`ifnull` become `coalesce`, `array_agg` becomes `collect` (which, unlike SQL's
`array_agg`, leaves out nulls), and `nullif` and
`nvl2` become `CASE`. `SUBSTRING` counts from 1, as in SQL; a start below 1
must be a literal, since a computed one fails. `TRIM`, `LTRIM`, and `RTRIM`
remove all surrounding whitespace, as Cypher's do, not only spaces. Other names
pass through, so the [Cypher profile](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#what-is-supported)
decides which functions run.

## What is not supported

These are rejected before anything runs, mostly with the error category
`unsupported_feature` and the detail `sql_construct`:

| Construct | Example | `details.detail` |
| - | - | - |
| Outer joins | `LEFT JOIN Movie m ON ..` | `sql_construct` |
| `ON` conditions other than one column equality | `ON m.id = p.movie AND m.year > 2000` | `sql_construct` |
| Subqueries | `WHERE p.id IN (SELECT ..)`, `EXISTS (..)` | `procedures_and_subqueries` |
| Set operations | `UNION`, `INTERSECT`, `EXCEPT` | `union` |
| Common table expressions and window functions | `WITH x AS (..)`, `row_number() OVER ()` | `sql_construct` |
| Upserts | `ON CONFLICT`, `ON DUPLICATE KEY`, `REPLACE`, `INSERT OR` | `merge` |
| `INSERT` without a column list | `INSERT INTO Person VALUES ('Ada')` | `invalid_statement` |
| `INSERT .. SELECT` | `INSERT INTO A (x) SELECT ..` | `sql_construct` |
| `LIKE` patterns that need a regular expression | `LIKE 'M_trix'`, `LIKE '%a%b%'` | `sql_construct` |
| Unparenthesized `XOR` inside `AND`, `NOT`, or a comparison | `NOT a XOR b` | `sql_construct` |
| Dates, intervals, and other casts | `DATE '2020-01-01'`, `CAST(x AS DATE)` | `sql_construct` |
| DDL and other statements | `CREATE TABLE ..` | `sql_construct` |
| Several statements in one request | `SELECT 1; SELECT 2` | `multiple_statements` |

Some fail as ordinary compile errors instead:

* An unqualified column in a statement with several tables is a `syntax_error`
  (`ambiguous_column`), as is a qualifier that names several tables.
* A selected column that is neither grouped nor aggregated is a `syntax_error`
  (`invalid_aggregation`).
* A function outside the [Cypher profile](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#what-is-not-supported),
  such as `sqrt`, reports that profile's error.

Nodes without a label, variable-length paths, and vector and full-text search
have no SQL form; use Cypher or native queries for them.

## Values and parameters

Values in parameters and results use the
[Cypher encoding](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#values-and-parameters):
ordinary JSON for most values, tagged values for large integers, non-finite
floats, nodes, relationships, and paths.

| Placeholder | Binds | `parameters` |
| - | - | - |
| `?` | The next value, in written order | A list: `[30, "Ada"]` |
| `:name`, `$name`, `@name` | The value named `name` | An object: `{"name": "Ada"}` |
| `$1`, `?1` | The value named `1` | An object: `{"1": 30}` |

A statement that uses `?` cannot also use numbered placeholders, since `?` takes
those numbers. `IN (?)` binds a list parameter. Labels, relationship types, and
column names are part of the statement, so keep values in parameters.

## Writes and transactions

Each statement that writes runs in one transaction, including its index changes,
as a [Cypher write](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#writes-and-transactions)
does. If any part fails, nothing the statement wrote is kept.

* A multi-row `INSERT` creates every row or none. Inserting into a relationship
  table that matches no node pair creates nothing and succeeds.
* `UPDATE` computes every new value from the row before the update, so
  `SET a = b, b = a` swaps. Setting a column to `NULL` removes the property.
* `DELETE` or `TRUNCATE` of a node that still has relationships fails, like a
  foreign key. Delete the relationships first, or use `TRUNCATE .. CASCADE`,
  which removes them.
* Stored properties follow the Cypher rules: scalars or lists of one scalar type,
  and values an index can store.
* One request is one statement, so a write followed by a read is two requests and
  two transactions.
* Do not retry a write automatically when its commit outcome is uncertain; see
  [error handling](https://docs.helix-db.com/database/helix-db/query-guides/error-handling.md#stability-and-retry-rules).

```sql theme={"languages":{"custom":["languages/helixql.json"]}}
UPDATE Person p SET age = p.age + 1, nickname = NULL WHERE p.name = 'Ada'
RETURNING p.age
```

## Indexes and performance

SQL runs as the translated Cypher, so it uses existing native indexes the same
way, without naming them. The
[Cypher rules](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#indexes-and-performance)
apply to the translated statement:

* An equality on a table's column, such as `WHERE u.email = ?`, can read an
  equality index, and so can `IN` lists and `OR`ed equalities on one column.
* A range such as `WHERE u.age >= 21 AND u.age < ?` can read a range index.
* `WHERE id(u) = ?`, or `WHERE u.v$id IN (1, 2)` with a list of literal ids,
  reads those nodes directly. A list parameter, as in `IN (?)`, scans the label.
* A join reads only the relationship type it names, so name the relationship
  table or join column rather than joining every relationship.
* Without `DISTINCT` or grouping, `ORDER BY`, `OFFSET`, and `LIMIT` apply to the
  matched tables before the select list reads columns, so
  `SELECT u.name FROM User u LIMIT 10` stops after ten nodes when its `WHERE`
  cannot fail.
* `ORDER BY` a range-indexed column in the index's direction, with a range bound
  on it and a `LIMIT`, reads the index in order and stops after the window, as in
  `SELECT u.name FROM User u WHERE u.rank >= 0 ORDER BY u.rank DESC LIMIT 10`
  with a descending index on `rank`.

An index is only used when every other condition on that table cannot fail.
Comparisons, string predicates, null tests, and their `AND`, `OR`, `XOR`, and
`NOT` combinations over literals, parameters, and columns qualify. Arithmetic and
function calls keep the label scan.

## Limits

A SQL statement has the [Cypher limits](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#limits),
plus 1,024 binary operators and about 500,000 tokens, where every word, number,
symbol, and whitespace character counts, including those inside quoted text.
Forms that repeat an operand, such as `BETWEEN` or an alias in `GROUP BY`, may
copy about 16 MB of it in total. Exceeding any of these fails with
`resource_limit` (HTTP `429`). Send large values as parameters, which do not
count.

## Errors

A failed statement returns the
[Cypher error body](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md#errors). Translation
errors and the errors of the translated statement both report a `span`, the byte
range of the SQL text that failed:

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "error": "unsupported_feature",
  "msg": "the Cypher MVP profile does not support function `sqrt`",
  "details": { "detail": "function", "phase": "compile", "span": { "start": 7, "end": 16 } }
}
```

The [error reference](https://docs.helix-db.com/database/helix-db/query-guides/error-handling.md#cypher-errors)
lists every category with its HTTP status and retry guidance.

## Next steps

> [card] [Cypher](https://docs.helix-db.com/database/helix-db/query-guides/cypher.md)
>
> The Cypher profile that SQL translates to, and how it plans indexes.

> [card] [HTTP API](https://docs.helix-db.com/database/helix-db/query-guides/http-api.md#sql)
>
> Request headers, status codes, and the OpenAPI contract for `/v2/sql`.

> [card] [Error handling](https://docs.helix-db.com/database/helix-db/query-guides/error-handling.md#cypher-errors)
>
> Error categories, HTTP statuses, and when to retry.

> [card] [Secondary indexes](https://docs.helix-db.com/database/helix-db/query-guides/secondary-indexes.md)
>
> Create the indexes that SQL and Cypher plans read.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.