mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-26 12:34:22 +01:00
994 lines
36 KiB
Rust
994 lines
36 KiB
Rust
use core::fmt;
|
||
use limbo_ext::{ConstraintInfo, ConstraintOp};
|
||
use limbo_sqlite3_parser::ast::{self, SortOrder};
|
||
use std::{
|
||
cmp::Ordering,
|
||
fmt::{Display, Formatter},
|
||
rc::Rc,
|
||
sync::Arc,
|
||
};
|
||
|
||
use crate::{
|
||
function::AggFunc,
|
||
schema::{BTreeTable, Column, Index, Table},
|
||
util::exprs_are_equivalent,
|
||
vdbe::{
|
||
builder::{CursorType, ProgramBuilder},
|
||
BranchOffset, CursorID,
|
||
},
|
||
Result, VirtualTable,
|
||
};
|
||
use crate::{
|
||
schema::{PseudoTable, Type},
|
||
types::SeekOp,
|
||
util::can_pushdown_predicate,
|
||
};
|
||
|
||
use super::{emitter::OperationMode, planner::determine_where_to_eval_term, schema::ParseSchema};
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct ResultSetColumn {
|
||
pub expr: ast::Expr,
|
||
pub alias: Option<String>,
|
||
// TODO: encode which aggregates (e.g. index bitmask of plan.aggregates) are present in this column
|
||
pub contains_aggregates: bool,
|
||
}
|
||
|
||
impl ResultSetColumn {
|
||
pub fn name<'a>(&'a self, tables: &'a [TableReference]) -> Option<&'a str> {
|
||
if let Some(alias) = &self.alias {
|
||
return Some(alias);
|
||
}
|
||
match &self.expr {
|
||
ast::Expr::Column { table, column, .. } => {
|
||
tables[*table].columns()[*column].name.as_deref()
|
||
}
|
||
ast::Expr::RowId { table, .. } => {
|
||
// If there is a rowid alias column, use its name
|
||
if let Table::BTree(table) = &tables[*table].table {
|
||
if let Some(rowid_alias_column) = table.get_rowid_alias_column() {
|
||
if let Some(name) = &rowid_alias_column.1.name {
|
||
return Some(name);
|
||
}
|
||
}
|
||
}
|
||
|
||
// If there is no rowid alias, use "rowid".
|
||
Some("rowid")
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct GroupBy {
|
||
pub exprs: Vec<ast::Expr>,
|
||
/// having clause split into a vec at 'AND' boundaries.
|
||
pub having: Option<Vec<ast::Expr>>,
|
||
pub sort_order: Option<Vec<SortOrder>>,
|
||
}
|
||
|
||
/// In a query plan, WHERE clause conditions and JOIN conditions are all folded into a vector of WhereTerm.
|
||
/// This is done so that we can evaluate the conditions at the correct loop depth.
|
||
/// We also need to keep track of whether the condition came from an OUTER JOIN. Take this example:
|
||
/// SELECT * FROM users u LEFT JOIN products p ON u.id = 5.
|
||
/// Even though the condition only refers to 'u', we CANNOT evaluate it at the users loop, because we need to emit NULL
|
||
/// values for the columns of 'p', for EVERY row in 'u', instead of completely skipping any rows in 'u' where the condition is false.
|
||
#[derive(Debug, Clone)]
|
||
pub struct WhereTerm {
|
||
/// The original condition expression.
|
||
pub expr: ast::Expr,
|
||
/// Is this condition originally from an OUTER JOIN, and which table number in the plan's [TableReference] vector?
|
||
/// If so, we need to evaluate it at the loop of the right table in that JOIN,
|
||
/// regardless of which tables it references.
|
||
/// We also cannot e.g. short circuit the entire query in the optimizer if the condition is statically false.
|
||
pub from_outer_join: Option<usize>,
|
||
}
|
||
|
||
impl WhereTerm {
|
||
pub fn is_constant(&self, join_order: &[JoinOrderMember]) -> bool {
|
||
let Ok(eval_at) = self.eval_at(join_order) else {
|
||
return false;
|
||
};
|
||
eval_at == EvalAt::BeforeLoop
|
||
}
|
||
|
||
pub fn should_eval_at_loop(&self, loop_idx: usize, join_order: &[JoinOrderMember]) -> bool {
|
||
let Ok(eval_at) = self.eval_at(join_order) else {
|
||
return false;
|
||
};
|
||
eval_at == EvalAt::Loop(loop_idx)
|
||
}
|
||
|
||
fn eval_at(&self, join_order: &[JoinOrderMember]) -> Result<EvalAt> {
|
||
determine_where_to_eval_term(&self, join_order)
|
||
}
|
||
}
|
||
|
||
use crate::ast::{Expr, Operator};
|
||
|
||
// This function takes an operator and returns the operator you would obtain if the operands were swapped.
|
||
// e.g. "literal < column"
|
||
// which is not the canonical order for constraint pushdown.
|
||
// This function will return > so that the expression can be treated as if it were written "column > literal"
|
||
fn reverse_operator(op: &Operator) -> Option<Operator> {
|
||
match op {
|
||
Operator::Equals => Some(Operator::Equals),
|
||
Operator::Less => Some(Operator::Greater),
|
||
Operator::LessEquals => Some(Operator::GreaterEquals),
|
||
Operator::Greater => Some(Operator::Less),
|
||
Operator::GreaterEquals => Some(Operator::LessEquals),
|
||
Operator::NotEquals => Some(Operator::NotEquals),
|
||
Operator::Is => Some(Operator::Is),
|
||
Operator::IsNot => Some(Operator::IsNot),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
fn to_ext_constraint_op(op: &Operator) -> Option<ConstraintOp> {
|
||
match op {
|
||
Operator::Equals => Some(ConstraintOp::Eq),
|
||
Operator::Less => Some(ConstraintOp::Lt),
|
||
Operator::LessEquals => Some(ConstraintOp::Le),
|
||
Operator::Greater => Some(ConstraintOp::Gt),
|
||
Operator::GreaterEquals => Some(ConstraintOp::Ge),
|
||
Operator::NotEquals => Some(ConstraintOp::Ne),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// This function takes a WhereTerm for a select involving a VTab at index 'table_index'.
|
||
/// It determines whether or not it involves the given table and whether or not it can
|
||
/// be converted into a ConstraintInfo which can be passed to the vtab module's xBestIndex
|
||
/// method, which will possibly calculate some information to improve the query plan, that we can send
|
||
/// back to it as arguments for the VFilter operation.
|
||
/// is going to be filtered against: e.g:
|
||
/// 'SELECT key, value FROM vtab WHERE key = 'some_key';
|
||
/// we need to send the OwnedValue('some_key') as an argument to VFilter, and possibly omit it from
|
||
/// the filtration in the vdbe layer.
|
||
pub fn convert_where_to_vtab_constraint(
|
||
term: &WhereTerm,
|
||
table_index: usize,
|
||
pred_idx: usize,
|
||
) -> Option<ConstraintInfo> {
|
||
if term.from_outer_join.is_some() {
|
||
return None;
|
||
}
|
||
let Expr::Binary(lhs, op, rhs) = &term.expr else {
|
||
return None;
|
||
};
|
||
let expr_is_ready = |e: &Expr| -> bool { can_pushdown_predicate(e, table_index) };
|
||
let (vcol_idx, op_for_vtab, usable, is_rhs) = match (&**lhs, &**rhs) {
|
||
(
|
||
Expr::Column {
|
||
table: tbl_l,
|
||
column: col_l,
|
||
..
|
||
},
|
||
Expr::Column {
|
||
table: tbl_r,
|
||
column: col_r,
|
||
..
|
||
},
|
||
) => {
|
||
// one side must be the virtual table
|
||
let vtab_on_l = *tbl_l == table_index;
|
||
let vtab_on_r = *tbl_r == table_index;
|
||
if vtab_on_l == vtab_on_r {
|
||
return None; // either both or none -> not convertible
|
||
}
|
||
|
||
if vtab_on_l {
|
||
// vtab on left side: operator unchanged
|
||
let usable = *tbl_r < table_index; // usable if the other table is already positioned
|
||
(col_l, op, usable, false)
|
||
} else {
|
||
// vtab on right side of the expr: reverse operator
|
||
let usable = *tbl_l < table_index;
|
||
(col_r, &reverse_operator(op).unwrap_or(*op), usable, true)
|
||
}
|
||
}
|
||
(Expr::Column { table, column, .. }, other) if *table == table_index => {
|
||
(
|
||
column,
|
||
op,
|
||
expr_is_ready(other), // literal / earlier‑table / deterministic func ?
|
||
false,
|
||
)
|
||
}
|
||
(other, Expr::Column { table, column, .. }) if *table == table_index => (
|
||
column,
|
||
&reverse_operator(op).unwrap_or(*op),
|
||
expr_is_ready(other),
|
||
true,
|
||
),
|
||
|
||
_ => return None, // does not involve the virtual table at all
|
||
};
|
||
|
||
Some(ConstraintInfo {
|
||
column_index: *vcol_idx as u32,
|
||
op: to_ext_constraint_op(op_for_vtab)?,
|
||
usable,
|
||
plan_info: ConstraintInfo::pack_plan_info(pred_idx as u32, is_rhs),
|
||
})
|
||
}
|
||
/// The loop index where to evaluate the condition.
|
||
/// For example, in `SELECT * FROM u JOIN p WHERE u.id = 5`, the condition can already be evaluated at the first loop (idx 0),
|
||
/// because that is the rightmost table that it references.
|
||
///
|
||
/// Conditions like 1=2 can be evaluated before the main loop is opened, because they are constant.
|
||
/// In theory we should be able to statically analyze them all and reduce them to a single boolean value,
|
||
/// but that is not implemented yet.
|
||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||
pub enum EvalAt {
|
||
Loop(usize),
|
||
BeforeLoop,
|
||
}
|
||
|
||
#[allow(clippy::non_canonical_partial_ord_impl)]
|
||
impl PartialOrd for EvalAt {
|
||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||
match (self, other) {
|
||
(EvalAt::Loop(a), EvalAt::Loop(b)) => a.partial_cmp(b),
|
||
(EvalAt::BeforeLoop, EvalAt::BeforeLoop) => Some(Ordering::Equal),
|
||
(EvalAt::BeforeLoop, _) => Some(Ordering::Less),
|
||
(_, EvalAt::BeforeLoop) => Some(Ordering::Greater),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Ord for EvalAt {
|
||
fn cmp(&self, other: &Self) -> Ordering {
|
||
self.partial_cmp(other)
|
||
.expect("total ordering not implemented for EvalAt")
|
||
}
|
||
}
|
||
|
||
/// A query plan is either a SELECT or a DELETE (for now)
|
||
#[derive(Debug, Clone)]
|
||
pub enum Plan {
|
||
Select(SelectPlan),
|
||
Delete(DeletePlan),
|
||
Update(UpdatePlan),
|
||
}
|
||
|
||
/// The type of the query, either top level or subquery
|
||
#[derive(Debug, Clone)]
|
||
pub enum SelectQueryType {
|
||
TopLevel,
|
||
Subquery {
|
||
/// The register that holds the program offset that handles jumping to/from the subquery.
|
||
yield_reg: usize,
|
||
/// The index of the first instruction in the bytecode that implements the subquery.
|
||
coroutine_implementation_start: BranchOffset,
|
||
},
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub struct JoinOrderMember {
|
||
/// The index of the table in the plan's vector of [TableReference]
|
||
pub table_no: usize,
|
||
/// Whether this member is the right side of an OUTER JOIN
|
||
pub is_outer: bool,
|
||
}
|
||
|
||
impl Default for JoinOrderMember {
|
||
fn default() -> Self {
|
||
Self {
|
||
table_no: 0,
|
||
is_outer: false,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct SelectPlan {
|
||
/// List of table references in loop order, outermost first.
|
||
pub table_references: Vec<TableReference>,
|
||
/// The order in which the tables are joined. Tables have usize Ids (their index in table_references)
|
||
pub join_order: Vec<JoinOrderMember>,
|
||
/// the columns inside SELECT ... FROM
|
||
pub result_columns: Vec<ResultSetColumn>,
|
||
/// where clause split into a vec at 'AND' boundaries. all join conditions also get shoved in here,
|
||
/// and we keep track of which join they came from (mainly for OUTER JOIN processing)
|
||
pub where_clause: Vec<WhereTerm>,
|
||
/// group by clause
|
||
pub group_by: Option<GroupBy>,
|
||
/// order by clause
|
||
pub order_by: Option<Vec<(ast::Expr, SortOrder)>>,
|
||
/// all the aggregates collected from the result columns, order by, and (TODO) having clauses
|
||
pub aggregates: Vec<Aggregate>,
|
||
/// limit clause
|
||
pub limit: Option<isize>,
|
||
/// offset clause
|
||
pub offset: Option<isize>,
|
||
/// query contains a constant condition that is always false
|
||
pub contains_constant_false_condition: bool,
|
||
/// query type (top level or subquery)
|
||
pub query_type: SelectQueryType,
|
||
}
|
||
|
||
impl SelectPlan {
|
||
pub fn agg_args_count(&self) -> usize {
|
||
self.aggregates.iter().map(|agg| agg.args.len()).sum()
|
||
}
|
||
|
||
pub fn group_by_col_count(&self) -> usize {
|
||
self.group_by
|
||
.as_ref()
|
||
.map_or(0, |group_by| group_by.exprs.len())
|
||
}
|
||
|
||
pub fn non_group_by_non_agg_columns(&self) -> impl Iterator<Item = &ast::Expr> {
|
||
self.result_columns
|
||
.iter()
|
||
.filter(|c| {
|
||
!c.contains_aggregates
|
||
&& !self.group_by.as_ref().map_or(false, |group_by| {
|
||
group_by
|
||
.exprs
|
||
.iter()
|
||
.any(|expr| exprs_are_equivalent(&c.expr, expr))
|
||
})
|
||
})
|
||
.map(|c| &c.expr)
|
||
}
|
||
|
||
pub fn non_group_by_non_agg_column_count(&self) -> usize {
|
||
self.non_group_by_non_agg_columns().count()
|
||
}
|
||
|
||
pub fn group_by_sorter_column_count(&self) -> usize {
|
||
self.agg_args_count() + self.group_by_col_count() + self.non_group_by_non_agg_column_count()
|
||
}
|
||
|
||
/// Reference: https://github.com/sqlite/sqlite/blob/5db695197b74580c777b37ab1b787531f15f7f9f/src/select.c#L8613
|
||
///
|
||
/// Checks to see if the query is of the format `SELECT count(*) FROM <tbl>`
|
||
pub fn is_simple_count(&self) -> bool {
|
||
if !self.where_clause.is_empty()
|
||
|| self.aggregates.len() != 1
|
||
|| matches!(self.query_type, SelectQueryType::Subquery { .. })
|
||
|| self.table_references.len() != 1
|
||
|| self.result_columns.len() != 1
|
||
|| self.group_by.is_some()
|
||
|| self.contains_constant_false_condition
|
||
// TODO: (pedrocarlo) maybe can optimize to use the count optmization with more columns
|
||
{
|
||
return false;
|
||
}
|
||
let table_ref = self.table_references.first().unwrap();
|
||
if !matches!(table_ref.table, crate::schema::Table::BTree(..)) {
|
||
return false;
|
||
}
|
||
let agg = self.aggregates.first().unwrap();
|
||
if !matches!(agg.func, AggFunc::Count0) {
|
||
return false;
|
||
}
|
||
|
||
let count = limbo_sqlite3_parser::ast::Expr::FunctionCall {
|
||
name: limbo_sqlite3_parser::ast::Id("count".to_string()),
|
||
distinctness: None,
|
||
args: None,
|
||
order_by: None,
|
||
filter_over: None,
|
||
};
|
||
let count_star = limbo_sqlite3_parser::ast::Expr::FunctionCallStar {
|
||
name: limbo_sqlite3_parser::ast::Id("count".to_string()),
|
||
filter_over: None,
|
||
};
|
||
let result_col_expr = &self.result_columns.get(0).unwrap().expr;
|
||
if *result_col_expr != count && *result_col_expr != count_star {
|
||
return false;
|
||
}
|
||
true
|
||
}
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
#[derive(Debug, Clone)]
|
||
pub struct DeletePlan {
|
||
/// List of table references. Delete is always a single table.
|
||
pub table_references: Vec<TableReference>,
|
||
/// the columns inside SELECT ... FROM
|
||
pub result_columns: Vec<ResultSetColumn>,
|
||
/// where clause split into a vec at 'AND' boundaries.
|
||
pub where_clause: Vec<WhereTerm>,
|
||
/// order by clause
|
||
pub order_by: Option<Vec<(ast::Expr, SortOrder)>>,
|
||
/// limit clause
|
||
pub limit: Option<isize>,
|
||
/// offset clause
|
||
pub offset: Option<isize>,
|
||
/// query contains a constant condition that is always false
|
||
pub contains_constant_false_condition: bool,
|
||
/// Indexes that must be updated by the delete operation.
|
||
pub indexes: Vec<Arc<Index>>,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct UpdatePlan {
|
||
// table being updated is always first
|
||
pub table_references: Vec<TableReference>,
|
||
// (colum index, new value) pairs
|
||
pub set_clauses: Vec<(usize, ast::Expr)>,
|
||
pub where_clause: Vec<WhereTerm>,
|
||
pub order_by: Option<Vec<(ast::Expr, SortOrder)>>,
|
||
pub limit: Option<isize>,
|
||
pub offset: Option<isize>,
|
||
// TODO: optional RETURNING clause
|
||
pub returning: Option<Vec<ResultSetColumn>>,
|
||
// whether the WHERE clause is always false
|
||
pub contains_constant_false_condition: bool,
|
||
pub indexes_to_update: Vec<Arc<Index>>,
|
||
pub parse_schema: ParseSchema,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum IterationDirection {
|
||
Forwards,
|
||
Backwards,
|
||
}
|
||
|
||
pub fn select_star(tables: &[TableReference], out_columns: &mut Vec<ResultSetColumn>) {
|
||
for (current_table_index, table) in tables.iter().enumerate() {
|
||
let maybe_using_cols = table
|
||
.join_info
|
||
.as_ref()
|
||
.and_then(|join_info| join_info.using.as_ref());
|
||
out_columns.extend(
|
||
table
|
||
.columns()
|
||
.iter()
|
||
.enumerate()
|
||
.filter(|(_, col)| {
|
||
// If we are joining with USING, we need to deduplicate the columns from the right table
|
||
// that are also present in the USING clause.
|
||
if let Some(using_cols) = maybe_using_cols {
|
||
!using_cols.iter().any(|using_col| {
|
||
col.name
|
||
.as_ref()
|
||
.map_or(false, |name| name.eq_ignore_ascii_case(&using_col.0))
|
||
})
|
||
} else {
|
||
true
|
||
}
|
||
})
|
||
.map(|(i, col)| ResultSetColumn {
|
||
alias: None,
|
||
expr: ast::Expr::Column {
|
||
database: None,
|
||
table: current_table_index,
|
||
column: i,
|
||
is_rowid_alias: col.is_rowid_alias,
|
||
},
|
||
contains_aggregates: false,
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Join information for a table reference.
|
||
#[derive(Debug, Clone)]
|
||
pub struct JoinInfo {
|
||
/// Whether this is an OUTER JOIN.
|
||
pub outer: bool,
|
||
/// The USING clause for the join, if any. NATURAL JOIN is transformed into USING (col1, col2, ...).
|
||
pub using: Option<ast::DistinctNames>,
|
||
}
|
||
|
||
/// A table reference in the query plan.
|
||
/// For example, SELECT * FROM users u JOIN products p JOIN (SELECT * FROM users) sub
|
||
/// has three table references:
|
||
/// 1. operation=Scan, table=users, table_identifier=u, reference_type=BTreeTable, join_info=None
|
||
/// 2. operation=Scan, table=products, table_identifier=p, reference_type=BTreeTable, join_info=Some(JoinInfo { outer: false, using: None }),
|
||
/// 3. operation=Subquery, table=users, table_identifier=sub, reference_type=Subquery, join_info=None
|
||
#[derive(Debug, Clone)]
|
||
pub struct TableReference {
|
||
/// The operation that this table reference performs.
|
||
pub op: Operation,
|
||
/// Table object, which contains metadata about the table, e.g. columns.
|
||
pub table: Table,
|
||
/// The name of the table as referred to in the query, either the literal name or an alias e.g. "users" or "u"
|
||
pub identifier: String,
|
||
/// The join info for this table reference, if it is the right side of a join (which all except the first table reference have)
|
||
pub join_info: Option<JoinInfo>,
|
||
/// Bitmask of columns that are referenced in the query.
|
||
/// Used to decide whether a covering index can be used.
|
||
pub col_used_mask: ColumnUsedMask,
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
#[repr(transparent)]
|
||
pub struct ColumnUsedMask(u128);
|
||
|
||
impl ColumnUsedMask {
|
||
pub fn new() -> Self {
|
||
Self(0)
|
||
}
|
||
|
||
pub fn set(&mut self, index: usize) {
|
||
assert!(
|
||
index < 128,
|
||
"ColumnUsedMask only supports up to 128 columns"
|
||
);
|
||
self.0 |= 1 << index;
|
||
}
|
||
|
||
pub fn get(&self, index: usize) -> bool {
|
||
assert!(
|
||
index < 128,
|
||
"ColumnUsedMask only supports up to 128 columns"
|
||
);
|
||
self.0 & (1 << index) != 0
|
||
}
|
||
|
||
pub fn contains_all_set_bits_of(&self, other: &Self) -> bool {
|
||
self.0 & other.0 == other.0
|
||
}
|
||
|
||
pub fn is_empty(&self) -> bool {
|
||
self.0 == 0
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub enum Operation {
|
||
// Scan operation
|
||
// This operation is used to scan a table.
|
||
// The iter_dir is used to indicate the direction of the iterator.
|
||
Scan {
|
||
iter_dir: IterationDirection,
|
||
/// The index that we are using to scan the table, if any.
|
||
index: Option<Arc<Index>>,
|
||
},
|
||
// Search operation
|
||
// This operation is used to search for a row in a table using an index
|
||
// (i.e. a primary key or a secondary index)
|
||
Search(Search),
|
||
/// Subquery operation
|
||
/// This operation is used to represent a subquery in the query plan.
|
||
/// The subquery itself (recursively) contains an arbitrary SelectPlan.
|
||
Subquery {
|
||
plan: Box<SelectPlan>,
|
||
result_columns_start_reg: usize,
|
||
},
|
||
}
|
||
|
||
impl Operation {
|
||
pub fn index(&self) -> Option<&Arc<Index>> {
|
||
match self {
|
||
Operation::Scan { index, .. } => index.as_ref(),
|
||
Operation::Search(Search::RowidEq { .. }) => None,
|
||
Operation::Search(Search::Seek { index, .. }) => index.as_ref(),
|
||
Operation::Subquery { .. } => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl TableReference {
|
||
/// Returns the btree table for this table reference, if it is a BTreeTable.
|
||
pub fn btree(&self) -> Option<Rc<BTreeTable>> {
|
||
match &self.table {
|
||
Table::BTree(_) => self.table.btree(),
|
||
_ => None,
|
||
}
|
||
}
|
||
pub fn virtual_table(&self) -> Option<Rc<VirtualTable>> {
|
||
match &self.table {
|
||
Table::Virtual(_) => self.table.virtual_table(),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// Creates a new TableReference for a subquery.
|
||
pub fn new_subquery(identifier: String, plan: SelectPlan, join_info: Option<JoinInfo>) -> Self {
|
||
let table = Table::Pseudo(Rc::new(PseudoTable::new_with_columns(
|
||
plan.result_columns
|
||
.iter()
|
||
.map(|rc| Column {
|
||
name: rc.name(&plan.table_references).map(String::from),
|
||
ty: Type::Text, // FIXME: infer proper type
|
||
ty_str: "TEXT".to_string(),
|
||
is_rowid_alias: false,
|
||
primary_key: false,
|
||
notnull: false,
|
||
default: None,
|
||
})
|
||
.collect(),
|
||
)));
|
||
Self {
|
||
op: Operation::Subquery {
|
||
plan: Box::new(plan),
|
||
result_columns_start_reg: 0, // Will be set in the bytecode emission phase
|
||
},
|
||
table,
|
||
identifier: identifier.clone(),
|
||
join_info,
|
||
col_used_mask: ColumnUsedMask::new(),
|
||
}
|
||
}
|
||
|
||
pub fn columns(&self) -> &[Column] {
|
||
self.table.columns()
|
||
}
|
||
|
||
/// Mark a column as used in the query.
|
||
/// This is used to determine whether a covering index can be used.
|
||
pub fn mark_column_used(&mut self, index: usize) {
|
||
self.col_used_mask.set(index);
|
||
}
|
||
|
||
/// Open the necessary cursors for this table reference.
|
||
/// Generally a table cursor is always opened unless a SELECT query can use a covering index.
|
||
/// An index cursor is opened if an index is used in any way for reading data from the table.
|
||
pub fn open_cursors(
|
||
&self,
|
||
program: &mut ProgramBuilder,
|
||
mode: OperationMode,
|
||
) -> Result<(Option<CursorID>, Option<CursorID>)> {
|
||
let index = self.op.index();
|
||
match &self.table {
|
||
Table::BTree(btree) => {
|
||
let use_covering_index = self.utilizes_covering_index();
|
||
let index_is_ephemeral = index.map_or(false, |index| index.ephemeral);
|
||
let table_not_required =
|
||
OperationMode::SELECT == mode && use_covering_index && !index_is_ephemeral;
|
||
let table_cursor_id = if table_not_required {
|
||
None
|
||
} else {
|
||
Some(program.alloc_cursor_id(
|
||
Some(self.identifier.clone()),
|
||
CursorType::BTreeTable(btree.clone()),
|
||
))
|
||
};
|
||
let index_cursor_id = if let Some(index) = index {
|
||
Some(program.alloc_cursor_id(
|
||
Some(index.name.clone()),
|
||
CursorType::BTreeIndex(index.clone()),
|
||
))
|
||
} else {
|
||
None
|
||
};
|
||
Ok((table_cursor_id, index_cursor_id))
|
||
}
|
||
Table::Virtual(virtual_table) => {
|
||
let table_cursor_id = Some(program.alloc_cursor_id(
|
||
Some(self.identifier.clone()),
|
||
CursorType::VirtualTable(virtual_table.clone()),
|
||
));
|
||
let index_cursor_id = None;
|
||
Ok((table_cursor_id, index_cursor_id))
|
||
}
|
||
Table::Pseudo(_) => Ok((None, None)),
|
||
}
|
||
}
|
||
|
||
/// Resolve the already opened cursors for this table reference.
|
||
pub fn resolve_cursors(
|
||
&self,
|
||
program: &mut ProgramBuilder,
|
||
) -> Result<(Option<CursorID>, Option<CursorID>)> {
|
||
let index = self.op.index();
|
||
let table_cursor_id = program.resolve_cursor_id_safe(&self.identifier);
|
||
let index_cursor_id = index.map(|index| program.resolve_cursor_id(&index.name));
|
||
Ok((table_cursor_id, index_cursor_id))
|
||
}
|
||
|
||
/// Returns true if a given index is a covering index for this [TableReference].
|
||
pub fn index_is_covering(&self, index: &Index) -> bool {
|
||
let Table::BTree(btree) = &self.table else {
|
||
return false;
|
||
};
|
||
if self.col_used_mask.is_empty() {
|
||
return false;
|
||
}
|
||
let mut index_cols_mask = ColumnUsedMask::new();
|
||
for col in index.columns.iter() {
|
||
index_cols_mask.set(col.pos_in_table);
|
||
}
|
||
|
||
// If a table has a rowid (i.e. is not a WITHOUT ROWID table), the index is guaranteed to contain the rowid as well.
|
||
if btree.has_rowid {
|
||
if let Some(pos_of_rowid_alias_col) = btree.get_rowid_alias_column().map(|(pos, _)| pos)
|
||
{
|
||
let mut empty_mask = ColumnUsedMask::new();
|
||
empty_mask.set(pos_of_rowid_alias_col);
|
||
if self.col_used_mask == empty_mask {
|
||
// However if the index would be ONLY used for the rowid, then let's not bother using it to cover the query.
|
||
// Example: if the query is SELECT id FROM t, and id is a rowid alias, then let's rather just scan the table
|
||
// instead of an index.
|
||
return false;
|
||
}
|
||
index_cols_mask.set(pos_of_rowid_alias_col);
|
||
}
|
||
}
|
||
|
||
index_cols_mask.contains_all_set_bits_of(&self.col_used_mask)
|
||
}
|
||
|
||
/// Returns true if the index selected for use with this [TableReference] is a covering index,
|
||
/// meaning that it contains all the columns that are referenced in the query.
|
||
pub fn utilizes_covering_index(&self) -> bool {
|
||
let Some(index) = self.op.index() else {
|
||
return false;
|
||
};
|
||
self.index_is_covering(index.as_ref())
|
||
}
|
||
|
||
pub fn column_is_used(&self, index: usize) -> bool {
|
||
self.col_used_mask.get(index)
|
||
}
|
||
}
|
||
|
||
/// A definition of a rowid/index search.
|
||
///
|
||
/// [SeekKey] is the condition that is used to seek to a specific row in a table/index.
|
||
/// [TerminationKey] is the condition that is used to terminate the search after a seek.
|
||
#[derive(Debug, Clone)]
|
||
pub struct SeekDef {
|
||
/// The key to use when seeking and when terminating the scan that follows the seek.
|
||
/// For example, given:
|
||
/// - CREATE INDEX i ON t (x, y desc)
|
||
/// - SELECT * FROM t WHERE x = 1 AND y >= 30
|
||
/// The key is [(1, ASC), (30, DESC)]
|
||
pub key: Vec<(ast::Expr, SortOrder)>,
|
||
/// The condition to use when seeking. See [SeekKey] for more details.
|
||
pub seek: Option<SeekKey>,
|
||
/// The condition to use when terminating the scan that follows the seek. See [TerminationKey] for more details.
|
||
pub termination: Option<TerminationKey>,
|
||
/// The direction of the scan that follows the seek.
|
||
pub iter_dir: IterationDirection,
|
||
}
|
||
|
||
/// A condition to use when seeking.
|
||
#[derive(Debug, Clone)]
|
||
pub struct SeekKey {
|
||
/// How many columns from [SeekDef::key] are used in seeking.
|
||
pub len: usize,
|
||
/// Whether to NULL pad the last column of the seek key to match the length of [SeekDef::key].
|
||
/// The reason it is done is that sometimes our full index key is not used in seeking,
|
||
/// but we want to find the lowest value that matches the non-null prefix of the key.
|
||
/// For example, given:
|
||
/// - CREATE INDEX i ON t (x, y)
|
||
/// - SELECT * FROM t WHERE x = 1 AND y < 30
|
||
/// We want to seek to the first row where x = 1, and then iterate forwards.
|
||
/// In this case, the seek key is GT(1, NULL) since NULL is always LT in index key comparisons.
|
||
/// We can't use just GT(1) because in index key comparisons, only the given number of columns are compared,
|
||
/// so this means any index keys with (x=1) will compare equal, e.g. (x=1, y=usize::MAX) will compare equal to the seek key (x:1)
|
||
pub null_pad: bool,
|
||
/// The comparison operator to use when seeking.
|
||
pub op: SeekOp,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
/// A condition to use when terminating the scan that follows a seek.
|
||
pub struct TerminationKey {
|
||
/// How many columns from [SeekDef::key] are used in terminating the scan that follows the seek.
|
||
pub len: usize,
|
||
/// Whether to NULL pad the last column of the termination key to match the length of [SeekDef::key].
|
||
/// See [SeekKey::null_pad].
|
||
pub null_pad: bool,
|
||
/// The comparison operator to use when terminating the scan that follows the seek.
|
||
pub op: SeekOp,
|
||
}
|
||
|
||
/// An enum that represents a search operation that can be used to search for a row in a table using an index
|
||
/// (i.e. a primary key or a secondary index)
|
||
#[allow(clippy::enum_variant_names)]
|
||
#[derive(Clone, Debug)]
|
||
pub enum Search {
|
||
/// A rowid equality point lookup. This is a special case that uses the SeekRowid bytecode instruction and does not loop.
|
||
RowidEq { cmp_expr: WhereTerm },
|
||
/// A search on a table btree (via `rowid`) or a secondary index search. Uses bytecode instructions like SeekGE, SeekGT etc.
|
||
Seek {
|
||
index: Option<Arc<Index>>,
|
||
seek_def: SeekDef,
|
||
},
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct Aggregate {
|
||
pub func: AggFunc,
|
||
pub args: Vec<ast::Expr>,
|
||
pub original_expr: ast::Expr,
|
||
}
|
||
|
||
impl Display for Aggregate {
|
||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||
let args_str = self
|
||
.args
|
||
.iter()
|
||
.map(|arg| arg.to_string())
|
||
.collect::<Vec<String>>()
|
||
.join(", ");
|
||
write!(f, "{:?}({})", self.func, args_str)
|
||
}
|
||
}
|
||
|
||
/// For EXPLAIN QUERY PLAN
|
||
impl Display for Plan {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||
match self {
|
||
Self::Select(select_plan) => select_plan.fmt(f),
|
||
Self::Delete(delete_plan) => delete_plan.fmt(f),
|
||
Self::Update(update_plan) => update_plan.fmt(f),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Display for SelectPlan {
|
||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||
writeln!(f, "QUERY PLAN")?;
|
||
|
||
// Print each table reference with appropriate indentation based on join depth
|
||
for (i, reference) in self.table_references.iter().enumerate() {
|
||
let is_last = i == self.table_references.len() - 1;
|
||
let indent = if i == 0 {
|
||
if is_last { "`--" } else { "|--" }.to_string()
|
||
} else {
|
||
format!(
|
||
" {}{}",
|
||
"| ".repeat(i - 1),
|
||
if is_last { "`--" } else { "|--" }
|
||
)
|
||
};
|
||
|
||
match &reference.op {
|
||
Operation::Scan { .. } => {
|
||
let table_name = if reference.table.get_name() == reference.identifier {
|
||
reference.identifier.clone()
|
||
} else {
|
||
format!("{} AS {}", reference.table.get_name(), reference.identifier)
|
||
};
|
||
|
||
writeln!(f, "{}SCAN {}", indent, table_name)?;
|
||
}
|
||
Operation::Search(search) => match search {
|
||
Search::RowidEq { .. } | Search::Seek { index: None, .. } => {
|
||
writeln!(
|
||
f,
|
||
"{}SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)",
|
||
indent, reference.identifier
|
||
)?;
|
||
}
|
||
Search::Seek {
|
||
index: Some(index), ..
|
||
} => {
|
||
writeln!(
|
||
f,
|
||
"{}SEARCH {} USING INDEX {}",
|
||
indent, reference.identifier, index.name
|
||
)?;
|
||
}
|
||
},
|
||
Operation::Subquery { plan, .. } => {
|
||
writeln!(f, "{}SUBQUERY {}", indent, reference.identifier)?;
|
||
// Indent and format the subquery plan
|
||
for line in format!("{}", plan).lines() {
|
||
writeln!(f, "{} {}", indent, line)?;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Display for DeletePlan {
|
||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||
writeln!(f, "QUERY PLAN")?;
|
||
|
||
// Delete plan should only have one table reference
|
||
if let Some(reference) = self.table_references.first() {
|
||
let indent = "`--";
|
||
|
||
match &reference.op {
|
||
Operation::Scan { .. } => {
|
||
let table_name = if reference.table.get_name() == reference.identifier {
|
||
reference.identifier.clone()
|
||
} else {
|
||
format!("{} AS {}", reference.table.get_name(), reference.identifier)
|
||
};
|
||
|
||
writeln!(f, "{}DELETE FROM {}", indent, table_name)?;
|
||
}
|
||
Operation::Search { .. } => {
|
||
panic!("DELETE plans should not contain search operations");
|
||
}
|
||
Operation::Subquery { .. } => {
|
||
panic!("DELETE plans should not contain subqueries");
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl fmt::Display for UpdatePlan {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
writeln!(f, "QUERY PLAN")?;
|
||
|
||
for (i, reference) in self.table_references.iter().enumerate() {
|
||
let is_last = i == self.table_references.len() - 1;
|
||
let indent = if i == 0 {
|
||
if is_last { "`--" } else { "|--" }.to_string()
|
||
} else {
|
||
format!(
|
||
" {}{}",
|
||
"| ".repeat(i - 1),
|
||
if is_last { "`--" } else { "|--" }
|
||
)
|
||
};
|
||
|
||
match &reference.op {
|
||
Operation::Scan { .. } => {
|
||
let table_name = if reference.table.get_name() == reference.identifier {
|
||
reference.identifier.clone()
|
||
} else {
|
||
format!("{} AS {}", reference.table.get_name(), reference.identifier)
|
||
};
|
||
|
||
if i == 0 {
|
||
writeln!(f, "{}UPDATE {}", indent, table_name)?;
|
||
} else {
|
||
writeln!(f, "{}SCAN {}", indent, table_name)?;
|
||
}
|
||
}
|
||
Operation::Search(search) => match search {
|
||
Search::RowidEq { .. } | Search::Seek { index: None, .. } => {
|
||
writeln!(
|
||
f,
|
||
"{}SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)",
|
||
indent, reference.identifier
|
||
)?;
|
||
}
|
||
Search::Seek {
|
||
index: Some(index), ..
|
||
} => {
|
||
writeln!(
|
||
f,
|
||
"{}SEARCH {} USING INDEX {}",
|
||
indent, reference.identifier, index.name
|
||
)?;
|
||
}
|
||
},
|
||
Operation::Subquery { plan, .. } => {
|
||
writeln!(f, "{}SUBQUERY {}", indent, reference.identifier)?;
|
||
for line in format!("{}", plan).lines() {
|
||
writeln!(f, "{} {}", indent, line)?;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if let Some(order_by) = &self.order_by {
|
||
writeln!(f, "ORDER BY:")?;
|
||
for (expr, dir) in order_by {
|
||
writeln!(
|
||
f,
|
||
" - {} {}",
|
||
expr,
|
||
if *dir == SortOrder::Asc {
|
||
"ASC"
|
||
} else {
|
||
"DESC"
|
||
}
|
||
)?;
|
||
}
|
||
}
|
||
if let Some(limit) = self.limit {
|
||
writeln!(f, "LIMIT: {}", limit)?;
|
||
}
|
||
if let Some(ret) = &self.returning {
|
||
writeln!(f, "RETURNING:")?;
|
||
for col in ret {
|
||
writeln!(f, " - {}", col.expr)?;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|