Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
40789f6
feat: analyze range transformation locality
benbellick Sep 18, 2026
c4d8ecb
docs: explain range transformation locality proof
benbellick Sep 18, 2026
cb3ec10
docs: clarify adjacent range boundary checks
benbellick Sep 18, 2026
568f77e
refactor: make adjacent value checks explicit
benbellick Sep 18, 2026
cab108c
refactor: clarify adjacent input validation
benbellick Sep 18, 2026
5bd8a85
test: expand checked adjacent value coverage
benbellick Sep 18, 2026
45824bf
test: focus checked adjacent value coverage
benbellick Sep 18, 2026
870cf1a
test: simplify checked adjacent value coverage
benbellick Sep 18, 2026
80f5979
docs: clarify range analysis capability goal
benbellick Sep 18, 2026
e43fa5a
refactor: separate range key locality analysis
benbellick Sep 20, 2026
8bdbc69
test: verify range analysis is denied by default
benbellick Sep 20, 2026
12aa6a4
test: focus range analysis contract
benbellick Sep 20, 2026
e8cff7d
docs: simplify range locality contracts
benbellick Sep 20, 2026
14308b9
docs: clarify range analysis null contract
benbellick Sep 20, 2026
1f266d4
docs: use ordered metadata terminology
benbellick Sep 20, 2026
a9a9ee5
refactor: simplify range analysis type gate
benbellick Sep 20, 2026
7e18d65
test: clarify date trunc capability fixture
benbellick Sep 20, 2026
5bd1290
test: document date trunc range analysis contract
benbellick Sep 20, 2026
74232b7
test: focus date trunc audit on null contract
benbellick Sep 20, 2026
7d5b0c5
test: restore audited date trunc range contract
benbellick Sep 20, 2026
1a51496
docs: illustrate timezone ordering reversal
benbellick Sep 20, 2026
e99e608
test: separate date trunc range contracts
benbellick Sep 20, 2026
ca929ca
feat: reuse range locality for aggregation
benbellick Sep 18, 2026
73dff8a
refactor: consume explicit range key locality
benbellick Sep 20, 2026
ea70fd8
docs: explain default-deny range analysis policy
benbellick Sep 20, 2026
1f46dcb
docs: clarify range partition intervals
benbellick Sep 20, 2026
6e11b1f
refactor: return bool from range locality check
benbellick Sep 20, 2026
8d58a03
refactor: reuse expression equivalence comparison
benbellick Sep 20, 2026
8e18b00
refactor: limit range analysis bridge visibility
benbellick Sep 20, 2026
6982816
docs: clarify strict range boundary proof
benbellick Sep 20, 2026
271c5da
docs: simplify descending boundary explanation
benbellick Sep 20, 2026
fbeab85
fix(expr-common): reject unsafe composite interval adjacency
benbellick Sep 21, 2026
58115e2
chore: remove range date_trunc benchmark
benbellick Sep 21, 2026
a2d87b2
refactor: limit range adjacency to timestamps
benbellick Sep 21, 2026
d3eddfc
test: remove redundant range aggregate coverage
benbellick Sep 21, 2026
7134bde
refactor(optimizer): separate aggregate locality checks
benbellick Sep 21, 2026
994bfca
test: remove redundant date_bin locality check
benbellick Sep 21, 2026
6cce1b4
chore(optimizer): address mutable-key Clippy warnings
benbellick Oct 15, 2026
542a9fa
fix(physical-expr): validate range locality boundaries
benbellick Oct 15, 2026
0ef2204
chore(optimizer): make lint suppressions feature-compatible
benbellick Oct 15, 2026
3e210d0
perf(core): benchmark range-partitioned time aggregation
benbellick Oct 15, 2026
88f5430
chore: remove unrelated lint and comment changes
benbellick Oct 17, 2026
d1840e2
refactor: clarify range-key locality analysis naming and docs
benbellick Oct 17, 2026
125b59c
refactor: clarify locality proof names and time-bin test comments
benbellick Oct 17, 2026
a9919b9
test: clarify range-transform aggregate regression coverage
benbellick Oct 17, 2026
bc2d5f8
test: clarify range-locality boundary and join coverage
benbellick Oct 17, 2026
65c3c78
chore: remove range aggregation benchmark
benbellick Oct 17, 2026
a6482e5
test: update time-bin plans for aggregate clustering terminology
benbellick Oct 17, 2026
c7a7f94
test: cover adjacent timestamp truncation overflow
benbellick Oct 19, 2026
f43f67b
chore: check for circular dependencies with `cargo publish` (#26582)
neilconway Oct 22, 2026
290e263
perf: add a GroupsAccumulator for array_agg(DISTINCT) (#26357)
zhuqi-lucas Oct 22, 2026
3e6058d
chore: skip slow test under `force_hash_collisions` (#26594)
neilconway Oct 22, 2026
a9fc673
feat: decorrelate a window below a correlated equality filter (#26013)
comphead Oct 22, 2026
d090d29
IN LIST: retain short integer, floating-point, and fixed-size binary …
geoffreyclaude Oct 22, 2026
c0ec512
perf: don't build a parquet stream for a file range with no row group…
Braedon-Wooding-Displayr Oct 22, 2026
5026ac4
fix: declare Hive partitioned scans as Range (#25279)
gstamatakis95 Oct 22, 2026
8ce9f60
fix: `PushDownLeafProjections` yields when the extraction cannot move…
adriangb Oct 23, 2026
bf08547
Merge branch 'main' into feat/issue-25344-range-transform-locality
jayzhan211 Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,6 @@ updates:
update-types:
- "minor"
- "patch"
# dev/depcheck is a standalone crate excluded from the workspace, so it is
# not covered by the workspace entry above
- package-ecosystem: cargo
directory: "/dev/depcheck"
schedule:
interval: weekly
target-branch: main
labels: [auto-dependencies]
# the wasm test app has its own npm lockfile
- package-ecosystem: npm
directory: "/datafusion/wasmtest/datafusion-wasm-app"
Expand Down
14 changes: 4 additions & 10 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -980,21 +980,15 @@ jobs:
working-directory: datafusion/proto
run: cargo msrv --output-format json --log-target stdout verify

# Check for dependency cycles between DataFusion crates with `dev/depcheck`.
# Check for dependency cycles between DataFusion crates, which would block
# publishing them to crates.io, with a publishing dry run.
depcheck:
name: Circular Dependency Check
if: ${{ !(github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'apache/datafusion') && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork && github.base_ref == 'main') }}
runs-on: ubuntu-latest
container:
image: amd64/rust
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: true
fetch-depth: 1
- name: Setup Rust toolchain
uses: ./.github/actions/setup-builder
with:
rust-version: stable
uses: ./.github/actions/setup-rustup
- name: Check dependencies
run: bash ci/scripts/check_circular_dependencies.sh
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ members = [
"datafusion/doc",
"xtask",
]
exclude = ["dev/depcheck"]
resolver = "2"

[workspace.package]
Expand Down
30 changes: 23 additions & 7 deletions ci/scripts/check_circular_dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,34 @@
# specific language governing permissions and limitations
# under the License.

# Checks for circular dependencies between DataFusion crates with
# `dev/depcheck`, the same way the "Circular Dependency Check" job does.
# Checks for circular dependencies between DataFusion crates, the same way the
# "Circular Dependency Check" job does.
#
# A cycle prevents publishing the crates to crates.io. A crate can be
# published only after every DataFusion crate it depends on, including through
# dev-dependencies, optional dependencies, and dependencies for other targets,
# so no crate in a cycle can be published first. Cargo refuses to build a
# cycle of normal dependencies, but it allows one that passes through a
# dev-dependency, because a dev-dependency is built only for the crate's own
# tests, examples, and benchmarks. Such a cycle builds and passes tests, and
# fails only at release time.
#
# The check is a publishing dry run. `cargo publish --workspace` packages every
# crate that does not set `publish = false`, in an order where each crate comes
# after the DataFusion crates it depends on, and fails if there is no such
# order. `--dry-run` stops before uploading, and `--no-verify` skips building
# the packages. The dry run also fails on any other problem that would stop the
# crates from being published, such as a dependency without a version.
# `--allow-dirty` lets the check run on a working tree with uncommitted
# changes.

set -euo pipefail

SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"

# depcheck is outside the root workspace and finds the root manifest from its
# own directory, so run Cargo from there.
cd "${ROOT_DIR}/dev/depcheck"
cd "${ROOT_DIR}"

echo "[${SCRIPT_NAME}] \`cargo run --locked\` in dev/depcheck"
cargo run --locked
echo "[${SCRIPT_NAME}] \`cargo publish --workspace --dry-run --no-verify --allow-dirty\`"
cargo publish --workspace --dry-run --no-verify --allow-dirty
53 changes: 32 additions & 21 deletions datafusion/catalog-listing/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ use async_trait::async_trait;
use datafusion_catalog::{ScanArgs, ScanResult, Session, TableProvider};
use datafusion_common::stats::{Precision, is_known_empty};
use datafusion_common::{
Constraints, DFSchema, SchemaExt, Statistics, internal_datafusion_err, plan_err,
project_schema,
Constraints, DFSchema, SchemaExt, SplitPoint, Statistics, internal_datafusion_err,
plan_err, project_schema,
};
use datafusion_datasource::file::FileSource;
use datafusion_datasource::file_groups::FileGroup;
use datafusion_datasource::file_scan_config::{
FileScanConfig, FileScanConfigBuilder, output_partitioning_from_partition_fields,
FileScanConfig, FileScanConfigBuilder, output_range_partitioning_from_split_points,
};
use datafusion_datasource::file_sink_config::{FileOutputMode, FileSinkConfig};
#[expect(deprecated)]
Expand Down Expand Up @@ -69,6 +69,8 @@ pub struct ListFilesResult {
pub statistics: Statistics,
/// Whether files are grouped by partition values.
pub grouped_by_partition: bool,
/// Boundaries between file groups, empty unless `grouped_by_partition` is true.
pub partition_split_points: Vec<SplitPoint>,
}

/// Built in [`TableProvider`] that reads data from one or more files as a single table.
Expand Down Expand Up @@ -668,6 +670,7 @@ impl ListingTable {
file_groups: mut partitioned_file_lists,
statistics,
grouped_by_partition: partitioned_by_file_group,
partition_split_points,
} = self
.list_files_for_scan(state, &partition_filters, statistic_file_limit)
.await?;
Expand All @@ -687,6 +690,7 @@ impl ListingTable {
.config_options()
.execution
.split_file_groups_by_statistics;
let mut regrouped_by_statistics = false;
match split_file_groups_by_statistics
.then(|| {
output_ordering.first().map(|output_ordering| {
Expand All @@ -704,6 +708,7 @@ impl ListingTable {
Some(Ok(new_groups)) => {
if new_groups.len() <= file_group_count {
partitioned_file_lists = new_groups;
regrouped_by_statistics = true;
} else {
log::debug!(
"attempted to split file groups by statistics, but there were more file groups than target_partitions; falling back to unordered"
Expand Down Expand Up @@ -737,26 +742,25 @@ impl ListingTable {
)?
}
};
Some(output_partitioning)
} else if partitioned_by_file_group && !regrouped_by_statistics {
output_range_partitioning_from_split_points(
&self.table_schema,
&table_partition_cols.into(),
partition_split_points,
)?
} else {
None
};
if let Some(output_partitioning) = &output_partitioning {
let partition_count = output_partitioning.partition_count();
if partitioned_file_lists.len() != partition_count {
return plan_err!(
"ListingTable output_partitioning has {partition_count} partitions, but the scan has {} file groups",
partitioned_file_lists.len()
);
}
Some(output_partitioning)
} else if partitioned_by_file_group {
// Files are grouped by partition column values: declare output
// partitioning on those columns so the optimizer can skip
// repartitioning for aggregates and joins on the partition columns.
output_partitioning_from_partition_fields(
&self.table_schema,
&table_partition_cols.clone().into(),
partitioned_file_lists.len(),
)
} else {
None
};
}

let Some(object_store_url) =
self.table_paths.first().map(ListingTableUrl::object_store)
Expand Down Expand Up @@ -1005,6 +1009,7 @@ impl ListingTable {
file_groups: vec![],
statistics: Statistics::new_unknown(&self.file_schema),
grouped_by_partition: false,
partition_split_points: vec![],
});
};
let (file_group, inexact_stats) = self
Expand All @@ -1018,28 +1023,31 @@ impl ListingTable {
// skip repartitioning for aggregates and joins on partition columns.
let threshold = ctx.config_options().optimizer.preserve_file_partitions;

let (file_groups, grouped_by_partition) =
let (file_groups, grouped_by_partition, partition_split_points) =
if threshold > 0 && !self.options.table_partition_cols.is_empty() {
let grouped = file_group.group_by_partition_values(file_group_count);
let (grouped, split_points) = file_group
.group_by_partition_values_with_split_points(file_group_count);
if grouped.len() >= threshold {
(grouped, true)
(grouped, true, split_points)
} else {
let all_files: Vec<_> =
grouped.into_iter().flat_map(|g| g.into_inner()).collect();
(
FileGroup::new(all_files).split_files(file_group_count),
false,
vec![],
)
}
} else {
(file_group.split_files(file_group_count), false)
(file_group.split_files(file_group_count), false, vec![])
};

self.list_files_result_from_groups(
ctx,
file_groups,
inexact_stats,
grouped_by_partition,
partition_split_points,
)
}

Expand Down Expand Up @@ -1067,6 +1075,7 @@ impl ListingTable {
file_groups: vec![],
statistics: Statistics::new_unknown(&self.file_schema),
grouped_by_partition: false,
partition_split_points: vec![],
});
};
let (file_group, inexact_stats) =
Expand All @@ -1078,7 +1087,7 @@ impl ListingTable {
let file_groups =
self.filter_declared_file_groups_by_partition_filters(file_groups, filters)?;

self.list_files_result_from_groups(ctx, file_groups, inexact_stats, false)
self.list_files_result_from_groups(ctx, file_groups, inexact_stats, false, vec![])
}

fn filter_declared_file_groups_by_partition_filters(
Expand Down Expand Up @@ -1113,6 +1122,7 @@ impl ListingTable {
file_groups: Vec<FileGroup>,
inexact_stats: bool,
grouped_by_partition: bool,
partition_split_points: Vec<SplitPoint>,
) -> datafusion_common::Result<ListFilesResult> {
let (file_groups, stats) = compute_all_files_statistics(
file_groups,
Expand All @@ -1129,6 +1139,7 @@ impl ListingTable {
file_groups,
statistics: stats,
grouped_by_partition,
partition_split_points,
})
}

Expand Down
22 changes: 11 additions & 11 deletions datafusion/common/src/nested_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,7 @@ fn validate_map_key_data_type(
}
Ok(())
}
_ if is_injective_map_key_cast(source_type, target_type) => Ok(()),
_ if is_injective_cast(source_type, target_type) => Ok(()),
_ => _plan_err!(
"Cannot safely evolve Map key type from {} to {}",
source_type,
Expand All @@ -912,15 +912,15 @@ fn validate_map_key_data_type(
}
}

/// Returns true when casting a Map key from `source_type` to `target_type` is
/// known to preserve key identity.
/// Returns true when casting from `source_type` to `target_type` is known to
/// preserve value identity, so distinct source values stay distinct.
///
/// Keep this list conservative. Map keys define equality/lookup identity, so
/// only representation widenings that cannot merge distinct source keys belong
/// here. Other Arrow-supported casts, including decimal/timestamp widening and
/// dictionaries, should stay rejected until their identity semantics are
/// deliberately reviewed.
fn is_injective_map_key_cast(source_type: &DataType, target_type: &DataType) -> bool {
/// Keep this list conservative. Map keys and decorrelated equality keys depend
/// on identity, so only representation widenings that cannot merge distinct
/// source values belong here. Other Arrow-supported casts, including
/// decimal/timestamp widening and dictionaries, should stay rejected until
/// their identity semantics are deliberately reviewed.
pub fn is_injective_cast(source_type: &DataType, target_type: &DataType) -> bool {
matches!(
(source_type, target_type),
(
Expand Down Expand Up @@ -2283,14 +2283,14 @@ mod tests {
(DataType::UInt16, DataType::Int64),
(DataType::UInt32, DataType::Int64),
] {
assert!(is_injective_map_key_cast(&source_type, &target_type));
assert!(is_injective_cast(&source_type, &target_type));
}
for (source_type, target_type) in [
(DataType::UInt8, DataType::Int8),
(DataType::UInt16, DataType::Int16),
(DataType::UInt32, DataType::Int32),
] {
assert!(!is_injective_map_key_cast(&source_type, &target_type));
assert!(!is_injective_cast(&source_type, &target_type));
}
}

Expand Down
4 changes: 4 additions & 0 deletions datafusion/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ name = "math_query_sql"
harness = false
name = "filter_query_sql"

[[bench]]
harness = false
name = "in_list_rewrite"

[[bench]]
harness = false
name = "struct_query_sql"
Expand Down
Loading