use std::collections::VecDeque;
use rudb_common::Result;
use rudb_plan::{BuildSide, Expr, JoinKind, Node, NodeRef, Plan, Slice};
use crate::estimate::{self, Facts};
use crate::pass::{Context, Pass, top_down};
use crate::tables::{TableSet, Tables, produced};
use crate::walk;
#[derive(Debug, Clone, Copy)]
pub struct MarkToSemi;
impl Pass for MarkToSemi {
fn name(&self) -> &'static str {
"mark_to_semi"
}
fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
convert(plan);
Ok(())
}
}
pub fn convert(plan: &mut Plan) {
for node in top_down(plan) {
let Node::Filter { input, predicate } = *plan.node(node) else {
continue;
};
let Node::Join { left, right, kind: JoinKind::Mark, conditions, build } = *plan.node(input)
else {
continue;
};
let Expr::Column(tested) = *plan.expr(predicate) else {
continue;
};
let Some(outputs) = walk::outputs(plan, right) else {
continue;
};
let Some((marker, _)) = outputs.last() else {
continue;
};
if tested != *marker || read_above(plan, right, node, input) {
continue;
}
*plan.node_mut(node) = Node::Join { left, right, kind: JoinKind::Semi, conditions, build };
}
}
#[derive(Debug, Clone, Copy)]
pub struct DistinctToSemi;
impl Pass for DistinctToSemi {
fn name(&self) -> &'static str {
"distinct_to_semi"
}
fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
narrow(plan);
Ok(())
}
}
pub fn narrow(plan: &mut Plan) {
for node in top_down(plan) {
let Node::Aggregate { input, groups, aggregates, .. } = *plan.node(node) else {
continue;
};
if !plan.expr_list(aggregates).is_empty() {
continue;
}
let Node::Join { left, right, kind: JoinKind::Inner, conditions, build } =
*plan.node(input)
else {
continue;
};
let driving = produced(plan, left);
let grouped = plan.expr_list(groups).iter().all(|&group| match *plan.expr(group) {
Expr::Column(binding) => driving.contains(binding.table),
_ => false,
});
if !grouped || parents(plan, input) != 1 || read_above(plan, right, node, input) {
continue;
}
*plan.node_mut(input) = Node::Join { left, right, kind: JoinKind::Semi, conditions, build };
}
}
#[derive(Debug, Clone, Copy)]
pub struct SemiPushdown;
impl Pass for SemiPushdown {
fn name(&self) -> &'static str {
"semi_pushdown"
}
fn run(&self, plan: &mut Plan, context: &Context) -> Result<()> {
lower(plan, context.facts());
Ok(())
}
}
pub fn lower(plan: &mut Plan, stats: &Facts) {
let mut tables = Tables::new();
let root = rebuilt(plan, plan.root(), &mut tables, stats);
plan.set_root(root);
}
fn rebuilt(plan: &mut Plan, at: NodeRef, tables: &mut Tables, stats: &Facts) -> NodeRef {
let children: Vec<NodeRef> = plan.node(at).children().into_iter().flatten().collect();
let done: Vec<NodeRef> =
children.iter().map(|&child| rebuilt(plan, child, tables, stats)).collect();
let here = if done == children {
at
} else {
let mut node = plan.node(at).clone();
walk::replace_children(&mut node, &done);
plan.add_node(node)
};
moved(plan, here, tables, stats).unwrap_or(here)
}
#[derive(Debug, Clone, Copy)]
enum Under {
Join(Slice, BuildSide),
Cross,
}
fn moved(plan: &mut Plan, at: NodeRef, tables: &mut Tables, stats: &Facts) -> Option<NodeRef> {
let Node::Join { left, right, kind, conditions, build } = *plan.node(at) else {
return None;
};
if !matches!(kind, JoinKind::Semi | JoinKind::Anti) || plan.expr_list(conditions).is_empty() {
return None;
}
let (first, second, under) = match *plan.node(left) {
Node::Join { left: a, right: b, kind: JoinKind::Inner, conditions: list, build } => {
(a, b, Under::Join(list, build))
}
Node::CrossProduct { left: a, right: b } => (a, b, Under::Cross),
_ => return None,
};
let mut needed = TableSet::new();
for condition in plan.expr_list(conditions).to_vec() {
needed.extend(&tables.of(plan, condition));
}
let gathered = produced(plan, right);
let into_first = answers(plan, first, &gathered, &needed);
let into_second = !into_first && answers(plan, second, &gathered, &needed);
if !into_first && !into_second {
return None;
}
let side = if into_first { first } else { second };
let other = if into_first { second } else { first };
if !expands(plan, side, other, under, stats) {
return None;
}
let semi = plan.add_node(Node::Join { left: side, right, kind, conditions, build });
let semi = moved(plan, semi, tables, stats).unwrap_or(semi);
let (left, right) = if into_first { (semi, second) } else { (first, semi) };
Some(plan.add_node(match under {
Under::Join(conditions, build) => {
Node::Join { left, right, kind: JoinKind::Inner, conditions, build }
}
Under::Cross => Node::CrossProduct { left, right },
}))
}
fn expands(plan: &Plan, side: NodeRef, other: NodeRef, under: Under, stats: &Facts) -> bool {
let Under::Join(conditions, _) = under else {
return true;
};
if plan.expr_list(conditions).is_empty() {
return true;
}
let (Some(side), Some(other)) =
(estimate::rows(plan, side, stats), estimate::rows(plan, other, stats))
else {
return false;
};
other >= side
}
fn answers(plan: &Plan, side: NodeRef, gathered: &TableSet, needed: &TableSet) -> bool {
let mut reachable = produced(plan, side);
reachable.extend(gathered);
needed.is_subset_of(&reachable)
}
fn parents(plan: &Plan, at: NodeRef) -> usize {
let mut found = 0;
for node in 0..u32::try_from(plan.node_count()).unwrap_or(u32::MAX) {
found +=
plan.node(node).children().into_iter().flatten().filter(|&child| child == at).count();
}
found
}
fn read_above(plan: &Plan, right: NodeRef, filter: NodeRef, join: NodeRef) -> bool {
let gathered = produced(plan, right);
let inside = subtree(plan, right);
let mut found = false;
for at in top_down(plan) {
if at == filter || at == join || inside.contains(&at) {
continue;
}
walk::node_columns(plan, at, &mut |_, binding| {
found |= gathered.contains(binding.table);
});
}
found
}
fn subtree(plan: &Plan, at: NodeRef) -> Vec<NodeRef> {
let mut found = Vec::new();
let mut pending = VecDeque::from([at]);
while let Some(node) = pending.pop_front() {
if found.contains(&node) {
continue;
}
found.push(node);
pending.extend(plan.node(node).children().into_iter().flatten());
}
found
}
#[cfg(test)]
mod tests {
use rudb_plan::Plan;
use super::{convert, lower, narrow};
use crate::estimate::Facts;
fn converted(text: &str) -> String {
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
convert(&mut plan);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
plan.to_string()
}
fn narrowed(text: &str) -> String {
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
narrow(&mut plan);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
plan.to_string()
}
fn lowered(text: &str) -> String {
let mut counts = Facts::new();
for (table, rows) in [("t", 1000), ("u", 10), ("v", 100), ("w", 100_000)] {
counts.record("memory", "main", table, rows);
}
let mut plan =
Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
lower(&mut plan, &counts);
plan.validate().unwrap_or_else(|error| panic!("{text} did not stay valid: {error}"));
let once = plan.to_string();
lower(&mut plan, &counts);
assert_eq!(plan.to_string(), once, "{text} moved again on a second run");
once
}
#[test]
fn a_filter_on_the_marker_becomes_the_join_itself() {
assert_eq!(
converted(concat!(
"Filter #1.1::BOOLEAN\n",
" Join MARK on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Project #1 [#2.0::BIGINT AS k, TRUE::BOOLEAN AS mark]\n",
" Get memory.main.u AS u #2 [k::BIGINT]\n",
)),
concat!(
"Join SEMI on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Project #1 [#2.0::BIGINT AS k, TRUE::BOOLEAN AS mark]\n",
" Get memory.main.u AS u #2 [k::BIGINT]\n",
)
);
}
#[test]
fn a_filter_on_something_other_than_the_marker_is_left_alone() {
let text = concat!(
"Filter (#0.0::BIGINT > 3::BIGINT)::BOOLEAN\n",
" Join MARK on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Project #1 [#2.0::BIGINT AS k, TRUE::BOOLEAN AS mark]\n",
" Get memory.main.u AS u #2 [k::BIGINT]\n",
);
assert_eq!(converted(text), text);
}
#[test]
fn a_filter_on_a_gathered_column_that_is_not_the_marker_is_left_alone() {
let text = concat!(
"Filter (#1.0::BIGINT > 3::BIGINT)::BOOLEAN\n",
" Join MARK on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Project #1 [#2.0::BIGINT AS k, TRUE::BOOLEAN AS mark]\n",
" Get memory.main.u AS u #2 [k::BIGINT]\n",
);
assert_eq!(converted(text), text);
}
#[test]
fn a_gathered_column_read_above_the_filter_stops_the_rewrite() {
let text = concat!(
"Project #3 [#1.0::BIGINT AS k]\n",
" Filter #1.1::BOOLEAN\n",
" Join MARK on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Project #1 [#2.0::BIGINT AS k, TRUE::BOOLEAN AS mark]\n",
" Get memory.main.u AS u #2 [k::BIGINT]\n",
);
assert_eq!(converted(text), text);
}
#[test]
fn a_driving_column_read_above_the_filter_does_not_stop_it() {
assert_eq!(
converted(concat!(
"Project #3 [#0.0::BIGINT AS a]\n",
" Filter #1.1::BOOLEAN\n",
" Join MARK on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Project #1 [#2.0::BIGINT AS k, TRUE::BOOLEAN AS mark]\n",
" Get memory.main.u AS u #2 [k::BIGINT]\n",
)),
concat!(
"Project #3 [#0.0::BIGINT AS a]\n",
" Join SEMI on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Project #1 [#2.0::BIGINT AS k, TRUE::BOOLEAN AS mark]\n",
" Get memory.main.u AS u #2 [k::BIGINT]\n",
)
);
}
#[test]
fn a_filter_over_a_join_that_is_not_a_mark_is_left_alone() {
let text = concat!(
"Filter #1.1::BOOLEAN\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Project #1 [#2.0::BIGINT AS k, TRUE::BOOLEAN AS mark]\n",
" Get memory.main.u AS u #2 [k::BIGINT]\n",
);
assert_eq!(converted(text), text);
}
#[test]
fn a_duplicate_eliminator_over_an_inner_join_makes_the_join_a_semi_join() {
assert_eq!(
narrowed(concat!(
"Aggregate #3 groups=[#0.0::BIGINT] aggregates=[]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Get memory.main.u AS u #1 [k::BIGINT]\n",
)),
concat!(
"Aggregate #3 groups=[#0.0::BIGINT] aggregates=[]\n",
" Join SEMI on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Get memory.main.u AS u #1 [k::BIGINT]\n",
)
);
}
#[test]
fn an_aggregate_that_actually_aggregates_is_left_alone() {
let text = concat!(
"Aggregate #3 groups=[#0.0::BIGINT] aggregates=[count_star()::BIGINT]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Get memory.main.u AS u #1 [k::BIGINT]\n",
);
assert_eq!(narrowed(text), text);
}
#[test]
fn a_group_that_reads_the_gathered_side_is_left_alone() {
let text = concat!(
"Aggregate #3 groups=[#0.0::BIGINT, #1.0::BIGINT] aggregates=[]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Get memory.main.u AS u #1 [k::BIGINT]\n",
);
assert_eq!(narrowed(text), text);
}
#[test]
fn a_group_over_an_expression_rather_than_a_column_is_left_alone() {
let text = concat!(
"Aggregate #3 groups=[\"+\"(#0.0::BIGINT, 1::BIGINT)::BIGINT] aggregates=[]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Get memory.main.u AS u #1 [k::BIGINT]\n",
);
assert_eq!(narrowed(text), text);
}
#[test]
fn a_join_a_second_node_also_points_at_is_left_alone() {
let text = concat!(
"Join INNER on=[(#0.0::BIGINT = #4.0::BIGINT)::BOOLEAN]\n",
" Aggregate #3 groups=[#0.0::BIGINT] aggregates=[]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Get memory.main.u AS u #1 [k::BIGINT]\n",
" Project #4 [#0.0::BIGINT AS a]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Get memory.main.u AS u #1 [k::BIGINT]\n",
);
assert_eq!(narrowed(text), text);
}
#[test]
fn a_gathered_column_read_somewhere_else_stops_the_narrowing() {
let text = concat!(
"Project #4 [#3.0::BIGINT AS a, #1.0::BIGINT AS k]\n",
" Aggregate #3 groups=[#0.0::BIGINT] aggregates=[]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT, b::BIGINT]\n",
" Get memory.main.u AS u #1 [k::BIGINT]\n",
);
assert_eq!(narrowed(text), text);
}
#[test]
fn a_semi_join_moves_into_the_side_of_the_inner_join_its_condition_reads() {
assert_eq!(
lowered(concat!(
"Join SEMI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
)),
concat!(
"Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Join SEMI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
)
);
}
#[test]
fn a_semi_join_that_reads_the_second_side_moves_into_that_one() {
assert_eq!(
lowered(concat!(
"Join SEMI on=[(#1.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
)),
concat!(
"Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Join SEMI on=[(#1.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
)
);
}
#[test]
fn a_condition_reading_both_sides_of_the_inner_join_stays_above_it() {
let text = concat!(
"Join SEMI on=[(\"+\"(#0.0::BIGINT, #1.0::BIGINT)::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
);
assert_eq!(lowered(text), text);
}
#[test]
fn an_anti_join_moves_the_same_way_a_semi_join_does() {
assert_eq!(
lowered(concat!(
"Join ANTI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
)),
concat!(
"Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Join ANTI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
)
);
}
#[test]
fn a_semi_join_over_a_cross_product_moves_whatever_the_two_sides_weigh() {
assert_eq!(
lowered(concat!(
"Join SEMI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" CrossProduct\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
)),
concat!(
"CrossProduct\n",
" Join SEMI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
)
);
}
#[test]
fn a_join_to_a_smaller_side_keeps_the_semi_join_above_it() {
let text = concat!(
"Join SEMI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.u AS u #1 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
);
assert_eq!(lowered(text), text);
}
#[test]
fn a_side_with_no_row_count_keeps_the_semi_join_above_it() {
let text = concat!(
"Join SEMI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.x AS x #1 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
);
assert_eq!(lowered(text), text);
}
#[test]
fn a_semi_join_with_no_conditions_is_left_where_it_is() {
let text = concat!(
"Join SEMI on=[]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
);
assert_eq!(lowered(text), text);
}
#[test]
fn a_semi_join_over_an_ordinary_node_is_left_where_it_is() {
let text = concat!(
"Join SEMI on=[(#3.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Project #3 [#0.0::BIGINT AS a]\n",
" Get memory.main.t AS t #0 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT]\n",
);
assert_eq!(lowered(text), text);
}
#[test]
fn a_semi_join_goes_past_two_inner_joins_in_a_row() {
assert_eq!(
lowered(concat!(
"Join SEMI on=[(#0.0::BIGINT = #3.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
" Get memory.main.w AS w #2 [d::BIGINT]\n",
" Get memory.main.v AS v #3 [c::BIGINT]\n",
)),
concat!(
"Join INNER on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Join SEMI on=[(#0.0::BIGINT = #3.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.v AS v #3 [c::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
" Get memory.main.w AS w #2 [d::BIGINT]\n",
)
);
}
#[test]
fn a_condition_that_reads_the_gathered_side_as_well_still_moves() {
assert_eq!(
lowered(concat!(
"Join SEMI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN, ",
"(#2.1::BIGINT > #0.0::BIGINT)::BOOLEAN]\n",
" Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT, e::BIGINT]\n",
)),
concat!(
"Join INNER on=[(#0.0::BIGINT = #1.0::BIGINT)::BOOLEAN]\n",
" Join SEMI on=[(#0.0::BIGINT = #2.0::BIGINT)::BOOLEAN, ",
"(#2.1::BIGINT > #0.0::BIGINT)::BOOLEAN]\n",
" Get memory.main.u AS u #0 [b::BIGINT]\n",
" Get memory.main.v AS v #2 [c::BIGINT, e::BIGINT]\n",
" Get memory.main.t AS t #1 [a::BIGINT]\n",
)
);
}
}