"""Integration-ish tests for ``read_parquet()`` on the DataSourceV2 path. These tests exercise planning-time behavior: schema inference, ``ListFiles → ReadFiles`` attachment to the logical plan, and unsupported-option gating. They call ``ray.data.read_parquet`` which triggers Ray auto-init, so they live alongside the other datasource integration tests rather than under ``tests/unit/``. """ import pyarrow as pa import pyarrow.parquet as pq import pytest import ray from ray.data._internal.datasource_v2.partitioners.round_robin_partitioner import ( RoundRobinPartitioner, ) from ray.data._internal.datasource_v2.scanners.parquet_scanner import ParquetScanner from ray.data._internal.logical.operators import ListFiles, ReadFiles from ray.data.context import DataContext def _write(path, table): pq.write_table(table, str(path)) @pytest.fixture def restore_ctx(): ctx = DataContext.get_current() original = ctx.use_datasource_v2 try: yield ctx finally: ctx.use_datasource_v2 = original def test_v2_flag_default(): # The default is driven by ``DEFAULT_USE_DATASOURCE_V2``. Asserting # either direction here would be brittle, so just check that the # default is a bool. ctx = DataContext() assert isinstance(ctx.use_datasource_v2, bool) def test_read_parquet_builds_list_files_read_files_chain(tmp_path, restore_ctx): f = tmp_path / "data.parquet" _write(f, pa.table({"a": [1, 2, 3], "b": ["x", "y", "z"]})) restore_ctx.use_datasource_v2 = True ds = ray.data.read_parquet(str(tmp_path)) assert isinstance(ds._logical_plan.dag, ReadFiles) assert isinstance(ds._logical_plan.dag.input_dependencies[0], ListFiles) schema = ds.schema() assert schema is not None assert "a" in schema.names assert "b" in schema.names def test_read_parquet_v2_hive_partitioned(tmp_path, restore_ctx): for p in ["a", "b"]: d = tmp_path / f"color={p}" d.mkdir() _write(d / "data.parquet", pa.table({"x": [1, 2]})) restore_ctx.use_datasource_v2 = True ds = ray.data.read_parquet(str(tmp_path)) schema = ds.schema() assert "x" in schema.names assert "color" in schema.names def test_read_parquet_v2_include_paths(tmp_path, restore_ctx): _write(tmp_path / "data.parquet", pa.table({"a": [1]})) restore_ctx.use_datasource_v2 = True ds = ray.data.read_parquet(str(tmp_path), include_paths=True) schema = ds.schema() assert "path" in schema.names def test_read_parquet_v2_include_row_hash(tmp_path, restore_ctx): _write(tmp_path / "data.parquet", pa.table({"a": [1, 2, 3]})) restore_ctx.use_datasource_v2 = True ds = ray.data.read_parquet(str(tmp_path), include_row_hash=True) schema = ds.schema() assert schema is not None assert "row_hash" in schema.names assert schema.types[schema.names.index("row_hash")] == pa.uint64() def test_read_parquet_v2_columns_applies_select_columns(tmp_path, restore_ctx): from ray.data._internal.logical.operators.map_operator import Project _write(tmp_path / "data.parquet", pa.table({"a": [1], "b": [2]})) restore_ctx.use_datasource_v2 = True with pytest.warns(DeprecationWarning, match="`columns=` on `read_parquet`"): ds = ray.data.read_parquet(str(tmp_path), columns=["a"]) # ``columns=`` is applied via ``ds.select_columns([...])``, which # wraps the ReadFiles op in a Project node. dag = ds._logical_plan.dag assert isinstance(dag, Project) assert [expr.name for expr in dag.exprs] == ["a"] assert isinstance(dag.input_dependencies[0], ReadFiles) def test_read_parquet_v2_columns_with_include_paths_preserves_path( tmp_path, restore_ctx ): from ray.data._internal.logical.operators.map_operator import Project _write(tmp_path / "data.parquet", pa.table({"a": [1], "b": [2]})) restore_ctx.use_datasource_v2 = True with pytest.warns(DeprecationWarning, match="`columns=` on `read_parquet`"): ds = ray.data.read_parquet(str(tmp_path), columns=["a"], include_paths=True) dag = ds._logical_plan.dag assert isinstance(dag, Project) # V1 ``columns=[...]`` retained ``"path"`` implicitly when # ``include_paths=True``; the V2 path appends it to keep that # behavior. assert [expr.name for expr in dag.exprs] == ["a", "path"] def test_read_parquet_v2_override_num_blocks_drives_partitioner(tmp_path, restore_ctx): _write(tmp_path / "data.parquet", pa.table({"a": [1, 2, 3]})) restore_ctx.use_datasource_v2 = True original = restore_ctx.read_op_min_num_blocks ds = ray.data.read_parquet(str(tmp_path), override_num_blocks=7) # The override should drive the ListFiles partitioner's bucket count # for this read only — the global DataContext must not be mutated. list_files_op = ds._logical_plan.dag.input_dependencies[0] assert isinstance(list_files_op, ListFiles) assert isinstance(list_files_op.file_partitioner, RoundRobinPartitioner) assert list_files_op.file_partitioner.num_buckets == 7 assert restore_ctx.read_op_min_num_blocks == original def test_read_parquet_v2_filter_raises(tmp_path, restore_ctx): import pyarrow.dataset as pds _write(tmp_path / "data.parquet", pa.table({"a": [1, 2, 3]})) restore_ctx.use_datasource_v2 = True with pytest.raises(ValueError, match="`filter=` on `read_parquet`"): ray.data.read_parquet(str(tmp_path), filter=pds.field("a") > 1) def test_read_parquet_v2_dataset_kwargs_rejects_partitioning(tmp_path, restore_ctx): _write(tmp_path / "data.parquet", pa.table({"a": [1]})) restore_ctx.use_datasource_v2 = True with pytest.warns(DeprecationWarning, match="`dataset_kwargs`"): with pytest.raises( ValueError, match="'partitioning' parameter isn't supported" ): ray.data.read_parquet( str(tmp_path), dataset_kwargs={"partitioning": "hive"} ) def test_read_parquet_v2_dataset_kwargs_rejects_filters(tmp_path, restore_ctx): _write(tmp_path / "data.parquet", pa.table({"a": [1]})) restore_ctx.use_datasource_v2 = True with pytest.warns(DeprecationWarning, match="`dataset_kwargs`"): with pytest.raises(ValueError, match="Row filtering via 'filters'"): ray.data.read_parquet( str(tmp_path), dataset_kwargs={"filters": [("a", ">", 0)]} ) def test_read_parquet_v2_dataset_kwargs_threads_through_to_scanner( tmp_path, restore_ctx ): _write(tmp_path / "data.parquet", pa.table({"a": [1, 2, 3]})) restore_ctx.use_datasource_v2 = True with pytest.warns(DeprecationWarning, match="`dataset_kwargs`"): ds = ray.data.read_parquet( str(tmp_path), dataset_kwargs={ "coerce_int96_timestamp_unit": "ms", "read_dictionary": ["a"], }, ) # ``read_dictionary`` is renamed to ``dictionary_columns`` to match # ``pds.ParquetFileFormat``; ``coerce_int96_timestamp_unit`` passes # through unchanged. read_files_op = ds._logical_plan.dag assert isinstance(read_files_op, ReadFiles) assert isinstance(read_files_op.scanner, ParquetScanner) assert read_files_op.scanner.parquet_format_kwargs == { "coerce_int96_timestamp_unit": "ms", "dictionary_columns": ["a"], } def test_read_parquet_v2_empty_dir_raises(tmp_path, restore_ctx): restore_ctx.use_datasource_v2 = True with pytest.raises(ValueError, match="no files found"): ray.data.read_parquet(str(tmp_path)) if __name__ == "__main__": import sys sys.exit(pytest.main([__file__, "-xvs"]))