Skip to content

Commit f94b82f

Browse files
committed
move null_equality to FunctionalDependence struct
1 parent 3f37107 commit f94b82f

2 files changed

Lines changed: 73 additions & 99 deletions

File tree

datafusion/common/src/functional_dependencies.rs

Lines changed: 72 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ pub struct FunctionalDependence {
144144
/// such as after LEFT JOIN or RIGHT JOIN operations, this property may
145145
/// change.
146146
pub nullable: bool,
147+
/// The NULL-comparison semantics under which this dependency holds. The
148+
/// conservative default, [`NullEquality::NullEqualsNothing`], means it
149+
/// holds only across rows whose determinant contains no NULLs; e.g. a
150+
/// nullable `UNIQUE` constraint permits multiple NULL rows that may differ.
151+
/// [`NullEquality::NullEqualsNull`] means it also holds when NULL
152+
/// determinant values are treated as equal; e.g. a `GROUP BY` key.
153+
pub null_equality: NullEquality,
147154
// The functional dependency mode:
148155
pub mode: Dependency,
149156
}
@@ -168,6 +175,8 @@ impl FunctionalDependence {
168175
source_indices,
169176
target_indices,
170177
nullable,
178+
// Assume the dependency does not hold across NULL rows by default:
179+
null_equality: NullEquality::NullEqualsNothing,
171180
// Start with the least restrictive mode by default:
172181
mode: Dependency::Multi,
173182
}
@@ -177,46 +186,43 @@ impl FunctionalDependence {
177186
self.mode = mode;
178187
self
179188
}
189+
190+
pub fn with_null_equality(mut self, null_equality: NullEquality) -> Self {
191+
self.null_equality = null_equality;
192+
self
193+
}
194+
195+
/// Returns `true` if this dependency remains usable for operations that
196+
/// treat NULL determinant values as equal (`GROUP BY`, `DISTINCT` and
197+
/// sorting, which place all NULL keys together): it must hold under
198+
/// NULLs-are-equal semantics, have a non-nullable determinant (e.g. a
199+
/// `PRIMARY KEY`), or have no nullable source field in the given `schema`.
200+
pub fn is_valid_across_nulls(&self, schema: &DFSchema) -> bool {
201+
self.null_equality == NullEquality::NullEqualsNull
202+
|| !self.nullable
203+
|| self
204+
.source_indices
205+
.iter()
206+
.all(|&source_idx| !schema.field(source_idx).is_nullable())
207+
}
180208
}
181209

182210
/// This object encapsulates all functional dependencies in a given relation.
183211
#[derive(Debug, Clone, PartialEq, Eq)]
184212
pub struct FunctionalDependencies {
185213
deps: Vec<FunctionalDependence>,
186-
null_equalities: Vec<NullEquality>,
187214
}
188215

189216
impl FunctionalDependencies {
190217
/// Creates an empty `FunctionalDependencies` object.
191218
pub fn empty() -> Self {
192-
Self {
193-
deps: vec![],
194-
null_equalities: vec![],
195-
}
219+
Self { deps: vec![] }
196220
}
197221

198222
/// Creates a new `FunctionalDependencies` object from a vector of
199223
/// `FunctionalDependence` objects.
200224
pub fn new(dependencies: Vec<FunctionalDependence>) -> Self {
201-
let null_equalities = vec![NullEquality::NullEqualsNothing; dependencies.len()];
202-
Self::new_with_null_equalities(dependencies, null_equalities)
203-
}
204-
205-
fn new_with_null_equalities(
206-
dependencies: Vec<FunctionalDependence>,
207-
null_equalities: Vec<NullEquality>,
208-
) -> Self {
209-
debug_assert_eq!(dependencies.len(), null_equalities.len());
210-
Self {
211-
deps: dependencies,
212-
null_equalities,
213-
}
214-
}
215-
216-
fn iter_with_null_equality(
217-
&self,
218-
) -> impl Iterator<Item = (&FunctionalDependence, NullEquality)> {
219-
self.deps.iter().zip(self.null_equalities.iter().copied())
225+
Self { deps: dependencies }
220226
}
221227

222228
/// Creates a new `FunctionalDependencies` object from the given constraints.
@@ -263,7 +269,6 @@ impl FunctionalDependencies {
263269
/// Merges the given functional dependencies with these.
264270
pub fn extend(&mut self, other: FunctionalDependencies) {
265271
self.deps.extend(other.deps);
266-
self.null_equalities.extend(other.null_equalities);
267272
}
268273

269274
/// Sanity checks if functional dependencies are valid. For example, if
@@ -320,16 +325,13 @@ impl FunctionalDependencies {
320325
n_out: usize,
321326
) -> FunctionalDependencies {
322327
let mut projected_func_dependencies = vec![];
323-
let mut projected_null_equalities = vec![];
324-
for (
325-
FunctionalDependence {
326-
source_indices,
327-
target_indices,
328-
nullable,
329-
mode,
330-
},
328+
for FunctionalDependence {
329+
source_indices,
330+
target_indices,
331+
nullable,
331332
null_equality,
332-
) in self.iter_with_null_equality()
333+
mode,
334+
} in &self.deps
333335
{
334336
let new_source_indices =
335337
update_elements_with_matching_indices(source_indices, proj_indices);
@@ -348,15 +350,12 @@ impl FunctionalDependencies {
348350
new_target_indices,
349351
*nullable,
350352
)
351-
.with_mode(*mode);
353+
.with_mode(*mode)
354+
.with_null_equality(*null_equality);
352355
projected_func_dependencies.push(new_func_dependence);
353-
projected_null_equalities.push(null_equality);
354356
}
355357
}
356-
FunctionalDependencies::new_with_null_equalities(
357-
projected_func_dependencies,
358-
projected_null_equalities,
359-
)
358+
FunctionalDependencies::new(projected_func_dependencies)
360359
}
361360

362361
/// This function joins this set of functional dependencies with the `other`
@@ -415,24 +414,13 @@ impl FunctionalDependencies {
415414
/// - If the dependency in question is PRIMARY KEY (i.e. not nullable), a new
416415
/// null value turns it into UNIQUE mode.
417416
fn downgrade_dependencies(&mut self) {
418-
let dependencies = std::mem::take(&mut self.deps);
419-
let null_equalities = std::mem::take(&mut self.null_equalities);
420-
let mut retained_dependencies = Vec::with_capacity(dependencies.len());
421-
let mut retained_null_equalities = Vec::with_capacity(null_equalities.len());
422-
423-
for (mut dependency, _) in dependencies.into_iter().zip(null_equalities) {
424-
// A dependency whose determinant was already nullable becomes
425-
// invalid. A non-nullable determinant becomes nullable, and NULL
426-
// values introduced by the join are not equal to one another.
427-
if !dependency.nullable {
428-
dependency.nullable = true;
429-
retained_dependencies.push(dependency);
430-
retained_null_equalities.push(NullEquality::NullEqualsNothing);
431-
}
432-
}
433-
434-
self.deps = retained_dependencies;
435-
self.null_equalities = retained_null_equalities;
417+
// Delete nullable dependencies, since they are no longer valid:
418+
self.deps.retain(|item| !item.nullable);
419+
// Survivors become nullable, and the new NULLs are not equal to one another:
420+
self.deps.iter_mut().for_each(|item| {
421+
item.nullable = true;
422+
item.null_equality = NullEquality::NullEqualsNothing;
423+
});
436424
}
437425

438426
/// This function ensures that functional dependencies involving uniquely
@@ -469,22 +457,19 @@ pub fn aggregate_functional_dependencies(
469457
aggr_schema: &DFSchema,
470458
) -> FunctionalDependencies {
471459
let mut aggregate_func_dependencies = vec![];
472-
let mut aggregate_null_equalities = vec![];
473460
let aggr_input_fields = aggr_input_schema.field_names();
474461
let aggr_fields = aggr_schema.fields();
475462
// Association covers the whole table:
476463
let target_indices = (0..aggr_schema.fields().len()).collect::<Vec<_>>();
477464
// Get functional dependencies of the schema:
478465
let func_dependencies = aggr_input_schema.functional_dependencies();
479-
for (
480-
FunctionalDependence {
481-
source_indices,
482-
nullable,
483-
mode,
484-
..
485-
},
486-
input_null_equality,
487-
) in func_dependencies.iter_with_null_equality()
466+
for FunctionalDependence {
467+
source_indices,
468+
nullable,
469+
null_equality,
470+
mode,
471+
..
472+
} in &func_dependencies.deps
488473
{
489474
// Keep source indices in a `HashSet` to prevent duplicate entries:
490475
let mut new_source_indices = vec![];
@@ -520,24 +505,23 @@ pub fn aggregate_functional_dependencies(
520505
};
521506
// All of the composite indices occur in the GROUP BY expression:
522507
if new_source_indices.len() == source_indices.len() {
523-
// GROUP BY treats NULL values as equal. When this determinant
524-
// covers the complete grouping key, at most one output row exists
525-
// for its NULL value too.
508+
// GROUP BY treats NULLs as equal: a determinant covering the
509+
// complete grouping key gets at most one output row per NULL too.
526510
let output_null_equality =
527511
if new_source_indices.len() == group_by_expr_names.len() {
528512
NullEquality::NullEqualsNull
529513
} else {
530-
input_null_equality
514+
*null_equality
531515
};
532516
aggregate_func_dependencies.push(
533517
FunctionalDependence::new(
534518
new_source_indices,
535519
target_indices.clone(),
536520
*nullable,
537521
)
538-
.with_mode(mode),
522+
.with_mode(mode)
523+
.with_null_equality(output_null_equality),
539524
);
540-
aggregate_null_equalities.push(output_null_equality);
541525
}
542526
}
543527

@@ -562,15 +546,13 @@ pub fn aggregate_functional_dependencies(
562546
// Use nullable property of the GROUP BY expression:
563547
aggregate_func_dependencies.push(
564548
FunctionalDependence::new(source_indices, target_indices, nullable)
565-
.with_mode(Dependency::Single),
549+
.with_mode(Dependency::Single)
550+
// Grouping collapses NULL keys into a single group:
551+
.with_null_equality(NullEquality::NullEqualsNull),
566552
);
567-
aggregate_null_equalities.push(NullEquality::NullEqualsNull);
568553
}
569554
}
570-
FunctionalDependencies::new_with_null_equalities(
571-
aggregate_func_dependencies,
572-
aggregate_null_equalities,
573-
)
555+
FunctionalDependencies::new(aggregate_func_dependencies)
574556
}
575557

576558
/// Returns target indices, for the determinant keys that are inside
@@ -680,24 +662,16 @@ pub fn get_required_sort_exprs_indices(
680662
};
681663

682664
// A sort expression is removable if its value is functionally determined
683-
// by fields that already appear earlier in the sort order. A nullable
684-
// dependency is only valid here when its determinant treats NULLs as
685-
// equal (for example, a GROUP BY key), or no determinant field can be NULL.
686-
let removable =
687-
dependencies
688-
.iter_with_null_equality()
689-
.any(|(dependency, null_equality)| {
690-
dependency.target_indices.contains(&field_idx)
691-
&& dependency
692-
.source_indices
693-
.iter()
694-
.all(|source_idx| known_field_indices.contains(source_idx))
695-
&& (null_equality == NullEquality::NullEqualsNull
696-
|| !dependency.nullable
697-
|| dependency.source_indices.iter().all(|&source_idx| {
698-
!schema.field(source_idx).is_nullable()
699-
}))
700-
});
665+
// by fields that already appear earlier in the sort order (and the
666+
// dependency remains valid across NULL rows).
667+
let removable = dependencies.deps.iter().any(|dependency| {
668+
dependency.target_indices.contains(&field_idx)
669+
&& dependency
670+
.source_indices
671+
.iter()
672+
.all(|source_idx| known_field_indices.contains(source_idx))
673+
&& dependency.is_valid_across_nulls(schema)
674+
});
701675

702676
if removable {
703677
continue;

datafusion/core/src/physical_planner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4142,7 +4142,7 @@ mod tests {
41424142
.expect_err("planning error")
41434143
.strip_backtrace();
41444144

4145-
insta::assert_snapshot!(e, @r#"Error during planning: Extension planner for NoOp created an ExecutionPlan with mismatched schema. LogicalPlan schema: DFSchema { inner: Schema { fields: [Field { name: "a", data_type: Int32 }], metadata: {} }, field_qualifiers: [None], functional_dependencies: FunctionalDependencies { deps: [], null_equalities: [] } }, ExecutionPlan schema: Schema { fields: [Field { name: "b", data_type: Int32 }], metadata: {} }"#);
4145+
insta::assert_snapshot!(e, @r#"Error during planning: Extension planner for NoOp created an ExecutionPlan with mismatched schema. LogicalPlan schema: DFSchema { inner: Schema { fields: [Field { name: "a", data_type: Int32 }], metadata: {} }, field_qualifiers: [None], functional_dependencies: FunctionalDependencies { deps: [] } }, ExecutionPlan schema: Schema { fields: [Field { name: "b", data_type: Int32 }], metadata: {} }"#);
41464146
}
41474147

41484148
#[tokio::test]

0 commit comments

Comments
 (0)