Skip to content

Commit a52475d

Browse files
committed
perf: normalize single-external query var order
1 parent ed766e0 commit a52475d

2 files changed

Lines changed: 220 additions & 1 deletion

File tree

core-relations/src/query.rs

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! APIs for building a query of a database.
22
3-
use std::{iter::once, sync::Arc};
3+
use std::{iter::once, mem, sync::Arc};
44

55
use crate::numeric_id::{DenseIdMap, IdVec, NumericId, define_id};
66
use smallvec::SmallVec;
@@ -444,7 +444,60 @@ impl RuleBuilder<'_, '_> {
444444
}
445445
}
446446

447+
fn normalize_single_external_arg_order(&mut self) {
448+
let Some(arg_vars) = single_external_arg_vars(&self.qb.instrs) else {
449+
return;
450+
};
451+
452+
let mut preferred = SmallVec::<[Variable; 4]>::new();
453+
preferred.extend(arg_vars.iter().copied());
454+
preferred.extend(
455+
self.qb
456+
.query
457+
.var_info
458+
.iter()
459+
.map(|(var, _)| var)
460+
.filter(|var| !arg_vars.contains(var)),
461+
);
462+
463+
let current = SmallVec::<[Variable; 4]>::from_iter(
464+
self.qb.query.var_info.iter().map(|(var, _)| var),
465+
);
466+
if preferred == current {
467+
return;
468+
}
469+
470+
let mut remap = DenseIdMap::with_capacity(self.qb.query.var_info.n_ids());
471+
for (idx, old_var) in preferred.iter().enumerate() {
472+
remap.insert(*old_var, Variable::from_usize(idx));
473+
}
474+
475+
let mut old_var_info = mem::take(&mut self.qb.query.var_info);
476+
for old_var in preferred {
477+
self.qb
478+
.query
479+
.var_info
480+
.push(old_var_info.take(old_var).expect("all vars must be present"));
481+
}
482+
483+
for (_, atom) in self.qb.query.atoms.iter_mut() {
484+
let old_var_to_column = mem::take(&mut atom.var_to_column);
485+
atom.var_to_column = old_var_to_column
486+
.into_iter()
487+
.map(|(var, col)| (remap[var], col))
488+
.collect();
489+
for (_, var) in atom.column_to_var.iter_mut() {
490+
*var = remap[*var];
491+
}
492+
}
493+
494+
for instr in self.qb.instrs.iter_mut() {
495+
remap_instr(instr, &remap);
496+
}
497+
}
498+
447499
pub fn build_with_description(mut self, desc: impl Into<String>) -> RuleId {
500+
self.normalize_single_external_arg_order();
448501
let var_info = &self.qb.query.var_info;
449502
let symbol_map = self.build_symbol_map();
450503
// Generate an id for our actions and slot them in.
@@ -769,6 +822,127 @@ impl RuleBuilder<'_, '_> {
769822
}
770823
}
771824

825+
fn single_external_arg_vars(instrs: &[Instr]) -> Option<SmallVec<[Variable; 4]>> {
826+
let [Instr::External { args, .. }] = instrs else {
827+
return None;
828+
};
829+
830+
let mut vars = SmallVec::<[Variable; 4]>::new();
831+
for arg in args {
832+
let QueryEntry::Var(var) = arg else {
833+
return None;
834+
};
835+
if vars.contains(var) {
836+
return None;
837+
}
838+
vars.push(*var);
839+
}
840+
Some(vars)
841+
}
842+
843+
fn remap_query_entry(entry: &mut QueryEntry, remap: &DenseIdMap<Variable, Variable>) {
844+
if let QueryEntry::Var(var) = entry {
845+
*var = remap[*var];
846+
}
847+
}
848+
849+
fn remap_write_val(val: &mut WriteVal, remap: &DenseIdMap<Variable, Variable>) {
850+
if let WriteVal::QueryEntry(entry) = val {
851+
remap_query_entry(entry, remap);
852+
}
853+
}
854+
855+
fn remap_instr(instr: &mut Instr, remap: &DenseIdMap<Variable, Variable>) {
856+
match instr {
857+
Instr::LookupOrInsertDefault {
858+
args,
859+
default,
860+
dst_var,
861+
..
862+
} => {
863+
args.iter_mut()
864+
.for_each(|entry| remap_query_entry(entry, remap));
865+
default
866+
.iter_mut()
867+
.for_each(|val| remap_write_val(val, remap));
868+
*dst_var = remap[*dst_var];
869+
}
870+
Instr::LookupWithDefault {
871+
args,
872+
default,
873+
dst_var,
874+
..
875+
} => {
876+
args.iter_mut()
877+
.for_each(|entry| remap_query_entry(entry, remap));
878+
remap_query_entry(default, remap);
879+
*dst_var = remap[*dst_var];
880+
}
881+
Instr::Lookup {
882+
args, dst_var, ..
883+
} => {
884+
args.iter_mut()
885+
.for_each(|entry| remap_query_entry(entry, remap));
886+
*dst_var = remap[*dst_var];
887+
}
888+
Instr::LookupWithFallback {
889+
table_key,
890+
func_args,
891+
dst_var,
892+
..
893+
} => {
894+
table_key
895+
.iter_mut()
896+
.for_each(|entry| remap_query_entry(entry, remap));
897+
func_args
898+
.iter_mut()
899+
.for_each(|entry| remap_query_entry(entry, remap));
900+
*dst_var = remap[*dst_var];
901+
}
902+
Instr::Insert { vals, .. } => {
903+
vals.iter_mut()
904+
.for_each(|entry| remap_query_entry(entry, remap));
905+
}
906+
Instr::InsertIfEq { l, r, vals, .. } => {
907+
remap_query_entry(l, remap);
908+
remap_query_entry(r, remap);
909+
vals.iter_mut()
910+
.for_each(|entry| remap_query_entry(entry, remap));
911+
}
912+
Instr::Remove { args, .. } => {
913+
args.iter_mut()
914+
.for_each(|entry| remap_query_entry(entry, remap));
915+
}
916+
Instr::External { args, dst, .. } => {
917+
args.iter_mut()
918+
.for_each(|entry| remap_query_entry(entry, remap));
919+
*dst = remap[*dst];
920+
}
921+
Instr::ExternalWithFallback {
922+
args1, args2, dst, ..
923+
} => {
924+
args1
925+
.iter_mut()
926+
.for_each(|entry| remap_query_entry(entry, remap));
927+
args2
928+
.iter_mut()
929+
.for_each(|entry| remap_query_entry(entry, remap));
930+
*dst = remap[*dst];
931+
}
932+
Instr::AssertEq(l, r) | Instr::AssertNe(l, r) => {
933+
remap_query_entry(l, remap);
934+
remap_query_entry(r, remap);
935+
}
936+
Instr::AssertAnyNe { ops, .. } => {
937+
ops.iter_mut()
938+
.for_each(|entry| remap_query_entry(entry, remap));
939+
}
940+
Instr::ReadCounter { dst, .. } => {
941+
*dst = remap[*dst];
942+
}
943+
}
944+
}
945+
772946
#[derive(Debug, Clone)]
773947
pub(crate) struct Atom {
774948
pub(crate) table: TableId,

core-relations/src/tests.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,51 @@ fn basic_query() {
122122
assert_eq!(res, Vec::from_iter((0..10).chain([13, 17].into_iter())));
123123
}
124124

125+
#[test]
126+
fn single_external_rule_normalizes_gap_vars_into_consecutive_window() {
127+
let mut db = Database::default();
128+
let tuple4 = db.add_table(
129+
SortedWritesTable::new(4, 4, None, vec![], Box::new(|_, _, _, _| false)),
130+
iter::empty(),
131+
iter::empty(),
132+
);
133+
let echo = db.add_external_function(Box::new(make_external_func(|_, args| args.first().copied())));
134+
135+
let mut rsb = RuleSetBuilder::new(&mut db);
136+
let mut query = rsb.new_rule();
137+
let a = query.new_var_named("a");
138+
let b = query.new_var_named("b");
139+
let c = query.new_var_named("c");
140+
let d = query.new_var_named("d");
141+
query
142+
.add_atom(tuple4, &[a.into(), b.into(), c.into(), d.into()], &[])
143+
.unwrap();
144+
let mut rule = query.build();
145+
let _ = rule.call_external(echo, &[a.into(), b.into(), d.into()]).unwrap();
146+
let rule_id = rule.build_with_description("single_external_gap");
147+
let rule_set = rsb.build();
148+
149+
let (_, _, _, action_id) = rule_set.plans.get(rule_id).unwrap();
150+
let action = &rule_set.actions[*action_id];
151+
let [crate::action::Instr::External { args, .. }] = action.instrs.as_ref().as_slice() else {
152+
panic!("expected single external instruction");
153+
};
154+
155+
let vars: Vec<_> = args
156+
.iter()
157+
.map(|arg| match arg {
158+
crate::action::QueryEntry::Var(var) => *var,
159+
crate::action::QueryEntry::Const(_) => panic!("expected var-only external args"),
160+
})
161+
.collect();
162+
163+
assert_eq!(
164+
vars,
165+
vec![crate::free_join::Variable::from_usize(0), crate::free_join::Variable::from_usize(1), crate::free_join::Variable::from_usize(2)],
166+
"eligible single-external rules should normalize arg vars into a consecutive window",
167+
);
168+
}
169+
125170
#[test]
126171
fn line_graph_1_fj_puresize() {
127172
line_graph_1_test(PlanStrategy::PureSize);

0 commit comments

Comments
 (0)