Skip to main content

datafusion_common/
functional_dependencies.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! FunctionalDependencies keeps track of functional dependencies
19//! inside DFSchema.
20
21use std::fmt::{Display, Formatter};
22use std::ops::Deref;
23use std::vec::IntoIter;
24
25use crate::utils::{merge_and_order_indices, set_difference};
26use crate::{DFSchema, HashSet, JoinType};
27
28/// This object defines a constraint on a table.
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)]
30pub enum Constraint {
31    /// Columns with the given indices form a composite primary key (they are
32    /// jointly unique and not nullable):
33    PrimaryKey(Vec<usize>),
34    /// Columns with the given indices form a composite unique key:
35    Unique(Vec<usize>),
36}
37
38/// This object encapsulates a list of functional constraints:
39#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd)]
40pub struct Constraints {
41    inner: Vec<Constraint>,
42}
43
44impl Constraints {
45    /// Create a new [`Constraints`] object from the given `constraints`.
46    /// Users should use the [`Constraints::default`] or [`SqlToRel::new_constraint_from_table_constraints`]
47    /// functions for constructing [`Constraints`] instances. This constructor
48    /// is for internal purposes only and does not check whether the argument
49    /// is valid. The user is responsible for supplying a valid vector of
50    /// [`Constraint`] objects.
51    ///
52    /// [`SqlToRel::new_constraint_from_table_constraints`]: https://docs.rs/datafusion/latest/datafusion/sql/planner/struct.SqlToRel.html#method.new_constraint_from_table_constraints
53    pub fn new_unverified(constraints: Vec<Constraint>) -> Self {
54        Self { inner: constraints }
55    }
56
57    /// Extends the current constraints with the given `other` constraints.
58    pub fn extend(&mut self, other: Constraints) {
59        self.inner.extend(other.inner);
60    }
61
62    /// Projects constraints using the given projection indices. Returns `None`
63    /// if any of the constraint columns are not included in the projection.
64    pub fn project(&self, proj_indices: &[usize]) -> Option<Self> {
65        let projected = self
66            .inner
67            .iter()
68            .filter_map(|constraint| {
69                match constraint {
70                    Constraint::PrimaryKey(indices) => {
71                        let new_indices =
72                            update_elements_with_matching_indices(indices, proj_indices);
73                        // Only keep the constraint if all columns are preserved:
74                        (new_indices.len() == indices.len())
75                            .then_some(Constraint::PrimaryKey(new_indices))
76                    }
77                    Constraint::Unique(indices) => {
78                        let new_indices =
79                            update_elements_with_matching_indices(indices, proj_indices);
80                        // Only keep the constraint if all columns are preserved:
81                        (new_indices.len() == indices.len())
82                            .then_some(Constraint::Unique(new_indices))
83                    }
84                }
85            })
86            .collect::<Vec<_>>();
87
88        (!projected.is_empty()).then_some(Constraints::new_unverified(projected))
89    }
90}
91
92impl IntoIterator for Constraints {
93    type Item = Constraint;
94    type IntoIter = IntoIter<Self::Item>;
95
96    fn into_iter(self) -> Self::IntoIter {
97        self.inner.into_iter()
98    }
99}
100
101impl Display for Constraints {
102    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
103        let pk = self
104            .inner
105            .iter()
106            .map(|c| format!("{c:?}"))
107            .collect::<Vec<_>>();
108        let pk = pk.join(", ");
109        write!(f, "constraints=[{pk}]")
110    }
111}
112
113impl Deref for Constraints {
114    type Target = [Constraint];
115
116    fn deref(&self) -> &Self::Target {
117        self.inner.as_slice()
118    }
119}
120
121/// This object defines a functional dependence in the schema. A functional
122/// dependence defines a relationship between determinant keys and dependent
123/// columns. A determinant key is a column, or a set of columns, whose value
124/// uniquely determines values of some other (dependent) columns. If two rows
125/// have the same determinant key, dependent columns in these rows are
126/// necessarily the same. If the determinant key is unique, the set of
127/// dependent columns is equal to the entire schema and the determinant key can
128/// serve as a primary key. Note that a primary key may "downgrade" into a
129/// determinant key due to an operation such as a join, and this object is
130/// used to track dependence relationships in such cases. For more information
131/// on functional dependencies, see:
132/// <https://www.scaler.com/topics/dbms/functional-dependency-in-dbms/>
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct FunctionalDependence {
135    // Column indices of the (possibly composite) determinant key:
136    pub source_indices: Vec<usize>,
137    // Column indices of dependent column(s):
138    pub target_indices: Vec<usize>,
139    /// Flag indicating whether one of the `source_indices` can receive NULL values.
140    /// For a data source, if the constraint in question is `Constraint::Unique`,
141    /// this flag is `true`. If the constraint in question is `Constraint::PrimaryKey`,
142    /// this flag is `false`.
143    /// Note that as the schema changes between different stages in a plan,
144    /// such as after LEFT JOIN or RIGHT JOIN operations, this property may
145    /// change.
146    pub nullable: bool,
147    // The functional dependency mode:
148    pub mode: Dependency,
149}
150
151/// Describes functional dependency mode.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum Dependency {
154    /// A determinant key may occur only once.
155    Single,
156    /// A determinant key may occur multiple times (in multiple rows).
157    Multi,
158}
159
160impl FunctionalDependence {
161    // Creates a new functional dependence.
162    pub fn new(
163        source_indices: Vec<usize>,
164        target_indices: Vec<usize>,
165        nullable: bool,
166    ) -> Self {
167        Self {
168            source_indices,
169            target_indices,
170            nullable,
171            // Start with the least restrictive mode by default:
172            mode: Dependency::Multi,
173        }
174    }
175
176    pub fn with_mode(mut self, mode: Dependency) -> Self {
177        self.mode = mode;
178        self
179    }
180}
181
182/// This object encapsulates all functional dependencies in a given relation.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct FunctionalDependencies {
185    deps: Vec<FunctionalDependence>,
186}
187
188impl FunctionalDependencies {
189    /// Creates an empty `FunctionalDependencies` object.
190    pub fn empty() -> Self {
191        Self { deps: vec![] }
192    }
193
194    /// Creates a new `FunctionalDependencies` object from a vector of
195    /// `FunctionalDependence` objects.
196    pub fn new(dependencies: Vec<FunctionalDependence>) -> Self {
197        Self { deps: dependencies }
198    }
199
200    /// Creates a new `FunctionalDependencies` object from the given constraints.
201    pub fn new_from_constraints(
202        constraints: Option<&Constraints>,
203        n_field: usize,
204    ) -> Self {
205        if let Some(Constraints { inner: constraints }) = constraints {
206            // Construct dependency objects based on each individual constraint:
207            let dependencies = constraints
208                .iter()
209                .map(|constraint| {
210                    // All the field indices are associated with the whole table
211                    // since we are dealing with table level constraints:
212                    let dependency = match constraint {
213                        Constraint::PrimaryKey(indices) => FunctionalDependence::new(
214                            indices.to_vec(),
215                            (0..n_field).collect::<Vec<_>>(),
216                            false,
217                        ),
218                        Constraint::Unique(indices) => FunctionalDependence::new(
219                            indices.to_vec(),
220                            (0..n_field).collect::<Vec<_>>(),
221                            true,
222                        ),
223                    };
224                    // As primary keys are guaranteed to be unique, set the
225                    // functional dependency mode to `Dependency::Single`:
226                    dependency.with_mode(Dependency::Single)
227                })
228                .collect::<Vec<_>>();
229            Self::new(dependencies)
230        } else {
231            // There is no constraint, return an empty object:
232            Self::empty()
233        }
234    }
235
236    pub fn with_dependency(mut self, mode: Dependency) -> Self {
237        self.deps.iter_mut().for_each(|item| item.mode = mode);
238        self
239    }
240
241    /// Merges the given functional dependencies with these.
242    pub fn extend(&mut self, other: FunctionalDependencies) {
243        self.deps.extend(other.deps);
244    }
245
246    /// Sanity checks if functional dependencies are valid. For example, if
247    /// there are 10 fields, we cannot receive any index further than 9.
248    pub fn is_valid(&self, n_field: usize) -> bool {
249        self.deps.iter().all(
250            |FunctionalDependence {
251                 source_indices,
252                 target_indices,
253                 ..
254             }| {
255                source_indices
256                    .iter()
257                    .max()
258                    .map(|&max_index| max_index < n_field)
259                    .unwrap_or(true)
260                    && target_indices
261                        .iter()
262                        .max()
263                        .map(|&max_index| max_index < n_field)
264                        .unwrap_or(true)
265            },
266        )
267    }
268
269    /// Adds the `offset` value to `source_indices` and `target_indices` for
270    /// each functional dependency.
271    pub fn add_offset(&mut self, offset: usize) {
272        self.deps.iter_mut().for_each(
273            |FunctionalDependence {
274                 source_indices,
275                 target_indices,
276                 ..
277             }| {
278                *source_indices = add_offset_to_vec(source_indices, offset);
279                *target_indices = add_offset_to_vec(target_indices, offset);
280            },
281        )
282    }
283
284    /// Updates `source_indices` and `target_indices` of each functional
285    /// dependence using the index mapping given in `proj_indices`.
286    ///
287    /// Assume that `proj_indices` is \[2, 5, 8\] and we have a functional
288    /// dependence \[5\] (`source_indices`) -> \[5, 8\] (`target_indices`).
289    /// In the updated schema, fields at indices \[2, 5, 8\] will transform
290    /// to \[0, 1, 2\]. Therefore, the resulting functional dependence will
291    /// be \[1\] -> \[1, 2\].
292    pub fn project_functional_dependencies(
293        &self,
294        proj_indices: &[usize],
295        // The argument `n_out` denotes the schema field length, which is needed
296        // to correctly associate a `Single`-mode dependence with the whole table.
297        n_out: usize,
298    ) -> FunctionalDependencies {
299        let mut projected_func_dependencies = vec![];
300        for FunctionalDependence {
301            source_indices,
302            target_indices,
303            nullable,
304            mode,
305        } in &self.deps
306        {
307            let new_source_indices =
308                update_elements_with_matching_indices(source_indices, proj_indices);
309            let new_target_indices = if *mode == Dependency::Single {
310                // Associate with all of the fields in the schema:
311                (0..n_out).collect()
312            } else {
313                // Update associations according to projection:
314                update_elements_with_matching_indices(target_indices, proj_indices)
315            };
316            // All of the composite indices should still be valid after projection;
317            // otherwise, functional dependency cannot be propagated.
318            if new_source_indices.len() == source_indices.len() {
319                let new_func_dependence = FunctionalDependence::new(
320                    new_source_indices,
321                    new_target_indices,
322                    *nullable,
323                )
324                .with_mode(*mode);
325                projected_func_dependencies.push(new_func_dependence);
326            }
327        }
328        FunctionalDependencies::new(projected_func_dependencies)
329    }
330
331    /// This function joins this set of functional dependencies with the `other`
332    /// according to the given `join_type`.
333    pub fn join(
334        &self,
335        other: &FunctionalDependencies,
336        join_type: &JoinType,
337        left_cols_len: usize,
338    ) -> FunctionalDependencies {
339        // Get mutable copies of left and right side dependencies:
340        let mut right_func_dependencies = other.clone();
341        let mut left_func_dependencies = self.clone();
342
343        match join_type {
344            JoinType::Inner | JoinType::Left | JoinType::Right => {
345                // Add offset to right schema:
346                right_func_dependencies.add_offset(left_cols_len);
347
348                // Result may have multiple values, update the dependency mode:
349                left_func_dependencies =
350                    left_func_dependencies.with_dependency(Dependency::Multi);
351                right_func_dependencies =
352                    right_func_dependencies.with_dependency(Dependency::Multi);
353
354                if *join_type == JoinType::Left {
355                    // Downgrade the right side, since it may have additional NULL values:
356                    right_func_dependencies.downgrade_dependencies();
357                } else if *join_type == JoinType::Right {
358                    // Downgrade the left side, since it may have additional NULL values:
359                    left_func_dependencies.downgrade_dependencies();
360                }
361                // Combine left and right functional dependencies:
362                left_func_dependencies.extend(right_func_dependencies);
363                left_func_dependencies
364            }
365            JoinType::LeftSemi | JoinType::LeftAnti | JoinType::LeftMark => {
366                // These joins preserve functional dependencies of the left side:
367                left_func_dependencies
368            }
369            JoinType::RightSemi | JoinType::RightAnti | JoinType::RightMark => {
370                // These joins preserve functional dependencies of the right side:
371                right_func_dependencies
372            }
373            JoinType::Full => {
374                // All of the functional dependencies are lost in a FULL join:
375                FunctionalDependencies::empty()
376            }
377        }
378    }
379
380    /// This function downgrades a functional dependency when nullability becomes
381    /// a possibility:
382    /// - If the dependency in question is UNIQUE (i.e. nullable), a new null value
383    ///   invalidates the dependency.
384    /// - If the dependency in question is PRIMARY KEY (i.e. not nullable), a new
385    ///   null value turns it into UNIQUE mode.
386    fn downgrade_dependencies(&mut self) {
387        // Delete nullable dependencies, since they are no longer valid:
388        self.deps.retain(|item| !item.nullable);
389        self.deps.iter_mut().for_each(|item| item.nullable = true);
390    }
391
392    /// This function ensures that functional dependencies involving uniquely
393    /// occurring determinant keys cover their entire table in terms of
394    /// dependent columns.
395    pub fn extend_target_indices(&mut self, n_out: usize) {
396        self.deps.iter_mut().for_each(
397            |FunctionalDependence {
398                 mode,
399                 target_indices,
400                 ..
401             }| {
402                // If unique, cover the whole table:
403                if *mode == Dependency::Single {
404                    *target_indices = (0..n_out).collect::<Vec<_>>();
405                }
406            },
407        )
408    }
409}
410
411impl Deref for FunctionalDependencies {
412    type Target = [FunctionalDependence];
413
414    fn deref(&self) -> &Self::Target {
415        self.deps.as_slice()
416    }
417}
418
419/// Calculates functional dependencies for aggregate output, when there is a GROUP BY expression.
420pub fn aggregate_functional_dependencies(
421    aggr_input_schema: &DFSchema,
422    group_by_expr_names: &[String],
423    aggr_schema: &DFSchema,
424) -> FunctionalDependencies {
425    let mut aggregate_func_dependencies = vec![];
426    let aggr_input_fields = aggr_input_schema.field_names();
427    let aggr_fields = aggr_schema.fields();
428    // Association covers the whole table:
429    let target_indices = (0..aggr_schema.fields().len()).collect::<Vec<_>>();
430    // Get functional dependencies of the schema:
431    let func_dependencies = aggr_input_schema.functional_dependencies();
432    for FunctionalDependence {
433        source_indices,
434        nullable,
435        mode,
436        ..
437    } in &func_dependencies.deps
438    {
439        // Keep source indices in a `HashSet` to prevent duplicate entries:
440        let mut new_source_indices = vec![];
441        let mut new_source_field_names = vec![];
442        let source_field_names = source_indices
443            .iter()
444            .map(|&idx| &aggr_input_fields[idx])
445            .collect::<Vec<_>>();
446
447        for (idx, group_by_expr_name) in group_by_expr_names.iter().enumerate() {
448            // When one of the input determinant expressions matches with
449            // the GROUP BY expression, add the index of the GROUP BY
450            // expression as a new determinant key:
451            if source_field_names.contains(&group_by_expr_name) {
452                new_source_indices.push(idx);
453                new_source_field_names.push(group_by_expr_name.clone());
454            }
455        }
456        let existing_target_indices =
457            get_target_functional_dependencies(aggr_input_schema, group_by_expr_names);
458        let new_target_indices = get_target_functional_dependencies(
459            aggr_input_schema,
460            &new_source_field_names,
461        );
462        let mode = if existing_target_indices == new_target_indices
463            && new_target_indices.is_some()
464        {
465            // If dependency covers all GROUP BY expressions, mode will be `Single`:
466            Dependency::Single
467        } else {
468            // Otherwise, existing mode is preserved:
469            *mode
470        };
471        // All of the composite indices occur in the GROUP BY expression:
472        if new_source_indices.len() == source_indices.len() {
473            aggregate_func_dependencies.push(
474                FunctionalDependence::new(
475                    new_source_indices,
476                    target_indices.clone(),
477                    *nullable,
478                )
479                .with_mode(mode),
480            );
481        }
482    }
483
484    // When we have a GROUP BY key, we can guarantee uniqueness after
485    // aggregation:
486    if !group_by_expr_names.is_empty() {
487        let count = group_by_expr_names.len();
488        let source_indices = (0..count).collect::<Vec<_>>();
489        let nullable = source_indices
490            .iter()
491            .any(|idx| aggr_fields[*idx].is_nullable());
492        // If GROUP BY expressions do not already act as a determinant:
493        if !aggregate_func_dependencies.iter().any(|item| {
494            // If `item.source_indices` is a subset of GROUP BY expressions, we shouldn't add
495            // them since `item.source_indices` defines this relation already.
496
497            // The following simple comparison is working well because
498            // GROUP BY expressions come here as a prefix.
499            item.source_indices.iter().all(|idx| idx < &count)
500        }) {
501            // Add a new functional dependency associated with the whole table:
502            // Use nullable property of the GROUP BY expression:
503            aggregate_func_dependencies.push(
504                // Use nullable property of the GROUP BY expression:
505                FunctionalDependence::new(source_indices, target_indices, nullable)
506                    .with_mode(Dependency::Single),
507            );
508        }
509    }
510    FunctionalDependencies::new(aggregate_func_dependencies)
511}
512
513/// Returns target indices, for the determinant keys that are inside
514/// group by expressions.
515pub fn get_target_functional_dependencies(
516    schema: &DFSchema,
517    group_by_expr_names: &[String],
518) -> Option<Vec<usize>> {
519    let mut combined_target_indices = HashSet::new();
520    let dependencies = schema.functional_dependencies();
521    let field_names = schema.field_names();
522    for FunctionalDependence {
523        source_indices,
524        target_indices,
525        ..
526    } in &dependencies.deps
527    {
528        let source_key_names = source_indices
529            .iter()
530            .map(|id_key_idx| &field_names[*id_key_idx])
531            .collect::<Vec<_>>();
532        // If the GROUP BY expression contains a determinant key, we can use
533        // the associated fields after aggregation even if they are not part
534        // of the GROUP BY expression.
535        if source_key_names
536            .iter()
537            .all(|source_key_name| group_by_expr_names.contains(source_key_name))
538        {
539            combined_target_indices.extend(target_indices.iter());
540        }
541    }
542    (!combined_target_indices.is_empty()).then_some({
543        let mut result = combined_target_indices.into_iter().collect::<Vec<_>>();
544        result.sort();
545        result
546    })
547}
548
549/// Returns indices for the minimal subset of GROUP BY expressions that are
550/// functionally equivalent to the original set of GROUP BY expressions.
551pub fn get_required_group_by_exprs_indices(
552    schema: &DFSchema,
553    group_by_expr_names: &[String],
554) -> Option<Vec<usize>> {
555    let dependencies = schema.functional_dependencies();
556    let field_names = schema.field_names();
557    let mut groupby_expr_indices = group_by_expr_names
558        .iter()
559        .map(|group_by_expr_name| {
560            field_names
561                .iter()
562                .position(|field_name| field_name == group_by_expr_name)
563        })
564        .collect::<Option<Vec<_>>>()?;
565
566    groupby_expr_indices.sort();
567    for FunctionalDependence {
568        source_indices,
569        target_indices,
570        ..
571    } in &dependencies.deps
572    {
573        if source_indices
574            .iter()
575            .all(|source_idx| groupby_expr_indices.contains(source_idx))
576        {
577            // If all source indices are among GROUP BY expression indices, we
578            // can remove target indices from GROUP BY expression indices and
579            // use source indices instead.
580            groupby_expr_indices = set_difference(&groupby_expr_indices, target_indices);
581            groupby_expr_indices =
582                merge_and_order_indices(groupby_expr_indices, source_indices);
583        }
584    }
585    groupby_expr_indices
586        .iter()
587        .map(|idx| {
588            group_by_expr_names
589                .iter()
590                .position(|name| &field_names[*idx] == name)
591        })
592        .collect()
593}
594
595/// Returns indices for the minimal subset of ORDER BY expressions that are
596/// functionally equivalent to the original set of ORDER BY expressions.
597pub fn get_required_sort_exprs_indices(
598    schema: &DFSchema,
599    sort_expr_names: &[String],
600) -> Vec<usize> {
601    let dependencies = schema.functional_dependencies();
602    let field_names = schema.field_names();
603
604    let mut known_field_indices = HashSet::new();
605    let mut required_sort_expr_indices = Vec::new();
606
607    for (sort_expr_idx, sort_expr_name) in sort_expr_names.iter().enumerate() {
608        // If the sort expression doesn't correspond to a known schema field
609        // (e.g. a computed expression), we can't reason about it via functional
610        // dependencies, so conservatively keep it.
611        let Some(field_idx) = field_names
612            .iter()
613            .position(|field_name| field_name == sort_expr_name)
614        else {
615            required_sort_expr_indices.push(sort_expr_idx);
616            continue;
617        };
618
619        // A sort expression is removable if its value is functionally determined
620        // by fields that already appear earlier in the sort order: if the earlier
621        // fields are fixed, this one's value is fixed too, so it adds no ordering
622        // information.
623        let removable = dependencies.deps.iter().any(|dependency| {
624            dependency.target_indices.contains(&field_idx)
625                && dependency
626                    .source_indices
627                    .iter()
628                    .all(|source_idx| known_field_indices.contains(source_idx))
629        });
630
631        if removable {
632            continue;
633        }
634
635        known_field_indices.insert(field_idx);
636        required_sort_expr_indices.push(sort_expr_idx);
637    }
638
639    required_sort_expr_indices
640}
641
642/// Updates entries inside the `entries` vector with their corresponding
643/// indices inside the `proj_indices` vector.
644fn update_elements_with_matching_indices(
645    entries: &[usize],
646    proj_indices: &[usize],
647) -> Vec<usize> {
648    entries
649        .iter()
650        .filter_map(|val| proj_indices.iter().position(|proj_idx| proj_idx == val))
651        .collect()
652}
653
654/// Adds `offset` value to each entry inside `in_data`.
655fn add_offset_to_vec<T: Copy + std::ops::Add<Output = T>>(
656    in_data: &[T],
657    offset: T,
658) -> Vec<T> {
659    in_data.iter().map(|&item| item + offset).collect()
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665
666    #[test]
667    fn constraints_iter() {
668        let constraints = Constraints::new_unverified(vec![
669            Constraint::PrimaryKey(vec![10]),
670            Constraint::Unique(vec![20]),
671        ]);
672        let mut iter = constraints.iter();
673        assert_eq!(iter.next(), Some(&Constraint::PrimaryKey(vec![10])));
674        assert_eq!(iter.next(), Some(&Constraint::Unique(vec![20])));
675        assert_eq!(iter.next(), None);
676    }
677
678    #[test]
679    fn test_project_constraints() {
680        let constraints = Constraints::new_unverified(vec![
681            Constraint::PrimaryKey(vec![1, 2]),
682            Constraint::Unique(vec![0, 3]),
683        ]);
684
685        // Project keeping columns 1,2,3
686        let projected = constraints.project(&[1, 2, 3]).unwrap();
687        assert_eq!(
688            projected,
689            Constraints::new_unverified(vec![Constraint::PrimaryKey(vec![0, 1])])
690        );
691
692        // Project keeping only column 0 - should return None as no constraints are preserved
693        assert!(constraints.project(&[0]).is_none());
694    }
695
696    #[test]
697    fn test_get_updated_id_keys() {
698        let fund_dependencies =
699            FunctionalDependencies::new(vec![FunctionalDependence::new(
700                vec![1],
701                vec![0, 1, 2],
702                true,
703            )]);
704        let res = fund_dependencies.project_functional_dependencies(&[1, 2], 2);
705        let expected = FunctionalDependencies::new(vec![FunctionalDependence::new(
706            vec![0],
707            vec![0, 1],
708            true,
709        )]);
710        assert_eq!(res, expected);
711    }
712}