use crate::error::{Error, Result};
use crate::exec::eval::Affinity;
use crate::sql::ast::{BinaryOp, Expr, JoinKind, Literal, OrderTerm, ResultColumn, Select};
use crate::value::{Collation, Value};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq)]
#[allow(missing_docs)] pub enum Op {
Integer { value: i64, dest: usize },
Real { value: f64, dest: usize },
Str { value: String, dest: usize },
Blob { value: Vec<u8>, dest: usize },
Null { dest: usize },
Arith {
op: BinaryOp,
lhs: usize,
rhs: usize,
dest: usize,
},
Bitwise {
op: BinaryOp,
lhs: usize,
rhs: usize,
dest: usize,
},
Is {
is: bool,
lhs: usize,
rhs: usize,
dest: usize,
la: Option<Affinity>,
ra: Option<Affinity>,
},
Truthy {
want: bool,
not: bool,
operand: usize,
dest: usize,
},
Like {
glob: bool,
lhs: usize,
rhs: usize,
dest: usize,
},
Json {
as_text: bool,
lhs: usize,
rhs: usize,
dest: usize,
},
Func {
name: String,
arg_start: usize,
arg_count: usize,
dest: usize,
},
Concat { lhs: usize, rhs: usize, dest: usize },
Compare {
op: BinaryOp,
lhs: usize,
rhs: usize,
dest: usize,
la: Option<Affinity>,
ra: Option<Affinity>,
coll: Collation,
},
And { lhs: usize, rhs: usize, dest: usize },
Or { lhs: usize, rhs: usize, dest: usize },
Not { reg: usize, dest: usize },
IsNull {
reg: usize,
negated: bool,
dest: usize,
},
Copy { src: usize, dest: usize },
Cast {
reg: usize,
type_name: String,
dest: usize,
},
Goto { target: usize },
IfFalse { reg: usize, target: usize },
Rewind { target: usize },
Column { col: usize, dest: usize },
Next { target: usize },
RewindC { cursor: usize, target: usize },
ColumnC {
cursor: usize,
col: usize,
dest: usize,
},
NextC { cursor: usize, target: usize },
NullRow { cursor: usize },
MarkMatched { cursor: usize },
IfMatched { cursor: usize, target: usize },
DecrJumpZero { reg: usize, target: usize },
IfPosDecr { reg: usize, target: usize },
Negate { reg: usize, dest: usize },
BitNot { reg: usize, dest: usize },
ResultRow { start: usize, count: usize },
DistinctCheck {
start: usize,
count: usize,
target: usize,
},
SorterInsert {
row_start: usize,
row_count: usize,
key_start: usize,
key_count: usize,
},
SorterSort { keys: Vec<SortKey> },
SorterRewind { target: usize },
SorterRow { start: usize, count: usize },
SorterNext { target: usize },
AggStep {
slot: usize,
kind: AggKind,
arg: Option<usize>,
distinct: bool,
filter: Option<usize>,
order: Vec<AggOrderKey>,
sep: Option<String>,
},
AggFinal {
slot: usize,
kind: AggKind,
dest: usize,
},
GroupStep {
key_start: usize,
key_count: usize,
repr_count: usize,
companion: Option<(usize, bool)>,
aggs: Vec<AggSpec>,
},
GroupEmit {
outputs: Vec<GroupOut>,
key_count: usize,
agg_kinds: Vec<AggKind>,
},
GroupFinalize {
agg_kinds: Vec<AggKind>,
key_count: usize,
target: usize,
},
GroupKey { key: usize, dest: usize },
GroupAgg { slot: usize, dest: usize },
GroupNext { target: usize },
Halt,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AggSpec {
pub kind: AggKind,
pub arg: Option<usize>,
pub distinct: bool,
pub filter: Option<usize>,
pub order: Vec<AggOrderKey>,
pub sep: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupOut {
Key(usize),
Agg(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum AggKind {
CountStar,
Count,
Sum,
Total,
Avg,
Min,
Max,
GroupConcat,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SortKey {
pub descending: bool,
pub nulls_first: Option<bool>,
pub collation: Collation,
}
#[derive(Debug, Clone, Default)]
struct AggAcc {
vals: Vec<Value>,
count: i64,
keys: Vec<Vec<Value>>,
dirs: Vec<(bool, Option<bool>, Collation)>,
sep: Option<String>,
}
type Group = (Vec<Value>, Vec<AggAcc>);
type AggCallSpec = (
AggKind,
Option<Expr>,
bool,
Option<Expr>,
Vec<OrderTerm>,
Option<String>,
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AggOrderKey {
pub reg: usize,
pub descending: bool,
pub nulls_first: Option<bool>,
pub collation: Collation,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Program {
pub ops: Vec<Op>,
pub n_registers: usize,
pub columns: Vec<String>,
}
impl Program {
pub fn explain_rows(&self) -> Vec<(usize, String, String)> {
self.ops
.iter()
.enumerate()
.map(|(addr, op)| {
let detail = alloc::format!("{op:?}");
let opcode = detail
.split([' ', '{'])
.next()
.unwrap_or("")
.trim()
.to_string();
(addr, opcode, detail)
})
.collect()
}
}
fn is_pure_scalar_fn(name: &str, argc: usize) -> bool {
let n = name.to_ascii_lowercase();
match n.as_str() {
"lower" | "upper" | "length" | "octet_length" | "trim" | "ltrim" | "rtrim" | "substr"
| "substring" | "replace" | "instr" | "hex" | "unhex" | "quote" | "char" | "unicode"
| "concat" | "concat_ws" | "soundex" | "unistr" | "unistr_quote" => true,
"sqlite_version" | "sqlite_source_id" => argc == 0,
"abs" | "round" | "sign" | "ceil" | "ceiling" | "floor" | "trunc" | "exp" | "ln"
| "log" | "log2" | "log10" | "sqrt" | "pow" | "power" | "mod" | "sin" | "cos" | "tan"
| "asin" | "acos" | "atan" | "atan2" | "sinh" | "cosh" | "tanh" | "asinh" | "acosh"
| "atanh" | "radians" | "degrees" | "pi" => true,
"typeof" | "nullif" | "zeroblob" | "likely" | "unlikely" => true,
"coalesce" | "ifnull" => argc >= 1,
"glob" => argc == 2,
"like" => argc == 2 || argc == 3,
"json"
| "json_valid"
| "json_type"
| "json_array_length"
| "json_extract"
| "jsonb"
| "json_error_position" => true,
"printf" | "format" => argc >= 1,
"date" | "time" | "datetime" | "julianday" | "unixepoch" => true,
"strftime" => argc >= 1,
"timediff" => argc == 2,
"min" | "max" => argc >= 2,
_ => false,
}
}
pub fn compile_const_select(sel: &Select) -> Result<Program> {
if sel.from.is_some()
|| !sel.group_by.is_empty()
|| sel.having.is_some()
|| !sel.compound.is_empty()
{
return Err(Error::Unsupported("VDBE spike: only constant SELECT lists"));
}
if sel.columns.is_empty() {
return Err(Error::Unsupported("VDBE spike: empty SELECT list"));
}
let count = sel.columns.len();
let mut c = Compiler {
ops: Vec::new(),
next_reg: count,
columns: Vec::new(),
tables: Vec::new(),
affinities: Vec::new(),
collations: Vec::new(),
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index: None,
cursor_boundaries: None,
};
let skip = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
let mut columns = Vec::new();
for (i, rc) in sel.columns.iter().enumerate() {
let ResultColumn::Expr {
expr,
alias,
source,
} = rc
else {
return Err(Error::Unsupported("VDBE spike: only scalar result columns"));
};
c.compile_expr_into(expr, i)?;
columns.push(result_label(expr, alias, source, i));
}
for term in &sel.order_by {
if super::positional_int(&term.expr).is_some() {
return Err(Error::Unsupported(
"VDBE: positional ORDER BY in const SELECT",
));
}
let saved_ops = c.ops.len();
let saved_reg = c.next_reg;
c.compile_expr(&term.expr)?;
c.ops.truncate(saved_ops);
c.next_reg = saved_reg;
}
let limit_zero = match &sel.limit {
None => false,
Some(e) => match fold_const_int(e) {
Some(n) => n == 0,
None => return Err(Error::Unsupported("VDBE: only constant integer LIMIT")),
},
};
let offset_skips = match &sel.offset {
None => false,
Some(e) => match fold_const_int(e) {
Some(n) => n > 0,
None => return Err(Error::Unsupported("VDBE: only constant integer OFFSET")),
},
};
if !(limit_zero || offset_skips) {
c.ops.push(Op::ResultRow { start: 0, count });
}
let halt = c.ops.len();
c.ops.push(Op::Halt);
if let Some(at) = skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = halt;
}
}
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns,
})
}
fn result_label(
expr: &Expr,
alias: &Option<String>,
source: &Option<String>,
idx: usize,
) -> String {
if let Some(a) = alias {
return a.clone();
}
match expr {
Expr::Column { column, .. } => column.clone(),
_ => source
.clone()
.unwrap_or_else(|| alloc::format!("col{}", idx + 1)),
}
}
fn unparen(e: &Expr) -> &Expr {
match e {
Expr::Paren(inner) => unparen(inner),
_ => e,
}
}
fn fold_const_int(e: &Expr) -> Option<i64> {
fn pure(e: &Expr) -> bool {
match e {
Expr::Literal(_) => true,
Expr::Paren(x) | Expr::Cast { expr: x, .. } => pure(x),
Expr::Unary { expr, .. } => pure(expr),
Expr::Binary { left, right, .. } => pure(left) && pure(right),
_ => false,
}
}
if !pure(e) {
return None;
}
let v = crate::exec::eval::eval(
e,
&crate::exec::eval::EvalCtx::rowless(&crate::exec::eval::Params::default()),
)
.ok()?;
fn exact_int(r: f64) -> Option<i64> {
(r.is_finite()
&& r == crate::util::float::trunc(r)
&& r >= i64::MIN as f64
&& r < 9_223_372_036_854_775_808.0)
.then_some(r as i64)
}
match v {
Value::Integer(i) => Some(i),
Value::Real(r) => exact_int(r),
Value::Text(s) => {
let t = s.trim();
if let Ok(i) = t.parse::<i64>() {
Some(i)
} else {
t.parse::<f64>().ok().and_then(exact_int)
}
}
Value::Null | Value::Blob(_) => None,
}
}
fn is_aggregate_expr(expr: &Expr) -> bool {
matches!(
expr,
Expr::Function { name, args, star, .. }
if crate::exec::func::is_aggregate_call(name, args.len(), *star)
)
}
fn expr_has_aggregate_or_window(e: &Expr) -> bool {
let mut found = false;
crate::exec::window::visit(e, &mut |node| {
if let Expr::Function {
name,
args,
star,
over,
..
} = node
{
if over.is_some() || crate::exec::func::is_aggregate_call(name, args.len(), *star) {
found = true;
}
}
});
found
}
pub(crate) fn reject_aggregate_or_window_in_predicates(sel: &Select) -> Result<()> {
if let Some(from) = &sel.from {
for j in &from.joins {
if let Some(on) = &j.on {
if expr_has_aggregate_or_window(on) {
return Err(Error::Unsupported(
"VDBE: aggregate/window in join ON predicate",
));
}
}
}
}
if let Some(w) = &sel.where_clause {
if expr_has_aggregate_or_window(w) {
return Err(Error::Unsupported(
"VDBE: aggregate/window in WHERE predicate",
));
}
}
Ok(())
}
fn compile_order_keys(c: &mut Compiler, order: &[OrderTerm]) -> Result<Vec<AggOrderKey>> {
order
.iter()
.map(|t| {
let reg = c.compile_expr(&t.expr)?;
let collation = c
.explicit_collation(&t.expr)
.or_else(|| c.col_collation(&t.expr))
.unwrap_or_default();
Ok(AggOrderKey {
reg,
descending: t.descending,
nulls_first: t.nulls_first,
collation,
})
})
.collect()
}
fn agg_kind_distinct(expr: &Expr) -> Option<AggCallSpec> {
let Expr::Function {
name,
distinct,
args,
star,
filter,
order_by,
over,
} = expr
else {
return None;
};
if over.is_some() {
return None;
}
let filter = filter.as_ref().map(|f| (**f).clone());
let order = order_by.clone();
let arg = args.first().cloned();
let kind = match name.to_ascii_lowercase().as_str() {
"count" if *star => {
return (!*distinct && order.is_empty()).then_some((
AggKind::CountStar,
None,
false,
filter,
Vec::new(),
None,
));
}
"count" if args.len() == 1 => (AggKind::Count, None),
"sum" if args.len() == 1 => (AggKind::Sum, None),
"total" if args.len() == 1 => (AggKind::Total, None),
"avg" if args.len() == 1 => (AggKind::Avg, None),
"min" if args.len() == 1 => (AggKind::Min, None),
"max" if args.len() == 1 => (AggKind::Max, None),
"group_concat" if args.len() == 1 => (AggKind::GroupConcat, None),
"group_concat" | "string_agg" if args.len() == 2 && !*distinct => {
match const_sep_text(&args[1]) {
Some(s) => (AggKind::GroupConcat, Some(s)),
None => return None,
}
}
_ => return None,
};
let (kind, sep) = kind;
if !order.is_empty() && (*distinct || kind != AggKind::GroupConcat) {
return None;
}
let collation_sensitive = *distinct || matches!(kind, AggKind::Min | AggKind::Max);
if collation_sensitive && arg.as_ref().is_some_and(explicit_non_binary_collation) {
return None;
}
Some((kind, arg, *distinct, filter, order, sep))
}
fn explicit_non_binary_collation(expr: &Expr) -> bool {
match unparen(expr) {
Expr::Collate { collation, .. } => {
Collation::parse(collation).is_some_and(|c| c != Collation::Binary)
}
_ => false,
}
}
fn projections_have_explicit_collation(projections: &[(Expr, String)]) -> bool {
let mut found = false;
for (e, _) in projections {
crate::exec::window::visit(e, &mut |node| {
if let Expr::Collate { collation, .. } = node {
if Collation::parse(collation).is_some_and(|c| c != Collation::Binary) {
found = true;
}
}
});
}
found
}
fn const_sep_text(expr: &Expr) -> Option<String> {
use crate::exec::eval;
let Expr::Literal(l) = unparen(expr) else {
return None;
};
let v = match l {
Literal::Null => Value::Null,
Literal::Integer(i) => Value::Integer(*i),
Literal::Real(r) => Value::Real(*r),
Literal::Str(s) => Value::Text(s.clone()),
Literal::Blob(b) => Value::Blob(b.clone()),
Literal::Boolean(b) => Value::Integer(*b as i64),
};
Some(eval::to_text(&v))
}
fn compile_aggregate_select(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
projections: &[(Expr, String)],
) -> Result<Program> {
if !sel.order_by.is_empty()
|| sel.limit.is_some()
|| sel.offset.is_some()
|| sel.distinct
|| sel.having.is_some()
{
return Err(Error::Unsupported("VDBE: bare aggregate only"));
}
if collations.iter().any(|c| *c != Collation::Binary) {
return Err(Error::Unsupported(
"VDBE: non-BINARY collation in aggregate",
));
}
let mut slots: Vec<AggCallSpec> = Vec::new();
for (e, _) in projections {
match agg_kind_distinct(e) {
Some(spec) => slots.push(spec),
None => return Err(Error::Unsupported("VDBE: unsupported aggregate")),
}
}
let count = projections.len();
let mut c = Compiler {
ops: Vec::new(),
next_reg: count,
columns: columns.to_vec(),
tables: tables.to_vec(),
affinities: affinities.to_vec(),
collations: collations.to_vec(),
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index: None,
cursor_boundaries: None,
};
let rewind = c.ops.len();
c.ops.push(Op::Rewind { target: 0 });
let body = c.ops.len();
let skip = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
for (slot, (kind, arg, distinct, filter, order, sep)) in slots.iter().enumerate() {
let arg_reg = match arg {
Some(expr) => Some(c.compile_expr(expr)?),
None => None,
};
let filter_reg = match filter {
Some(expr) => Some(c.compile_expr(expr)?),
None => None,
};
let order_keys = compile_order_keys(&mut c, order)?;
c.ops.push(Op::AggStep {
slot,
kind: *kind,
arg: arg_reg,
distinct: *distinct,
filter: filter_reg,
order: order_keys,
sep: sep.clone(),
});
}
let next = c.ops.len();
c.ops.push(Op::Next { target: body });
if let Some(at) = skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = next;
}
}
let end = c.ops.len();
if let Op::Rewind { target } = &mut c.ops[rewind] {
*target = end;
}
for (slot, (kind, _, _, _, _, _)) in slots.iter().enumerate() {
c.ops.push(Op::AggFinal {
slot,
kind: *kind,
dest: slot,
});
}
c.ops.push(Op::ResultRow { start: 0, count });
c.ops.push(Op::Halt);
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns: projections.iter().map(|(_, l)| l.clone()).collect(),
})
}
fn collect_aggregates(expr: &Expr, out: &mut Vec<Expr>) {
if is_aggregate_expr(expr) {
if !out.iter().any(|e| e == expr) {
out.push(expr.clone());
}
return;
}
match expr {
Expr::Paren(inner)
| Expr::Unary { expr: inner, .. }
| Expr::IsNull { expr: inner, .. }
| Expr::Cast { expr: inner, .. } => collect_aggregates(inner, out),
Expr::Binary { left, right, .. } => {
collect_aggregates(left, out);
collect_aggregates(right, out);
}
Expr::Function { args, .. } => {
for a in args {
collect_aggregates(a, out);
}
}
Expr::Case {
operand,
when_then,
else_result,
} => {
if let Some(o) = operand {
collect_aggregates(o, out);
}
for (w, t) in when_then {
collect_aggregates(w, out);
collect_aggregates(t, out);
}
if let Some(e) = else_result {
collect_aggregates(e, out);
}
}
_ => {}
}
}
fn collect_bare_columns(expr: &Expr, out: &mut Vec<(Option<String>, String)>) {
if is_aggregate_expr(expr) {
return;
}
match expr {
Expr::Column { table, column, .. } => {
let key = (table.clone(), column.clone());
if !out.contains(&key) {
out.push(key);
}
}
Expr::Paren(inner)
| Expr::Unary { expr: inner, .. }
| Expr::IsNull { expr: inner, .. }
| Expr::Cast { expr: inner, .. }
| Expr::Collate { expr: inner, .. } => collect_bare_columns(inner, out),
Expr::Binary { left, right, .. } => {
collect_bare_columns(left, out);
collect_bare_columns(right, out);
}
Expr::Function { args, .. } => {
for a in args {
collect_bare_columns(a, out);
}
}
Expr::Case {
operand,
when_then,
else_result,
} => {
if let Some(o) = operand {
collect_bare_columns(o, out);
}
for (w, t) in when_then {
collect_bare_columns(w, out);
collect_bare_columns(t, out);
}
if let Some(e) = else_result {
collect_bare_columns(e, out);
}
}
_ => {}
}
}
enum GroupKeySpec {
Col(usize),
Expr(Expr),
}
fn emit_group_fold(
c: &mut Compiler,
sel: &Select,
group_keys: &[GroupKeySpec],
repr_cols: &[usize],
agg_specs: &[AggCallSpec],
) -> Result<()> {
let bounds = c.cursor_boundaries.clone();
let key_start = c.next_reg;
for _ in group_keys {
c.alloc();
}
for _ in repr_cols {
c.alloc();
}
let mut rewind_at: Vec<usize> = Vec::new();
let mut single_rewind: Option<usize> = None;
match &bounds {
Some(b) => {
rewind_at = alloc::vec![0usize; b.len()];
for (i, slot) in rewind_at.iter_mut().enumerate() {
*slot = c.ops.len();
c.ops.push(Op::RewindC {
cursor: i,
target: 0,
});
}
}
None => {
single_rewind = Some(c.ops.len());
c.ops.push(Op::Rewind { target: 0 });
}
}
let body = c.ops.len();
let skip = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
for (k, key) in group_keys.iter().enumerate() {
match key {
GroupKeySpec::Col(ci) => match &bounds {
Some(b) => {
let (cursor, col) = Compiler::cursor_of(b, *ci);
c.ops.push(Op::ColumnC {
cursor,
col,
dest: key_start + k,
});
}
None => c.ops.push(Op::Column {
col: *ci,
dest: key_start + k,
}),
},
GroupKeySpec::Expr(e) => {
c.compile_expr_into(e, key_start + k)?;
}
}
}
let kn = group_keys.len();
for (r, &ci) in repr_cols.iter().enumerate() {
match &bounds {
Some(b) => {
let (cursor, col) = Compiler::cursor_of(b, ci);
c.ops.push(Op::ColumnC {
cursor,
col,
dest: key_start + kn + r,
});
}
None => c.ops.push(Op::Column {
col: ci,
dest: key_start + kn + r,
}),
}
}
let mut aggs: Vec<AggSpec> = Vec::new();
for (kind, arg, distinct, filter, order, sep) in agg_specs {
let arg_reg = match arg {
Some(expr) => Some(c.compile_expr(expr)?),
None => None,
};
let filter_reg = match filter {
Some(expr) => Some(c.compile_expr(expr)?),
None => None,
};
let order_keys = compile_order_keys(c, order)?;
aggs.push(AggSpec {
kind: *kind,
arg: arg_reg,
distinct: *distinct,
filter: filter_reg,
order: order_keys,
sep: sep.clone(),
});
}
let companion = if repr_cols.is_empty() {
None
} else {
let mm: Vec<usize> = aggs
.iter()
.enumerate()
.filter(|(_, a)| matches!(a.kind, AggKind::Min | AggKind::Max))
.map(|(i, _)| i)
.collect();
match mm.as_slice() {
[j] => aggs[*j]
.arg
.map(|areg| (areg, aggs[*j].kind == AggKind::Max)),
_ => None,
}
};
c.ops.push(Op::GroupStep {
key_start,
key_count: group_keys.len(),
repr_count: repr_cols.len(),
companion,
aggs,
});
match &bounds {
Some(b) => {
let n = b.len();
let mut next_at = alloc::vec![0usize; n];
for i in (0..n).rev() {
next_at[i] = c.ops.len();
let target = if i == n - 1 { body } else { rewind_at[i + 1] };
c.ops.push(Op::NextC { cursor: i, target });
}
let end = c.ops.len();
for i in 0..n {
let target = if i == 0 { end } else { next_at[i - 1] };
if let Op::RewindC { target: t, .. } = &mut c.ops[rewind_at[i]] {
*t = target;
}
}
if let Some(at) = skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = next_at[n - 1];
}
}
}
None => {
let next = c.ops.len();
c.ops.push(Op::Next { target: body });
if let Some(at) = skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = next;
}
}
let end = c.ops.len();
if let Some(rw) = single_rewind {
if let Op::Rewind { target } = &mut c.ops[rw] {
*target = end;
}
}
}
}
Ok(())
}
fn compile_group_select(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
projections: &[(Expr, String)],
boundaries: Option<&[usize]>,
) -> Result<Program> {
if collations.iter().any(|c| *c != Collation::Binary) {
return Err(Error::Unsupported("VDBE: non-BINARY collation in GROUP BY"));
}
let has_having = sel.having.is_some();
let has_order = !sel.order_by.is_empty();
let has_limit = sel.limit.is_some() || sel.offset.is_some();
let mut c = Compiler {
ops: Vec::new(),
next_reg: 0,
columns: columns.to_vec(),
tables: tables.to_vec(),
affinities: affinities.to_vec(),
collations: collations.to_vec(),
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index: None,
cursor_boundaries: boundaries.map(|b| b.to_vec()),
};
let effective_group: Vec<Expr> = sel
.group_by
.iter()
.map(|g| {
if let Expr::Column {
table: None,
column,
..
} = g
{
if !columns.iter().any(|n| n.eq_ignore_ascii_case(column)) {
if let Some((e, _)) = projections
.iter()
.find(|(_, l)| l.eq_ignore_ascii_case(column))
{
let mut aggs = Vec::new();
collect_aggregates(e, &mut aggs);
if aggs.is_empty() {
return e.clone();
}
}
}
}
g.clone()
})
.collect();
let mut group_keys: Vec<GroupKeySpec> = Vec::new();
for g in &effective_group {
match g {
Expr::Column { table, column, .. } => {
group_keys.push(GroupKeySpec::Col(
c.resolve_column(table.as_deref(), column)?,
));
}
other => {
let mut refs = Vec::new();
collect_bare_columns(other, &mut refs);
if refs.is_empty() {
return Err(Error::Unsupported("VDBE: constant GROUP BY key"));
}
if c.col_collation(other)
.is_some_and(|co| co != Collation::Binary)
{
return Err(Error::Unsupported(
"VDBE: non-BINARY collation in GROUP BY key",
));
}
group_keys.push(GroupKeySpec::Expr(other.clone()));
}
}
}
let all_col_keys = group_keys.iter().all(|k| matches!(k, GroupKeySpec::Col(_)));
let key_col_indices: Vec<usize> = group_keys
.iter()
.filter_map(|k| match k {
GroupKeySpec::Col(i) => Some(*i),
GroupKeySpec::Expr(_) => None,
})
.collect();
if sel.distinct
&& projections
.iter()
.any(|(e, _)| c.col_collation(e).is_some_and(|co| co != Collation::Binary))
{
return Err(Error::Unsupported(
"VDBE: non-BINARY collation in DISTINCT output",
));
}
if !has_having && !has_order && !has_limit && all_col_keys && !sel.distinct {
return compile_group_emit(
sel,
columns,
tables,
affinities,
collations,
projections,
&key_col_indices,
boundaries,
);
}
let mut agg_exprs: Vec<Expr> = Vec::new();
for (e, _) in projections {
collect_aggregates(e, &mut agg_exprs);
}
if let Some(h) = &sel.having {
collect_aggregates(h, &mut agg_exprs);
}
for term in &sel.order_by {
collect_aggregates(&term.expr, &mut agg_exprs);
}
let mut agg_specs: Vec<AggCallSpec> = Vec::new();
for e in &agg_exprs {
match agg_kind_distinct(e) {
Some(spec) => agg_specs.push(spec),
None => return Err(Error::Unsupported("VDBE: unsupported aggregate")),
}
}
let mut bare_refs: Vec<(Option<String>, String)> = Vec::new();
for (e, _) in projections {
collect_bare_columns(e, &mut bare_refs);
}
if let Some(h) = &sel.having {
collect_bare_columns(h, &mut bare_refs);
}
for term in &sel.order_by {
collect_bare_columns(&term.expr, &mut bare_refs);
}
let single_minmax = super::single_minmax_arg(sel).is_some();
let mut repr_cols: Vec<usize> = Vec::new();
for (t, col) in &bare_refs {
let Ok(ci) = c.resolve_column(t.as_deref(), col) else {
continue;
};
if !single_minmax && key_col_indices.contains(&ci) {
continue;
}
if !repr_cols.contains(&ci) {
repr_cols.push(ci);
}
}
if !repr_cols.is_empty()
&& agg_specs
.iter()
.filter(|(k, ..)| matches!(k, AggKind::Min | AggKind::Max))
.count()
> 1
{
return Err(Error::Unsupported(
"VDBE: bare column with multiple min/max companions",
));
}
emit_group_fold(&mut c, sel, &group_keys, &repr_cols, &agg_specs)?;
let gkey_start = c.next_reg;
for _ in &group_keys {
c.alloc();
}
let gagg_start = c.next_reg;
for _ in &agg_specs {
c.alloc();
}
for (k, key) in group_keys.iter().enumerate() {
let ci = match key {
GroupKeySpec::Expr(e) => {
c.bindings.push((e.clone(), gkey_start + k));
continue;
}
GroupKeySpec::Col(ci) => *ci,
};
if single_minmax && repr_cols.contains(&ci) {
continue;
}
c.bindings.push((
Expr::Column {
schema: None,
table: None,
column: columns[ci].clone(),
quoted: false,
},
gkey_start + k,
));
if let Some(t) = tables.get(ci) {
c.bindings.push((
Expr::Column {
schema: None,
table: Some(t.clone()),
column: columns[ci].clone(),
quoted: false,
},
gkey_start + k,
));
}
}
for (j, e) in agg_exprs.iter().enumerate() {
c.bindings.push((e.clone(), gagg_start + j));
}
let grepr_start = c.next_reg;
for _ in &repr_cols {
c.alloc();
}
for (r, &ci) in repr_cols.iter().enumerate() {
c.bindings.push((
Expr::Column {
schema: None,
table: None,
column: columns[ci].clone(),
quoted: false,
},
grepr_start + r,
));
if let Some(t) = tables.get(ci) {
c.bindings.push((
Expr::Column {
schema: None,
table: Some(t.clone()),
column: columns[ci].clone(),
quoted: false,
},
grepr_start + r,
));
}
}
let count = projections.len();
let out_start = c.next_reg;
for _ in 0..count {
c.alloc();
}
let limit_reg = match &sel.limit {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n < 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer LIMIT")),
},
};
let offset_reg = match &sel.offset {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer OFFSET")),
},
};
let labels: Vec<String> = projections.iter().map(|(_, l)| l.clone()).collect();
let mut key_specs: Vec<(Expr, SortKey)> = Vec::new();
for term in &sel.order_by {
if super::positional_int(&term.expr).is_some()
&& !matches!(&term.expr, Expr::Literal(Literal::Integer(k)) if *k >= 1 && (*k as usize) <= count)
{
return Err(Error::Unsupported("VDBE: positional ORDER BY term"));
}
let expr = match super::resolve_order_index(&term.expr, &labels, count) {
Some(idx) => projections[idx].0.clone(),
None => term.expr.clone(),
};
let collation = c
.explicit_collation(&term.expr)
.or_else(|| c.col_collation(&expr))
.unwrap_or_default();
key_specs.push((
expr,
SortKey {
descending: term.descending,
nulls_first: term.nulls_first,
collation,
},
));
}
let key_start2 = c.next_reg;
for _ in &key_specs {
c.alloc();
}
let limit_skip = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfFalse { reg: r, target: 0 });
at
});
let gfin = c.ops.len();
c.ops.push(Op::GroupFinalize {
agg_kinds: agg_specs.iter().map(|(k, _, _, _, _, _)| *k).collect(),
key_count: group_keys.len(),
target: 0,
});
let gbody = c.ops.len();
for k in 0..group_keys.len() {
c.ops.push(Op::GroupKey {
key: k,
dest: gkey_start + k,
});
}
for j in 0..agg_specs.len() {
c.ops.push(Op::GroupAgg {
slot: j,
dest: gagg_start + j,
});
}
for r in 0..repr_cols.len() {
c.ops.push(Op::GroupKey {
key: group_keys.len() + r,
dest: grepr_start + r,
});
}
c.forbid_raw_columns = true;
for (i, (expr, _)) in projections.iter().enumerate() {
c.compile_expr_into(expr, out_start + i)?;
}
for (i, (expr, label)) in projections.iter().enumerate() {
let is_bare_col = matches!(expr, Expr::Column { .. });
if !is_bare_col && !columns.iter().any(|c| c.eq_ignore_ascii_case(label)) {
c.bindings.push((
Expr::Column {
schema: None,
table: None,
column: label.clone(),
quoted: false,
},
out_start + i,
));
}
}
let having_skip = match &sel.having {
Some(h) => {
let preg = c.compile_expr(h)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
let distinct_skip = if sel.distinct {
let at = c.ops.len();
c.ops.push(Op::DistinctCheck {
start: out_start,
count,
target: 0,
});
Some(at)
} else {
None
};
let mut limit_done = None;
let mut offset_skip = None;
if has_order {
for (j, (expr, _)) in key_specs.iter().enumerate() {
c.compile_expr_into(expr, key_start2 + j)?;
}
c.ops.push(Op::SorterInsert {
row_start: out_start,
row_count: count,
key_start: key_start2,
key_count: key_specs.len(),
});
} else {
offset_skip = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::ResultRow {
start: out_start,
count,
});
if let Some(r) = limit_reg {
limit_done = Some(c.ops.len());
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
}
}
let gnext = c.ops.len();
c.ops.push(Op::GroupNext { target: gbody });
if let Some(at) = having_skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = gnext;
}
}
if let Some(at) = distinct_skip {
if let Op::DistinctCheck { target, .. } = &mut c.ops[at] {
*target = gnext;
}
}
if let Some(at) = offset_skip {
if let Op::IfPosDecr { target, .. } = &mut c.ops[at] {
*target = gnext;
}
}
let gend = c.ops.len();
if let Op::GroupFinalize { target, .. } = &mut c.ops[gfin] {
*target = gend;
}
if let Some(at) = limit_done {
if let Op::DecrJumpZero { target, .. } = &mut c.ops[at] {
*target = gend;
}
}
if has_order {
c.ops.push(Op::SorterSort {
keys: key_specs.iter().map(|(_, k)| k.clone()).collect(),
});
let srewind = c.ops.len();
c.ops.push(Op::SorterRewind { target: 0 });
let ebody = c.ops.len();
let eoffset = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::SorterRow {
start: out_start,
count,
});
c.ops.push(Op::ResultRow {
start: out_start,
count,
});
let elimit = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
let snext = c.ops.len();
c.ops.push(Op::SorterNext { target: ebody });
let eend = c.ops.len();
if let Op::SorterRewind { target } = &mut c.ops[srewind] {
*target = eend;
}
if let Some(at) = eoffset {
if let Op::IfPosDecr { target, .. } = &mut c.ops[at] {
*target = snext;
}
}
if let Some(at) = elimit {
if let Op::DecrJumpZero { target, .. } = &mut c.ops[at] {
*target = eend;
}
}
}
let final_end = c.ops.len();
if let Some(at) = limit_skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = final_end;
}
}
c.ops.push(Op::Halt);
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns: projections.iter().map(|(_, l)| l.clone()).collect(),
})
}
#[allow(clippy::too_many_arguments)] fn compile_group_emit(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
projections: &[(Expr, String)],
group_cols: &[usize],
boundaries: Option<&[usize]>,
) -> Result<Program> {
let mut c = Compiler {
ops: Vec::new(),
next_reg: 0,
columns: columns.to_vec(),
tables: tables.to_vec(),
affinities: affinities.to_vec(),
collations: collations.to_vec(),
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index: None,
cursor_boundaries: boundaries.map(|b| b.to_vec()),
};
let single_minmax = super::single_minmax_arg(sel).is_some();
let mut outputs: Vec<GroupOut> = Vec::new();
let mut agg_specs: Vec<AggCallSpec> = Vec::new();
let mut repr_cols: Vec<usize> = Vec::new();
for (e, _) in projections {
if is_aggregate_expr(e) {
match agg_kind_distinct(e) {
Some(spec) => {
outputs.push(GroupOut::Agg(agg_specs.len()));
agg_specs.push(spec);
}
None => return Err(Error::Unsupported("VDBE: unsupported aggregate")),
}
} else if let Expr::Column { table, column, .. } = e {
let ci = c.resolve_column(table.as_deref(), column)?;
match group_cols.iter().position(|&g| g == ci) {
Some(k) if !single_minmax => outputs.push(GroupOut::Key(k)),
_ => {
let r = repr_cols.len();
repr_cols.push(ci);
outputs.push(GroupOut::Key(group_cols.len() + r));
}
}
} else {
return Err(Error::Unsupported(
"VDBE: GROUP BY output must be key or aggregate",
));
}
}
if !repr_cols.is_empty()
&& agg_specs
.iter()
.filter(|(k, ..)| matches!(k, AggKind::Min | AggKind::Max))
.count()
> 1
{
return Err(Error::Unsupported(
"VDBE: bare column with multiple min/max companions",
));
}
let group_keys: Vec<GroupKeySpec> = group_cols.iter().map(|&i| GroupKeySpec::Col(i)).collect();
emit_group_fold(&mut c, sel, &group_keys, &repr_cols, &agg_specs)?;
c.ops.push(Op::GroupEmit {
outputs,
key_count: group_cols.len(),
agg_kinds: agg_specs.iter().map(|(k, _, _, _, _, _)| *k).collect(),
});
c.ops.push(Op::Halt);
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns: projections.iter().map(|(_, l)| l.clone()).collect(),
})
}
pub fn compile_table_select(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
rowid: bool,
) -> Result<Program> {
if !sel.compound.is_empty() {
return Err(Error::Unsupported("VDBE: only plain table projections"));
}
let projections = expand_projections(sel, columns, tables)?;
if sel.distinct && projections_have_explicit_collation(&projections) {
return Err(Error::Unsupported("VDBE: explicit COLLATE with DISTINCT"));
}
if matches!(&sel.limit, Some(Expr::Literal(Literal::Integer(0)))) {
return Ok(Program {
ops: alloc::vec![Op::Halt],
n_registers: projections.len(),
columns: projections.iter().map(|(_, l)| l.clone()).collect(),
});
}
if !sel.group_by.is_empty() {
return compile_group_select(
sel,
columns,
tables,
affinities,
collations,
&projections,
None,
);
}
if projections.iter().any(|(e, _)| is_aggregate_expr(e)) {
return compile_aggregate_select(
sel,
columns,
tables,
affinities,
collations,
&projections,
);
}
if sel.having.is_some() {
return Err(Error::Unsupported("VDBE: HAVING without GROUP BY"));
}
let count = projections.len();
let mut affinities = affinities.to_vec();
let mut collations = collations.to_vec();
let rowid_index = if rowid {
affinities.push(Affinity::Integer);
collations.push(Collation::Binary);
Some(columns.len())
} else {
None
};
let mut c = Compiler {
ops: Vec::new(),
next_reg: count,
columns: columns.to_vec(),
tables: tables.to_vec(),
affinities,
collations,
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index,
cursor_boundaries: None,
};
let limit_reg = match &sel.limit {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n < 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer LIMIT")),
},
};
let offset_reg = match &sel.offset {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer OFFSET")),
},
};
let ordering = !sel.order_by.is_empty();
let key_specs = if ordering {
build_sort_keys(&c, sel, &projections, count)?
} else {
Vec::new()
};
let key_start = c.next_reg;
for _ in &key_specs {
c.alloc();
}
let rewind = c.ops.len();
c.ops.push(Op::Rewind { target: 0 });
let limit_skip = if ordering {
None
} else {
limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfFalse { reg: r, target: 0 });
at
})
};
let body = c.ops.len();
let skip = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
for (i, (expr, _)) in projections.iter().enumerate() {
c.compile_expr_into(expr, i)?;
}
if sel.distinct && c.collations.iter().any(|cl| *cl != Collation::Binary) {
return Err(Error::Unsupported(
"VDBE: non-BINARY collation with DISTINCT",
));
}
let distinct_skip = if sel.distinct {
let at = c.ops.len();
c.ops.push(Op::DistinctCheck {
start: 0,
count,
target: 0,
});
Some(at)
} else {
None
};
let offset_skip = if ordering {
None
} else {
offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
})
};
let mut limit_done = None;
if ordering {
for (j, (expr, _)) in key_specs.iter().enumerate() {
c.compile_expr_into(expr, key_start + j)?;
}
c.ops.push(Op::SorterInsert {
row_start: 0,
row_count: count,
key_start,
key_count: key_specs.len(),
});
} else {
c.ops.push(Op::ResultRow { start: 0, count });
if let Some(r) = limit_reg {
limit_done = Some(c.ops.len());
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
}
}
let next = c.ops.len();
c.ops.push(Op::Next { target: body });
if let Some(at) = skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = next; }
}
if let Some(at) = offset_skip {
if let Op::IfPosDecr { target, .. } = &mut c.ops[at] {
*target = next; }
}
if let Some(at) = distinct_skip {
if let Op::DistinctCheck { target, .. } = &mut c.ops[at] {
*target = next; }
}
let end = c.ops.len();
if let Op::Rewind { target } = &mut c.ops[rewind] {
*target = end;
}
if let Some(at) = limit_skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = end;
}
}
if let Some(at) = limit_done {
if let Op::DecrJumpZero { target, .. } = &mut c.ops[at] {
*target = end;
}
}
if ordering {
c.ops.push(Op::SorterSort {
keys: key_specs.iter().map(|(_, k)| k.clone()).collect(),
});
let srewind = c.ops.len();
c.ops.push(Op::SorterRewind { target: 0 });
let ebody = c.ops.len();
let eoffset = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::SorterRow { start: 0, count });
c.ops.push(Op::ResultRow { start: 0, count });
let elimit = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
let snext = c.ops.len();
c.ops.push(Op::SorterNext { target: ebody });
let eend = c.ops.len();
if let Op::SorterRewind { target } = &mut c.ops[srewind] {
*target = eend;
}
if let Some(at) = eoffset {
if let Op::IfPosDecr { target, .. } = &mut c.ops[at] {
*target = snext; }
}
if let Some(at) = elimit {
if let Op::DecrJumpZero { target, .. } = &mut c.ops[at] {
*target = eend;
}
}
}
c.ops.push(Op::Halt);
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns: projections.into_iter().map(|(_, l)| l).collect(),
})
}
fn expand_projections(
sel: &Select,
columns: &[String],
tables: &[String],
) -> Result<Vec<(Expr, String)>> {
let mut projections: Vec<(Expr, String)> = Vec::new();
for rc in &sel.columns {
match rc {
ResultColumn::Wildcard => {
for (i, name) in columns.iter().enumerate() {
let table = tables.get(i).filter(|t| !t.is_empty()).cloned();
projections.push((
Expr::Column {
schema: None,
table,
column: name.clone(),
quoted: false,
},
name.clone(),
));
}
}
ResultColumn::Expr {
expr,
alias,
source,
} => {
let label = result_label(expr, alias, source, projections.len());
projections.push((expr.clone(), label));
}
ResultColumn::TableWildcard(q) => {
let mut any = false;
for (i, name) in columns.iter().enumerate() {
if tables.get(i).is_some_and(|t| t.eq_ignore_ascii_case(q)) {
projections.push((
Expr::Column {
schema: None,
table: Some(q.clone()),
column: name.clone(),
quoted: false,
},
name.clone(),
));
any = true;
}
}
if !any {
return Err(Error::Unsupported("VDBE: unknown table.* qualifier"));
}
}
}
}
if projections.is_empty() {
return Err(Error::Unsupported("VDBE: empty projection"));
}
Ok(projections)
}
fn build_sort_keys(
c: &Compiler,
sel: &Select,
projections: &[(Expr, String)],
count: usize,
) -> Result<Vec<(Expr, SortKey)>> {
let labels: Vec<String> = projections.iter().map(|(_, l)| l.clone()).collect();
let mut key_specs: Vec<(Expr, SortKey)> = Vec::new();
for term in &sel.order_by {
if super::positional_int(&term.expr).is_some()
&& !matches!(&term.expr, Expr::Literal(Literal::Integer(k)) if *k >= 1 && (*k as usize) <= count)
{
return Err(Error::Unsupported("VDBE: positional ORDER BY term"));
}
let expr = match super::resolve_order_index(&term.expr, &labels, count) {
Some(idx) => projections[idx].0.clone(),
None => term.expr.clone(),
};
let collation = c
.explicit_collation(&term.expr)
.or_else(|| c.col_collation(&expr))
.unwrap_or_default();
key_specs.push((
expr,
SortKey {
descending: term.descending,
nulls_first: term.nulls_first,
collation,
},
));
}
Ok(key_specs)
}
pub fn compile_join2(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
boundaries: &[usize],
) -> Result<Program> {
let n = boundaries.len();
debug_assert!(n >= 2 && boundaries[n - 1] == columns.len());
if !sel.compound.is_empty() || !sel.group_by.is_empty() || sel.having.is_some() {
return Err(Error::Unsupported("VDBE: join shape not nested-loopable"));
}
reject_aggregate_or_window_in_predicates(sel)?;
if sel.distinct && collations.iter().any(|cl| *cl != Collation::Binary) {
return Err(Error::Unsupported(
"VDBE: non-BINARY collation with DISTINCT",
));
}
let projections = expand_projections(sel, columns, tables)?;
if sel.distinct && projections_have_explicit_collation(&projections) {
return Err(Error::Unsupported("VDBE: explicit COLLATE with DISTINCT"));
}
if projections.iter().any(|(e, _)| is_aggregate_expr(e)) {
return Err(Error::Unsupported("VDBE: aggregate over a join"));
}
let count = projections.len();
if matches!(&sel.limit, Some(Expr::Literal(Literal::Integer(0)))) {
return Ok(Program {
ops: alloc::vec![Op::Halt],
n_registers: count,
columns: projections.into_iter().map(|(_, l)| l).collect(),
});
}
let mut c = Compiler {
ops: Vec::new(),
next_reg: count,
columns: columns.to_vec(),
tables: tables.to_vec(),
affinities: affinities.to_vec(),
collations: collations.to_vec(),
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index: None,
cursor_boundaries: Some(boundaries.to_vec()),
};
let limit_reg = match &sel.limit {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n < 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer LIMIT")),
},
};
let offset_reg = match &sel.offset {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n <= 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer OFFSET")),
},
};
let ordering = !sel.order_by.is_empty();
let key_specs = if ordering {
build_sort_keys(&c, sel, &projections, count)?
} else {
Vec::new()
};
let key_start = c.next_reg;
for _ in &key_specs {
c.alloc();
}
let mut rewind_at = alloc::vec![0usize; n];
for (i, slot) in rewind_at.iter_mut().enumerate() {
*slot = c.ops.len();
c.ops.push(Op::RewindC {
cursor: i,
target: 0,
});
}
let body = c.ops.len();
let skip = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
for (i, (expr, _)) in projections.iter().enumerate() {
c.compile_expr_into(expr, i)?;
}
let distinct_skip = if sel.distinct {
let at = c.ops.len();
c.ops.push(Op::DistinctCheck {
start: 0,
count,
target: 0,
});
Some(at)
} else {
None
};
let (offset_skip, limit_done) = if ordering {
for (j, (expr, _)) in key_specs.iter().enumerate() {
c.compile_expr_into(expr, key_start + j)?;
}
c.ops.push(Op::SorterInsert {
row_start: 0,
row_count: count,
key_start,
key_count: key_specs.len(),
});
(None, None)
} else {
let offset_skip = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::ResultRow { start: 0, count });
let limit_done = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
(offset_skip, limit_done)
};
let mut next_at = alloc::vec![0usize; n];
for i in (0..n).rev() {
next_at[i] = c.ops.len();
let target = if i == n - 1 { body } else { rewind_at[i + 1] };
c.ops.push(Op::NextC { cursor: i, target });
}
let elimit = if ordering {
c.ops.push(Op::SorterSort {
keys: key_specs.iter().map(|(_, k)| k.clone()).collect(),
});
let srewind = c.ops.len();
c.ops.push(Op::SorterRewind { target: 0 });
let ebody = c.ops.len();
let eoffset = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::SorterRow { start: 0, count });
c.ops.push(Op::ResultRow { start: 0, count });
let elimit = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
let snext = c.ops.len();
c.ops.push(Op::SorterNext { target: ebody });
let eend = c.ops.len();
if let Op::SorterRewind { target } = &mut c.ops[srewind] {
*target = eend;
}
if let Some(at) = eoffset {
if let Op::IfPosDecr { target, .. } = &mut c.ops[at] {
*target = snext;
}
}
elimit
} else {
None
};
let end = c.ops.len();
c.ops.push(Op::Halt);
for i in 0..n {
let target = if i == 0 { end } else { next_at[i - 1] };
if let Op::RewindC { target: t, .. } = &mut c.ops[rewind_at[i]] {
*t = target;
}
}
let inner_next = next_at[n - 1];
if let Some(at) = skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = inner_next;
}
}
if let Some(at) = distinct_skip {
if let Op::DistinctCheck { target, .. } = &mut c.ops[at] {
*target = inner_next; }
}
if let Some(at) = offset_skip {
if let Op::IfPosDecr { target, .. } = &mut c.ops[at] {
*target = inner_next;
}
}
if let Some(at) = limit_done {
if let Op::DecrJumpZero { target, .. } = &mut c.ops[at] {
*target = end;
}
}
if let Some(at) = elimit {
if let Op::DecrJumpZero { target, .. } = &mut c.ops[at] {
*target = end;
}
}
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns: projections.into_iter().map(|(_, l)| l).collect(),
})
}
pub fn compile_aggregate_join(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
boundaries: &[usize],
) -> Result<Program> {
let n = boundaries.len();
debug_assert!(n >= 2 && boundaries[n - 1] == columns.len());
if !sel.compound.is_empty()
|| !sel.group_by.is_empty()
|| sel.having.is_some()
|| !sel.order_by.is_empty()
|| sel.limit.is_some()
|| sel.offset.is_some()
|| sel.distinct
{
return Err(Error::Unsupported("VDBE: bare aggregate join only"));
}
if collations.iter().any(|cl| *cl != Collation::Binary) {
return Err(Error::Unsupported(
"VDBE: non-BINARY collation in aggregate",
));
}
let projections = expand_projections(sel, columns, tables)?;
if sel.distinct && projections_have_explicit_collation(&projections) {
return Err(Error::Unsupported("VDBE: explicit COLLATE with DISTINCT"));
}
let mut slots: Vec<AggCallSpec> = Vec::new();
for (e, _) in &projections {
match agg_kind_distinct(e) {
Some(spec) => slots.push(spec),
None => return Err(Error::Unsupported("VDBE: unsupported aggregate")),
}
}
let count = projections.len();
let mut c = Compiler {
ops: Vec::new(),
next_reg: count,
columns: columns.to_vec(),
tables: tables.to_vec(),
affinities: affinities.to_vec(),
collations: collations.to_vec(),
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index: None,
cursor_boundaries: Some(boundaries.to_vec()),
};
let mut rewind_at = alloc::vec![0usize; n];
for (i, slot) in rewind_at.iter_mut().enumerate() {
*slot = c.ops.len();
c.ops.push(Op::RewindC {
cursor: i,
target: 0,
});
}
let body = c.ops.len();
let skip = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
for (slot, (kind, arg, distinct, filter, order, sep)) in slots.iter().enumerate() {
let arg_reg = match arg {
Some(expr) => Some(c.compile_expr(expr)?),
None => None,
};
let filter_reg = match filter {
Some(expr) => Some(c.compile_expr(expr)?),
None => None,
};
let order_keys = compile_order_keys(&mut c, order)?;
c.ops.push(Op::AggStep {
slot,
kind: *kind,
arg: arg_reg,
distinct: *distinct,
filter: filter_reg,
order: order_keys,
sep: sep.clone(),
});
}
let mut next_at = alloc::vec![0usize; n];
for i in (0..n).rev() {
next_at[i] = c.ops.len();
let target = if i == n - 1 { body } else { rewind_at[i + 1] };
c.ops.push(Op::NextC { cursor: i, target });
}
let end = c.ops.len();
for i in 0..n {
let target = if i == 0 { end } else { next_at[i - 1] };
if let Op::RewindC { target: t, .. } = &mut c.ops[rewind_at[i]] {
*t = target;
}
}
if let Some(at) = skip {
if let Op::IfFalse { target, .. } = &mut c.ops[at] {
*target = next_at[n - 1];
}
}
for (slot, (kind, _, _, _, _, _)) in slots.iter().enumerate() {
c.ops.push(Op::AggFinal {
slot,
kind: *kind,
dest: slot,
});
}
c.ops.push(Op::ResultRow { start: 0, count });
c.ops.push(Op::Halt);
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns: projections.into_iter().map(|(_, l)| l).collect(),
})
}
pub fn compile_group_join(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
boundaries: &[usize],
) -> Result<Program> {
let n = boundaries.len();
debug_assert!(n >= 2 && boundaries[n - 1] == columns.len());
if !sel.compound.is_empty() || sel.group_by.is_empty() {
return Err(Error::Unsupported("VDBE: GROUP BY join requires GROUP BY"));
}
let projections = expand_projections(sel, columns, tables)?;
if sel.distinct && projections_have_explicit_collation(&projections) {
return Err(Error::Unsupported("VDBE: explicit COLLATE with DISTINCT"));
}
compile_group_select(
sel,
columns,
tables,
affinities,
collations,
&projections,
Some(boundaries),
)
}
#[allow(clippy::too_many_arguments)]
pub fn compile_left_join2(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
n_left: usize,
on: &Option<Expr>,
) -> Result<Program> {
if !sel.compound.is_empty() || !sel.group_by.is_empty() || sel.having.is_some() {
return Err(Error::Unsupported(
"VDBE: left-join shape not nested-loopable",
));
}
reject_aggregate_or_window_in_predicates(sel)?;
if sel.distinct && collations.iter().any(|cl| *cl != Collation::Binary) {
return Err(Error::Unsupported(
"VDBE: non-BINARY collation with DISTINCT",
));
}
let projections = expand_projections(sel, columns, tables)?;
if sel.distinct && projections_have_explicit_collation(&projections) {
return Err(Error::Unsupported("VDBE: explicit COLLATE with DISTINCT"));
}
if projections.iter().any(|(e, _)| is_aggregate_expr(e)) {
return Err(Error::Unsupported("VDBE: aggregate over a left join"));
}
let count = projections.len();
if matches!(&sel.limit, Some(Expr::Literal(Literal::Integer(0)))) {
return Ok(Program {
ops: alloc::vec![Op::Halt],
n_registers: count,
columns: projections.into_iter().map(|(_, l)| l).collect(),
});
}
let mut c = Compiler {
ops: Vec::new(),
next_reg: count,
columns: columns.to_vec(),
tables: tables.to_vec(),
affinities: affinities.to_vec(),
collations: collations.to_vec(),
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index: None,
cursor_boundaries: Some(alloc::vec![n_left, columns.len()]),
};
let limit_reg = match &sel.limit {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n < 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer LIMIT")),
},
};
let offset_reg = match &sel.offset {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n <= 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer OFFSET")),
},
};
let matched = c.alloc();
let ordering = !sel.order_by.is_empty();
let key_specs = if ordering {
build_sort_keys(&c, sel, &projections, count)?
} else {
Vec::new()
};
let key_start = c.next_reg;
for _ in &key_specs {
c.alloc();
}
let rewind0 = c.ops.len();
c.ops.push(Op::RewindC {
cursor: 0,
target: 0,
});
let loop0 = c.ops.len();
c.ops.push(Op::Integer {
value: 0,
dest: matched,
});
let rewind1 = c.ops.len();
c.ops.push(Op::RewindC {
cursor: 1,
target: 0,
});
let body = c.ops.len();
let on_skip = match on {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
c.ops.push(Op::Integer {
value: 1,
dest: matched,
});
let where_skip_m = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
for (i, (expr, _)) in projections.iter().enumerate() {
c.compile_expr_into(expr, i)?;
}
let distinct_skip_m = if sel.distinct {
let at = c.ops.len();
c.ops.push(Op::DistinctCheck {
start: 0,
count,
target: 0,
});
Some(at)
} else {
None
};
let (offset_skip_m, limit_m) = if ordering {
for (j, (expr, _)) in key_specs.iter().enumerate() {
c.compile_expr_into(expr, key_start + j)?;
}
c.ops.push(Op::SorterInsert {
row_start: 0,
row_count: count,
key_start,
key_count: key_specs.len(),
});
(None, None)
} else {
let offset_skip_m = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::ResultRow { start: 0, count });
let limit_m = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
(offset_skip_m, limit_m)
};
let next1 = c.ops.len();
c.ops.push(Op::NextC {
cursor: 1,
target: body,
});
let after_inner = c.ops.len();
c.ops.push(Op::IfFalse {
reg: matched,
target: 0,
}); let goto_next0 = c.ops.len();
c.ops.push(Op::Goto { target: 0 });
let null_pad = c.ops.len();
c.ops.push(Op::NullRow { cursor: 1 });
let where_skip_n = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
for (i, (expr, _)) in projections.iter().enumerate() {
c.compile_expr_into(expr, i)?;
}
let distinct_skip_n = if sel.distinct {
let at = c.ops.len();
c.ops.push(Op::DistinctCheck {
start: 0,
count,
target: 0,
});
Some(at)
} else {
None
};
let (offset_skip_n, limit_n) = if ordering {
for (j, (expr, _)) in key_specs.iter().enumerate() {
c.compile_expr_into(expr, key_start + j)?;
}
c.ops.push(Op::SorterInsert {
row_start: 0,
row_count: count,
key_start,
key_count: key_specs.len(),
});
(None, None)
} else {
let offset_skip_n = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::ResultRow { start: 0, count });
let limit_n = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
(offset_skip_n, limit_n)
};
let next0 = c.ops.len();
c.ops.push(Op::NextC {
cursor: 0,
target: loop0,
});
let scan_done = c.ops.len();
let elimit = if ordering {
c.ops.push(Op::SorterSort {
keys: key_specs.iter().map(|(_, k)| k.clone()).collect(),
});
let srewind = c.ops.len();
c.ops.push(Op::SorterRewind { target: 0 });
let ebody = c.ops.len();
let eoffset = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::SorterRow { start: 0, count });
c.ops.push(Op::ResultRow { start: 0, count });
let elimit = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
let snext = c.ops.len();
c.ops.push(Op::SorterNext { target: ebody });
let eend = c.ops.len();
if let Op::SorterRewind { target } = &mut c.ops[srewind] {
*target = eend;
}
if let Some(at) = eoffset {
if let Op::IfPosDecr { target, .. } = &mut c.ops[at] {
*target = snext;
}
}
elimit
} else {
None
};
let end = c.ops.len();
c.ops.push(Op::Halt);
let set = |ops: &mut [Op], at: usize, tgt: usize| match &mut ops[at] {
Op::IfFalse { target, .. }
| Op::IfPosDecr { target, .. }
| Op::DecrJumpZero { target, .. }
| Op::Goto { target }
| Op::DistinctCheck { target, .. }
| Op::RewindC { target, .. } => *target = tgt,
_ => {}
};
set(&mut c.ops, rewind0, scan_done); set(&mut c.ops, rewind1, after_inner); if let Some(at) = on_skip {
set(&mut c.ops, at, next1); }
if let Some(at) = where_skip_m {
set(&mut c.ops, at, next1); }
if let Some(at) = distinct_skip_m {
set(&mut c.ops, at, next1); }
if let Some(at) = offset_skip_m {
set(&mut c.ops, at, next1);
}
if let Some(at) = limit_m {
set(&mut c.ops, at, end);
}
set(&mut c.ops, after_inner, null_pad); set(&mut c.ops, goto_next0, next0); if let Some(at) = where_skip_n {
set(&mut c.ops, at, next0); }
if let Some(at) = distinct_skip_n {
set(&mut c.ops, at, next0); }
if let Some(at) = offset_skip_n {
set(&mut c.ops, at, next0);
}
if let Some(at) = limit_n {
set(&mut c.ops, at, end);
}
if let Some(at) = elimit {
set(&mut c.ops, at, end);
}
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns: projections.into_iter().map(|(_, l)| l).collect(),
})
}
fn emit_output_row(
c: &mut Compiler,
projections: &[(Expr, String)],
count: usize,
offset_reg: Option<usize>,
limit_reg: Option<usize>,
sorter: Option<(usize, &[(Expr, SortKey)])>,
distinct: bool,
) -> Result<(Option<usize>, Option<usize>, Option<usize>)> {
for (i, (expr, _)) in projections.iter().enumerate() {
c.compile_expr_into(expr, i)?;
}
let distinct_skip = if distinct {
let at = c.ops.len();
c.ops.push(Op::DistinctCheck {
start: 0,
count,
target: 0,
});
Some(at)
} else {
None
};
if let Some((key_start, key_specs)) = sorter {
for (j, (expr, _)) in key_specs.iter().enumerate() {
c.compile_expr_into(expr, key_start + j)?;
}
c.ops.push(Op::SorterInsert {
row_start: 0,
row_count: count,
key_start,
key_count: key_specs.len(),
});
return Ok((None, None, distinct_skip));
}
let offset_skip = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::ResultRow { start: 0, count });
let limit_done = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
Ok((offset_skip, limit_done, distinct_skip))
}
#[allow(clippy::too_many_arguments)]
pub fn compile_full_join2(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
n_left: usize,
on: &Option<Expr>,
) -> Result<Program> {
if !sel.compound.is_empty() || !sel.group_by.is_empty() || sel.having.is_some() {
return Err(Error::Unsupported(
"VDBE: full-join shape not nested-loopable",
));
}
reject_aggregate_or_window_in_predicates(sel)?;
if sel.distinct && collations.iter().any(|cl| *cl != Collation::Binary) {
return Err(Error::Unsupported(
"VDBE: non-BINARY collation with DISTINCT",
));
}
let projections = expand_projections(sel, columns, tables)?;
if sel.distinct && projections_have_explicit_collation(&projections) {
return Err(Error::Unsupported("VDBE: explicit COLLATE with DISTINCT"));
}
if projections.iter().any(|(e, _)| is_aggregate_expr(e)) {
return Err(Error::Unsupported("VDBE: aggregate over a full join"));
}
let count = projections.len();
if matches!(&sel.limit, Some(Expr::Literal(Literal::Integer(0)))) {
return Ok(Program {
ops: alloc::vec![Op::Halt],
n_registers: count,
columns: projections.into_iter().map(|(_, l)| l).collect(),
});
}
let mut c = Compiler {
ops: Vec::new(),
next_reg: count,
columns: columns.to_vec(),
tables: tables.to_vec(),
affinities: affinities.to_vec(),
collations: collations.to_vec(),
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index: None,
cursor_boundaries: Some(alloc::vec![n_left, columns.len()]),
};
let limit_reg = match &sel.limit {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n < 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer LIMIT")),
},
};
let offset_reg = match &sel.offset {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n <= 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer OFFSET")),
},
};
let matched = c.alloc();
let ordering = !sel.order_by.is_empty();
let key_specs = if ordering {
build_sort_keys(&c, sel, &projections, count)?
} else {
Vec::new()
};
let key_start = c.next_reg;
for _ in &key_specs {
c.alloc();
}
let sorter = if ordering {
Some((key_start, key_specs.as_slice()))
} else {
None
};
let set = |ops: &mut [Op], at: usize, tgt: usize| match &mut ops[at] {
Op::IfFalse { target, .. }
| Op::IfPosDecr { target, .. }
| Op::DecrJumpZero { target, .. }
| Op::Goto { target }
| Op::IfMatched { target, .. }
| Op::DistinctCheck { target, .. }
| Op::RewindC { target, .. } => *target = tgt,
_ => {}
};
let rewind0 = c.ops.len();
c.ops.push(Op::RewindC {
cursor: 0,
target: 0,
});
let loop0 = c.ops.len();
c.ops.push(Op::Integer {
value: 0,
dest: matched,
});
let rewind1 = c.ops.len();
c.ops.push(Op::RewindC {
cursor: 1,
target: 0,
});
let body = c.ops.len();
let on_skip = match on {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
c.ops.push(Op::Integer {
value: 1,
dest: matched,
});
c.ops.push(Op::MarkMatched { cursor: 1 });
let where_m = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
let (offset_m, limit_m, distinct_m) = emit_output_row(
&mut c,
&projections,
count,
offset_reg,
limit_reg,
sorter,
sel.distinct,
)?;
let next1 = c.ops.len();
c.ops.push(Op::NextC {
cursor: 1,
target: body,
});
let after_inner = c.ops.len();
c.ops.push(Op::IfFalse {
reg: matched,
target: 0,
});
let goto_next0 = c.ops.len();
c.ops.push(Op::Goto { target: 0 });
let null_pad = c.ops.len();
c.ops.push(Op::NullRow { cursor: 1 });
let where_lnull = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
let (offset_lnull, limit_lnull, distinct_lnull) = emit_output_row(
&mut c,
&projections,
count,
offset_reg,
limit_reg,
sorter,
sel.distinct,
)?;
let next0 = c.ops.len();
c.ops.push(Op::NextC {
cursor: 0,
target: loop0,
});
let pass2 = c.ops.len();
c.ops.push(Op::NullRow { cursor: 0 });
let rewind1b = c.ops.len();
c.ops.push(Op::RewindC {
cursor: 1,
target: 0,
});
let loop2 = c.ops.len();
let if_matched = c.ops.len();
c.ops.push(Op::IfMatched {
cursor: 1,
target: 0,
});
let where_rnull = match &sel.where_clause {
Some(pred) => {
let preg = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
let (offset_rnull, limit_rnull, distinct_rnull) = emit_output_row(
&mut c,
&projections,
count,
offset_reg,
limit_reg,
sorter,
sel.distinct,
)?;
let next2 = c.ops.len();
c.ops.push(Op::NextC {
cursor: 1,
target: loop2,
});
let scan_done = c.ops.len();
let elimit = if ordering {
c.ops.push(Op::SorterSort {
keys: key_specs.iter().map(|(_, k)| k.clone()).collect(),
});
let srewind = c.ops.len();
c.ops.push(Op::SorterRewind { target: 0 });
let ebody = c.ops.len();
let eoffset = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::SorterRow { start: 0, count });
c.ops.push(Op::ResultRow { start: 0, count });
let elimit = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
let snext = c.ops.len();
c.ops.push(Op::SorterNext { target: ebody });
let eend = c.ops.len();
if let Op::SorterRewind { target } = &mut c.ops[srewind] {
*target = eend;
}
if let Some(at) = eoffset {
if let Op::IfPosDecr { target, .. } = &mut c.ops[at] {
*target = snext;
}
}
elimit
} else {
None
};
let end = c.ops.len();
c.ops.push(Op::Halt);
set(&mut c.ops, rewind0, pass2); set(&mut c.ops, rewind1, after_inner); if let Some(at) = on_skip {
set(&mut c.ops, at, next1);
}
if let Some(at) = where_m {
set(&mut c.ops, at, next1);
}
if let Some(at) = distinct_m {
set(&mut c.ops, at, next1); }
if let Some(at) = offset_m {
set(&mut c.ops, at, next1);
}
if let Some(at) = limit_m {
set(&mut c.ops, at, end);
}
set(&mut c.ops, after_inner, null_pad);
set(&mut c.ops, goto_next0, next0);
if let Some(at) = where_lnull {
set(&mut c.ops, at, next0);
}
if let Some(at) = distinct_lnull {
set(&mut c.ops, at, next0); }
if let Some(at) = offset_lnull {
set(&mut c.ops, at, next0);
}
if let Some(at) = limit_lnull {
set(&mut c.ops, at, end);
}
set(&mut c.ops, rewind1b, scan_done); set(&mut c.ops, if_matched, next2); if let Some(at) = where_rnull {
set(&mut c.ops, at, next2);
}
if let Some(at) = distinct_rnull {
set(&mut c.ops, at, next2); }
if let Some(at) = offset_rnull {
set(&mut c.ops, at, next2);
}
if let Some(at) = limit_rnull {
set(&mut c.ops, at, end);
}
if let Some(at) = elimit {
set(&mut c.ops, at, end);
}
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns: projections.into_iter().map(|(_, l)| l).collect(),
})
}
struct LeftJoinNCtx<'a> {
sel: &'a Select,
projections: &'a [(Expr, String)],
count: usize,
distinct: bool,
offset_reg: Option<usize>,
limit_reg: Option<usize>,
key_start: usize,
key_specs: &'a [(Expr, SortKey)],
ordering: bool,
kinds: &'a [JoinKind],
ons: &'a [Option<Expr>],
matched_regs: &'a [usize],
n_cursors: usize,
}
fn emit_join_level(
c: &mut Compiler,
k: usize,
ctx: &LeftJoinNCtx,
limit_fixups: &mut Vec<usize>,
) -> Result<()> {
fn bp(ops: &mut [Op], at: usize, tgt: usize) {
match &mut ops[at] {
Op::IfFalse { target, .. }
| Op::IfPosDecr { target, .. }
| Op::DecrJumpZero { target, .. }
| Op::Goto { target }
| Op::DistinctCheck { target, .. }
| Op::RewindC { target, .. }
| Op::NextC { target, .. } => *target = tgt,
_ => {}
}
}
if k == ctx.n_cursors {
let where_skip = match &ctx.sel.where_clause {
Some(pred) => {
let r = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse { reg: r, target: 0 });
Some(at)
}
None => None,
};
let sorter = if ctx.ordering {
Some((ctx.key_start, ctx.key_specs))
} else {
None
};
let (offset_skip, limit_done, distinct_skip) = emit_output_row(
c,
ctx.projections,
ctx.count,
ctx.offset_reg,
ctx.limit_reg,
sorter,
ctx.distinct,
)?;
let cont = c.ops.len();
if let Some(at) = where_skip {
bp(&mut c.ops, at, cont);
}
if let Some(at) = offset_skip {
bp(&mut c.ops, at, cont);
}
if let Some(at) = distinct_skip {
bp(&mut c.ops, at, cont);
}
if let Some(at) = limit_done {
limit_fixups.push(at);
}
return Ok(());
}
let mreg = ctx.matched_regs[k - 1];
let kind = ctx.kinds[k - 1];
let on = &ctx.ons[k - 1];
c.ops.push(Op::Integer {
value: 0,
dest: mreg,
});
let rewind = c.ops.len();
c.ops.push(Op::RewindC {
cursor: k,
target: 0,
}); let loopk = c.ops.len();
let on_skip = match on {
Some(pred) => {
let r = c.compile_expr(pred)?;
let at = c.ops.len();
c.ops.push(Op::IfFalse { reg: r, target: 0 });
Some(at)
}
None => None,
};
c.ops.push(Op::Integer {
value: 1,
dest: mreg,
});
emit_join_level(c, k + 1, ctx, limit_fixups)?;
let nextk = c.ops.len();
c.ops.push(Op::NextC {
cursor: k,
target: loopk,
});
if let Some(at) = on_skip {
bp(&mut c.ops, at, nextk);
}
let nullcheck = c.ops.len();
bp(&mut c.ops, rewind, nullcheck);
if matches!(kind, JoinKind::Left) {
let if_unmatched = c.ops.len();
c.ops.push(Op::IfFalse {
reg: mreg,
target: 0,
}); let goto_after = c.ops.len();
c.ops.push(Op::Goto { target: 0 }); let null_body = c.ops.len();
bp(&mut c.ops, if_unmatched, null_body);
c.ops.push(Op::NullRow { cursor: k });
emit_join_level(c, k + 1, ctx, limit_fixups)?;
let after = c.ops.len();
bp(&mut c.ops, goto_after, after);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn compile_left_join_n(
sel: &Select,
columns: &[String],
tables: &[String],
affinities: &[Affinity],
collations: &[Collation],
boundaries: &[usize],
kinds: &[JoinKind],
ons: &[Option<Expr>],
) -> Result<Program> {
let n_cursors = boundaries.len();
debug_assert!(n_cursors >= 2 && boundaries[n_cursors - 1] == columns.len());
debug_assert!(kinds.len() == n_cursors - 1 && ons.len() == n_cursors - 1);
if !sel.compound.is_empty() || !sel.group_by.is_empty() || sel.having.is_some() {
return Err(Error::Unsupported(
"VDBE: outer-join shape not nested-loopable",
));
}
reject_aggregate_or_window_in_predicates(sel)?;
if sel.distinct && collations.iter().any(|cl| *cl != Collation::Binary) {
return Err(Error::Unsupported(
"VDBE: non-BINARY collation with DISTINCT",
));
}
let projections = expand_projections(sel, columns, tables)?;
if sel.distinct && projections_have_explicit_collation(&projections) {
return Err(Error::Unsupported("VDBE: explicit COLLATE with DISTINCT"));
}
if projections.iter().any(|(e, _)| is_aggregate_expr(e)) {
return Err(Error::Unsupported("VDBE: aggregate over an outer join"));
}
let count = projections.len();
if matches!(&sel.limit, Some(Expr::Literal(Literal::Integer(0)))) {
return Ok(Program {
ops: alloc::vec![Op::Halt],
n_registers: count,
columns: projections.into_iter().map(|(_, l)| l).collect(),
});
}
let mut c = Compiler {
ops: Vec::new(),
next_reg: count,
columns: columns.to_vec(),
tables: tables.to_vec(),
affinities: affinities.to_vec(),
collations: collations.to_vec(),
bindings: Vec::new(),
forbid_raw_columns: false,
rowid_index: None,
cursor_boundaries: Some(boundaries.to_vec()),
};
let limit_reg = match &sel.limit {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n < 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer LIMIT")),
},
};
let offset_reg = match &sel.offset {
None => None,
Some(e) => match fold_const_int(e) {
Some(n) if n <= 0 => None,
Some(n) => {
let r = c.alloc();
c.ops.push(Op::Integer { value: n, dest: r });
Some(r)
}
None => return Err(Error::Unsupported("VDBE: only constant integer OFFSET")),
},
};
let matched_regs: Vec<usize> = (0..n_cursors - 1).map(|_| c.alloc()).collect();
let ordering = !sel.order_by.is_empty();
let key_specs = if ordering {
build_sort_keys(&c, sel, &projections, count)?
} else {
Vec::new()
};
let key_start = c.next_reg;
for _ in &key_specs {
c.alloc();
}
let mut limit_fixups: Vec<usize> = Vec::new();
let ctx = LeftJoinNCtx {
sel,
projections: &projections,
count,
distinct: sel.distinct,
offset_reg,
limit_reg,
key_start,
key_specs: &key_specs,
ordering,
kinds,
ons,
matched_regs: &matched_regs,
n_cursors,
};
let rewind0 = c.ops.len();
c.ops.push(Op::RewindC {
cursor: 0,
target: 0,
});
let loop0 = c.ops.len();
emit_join_level(&mut c, 1, &ctx, &mut limit_fixups)?;
c.ops.push(Op::NextC {
cursor: 0,
target: loop0,
});
let scan_done = c.ops.len();
let elimit = if ordering {
c.ops.push(Op::SorterSort {
keys: key_specs.iter().map(|(_, k)| k.clone()).collect(),
});
let srewind = c.ops.len();
c.ops.push(Op::SorterRewind { target: 0 });
let ebody = c.ops.len();
let eoffset = offset_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::IfPosDecr { reg: r, target: 0 });
at
});
c.ops.push(Op::SorterRow { start: 0, count });
c.ops.push(Op::ResultRow { start: 0, count });
let elimit = limit_reg.map(|r| {
let at = c.ops.len();
c.ops.push(Op::DecrJumpZero { reg: r, target: 0 });
at
});
let snext = c.ops.len();
c.ops.push(Op::SorterNext { target: ebody });
let eend = c.ops.len();
if let Op::SorterRewind { target } = &mut c.ops[srewind] {
*target = eend;
}
if let Some(at) = eoffset {
if let Op::IfPosDecr { target, .. } = &mut c.ops[at] {
*target = snext;
}
}
elimit
} else {
None
};
let end = c.ops.len();
c.ops.push(Op::Halt);
if let Op::RewindC { target, .. } = &mut c.ops[rewind0] {
*target = scan_done; }
for at in limit_fixups {
if let Op::DecrJumpZero { target, .. } = &mut c.ops[at] {
*target = end;
}
}
if let Some(at) = elimit {
if let Op::DecrJumpZero { target, .. } = &mut c.ops[at] {
*target = end;
}
}
Ok(Program {
ops: c.ops,
n_registers: c.next_reg,
columns: projections.into_iter().map(|(_, l)| l).collect(),
})
}
struct Compiler {
ops: Vec<Op>,
next_reg: usize,
columns: Vec<String>,
tables: Vec<String>,
affinities: Vec<Affinity>,
collations: Vec<Collation>,
bindings: Vec<(Expr, usize)>,
forbid_raw_columns: bool,
rowid_index: Option<usize>,
cursor_boundaries: Option<Vec<usize>>,
}
impl Compiler {
fn cursor_of(boundaries: &[usize], idx: usize) -> (usize, usize) {
let mut prev = 0;
for (cur, &end) in boundaries.iter().enumerate() {
if idx < end {
return (cur, idx - prev);
}
prev = end;
}
let last = boundaries.len().saturating_sub(1);
(
last,
idx - boundaries.get(last.wrapping_sub(1)).copied().unwrap_or(0),
)
}
}
impl Compiler {
fn alloc(&mut self) -> usize {
let r = self.next_reg;
self.next_reg += 1;
r
}
fn resolve_column(&self, table: Option<&str>, column: &str) -> Result<usize> {
let mut found = None;
for (i, c) in self.columns.iter().enumerate() {
if !c.eq_ignore_ascii_case(column) {
continue;
}
if let Some(t) = table {
if !self
.tables
.get(i)
.is_some_and(|tn| tn.eq_ignore_ascii_case(t))
{
continue;
}
}
if found.is_some() {
return Err(Error::Unsupported("VDBE: ambiguous column reference"));
}
found = Some(i);
}
if found.is_none() {
if let Some(ri) = self.rowid_index {
let is_alias = matches!(
column.to_ascii_lowercase().as_str(),
"rowid" | "_rowid_" | "oid"
);
let table_ok = table.is_none_or(|t| {
self.tables
.first()
.is_some_and(|tn| tn.eq_ignore_ascii_case(t))
});
if is_alias && table_ok {
return Ok(ri);
}
}
}
found.ok_or(Error::Unsupported("VDBE: unresolved column reference"))
}
fn expr_affinity(&self, expr: &Expr) -> Option<Affinity> {
match expr {
Expr::Column { table, column, .. } => self
.resolve_column(table.as_deref(), column)
.ok()
.and_then(|i| self.affinities.get(i).copied()),
Expr::Cast { type_name, .. } => Some(Affinity::from_type(Some(type_name))),
Expr::Paren(e) => self.expr_affinity(e),
Expr::Collate { expr, .. } => self.expr_affinity(expr),
_ => None,
}
}
fn explicit_collation(&self, expr: &Expr) -> Option<Collation> {
match expr {
Expr::Paren(e) => self.explicit_collation(e),
Expr::Collate { expr, collation } => {
Collation::parse(collation).or_else(|| self.explicit_collation(expr))
}
_ => None,
}
}
fn implicit_collation(&self, expr: &Expr) -> Option<Collation> {
match expr {
Expr::Column { table, column, .. } => self
.resolve_column(table.as_deref(), column)
.ok()
.and_then(|i| self.collations.get(i).copied()),
Expr::Paren(e) | Expr::Collate { expr: e, .. } => self.implicit_collation(e),
_ => None,
}
}
fn col_collation(&self, expr: &Expr) -> Option<Collation> {
self.explicit_collation(expr)
.or_else(|| self.implicit_collation(expr))
}
fn compare_collation(&self, left: &Expr, right: &Expr) -> Collation {
self.explicit_collation(left)
.or_else(|| self.explicit_collation(right))
.or_else(|| self.implicit_collation(left))
.or_else(|| self.implicit_collation(right))
.unwrap_or_default()
}
fn push_compare(
&mut self,
op: BinaryOp,
lhs: usize,
left: &Expr,
rhs: usize,
right: &Expr,
dest: usize,
) {
let coll = self.compare_collation(left, right);
self.push_compare_coll(op, lhs, left, rhs, right, dest, coll);
}
#[allow(clippy::too_many_arguments)]
fn push_compare_coll(
&mut self,
op: BinaryOp,
lhs: usize,
left: &Expr,
rhs: usize,
right: &Expr,
dest: usize,
coll: Collation,
) {
let ra = self.expr_affinity(right);
self.push_compare_coll_ra(op, lhs, left, rhs, ra, dest, coll);
}
#[allow(clippy::too_many_arguments)]
fn push_compare_coll_ra(
&mut self,
op: BinaryOp,
lhs: usize,
left: &Expr,
rhs: usize,
ra: Option<Affinity>,
dest: usize,
coll: Collation,
) {
let la = self.expr_affinity(left);
self.ops.push(Op::Compare {
op,
lhs,
rhs,
dest,
la,
ra,
coll,
});
}
fn compile_expr(&mut self, expr: &Expr) -> Result<usize> {
let dest = self.alloc();
self.compile_expr_into(expr, dest)?;
Ok(dest)
}
fn compile_expr_into(&mut self, expr: &Expr, dest: usize) -> Result<()> {
if let Some(&(_, src)) = self.bindings.iter().find(|(e, _)| e == expr) {
if src != dest {
self.ops.push(Op::Copy { src, dest });
}
return Ok(());
}
match expr {
Expr::Literal(l) => {
let op = match l {
Literal::Integer(i) => Op::Integer { value: *i, dest },
Literal::Real(r) => Op::Real { value: *r, dest },
Literal::Str(s) => Op::Str {
value: s.clone(),
dest,
},
Literal::Null => Op::Null { dest },
Literal::Boolean(b) => Op::Integer {
value: *b as i64,
dest,
},
Literal::Blob(b) => Op::Blob {
value: b.clone(),
dest,
},
};
self.ops.push(op);
Ok(())
}
Expr::Column { table, column, .. } => {
if self.forbid_raw_columns {
return Err(Error::Unsupported("VDBE: bare column in grouped output"));
}
let idx = self.resolve_column(table.as_deref(), column)?;
match &self.cursor_boundaries {
Some(bounds) => {
let (cursor, col) = Self::cursor_of(bounds, idx);
self.ops.push(Op::ColumnC { cursor, col, dest });
}
None => self.ops.push(Op::Column { col: idx, dest }),
}
Ok(())
}
Expr::Paren(inner) => self.compile_expr_into(inner, dest),
Expr::Unary {
op: crate::sql::ast::UnaryOp::Negate,
expr: inner,
} => {
let r = self.compile_expr(inner)?;
self.ops.push(Op::Negate { reg: r, dest });
Ok(())
}
Expr::Unary {
op: crate::sql::ast::UnaryOp::Not,
expr: inner,
} => {
let r = self.compile_expr(inner)?;
self.ops.push(Op::Not { reg: r, dest });
Ok(())
}
Expr::Unary {
op: crate::sql::ast::UnaryOp::BitNot,
expr: inner,
} => {
let r = self.compile_expr(inner)?;
self.ops.push(Op::BitNot { reg: r, dest });
Ok(())
}
Expr::Unary {
op: crate::sql::ast::UnaryOp::Identity,
expr: inner,
} => self.compile_expr_into(inner, dest),
Expr::IsNull {
expr: inner,
negated,
} => {
let r = self.compile_expr(inner)?;
self.ops.push(Op::IsNull {
reg: r,
negated: *negated,
dest,
});
Ok(())
}
Expr::Binary { op, left, right } => {
let l = self.compile_expr(left)?;
let r = self.compile_expr(right)?;
use BinaryOp::*;
match op {
Add | Sub | Mul | Div | Mod => {
self.ops.push(Op::Arith {
op: *op,
lhs: l,
rhs: r,
dest,
});
Ok(())
}
BitAnd | BitOr | LShift | RShift => {
self.ops.push(Op::Bitwise {
op: *op,
lhs: l,
rhs: r,
dest,
});
Ok(())
}
Concat => {
self.ops.push(Op::Concat {
lhs: l,
rhs: r,
dest,
});
Ok(())
}
Eq | NotEq | Lt | LtEq | Gt | GtEq => {
self.push_compare(*op, l, left, r, right, dest);
Ok(())
}
And => {
self.ops.push(Op::And {
lhs: l,
rhs: r,
dest,
});
Ok(())
}
Or => {
self.ops.push(Op::Or {
lhs: l,
rhs: r,
dest,
});
Ok(())
}
Is | IsNot => {
if let Expr::Literal(Literal::Boolean(want)) = unparen(right) {
self.ops.push(Op::Truthy {
want: *want,
not: matches!(op, IsNot),
operand: l,
dest,
});
} else {
self.ops.push(Op::Is {
is: matches!(op, Is),
lhs: l,
rhs: r,
dest,
la: self.expr_affinity(left),
ra: self.expr_affinity(right),
});
}
Ok(())
}
Like | Glob => {
self.ops.push(Op::Like {
glob: matches!(op, Glob),
lhs: l,
rhs: r,
dest,
});
Ok(())
}
JsonExtract | JsonExtractText => {
self.ops.push(Op::Json {
as_text: matches!(op, JsonExtractText),
lhs: l,
rhs: r,
dest,
});
Ok(())
}
}
}
Expr::Cast {
expr: inner,
type_name,
} => {
let r = self.compile_expr(inner)?;
self.ops.push(Op::Cast {
reg: r,
type_name: type_name.clone(),
dest,
});
Ok(())
}
Expr::Collate { expr: inner, .. } => self.compile_expr_into(inner, dest),
Expr::Case {
operand,
when_then,
else_result,
} => self.compile_case(operand.as_deref(), when_then, else_result.as_deref(), dest),
Expr::Between {
expr: inner,
low,
high,
negated,
} => {
let x = self.compile_expr(inner)?;
let lo = self.compile_expr(low)?;
let hi = self.compile_expr(high)?;
let ge = self.alloc();
self.push_compare(BinaryOp::GtEq, x, inner, lo, low, ge);
let le = self.alloc();
self.push_compare(BinaryOp::LtEq, x, inner, hi, high, le);
if *negated {
let both = self.alloc();
self.ops.push(Op::And {
lhs: ge,
rhs: le,
dest: both,
});
self.ops.push(Op::Not { reg: both, dest });
} else {
self.ops.push(Op::And {
lhs: ge,
rhs: le,
dest,
});
}
Ok(())
}
Expr::InList {
expr: inner,
list,
negated,
candidate_affinity,
} => {
let x = self.compile_expr(inner)?;
let in_coll = if list.len() == 1 {
self.compare_collation(inner, &list[0])
} else {
self.col_collation(inner).unwrap_or_default()
};
let cand_ra = candidate_affinity
.as_deref()
.map(|t| Affinity::from_type(Some(t)));
let acc = self.alloc();
self.ops.push(Op::Integer {
value: 0,
dest: acc,
});
for elem in list {
let e = self.compile_expr(elem)?;
let eq = self.alloc();
self.push_compare_coll_ra(BinaryOp::Eq, x, inner, e, cand_ra, eq, in_coll);
let next = self.alloc();
self.ops.push(Op::Or {
lhs: acc,
rhs: eq,
dest: next,
});
self.ops.push(Op::Copy {
src: next,
dest: acc,
});
}
if *negated {
self.ops.push(Op::Not { reg: acc, dest });
} else {
self.ops.push(Op::Copy { src: acc, dest });
}
Ok(())
}
Expr::Function {
name,
distinct,
args,
star,
filter,
order_by,
over,
} if !*distinct
&& !*star
&& filter.is_none()
&& order_by.is_empty()
&& over.is_none()
&& is_pure_scalar_fn(name, args.len()) =>
{
if args.iter().any(|a| self.explicit_collation(a).is_some()) {
return Err(Error::Unsupported("VDBE: COLLATE in a function argument"));
}
let arg_start = self.next_reg;
for _ in 0..args.len() {
self.alloc();
}
for (i, a) in args.iter().enumerate() {
self.compile_expr_into(a, arg_start + i)?;
}
self.ops.push(Op::Func {
name: name.clone(),
arg_start,
arg_count: args.len(),
dest,
});
Ok(())
}
Expr::Subquery(sel) => {
if sel.from.is_some()
|| !sel.ctes.is_empty()
|| !sel.window_defs.is_empty()
|| !sel.group_by.is_empty()
|| sel.having.is_some()
|| !sel.compound.is_empty()
|| !sel.order_by.is_empty()
|| sel.limit.is_some()
|| sel.offset.is_some()
{
return Err(Error::Unsupported("VDBE: scalar subquery shape"));
}
let [ResultColumn::Expr { expr: inner, .. }] = &sel.columns[..] else {
return Err(Error::Unsupported("VDBE: scalar subquery arity"));
};
if is_aggregate_expr(inner) {
return Err(Error::Unsupported("VDBE: aggregate scalar subquery"));
}
self.ops.push(Op::Null { dest });
let skip = match &sel.where_clause {
Some(pred) => {
let preg = self.compile_expr(pred)?;
let at = self.ops.len();
self.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
Some(at)
}
None => None,
};
self.compile_expr_into(inner, dest)?;
let end = self.ops.len();
if let Some(at) = skip {
if let Op::IfFalse { target, .. } = &mut self.ops[at] {
*target = end;
}
}
Ok(())
}
Expr::Exists {
select: sel,
negated,
} => {
if sel.from.is_some()
|| !sel.ctes.is_empty()
|| !sel.window_defs.is_empty()
|| !sel.group_by.is_empty()
|| sel.having.is_some()
|| !sel.compound.is_empty()
|| !sel.order_by.is_empty()
|| sel.limit.is_some()
|| sel.offset.is_some()
{
return Err(Error::Unsupported("VDBE: exists subquery shape"));
}
for rc in &sel.columns {
let ResultColumn::Expr { expr: inner, .. } = rc else {
return Err(Error::Unsupported("VDBE: exists wildcard projection"));
};
if is_aggregate_expr(inner) {
return Err(Error::Unsupported("VDBE: aggregate exists subquery"));
}
let saved_ops = self.ops.len();
let saved_reg = self.next_reg;
self.compile_expr(inner)?;
self.ops.truncate(saved_ops);
self.next_reg = saved_reg;
}
match &sel.where_clause {
None => self.ops.push(Op::Integer {
value: if *negated { 0 } else { 1 },
dest,
}),
Some(pred) => {
let (default_v, match_v) = if *negated { (1, 0) } else { (0, 1) };
self.ops.push(Op::Integer {
value: default_v,
dest,
});
let preg = self.compile_expr(pred)?;
let at = self.ops.len();
self.ops.push(Op::IfFalse {
reg: preg,
target: 0,
});
self.ops.push(Op::Integer {
value: match_v,
dest,
});
let end = self.ops.len();
if let Op::IfFalse { target, .. } = &mut self.ops[at] {
*target = end;
}
}
}
Ok(())
}
_ => Err(Error::Unsupported("VDBE spike: this expression")),
}
}
fn compile_case(
&mut self,
operand: Option<&Expr>,
when_then: &[(Expr, Expr)],
else_result: Option<&Expr>,
dest: usize,
) -> Result<()> {
let operand_reg = match operand {
Some(o) => Some(self.compile_expr(o)?),
None => None,
};
let mut end_jumps = Vec::new();
for (when, then) in when_then {
let cond = match operand_reg {
Some(oreg) => {
let wreg = self.compile_expr(when)?;
let c = self.alloc();
self.push_compare(BinaryOp::Eq, oreg, operand.unwrap(), wreg, when, c);
c
}
None => self.compile_expr(when)?,
};
let skip = self.ops.len();
self.ops.push(Op::IfFalse {
reg: cond,
target: 0,
});
self.compile_expr_into(then, dest)?;
end_jumps.push(self.ops.len());
self.ops.push(Op::Goto { target: 0 });
let here = self.ops.len();
if let Op::IfFalse { target, .. } = &mut self.ops[skip] {
*target = here;
}
}
match else_result {
Some(e) => self.compile_expr_into(e, dest)?,
None => self.ops.push(Op::Null { dest }),
}
let end = self.ops.len();
for j in end_jumps {
if let Op::Goto { target } = &mut self.ops[j] {
*target = end;
}
}
Ok(())
}
}
pub fn run(program: &Program) -> Result<Vec<Vec<Value>>> {
run_rows(program, &[])
}
pub fn run_rows(program: &Program, table_rows: &[Vec<Value>]) -> Result<Vec<Vec<Value>>> {
run_rows_multi(program, &[table_rows])
}
pub fn run_rows_multi(program: &Program, rowsets: &[&[Vec<Value>]]) -> Result<Vec<Vec<Value>>> {
let mut regs: Vec<Value> = alloc::vec![Value::Null; program.n_registers];
let mut out = Vec::new();
let table_rows: &[Vec<Value>] = rowsets.first().copied().unwrap_or(&[]);
let mut cursor: usize = 0; let mut positions: Vec<usize> = alloc::vec![0; rowsets.len().max(1)];
let mut null_flags: Vec<bool> = alloc::vec![false; rowsets.len().max(1)];
let mut matched_rows: Vec<Vec<bool>> = rowsets
.iter()
.map(|rs| alloc::vec![false; rs.len()])
.collect();
if matched_rows.is_empty() {
matched_rows.push(Vec::new());
}
let mut sorter: Vec<(Vec<Value>, Vec<Value>)> = Vec::new();
let mut scursor: usize = 0;
let mut seen: Vec<Vec<Value>> = Vec::new();
let mut agg: Vec<AggAcc> = Vec::new();
let mut groups: Vec<Group> = Vec::new();
let mut emit_groups: Vec<(Vec<Value>, Vec<Value>)> = Vec::new();
let mut gcursor: usize = 0;
let mut pc = 0usize;
while pc < program.ops.len() {
let op = &program.ops[pc];
pc += 1;
match op {
Op::Rewind { target } => {
cursor = 0;
if table_rows.is_empty() {
pc = *target;
}
}
Op::Column { col, dest } => {
regs[*dest] = table_rows
.get(cursor)
.and_then(|r| r.get(*col))
.cloned()
.unwrap_or(Value::Null);
}
Op::Next { target } => {
cursor += 1;
if cursor < table_rows.len() {
pc = *target;
}
}
Op::RewindC { cursor: c, target } => {
positions[*c] = 0;
null_flags[*c] = false;
if rowsets.get(*c).is_none_or(|rs| rs.is_empty()) {
pc = *target;
}
}
Op::ColumnC {
cursor: c,
col,
dest,
} => {
regs[*dest] = if null_flags[*c] {
Value::Null
} else {
rowsets
.get(*c)
.and_then(|rs| rs.get(positions[*c]))
.and_then(|r| r.get(*col))
.cloned()
.unwrap_or(Value::Null)
};
}
Op::NullRow { cursor: c } => {
null_flags[*c] = true;
}
Op::MarkMatched { cursor: c } => {
if let Some(row) = matched_rows
.get_mut(*c)
.and_then(|m| m.get_mut(positions[*c]))
{
*row = true;
}
}
Op::IfMatched { cursor: c, target } => {
if matched_rows
.get(*c)
.and_then(|m| m.get(positions[*c]))
.copied()
.unwrap_or(false)
{
pc = *target;
}
}
Op::NextC { cursor: c, target } => {
positions[*c] += 1;
if positions[*c] < rowsets.get(*c).map_or(0, |rs| rs.len()) {
pc = *target;
}
}
Op::DecrJumpZero { reg, target } => {
let n = match ®s[*reg] {
Value::Integer(i) => *i,
other => crate::exec::eval::to_i64(other),
};
regs[*reg] = Value::Integer(n - 1);
if n - 1 <= 0 {
pc = *target;
}
}
Op::IfPosDecr { reg, target } => {
let n = match ®s[*reg] {
Value::Integer(i) => *i,
other => crate::exec::eval::to_i64(other),
};
if n > 0 {
regs[*reg] = Value::Integer(n - 1);
pc = *target;
}
}
Op::Goto { target } => {
pc = *target;
}
Op::IfFalse { reg, target } => {
if crate::exec::eval::truth(®s[*reg]) != Some(true) {
pc = *target;
}
}
Op::Copy { src, dest } => regs[*dest] = regs[*src].clone(),
Op::Cast {
reg,
type_name,
dest,
} => {
regs[*dest] = crate::exec::eval::cast(regs[*reg].clone(), type_name);
}
Op::Integer { value, dest } => regs[*dest] = Value::Integer(*value),
Op::Real { value, dest } => regs[*dest] = Value::Real(*value),
Op::Str { value, dest } => regs[*dest] = Value::Text(value.clone()),
Op::Blob { value, dest } => regs[*dest] = Value::Blob(value.clone()),
Op::Null { dest } => regs[*dest] = Value::Null,
Op::Negate { reg, dest } => {
regs[*dest] = match crate::exec::eval::to_number(®s[*reg]) {
Value::Integer(i) => i
.checked_neg()
.map(Value::Integer)
.unwrap_or(Value::Real(-(i as f64))),
Value::Real(r) => Value::Real(-r),
_ => Value::Null,
};
}
Op::BitNot { reg, dest } => {
regs[*dest] = match ®s[*reg] {
Value::Null => Value::Null,
v => Value::Integer(!crate::exec::eval::to_i64(v)),
};
}
Op::Arith { op, lhs, rhs, dest } => {
regs[*dest] = crate::exec::eval::arithmetic_values(*op, ®s[*lhs], ®s[*rhs]);
}
Op::Bitwise { op, lhs, rhs, dest } => {
regs[*dest] = crate::exec::eval::bitwise_values(*op, ®s[*lhs], ®s[*rhs]);
}
Op::Is {
is,
lhs,
rhs,
dest,
la,
ra,
} => {
let (l, r) = crate::exec::eval::apply_comparison_affinity(
regs[*lhs].clone(),
*la,
regs[*rhs].clone(),
*ra,
);
regs[*dest] = crate::exec::eval::is_values(*is, &l, &r);
}
Op::Truthy {
want,
not,
operand,
dest,
} => {
let t = crate::exec::eval::truth(®s[*operand]) == Some(*want);
regs[*dest] = Value::Integer((t ^ *not) as i64);
}
Op::Like {
glob,
lhs,
rhs,
dest,
} => {
regs[*dest] = crate::exec::eval::like_glob_values(*glob, ®s[*lhs], ®s[*rhs]);
}
Op::Json {
as_text,
lhs,
rhs,
dest,
} => {
regs[*dest] = crate::exec::json::arrow(®s[*lhs], ®s[*rhs], *as_text)?;
}
Op::Func {
name,
arg_start,
arg_count,
dest,
} => {
use crate::sql::ast::{Expr, Literal};
let lit_args: Vec<Expr> = regs[*arg_start..*arg_start + *arg_count]
.iter()
.map(|v| {
Expr::Literal(match v {
Value::Null => Literal::Null,
Value::Integer(i) => Literal::Integer(*i),
Value::Real(r) => Literal::Real(*r),
Value::Text(s) => Literal::Str(s.clone()),
Value::Blob(b) => Literal::Blob(b.clone()),
})
})
.collect();
let params = crate::exec::eval::Params::default();
let ctx = crate::exec::eval::EvalCtx::rowless(¶ms);
regs[*dest] = crate::exec::func::eval_scalar(name, &lit_args, false, &ctx)?;
}
Op::Concat { lhs, rhs, dest } => {
regs[*dest] = crate::exec::eval::concat_values(®s[*lhs], ®s[*rhs]);
}
Op::Compare {
op,
lhs,
rhs,
dest,
la,
ra,
coll,
} => {
let (l, r) = crate::exec::eval::apply_comparison_affinity(
regs[*lhs].clone(),
*la,
regs[*rhs].clone(),
*ra,
);
regs[*dest] = crate::exec::eval::compare_op(*op, &l, &r, *coll);
}
Op::And { lhs, rhs, dest } => {
regs[*dest] = three_valued_and(®s[*lhs], ®s[*rhs]);
}
Op::Or { lhs, rhs, dest } => {
regs[*dest] = three_valued_or(®s[*lhs], ®s[*rhs]);
}
Op::Not { reg, dest } => {
regs[*dest] = match crate::exec::eval::truth(®s[*reg]) {
Some(b) => Value::Integer(!b as i64),
None => Value::Null,
};
}
Op::IsNull { reg, negated, dest } => {
let is_null = matches!(regs[*reg], Value::Null);
regs[*dest] = Value::Integer((is_null != *negated) as i64);
}
Op::ResultRow { start, count } => {
out.push(regs[*start..*start + *count].to_vec());
}
Op::DistinctCheck {
start,
count,
target,
} => {
let row = ®s[*start..*start + *count];
let dup = seen.iter().any(|prev| {
prev.len() == row.len() && prev.iter().zip(row).all(|(a, b)| distinct_eq(a, b))
});
if dup {
pc = *target;
} else {
seen.push(row.to_vec());
}
}
Op::SorterInsert {
row_start,
row_count,
key_start,
key_count,
} => {
let row = regs[*row_start..*row_start + *row_count].to_vec();
let keys = regs[*key_start..*key_start + *key_count].to_vec();
sorter.push((keys, row));
}
Op::SorterSort { keys } => {
sorter.sort_by(|a, b| {
for (i, k) in keys.iter().enumerate() {
let ord = crate::exec::cmp_order(
&a.0[i],
&b.0[i],
k.descending,
k.nulls_first,
k.collation,
);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
});
}
Op::SorterRewind { target } => {
scursor = 0;
if sorter.is_empty() {
pc = *target;
}
}
Op::SorterRow { start, count } => {
if let Some((_, row)) = sorter.get(scursor) {
for (i, v) in row.iter().take(*count).enumerate() {
regs[*start + i] = v.clone();
}
}
}
Op::SorterNext { target } => {
scursor += 1;
if scursor < sorter.len() {
pc = *target;
}
}
Op::AggStep {
slot,
kind,
arg,
distinct,
filter,
order,
sep,
} => {
if *slot >= agg.len() {
agg.resize(*slot + 1, AggAcc::default());
}
if sep.is_some() && agg[*slot].sep.is_none() {
agg[*slot].sep = sep.clone();
}
let pass = filter.is_none_or(|f| crate::exec::eval::truth(®s[f]) == Some(true));
if !pass {
} else if *kind == AggKind::CountStar {
agg[*slot].count += 1;
} else if let Some(r) = arg {
if !matches!(regs[*r], Value::Null) {
let v = regs[*r].clone();
if !*distinct || !agg[*slot].vals.iter().any(|p| distinct_eq(p, &v)) {
agg[*slot].vals.push(v);
push_order_key(&mut agg[*slot], order, ®s);
}
}
}
}
Op::AggFinal { slot, kind, dest } => {
let acc = match agg.get_mut(*slot) {
Some(e) => core::mem::take(e),
None => AggAcc::default(),
};
regs[*dest] = finalize_agg(*kind, acc)?;
}
Op::GroupStep {
key_start,
key_count,
repr_count,
companion,
aggs,
} => {
let total = *key_count + *repr_count;
let gi = match groups.iter().position(|(k, _)| {
k.iter()
.zip(regs[*key_start..].iter())
.take(*key_count)
.all(|(a, b)| distinct_eq(a, b))
}) {
Some(i) => {
if let Some((arg, is_max)) = companion {
let gv = ®s[*arg];
if !matches!(gv, Value::Null) {
let best = &groups[i].0[total];
let beats = matches!(best, Value::Null) || {
let o = crate::exec::eval::compare(gv, best);
if *is_max {
o == core::cmp::Ordering::Greater
} else {
o == core::cmp::Ordering::Less
}
};
if beats {
groups[i].0[total] = gv.clone();
for r in 0..*repr_count {
groups[i].0[*key_count + r] =
regs[*key_start + *key_count + r].clone();
}
}
}
}
i
}
None => {
let mut key = regs[*key_start..*key_start + total].to_vec();
if let Some((arg, _)) = companion {
key.push(regs[*arg].clone());
}
groups.push((key, alloc::vec![AggAcc::default(); aggs.len()]));
groups.len() - 1
}
};
for (j, spec) in aggs.iter().enumerate() {
if spec.sep.is_some() && groups[gi].1[j].sep.is_none() {
groups[gi].1[j].sep = spec.sep.clone();
}
let pass = spec
.filter
.is_none_or(|f| crate::exec::eval::truth(®s[f]) == Some(true));
if !pass {
continue;
}
if spec.kind == AggKind::CountStar {
groups[gi].1[j].count += 1;
} else if let Some(r) = spec.arg {
if !matches!(regs[r], Value::Null) {
let v = regs[r].clone();
if !spec.distinct
|| !groups[gi].1[j].vals.iter().any(|p| distinct_eq(p, &v))
{
groups[gi].1[j].vals.push(v);
push_order_key(&mut groups[gi].1[j], &spec.order, ®s);
}
}
}
}
}
Op::GroupEmit {
outputs,
key_count,
agg_kinds,
} => {
sort_groups_by_key(&mut groups, *key_count);
for (key, accs) in groups.drain(..) {
let finals: Vec<Value> = agg_kinds
.iter()
.zip(accs)
.map(|(k, acc)| finalize_agg(*k, acc))
.collect::<Result<_>>()?;
let row: Vec<Value> = outputs
.iter()
.map(|o| match o {
GroupOut::Key(i) => key[*i].clone(),
GroupOut::Agg(j) => finals[*j].clone(),
})
.collect();
out.push(row);
}
}
Op::GroupFinalize {
agg_kinds,
key_count,
target,
} => {
sort_groups_by_key(&mut groups, *key_count);
emit_groups.clear();
for (key, accs) in groups.drain(..) {
let finals: Vec<Value> = agg_kinds
.iter()
.zip(accs)
.map(|(k, acc)| finalize_agg(*k, acc))
.collect::<Result<_>>()?;
emit_groups.push((key, finals));
}
gcursor = 0;
if emit_groups.is_empty() {
pc = *target;
}
}
Op::GroupKey { key, dest } => {
regs[*dest] = emit_groups
.get(gcursor)
.and_then(|(k, _)| k.get(*key))
.cloned()
.unwrap_or(Value::Null);
}
Op::GroupAgg { slot, dest } => {
regs[*dest] = emit_groups
.get(gcursor)
.and_then(|(_, a)| a.get(*slot))
.cloned()
.unwrap_or(Value::Null);
}
Op::GroupNext { target } => {
gcursor += 1;
if gcursor < emit_groups.len() {
pc = *target;
}
}
Op::Halt => break,
}
}
Ok(out)
}
fn sort_groups_by_key(groups: &mut [(Vec<Value>, Vec<AggAcc>)], key_count: usize) {
groups.sort_by(|a, b| {
for k in 0..key_count {
let ord = crate::value::cmp_values(&a.0[k], &b.0[k]);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
});
}
fn push_order_key(acc: &mut AggAcc, order: &[AggOrderKey], regs: &[Value]) {
if order.is_empty() {
return;
}
if acc.dirs.is_empty() {
acc.dirs = order
.iter()
.map(|k| (k.descending, k.nulls_first, k.collation))
.collect();
}
acc.keys
.push(order.iter().map(|k| regs[k.reg].clone()).collect());
}
fn cmp_key_rows(
a: &[Value],
b: &[Value],
dirs: &[(bool, Option<bool>, Collation)],
) -> core::cmp::Ordering {
use core::cmp::Ordering;
for (k, &(descending, nulls_first, collation)) in dirs.iter().enumerate() {
let (x, y) = (&a[k], &b[k]);
let nulls_first = nulls_first.unwrap_or(!descending);
let ord = match (x, y) {
(Value::Null, Value::Null) => Ordering::Equal,
(Value::Null, _) => {
return if nulls_first {
Ordering::Less
} else {
Ordering::Greater
};
}
(_, Value::Null) => {
return if nulls_first {
Ordering::Greater
} else {
Ordering::Less
};
}
_ => {
let base = crate::value::cmp_values_coll(x, y, collation);
if descending {
base.reverse()
} else {
base
}
}
};
if ord != Ordering::Equal {
return ord;
}
}
Ordering::Equal
}
fn finalize_agg(kind: AggKind, acc: AggAcc) -> Result<Value> {
use crate::exec::eval;
use core::cmp::Ordering;
let AggAcc {
vals,
count: star,
keys,
dirs,
sep,
} = acc;
let sep = sep.as_deref().unwrap_or(",");
Ok(match kind {
AggKind::CountStar => Value::Integer(star),
AggKind::Count => Value::Integer(vals.len() as i64),
AggKind::Sum => eval::sum_values(&vals)?,
AggKind::Total => Value::Real(eval::total_value(&vals)),
AggKind::Avg => match eval::avg_value(&vals) {
Some(r) => Value::Real(r),
None => Value::Null,
},
AggKind::Min => vals
.into_iter()
.reduce(|a, b| {
if eval::compare(&b, &a) == Ordering::Less {
b
} else {
a
}
})
.unwrap_or(Value::Null),
AggKind::Max => vals
.into_iter()
.reduce(|a, b| {
if eval::compare(&b, &a) == Ordering::Greater {
b
} else {
a
}
})
.unwrap_or(Value::Null),
AggKind::GroupConcat => {
if vals.is_empty() {
Value::Null
} else if keys.is_empty() {
let parts: Vec<String> = vals.iter().map(eval::to_text).collect();
Value::Text(parts.join(sep))
} else {
let mut idx: Vec<usize> = (0..vals.len()).collect();
idx.sort_by(|&i, &j| cmp_key_rows(&keys[i], &keys[j], &dirs));
let parts: Vec<String> = idx.iter().map(|&i| eval::to_text(&vals[i])).collect();
Value::Text(parts.join(sep))
}
}
})
}
fn distinct_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Null, Value::Null) => true,
(Value::Null, _) | (_, Value::Null) => false,
_ => {
crate::value::cmp_values_coll(a, b, crate::value::Collation::Binary)
== core::cmp::Ordering::Equal
}
}
}
fn three_valued_and(a: &Value, b: &Value) -> Value {
use crate::exec::eval::truth;
match (truth(a), truth(b)) {
(Some(false), _) | (_, Some(false)) => Value::Integer(0),
(Some(true), Some(true)) => Value::Integer(1),
_ => Value::Null,
}
}
fn three_valued_or(a: &Value, b: &Value) -> Value {
use crate::exec::eval::truth;
match (truth(a), truth(b)) {
(Some(true), _) | (_, Some(true)) => Value::Integer(1),
(Some(false), Some(false)) => Value::Integer(0),
_ => Value::Null,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sql::ast::Statement;
use crate::sql::parse_one;
use alloc::vec;
fn run_sql(sql: &str) -> Vec<Vec<Value>> {
let Statement::Select(sel) = parse_one(sql).unwrap() else {
panic!("not a select")
};
let prog = compile_const_select(&sel).unwrap();
run(&prog).unwrap()
}
#[test]
fn arithmetic_and_concat() {
assert_eq!(run_sql("SELECT 1 + 2 * 3"), vec![vec![Value::Integer(7)]]);
assert_eq!(
run_sql("SELECT 10 - 4, 8 / 2"),
vec![vec![Value::Integer(6), Value::Integer(4)]]
);
assert_eq!(
run_sql("SELECT 'a' || 'b' || 'c'"),
vec![vec![Value::Text("abc".into())]]
);
assert_eq!(
run_sql("SELECT -5, 3.5"),
vec![vec![Value::Integer(-5), Value::Real(3.5)]]
);
}
#[test]
fn rejects_unsupported() {
let Statement::Select(sel) = parse_one("SELECT * FROM t").unwrap() else {
panic!()
};
assert!(compile_const_select(&sel).is_err());
}
#[test]
fn nested_loop_join_two_cursors() {
let Statement::Select(sel) =
parse_one("SELECT a.x, b.q FROM a, b WHERE a.x = b.p").unwrap()
else {
panic!()
};
let columns = vec![
"x".to_string(),
"y".to_string(),
"p".to_string(),
"q".to_string(),
];
let tables = vec![
"a".to_string(),
"a".to_string(),
"b".to_string(),
"b".to_string(),
];
let aff = vec![Affinity::Blob; 4];
let coll = vec![Collation::Binary; 4];
let prog = compile_join2(&sel, &columns, &tables, &aff, &coll, &[2, 4]).unwrap();
let left: Vec<Vec<Value>> = vec![
vec![Value::Integer(1), Value::Text("a".into())],
vec![Value::Integer(2), Value::Text("b".into())],
];
let right: Vec<Vec<Value>> = vec![
vec![Value::Integer(1), Value::Text("P".into())],
vec![Value::Integer(2), Value::Text("Q".into())],
vec![Value::Integer(2), Value::Text("R".into())],
];
assert_eq!(
run_rows_multi(&prog, &[&left, &right]).unwrap(),
vec![
vec![Value::Integer(1), Value::Text("P".into())],
vec![Value::Integer(2), Value::Text("Q".into())],
vec![Value::Integer(2), Value::Text("R".into())],
]
);
assert_eq!(prog.columns.len(), 2);
assert!(run_rows_multi(&prog, &[&left, &[]]).unwrap().is_empty());
assert!(run_rows_multi(&prog, &[&[], &right]).unwrap().is_empty());
}
#[test]
fn left_join_null_pads_unmatched_rows() {
let Statement::Select(sel) =
parse_one("SELECT a.x, b.q FROM a LEFT JOIN b ON a.x = b.p").unwrap()
else {
panic!()
};
let on = sel.from.as_ref().unwrap().joins[0].on.clone();
let columns = vec![
"x".to_string(),
"y".to_string(),
"p".to_string(),
"q".to_string(),
];
let tables = vec![
"a".to_string(),
"a".to_string(),
"b".to_string(),
"b".to_string(),
];
let aff = vec![Affinity::Blob; 4];
let coll = vec![Collation::Binary; 4];
let prog = compile_left_join2(&sel, &columns, &tables, &aff, &coll, 2, &on).unwrap();
let left: Vec<Vec<Value>> = vec![
vec![Value::Integer(1), Value::Text("a".into())],
vec![Value::Integer(2), Value::Text("b".into())],
vec![Value::Integer(3), Value::Text("c".into())],
];
let right: Vec<Vec<Value>> = vec![
vec![Value::Integer(1), Value::Text("P".into())],
vec![Value::Integer(2), Value::Text("Q".into())],
];
assert_eq!(
run_rows_multi(&prog, &[&left, &right]).unwrap(),
vec![
vec![Value::Integer(1), Value::Text("P".into())],
vec![Value::Integer(2), Value::Text("Q".into())],
vec![Value::Integer(3), Value::Null],
]
);
assert_eq!(
run_rows_multi(&prog, &[&left, &[]]).unwrap(),
vec![
vec![Value::Integer(1), Value::Null],
vec![Value::Integer(2), Value::Null],
vec![Value::Integer(3), Value::Null],
]
);
}
#[test]
fn full_join_two_pass_null_pads_both_sides() {
let Statement::Select(sel) =
parse_one("SELECT a.x, b.p FROM a FULL JOIN b ON a.x = b.p").unwrap()
else {
panic!()
};
let on = sel.from.as_ref().unwrap().joins[0].on.clone();
let columns = vec!["x".to_string(), "p".to_string()];
let tables = vec!["a".to_string(), "b".to_string()];
let aff = vec![Affinity::Blob; 2];
let coll = vec![Collation::Binary; 2];
let prog = compile_full_join2(&sel, &columns, &tables, &aff, &coll, 1, &on).unwrap();
let left: Vec<Vec<Value>> = vec![
vec![Value::Integer(1)],
vec![Value::Integer(2)],
vec![Value::Integer(3)],
];
let right: Vec<Vec<Value>> = vec![
vec![Value::Integer(2)],
vec![Value::Integer(3)],
vec![Value::Integer(4)],
];
assert_eq!(
run_rows_multi(&prog, &[&left, &right]).unwrap(),
vec![
vec![Value::Integer(1), Value::Null],
vec![Value::Integer(2), Value::Integer(2)],
vec![Value::Integer(3), Value::Integer(3)],
vec![Value::Null, Value::Integer(4)],
]
);
assert_eq!(
run_rows_multi(&prog, &[&[], &right]).unwrap(),
vec![
vec![Value::Null, Value::Integer(2)],
vec![Value::Null, Value::Integer(3)],
vec![Value::Null, Value::Integer(4)],
]
);
}
#[test]
fn nested_loop_join_bails_on_aggregate_and_order_by() {
let cols = vec!["x".to_string(), "p".to_string()];
let tabs = vec!["a".to_string(), "b".to_string()];
let aff = vec![Affinity::Blob; 2];
let coll = vec![Collation::Binary; 2];
for sql in [
"SELECT count(*) FROM a, b",
"SELECT a.x FROM a, b GROUP BY a.x",
"SELECT a.x, count(*) FROM a, b GROUP BY a.x HAVING count(*) > 1",
] {
let Statement::Select(sel) = parse_one(sql).unwrap() else {
panic!()
};
assert!(
compile_join2(&sel, &cols, &tabs, &aff, &coll, &[1, 2]).is_err(),
"{sql} should bail to the cross-product path"
);
}
for sql in [
"SELECT DISTINCT a.x FROM a, b",
"SELECT a.x FROM a, b ORDER BY a.x",
"SELECT DISTINCT a.x FROM a, b ORDER BY a.x DESC LIMIT 2",
] {
let Statement::Select(sel) = parse_one(sql).unwrap() else {
panic!()
};
assert!(
compile_join2(&sel, &cols, &tabs, &aff, &coll, &[1, 2]).is_ok(),
"{sql} should compile on the VDBE"
);
}
}
#[test]
fn bare_aggregate_join_compiles_and_bails_correctly() {
let cols = vec!["x".to_string(), "p".to_string()];
let tabs = vec!["a".to_string(), "b".to_string()];
let aff = vec![Affinity::Blob; 2];
let coll = vec![Collation::Binary; 2];
for sql in [
"SELECT count(*) FROM a, b",
"SELECT sum(a.x), max(b.p) FROM a, b",
"SELECT group_concat(b.p) FROM a, b WHERE a.x = b.p",
"SELECT count(DISTINCT a.x) FROM a, b",
"SELECT sum(DISTINCT b.p) FROM a, b WHERE a.x = b.p",
] {
let Statement::Select(sel) = parse_one(sql).unwrap() else {
panic!()
};
assert!(
compile_aggregate_join(&sel, &cols, &tabs, &aff, &coll, &[1, 2]).is_ok(),
"{sql} should compile as an aggregate join"
);
}
for sql in [
"SELECT a.x FROM a, b", "SELECT count(*) FROM a, b GROUP BY a.x", "SELECT count(*) FROM a, b ORDER BY 1", "SELECT count(*) FROM a, b LIMIT 1", ] {
let Statement::Select(sel) = parse_one(sql).unwrap() else {
panic!()
};
assert!(
compile_aggregate_join(&sel, &cols, &tabs, &aff, &coll, &[1, 2]).is_err(),
"{sql} should bail from the aggregate-join path"
);
}
}
#[test]
fn group_by_join_compiles_and_bails_correctly() {
let cols = vec!["x".to_string(), "p".to_string()];
let tabs = vec!["a".to_string(), "b".to_string()];
let aff = vec![Affinity::Blob; 2];
let coll = vec![Collation::Binary; 2];
for sql in [
"SELECT a.x, count(*) FROM a, b GROUP BY a.x",
"SELECT x, sum(p) FROM a, b GROUP BY x",
"SELECT a.x FROM a, b GROUP BY a.x",
"SELECT a.x, b.p FROM a, b GROUP BY a.x",
"SELECT a.x, count(*) FROM a, b GROUP BY a.x HAVING count(*) > 1",
"SELECT a.x, count(*) FROM a, b GROUP BY a.x ORDER BY a.x",
"SELECT a.x, count(*) FROM a, b GROUP BY a.x LIMIT 1",
"SELECT a.x, count(*) AS n FROM a, b GROUP BY a.x ORDER BY n DESC LIMIT 2 OFFSET 1",
"SELECT a.x, b.p, max(b.p) FROM a, b GROUP BY a.x",
"SELECT DISTINCT a.x, count(*) FROM a, b GROUP BY a.x",
] {
let Statement::Select(sel) = parse_one(sql).unwrap() else {
panic!()
};
assert!(
compile_group_join(&sel, &cols, &tabs, &aff, &coll, &[1, 2]).is_ok(),
"{sql} should compile as a GROUP BY join"
);
}
for sql in [
"SELECT a.x, b.p, max(b.p), min(b.p) FROM a, b GROUP BY a.x", "SELECT count(*) FROM a, b", ] {
let Statement::Select(sel) = parse_one(sql).unwrap() else {
panic!()
};
assert!(
compile_group_join(&sel, &cols, &tabs, &aff, &coll, &[1, 2]).is_err(),
"{sql} should bail from the GROUP BY join path"
);
}
}
}