A mock server that implements the full Altertable API surface — Arrow Flight SQL (gRPC), Lakehouse REST, and Product Analytics REST — backed by DuckLake on DuckDB 2.0.0. Intended for local development and testing.
Altertable exposes three interfaces:
- Arrow Flight SQL (gRPC) — high-performance query and data ingestion protocol
- Lakehouse REST (HTTP) — async SQL queries, validation, and data upload/upsert/append
- Product Analytics REST (HTTP) — event tracking and user identity management
This server speaks all three protocols and behaves similarly to the real service, without requiring a real Altertable account or any external infrastructure.
Each authenticated user gets their own DuckLake. Its catalog is an in-memory DuckDB database and its data files live in a temporary directory for the life of that lakehouse. The default catalog name remains memory. Sessions within a user share that lakehouse. The server supports:
- Ad-hoc SQL queries and DML statements
- Prepared statements with parameter binding
- Bulk Arrow record batch ingestion (including upsert / MERGE semantics)
- Transactions (
BEGIN,COMMIT,ROLLBACK) - Schema introspection (catalogs, schemas, tables, SQL info)
- Run integration tests against a real Flight SQL endpoint without hitting production
- Develop locally without network access or credentials
- Deterministic, ephemeral state — DuckLake metadata is discarded when the server stops
- Fast and self-contained — no external services required
The server listens on three ports by default:
| Port | Protocol | Description |
|---|---|---|
15000 |
HTTP (REST) | Lakehouse REST API |
15001 |
HTTP (REST) | Product Analytics API |
15002 |
gRPC (Flight SQL) | Arrow Flight SQL API |
A pre-built image is published at ghcr.io/altertable-ai/altertable-mock:
docker run -p 15000:15000 -p 15001:15001 -p 15002:15002 \
-e ALTERTABLE_MOCK_USERS="alice:secret,bob:hunter2" \
-e ALTERTABLE_MOCK_API_KEYS="myapikey" \
-e ALTERTABLE_MOCK_ENVIRONMENTS="production,staging" \
ghcr.io/altertable-ai/altertable-mockcargo build --release
./target/release/altertable-mock --user alice:secret --user bob:hunter2POST /query accepts a params object with string, numeric, boolean, or null values.
DuckDB binds them to named placeholders without changing the statement:
{ "statement": "SELECT $value AS value", "params": { "value": 42 } }Missing, unknown, or non-scalar parameters return a query error through the usual response format.
| Flag | Short | Env var | Default | Description |
|---|---|---|---|---|
--flight-port |
-f |
ALTERTABLE_MOCK_FLIGHT_PORT |
15002 |
Port for the Arrow Flight SQL (gRPC) server |
--lakehouse-port |
-p |
ALTERTABLE_MOCK_LAKEHOUSE_PORT |
15000 |
Port for the Lakehouse REST (HTTP) server |
--analytics-port |
-a |
ALTERTABLE_MOCK_ANALYTICS_PORT |
15001 |
Port for the Product Analytics REST (HTTP) server |
--user |
-u |
ALTERTABLE_MOCK_USERS |
— | username:password pairs (repeatable, or comma-separated via env var) |
--api-key |
-k |
ALTERTABLE_MOCK_API_KEYS |
— | API keys for the Product Analytics server (repeatable, or comma-separated via env var) |
Examples:
# Custom ports, multiple users and an API key via flags
altertable-mock --flight-port 9090 --lakehouse-port 9091 --analytics-port 9092 \
--user alice:s3cr3t --user bob:hunter2 --api-key myapikey
# Via environment variables
ALTERTABLE_MOCK_FLIGHT_PORT=9090 ALTERTABLE_MOCK_LAKEHOUSE_PORT=9091 \
ALTERTABLE_MOCK_ANALYTICS_PORT=9092 ALTERTABLE_MOCK_USERS=alice:s3cr3t,bob:hunter2 \
ALTERTABLE_MOCK_API_KEYS=myapikey \
altertable-mockLog verbosity is controlled via the standard RUST_LOG environment variable (default: info).
Connect using any Arrow Flight SQL client pointed at localhost:15002. The altertable-flightsql Python library is the recommended client:
pip install altertable-flightsqlfrom altertable_flightsql import Client
with Client(username="alice", password="secret", host="localhost", port=15002) as client:
reader = client.query("SELECT * FROM my_table")
for batch in reader:
print(batch.data.to_pandas())Authenticate with HTTP Basic Auth. All endpoints are on localhost:15000.
| Method | Path | Description |
|---|---|---|
POST |
/query |
Submit an async SQL query |
GET |
/query/{query_id} |
Poll / fetch results for a query |
DELETE |
/query/{query_id} |
Cancel a query |
POST |
/validate |
Validate a SQL statement |
POST |
/upload |
Upload data (CSV, JSON, Parquet; modes: create, append, create_append, overwrite) |
POST |
/upsert |
Upsert data by one or more primary key columns, newest cursor_field value wins (CSV, JSON, Parquet) |
POST |
/append |
Append Arrow data |
curl -u alice:secret -X POST http://localhost:15000/query \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT 42 AS answer"}'Authenticate with an API key passed as either an X-API-Key header or an Authorization: Bearer <token> header. Each API key selects its own isolated in-memory DuckDB database.
The API key determines the environment. Legacy payload environment fields and
--environment / ALTERTABLE_MOCK_ENVIRONMENTS settings are accepted and ignored.
| Method | Path | Description |
|---|---|---|
POST |
/track |
Ingest one or a batch of analytics events |
POST |
/identify |
Create or update a user identity with traits |
POST |
/alias |
Alias an anonymous ID to an identified user |
All endpoints accept either a single object or a JSON array of objects.
POST /track
curl -H "X-API-Key: myapikey" -X POST http://localhost:15001/track \
-H "Content-Type: application/json" \
-d '{
"event": "Button Clicked",
"properties": {"button": "signup"},
"distinct_id": "user_42",
"anonymous_id": "anon_abc",
"timestamp": "2025-06-15T12:00:00Z"
}'| Field | Type | Required | Description |
|---|---|---|---|
event |
string | yes | Event name |
properties |
object | yes | Arbitrary event properties |
distinct_id |
string | no | Identified user ID |
anonymous_id |
string | no | Anonymous / device ID (linked to distinct_id if both provided) |
device_id |
string | no | Device identifier |
session_id |
string | no | Session identifier |
timestamp |
string or number | no | ISO 8601 string or Unix timestamp in milliseconds; defaults to now |
mode |
string | no | product (default) or audience; see below |
With "mode": "audience", the event is collected without identity: distinct_id,
anonymous_id, device_id and session_id are rejected with 400
(audience-identity-not-allowed, the whole batch is refused), a distinct_id is derived
from a daily-rotating salted HMAC of the API key, $url hostname, client IP and
user agent, and IP, geo, user-agent, referer, device and language properties are dropped.
The mode is stored in the mode column of events.
POST /identify
curl -H "X-API-Key: myapikey" -X POST http://localhost:15001/identify \
-H "Content-Type: application/json" \
-d '{
"distinct_id": "user_42",
"traits": {"email": "alice@example.com", "plan": "pro"},
"anonymous_id": "anon_abc"
}'| Field | Type | Required | Description |
|---|---|---|---|
distinct_id |
string | yes | Identified user ID |
traits |
object | no | User traits to shallow-merge into existing traits |
anonymous_id |
string | no | Anonymous ID to link to this identity |
POST /alias
curl -H "X-API-Key: myapikey" -X POST http://localhost:15001/alias \
-H "Content-Type: application/json" \
-d '{
"distinct_id": "anon_abc",
"new_user_id": "user_42"
}'| Field | Type | Required | Description |
|---|---|---|---|
distinct_id |
string | yes | The source ID to be merged away |
new_user_id |
string | yes | The target ID that survives the merge |
All endpoints return {"ok": true} on success or {"ok": false, "error_code": "..."} on failure.
The build links stock DuckDB 2.0.0, downloaded from the version pinned by duckdb-rs. Set DUCKDB_LIB_DIR to link a local copy instead. Lakehouse connections install and load ducklake (INSTALL ducklake; LOAD ducklake;) and the semantic_layer community extension (INSTALL semantic_layer FROM community; LOAD semantic_layer;). The Docker image downloads both extensions first, so INSTALL finds them already present and LOAD succeeds.
cargo build --releaseThe e2e crate tests the Docker image the way its users run it. It builds the repository's Dockerfile with docker build, starts containers with testcontainers, and talks to them only over the published ports. It needs a running Docker daemon:
cargo test --manifest-path e2e/Cargo.tomlThe first run builds the image from scratch; later runs reuse Docker's layer cache. Set ALTERTABLE_MOCK_IMAGE to test an existing image instead, such as ghcr.io/altertable-ai/altertable-mock:latest.
Lakehouse queries are golden files in e2e/cases/lakehouse. Each file runs as its own user, so files never share tables. A case is the SQL sent to POST /query and every response line after the header (the column line, then the rows or the error):
query {"params": {"value": 42}}
SELECT $value AS answer
----
[{"name":"answer","type":"BIGINT"}]
[42]
statement ok runs setup SQL and only checks that it succeeds. A JSON object after the directive is merged into the request body. To write or refresh the expected output, run the tests with E2E_BLESS=1 and review the diff.