This guide explains how to add support for a new DataFrame platform to BenchBox.
Before opening a PR, run through the New Platform Acceptance Checklist. DataFrame platforms need registry metadata, a support_status, optional dependency isolation, native query coverage, docs, and parity validation separate from SQL platforms.
BenchBox uses a family-based architecture that minimizes code duplication when adding new platforms. Most new platforms require:
- Determine which family the platform belongs to
- Implement a DataFrameContext subclass
- Implement a platform adapter
- Register the platform
- Add tests
DataFrame platforms fall into two families based on their API style:
Use expression objects for column references and operations.
Members: Polars, PySpark, DataFusion
This is expression-style syntax:
result = (
df.filter(col('status') == lit('active'))
.group_by('category')
.agg(col('amount').sum().alias('total'))
)Key characteristics:
col()function for column referenceslit()function for literal values- Method chaining with expression composition
- Often supports lazy evaluation
Use string-based column access and boolean indexing.
Members: Pandas, cuDF, Dask, Vaex
This is Pandas-style syntax:
filtered = df[df['status'] == 'active']
result = filtered.groupby('category').agg({'amount': 'sum'})Key characteristics:
- String-based column names:
df['column'] - Boolean indexing for filtering
- Dictionary-based aggregation specifications
- Usually eager evaluation (except Dask)
Create a context class that provides table access and family-specific helpers.
Put this class in benchbox/core/dataframe/context.py.
class MyPlatformDataFrameContext(DataFrameContext):
def __init__(self):
self._tables: dict[str, Any] = {}
@property
def family(self) -> str:
return "expression"
def get_table(self, name: str) -> Any:
if name not in self._tables:
raise KeyError(f"Table '{name}' not registered")
return self._tables[name]
def register_table(self, name: str, df: Any) -> None:
self._tables[name] = df
@property
def col(self):
from myplatform import col
return col
@property
def lit(self):
from myplatform import lit
return litget_table returns a copy for safety. col and lit return None because the pandas family does not use them.
class MyPandasLikeContext(DataFrameContext):
def __init__(self):
self._tables: dict[str, Any] = {}
@property
def family(self) -> str:
return "pandas"
def get_table(self, name: str) -> Any:
if name not in self._tables:
raise KeyError(f"Table '{name}' not registered")
return self._tables[name].copy()
def register_table(self, name: str, df: Any) -> None:
self._tables[name] = df
@property
def col(self):
return None
@property
def lit(self):
return NoneCreate an adapter that handles data loading and query execution.
For expression family platforms, inherit from ExpressionFamilyAdapter. Put the adapter in
benchbox/platforms/dataframe/myplatform_df.py. In execute_query, the adapter collects the result if it is lazy.
from benchbox.platforms.dataframe.expression_family import ExpressionFamilyAdapter
class MyPlatformAdapter(ExpressionFamilyAdapter[MyDF, MyLazyDF, MyExpr]):
platform_name = "myplatform-df"
def __init__(self, working_dir: str, **options):
super().__init__(working_dir)
self.options = options
def create_context(self) -> MyPlatformDataFrameContext:
return MyPlatformDataFrameContext()
def load_tables(self, ctx: DataFrameContext, data_dir: str) -> None:
import myplatform as mp
from pathlib import Path
data_path = Path(data_dir)
parquet_dir = data_path / "parquet"
for table_file in parquet_dir.glob("*.parquet"):
table_name = table_file.stem
df = mp.read_parquet(str(table_file))
ctx.register_table(table_name, df)
def execute_query(self, ctx: DataFrameContext, query: DataFrameQuery) -> Any:
impl = query.get_impl_for_family(self.family)
if impl is None:
raise ValueError(f"No {self.family} implementation for {query.query_id}")
result = impl(ctx)
if hasattr(result, 'collect'):
result = result.collect()
return result
@staticmethod
def is_available() -> bool:
try:
import myplatform
return True
except ImportError:
return False
@staticmethod
def get_version() -> str | None:
try:
import myplatform
return myplatform.__version__
except ImportError:
return NoneAdd the platform to the DataFrame adapter registry and PlatformRegistry metadata. DataFrame-only platforms must set supports_sql=False, supports_dataframe=True, and exactly one support_status.
The registry lives in benchbox/platforms/dataframe/__init__.py. The myplatform-df entry is the new platform.
from benchbox.platforms.dataframe.myplatform import MyPlatformAdapter
DATAFRAME_ADAPTERS = {
"polars-df": PolarsDataFrameAdapter,
"pandas-df": PandasDataFrameAdapter,
"myplatform-df": MyPlatformAdapter,
}Before implementation, maintainers can print the expected file plan with the
platform scaffold script (--kind dataframe).
Declare compute and gateway support with the compute and gateway items of the
{ref}SQL platform checklist <new-platform-checklist-sql>.
Declare execution engines when the platform offers more than one way to execute a query, following the same
manifest capabilities.execution_engines block, closed class list, value-naming rule and reserved-word ban as
SQL platforms (see docs/development/adding-new-platforms.md). The worked example is polars-df, which
declares auto (adaptive), in-memory and streaming under the platform's own documented names. DataFrame
adapters inherit resolve_execution_engine from the adapter base and map the resolved engine into their
collect or compute call; unsupported values fail closed.
Put the unit tests in tests/unit/platforms/test_myplatform_adapter.py. test_is_available passes whether or not
myplatform is installed, because the result is True only when it is installed. The family assertion expects
"expression" for an expression family platform and "pandas" for a pandas family platform.
import pytest
from benchbox.platforms.dataframe.myplatform import MyPlatformAdapter
class TestMyPlatformAdapter:
def test_is_available(self):
result = MyPlatformAdapter.is_available()
assert isinstance(result, bool)
@pytest.mark.skipif(
not MyPlatformAdapter.is_available(),
reason="myplatform not installed"
)
def test_create_context(self, tmp_path):
adapter = MyPlatformAdapter(str(tmp_path))
ctx = adapter.create_context()
assert ctx.family == "expression"
@pytest.mark.skipif(
not MyPlatformAdapter.is_available(),
reason="myplatform not installed"
)
def test_query_execution(self, tmp_path, sample_data):
adapter = MyPlatformAdapter(str(tmp_path))
ctx = adapter.create_context()
adapter.load_tables(ctx, sample_data)
from benchbox.core.tpch.dataframe_queries import get_query
query = get_query("Q1")
result = adapter.execute_query(ctx, query)
assert len(result) > 0Put the integration tests in tests/integration/test_myplatform_tpch.py.
import pytest
from benchbox.platforms.dataframe.myplatform import MyPlatformAdapter
@pytest.mark.integration
@pytest.mark.skipif(
not MyPlatformAdapter.is_available(),
reason="myplatform not installed"
)
class TestMyPlatformTPCH:
def test_all_tpch_queries(self, tpch_data_dir):
from benchbox.core.tpch.dataframe_queries import TPCH_DATAFRAME_QUERIES
adapter = MyPlatformAdapter(str(tpch_data_dir))
ctx = adapter.create_context()
adapter.load_tables(ctx, str(tpch_data_dir))
for query in TPCH_DATAFRAME_QUERIES.get_all_queries():
result = adapter.execute_query(ctx, query)
assert result is not None- Requires Spark session management
- Use
spark.createDataFrame()for table registration - Handle distributed execution semantics
- Requires CUDA-enabled GPU
- Memory limited to GPU VRAM
- Similar to Pandas family API
- Supports larger-than-memory datasets
- Lazy evaluation like expression family
- Uses Pandas-style API
Before submitting a PR:
- Unit tests pass:
uv run -- python -m pytest tests/unit/platforms/test_myplatform_adapter.py - Integration tests pass (if platform installed)
- Platform availability check works correctly
- Version detection works
- All TPC-H queries execute successfully
-
support_statusand registry capabilities match the platform family - Documentation updated
- Example script created in
examples/dataframe/