Skip to content

Commit e0d8683

Browse files
committed
store: Fix iterator bug in fields_exist_in_dest for multi-column indexes
fields_exist_in_dest used a single consuming iterator over table columns and checked each index column with .any(), which advances the iterator. When index columns appeared in a different order than the table columns, later lookups would miss columns the iterator had already passed, causing valid multi-column indexes to be silently dropped during subgraph copy/graft. Replace the consuming iterator with direct lookups on dest_table.columns for each column check, so column order no longer matters.
1 parent 2523f13 commit e0d8683

1 file changed

Lines changed: 73 additions & 19 deletions

File tree

store/postgres/src/relational/index.rs

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -647,15 +647,13 @@ impl CreateIndex {
647647
}
648648

649649
pub fn fields_exist_in_dest(&self, dest_table: &Table) -> bool {
650-
fn column_exists<'a>(it: &mut impl Iterator<Item = &'a str>, column_name: &str) -> bool {
651-
it.any(|c| *c == *column_name)
650+
fn column_exists(dest_table: &Table, column_name: &str) -> bool {
651+
dest_table
652+
.columns
653+
.iter()
654+
.any(|c| c.name.as_str() == column_name)
652655
}
653656

654-
fn some_column_contained<'a>(expr: &str, it: &mut impl Iterator<Item = &'a str>) -> bool {
655-
it.any(|c| expr.contains(c))
656-
}
657-
658-
let cols = &mut dest_table.columns.iter().map(|i| i.name.as_str());
659657
match self {
660658
CreateIndex::Unknown { defn: _ } => return true,
661659
CreateIndex::Parsed {
@@ -665,12 +663,12 @@ impl CreateIndex {
665663
for c in parsed_cols {
666664
match c {
667665
Expr::Column(column_name) => {
668-
if !column_exists(cols, column_name) {
666+
if !column_exists(dest_table, column_name) {
669667
return false;
670668
}
671669
}
672670
Expr::Prefix(column_name, _) => {
673-
if !column_exists(cols, column_name) {
671+
if !column_exists(dest_table, column_name) {
674672
return false;
675673
}
676674
}
@@ -686,18 +684,15 @@ impl CreateIndex {
686684
}
687685
}
688686
Expr::Unknown(expression) => {
689-
if some_column_contained(
690-
expression,
691-
&mut (vec!["block_range"]).into_iter(),
692-
) && dest_table.immutable
693-
{
687+
if expression.contains("block_range") && dest_table.immutable {
694688
return false;
695689
}
696-
if !some_column_contained(expression, cols)
697-
&& !some_column_contained(
698-
expression,
699-
&mut (vec!["block_range", "vid"]).into_iter(),
700-
)
690+
if !dest_table
691+
.columns
692+
.iter()
693+
.any(|c| expression.contains(c.name.as_str()))
694+
&& !expression.contains("block_range")
695+
&& !expression.contains("vid")
701696
{
702697
return false;
703698
}
@@ -1155,3 +1150,62 @@ fn parse() {
11551150
};
11561151
parse_one(sql, exp);
11571152
}
1153+
1154+
#[cfg(test)]
1155+
fn test_layout(gql: &str) -> Layout {
1156+
use std::collections::BTreeSet;
1157+
use std::sync::Arc;
1158+
1159+
use graph::prelude::DeploymentHash;
1160+
use graph::schema::InputSchema;
1161+
1162+
use crate::layout_for_tests::{make_dummy_site, Namespace};
1163+
use crate::relational::Catalog;
1164+
1165+
let subgraph = DeploymentHash::new("subgraph").unwrap();
1166+
let schema = InputSchema::parse_latest(gql, subgraph.clone()).expect("Test schema invalid");
1167+
let namespace = Namespace::new("sgd0815".to_owned()).unwrap();
1168+
let site = Arc::new(make_dummy_site(subgraph, namespace, "anet".to_string()));
1169+
let catalog =
1170+
Catalog::for_tests(site.clone(), BTreeSet::new()).expect("Can not create catalog");
1171+
Layout::new(site, &schema, catalog).expect("Failed to construct Layout")
1172+
}
1173+
1174+
#[test]
1175+
fn fields_exist_in_dest_out_of_order() {
1176+
let gql = "type Thing @entity {
1177+
id: Bytes!
1178+
early: String!
1179+
mid: String!
1180+
late: String!
1181+
}";
1182+
let layout = test_layout(gql);
1183+
let table = layout
1184+
.table_for_entity(&layout.input_schema.entity_type("Thing").unwrap())
1185+
.unwrap();
1186+
1187+
// Index references columns in reverse order vs the table definition.
1188+
// Table columns: id, early, mid, late
1189+
// Index columns: late, mid, early
1190+
// The consuming iterator finds 'late', advances past 'mid' and 'early',
1191+
// then can't find 'mid' because the iterator is already past it.
1192+
let index = CreateIndex::Parsed {
1193+
unique: false,
1194+
name: "test_reverse".to_string(),
1195+
nsp: "sgd0815".to_string(),
1196+
table: "thing".to_string(),
1197+
method: Method::BTree,
1198+
columns: vec![
1199+
Expr::Column("late".to_string()),
1200+
Expr::Column("mid".to_string()),
1201+
Expr::Column("early".to_string()),
1202+
],
1203+
cond: None,
1204+
with: None,
1205+
};
1206+
1207+
assert!(
1208+
index.fields_exist_in_dest(table),
1209+
"index columns exist in table regardless of order"
1210+
);
1211+
}

0 commit comments

Comments
 (0)