Cloudbox emulates the Cloud Spanner REST API (v1) using DuckDB as the backend. The
google-cloud-spanner Python SDK works against it with minimal configuration changes.
Port: 9010 (override with CLOUDBOX_SPANNER_PORT)
from google.auth.credentials import AnonymousCredentials
from google.cloud import spanner
client = spanner.Client(
project="local-project",
credentials=AnonymousCredentials(),
client_options={"api_endpoint": "http://localhost:9010"},
)
instance = client.instance("my-instance")
database = instance.database("my-database")GET /v1/projects/{project}/instanceConfigs
GET /v1/projects/{project}/instanceConfigs/{config}
Returns a list of available instance configurations. The emulator advertises a single
config: regional-us-central1. This is a no-op — all instances share the same DuckDB
backend.
POST /v1/projects/{project}/instances
{
"instanceId": "my-instance",
"instance": {
"config": "projects/local-project/instanceConfigs/regional-us-central1",
"displayName": "My Instance",
"nodeCount": 1,
"labels": {}
}
}Returns a Long-Running Operation (LRO) that is immediately marked as done: true.
GET /v1/projects/{project}/instances/{instance_id}
GET /v1/projects/{project}/instances
PATCH /v1/projects/{project}/instances/{instance_id}
Updates displayName, nodeCount, or labels. Returns an LRO.
DELETE /v1/projects/{project}/instances/{instance_id}
Drops all databases in the instance.
POST /v1/projects/{project}/instances/{instance_id}/databases
{
"createStatement": "CREATE DATABASE `my-database`",
"extraStatements": [
"CREATE TABLE users (id INT64 NOT NULL, name STRING(MAX)) PRIMARY KEY (id)"
]
}extraStatements are DDL statements executed after creating the database. Each statement
creates or alters a DuckDB table. Returns an LRO.
GET /v1/projects/{project}/instances/{instance_id}/databases/{database_id}
GET /v1/projects/{project}/instances/{instance_id}/databases
PATCH /v1/projects/{project}/instances/{instance_id}/databases/{database_id}/ddl
{
"statements": [
"ALTER TABLE users ADD COLUMN email STRING(MAX)",
"CREATE TABLE orders (id INT64, user_id INT64) PRIMARY KEY (id)"
]
}Applies DDL statements to the DuckDB database. Returns an LRO.
GET /v1/projects/{project}/instances/{instance_id}/databases/{database_id}/ddl
Returns the DDL statements used to create all tables in the database.
DELETE /v1/projects/{project}/instances/{instance_id}/databases/{database_id}
All data operations require a session. Sessions are lightweight — they track which database is being used but do not hold connection state between requests.
POST /v1/projects/{project}/instances/{instance_id}/databases/{database_id}/sessions
Returns { "name": "...databases/{db}/sessions/{uuid}" }.
POST /v1/projects/{project}/instances/{instance_id}/databases/{database_id}/sessions:batchCreate
{ "sessionCount": 5 }Returns { "session": [...] } with sessionCount sessions.
GET /v1/projects/{project}/instances/{instance_id}/databases/{database_id}/sessions
GET /v1/projects/{project}/instances/{instance_id}/databases/{database_id}/sessions/{session_id}
DELETE /v1/projects/{project}/instances/{instance_id}/databases/{database_id}/sessions/{session_id}
POST /v1/.../{database_id}/sessions/{session_id}:read
{
"table": "users",
"columns": ["id", "name", "email"],
"keySet": {
"keys": [["1"], ["2"]],
"ranges": [{ "startClosed": ["10"], "endOpen": ["20"] }],
"all": false
},
"limit": 100,
"index": ""
}keySet.all: true reads all rows. keySet.keys reads specific primary key values.
keySet.ranges reads key ranges (inclusive/exclusive startClosed/startOpen/
endClosed/endOpen).
Returns:
{
"metadata": { "rowType": { "fields": [{ "name": "id", "type": { "code": "INT64" } }, ...] } },
"rows": [["1", "Alice", "alice@example.com"]]
}POST /v1/.../{database_id}/sessions/{session_id}:streamingRead
Same request body as :read. Returns a newline-delimited JSON stream of
PartialResultSet objects (compatible with the SDK's streaming path).
POST /v1/.../{database_id}/sessions/{session_id}:executeSql
{
"sql": "SELECT id, name FROM users WHERE id = @user_id",
"params": { "user_id": "1" },
"paramTypes": { "user_id": { "code": "INT64" } }
}For SELECT queries, returns a ResultSet with metadata and rows. For DML
(INSERT, UPDATE, DELETE), returns { "stats": { "rowCountExact": "N" } }.
POST /v1/.../{database_id}/sessions/{session_id}:executeStreamingSql
Same as :executeSql but streams results as newline-delimited PartialResultSet JSON.
POST /v1/.../{database_id}/sessions/{session_id}:executeBatchDml
{
"statements": [
{
"sql": "INSERT INTO users (id, name) VALUES (@id, @name)",
"params": { "id": "1", "name": "Alice" },
"paramTypes": { "id": { "code": "INT64" }, "name": { "code": "STRING" } }
},
{ "sql": "UPDATE users SET name = 'Bob' WHERE id = 2", "params": {}, "paramTypes": {} }
]
}Executes multiple DML statements in sequence. Returns:
{
"resultSets": [
{ "stats": { "rowCountExact": "1" } },
{ "stats": { "rowCountExact": "1" } }
],
"status": {}
}POST /v1/.../{database_id}/sessions/{session_id}:beginTransaction
{
"options": {
"readWrite": {},
"readOnly": { "strong": true }
}
}Returns { "id": "<base64-transaction-id>" }.
POST /v1/.../{database_id}/sessions/{session_id}:commit
{
"transactionId": "<base64-id>",
"mutations": [
{
"insert": {
"table": "users",
"columns": ["id", "name"],
"values": [["3", "Carol"]]
}
},
{
"update": {
"table": "users",
"columns": ["id", "name"],
"values": [["1", "Alice Updated"]]
}
},
{
"delete": {
"table": "users",
"keySet": { "keys": [["2"]] }
}
}
]
}Supported mutation types: insert, update, insertOrUpdate, replace, delete.
Returns { "commitTimestamp": "..." }.
POST /v1/.../{database_id}/sessions/{session_id}:rollback
{ "transactionId": "<base64-id>" }No-op — returns {}.
Instance and database creation/modification operations return LRO resources:
{
"name": "projects/.../instances/my-instance/operations/op-uuid",
"done": true,
"response": { ... }
}All operations complete immediately (done: true). Operation status can be polled:
GET /v1/projects/{project}/instances/{instance_id}/operations/{op_id}
GET /v1/projects/{project}/instances/{instance_id}/databases/{database_id}/operations/{op_id}
| Spanner type | DuckDB type |
|---|---|
BOOL |
BOOLEAN |
INT64 |
BIGINT |
FLOAT64 |
DOUBLE |
STRING(N) / STRING(MAX) |
VARCHAR |
BYTES(N) / BYTES(MAX) |
BLOB |
DATE |
DATE |
TIMESTAMP |
TIMESTAMPTZ |
NUMERIC |
DECIMAL(38, 9) |
JSON |
JSON |
ARRAY<T> |
DuckDB array type |
| Feature | Notes |
|---|---|
| Partitioned reads and DML | partitionRead / partitionQuery not implemented |
| Stale reads | readTimestamp / exactStaleness accepted but returns current data |
| Read isolation | Transactions do not provide MVCC — concurrent writes are visible mid-transaction |
| Change streams | Not implemented |
| Full-text search indexes | Not implemented |
| IAM | Not enforced |
uv run python examples/spanner/basic.py