This is an automated email from the ASF dual-hosted git repository.
andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git
The following commit(s) were added to refs/heads/main by this push:
new 9d75f1148 chore: add script and CI check to sync vendored DataFusion
protos (#1991)
9d75f1148 is described below
commit 9d75f11488fc7c655a1a29898acdaaaf8b5d2e7e
Author: Andy Grove <[email protected]>
AuthorDate: Sat Jul 11 07:01:29 2026 -0600
chore: add script and CI check to sync vendored DataFusion protos (#1991)
* chore: add script and CI check to sync vendored DataFusion protos
The vendored `ballista/core/proto/datafusion.proto` and
`datafusion_common.proto`
are build-time stubs so protoc can resolve `ballista.proto`'s imports; no
Rust is
generated from them (build.rs maps their packages to the datafusion-proto
crates
via extern_path). They must stay compatible with the pinned DataFusion
version but
were previously synced by hand, which drifts.
Add `dev/update_datafusion_proto.py` to copy the `.proto` files out of the
crate
source resolved by `cargo metadata --locked` (rewriting the one differing
import),
with a `--check` mode wired into CI via `ci/scripts/rust_proto_check.sh`.
Releases
that do not ship a `.proto` (datafusion-proto-common before v54) are
skipped rather
than failing. Also remind the datafusion version-bump script to re-run the
sync.
* chore: sync vendored DataFusion protos to 54.0.0
Run dev/update_datafusion_proto.py to bring the vendored proto stubs in line
with the pinned datafusion-proto / datafusion-proto-common 54.0.0 crates.
The
crates were upgraded to 54 without re-syncing these files; no Rust is
generated
from them so the drift was benign, but the new CI check now enforces parity.
---
.github/workflows/rust.yml | 15 +++
ballista/core/proto/datafusion.proto | 94 ++++++++++++++++--
ballista/core/proto/datafusion_common.proto | 50 +++++++++-
ci/scripts/rust_proto_check.sh | 24 +++++
dev/update_datafusion_proto.py | 143 ++++++++++++++++++++++++++++
dev/update_datafusion_versions.py | 8 ++
6 files changed, 324 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index cb0a3a48e..981829053 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -251,6 +251,21 @@ jobs:
exit 1
fi
+ datafusion-proto-sync-check:
+ name: Check vendored DataFusion proto is in sync
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/[email protected]
+ with:
+ fetch-depth: 1
+ - name: Setup Rust toolchain
+ run: |
+ rustup toolchain install stable
+ rustup default stable
+ - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
#2.9.1
+ - name: Check vendored DataFusion proto matches pinned crate
+ run: ci/scripts/rust_proto_check.sh
+
# Coverage job was failing.
https://github.com/apache/arrow-datafusion/issues/590 tracks re-instating it
# coverage:
diff --git a/ballista/core/proto/datafusion.proto
b/ballista/core/proto/datafusion.proto
index fe81be0d4..69c003acb 100644
--- a/ballista/core/proto/datafusion.proto
+++ b/ballista/core/proto/datafusion.proto
@@ -62,6 +62,7 @@ message LogicalPlanNode {
RecursiveQueryNode recursive_query = 31;
CteWorkTableScanNode cte_work_table_scan = 32;
DmlNode dml = 33;
+ EmptyTableScanNode empty_table_scan = 34;
}
}
@@ -228,6 +229,7 @@ message AnalyzeNode {
message ExplainNode {
LogicalPlanNode input = 1;
bool verbose = 2;
+ datafusion_common.ExplainFormat format = 3;
}
message AggregateNode {
@@ -270,6 +272,25 @@ message CopyToNode {
repeated string partition_by = 7;
}
+// Identifies a built-in file format supported by DataFusion.
+// Used by DefaultLogicalExtensionCodec to serialize/deserialize
+// FileFormatFactory instances (e.g. in CopyTo plans).
+enum FileFormatKind {
+ FILE_FORMAT_KIND_UNSPECIFIED = 0;
+ FILE_FORMAT_KIND_CSV = 1;
+ FILE_FORMAT_KIND_JSON = 2;
+ FILE_FORMAT_KIND_PARQUET = 3;
+ FILE_FORMAT_KIND_ARROW = 4;
+ FILE_FORMAT_KIND_AVRO = 5;
+}
+
+// Wraps a serialized FileFormatFactory with its format kind tag,
+// so the decoder can dispatch to the correct format-specific codec.
+message FileFormatProto {
+ FileFormatKind kind = 1;
+ bytes encoded_file_format = 2;
+}
+
message DmlNode{
enum Type {
UPDATE = 0;
@@ -407,6 +428,8 @@ message LogicalExprNode {
Unnest unnest = 35;
+ // Subquery expressions
+ ScalarSubqueryExprNode scalar_subquery_expr = 36;
}
}
@@ -414,6 +437,15 @@ message Wildcard {
TableReference qualifier = 1;
}
+message SubqueryNode {
+ LogicalPlanNode subquery = 1;
+ repeated LogicalExprNode outer_ref_columns = 2;
+}
+
+message ScalarSubqueryExprNode {
+ SubqueryNode subquery = 1;
+}
+
message PlaceholderNode {
string id = 1;
// We serialize the data type, metadata, and nullability separately to
maintain
@@ -594,11 +626,15 @@ message WhenThen {
message CastNode {
LogicalExprNode expr = 1;
datafusion_common.ArrowType arrow_type = 2;
+ map<string, string> metadata = 3;
+ optional bool nullable = 4;
}
message TryCastNode {
LogicalExprNode expr = 1;
datafusion_common.ArrowType arrow_type = 2;
+ map<string, string> metadata = 3;
+ optional bool nullable = 4;
}
message SortExprNode {
@@ -752,6 +788,7 @@ message PhysicalPlanNode {
AsyncFuncExecNode async_func = 36;
BufferExecNode buffer = 37;
ArrowScanExecNode arrow_scan = 38;
+ ScalarSubqueryExecNode scalar_subquery = 39;
}
}
@@ -853,11 +890,9 @@ message PhysicalExprNode {
reserved 17;
// Unique identifier for this expression to do deduplication during
deserialization.
- // When serializing, this is set to a unique identifier for each combination
of
- // expression, process and serialization run.
- // When deserializing, if this ID has been seen before, the cached Arc is
returned
- // instead of creating a new one, enabling reconstruction of referential
integrity
- // across serde roundtrips.
+ // When serializing, this is set via `PhysicalExpr::expression_id`. When
deserializing,
+ // this id is used by the `DeduplicatingProtoConverter` to preserve
referential
+ // integrity across serde roundtrips for different expressions with the same
id.
optional uint64 expr_id = 30;
oneof ExprType {
@@ -897,9 +932,21 @@ message PhysicalExprNode {
UnknownColumn unknown_column = 20;
PhysicalHashExprNode hash_expr = 21;
+
+ PhysicalScalarSubqueryExprNode scalar_subquery = 22;
+
+ PhysicalDynamicFilterNode dynamic_filter = 23;
}
}
+message PhysicalDynamicFilterNode {
+ repeated PhysicalExprNode children = 1;
+ repeated PhysicalExprNode remapped_children = 2;
+ uint64 generation = 3;
+ PhysicalExprNode inner_expr = 4;
+ bool is_complete = 5;
+}
+
message PhysicalScalarUdfNode {
string name = 1;
repeated PhysicalExprNode args = 2;
@@ -958,6 +1005,9 @@ message PhysicalBinaryExprNode {
PhysicalExprNode l = 1;
PhysicalExprNode r = 2;
string op = 3;
+ // Linearized operands for chains of the same operator (e.g. a AND b AND c).
+ // When present, `l` and `r` are ignored and `operands` holds the flattened
list.
+ repeated PhysicalExprNode operands = 4;
}
message PhysicalDateTimeIntervalExprNode {
@@ -1018,9 +1068,6 @@ message PhysicalExtensionExprNode {
message PhysicalHashExprNode {
repeated PhysicalExprNode on_columns = 1;
uint64 seed0 = 2;
- uint64 seed1 = 3;
- uint64 seed2 = 4;
- uint64 seed3 = 5;
string description = 6;
}
@@ -1141,6 +1188,8 @@ message HashJoinExecNode {
JoinFilter filter = 8;
repeated uint32 projection = 9;
bool null_aware = 10;
+ // Optional dynamic filter expression for pushing down to the probe side.
+ PhysicalExprNode dynamic_filter = 11;
}
enum StreamPartitionMode {
@@ -1179,6 +1228,10 @@ message AnalyzeExecNode {
bool show_statistics = 2;
PhysicalPlanNode input = 3;
datafusion_common.Schema schema = 4;
+ // Optional metric category filter.
+ // Empty means "plan only". Absent (has_metric_categories=false) means "all".
+ bool has_metric_categories = 5;
+ repeated string metric_categories = 6;
}
message CrossJoinExecNode {
@@ -1268,6 +1321,8 @@ message AggregateExecNode {
repeated MaybeFilter filter_expr = 10;
AggLimit limit = 11;
bool has_grouping_set = 12;
+ // Optional dynamic filter expression for pushing down to the child.
+ PhysicalExprNode dynamic_filter = 13;
}
message GlobalLimitExecNode {
@@ -1289,6 +1344,8 @@ message SortExecNode {
// Maximum number of highest/lowest rows to fetch; negative means no limit
int64 fetch = 3;
bool preserve_partitioning = 4;
+ // Optional dynamic filter expression for TopK pushdown.
+ PhysicalExprNode dynamic_filter = 5;
}
message SortPreservingMergeExecNode {
@@ -1330,6 +1387,7 @@ message RepartitionExecNode{
// uint64 unknown = 4;
// }
Partitioning partitioning = 5;
+ bool preserve_order = 6;
}
message Partitioning {
@@ -1384,6 +1442,13 @@ message CteWorkTableScanNode {
datafusion_common.Schema schema = 2;
}
+message EmptyTableScanNode {
+ TableReference table_name = 1;
+ datafusion_common.Schema schema = 2;
+ ProjectionColumns projection = 3;
+ repeated LogicalExprNode filters = 4;
+}
+
enum GenerateSeriesName {
GS_GENERATE_SERIES = 0;
GS_RANGE = 1;
@@ -1449,4 +1514,15 @@ message AsyncFuncExecNode {
message BufferExecNode {
PhysicalPlanNode input = 1;
uint64 capacity = 2;
-}
\ No newline at end of file
+}
+
+message ScalarSubqueryExecNode {
+ PhysicalPlanNode input = 1;
+ repeated PhysicalPlanNode subqueries = 2;
+}
+
+message PhysicalScalarSubqueryExprNode {
+ datafusion_common.ArrowType data_type = 1;
+ bool nullable = 2;
+ uint32 index = 3;
+}
diff --git a/ballista/core/proto/datafusion_common.proto
b/ballista/core/proto/datafusion_common.proto
index 5848455cb..dd3588d13 100644
--- a/ballista/core/proto/datafusion_common.proto
+++ b/ballista/core/proto/datafusion_common.proto
@@ -199,7 +199,7 @@ message Union{
repeated int32 type_ids = 3;
}
-// Used for List/FixedSizeList/LargeList/Struct/Map
+// Used for List/FixedSizeList/LargeList/ListView/LargeListView/Struct/Map
message ScalarNestedValue {
message Dictionary {
bytes ipc_message = 1;
@@ -306,6 +306,8 @@ message ScalarValue{
ScalarNestedValue large_list_value = 16;
ScalarNestedValue list_value = 17;
ScalarNestedValue fixed_size_list_value = 18;
+ ScalarNestedValue list_view_value = 46;
+ ScalarNestedValue large_list_view_value = 47;
ScalarNestedValue struct_value = 32;
ScalarNestedValue map_value = 41;
@@ -398,6 +400,8 @@ message ArrowType{
List LIST = 25;
List LARGE_LIST = 26;
FixedSizeList FIXED_SIZE_LIST = 27;
+ List LIST_VIEW = 43;
+ List LARGE_LIST_VIEW = 44;
Struct STRUCT = 28;
Union UNION = 29;
Dictionary DICTIONARY = 30;
@@ -430,6 +434,13 @@ message JsonWriterOptions {
}
+enum CsvQuoteStyle {
+ NECESSARY = 0;
+ ALWAYS = 1;
+ NON_NUMERIC = 2;
+ NEVER = 3;
+}
+
message CsvWriterOptions {
// Compression type
CompressionTypeVariant compression = 1;
@@ -453,6 +464,12 @@ message CsvWriterOptions {
string escape = 10;
// Optional flag whether to double quotes, instead of escaping. Defaults to
`true`
bool double_quote = 11;
+ // Quote style for CSV writing
+ CsvQuoteStyle quote_style = 12;
+ // Whether to ignore leading whitespace in string values
+ bool ignore_leading_whitespace = 13;
+ // Whether to ignore trailing whitespace in string values
+ bool ignore_trailing_whitespace = 14;
}
// Options controlling CSV format
@@ -476,6 +493,12 @@ message CsvOptions {
bytes terminator = 17; // Optional terminator character as a byte
bytes truncated_rows = 18; // Indicates if truncated rows are allowed
optional uint32 compression_level = 19; // Optional compression level
+ // Quote style for CSV writing
+ CsvQuoteStyle quote_style = 20;
+ // Whether to ignore leading whitespace in string values
+ bytes ignore_leading_whitespace = 21;
+ // Whether to ignore trailing whitespace in string values
+ bytes ignore_trailing_whitespace = 22;
}
// Options controlling CSV format
@@ -603,6 +626,24 @@ message ParquetOptions {
oneof max_predicate_cache_size_opt {
uint64 max_predicate_cache_size = 33;
}
+
+ ParquetCdcOptions content_defined_chunking = 35;
+
+ // Optional timezone applied to INT96-coerced timestamps when `coerce_int96`
+ // is set. When `Some`, INT96 columns coerce to
+ // `Timestamp(<coerce_int96>, Some(<tz>))` instead of the default
+ // `Timestamp(<coerce_int96>, None)`. No effect when `coerce_int96` is unset.
+ oneof coerce_int96_tz_opt {
+ string coerce_int96_tz = 36;
+ }
+}
+
+// Content-defined chunking (CDC) options for writing parquet files.
+message ParquetCdcOptions {
+ bool enabled = 1;
+ uint64 min_chunk_size = 2;
+ uint64 max_chunk_size = 3;
+ int32 norm_level = 4;
}
enum JoinSide {
@@ -637,4 +678,11 @@ message ColumnStats {
Precision null_count = 3;
Precision distinct_count = 4;
Precision byte_size = 6;
+}
+
+enum ExplainFormat {
+ EXPLAIN_FORMAT_INDENT = 0;
+ EXPLAIN_FORMAT_TREE = 1;
+ EXPLAIN_FORMAT_PGJSON = 2;
+ EXPLAIN_FORMAT_GRAPHVIZ = 3;
}
\ No newline at end of file
diff --git a/ci/scripts/rust_proto_check.sh b/ci/scripts/rust_proto_check.sh
new file mode 100755
index 000000000..434712687
--- /dev/null
+++ b/ci/scripts/rust_proto_check.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Fail if the vendored DataFusion proto files drift from the datafusion-proto /
+# datafusion-proto-common versions pinned in Cargo.lock. See the script header
of
+# dev/update_datafusion_proto.py for why these files are vendored.
+set -ex
+python3 dev/update_datafusion_proto.py --check
diff --git a/dev/update_datafusion_proto.py b/dev/update_datafusion_proto.py
new file mode 100755
index 000000000..bc627b5b8
--- /dev/null
+++ b/dev/update_datafusion_proto.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Sync the vendored DataFusion proto files with the version pinned in
Cargo.lock.
+#
+# `ballista/core/proto/ballista.proto` imports `datafusion.proto` and
+# `datafusion_common.proto`. Those two files are NOT compiled into Rust here --
+# `ballista/core/build.rs` maps their packages to the real `datafusion-proto` /
+# `datafusion-proto-common` crates via `extern_path`. They exist only so that
+# `protoc` can resolve `ballista.proto`'s imports at build time. Because of
that
+# they must stay compatible with the DataFusion version Ballista depends on.
+#
+# The `datafusion-proto` / `datafusion-proto-common` crates ship their `.proto`
+# files, but expose no `links`/`DEP_*` path for a build script to consume, so
we
+# vendor a copy. This script keeps that copy honest: it reads the exact crate
+# source resolved by `cargo metadata` and copies the `.proto` files in,
rewriting
+# the one import path that differs from our flat layout.
+#
+# Usage:
+# dev/update_datafusion_proto.py # update the vendored files in
place
+# dev/update_datafusion_proto.py --check # fail if vendored files are
stale (CI)
+
+import argparse
+import difflib
+import json
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).parent.parent.absolute()
+VENDOR_DIR = REPO_ROOT / "ballista" / "core" / "proto"
+
+# vendored file name -> (crate name, file name within the crate's `proto/` dir)
+FILES = {
+ "datafusion.proto": ("datafusion-proto", "datafusion.proto"),
+ "datafusion_common.proto": ("datafusion-proto-common",
"datafusion_common.proto"),
+}
+
+
+def crate_source_dirs():
+ """Map crate name -> source directory, as resolved in Cargo.lock."""
+ out = subprocess.run(
+ ["cargo", "metadata", "--format-version", "1", "--locked"],
+ cwd=REPO_ROOT,
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+ metadata = json.loads(out.stdout)
+ dirs = {}
+ for pkg in metadata["packages"]:
+ if pkg["name"] in ("datafusion-proto", "datafusion-proto-common"):
+ # manifest_path points at the crate's Cargo.toml; protos sit
alongside it.
+ dirs[pkg["name"]] = (Path(pkg["manifest_path"]).parent,
pkg["version"])
+ missing = {"datafusion-proto", "datafusion-proto-common"} - dirs.keys()
+ if missing:
+ sys.exit(f"could not resolve crate(s) from cargo metadata:
{sorted(missing)}")
+ return dirs
+
+
+def rewrite(content: str) -> str:
+ """Rewrite crate-relative imports to our flat vendored layout.
+
+ Upstream imports the common proto by its repo path, e.g.
+ `import "datafusion/proto-common/proto/datafusion_common.proto";`
+ but we vendor it flat next to `datafusion.proto`.
+ """
+ return re.sub(
+ r'import\s+"[^"]*datafusion_common\.proto";',
+ 'import "datafusion_common.proto";',
+ content,
+ )
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--check",
+ action="store_true",
+ help="verify the vendored files are up to date; do not write. Exits 1
on drift.",
+ )
+ args = parser.parse_args()
+
+ dirs = crate_source_dirs()
+ stale = []
+ for vendor_name, (crate, src_name) in FILES.items():
+ crate_dir, version = dirs[crate]
+ src = crate_dir / "proto" / src_name
+ if not src.exists():
+ # Some releases don't publish the `.proto` file (e.g.
datafusion-proto-common
+ # only started shipping it in v54). Nothing to sync against, so
leave the
+ # vendored copy as-is rather than failing.
+ print(
+ f"note: {crate}@{version} does not ship proto/{src_name}; "
+ f"leaving vendored {vendor_name} unchanged",
+ file=sys.stderr,
+ )
+ continue
+ expected = rewrite(src.read_text())
+ dest = VENDOR_DIR / vendor_name
+
+ if args.check:
+ current = dest.read_text() if dest.exists() else ""
+ if current != expected:
+ stale.append(vendor_name)
+ diff = difflib.unified_diff(
+ current.splitlines(keepends=True),
+ expected.splitlines(keepends=True),
+ fromfile=f"vendored/{vendor_name}",
+ tofile=f"{crate}@{version}/proto/{src_name}",
+ )
+ sys.stderr.writelines(diff)
+ else:
+ dest.write_text(expected)
+ print(f"updated {dest.relative_to(REPO_ROOT)} from
{crate}@{version}")
+
+ if args.check and stale:
+ sys.exit(
+ "\nVendored DataFusion proto file(s) out of date: "
+ + ", ".join(stale)
+ + "\nRun `dev/update_datafusion_proto.py` and commit the result."
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/dev/update_datafusion_versions.py
b/dev/update_datafusion_versions.py
index aed2cee3a..d21096ea1 100755
--- a/dev/update_datafusion_versions.py
+++ b/dev/update_datafusion_versions.py
@@ -133,6 +133,14 @@ def main():
update_docs("README.md", new_version)
update_pyproject_toml(os.path.join(repo_root, "python", "pyproject.toml"),
new_version)
+ print(
+ '\nNext steps:\n'
+ ' 1. Run `cargo update` (or `cargo build`) so Cargo.lock resolves the
new '
+ 'datafusion crates.\n'
+ ' 2. Run `dev/update_datafusion_proto.py` to re-sync the vendored '
+ 'DataFusion proto files, then commit the result.'
+ )
+
if __name__ == "__main__":
main()
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]