blob: 63962998ad18b0c3dd1697741fb37973741c96f0 [file]
// 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.
//! FunctionalDependencies keeps track of functional dependencies
//! inside DFSchema.
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::vec::IntoIter;
use crate::utils::{merge_and_order_indices, set_difference};
use crate::{DFSchema, HashSet, JoinType};
/// This object defines a constraint on a table.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)]
pub enum Constraint {
/// Columns with the given indices form a composite primary key (they are
/// jointly unique and not nullable):
PrimaryKey(Vec<usize>),
/// Columns with the given indices form a composite unique key:
Unique(Vec<usize>),
}
/// This object encapsulates a list of functional constraints:
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd)]
pub struct Constraints {
inner: Vec<Constraint>,
}
impl Constraints {
/// Create a new [`Constraints`] object from the given `constraints`.
/// Users should use the [`Constraints::default`] or [`SqlToRel::new_constraint_from_table_constraints`]
/// functions for constructing [`Constraints`] instances. This constructor
/// is for internal purposes only and does not check whether the argument
/// is valid. The user is responsible for supplying a valid vector of
/// [`Constraint`] objects.
///
/// [`SqlToRel::new_constraint_from_table_constraints`]: https://docs.rs/datafusion/latest/datafusion/sql/planner/struct.SqlToRel.html#method.new_constraint_from_table_constraints
pub fn new_unverified(constraints: Vec<Constraint>) -> Self {
Self { inner: constraints }
}
/// Extends the current constraints with the given `other` constraints.
pub fn extend(&mut self, other: Constraints) {
self.inner.extend(other.inner);
}
/// Projects constraints using the given projection indices. Returns `None`
/// if any of the constraint columns are not included in the projection.
pub fn project(&self, proj_indices: &[usize]) -> Option<Self> {
let projected = self
.inner
.iter()
.filter_map(|constraint| {
match constraint {
Constraint::PrimaryKey(indices) => {
let new_indices =
update_elements_with_matching_indices(indices, proj_indices);
// Only keep the constraint if all columns are preserved:
(new_indices.len() == indices.len())
.then_some(Constraint::PrimaryKey(new_indices))
}
Constraint::Unique(indices) => {
let new_indices =
update_elements_with_matching_indices(indices, proj_indices);
// Only keep the constraint if all columns are preserved:
(new_indices.len() == indices.len())
.then_some(Constraint::Unique(new_indices))
}
}
})
.collect::<Vec<_>>();
(!projected.is_empty()).then_some(Constraints::new_unverified(projected))
}
}
impl IntoIterator for Constraints {
type Item = Constraint;
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.inner.into_iter()
}
}
impl Display for Constraints {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let pk = self
.inner
.iter()
.map(|c| format!("{c:?}"))
.collect::<Vec<_>>();
let pk = pk.join(", ");
write!(f, "constraints=[{pk}]")
}
}
impl Deref for Constraints {
type Target = [Constraint];
fn deref(&self) -> &Self::Target {
self.inner.as_slice()
}
}
/// This object defines a functional dependence in the schema. A functional
/// dependence defines a relationship between determinant keys and dependent
/// columns. A determinant key is a column, or a set of columns, whose value
/// uniquely determines values of some other (dependent) columns. If two rows
/// have the same determinant key, dependent columns in these rows are
/// necessarily the same. If the determinant key is unique, the set of
/// dependent columns is equal to the entire schema and the determinant key can
/// serve as a primary key. Note that a primary key may "downgrade" into a
/// determinant key due to an operation such as a join, and this object is
/// used to track dependence relationships in such cases. For more information
/// on functional dependencies, see:
/// <https://www.scaler.com/topics/dbms/functional-dependency-in-dbms/>
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FunctionalDependence {
// Column indices of the (possibly composite) determinant key:
pub source_indices: Vec<usize>,
// Column indices of dependent column(s):
pub target_indices: Vec<usize>,
/// Flag indicating whether one of the `source_indices` can receive NULL values.
/// For a data source, if the constraint in question is `Constraint::Unique`,
/// this flag is `true`. If the constraint in question is `Constraint::PrimaryKey`,
/// this flag is `false`.
/// Note that as the schema changes between different stages in a plan,
/// such as after LEFT JOIN or RIGHT JOIN operations, this property may
/// change.
pub nullable: bool,
// The functional dependency mode:
pub mode: Dependency,
}
/// Describes functional dependency mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dependency {
Single, // A determinant key may occur only once.
Multi, // A determinant key may occur multiple times (in multiple rows).
}
impl FunctionalDependence {
// Creates a new functional dependence.
pub fn new(
source_indices: Vec<usize>,
target_indices: Vec<usize>,
nullable: bool,
) -> Self {
Self {
source_indices,
target_indices,
nullable,
// Start with the least restrictive mode by default:
mode: Dependency::Multi,
}
}
pub fn with_mode(mut self, mode: Dependency) -> Self {
self.mode = mode;
self
}
}
/// This object encapsulates all functional dependencies in a given relation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FunctionalDependencies {
deps: Vec<FunctionalDependence>,
}
impl FunctionalDependencies {
/// Creates an empty `FunctionalDependencies` object.
pub fn empty() -> Self {
Self { deps: vec![] }
}
/// Creates a new `FunctionalDependencies` object from a vector of
/// `FunctionalDependence` objects.
pub fn new(dependencies: Vec<FunctionalDependence>) -> Self {
Self { deps: dependencies }
}
/// Creates a new `FunctionalDependencies` object from the given constraints.
pub fn new_from_constraints(
constraints: Option<&Constraints>,
n_field: usize,
) -> Self {
if let Some(Constraints { inner: constraints }) = constraints {
// Construct dependency objects based on each individual constraint:
let dependencies = constraints
.iter()
.map(|constraint| {
// All the field indices are associated with the whole table
// since we are dealing with table level constraints:
let dependency = match constraint {
Constraint::PrimaryKey(indices) => FunctionalDependence::new(
indices.to_vec(),
(0..n_field).collect::<Vec<_>>(),
false,
),
Constraint::Unique(indices) => FunctionalDependence::new(
indices.to_vec(),
(0..n_field).collect::<Vec<_>>(),
true,
),
};
// As primary keys are guaranteed to be unique, set the
// functional dependency mode to `Dependency::Single`:
dependency.with_mode(Dependency::Single)
})
.collect::<Vec<_>>();
Self::new(dependencies)
} else {
// There is no constraint, return an empty object:
Self::empty()
}
}
pub fn with_dependency(mut self, mode: Dependency) -> Self {
self.deps.iter_mut().for_each(|item| item.mode = mode);
self
}
/// Merges the given functional dependencies with these.
pub fn extend(&mut self, other: FunctionalDependencies) {
self.deps.extend(other.deps);
}
/// Sanity checks if functional dependencies are valid. For example, if
/// there are 10 fields, we cannot receive any index further than 9.
pub fn is_valid(&self, n_field: usize) -> bool {
self.deps.iter().all(
|FunctionalDependence {
source_indices,
target_indices,
..
}| {
source_indices
.iter()
.max()
.map(|&max_index| max_index < n_field)
.unwrap_or(true)
&& target_indices
.iter()
.max()
.map(|&max_index| max_index < n_field)
.unwrap_or(true)
},
)
}
/// Adds the `offset` value to `source_indices` and `target_indices` for
/// each functional dependency.
pub fn add_offset(&mut self, offset: usize) {
self.deps.iter_mut().for_each(
|FunctionalDependence {
source_indices,
target_indices,
..
}| {
*source_indices = add_offset_to_vec(source_indices, offset);
*target_indices = add_offset_to_vec(target_indices, offset);
},
)
}
/// Updates `source_indices` and `target_indices` of each functional
/// dependence using the index mapping given in `proj_indices`.
///
/// Assume that `proj_indices` is \[2, 5, 8\] and we have a functional
/// dependence \[5\] (`source_indices`) -> \[5, 8\] (`target_indices`).
/// In the updated schema, fields at indices \[2, 5, 8\] will transform
/// to \[0, 1, 2\]. Therefore, the resulting functional dependence will
/// be \[1\] -> \[1, 2\].
pub fn project_functional_dependencies(
&self,
proj_indices: &[usize],
// The argument `n_out` denotes the schema field length, which is needed
// to correctly associate a `Single`-mode dependence with the whole table.
n_out: usize,
) -> FunctionalDependencies {
let mut projected_func_dependencies = vec![];
for FunctionalDependence {
source_indices,
target_indices,
nullable,
mode,
} in &self.deps
{
let new_source_indices =
update_elements_with_matching_indices(source_indices, proj_indices);
let new_target_indices = if *mode == Dependency::Single {
// Associate with all of the fields in the schema:
(0..n_out).collect()
} else {
// Update associations according to projection:
update_elements_with_matching_indices(target_indices, proj_indices)
};
// All of the composite indices should still be valid after projection;
// otherwise, functional dependency cannot be propagated.
if new_source_indices.len() == source_indices.len() {
let new_func_dependence = FunctionalDependence::new(
new_source_indices,
new_target_indices,
*nullable,
)
.with_mode(*mode);
projected_func_dependencies.push(new_func_dependence);
}
}
FunctionalDependencies::new(projected_func_dependencies)
}
/// This function joins this set of functional dependencies with the `other`
/// according to the given `join_type`.
pub fn join(
&self,
other: &FunctionalDependencies,
join_type: &JoinType,
left_cols_len: usize,
) -> FunctionalDependencies {
// Get mutable copies of left and right side dependencies:
let mut right_func_dependencies = other.clone();
let mut left_func_dependencies = self.clone();
match join_type {
JoinType::Inner | JoinType::Left | JoinType::Right => {
// Add offset to right schema:
right_func_dependencies.add_offset(left_cols_len);
// Result may have multiple values, update the dependency mode:
left_func_dependencies =
left_func_dependencies.with_dependency(Dependency::Multi);
right_func_dependencies =
right_func_dependencies.with_dependency(Dependency::Multi);
if *join_type == JoinType::Left {
// Downgrade the right side, since it may have additional NULL values:
right_func_dependencies.downgrade_dependencies();
} else if *join_type == JoinType::Right {
// Downgrade the left side, since it may have additional NULL values:
left_func_dependencies.downgrade_dependencies();
}
// Combine left and right functional dependencies:
left_func_dependencies.extend(right_func_dependencies);
left_func_dependencies
}
JoinType::LeftSemi | JoinType::LeftAnti | JoinType::LeftMark => {
// These joins preserve functional dependencies of the left side:
left_func_dependencies
}
JoinType::RightSemi | JoinType::RightAnti | JoinType::RightMark => {
// These joins preserve functional dependencies of the right side:
right_func_dependencies
}
JoinType::Full => {
// All of the functional dependencies are lost in a FULL join:
FunctionalDependencies::empty()
}
}
}
/// This function downgrades a functional dependency when nullability becomes
/// a possibility:
/// - If the dependency in question is UNIQUE (i.e. nullable), a new null value
/// invalidates the dependency.
/// - If the dependency in question is PRIMARY KEY (i.e. not nullable), a new
/// null value turns it into UNIQUE mode.
fn downgrade_dependencies(&mut self) {
// Delete nullable dependencies, since they are no longer valid:
self.deps.retain(|item| !item.nullable);
self.deps.iter_mut().for_each(|item| item.nullable = true);
}
/// This function ensures that functional dependencies involving uniquely
/// occurring determinant keys cover their entire table in terms of
/// dependent columns.
pub fn extend_target_indices(&mut self, n_out: usize) {
self.deps.iter_mut().for_each(
|FunctionalDependence {
mode,
target_indices,
..
}| {
// If unique, cover the whole table:
if *mode == Dependency::Single {
*target_indices = (0..n_out).collect::<Vec<_>>();
}
},
)
}
}
impl Deref for FunctionalDependencies {
type Target = [FunctionalDependence];
fn deref(&self) -> &Self::Target {
self.deps.as_slice()
}
}
/// Calculates functional dependencies for aggregate output, when there is a GROUP BY expression.
pub fn aggregate_functional_dependencies(
aggr_input_schema: &DFSchema,
group_by_expr_names: &[String],
aggr_schema: &DFSchema,
) -> FunctionalDependencies {
let mut aggregate_func_dependencies = vec![];
let aggr_input_fields = aggr_input_schema.field_names();
let aggr_fields = aggr_schema.fields();
// Association covers the whole table:
let target_indices = (0..aggr_schema.fields().len()).collect::<Vec<_>>();
// Get functional dependencies of the schema:
let func_dependencies = aggr_input_schema.functional_dependencies();
for FunctionalDependence {
source_indices,
nullable,
mode,
..
} in &func_dependencies.deps
{
// Keep source indices in a `HashSet` to prevent duplicate entries:
let mut new_source_indices = vec![];
let mut new_source_field_names = vec![];
let source_field_names = source_indices
.iter()
.map(|&idx| &aggr_input_fields[idx])
.collect::<Vec<_>>();
for (idx, group_by_expr_name) in group_by_expr_names.iter().enumerate() {
// When one of the input determinant expressions matches with
// the GROUP BY expression, add the index of the GROUP BY
// expression as a new determinant key:
if source_field_names.contains(&group_by_expr_name) {
new_source_indices.push(idx);
new_source_field_names.push(group_by_expr_name.clone());
}
}
let existing_target_indices =
get_target_functional_dependencies(aggr_input_schema, group_by_expr_names);
let new_target_indices = get_target_functional_dependencies(
aggr_input_schema,
&new_source_field_names,
);
let mode = if existing_target_indices == new_target_indices
&& new_target_indices.is_some()
{
// If dependency covers all GROUP BY expressions, mode will be `Single`:
Dependency::Single
} else {
// Otherwise, existing mode is preserved:
*mode
};
// All of the composite indices occur in the GROUP BY expression:
if new_source_indices.len() == source_indices.len() {
aggregate_func_dependencies.push(
FunctionalDependence::new(
new_source_indices,
target_indices.clone(),
*nullable,
)
.with_mode(mode),
);
}
}
// When we have a GROUP BY key, we can guarantee uniqueness after
// aggregation:
if !group_by_expr_names.is_empty() {
let count = group_by_expr_names.len();
let source_indices = (0..count).collect::<Vec<_>>();
let nullable = source_indices
.iter()
.any(|idx| aggr_fields[*idx].is_nullable());
// If GROUP BY expressions do not already act as a determinant:
if !aggregate_func_dependencies.iter().any(|item| {
// If `item.source_indices` is a subset of GROUP BY expressions, we shouldn't add
// them since `item.source_indices` defines this relation already.
// The following simple comparison is working well because
// GROUP BY expressions come here as a prefix.
item.source_indices.iter().all(|idx| idx < &count)
}) {
// Add a new functional dependency associated with the whole table:
// Use nullable property of the GROUP BY expression:
aggregate_func_dependencies.push(
// Use nullable property of the GROUP BY expression:
FunctionalDependence::new(source_indices, target_indices, nullable)
.with_mode(Dependency::Single),
);
}
}
FunctionalDependencies::new(aggregate_func_dependencies)
}
/// Returns target indices, for the determinant keys that are inside
/// group by expressions.
pub fn get_target_functional_dependencies(
schema: &DFSchema,
group_by_expr_names: &[String],
) -> Option<Vec<usize>> {
let mut combined_target_indices = HashSet::new();
let dependencies = schema.functional_dependencies();
let field_names = schema.field_names();
for FunctionalDependence {
source_indices,
target_indices,
..
} in &dependencies.deps
{
let source_key_names = source_indices
.iter()
.map(|id_key_idx| &field_names[*id_key_idx])
.collect::<Vec<_>>();
// If the GROUP BY expression contains a determinant key, we can use
// the associated fields after aggregation even if they are not part
// of the GROUP BY expression.
if source_key_names
.iter()
.all(|source_key_name| group_by_expr_names.contains(source_key_name))
{
combined_target_indices.extend(target_indices.iter());
}
}
(!combined_target_indices.is_empty()).then_some({
let mut result = combined_target_indices.into_iter().collect::<Vec<_>>();
result.sort();
result
})
}
/// Returns indices for the minimal subset of GROUP BY expressions that are
/// functionally equivalent to the original set of GROUP BY expressions.
pub fn get_required_group_by_exprs_indices(
schema: &DFSchema,
group_by_expr_names: &[String],
) -> Option<Vec<usize>> {
let dependencies = schema.functional_dependencies();
let field_names = schema.field_names();
let mut groupby_expr_indices = group_by_expr_names
.iter()
.map(|group_by_expr_name| {
field_names
.iter()
.position(|field_name| field_name == group_by_expr_name)
})
.collect::<Option<Vec<_>>>()?;
groupby_expr_indices.sort();
for FunctionalDependence {
source_indices,
target_indices,
..
} in &dependencies.deps
{
if source_indices
.iter()
.all(|source_idx| groupby_expr_indices.contains(source_idx))
{
// If all source indices are among GROUP BY expression indices, we
// can remove target indices from GROUP BY expression indices and
// use source indices instead.
groupby_expr_indices = set_difference(&groupby_expr_indices, target_indices);
groupby_expr_indices =
merge_and_order_indices(groupby_expr_indices, source_indices);
}
}
groupby_expr_indices
.iter()
.map(|idx| {
group_by_expr_names
.iter()
.position(|name| &field_names[*idx] == name)
})
.collect()
}
/// Updates entries inside the `entries` vector with their corresponding
/// indices inside the `proj_indices` vector.
fn update_elements_with_matching_indices(
entries: &[usize],
proj_indices: &[usize],
) -> Vec<usize> {
entries
.iter()
.filter_map(|val| proj_indices.iter().position(|proj_idx| proj_idx == val))
.collect()
}
/// Adds `offset` value to each entry inside `in_data`.
fn add_offset_to_vec<T: Copy + std::ops::Add<Output = T>>(
in_data: &[T],
offset: T,
) -> Vec<T> {
in_data.iter().map(|&item| item + offset).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constraints_iter() {
let constraints = Constraints::new_unverified(vec![
Constraint::PrimaryKey(vec![10]),
Constraint::Unique(vec![20]),
]);
let mut iter = constraints.iter();
assert_eq!(iter.next(), Some(&Constraint::PrimaryKey(vec![10])));
assert_eq!(iter.next(), Some(&Constraint::Unique(vec![20])));
assert_eq!(iter.next(), None);
}
#[test]
fn test_project_constraints() {
let constraints = Constraints::new_unverified(vec![
Constraint::PrimaryKey(vec![1, 2]),
Constraint::Unique(vec![0, 3]),
]);
// Project keeping columns 1,2,3
let projected = constraints.project(&[1, 2, 3]).unwrap();
assert_eq!(
projected,
Constraints::new_unverified(vec![Constraint::PrimaryKey(vec![0, 1])])
);
// Project keeping only column 0 - should return None as no constraints are preserved
assert!(constraints.project(&[0]).is_none());
}
#[test]
fn test_get_updated_id_keys() {
let fund_dependencies =
FunctionalDependencies::new(vec![FunctionalDependence::new(
vec![1],
vec![0, 1, 2],
true,
)]);
let res = fund_dependencies.project_functional_dependencies(&[1, 2], 2);
let expected = FunctionalDependencies::new(vec![FunctionalDependence::new(
vec![0],
vec![0, 1],
true,
)]);
assert_eq!(res, expected);
}
}