Skip to content

Commit 2f4148e

Browse files
authored
Add create_tables method to system adapter protocol (#70)
* feat: Add create_tables method to system adapter protocol and update related documentation * fix: Correct formatting in README for benchmark phases table * feat: Add support for Databricks variant in the system adapter and update related configurations * Update
1 parent 8fcf664 commit 2f4148e

16 files changed

Lines changed: 385 additions & 83 deletions

File tree

.claude/skills/system-adapter-builder/SKILL.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: system-adapter-builder
3-
description: Build or update a SpiceBench system adapter with JSON-RPC over stdio and HTTP, including setup/query_method/teardown/metrics support and template validation.
3+
description: Build or update a SpiceBench system adapter with JSON-RPC over stdio and HTTP, including setup/create_tables/query_method/teardown/metrics support and template validation.
44
---
55

66
# SpiceBench System Adapter Builder
@@ -17,6 +17,7 @@ A JSON-RPC 2.0 adapter that supports both transports:
1717
Required methods:
1818

1919
- `setup(run_id, datasets)`
20+
- `create_tables(run_id)`
2021
- `query_method(run_id)`
2122
- `teardown(run_id)`
2223
- `metrics(run_id)`
@@ -34,15 +35,17 @@ Required methods:
3435

3536
1. Copy the nearest template from `system-adapters/templates/<language>`.
3637
2. Keep request/response envelopes JSON-RPC 2.0 compliant (`jsonrpc`, `id`, `method`, `params`).
37-
3. Implement `setup` and `teardown` with run-scoped resources keyed by `run_id`.
38-
4. Implement `query_method` to return:
38+
3. Implement `setup` with run-scoped resources keyed by `run_id`.
39+
4. Implement `create_tables` so the adapter creates/registers benchmark destination tables.
40+
5. Implement `query_method` to return:
3941
- `driver`: typically `flightsql` or `databricks`
4042
- `db_kwargs`: real endpoint + auth kwargs for the SUT
41-
5. Implement `metrics` to return both objects:
43+
6. Implement `teardown` with run-scoped cleanup keyed by `run_id`.
44+
7. Implement `metrics` to return both objects:
4245
- `resource`: CPU, memory, disk bytes, disk IOPS
4346
- `ingestion`: rows, bytes, rows/s, active connections
44-
6. Keep stdio and HTTP using the same dispatcher so behavior is identical.
45-
7. Return JSON-RPC errors with standard codes:
47+
8. Keep stdio and HTTP using the same dispatcher so behavior is identical.
48+
9. Return JSON-RPC errors with standard codes:
4649
- `-32700` parse error
4750
- `-32600` invalid request
4851
- `-32601` method not found
@@ -70,6 +73,7 @@ If any metric is unavailable, return `0`/`0.0` and document why.
7073

7174
- Adapter responds to all required methods over stdio and HTTP.
7275
- `rpc.methods` includes every exposed method.
76+
- `create_tables` creates/registers benchmark tables for each dataset.
7377
- `query_method` returns a valid `driver` and complete `db_kwargs`.
7478
- `metrics` returns both `resource` and `ingestion` objects.
7579
- Language build/syntax checks pass:

.github/workflows/validate_system_adapter_templates.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,10 @@ jobs:
152152
working-directory: system-adapters/templates/go
153153
run: go build ./...
154154
- name: Smoke test Go template (HTTP setup)
155+
working-directory: system-adapters/templates/go
155156
run: |
156157
set -euo pipefail
157-
go run ./system-adapters/templates/go --transport http --host 127.0.0.1 --port 18083 --path /jsonrpc > /tmp/go-template.log 2>&1 &
158+
go run . --transport http --host 127.0.0.1 --port 18083 --path /jsonrpc > /tmp/go-template.log 2>&1 &
158159
pid=$!
159160
trap 'kill "$pid" 2>/dev/null || true' EXIT
160161

README.md

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ flowchart TB
1717
direction TB
1818
1919
subgraph setup_phase["1 · Setup (JSON-RPC)"]
20-
adapter_iface["System Adapter Protocol\n(setup / query_method /\nteardown / metrics)"]
20+
adapter_iface["System Adapter Protocol\n(setup / create_tables /\nquery_method / teardown / metrics)"]
2121
spice["Spice Cloud Adapter"]
2222
databricks["Databricks Adapter"]
2323
other["... Other Adapters"]
@@ -104,7 +104,8 @@ flowchart TB
104104
105105
orchestrator -->|"start run"| run
106106
107-
adapter_iface -->|"setup(run_id)"| sut
107+
adapter_iface -->|"setup(run_id, datasets)"| sut
108+
adapter_iface -->|"create_tables(run_id)"| sut
108109
adapter_iface -->|"query_method(run_id)\n→ ADBC driver + kwargs"| executors
109110
setup_phase -->|"system ready"| bench_phase
110111
bench_phase -->|"benchmark complete"| teardown_phase
@@ -120,11 +121,11 @@ flowchart TB
120121

121122
A **Run** is a single end-to-end execution of the benchmark for one system. Each Run proceeds through three phases:
122123

123-
| Phase | What happens | Timed? |
124-
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
125-
| **1. Setup** | Connect to system adapter via JSON-RPC (stdio or HTTP). Call `setup(run_id, datasets)` to provision the SUT then `query_method(run_id)` to get ADBC driver config. | No |
126-
| **2. Benchmark (timed)** | Three sequential stages — warm-up (1× query set), baseline (10% of duration, 60s–600s), and load test (full duration with concurrent clients). | Yes |
127-
| **3. Teardown** | Call `teardown(run_id)` via the adapter to deprovision resources and clean up. | No |
124+
| Phase | What happens | Timed? |
125+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
126+
| **1. Setup** | Connect to system adapter via JSON-RPC (stdio or HTTP). Call `setup(run_id, datasets)` to provision the SUT, then `create_tables(run_id)`, then `query_method(run_id)` to get ADBC driver config. | No |
127+
| **2. Benchmark (timed)** | Three sequential stages — warm-up (1× query set), baseline (10% of duration, 60s–600s), and load test (full duration with concurrent clients). | Yes |
128+
| **3. Teardown** | Call `teardown(run_id)` via the adapter to deprovision resources and clean up. | No |
128129

129130
The **E2E benchmark duration** (phase 2, load test stage) is the primary ranking metric. After the load test, each query's p99 latency is compared against the baseline: >20% increase = FAIL, 10–20% = WARN, ≥3 WARNs = FAIL.
130131

@@ -147,7 +148,7 @@ Common CLI/workflow usage:
147148
| Component | Responsibility |
148149
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
149150
| **GitHub Actions** | Orchestrates Runs on schedule, PR, or manual dispatch. Manages the full Run lifecycle across phases. |
150-
| **System Adapter Protocol** | JSON-RPC 2.0 interface (stdio or HTTP) for each platform. Methods: `setup`, `query_method`, `teardown`, `metrics`. |
151+
| **System Adapter Protocol** | JSON-RPC 2.0 interface (stdio or HTTP) for each platform. Methods: `setup`, `create_tables`, `query_method`, `teardown`, `metrics`. |
151152
| **Query Executors** | Pluggable query execution: ADBC direct (FlightSQL/Databricks drivers), HTTP (`/v1/sql`), or distributed (`/v1/queries` with polling). |
152153
| **Data Generator** | Standalone binary (`data-generation`) that produces TPC-H partitioned Parquet batches and writes them to S3. |
153154
| **Test Framework** | Core engine managing the warm-up → baseline → load test pipeline, query sets (TPC-H, TPC-DS, ClickBench, parameterized, scenario), and statistics collection. |
@@ -216,9 +217,10 @@ Results from every Run are published to [SpiceBench.com](https://spicebench.com)
216217
To benchmark a new platform, implement the JSON-RPC 2.0 adapter with these methods:
217218

218219
1. **`setup(run_id, datasets)`** — Provision infrastructure and configure the target system.
219-
2. **`query_method(run_id)`** — Return the ADBC driver type (`flightsql` or `databricks`) and connection kwargs so SpiceBench can establish a direct query connection.
220-
3. **`teardown(run_id)`** — Clean up provisioned resources.
221-
4. **`metrics(run_id)`** *(optional)* — Return current resource usage (CPU, memory, disk, IOPS) and ingestion progress (rows, bytes, rows/s, active connections).
220+
2. **`create_tables(run_id)`** — Create/register destination tables for the benchmark datasets.
221+
3. **`query_method(run_id)`** — Return the ADBC driver type (`flightsql` or `databricks`) and connection kwargs so SpiceBench can establish a direct query connection.
222+
4. **`teardown(run_id)`** — Clean up provisioned resources.
223+
5. **`metrics(run_id)`** *(optional)* — Return current resource usage (CPU, memory, disk, IOPS) and ingestion progress (rows, bytes, rows/s, active connections).
222224

223225
The adapter can run as a **stdio** child process or as an **HTTP** server.
224226

@@ -237,7 +239,30 @@ The `spicebench` CLI connects to a system adapter using JSON-RPC 2.0 over either
237239
- **stdio transport**: use `--system-adapter-stdio-cmd` (SpiceBench starts the child process).
238240
- **HTTP transport**: use `--system-adapter-http-url` (SpiceBench connects to a remote adapter endpoint).
239241
- **execution mode**: `adapter-command` (default) dispatches `spicebench run ...` to adapter JSON-RPC `run.load`.
240-
- **execution mode**: `direct-query` runs the load/query path directly via ADBC, using the adapter only for setup/teardown/metrics.
242+
- **execution mode**: `direct-query` runs the load/query path directly via ADBC, using the adapter for setup/table creation/teardown/metrics.
243+
244+
#### Adapter lifecycle (direct-query mode)
245+
246+
For each run, SpiceBench calls adapter JSON-RPC methods in this order:
247+
248+
1. `setup(run_id, datasets, metadata)`
249+
2. `create_tables(run_id)`
250+
3. `query_method(run_id)`
251+
4. benchmark execution and optional periodic `metrics(run_id)` scraping
252+
5. `teardown(run_id)`
253+
254+
Tiny `create_tables` request example:
255+
256+
```json
257+
{
258+
"jsonrpc": "2.0",
259+
"id": 2,
260+
"method": "create_tables",
261+
"params": {
262+
"run_id": "00000000-0000-0000-0000-000000000000"
263+
}
264+
}
265+
```
241266

242267
#### Stdio example (child process started by SpiceBench)
243268

@@ -269,7 +294,7 @@ Notes:
269294
- `--system-adapter-stdio-args` passes CLI args to the stdio adapter command.
270295
- `--system-adapter-env` is only valid for stdio transport.
271296

272-
#### Direct-query example (ADBC query path, adapter for setup/teardown only)
297+
#### Direct-query example (ADBC query path, adapter for setup/table creation/teardown)
273298

274299
```bash
275300
spicebench \

crates/data-generation/src/metrics.rs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -84,29 +84,29 @@ impl Metrics {
8484
self.inner.batches_generated.fetch_add(1, Ordering::Relaxed);
8585

8686
// Count inserts, updates, and deletes from the `_op` column.
87-
if let Ok(idx) = batch.schema().index_of("_op") {
88-
if let Some(op_array) = batch.column(idx).as_any().downcast_ref::<StringArray>() {
89-
let mut creates = 0u64;
90-
let mut updates = 0u64;
91-
let mut deletes = 0u64;
92-
for i in 0..op_array.len() {
93-
match op_array.value(i) {
94-
"c" => creates += 1,
95-
"u" => updates += 1,
96-
"d" => deletes += 1,
97-
_ => {}
98-
}
87+
if let Ok(idx) = batch.schema().index_of("_op")
88+
&& let Some(op_array) = batch.column(idx).as_any().downcast_ref::<StringArray>()
89+
{
90+
let mut creates = 0u64;
91+
let mut updates = 0u64;
92+
let mut deletes = 0u64;
93+
for i in 0..op_array.len() {
94+
match op_array.value(i) {
95+
"c" => creates += 1,
96+
"u" => updates += 1,
97+
"d" => deletes += 1,
98+
_ => {}
9999
}
100-
self.inner
101-
.rows_created
102-
.fetch_add(creates, Ordering::Relaxed);
103-
self.inner
104-
.rows_updated
105-
.fetch_add(updates, Ordering::Relaxed);
106-
self.inner
107-
.rows_deleted
108-
.fetch_add(deletes, Ordering::Relaxed);
109100
}
101+
self.inner
102+
.rows_created
103+
.fetch_add(creates, Ordering::Relaxed);
104+
self.inner
105+
.rows_updated
106+
.fetch_add(updates, Ordering::Relaxed);
107+
self.inner
108+
.rows_deleted
109+
.fetch_add(deletes, Ordering::Relaxed);
110110
}
111111
}
112112

crates/etl/src/sink/adbc.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ const DEFAULT_INSERT_ROWS_PER_STATEMENT: usize = 2048;
3434

3535
/// ETL sink that writes transformed batches directly into the SUT via ADBC SQL.
3636
///
37-
/// This sink creates destination tables on first write (`CREATE TABLE IF NOT EXISTS`)
38-
/// and appends rows with batched `INSERT INTO ... VALUES` statements.
37+
/// This sink appends rows with batched `INSERT INTO ... VALUES` statements.
38+
/// Table auto-creation is optional and can be disabled when tables are managed
39+
/// externally (for example by a system adapter RPC method).
3940
pub struct AdbcSink {
4041
conn: Arc<Mutex<AdbcConnection>>,
4142
created_tables: TokioMutex<HashSet<String>>,
4243
schema_name: Option<String>,
4344
insert_rows_per_statement: usize,
45+
auto_create_tables: bool,
4446
}
4547

4648
impl AdbcSink {
@@ -51,6 +53,18 @@ impl AdbcSink {
5153
created_tables: TokioMutex::new(HashSet::new()),
5254
schema_name,
5355
insert_rows_per_statement: DEFAULT_INSERT_ROWS_PER_STATEMENT,
56+
auto_create_tables: true,
57+
}
58+
}
59+
60+
#[must_use]
61+
pub fn new_without_table_creation(conn: AdbcConnection, schema_name: Option<String>) -> Self {
62+
Self {
63+
conn: Arc::new(Mutex::new(conn)),
64+
created_tables: TokioMutex::new(HashSet::new()),
65+
schema_name,
66+
insert_rows_per_statement: DEFAULT_INSERT_ROWS_PER_STATEMENT,
67+
auto_create_tables: false,
5468
}
5569
}
5670

@@ -132,7 +146,7 @@ impl Sink for AdbcSink {
132146
let should_ensure_table = matches!(op, InsertOp::Insert | InsertOp::Update { .. });
133147
let mut newly_created = false;
134148

135-
if should_ensure_table {
149+
if should_ensure_table && self.auto_create_tables {
136150
let created = self.created_tables.lock().await;
137151
if !created.contains(table_name) {
138152
preamble_statements.push(self.create_table_sql(table_name, &batch.schema())?);

crates/system-adapter-protocol/src/client.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,19 @@ impl Client {
194194
.ok_or_else(|| ClientError::InvalidResponse("Missing result".to_string()))
195195
}
196196

197+
/// Create benchmark tables for a benchmark run
198+
pub async fn create_tables(
199+
&mut self,
200+
run_id: uuid::Uuid,
201+
) -> Result<crate::CreateTablesResponse> {
202+
let request = crate::CreateTablesRequest { run_id };
203+
let rpc_request = JsonRpcRequest::new(1, crate::methods::CREATE_TABLES, request);
204+
let response = self.call_typed(rpc_request).await?;
205+
response
206+
.result
207+
.ok_or_else(|| ClientError::InvalidResponse("Missing result".to_string()))
208+
}
209+
197210
/// Get query method/driver information for a benchmark run
198211
pub async fn query_method(&mut self, run_id: uuid::Uuid) -> Result<crate::QueryMethodResponse> {
199212
let request = crate::QueryMethodRequest { run_id };

crates/system-adapter-protocol/src/lib.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ limitations under the License.
2222
//!
2323
//! # Features
2424
//!
25-
//! - **Protocol types**: Request/response types for setup, query_method, and teardown
25+
//! - **Protocol types**: Request/response types for setup, create_tables, query_method, and teardown
2626
//! - **Client**: Ready-to-use client with Stdio and HTTP transports (requires `client` feature)
2727
//! - **Server**: Easy server implementation via Handler trait (requires `server` feature)
2828
//! - **JSON-RPC**: Standard JSON-RPC 2.0 envelope types
@@ -42,6 +42,7 @@ limitations under the License.
4242
//! // Setup a benchmark run
4343
//! let run_id = Uuid::new_v4();
4444
//! let setup_response = client.setup(run_id, HashMap::new(), HashMap::new()).await?;
45+
//! let create_tables_response = client.create_tables(run_id).await?;
4546
//!
4647
//! // Get query method information
4748
//! let query_response = client.query_method(run_id).await?;
@@ -59,8 +60,8 @@ limitations under the License.
5960
//! # #[cfg(feature = "server")]
6061
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
6162
//! use system_adapter_protocol::{
62-
//! Handler, Server, SetupResponse, QueryMethodResponse, TeardownResponse,
63-
//! AdbcDriver, DatasetConfig
63+
//! AdbcDriver, CreateTablesResponse, DatasetConfig, Handler, QueryMethodResponse, Server,
64+
//! SetupResponse, TeardownResponse,
6465
//! };
6566
//! use async_trait::async_trait;
6667
//! use std::collections::HashMap;
@@ -88,6 +89,10 @@ limitations under the License.
8889
//! })
8990
//! }
9091
//!
92+
//! async fn create_tables(&mut self, run_id: Uuid) -> Result<CreateTablesResponse, String> {
93+
//! Ok(CreateTablesResponse { ok: true })
94+
//! }
95+
//!
9196
//! async fn teardown(&mut self, run_id: Uuid) -> Result<TeardownResponse, String> {
9297
//! Ok(TeardownResponse { ok: true })
9398
//! }
@@ -164,6 +169,22 @@ pub struct SetupResponse {
164169
pub ok: bool,
165170
}
166171

172+
/// Request to create benchmark tables in the system under test.
173+
///
174+
/// JSON-RPC method: `create_tables`
175+
#[derive(Debug, Clone, Serialize, Deserialize)]
176+
pub struct CreateTablesRequest {
177+
/// Unique identifier for this benchmark run
178+
pub run_id: Uuid,
179+
}
180+
181+
/// Response from create_tables request
182+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
183+
pub struct CreateTablesResponse {
184+
/// Indicates if table creation was successful
185+
pub ok: bool,
186+
}
187+
167188
/// Request to get query method/driver information
168189
///
169190
/// JSON-RPC method: `query_method`
@@ -355,6 +376,7 @@ pub mod error_codes {
355376
/// Method names for the system adapter protocol
356377
pub mod methods {
357378
pub const SETUP: &str = "setup";
379+
pub const CREATE_TABLES: &str = "create_tables";
358380
pub const QUERY_METHOD: &str = "query_method";
359381
pub const TEARDOWN: &str = "teardown";
360382
pub const METRICS: &str = "metrics";

0 commit comments

Comments
 (0)