Cloud Spanner¶
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.
Connection¶
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")
Instances¶
Instance configs¶
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.
Create instance¶
{
"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 instance¶
List instances¶
Update instance¶
Updates displayName, nodeCount, or labels. Returns an LRO.
Delete instance¶
Drops all databases in the instance.
Databases¶
Create database¶
{
"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 database¶
List databases¶
Update database 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 database DDL¶
Returns the DDL statements used to create all tables in the database.
Delete database¶
Sessions¶
All data operations require a session. Sessions are lightweight — they track which database is being used but do not hold connection state between requests.
Create session¶
Returns { "name": "...databases/{db}/sessions/{uuid}" }.
Batch create sessions¶
Returns { "session": [...] } with sessionCount sessions.
List sessions¶
Get session¶
Delete session¶
Reading data¶
Key-set 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"]]
}
Streaming read¶
Same request body as :read. Returns a newline-delimited JSON stream of
PartialResultSet objects (compatible with the SDK's streaming path).
SQL queries and DML¶
Execute SQL¶
{
"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" } }.
Execute streaming SQL¶
Same as :executeSql but streams results as newline-delimited PartialResultSet JSON.
Batch DML¶
{
"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": {}
}
Mutations and transactions¶
Begin transaction¶
Returns { "id": "<base64-transaction-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": "..." }.
Rollback¶
No-op — returns {}.
Long-Running Operations¶
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 mapping¶
| 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 |
Known limitations¶
| 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 |