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}