Zero-copy Arrow interop (to_arrow / __arrow_c_stream__ / from_arrow)#596
Zero-copy Arrow interop (to_arrow / __arrow_c_stream__ / from_arrow)#596wudidapaopao wants to merge 4 commits into
Conversation
…__ / from_arrow)
Hand DataStore results to Polars/DuckDB/pandas 3.0 without a pandas round-trip.
- to_arrow(types='native'|'pandas'): fully pushed-down queries return a
pyarrow.Table straight from chDB's ArrowTable output, keeping ClickHouse-native
Arrow types (UInt64->uint64, nullable ints stay nullable); falls back to the
pandas result for pipelines that need pandas-side ops.
- __arrow_c_stream__(): Arrow PyCapsule interface (the pandas 3.0 interchange
standard) so pa.table(ds) / pl.from_arrow(ds) / duckdb.sql('... FROM ds') /
pd.DataFrame.from_arrow(ds) work with no glue code.
- from_arrow(): ingest any PyCapsule producer (pyarrow Table/RecordBatch/reader,
Polars, pandas 3.x, DuckDB, another DataStore) via faithful ArrowDtype.
Adds Connection.query_arrow / Executor.query_arrow and a test suite covering
native types, pushdown, interop, ingest, and round-trips.
Resolves chdb-io#595
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add TestPandas3xIngestDtypes / TestPandas3xConsumer / TestNativeArrowTypes: - from_arrow() ingest of pandas 3.x dtypes: nullable Int64/boolean/Float64, the pandas 3.0 default string dtype, ArrowDtype, datetime, tz-aware datetime, Categorical (values + null positions preserved). - pd.DataFrame.from_arrow(ds) (the pandas 3.0 API): uint64 stays unsigned, null positions preserved. - Native export types with no pandas coercion: decimal128, list, computed columns via assign, empty tables, all-null columns. 36 tests total, all passing. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… coverage CI runs the suite under both pandas 2.x and 3.x, so: - from_arrow(): accept a plain pandas.DataFrame directly (via from_df). pandas 2.x DataFrames don't implement the Arrow PyCapsule interface, so they'd otherwise fall through to TypeError. - Gate the two pandas-3.0-only tests (pd.DataFrame.from_arrow constructor and the default string dtype) behind PANDAS_3_PLUS. New edge-case coverage (all pandas-version agnostic — type fidelity via the native from_file path; streaming/requested_schema via pyarrow): - Nested/binary types: Map, struct (Tuple), fixed_size_binary, awkward column names (spaces / unicode). - Streaming: multi-batch RecordBatchReader ingest; DataStore consumed via pa.RecordBatchReader.from_stream; __arrow_c_stream__ requested_schema (compatible cast honored, incompatible cast raises). - Ibis interop (importorskip). Verified: pandas 3.0.3 (py3.14) and pandas 2.3.3 (py3.12) both green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
@chibugai, review this PR. |
| self._log_query(sql, "Connection", "ArrowTable") | ||
| return self._conn.query(sql, "ArrowTable") |
There was a problem hiding this comment.
Unlike execute and query_df, this method does not wrap the _conn.query() call in a try/except. On failure, raw chDB exceptions propagate untranslated instead of being converted into a friendly ExecutionError. This breaks the established error-handling pattern and exposes low-level errors to callers. Consider wrapping the call consistently:
try:
return self._conn.query(sql, "ArrowTable")
except Exception as e:
self._logger.error("[chDB] Arrow query failed: %s", e)
friendly_msg = translate_remote_error(e)
raise ExecutionError(f"Query execution failed: {friendly_msg}\nSQL: {sql}")There was a problem hiding this comment.
Good catch — fixed in 54b97e9. query_arrow now wraps the _conn.query() call in the same try/except as execute(), logging and translating failures via translate_remote_error into a friendly ExecutionError instead of leaking the raw chDB exception. Added a regression test (TestQueryArrowErrorHandling) that asserts an invalid query raises ExecutionError.
| if table is not None: | ||
| return table | ||
|
|
||
| return pa.Table.from_pandas(self._execute(), preserve_index=False) |
There was a problem hiding this comment.
The fallback uses preserve_index=False, which drops the index instead of converting it to columns. This contradicts the docstring Note ("group keys from groupby(as_index=True) appears as regular columns here") and silently loses data: a groupby(as_index=True).agg(...) pipeline routed through this fallback (e.g. a DataFrame source, or any pandas-side op, or types="pandas") returns a Table missing the group-key columns. This also diverges from the native path (_try_native_arrow), where group keys stay as columns. Consider resetting the index into columns before converting, e.g.:
df = self._execute()
if df.index.name is not None or isinstance(df.index, pd.MultiIndex) or df.index.names != [None]:
df = df.reset_index()
return pa.Table.from_pandas(df, preserve_index=False)
(or use preserve_index=True only for named/group indexes) so the fallback matches both the documented behavior and the native path.
There was a problem hiding this comment.
Agreed, this was a real data-loss bug — fixed in 54b97e9. The fallback now resets a meaningful index into columns before conversion:
df = self._execute()
if df.index.names != [None]:
df = df.reset_index()
return pa.Table.from_pandas(df, preserve_index=False)So group keys from groupby(as_index=True) (and any named index / MultiIndex) are preserved as columns, matching both the native path and the docstring Note. A plain unnamed RangeIndex is still left out (no spurious index column). Confirmed the repro (from_df(...).groupby("city").agg(...) previously returned only ["score"], now ["city", "score"]). Added regressions for the from_df-groupby, types="pandas"-groupby, and named-index cases.
…in Arrow fallback Review feedback on PR chdb-io#596: - Connection.query_arrow now wraps chDB errors in ExecutionError via translate_remote_error, matching execute()/query_df() instead of leaking raw low-level exceptions. - to_arrow() pandas fallback no longer drops the row index: a meaningful index (named index, or group keys from groupby(as_index=True), or a MultiIndex) is reset into columns before conversion. Previously preserve_index=False silently dropped groupby group-key columns on the fallback path (DataFrame source / pandas-side ops / types="pandas"), diverging from the native path and losing data. Regression tests added for both; verified on pandas 3.0.3 (py3.14) and 2.3.3 (py3.12). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds Arrow interop to
DataStore, built on the Arrow PyCapsule interface (the pandas 3.0 standard). Lets DataStore results flow to Polars / DuckDB / pandas 3.0 without a pandas round-trip.What
to_arrow(types='native'|'pandas')— a fully pushed-down pipeline returns apyarrow.Tablestraight from chDB'sArrowTableoutput (no pandas round-trip), keeping ClickHouse-native types (UInt64→uint64, nullable ints stay nullable). Falls back to converting the pandas result for pipelines that need pandas-side ops.__arrow_c_stream__()—pa.table(ds),pl.from_arrow(ds),duckdb.sql('... FROM ds')andpd.DataFrame.from_arrow(ds)all work with no glue code.from_arrow(data)— ingest any PyCapsule producer (pyarrow Table/RecordBatch/reader, Polars, pandas 3.x, DuckDB, another DataStore).Connection.query_arrow/Executor.query_arrow.Notes
to_df()and existing behavior unchanged.groupby(as_index=True)appear as regular columns.from_arrow), so it is not zero-copy on import; the zero-copy/native-typed path is on export.Tests
datastore/tests/test_arrow_interop.py— 23 tests: native types, filter/select/groupby pushdown, interop (pyarrow/Polars/DuckDB/pandas), ingest, round-trips. Passing locally.Closes #595
Note
Add zero-copy Arrow interop (
to_arrow,from_arrow,__arrow_c_stream__) toDataStoreDataStore.to_arrow(types='native'|'pandas')which returns apyarrow.Table; in'native'mode it pushes down a single SQL query to chDB's Arrow output format, falling back to pandas conversion when pushdown is unavailable.DataStore.from_arrow(data)classmethod accepting Arrow PyCapsule objects,pyarrow.Table/RecordBatch/RecordBatchReader, and pandas/Polars DataFrames.DataStore.__arrow_c_stream__soDataStoreobjects are directly consumable by PyArrow, Polars, DuckDB, and pandas 3.x without intermediate copies.Connection.query_arrowandExecutor.query_arrowto execute SQL and return apyarrow.Tablevia chDB'sArrowTableoutput format.requested_schemacasting, and error handling.Macroscope summarized 54b97e9.