Back to Blog

Native ArcadeDB Drivers for Python and TypeScript, Over HTTP and gRPC

Two shared contracts, an OpenAPI specification and a Protobuf file, generating four published ArcadeDB clients: arcadedb-driver and @arcadedb/driver over HTTP, arcadedb-driver-grpc and @arcadedb/driver-grpc over gRPC

Until now, talking to ArcadeDB from Python or Node meant one of two things: writing HTTP calls by hand against the REST API, or borrowing a driver built for another database and living inside the subset of ArcadeDB that other database’s protocol happens to expose. Both work. Neither is a client anyone would call native.

That changes with arcadedb-drivers, a new repository holding four published clients: an HTTP driver and a gRPC driver for Python, and an HTTP driver and a gRPC driver for TypeScript/JavaScript. All four are Apache-2.0, all four are generated from contracts ArcadeDB itself publishes, and all four are on the public registries today. They are also young: 0.1.0 releases under heavy development, with more languages on the way.

The four packages

Package Language API Install
arcadedb-driver Python HTTP pip install arcadedb-driver
arcadedb-driver-grpc Python gRPC pip install arcadedb-driver-grpc
@arcadedb/driver TypeScript/JS HTTP npm install @arcadedb/driver
@arcadedb/driver-grpc TypeScript/JS gRPC npm install @arcadedb/driver-grpc

All four talk to a running server, so they assume the client-server deployment rather than the embedded one. The Python packages need Python 3.10 or newer. The TypeScript packages need Node 20 or newer and are ESM only: import them, do not require() them. Version 0.1.0 of each targets ArcadeDB server 26.9.1, and every package README carries a compatibility table mapping driver versions to server versions.

HTTP or gRPC?

Two drivers per language is not indecision. HTTP and gRPC are the two transports ArcadeDB speaks, and each handles some workloads better than the other.

Use the HTTP driver when Use the gRPC driver when
You are writing browser code You are writing server-to-server code
You want the fewest dependencies You are reading a large result set and want to stream it
You are in a serverless function with cold starts to worry about You are bulk-inserting and want one long-lived connection
Your traffic is ordinary request/response Your traffic is throughput-sensitive and sustained
You want to reuse existing HTTP infrastructure: proxies, gateways, tracing You want native bidirectional streaming

Start with HTTP. It works everywhere, it needs nothing beyond fetch or httpx, and for most application traffic the protocol is not the bottleneck. Move a workload to gRPC when you can point at the throughput number that justifies it.

One constraint is easy to lose an afternoon to, so here it is plainly. There is no browser build of the gRPC driver, and there will not be one until the server changes. ArcadeDB’s GrpcServerPlugin is plain grpc-java over HTTP/2, built on Netty, with no gRPC-Web handler, no Connect protocol, and no servlet adapter in front of it. A browser cannot speak raw HTTP/2 gRPC framing, so no client library in any language can reach this server from a browser tab. Browser code uses the HTTP driver. The limit is in the server.

Connecting and querying

The HTTP drivers are the place to start. Python, synchronously:

from arcadedb_driver import ArcadeDBServer, basic_auth

with ArcadeDBServer(base_url="http://localhost:2480", auth=basic_auth("root", "playwithdata")) as srv:
    db = srv.db("mydb")
    envelope = db.query(language="sql", command="SELECT FROM Person WHERE age > ?", params={"1": 21})
    print(envelope.result)

The async facade mirrors the sync one method for method:

import asyncio
from arcadedb_driver import AsyncArcadeDBServer, basic_auth

async def main() -> None:
    async with AsyncArcadeDBServer(base_url="http://localhost:2480", auth=basic_auth("root", "playwithdata")) as srv:
        db = srv.db("mydb")
        envelope = await db.query(language="sql", command="SELECT FROM Person WHERE age > ?", params={"1": 21})
        print(envelope.result)

asyncio.run(main())

Both are context managers because both own an httpx client with its own connection pool that has to be released. One detail that surprises people in production: omitting timeout disables timeouts entirely instead of falling back to httpx’s five-second default, because in httpx an explicit timeout=None means exactly that. Pass an httpx.Timeout if you want requests bounded.

TypeScript, same query:

import { createClient, basicAuth } from "@arcadedb/driver";

const server = createClient({
  baseUrl: "http://localhost:2480",
  auth: basicAuth("root", "playwithdata"),
});

const db = server.db("mydb");
const { result } = await db.query({
  language: "sql",
  command: "SELECT FROM Person WHERE age > ?",
  params: { 1: 21 },
});

A bearer token, such as a session token returned by /api/v1/login, works the same way in both languages: swap basic_auth for bearer_auth, or basicAuth for bearerAuth.

Because ArcadeDB is multi-model, language does real work here. "sql", "cypher", "gremlin": the same query call reaches all of them, and the driver does not care which one you picked.

The result envelope, and why truncated matters

Neither HTTP driver returns a bare array of rows. Both return the whole response envelope:

interface QueryEnvelope<T> {
  result: T[];
  limit: number;
  returned: number;
  truncated: boolean;
}

Of every API decision in the drivers, this is the one most likely to look like pointless ceremony, so here is the case for it. truncated is true when the server’s serializer hit its row cap while the query still had rows left to write. When that happens, result is a partial answer, not a short but complete one, and the two are indistinguishable by shape: a 5,000-row array that stopped early looks exactly like a 5,000-row array that ran out of matching records. A driver that unwrapped the envelope and returned only result would be handing you a value you cannot check.

So check it. When truncated is true, re-query with a narrower filter or a higher limit. Raising limit is not always the fix, though: a result whose true size exceeds the server’s hard ceiling (arcadedb.server.httpQueryMaxResultRows) is refused outright with a 413 rather than truncated, and past that point a narrower filter is the only way forward.

Transactions

Both HTTP drivers wrap ArcadeDB’s server-side transaction sessions in the idiom their language already has. Python uses a context manager:

with srv.db("mydb").transaction() as tx:
    tx.command(language="sql", command="INSERT INTO Account SET balance = 100")
    total = tx.query(language="sql", command="SELECT sum(balance) as total FROM Account").result[0]["total"]

TypeScript uses a callback:

const total = await db.transaction(async (tx) => {
  await tx.command({ language: "sql", command: "INSERT INTO Account SET balance = 100" });
  const { result } = await tx.query({ language: "sql", command: "SELECT sum(balance) as total FROM Account" });
  return result[0].total;
});

The rule in both is the same, and it is the one to get right: every call that should take part in the transaction goes through the tx handle, not the outer db object you opened it from. A call made through the outer handle while a transaction is open auto-commits on its own, outside the transaction, exactly as if no transaction were open.

The commit and rollback contract has three clauses in both languages. The block exits cleanly and the transaction commits. The block raises and the transaction rolls back, with the block’s own exception propagating: if the rollback also fails, that failure is attached as __cause__ (Python) or err.cause (TypeScript) rather than replacing the error you actually asked about. And if the commit itself fails, a best-effort rollback is issued first, so the server-side session is not left open until arcadedb.server.httpTxExpireTimeout reaps it, before the commit’s error is re-raised.

Streaming, over gRPC

The gRPC drivers exist for the workloads where HTTP’s request/response shape is the cost. A large result set over HTTP means paging through repeated calls; over gRPC it is one stream.

from arcadedb_driver_grpc import create_client, messages

with create_client("localhost:50051", insecure=True) as client:
    response = client.raw.ExecuteQuery(
        messages.ExecuteQueryRequest(database="mydb", query="SELECT FROM Person WHERE age > 21", language="sql")
    )

Note the target: gRPC’s native host:port form, not a URL. There is no scheme to parse and nothing to default, so you pass credentials=grpc.ssl_channel_credentials() for TLS or say insecure=True explicitly.

raw is the generated stub for the whole ArcadeDbService, so every RPC in the contract is reachable through it. On top, each client adds three wrappers for the RPCs the generated stub alone handles badly: stream_query, insert_stream, and transaction. Streaming a query in TypeScript:

for await (const row of grpc.streamQuery({
  database: "mydb",
  query: "SELECT FROM Person",
  language: "sql",
})) {
  console.log(row.rid, row.properties);
}

streamQuery flattens the server’s stream of row batches into one row at a time, and that is the only thing it does. It deliberately does not pick retrievalMode for you, because the three modes differ in ways only the caller can weigh:

  • CURSOR, the default: runs the query once and streams results as you iterate. Pick it for a large result set you want to bound memory on.
  • MATERIALIZE_ALL: loads the whole result set on the server first, then emits it in batches. Pick it when you need a stable snapshot and can afford to hold it server-side.
  • PAGED: re-issues the query with LIMIT/SKIP per batch. Pick it when you want each batch’s consistency independent of the others.

Streaming inserts work the other way round. You hand insert_stream an async iterable of row batches, decide yourself how many rows go in a batch, and the driver owns the envelope bookkeeping that is easy to get wrong by hand: one stable session id for the whole stream, chunk_seq starting at 1 and incrementing, database set on the first chunk only, and last: true on the final one.

One security rule the gRPC drivers enforce in code instead of documenting and hoping. password_auth sends the password in plaintext gRPC metadata, so create_client refuses to pair it with a channel that has no transport credentials unless you pass insecure=True and say you meant it:

create_client("localhost:50051", auth=password_auth("root", "playwithdata"))
# raises InsecureChannelError

create_client("localhost:50051", auth=password_auth("root", "playwithdata"), credentials=grpc.ssl_channel_credentials())
# fine, the channel is encrypted

A bearer token is not a password and never trips this guard.

One contract, many clients

In a year, the four packages will matter less than where their code comes from.

contracts/ in the repository holds two files: the OpenAPI specification every HTTP client is generated from, and the Protobuf .proto every gRPC client is generated from. Both are fetched from ArcadeDB itself; neither is maintained by hand alongside it. No client package edits its generated types. Each client’s build regenerates from the contract and fails on a drift gate: if the checked-in generated code and a fresh regeneration disagree, the build breaks rather than shipping a client that quietly describes a server that no longer exists.

This is the failure mode the design is aimed at. Hand-written drivers rot silently. The server adds a field or tightens a response, and the driver keeps compiling and keeps returning plausible values until someone loses a day to it. A generated client with a drift gate cannot get there: the disagreement becomes a red build the day the contract moves, and adding a language later means writing a generator config, not re-reading the server’s source and hand-writing types.

Two smaller things follow from the same idea. Every release is built and published by CI from a clean checkout, never from anyone’s laptop: the npm packages carry provenance attestations, and the PyPI packages go out through trusted publishing with no long-lived token anywhere in the chain. And nothing publishes automatically. Every release is a human-triggered workflow dispatch.

What is next

These drivers are under heavy development. They are 0.1.0 releases, and while the APIs above are the ones we intend to keep, some of them will change before 1.0. Pin a version if you need the surface to hold still, and read the changelog before you move off it.

More languages are planned. The repository is already laid out for them: go/ and other language directories will appear as siblings of typescript/ and python/, each generated from the same two contracts. None exist yet, but the contract-first design is what makes adding one tractable.

Run them against a real workload and tell us where they get in the way. Issues and pull requests go to ArcadeData/arcadedb-drivers.