Adding New Platform Adapters
This guide provides step-by-step instructions for implementing new platform adapters in BenchBox. Platform adapters enable BenchBox to run benchmarks on different database systems.
Before opening a PR, run through the New Platform Acceptance Checklist. It covers support_status, registry metadata, optional dependencies, SQL compatibility governance, docs, tests, and UAT scope.
Overview
Platform adapters in BenchBox follow a consistent architecture based on the PlatformAdapter base class. Each adapter provides:
- Database connection management
- Schema creation and data loading
- Query execution with dialect translation
- Performance tuning and optimization
- Platform-specific features integration
Platform Manifest Authority
benchbox/core/platform_manifest.py is the only static authority for platform
identity and metadata. Each typed manifest entry owns its canonical key, every
scoped alias and its implied execution mode, support status,
execution/deployment capabilities, and the optional adapter module/class/order
coordinates. The module uses only the Python standard library; reading catalog
metadata must not import an optional SDK or instantiate an adapter.
The remaining platform-shaped maps have narrower runtime ownership:
PlatformRegistryderives metadata, only registry-scoped compatibility aliases, and lazy adapter imports from the manifest, then owns loaded adapter classes and availability state. Its public adapter list preserves the manifest’s explicitregistration_order.- CLI platform normalization derives only CLI-scoped aliases. A
-dfspelling must also declareimplied_mode: dataframe; it is not automatically a core registry alias. - platform hooks, local DataFrame factories, tuning aliases, and file-format
capabilities describe behavior rather than platform identity. They remain
local to their subsystem, but
make platform-manifest-checkrejects keys or aliases that do not resolve through the manifest. benchbox.platformslazy exports are API exposure. They are checked against manifest runtime classes or the DataFrame factory, not treated as another registry.- Every DataFrame factory row is checked for a CLI-scoped, DataFrame-implying spelling, DataFrame-capable canonical target, real lazy adapter export, real lazy availability constant, and nonempty installation guidance.
Do not add a second canonical-key, alias, capability, support-status, or adapter-coordinate registry. In particular, decorator-based discovery was not introduced: it would require importing adapter modules to discover static metadata and would weaken the lazy optional-dependency boundary.
benchbox.platforms.manifest remains a compatibility import path that only
re-exports the core-owned authority. New internal code should import
benchbox.core.platform_manifest directly so the enforced
utils < core < platforms < cli dependency direction remains intact.
The following inventory is generated from the manifest:
| Canonical key | Scoped aliases | Modes | Support | Runtime adapter order and coordinate |
|---|---|---|---|---|
athena | — | SQL | beta | #32 benchbox.platforms.athena:AthenaAdapter |
athena-spark | — | SQL + DataFrame | experimental | #35 benchbox.platforms.aws:AthenaSparkAdapter |
bigquery | bq (cli), gbq (cli) | SQL | beta | #12 benchbox.platforms.bigquery:BigQueryAdapter |
cedardb | — | SQL | experimental | #23 benchbox.platforms.cedardb:CedarDBAdapter |
citus | — | SQL | experimental | #48 benchbox.platforms.citus:CitusAdapter |
clickhouse | — | SQL | deprecated | #6 benchbox.platforms.clickhouse:ClickHouseAdapter |
clickhouse-cloud | — | SQL | beta | #9 benchbox.platforms.clickhouse_cloud:ClickHouseCloudAdapter |
clickhouse-local | ch (cli) | SQL | beta | #7 benchbox.platforms.clickhouse_local:ClickHouseLocalAdapter |
clickhouse-server | — | SQL | beta | #8 benchbox.platforms.clickhouse_server:ClickHouseServerAdapter |
cudf | cudf-df (cli, mode=dataframe) | DataFrame | experimental | DataFrame factory only |
dask | dask-df (cli, mode=dataframe) | DataFrame | beta | DataFrame factory only |
databend | — | SQL | beta | #27 benchbox.platforms.databend:DatabendAdapter |
databricks | dbx (cli) | SQL + DataFrame | beta | #4 benchbox.platforms.databricks:DatabricksAdapter |
databricks-df | — | SQL + DataFrame | experimental | #5 benchbox.platforms.databricks:DatabricksDataFrameAdapter |
datafusion | datafusion-df (cli, mode=dataframe), fusion (cli) | SQL + DataFrame | stable | #3 benchbox.platforms.datafusion:DataFusionAdapter |
dataproc | — | SQL + DataFrame | experimental | #36 benchbox.platforms.gcp:DataprocAdapter |
dataproc-serverless | — | SQL + DataFrame | experimental | #37 benchbox.platforms.gcp:DataprocServerlessAdapter |
doris | — | SQL | beta | #28 benchbox.platforms.doris:DorisAdapter |
duckdb | duck (cli) | SQL | stable | #0 benchbox.platforms.duckdb:DuckDBAdapter |
ducklake | — | SQL | beta | #2 benchbox.platforms.ducklake:DuckLakeAdapter |
emr-serverless | — | SQL + DataFrame | experimental | #34 benchbox.platforms.aws:EMRServerlessAdapter |
fabric-lakehouse | fabric_lakehouse (registry) | SQL | beta | #39 benchbox.platforms.fabric_lakehouse:FabricLakehouseAdapter |
fabric-spark | — | SQL + DataFrame | experimental | #38 benchbox.platforms.azure:FabricSparkAdapter |
fabric_dw | fabric-dw (cli/registry) | SQL | beta | #31 benchbox.platforms.fabric_warehouse:FabricWarehouseAdapter |
firebolt | — | SQL | beta | #26 benchbox.platforms.firebolt:FireboltAdapter |
glue | — | SQL + DataFrame | experimental | #33 benchbox.platforms.aws:AWSGlueAdapter |
influxdb | — | SQL | beta | #30 benchbox.platforms.influxdb:InfluxDBAdapter |
lakesail | lakesail-df (cli, mode=dataframe) | SQL + DataFrame | experimental | #42 benchbox.platforms.lakesail:LakeSailAdapter |
motherduck | — | SQL | beta | #1 benchbox.platforms.motherduck:MotherDuckAdapter |
pandas | pandas-df (cli, mode=dataframe) | DataFrame | stable | DataFrame factory only |
paradedb | — | SQL | experimental | #47 benchbox.platforms.paradedb:ParadeDBAdapter |
pg-duckdb | pg_duckdb (registry) | SQL | experimental | #20 benchbox.platforms.pg_duckdb:PgDuckDBAdapter |
pg-mooncake | — | SQL | experimental | #21 benchbox.platforms.pg_mooncake:PgMooncakeAdapter |
polars | polars-df (cli, mode=dataframe) | DataFrame | stable | #44 benchbox.platforms.polars_platform:PolarsAdapter |
postgresql | pg (cli), pgsql (cli), postgres (cli) | SQL | beta | #18 benchbox.platforms.postgresql:PostgreSQLAdapter |
presto | prestodb (cli) | SQL | beta | #17 benchbox.platforms.presto:PrestoAdapter |
pyspark | pyspark-df (cli, mode=dataframe) | SQL + DataFrame | beta | #25 benchbox.platforms.pyspark:PySparkSQLAdapter |
quanton | — | SQL + DataFrame | experimental | #46 benchbox.platforms.onehouse:QuantonAdapter |
questdb | — | SQL | beta | #22 benchbox.platforms.questdb:QuestDBAdapter |
redshift | rs (cli) | SQL | beta | #13 benchbox.platforms.redshift:RedshiftAdapter |
singlestore | — | SQL | beta | #29 benchbox.platforms.singlestore:SingleStoreAdapter |
snowflake | snow (cli) | SQL | beta | #14 benchbox.platforms.snowflake:SnowflakeAdapter |
snowpark-connect | — | SQL + DataFrame | experimental | #45 benchbox.platforms.snowpark_connect:SnowparkConnectAdapter |
spark | — | SQL | beta | #41 benchbox.platforms.spark:SparkAdapter |
sqlite | sqlite3 (registry) | SQL | stable | #11 benchbox.platforms.sqlite:SQLiteAdapter |
starburst | — | SQL | beta | #16 benchbox.platforms.starburst:StarburstAdapter |
starrocks | — | SQL | beta | #10 benchbox.platforms.starrocks:StarRocksAdapter |
synapse | azure-synapse (cli), azure_synapse (registry), azuresynapse (cli) | SQL | beta | #24 benchbox.platforms.azure_synapse:AzureSynapseAdapter |
synapse-spark | — | SQL + DataFrame | experimental | #40 benchbox.platforms.azure:SynapseSparkAdapter |
timescaledb | — | SQL | beta | #19 benchbox.platforms.timescaledb:TimescaleDBAdapter |
trino | trinodb (cli) | SQL | beta | #15 benchbox.platforms.trino:TrinoAdapter |
velox | — | SQL | experimental | #43 benchbox.platforms.velox:VeloxAdapter |
Prerequisites
Before implementing a new platform adapter, ensure you have:
- Understanding of the target database platform
- Access to the platform for testing
- Python client library implementing DB API 2.0 specification (see DB API 2.0 documentation)
- Familiarity with BenchBox architecture
- Knowledge of SQL dialect differences
DB API 2.0 Requirement
BenchBox platform selection is based on the availability of a robust Python library using DB API 2.0 (PEP 249). This standardized interface enables universal database access and simplifies platform integration. See the DB API 2.0 documentation for detailed information on:
- Why DB API 2.0 is fundamental to BenchBox
- How BenchBox uses DB API 2.0 protocols and patterns
- Platform compliance levels and requirements
- Testing and validation procedures
Architecture Overview
Base Class Structure
All platform adapters inherit from PlatformAdapter in benchbox/platforms/base/:
from benchbox.platforms.base import PlatformAdapter
class NewDatabaseAdapter(PlatformAdapter):
@property def platform_name(self) -> str: return "NewDatabase"
def get_target_dialect(self) -> str: return "newdatabase"get_target_dialect() returns the SQL dialect identifier. This snippet is abridged: a real adapter implements all required abstract methods.
Lifecycle and Run-Scoped State
BenchBox treats PlatformAdapter instances as serial execution objects. Reusing
one adapter instance for multiple benchmark runs is supported only
sequentially; concurrent run_benchmark() calls on the same adapter instance
are not supported.
The base adapter resets its run-scoped caches at the start of each run,
including database-reuse decisions, cached power/throughput phase results,
sorted-ingestion metadata, plan-capture counters, and captured SQL buffers when
explicit dry-run mode is active. Run-config plan-capture overrides
(capture_plans, strict_plan_capture, and plan_capture_timeout_seconds)
are restored after the run.
Adapter authors should keep connection handles, phase-result caches, plan-capture counters, skipped-query tracking, and dry-run SQL capture out of class-level state. Store them on the adapter only when they are truly adapter-scoped and can be reset or overwritten before each run. If an adapter needs concurrent execution, design a per-run context first and update the public contract map before claiming support.
Required Dependencies
Add platform dependencies to pyproject.toml via the [project.optional-dependencies] table:
[project.optional-dependencies]newdatabase = ["newdatabase-python>=2.0.0"]all = ["newdatabase-python>=2.0.0", "benchbox[cloud]"]This allows contributors to install the adapter with uv pip install "benchbox[newdatabase]".
Step-by-Step Implementation
Step 1: Create Adapter File
Create a new file: benchbox/platforms/newdatabase.py
import timeimport loggingfrom pathlib import Pathfrom typing import Any, Dict, Tuple, Optional
from .base import DriverIsolationCapability, PlatformAdapter, ConnectionConfig
try: import newdatabase from newdatabase import Connection from newdatabase.errors import DatabaseErrorexcept ImportError: newdatabase = None Connection = None DatabaseError = Exception
logger = logging.getLogger(__name__)
class NewDatabaseAdapter(PlatformAdapter):
driver_isolation_capability = DriverIsolationCapability.FEASIBLE_CLIENT_ONLY
def __init__(self, **config): super().__init__(**config) if newdatabase is None: raise ImportError("NewDatabase client not installed. Install with: uv add newdatabase-python")
self.host = config.get('host', 'localhost') self.port = config.get('port', 5432) self.database = config.get('database', 'benchbox') self.username = config.get('username', 'user') self.password = config.get('password', '')
self.connection_pool_size = config.get('connection_pool_size', 5) self.query_timeout = config.get('query_timeout', 300)
@property def platform_name(self) -> str: return "NewDatabase"
def get_target_dialect(self) -> str: return "newdatabase"Declaring driver_isolation_capability is required, and CI tests validate it. Choose one of:
SUPPORTED: full driver isolation (DuckDB, DataFusion).FEASIBLE_CLIENT_ONLY: the client can be isolated, but the engine version is external.NOT_FEASIBLE: technical constraints prevent isolation (JVM, C libraries).NOT_APPLICABLE: no versioned driver package (for example SQLite and DataFrames).
The remaining required methods are implemented in the following steps.
Step 2: Implement Connection Management
Important: Ensure the returned connection object is DB API 2.0 compliant. It must support either:
- Standard cursor pattern:
connection.cursor()returning a cursor withexecute()method - Direct execute pattern:
connection.execute()method available directly
See DB API 2.0 documentation for details on both patterns.
The example below creates the connection, checks existing databases, and tests the connection with the standard cursor pattern. check_server_database_exists uses an admin connection that does not specify a database.
def create_connection(self, **connection_config) -> Any: self.handle_existing_database(**connection_config)
host = connection_config.get('host', self.host) port = connection_config.get('port', self.port) database = connection_config.get('database', self.database) username = connection_config.get('username', self.username) password = connection_config.get('password', self.password)
try: connection = newdatabase.connect( host=host, port=port, database=database, username=username, password=password, timeout=self.query_timeout )
cursor = connection.cursor() cursor.execute("SELECT 1") cursor.fetchall() cursor.close()
logger.info(f"Connected to NewDatabase at {host}:{port}") return connection
except Exception as e: logger.error(f"Failed to connect to NewDatabase: {e}") raise
def check_server_database_exists(self, **connection_config) -> bool: try: admin_connection = newdatabase.connect( host=connection_config.get('host', self.host), port=connection_config.get('port', self.port), username=connection_config.get('username', self.username), password=connection_config.get('password', self.password) )
database = connection_config.get('database', self.database)
cursor = admin_connection.cursor() cursor.execute("SHOW DATABASES") databases = [row[0] for row in cursor.fetchall()]
return database in databases
except Exception: return False finally: if 'admin_connection' in locals() and admin_connection: admin_connection.close()
def drop_database(self, **connection_config) -> None: try: admin_connection = newdatabase.connect( host=connection_config.get('host', self.host), port=connection_config.get('port', self.port), username=connection_config.get('username', self.username), password=connection_config.get('password', self.password) )
database = connection_config.get('database', self.database)
cursor = admin_connection.cursor() cursor.execute(f"DROP DATABASE IF EXISTS {database}") cursor.close()
except Exception as e: raise RuntimeError(f"Failed to drop NewDatabase database: {e}") finally: if 'admin_connection' in locals() and admin_connection: admin_connection.close()Step 3: Implement Schema Creation
def create_schema( self, benchmark, connection: Any, enable_primary_keys: bool = True, enable_foreign_keys: bool = True) -> float: start_time = time.time()
try: schema_sql = benchmark.get_create_tables_sql( enable_primary_keys=enable_primary_keys, enable_foreign_keys=enable_foreign_keys )
if hasattr(self, 'translate_sql'): schema_sql = self.translate_sql(schema_sql, "duckdb")
statements = [stmt.strip() for stmt in schema_sql.split(';') if stmt.strip()]
cursor = connection.cursor() for statement in statements: statement = self._optimize_table_definition(statement) cursor.execute(statement) logger.debug(f"Executed schema statement: {statement[:100]}...")
cursor.close() connection.commit()
logger.info(f"Schema created (PKs: {enable_primary_keys}, FKs: {enable_foreign_keys})")
except Exception as e: logger.error(f"Schema creation failed: {e}") raise
return time.time() - start_time
def _optimize_table_definition(self, statement: str) -> str: if not statement.upper().startswith('CREATE TABLE'): return statement
if 'ENGINE' not in statement.upper(): statement += " ENGINE=InnoDB"
return statementThe translate_sql(schema_sql, "duckdb") call translates from the DuckDB dialect. _optimize_table_definition is an example hook for adding a storage engine or other platform-specific options. The ENGINE=InnoDB option shown is an example for MySQL-like databases.
Step 4: Implement Data Loading
def load_data(self, benchmark, connection: Any, data_dir: Path) -> Tuple[Dict[str, int], float]: start_time = time.time() table_stats = {}
if hasattr(benchmark, 'tables') and benchmark.tables: data_files = benchmark.tables else: raise ValueError("No data files found. Ensure benchmark.generate_data() was called first.")
cursor = connection.cursor()
for table_name, file_path in data_files.items(): file_path = Path(file_path) if not file_path.exists() or file_path.stat().st_size == 0: logger.warning(f"Skipping {table_name} - no data file or empty file") table_stats[table_name] = 0 continue
try: load_start = time.time() table_name_upper = table_name.upper()
if self._supports_bulk_copy(): copy_command = self._build_copy_command(table_name_upper, file_path) cursor.execute(copy_command) else: self._load_via_inserts(cursor, table_name_upper, file_path)
cursor.execute(f"SELECT COUNT(*) FROM {table_name_upper}") row_count = cursor.fetchone()[0] table_stats[table_name_upper] = row_count
load_time = time.time() - load_start logger.info(f"✅ Loaded {row_count:,} rows into {table_name_upper} in {load_time:.2f}s")
except Exception as e: logger.error(f"Failed to load {table_name}: {str(e)[:100]}...") table_stats[table_name.upper()] = 0
cursor.close() connection.commit()
total_time = time.time() - start_time total_rows = sum(table_stats.values()) logger.info(f"✅ Loaded {total_rows:,} total rows in {total_time:.2f}s")
return table_stats, total_time
def _supports_bulk_copy(self) -> bool: return True
def _build_copy_command(self, table_name: str, file_path: Path) -> str: delimiter = '|' if file_path.suffix == '.tbl' else ',' return f""" COPY {table_name} FROM '{file_path}' WITH (FORMAT CSV, DELIMITER '{delimiter}', HEADER FALSE) """
def _load_via_inserts(self, cursor, table_name: str, file_path: Path): delimiter = '|' if file_path.suffix == '.tbl' else ','
with open(file_path, 'r') as f: batch = [] batch_size = 1000
for line in f: line = line.strip() if line.endswith(delimiter): line = line[:-1]
values = line.split(delimiter) placeholders = ','.join(['?' for _ in values])
if not batch: insert_sql = f"INSERT INTO {table_name} VALUES ({placeholders})"
batch.append(tuple(values))
if len(batch) >= batch_size: cursor.executemany(insert_sql, batch) batch = []
if batch: cursor.executemany(insert_sql, batch)load_data uses the platform’s bulk loading method when _supports_bulk_copy() is true (a COPY command or equivalent), and falls back to INSERT statements otherwise. The return True in _supports_bulk_copy is a placeholder: implement it based on platform capabilities.
Step 5: Implement Query Execution
Important: Use standard DB API 2.0 cursor methods for query execution. BenchBox’s DatabaseConnection wrapper handles both cursor patterns automatically, but adapter implementations should follow DB API 2.0 conventions.
execute_query Variant Families (2026-02)
execute_query is not one universal flow across SQL platforms. Use the
family model below before deciding to consolidate logic.
| Family | Representative adapters | Core flow | Consolidation status |
|---|---|---|---|
| DBAPI cursor + validation helper | firebolt, presto, trino | cursor.execute() -> fetchall() -> optional QueryValidator row-count check -> standardized result payload | Consolidated in CursorValidationQueryExecutionMixin (benchbox/core/benchmark_mixins.py) |
| Spark SQL collect | spark, lakesail | spark.sql(query) -> .collect() -> optional row-count validation -> standardized result payload | Similar enough for shared helpers; keep session/config differences adapter-local |
| Cloud Spark submit/wait/fetch job | emr_serverless, dataproc, dataproc_serverless, athena_spark, glue, quanton | Submit remote job/batch -> poll async state -> fetch JSON results from object storage or API | Intentionally distinct from DBAPI cursor flow; async control plane and artifact retrieval dominate behavior |
| Livy statement API | synapse_spark, fabric_spark | Submit statement to Livy session -> poll statement state -> parse data.values payload | Keep as a separate family; may share Livy polling/parsing utilities |
Engine-specific outliers should remain separate unless their full contracts are aligned:
bigquery: query jobs with bytes billed/slot metrics and table qualification.datafusion:SessionContext.sql().collect()plus SQL compatibility rewrite pipeline.databend: explicitUSE <database>andquery_iterresult streaming.motherduck: legacy tuple-return API over DuckDB (rows,execution_time).influxdb: time-series-specific tuple return (execution_time,row_count,query_plan).
Rule of thumb:
- Consolidate only within the same family and same output contract.
- Keep different transport/protocol families distinct, even if control flow looks superficially similar.
def execute_query(self, connection: Any, query: str, query_id: str) -> Dict[str, Any]: start_time = time.time()
try: cursor = connection.cursor()
self._apply_query_optimizations(cursor)
cursor.execute(query)
results = cursor.fetchall()
execution_time = time.time() - start_time
query_metrics = self._get_query_metrics(cursor)
cursor.close()
return { 'query_id': query_id, 'status': 'SUCCESS', 'execution_time': execution_time, 'rows_returned': len(results), 'first_row': results[0] if results else None, 'platform_metrics': query_metrics }
except Exception as e: execution_time = time.time() - start_time
return { 'query_id': query_id, 'status': 'FAILED', 'execution_time': execution_time, 'rows_returned': 0, 'error': str(e), 'error_type': type(e).__name__ }
def _apply_query_optimizations(self, cursor): cursor.execute("SET query_cache = ON") cursor.execute("SET optimizer_mode = 'performance'")
def _get_query_metrics(self, cursor) -> Dict[str, Any]: try: cursor.execute("SHOW QUERY STATS") stats = cursor.fetchall() return {'query_stats': stats} except: return {}
def configure_for_benchmark(self, connection: Any, benchmark_type: str) -> None: cursor = connection.cursor()
if benchmark_type.lower() in ['olap', 'analytics', 'tpch', 'tpcds']: cursor.execute("SET join_algorithm = 'hash'") cursor.execute("SET parallel_workers = 8") cursor.execute("SET work_mem = '256MB'") elif benchmark_type.lower() in ['oltp', 'transactional']: cursor.execute("SET synchronous_commit = ON") cursor.execute("SET random_page_cost = 1.1")
cursor.close()The first branch applies OLAP optimizations. The second applies OLTP optimizations.
Step 6: Implement Platform Metadata
def _get_platform_metadata(self, connection: Any) -> Dict[str, Any]: metadata = { "platform": self.platform_name, "host": self.host, "port": self.port, "database": self.database }
try: cursor = connection.cursor()
cursor.execute("SELECT VERSION()") version_result = cursor.fetchone() metadata["version"] = version_result[0] if version_result else "unknown"
cursor.execute("SHOW VARIABLES LIKE 'max_connections'") settings = cursor.fetchall() metadata["settings"] = {name: value for name, value in settings}
cursor.execute(""" SELECT table_name, table_rows, data_length, index_length FROM information_schema.tables WHERE table_schema = DATABASE() """)
tables = cursor.fetchall() metadata["tables"] = [ { "name": table[0], "rows": table[1], "data_size": table[2], "index_size": table[3] } for table in tables ]
cursor.close()
except Exception as e: metadata["metadata_error"] = str(e)
return metadataStep 7: Implement Performance Tuning (Optional)
In the example, SORTING is supported through indexes, CLUSTERING through clustered indexes, and PARTITIONING through table partitioning.
def supports_tuning_type(self, tuning_type) -> bool: try: from benchbox.core.tuning.interface import TuningType supported_types = { TuningType.SORTING, TuningType.CLUSTERING, TuningType.PARTITIONING } return tuning_type in supported_types except ImportError: return False
def generate_tuning_clause(self, table_tuning) -> str: if not table_tuning or not table_tuning.has_any_tuning(): return ""
clauses = []
try: from benchbox.core.tuning.interface import TuningType
partition_columns = table_tuning.get_columns_by_type(TuningType.PARTITIONING) if partition_columns: sorted_cols = sorted(partition_columns, key=lambda col: col.order) partition_col = sorted_cols[0].name clauses.append(f"PARTITION BY HASH({partition_col})")
cluster_columns = table_tuning.get_columns_by_type(TuningType.CLUSTERING) if cluster_columns: sorted_cols = sorted(cluster_columns, key=lambda col: col.order) column_names = [col.name for col in sorted_cols] clauses.append(f"CLUSTERED INDEX ({', '.join(column_names)})")
except ImportError: pass
return " ".join(clauses)
def apply_table_tunings(self, table_tuning, connection: Any) -> None: if not table_tuning or not table_tuning.has_any_tuning(): return
table_name = table_tuning.table_name cursor = connection.cursor()
try: from benchbox.core.tuning.interface import TuningType
sort_columns = table_tuning.get_columns_by_type(TuningType.SORTING) if sort_columns: sorted_cols = sorted(sort_columns, key=lambda col: col.order) column_names = [col.name for col in sorted_cols]
index_name = f"idx_{table_name.lower()}_sort" index_sql = f"CREATE INDEX {index_name} ON {table_name} ({', '.join(column_names)})"
cursor.execute(index_sql) logger.info(f"Created sort index on {table_name}: {', '.join(column_names)}")
cursor.close() connection.commit()
except Exception as e: logger.error(f"Failed to apply tunings to {table_name}: {e}") raiseStep 8: Add Power/Throughput Tests (Optional)
def run_power_test(self, benchmark, **kwargs) -> Dict[str, Any]: return self.run_benchmark(benchmark, **kwargs).__dict__
def run_throughput_test(self, benchmark, **kwargs) -> Dict[str, Any]: return self.run_power_test(benchmark, **kwargs)
def run_maintenance_test(self, benchmark, **kwargs) -> Dict[str, Any]: return {"status": "NOT_IMPLEMENTED", "message": "Maintenance test not implemented"}The throughput test runs as a single stream for now. Extend it for true multi-stream execution later.
Integration Steps
Step 1: Register the Adapter
Add one typed entry to the manifest source of truth, benchbox/core/platform_manifest.py. The entry below is abridged and JSON-shaped. A real entry also carries display metadata, libraries, requirements, and adoption fields. Set registration_order to the next contiguous value, and do not reorder existing adapters:
{ "key": "newdatabase", "aliases": [ {"name": "newdb", "scopes": ["cli"]}, ], "adapter": { "module": "benchbox.platforms.newdatabase", "class_name": "NewDatabaseAdapter", "registration_order": 47, }, "support_status": "experimental", "capabilities": { "supports_sql": True, "supports_dataframe": False, "default_mode": "sql", },}Do not add parallel entries to PlatformRegistry or the CLI. They derive their
metadata, scoped aliases, and runtime import rows from the manifest. Use
registry scope only for core API compatibility spellings; use cli for CLI
shorthands. If an alias implies DataFrame execution, declare
"implied_mode": "dataframe". Preserve existing registration order and assign
the next contiguous integer to a new runtime adapter. support_status is a
product support classification, not local dependency availability.
If the adapter should be importable from benchbox.platforms, add a lazy entry and __all__ export in benchbox/platforms/__init__.py; keep optional SDK imports guarded so import benchbox does not load the platform dependency.
If the platform has subsystem behavior, add only the relevant local entries: platform option specs/config builders, DataFrame factory routing, tuning capabilities, or format preferences. Those maps do not define platform identity and their keys must resolve through the manifest drift gate.
Before implementation, maintainers can print the expected file plan with the
platform scaffold script (--kind sql).
Declare compute and gateway support with the compute and gateway items of the SQL platform checklist.
Regenerate the inventory and run the drift gate after editing the manifest:
make platform-manifestmake platform-manifest-checkDeclare execution engines when the platform offers more than one way to execute a query. Add a manifest
capabilities.execution_engines block where each value carries class, display_name, description,
dependencies and selectable. The class comes from the closed core list (adaptive, in-memory, streaming,
gpu, native-vectorized, standard, distributed, jit, delegated); the vocabulary parity test rejects
unknown classes and enforces that one engine name maps to one class on every platform. Name each value with
the platform’s own documented term when one exists, otherwise the class name. default is a reserved sentinel
and is never declared. The worked example is polars-df:
{ "key": "polars", "capabilities": { "execution_engines": { "auto": { "class": "adaptive", "display_name": "Auto", "description": "Polars chooses the engine for each query", "dependencies": ["polars"], "selectable": True, }, "in-memory": { "class": "in-memory", "display_name": "In-memory", "description": "Materializes each query in memory", "dependencies": ["polars"], "selectable": True, }, "streaming": { "class": "streaming", "display_name": "Streaming", "description": "Processes each query in batches to bound memory", "dependencies": ["polars"], "selectable": True, }, }, },}Adapters inherit resolve_execution_engine from the adapter base and map the resolved engine into their
query path; unsupported values fail closed. Never name a canonical option engine, mode, backend,
runtime, executor or warehouse; a reserved-word spelling survives only as a deprecated alias registered
in the core DEPRECATED_OPTION_ALIASES table with its removal release.
Step 2: Add SQL Dialect Support
If your platform has a unique SQL dialect, add SQL compatibility rules or an explicit exemption under the phase-aware SQL compatibility system. Do not hide CREATE TABLE rewrites inside the adapter without registering or exempting them in the DDL governance inventory. Put the rules in benchbox/sql_compat/rules/ddl_optimize/newdatabase_ddl_rewrites.py:
from benchbox.sql_compat.rules._registration import register_ddl_rewrite
register_ddl_rewrite( platform="newdatabase", rule_name="convert_to_newdatabase_table", transformer_id="newdatabase_convert_to_newdatabase_table", description="What the adapter rewrites in CREATE TABLE for this platform.", reason="Why this platform needs the rewrite.",)Step 3: Create Tests
Create comprehensive tests in tests/unit/platforms/test_newdatabase_adapter.py. The TestNewDatabaseIntegration class at the end needs an actual NewDatabase instance and is skipped when the client library is not installed:
import pytestfrom unittest.mock import Mock, patch, MagicMockfrom benchbox.platforms.newdatabase import NewDatabaseAdapter
class TestNewDatabaseAdapter:
@pytest.fixture def adapter(self): return NewDatabaseAdapter( host='localhost', port=5432, database='test', username='test', password='test' )
def test_platform_name(self, adapter): assert adapter.platform_name == "NewDatabase"
def test_target_dialect(self, adapter): assert adapter.get_target_dialect() == "newdatabase"
@patch('newdatabase.connect') def test_create_connection_success(self, mock_connect, adapter): mock_connection = Mock() mock_cursor = Mock() mock_connection.cursor.return_value = mock_cursor mock_connect.return_value = mock_connection
connection = adapter.create_connection()
assert connection is mock_connection mock_connect.assert_called_once() mock_cursor.execute.assert_called_with("SELECT 1")
@patch('newdatabase.connect') def test_create_connection_failure(self, mock_connect, adapter): mock_connect.side_effect = Exception("Connection failed")
with pytest.raises(Exception, match="Connection failed"): adapter.create_connection()
def test_schema_creation(self, adapter): mock_benchmark = Mock() mock_benchmark.get_create_tables_sql.return_value = """ CREATE TABLE test_table (id INTEGER, name VARCHAR(50)); CREATE TABLE test_table2 (id INTEGER, value DECIMAL(10,2)); """
mock_connection = Mock() mock_cursor = Mock() mock_connection.cursor.return_value = mock_cursor
duration = adapter.create_schema(mock_benchmark, mock_connection)
assert duration > 0 assert mock_cursor.execute.call_count == 2 mock_connection.commit.assert_called_once()
def test_data_loading(self, adapter, tmp_path): test_data_file = tmp_path / "test_table.tbl" test_data_file.write_text("1|John Doe\n2|Jane Smith\n")
mock_benchmark = Mock() mock_benchmark.tables = {"test_table": str(test_data_file)}
mock_connection = Mock() mock_cursor = Mock() mock_cursor.fetchone.return_value = (2,) mock_connection.cursor.return_value = mock_cursor
with patch.object(adapter, '_supports_bulk_copy', return_value=True): with patch.object(adapter, '_build_copy_command', return_value="COPY test_table FROM 'file'"): table_stats, load_time = adapter.load_data(mock_benchmark, mock_connection, tmp_path)
assert table_stats["TEST_TABLE"] == 2 assert load_time > 0
def test_query_execution_success(self, adapter): mock_connection = Mock() mock_cursor = Mock() mock_cursor.fetchall.return_value = [(1, 'test'), (2, 'data')] mock_connection.cursor.return_value = mock_cursor
result = adapter.execute_query(mock_connection, "SELECT * FROM test", "Q1")
assert result['status'] == 'SUCCESS' assert result['query_id'] == 'Q1' assert result['rows_returned'] == 2 assert result['execution_time'] > 0
def test_query_execution_failure(self, adapter): mock_connection = Mock() mock_cursor = Mock() mock_cursor.execute.side_effect = Exception("SQL error") mock_connection.cursor.return_value = mock_cursor
result = adapter.execute_query(mock_connection, "INVALID SQL", "Q1")
assert result['status'] == 'FAILED' assert result['query_id'] == 'Q1' assert result['rows_returned'] == 0 assert 'SQL error' in result['error']
def test_platform_metadata(self, adapter): mock_connection = Mock() mock_cursor = Mock() mock_cursor.fetchone.return_value = ("NewDatabase 1.0",) mock_cursor.fetchall.return_value = [("max_connections", "100")] mock_connection.cursor.return_value = mock_cursor
metadata = adapter._get_platform_metadata(mock_connection)
assert metadata['platform'] == 'NewDatabase' assert metadata['version'] == 'NewDatabase 1.0' assert 'settings' in metadata
class TestNewDatabaseIntegration:
@pytest.fixture def integration_adapter(self): pytest.importorskip("newdatabase")
return NewDatabaseAdapter( host=os.getenv('NEWDATABASE_HOST', 'localhost'), port=int(os.getenv('NEWDATABASE_PORT', '5432')), database=os.getenv('NEWDATABASE_DATABASE', 'test'), username=os.getenv('NEWDATABASE_USERNAME', 'test'), password=os.getenv('NEWDATABASE_PASSWORD', 'test') )
@pytest.mark.integration def test_end_to_end_benchmark(self, integration_adapter): from benchbox import ReadPrimitives
benchmark = ReadPrimitives(scale_factor=0.001) results = integration_adapter.run_benchmark(benchmark)
assert results.status == 'SUCCESS' assert results.total_time > 0 assert len(results.query_results) > 0Step 4: Add Documentation
Create platform documentation following the existing pattern in docs/platforms/newdatabase.md.
Step 5: Update Configuration
Add the new platform to the example configuration files:
platforms: newdatabase: host: "localhost" port: 5432 database: "benchbox" username: "benchbox" password: "secure_password"Testing Your Adapter
Unit Tests
Run unit tests to verify basic functionality:
uv run -- python -m pytest tests/unit/platforms/test_newdatabase_adapter.py -v
uv run -- python -m pytest tests/unit/platforms/ -vIntegration Tests
Test with actual database instance:
export NEWDATABASE_HOST=localhostexport NEWDATABASE_PORT=5432export NEWDATABASE_DATABASE=testexport NEWDATABASE_USERNAME=testexport NEWDATABASE_PASSWORD=test
uv run -- python -m pytest tests/integration/ -k newdatabase -vBenchmark Tests
Test with small benchmark:
from benchbox import ReadPrimitivesfrom benchbox.platforms.newdatabase import NewDatabaseAdapter
benchmark = ReadPrimitives(scale_factor=0.001)adapter = NewDatabaseAdapter( host="localhost", database="test", username="test", password="test")
results = adapter.run_benchmark(benchmark)print(f"Benchmark completed in {results.total_time:.2f}s")Best Practices
Error Handling
- Implement comprehensive error handling for connection failures
- Provide clear error messages with troubleshooting hints
- Handle platform-specific exceptions gracefully
- Log errors at appropriate levels
Performance Optimization
- Use platform-native bulk loading methods when available
- Implement connection pooling for concurrent workloads
- Apply platform-specific query optimizations
- Monitor and report platform-specific metrics
Configuration Management
- Support environment variable configuration
- Provide sensible defaults for all parameters
- Validate configuration parameters early
- Document all configuration options
Testing Strategy
- Write comprehensive unit tests with mocking
- Create integration tests for real database connections
- Test error conditions and edge cases
- Validate performance characteristics
Documentation
- Document all configuration parameters
- Provide installation and setup instructions
- Include troubleshooting guides
- Add usage examples and best practices
Common Patterns
Connection String Parsing
The example parses connection strings of the form newdatabase://user:pass@host:port/database:
def _parse_connection_string(self, conn_str: str) -> Dict[str, str]: import re
pattern = r"newdatabase://(?:([^:]*):([^@]*)@)?([^:]*):(\d+)/(.+)" match = re.match(pattern, conn_str)
if not match: raise ValueError(f"Invalid connection string: {conn_str}")
return { 'username': match.group(1) or '', 'password': match.group(2) or '', 'host': match.group(3), 'port': int(match.group(4)), 'database': match.group(5) }Retry Logic
Retry transient failures with exponential backoff (the wait doubles after each attempt):
def _execute_with_retry(self, cursor, query: str, max_retries: int = 3): import time
for attempt in range(max_retries): try: return cursor.execute(query) except TransientError as e: if attempt == max_retries - 1: raise
wait_time = 2 ** attempt logger.warning(f"Query failed (attempt {attempt + 1}), retrying in {wait_time}s: {e}") time.sleep(wait_time)Resource Management
from contextlib import contextmanager
@contextmanagerdef managed_connection(self, **config): connection = None try: connection = self.create_connection(**config) yield connection finally: if connection: connection.close()
@contextmanagerdef managed_cursor(self, connection): cursor = None try: cursor = connection.cursor() yield cursor finally: if cursor: cursor.close()Advanced Features
Connection Pooling
class PooledNewDatabaseAdapter(NewDatabaseAdapter):
def __init__(self, **config): super().__init__(**config) self._connection_pool = None self._pool_size = config.get('pool_size', 10)
def _get_connection_pool(self): if self._connection_pool is None: from newdatabase.pool import ConnectionPool
self._connection_pool = ConnectionPool( host=self.host, port=self.port, database=self.database, username=self.username, password=self.password, max_connections=self._pool_size )
return self._connection_pool
def create_connection(self, **connection_config) -> Any: pool = self._get_connection_pool() return pool.get_connection()Async Support
import asynciofrom typing import AsyncGenerator
class AsyncNewDatabaseAdapter(NewDatabaseAdapter):
async def create_async_connection(self, **config): import newdatabase.asyncio as async_newdb
return await async_newdb.connect( host=self.host, port=self.port, database=self.database, username=self.username, password=self.password )
async def execute_query_async(self, connection, query: str, query_id: str): start_time = time.time()
try: cursor = await connection.cursor() await cursor.execute(query) results = await cursor.fetchall()
return { 'query_id': query_id, 'status': 'SUCCESS', 'execution_time': time.time() - start_time, 'rows_returned': len(results), 'first_row': results[0] if results else None }
except Exception as e: return { 'query_id': query_id, 'status': 'FAILED', 'execution_time': time.time() - start_time, 'error': str(e) }Troubleshooting
Common Issues
- Import Errors: Ensure client library is installed and accessible
- Connection Failures: Check network connectivity, credentials, and firewall settings
- SQL Compatibility: Verify dialect translation is working correctly
- Performance Issues: Check for proper indexing and query optimization
- Memory Usage: Monitor memory consumption during large data loads
Debugging Tips
- Enable debug logging to trace execution flow
- Use database-specific profiling tools
- Test with small datasets first
- Validate schema creation independently
- Check platform-specific documentation for optimization hints
Contributing Your Adapter
When your adapter is complete and tested:
- Create a pull request with your implementation
- Include comprehensive tests and documentation
- Add the platform to the CI/CD pipeline
- Update the README with the new platform support
- Consider adding examples and tutorials
Your contribution helps make BenchBox more comprehensive and valuable for the analytics community!
Resources
- Base Platform Adapter:
benchbox/platforms/base/package - Existing Adapters:
benchbox/platforms/directory - SQL Compatibility:
benchbox/sql_compat/anddocs/compat/ - Test Examples:
tests/unit/platforms/ - Documentation Examples:
docs/platforms/ - Acceptance Checklist: new-platform-acceptance-checklist.md
- BenchBox Architecture: architecture.md