use std::{cell::RefCell, collections::HashMap};
use limbo_sqlite3_parser::ast::TableInternalId;
use crate::{
translate::{
optimizer::{cost::Cost, order::plan_satisfies_order_target},
plan::{JoinOrderMember, JoinedTable},
planner::TableMask,
},
Result,
};
use super::{
access_method::{find_best_access_method_for_join_order, AccessMethod},
constraints::TableConstraints,
cost::ESTIMATED_HARDCODED_ROWS_PER_TABLE,
order::OrderTarget,
};
#[derive(Debug, Clone)]
pub struct JoinN {
pub data: Vec<(usize, usize)>,
pub output_cardinality: usize,
pub cost: Cost,
}
impl JoinN {
pub fn table_numbers(&self) -> impl Iterator<Item = usize> + use<'_> {
self.data.iter().map(|(table_number, _)| *table_number)
}
pub fn best_access_methods(&self) -> impl Iterator<Item = usize> + use<'_> {
self.data
.iter()
.map(|(_, access_method_index)| *access_method_index)
}
}
pub fn join_lhs_and_rhs<'a>(
lhs: Option<&JoinN>,
rhs_table_reference: &JoinedTable,
rhs_constraints: &'a TableConstraints,
join_order: &[JoinOrderMember],
maybe_order_target: Option<&OrderTarget>,
access_methods_arena: &'a RefCell<Vec<AccessMethod<'a>>>,
cost_upper_bound: Cost,
) -> Result<Option<JoinN>> {
let input_cardinality = lhs.map_or(1, |l| l.output_cardinality);
let best_access_method = find_best_access_method_for_join_order(
rhs_table_reference,
rhs_constraints,
&join_order,
maybe_order_target,
input_cardinality as f64,
)?;
let lhs_cost = lhs.map_or(Cost(0.0), |l| l.cost);
let cost = lhs_cost + best_access_method.cost;
if cost > cost_upper_bound {
return Ok(None);
}
access_methods_arena.borrow_mut().push(best_access_method);
let mut best_access_methods = Vec::with_capacity(join_order.len());
best_access_methods.extend(lhs.map_or(vec![], |l| l.data.clone()));
let rhs_table_number = join_order.last().unwrap().original_idx;
best_access_methods.push((rhs_table_number, access_methods_arena.borrow().len() - 1));
let lhs_mask = lhs.map_or(TableMask::new(), |l| {
TableMask::from_table_number_iter(l.table_numbers())
});
let output_cardinality_multiplier = rhs_constraints
.constraints
.iter()
.filter(|c| lhs_mask.contains_all(&c.lhs_mask))
.map(|c| c.selectivity)
.product::<f64>();
let output_cardinality = (input_cardinality as f64
* ESTIMATED_HARDCODED_ROWS_PER_TABLE as f64
* output_cardinality_multiplier)
.ceil() as usize;
Ok(Some(JoinN {
data: best_access_methods,
output_cardinality,
cost,
}))
}
#[derive(Debug)]
pub struct BestJoinOrderResult {
pub best_plan: JoinN,
pub best_ordered_plan: Option<JoinN>,
}
pub fn compute_best_join_order<'a>(
joined_tables: &[JoinedTable],
maybe_order_target: Option<&OrderTarget>,
constraints: &'a [TableConstraints],
access_methods_arena: &'a RefCell<Vec<AccessMethod<'a>>>,
) -> Result<Option<BestJoinOrderResult>> {
if joined_tables.is_empty() {
return Ok(None);
}
let num_tables = joined_tables.len();
let naive_plan = compute_naive_left_deep_plan(
joined_tables,
maybe_order_target,
access_methods_arena,
&constraints,
)?;
let mut best_ordered_plan: Option<JoinN> = None;
let mut best_plan_is_also_ordered = if let Some(ref order_target) = maybe_order_target {
plan_satisfies_order_target(
&naive_plan,
&access_methods_arena,
joined_tables,
order_target,
)
} else {
false
};
if joined_tables.len() == 1 {
return Ok(Some(BestJoinOrderResult {
best_plan: naive_plan,
best_ordered_plan: None,
}));
}
let mut best_plan = naive_plan;
let mut join_order = Vec::with_capacity(num_tables);
join_order.push(JoinOrderMember {
table_id: TableInternalId::default(),
original_idx: 0,
is_outer: false,
});
let cost_upper_bound = best_plan.cost;
let cost_upper_bound_ordered = best_plan.cost;
let mut best_plan_memo: HashMap<TableMask, JoinN> =
HashMap::with_capacity(2usize.pow(num_tables as u32 - 1));
for i in 0..num_tables {
let mut mask = TableMask::new();
mask.add_table(i);
let table_ref = &joined_tables[i];
join_order[0] = JoinOrderMember {
table_id: table_ref.internal_id,
original_idx: i,
is_outer: false,
};
assert!(join_order.len() == 1);
let rel = join_lhs_and_rhs(
None,
table_ref,
&constraints[i],
&join_order,
maybe_order_target,
access_methods_arena,
cost_upper_bound_ordered,
)?;
if let Some(rel) = rel {
best_plan_memo.insert(mask, rel);
}
}
join_order.clear();
let left_join_illegal_map = {
let left_join_count = joined_tables
.iter()
.filter(|t| t.join_info.as_ref().map_or(false, |j| j.outer))
.count();
if left_join_count == 0 {
None
} else {
let mut left_join_illegal_map: HashMap<usize, TableMask> =
HashMap::with_capacity(left_join_count);
for (i, _) in joined_tables.iter().enumerate() {
for j in i + 1..joined_tables.len() {
if joined_tables[j]
.join_info
.as_ref()
.map_or(false, |j| j.outer)
{
if let Some(illegal_lhs) = left_join_illegal_map.get_mut(&i) {
illegal_lhs.add_table(j);
} else {
let mut mask = TableMask::new();
mask.add_table(j);
left_join_illegal_map.insert(i, mask);
}
}
}
}
Some(left_join_illegal_map)
}
};
for subset_size in 2..=num_tables {
for mask in generate_join_bitmasks(num_tables, subset_size) {
let mut best_for_mask: Option<JoinN> = None;
let (mut best_ordered_for_mask, mut best_for_mask_is_also_ordered) = (None, false);
for rhs_idx in 0..num_tables {
if !mask.contains_table(rhs_idx) {
continue;
}
let lhs_mask = mask.without_table(rhs_idx);
if lhs_mask.is_empty() {
continue;
}
if let Some(illegal_lhs) = left_join_illegal_map
.as_ref()
.and_then(|deps| deps.get(&rhs_idx))
{
let legal = !lhs_mask.intersects(illegal_lhs);
if !legal {
continue; }
}
let Some(lhs) = best_plan_memo.get(&lhs_mask) else {
continue;
};
for table_no in lhs.table_numbers() {
join_order.push(JoinOrderMember {
table_id: joined_tables[table_no].internal_id,
original_idx: table_no,
is_outer: joined_tables[table_no]
.join_info
.as_ref()
.map_or(false, |j| j.outer),
});
}
join_order.push(JoinOrderMember {
table_id: joined_tables[rhs_idx].internal_id,
original_idx: rhs_idx,
is_outer: joined_tables[rhs_idx]
.join_info
.as_ref()
.map_or(false, |j| j.outer),
});
assert!(join_order.len() == subset_size);
let rel = join_lhs_and_rhs(
Some(lhs),
&joined_tables[rhs_idx],
&constraints[rhs_idx],
&join_order,
maybe_order_target,
access_methods_arena,
cost_upper_bound_ordered,
)?;
join_order.clear();
let Some(rel) = rel else {
continue;
};
let satisfies_order_target = if let Some(ref order_target) = maybe_order_target {
plan_satisfies_order_target(
&rel,
&access_methods_arena,
joined_tables,
order_target,
)
} else {
false
};
if rel.cost >= cost_upper_bound {
if !satisfies_order_target {
continue;
}
let existing_ordered_cost: Cost = best_ordered_for_mask
.as_ref()
.map_or(Cost(f64::MAX), |p: &JoinN| p.cost);
if rel.cost < existing_ordered_cost {
best_ordered_for_mask = Some(rel);
}
} else if best_for_mask.is_none() || rel.cost < best_for_mask.as_ref().unwrap().cost
{
best_for_mask = Some(rel);
best_for_mask_is_also_ordered = satisfies_order_target;
}
}
if let Some(rel) = best_ordered_for_mask.take() {
let cost = rel.cost;
let has_all_tables = mask.table_count() == num_tables;
if has_all_tables && cost_upper_bound_ordered > cost {
best_ordered_plan = Some(rel);
}
}
if let Some(rel) = best_for_mask.take() {
let cost = rel.cost;
let has_all_tables = mask.table_count() == num_tables;
if has_all_tables {
if cost_upper_bound > cost {
best_plan = rel;
best_plan_is_also_ordered = best_for_mask_is_also_ordered;
}
} else {
best_plan_memo.insert(mask, rel);
}
}
}
}
Ok(Some(BestJoinOrderResult {
best_plan,
best_ordered_plan: if best_plan_is_also_ordered {
None
} else {
best_ordered_plan
},
}))
}
pub fn compute_naive_left_deep_plan<'a>(
joined_tables: &[JoinedTable],
maybe_order_target: Option<&OrderTarget>,
access_methods_arena: &'a RefCell<Vec<AccessMethod<'a>>>,
constraints: &'a [TableConstraints],
) -> Result<JoinN> {
let n = joined_tables.len();
assert!(n > 0);
let join_order = joined_tables
.iter()
.enumerate()
.map(|(i, t)| JoinOrderMember {
table_id: t.internal_id,
original_idx: i,
is_outer: t.join_info.as_ref().map_or(false, |j| j.outer),
})
.collect::<Vec<_>>();
let mut best_plan = join_lhs_and_rhs(
None,
&joined_tables[0],
&constraints[0],
&join_order[..1],
maybe_order_target,
access_methods_arena,
Cost(f64::MAX),
)?
.expect("call to join_lhs_and_rhs in compute_naive_left_deep_plan always returns Some(JoinN)");
for i in 1..n {
best_plan = join_lhs_and_rhs(
Some(&best_plan),
&joined_tables[i],
&constraints[i],
&join_order[..=i],
maybe_order_target,
access_methods_arena,
Cost(f64::MAX),
)?
.expect(
"call to join_lhs_and_rhs in compute_naive_left_deep_plan always returns Some(JoinN)",
);
}
Ok(best_plan)
}
struct JoinBitmaskIter {
current: u128,
max_exclusive: u128,
}
impl JoinBitmaskIter {
fn new(table_number_max_exclusive: usize, how_many: usize) -> Self {
Self {
current: (1 << how_many) - 1, max_exclusive: 1 << table_number_max_exclusive,
}
}
}
impl Iterator for JoinBitmaskIter {
type Item = TableMask;
fn next(&mut self) -> Option<Self::Item> {
if self.current >= self.max_exclusive {
return None;
}
let result = TableMask::from_bits(self.current);
let c = self.current & (!self.current + 1); let r = self.current + c; let ones = self.current ^ r; let ones = (ones >> 2) / c; self.current = r | ones;
Some(result)
}
}
fn generate_join_bitmasks(table_number_max_exclusive: usize, how_many: usize) -> JoinBitmaskIter {
JoinBitmaskIter::new(table_number_max_exclusive, how_many)
}
#[cfg(test)]
mod tests {
use std::{cell::Cell, rc::Rc, sync::Arc};
use limbo_sqlite3_parser::ast::{self, Expr, Operator, SortOrder, TableInternalId};
use super::*;
use crate::{
schema::{BTreeTable, Column, Index, IndexColumn, Table, Type},
translate::{
optimizer::constraints::{constraints_from_where_clause, BinaryExprSide},
plan::{
ColumnUsedMask, IterationDirection, JoinInfo, Operation, TableReferences, WhereTerm,
},
planner::TableMask,
},
vdbe::builder::TableRefIdCounter,
};
#[test]
fn test_generate_bitmasks() {
let bitmasks = generate_join_bitmasks(4, 2).collect::<Vec<_>>();
assert!(bitmasks.contains(&TableMask(0b110))); assert!(bitmasks.contains(&TableMask(0b1010))); assert!(bitmasks.contains(&TableMask(0b1100))); assert!(bitmasks.contains(&TableMask(0b10010))); assert!(bitmasks.contains(&TableMask(0b10100))); assert!(bitmasks.contains(&TableMask(0b11000))); }
#[test]
fn test_compute_best_join_order_empty() {
let table_references = TableReferences::new(vec![], vec![]);
let available_indexes = HashMap::new();
let where_clause = vec![];
let access_methods_arena = RefCell::new(Vec::new());
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let result = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap();
assert!(result.is_none());
}
#[test]
fn test_compute_best_join_order_single_table_no_indexes() {
let t1 = _create_btree_table("test_table", _create_column_list(&["id"], Type::Integer));
let mut table_id_counter = TableRefIdCounter::new();
let joined_tables = vec![_create_table_reference(
t1.clone(),
None,
table_id_counter.next(),
)];
let table_references = TableReferences::new(joined_tables, vec![]);
let available_indexes = HashMap::new();
let where_clause = vec![];
let access_methods_arena = RefCell::new(Vec::new());
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let BestJoinOrderResult { best_plan, .. } = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap()
.unwrap();
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
}
#[test]
fn test_compute_best_join_order_single_table_rowid_eq() {
let t1 = _create_btree_table("test_table", vec![_create_column_rowid_alias("id")]);
let mut table_id_counter = TableRefIdCounter::new();
let joined_tables = vec![_create_table_reference(
t1.clone(),
None,
table_id_counter.next(),
)];
let where_clause = vec![_create_binary_expr(
_create_column_expr(joined_tables[0].internal_id, 0, true), ast::Operator::Equals,
_create_numeric_literal("42"),
)];
let table_references = TableReferences::new(joined_tables, vec![]);
let access_methods_arena = RefCell::new(Vec::new());
let available_indexes = HashMap::new();
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let result = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap();
assert!(result.is_some());
let BestJoinOrderResult { best_plan, .. } = result.unwrap();
assert_eq!(best_plan.table_numbers().collect::<Vec<_>>(), vec![0]);
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(!access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.constraint_refs.len() == 1);
assert!(
table_constraints[0].constraints[access_method.constraint_refs[0].constraint_vec_pos]
.where_clause_pos
== (0, BinaryExprSide::Rhs)
);
}
#[test]
fn test_compute_best_join_order_single_table_pk_eq() {
let t1 = _create_btree_table(
"test_table",
vec![_create_column_of_type("id", Type::Integer)],
);
let mut table_id_counter = TableRefIdCounter::new();
let joined_tables = vec![_create_table_reference(
t1.clone(),
None,
table_id_counter.next(),
)];
let where_clause = vec![_create_binary_expr(
_create_column_expr(joined_tables[0].internal_id, 0, false), ast::Operator::Equals,
_create_numeric_literal("42"),
)];
let table_references = TableReferences::new(joined_tables, vec![]);
let access_methods_arena = RefCell::new(Vec::new());
let mut available_indexes = HashMap::new();
let index = Arc::new(Index {
name: "sqlite_autoindex_test_table_1".to_string(),
table_name: "test_table".to_string(),
columns: vec![IndexColumn {
name: "id".to_string(),
order: SortOrder::Asc,
pos_in_table: 0,
collation: None,
default: None,
}],
unique: true,
ephemeral: false,
root_page: 1,
has_rowid: true,
});
available_indexes.insert("test_table".to_string(), vec![index]);
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let result = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap();
assert!(result.is_some());
let BestJoinOrderResult { best_plan, .. } = result.unwrap();
assert_eq!(best_plan.table_numbers().collect::<Vec<_>>(), vec![0]);
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(!access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.as_ref().unwrap().name == "sqlite_autoindex_test_table_1");
assert!(access_method.constraint_refs.len() == 1);
assert!(
table_constraints[0].constraints[access_method.constraint_refs[0].constraint_vec_pos]
.where_clause_pos
== (0, BinaryExprSide::Rhs)
);
}
#[test]
fn test_compute_best_join_order_two_tables() {
let t1 = _create_btree_table("table1", _create_column_list(&["id"], Type::Integer));
let t2 = _create_btree_table("table2", _create_column_list(&["id"], Type::Integer));
let mut table_id_counter = TableRefIdCounter::new();
let joined_tables = vec![
_create_table_reference(t1.clone(), None, table_id_counter.next()),
_create_table_reference(
t2.clone(),
Some(JoinInfo {
outer: false,
using: None,
}),
table_id_counter.next(),
),
];
const TABLE1: usize = 0;
const TABLE2: usize = 1;
let mut available_indexes = HashMap::new();
let index1 = Arc::new(Index {
name: "index1".to_string(),
table_name: "table1".to_string(),
columns: vec![IndexColumn {
name: "id".to_string(),
order: SortOrder::Asc,
pos_in_table: 0,
collation: None,
default: None,
}],
unique: true,
ephemeral: false,
root_page: 1,
has_rowid: true,
});
available_indexes.insert("table1".to_string(), vec![index1]);
let where_clause = vec![_create_binary_expr(
_create_column_expr(joined_tables[TABLE1].internal_id, 0, false), ast::Operator::Equals,
_create_column_expr(joined_tables[TABLE2].internal_id, 0, false), )];
let table_references = TableReferences::new(joined_tables, vec![]);
let access_methods_arena = RefCell::new(Vec::new());
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let result = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap();
assert!(result.is_some());
let BestJoinOrderResult { best_plan, .. } = result.unwrap();
assert_eq!(best_plan.table_numbers().collect::<Vec<_>>(), vec![1, 0]);
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
let access_method = &access_methods_arena.borrow()[best_plan.data[1].1];
assert!(!access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.as_ref().unwrap().name == "index1");
assert!(access_method.constraint_refs.len() == 1);
assert!(
table_constraints[TABLE1].constraints
[access_method.constraint_refs[0].constraint_vec_pos]
.where_clause_pos
== (0, BinaryExprSide::Rhs)
);
}
#[test]
fn test_compute_best_join_order_three_tables_indexed() {
let table_orders = _create_btree_table(
"orders",
vec![
_create_column_of_type("id", Type::Integer),
_create_column_of_type("customer_id", Type::Integer),
_create_column_of_type("total", Type::Integer),
],
);
let table_customers = _create_btree_table(
"customers",
vec![
_create_column_of_type("id", Type::Integer),
_create_column_of_type("name", Type::Integer),
],
);
let table_order_items = _create_btree_table(
"order_items",
vec![
_create_column_of_type("id", Type::Integer),
_create_column_of_type("order_id", Type::Integer),
_create_column_of_type("product_id", Type::Integer),
_create_column_of_type("quantity", Type::Integer),
],
);
let mut table_id_counter = TableRefIdCounter::new();
let joined_tables = vec![
_create_table_reference(table_orders.clone(), None, table_id_counter.next()),
_create_table_reference(
table_customers.clone(),
Some(JoinInfo {
outer: false,
using: None,
}),
table_id_counter.next(),
),
_create_table_reference(
table_order_items.clone(),
Some(JoinInfo {
outer: false,
using: None,
}),
table_id_counter.next(),
),
];
const TABLE_NO_ORDERS: usize = 0;
const TABLE_NO_CUSTOMERS: usize = 1;
const TABLE_NO_ORDER_ITEMS: usize = 2;
let mut available_indexes = HashMap::new();
["orders", "customers", "order_items"]
.iter()
.for_each(|table_name| {
let index_name = format!("sqlite_autoindex_{}_1", table_name);
let index = Arc::new(Index {
name: index_name,
table_name: table_name.to_string(),
columns: vec![IndexColumn {
name: "id".to_string(),
order: SortOrder::Asc,
pos_in_table: 0,
collation: None,
default: None,
}],
unique: true,
ephemeral: false,
root_page: 1,
has_rowid: true,
});
available_indexes.insert(table_name.to_string(), vec![index]);
});
let customer_id_idx = Arc::new(Index {
name: "orders_customer_id_idx".to_string(),
table_name: "orders".to_string(),
columns: vec![IndexColumn {
name: "customer_id".to_string(),
order: SortOrder::Asc,
pos_in_table: 1,
collation: None,
default: None,
}],
unique: false,
ephemeral: false,
root_page: 1,
has_rowid: true,
});
let order_id_idx = Arc::new(Index {
name: "order_items_order_id_idx".to_string(),
table_name: "order_items".to_string(),
columns: vec![IndexColumn {
name: "order_id".to_string(),
order: SortOrder::Asc,
pos_in_table: 1,
collation: None,
default: None,
}],
unique: false,
ephemeral: false,
root_page: 1,
has_rowid: true,
});
available_indexes
.entry("orders".to_string())
.and_modify(|v| v.push(customer_id_idx));
available_indexes
.entry("order_items".to_string())
.and_modify(|v| v.push(order_id_idx));
let where_clause = vec![
_create_binary_expr(
_create_column_expr(joined_tables[TABLE_NO_ORDERS].internal_id, 1, false), ast::Operator::Equals,
_create_column_expr(joined_tables[TABLE_NO_CUSTOMERS].internal_id, 0, false), ),
_create_binary_expr(
_create_column_expr(joined_tables[TABLE_NO_ORDERS].internal_id, 0, false), ast::Operator::Equals,
_create_column_expr(joined_tables[TABLE_NO_ORDER_ITEMS].internal_id, 1, false), ),
_create_binary_expr(
_create_column_expr(joined_tables[TABLE_NO_CUSTOMERS].internal_id, 0, false), ast::Operator::Equals,
_create_numeric_literal("42"),
),
];
let table_references = TableReferences::new(joined_tables, vec![]);
let access_methods_arena = RefCell::new(Vec::new());
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let result = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap();
assert!(result.is_some());
let BestJoinOrderResult { best_plan, .. } = result.unwrap();
assert_eq!(
best_plan.table_numbers().collect::<Vec<_>>(),
vec![TABLE_NO_CUSTOMERS, TABLE_NO_ORDERS, TABLE_NO_ORDER_ITEMS]
);
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(!access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.as_ref().unwrap().name == "sqlite_autoindex_customers_1");
assert!(access_method.constraint_refs.len() == 1);
let constraint = &table_constraints[TABLE_NO_CUSTOMERS].constraints
[access_method.constraint_refs[0].constraint_vec_pos];
assert!(constraint.lhs_mask.is_empty());
let access_method = &access_methods_arena.borrow()[best_plan.data[1].1];
assert!(!access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.as_ref().unwrap().name == "orders_customer_id_idx");
assert!(access_method.constraint_refs.len() == 1);
let constraint = &table_constraints[TABLE_NO_ORDERS].constraints
[access_method.constraint_refs[0].constraint_vec_pos];
assert!(constraint.lhs_mask.contains_table(TABLE_NO_CUSTOMERS));
let access_method = &access_methods_arena.borrow()[best_plan.data[2].1];
assert!(!access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.as_ref().unwrap().name == "order_items_order_id_idx");
assert!(access_method.constraint_refs.len() == 1);
let constraint = &table_constraints[TABLE_NO_ORDER_ITEMS].constraints
[access_method.constraint_refs[0].constraint_vec_pos];
assert!(constraint.lhs_mask.contains_table(TABLE_NO_ORDERS));
}
struct TestColumn {
name: String,
ty: Type,
is_rowid_alias: bool,
}
impl Default for TestColumn {
fn default() -> Self {
Self {
name: "a".to_string(),
ty: Type::Integer,
is_rowid_alias: false,
}
}
}
#[test]
fn test_join_order_three_tables_no_indexes() {
let t1 = _create_btree_table("t1", _create_column_list(&["id", "foo"], Type::Integer));
let t2 = _create_btree_table("t2", _create_column_list(&["id", "foo"], Type::Integer));
let t3 = _create_btree_table("t3", _create_column_list(&["id", "foo"], Type::Integer));
let mut table_id_counter = TableRefIdCounter::new();
let joined_tables = vec![
_create_table_reference(t1.clone(), None, table_id_counter.next()),
_create_table_reference(
t2.clone(),
Some(JoinInfo {
outer: false,
using: None,
}),
table_id_counter.next(),
),
_create_table_reference(
t3.clone(),
Some(JoinInfo {
outer: false,
using: None,
}),
table_id_counter.next(),
),
];
let where_clause = vec![
_create_binary_expr(
_create_column_expr(joined_tables[1].internal_id, 1, false), ast::Operator::Equals,
_create_numeric_literal("42"),
),
_create_binary_expr(
_create_column_expr(joined_tables[0].internal_id, 1, false), ast::Operator::Greater,
_create_numeric_literal("10"),
),
];
let table_references = TableReferences::new(joined_tables, vec![]);
let available_indexes = HashMap::new();
let access_methods_arena = RefCell::new(Vec::new());
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let BestJoinOrderResult { best_plan, .. } = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap()
.unwrap();
assert_eq!(best_plan.table_numbers().nth(0).unwrap(), 1);
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.is_none());
let access_method = &access_methods_arena.borrow()[best_plan.data[1].1];
assert!(access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.is_none());
let access_method = &access_methods_arena.borrow()[best_plan.data[2].1];
assert!(access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.is_none());
}
#[test]
fn test_compute_best_join_order_star_schema() {
const NUM_DIM_TABLES: usize = 9;
const FACT_TABLE_IDX: usize = 9;
let mut fact_columns = vec![_create_column_rowid_alias("id")];
for i in 0..NUM_DIM_TABLES {
fact_columns.push(_create_column_of_type(
&format!("dim{}_id", i),
Type::Integer,
));
}
let fact_table = _create_btree_table("fact", fact_columns);
let dim_tables: Vec<_> = (0..NUM_DIM_TABLES)
.map(|i| {
_create_btree_table(
&format!("dim{}", i),
vec![
_create_column_rowid_alias("id"),
_create_column_of_type("value", Type::Integer),
],
)
})
.collect();
let mut table_id_counter = TableRefIdCounter::new();
let joined_tables = {
let mut refs = vec![_create_table_reference(
dim_tables[0].clone(),
None,
table_id_counter.next(),
)];
refs.extend(dim_tables.iter().skip(1).map(|t| {
_create_table_reference(
t.clone(),
Some(JoinInfo {
outer: false,
using: None,
}),
table_id_counter.next(),
)
}));
refs.push(_create_table_reference(
fact_table.clone(),
Some(JoinInfo {
outer: false,
using: None,
}),
table_id_counter.next(),
));
refs
};
let mut where_clause = vec![];
for i in 0..NUM_DIM_TABLES {
let internal_id_fact = joined_tables[FACT_TABLE_IDX].internal_id;
let internal_id_other = joined_tables[i].internal_id;
where_clause.push(_create_binary_expr(
_create_column_expr(internal_id_fact, i + 1, false), ast::Operator::Equals,
_create_column_expr(internal_id_other, 0, true), ));
}
let table_references = TableReferences::new(joined_tables, vec![]);
let access_methods_arena = RefCell::new(Vec::new());
let available_indexes = HashMap::new();
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let result = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap();
assert!(result.is_some());
let BestJoinOrderResult { best_plan, .. } = result.unwrap();
assert_eq!(
best_plan.table_numbers().nth(0).unwrap(),
FACT_TABLE_IDX,
"First table should be fact (table {}) due to available index, got table {} instead",
FACT_TABLE_IDX,
best_plan.table_numbers().nth(0).unwrap()
);
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.is_none());
assert!(access_method.constraint_refs.is_empty());
for (table_number, access_method_index) in best_plan.data.iter().skip(1) {
let access_method = &access_methods_arena.borrow()[*access_method_index];
assert!(!access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.is_none());
assert!(access_method.constraint_refs.len() == 1);
let constraint = &table_constraints[*table_number].constraints
[access_method.constraint_refs[0].constraint_vec_pos];
assert!(constraint.lhs_mask.contains_table(FACT_TABLE_IDX));
assert!(constraint.operator == ast::Operator::Equals);
}
}
#[test]
fn test_compute_best_join_order_linked_list() {
const NUM_TABLES: usize = 5;
let mut tables = Vec::with_capacity(NUM_TABLES);
for i in 0..NUM_TABLES {
let mut columns = vec![_create_column_rowid_alias("id")];
if i < NUM_TABLES - 1 {
columns.push(_create_column_of_type(&format!("next_id"), Type::Integer));
}
tables.push(_create_btree_table(&format!("t{}", i + 1), columns));
}
let available_indexes = HashMap::new();
let mut table_id_counter = TableRefIdCounter::new();
let joined_tables: Vec<_> = tables
.iter()
.map(|t| _create_table_reference(t.clone(), None, table_id_counter.next()))
.collect();
let mut where_clause = Vec::new();
for i in 0..NUM_TABLES - 1 {
let internal_id_left = joined_tables[i].internal_id;
let internal_id_right = joined_tables[i + 1].internal_id;
where_clause.push(_create_binary_expr(
_create_column_expr(internal_id_left, 1, false), ast::Operator::Equals,
_create_column_expr(internal_id_right, 0, true), ));
}
let table_references = TableReferences::new(joined_tables, vec![]);
let access_methods_arena = RefCell::new(Vec::new());
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let BestJoinOrderResult { best_plan, .. } = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap()
.unwrap();
for i in 0..NUM_TABLES {
assert_eq!(
best_plan.table_numbers().nth(i).unwrap(),
i,
"Expected table {} at position {}, got table {} instead",
i,
i,
best_plan.table_numbers().nth(i).unwrap()
);
}
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.is_none());
assert!(access_method.constraint_refs.is_empty());
for i in 1..NUM_TABLES {
let access_method = &access_methods_arena.borrow()[best_plan.data[i].1];
assert!(!access_method.is_scan());
assert!(access_method.iter_dir == IterationDirection::Forwards);
assert!(access_method.index.is_none());
assert!(access_method.constraint_refs.len() == 1);
let constraint = &table_constraints[i].constraints
[access_method.constraint_refs[0].constraint_vec_pos];
assert!(constraint.lhs_mask.contains_table(i - 1));
assert!(constraint.operator == ast::Operator::Equals);
}
}
#[test]
fn test_index_second_column_only() {
let mut joined_tables = Vec::new();
let mut table_id_counter = TableRefIdCounter::new();
let table = _create_btree_table("t1", _create_column_list(&["x", "y"], Type::Integer));
let index = Arc::new(Index {
name: "idx_xy".to_string(),
table_name: "t1".to_string(),
columns: vec![
IndexColumn {
name: "x".to_string(),
order: SortOrder::Asc,
pos_in_table: 0,
collation: None,
default: None,
},
IndexColumn {
name: "y".to_string(),
order: SortOrder::Asc,
pos_in_table: 1,
collation: None,
default: None,
},
],
unique: false,
root_page: 2,
ephemeral: false,
has_rowid: true,
});
let mut available_indexes = HashMap::new();
available_indexes.insert("t1".to_string(), vec![index]);
joined_tables.push(JoinedTable {
table: Table::BTree(table),
internal_id: table_id_counter.next(),
op: Operation::Scan {
iter_dir: IterationDirection::Forwards,
index: None,
},
identifier: "t1".to_string(),
join_info: None,
col_used_mask: ColumnUsedMask::new(),
});
let where_clause = vec![WhereTerm {
expr: Expr::Binary(
Box::new(Expr::Column {
database: None,
table: joined_tables[0].internal_id,
column: 1,
is_rowid_alias: false,
}),
ast::Operator::Equals,
Box::new(Expr::Literal(ast::Literal::Numeric(5.to_string()))),
),
from_outer_join: None,
consumed: Cell::new(false),
}];
let table_references = TableReferences::new(joined_tables, vec![]);
let access_methods_arena = RefCell::new(Vec::new());
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let BestJoinOrderResult { best_plan, .. } = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap()
.unwrap();
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(access_method.is_scan());
}
#[test]
fn test_index_skips_middle_column() {
let mut table_id_counter = TableRefIdCounter::new();
let mut joined_tables = Vec::new();
let mut available_indexes = HashMap::new();
let columns = _create_column_list(&["c1", "c2", "c3"], Type::Integer);
let table = _create_btree_table("t1", columns);
let index = Arc::new(Index {
name: "idx1".to_string(),
table_name: "t1".to_string(),
columns: vec![
IndexColumn {
name: "c1".to_string(),
order: SortOrder::Asc,
pos_in_table: 0,
collation: None,
default: None,
},
IndexColumn {
name: "c2".to_string(),
order: SortOrder::Asc,
pos_in_table: 1,
collation: None,
default: None,
},
IndexColumn {
name: "c3".to_string(),
order: SortOrder::Asc,
pos_in_table: 2,
collation: None,
default: None,
},
],
unique: false,
root_page: 2,
ephemeral: false,
has_rowid: true,
});
available_indexes.insert("t1".to_string(), vec![index]);
joined_tables.push(JoinedTable {
table: Table::BTree(table),
internal_id: table_id_counter.next(),
op: Operation::Scan {
iter_dir: IterationDirection::Forwards,
index: None,
},
identifier: "t1".to_string(),
join_info: None,
col_used_mask: ColumnUsedMask::new(),
});
let where_clause = vec![
WhereTerm {
expr: Expr::Binary(
Box::new(Expr::Column {
database: None,
table: joined_tables[0].internal_id,
column: 0, is_rowid_alias: false,
}),
ast::Operator::Equals,
Box::new(Expr::Literal(ast::Literal::Numeric(5.to_string()))),
),
from_outer_join: None,
consumed: Cell::new(false),
},
WhereTerm {
expr: Expr::Binary(
Box::new(Expr::Column {
database: None,
table: joined_tables[0].internal_id,
column: 2, is_rowid_alias: false,
}),
ast::Operator::Equals,
Box::new(Expr::Literal(ast::Literal::Numeric(7.to_string()))),
),
from_outer_join: None,
consumed: Cell::new(false),
},
];
let table_references = TableReferences::new(joined_tables, vec![]);
let access_methods_arena = RefCell::new(Vec::new());
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let BestJoinOrderResult { best_plan, .. } = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap()
.unwrap();
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(!access_method.is_scan());
assert!(access_method
.index
.as_ref()
.is_some_and(|i| i.name == "idx1"));
assert!(access_method.constraint_refs.len() == 1);
let constraint =
&table_constraints[0].constraints[access_method.constraint_refs[0].constraint_vec_pos];
assert!(constraint.operator == ast::Operator::Equals);
assert!(constraint.table_col_pos == 0); }
#[test]
fn test_index_stops_at_range_operator() {
let mut table_id_counter = TableRefIdCounter::new();
let mut joined_tables = Vec::new();
let mut available_indexes = HashMap::new();
let columns = _create_column_list(&["c1", "c2", "c3"], Type::Integer);
let table = _create_btree_table("t1", columns);
let index = Arc::new(Index {
name: "idx1".to_string(),
table_name: "t1".to_string(),
columns: vec![
IndexColumn {
name: "c1".to_string(),
order: SortOrder::Asc,
pos_in_table: 0,
collation: None,
default: None,
},
IndexColumn {
name: "c2".to_string(),
order: SortOrder::Asc,
pos_in_table: 1,
collation: None,
default: None,
},
IndexColumn {
name: "c3".to_string(),
order: SortOrder::Asc,
pos_in_table: 2,
collation: None,
default: None,
},
],
root_page: 2,
ephemeral: false,
has_rowid: true,
unique: false,
});
available_indexes.insert("t1".to_string(), vec![index]);
joined_tables.push(JoinedTable {
table: Table::BTree(table),
internal_id: table_id_counter.next(),
op: Operation::Scan {
iter_dir: IterationDirection::Forwards,
index: None,
},
identifier: "t1".to_string(),
join_info: None,
col_used_mask: ColumnUsedMask::new(),
});
let where_clause = vec![
WhereTerm {
expr: Expr::Binary(
Box::new(Expr::Column {
database: None,
table: joined_tables[0].internal_id,
column: 0, is_rowid_alias: false,
}),
ast::Operator::Equals,
Box::new(Expr::Literal(ast::Literal::Numeric(5.to_string()))),
),
from_outer_join: None,
consumed: Cell::new(false),
},
WhereTerm {
expr: Expr::Binary(
Box::new(Expr::Column {
database: None,
table: joined_tables[0].internal_id,
column: 1, is_rowid_alias: false,
}),
ast::Operator::Greater,
Box::new(Expr::Literal(ast::Literal::Numeric(10.to_string()))),
),
from_outer_join: None,
consumed: Cell::new(false),
},
WhereTerm {
expr: Expr::Binary(
Box::new(Expr::Column {
database: None,
table: joined_tables[0].internal_id,
column: 2, is_rowid_alias: false,
}),
ast::Operator::Equals,
Box::new(Expr::Literal(ast::Literal::Numeric(7.to_string()))),
),
from_outer_join: None,
consumed: Cell::new(false),
},
];
let table_references = TableReferences::new(joined_tables, vec![]);
let access_methods_arena = RefCell::new(Vec::new());
let table_constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let BestJoinOrderResult { best_plan, .. } = compute_best_join_order(
table_references.joined_tables(),
None,
&table_constraints,
&access_methods_arena,
)
.unwrap()
.unwrap();
let access_method = &access_methods_arena.borrow()[best_plan.data[0].1];
assert!(!access_method.is_scan());
assert!(access_method
.index
.as_ref()
.is_some_and(|i| i.name == "idx1"));
assert!(access_method.constraint_refs.len() == 2);
let constraint =
&table_constraints[0].constraints[access_method.constraint_refs[0].constraint_vec_pos];
assert!(constraint.operator == ast::Operator::Equals);
assert!(constraint.table_col_pos == 0); let constraint =
&table_constraints[0].constraints[access_method.constraint_refs[1].constraint_vec_pos];
assert!(constraint.operator == ast::Operator::Greater);
assert!(constraint.table_col_pos == 1); }
fn _create_column(c: &TestColumn) -> Column {
Column {
name: Some(c.name.clone()),
ty: c.ty,
ty_str: c.ty.to_string(),
is_rowid_alias: c.is_rowid_alias,
primary_key: false,
notnull: false,
default: None,
unique: false,
collation: None,
}
}
fn _create_column_of_type(name: &str, ty: Type) -> Column {
_create_column(&TestColumn {
name: name.to_string(),
ty,
is_rowid_alias: false,
})
}
fn _create_column_list(names: &[&str], ty: Type) -> Vec<Column> {
names
.iter()
.map(|name| _create_column_of_type(name, ty))
.collect()
}
fn _create_column_rowid_alias(name: &str) -> Column {
_create_column(&TestColumn {
name: name.to_string(),
ty: Type::Integer,
is_rowid_alias: true,
})
}
fn _create_btree_table(name: &str, columns: Vec<Column>) -> Rc<BTreeTable> {
Rc::new(BTreeTable {
root_page: 1, name: name.to_string(),
primary_key_columns: vec![],
columns,
has_rowid: true,
is_strict: false,
unique_sets: None,
})
}
fn _create_table_reference(
table: Rc<BTreeTable>,
join_info: Option<JoinInfo>,
internal_id: TableInternalId,
) -> JoinedTable {
let name = table.name.clone();
JoinedTable {
table: Table::BTree(table),
op: Operation::Scan {
iter_dir: IterationDirection::Forwards,
index: None,
},
identifier: name,
internal_id,
join_info,
col_used_mask: ColumnUsedMask::new(),
}
}
fn _create_column_expr(table: TableInternalId, column: usize, is_rowid_alias: bool) -> Expr {
Expr::Column {
database: None,
table,
column,
is_rowid_alias,
}
}
fn _create_binary_expr(lhs: Expr, op: Operator, rhs: Expr) -> WhereTerm {
WhereTerm {
expr: Expr::Binary(Box::new(lhs), op, Box::new(rhs)),
from_outer_join: None,
consumed: Cell::new(false),
}
}
fn _create_numeric_literal(value: &str) -> Expr {
Expr::Literal(ast::Literal::Numeric(value.to_string()))
}
}