Skip to content

Base Benchmark API

The benchbox.base module provides the foundational abstract class that all benchmarks inherit from.

Overview

Every benchmark in BenchBox extends BaseBenchmark, which provides a standardized interface for:

  • Data generation and schema setup
  • Query execution and timing
  • Platform adapter integration
  • SQL dialect translation
  • Results collection and formatting

This abstraction ensures consistent behavior across all benchmark implementations (TPC-H, TPC-DS, ClickBench, etc.).

Runtime Contract Notes

The lifecycle runner expects loader-resolved benchmarks to provide a shared runtime contract:

  • generate_data
  • get_queries / get_query
  • create_enhanced_benchmark_result
  • create_minimal_benchmark_result
  • validate_preflight / validate_manifest / validate_loaded_data

BaseBenchmark defines the first three as abstract methods and gets the rest from a mixin, so a subclass of benchbox.base.BaseBenchmark provides all of them once it implements generate_data, get_queries and get_query.

Compatibility boundaries:

  • Public benchmark implementations should inherit benchbox.base.BaseBenchmark.
  • benchbox.core.base_benchmark.BaseBenchmark is a separate internal class, not a subclass of the public one. It remains temporarily for internal compatibility.
  • create_enhanced_benchmark_result() accepts the legacy keyword arguments table_statistics (a mapping of table name to row count) and data_loading_time (seconds).

Quick Example

from benchbox.tpch import TPCH
from benchbox.platforms import DuckDBAdapter
benchmark = TPCH(scale_factor=0.01, output_dir="tpch_data")
data_files = benchmark.generate_data()
print(len(data_files), data_files[0].name)
adapter = DuckDBAdapter()
results = benchmark.run_with_platform(adapter, query_subset=[1, 3, 6])
print(f"Completed {results.successful_queries}/{results.total_queries} queries")
print(f"Average query time: {results.average_query_time:.3f}s")

Output on 0.4.1 (the adapter also prints its progress lines, which are omitted here; the timing varies by machine):

8 customer.tbl
Completed 3/3 queries
Average query time: 0.007s

Core Classes

benchbox.BaseBenchmark

The abstract base class for every BenchBox benchmark. Subclass it and implement generate_data, get_queries and get_query.

Import: from benchbox import BaseBenchmark · Extras: none

Alias: from benchbox.base import BaseBenchmark returns the same object.

Parameters

NameTypeDefaultMeaning
scale_factorfloat1.0The benchmark scale factor.
output_dirstr, Path or NoneNoneDirectory for generated data.
**kwargsanynoneverbose (bool or int) and quiet (bool) set the logging verbosity. Every other keyword is stored as an attribute of the instance.

Full signature: (scale_factor: float = 1.0, output_dir: Union[str, pathlib.Path, NoneType] = None, **kwargs: Any) -> None

scale_factor must be positive, and a value of 1 or more must be a whole number. When output_dir is None, the directory is benchmark_runs/datagen/<name>_sf<token> under the current working directory, for example benchmark_runs/datagen/dummy_sf01 for scale factor 0.1. If the BENCHBOX_OUTPUT_DIR environment variable is set, the directory is datagen/<name>_sf<token> under that path instead.

Returns: an instance of the subclass you construct, because BaseBenchmark itself is abstract. The instance keeps scale_factor as passed and output_dir as a pathlib.Path (or a cloud path object), and every extra keyword as an attribute of the same name. The constructor creates no files.

Raises

  • ValueError: scale_factor is zero or negative (Scale factor must be positive), or is 1 or more and not a whole number (Scale factors >= 1 must be whole integers).
  • TypeError: scale_factor is not a number, or the class still has abstract methods.
  • ImportError: output_dir is a cloud storage URL such as s3://bucket/path and the cloudstorage extra is not installed.

Example

from benchbox import BaseBenchmark
class TinyBenchmark(BaseBenchmark):
def generate_data(self):
return []
def get_queries(self):
return {"q1": "SELECT 1 AS one", "q2": "SELECT 2 AS two"}
def get_query(self, query_id, *, params=None):
return self.get_queries()[str(query_id)]
TinyBenchmark(scale_factor=1.5)
ValueError: Scale factors >= 1 must be whole integers. Got: 1.5. Use values like 1, 2, 10, etc. for large scale factors. Use values like 0.1, 0.01, 0.001, etc. for small scale factors.

Compatibility

  • Instances keep output_dir as a pathlib.Path (or a cloud path object), whatever type you passed in.
  • benchbox.core.base_benchmark.BaseBenchmark is a different class; do not use it as a base for new code.

Class attributes

api_surface is the string "beta-public".

run_with_platform_api_surface is also the string "beta-public". Both labels mark the class and run_with_platform as public but not yet frozen.

DATA_SOURCE_BENCHMARK is None by default. Set it to the lower-case identifier of another benchmark (for example "tpch") when your benchmark reuses that benchmark’s generated data. get_data_source_benchmark() returns it.

SKIP_DATA_LOADING is a bool class attribute that defaults to False. Set it to True for a benchmark that needs schema objects but no data files; the platform adapter then creates the schema and skips data loading. Added in 0.4.2.

Other members

tables is a dict that maps table names to data file paths. It is empty on a new instance and TPC-H fills it when generate_data runs. You can assign a new mapping.

csv_delimiter is a read-only str | None. It is None unless a benchmark sets it, and assigning to it raises AttributeError.

csv_null_marker is a str | None that you can assign. It is None unless a benchmark sets it.

get_csv_loading_config(table_name) returns a list of CSV reader options (strings) for the table, or None when the benchmark has no configuration. The base class returns None; ClickBench returns a list such as ["delim='|'", 'header=false', "nullstr='__NULL__'", 'ignore_errors=true', 'auto_detect=true'].

cleanup() does nothing in the base class and returns None. Subclasses may override it to release resources.

Key Methods

Data Generation

generate_data()

Required override. Generates the benchmark data files and returns their paths.

Returns: list[str | Path]. The built-in benchmarks return pathlib.Path objects, for example eight .tbl files for TPC-H.

BaseBenchmark declares the method abstract, so a subclass that does not implement it cannot be instantiated.

Query Access

get_queries()

Required override. Returns every query of the benchmark as a dict[str, str] that maps query ID to SQL text.

The IDs are benchmark specific. TPC-H uses the strings '1' to '22':

from benchbox import TPCH
benchmark = TPCH(scale_factor=0.01, output_dir="tpch_data")
queries = benchmark.get_queries()
print(len(queries), list(queries)[:3])
22 ['1', '2', '3']

get_query(query_id, *, params=None)

Required override. Returns the SQL text for one query.

NameTypeDefaultMeaning
query_idint or strrequiredThe query ID. Each benchmark decides which types it accepts.
paramsdict[str, Any] or NoneNoneQuery parameters, keyword-only. Which keys are used depends on the benchmark.

Returns: str, the query with its parameters resolved.

Raises: ValueError for an unknown query ID in the built-in benchmarks.

The built-in TPC-H and TPC-DS classes take integer IDs: get_query(1) works, get_query("1") raises TypeError: query_id must be an integer, got str, and get_query(999) on TPC-H raises ValueError: Query ID must be 1-22, got 999. Check the reference page for the benchmark you use.

query_sql = benchmark.get_query(1)
print(query_sql[:30])
SELECT l_returnflag, l_linesta

Database Setup

setup_database(connection)

Generates the data if this instance has not generated it yet, then loads it through the subclass’s _load_data(connection) method.

NameTypeDefaultMeaning
connectionDatabaseConnectionrequiredThe connection to load into.

Returns: None.

Raises: NotImplementedError unless the subclass overrides _load_data. The built-in benchmarks do not override it, so setup_database and run_benchmark with the default setup_database=True fail on them with TPCH must implement _load_data() method to support database execution functionality. Load built-in benchmarks through run_with_platform instead. Any other error from data generation or loading is re-raised.

generate_data runs once per instance: a second call to setup_database loads again but does not regenerate.

Execution

run_query(query_id, connection, params=None, fetch_results=False)

Runs one query on a connection and times it.

NameTypeDefaultMeaning
query_idint or strrequiredThe query to run, as accepted by get_query.
connectionDatabaseConnectionrequiredAn object with execute(sql) and fetchall(cursor).
paramsdict[str, Any] or NoneNonePassed to get_query.
fetch_resultsboolFalseFetch all rows.

Returns: a dict with the keys query_id, execution_time_seconds, query_text, results and row_count. results is None and row_count is 0 unless fetch_results is true.

Raises: whatever get_query or the connection raises, after logging it. A failed query raises; it is not returned as an error entry.

import sqlite3
from benchbox.core.connection import DatabaseConnection
connection = DatabaseConnection(sqlite3.connect(":memory:"))
benchmark = TinyBenchmark(scale_factor=0.01, output_dir="tiny_out")
print(benchmark.run_query("q1", connection, fetch_results=True)["results"])
[(1,)]

run_benchmark(connection, query_ids=None, fetch_results=False, setup_database=True)

Runs a set of queries one after another and summarises the timings.

NameTypeDefaultMeaning
connectionDatabaseConnectionrequiredThe connection to run on.
query_idslist[int | str] or NoneNoneQueries to run. None runs every key of get_queries().
fetch_resultsboolFalsePassed to run_query.
setup_databaseboolTrueCall setup_database first.

Returns: a dict with benchmark_name (the class name, for example "TinyBenchmark"), total_execution_time, total_queries, successful_queries, failed_queries, query_results, setup_time, average_query_time, min_query_time and max_query_time. Times are in seconds and exclude setup. The three summary times cover successful queries only and are 0.0 when none succeeded.

A query that fails does not stop the run. Its entry in query_results has execution_time_seconds of 0.0, query_text of None, row_count of 0 and an extra error string, and failed_queries counts it.

Raises: errors from setup_database when setup_database=True, including the NotImplementedError described above.

params is not accepted: each query runs with its default parameters.

results = benchmark.run_benchmark(connection, query_ids=["q1", "q2"], setup_database=False)
print(results["total_queries"], results["successful_queries"], results["failed_queries"])
print(benchmark.format_results(results).splitlines()[0])
2 2 0
Benchmark: TinyBenchmark

run_with_platform(platform_adapter, **run_config)

Recommended entry point. Runs the benchmark through a platform adapter, which handles connection, data loading and query execution.

NameTypeDefaultMeaning
platform_adapteradapterrequiredAn object with a run_benchmark(benchmark, **config) method, such as DuckDBAdapter().
**run_configanynonePassed unchanged to the adapter. query_subset (a list of query IDs) selects queries on the built-in adapters.

Returns: the BenchmarkResults that the adapter returns.

run_with_platform sets benchmark_type to "olap" in run_config when you do not supply it, then calls platform_adapter.run_benchmark(self, **run_config). A subclass can change the default by overriding _get_default_benchmark_type.

Raises: CapabilityContractError (message object does not provide SQL benchmark execution; missing: run_benchmark) when the adapter has no run_benchmark method. Everything else comes from the adapter.

from benchbox.tpch import TPCH
from benchbox.platforms import DuckDBAdapter
benchmark = TPCH(scale_factor=0.01, output_dir="tpch_data")
results = benchmark.run_with_platform(DuckDBAdapter(), query_subset=[1, 6])
print(results.successful_queries, results.total_queries)
2 2

Adapters for cloud warehouses such as Databricks are constructed with keyword configuration (DatabricksAdapter(**config)) and need live credentials; see Platform Selection Guide.

SQL Translation

translate_query(query_id, dialect)

Translates a query to another SQL dialect with sqlglot.

NameTypeDefaultMeaning
query_idint or strrequiredThe query to translate, as accepted by get_query.
dialectstrrequiredThe target dialect name.

Returns: str. The query is read as PostgreSQL SQL, and all identifiers are quoted in the output. The query is fetched with default parameters.

Raises: ValueError for an unknown query ID or an unknown dialect (Error translating to dialect 'nonsense': Unknown dialect 'nonsense'.).

Any dialect that sqlglot knows is accepted. These all translated TPC-H query 6 on 0.4.1: postgres, mysql, sqlite, duckdb, snowflake, bigquery, redshift, clickhouse, databricks, trino, athena and spark. The dialect name is case-insensitive ("SNOWFLAKE" works).

from benchbox import TPCH
benchmark = TPCH(scale_factor=0.01, output_dir="tpch_data")
print(benchmark.translate_query(6, "snowflake"))
print(benchmark.translate_query(6, "bigquery")[:40])
SELECT SUM("l_extendedprice" * "l_discount") AS "revenue" FROM "lineitem" WHERE "l_shipdate" >= CAST('1994-01-01' AS DATE) AND "l_shipdate" < CAST('1994-01-01' AS DATE) + INTERVAL '1 YEAR' AND "l_discount" BETWEEN 0.06 - 0.01 AND 0.06 + 0.01 AND "l_quantity" < 24
SELECT SUM(`l_extendedprice` * `l_discou

Results Creation

create_enhanced_benchmark_result and the other result and validation methods come from a mixin that BaseBenchmark inherits, not from BaseBenchmark itself.

create_enhanced_benchmark_result(platform, query_results, ...)

Builds a BenchmarkResults object from query results and metadata. Platform adapters use it so results have a consistent shape.

NameTypeDefaultMeaning
platformstrrequiredThe platform name.
query_resultslist[dict]requiredOne dict per executed query.
execution_metadatadict or NoneNoneRun metadata.
phasesdict or NoneNonePer-phase details.
resource_utilizationdict or NoneNoneResource usage.
performance_characteristicsdict or NoneNonePerformance summary.
duration_secondsfloat or NoneNoneTotal run duration.
**kwargsanynoneIncludes the legacy table_statistics and data_loading_time.

Returns: benchbox.core.results.models.BenchmarkResults.

result = benchmark.create_enhanced_benchmark_result(
"duckdb",
[{"query_id": "1", "execution_time_seconds": 0.1, "status": "SUCCESS", "rows_returned": 4}],
table_statistics={"lineitem": 10},
data_loading_time=1.5,
)
print(result.benchmark_name, result.total_queries, result.table_statistics, result.data_loading_time)
TPC-H 1 {'lineitem': {'rows': 10}} 1.5

Properties

benchmark_name is a read-only str: the human-readable benchmark name. The built-in benchmarks return names such as "TPC-H Benchmark" and "ClickBench", and a subclass that sets no name returns its class name.

scale_factor is the float you passed to the constructor (1.0 = standard size, 0.01 = 1% size, 10 = 10x size). It is a plain attribute, so assigning to it later skips the constructor’s validation.

output_dir is the directory for generated data, stored as a pathlib.Path (or cloud path object). Assigning a str or Path converts it; assigning None clears it.

Utility Methods

format_results(benchmark_result)

Formats the dictionary returned by run_benchmark as a multi-line text report. It lists the query counts, the setup and total times, the average, minimum and maximum query times, and one line per query (Query q2: FAILED - boom for a failed one).

Returns: str. Passing a dict without the keys that run_benchmark produces raises KeyError.

get_data_source_benchmark()

Returns the lower-case name of the benchmark whose data this one reuses, or None when it generates its own. ReadPrimitives returns "tpch", and TPCH returns None.

Best Practices

  1. Always use platform adapters - Call run_with_platform instead of direct run_benchmark for production use. Platform adapters provide optimized data loading and query execution, and the built-in benchmarks cannot load data through run_benchmark.

  2. Handle scale factors carefully - Scale factors ≥1 must be integers. Use 0.1, 0.01, etc. for small-scale testing.

  3. Check data generation - Call generate_data explicitly if you need to inspect or manipulate data files before loading.

  4. Use query subsets for debugging - Pass query_subset=[1] to test single queries during development.

  5. Use SQL translation - Call translate_query to adapt queries to platform-specific dialects when needed.

See Also