pub mod datetime;
pub mod eval;
pub mod func;
pub mod json;
pub mod vdbe;
mod window;
use crate::btree::{
clear_index, clear_table, create_index_root, create_table_root, delete_table, free_tree,
insert_index, insert_table, table_has_empty_leaf, IndexCursor, TableCursor,
};
use crate::error::{Error, Result};
use crate::format::record::{decode_record, encode_record};
use crate::pager::{AutoVacuum, PageSource, WritePager};
use crate::schema::Schema;
use crate::sql::ast::*;
use crate::sql::{self};
use crate::value::Value;
use crate::vfs::{OpenFlags, Vfs};
use crate::vtab::{
ConstraintOp, DynVTabModule, IndexConstraint, IndexPlan, VTabChange, VTabRegistry, VTabStore,
};
use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use eval::{ColumnInfo, EvalCtx, Params};
#[derive(Debug, Clone, PartialEq)]
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<Vec<Value>>,
}
enum Backend {
Write(Box<WritePager>),
Read(Box<dyn PageSource>),
}
impl Backend {
fn source(&self) -> &dyn PageSource {
match self {
Backend::Write(w) => w.as_ref(),
Backend::Read(r) => r.as_ref(),
}
}
fn writer(&mut self) -> Result<&mut WritePager> {
match self {
Backend::Write(w) => Ok(w),
Backend::Read(_) => Err(Error::Error("database is read-only".into())),
}
}
fn wal_mode(&self) -> bool {
matches!(self, Backend::Write(w) if w.wal_mode())
}
}
pub struct Connection {
backend: Backend,
schema: Schema,
main_file: String,
attached: Vec<AttachedDb>,
temp_db: Option<AttachedDb>,
in_tx: bool,
cte_env: core::cell::RefCell<Vec<CteBinding>>,
outer_scope: core::cell::RefCell<Vec<OuterFrame>>,
foreign_keys: bool,
trigger_depth: core::cell::Cell<usize>,
stmt_keep_partial: core::cell::Cell<bool>,
stmt_rollback_tx: core::cell::Cell<bool>,
raise_ignore: core::cell::Cell<bool>,
recursive_triggers: bool,
returning_rows: core::cell::RefCell<Vec<Vec<Value>>>,
open_savepoints: usize,
last_insert_rowid: core::cell::Cell<i64>,
changes: core::cell::Cell<i64>,
total_changes: core::cell::Cell<i64>,
read_default: core::cell::Cell<DbRef>,
vtab_registry: VTabRegistry,
rng_state: core::cell::Cell<u64>,
cache_size: core::cell::Cell<i64>,
analysis_limit: core::cell::Cell<i64>,
busy_timeout: core::cell::Cell<i64>,
secure_delete: core::cell::Cell<i64>,
functions: alloc::collections::BTreeMap<String, ScalarFunction>,
aggregates: alloc::collections::BTreeMap<String, AggregateFactory>,
#[cfg(feature = "fts5")]
fts5_rank: core::cell::RefCell<Option<Fts5QueryCtx>>,
use_vdbe: core::cell::Cell<bool>,
}
pub type ScalarFunction = Box<dyn Fn(&[Value]) -> Result<Value>>;
pub trait AggregateFunction {
fn step(&mut self, args: &[Value]) -> Result<()>;
fn finalize(&mut self) -> Result<Value>;
}
pub type AggregateFactory = Box<dyn Fn() -> Box<dyn AggregateFunction>>;
fn initial_rng_seed() -> u64 {
#[cfg(feature = "std")]
{
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
nanos ^ 0x9E37_79B9_7F4A_7C15
}
#[cfg(not(feature = "std"))]
{
0x9E37_79B9_7F4A_7C15
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DbRef {
Main,
Temp,
Attached(usize),
}
struct AttachedDb {
name: String,
file: String,
backend: Backend,
schema: Schema,
}
struct CteBinding {
name: String,
columns: Vec<ColumnInfo>,
rows: Vec<InputRow>,
}
struct OuterFrame {
columns: Vec<ColumnInfo>,
row: Vec<Value>,
rowid: Option<i64>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TrigEvent {
Insert,
Update,
Delete,
}
impl Connection {
fn from_pager(db: WritePager) -> Result<Connection> {
let backend = Backend::Write(Box::new(db));
let schema = Schema::read(backend.source())?;
Ok(Connection {
backend,
schema,
main_file: String::new(),
attached: Vec::new(),
temp_db: None,
in_tx: false,
cte_env: core::cell::RefCell::new(Vec::new()),
outer_scope: core::cell::RefCell::new(Vec::new()),
foreign_keys: false,
trigger_depth: core::cell::Cell::new(0),
stmt_keep_partial: core::cell::Cell::new(false),
stmt_rollback_tx: core::cell::Cell::new(false),
raise_ignore: core::cell::Cell::new(false),
recursive_triggers: false,
returning_rows: core::cell::RefCell::new(Vec::new()),
open_savepoints: 0,
last_insert_rowid: core::cell::Cell::new(0),
changes: core::cell::Cell::new(0),
total_changes: core::cell::Cell::new(0),
read_default: core::cell::Cell::new(DbRef::Main),
vtab_registry: VTabRegistry::with_builtins(),
rng_state: core::cell::Cell::new(initial_rng_seed()),
cache_size: core::cell::Cell::new(-2000),
analysis_limit: core::cell::Cell::new(0),
busy_timeout: core::cell::Cell::new(0),
secure_delete: core::cell::Cell::new(0),
functions: alloc::collections::BTreeMap::new(),
aggregates: alloc::collections::BTreeMap::new(),
#[cfg(feature = "fts5")]
fts5_rank: core::cell::RefCell::new(None),
use_vdbe: core::cell::Cell::new(true),
})
}
fn from_read_backend(backend: Box<dyn PageSource>) -> Result<Connection> {
let backend = Backend::Read(backend);
let schema = Schema::read(backend.source())?;
Ok(Connection {
backend,
schema,
main_file: String::new(),
attached: Vec::new(),
temp_db: None,
in_tx: false,
cte_env: core::cell::RefCell::new(Vec::new()),
outer_scope: core::cell::RefCell::new(Vec::new()),
foreign_keys: false,
trigger_depth: core::cell::Cell::new(0),
stmt_keep_partial: core::cell::Cell::new(false),
stmt_rollback_tx: core::cell::Cell::new(false),
raise_ignore: core::cell::Cell::new(false),
recursive_triggers: false,
returning_rows: core::cell::RefCell::new(Vec::new()),
open_savepoints: 0,
last_insert_rowid: core::cell::Cell::new(0),
changes: core::cell::Cell::new(0),
total_changes: core::cell::Cell::new(0),
read_default: core::cell::Cell::new(DbRef::Main),
vtab_registry: VTabRegistry::with_builtins(),
rng_state: core::cell::Cell::new(initial_rng_seed()),
cache_size: core::cell::Cell::new(-2000),
analysis_limit: core::cell::Cell::new(0),
busy_timeout: core::cell::Cell::new(0),
secure_delete: core::cell::Cell::new(0),
functions: alloc::collections::BTreeMap::new(),
aggregates: alloc::collections::BTreeMap::new(),
#[cfg(feature = "fts5")]
fts5_rank: core::cell::RefCell::new(None),
use_vdbe: core::cell::Cell::new(true),
})
}
pub fn open_vfs(vfs: &dyn Vfs, path: &str) -> Result<Connection> {
let main = vfs.open(path, OpenFlags::READ_WRITE)?;
let journal = vfs.open(&journal_path(path), OpenFlags::READ_WRITE_CREATE)?;
let wal = vfs.open(&wal_path(path), OpenFlags::READ_WRITE_CREATE)?;
let mut c = Connection::from_pager(WritePager::open_wal(main, Some(journal), Some(wal))?)?;
c.main_file = path.to_string();
Ok(c)
}
pub fn open_readonly_vfs(vfs: &dyn Vfs, path: &str) -> Result<Connection> {
let main = vfs.open(path, OpenFlags::READ_ONLY)?;
let wal_path = wal_path(path);
if vfs.exists(&wal_path)? {
let mut wal = vfs.open(&wal_path, OpenFlags::READ_ONLY)?;
let reader = crate::pager::WalReader::open(main, wal.as_mut())?;
let mut c = Connection::from_read_backend(Box::new(reader))?;
c.main_file = path.to_string();
return Ok(c);
}
let mut c = Connection::from_read_backend(Box::new(WritePager::open(main, None)?))?;
c.main_file = path.to_string();
Ok(c)
}
pub fn create_vfs(vfs: &dyn Vfs, path: &str, page_size: u32) -> Result<Connection> {
let main = vfs.open(path, OpenFlags::READ_WRITE_CREATE)?;
let journal = vfs.open(&journal_path(path), OpenFlags::READ_WRITE_CREATE)?;
let wal = vfs.open(&wal_path(path), OpenFlags::READ_WRITE_CREATE)?;
let mut db = WritePager::create_wal(main, Some(journal), Some(wal), page_size)?;
db.commit()?;
let mut c = Connection::from_pager(db)?;
c.main_file = path.to_string();
Ok(c)
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn open(path: &str) -> Result<Connection> {
Connection::open_vfs(&crate::vfs::std_file::StdVfs::new(), path)
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn open_readonly(path: &str) -> Result<Connection> {
Connection::open_readonly_vfs(&crate::vfs::std_file::StdVfs::new(), path)
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn create(path: &str) -> Result<Connection> {
Connection::create_vfs(&crate::vfs::std_file::StdVfs::new(), path, 4096)
}
pub fn open_memory() -> Result<Connection> {
let vfs = crate::vfs::memory::MemoryVfs::new();
let main = vfs.open("main", OpenFlags::READ_WRITE_CREATE)?;
let mut db = WritePager::create(main, None, 4096)?;
db.commit()?;
Connection::from_pager(db)
}
pub fn schema(&self) -> &Schema {
&self.schema
}
pub fn query(&self, sql: &str) -> Result<QueryResult> {
self.query_params(sql, &Params::default())
}
pub fn query_vdbe(&self, sql: &str) -> Result<QueryResult> {
let Statement::Select(sel) = sql::parse_one(sql)? else {
return Err(Error::Unsupported("query_vdbe expects SELECT"));
};
self.run_select_vdbe(&sel)
}
pub fn set_use_vdbe(&self, on: bool) {
self.use_vdbe.set(on);
}
fn compile_select_program(&self, sel: &Select) -> Result<vdbe::Program> {
let Some(from) = &sel.from else {
return vdbe::compile_const_select(sel);
};
if !from.joins.is_empty() {
return Err(Error::Unsupported(
"EXPLAIN: VDBE join programs not yet listed",
));
}
if from.first.subquery.is_some() || from.first.tvf_args.is_some() {
return Err(Error::Unsupported("EXPLAIN: only plain table sources"));
}
let meta = self.table_meta(&from.first.name, from.first.alias.as_deref())?;
let cols: Vec<String> = meta.columns.iter().map(|c| c.name.clone()).collect();
let qualifier = from
.first
.alias
.clone()
.unwrap_or_else(|| from.first.name.clone());
let tables: Vec<String> = meta.columns.iter().map(|_| qualifier.clone()).collect();
let affinities: Vec<eval::Affinity> = meta.columns.iter().map(|c| c.affinity).collect();
let collations: Vec<crate::value::Collation> =
meta.columns.iter().map(|c| c.collation).collect();
vdbe::compile_table_select(
sel,
&cols,
&tables,
&affinities,
&collations,
!meta.without_rowid,
)
}
fn explain_bytecode(&self, stmt: &Statement) -> Result<QueryResult> {
let Statement::Select(sel) = stmt else {
return Err(Error::Unsupported(
"EXPLAIN: only SELECT is compiled to bytecode",
));
};
let prog = self.compile_select_program(sel)?;
let rows = prog
.explain_rows()
.into_iter()
.map(|(addr, opcode, detail)| {
alloc::vec![
Value::Integer(addr as i64),
Value::Text(opcode),
Value::Text(detail),
]
})
.collect();
Ok(QueryResult {
columns: alloc::vec!["addr".into(), "opcode".into(), "detail".into()],
rows,
})
}
fn fold_vdbe_subqueries(&self, sel: &Select) -> Option<Select> {
let mut changed = false;
let mut out = sel.clone();
for rc in &mut out.columns {
if let sql::ast::ResultColumn::Expr { expr, .. } = rc {
*expr = self.fold_subquery_expr(expr, &mut changed);
}
}
if let Some(w) = out.where_clause.take() {
out.where_clause = Some(self.fold_subquery_expr(&w, &mut changed));
}
if let Some(h) = out.having.take() {
out.having = Some(self.fold_subquery_expr(&h, &mut changed));
}
for g in &mut out.group_by {
*g = self.fold_subquery_expr(g, &mut changed);
}
for o in &mut out.order_by {
o.expr = self.fold_subquery_expr(&o.expr, &mut changed);
}
if let Some(l) = out.limit.take() {
out.limit = Some(self.fold_subquery_expr(&l, &mut changed));
}
if let Some(o) = out.offset.take() {
out.offset = Some(self.fold_subquery_expr(&o, &mut changed));
}
if let Some(from) = &mut out.from {
for j in &mut from.joins {
if let Some(on) = j.on.take() {
j.on = Some(self.fold_subquery_expr(&on, &mut changed));
}
}
}
if changed {
Some(out)
} else {
None
}
}
fn fold_subquery_expr(&self, e: &Expr, changed: &mut bool) -> Expr {
use sql::ast::Expr as E;
match e {
E::Subquery(sel2) => match self.eval_foldable_scalar(sel2) {
Some(v) => {
*changed = true;
E::Literal(value_to_literal(v))
}
None => e.clone(),
},
E::Exists { select, negated } => match self.eval_foldable_exists(select) {
Some(found) => {
*changed = true;
E::Literal(Literal::Integer((found ^ *negated) as i64))
}
None => e.clone(),
},
E::Unary { op, expr } => E::Unary {
op: *op,
expr: alloc::boxed::Box::new(self.fold_subquery_expr(expr, changed)),
},
E::Binary { op, left, right } => E::Binary {
op: *op,
left: alloc::boxed::Box::new(self.fold_subquery_expr(left, changed)),
right: alloc::boxed::Box::new(self.fold_subquery_expr(right, changed)),
},
E::IsNull { expr, negated } => E::IsNull {
expr: alloc::boxed::Box::new(self.fold_subquery_expr(expr, changed)),
negated: *negated,
},
E::InList {
expr,
list,
negated,
candidate_affinity,
} => E::InList {
expr: alloc::boxed::Box::new(self.fold_subquery_expr(expr, changed)),
list: list
.iter()
.map(|x| self.fold_subquery_expr(x, changed))
.collect(),
negated: *negated,
candidate_affinity: candidate_affinity.clone(),
},
E::Between {
expr,
low,
high,
negated,
} => E::Between {
expr: alloc::boxed::Box::new(self.fold_subquery_expr(expr, changed)),
low: alloc::boxed::Box::new(self.fold_subquery_expr(low, changed)),
high: alloc::boxed::Box::new(self.fold_subquery_expr(high, changed)),
negated: *negated,
},
E::Case {
operand,
when_then,
else_result,
} => E::Case {
operand: operand
.as_ref()
.map(|o| alloc::boxed::Box::new(self.fold_subquery_expr(o, changed))),
when_then: when_then
.iter()
.map(|(w, t)| {
(
self.fold_subquery_expr(w, changed),
self.fold_subquery_expr(t, changed),
)
})
.collect(),
else_result: else_result
.as_ref()
.map(|x| alloc::boxed::Box::new(self.fold_subquery_expr(x, changed))),
},
E::Cast { expr, type_name } => E::Cast {
expr: alloc::boxed::Box::new(self.fold_subquery_expr(expr, changed)),
type_name: type_name.clone(),
},
E::Paren(inner) => E::Paren(alloc::boxed::Box::new(
self.fold_subquery_expr(inner, changed),
)),
E::Collate { expr, collation } => E::Collate {
expr: alloc::boxed::Box::new(self.fold_subquery_expr(expr, changed)),
collation: collation.clone(),
},
E::RowValue(items) => E::RowValue(
items
.iter()
.map(|x| self.fold_subquery_expr(x, changed))
.collect(),
),
E::Function {
name,
distinct,
args,
star,
filter,
order_by,
over,
} if over.is_none() => E::Function {
name: name.clone(),
distinct: *distinct,
args: args
.iter()
.map(|a| self.fold_subquery_expr(a, changed))
.collect(),
star: *star,
filter: filter
.as_ref()
.map(|f| alloc::boxed::Box::new(self.fold_subquery_expr(f, changed))),
order_by: order_by.clone(),
over: None,
},
E::InSelect {
expr,
select,
negated,
} => match self.eval_foldable_in_select(select) {
Some((values, candidate_affinity)) => {
*changed = true;
E::InList {
expr: alloc::boxed::Box::new(self.fold_subquery_expr(expr, changed)),
list: values
.into_iter()
.map(|v| E::Literal(value_to_literal(v)))
.collect(),
negated: *negated,
candidate_affinity,
}
}
None => e.clone(),
},
_ => e.clone(),
}
}
fn eval_foldable_scalar(&self, sel2: &Select) -> Option<Value> {
if !self.vdbe_subquery_foldable(sel2) {
return None;
}
if sel2.columns.len() != 1 {
return None;
}
let sql::ast::ResultColumn::Expr { expr, .. } = &sel2.columns[0] else {
return None;
};
if is_bare_column_expr(expr) {
return None;
}
let r = self.run_select(sel2, &Params::default()).ok()?;
Some(
r.rows
.first()
.and_then(|row| row.first())
.cloned()
.unwrap_or(Value::Null),
)
}
fn eval_foldable_in_select(&self, sel2: &Select) -> Option<(Vec<Value>, Option<String>)> {
if !self.vdbe_subquery_foldable(sel2) {
return None;
}
if sel2.columns.len() != 1 {
return None;
}
let sql::ast::ResultColumn::Expr { expr, .. } = &sel2.columns[0] else {
return None;
};
let candidate_affinity = if is_bare_column_expr(expr) {
let origins = self.subquery_column_origins(sel2)?;
let (aff, _coll) = origins.first().copied()?;
Some(affinity_type_name(aff))
} else {
None
};
let r = self.run_select(sel2, &Params::default()).ok()?;
Some((
r.rows
.into_iter()
.map(|row| row.into_iter().next().unwrap_or(Value::Null))
.collect(),
candidate_affinity,
))
}
fn eval_foldable_exists(&self, sel2: &Select) -> Option<bool> {
if !self.vdbe_subquery_foldable(sel2) {
return None;
}
let r = self.run_select(sel2, &Params::default()).ok()?;
Some(!r.rows.is_empty())
}
fn vdbe_subquery_foldable(&self, sel2: &Select) -> bool {
if !sel2.compound.is_empty() || !sel2.ctes.is_empty() {
return false;
}
let Some(from) = &sel2.from else {
return self.expr_positions_internal(sel2, &[], &[]);
};
let mut quals: Vec<String> = Vec::new();
let mut cols: Vec<String> = Vec::new();
let mut collect = |tr: &sql::ast::TableRef| -> bool {
if tr.subquery.is_some()
|| tr.tvf_args.is_some()
|| tr.schema.is_some()
|| tr.name.is_empty()
{
return false;
}
let Ok(meta) = self.table_meta(&tr.name, None) else {
return false;
};
quals.push(tr.name.clone());
if let Some(a) = &tr.alias {
quals.push(a.clone());
}
for c in &meta.columns {
cols.push(c.name.clone());
}
true
};
if !collect(&from.first) {
return false;
}
for j in &from.joins {
if !collect(&j.table) {
return false;
}
}
self.expr_positions_internal(sel2, &quals, &cols)
}
fn expr_positions_internal(&self, sel2: &Select, quals: &[String], cols: &[String]) -> bool {
let ok = |e: &Expr| expr_is_internal(e, quals, cols);
for rc in &sel2.columns {
if let sql::ast::ResultColumn::Expr { expr, .. } = rc {
if !ok(expr) {
return false;
}
}
}
if let Some(w) = &sel2.where_clause {
if !ok(w) {
return false;
}
}
if let Some(h) = &sel2.having {
if !ok(h) {
return false;
}
}
if !sel2.group_by.iter().all(&ok) {
return false;
}
if !sel2.order_by.iter().all(|t| ok(&t.expr)) {
return false;
}
if let Some(from) = &sel2.from {
for j in &from.joins {
if let Some(on) = &j.on {
if !ok(on) {
return false;
}
}
}
}
if let Some(l) = &sel2.limit {
if !ok(l) {
return false;
}
}
if let Some(o) = &sel2.offset {
if !ok(o) {
return false;
}
}
true
}
fn run_select_vdbe(&self, sel: &Select) -> Result<QueryResult> {
if self.temp_db.is_some()
|| !self.attached.is_empty()
|| self.read_default.get() != DbRef::Main
{
return Err(Error::Unsupported("VDBE: non-main schema in scope"));
}
if let Some(f) = &sel.from {
if f.first.schema.is_some() || f.joins.iter().any(|j| j.table.schema.is_some()) {
return Err(Error::Unsupported("VDBE: schema-qualified source"));
}
}
if !sel.compound.is_empty() {
return self.run_compound_vdbe(sel);
}
if sel.from.is_some()
&& !sel.order_by.is_empty()
&& self
.order_satisfied_by_scan(sel, &eval::Params::default())
.is_some()
{
return Err(Error::Unsupported("VDBE: ORDER BY satisfied by a scan"));
}
if window::has_window(sel) {
return self.run_window_vdbe(sel);
}
let folded;
let sel = match self.fold_vdbe_subqueries(sel) {
Some(s) => {
folded = s;
&folded
}
None => sel,
};
let regrouped;
let sel = if sel
.group_by
.iter()
.any(|g| matches!(g, Expr::Literal(Literal::Integer(_))))
{
if sel
.columns
.iter()
.any(|c| matches!(c, ResultColumn::Wildcard | ResultColumn::TableWildcard(_)))
{
return Err(Error::Unsupported(
"VDBE: positional GROUP BY with wildcard projection",
));
}
let mut s = sel.clone();
for g in &mut s.group_by {
if let Expr::Literal(Literal::Integer(n)) = g {
match usize::try_from(*n)
.ok()
.filter(|&n| n >= 1)
.and_then(|n| sel.columns.get(n - 1))
{
Some(ResultColumn::Expr { expr, .. }) => *g = expr.clone(),
_ => {
return Err(Error::Unsupported(
"VDBE: positional GROUP BY out of range",
))
}
}
}
}
regrouped = s;
®rouped
} else {
sel
};
let Some(from) = &sel.from else {
let prog = vdbe::compile_const_select(sel)?;
let rows = vdbe::run(&prog)?;
return Ok(QueryResult {
columns: prog.columns,
rows,
});
};
type ScanOut = (
Vec<String>,
Vec<String>,
Vec<eval::Affinity>,
Vec<crate::value::Collation>,
Vec<Vec<Value>>,
Option<Vec<i64>>,
);
let scan_one = |tr: &sql::ast::TableRef| -> Result<ScanOut> {
if let Some(sub) = &tr.subquery {
if tr.tvf_args.is_some() || !sub.compound.is_empty() {
return Err(Error::Unsupported("VDBE: complex subquery source"));
}
let sfrom = sub
.from
.as_ref()
.ok_or(Error::Unsupported("VDBE: subquery source without FROM"))?;
if !sfrom.joins.is_empty()
|| sfrom.first.subquery.is_some()
|| sfrom.first.tvf_args.is_some()
|| sfrom.first.index_hint.is_some()
|| sfrom.first.schema.is_some()
|| self.schema.table(&sfrom.first.name).is_none()
{
return Err(Error::Unsupported("VDBE: complex subquery source"));
}
let base = self.table_meta(&sfrom.first.name, None)?;
if base
.columns
.iter()
.any(|c| c.collation != crate::value::Collation::default())
{
return Err(Error::Unsupported(
"VDBE: subquery over a non-BINARY column",
));
}
let named = self
.resolved_view_columns(sub)
.ok_or(Error::Unsupported("VDBE: subquery columns unresolved"))?;
let result = self.run_select(sub, &eval::Params::default())?;
if result.columns.len() != named.len() {
return Err(Error::Unsupported("VDBE: subquery column count mismatch"));
}
let qualifier = tr.alias.clone().unwrap_or_default();
let tables = result.columns.iter().map(|_| qualifier.clone()).collect();
let affinities = named
.iter()
.map(|(_, t)| eval::Affinity::from_type(t.as_deref()))
.collect();
let collations = result
.columns
.iter()
.map(|_| crate::value::Collation::default())
.collect();
return Ok((
result.columns,
tables,
affinities,
collations,
result.rows,
None,
));
}
if tr.tvf_args.is_some() {
return Err(Error::Unsupported("VDBE: only plain table sources"));
}
if tr.index_hint.is_some() {
return Err(Error::Unsupported("VDBE: index hint"));
}
let meta = self.table_meta(&tr.name, tr.alias.as_deref())?;
let cols = meta.columns.iter().map(|c| c.name.clone()).collect();
let collations = meta.columns.iter().map(|c| c.collation).collect();
let qualifier = tr.alias.clone().unwrap_or_else(|| tr.name.clone());
let tables = meta.columns.iter().map(|_| qualifier.clone()).collect();
let affinities = meta.columns.iter().map(|c| c.affinity).collect();
let (rows, rowids): (Vec<Vec<Value>>, Option<Vec<i64>>) = if meta.without_rowid {
(self.scan_without_rowid(&meta)?, None)
} else {
let scanned = self.scan_table(&meta)?;
let ids = scanned.iter().map(|(r, _)| *r).collect();
(scanned.into_iter().map(|(_, v)| v).collect(), Some(ids))
};
Ok((cols, tables, affinities, collations, rows, rowids))
};
if from.joins.iter().any(|j| {
matches!(
j.kind,
sql::ast::JoinKind::Left | sql::ast::JoinKind::Right | sql::ast::JoinKind::Full
) || j.natural
|| !j.using.is_empty()
}) {
let has_coalesce = from.joins.iter().any(|j| j.natural || !j.using.is_empty());
if has_coalesce
&& sel
.columns
.iter()
.any(|rc| matches!(rc, sql::ast::ResultColumn::TableWildcard(_)))
{
return Err(Error::Unsupported("VDBE: table.* over NATURAL/USING join"));
}
let on_params = eval::Params::default();
let first = scan_one(&from.first)?;
let mut names = first.0;
let mut tabs = first.1;
let mut affs = first.2;
let mut colls = first.3;
let mut rows: Vec<Vec<Value>> = first.4;
for j in &from.joins {
let src = scan_one(&j.table)?;
let lw = names.len();
let rw = src.0.len();
let mut n_names = names.clone();
n_names.extend(src.0.iter().cloned());
let mut n_tabs = tabs.clone();
n_tabs.extend(src.1.iter().cloned());
let mut n_affs = affs.clone();
n_affs.extend(src.2.iter().copied());
let mut n_colls = colls.clone();
n_colls.extend(src.3.iter().copied());
let cinfos: Vec<ColumnInfo> = (0..n_names.len())
.map(|i| ColumnInfo {
name: n_names[i].clone(),
table: n_tabs[i].clone(),
affinity: n_affs[i],
collation: n_colls[i],
})
.collect();
let pairs: Vec<(usize, usize)> = if j.natural {
src.0
.iter()
.enumerate()
.filter_map(|(rl, rn)| {
names
.iter()
.position(|n| n.eq_ignore_ascii_case(rn))
.map(|li| (li, rl))
})
.collect()
} else if !j.using.is_empty() {
let mut v = Vec::with_capacity(j.using.len());
for name in &j.using {
let li = names.iter().position(|n| n.eq_ignore_ascii_case(name));
let rl = src.0.iter().position(|n| n.eq_ignore_ascii_case(name));
match (li, rl) {
(Some(li), Some(rl)) => v.push((li, rl)),
_ => {
return Err(Error::Error(format!(
"cannot join using column {name} - column not present in both tables"
)))
}
}
}
v
} else {
Vec::new()
};
let keep_unmatched_left =
matches!(j.kind, sql::ast::JoinKind::Left | sql::ast::JoinKind::Full);
let keep_unmatched_right =
matches!(j.kind, sql::ast::JoinKind::Right | sql::ast::JoinKind::Full);
let mut matched_right = alloc::vec![false; src.4.len()];
let mut next: Vec<Vec<Value>> = Vec::new();
for a in &rows {
let mut any = false;
for (rj, b) in src.4.iter().enumerate() {
let mut row = a.clone();
row.extend(b.iter().cloned());
let keep = if !pairs.is_empty() {
pairs.iter().all(|&(li, rl)| {
let (lv, rv) = eval::apply_comparison_affinity(
row[li].clone(),
Some(n_affs[li]),
row[lw + rl].clone(),
Some(n_affs[lw + rl]),
);
eval::truth(&eval::compare_op(
sql::ast::BinaryOp::Eq,
&lv,
&rv,
n_colls[li],
)) == Some(true)
})
} else {
match &j.on {
Some(p) => {
let ir = InputRow {
values: row.clone(),
rowid: None,
};
let ctx = ir.ctx(&cinfos, &on_params).with_subqueries(self);
eval::truth(&eval::eval(p, &ctx)?) == Some(true)
}
None => true,
}
};
if keep {
next.push(row);
matched_right[rj] = true;
any = true;
}
}
if keep_unmatched_left && !any {
let mut row = a.clone();
row.extend(core::iter::repeat_n(Value::Null, rw));
next.push(row);
}
}
if keep_unmatched_right {
for (rj, b) in src.4.iter().enumerate() {
if !matched_right[rj] {
let mut row = alloc::vec![Value::Null; lw];
row.extend(b.iter().cloned());
next.push(row);
}
}
}
if !pairs.is_empty() {
let mut drop: Vec<usize> = pairs.iter().map(|&(_, rl)| lw + rl).collect();
drop.sort_unstable();
drop.dedup();
for row in &mut next {
for &(li, rl) in &pairs {
if matches!(row[li], Value::Null) {
row[li] = row[lw + rl].clone();
}
}
for &d in drop.iter().rev() {
row.remove(d);
}
}
for &d in drop.iter().rev() {
n_names.remove(d);
n_tabs.remove(d);
n_affs.remove(d);
n_colls.remove(d);
}
}
rows = next;
names = n_names;
tabs = n_tabs;
affs = n_affs;
colls = n_colls;
}
let join_cols: Vec<ColumnInfo> = (0..names.len())
.map(|i| ColumnInfo {
name: names[i].clone(),
table: tabs[i].clone(),
affinity: affs[i],
collation: colls[i],
})
.collect();
if validate_unambiguous_columns(sel, &join_cols).is_err() {
return Err(Error::Unsupported("VDBE: ambiguous column name"));
}
let prog = vdbe::compile_table_select(sel, &names, &tabs, &affs, &colls, false)?;
let result = vdbe::run_rows(&prog, &rows)?;
return Ok(QueryResult {
columns: prog.columns,
rows: result,
});
}
if !from.joins.is_empty() {
let single =
from.joins.len() == 1 && !from.joins[0].natural && from.joins[0].using.is_empty();
let is_left_2 = single && from.joins[0].kind == sql::ast::JoinKind::Left;
let is_right_2 = single && from.joins[0].kind == sql::ast::JoinKind::Right;
let is_full_2 = single && from.joins[0].kind == sql::ast::JoinKind::Full;
let left_inner_chain = from.joins.len() >= 2
&& from.joins.len() <= 4
&& from.joins.iter().all(|j| {
matches!(j.kind, sql::ast::JoinKind::Left | sql::ast::JoinKind::Inner)
&& !j.natural
&& j.using.is_empty()
});
let is_left_chain = left_inner_chain
&& from
.joins
.iter()
.any(|j| j.kind == sql::ast::JoinKind::Left);
if !is_left_2
&& !is_right_2
&& !is_full_2
&& !is_left_chain
&& from.joins.iter().any(|j| {
j.kind != sql::ast::JoinKind::Inner || j.natural || !j.using.is_empty()
})
{
return Err(Error::Unsupported("VDBE: only plain inner joins"));
}
let mut sources = alloc::vec![scan_one(&from.first)?];
for j in &from.joins {
sources.push(scan_one(&j.table)?);
}
let mut combined: Vec<String> = Vec::new();
let mut combined_tables: Vec<String> = Vec::new();
let mut combined_aff: Vec<eval::Affinity> = Vec::new();
let mut combined_coll: Vec<crate::value::Collation> = Vec::new();
for (c, t, a, l, _, _) in &sources {
combined.extend(c.iter().cloned());
combined_tables.extend(t.iter().cloned());
combined_aff.extend(a.iter().copied());
combined_coll.extend(l.iter().copied());
}
if is_left_chain {
let join_cols: Vec<ColumnInfo> = (0..combined.len())
.map(|i| ColumnInfo {
name: combined[i].clone(),
table: combined_tables[i].clone(),
affinity: combined_aff[i],
collation: combined_coll[i],
})
.collect();
if validate_unambiguous_columns(sel, &join_cols).is_err() {
return Err(Error::Unsupported("VDBE: ambiguous column name"));
}
let mut boundaries = Vec::with_capacity(sources.len());
let mut acc = 0;
for src in &sources {
acc += src.0.len();
boundaries.push(acc);
}
let kinds: Vec<sql::ast::JoinKind> = from.joins.iter().map(|j| j.kind).collect();
let ons: Vec<Option<sql::ast::Expr>> =
from.joins.iter().map(|j| j.on.clone()).collect();
let prog = vdbe::compile_left_join_n(
sel,
&combined,
&combined_tables,
&combined_aff,
&combined_coll,
&boundaries,
&kinds,
&ons,
)?;
let rowsets: Vec<&[Vec<Value>]> = sources.iter().map(|s| s.4.as_slice()).collect();
let result = vdbe::run_rows_multi(&prog, &rowsets)?;
return Ok(QueryResult {
columns: prog.columns,
rows: result,
});
}
if is_left_2 || is_right_2 || is_full_2 {
let (outer, inner) = if is_right_2 {
(1usize, 0usize)
} else {
(0usize, 1usize)
};
let mut oj_cols: Vec<String> = Vec::new();
let mut oj_tables: Vec<String> = Vec::new();
let mut oj_aff: Vec<eval::Affinity> = Vec::new();
let mut oj_coll: Vec<crate::value::Collation> = Vec::new();
for &si in &[outer, inner] {
let (c, t, a, l, _, _) = &sources[si];
oj_cols.extend(c.iter().cloned());
oj_tables.extend(t.iter().cloned());
oj_aff.extend(a.iter().copied());
oj_coll.extend(l.iter().copied());
}
let join_cols: Vec<ColumnInfo> = (0..oj_cols.len())
.map(|i| ColumnInfo {
name: oj_cols[i].clone(),
table: oj_tables[i].clone(),
affinity: oj_aff[i],
collation: oj_coll[i],
})
.collect();
if validate_unambiguous_columns(sel, &join_cols).is_err() {
return Err(Error::Unsupported("VDBE: ambiguous column name"));
}
let n_outer = sources[outer].0.len();
let on = &from.joins[0].on;
let prog = if is_full_2 {
vdbe::compile_full_join2(
sel, &oj_cols, &oj_tables, &oj_aff, &oj_coll, n_outer, on,
)?
} else {
vdbe::compile_left_join2(
sel, &oj_cols, &oj_tables, &oj_aff, &oj_coll, n_outer, on,
)?
};
let result = vdbe::run_rows_multi(&prog, &[&sources[outer].4, &sources[inner].4])?;
return Ok(QueryResult {
columns: prog.columns,
rows: result,
});
}
let mut merged = sel.where_clause.clone();
for j in &from.joins {
if let Some(on) = &j.on {
merged = Some(match merged {
Some(w) => sql::ast::Expr::Binary {
op: sql::ast::BinaryOp::And,
left: alloc::boxed::Box::new(w),
right: alloc::boxed::Box::new(on.clone()),
},
None => on.clone(),
});
}
}
let mut joined = sel.clone();
joined.where_clause = merged;
let join_cols: Vec<ColumnInfo> = (0..combined.len())
.map(|i| ColumnInfo {
name: combined[i].clone(),
table: combined_tables[i].clone(),
affinity: combined_aff[i],
collation: combined_coll[i],
})
.collect();
if validate_unambiguous_columns(sel, &join_cols).is_err() {
return Err(Error::Unsupported("VDBE: ambiguous column name"));
}
{
let mut boundaries = Vec::with_capacity(sources.len());
let mut acc = 0;
for src in &sources {
acc += src.0.len();
boundaries.push(acc);
}
if let Ok(prog) = vdbe::compile_aggregate_join(
&joined,
&combined,
&combined_tables,
&combined_aff,
&combined_coll,
&boundaries,
) {
let rowsets: Vec<&[Vec<Value>]> =
sources.iter().map(|s| s.4.as_slice()).collect();
let result = vdbe::run_rows_multi(&prog, &rowsets)?;
return Ok(QueryResult {
columns: prog.columns,
rows: result,
});
}
if let Ok(prog) = vdbe::compile_group_join(
&joined,
&combined,
&combined_tables,
&combined_aff,
&combined_coll,
&boundaries,
) {
let rowsets: Vec<&[Vec<Value>]> =
sources.iter().map(|s| s.4.as_slice()).collect();
let result = vdbe::run_rows_multi(&prog, &rowsets)?;
return Ok(QueryResult {
columns: prog.columns,
rows: result,
});
}
if let Ok(prog) = vdbe::compile_join2(
&joined,
&combined,
&combined_tables,
&combined_aff,
&combined_coll,
&boundaries,
) {
let rowsets: Vec<&[Vec<Value>]> =
sources.iter().map(|s| s.4.as_slice()).collect();
let result = vdbe::run_rows_multi(&prog, &rowsets)?;
return Ok(QueryResult {
columns: prog.columns,
rows: result,
});
}
}
let mut rows: Vec<Vec<Value>> = sources[0].4.clone();
for src in &sources[1..] {
let mut next = Vec::with_capacity(rows.len().saturating_mul(src.4.len()));
for a in &rows {
for b in &src.4 {
let mut row = a.clone();
row.extend(b.iter().cloned());
next.push(row);
}
}
rows = next;
}
let prog = vdbe::compile_table_select(
&joined,
&combined,
&combined_tables,
&combined_aff,
&combined_coll,
false,
)?;
let result = vdbe::run_rows(&prog, &rows)?;
return Ok(QueryResult {
columns: prog.columns,
rows: result,
});
}
if from.first.tvf_args.is_some() {
return Err(Error::Unsupported("VDBE: table-valued function source"));
}
let (col_names, col_tables, col_aff, col_coll, mut rows, rowids) = scan_one(&from.first)?;
let has_rowid = rowids.is_some();
if let Some(ids) = rowids {
for (row, id) in rows.iter_mut().zip(ids) {
row.push(Value::Integer(id));
}
}
for rc in &sel.columns {
if let sql::ast::ResultColumn::TableWildcard(q) = rc {
let matches = q.eq_ignore_ascii_case(&from.first.name)
|| from
.first
.alias
.as_deref()
.is_some_and(|a| q.eq_ignore_ascii_case(a));
if !matches {
return Err(Error::Unsupported("VDBE: unknown table.* qualifier"));
}
}
}
let prog = vdbe::compile_table_select(
sel,
&col_names,
&col_tables,
&col_aff,
&col_coll,
has_rowid,
)?;
let result = vdbe::run_rows(&prog, &rows)?;
Ok(QueryResult {
columns: prog.columns,
rows: result,
})
}
fn run_compound_vdbe(&self, sel: &Select) -> Result<QueryResult> {
if !sel.ctes.is_empty() {
return Err(Error::Unsupported("VDBE: compound with CTEs"));
}
if sel
.compound
.iter()
.any(|(_, c)| !c.compound.is_empty() || !c.ctes.is_empty())
{
return Err(Error::Unsupported("VDBE: nested compound arm"));
}
let mut first = sel.clone();
first.compound = Vec::new();
first.order_by = Vec::new();
first.limit = None;
first.offset = None;
let mut result = self.run_select_vdbe(&first)?;
let params = eval::Params::default();
let colls = {
let (cols, _) = self.scan_source(&first, ¶ms)?;
self.output_collations(&first, &cols, ¶ms)
};
for (op, operand) in &sel.compound {
let r = self.run_select_vdbe(operand)?;
if r.columns.len() != result.columns.len() {
let kw = match op {
CompoundOp::Union => "UNION",
CompoundOp::UnionAll => "UNION ALL",
CompoundOp::Intersect => "INTERSECT",
CompoundOp::Except => "EXCEPT",
};
return Err(Error::Error(alloc::format!(
"SELECTs to the left and right of {kw} do not have the same \
number of result columns"
)));
}
result.rows = apply_compound(*op, result.rows, r.rows, &colls);
}
if sel.order_by.is_empty()
&& sel
.compound
.iter()
.any(|(op, _)| *op != CompoundOp::UnionAll)
{
result.rows.sort_by(|a, b| {
for (i, va) in a.iter().enumerate() {
let coll = colls.get(i).copied().unwrap_or_default();
let ord = crate::value::cmp_values_coll(va, &b[i], coll);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
});
}
self.compound_order_limit(&mut result, sel, ¶ms, &colls)?;
Ok(result)
}
pub fn query_params(&self, sql: &str, params: &Params) -> Result<QueryResult> {
match sql::parse_one(sql)? {
Statement::Select(sel) => self.run_select(&sel, params),
Statement::Pragma(p) => self.run_pragma(&p),
Statement::Explain { query_plan, stmt } => {
if query_plan {
self.explain_query_plan(&stmt, params)
} else {
self.explain_bytecode(&stmt)
}
}
_ => Err(Error::Unsupported(
"use execute() for non-SELECT statements",
)),
}
}
fn run_pragma(&self, p: &Pragma) -> Result<QueryResult> {
let name = p.name.to_ascii_lowercase();
let header = self.backend.source().header();
let single = |col: &str, v: Value| QueryResult {
columns: alloc::vec![String::from(col)],
rows: alloc::vec![alloc::vec![v]],
};
match name.as_str() {
"page_size" => Ok(single("page_size", Value::Integer(header.page_size as i64))),
"page_count" => Ok(single(
"page_count",
Value::Integer(self.backend.source().page_count() as i64),
)),
"user_version" => Ok(single(
"user_version",
Value::Integer(header.user_version as i32 as i64),
)),
"schema_version" => Ok(single(
"schema_version",
Value::Integer(header.schema_cookie as i64),
)),
"encoding" => Ok(single(
"encoding",
Value::Text(
match header.text_encoding {
crate::format::TextEncoding::Utf8 => "UTF-8",
crate::format::TextEncoding::Utf16Le => "UTF-16le",
crate::format::TextEncoding::Utf16Be => "UTF-16be",
}
.into(),
),
)),
"freelist_count" => Ok(single(
"freelist_count",
Value::Integer(header.freelist_count as i64),
)),
"auto_vacuum" => Ok(single(
"auto_vacuum",
Value::Integer(auto_vacuum_mode(header) as i64),
)),
"application_id" => Ok(single(
"application_id",
Value::Integer(header.application_id as i32 as i64),
)),
"data_version" => Ok(single("data_version", Value::Integer(1))),
"table_info" => self.pragma_table_info(p, false),
"table_xinfo" => self.pragma_table_info(p, true),
"index_list" => self.pragma_index_list(p),
"index_info" => self.pragma_index_info(p, false),
"index_xinfo" => self.pragma_index_info(p, true),
"database_list" => Ok(self.pragma_database_list()),
"table_list" => self.pragma_table_list(p),
"collation_list" => Ok(QueryResult {
columns: alloc::vec!["seq".into(), "name".into()],
rows: ["RTRIM", "NOCASE", "BINARY"]
.iter()
.enumerate()
.map(|(i, n)| alloc::vec![Value::Integer(i as i64), Value::Text((*n).into())])
.collect(),
}),
"foreign_key_list" => self.pragma_foreign_key_list(p),
"foreign_key_check" => self.pragma_foreign_key_check(p),
"integrity_check" | "quick_check" => self.pragma_integrity_check(),
"foreign_keys" => Ok(single(
"foreign_keys",
Value::Integer(self.foreign_keys as i64),
)),
"recursive_triggers" => Ok(single(
"recursive_triggers",
Value::Integer(self.recursive_triggers as i64),
)),
"journal_mode" => {
let mode = if self.backend.wal_mode() {
"wal"
} else if self.main_file.is_empty() {
"memory"
} else {
"delete"
};
Ok(single("journal_mode", Value::Text(mode.into())))
}
"cache_size" => Ok(single("cache_size", Value::Integer(self.cache_size.get()))),
"mmap_size" => Ok(QueryResult {
columns: alloc::vec![String::from("mmap_size")],
rows: Vec::new(),
}),
"synchronous" => Ok(single("synchronous", Value::Integer(2))),
"temp_store" => Ok(single("temp_store", Value::Integer(0))),
"secure_delete" => Ok(single(
"secure_delete",
Value::Integer(self.secure_delete.get()),
)),
"read_uncommitted" => Ok(single("read_uncommitted", Value::Integer(0))),
"cell_size_check" => Ok(single("cell_size_check", Value::Integer(0))),
"checkpoint_fullfsync" => Ok(single("checkpoint_fullfsync", Value::Integer(0))),
"fullfsync" => Ok(single("fullfsync", Value::Integer(0))),
"busy_timeout" => {
if let Some(e) = &p.value {
let v = eval::to_i64(&eval::eval(e, &EvalCtx::rowless(&Params::default()))?);
self.busy_timeout.set(v.max(0));
}
Ok(single("timeout", Value::Integer(self.busy_timeout.get())))
}
"wal_checkpoint" => Ok(QueryResult {
columns: alloc::vec![
String::from("busy"),
String::from("log"),
String::from("checkpointed"),
],
rows: alloc::vec![alloc::vec![
Value::Integer(0),
Value::Integer(-1),
Value::Integer(-1),
]],
}),
"wal_autocheckpoint" => Ok(single("wal_autocheckpoint", Value::Integer(1000))),
"max_page_count" => Ok(single("max_page_count", Value::Integer(4294967294))),
"locking_mode" => Ok(single("locking_mode", Value::Text("normal".into()))),
"legacy_file_format" | "case_sensitive_like" => Ok(QueryResult {
columns: alloc::vec![name.clone()],
rows: Vec::new(),
}),
"analysis_limit" => {
if let Some(e) = &p.value {
let v = eval::to_i64(&eval::eval(e, &EvalCtx::rowless(&Params::default()))?);
self.analysis_limit.set(v.max(0));
}
Ok(single(
"analysis_limit",
Value::Integer(self.analysis_limit.get()),
))
}
"optimize" => Ok(QueryResult {
columns: alloc::vec![name.clone()],
rows: Vec::new(),
}),
"short_column_names" | "automatic_index" => Ok(single(&name, Value::Integer(1))),
"legacy_alter_table"
| "count_changes"
| "full_column_names"
| "empty_result_callbacks"
| "defer_foreign_keys"
| "ignore_check_constraints"
| "reverse_unordered_selects"
| "query_only"
| "writable_schema"
| "threads"
| "soft_heap_limit"
| "hard_heap_limit" => Ok(single(&name, Value::Integer(0))),
"incremental_vacuum" => Err(Error::Unsupported(
"PRAGMA incremental_vacuum modifies the database; use execute()",
)),
_ => Ok(QueryResult {
columns: alloc::vec![name.clone()],
rows: Vec::new(),
}),
}
}
fn pragma_table_list(&self, p: &Pragma) -> Result<QueryResult> {
use crate::schema::ObjectType;
let filter = match &p.value {
Some(Expr::Column { column, .. }) => Some(column.clone()),
Some(Expr::Literal(Literal::Str(s))) => Some(s.clone()),
_ => None,
};
let params = Params::default();
let mut dbs: Vec<(String, DbRef, &str)> = alloc::vec![
("main".into(), DbRef::Main, "sqlite_schema"),
("temp".into(), DbRef::Temp, "sqlite_temp_schema"),
];
for (i, d) in self.attached.iter().enumerate() {
dbs.push((d.name.clone(), DbRef::Attached(i), "sqlite_schema"));
}
let matches = |n: &str| filter.as_deref().is_none_or(|f| f.eq_ignore_ascii_case(n));
let mut rows: Vec<Vec<Value>> = Vec::new();
for (db_name, db, schema_tab) in &dbs {
let objects: &[crate::schema::SchemaObject] =
if matches!(db, DbRef::Temp) && self.temp_db.is_none() {
&[]
} else {
self.db_parts(*db).0.objects()
};
for obj in objects {
let typ = match obj.obj_type {
ObjectType::Table => "table",
ObjectType::View => "view",
_ => continue,
};
if !matches(&obj.name) {
continue;
}
let (ncol, wr, strict) = self.table_list_dims(*db, obj, ¶ms);
rows.push(alloc::vec![
Value::Text(db_name.clone()),
Value::Text(obj.name.clone()),
Value::Text(typ.into()),
Value::Integer(ncol),
Value::Integer(wr),
Value::Integer(strict),
]);
}
if matches(schema_tab)
|| filter
.as_deref()
.is_some_and(|f| f.eq_ignore_ascii_case("sqlite_master"))
{
rows.push(alloc::vec![
Value::Text(db_name.clone()),
Value::Text((*schema_tab).into()),
Value::Text("table".into()),
Value::Integer(5),
Value::Integer(0),
Value::Integer(0),
]);
}
}
Ok(QueryResult {
columns: alloc::vec![
"schema".into(),
"name".into(),
"type".into(),
"ncol".into(),
"wr".into(),
"strict".into(),
],
rows,
})
}
fn table_list_dims(
&self,
db: DbRef,
obj: &crate::schema::SchemaObject,
params: &Params,
) -> (i64, i64, i64) {
use crate::schema::ObjectType;
match obj.obj_type {
ObjectType::Table => {
let (schema, _) = self.db_parts(db);
match self.table_meta_in(schema, &obj.name, None) {
Ok(m) => (
m.columns.len() as i64,
m.without_rowid as i64,
m.strict_types.is_some() as i64,
),
Err(_) => (0, 0, 0),
}
}
ObjectType::View => {
let ncol = self
.scan_db_view(db, &obj.name, None, params)
.ok()
.flatten()
.map_or(0, |(c, _)| c.len() as i64);
(ncol, 0, 0)
}
_ => (0, 0, 0),
}
}
fn pragma_database_list(&self) -> QueryResult {
let mut rows = alloc::vec![alloc::vec![
Value::Integer(0),
Value::Text("main".into()),
Value::Text(self.main_file.clone()),
]];
if self.temp_db.is_some() {
rows.push(alloc::vec![
Value::Integer(1),
Value::Text("temp".into()),
Value::Text(String::new()),
]);
}
for (i, db) in self.attached.iter().enumerate() {
rows.push(alloc::vec![
Value::Integer((i + 2) as i64),
Value::Text(db.name.clone()),
Value::Text(db.file.clone()),
]);
}
QueryResult {
columns: alloc::vec!["seq".into(), "name".into(), "file".into()],
rows,
}
}
fn pragma_table_info(&self, p: &Pragma, extended: bool) -> Result<QueryResult> {
let table = match &p.value {
Some(Expr::Column { column, .. }) => column.clone(),
Some(Expr::Literal(Literal::Str(s))) => s.clone(),
_ => {
return Err(Error::Error(
"PRAGMA table_info requires a table name".into(),
))
}
};
if matches!(
table.to_ascii_lowercase().as_str(),
"sqlite_master" | "sqlite_schema" | "sqlite_temp_master" | "sqlite_temp_schema"
) {
let cols = [
("type", "TEXT"),
("name", "TEXT"),
("tbl_name", "TEXT"),
("rootpage", "INT"),
("sql", "TEXT"),
];
let mut rows = Vec::new();
for (i, (name, ty)) in cols.iter().enumerate() {
let mut row = alloc::vec![
Value::Integer(i as i64),
Value::Text((*name).into()),
Value::Text((*ty).into()),
Value::Integer(0),
Value::Null,
Value::Integer(0),
];
if extended {
row.push(Value::Integer(0));
}
rows.push(row);
}
let columns = table_info_columns(extended);
return Ok(QueryResult { columns, rows });
}
if self.schema.table(&table).is_none() {
let lower = table.to_ascii_lowercase();
let fixed: &[(&str, &str, i64, bool)] = match lower.as_str() {
"sqlite_dbpage" => &[
("pgno", "INTEGER", 1, false),
("data", "BLOB", 0, false),
("schema", "", 0, true),
],
"dbstat" => &[
("name", "TEXT", 0, false),
("path", "TEXT", 0, false),
("pageno", "INTEGER", 0, false),
("pagetype", "TEXT", 0, false),
("ncell", "INTEGER", 0, false),
("payload", "INTEGER", 0, false),
("unused", "INTEGER", 0, false),
("mx_payload", "INTEGER", 0, false),
("pgoffset", "INTEGER", 0, false),
("pgsize", "INTEGER", 0, false),
("schema", "TEXT", 0, true),
("aggregate", "BOOLEAN", 0, true),
],
_ => &[],
};
if !fixed.is_empty() {
let rows = fixed
.iter()
.filter(|(_, _, _, hidden)| extended || !hidden)
.enumerate()
.map(|(i, (name, ty, pk, hidden))| {
let mut row = alloc::vec![
Value::Integer(i as i64),
Value::Text((*name).into()),
Value::Text((*ty).into()),
Value::Integer(0),
Value::Null,
Value::Integer(*pk),
];
if extended {
row.push(Value::Integer(*hidden as i64));
}
row
})
.collect();
return Ok(QueryResult {
columns: table_info_columns(extended),
rows,
});
}
}
if let Some(vobj) = self.schema.objects().iter().find(|o| {
o.obj_type == crate::schema::ObjectType::View && o.name.eq_ignore_ascii_case(&table)
}) {
if let Some(sql) = &vobj.sql {
if let Statement::CreateView(cv) = sql::parse_one(sql)? {
return self.view_table_info(&cv, &table, extended);
}
}
}
if self.is_virtual_table(&table) {
let (_, _, schema) = self.vtab_meta(&table)?;
let rows = schema
.columns
.iter()
.enumerate()
.map(|(i, name)| {
let ty = schema.types.get(i).cloned().unwrap_or_default();
let mut row = alloc::vec![
Value::Integer(i as i64),
Value::Text(name.clone()),
Value::Text(ty),
Value::Integer(0),
Value::Null,
Value::Integer(0),
];
if extended {
row.push(Value::Integer(0));
}
row
})
.collect();
return Ok(QueryResult {
columns: table_info_columns(extended),
rows,
});
}
let Some(obj) = self.schema.table(&table) else {
return Ok(QueryResult {
columns: table_info_columns(extended),
rows: Vec::new(),
});
};
let sql = obj.sql.as_deref().unwrap_or("");
let Statement::CreateTable(ct) = sql::parse_one(sql)? else {
return Err(Error::Corrupt("schema sql is not CREATE TABLE".into()));
};
let pk_positions = primary_key_positions(&ct);
let mut rows = Vec::new();
for (i, col) in ct.columns.iter().enumerate() {
let generated = col.constraints.iter().find_map(|c| match c {
ColumnConstraint::Generated { stored, .. } => Some(*stored),
_ => None,
});
if generated.is_some() && !extended {
continue;
}
let hidden = match generated {
None => 0,
Some(false) => 2,
Some(true) => 3,
};
let notnull = col
.constraints
.iter()
.any(|c| matches!(c, ColumnConstraint::NotNull(_)));
let dflt = col.constraints.iter().find_map(|c| match c {
ColumnConstraint::Default(e) => Some(sql::print::expr(e)),
_ => None,
});
let pk = pk_positions
.iter()
.position(|&pos| pos == i)
.map_or(0, |n| n as i64 + 1);
let mut row = alloc::vec![
Value::Integer(i as i64),
Value::Text(col.name.clone()),
Value::Text(col.type_name.clone().unwrap_or_default()),
Value::Integer(notnull as i64),
dflt.map(Value::Text).unwrap_or(Value::Null),
Value::Integer(pk),
];
if extended {
row.push(Value::Integer(hidden));
}
rows.push(row);
}
Ok(QueryResult {
columns: table_info_columns(extended),
rows,
})
}
fn view_table_info(
&self,
cv: &CreateView,
view_name: &str,
extended: bool,
) -> Result<QueryResult> {
let mut cols: NamedColumns = match self.resolved_view_columns(&cv.select) {
Some(c) => c,
None => self
.view_columns(view_name, &Params::default())?
.into_iter()
.map(|c| (c.name, None))
.collect(),
};
if !cv.columns.is_empty() && cv.columns.len() == cols.len() {
for (slot, name) in cols.iter_mut().zip(&cv.columns) {
slot.0 = name.clone();
}
}
let rows = cols
.into_iter()
.enumerate()
.map(|(i, (name, ty))| {
let mut row = alloc::vec![
Value::Integer(i as i64),
Value::Text(name),
Value::Text(ty.unwrap_or_default()),
Value::Integer(0),
Value::Null,
Value::Integer(0),
];
if extended {
row.push(Value::Integer(0));
}
row
})
.collect();
Ok(QueryResult {
columns: table_info_columns(extended),
rows,
})
}
fn resolved_view_columns(&self, select: &Select) -> Option<NamedColumns> {
let mut sources: Vec<(String, NamedColumns)> = Vec::new();
if let Some(fc) = &select.from {
let mut refs = alloc::vec![&fc.first];
let mut coalesced = false;
for j in &fc.joins {
refs.push(&j.table);
if j.natural || !j.using.is_empty() {
coalesced = true;
}
}
let has_wild = select
.columns
.iter()
.any(|c| matches!(c, ResultColumn::Wildcard | ResultColumn::TableWildcard(_)));
if coalesced && has_wild {
return None; }
for tref in refs {
let label = tref.alias.clone().unwrap_or_else(|| tref.name.clone());
sources.push((label, self.source_columns_of(tref)?));
}
}
let lookup = |table: Option<&str>, col: &str| -> Option<String> {
for (label, cols) in &sources {
if table.is_some_and(|t| !t.eq_ignore_ascii_case(label)) {
continue;
}
if let Some((_, ty)) = cols.iter().find(|(n, _)| n.eq_ignore_ascii_case(col)) {
return ty.clone();
}
}
None
};
let mut out = Vec::new();
for rc in &select.columns {
match rc {
ResultColumn::Wildcard => {
for (_, cols) in &sources {
out.extend(cols.iter().cloned());
}
}
ResultColumn::TableWildcard(t) => {
let (_, cols) = sources.iter().find(|(l, _)| l.eq_ignore_ascii_case(t))?;
out.extend(cols.iter().cloned());
}
ResultColumn::Expr {
expr,
alias,
source,
} => {
let name = result_column_label(expr, alias, source);
let ty = match expr {
Expr::Column { table, column } => lookup(table.as_deref(), column),
_ => None,
};
out.push((name, ty));
}
}
}
Some(out)
}
fn source_columns_of(&self, tref: &TableRef) -> Option<NamedColumns> {
if tref.tvf_args.is_some() {
return None;
}
if let Some(sub) = &tref.subquery {
return self.resolved_view_columns(sub);
}
if let Some(o) = self.schema.objects().iter().find(|o| {
o.obj_type == crate::schema::ObjectType::View && o.name.eq_ignore_ascii_case(&tref.name)
}) {
if let Some(Ok(Statement::CreateView(cv))) = o.sql.as_deref().map(sql::parse_one) {
return self.resolved_view_columns(&cv.select);
}
return None;
}
let obj = self.schema.table(&tref.name)?;
let Ok(Statement::CreateTable(ct)) = sql::parse_one(obj.sql.as_deref()?) else {
return None;
};
Some(
ct.columns
.iter()
.map(|c| {
let ty = c.type_name.clone().unwrap_or_else(|| String::from("BLOB"));
(c.name.clone(), Some(ty))
})
.collect(),
)
}
fn pragma_arg_name(p: &Pragma) -> Result<String> {
match &p.value {
Some(Expr::Column { column, .. }) => Ok(column.clone()),
Some(Expr::Literal(Literal::Str(s))) => Ok(s.clone()),
_ => Err(Error::Error("PRAGMA requires a name argument".into())),
}
}
fn pragma_index_list(&self, p: &Pragma) -> Result<QueryResult> {
let table = Self::pragma_arg_name(p)?;
let objs: Vec<_> = self.schema.indexes_on(&table).collect();
let pk_set: Vec<usize> = self
.schema
.table(&table)
.and_then(|o| o.sql.as_deref())
.and_then(|sql| sql::parse_one(sql).ok())
.and_then(|st| match st {
Statement::CreateTable(ct) => {
let ipk = find_integer_primary_key(&ct);
let pk = primary_key_positions(&ct);
if pk.is_empty() || (pk.len() == 1 && Some(pk[0]) == ipk) {
None
} else {
Some(pk)
}
}
_ => None,
})
.unwrap_or_default();
let tmeta = self.table_meta(&table, None).ok();
let mut rows = Vec::new();
for obj in objs.iter().rev() {
let (unique, origin, partial) = match &obj.sql {
Some(sql) => match sql::parse_one(sql) {
Ok(Statement::CreateIndex(ci)) => {
(ci.unique as i64, "c", ci.where_clause.is_some() as i64)
}
_ => (0, "c", 0),
},
None => {
let cols = autoindex_number(&obj.name, &table)
.and_then(|n| tmeta.as_ref().and_then(|m| m.unique.get(n - 1)))
.map(|s| s.0.clone())
.unwrap_or_default();
let origin = if !pk_set.is_empty() && cols == pk_set {
"pk"
} else {
"u"
};
(1, origin, 0)
}
};
rows.push(alloc::vec![
Value::Integer(rows.len() as i64),
Value::Text(obj.name.clone()),
Value::Integer(unique),
Value::Text(origin.into()),
Value::Integer(partial),
]);
}
if tmeta.as_ref().is_some_and(|m| m.without_rowid) && !pk_set.is_empty() {
rows.push(alloc::vec![
Value::Integer(rows.len() as i64),
Value::Text(alloc::format!("sqlite_autoindex_{table}_1")),
Value::Integer(1),
Value::Text("pk".into()),
Value::Integer(0),
]);
}
Ok(QueryResult {
columns: ["seq", "name", "unique", "origin", "partial"]
.iter()
.map(|s| String::from(*s))
.collect(),
rows,
})
}
fn pragma_index_info(&self, p: &Pragma, extended: bool) -> Result<QueryResult> {
let index = Self::pragma_arg_name(p)?;
let obj = self
.schema
.index(&index)
.ok_or_else(|| Error::Error(format!("no such index: {index}")))?;
let tmeta = self.table_meta(&obj.tbl_name, None)?;
type Key = (i64, Option<String>, bool, crate::value::Collation);
let keys: Vec<Key> = match &obj.sql {
Some(sql) => match sql::parse_one(sql)? {
Statement::CreateIndex(ci) => ci
.columns
.iter()
.map(|term| {
let (inner, explicit) = match &term.expr {
Expr::Collate { expr, collation } => {
(expr.as_ref(), crate::value::Collation::parse(collation))
}
e => (e, None),
};
match inner {
Expr::Column { column, .. } => {
match tmeta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(column))
{
Some(p) => (
p as i64,
Some(tmeta.columns[p].name.clone()),
term.descending,
explicit.unwrap_or(tmeta.columns[p].collation),
),
None => {
(-2, None, term.descending, explicit.unwrap_or_default())
}
}
}
_ => (-2, None, term.descending, explicit.unwrap_or_default()),
}
})
.collect(),
_ => Vec::new(),
},
None => autoindex_number(&obj.name, &obj.tbl_name)
.and_then(|n| tmeta.unique.get(n - 1))
.map(|s| s.0.clone())
.unwrap_or_default()
.into_iter()
.map(|cid| {
(
cid as i64,
Some(tmeta.columns[cid].name.clone()),
false,
tmeta.columns[cid].collation,
)
})
.collect(),
};
let coll_name = |c: crate::value::Collation| match c {
crate::value::Collation::NoCase => "NOCASE",
crate::value::Collation::RTrim => "RTRIM",
crate::value::Collation::Binary => "BINARY",
};
let mut rows = Vec::new();
for (seqno, (cid, name, desc, coll)) in keys.iter().enumerate() {
let name_val = name.clone().map_or(Value::Null, Value::Text);
if extended {
rows.push(alloc::vec![
Value::Integer(seqno as i64),
Value::Integer(*cid),
name_val,
Value::Integer(*desc as i64),
Value::Text(coll_name(*coll).into()),
Value::Integer(1), ]);
} else {
rows.push(alloc::vec![
Value::Integer(seqno as i64),
Value::Integer(*cid),
name_val
]);
}
}
if extended {
if tmeta.without_rowid {
let key_cids: Vec<i64> = keys.iter().map(|(cid, ..)| *cid).collect();
let mut seqno = keys.len();
for &pcid in &tmeta.storage_order[..tmeta.pk_len] {
if key_cids.contains(&(pcid as i64)) {
continue;
}
rows.push(alloc::vec![
Value::Integer(seqno as i64),
Value::Integer(pcid as i64),
Value::Text(tmeta.columns[pcid].name.clone()),
Value::Integer(0),
Value::Text(coll_name(tmeta.columns[pcid].collation).into()),
Value::Integer(0), ]);
seqno += 1;
}
} else {
rows.push(alloc::vec![
Value::Integer(keys.len() as i64),
Value::Integer(-1),
Value::Null,
Value::Integer(0),
Value::Text("BINARY".into()),
Value::Integer(0),
]);
}
}
let columns: Vec<String> = if extended {
["seqno", "cid", "name", "desc", "coll", "key"]
.iter()
.map(|s| String::from(*s))
.collect()
} else {
["seqno", "cid", "name"]
.iter()
.map(|s| String::from(*s))
.collect()
};
Ok(QueryResult { columns, rows })
}
fn pragma_foreign_key_list(&self, p: &Pragma) -> Result<QueryResult> {
let table = Self::pragma_arg_name(p)?;
let obj = self
.schema
.table(&table)
.ok_or_else(|| Error::Error(format!("no such table: {table}")))?;
let Statement::CreateTable(ct) = sql::parse_one(obj.sql.as_deref().unwrap_or(""))? else {
return Ok(QueryResult {
columns: [
"id",
"seq",
"table",
"from",
"to",
"on_update",
"on_delete",
"match",
]
.iter()
.map(|s| String::from(*s))
.collect(),
rows: Vec::new(),
});
};
let action = |a: FkAction| -> &'static str {
match a {
FkAction::NoAction => "NO ACTION",
FkAction::Restrict => "RESTRICT",
FkAction::Cascade => "CASCADE",
FkAction::SetNull => "SET NULL",
FkAction::SetDefault => "SET DEFAULT",
}
};
let mut fks: Vec<(Vec<String>, &ForeignKey)> = Vec::new();
for col in &ct.columns {
for c in &col.constraints {
if let ColumnConstraint::References(fk) = c {
fks.push((alloc::vec![col.name.clone()], fk));
}
}
}
for c in &ct.constraints {
if let TableConstraint::ForeignKey(fk) = c {
fks.push((fk.columns.clone(), fk));
}
}
let mut rows = Vec::new();
let n = fks.len();
for (i, (from_cols, fk)) in fks.iter().enumerate().rev() {
let id = (n - 1 - i) as i64;
for (seq, from) in from_cols.iter().enumerate() {
let to = fk.ref_columns.get(seq).cloned().unwrap_or_default();
rows.push(alloc::vec![
Value::Integer(id),
Value::Integer(seq as i64),
Value::Text(fk.ref_table.clone()),
Value::Text(from.clone()),
if to.is_empty() {
Value::Null
} else {
Value::Text(to)
},
Value::Text(action(fk.on_update).into()),
Value::Text(action(fk.on_delete).into()),
Value::Text("NONE".into()),
]);
}
}
Ok(QueryResult {
columns: [
"id",
"seq",
"table",
"from",
"to",
"on_update",
"on_delete",
"match",
]
.iter()
.map(|s| String::from(*s))
.collect(),
rows,
})
}
fn pragma_foreign_key_check(&self, p: &Pragma) -> Result<QueryResult> {
use crate::schema::ObjectType;
let tables: Vec<String> = match &p.value {
Some(_) => alloc::vec![Self::pragma_arg_name(p)?],
None => self
.schema
.objects()
.iter()
.filter(|o| o.obj_type == ObjectType::Table && !o.name.starts_with("sqlite_"))
.map(|o| o.name.clone())
.collect(),
};
let mut rows = Vec::new();
for table in &tables {
let meta = self.table_meta(table, None)?;
if meta.without_rowid {
continue; }
let fks = self.foreign_keys_of(table)?;
if fks.is_empty() {
continue;
}
let n = fks.len();
for (rowid, values) in self.scan_table(&meta)? {
for (i, fk) in fks.iter().enumerate() {
let Some(key) = self.child_key_values(&meta, fk, &values) else {
continue; };
if !self.parent_has_key(fk, &key)? {
rows.push(alloc::vec![
Value::Text(table.clone()),
Value::Integer(rowid),
Value::Text(fk.ref_table.clone()),
Value::Integer((n - 1 - i) as i64),
]);
}
}
}
}
Ok(QueryResult {
columns: ["table", "rowid", "parent", "fkid"]
.iter()
.map(|s| String::from(*s))
.collect(),
rows,
})
}
fn pragma_integrity_check(&self) -> Result<QueryResult> {
use crate::schema::ObjectType;
let single = |v: Value| QueryResult {
columns: alloc::vec![String::from("integrity_check")],
rows: alloc::vec![alloc::vec![v]],
};
let tables: Vec<String> = self
.schema
.objects()
.iter()
.filter(|o| {
o.obj_type == ObjectType::Table
&& !o.name.starts_with("sqlite_")
&& !matches!(
o.sql.as_deref().map(sql::parse_one),
Some(Ok(Statement::CreateVirtualTable(_)))
)
})
.map(|o| o.name.clone())
.collect();
let mut problems = Vec::new();
for table in &tables {
let meta = self.table_meta(table, None)?;
let rows: Vec<Vec<Value>> = if meta.without_rowid {
self.scan_without_rowid(&meta)?
} else {
self.scan_table(&meta)?
.into_iter()
.map(|(_, v)| v)
.collect()
};
let no_params = Params::default();
for idx in self.indexes_of(table)? {
let expected = rows
.iter()
.filter_map(|r| self.row_in_index(&idx, &meta, r, None, &no_params).ok())
.filter(|&keep| keep)
.count();
let mut cur = crate::btree::IndexCursor::new(self.backend.source(), idx.root);
let mut got = 0usize;
while cur.next()?.is_some() {
got += 1;
}
if got != expected {
problems.push(alloc::format!("wrong # of entries in index {}", idx.name));
}
}
}
if problems.is_empty() {
Ok(single(Value::Text("ok".into())))
} else {
Ok(QueryResult {
columns: alloc::vec![String::from("integrity_check")],
rows: problems
.into_iter()
.map(|p| alloc::vec![Value::Text(p)])
.collect(),
})
}
}
pub fn execute(&mut self, sql: &str) -> Result<usize> {
self.execute_params(sql, &Params::default())
}
pub fn register_module(
&mut self,
name: &str,
module: impl DynVTabModule + 'static,
) -> Result<()> {
self.vtab_registry.register(name, Box::new(module))
}
pub fn register_function(
&mut self,
name: &str,
f: impl Fn(&[Value]) -> Result<Value> + 'static,
) {
self.functions
.insert(name.to_ascii_lowercase(), Box::new(f));
}
pub fn register_aggregate_function(
&mut self,
name: &str,
factory: impl Fn() -> Box<dyn AggregateFunction> + 'static,
) {
self.aggregates
.insert(name.to_ascii_lowercase(), Box::new(factory));
}
pub fn execute_batch(&mut self, sql: &str) -> Result<()> {
for stmt in split_sql_script(sql) {
if matches!(sql::parse_one(stmt), Ok(Statement::Select(_))) {
self.query(stmt)?;
} else {
self.execute_params(stmt, &Params::default())?;
}
}
Ok(())
}
pub fn execute_params(&mut self, sql: &str, params: &Params) -> Result<usize> {
let stmt = sql::parse_one(sql)?;
match &stmt {
Statement::Begin => {
if self.in_tx {
return Err(Error::Error(
"cannot start a transaction within a transaction".into(),
));
}
self.in_tx = true;
return Ok(0);
}
Statement::Commit => {
if !self.in_tx && self.open_savepoints == 0 {
return Err(Error::Error(
"cannot commit - no transaction is active".into(),
));
}
self.check_deferred_fks()?;
self.backend.writer()?.commit()?;
self.commit_attached()?;
self.in_tx = false;
self.open_savepoints = 0;
return Ok(0);
}
Statement::Savepoint(name) => {
self.backend.writer()?.savepoint(name);
self.savepoint_attached(name)?;
self.open_savepoints += 1;
return Ok(0);
}
Statement::Release(name) => {
self.backend.writer()?.release_savepoint(name)?;
self.release_attached(name)?;
self.open_savepoints = self.backend.writer()?.savepoint_depth();
if self.open_savepoints == 0 && !self.in_tx {
self.check_deferred_fks()?;
self.backend.writer()?.commit()?;
self.commit_attached()?;
self.schema = Schema::read(self.backend.source())?;
}
return Ok(0);
}
Statement::RollbackTo(name) => {
self.backend.writer()?.rollback_to_savepoint(name)?;
self.rollback_to_attached(name)?;
self.open_savepoints = self.backend.writer()?.savepoint_depth();
self.schema = Schema::read(self.backend.source())?;
return Ok(0);
}
Statement::Rollback => {
if !self.in_tx && self.open_savepoints == 0 {
return Err(Error::Error(
"cannot rollback - no transaction is active".into(),
));
}
self.backend.writer()?.rollback();
self.rollback_attached()?;
self.in_tx = false;
self.open_savepoints = 0;
self.schema = Schema::read(self.backend.source())?;
return Ok(0);
}
_ => {}
}
let target = self.target_db(&stmt)?;
if target == DbRef::Temp {
self.ensure_temp()?;
}
match target {
DbRef::Main => self.exec_parsed(stmt, sql, params),
other => {
let stmt = self.prematerialize_insert_source(stmt, params);
self.swap_db(other);
let r = self.exec_parsed(stmt, sql, params);
self.swap_db(other);
r
}
}
}
fn prematerialize_insert_source(&self, stmt: Statement, params: &Params) -> Statement {
if let Statement::Insert(mut ins) = stmt {
match &ins.source {
InsertSource::Select(sel) => {
if let Ok(result) = self.run_select(sel, params) {
let rows: Vec<Vec<Expr>> = result
.rows
.into_iter()
.map(|row| row.into_iter().map(value_to_literal_expr).collect())
.collect();
ins.source = InsertSource::Values(rows);
}
}
InsertSource::Values(rows) if rows.iter().flatten().any(expr_has_subquery) => {
let ctx = EvalCtx::rowless(params).with_subqueries(self);
let mut out = Vec::with_capacity(rows.len());
let materialized = rows.iter().try_for_each(|row| {
let mut r = Vec::with_capacity(row.len());
for e in row {
r.push(value_to_literal_expr(eval::eval(e, &ctx)?));
}
out.push(r);
Ok::<(), Error>(())
});
if materialized.is_ok() {
ins.source = InsertSource::Values(out);
}
}
_ => {}
}
return Statement::Insert(ins);
}
stmt
}
fn target_db(&self, stmt: &Statement) -> Result<DbRef> {
let resolved = |s: Option<&str>, name: &str| -> Result<DbRef> {
match s {
Some(_) => self.resolve_db(s),
None => Ok(self.unqualified_db(name)),
}
};
match stmt {
Statement::CreateTable(s) => self.resolve_db(s.schema.as_deref()),
Statement::Insert(s) => resolved(s.schema.as_deref(), &s.table),
Statement::Update(s) => resolved(s.schema.as_deref(), &s.table),
Statement::Delete(s) => resolved(s.schema.as_deref(), &s.table),
Statement::Drop(s) => resolved(s.schema.as_deref(), &s.name),
Statement::Alter(a) => resolved(a.schema.as_deref(), &a.table),
Statement::CreateIndex(ci) => resolved(ci.schema.as_deref(), &ci.table),
Statement::CreateView(cv) => self.resolve_db(cv.schema.as_deref()),
Statement::CreateTrigger(ct) => resolved(ct.schema.as_deref(), &ct.table),
Statement::CreateVirtualTable(cvt) => self.resolve_db(cvt.schema.as_deref()),
_ => Ok(DbRef::Main),
}
}
fn commit_attached(&mut self) -> Result<()> {
if let Some(t) = &mut self.temp_db {
t.backend.writer()?.commit()?;
t.schema = Schema::read(t.backend.source())?;
}
for d in &mut self.attached {
d.backend.writer()?.commit()?;
d.schema = Schema::read(d.backend.source())?;
}
Ok(())
}
fn rollback_attached(&mut self) -> Result<()> {
if let Some(t) = &mut self.temp_db {
t.backend.writer()?.rollback();
t.schema = Schema::read(t.backend.source())?;
}
for d in &mut self.attached {
d.backend.writer()?.rollback();
d.schema = Schema::read(d.backend.source())?;
}
Ok(())
}
fn savepoint_attached(&mut self, name: &str) -> Result<()> {
if let Some(t) = &mut self.temp_db {
t.backend.writer()?.savepoint(name);
}
for d in &mut self.attached {
d.backend.writer()?.savepoint(name);
}
Ok(())
}
fn release_attached(&mut self, name: &str) -> Result<()> {
if let Some(t) = &mut self.temp_db {
let _ = t.backend.writer()?.release_savepoint(name);
}
for d in &mut self.attached {
let _ = d.backend.writer()?.release_savepoint(name);
}
Ok(())
}
fn rollback_to_attached(&mut self, name: &str) -> Result<()> {
if let Some(t) = &mut self.temp_db {
let did = t.backend.writer()?.rollback_to_savepoint(name).is_ok();
if did {
t.schema = Schema::read(t.backend.source())?;
}
}
for d in &mut self.attached {
let did = d.backend.writer()?.rollback_to_savepoint(name).is_ok();
if did {
d.schema = Schema::read(d.backend.source())?;
}
}
Ok(())
}
fn swap_db(&mut self, db: DbRef) {
match db {
DbRef::Main => {}
DbRef::Temp => {
let t = self.temp_db.as_mut().expect("temp db exists");
core::mem::swap(&mut self.backend, &mut t.backend);
core::mem::swap(&mut self.schema, &mut t.schema);
}
DbRef::Attached(i) => self.swap_attached(i),
}
}
fn swap_attached(&mut self, i: usize) {
core::mem::swap(&mut self.backend, &mut self.attached[i].backend);
core::mem::swap(&mut self.schema, &mut self.attached[i].schema);
}
fn exec_parsed(&mut self, stmt: Statement, sql: &str, params: &Params) -> Result<usize> {
let is_dml = matches!(
stmt,
Statement::Insert(_) | Statement::Update(_) | Statement::Delete(_)
);
if is_dml {
self.stmt_keep_partial.set(false);
self.stmt_rollback_tx.set(false);
return self.run_dml_atomic(stmt, params);
}
match &stmt {
Statement::CreateTable(ct) => reject_reserved_name(&ct.name)?,
Statement::CreateIndex(ci) => reject_reserved_name(&ci.name)?,
Statement::CreateView(cv) => reject_reserved_name(&cv.name)?,
Statement::CreateTrigger(ct) => reject_reserved_name(&ct.name)?,
Statement::CreateVirtualTable(cvt) => reject_reserved_name(&cvt.name)?,
Statement::Alter(a) => {
if let AlterAction::RenameTable(new) = &a.action {
reject_reserved_name(new)?;
}
}
_ => {}
}
let affected = match stmt {
Statement::CreateTable(ct) => {
self.exec_create_table(&ct, ddl_text(sql))?;
0
}
Statement::Insert(_) | Statement::Delete(_) | Statement::Update(_) => unreachable!(),
Statement::CreateIndex(ci) => {
self.exec_create_index(&ci, ddl_text(sql))?;
0
}
Statement::CreateView(cv) => {
self.exec_create_view(&cv, ddl_text(sql))?;
0
}
Statement::CreateTrigger(ct) => {
self.exec_create_trigger(&ct, ddl_text(sql))?;
0
}
Statement::CreateVirtualTable(cvt) => {
self.exec_create_virtual_table(&cvt, ddl_text(sql))?;
0
}
Statement::Drop(d) => {
self.exec_drop(&d)?;
0
}
Statement::Alter(a) => {
self.exec_alter(&a)?;
0
}
Statement::Pragma(p) => {
self.exec_pragma(&p, params)?;
0
}
Statement::Vacuum { schema, into } => {
if let Some(name) = schema {
let name = name.as_str();
let known = name.eq_ignore_ascii_case("main")
|| name.eq_ignore_ascii_case("temp")
|| self
.attached
.iter()
.any(|a| a.name.eq_ignore_ascii_case(name));
if !known {
return Err(Error::Error(format!("unknown database {name}")));
}
}
self.exec_vacuum(into.as_deref())?;
0
}
Statement::Reindex(target) => {
if let Some(name) = target {
let name = name.as_str();
let known = crate::value::Collation::parse(name).is_some()
|| self.schema.table(name).is_some()
|| self.schema.index(name).is_some();
if !known {
return Err(Error::Error(
"unable to identify the object to be reindexed".into(),
));
}
}
0
}
Statement::Analyze(target) => {
self.exec_analyze(target.as_deref())?;
0
}
Statement::Attach { file, name } => {
self.exec_attach(&file, &name, params)?;
0
}
Statement::Detach(name) => {
self.exec_detach(&name)?;
0
}
Statement::Select(_) => return Err(Error::Unsupported("use query() for SELECT")),
Statement::Explain { .. } => return Err(Error::Unsupported("use query() for EXPLAIN")),
Statement::Begin
| Statement::Commit
| Statement::Rollback
| Statement::Savepoint(_)
| Statement::Release(_)
| Statement::RollbackTo(_) => unreachable!(),
};
if !self.in_tx && self.open_savepoints == 0 {
self.backend.writer()?.commit()?;
self.schema = Schema::read(self.backend.source())?;
}
Ok(affected)
}
fn conflict_error(&self, oc: OnConflict, msg: &str) -> Error {
match oc {
OnConflict::Fail => self.stmt_keep_partial.set(true),
OnConflict::Rollback => self.stmt_rollback_tx.set(true),
_ => {}
}
Error::Constraint(String::from(msg))
}
fn resolve_not_null(
&self,
meta: &TableMeta,
values: &mut [Value],
stmt_oc: OnConflict,
stmt_explicit: bool,
params: &Params,
) -> Result<bool> {
for (i, slot) in values.iter_mut().enumerate() {
if !matches!(slot, Value::Null) {
continue;
}
let Some(col_oc) = meta.not_null[i] else {
continue;
};
let oc = if stmt_explicit { stmt_oc } else { col_oc };
let fail = || {
let msg = format!(
"NOT NULL constraint failed: {}.{}",
meta.columns[i].table, meta.columns[i].name
);
self.conflict_error(oc, &msg)
};
match oc {
OnConflict::Ignore => return Ok(false),
OnConflict::Replace => {
let v = match &meta.defaults[i] {
Some(e) => eval::eval(e, &EvalCtx::rowless(params)).unwrap_or(Value::Null),
None => Value::Null,
};
if matches!(v, Value::Null) {
return Err(fail());
}
*slot = v;
}
_ => return Err(fail()),
}
}
Ok(true)
}
fn run_dml_atomic(&mut self, stmt: Statement, params: &Params) -> Result<usize> {
const SP: &str = "\u{0}graphite_stmt";
self.backend.writer()?.savepoint(SP);
self.savepoint_attached(SP)?;
let result = match stmt {
Statement::Insert(ins) => self.exec_insert(&ins, params),
Statement::Delete(del) => self.exec_delete(&del, params),
Statement::Update(upd) => self.exec_update(&upd, params),
_ => unreachable!("run_dml_atomic only handles DML"),
};
match result {
Ok(affected) => {
let _ = self.backend.writer()?.release_savepoint(SP);
let _ = self.release_attached(SP);
self.changes.set(affected as i64);
self.total_changes
.set(self.total_changes.get() + affected as i64);
if !self.in_tx && self.open_savepoints == 0 {
self.backend.writer()?.commit()?;
self.schema = Schema::read(self.backend.source())?;
}
Ok(affected)
}
Err(e) => {
if self.stmt_rollback_tx.get() {
self.backend.writer()?.rollback();
self.rollback_attached()?;
self.in_tx = false;
self.open_savepoints = 0;
self.schema = Schema::read(self.backend.source())?;
} else if self.stmt_keep_partial.get() {
let _ = self.backend.writer()?.release_savepoint(SP);
let _ = self.release_attached(SP);
if !self.in_tx && self.open_savepoints == 0 {
self.backend.writer()?.commit()?;
self.schema = Schema::read(self.backend.source())?;
}
} else {
let _ = self.backend.writer()?.rollback_to_savepoint(SP);
let _ = self.backend.writer()?.release_savepoint(SP);
let _ = self.rollback_to_attached(SP);
let _ = self.release_attached(SP);
if !self.in_tx && self.open_savepoints == 0 {
self.backend.writer()?.rollback();
self.rollback_attached()?;
self.schema = Schema::read(self.backend.source())?;
}
}
Err(e)
}
}
}
pub fn execute_returning(&mut self, sql: &str, params: &Params) -> Result<QueryResult> {
let stmt = sql::parse_one(sql)?;
let returning: &[ResultColumn] = match &stmt {
Statement::Insert(i) => &i.returning,
Statement::Update(u) => &u.returning,
Statement::Delete(d) => &d.returning,
_ => {
return Err(Error::Unsupported(
"execute_returning expects INSERT/UPDATE/DELETE",
))
}
};
if returning.is_empty() {
self.execute_params(sql, params)?;
return Ok(QueryResult {
columns: Vec::new(),
rows: Vec::new(),
});
}
let table = match &stmt {
Statement::Insert(i) => &i.table,
Statement::Update(u) => &u.table,
Statement::Delete(d) => &d.table,
_ => unreachable!(),
};
let meta = self.table_meta(table, None)?;
let columns = returning_labels(returning, &meta.columns);
self.returning_rows.borrow_mut().clear();
self.execute_params(sql, params)?;
let rows = core::mem::take(&mut *self.returning_rows.borrow_mut());
Ok(QueryResult { columns, rows })
}
fn exec_attach(&mut self, file: &Expr, name: &str, params: &Params) -> Result<()> {
let path = {
let ctx = EvalCtx::rowless(params).with_subqueries(self);
eval::to_text(&eval::eval(file, &ctx)?)
};
if name.eq_ignore_ascii_case("main")
|| name.eq_ignore_ascii_case("temp")
|| self
.attached
.iter()
.any(|d| d.name.eq_ignore_ascii_case(name))
{
return Err(Error::Error(alloc::format!(
"database {name} is already in use"
)));
}
let (backend, file) = if path.is_empty() || path.eq_ignore_ascii_case(":memory:") {
let vfs = crate::vfs::memory::MemoryVfs::new();
let f = vfs.open(name, OpenFlags::READ_WRITE_CREATE)?;
let mut db = WritePager::create(f, None, 4096)?;
db.commit()?;
(Backend::Write(Box::new(db)), String::new())
} else {
(self.open_attached_file(&path)?, path)
};
let schema = Schema::read(backend.source())?;
self.attached.push(AttachedDb {
name: name.to_string(),
file,
backend,
schema,
});
Ok(())
}
#[cfg(feature = "std")]
fn open_attached_file(&self, path: &str) -> Result<Backend> {
let vfs = crate::vfs::std_file::StdVfs::new();
let main = vfs.open(path, OpenFlags::READ_WRITE_CREATE)?;
let journal = vfs.open(&journal_path(path), OpenFlags::READ_WRITE_CREATE)?;
let db = if main.size()? == 0 {
let mut db = WritePager::create(main, Some(journal), 4096)?;
db.commit()?;
db
} else {
WritePager::open(main, Some(journal))?
};
Ok(Backend::Write(Box::new(db)))
}
#[cfg(not(feature = "std"))]
fn open_attached_file(&self, _path: &str) -> Result<Backend> {
Err(Error::Unsupported("ATTACH of a file database requires std"))
}
fn exec_detach(&mut self, name: &str) -> Result<()> {
if name.eq_ignore_ascii_case("main") || name.eq_ignore_ascii_case("temp") {
return Err(Error::Error(alloc::format!(
"cannot detach database {name}"
)));
}
match self
.attached
.iter()
.position(|d| d.name.eq_ignore_ascii_case(name))
{
Some(i) => {
self.attached.remove(i);
Ok(())
}
None => Err(Error::Error(alloc::format!("no such database: {name}"))),
}
}
fn exec_vacuum(&mut self, into: Option<&Expr>) -> Result<()> {
use crate::schema::ObjectType;
if into.is_none() && !matches!(self.backend, Backend::Write(_)) {
return Ok(());
}
if into.is_none() && self.backend.wal_mode() {
self.backend.writer()?.checkpoint()?;
}
let user_version = self.backend.source().header().user_version;
let objs: Vec<(ObjectType, String, Option<String>)> = self
.schema
.objects()
.iter()
.map(|o| (o.obj_type, o.name.clone(), o.sql.clone()))
.collect();
let quote = |n: &str| alloc::format!("\"{}\"", n.replace('"', "\"\""));
let is_vtab = |sql: &Option<String>| {
matches!(
sql.as_deref().map(sql::parse_one),
Some(Ok(Statement::CreateVirtualTable(_)))
)
};
let vtab_names: alloc::collections::BTreeSet<String> = objs
.iter()
.filter(|(ty, _, sql)| *ty == ObjectType::Table && is_vtab(sql))
.map(|(_, n, _)| n.clone())
.collect();
let table_names: alloc::collections::BTreeSet<String> = objs
.iter()
.filter(|(ty, _, _)| *ty == ObjectType::Table)
.map(|(_, n, _)| n.clone())
.collect();
let is_backing = |name: &str| {
[
"_data", "_node", "_rowid", "_parent", "_content", "_docsize", "_config", "_idx",
]
.iter()
.any(|sfx| {
name.strip_suffix(sfx)
.is_some_and(|p| vtab_names.contains(p))
})
};
let persistent_vtab = |name: &str| {
vtab_names.contains(name)
&& (table_names.contains(&alloc::format!("{name}_data"))
|| table_names.contains(&alloc::format!("{name}_node")))
};
let mut tmp = Connection::open_memory()?;
for (ty, name, sql) in &objs {
if *ty == ObjectType::Table && !is_backing(name) {
if let Some(s) = sql {
tmp.execute(s)?;
}
}
}
for (ty, _, sql) in &objs {
if *ty == ObjectType::Index {
if let Some(s) = sql {
tmp.execute(s)?;
}
}
}
for (ty, name, _) in &objs {
if *ty != ObjectType::Table
|| is_backing(name)
|| (vtab_names.contains(name) && !persistent_vtab(name))
{
continue;
}
let result = self.query(&alloc::format!("SELECT * FROM {}", quote(name)))?;
let ncols = result.columns.len();
if ncols == 0 {
continue;
}
let placeholders = (1..=ncols)
.map(|i| alloc::format!("?{i}"))
.collect::<Vec<_>>()
.join(",");
let stmt = alloc::format!("INSERT INTO {} VALUES ({placeholders})", quote(name));
for row in result.rows {
let params = Params {
positional: row,
named: Vec::new(),
};
tmp.execute_params(&stmt, ¶ms)?;
}
}
for (ty, _, sql) in &objs {
if *ty == ObjectType::View {
if let Some(s) = sql {
tmp.execute(s)?;
}
}
}
for (ty, _, sql) in &objs {
if *ty == ObjectType::Trigger {
if let Some(s) = sql {
tmp.execute(s)?;
}
}
}
let count = tmp.backend.source().page_count();
let mut image = Vec::with_capacity(count as usize);
for n in 1..=count {
image.push(tmp.backend.source().page(n)?.data().to_vec());
}
if let Some(expr) = into {
return self.vacuum_write_into(expr, image);
}
self.backend.writer()?.replace_image(image)?;
if user_version != 0 {
self.backend.writer()?.header_mut().user_version = user_version;
let mut page1 = self.backend.writer()?.read_page(1)?;
self.backend.writer()?.header().write_to(&mut page1)?;
self.backend.writer()?.write_page(1, page1)?;
self.backend.writer()?.commit()?;
}
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
#[cfg(feature = "std")]
fn vacuum_write_into(&self, expr: &Expr, image: Vec<Vec<u8>>) -> Result<()> {
let params = Params::default();
let path = match eval::eval(expr, &EvalCtx::rowless(¶ms))? {
Value::Null => return Err(Error::Error("VACUUM INTO target is NULL".into())),
Value::Text(s) => s,
other => eval::to_text(&other),
};
if std::path::Path::new(&path).exists() {
return Err(Error::Error(alloc::format!(
"output file already exists: {path}"
)));
}
let user_version = self.backend.source().header().user_version;
let mut dst = Connection::create(&path)?;
dst.backend.writer()?.replace_image(image)?;
if user_version != 0 {
dst.backend.writer()?.header_mut().user_version = user_version;
}
let mut page1 = dst.backend.writer()?.read_page(1)?;
dst.backend.writer()?.header().write_to(&mut page1)?;
dst.backend.writer()?.write_page(1, page1)?;
dst.backend.writer()?.commit()?;
Ok(())
}
#[cfg(not(feature = "std"))]
fn vacuum_write_into(&self, _expr: &Expr, _image: Vec<Vec<u8>>) -> Result<()> {
Err(Error::Error(
"VACUUM INTO requires the std feature (file I/O)".into(),
))
}
fn exec_analyze(&mut self, target: Option<&str>) -> Result<()> {
use crate::schema::ObjectType;
let analyze: Vec<String> = match target {
None => self
.schema
.objects()
.iter()
.filter(|o| o.obj_type == ObjectType::Table && !o.name.starts_with("sqlite_"))
.map(|o| o.name.clone())
.collect(),
Some(name) => {
if let Some(t) = self.schema.table(name) {
alloc::vec![t.name.clone()]
} else if let Some(ix) = self.schema.index(name) {
alloc::vec![ix.tbl_name.clone()]
} else if name.eq_ignore_ascii_case("main") {
self.schema
.objects()
.iter()
.filter(|o| {
o.obj_type == ObjectType::Table && !o.name.starts_with("sqlite_")
})
.map(|o| o.name.clone())
.collect()
} else if is_main_schema_table(name)
|| name.eq_ignore_ascii_case("temp")
|| self
.attached
.iter()
.any(|a| a.name.eq_ignore_ascii_case(name))
{
Vec::new()
} else {
return Err(Error::Error(format!("no such table: {name}")));
}
}
};
let mut new_rows: Vec<(String, Option<String>, String)> = Vec::new();
for tname in &analyze {
let meta = self.table_meta(tname, None)?;
let rows: Vec<Vec<Value>> = if meta.without_rowid {
self.scan_without_rowid(&meta)?
} else {
self.scan_table(&meta)?
.into_iter()
.map(|(_, v)| v)
.collect()
};
let n = rows.len();
let indexes = self.indexes_of(tname)?;
if indexes.is_empty() {
if n > 0 {
new_rows.push((tname.clone(), None, alloc::format!("{n}")));
}
} else {
for idx in &indexes {
if n == 0 {
continue; }
let stat = index_stat_string(&idx.cols, &idx.collations, &rows);
new_rows.push((tname.clone(), Some(idx.name.clone()), stat));
}
}
}
if self.schema.table("sqlite_stat1").is_none() {
const STAT1_SQL: &str = "CREATE TABLE sqlite_stat1(tbl,idx,stat)";
let Statement::CreateTable(ct) = sql::parse_one(STAT1_SQL)? else {
unreachable!()
};
self.exec_create_table(&ct, STAT1_SQL)?;
}
let stat_root = self.schema.table("sqlite_stat1").unwrap().rootpage;
let stat_meta = self.table_meta("sqlite_stat1", None)?;
let victims: Vec<i64> = self
.scan_table(&stat_meta)?
.into_iter()
.filter(
|(_, vals)| matches!(&vals[0], Value::Text(t) if analyze.iter().any(|a| a == t)),
)
.map(|(rid, _)| rid)
.collect();
for rid in victims {
delete_table(self.backend.writer()?, stat_root, rid)?;
}
let base = self.next_rowid(stat_root)?;
for (i, (tbl, idx, stat)) in new_rows.into_iter().enumerate() {
let rec = encode_record(&[
Value::Text(tbl),
idx.map_or(Value::Null, Value::Text),
Value::Text(stat),
]);
insert_table(self.backend.writer()?, stat_root, base + i as i64, &rec)?;
}
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
fn exec_create_table(&mut self, ct: &CreateTable, sql_text: &str) -> Result<()> {
if let Some(select) = &ct.as_select {
return self.exec_create_table_as_select(ct, select);
}
let reprinted;
let sql_text = if ct.schema.is_some() {
reprinted = sql::print::create_table(ct);
reprinted.as_str()
} else {
sql_text
};
if self.schema.table(&ct.name).is_some() {
if ct.if_not_exists {
return Ok(());
}
return Err(Error::Error(format!("table {} already exists", ct.name)));
}
if ct.strict {
for c in &ct.columns {
if strict_column_type(c.type_name.as_deref()).is_none() {
return Err(match &c.type_name {
Some(t) => Error::Error(format!(
"unknown datatype for {}.{}: \"{t}\"",
ct.name, c.name
)),
None => {
Error::Error(format!("missing datatype for {}.{}", ct.name, c.name))
}
});
}
}
}
for c in &ct.columns {
for k in &c.constraints {
match k {
ColumnConstraint::Check(e, _) if expr_has_subquery(e) => {
return Err(Error::Error(
"subqueries prohibited in CHECK constraints".into(),
));
}
ColumnConstraint::Generated { expr, .. } if expr_has_subquery(expr) => {
return Err(Error::Error(
"subqueries prohibited in generated columns".into(),
));
}
ColumnConstraint::Generated { expr, .. } if expr_is_nondeterministic(expr) => {
return Err(Error::Error(
"non-deterministic functions prohibited in generated columns".into(),
));
}
_ => {}
}
}
}
let table_pk_cols: Vec<&str> = ct
.constraints
.iter()
.filter_map(|tc| match tc {
TableConstraint::PrimaryKey(cols, _) => Some(cols),
_ => None,
})
.flatten()
.map(String::as_str)
.collect();
for c in &ct.columns {
let generated = c
.constraints
.iter()
.filter(|k| matches!(k, ColumnConstraint::Generated { .. }))
.count();
if generated == 0 {
continue;
}
if generated > 1 {
return Err(Error::Error(alloc::format!(
"error in generated column \"{}\"",
c.name
)));
}
if c.constraints
.iter()
.any(|k| matches!(k, ColumnConstraint::Default(_)))
{
return Err(Error::Error(
"cannot use DEFAULT on a generated column".into(),
));
}
let in_primary_key = c
.constraints
.iter()
.any(|k| matches!(k, ColumnConstraint::PrimaryKey { .. }))
|| table_pk_cols
.iter()
.any(|p| p.eq_ignore_ascii_case(&c.name));
if in_primary_key {
return Err(Error::Error(
"generated columns cannot be part of the PRIMARY KEY".into(),
));
}
}
for c in &ct.columns {
for k in &c.constraints {
let agg = match k {
ColumnConstraint::Check(e, _) | ColumnConstraint::Generated { expr: e, .. } => {
first_aggregate_call_name(e)
}
_ => None,
};
if let Some(name) = agg {
return Err(Error::Error(format!(
"misuse of aggregate function {name}()"
)));
}
}
}
for tc in &ct.constraints {
if let TableConstraint::Check(e, _) = tc {
if expr_has_subquery(e) {
return Err(Error::Error(
"subqueries prohibited in CHECK constraints".into(),
));
}
if let Some(name) = first_aggregate_call_name(e) {
return Err(Error::Error(format!(
"misuse of aggregate function {name}()"
)));
}
}
}
let known: Vec<String> = ct.columns.iter().map(|c| c.name.clone()).collect();
for c in &ct.columns {
for k in &c.constraints {
if let ColumnConstraint::Collate(name) = k {
if crate::value::Collation::parse(name).is_none() {
return Err(Error::Error(format!("no such collation sequence: {name}")));
}
}
let bad = match k {
ColumnConstraint::Check(e, _) => {
unknown_column_ref(e, &known, true, Some(&ct.name))
}
ColumnConstraint::Generated { expr, .. } => {
unknown_column_ref(expr, &known, false, Some(&ct.name))
}
_ => None,
};
if let Some(col) = bad {
return Err(Error::Error(format!("no such column: {col}")));
}
if let ColumnConstraint::Generated { expr, .. } = k {
if has_resolved_dotted_ref(expr, &known, false, &ct.name) {
return Err(Error::Error(
"the \".\" operator prohibited in generated columns".into(),
));
}
}
if let ColumnConstraint::Default(e) = k {
if unknown_column_ref(e, &[], false, None).is_some() {
return Err(Error::Error(format!(
"default value of column [{}] is not constant",
c.name
)));
}
}
if let ColumnConstraint::References(fk) = k {
if fk.ref_columns.len() > 1 {
return Err(Error::Error(format!(
"foreign key on {} should reference only one column of table {}",
c.name, fk.ref_table
)));
}
}
}
}
for tc in &ct.constraints {
if let TableConstraint::Check(e, _) = tc {
if let Some(col) = unknown_column_ref(e, &known, true, Some(&ct.name)) {
return Err(Error::Error(format!("no such column: {col}")));
}
}
if let TableConstraint::ForeignKey(fk) = tc {
for col in &fk.columns {
if !known.iter().any(|k| k.eq_ignore_ascii_case(col)) {
return Err(Error::Error(format!(
"unknown column \"{col}\" in foreign key definition"
)));
}
}
if !fk.ref_columns.is_empty() && fk.ref_columns.len() != fk.columns.len() {
return Err(Error::Error(
"number of columns in foreign key does not match the number of \
columns in the referenced table"
.into(),
));
}
}
}
if !ct.columns.is_empty()
&& ct.columns.iter().all(|c| {
c.constraints
.iter()
.any(|k| matches!(k, ColumnConstraint::Generated { .. }))
})
{
return Err(Error::Error(
"must have at least one non-generated column".into(),
));
}
for (i, c) in ct.columns.iter().enumerate() {
if ct.columns[..i]
.iter()
.any(|p| p.name.eq_ignore_ascii_case(&c.name))
{
return Err(Error::Error(alloc::format!(
"duplicate column name: {}",
c.name
)));
}
}
let pk_count = ct
.columns
.iter()
.flat_map(|c| &c.constraints)
.filter(|k| matches!(k, ColumnConstraint::PrimaryKey { .. }))
.count()
+ ct.constraints
.iter()
.filter(|tc| matches!(tc, TableConstraint::PrimaryKey(..)))
.count();
if pk_count > 1 {
return Err(Error::Error(alloc::format!(
"table \"{}\" has more than one primary key",
ct.name
)));
}
for tc in &ct.constraints {
let cols = match tc {
TableConstraint::PrimaryKey(cols, _) | TableConstraint::Unique(cols, _) => cols,
_ => continue,
};
for name in cols {
if !ct.columns.iter().any(|c| c.name.eq_ignore_ascii_case(name)) {
return Err(Error::Error(alloc::format!("no such column: {name}")));
}
}
}
let ipk = find_integer_primary_key(ct);
let has_autoinc = |i: usize| {
ct.columns[i].constraints.iter().any(|k| {
matches!(
k,
ColumnConstraint::PrimaryKey {
autoincrement: true,
..
}
)
})
};
if (0..ct.columns.len()).any(has_autoinc) {
if ct.without_rowid {
return Err(Error::Error(
"AUTOINCREMENT not allowed on WITHOUT ROWID tables".into(),
));
}
if !(0..ct.columns.len()).any(|i| has_autoinc(i) && Some(i) == ipk) {
return Err(Error::Error(
"AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY".into(),
));
}
}
let root = if ct.without_rowid {
if primary_key_positions(ct).is_empty() {
return Err(Error::Error(format!(
"PRIMARY KEY missing on table {}",
ct.name
)));
}
create_index_root(self.backend.writer()?)?
} else {
create_table_root(self.backend.writer()?)?
};
let next = self.next_rowid(crate::schema::SCHEMA_ROOT_PAGE)?;
let row = encode_record(&[
Value::Text("table".into()),
Value::Text(ct.name.clone()),
Value::Text(ct.name.clone()),
Value::Integer(root as i64),
Value::Text(sql_text.into()),
]);
insert_table(
self.backend.writer()?,
crate::schema::SCHEMA_ROOT_PAGE,
next,
&row,
)?;
let ipk = if ct.without_rowid {
None
} else {
find_integer_primary_key(ct)
};
let unique = collect_unique_sets(ct, ipk);
let pk = if ct.without_rowid {
primary_key_positions(ct)
} else {
Vec::new()
};
let mut schema_rowid = next + 1;
for (n, (set, _)) in unique.iter().enumerate() {
if ct.without_rowid && *set == pk {
continue;
}
let idx_root = create_index_root(self.backend.writer()?)?;
let idx_row = encode_record(&[
Value::Text("index".into()),
Value::Text(alloc::format!("sqlite_autoindex_{}_{}", ct.name, n + 1)),
Value::Text(ct.name.clone()),
Value::Integer(idx_root as i64),
Value::Null, ]);
insert_table(
self.backend.writer()?,
crate::schema::SCHEMA_ROOT_PAGE,
schema_rowid,
&idx_row,
)?;
schema_rowid += 1;
}
let is_autoinc = ipk.is_some_and(|i| {
ct.columns[i].constraints.iter().any(|k| {
matches!(
k,
ColumnConstraint::PrimaryKey {
autoincrement: true,
..
}
)
})
});
if is_autoinc && self.schema.table("sqlite_sequence").is_none() {
const SEQ_SQL: &str = "CREATE TABLE sqlite_sequence(name,seq)";
let Statement::CreateTable(seq_ct) = sql::parse_one(SEQ_SQL)? else {
unreachable!()
};
self.exec_create_table(&seq_ct, SEQ_SQL)?;
}
let cookie = self
.backend
.writer()?
.header()
.schema_cookie
.wrapping_add(1);
self.backend.writer()?.header_mut().schema_cookie = cookie;
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
fn exec_create_table_as_select(&mut self, ct: &CreateTable, select: &Select) -> Result<()> {
if self.schema.table(&ct.name).is_some() {
if ct.if_not_exists {
return Ok(());
}
return Err(Error::Error(format!("table {} already exists", ct.name)));
}
let result = self.run_select(select, &Params::default())?;
let mut counts: alloc::collections::BTreeMap<String, usize> =
alloc::collections::BTreeMap::new();
let deduped: Vec<String> = result
.columns
.iter()
.map(|c| {
let n = counts.entry(c.to_ascii_lowercase()).or_insert(0);
let name = if *n == 0 {
c.clone()
} else {
alloc::format!("{c}:{n}")
};
*n += 1;
name
})
.collect();
let cols = deduped
.iter()
.map(|c| crate::sql::print::ident(c))
.collect::<Vec<_>>()
.join(", ");
let create_sql = format!(
"CREATE TABLE {}({cols})",
crate::sql::print::ident(&ct.name)
);
let Statement::CreateTable(syn) = sql::parse_one(&create_sql)? else {
return Err(Error::Corrupt("generated CTAS schema is invalid".into()));
};
self.exec_create_table(&syn, &create_sql)?;
if !result.rows.is_empty() {
let value_rows: Vec<Vec<Expr>> = result
.rows
.into_iter()
.map(|row| {
row.into_iter()
.map(|v| Expr::Literal(value_to_literal(v)))
.collect()
})
.collect();
let ins = Insert {
table: ct.name.clone(),
schema: None,
columns: Vec::new(),
source: InsertSource::Values(value_rows),
on_conflict: OnConflict::Abort,
on_conflict_explicit: true,
upsert: Vec::new(),
returning: Vec::new(),
};
self.exec_insert(&ins, &Params::default())?;
}
Ok(())
}
fn exec_pragma(&mut self, p: &Pragma, params: &Params) -> Result<()> {
if p.name.eq_ignore_ascii_case("foreign_keys") {
if let Some(e) = &p.value {
self.foreign_keys = pragma_truth(e, params);
}
} else if p.name.eq_ignore_ascii_case("recursive_triggers") {
if let Some(e) = &p.value {
self.recursive_triggers = pragma_truth(e, params);
}
} else if p.name.eq_ignore_ascii_case("cache_size") {
if let Some(e) = &p.value {
self.cache_size
.set(eval::to_i64(&eval::eval(e, &EvalCtx::rowless(params))?));
}
} else if p.name.eq_ignore_ascii_case("analysis_limit") {
if let Some(e) = &p.value {
let v = eval::to_i64(&eval::eval(e, &EvalCtx::rowless(params))?);
self.analysis_limit.set(v.max(0));
}
} else if p.name.eq_ignore_ascii_case("busy_timeout") {
if let Some(e) = &p.value {
let v = eval::to_i64(&eval::eval(e, &EvalCtx::rowless(params))?);
self.busy_timeout.set(v.max(0));
}
} else if p.name.eq_ignore_ascii_case("secure_delete") {
if let Some(e) = &p.value {
let v = match pragma_text(e).to_ascii_lowercase().as_str() {
"fast" => 2,
_ if pragma_truth(e, params) => 1,
_ => 0,
};
self.secure_delete.set(v);
if let Backend::Write(w) = &mut self.backend {
w.set_secure_delete(v != 0);
}
}
} else if p.name.eq_ignore_ascii_case("journal_mode") {
if let Some(e) = &p.value {
if pragma_text(e).eq_ignore_ascii_case("wal") {
self.backend.writer()?.set_wal_mode()?;
}
}
} else if p.name.eq_ignore_ascii_case("wal_checkpoint") {
self.backend.writer()?.checkpoint()?;
} else if p.name.eq_ignore_ascii_case("user_version") {
if let Some(e) = &p.value {
let v = eval::to_i64(&eval::eval(e, &EvalCtx::rowless(params))?) as u32;
self.backend.writer()?.header_mut().user_version = v;
}
} else if p.name.eq_ignore_ascii_case("application_id") {
if let Some(e) = &p.value {
let v = eval::to_i64(&eval::eval(e, &EvalCtx::rowless(params))?) as u32;
self.backend.writer()?.header_mut().application_id = v;
}
} else if p.name.eq_ignore_ascii_case("auto_vacuum") {
if let Some(e) = &p.value {
let mode = match pragma_text(e).to_ascii_lowercase().as_str() {
"none" => 0,
"full" => 1,
"incremental" => 2,
_ => eval::to_i64(&eval::eval(e, &EvalCtx::rowless(params))?),
};
let target = match mode {
0 => AutoVacuum::None,
1 => AutoVacuum::Full,
2 => AutoVacuum::Incremental,
_ => return Err(Error::Error(format!("invalid auto_vacuum mode {mode}"))),
};
self.backend.writer()?.set_auto_vacuum_if_empty(target)?;
}
} else if p.name.eq_ignore_ascii_case("incremental_vacuum") {
let n = match &p.value {
Some(e) => eval::to_i64(&eval::eval(e, &EvalCtx::rowless(params))?),
None => 0,
};
self.backend.writer()?.incremental_vacuum(n)?;
}
Ok(())
}
fn foreign_keys_of(&self, table: &str) -> Result<Vec<ForeignKey>> {
let Some(obj) = self.schema.table(table) else {
return Ok(Vec::new());
};
let Some(sql) = &obj.sql else {
return Ok(Vec::new());
};
let Statement::CreateTable(ct) = sql::parse_one(sql)? else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for col in &ct.columns {
for c in &col.constraints {
if let ColumnConstraint::References(fk) = c {
out.push(self.resolve_fk(fk)?);
}
}
}
for c in &ct.constraints {
if let TableConstraint::ForeignKey(fk) = c {
out.push(self.resolve_fk(fk)?);
}
}
Ok(out)
}
fn resolve_fk(&self, fk: &ForeignKey) -> Result<ForeignKey> {
let mut fk = fk.clone();
if fk.ref_columns.is_empty() {
fk.ref_columns = self.primary_key_columns(&fk.ref_table)?;
}
Ok(fk)
}
fn primary_key_columns(&self, table: &str) -> Result<Vec<String>> {
let Some(obj) = self.schema.table(table) else {
return Err(Error::Error(format!("no such table: {table}")));
};
let sql = obj.sql.as_deref().unwrap_or("");
let Statement::CreateTable(ct) = sql::parse_one(sql)? else {
return Ok(Vec::new());
};
for col in &ct.columns {
if col
.constraints
.iter()
.any(|c| matches!(c, ColumnConstraint::PrimaryKey { .. }))
{
return Ok(alloc::vec![col.name.clone()]);
}
}
for c in &ct.constraints {
if let TableConstraint::PrimaryKey(cols, _) = c {
return Ok(cols.clone());
}
}
Ok(Vec::new())
}
fn check_fk_child(&self, table: &str, meta: &TableMeta, values: &[Value]) -> Result<()> {
if !self.foreign_keys {
return Ok(());
}
for fk in self.foreign_keys_of(table)? {
if fk.initially_deferred && self.in_tx {
continue;
}
let key = match self.child_key_values(meta, &fk, values) {
Some(k) => k,
None => continue, };
if !self.parent_has_key(&fk, &key)? {
return Err(Error::Constraint("FOREIGN KEY constraint failed".into()));
}
}
Ok(())
}
fn check_deferred_fks(&self) -> Result<()> {
if !self.foreign_keys {
return Ok(());
}
for obj in self.schema.objects() {
if obj.obj_type != crate::schema::ObjectType::Table {
continue;
}
let fks: Vec<ForeignKey> = self
.foreign_keys_of(&obj.name)?
.into_iter()
.filter(|fk| fk.initially_deferred)
.collect();
if fks.is_empty() {
continue;
}
let meta = self.table_meta(&obj.name, None)?;
for (_, row) in self.scan_table(&meta)? {
for fk in &fks {
if let Some(key) = self.child_key_values(&meta, fk, &row) {
if !self.parent_has_key(fk, &key)? {
return Err(Error::Constraint("FOREIGN KEY constraint failed".into()));
}
}
}
}
}
Ok(())
}
fn child_key_values(
&self,
meta: &TableMeta,
fk: &ForeignKey,
values: &[Value],
) -> Option<Vec<Value>> {
let mut key = Vec::with_capacity(fk.columns.len());
for cname in &fk.columns {
let pos = meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(cname))?;
let v = values.get(pos)?;
if matches!(v, Value::Null) {
return None;
}
key.push(v.clone());
}
Some(key)
}
fn parent_has_key(&self, fk: &ForeignKey, key: &[Value]) -> Result<bool> {
let pmeta = self.table_meta(&fk.ref_table, None)?;
let positions = self.column_positions(&pmeta, &fk.ref_columns)?;
for (_, row) in self.scan_table(&pmeta)? {
if positions.iter().zip(key).all(|(&p, k)| {
let (pv, kv) = eval::apply_comparison_affinity(
row[p].clone(),
Some(pmeta.columns[p].affinity),
k.clone(),
None,
);
crate::value::cmp_values_coll(&pv, &kv, pmeta.columns[p].collation)
== core::cmp::Ordering::Equal
}) {
return Ok(true);
}
}
Ok(false)
}
fn column_positions(&self, meta: &TableMeta, names: &[String]) -> Result<Vec<usize>> {
names
.iter()
.map(|n| {
meta.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(n))
.ok_or_else(|| Error::Error(format!("no such column: {n}")))
})
.collect()
}
fn enforce_parent_change(
&mut self,
parent_table: &str,
old_vals: &[Value],
new_vals: Option<&[Value]>,
params: &Params,
) -> Result<()> {
if !self.foreign_keys {
return Ok(());
}
let table_names: Vec<String> = self
.schema
.objects()
.iter()
.filter(|o| o.obj_type == crate::schema::ObjectType::Table)
.map(|o| o.name.clone())
.collect();
let mut referencing: Vec<(String, ForeignKey)> = Vec::new();
for name in table_names {
for fk in self.foreign_keys_of(&name)? {
if fk.ref_table.eq_ignore_ascii_case(parent_table) {
referencing.push((name.clone(), fk));
}
}
}
if referencing.is_empty() {
return Ok(());
}
let pmeta = self.table_meta(parent_table, None)?;
for (child_table, fk) in referencing {
let ppos = self.column_positions(&pmeta, &fk.ref_columns)?;
let old_key: Vec<Value> = ppos.iter().map(|&p| old_vals[p].clone()).collect();
if old_key.iter().any(|v| matches!(v, Value::Null)) {
continue;
}
let is_delete = new_vals.is_none();
let action = if is_delete {
fk.on_delete
} else {
fk.on_update
};
if action == FkAction::NoAction && fk.initially_deferred && self.in_tx {
continue;
}
if let Some(nv) = new_vals {
let new_key: Vec<Value> = ppos.iter().map(|&p| nv[p].clone()).collect();
if new_key
.iter()
.zip(&old_key)
.all(|(a, b)| eval::compare(a, b) == core::cmp::Ordering::Equal)
{
continue;
}
}
self.apply_fk_action(&child_table, &fk, &old_key, new_vals, &ppos, action, params)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn apply_fk_action(
&mut self,
child_table: &str,
fk: &ForeignKey,
old_key: &[Value],
new_parent: Option<&[Value]>,
parent_pos: &[usize],
action: FkAction,
params: &Params,
) -> Result<()> {
let cmeta = self.table_meta(child_table, None)?;
let cpos = self.column_positions(&cmeta, &fk.columns)?;
let pmeta = self.table_meta(&fk.ref_table, None)?;
let mut matches: Vec<i64> = Vec::new();
for (rowid, row) in self.scan_table(&cmeta)? {
if cpos
.iter()
.zip(old_key)
.zip(parent_pos)
.all(|((&cp, k), &pp)| {
let (pv, kv) = eval::apply_comparison_affinity(
k.clone(),
Some(pmeta.columns[pp].affinity),
row[cp].clone(),
None,
);
crate::value::cmp_values_coll(&pv, &kv, pmeta.columns[pp].collation)
== core::cmp::Ordering::Equal
})
{
matches.push(rowid);
}
}
if matches.is_empty() {
return Ok(());
}
match action {
FkAction::NoAction | FkAction::Restrict => {
Err(Error::Constraint("FOREIGN KEY constraint failed".into()))
}
FkAction::Cascade if new_parent.is_none() => {
for rowid in matches {
self.delete_row_cascade(child_table, &cmeta, rowid, params)?;
}
Ok(())
}
FkAction::Cascade => {
let new_parent = new_parent.unwrap();
let new_key: Vec<Value> =
parent_pos.iter().map(|&p| new_parent[p].clone()).collect();
for rowid in matches {
self.update_child_key(&cmeta, child_table, rowid, &cpos, &new_key)?;
}
Ok(())
}
FkAction::SetNull => {
let nulls = alloc::vec![Value::Null; cpos.len()];
for rowid in matches {
self.update_child_key(&cmeta, child_table, rowid, &cpos, &nulls)?;
}
Ok(())
}
FkAction::SetDefault => {
let defaults: Vec<Value> = cpos
.iter()
.map(|&p| match &cmeta.defaults[p] {
Some(e) => eval::eval(e, &EvalCtx::rowless(params)).unwrap_or(Value::Null),
None => Value::Null,
})
.collect();
for rowid in matches {
self.update_child_key(&cmeta, child_table, rowid, &cpos, &defaults)?;
}
Ok(())
}
}
}
fn delete_row_cascade(
&mut self,
table: &str,
meta: &TableMeta,
rowid: i64,
params: &Params,
) -> Result<()> {
let old = self.read_row(meta, rowid)?;
if let Some(old) = old {
self.enforce_parent_change(table, &old, None, params)?;
}
delete_table(self.backend.writer()?, meta.root, rowid)?;
let indexes = self.indexes_of(table)?;
if !indexes.is_empty() {
self.rebuild_indexes(meta, &indexes)?;
}
Ok(())
}
fn update_child_key(
&mut self,
meta: &TableMeta,
table: &str,
rowid: i64,
positions: &[usize],
new_vals: &[Value],
) -> Result<()> {
let Some(mut row) = self.read_row(meta, rowid)? else {
return Ok(());
};
for (&p, v) in positions.iter().zip(new_vals) {
row[p] = v.clone();
}
let mut stored = row.clone();
if let Some(ipk) = meta.ipk {
stored[ipk] = Value::Null;
}
let record = encode_record(&stored);
insert_table(self.backend.writer()?, meta.root, rowid, &record)?;
let indexes = self.indexes_of(table)?;
if !indexes.is_empty() {
self.rebuild_indexes(meta, &indexes)?;
}
Ok(())
}
fn read_row(&self, meta: &TableMeta, rowid: i64) -> Result<Option<Vec<Value>>> {
let encoding = self.backend.source().header().text_encoding;
let mut cur = TableCursor::new(self.backend.source(), meta.root);
if cur.seek(rowid)? {
let values = self.decode_full_row(meta, rowid, &cur.payload()?, encoding)?;
Ok(Some(values))
} else {
Ok(None)
}
}
fn exec_create_trigger(&mut self, ct: &CreateTrigger, sql_text: &str) -> Result<()> {
let stripped;
let sql_text = match ct.schema.as_deref() {
Some(s) => {
stripped = strip_schema_qualifier(sql_text, s)?;
stripped.as_str()
}
None => sql_text,
};
if self
.schema
.objects()
.iter()
.any(|o| o.name.eq_ignore_ascii_case(&ct.name))
{
if ct.if_not_exists {
return Ok(());
}
return Err(Error::Error(format!("trigger {} already exists", ct.name)));
}
let table_in_other = self
.temp_db
.as_ref()
.is_some_and(|t| t.schema.table(&ct.table).is_some());
if self.schema.table(&ct.table).is_none() && !table_in_other && !self.is_view(&ct.table) {
return Err(Error::Error(format!(
"no such table: {}.{}",
ct.schema.as_deref().unwrap_or("main"),
ct.table
)));
}
let target_is_view = self.is_view(&ct.table);
match ct.timing {
TriggerTiming::InsteadOf if !target_is_view => {
return Err(Error::Error(format!(
"cannot create INSTEAD OF trigger on table: {}",
ct.table
)));
}
TriggerTiming::Before | TriggerTiming::After if target_is_view => {
let kind = if ct.timing == TriggerTiming::Before {
"BEFORE"
} else {
"AFTER"
};
return Err(Error::Error(format!(
"cannot create {kind} trigger on view: {}",
ct.table
)));
}
_ => {}
}
let next = self.next_rowid(crate::schema::SCHEMA_ROOT_PAGE)?;
let row = encode_record(&[
Value::Text("trigger".into()),
Value::Text(ct.name.clone()),
Value::Text(ct.table.clone()),
Value::Integer(0),
Value::Text(sql_text.into()),
]);
insert_table(
self.backend.writer()?,
crate::schema::SCHEMA_ROOT_PAGE,
next,
&row,
)?;
let cookie = self
.backend
.writer()?
.header()
.schema_cookie
.wrapping_add(1);
self.backend.writer()?.header_mut().schema_cookie = cookie;
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
fn triggers_for(
&self,
table: &str,
kind: TrigEvent,
timing: TriggerTiming,
) -> Result<Vec<CreateTrigger>> {
let mut out = Vec::new();
self.collect_triggers(self.schema.objects(), table, kind, timing, &mut out);
if let Some(t) = &self.temp_db {
self.collect_triggers(t.schema.objects(), table, kind, timing, &mut out);
}
out.reverse();
Ok(out)
}
fn collect_triggers(
&self,
objects: &[crate::schema::SchemaObject],
table: &str,
kind: TrigEvent,
timing: TriggerTiming,
out: &mut Vec<CreateTrigger>,
) {
for obj in objects {
if obj.obj_type != crate::schema::ObjectType::Trigger
|| !obj.tbl_name.eq_ignore_ascii_case(table)
{
continue;
}
let Some(sql) = &obj.sql else { continue };
let Ok(Statement::CreateTrigger(ct)) = sql::parse_one(sql) else {
continue;
};
let event_ok = matches!(
(&ct.event, kind),
(TriggerEvent::Insert, TrigEvent::Insert)
| (TriggerEvent::Delete, TrigEvent::Delete)
| (TriggerEvent::Update(_), TrigEvent::Update)
);
if ct.timing == timing && event_ok {
out.push(ct);
}
}
}
#[allow(clippy::too_many_arguments)]
fn fire_triggers(
&mut self,
table: &str,
kind: TrigEvent,
timing: TriggerTiming,
columns: &[ColumnInfo],
old: Option<(&[Value], i64)>,
new: Option<(&[Value], i64)>,
params: &Params,
changed_cols: Option<&[String]>,
) -> Result<bool> {
let depth = self.trigger_depth.get();
let limit = if self.recursive_triggers { 1000 } else { 1 };
if depth >= limit {
return if self.recursive_triggers {
Err(Error::Error("too many levels of trigger recursion".into()))
} else {
Ok(false)
};
}
let mut trigs = self.triggers_for(table, kind, timing)?;
if let Some(changed) = changed_cols {
trigs.retain(|t| match &t.event {
TriggerEvent::Update(cols) if !cols.is_empty() => cols
.iter()
.any(|c| changed.iter().any(|ch| ch.eq_ignore_ascii_case(c))),
_ => true,
});
}
if trigs.is_empty() {
return Ok(false);
}
self.trigger_depth.set(depth + 1);
let base = self.outer_scope.borrow().len();
if let Some((vals, rid)) = old {
self.push_row_frame("old", columns, vals, rid);
}
if let Some((vals, rid)) = new {
self.push_row_frame("new", columns, vals, rid);
}
let result = self.run_trigger_bodies(&trigs, params);
self.outer_scope.borrow_mut().truncate(base);
self.trigger_depth.set(depth);
result.map(|()| true)
}
fn push_row_frame(&self, label: &str, columns: &[ColumnInfo], values: &[Value], rowid: i64) {
let columns = columns
.iter()
.map(|c| ColumnInfo {
name: c.name.clone(),
table: String::from(label),
affinity: c.affinity,
collation: c.collation,
})
.collect();
self.outer_scope.borrow_mut().push(OuterFrame {
columns,
row: values.to_vec(),
rowid: Some(rowid),
});
}
fn is_view(&self, name: &str) -> bool {
self.temp_has_view(name)
|| self.schema.objects().iter().any(|o| {
o.obj_type == crate::schema::ObjectType::View && o.name.eq_ignore_ascii_case(name)
})
}
fn temp_has_view(&self, name: &str) -> bool {
self.temp_db.as_ref().is_some_and(|t| {
t.schema.objects().iter().any(|o| {
o.obj_type == crate::schema::ObjectType::View && o.name.eq_ignore_ascii_case(name)
})
})
}
fn view_columns(&self, name: &str, params: &Params) -> Result<Vec<ColumnInfo>> {
match self.try_view(name, None, params)? {
Some((cols, _)) => Ok(cols),
None => Err(Error::Error(format!("no such view: {name}"))),
}
}
fn exec_view_insert(
&mut self,
ins: &Insert,
rows: &[Vec<Expr>],
params: &Params,
) -> Result<usize> {
let cols = self.view_columns(&ins.table, params)?;
if self
.triggers_for(&ins.table, TrigEvent::Insert, TriggerTiming::InsteadOf)?
.is_empty()
{
return Err(Error::Error(format!(
"cannot modify {} because it is a view",
ins.table
)));
}
let target: Vec<usize> = if ins.columns.is_empty() {
(0..cols.len()).collect()
} else {
ins.columns
.iter()
.map(|name| {
cols.iter()
.position(|c| c.name.eq_ignore_ascii_case(name))
.ok_or_else(|| Error::Error(format!("no such column: {name}")))
})
.collect::<Result<_>>()?
};
let mut affected = 0;
for row_exprs in rows {
let ctx = EvalCtx::rowless(params).with_subqueries(self);
let mut new = alloc::vec![Value::Null; cols.len()];
for (i, e) in row_exprs.iter().enumerate() {
new[target[i]] = eval::eval(e, &ctx)?;
}
self.fire_triggers(
&ins.table,
TrigEvent::Insert,
TriggerTiming::InsteadOf,
&cols,
None,
Some((&new, 0)),
params,
None,
)?;
if self.raise_ignore.replace(false) {
continue;
}
affected += 1;
}
Ok(affected)
}
fn exec_view_delete(&mut self, del: &Delete, params: &Params) -> Result<usize> {
let (cols, rows) = self
.try_view(&del.table, None, params)?
.ok_or_else(|| Error::Error(format!("no such view: {}", del.table)))?;
if self
.triggers_for(&del.table, TrigEvent::Delete, TriggerTiming::InsteadOf)?
.is_empty()
{
return Err(Error::Error(format!(
"cannot modify {} because it is a view",
del.table
)));
}
let mut affected = 0;
for row in rows {
if let Some(p) = &del.where_clause {
let ctx = row_ctx(&row.values, &cols, None, params).with_subqueries(self);
if eval::truth(&eval::eval(p, &ctx)?) != Some(true) {
continue;
}
}
self.fire_triggers(
&del.table,
TrigEvent::Delete,
TriggerTiming::InsteadOf,
&cols,
Some((&row.values, 0)),
None,
params,
None,
)?;
if self.raise_ignore.replace(false) {
continue;
}
affected += 1;
}
Ok(affected)
}
fn apply_row_subquery_assignments(
&self,
row_assignments: &[(Vec<String>, Box<Select>)],
cols: &[ColumnInfo],
meta: Option<&TableMeta>,
ctx: &EvalCtx,
target: &mut [Value],
) -> Result<()> {
for (targets, select) in row_assignments {
let mut positions = Vec::with_capacity(targets.len());
for c in targets {
let pos = cols
.iter()
.position(|mc| mc.name.eq_ignore_ascii_case(c))
.ok_or_else(|| Error::Error(format!("no such column: {c}")))?;
if meta.is_some_and(|m| m.is_generated(pos)) {
return Err(Error::Error(format!(
"cannot UPDATE generated column \"{c}\""
)));
}
positions.push(pos);
}
let produced = eval::Subqueries::rows(self, select, ctx)?;
let first = produced.into_iter().next();
if let Some(r) = &first {
if r.len() != positions.len() {
return Err(Error::Error(format!(
"{} columns assigned {} values",
positions.len(),
r.len()
)));
}
}
for (i, &pos) in positions.iter().enumerate() {
target[pos] = first.as_ref().map_or(Value::Null, |r| r[i].clone());
}
}
Ok(())
}
fn exec_view_update(&mut self, upd: &Update, params: &Params) -> Result<usize> {
let (cols, rows) = self
.try_view(&upd.table, None, params)?
.ok_or_else(|| Error::Error(format!("no such view: {}", upd.table)))?;
if self
.triggers_for(&upd.table, TrigEvent::Update, TriggerTiming::InsteadOf)?
.is_empty()
{
return Err(Error::Error(format!(
"cannot modify {} because it is a view",
upd.table
)));
}
let mut changed: Vec<String> = upd.assignments.iter().map(|(c, _)| c.clone()).collect();
for (rcols, _) in &upd.row_assignments {
changed.extend(rcols.iter().cloned());
}
let mut affected = 0;
for row in rows {
let old = row.values.clone();
if let Some(p) = &upd.where_clause {
let ctx = row_ctx(&old, &cols, None, params).with_subqueries(self);
if eval::truth(&eval::eval(p, &ctx)?) != Some(true) {
continue;
}
}
let mut new = old.clone();
for (col, expr) in &upd.assignments {
let pos = cols
.iter()
.position(|c| c.name.eq_ignore_ascii_case(col))
.ok_or_else(|| Error::Error(format!("no such column: {col}")))?;
let ctx = row_ctx(&old, &cols, None, params).with_subqueries(self);
new[pos] = eval::eval(expr, &ctx)?;
}
if !upd.row_assignments.is_empty() {
let ctx = row_ctx(&old, &cols, None, params).with_subqueries(self);
self.apply_row_subquery_assignments(
&upd.row_assignments,
&cols,
None,
&ctx,
&mut new,
)?;
}
self.fire_triggers(
&upd.table,
TrigEvent::Update,
TriggerTiming::InsteadOf,
&cols,
Some((&old, 0)),
Some((&new, 0)),
params,
Some(&changed),
)?;
if self.raise_ignore.replace(false) {
continue;
}
affected += 1;
}
Ok(affected)
}
fn run_trigger_bodies(&mut self, trigs: &[CreateTrigger], params: &Params) -> Result<()> {
for trig in trigs {
if let Some(when) = &trig.when {
let fires = {
let ctx = EvalCtx::rowless(params).with_subqueries(self);
eval::truth(&eval::eval(when, &ctx)?) == Some(true)
};
if !fires {
continue;
}
}
for stmt in &trig.body {
match stmt {
Statement::Insert(ins) => {
self.exec_insert(ins, params)?;
}
Statement::Update(u) => {
self.exec_update(u, params)?;
}
Statement::Delete(d) => {
self.exec_delete(d, params)?;
}
Statement::Select(sel) => {
self.run_trigger_select(sel, params)?;
if self.raise_ignore.get() {
return Ok(());
}
}
_ => return Err(Error::Unsupported("statement type in trigger body")),
}
}
}
Ok(())
}
fn run_trigger_select(&self, sel: &Select, params: &Params) -> Result<()> {
if sel.from.is_none() {
if let Some(w) = &sel.where_clause {
let ctx = EvalCtx::rowless(params).with_subqueries(self);
if eval::truth(&eval::eval(w, &ctx)?) != Some(true) {
return Ok(());
}
}
}
for col in &sel.columns {
if let ResultColumn::Expr { expr, .. } = col {
self.eval_raise_expr(expr, params)?;
if self.raise_ignore.get() {
return Ok(());
}
}
}
Ok(())
}
fn eval_raise_expr(&self, expr: &Expr, params: &Params) -> Result<()> {
match expr {
Expr::Function { name, args, .. } if name.eq_ignore_ascii_case("raise") => {
self.fire_raise(args, params)
}
Expr::Paren(inner) => self.eval_raise_expr(inner, params),
Expr::Case {
operand,
when_then,
else_result,
} => {
let ctx = EvalCtx::rowless(params).with_subqueries(self);
let base = match operand {
Some(op) => Some(eval::eval(op, &ctx)?),
None => None,
};
for (when, then) in when_then {
let hit = match &base {
Some(b) => {
let w = eval::eval(when, &ctx)?;
crate::value::cmp_values(b, &w) == core::cmp::Ordering::Equal
}
None => eval::truth(&eval::eval(when, &ctx)?) == Some(true),
};
if hit {
return self.eval_raise_expr(then, params);
}
}
if let Some(e) = else_result {
return self.eval_raise_expr(e, params);
}
Ok(())
}
_ => Ok(()),
}
}
fn fire_raise(&self, args: &[Expr], params: &Params) -> Result<()> {
let action = match args.first() {
Some(Expr::Literal(Literal::Str(s))) => s.as_str(),
_ => return Err(Error::Error("malformed RAISE()".into())),
};
if action == "ignore" {
self.raise_ignore.set(true);
return Ok(());
}
let ctx = EvalCtx::rowless(params).with_subqueries(self);
let msg = match args.get(1) {
Some(e) => match eval::eval(e, &ctx)? {
Value::Null => String::new(),
Value::Text(s) => s,
Value::Integer(i) => {
let mut s = String::new();
let _ = core::fmt::write(&mut s, format_args!("{i}"));
s
}
Value::Real(r) => eval::format_real(r),
Value::Blob(_) => String::new(),
},
None => String::new(),
};
match action {
"fail" => self.stmt_keep_partial.set(true),
"rollback" => self.stmt_rollback_tx.set(true),
_ => {} }
Err(Error::Constraint(msg))
}
fn sequence_value(&self, table: &str) -> Result<Option<i64>> {
if self.schema.table("sqlite_sequence").is_none() {
return Ok(None);
}
let meta = self.table_meta("sqlite_sequence", None)?;
for (_, vals) in self.scan_table(&meta)? {
if matches!(&vals[0], Value::Text(t) if t == table) {
return Ok(Some(eval::to_i64(&vals[1])));
}
}
Ok(None)
}
fn set_sequence(&mut self, table: &str, seq: i64) -> Result<()> {
let Some(seq_obj) = self.schema.table("sqlite_sequence") else {
return Ok(());
};
let root = seq_obj.rootpage;
let meta = self.table_meta("sqlite_sequence", None)?;
let existing: Option<i64> = self
.scan_table(&meta)?
.into_iter()
.find(|(_, v)| matches!(&v[0], Value::Text(t) if t == table))
.map(|(rid, _)| rid);
let rec = encode_record(&[Value::Text(table.into()), Value::Integer(seq)]);
let rid = match existing {
Some(rid) => {
delete_table(self.backend.writer()?, root, rid)?;
rid
}
None => self.next_rowid(root)?,
};
insert_table(self.backend.writer()?, root, rid, &rec)?;
Ok(())
}
fn exec_insert(&mut self, ins: &Insert, params: &Params) -> Result<usize> {
reject_schema_write(&ins.table)?;
if self.is_virtual_table(&ins.table) {
let rows: Vec<Vec<Expr>> = match &ins.source {
InsertSource::Values(rows) => rows.clone(),
InsertSource::DefaultValues => alloc::vec![Vec::new()],
InsertSource::Select(sel) => self
.run_select(sel, params)?
.rows
.into_iter()
.map(|row| row.into_iter().map(value_to_literal_expr).collect())
.collect(),
};
return self.exec_vtab_insert(ins, &rows, params);
}
if let InsertSource::Values(rows) = &ins.source {
if let Some(first) = rows.first() {
if rows.iter().any(|r| r.len() != first.len()) {
return Err(Error::Error(
"all VALUES must have the same number of terms".into(),
));
}
}
}
let (rows, is_default_values) = match &ins.source {
InsertSource::Values(rows) => (rows.clone(), false),
InsertSource::DefaultValues => (alloc::vec![Vec::new()], true),
InsertSource::Select(sel) => {
let result = self.run_select(sel, params)?;
let rows = result
.rows
.into_iter()
.map(|row| row.into_iter().map(value_to_literal_expr).collect())
.collect();
(rows, false)
}
};
if self.is_view(&ins.table) {
return self.exec_view_insert(ins, &rows, params);
}
let meta = self.table_meta(&ins.table, None)?;
if meta.without_rowid {
if !ins.upsert.is_empty() || !ins.returning.is_empty() {
return Err(Error::Unsupported(
"UPSERT / RETURNING on WITHOUT ROWID tables",
));
}
return self.exec_insert_without_rowid(ins, &meta, &rows, is_default_values, params);
}
let n_cols = meta.columns.len();
let target: Vec<usize> = if ins.columns.is_empty() {
(0..n_cols).filter(|&i| !meta.is_generated(i)).collect()
} else {
let mut t = Vec::new();
for name in &ins.columns {
let pos = meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(name))
.ok_or_else(|| {
Error::Error(format!("table {} has no column named {name}", ins.table))
})?;
t.push(pos);
}
t
};
let indexes = self.indexes_of(&ins.table)?;
let mut next_auto = self.next_rowid(meta.root)?;
if meta.autoincrement {
if let Some(seq) = self.sequence_value(&ins.table)? {
next_auto = next_auto.max(seq + 1);
}
}
let mut affected = 0;
let mut replaced = false;
for row_exprs in &rows {
if !is_default_values && row_exprs.len() != target.len() {
return Err(insert_count_mismatch(
&ins.table,
!ins.columns.is_empty(),
target.len(),
row_exprs.len(),
));
}
let ctx = EvalCtx::rowless(params).with_subqueries(self);
let mut values: Vec<Value> = meta
.defaults
.iter()
.map(|d| match d {
Some(e) => eval::eval(e, &ctx),
None => Ok(Value::Null),
})
.collect::<Result<_>>()?;
for (i, e) in row_exprs.iter().enumerate() {
if meta.is_generated(target[i]) {
return Err(Error::Error(format!(
"cannot INSERT into generated column \"{}\"",
meta.columns[target[i]].name
)));
}
values[target[i]] = eval::eval(e, &ctx)?;
}
apply_column_affinity(&meta, &mut values);
self.materialize_generated(&meta, &mut values, params)?;
let rowid = match meta.ipk {
Some(ipk) if !matches!(values[ipk], Value::Null) => {
let r = match &values[ipk] {
Value::Integer(i) => *i,
_ => return Err(Error::Error("datatype mismatch".into())),
};
next_auto = next_auto.max(r.saturating_add(1));
r
}
_ => {
let r = self.auto_rowid(meta.root, meta.autoincrement, next_auto)?;
next_auto = if r >= next_auto {
r.saturating_add(1)
} else {
i64::MAX
};
r
}
};
if let Some(ipk) = meta.ipk {
values[ipk] = Value::Integer(rowid);
}
{
if !self.resolve_not_null(
&meta,
&mut values,
ins.on_conflict,
ins.on_conflict_explicit,
params,
)? {
continue;
}
let r = self
.check_strict_types(&meta, &values)
.and_then(|()| self.check_constraints(&meta, &values, Some(rowid), params));
match r {
Ok(()) => {}
Err(Error::Constraint(_)) if ins.on_conflict == OnConflict::Ignore => continue,
Err(Error::Constraint(m)) => {
return Err(self.conflict_error(ins.on_conflict, &m))
}
Err(e) => return Err(e),
}
}
self.check_fk_child(&ins.table, &meta, &values)?;
let (conflicts, constraint_oc) =
self.find_conflicts(&ins.table, &meta, rowid, &values, None, params)?;
let effective_oc = if ins.on_conflict_explicit {
ins.on_conflict
} else {
constraint_oc
};
if !conflicts.is_empty() {
let mut matched = None;
for up in &ins.upsert {
if self.upsert_target_matches(&meta, up, &conflicts, &values, rowid, params)? {
matched = Some(up);
break;
}
}
if let Some(up) = matched {
match &up.action {
UpsertAction::Nothing => continue, UpsertAction::Update {
assignments,
where_clause,
} => {
if self.upsert_do_update(
&ins.table,
&meta,
conflicts[0],
&values,
assignments,
where_clause.as_ref(),
&ins.returning,
params,
)? {
affected += 1;
replaced = true; }
continue;
}
}
}
match effective_oc {
oc @ (OnConflict::Abort | OnConflict::Fail | OnConflict::Rollback) => {
let m = self.unique_violation_message(
&ins.table, &meta, rowid, &values, None, params,
);
return Err(self.conflict_error(oc, &m));
}
OnConflict::Ignore => continue, OnConflict::Replace => {
for cr in conflicts {
self.delete_row_cascade(&ins.table, &meta, cr, params)?;
}
replaced = true;
}
}
}
let index_values = values.clone();
self.fire_triggers(
&ins.table,
TrigEvent::Insert,
TriggerTiming::Before,
&meta.columns,
None,
Some((&index_values, rowid)),
params,
None,
)?;
if self.raise_ignore.replace(false) {
continue;
}
let record = self.encode_table_record(&meta, &index_values);
insert_table(self.backend.writer()?, meta.root, rowid, &record)?;
self.last_insert_rowid.set(rowid);
for idx in &indexes {
if !self.row_in_index(idx, &meta, &index_values, Some(rowid), params)? {
continue; }
let key = self.index_key_bytes(idx, &meta, &index_values, rowid, params)?;
insert_index(self.backend.writer()?, idx.root, &key, &idx.collations)?;
}
self.fire_triggers(
&ins.table,
TrigEvent::Insert,
TriggerTiming::After,
&meta.columns,
None,
Some((&index_values, rowid)),
params,
None,
)?;
if !ins.returning.is_empty() {
self.collect_returning(&ins.returning, &meta, &index_values, Some(rowid), params)?;
}
affected += 1;
}
if meta.autoincrement && affected > 0 {
let high = next_auto - 1;
if high > self.sequence_value(&ins.table)?.unwrap_or(i64::MIN) {
self.set_sequence(&ins.table, high)?;
}
}
if replaced {
self.rebuild_indexes(&meta, &indexes)?;
}
Ok(affected)
}
#[allow(clippy::too_many_arguments)]
fn upsert_do_update(
&mut self,
table: &str,
meta: &TableMeta,
existing_rowid: i64,
proposed: &[Value],
assignments: &[(String, Expr)],
where_clause: Option<&Expr>,
returning: &[ResultColumn],
params: &Params,
) -> Result<bool> {
let Some(old_row) = self.read_row(meta, existing_rowid)? else {
return Ok(false);
};
let changed: Vec<String> = assignments.iter().map(|(c, _)| c.clone()).collect();
let mut cols: Vec<ColumnInfo> = meta.columns.clone();
cols.extend(meta.columns.iter().map(|c| ColumnInfo {
name: c.name.clone(),
table: String::from("excluded"),
affinity: c.affinity,
collation: c.collation,
}));
let mut values = old_row.clone();
{
let mut combined = old_row.clone();
combined.extend_from_slice(proposed);
let ctx = EvalCtx {
row: &combined,
columns: &cols,
rowid: Some(existing_rowid),
params,
anon_counter: core::cell::Cell::new(0),
subqueries: None,
}
.with_subqueries(self);
if let Some(w) = where_clause {
if eval::truth(&eval::eval(w, &ctx)?) != Some(true) {
return Ok(false);
}
}
for (col, e) in assignments {
let pos = meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(col))
.ok_or_else(|| Error::Error(format!("no such column: {col}")))?;
if meta.is_generated(pos) {
return Err(Error::Error(format!(
"cannot UPDATE generated column \"{col}\""
)));
}
values[pos] = eval::eval(e, &ctx)?;
}
}
apply_column_affinity(meta, &mut values);
self.materialize_generated(meta, &mut values, params)?;
if let Some(ipk) = meta.ipk {
if !matches!(values[ipk], Value::Integer(_)) {
return Err(Error::Error("datatype mismatch".into()));
}
}
check_not_null(meta, &values)?;
self.check_strict_types(meta, &values)?;
self.check_constraints(meta, &values, Some(existing_rowid), params)?;
self.check_fk_child(table, meta, &values)?;
if self.foreign_keys {
self.enforce_parent_change(table, &old_row, Some(&values), params)?;
}
let new_rowid = match meta.ipk {
Some(ipk) => eval::to_i64(&values[ipk]),
None => existing_rowid,
};
self.fire_triggers(
table,
TrigEvent::Update,
TriggerTiming::Before,
&meta.columns,
Some((&old_row, existing_rowid)),
Some((&values, new_rowid)),
params,
Some(&changed),
)?;
if !self
.find_conflicts(
table,
meta,
new_rowid,
&values,
Some(existing_rowid),
params,
)?
.0
.is_empty()
{
return Err(Error::Constraint(self.unique_violation_message(
table,
meta,
new_rowid,
&values,
Some(existing_rowid),
params,
)));
}
let new_full = values.clone();
let record = self.encode_table_record(meta, &new_full);
delete_table(self.backend.writer()?, meta.root, existing_rowid)?;
insert_table(self.backend.writer()?, meta.root, new_rowid, &record)?;
self.fire_triggers(
table,
TrigEvent::Update,
TriggerTiming::After,
&meta.columns,
Some((&old_row, existing_rowid)),
Some((&new_full, new_rowid)),
params,
Some(&changed),
)?;
if !returning.is_empty() {
self.collect_returning(returning, meta, &new_full, Some(new_rowid), params)?;
}
Ok(true)
}
fn collect_returning(
&self,
returning: &[ResultColumn],
meta: &TableMeta,
values: &[Value],
rowid: Option<i64>,
params: &Params,
) -> Result<()> {
let ctx = row_ctx(values, &meta.columns, rowid, params).with_subqueries(self);
let mut out = Vec::new();
for col in returning {
project_column(col, &meta.columns, &ctx, &mut out)?;
}
self.returning_rows.borrow_mut().push(out);
Ok(())
}
fn stat1_map(&self) -> alloc::collections::BTreeMap<String, Vec<u64>> {
let mut map = alloc::collections::BTreeMap::new();
if self.schema.table("sqlite_stat1").is_none() {
return map;
}
let Ok(meta) = self.table_meta("sqlite_stat1", None) else {
return map;
};
let Ok(rows) = self.scan_table(&meta) else {
return map;
};
for (_, vals) in rows {
if let (Some(Value::Text(idx)), Some(Value::Text(stat))) = (vals.get(1), vals.get(2)) {
let nums: Vec<u64> = stat
.split_whitespace()
.filter_map(|t| t.parse().ok())
.collect();
if !nums.is_empty() {
map.insert(idx.clone(), nums);
}
}
}
map
}
fn find_conflicts(
&self,
table: &str,
meta: &TableMeta,
rowid: i64,
values: &[Value],
exclude: Option<i64>,
params: &Params,
) -> Result<(Vec<i64>, OnConflict)> {
let uniq_idx: Vec<(IndexMeta, Vec<Value>)> = self
.indexes_of(table)?
.into_iter()
.filter(|i| i.unique && autoindex_number(&i.name, table).is_none())
.filter_map(|i| {
if !self
.row_in_index(&i, meta, values, Some(rowid), params)
.unwrap_or(false)
{
return None;
}
let key = self
.index_key_values(&i, meta, values, rowid, params)
.ok()?;
if key.iter().any(|v| matches!(v, Value::Null)) {
return None; }
Some((i, key))
})
.collect();
let mut out = Vec::new();
let mut action: Option<OnConflict> = None;
for (er, ev) in self.scan_table(meta)? {
if Some(er) == exclude {
continue;
}
if er == rowid {
out.push(er);
continue;
}
let mut conflicted = false;
for (set, set_oc) in &meta.unique {
let new_tuple: Vec<&Value> = set.iter().map(|&i| &values[i]).collect();
if new_tuple.iter().any(|v| matches!(v, Value::Null)) {
continue; }
let conflict = set.iter().zip(&new_tuple).all(|(&i, nv)| {
crate::value::cmp_values_coll(&ev[i], nv, meta.columns[i].collation)
== core::cmp::Ordering::Equal
});
if conflict {
out.push(er);
action.get_or_insert(*set_oc);
conflicted = true;
break;
}
}
if conflicted {
continue;
}
for (idx, new_key) in &uniq_idx {
if !self.row_in_index(idx, meta, &ev, Some(er), params)? {
continue; }
let ex_key = self.index_key_values(idx, meta, &ev, er, params)?;
let conflict = ex_key.len() == new_key.len()
&& ex_key
.iter()
.zip(new_key)
.zip(&idx.collations)
.all(|((a, b), &coll)| {
crate::value::cmp_values_coll(a, b, coll) == core::cmp::Ordering::Equal
});
if conflict {
out.push(er);
break;
}
}
}
Ok((out, action.unwrap_or(OnConflict::Abort)))
}
fn unique_violation_message(
&self,
table: &str,
meta: &TableMeta,
rowid: i64,
values: &[Value],
exclude: Option<i64>,
params: &Params,
) -> String {
let bare = String::from("UNIQUE constraint failed");
let qualify = |cols: &[usize]| {
cols.iter()
.map(|&i| alloc::format!("{}.{}", meta.columns[i].table, meta.columns[i].name))
.collect::<Vec<_>>()
.join(", ")
};
let rows = match self.scan_table(meta) {
Ok(r) => r,
Err(_) => return bare,
};
if let Some(ipk) = meta.ipk {
if rows
.iter()
.any(|(er, _)| *er == rowid && Some(*er) != exclude)
{
return alloc::format!("UNIQUE constraint failed: {}", qualify(&[ipk]));
}
}
for (set, _) in &meta.unique {
if set.iter().any(|&i| matches!(values[i], Value::Null)) {
continue;
}
let hit = rows.iter().any(|(er, ev)| {
Some(*er) != exclude
&& set.iter().all(|&i| {
crate::value::cmp_values_coll(&ev[i], &values[i], meta.columns[i].collation)
== core::cmp::Ordering::Equal
})
});
if hit {
return alloc::format!("UNIQUE constraint failed: {}", qualify(set));
}
}
if let Ok(idxs) = self.indexes_of(table) {
for idx in idxs
.iter()
.filter(|i| i.unique && autoindex_number(&i.name, table).is_none())
{
if !self
.row_in_index(idx, meta, values, Some(rowid), params)
.unwrap_or(false)
{
continue;
}
let Ok(new_key) = self.index_key_values(idx, meta, values, rowid, params) else {
continue;
};
if new_key.iter().any(|v| matches!(v, Value::Null)) {
continue;
}
let hit = rows.iter().any(|(er, ev)| {
Some(*er) != exclude
&& self
.row_in_index(idx, meta, ev, Some(*er), params)
.unwrap_or(false)
&& self
.index_key_values(idx, meta, ev, *er, params)
.map(|ek| {
ek.iter().zip(&new_key).enumerate().all(|(k, (a, b))| {
crate::value::cmp_values_coll(a, b, idx.collations[k])
== core::cmp::Ordering::Equal
})
})
.unwrap_or(false)
});
if hit {
let detail = if idx.key_exprs.is_some() {
alloc::format!("index '{}'", idx.name)
} else {
qualify(&idx.cols)
};
return alloc::format!("UNIQUE constraint failed: {detail}");
}
}
}
bare
}
#[allow(clippy::too_many_arguments)]
fn upsert_target_matches(
&self,
meta: &TableMeta,
up: &Upsert,
conflicts: &[i64],
values: &[Value],
rowid: i64,
params: &Params,
) -> Result<bool> {
if up.target.is_empty() {
return Ok(true);
}
let target_cols: Vec<usize> = up
.target
.iter()
.map(|name| {
meta.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(name))
.ok_or_else(|| Error::Error(format!("no such column: {name}")))
})
.collect::<Result<_>>()?;
if let Some(ipk) = meta.ipk {
if target_cols == [ipk] {
return Ok(conflicts.contains(&rowid));
}
}
for &er in conflicts {
let Some(existing) = self.read_row(meta, er)? else {
continue;
};
let collide = target_cols.iter().all(|&c| {
!matches!(values[c], Value::Null)
&& crate::value::cmp_values_coll(
&existing[c],
&values[c],
meta.columns[c].collation,
) == core::cmp::Ordering::Equal
});
if collide {
return Ok(true);
}
}
let _ = params;
Ok(false)
}
fn index_key_values(
&self,
idx: &IndexMeta,
meta: &TableMeta,
values: &[Value],
rowid: i64,
params: &Params,
) -> Result<Vec<Value>> {
match &idx.key_exprs {
None => Ok(idx.cols.iter().map(|&c| values[c].clone()).collect()),
Some(exprs) => {
let ctx = row_ctx(values, &meta.columns, Some(rowid), params).with_subqueries(self);
exprs.iter().map(|e| eval::eval(e, &ctx)).collect()
}
}
}
fn wr_index_collision(
&self,
table: &str,
meta: &TableMeta,
a: &[Value],
b: &[Value],
params: &Params,
) -> Result<bool> {
for idx in self
.indexes_of(table)?
.iter()
.filter(|i| i.unique && autoindex_number(&i.name, table).is_none())
{
if !self.row_in_index(idx, meta, a, None, params)?
|| !self.row_in_index(idx, meta, b, None, params)?
{
continue;
}
let ka = self.index_key_values(idx, meta, a, 0, params)?;
if ka.iter().any(|v| matches!(v, Value::Null)) {
continue; }
let kb = self.index_key_values(idx, meta, b, 0, params)?;
let eq = ka.len() == kb.len()
&& ka.iter().zip(&kb).zip(&idx.collations).all(|((x, y), &c)| {
crate::value::cmp_values_coll(x, y, c) == core::cmp::Ordering::Equal
});
if eq {
return Ok(true);
}
}
Ok(false)
}
fn exec_delete(&mut self, del: &Delete, params: &Params) -> Result<usize> {
if del.ctes.is_empty() {
return self.exec_delete_inner(del, params);
}
let base = self.cte_env.borrow().len();
let pushed = self.push_ctes(&del.ctes, params, None);
let result = pushed.and_then(|()| self.exec_delete_inner(del, params));
self.cte_env.borrow_mut().truncate(base);
result
}
fn exec_delete_inner(&mut self, del: &Delete, params: &Params) -> Result<usize> {
reject_schema_write(&del.table)?;
if self.is_virtual_table(&del.table) {
return self.exec_vtab_delete(del, params);
}
if self.is_view(&del.table) {
return self.exec_view_delete(del, params);
}
let meta = self.table_meta(&del.table, None)?;
if self.outer_scope.borrow().is_empty() {
if let Some(w) = &del.where_clause {
self.validate_dml_refs(&del.table, &meta.columns, &[w])?;
}
}
if meta.without_rowid {
if !del.returning.is_empty() {
return Err(Error::Unsupported("RETURNING on WITHOUT ROWID tables"));
}
return self.exec_delete_without_rowid(del, &meta, params);
}
let indexes = self.indexes_of(&del.table)?;
let mut victims = self.matching_rowids(&meta, del.where_clause.as_ref(), params)?;
if !del.order_by.is_empty() || del.limit.is_some() || del.offset.is_some() {
victims = self.order_limit_rowids(
&meta,
victims,
&del.order_by,
del.limit.as_ref(),
del.offset.as_ref(),
params,
)?;
}
let mut deleted = 0;
for rowid in &victims {
let old = self.read_row(&meta, *rowid)?;
if let Some(old) = &old {
self.fire_triggers(
&del.table,
TrigEvent::Delete,
TriggerTiming::Before,
&meta.columns,
Some((old, *rowid)),
None,
params,
None,
)?;
if self.raise_ignore.replace(false) {
continue;
}
if !del.returning.is_empty() {
self.collect_returning(&del.returning, &meta, old, Some(*rowid), params)?;
}
}
if self.foreign_keys {
if let Some(old) = &old {
self.enforce_parent_change(&del.table, old, None, params)?;
}
}
delete_table(self.backend.writer()?, meta.root, *rowid)?;
deleted += 1;
if let Some(old) = &old {
self.fire_triggers(
&del.table,
TrigEvent::Delete,
TriggerTiming::After,
&meta.columns,
Some((old, *rowid)),
None,
params,
None,
)?;
}
}
if deleted > 0 {
self.compact_table(&meta)?;
self.rebuild_indexes(&meta, &indexes)?;
}
Ok(deleted)
}
fn compact_table(&mut self, meta: &TableMeta) -> Result<()> {
if !table_has_empty_leaf(self.backend.source(), meta.root)? {
return Ok(());
}
let mut rows: Vec<(i64, Vec<u8>)> = Vec::new();
{
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut ok = cur.first()?;
while ok {
rows.push((cur.rowid()?, cur.payload()?));
ok = cur.next()?;
}
}
let w = self.backend.writer()?;
clear_table(w, meta.root)?;
for (rowid, payload) in &rows {
insert_table(w, meta.root, *rowid, payload)?;
}
Ok(())
}
fn exec_update(&mut self, upd: &Update, params: &Params) -> Result<usize> {
if upd.ctes.is_empty() {
return self.exec_update_inner(upd, params);
}
let base = self.cte_env.borrow().len();
let pushed = self.push_ctes(&upd.ctes, params, None);
let result = pushed.and_then(|()| self.exec_update_inner(upd, params));
self.cte_env.borrow_mut().truncate(base);
result
}
fn exec_update_inner(&mut self, upd: &Update, params: &Params) -> Result<usize> {
reject_schema_write(&upd.table)?;
if self.is_virtual_table(&upd.table) {
return self.exec_vtab_update(upd, params);
}
if self.is_view(&upd.table) {
return self.exec_view_update(upd, params);
}
let meta = self.table_meta(&upd.table, None)?;
for col in upd
.assignments
.iter()
.map(|(c, _)| c)
.chain(upd.row_assignments.iter().flat_map(|(cs, _)| cs))
{
if !meta
.columns
.iter()
.any(|c| c.name.eq_ignore_ascii_case(col))
{
return Err(Error::Error(alloc::format!("no such column: {col}")));
}
}
if upd.from.is_none() && self.outer_scope.borrow().is_empty() {
let mut refs: Vec<&Expr> = upd.assignments.iter().map(|(_, e)| e).collect();
if let Some(w) = &upd.where_clause {
refs.push(w);
}
self.validate_dml_refs(&upd.table, &meta.columns, &refs)?;
}
if meta.without_rowid {
if !upd.returning.is_empty() {
return Err(Error::Unsupported("RETURNING on WITHOUT ROWID tables"));
}
if upd.from.is_some() {
return Err(Error::Unsupported("UPDATE … FROM on WITHOUT ROWID tables"));
}
return self.exec_update_without_rowid(upd, &meta, params);
}
let indexes = self.indexes_of(&upd.table)?;
let mut changed: Vec<String> = upd.assignments.iter().map(|(c, _)| c.clone()).collect();
for (rcols, _) in &upd.row_assignments {
changed.extend(rcols.iter().cloned());
}
let from_data = match &upd.from {
Some(fc) => {
let synth = Select {
ctes: Vec::new(),
compound: Vec::new(),
distinct: false,
columns: Vec::new(),
from: Some(fc.clone()),
where_clause: None,
group_by: Vec::new(),
having: None,
window_defs: Vec::new(),
order_by: Vec::new(),
limit: None,
offset: None,
};
let (cols, rows) = self.scan_source(&synth, params)?;
Some((cols, rows.into_iter().map(|r| r.values).collect::<Vec<_>>()))
}
None => None,
};
let combined_columns: Vec<ColumnInfo> = match &from_data {
Some((cols, _)) => meta.columns.iter().chain(cols).cloned().collect(),
None => Vec::new(),
};
let mut targets: Vec<(i64, Vec<Value>, Option<Vec<Value>>)> = Vec::new();
{
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let encoding = self.backend.source().header().text_encoding;
let mut ok = cur.first()?;
while ok {
let rowid = cur.rowid()?;
let values = self.decode_full_row(&meta, rowid, &cur.payload()?, encoding)?;
match &from_data {
Some((_, from_rows)) => {
let mut matched = None;
for fr in from_rows {
let mut combined = values.clone();
combined.extend_from_slice(fr);
let ok = match &upd.where_clause {
Some(p) => {
let ctx =
row_ctx(&combined, &combined_columns, Some(rowid), params)
.with_subqueries(self);
eval::truth(&eval::eval(p, &ctx)?) == Some(true)
}
None => true,
};
if ok {
matched = Some(fr.clone());
break;
}
}
if let Some(fr) = matched {
targets.push((rowid, values, Some(fr)));
}
}
None => {
let matches = match &upd.where_clause {
Some(p) => {
let ctx = row_ctx(&values, &meta.columns, Some(rowid), params)
.with_subqueries(self);
eval::truth(&eval::eval(p, &ctx)?) == Some(true)
}
None => true,
};
if matches {
targets.push((rowid, values, None));
}
}
}
ok = cur.next()?;
}
}
if !upd.order_by.is_empty() || upd.limit.is_some() || upd.offset.is_some() {
let rowids: Vec<i64> = targets.iter().map(|(r, _, _)| *r).collect();
let kept = self.order_limit_rowids(
&meta,
rowids,
&upd.order_by,
upd.limit.as_ref(),
upd.offset.as_ref(),
params,
)?;
let mut by_id: alloc::collections::BTreeMap<i64, (Vec<Value>, Option<Vec<Value>>)> =
targets.into_iter().map(|(r, v, f)| (r, (v, f))).collect();
targets = kept
.into_iter()
.filter_map(|r| by_id.remove(&r).map(|(v, f)| (r, v, f)))
.collect();
}
let mut prepared: Vec<(i64, Vec<Value>, Vec<Value>)> = Vec::with_capacity(targets.len());
for (rowid, mut values, matched_from) in targets {
let old_row = values.clone();
for (col, expr) in &upd.assignments {
let pos = meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(col))
.ok_or_else(|| Error::Error(format!("no such column: {col}")))?;
if meta.is_generated(pos) {
return Err(Error::Error(format!(
"cannot UPDATE generated column \"{col}\""
)));
}
let new = match &matched_from {
Some(fr) => {
let mut combined = old_row.clone();
combined.extend_from_slice(fr);
let ctx = row_ctx(&combined, &combined_columns, Some(rowid), params)
.with_subqueries(self);
eval::eval(expr, &ctx)?
}
None => {
let ctx = row_ctx(&old_row, &meta.columns, Some(rowid), params)
.with_subqueries(self);
eval::eval(expr, &ctx)?
}
};
values[pos] = new;
}
if !upd.row_assignments.is_empty() {
let combined_row;
let (ctx_row, ctx_cols): (&[Value], &[ColumnInfo]) = match &matched_from {
Some(fr) => {
let mut c = old_row.clone();
c.extend_from_slice(fr);
combined_row = c;
(&combined_row, &combined_columns)
}
None => (&old_row, &meta.columns),
};
let ctx = row_ctx(ctx_row, ctx_cols, Some(rowid), params).with_subqueries(self);
self.apply_row_subquery_assignments(
&upd.row_assignments,
&meta.columns,
Some(&meta),
&ctx,
&mut values,
)?;
}
apply_column_affinity(&meta, &mut values);
self.materialize_generated(&meta, &mut values, params)?;
prepared.push((rowid, old_row, values));
}
let mut affected = 0;
for (rowid, old_row, mut values) in prepared {
if let Some(ipk) = meta.ipk {
if !matches!(values[ipk], Value::Integer(_)) {
return Err(Error::Error("datatype mismatch".into()));
}
}
{
if !self.resolve_not_null(
&meta,
&mut values,
upd.on_conflict,
upd.on_conflict_explicit,
params,
)? {
continue;
}
let r = self
.check_strict_types(&meta, &values)
.and_then(|()| self.check_constraints(&meta, &values, Some(rowid), params));
match r {
Ok(()) => {}
Err(Error::Constraint(_)) if upd.on_conflict == OnConflict::Ignore => continue,
Err(Error::Constraint(m)) => {
return Err(self.conflict_error(upd.on_conflict, &m))
}
Err(e) => return Err(e),
}
}
self.check_fk_child(&upd.table, &meta, &values)?;
if self.foreign_keys {
self.enforce_parent_change(&upd.table, &old_row, Some(&values), params)?;
}
let new_rowid = match meta.ipk {
Some(ipk) => eval::to_i64(&values[ipk]),
None => rowid,
};
self.fire_triggers(
&upd.table,
TrigEvent::Update,
TriggerTiming::Before,
&meta.columns,
Some((&old_row, rowid)),
Some((&values, new_rowid)),
params,
Some(&changed),
)?;
if self.raise_ignore.replace(false) {
continue;
}
let (conflicts, constraint_oc) =
self.find_conflicts(&upd.table, &meta, new_rowid, &values, Some(rowid), params)?;
let effective_oc = if upd.on_conflict_explicit {
upd.on_conflict
} else {
constraint_oc
};
if !conflicts.is_empty() {
match effective_oc {
OnConflict::Ignore => continue,
OnConflict::Replace => {
for cr in conflicts {
delete_table(self.backend.writer()?, meta.root, cr)?;
}
}
oc @ (OnConflict::Abort | OnConflict::Fail | OnConflict::Rollback) => {
let m = self.unique_violation_message(
&upd.table,
&meta,
new_rowid,
&values,
Some(rowid),
params,
);
return Err(self.conflict_error(oc, &m));
}
}
}
let new_full = values.clone();
let record = self.encode_table_record(&meta, &new_full);
delete_table(self.backend.writer()?, meta.root, rowid)?;
insert_table(self.backend.writer()?, meta.root, new_rowid, &record)?;
self.fire_triggers(
&upd.table,
TrigEvent::Update,
TriggerTiming::After,
&meta.columns,
Some((&old_row, rowid)),
Some((&new_full, new_rowid)),
params,
Some(&changed),
)?;
if !upd.returning.is_empty() {
self.collect_returning(&upd.returning, &meta, &new_full, Some(new_rowid), params)?;
}
affected += 1;
}
if affected > 0 {
self.compact_table(&meta)?;
self.rebuild_indexes(&meta, &indexes)?;
}
Ok(affected)
}
fn exec_create_index(&mut self, ci: &CreateIndex, sql_text: &str) -> Result<()> {
let reprinted;
let sql_text = if ci.schema.is_some() {
reprinted = sql::print::create_index(ci);
reprinted.as_str()
} else {
sql_text
};
if self.schema.index(&ci.name).is_some() {
if ci.if_not_exists {
return Ok(());
}
return Err(Error::Error(format!("index {} already exists", ci.name)));
}
if self.is_virtual_table(&ci.table) {
return Err(Error::Error("virtual tables may not be indexed".into()));
}
if self.schema.table(&ci.table).is_none() {
return Err(Error::Error(format!(
"no such table: {}.{}",
ci.schema.as_deref().unwrap_or("main"),
ci.table
)));
}
let tmeta = self.table_meta(&ci.table, None)?;
if ci.columns.iter().any(|t| expr_is_nondeterministic(&t.expr))
|| ci
.where_clause
.as_ref()
.is_some_and(expr_is_nondeterministic)
{
return Err(Error::Error(
"non-deterministic functions prohibited in index expressions".into(),
));
}
for term in &ci.columns {
if let Some(name) = unknown_collation(&term.expr) {
return Err(Error::Error(format!("no such collation sequence: {name}")));
}
}
if let Some(p) = &ci.where_clause {
let known: Vec<String> = tmeta.columns.iter().map(|c| c.name.clone()).collect();
if let Some(col) = unknown_column_ref(p, &known, true, Some(&ci.table)) {
return Err(Error::Error(format!("no such column: {col}")));
}
}
{
let known: Vec<String> = tmeta.columns.iter().map(|c| c.name.clone()).collect();
for term in &ci.columns {
let key = match &term.expr {
Expr::Collate { expr, .. } => expr.as_ref(),
other => other,
};
if let Some(col) = unknown_column_ref(key, &known, false, Some(&ci.table)) {
return Err(Error::Error(format!("no such column: {col}")));
}
if has_resolved_dotted_ref(key, &known, false, &ci.table) {
return Err(Error::Error(
"the \".\" operator prohibited in index expressions".into(),
));
}
}
}
let (cols, key_exprs, colls) = self.index_key_spec(&tmeta, ci)?;
if key_exprs.is_some() && tmeta.without_rowid {
return Err(Error::Unsupported(
"expression indexes on WITHOUT ROWID tables",
));
}
let schema_next = self.next_rowid(crate::schema::SCHEMA_ROOT_PAGE)?;
let no_params = Params::default();
let keep_row = |values: &[Value], rowid: Option<i64>| -> Result<bool> {
match &ci.where_clause {
None => Ok(true),
Some(p) => {
let ctx =
row_ctx(values, &tmeta.columns, rowid, &no_params).with_subqueries(self);
Ok(eval::truth(&eval::eval(p, &ctx)?) == Some(true))
}
}
};
let root = if tmeta.without_rowid {
let rows = self.scan_without_rowid(&tmeta)?;
let keep: Vec<bool> = rows
.iter()
.map(|row| keep_row(row, None))
.collect::<Result<_>>()?;
let pk_cols = tmeta.storage_order[..tmeta.pk_len].to_vec();
let mut key_colls = colls.clone();
key_colls.extend(self.col_collations(&tmeta, &pk_cols));
let w = self.backend.writer()?;
let root = create_index_root(w)?;
for (row, &k) in rows.iter().zip(&keep) {
if k {
insert_index(w, root, &wr_index_key(&cols, &pk_cols, row), &key_colls)?;
}
}
root
} else {
let rows = self.scan_table(&tmeta)?;
let mut keys: Vec<Vec<u8>> = Vec::new();
for (rowid, values) in &rows {
if !keep_row(values, Some(*rowid))? {
continue;
}
keys.push(match &key_exprs {
None => index_key(&cols, values, *rowid),
Some(exprs) => {
let ctx = row_ctx(values, &tmeta.columns, Some(*rowid), &no_params)
.with_subqueries(self);
let mut k: Vec<Value> = exprs
.iter()
.map(|e| eval::eval(e, &ctx))
.collect::<Result<_>>()?;
k.push(Value::Integer(*rowid));
encode_record(&k)
}
});
}
let w = self.backend.writer()?;
let root = create_index_root(w)?;
for key in &keys {
insert_index(w, root, key, &colls)?;
}
root
};
let w = self.backend.writer()?;
let schema_row = encode_record(&[
Value::Text("index".into()),
Value::Text(ci.name.clone()),
Value::Text(ci.table.clone()),
Value::Integer(root as i64),
Value::Text(sql_text.into()),
]);
insert_table(w, crate::schema::SCHEMA_ROOT_PAGE, schema_next, &schema_row)?;
let cookie = w.header().schema_cookie.wrapping_add(1);
w.header_mut().schema_cookie = cookie;
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
fn exec_create_view(&mut self, cv: &CreateView, sql_text: &str) -> Result<()> {
let stripped;
let sql_text = match cv.schema.as_deref() {
Some(s) => {
stripped = strip_schema_qualifier(sql_text, s)?;
stripped.as_str()
}
None => sql_text,
};
let exists = self.schema.objects().iter().any(|o| o.name == cv.name);
if exists {
if cv.if_not_exists {
return Ok(());
}
return Err(Error::Error(format!("table {} already exists", cv.name)));
}
let next = self.next_rowid(crate::schema::SCHEMA_ROOT_PAGE)?;
let row = encode_record(&[
Value::Text("view".into()),
Value::Text(cv.name.clone()),
Value::Text(cv.name.clone()),
Value::Integer(0), Value::Text(sql_text.into()),
]);
insert_table(
self.backend.writer()?,
crate::schema::SCHEMA_ROOT_PAGE,
next,
&row,
)?;
let cookie = self
.backend
.writer()?
.header()
.schema_cookie
.wrapping_add(1);
self.backend.writer()?.header_mut().schema_cookie = cookie;
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
fn exec_create_virtual_table(
&mut self,
cvt: &CreateVirtualTable,
sql_text: &str,
) -> Result<()> {
let stripped;
let sql_text = match cvt.schema.as_deref() {
Some(s) => {
stripped = strip_schema_qualifier(sql_text, s)?;
stripped.as_str()
}
None => sql_text,
};
let exists = self.schema.objects().iter().any(|o| o.name == cvt.name);
if exists {
if cvt.if_not_exists {
return Ok(());
}
return Err(Error::Error(format!("table {} already exists", cvt.name)));
}
let module = self
.vtab_registry
.get(&cvt.module)
.ok_or_else(|| Error::Error(format!("no such module: {}", cvt.module)))?;
let arg_refs: Vec<&str> = cvt.args.iter().map(String::as_str).collect();
let schema = module.dyn_connect(&arg_refs)?;
let persistent = module.dyn_persistent();
let cols = schema.columns;
let rtree_n_coord = (cvt.module.eq_ignore_ascii_case("rtree")
|| cvt.module.eq_ignore_ascii_case("rtree_i32"))
.then(|| crate::vtab::RTreeModule::n_coords(&arg_refs))
.filter(|n| cols.len() == 1 + n);
#[cfg(feature = "fts5")]
let is_fts5 = cvt.module.eq_ignore_ascii_case("fts5");
#[cfg(not(feature = "fts5"))]
let is_fts5 = false;
if let Some(n_coord) = rtree_n_coord {
let integer = cvt.module.eq_ignore_ascii_case("rtree_i32");
self.rtree_create_storage(&cvt.name, n_coord, integer)?;
} else if is_fts5 {
#[cfg(feature = "fts5")]
self.fts5_create_storage(&cvt.name, cols.len())?;
} else if persistent {
let coldefs = cols
.iter()
.map(|c| sql::print::ident(c))
.collect::<Vec<_>>()
.join(", ");
let backing_sql = format!(
"CREATE TABLE {}({coldefs})",
sql::print::ident(&format!("{}_data", cvt.name))
);
let Statement::CreateTable(ct) = sql::parse_one(&backing_sql)? else {
unreachable!("constructed a CREATE TABLE");
};
self.exec_create_table(&ct, &backing_sql)?;
}
let next = self.next_rowid(crate::schema::SCHEMA_ROOT_PAGE)?;
let row = encode_record(&[
Value::Text("table".into()),
Value::Text(cvt.name.clone()),
Value::Text(cvt.name.clone()),
Value::Integer(0), Value::Text(sql_text.into()),
]);
insert_table(
self.backend.writer()?,
crate::schema::SCHEMA_ROOT_PAGE,
next,
&row,
)?;
let cookie = self
.backend
.writer()?
.header()
.schema_cookie
.wrapping_add(1);
self.backend.writer()?.header_mut().schema_cookie = cookie;
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
fn is_virtual_table(&self, name: &str) -> bool {
self.schema
.objects()
.iter()
.filter(|o| {
o.obj_type == crate::schema::ObjectType::Table && o.name.eq_ignore_ascii_case(name)
})
.any(|o| {
matches!(
o.sql.as_deref().map(sql::parse_one),
Some(Ok(Statement::CreateVirtualTable(_)))
)
})
}
fn vtab_meta(&self, name: &str) -> Result<(String, Vec<String>, crate::vtab::VTabSchema)> {
use crate::schema::ObjectType;
let obj = self
.schema
.objects()
.iter()
.find(|o| o.obj_type == ObjectType::Table && o.name.eq_ignore_ascii_case(name))
.ok_or_else(|| Error::Error(format!("no such table: {name}")))?;
let Some(Ok(Statement::CreateVirtualTable(cvt))) = obj.sql.as_deref().map(sql::parse_one)
else {
return Err(Error::Error(format!("{name} is not a virtual table")));
};
let module = self
.vtab_registry
.get(&cvt.module)
.ok_or_else(|| Error::Error(format!("no such module: {}", cvt.module)))?;
let arg_refs: Vec<&str> = cvt.args.iter().map(String::as_str).collect();
let schema = module.dyn_connect(&arg_refs)?;
Ok((cvt.module.clone(), cvt.args.clone(), schema))
}
fn with_vtab_store<F>(
&mut self,
module_name: &str,
args: &[String],
table: &str,
f: F,
) -> Result<usize>
where
F: FnOnce(&dyn DynVTabModule, &mut dyn VTabStore, &[&str]) -> Result<usize>,
{
let module = self
.vtab_registry
.unregister(module_name)
.ok_or_else(|| Error::Error(format!("no such module: {module_name}")))?;
let backing = if module_name.eq_ignore_ascii_case("fts5") {
format!("{table}_content")
} else {
format!("{table}_data")
};
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let result = {
let mut store = ExecVTabStore {
conn: self,
backing: &backing,
ipk_prefix: module_name.eq_ignore_ascii_case("fts5"),
};
f(&*module, &mut store, &arg_refs)
};
self.vtab_registry.register(module_name, module)?;
result
}
fn exec_vtab_insert(
&mut self,
ins: &Insert,
rows: &[Vec<Expr>],
params: &Params,
) -> Result<usize> {
if !ins.upsert.is_empty() || !ins.returning.is_empty() {
return Err(Error::Unsupported("UPSERT / RETURNING on a virtual table"));
}
let (module_name, args, schema) = self.vtab_meta(&ins.table)?;
let col_names = schema.columns;
let ncols = col_names.len();
#[cfg(feature = "fts5")]
if module_name.eq_ignore_ascii_case("fts5")
&& ins.columns.len() == 1
&& ins.columns[0].eq_ignore_ascii_case(&ins.table)
{
let commands = rows
.iter()
.map(|row| {
let ctx = EvalCtx::rowless(params).with_subqueries(self);
Ok(eval::to_text(&eval::eval(&row[0], &ctx)?))
})
.collect::<Result<Vec<_>>>()?;
if commands
.iter()
.all(|c| matches!(c.as_str(), "rebuild" | "optimize"))
{
return Ok(0);
}
}
let target: Vec<Option<usize>> = if ins.columns.is_empty() {
(0..ncols).map(Some).collect()
} else {
ins.columns
.iter()
.map(
|name| match col_names.iter().position(|c| c.eq_ignore_ascii_case(name)) {
Some(p) => Ok(Some(p)),
None if matches!(
name.to_ascii_lowercase().as_str(),
"rowid" | "_rowid_" | "oid"
) =>
{
Ok(None)
}
None => Err(Error::Error(format!("no such column: {name}"))),
},
)
.collect::<Result<_>>()?
};
let mut changes: Vec<(Option<i64>, Vec<Value>)> = Vec::with_capacity(rows.len());
for row in rows {
if row.len() != target.len() {
return Err(Error::Error(format!(
"{} values for {} columns",
row.len(),
target.len()
)));
}
let mut values = alloc::vec![Value::Null; ncols];
let mut rowid = None;
for (j, expr) in row.iter().enumerate() {
let ctx = EvalCtx::rowless(params).with_subqueries(self);
let v = eval::eval(expr, &ctx)?;
match target[j] {
Some(col) => values[col] = v,
None => rowid = Some(eval::to_i64(&v)),
}
}
changes.push((rowid, values));
}
let on_conflict = ins.on_conflict;
let table = ins.table.clone();
let id_col = col_names
.first()
.cloned()
.unwrap_or_else(|| String::from("rowid"));
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let rtree_nc = (module_name.eq_ignore_ascii_case("rtree")
|| module_name.eq_ignore_ascii_case("rtree_i32"))
.then(|| crate::vtab::RTreeModule::n_coords(&arg_refs))
.filter(|n| ncols == 1 + n);
if let Some(n_coord) = rtree_nc {
let integer = module_name.eq_ignore_ascii_case("rtree_i32");
let mut existing: alloc::collections::BTreeSet<i64> = self
.rtree_entries(&table, n_coord, integer)?
.iter()
.map(|c| c.key)
.collect();
let mut next_auto = existing.iter().max().copied().unwrap_or(0) + 1;
let mut cells: Vec<RtreeCell> = Vec::new();
let mut n = 0;
for (rowid, values) in &changes {
let rid = rowid
.or(match values.first() {
Some(Value::Integer(i)) => Some(*i),
_ => None,
})
.unwrap_or_else(|| {
let r = next_auto;
next_auto += 1;
r
});
if existing.contains(&rid) {
match on_conflict {
OnConflict::Replace => {}
OnConflict::Ignore => continue,
_ => {
return Err(Error::Constraint(format!(
"UNIQUE constraint failed: {table}.{id_col}"
)))
}
}
}
existing.insert(rid);
cells.retain(|c| c.key != rid); cells.push(rtree_cell_from_values(rid, values, n_coord, integer)?);
n += 1;
}
self.rtree_apply(&table, n_coord, integer, cells, &[])?;
return Ok(n);
}
let inserted = self.with_vtab_store(
&module_name,
&args,
&ins.table,
|module, store, arg_refs| {
let mut existing: alloc::collections::BTreeSet<i64> = store
.rows()
.map(|rows| rows.iter().map(|(r, _)| *r).collect())
.unwrap_or_default();
let rowid_col = module.dyn_rowid_column();
let mut n = 0;
for (rowid, values) in &changes {
let effective = rowid.or_else(|| {
let v = values.get(rowid_col?)?;
(!matches!(v, Value::Null)).then(|| eval::to_i64(v))
});
if let Some(id) = effective {
if existing.contains(&id) {
match on_conflict {
OnConflict::Replace => {}
OnConflict::Ignore => continue,
_ => {
return Err(Error::Constraint(format!(
"UNIQUE constraint failed: {table}.{id_col}"
)))
}
}
}
}
let assigned = module.dyn_update(
arg_refs,
VTabChange::Insert {
rowid: *rowid,
values,
},
store,
)?;
existing.insert(assigned);
n += 1;
}
Ok(n)
},
)?;
self.fts5_maybe_rebuild(&module_name, &ins.table)?;
Ok(inserted)
}
fn exec_vtab_delete(&mut self, del: &Delete, params: &Params) -> Result<usize> {
if !del.returning.is_empty() {
return Err(Error::Unsupported("RETURNING on a virtual table"));
}
let (module_name, args, _) = self.vtab_meta(&del.table)?;
let (columns, rows) = self
.try_virtual_table(&del.table, None, None)?
.ok_or_else(|| Error::Error(format!("{} is not a virtual table", del.table)))?;
let mut victims: Vec<i64> = Vec::new();
for r in &rows {
if let Some(pred) = &del.where_clause {
let ctx = r.ctx(&columns, params).with_subqueries(self);
if eval::truth(&eval::eval(pred, &ctx)?) != Some(true) {
continue;
}
}
victims.push(
r.rowid
.ok_or_else(|| Error::Error("virtual-table row has no rowid".into()))?,
);
}
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let rtree_nc = (module_name.eq_ignore_ascii_case("rtree")
|| module_name.eq_ignore_ascii_case("rtree_i32"))
.then(|| crate::vtab::RTreeModule::n_coords(&arg_refs))
.filter(|n| columns.len() == 1 + n);
if let Some(n_coord) = rtree_nc {
let integer = module_name.eq_ignore_ascii_case("rtree_i32");
self.rtree_apply(&del.table, n_coord, integer, Vec::new(), &victims)?;
return Ok(victims.len());
}
let deleted = self.with_vtab_store(
&module_name,
&args,
&del.table,
|module, store, arg_refs| {
for rowid in &victims {
module.dyn_update(arg_refs, VTabChange::Delete { rowid: *rowid }, store)?;
}
Ok(victims.len())
},
)?;
self.fts5_maybe_rebuild(&module_name, &del.table)?;
Ok(deleted)
}
fn exec_vtab_update(&mut self, upd: &Update, params: &Params) -> Result<usize> {
if !upd.returning.is_empty() {
return Err(Error::Unsupported("RETURNING on a virtual table"));
}
if !upd.row_assignments.is_empty() {
return Err(Error::Unsupported(
"UPDATE SET (…) = (SELECT …) on a virtual table",
));
}
if upd.from.is_some() {
return Err(Error::Unsupported("UPDATE … FROM on a virtual table"));
}
let (module_name, args, schema) = self.vtab_meta(&upd.table)?;
let col_names = schema.columns;
let assigns: Vec<(usize, &Expr)> = upd
.assignments
.iter()
.map(|(name, value)| {
col_names
.iter()
.position(|c| c.eq_ignore_ascii_case(name))
.map(|pos| (pos, value))
.ok_or_else(|| Error::Error(format!("no such column: {name}")))
})
.collect::<Result<_>>()?;
let (columns, rows) = self
.try_virtual_table(&upd.table, None, None)?
.ok_or_else(|| Error::Error(format!("{} is not a virtual table", upd.table)))?;
let mut changes: Vec<(i64, Vec<Value>)> = Vec::new();
for r in &rows {
let ctx = r.ctx(&columns, params).with_subqueries(self);
if let Some(pred) = &upd.where_clause {
if eval::truth(&eval::eval(pred, &ctx)?) != Some(true) {
continue;
}
}
let mut values = r.values.clone();
for (pos, expr) in &assigns {
values[*pos] = eval::eval(expr, &ctx)?;
}
let rowid = r
.rowid
.ok_or_else(|| Error::Error("virtual-table row has no rowid".into()))?;
changes.push((rowid, values));
}
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let rtree_nc = (module_name.eq_ignore_ascii_case("rtree")
|| module_name.eq_ignore_ascii_case("rtree_i32"))
.then(|| crate::vtab::RTreeModule::n_coords(&arg_refs))
.filter(|n| columns.len() == 1 + n);
if let Some(n_coord) = rtree_nc {
let integer = module_name.eq_ignore_ascii_case("rtree_i32");
let mut deletes = Vec::with_capacity(changes.len());
let mut inserts = Vec::with_capacity(changes.len());
for (old_rowid, values) in &changes {
deletes.push(*old_rowid);
let new_rid = match values.first() {
Some(Value::Null) | None => *old_rowid,
Some(v) => eval::to_i64(v),
};
inserts.push(rtree_cell_from_values(new_rid, values, n_coord, integer)?);
}
self.rtree_apply(&upd.table, n_coord, integer, inserts, &deletes)?;
return Ok(changes.len());
}
let updated = self.with_vtab_store(
&module_name,
&args,
&upd.table,
|module, store, arg_refs| {
for (rowid, values) in &changes {
module.dyn_update(
arg_refs,
VTabChange::Update {
rowid: *rowid,
new_rowid: *rowid,
values,
},
store,
)?;
}
Ok(changes.len())
},
)?;
self.fts5_maybe_rebuild(&module_name, &upd.table)?;
Ok(updated)
}
#[cfg(feature = "fts5")]
fn fts5_try_index_match(
&self,
name: &str,
alias: Option<&str>,
arg_refs: &[&str],
pushdown: Option<(&Select, &Params)>,
) -> Result<Option<Vec<InputRow>>> {
let (sel, params) = match pushdown {
Some(p) => p,
None => return Ok(None),
};
let where_expr = match sel.where_clause.as_ref() {
Some(e) => e,
None => return Ok(None),
};
let (query, operand) = match self.fts5_match_query(where_expr, params) {
Some(qo) => qo,
None => return Ok(None),
};
let names_table = operand.eq_ignore_ascii_case(name)
|| alias.is_some_and(|a| operand.eq_ignore_ascii_case(a));
if !names_table {
return Ok(None);
}
let indexed = crate::vtab::fts5_indexed_columns(arg_refs);
let ncols = self.vtab_meta(name)?.2.columns.len();
if indexed.len() != ncols {
return Ok(None);
}
let tok = crate::vtab::fts5_tok_config(arg_refs);
enum Routed {
AnyColumn(Vec<u8>),
InColumn(Vec<u8>, usize),
Phrase(Vec<Vec<u8>>),
PhraseInColumn(Vec<Vec<u8>>, usize),
Near(Vec<u8>, Vec<u8>, u32),
BoolTree(crate::vtab::Fts5BoolTree),
PrefixAnyColumn(Vec<u8>),
PrefixInColumn(Vec<u8>, usize),
}
let resolve_col = |col: &str| -> Result<Option<usize>> {
Ok(self
.vtab_meta(name)?
.2
.columns
.iter()
.position(|c| c.eq_ignore_ascii_case(col)))
};
let routed = if let Some(t) = crate::vtab::fts5_single_bare_term(&query, tok) {
Routed::AnyColumn(t)
} else if let Some((col, t)) = crate::vtab::fts5_single_bare_term_column(&query, tok) {
match resolve_col(&col)? {
Some(ci) => Routed::InColumn(t, ci),
None => return Ok(None),
}
} else if let Some(terms) = crate::vtab::fts5_phrase_terms(&query, tok) {
Routed::Phrase(terms)
} else if let Some((col, terms)) = crate::vtab::fts5_phrase_terms_column(&query, tok) {
match resolve_col(&col)? {
Some(ci) => Routed::PhraseInColumn(terms, ci),
None => return Ok(None),
}
} else if let Some((a, b, n)) = crate::vtab::fts5_two_term_near(&query, tok) {
Routed::Near(a, b, n as u32)
} else if let Some(tree) = crate::vtab::fts5_bare_term_bool_tree(&query, tok) {
Routed::BoolTree(tree)
} else if let Some(p) = crate::vtab::fts5_single_prefix_term(&query, tok) {
Routed::PrefixAnyColumn(p)
} else if let Some((col, p)) = crate::vtab::fts5_single_prefix_term_column(&query, tok) {
match resolve_col(&col)? {
Some(ci) => Routed::PrefixInColumn(p, ci),
None => return Ok(None),
}
} else {
return Ok(None);
};
let dmeta = self.table_meta(&format!("{name}_data"), None)?;
let data: Vec<(i64, Vec<u8>)> = self
.scan_table(&dmeta)?
.into_iter()
.filter_map(|(rowid, mut values)| {
match values.drain(..).nth(1) {
Some(Value::Blob(b)) => Some((rowid, b)),
_ => None,
}
})
.collect();
let rowids_opt = match &routed {
Routed::AnyColumn(term) => crate::fts5_index::lookup_term_rowids(&data, term),
Routed::InColumn(term, ci) => {
crate::fts5_index::lookup_term_rowids_in_column(&data, term, *ci)
}
Routed::Phrase(terms) => {
let refs: Vec<&[u8]> = terms.iter().map(Vec::as_slice).collect();
crate::fts5_index::lookup_phrase_rowids_k(&data, &refs)
}
Routed::PhraseInColumn(terms, ci) => {
let refs: Vec<&[u8]> = terms.iter().map(Vec::as_slice).collect();
crate::fts5_index::lookup_phrase_rowids_in_column_k(&data, &refs, *ci)
}
Routed::Near(a, b, n) => crate::fts5_index::lookup_near_rowids(&data, a, b, *n),
Routed::BoolTree(tree) => crate::fts5_index::lookup_bool_tree_rowids(&data, tree),
Routed::PrefixAnyColumn(p) => crate::fts5_index::lookup_prefix_rowids(&data, p),
Routed::PrefixInColumn(p, ci) => {
crate::fts5_index::lookup_prefix_rowids_in_column(&data, p, *ci)
}
};
let rowids = match rowids_opt {
Some(r) => r,
None => return Ok(None),
};
let cmeta = self.table_meta(&format!("{name}_content"), None)?;
let encoding = self.backend.source().header().text_encoding;
let mut rows = Vec::with_capacity(rowids.len());
let mut cur = TableCursor::new(self.backend.source(), cmeta.root);
for rid in rowids {
if cur.seek(rid)? {
let mut values = self.decode_full_row(&cmeta, rid, &cur.payload()?, encoding)?;
if !values.is_empty() {
values.remove(0);
}
rows.push(InputRow {
values,
rowid: Some(rid),
});
}
}
Ok(Some(rows))
}
fn try_virtual_table(
&self,
name: &str,
alias: Option<&str>,
pushdown: Option<(&Select, &Params)>,
) -> Result<Option<(Vec<ColumnInfo>, Vec<InputRow>)>> {
use crate::schema::ObjectType;
let obj = match self
.schema
.objects()
.iter()
.find(|o| o.obj_type == ObjectType::Table && o.name.eq_ignore_ascii_case(name))
{
Some(o) => o,
None => return Ok(None),
};
let sql = match obj.sql.as_deref() {
Some(s) => s,
None => return Ok(None),
};
let cvt = match sql::parse_one(sql) {
Ok(Statement::CreateVirtualTable(cvt)) => cvt,
_ => return Ok(None),
};
#[cfg(feature = "fts5")]
if cvt.module.eq_ignore_ascii_case("fts5vocab") {
return Ok(Some(self.scan_fts5vocab(&cvt.args, name, alias)?));
}
let module = self
.vtab_registry
.get(&cvt.module)
.ok_or_else(|| Error::Error(format!("no such module: {}", cvt.module)))?;
let arg_refs: Vec<&str> = cvt.args.iter().map(String::as_str).collect();
let schema = module.dyn_connect(&arg_refs)?;
let label = alias.unwrap_or(name).to_string();
let columns: Vec<ColumnInfo> = schema
.columns
.iter()
.map(|n| ColumnInfo {
name: n.clone(),
table: label.clone(),
affinity: eval::Affinity::Blob,
collation: crate::value::Collation::default(),
})
.collect();
if module.dyn_persistent() {
let rtree = cvt.module.eq_ignore_ascii_case("rtree")
|| cvt.module.eq_ignore_ascii_case("rtree_i32");
if rtree
&& self.schema.table(&format!("{name}_node")).is_some()
&& self.schema.table(&format!("{name}_data")).is_none()
{
let n_coords = crate::vtab::RTreeModule::n_coords(&arg_refs);
if columns.len() == 1 + n_coords {
let integer = cvt.module.eq_ignore_ascii_case("rtree_i32");
let bbox: Vec<(usize, ConstraintOp, f64)> = match pushdown {
Some((sel, params)) => {
let (cs, vs) = collect_vtab_constraints(sel, &columns, params);
cs.iter()
.zip(vs)
.filter_map(|(c, v)| {
let ci = c.column.checked_sub(1)?;
if ci >= n_coords {
return None;
}
let fv = match v {
Value::Integer(i) => i as f64,
Value::Real(r) => r,
_ => return None,
};
matches!(
c.op,
ConstraintOp::Eq
| ConstraintOp::Gt
| ConstraintOp::Le
| ConstraintOp::Lt
| ConstraintOp::Ge
)
.then_some((ci, c.op, fv))
})
.collect()
}
None => Vec::new(),
};
let rows = self.scan_rtree_nodes(name, n_coords, integer, &bbox)?;
return Ok(Some((columns, rows)));
}
}
#[cfg(feature = "fts5")]
if cvt.module.eq_ignore_ascii_case("fts5")
&& self.schema.table(&format!("{name}_content")).is_some()
{
if let Some(rows) = self.fts5_try_index_match(name, alias, &arg_refs, pushdown)? {
return Ok(Some((columns, rows)));
}
let cmeta = self.table_meta(&format!("{name}_content"), None)?;
let rows = self
.scan_table(&cmeta)?
.into_iter()
.map(|(rowid, mut values)| {
if !values.is_empty() {
values.remove(0);
}
InputRow {
values,
rowid: Some(rowid),
}
})
.collect();
return Ok(Some((columns, rows)));
}
let backing = format!("{name}_data");
let bmeta = self.table_meta(&backing, None)?;
let rows = self
.scan_table(&bmeta)?
.into_iter()
.map(|(rowid, values)| InputRow {
values,
rowid: Some(rowid),
})
.collect();
return Ok(Some((columns, rows)));
}
let (constraints, bound_values) = match pushdown {
Some((sel, params)) => collect_vtab_constraints(sel, &columns, params),
None => (Vec::new(), Vec::new()),
};
let plan = module.dyn_best_index(&constraints)?;
let argv = order_vtab_argv(&plan, &bound_values);
let mut cursor = module.dyn_open(&arg_refs, &plan, &argv)?;
let ncols = columns.len();
let mut rows = Vec::new();
while let Some(row) = cursor.dyn_next()? {
let values = (0..ncols).map(|i| row.dyn_column(i)).collect();
rows.push(InputRow {
values,
rowid: Some(row.dyn_rowid()),
});
}
Ok(Some((columns, rows)))
}
fn push_ctes(&self, ctes: &[Cte], params: &Params, outer_cap: Option<usize>) -> Result<()> {
for (i, cte) in ctes.iter().enumerate() {
if ctes[..i]
.iter()
.any(|prev| prev.name.eq_ignore_ascii_case(&cte.name))
{
return Err(Error::Error(alloc::format!(
"duplicate WITH table name: {}",
cte.name
)));
}
}
for cte in ctes {
let binding = if references_name(&cte.select, &cte.name) {
self.eval_recursive_cte(cte, params, outer_cap)?
} else {
self.materialize_plain_cte(cte, params)?
};
self.cte_env.borrow_mut().push(binding);
}
Ok(())
}
fn lookup_cte(
&self,
name: &str,
alias: Option<&str>,
) -> Option<(Vec<ColumnInfo>, Vec<InputRow>)> {
let env = self.cte_env.borrow();
let b = env
.iter()
.rev()
.find(|b| b.name.eq_ignore_ascii_case(name))?;
let label = alias.unwrap_or(&b.name);
let columns = b
.columns
.iter()
.map(|c| ColumnInfo {
name: c.name.clone(),
table: label.to_string(),
affinity: c.affinity,
collation: c.collation,
})
.collect();
Some((columns, b.rows.clone()))
}
fn cte_columns(&self, cte: &Cte, body_cols: &[String]) -> Result<Vec<ColumnInfo>> {
let names = if cte.columns.is_empty() {
body_cols.to_vec()
} else {
if cte.columns.len() != body_cols.len() {
return Err(Error::Error(alloc::format!(
"table {} has {} values for {} columns",
cte.name,
body_cols.len(),
cte.columns.len()
)));
}
cte.columns.clone()
};
Ok(names
.into_iter()
.map(|n| ColumnInfo {
name: n,
table: cte.name.clone(),
affinity: eval::Affinity::Blob,
collation: crate::value::Collation::default(),
})
.collect())
}
fn materialize_plain_cte(&self, cte: &Cte, params: &Params) -> Result<CteBinding> {
let result = self.run_select(&cte.select, params)?;
let columns = self.cte_columns(cte, &result.columns)?;
let rows = result
.rows
.into_iter()
.map(|values| InputRow {
values,
rowid: None,
})
.collect();
Ok(CteBinding {
name: cte.name.clone(),
columns,
rows,
})
}
fn eval_recursive_cte(
&self,
cte: &Cte,
params: &Params,
outer_cap: Option<usize>,
) -> Result<CteBinding> {
let mut arms: Vec<(Option<CompoundOp>, Select)> = Vec::new();
let mut base = (*cte.select).clone();
let rec_limit = match &base.limit {
Some(e) => {
let n = must_be_int(eval::eval(
e,
&EvalCtx::rowless(params).with_subqueries(self),
)?)?;
(n >= 0).then_some(n as usize)
}
None => None,
};
let rec_offset = match &base.offset {
Some(e) => must_be_int(eval::eval(
e,
&EvalCtx::rowless(params).with_subqueries(self),
)?)?
.max(0) as usize,
None => 0,
};
let compound = core::mem::take(&mut base.compound);
base.order_by.clear();
base.limit = None;
base.offset = None;
arms.push((None, base));
for (op, mut s) in compound {
s.order_by.clear();
s.limit = None;
s.offset = None;
arms.push((Some(op), s));
}
let mut anchor: Vec<Select> = Vec::new();
let mut recursive: Vec<Select> = Vec::new();
let mut rec_distinct = false;
let mut in_rec = false;
for (op, s) in arms {
if !in_rec && references_name_select(&s, &cte.name) {
in_rec = true;
rec_distinct = matches!(op, Some(CompoundOp::Union));
}
if in_rec {
recursive.push(s);
} else {
anchor.push(s);
}
}
if anchor.is_empty() || recursive.is_empty() {
return Err(Error::Unsupported(
"recursive CTE must have a non-recursive anchor and a recursive term",
));
}
let mut anchor_rows: Vec<Vec<Value>> = Vec::new();
for a in &anchor {
let r = self.run_select(a, params)?;
anchor_rows.extend(r.rows);
}
let body_cols = self.run_select(&anchor[0], params)?.columns;
let columns = self.cte_columns(cte, &body_cols)?;
if rec_distinct {
dedup_rows(&mut anchor_rows);
}
let mut all_rows = anchor_rows.clone();
let mut working = anchor_rows;
let slot = self.cte_env.borrow().len();
self.cte_env.borrow_mut().push(CteBinding {
name: cte.name.clone(),
columns: columns.clone(),
rows: Vec::new(),
});
let mut guard = 0usize;
let result = loop {
guard += 1;
if guard > 1_000_000 {
self.cte_env.borrow_mut().truncate(slot);
return Err(Error::Error("recursive CTE did not terminate".into()));
}
self.cte_env.borrow_mut()[slot].rows = working
.iter()
.cloned()
.map(|values| InputRow {
values,
rowid: None,
})
.collect();
let mut produced: Vec<Vec<Value>> = Vec::new();
for r in &recursive {
match self.run_select(r, params) {
Ok(res) => produced.extend(res.rows),
Err(e) => {
self.cte_env.borrow_mut().truncate(slot);
return Err(e);
}
}
}
let mut fresh: Vec<Vec<Value>> = Vec::new();
for row in produced {
if rec_distinct && all_rows.iter().any(|s| rows_equal(s, &row)) {
continue;
}
fresh.push(row);
}
if fresh.is_empty() {
break Ok(());
}
all_rows.extend(fresh.iter().cloned());
working = fresh;
if let Some(lim) = rec_limit {
if all_rows.len() >= rec_offset.saturating_add(lim) {
break Ok(());
}
}
if let Some(cap) = outer_cap {
if all_rows.len() >= cap {
break Ok(());
}
}
};
self.cte_env.borrow_mut().truncate(slot);
result?;
if rec_offset > 0 {
all_rows.drain(..rec_offset.min(all_rows.len()));
}
if let Some(lim) = rec_limit {
all_rows.truncate(lim);
}
let rows = all_rows
.into_iter()
.map(|values| InputRow {
values,
rowid: None,
})
.collect();
Ok(CteBinding {
name: cte.name.clone(),
columns,
rows,
})
}
fn try_view(
&self,
name: &str,
alias: Option<&str>,
params: &Params,
) -> Result<Option<(Vec<ColumnInfo>, Vec<InputRow>)>> {
use crate::schema::ObjectType;
if self.temp_has_view(name) {
return self.scan_db_view(DbRef::Temp, name, alias, params);
}
let obj = match self
.schema
.objects()
.iter()
.find(|o| o.obj_type == ObjectType::View && o.name.eq_ignore_ascii_case(name))
{
Some(o) => o.clone(),
None => return Ok(None),
};
let sql = obj
.sql
.as_deref()
.ok_or_else(|| Error::Corrupt("view has no CREATE statement".into()))?;
let Statement::CreateView(cv) = sql::parse_one(sql)? else {
return Err(Error::Corrupt("schema sql is not CREATE VIEW".into()));
};
let result = self.run_select(&cv.select, params)?;
let label = alias.unwrap_or(name).to_string();
let names = if cv.columns.is_empty() {
result.columns.clone()
} else {
cv.columns.clone()
};
let origins = self.subquery_column_origins(&cv.select);
let columns: Vec<ColumnInfo> = names
.into_iter()
.enumerate()
.map(|(i, n)| {
let (affinity, collation) = origins
.as_ref()
.and_then(|o| o.get(i).copied())
.unwrap_or((eval::Affinity::Blob, crate::value::Collation::default()));
ColumnInfo {
name: n,
table: label.clone(),
affinity,
collation,
}
})
.collect();
let rows = result
.rows
.into_iter()
.map(|values| InputRow {
values,
rowid: None,
})
.collect();
Ok(Some((columns, rows)))
}
fn exec_drop(&mut self, d: &Drop) -> Result<()> {
use crate::schema::ObjectType;
if matches!(d.kind, DropKind::Table) && self.is_virtual_table(&d.name) {
for suffix in [
"_data", "_node", "_rowid", "_parent", "_content", "_docsize", "_config", "_idx",
] {
let backing = format!("{}{suffix}", d.name);
if self.schema.table(&backing).is_some() {
self.exec_drop(&Drop {
kind: DropKind::Table,
if_exists: false,
name: backing,
schema: d.schema.clone(),
})?;
}
}
}
let want = match d.kind {
DropKind::Table => ObjectType::Table,
DropKind::Index => ObjectType::Index,
DropKind::View => ObjectType::View,
DropKind::Trigger => ObjectType::Trigger,
};
let target = self
.schema
.objects()
.iter()
.find(|o| o.obj_type == want && o.name == d.name)
.cloned();
let Some(obj) = target else {
if let Some(other) = self.schema.objects().iter().find(|o| o.name == d.name) {
match (d.kind, other.obj_type) {
(DropKind::Table, ObjectType::View) => {
return Err(Error::Error(format!(
"use DROP VIEW to delete view {}",
d.name
)))
}
(DropKind::View, ObjectType::Table) => {
return Err(Error::Error(format!(
"use DROP TABLE to delete table {}",
d.name
)))
}
_ => {}
}
}
if d.if_exists {
return Ok(());
}
let kind = match d.kind {
DropKind::Table => "table",
DropKind::Index => "index",
DropKind::View => "view",
DropKind::Trigger => "trigger",
};
return Err(Error::Error(format!("no such {kind}: {}", d.name)));
};
let mut roots_to_free = Vec::new();
let mut names_to_remove = Vec::new();
roots_to_free.push(obj.rootpage);
names_to_remove.push(obj.name.clone());
if want == ObjectType::Table {
for idx in self.schema.indexes_on(&obj.name) {
roots_to_free.push(idx.rootpage);
names_to_remove.push(idx.name.clone());
}
for o in self.schema.objects() {
if o.obj_type == ObjectType::Trigger && o.tbl_name.eq_ignore_ascii_case(&obj.name) {
roots_to_free.push(o.rootpage); names_to_remove.push(o.name.clone());
}
}
}
let victim_rowids = self.schema_rowids_for(&names_to_remove)?;
let w = self.backend.writer()?;
for root in roots_to_free {
if root != 0 {
free_tree(w, root)?;
}
}
for rid in victim_rowids {
delete_table(w, crate::schema::SCHEMA_ROOT_PAGE, rid)?;
}
let cookie = w.header().schema_cookie.wrapping_add(1);
w.header_mut().schema_cookie = cookie;
self.schema = Schema::read(self.backend.source())?;
if want == ObjectType::Table && self.schema.table("sqlite_sequence").is_some() {
let root = self.schema.table("sqlite_sequence").unwrap().rootpage;
let meta = self.table_meta("sqlite_sequence", None)?;
let victims: Vec<i64> = self
.scan_table(&meta)?
.into_iter()
.filter(|(_, v)| matches!(&v[0], Value::Text(t) if t == &obj.name))
.map(|(rid, _)| rid)
.collect();
for rid in victims {
delete_table(self.backend.writer()?, root, rid)?;
}
}
Ok(())
}
fn exec_alter(&mut self, a: &Alter) -> Result<()> {
if self.is_virtual_table(&a.table) {
if let AlterAction::RenameTable(new_name) = &a.action {
return self.rename_virtual_table(&a.table, new_name);
}
return Err(Error::Error("virtual tables may not be altered".into()));
}
let obj = self
.schema
.table(&a.table)
.cloned()
.ok_or_else(|| Error::Error(format!("no such table: {}", a.table)))?;
let sql = obj
.sql
.as_deref()
.ok_or_else(|| Error::Corrupt("table has no CREATE statement".into()))?;
let Statement::CreateTable(mut ct) = sql::parse_one(sql)? else {
return Err(Error::Corrupt("schema sql is not CREATE TABLE".into()));
};
if let AlterAction::DropColumn(name) = &a.action {
return self.exec_drop_column(a, ct, name);
}
match &a.action {
AlterAction::DropColumn(_) => unreachable!("handled above"),
AlterAction::AddColumn(cd, col_text) => {
if ct
.columns
.iter()
.any(|c| c.name.eq_ignore_ascii_case(&cd.name))
{
return Err(Error::Error(format!("duplicate column name: {}", cd.name)));
}
for k in &cd.constraints {
match k {
ColumnConstraint::Unique(_) => {
return Err(Error::Error("Cannot add a UNIQUE column".into()));
}
ColumnConstraint::PrimaryKey { .. } => {
return Err(Error::Error("Cannot add a PRIMARY KEY column".into()));
}
ColumnConstraint::Default(e)
if unknown_column_ref(e, &[], false, None).is_some() =>
{
return Err(Error::Error(format!(
"default value of column [{}] is not constant",
cd.name
)));
}
_ => {}
}
}
let not_null = cd
.constraints
.iter()
.any(|k| matches!(k, ColumnConstraint::NotNull(_)));
if not_null {
let default = cd.constraints.iter().find_map(|k| match k {
ColumnConstraint::Default(e) => Some(e),
_ => None,
});
let no_params = Params::default();
let default_is_null = match default {
None => true,
Some(e) => {
let ctx = EvalCtx::rowless(&no_params).with_subqueries(self);
matches!(eval::eval(e, &ctx), Ok(Value::Null) | Err(_))
}
};
if default_is_null && !self.table_is_empty(&a.table)? {
return Err(Error::Error(
"Cannot add a NOT NULL column with default value NULL".into(),
));
}
}
ct.columns.push(cd.clone());
let reprint = sql::print::create_table(&ct);
let table = a.table.clone();
let col_text = col_text.clone();
self.rewrite_schema_rows(|cols| {
if is_text(&cols[0], "table") && is_text(&cols[1], &table) {
let updated = match (&col_text, cols.get(4)) {
(Some(t), Some(Value::Text(old))) => {
append_column_to_create(old, t).unwrap_or_else(|| reprint.clone())
}
_ => reprint.clone(),
};
cols[4] = Value::Text(updated);
true
} else {
false
}
})?;
}
AlterAction::RenameTable(new_name) => {
if self
.schema
.objects()
.iter()
.any(|o| o.name.eq_ignore_ascii_case(new_name))
{
return Err(Error::Error(format!(
"there is already another table or index with this name: {new_name}"
)));
}
let old = a.table.clone();
let new_name = new_name.clone();
self.rewrite_schema_rows(|cols| {
if is_text(&cols[0], "table") && is_text(&cols[1], &old) {
cols[1] = Value::Text(new_name.clone());
cols[2] = Value::Text(new_name.clone());
if let Some(Value::Text(old_sql)) = cols.get(4).cloned() {
let renamed = rename_table_token_after(&old_sql, "table", &new_name);
cols[4] = Value::Text(rewrite_fk_references(&renamed, &old, &new_name));
}
true
} else if is_text(&cols[2], &old) {
cols[2] = Value::Text(new_name.clone());
if is_text(&cols[0], "index") {
if let Some(Value::Text(isql)) = cols.get(4).cloned() {
cols[4] =
Value::Text(rename_table_token_after(&isql, "on", &new_name));
}
} else if is_text(&cols[0], "trigger") {
if let Some(Value::Text(tsql)) = cols.get(4).cloned() {
cols[4] = Value::Text(rewrite_ident_tokens(
&tsql,
&old,
&sql::print::ident(&new_name),
));
}
}
true
} else if is_text(&cols[0], "view") {
match cols.get(4).cloned() {
Some(Value::Text(vsql)) if view_uses_table(&vsql, &old) => {
cols[4] = Value::Text(rewrite_ident_tokens(
&vsql,
&old,
&sql::print::ident(&new_name),
));
true
}
_ => false,
}
} else if is_text(&cols[0], "trigger") {
match cols.get(4).cloned() {
Some(Value::Text(tsql)) if trigger_uses_table(&tsql, &old) => {
cols[4] = Value::Text(rewrite_ident_tokens(
&tsql,
&old,
&sql::print::ident(&new_name),
));
true
}
_ => false,
}
} else if is_text(&cols[0], "table") {
match cols.get(4).cloned() {
Some(Value::Text(tsql)) => {
let rewritten = rewrite_fk_references(&tsql, &old, &new_name);
if rewritten != tsql {
cols[4] = Value::Text(rewritten);
true
} else {
false
}
}
_ => false,
}
} else {
false
}
})?;
}
AlterAction::RenameColumn { old, new, new_text } => {
let pos = ct
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(old))
.ok_or_else(|| Error::Error(format!("no such column: \"{old}\"")))?;
if ct
.columns
.iter()
.enumerate()
.any(|(i, c)| i != pos && c.name.eq_ignore_ascii_case(new))
{
return Err(Error::Error(format!("duplicate column name: {new}")));
}
ct.columns[pos].name = new.clone();
let rename = |e: &mut Expr| rename_column_ref(e, &a.table, old, new);
for col in &mut ct.columns {
for k in &mut col.constraints {
match k {
ColumnConstraint::Check(e, _) | ColumnConstraint::Default(e) => {
rename(e)
}
ColumnConstraint::Generated { expr, .. } => rename(expr),
_ => {}
}
}
}
for tc in &mut ct.constraints {
match tc {
TableConstraint::PrimaryKey(n, _) | TableConstraint::Unique(n, _) => {
for nm in n {
if nm.eq_ignore_ascii_case(old) {
*nm = new.clone();
}
}
}
TableConstraint::Check(e, _) => rename(e),
TableConstraint::ForeignKey(fk) => {
for nm in &mut fk.columns {
if nm.eq_ignore_ascii_case(old) {
*nm = new.clone();
}
}
}
}
}
let reprint = sql::print::create_table(&ct);
let table = a.table.clone();
let old = old.clone();
let new_text = new_text.clone();
let table_cols: alloc::collections::BTreeMap<String, Vec<String>> = self
.schema
.objects()
.iter()
.filter(|o| o.obj_type == crate::schema::ObjectType::Table)
.filter_map(|o| {
self.table_meta(&o.name, None).ok().map(|m| {
(
o.name.clone(),
m.columns.iter().map(|c| c.name.clone()).collect(),
)
})
})
.collect();
self.rewrite_schema_rows(|cols| {
if is_text(&cols[0], "table") && is_text(&cols[1], &table) {
cols[4] = Value::Text(match cols.get(4) {
Some(Value::Text(s)) => rewrite_ident_tokens(s, &old, &new_text),
_ => reprint.clone(),
});
true
} else if is_text(&cols[0], "index") && is_text(&cols[2], &table) {
if let Some(Value::Text(isql)) = cols.get(4).cloned() {
let rewritten = rewrite_ident_tokens(&isql, &old, &new_text);
if rewritten != isql {
cols[4] = Value::Text(rewritten);
return true;
}
}
false
} else if is_text(&cols[0], "table") {
if let Some(Value::Text(csql)) = cols.get(4).cloned() {
let rewritten =
rewrite_fk_parent_column(&csql, &table, &old, &new_text);
if rewritten != csql {
cols[4] = Value::Text(rewritten);
return true;
}
}
false
} else if is_text(&cols[0], "view") {
match cols.get(4).cloned() {
Some(Value::Text(vsql)) => {
let rewritten = if let Some(quals) =
view_single_source_column_quals(&vsql, &table, &old)
{
rewrite_column_tokens(&vsql, &quals, &old, &new_text, true)
} else if let Some((quals, bare)) =
view_multi_source_quals(&vsql, &table, &old, &table_cols)
{
rewrite_column_tokens(&vsql, &quals, &old, &new_text, bare)
} else {
vsql.clone()
};
if rewritten != vsql {
cols[4] = Value::Text(rewritten);
return true;
}
false
}
_ => false,
}
} else if is_text(&cols[0], "trigger") {
match cols.get(4).cloned() {
Some(Value::Text(tsql)) => {
let rewritten = if let Some(quals) =
trigger_single_source_quals(&tsql, &table, &old)
{
rewrite_column_tokens(&tsql, &quals, &old, &new_text, true)
} else if trigger_on_renamed_table(&tsql, &table, &old) {
rewrite_column_tokens(
&tsql,
&[String::from("NEW"), String::from("OLD")],
&old,
&new_text,
false,
)
} else if trigger_body_single_source_over(&tsql, &table, &old) {
rewrite_column_tokens(
&tsql,
core::slice::from_ref(&table),
&old,
&new_text,
true,
)
} else {
tsql.clone()
};
if rewritten != tsql {
cols[4] = Value::Text(rewritten);
return true;
}
false
}
_ => false,
}
} else {
false
}
})?;
}
}
let cookie = self
.backend
.writer()?
.header()
.schema_cookie
.wrapping_add(1);
self.backend.writer()?.header_mut().schema_cookie = cookie;
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
fn rename_virtual_table(&mut self, old: &str, new: &str) -> Result<()> {
if self
.schema
.objects()
.iter()
.any(|o| o.name.eq_ignore_ascii_case(new))
{
return Err(Error::Error(format!(
"there is already another table or index with this name: {new}"
)));
}
for suffix in [
"_data", "_node", "_rowid", "_parent", "_content", "_docsize", "_config", "_idx",
] {
let backing_old = format!("{old}{suffix}");
if self.schema.table(&backing_old).is_some() {
self.exec_alter(&Alter {
schema: None,
table: backing_old,
action: AlterAction::RenameTable(format!("{new}{suffix}")),
})?;
}
}
let old_s = old.to_string();
let new_s = new.to_string();
self.rewrite_schema_rows(|cols| {
if is_text(&cols[0], "table") && is_text(&cols[1], &old_s) {
cols[1] = Value::Text(new_s.clone());
cols[2] = Value::Text(new_s.clone());
if let Some(Value::Text(s)) = cols.get(4).cloned() {
cols[4] =
Value::Text(rewrite_ident_tokens(&s, &old_s, &sql::print::ident(&new_s)));
}
true
} else {
false
}
})?;
let cookie = self
.backend
.writer()?
.header()
.schema_cookie
.wrapping_add(1);
self.backend.writer()?.header_mut().schema_cookie = cookie;
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
fn exec_drop_column(&mut self, a: &Alter, mut ct: CreateTable, name: &str) -> Result<()> {
let pos = ct
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(name))
.ok_or_else(|| Error::Error(format!("no such column: \"{name}\"")))?;
if ct.columns.len() <= 1 {
return Err(Error::Error(format!(
"cannot drop column \"{name}\": no other columns exist"
)));
}
let cannot = |why: &str| {
Err(Error::Error(format!(
"cannot drop column \"{name}\": {why}"
)))
};
for c in &ct.columns[pos].constraints {
match c {
ColumnConstraint::PrimaryKey { .. } => return cannot("PRIMARY KEY"),
ColumnConstraint::Unique(_) => return cannot("UNIQUE"),
ColumnConstraint::Check(..) => return cannot("CHECK"),
ColumnConstraint::References(_) => return cannot("FOREIGN KEY"),
ColumnConstraint::Generated { .. } => return cannot("generated"),
_ => {}
}
}
for tc in &ct.constraints {
match tc {
TableConstraint::PrimaryKey(n, _) | TableConstraint::Unique(n, _) => {
if n.iter().any(|x| x.eq_ignore_ascii_case(name)) {
return cannot("PRIMARY KEY or UNIQUE");
}
}
TableConstraint::Check(..) => return cannot("a table CHECK constraint exists"),
TableConstraint::ForeignKey(_) => {
return cannot("a table FOREIGN KEY constraint exists")
}
}
}
if ct.columns.iter().enumerate().any(|(i, c)| {
i != pos
&& c.constraints
.iter()
.any(|x| matches!(x, ColumnConstraint::Generated { .. }))
}) {
return cannot("a generated column exists");
}
let meta = self.table_meta(&a.table, None)?;
if meta.ipk == Some(pos) {
return cannot("PRIMARY KEY");
}
let indexes = self.indexes_of(&a.table)?;
if indexes
.iter()
.any(|i| i.cols.contains(&pos) || i.key_exprs.is_some() || i.partial.is_some())
{
return cannot("it is indexed");
}
let new_rows: Vec<(i64, Vec<Value>)> = self
.scan_table(&meta)?
.into_iter()
.map(|(rid, mut vals)| {
vals.remove(pos);
(rid, vals)
})
.collect();
ct.columns.remove(pos);
let reprint = sql::print::create_table(&ct);
let table = a.table.clone();
let dropped = name.to_string();
self.rewrite_schema_rows(|cols| {
if is_text(&cols[0], "table") && is_text(&cols[1], &table) {
let updated = match cols.get(4) {
Some(Value::Text(old)) => {
drop_column_from_create(old, &dropped).unwrap_or_else(|| reprint.clone())
}
_ => reprint.clone(),
};
cols[4] = Value::Text(updated);
true
} else {
false
}
})?;
self.schema = Schema::read(self.backend.source())?;
let new_meta = self.table_meta(&a.table, None)?;
clear_table(self.backend.writer()?, new_meta.root)?;
for (rid, vals) in &new_rows {
let mut stored = vals.clone();
if let Some(ipk) = new_meta.ipk {
stored[ipk] = Value::Null;
}
let record = encode_record(&stored);
insert_table(self.backend.writer()?, new_meta.root, *rid, &record)?;
}
let new_indexes = self.indexes_of(&a.table)?;
self.rebuild_indexes(&new_meta, &new_indexes)?;
let cookie = self
.backend
.writer()?
.header()
.schema_cookie
.wrapping_add(1);
self.backend.writer()?.header_mut().schema_cookie = cookie;
self.schema = Schema::read(self.backend.source())?;
Ok(())
}
fn rewrite_schema_rows(&mut self, mut f: impl FnMut(&mut Vec<Value>) -> bool) -> Result<()> {
let encoding = self.backend.source().header().text_encoding;
let mut changes: Vec<(i64, Vec<u8>)> = Vec::new();
{
let mut cur = TableCursor::new(self.backend.source(), crate::schema::SCHEMA_ROOT_PAGE);
let mut ok = cur.first()?;
while ok {
let mut cols = decode_record(&cur.payload()?, encoding)?;
cols.resize(5, Value::Null);
if f(&mut cols) {
changes.push((cur.rowid()?, encode_record(&cols)));
}
ok = cur.next()?;
}
}
let w = self.backend.writer()?;
for (rid, rec) in changes {
delete_table(w, crate::schema::SCHEMA_ROOT_PAGE, rid)?;
insert_table(w, crate::schema::SCHEMA_ROOT_PAGE, rid, &rec)?;
}
Ok(())
}
fn schema_rowids_for(&self, names: &[String]) -> Result<Vec<i64>> {
let encoding = self.backend.source().header().text_encoding;
let mut out = Vec::new();
let mut cur = TableCursor::new(self.backend.source(), crate::schema::SCHEMA_ROOT_PAGE);
let mut ok = cur.first()?;
while ok {
let cols = decode_record(&cur.payload()?, encoding)?;
if let Some(Value::Text(name)) = cols.get(1) {
if names.iter().any(|n| n == name) {
out.push(cur.rowid()?);
}
}
ok = cur.next()?;
}
Ok(out)
}
fn try_without_rowid_pk_seek(
&self,
meta: &TableMeta,
sel: &Select,
params: &Params,
) -> Result<Option<Vec<InputRow>>> {
let Some(where_expr) = &sel.where_clause else {
return Ok(None);
};
let hint = sel.from.as_ref().and_then(|f| f.first.index_hint.as_ref());
if matches!(hint, Some(IndexHint::NotIndexed)) {
return Ok(None);
}
let pk = &meta.storage_order[..meta.pk_len];
if pk.is_empty() {
return Ok(None);
}
let mut eqs: Vec<(usize, Value)> = Vec::new();
collect_eq_constraints(where_expr, &meta.columns, params, &mut eqs);
let storage_colls = wr_storage_collations(meta);
let mut key = Vec::new();
let mut colls = Vec::new();
for (i, &c) in pk.iter().enumerate() {
let Some((_, v)) = eqs.iter().find(|(col, _)| *col == c) else {
break;
};
if matches!(v, Value::Null) {
break; }
key.push(meta.columns[c].affinity.coerce(v.clone()));
colls.push(storage_colls[i]);
}
if key.is_empty() {
return Ok(None);
}
let records =
crate::btree::index_seek_records(self.backend.source(), meta.root, &key, &colls)?;
let mut out = Vec::with_capacity(records.len());
for storage in records {
let mut row = unpermute_row(meta, storage);
self.compute_generated(meta, &mut row, params)?;
out.push(InputRow {
values: row,
rowid: None,
});
}
Ok(Some(out))
}
fn try_without_rowid_pk_range(
&self,
meta: &TableMeta,
sel: &Select,
params: &Params,
) -> Result<Option<Vec<InputRow>>> {
let Some(where_expr) = &sel.where_clause else {
return Ok(None);
};
let hint = sel.from.as_ref().and_then(|f| f.first.index_hint.as_ref());
if matches!(hint, Some(IndexHint::NotIndexed)) {
return Ok(None);
}
let pk = &meta.storage_order[..meta.pk_len];
let Some(&lead) = pk.first() else {
return Ok(None);
};
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
let Some(b) = ranges.get(&lead) else {
return Ok(None);
};
let aff = meta.columns[lead].affinity;
let coll = wr_storage_collations(meta)[0];
let lower = b.lower.as_ref().map(|(v, i)| (aff.coerce(v.clone()), *i));
let upper = b.upper.as_ref().map(|(v, i)| (aff.coerce(v.clone()), *i));
let colls = [coll];
let lower_arg = lower
.as_ref()
.map(|(v, inc)| (core::slice::from_ref(v), *inc));
let upper_arg = upper
.as_ref()
.map(|(v, inc)| (core::slice::from_ref(v), *inc));
let records = crate::btree::index_range_records(
self.backend.source(),
meta.root,
lower_arg,
upper_arg,
&colls,
)?;
let mut out = Vec::with_capacity(records.len());
for storage in records {
let mut row = unpermute_row(meta, storage);
self.compute_generated(meta, &mut row, params)?;
out.push(InputRow {
values: row,
rowid: None,
});
}
Ok(Some(out))
}
fn try_without_rowid_index_seek(
&self,
meta: &TableMeta,
table_name: &str,
sel: &Select,
params: &Params,
) -> Result<Option<Vec<InputRow>>> {
let Some(where_expr) = &sel.where_clause else {
return Ok(None);
};
let hint = sel.from.as_ref().and_then(|f| f.first.index_hint.as_ref());
if matches!(hint, Some(IndexHint::NotIndexed)) {
return Ok(None);
}
let mut eqs: Vec<(usize, Value)> = Vec::new();
collect_eq_constraints(where_expr, &meta.columns, params, &mut eqs);
eqs.retain(|(_, v)| !matches!(v, Value::Null));
if eqs.is_empty() {
return Ok(None);
}
let pk: Vec<usize> = meta.storage_order[..meta.pk_len].to_vec();
let indexes = self.indexes_of(table_name)?;
if let Some(IndexHint::IndexedBy(n)) = hint {
if !indexes.iter().any(|i| i.name.eq_ignore_ascii_case(n)) {
return Err(Error::Error(alloc::format!("no such index: {n}")));
}
}
let src = self.backend.source();
for idx in &indexes {
if let Some(IndexHint::IndexedBy(n)) = hint {
if !idx.name.eq_ignore_ascii_case(n) {
continue;
}
}
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
let mut key = Vec::new();
let mut colls = Vec::new();
for (i, &c) in idx.cols.iter().enumerate() {
let Some((_, v)) = eqs.iter().find(|(col, _)| *col == c) else {
break;
};
key.push(meta.columns[c].affinity.coerce(v.clone()));
colls.push(idx.collations.get(i).copied().unwrap_or_default());
}
if key.is_empty() {
continue;
}
let records = crate::btree::index_seek_records(src, idx.root, &key, &colls)?;
let covering = self.wr_index_covers(idx, &pk, meta, sel, where_expr);
return Ok(Some(
self.wr_index_rows(meta, idx, &pk, records, covering, params)?,
));
}
Ok(None)
}
fn wr_index_covers(
&self,
idx: &IndexMeta,
pk: &[usize],
meta: &TableMeta,
sel: &Select,
where_expr: &Expr,
) -> bool {
let mut avail = idx.cols.clone();
if !idx.name.starts_with("sqlite_autoindex_") {
for &p in pk {
if !avail.contains(&p) {
avail.push(p);
}
}
}
self.seek_index_covers(sel, meta, &avail, where_expr)
}
fn wr_index_rows(
&self,
meta: &TableMeta,
idx: &IndexMeta,
pk: &[usize],
records: Vec<Vec<Value>>,
covering: bool,
params: &Params,
) -> Result<Vec<InputRow>> {
let mut out = Vec::with_capacity(records.len());
if covering {
for rec in &records {
let mut values = alloc::vec![Value::Null; meta.columns.len()];
for (i, &mc) in idx.cols.iter().enumerate() {
values[mc] = rec[i].clone();
}
for (j, &pc) in pk.iter().enumerate() {
values[pc] = rec[idx.cols.len() + j].clone();
}
out.push(InputRow {
values,
rowid: None,
});
}
} else {
let src = self.backend.source();
let pk_colls: Vec<crate::value::Collation> =
wr_storage_collations(meta)[..pk.len()].to_vec();
for rec in &records {
let pk_key: Vec<Value> = (0..pk.len())
.map(|j| rec[idx.cols.len() + j].clone())
.collect();
for storage in crate::btree::index_seek_records(src, meta.root, &pk_key, &pk_colls)?
{
let mut row = unpermute_row(meta, storage);
self.compute_generated(meta, &mut row, params)?;
out.push(InputRow {
values: row,
rowid: None,
});
}
}
}
Ok(out)
}
fn try_without_rowid_index_range(
&self,
meta: &TableMeta,
table_name: &str,
sel: &Select,
params: &Params,
) -> Result<Option<Vec<InputRow>>> {
let Some(where_expr) = &sel.where_clause else {
return Ok(None);
};
let hint = sel.from.as_ref().and_then(|f| f.first.index_hint.as_ref());
if matches!(hint, Some(IndexHint::NotIndexed)) {
return Ok(None);
}
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
if ranges.is_empty() {
return Ok(None);
}
let pk: Vec<usize> = meta.storage_order[..meta.pk_len].to_vec();
let indexes = self.indexes_of(table_name)?;
if let Some(IndexHint::IndexedBy(n)) = hint {
if !indexes.iter().any(|i| i.name.eq_ignore_ascii_case(n)) {
return Err(Error::Error(alloc::format!("no such index: {n}")));
}
}
for idx in &indexes {
if let Some(IndexHint::IndexedBy(n)) = hint {
if !idx.name.eq_ignore_ascii_case(n) {
continue;
}
}
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
let Some(&lead) = idx.cols.first() else {
continue;
};
let Some(b) = ranges.get(&lead) else {
continue;
};
let aff = meta.columns[lead].affinity;
let coll = idx.collations.first().copied().unwrap_or_default();
let lower = b.lower.as_ref().map(|(v, i)| (aff.coerce(v.clone()), *i));
let upper = b.upper.as_ref().map(|(v, i)| (aff.coerce(v.clone()), *i));
let colls = [coll];
let lower_arg = lower
.as_ref()
.map(|(v, inc)| (core::slice::from_ref(v), *inc));
let upper_arg = upper
.as_ref()
.map(|(v, inc)| (core::slice::from_ref(v), *inc));
let records = crate::btree::index_range_records(
self.backend.source(),
idx.root,
lower_arg,
upper_arg,
&colls,
)?;
let covering = self.wr_index_covers(idx, &pk, meta, sel, where_expr);
return Ok(Some(
self.wr_index_rows(meta, idx, &pk, records, covering, params)?,
));
}
Ok(None)
}
fn try_index_lookup(
&self,
meta: &TableMeta,
table_name: &str,
sel: &Select,
params: &Params,
) -> Result<Option<Vec<InputRow>>> {
let Some(where_expr) = &sel.where_clause else {
return Ok(None);
};
let hint = sel.from.as_ref().and_then(|f| f.first.index_hint.as_ref());
if matches!(hint, Some(IndexHint::NotIndexed)) {
return Ok(None);
}
if !matches!(hint, Some(IndexHint::IndexedBy(_))) {
if let Some(rowids) = rowid_seek_constraint(where_expr, &meta.columns, params) {
let encoding = self.backend.source().header().text_encoding;
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut out = Vec::new();
let mut seen: Vec<i64> = Vec::new();
for rid in rowids {
if seen.contains(&rid) {
continue;
}
seen.push(rid);
if cur.seek(rid)? {
let values = self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
out.push(InputRow {
values,
rowid: Some(rid),
});
}
}
return Ok(Some(out));
}
}
let mut eqs: Vec<(usize, Value)> = Vec::new();
collect_eq_constraints(where_expr, &meta.columns, params, &mut eqs);
if eqs.is_empty() || eqs.iter().any(|(_, v)| matches!(v, Value::Null)) {
return self.partial_expr_lookup(meta, table_name, sel, where_expr, params);
}
if !matches!(hint, Some(IndexHint::IndexedBy(_))) {
if let Some(ipk) = meta.ipk {
if let Some((_, v)) = eqs.iter().find(|(c, _)| *c == ipk) {
let rid = eval::to_i64(v);
let encoding = self.backend.source().header().text_encoding;
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut out = Vec::new();
if cur.seek(rid)? {
let values = self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
out.push(InputRow {
values,
rowid: Some(rid),
});
}
return Ok(Some(out));
}
}
}
let indexes = self.indexes_of(table_name)?;
if let Some(IndexHint::IndexedBy(n)) = hint {
if !indexes.iter().any(|i| i.name.eq_ignore_ascii_case(n)) {
return Err(Error::Error(alloc::format!("no such index: {n}")));
}
}
let stats = self.stat1_map();
#[allow(clippy::type_complexity)]
let mut best: Option<(
u32,
Vec<Value>,
Vec<crate::value::Collation>,
Vec<usize>,
u64,
)> = None;
for idx in &indexes {
if let Some(IndexHint::IndexedBy(n)) = hint {
if !idx.name.eq_ignore_ascii_case(n) {
continue;
}
}
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
let mut key = Vec::new();
for &c in &idx.cols {
match eqs.iter().find(|(col, _)| *col == c) {
Some((_, v)) => key.push(meta.columns[c].affinity.coerce(v.clone())),
None => break,
}
}
if key.is_empty() {
continue;
}
let est = stats
.get(&idx.name)
.and_then(|s| s.get(key.len()).copied())
.unwrap_or(u64::MAX - key.len() as u64);
let better = match &best {
None => true,
Some((_, bk, _, _, be)) => est < *be || (est == *be && key.len() > bk.len()),
};
if better {
best = Some((idx.root, key, idx.collations.clone(), idx.cols.clone(), est));
}
}
let (root, key, full_colls, idx_cols) = match best {
Some((root, key, colls, idx_cols, _)) => (root, key, colls, idx_cols),
None => {
return self.partial_expr_lookup(meta, table_name, sel, where_expr, params);
}
};
if key.is_empty() {
return Ok(None);
}
if self.seek_index_covers(sel, meta, &idx_cols, where_expr) {
return Ok(Some(self.covering_seek_rows(meta, root, &idx_cols)?));
}
let next_pos = key.len();
if let Some(&next_col) = idx_cols.get(next_pos) {
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
if let Some(b) = ranges.get(&next_col) {
let aff = meta.columns[next_col].affinity;
let colls = full_colls[..=next_pos].to_vec();
let mut lo_key = key.clone();
let lo_inc = match b.lower.as_ref() {
Some((v, inc)) => {
lo_key.push(aff.coerce(v.clone()));
*inc
}
None => true,
};
let mut hi_key = key.clone();
let hi_inc = match b.upper.as_ref() {
Some((v, inc)) => {
hi_key.push(aff.coerce(v.clone()));
*inc
}
None => true,
};
let rowids = crate::btree::index_range_rowids(
self.backend.source(),
root,
Some((lo_key.as_slice(), lo_inc)),
Some((hi_key.as_slice(), hi_inc)),
&colls,
)?;
let encoding = self.backend.source().header().text_encoding;
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut out = Vec::new();
for rid in rowids {
if cur.seek(rid)? {
let values = self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
out.push(InputRow {
values,
rowid: Some(rid),
});
}
}
return Ok(Some(out));
}
}
self.index_seek_fetch(meta, root, &key, &full_colls[..key.len()])
}
fn index_seek_fetch(
&self,
meta: &TableMeta,
root: u32,
key: &[Value],
colls: &[crate::value::Collation],
) -> Result<Option<Vec<InputRow>>> {
let rowids = crate::btree::index_seek_rowids(self.backend.source(), root, key, colls)?;
let encoding = self.backend.source().header().text_encoding;
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut out = Vec::new();
for rid in rowids {
if cur.seek(rid)? {
let values = self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
out.push(InputRow {
values,
rowid: Some(rid),
});
}
}
Ok(Some(out))
}
fn partial_expr_lookup(
&self,
meta: &TableMeta,
table_name: &str,
sel: &Select,
where_expr: &Expr,
params: &Params,
) -> Result<Option<Vec<InputRow>>> {
let hint = sel.from.as_ref().and_then(|f| f.first.index_hint.as_ref());
for idx in self.indexes_of(table_name)? {
if let Some(IndexHint::IndexedBy(n)) = hint {
if !idx.name.eq_ignore_ascii_case(n) {
continue;
}
}
if let Some((key, colls)) = self.partial_expr_seek(&idx, where_expr, meta, params)? {
return self.index_seek_fetch(meta, idx.root, &key, &colls);
}
}
Ok(None)
}
fn partial_expr_seek(
&self,
idx: &IndexMeta,
where_expr: &Expr,
meta: &TableMeta,
params: &Params,
) -> Result<Option<(Vec<Value>, Vec<crate::value::Collation>)>> {
if idx.partial.is_none() && idx.key_exprs.is_none() {
return Ok(None);
}
let mut conjuncts = Vec::new();
and_conjuncts(where_expr, &mut conjuncts);
if let Some(pred) = &idx.partial {
if !conjuncts.iter().any(|c| expr_eq_modulo_parens(c, pred)) {
return Ok(None);
}
}
match &idx.key_exprs {
None => {
let mut key = Vec::new();
let mut colls = Vec::new();
let mut eqs: Vec<(usize, Value)> = Vec::new();
collect_eq_constraints(where_expr, &meta.columns, params, &mut eqs);
for (pos, &c) in idx.cols.iter().enumerate() {
match eqs
.iter()
.find(|(col, v)| *col == c && !matches!(v, Value::Null))
{
Some((_, v)) => {
key.push(meta.columns[c].affinity.coerce(v.clone()));
colls.push(idx.collations[pos]);
}
None => break,
}
}
if key.is_empty() {
return Ok(None);
}
Ok(Some((key, colls)))
}
Some(exprs) => {
let [key_expr] = exprs.as_slice() else {
return Ok(None);
};
for c in &conjuncts {
let Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} = unparen(c)
else {
continue;
};
let val = if expr_eq_modulo_parens(left, key_expr) {
const_value(right, params)
} else if expr_eq_modulo_parens(right, key_expr) {
const_value(left, params)
} else {
None
};
if let Some(v) = val {
if matches!(v, Value::Null) {
continue; }
let coll = idx.collations.first().copied().unwrap_or_default();
return Ok(Some((alloc::vec![v], alloc::vec![coll])));
}
}
Ok(None)
}
}
}
fn try_index_range(
&self,
meta: &TableMeta,
table_name: &str,
sel: &Select,
params: &Params,
) -> Result<Option<Vec<InputRow>>> {
let Some(where_expr) = &sel.where_clause else {
return Ok(None);
};
let hint = sel.from.as_ref().and_then(|f| f.first.index_hint.as_ref());
if matches!(hint, Some(IndexHint::NotIndexed)) {
return Ok(None);
}
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
if ranges.is_empty() {
return Ok(None);
}
if !matches!(hint, Some(IndexHint::IndexedBy(_))) {
if let Some(ipk) = meta.ipk {
if let Some(b) = ranges.get(&ipk) {
let int_bound = |o: &Option<(Value, bool)>| match o {
Some((Value::Integer(i), _)) => Some(*i),
None => None,
_ => Some(i64::MAX), };
let lo = int_bound(&b.lower);
let hi = int_bound(&b.upper);
let lo_ok =
b.lower.is_none() || matches!(b.lower, Some((Value::Integer(_), _)));
let hi_ok =
b.upper.is_none() || matches!(b.upper, Some((Value::Integer(_), _)));
if lo_ok && hi_ok {
let start = lo.unwrap_or(i64::MIN);
let stop = hi.unwrap_or(i64::MAX);
let encoding = self.backend.source().header().text_encoding;
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut out = Vec::new();
let mut ok = if start == i64::MIN {
cur.first()?
} else {
cur.seek(start)?;
cur.is_valid()
};
while ok {
let rid = cur.rowid()?;
if rid > stop {
break;
}
let values =
self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
out.push(InputRow {
values,
rowid: Some(rid),
});
ok = cur.next()?;
}
return Ok(Some(out));
}
}
}
}
let indexes = self.indexes_of(table_name)?;
if let Some(IndexHint::IndexedBy(n)) = hint {
if !indexes.iter().any(|i| i.name.eq_ignore_ascii_case(n)) {
return Err(Error::Error(alloc::format!("no such index: {n}")));
}
}
let mut chosen: Option<(u32, RangeBound, crate::value::Collation, Vec<usize>)> = None;
for idx in &indexes {
if let Some(IndexHint::IndexedBy(n)) = hint {
if !idx.name.eq_ignore_ascii_case(n) {
continue;
}
}
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
let Some(&lead) = idx.cols.first() else {
continue;
};
if let Some(b) = ranges.get(&lead) {
let coll = idx.collations.first().copied().unwrap_or_default();
let aff = meta.columns[lead].affinity;
let b = RangeBound {
lower: b.lower.as_ref().map(|(v, i)| (aff.coerce(v.clone()), *i)),
upper: b.upper.as_ref().map(|(v, i)| (aff.coerce(v.clone()), *i)),
};
chosen = Some((idx.root, b, coll, idx.cols.clone()));
break;
}
}
match chosen {
Some((root, bound, coll, idx_cols)) => {
if self.seek_index_covers(sel, meta, &idx_cols, where_expr) {
return Ok(Some(self.covering_seek_rows(meta, root, &idx_cols)?));
}
Ok(Some(self.range_seek_fetch(meta, root, &bound, coll)?))
}
None => {
for idx in &indexes {
if let Some(IndexHint::IndexedBy(n)) = hint {
if !idx.name.eq_ignore_ascii_case(n) {
continue;
}
}
if let Some((bound, coll)) =
self.partial_expr_range(idx, where_expr, meta, params)
{
return Ok(Some(self.range_seek_fetch(meta, idx.root, &bound, coll)?));
}
}
Ok(None)
}
}
}
fn range_seek_fetch(
&self,
meta: &TableMeta,
root: u32,
bound: &RangeBound,
coll: crate::value::Collation,
) -> Result<Vec<InputRow>> {
let colls = [coll];
let lower_key = bound.lower.as_ref().map(|(v, _)| core::slice::from_ref(v));
let upper_key = bound.upper.as_ref().map(|(v, _)| core::slice::from_ref(v));
let lower = lower_key.map(|k| (k, bound.lower.as_ref().unwrap().1));
let upper = upper_key.map(|k| (k, bound.upper.as_ref().unwrap().1));
let rowids =
crate::btree::index_range_rowids(self.backend.source(), root, lower, upper, &colls)?;
let encoding = self.backend.source().header().text_encoding;
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut out = Vec::new();
for rid in rowids {
if cur.seek(rid)? {
let values = self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
out.push(InputRow {
values,
rowid: Some(rid),
});
}
}
Ok(out)
}
fn in_seek_fetch(
&self,
meta: &TableMeta,
root: u32,
colls: &[crate::value::Collation],
keys: &[Vec<Value>],
) -> Result<Vec<InputRow>> {
let src = self.backend.source();
let encoding = src.header().text_encoding;
let mut rowids: Vec<i64> = Vec::new();
for key in keys {
for rid in crate::btree::index_seek_rowids(src, root, key, colls)? {
if !rowids.contains(&rid) {
rowids.push(rid);
}
}
}
let mut cur = TableCursor::new(src, meta.root);
let mut out = Vec::new();
for rid in rowids {
if cur.seek(rid)? {
let values = self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
out.push(InputRow {
values,
rowid: Some(rid),
});
}
}
Ok(out)
}
fn partial_expr_range(
&self,
idx: &IndexMeta,
where_expr: &Expr,
meta: &TableMeta,
params: &Params,
) -> Option<(RangeBound, crate::value::Collation)> {
if idx.partial.is_none() && idx.key_exprs.is_none() {
return None;
}
let mut conjuncts = Vec::new();
and_conjuncts(where_expr, &mut conjuncts);
if let Some(pred) = &idx.partial {
if !conjuncts.iter().any(|c| expr_eq_modulo_parens(c, pred)) {
return None;
}
}
let coll = idx.collations.first().copied().unwrap_or_default();
match &idx.key_exprs {
None => {
let lead = *idx.cols.first()?;
let mut ranges = alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
let b = ranges.get(&lead)?;
let aff = meta.columns[lead].affinity;
Some((
RangeBound {
lower: b.lower.as_ref().map(|(v, i)| (aff.coerce(v.clone()), *i)),
upper: b.upper.as_ref().map(|(v, i)| (aff.coerce(v.clone()), *i)),
},
coll,
))
}
Some(exprs) => {
let [key_expr] = exprs.as_slice() else {
return None;
};
let mut bound = RangeBound {
lower: None,
upper: None,
};
for c in &conjuncts {
let Expr::Binary { op, left, right } = unparen(c) else {
continue;
};
let (val, op) = if expr_eq_modulo_parens(left, key_expr) {
(const_value(right, params), *op)
} else if expr_eq_modulo_parens(right, key_expr) {
(const_value(left, params), mirror_comparison(*op))
} else {
continue;
};
let Some(v) = val else { continue };
if matches!(v, Value::Null) {
continue;
}
match op {
BinaryOp::Gt => bound.lower = Some((v, false)),
BinaryOp::GtEq => bound.lower = Some((v, true)),
BinaryOp::Lt => bound.upper = Some((v, false)),
BinaryOp::LtEq => bound.upper = Some((v, true)),
_ => {}
}
}
if bound.lower.is_none() && bound.upper.is_none() {
return None;
}
Some((bound, coll))
}
}
}
fn try_index_in(
&self,
meta: &TableMeta,
table_name: &str,
sel: &Select,
params: &Params,
) -> Result<Option<Vec<InputRow>>> {
let Some(where_expr) = &sel.where_clause else {
return Ok(None);
};
let hint = sel.from.as_ref().and_then(|f| f.first.index_hint.as_ref());
if matches!(hint, Some(IndexHint::NotIndexed)) {
return Ok(None);
}
let indexes = self.indexes_of(table_name)?;
if let Some(IndexHint::IndexedBy(n)) = hint {
if !indexes.iter().any(|i| i.name.eq_ignore_ascii_case(n)) {
return Err(Error::Error(alloc::format!("no such index: {n}")));
}
}
let by_name = |idx: &IndexMeta| match hint {
Some(IndexHint::IndexedBy(n)) => idx.name.eq_ignore_ascii_case(n),
_ => true,
};
if let Some((col, values)) = find_in_constraint(where_expr, &meta.columns, params) {
if !values.iter().any(|v| matches!(v, Value::Null)) {
let encoding = self.backend.source().header().text_encoding;
let aff = meta.columns[col].affinity;
if !matches!(hint, Some(IndexHint::IndexedBy(_))) {
if let Some(ipk) = meta.ipk {
if col == ipk {
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut out = Vec::new();
let mut seen: Vec<i64> = Vec::new();
for v in &values {
let rid = eval::to_i64(v);
if seen.contains(&rid) {
continue;
}
seen.push(rid);
if cur.seek(rid)? {
let values =
self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
out.push(InputRow {
values,
rowid: Some(rid),
});
}
}
return Ok(Some(out));
}
}
}
let keys: Vec<Vec<Value>> = values
.iter()
.map(|v| alloc::vec![aff.coerce(v.clone())])
.collect();
for idx in &indexes {
if !by_name(idx) || idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
if idx.cols.first() == Some(&col) {
if self.seek_index_covers(sel, meta, &idx.cols, where_expr) {
return Ok(Some(self.covering_seek_rows(meta, idx.root, &idx.cols)?));
}
let coll = idx.collations.first().copied().unwrap_or_default();
return Ok(Some(self.in_seek_fetch(meta, idx.root, &[coll], &keys)?));
}
}
for idx in &indexes {
if !by_name(idx) || idx.key_exprs.is_some() || idx.partial.is_none() {
continue;
}
if idx.cols.first() == Some(&col) && partial_pred_guaranteed(idx, where_expr) {
let coll = idx.collations.first().copied().unwrap_or_default();
return Ok(Some(self.in_seek_fetch(meta, idx.root, &[coll], &keys)?));
}
}
}
}
for idx in &indexes {
if !by_name(idx) {
continue;
}
let Some(exprs) = &idx.key_exprs else {
continue;
};
let [key_expr] = exprs.as_slice() else {
continue;
};
if !partial_pred_guaranteed(idx, where_expr) {
continue;
}
let Some(values) = find_expr_in_values(key_expr, where_expr, params) else {
continue;
};
if values.iter().any(|v| matches!(v, Value::Null)) {
continue;
}
let coll = idx.collations.first().copied().unwrap_or_default();
let keys: Vec<Vec<Value>> = values.iter().map(|v| alloc::vec![v.clone()]).collect();
return Ok(Some(self.in_seek_fetch(meta, idx.root, &[coll], &keys)?));
}
Ok(None)
}
fn leading_index_for(
&self,
table_name: &str,
col: usize,
hint: Option<&IndexHint>,
) -> Result<Option<(u32, crate::value::Collation)>> {
for idx in &self.indexes_of(table_name)? {
if let Some(IndexHint::IndexedBy(n)) = hint {
if !idx.name.eq_ignore_ascii_case(n) {
continue;
}
}
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
if idx.cols.first() == Some(&col) {
return Ok(Some((
idx.root,
idx.collations.first().copied().unwrap_or_default(),
)));
}
}
Ok(None)
}
fn seek_col_values(
&self,
meta: &TableMeta,
table_name: &str,
hint: Option<&IndexHint>,
col: usize,
values: &[Value],
) -> Result<Option<Vec<i64>>> {
let mut rowids: Vec<i64> = Vec::new();
if !matches!(hint, Some(IndexHint::IndexedBy(_))) && meta.ipk == Some(col) {
for v in values {
let rid = eval::to_i64(v);
if !rowids.contains(&rid) {
rowids.push(rid);
}
}
return Ok(Some(rowids));
}
let Some((root, coll)) = self.leading_index_for(table_name, col, hint)? else {
return Ok(None);
};
let aff = meta.columns[col].affinity;
let colls = [coll];
for v in values {
let key = [aff.coerce(v.clone())];
for rid in crate::btree::index_seek_rowids(self.backend.source(), root, &key, &colls)? {
if !rowids.contains(&rid) {
rowids.push(rid);
}
}
}
Ok(Some(rowids))
}
fn seek_col_range(
&self,
meta: &TableMeta,
table_name: &str,
hint: Option<&IndexHint>,
col: usize,
bound: &RangeBound,
) -> Result<Option<Vec<i64>>> {
if !matches!(hint, Some(IndexHint::IndexedBy(_))) && meta.ipk == Some(col) {
let lo_int =
bound.lower.is_none() || matches!(bound.lower, Some((Value::Integer(_), _)));
let hi_int =
bound.upper.is_none() || matches!(bound.upper, Some((Value::Integer(_), _)));
if !(lo_int && hi_int) {
return Ok(None);
}
let start = match &bound.lower {
Some((Value::Integer(i), _)) => *i,
_ => i64::MIN,
};
let stop = match &bound.upper {
Some((Value::Integer(i), _)) => *i,
_ => i64::MAX,
};
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut rowids = Vec::new();
let mut ok = if start == i64::MIN {
cur.first()?
} else {
cur.seek(start)?;
cur.is_valid()
};
while ok {
let rid = cur.rowid()?;
if rid > stop {
break;
}
rowids.push(rid);
ok = cur.next()?;
}
return Ok(Some(rowids));
}
let Some((root, coll)) = self.leading_index_for(table_name, col, hint)? else {
return Ok(None);
};
let aff = meta.columns[col].affinity;
let lo = bound
.lower
.as_ref()
.map(|(v, i)| (aff.coerce(v.clone()), *i));
let hi = bound
.upper
.as_ref()
.map(|(v, i)| (aff.coerce(v.clone()), *i));
let colls = [coll];
let lower = lo.as_ref().map(|(v, i)| (core::slice::from_ref(v), *i));
let upper = hi.as_ref().map(|(v, i)| (core::slice::from_ref(v), *i));
let rowids =
crate::btree::index_range_rowids(self.backend.source(), root, lower, upper, &colls)?;
Ok(Some(rowids))
}
fn predicate_rowids(
&self,
meta: &TableMeta,
table_name: &str,
hint: Option<&IndexHint>,
pred: &Expr,
params: &Params,
) -> Result<Option<Vec<i64>>> {
if let Some((col, vals)) = find_in_constraint(pred, &meta.columns, params) {
if vals.iter().any(|v| matches!(v, Value::Null)) {
return Ok(None);
}
return self.seek_col_values(meta, table_name, hint, col, &vals);
}
let mut eqs: Vec<(usize, Value)> = Vec::new();
collect_eq_constraints(pred, &meta.columns, params, &mut eqs);
eqs.retain(|(_, v)| !matches!(v, Value::Null));
if let Some((col, v)) = eqs.into_iter().next() {
return self.seek_col_values(meta, table_name, hint, col, &[v]);
}
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(pred, &meta.columns, params, &mut ranges);
if let Some((&col, bound)) = ranges.iter().next() {
return self.seek_col_range(meta, table_name, hint, col, bound);
}
Ok(None)
}
fn try_index_or(
&self,
meta: &TableMeta,
table_name: &str,
sel: &Select,
params: &Params,
) -> Result<Option<Vec<InputRow>>> {
let Some(where_expr) = &sel.where_clause else {
return Ok(None);
};
let hint = sel.from.as_ref().and_then(|f| f.first.index_hint.as_ref());
if matches!(hint, Some(IndexHint::NotIndexed)) {
return Ok(None);
}
let mut disjuncts: Vec<&Expr> = Vec::new();
flatten_or(where_expr, &mut disjuncts);
if disjuncts.len() < 2 {
return Ok(None);
}
let mut rowids: Vec<i64> = Vec::new();
for d in disjuncts {
match self.predicate_rowids(meta, table_name, hint, d, params)? {
Some(rs) => {
for r in rs {
if !rowids.contains(&r) {
rowids.push(r);
}
}
}
None => return Ok(None),
}
}
let encoding = self.backend.source().header().text_encoding;
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut out = Vec::new();
for rid in rowids {
if cur.seek(rid)? {
let values = self.decode_full_row(meta, rid, &cur.payload()?, encoding)?;
out.push(InputRow {
values,
rowid: Some(rid),
});
}
}
Ok(Some(out))
}
fn explain_query_plan(&self, stmt: &Statement, params: &Params) -> Result<QueryResult> {
let mut details: Vec<(i64, i64, String)> = Vec::new();
let mut next_id = 1i64;
match stmt {
Statement::Select(sel) => {
self.eqp_select(sel, 0, &mut next_id, &mut details, params)?
}
Statement::Delete(d) => {
let meta = self.table_meta(&d.table, None)?;
let detail = self.eqp_access(
&d.table,
&d.table,
&meta,
d.where_clause.as_ref(),
None,
params,
)?;
details.push((next_id, 0, detail));
}
Statement::Update(u) => {
let meta = self.table_meta(&u.table, None)?;
let detail = self.eqp_access(
&u.table,
&u.table,
&meta,
u.where_clause.as_ref(),
None,
params,
)?;
details.push((next_id, 0, detail));
}
Statement::Insert(ins) => {
if let InsertSource::Select(sel) = &ins.source {
self.eqp_select(sel, 0, &mut next_id, &mut details, params)?;
}
}
_ => return Err(Error::Unsupported("EXPLAIN QUERY PLAN for this statement")),
}
Ok(QueryResult {
columns: alloc::vec![
String::from("id"),
String::from("parent"),
String::from("notused"),
String::from("detail"),
],
rows: details
.into_iter()
.map(|(id, parent, detail)| {
alloc::vec![
Value::Integer(id),
Value::Integer(parent),
Value::Integer(0),
Value::Text(detail),
]
})
.collect(),
})
}
fn eqp_vtab_detail(
&self,
name: &str,
label: &str,
sel: &Select,
params: &Params,
) -> Result<String> {
use crate::schema::ObjectType;
let plain = || alloc::format!("SCAN {label} VIRTUAL TABLE INDEX 0:");
let cvt = self
.schema
.objects()
.iter()
.find(|o| o.obj_type == ObjectType::Table && o.name.eq_ignore_ascii_case(name))
.and_then(|o| o.sql.as_deref())
.and_then(|s| match sql::parse_one(s) {
Ok(Statement::CreateVirtualTable(cvt)) => Some(cvt),
_ => None,
});
let Some(cvt) = cvt else { return Ok(plain()) };
let Some(module) = self.vtab_registry.get(&cvt.module) else {
return Ok(plain());
};
let arg_refs: Vec<&str> = cvt.args.iter().map(String::as_str).collect();
let schema = module.dyn_connect(&arg_refs)?;
let columns: Vec<ColumnInfo> = schema
.columns
.iter()
.map(|n| ColumnInfo {
name: n.clone(),
table: label.to_string(),
affinity: eval::Affinity::Blob,
collation: crate::value::Collation::default(),
})
.collect();
#[cfg(feature = "fts5")]
if cvt.module.eq_ignore_ascii_case("fts5") {
let mut idx_str = String::new();
let mut matched = false;
if let Some(where_expr) = &sel.where_clause {
if let Some((_, operand)) = self.fts5_match_query(where_expr, params) {
let col = schema
.columns
.iter()
.position(|c| c.eq_ignore_ascii_case(&operand))
.unwrap_or(schema.columns.len());
idx_str = alloc::format!("M{col}");
matched = true;
} else if fts5_rowid_eq(where_expr, params) {
idx_str.push('=');
}
}
let order_bit = if matched && sel.order_by.len() == 1 && !sel.order_by[0].descending {
match &sel.order_by[0].expr {
Expr::Column {
table: None,
column,
} if column.eq_ignore_ascii_case("rank") => 32,
Expr::Column {
table: None,
column,
} if matches!(
column.to_ascii_lowercase().as_str(),
"rowid" | "_rowid_" | "oid"
) =>
{
64
}
_ => 0,
}
} else {
0
};
return Ok(alloc::format!(
"SCAN {label} VIRTUAL TABLE INDEX {order_bit}:{idx_str}"
));
}
let (constraints, _) = collect_vtab_constraints(sel, &columns, params);
let plan = module.dyn_best_index(&constraints)?;
Ok(alloc::format!(
"SCAN {label} VIRTUAL TABLE INDEX {}:{}",
plan.idx_num,
plan.idx_str.as_deref().unwrap_or("")
))
}
fn eqp_select(
&self,
sel: &Select,
parent: i64,
next_id: &mut i64,
out: &mut Vec<(i64, i64, String)>,
params: &Params,
) -> Result<()> {
let rewritten;
let sel = match promote_comma_join_ons(sel) {
Some(r) => {
rewritten = r;
&rewritten
}
None => sel,
};
let Some(from) = &sel.from else {
return Ok(()); };
let label = eqp_label(&from.first);
if from.joins.is_empty()
&& from.first.subquery.is_none()
&& from.first.tvf_args.is_none()
&& self.lookup_cte(&from.first.name, None).is_none()
&& self.is_virtual_table(&from.first.name)
{
let detail = self.eqp_vtab_detail(&from.first.name, &label, sel, params)?;
let id = *next_id;
*next_id += 1;
out.push((id, parent, detail));
return Ok(());
}
let meta = self.table_meta(&from.first.name, from.first.alias.as_deref())?;
if from.joins.is_empty()
&& self.eqp_or_plan(
&label,
&from.first.name,
&meta,
sel.where_clause.as_ref(),
parent,
next_id,
out,
params,
)?
{
} else {
let detail = if from.joins.is_empty() {
if let Some((name, _)) = self.count_covering_index(sel) {
alloc::format!("SCAN {label} USING COVERING INDEX {name}")
}
else if let Some(s) = self.order_index_scan(sel) {
let kind = if s.covering {
"COVERING INDEX"
} else {
"INDEX"
};
alloc::format!("SCAN {label} USING {kind} {}", s.name)
}
else if let Some((name, _, _)) = self.covering_scan(sel, &meta, params) {
alloc::format!("SCAN {label} USING COVERING INDEX {name}")
} else {
self.eqp_access(
&label,
&from.first.name,
&meta,
sel.where_clause.as_ref(),
Some(sel),
params,
)?
}
} else {
alloc::format!("SCAN {label}")
};
let id = *next_id;
*next_id += 1;
out.push((id, parent, detail));
}
if !from.joins.is_empty() {
let mut left_columns = self.resolve_join_source(&from.first, params)?.0;
for join in &from.joins {
let label = eqp_label(&join.table);
let (details, jcols): (Vec<String>, Vec<ColumnInfo>) = if let Some((
_,
inner_meta,
)) =
self.rowid_join_seek(join, &left_columns)
{
(
alloc::vec![alloc::format!(
"SEARCH {label} USING INTEGER PRIMARY KEY (rowid=?)"
)],
inner_meta.columns,
)
} else if let Some((_, inner_meta, idx)) = self.index_join_seek(join, &left_columns)
{
let col = &inner_meta.columns[idx.cols[0]].name;
(
alloc::vec![alloc::format!(
"SEARCH {label} USING INDEX {} ({col}=?)",
idx.name
)],
inner_meta.columns,
)
} else if let Some((_, inner_meta)) =
self.without_rowid_pk_join_seek(join, &left_columns)
{
let col = &inner_meta.columns[inner_meta.storage_order[0]].name;
let suffix = if matches!(join.kind, JoinKind::Left) {
" LEFT-JOIN"
} else {
""
};
(
alloc::vec![alloc::format!(
"SEARCH {label} USING PRIMARY KEY ({col}=?){suffix}"
)],
inner_meta.columns,
)
} else {
let jcols = self.resolve_join_source(&join.table, params)?.0;
let auto_col = if join.natural
|| !join.using.is_empty()
|| !matches!(join.kind, JoinKind::Inner | JoinKind::Left)
{
None
} else {
join.on.as_ref().and_then(|on| {
let mut combined = left_columns.clone();
combined.extend(jcols.iter().cloned());
join_equi_cols(on, &combined, left_columns.len())
.map(|(_, ri)| jcols[ri].name.clone())
})
};
match auto_col {
Some(col) => {
let suffix = if matches!(join.kind, JoinKind::Left) {
" LEFT-JOIN"
} else {
""
};
(
alloc::vec![
alloc::format!("BLOOM FILTER ON {label} ({col}=?)"),
alloc::format!(
"SEARCH {label} USING AUTOMATIC COVERING INDEX ({col}=?){suffix}"
),
],
jcols,
)
}
None => (alloc::vec![alloc::format!("SCAN {label}")], jcols),
}
};
for detail in details {
let id = *next_id;
*next_id += 1;
out.push((id, parent, detail));
}
let left_width = left_columns.len();
left_columns.extend(jcols);
if join.natural || !join.using.is_empty() {
let mut drop: Vec<usize> = if join.natural {
(left_width..left_columns.len())
.filter(|&rl| {
left_columns[..left_width]
.iter()
.any(|c| c.name.eq_ignore_ascii_case(&left_columns[rl].name))
})
.collect()
} else {
join.using
.iter()
.filter_map(|name| {
(left_width..left_columns.len())
.find(|&rl| left_columns[rl].name.eq_ignore_ascii_case(name))
})
.collect()
};
drop.sort_unstable();
drop.dedup();
for &d in drop.iter().rev() {
left_columns.remove(d);
}
}
}
}
if !sel.order_by.is_empty() && self.order_satisfied_by_scan(sel, params).is_none() {
let n = sel.order_by.len();
let sorted = if let Some(s) = self.order_index_scan(sel) {
s.sorted_suffix.min(n)
} else if let Some((k, _)) = self.seek_order_prefix(sel, params) {
n - k.min(n)
} else {
n - self.scan_order_prefix(sel, params).min(n)
};
let detail = match sorted {
_ if sorted >= n => String::from("USE TEMP B-TREE FOR ORDER BY"),
1 => String::from("USE TEMP B-TREE FOR LAST TERM OF ORDER BY"),
_ => alloc::format!("USE TEMP B-TREE FOR LAST {sorted} TERMS OF ORDER BY"),
};
let id = *next_id;
*next_id += 1;
out.push((id, parent, detail));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn eqp_or_plan(
&self,
label: &str,
table: &str,
meta: &TableMeta,
where_clause: Option<&Expr>,
parent: i64,
next_id: &mut i64,
out: &mut Vec<(i64, i64, String)>,
params: &Params,
) -> Result<bool> {
let Some(where_expr) = where_clause else {
return Ok(false);
};
let mut disjuncts: Vec<&Expr> = Vec::new();
flatten_or(where_expr, &mut disjuncts);
if disjuncts.len() < 2 {
return Ok(false);
}
let mut details = Vec::with_capacity(disjuncts.len());
for d in &disjuncts {
let detail = self.eqp_access(label, table, meta, Some(d), None, params)?;
if !detail.starts_with("SEARCH") {
return Ok(false);
}
details.push(detail);
}
let or_id = *next_id;
*next_id += 1;
out.push((or_id, parent, String::from("MULTI-INDEX OR")));
for (i, detail) in details.into_iter().enumerate() {
let idx_id = *next_id;
*next_id += 1;
out.push((idx_id, or_id, alloc::format!("INDEX {}", i + 1)));
let search_id = *next_id;
*next_id += 1;
out.push((search_id, idx_id, detail));
}
Ok(true)
}
fn eqp_access(
&self,
label: &str,
table: &str,
meta: &TableMeta,
where_clause: Option<&Expr>,
sel: Option<&Select>,
params: &Params,
) -> Result<String> {
let Some(where_expr) = where_clause else {
return Ok(alloc::format!("SCAN {label}"));
};
let index_kw = |idx_cols: &[usize]| -> &'static str {
match sel {
Some(s) if self.seek_index_covers(s, meta, idx_cols, where_expr) => {
"COVERING INDEX"
}
_ => "INDEX",
}
};
let mut eqs: Vec<(usize, Value)> = Vec::new();
collect_eq_constraints(where_expr, &meta.columns, params, &mut eqs);
eqs.retain(|(_, v)| !matches!(v, Value::Null));
if meta.without_rowid {
let pk = &meta.storage_order[..meta.pk_len];
let mut names = Vec::new();
for &c in pk {
if eqs.iter().any(|(col, _)| *col == c) {
names.push(alloc::format!("{}=?", meta.columns[c].name));
} else {
break;
}
}
if !names.is_empty() {
return Ok(alloc::format!(
"SEARCH {label} USING PRIMARY KEY ({})",
names.join(" AND ")
));
}
if let Some(&lead) = pk.first() {
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
if let Some(b) = ranges.get(&lead) {
let name = &meta.columns[lead].name;
let cond = match (&b.lower, &b.upper) {
(Some(_), Some(_)) => alloc::format!("{name}>? AND {name}<?"),
(Some(_), None) => alloc::format!("{name}>?"),
(None, Some(_)) => alloc::format!("{name}<?"),
(None, None) => String::new(),
};
if !cond.is_empty() {
return Ok(alloc::format!("SEARCH {label} USING PRIMARY KEY ({cond})"));
}
}
}
for idx in self.indexes_of(table)? {
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
let mut matched = Vec::new();
for &c in &idx.cols {
if eqs.iter().any(|(col, _)| *col == c) {
matched.push(c);
} else {
break;
}
}
if matched.is_empty() {
continue;
}
let mut avail = idx.cols.clone();
if !idx.name.starts_with("sqlite_autoindex_") {
for &p in pk {
if !avail.contains(&p) {
avail.push(p);
}
}
}
let kw = match sel {
Some(s) if self.seek_index_covers(s, meta, &avail, where_expr) => {
"COVERING INDEX"
}
_ => "INDEX",
};
let cond = matched
.iter()
.map(|&c| alloc::format!("{}=?", meta.columns[c].name))
.collect::<Vec<_>>()
.join(" AND ");
return Ok(alloc::format!(
"SEARCH {label} USING {kw} {} ({cond})",
idx.name
));
}
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
for idx in self.indexes_of(table)? {
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
let Some(&lead) = idx.cols.first() else {
continue;
};
let Some(b) = ranges.get(&lead) else {
continue;
};
let name = &meta.columns[lead].name;
let cond = match (&b.lower, &b.upper) {
(Some(_), Some(_)) => alloc::format!("{name}>? AND {name}<?"),
(Some(_), None) => alloc::format!("{name}>?"),
(None, Some(_)) => alloc::format!("{name}<?"),
(None, None) => continue,
};
let mut avail = idx.cols.clone();
if !idx.name.starts_with("sqlite_autoindex_") {
for &p in pk {
if !avail.contains(&p) {
avail.push(p);
}
}
}
let kw = match sel {
Some(s) if self.seek_index_covers(s, meta, &avail, where_expr) => {
"COVERING INDEX"
}
_ => "INDEX",
};
return Ok(alloc::format!(
"SEARCH {label} USING {kw} {} ({cond})",
idx.name
));
}
return Ok(alloc::format!("SCAN {label}"));
}
if rowid_seek_constraint(where_expr, &meta.columns, params).is_some() {
return Ok(alloc::format!(
"SEARCH {label} USING INTEGER PRIMARY KEY (rowid=?)"
));
}
if let Some(ipk) = meta.ipk {
if eqs.iter().any(|(c, _)| *c == ipk) {
return Ok(alloc::format!(
"SEARCH {label} USING INTEGER PRIMARY KEY (rowid=?)"
));
}
}
let stats = self.stat1_map();
#[allow(clippy::type_complexity)]
let mut best: Option<(String, Vec<usize>, Vec<usize>, u64)> = None;
for idx in self.indexes_of(table)? {
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
let mut matched = Vec::new();
for &c in &idx.cols {
if eqs.iter().any(|(col, _)| *col == c) {
matched.push(c);
} else {
break;
}
}
if matched.is_empty() {
continue;
}
let est = stats
.get(&idx.name)
.and_then(|s| s.get(matched.len()).copied())
.unwrap_or(u64::MAX - matched.len() as u64);
let better = match &best {
None => true,
Some((_, bm, _, be)) => est < *be || (est == *be && matched.len() > bm.len()),
};
if better {
best = Some((idx.name.clone(), matched, idx.cols.clone(), est));
}
}
if let Some((idx_name, matched, idx_cols, _)) = best {
if !matched.is_empty() {
let mut conds = matched
.iter()
.map(|&c| alloc::format!("{}=?", meta.columns[c].name))
.collect::<Vec<_>>();
if let Some(&next_col) = idx_cols.get(matched.len()) {
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
if let Some(b) = ranges.get(&next_col) {
let name = &meta.columns[next_col].name;
if b.lower.is_some() {
conds.push(alloc::format!("{name}>?"));
}
if b.upper.is_some() {
conds.push(alloc::format!("{name}<?"));
}
}
}
let kw = index_kw(&idx_cols);
return Ok(alloc::format!(
"SEARCH {label} USING {kw} {idx_name} ({})",
conds.join(" AND ")
));
}
}
for idx in self.indexes_of(table)? {
if self
.partial_expr_seek(&idx, where_expr, meta, params)?
.is_some()
{
let cond = match &idx.key_exprs {
None => idx
.cols
.iter()
.take_while(|&&c| eqs.iter().any(|(col, _)| *col == c))
.map(|&c| alloc::format!("{}=?", meta.columns[c].name))
.collect::<Vec<_>>()
.join(" AND "),
Some(_) => "<expr>=?".into(),
};
return Ok(alloc::format!(
"SEARCH {label} USING INDEX {} ({cond})",
idx.name
));
}
}
let leading_index = |target: usize| -> Option<(String, Vec<usize>)> {
for obj in self.schema.indexes_on(table) {
let sql = obj.sql.as_ref()?;
let Ok(Statement::CreateIndex(ci)) = sql::parse_one(sql) else {
continue;
};
if ci.where_clause.is_some() {
continue;
}
let Ok(cols) = self.index_columns(meta, &ci) else {
continue;
};
if cols.first() == Some(&target) {
return Some((obj.name.clone(), cols));
}
}
None
};
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
if let Some(ipk) = meta.ipk {
if let Some(b) = ranges.get(&ipk) {
let lo_int = b.lower.is_none() || matches!(b.lower, Some((Value::Integer(_), _)));
let hi_int = b.upper.is_none() || matches!(b.upper, Some((Value::Integer(_), _)));
if lo_int && hi_int {
let cond = match (&b.lower, &b.upper) {
(Some(_), Some(_)) => "rowid>? AND rowid<?",
(Some(_), None) => "rowid>?",
(None, Some(_)) => "rowid<?",
(None, None) => "",
};
if !cond.is_empty() {
return Ok(alloc::format!(
"SEARCH {label} USING INTEGER PRIMARY KEY ({cond})"
));
}
}
}
}
for (&col, bound) in &ranges {
if let Some((idx_name, idx_cols)) = leading_index(col) {
let name = &meta.columns[col].name;
let cond = match (&bound.lower, &bound.upper) {
(Some(_), Some(_)) => alloc::format!("{name}>? AND {name}<?"),
(Some(_), None) => alloc::format!("{name}>?"),
(None, Some(_)) => alloc::format!("{name}<?"),
(None, None) => continue,
};
let kw = index_kw(&idx_cols);
return Ok(alloc::format!(
"SEARCH {label} USING {kw} {idx_name} ({cond})"
));
}
}
for idx in self.indexes_of(table)? {
if let Some((bound, _)) = self.partial_expr_range(&idx, where_expr, meta, params) {
let cond = |name: &str| match (&bound.lower, &bound.upper) {
(Some(_), Some(_)) => alloc::format!("{name}>? AND {name}<?"),
(Some(_), None) => alloc::format!("{name}>?"),
(None, Some(_)) => alloc::format!("{name}<?"),
(None, None) => String::new(),
};
let rendered = match &idx.key_exprs {
None => cond(&meta.columns[idx.cols[0]].name),
Some(_) => cond("<expr>"),
};
if !rendered.is_empty() {
return Ok(alloc::format!(
"SEARCH {label} USING INDEX {} ({rendered})",
idx.name
));
}
}
}
if let Some((col, _)) = find_in_constraint(where_expr, &meta.columns, params) {
if meta.ipk == Some(col) {
return Ok(alloc::format!(
"SEARCH {label} USING INTEGER PRIMARY KEY (rowid=?)"
));
}
if let Some((idx_name, idx_cols)) = leading_index(col) {
let name = &meta.columns[col].name;
let kw = index_kw(&idx_cols);
return Ok(alloc::format!(
"SEARCH {label} USING {kw} {idx_name} ({name}=?)"
));
}
for idx in self.indexes_of(table)? {
if idx.key_exprs.is_some() || idx.partial.is_none() {
continue;
}
if idx.cols.first() == Some(&col) && partial_pred_guaranteed(&idx, where_expr) {
let name = &meta.columns[col].name;
return Ok(alloc::format!(
"SEARCH {label} USING INDEX {} ({name}=?)",
idx.name
));
}
}
}
for idx in self.indexes_of(table)? {
let Some(exprs) = &idx.key_exprs else {
continue;
};
let [key_expr] = exprs.as_slice() else {
continue;
};
if partial_pred_guaranteed(&idx, where_expr)
&& find_expr_in_values(key_expr, where_expr, params).is_some()
{
return Ok(alloc::format!(
"SEARCH {label} USING INDEX {} (<expr>=?)",
idx.name
));
}
}
Ok(alloc::format!("SCAN {label}"))
}
fn indexes_of(&self, table: &str) -> Result<Vec<IndexMeta>> {
let tmeta = match self.schema.table(table) {
Some(_) => self.table_meta(table, None)?,
None => return Ok(Vec::new()),
};
let mut out = Vec::new();
for obj in self.schema.indexes_on(table) {
match &obj.sql {
Some(sql) => {
let Statement::CreateIndex(ci) = sql::parse_one(sql)? else {
continue;
};
let (cols, key_exprs, collations) = self.index_key_spec(&tmeta, &ci)?;
out.push(IndexMeta {
name: obj.name.clone(),
root: obj.rootpage,
cols,
collations,
partial: ci.where_clause.clone(),
key_exprs,
unique: ci.unique,
});
}
None => {
if let Some(n) = autoindex_number(&obj.name, table) {
if let Some((cols, _)) = tmeta.unique.get(n - 1) {
let collations = self.col_collations(&tmeta, cols);
out.push(IndexMeta {
name: obj.name.clone(),
root: obj.rootpage,
cols: cols.clone(),
collations,
partial: None,
key_exprs: None,
unique: true,
});
}
}
}
}
}
Ok(out)
}
fn index_columns(&self, tmeta: &TableMeta, ci: &CreateIndex) -> Result<Vec<usize>> {
Ok(self.index_columns_coll(tmeta, ci)?.0)
}
fn index_columns_coll(
&self,
tmeta: &TableMeta,
ci: &CreateIndex,
) -> Result<(Vec<usize>, Vec<crate::value::Collation>)> {
let mut cols = Vec::new();
let mut colls = Vec::new();
for term in &ci.columns {
let (inner, explicit) = match &term.expr {
Expr::Collate { expr, collation } => {
(expr.as_ref(), crate::value::Collation::parse(collation))
}
e => (e, None),
};
let Expr::Column { column, .. } = inner else {
return Err(Error::Unsupported("expression indexes"));
};
let pos = tmeta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(column))
.ok_or_else(|| Error::Error(format!("no such column: {column}")))?;
cols.push(pos);
colls.push(explicit.unwrap_or(tmeta.columns[pos].collation));
}
Ok((cols, colls))
}
#[allow(clippy::type_complexity)]
fn index_key_spec(
&self,
tmeta: &TableMeta,
ci: &CreateIndex,
) -> Result<(Vec<usize>, Option<Vec<Expr>>, Vec<crate::value::Collation>)> {
let mut cols = Vec::new();
let mut exprs = Vec::new();
let mut colls = Vec::new();
let mut is_expr = false;
for term in &ci.columns {
let (inner, explicit) = match &term.expr {
Expr::Collate { expr, collation } => {
(expr.as_ref(), crate::value::Collation::parse(collation))
}
e => (e, None),
};
exprs.push(inner.clone());
match inner {
Expr::Column { column, .. } => {
let pos = tmeta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(column))
.ok_or_else(|| Error::Error(format!("no such column: {column}")))?;
cols.push(pos);
colls.push(explicit.unwrap_or(tmeta.columns[pos].collation));
}
_ => {
is_expr = true;
colls.push(explicit.unwrap_or_default());
}
}
}
if is_expr {
Ok((Vec::new(), Some(exprs), colls))
} else {
Ok((cols, None, colls))
}
}
fn index_key_bytes(
&self,
idx: &IndexMeta,
meta: &TableMeta,
values: &[Value],
rowid: i64,
params: &Params,
) -> Result<Vec<u8>> {
match &idx.key_exprs {
None => Ok(index_key(&idx.cols, values, rowid)),
Some(exprs) => {
let ctx = row_ctx(values, &meta.columns, Some(rowid), params).with_subqueries(self);
let mut key: Vec<Value> = exprs
.iter()
.map(|e| eval::eval(e, &ctx))
.collect::<Result<_>>()?;
key.push(Value::Integer(rowid));
Ok(encode_record(&key))
}
}
}
fn col_collations(&self, tmeta: &TableMeta, cols: &[usize]) -> Vec<crate::value::Collation> {
cols.iter().map(|&c| tmeta.columns[c].collation).collect()
}
fn row_in_index(
&self,
idx: &IndexMeta,
tmeta: &TableMeta,
values: &[Value],
rowid: Option<i64>,
params: &Params,
) -> Result<bool> {
match &idx.partial {
None => Ok(true),
Some(pred) => {
let ctx = row_ctx(values, &tmeta.columns, rowid, params).with_subqueries(self);
Ok(eval::truth(&eval::eval(pred, &ctx)?) == Some(true))
}
}
}
fn rebuild_indexes(&mut self, tmeta: &TableMeta, indexes: &[IndexMeta]) -> Result<()> {
if indexes.is_empty() {
return Ok(());
}
let rows = self.scan_table(tmeta)?;
let no_params = Params::default();
let mut per_index: Vec<Vec<Vec<u8>>> = Vec::with_capacity(indexes.len());
for idx in indexes {
let mut keys = Vec::new();
for (rowid, values) in &rows {
if self.row_in_index(idx, tmeta, values, Some(*rowid), &no_params)? {
keys.push(self.index_key_bytes(idx, tmeta, values, *rowid, &no_params)?);
}
}
per_index.push(keys);
}
let w = self.backend.writer()?;
for (idx, keys) in indexes.iter().zip(&per_index) {
clear_index(w, idx.root)?;
for key in keys {
insert_index(w, idx.root, key, &idx.collations)?;
}
}
Ok(())
}
fn order_limit_rowids(
&self,
meta: &TableMeta,
rowids: Vec<i64>,
order_by: &[OrderTerm],
limit: Option<&Expr>,
offset: Option<&Expr>,
params: &Params,
) -> Result<Vec<i64>> {
let mut rowids = rowids;
if !order_by.is_empty() {
let mut keyed: Vec<(i64, Vec<Value>)> = Vec::with_capacity(rowids.len());
for rid in rowids {
let row = self.read_row(meta, rid)?.unwrap_or_default();
let ctx = row_ctx(&row, &meta.columns, Some(rid), params).with_subqueries(self);
let keys = order_by
.iter()
.map(|t| eval::eval(&t.expr, &ctx))
.collect::<Result<Vec<_>>>()?;
keyed.push((rid, keys));
}
keyed.sort_by(|a, b| {
for (i, t) in order_by.iter().enumerate() {
let o = cmp_order(
&a.1[i],
&b.1[i],
t.descending,
t.nulls_first,
crate::value::Collation::Binary,
);
if o != core::cmp::Ordering::Equal {
return o;
}
}
core::cmp::Ordering::Equal
});
rowids = keyed.into_iter().map(|(r, _)| r).collect();
}
let off = match offset {
Some(e) => must_be_int(eval::eval(
e,
&EvalCtx::rowless(params).with_subqueries(self),
)?)?
.max(0) as usize,
None => 0,
};
if off > 0 {
rowids.drain(0..off.min(rowids.len()));
}
if let Some(e) = limit {
let n = eval::to_i64(&eval::eval(e, &EvalCtx::rowless(params))?);
if n >= 0 {
rowids.truncate(n as usize);
}
}
Ok(rowids)
}
fn matching_rowids(
&self,
meta: &TableMeta,
pred: Option<&Expr>,
params: &Params,
) -> Result<Vec<i64>> {
let mut out = Vec::new();
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let encoding = self.backend.source().header().text_encoding;
let mut ok = cur.first()?;
while ok {
let rowid = cur.rowid()?;
let values = self.decode_full_row(meta, rowid, &cur.payload()?, encoding)?;
let keep = match pred {
Some(p) => {
let ctx =
row_ctx(&values, &meta.columns, Some(rowid), params).with_subqueries(self);
eval::truth(&eval::eval(p, &ctx)?) == Some(true)
}
None => true,
};
if keep {
out.push(rowid);
}
ok = cur.next()?;
}
Ok(out)
}
fn next_rowid(&self, root: u32) -> Result<i64> {
let mut cur = TableCursor::new(self.backend.source(), root);
if cur.last()? {
Ok(cur.rowid()?.saturating_add(1))
} else {
Ok(1)
}
}
fn auto_rowid(&self, root: u32, autoincrement: bool, cand: i64) -> Result<i64> {
let occupied = |r: i64| -> Result<bool> {
let mut cur = TableCursor::new(self.backend.source(), root);
cur.seek(r)
};
if cand < i64::MAX || !occupied(cand)? {
return Ok(cand);
}
if autoincrement {
return Err(Error::Error("database or disk is full".into()));
}
loop {
let r = (eval::Subqueries::next_random(self) & i64::MAX).max(1);
if !occupied(r)? {
return Ok(r);
}
}
}
fn recursive_cte_outer_cap(&self, sel: &Select, params: &Params) -> Option<usize> {
if sel.ctes.len() != 1
|| !sel.compound.is_empty()
|| sel.distinct
|| !sel.group_by.is_empty()
|| !sel.order_by.is_empty()
|| sel.where_clause.is_some()
|| sel.having.is_some()
|| self.has_aggregate(sel)
{
return None;
}
let cte = &sel.ctes[0];
if !references_name(&cte.select, &cte.name) {
return None; }
let from = sel.from.as_ref()?;
if !from.joins.is_empty()
|| from.first.subquery.is_some()
|| from.first.tvf_args.is_some()
|| !from.first.name.eq_ignore_ascii_case(&cte.name)
{
return None;
}
let ctx = EvalCtx::rowless(params).with_subqueries(self);
let n = must_be_int(eval::eval(sel.limit.as_ref()?, &ctx).ok()?).ok()?;
if n < 0 {
return None; }
let offset = match &sel.offset {
Some(e) => must_be_int(eval::eval(e, &ctx).ok()?).ok()?.max(0) as usize,
None => 0,
};
Some((n as usize).saturating_add(offset))
}
fn run_select(&self, sel: &Select, params: &Params) -> Result<QueryResult> {
validate_used_collations(sel)?;
let base = self.cte_env.borrow().len();
let outer_cap = self.recursive_cte_outer_cap(sel, params);
let pushed = self.push_ctes(&sel.ctes, params, outer_cap);
let result = pushed.and_then(|()| self.run_select_compound(sel, params));
self.cte_env.borrow_mut().truncate(base);
result
}
fn run_select_compound(&self, sel: &Select, params: &Params) -> Result<QueryResult> {
if sel.compound.is_empty() {
return self.run_core(sel, params);
}
let mut first = sel.clone();
first.compound = Vec::new();
first.order_by = Vec::new();
first.limit = None;
first.offset = None;
let mut result = self.run_core(&first, params)?;
let colls = {
let (cols, _) = self.scan_source(&first, params)?;
self.output_collations(&first, &cols, params)
};
let is_values = sel.from.is_none()
&& sel.where_clause.is_none()
&& sel.group_by.is_empty()
&& is_values_projection(&sel.columns)
&& sel.compound.iter().all(|(op, c)| {
*op == CompoundOp::UnionAll && c.from.is_none() && is_values_projection(&c.columns)
});
for (op, operand) in &sel.compound {
let r = self.run_select_compound(operand, params)?;
if r.columns.len() != result.columns.len() {
return Err(Error::Error(if is_values {
"all VALUES must have the same number of terms".into()
} else {
let kw = match op {
CompoundOp::Union => "UNION",
CompoundOp::UnionAll => "UNION ALL",
CompoundOp::Intersect => "INTERSECT",
CompoundOp::Except => "EXCEPT",
};
alloc::format!(
"SELECTs to the left and right of {kw} do not have the same \
number of result columns"
)
}));
}
result.rows = apply_compound(*op, result.rows, r.rows, &colls);
}
if sel.order_by.is_empty()
&& sel
.compound
.iter()
.any(|(op, _)| *op != CompoundOp::UnionAll)
{
result.rows.sort_by(|a, b| {
for (i, va) in a.iter().enumerate() {
let coll = colls.get(i).copied().unwrap_or_default();
let ord = crate::value::cmp_values_coll(va, &b[i], coll);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
});
}
self.compound_order_limit(&mut result, sel, params, &colls)?;
Ok(result)
}
fn compound_order_limit(
&self,
result: &mut QueryResult,
sel: &Select,
params: &Params,
colls: &[crate::value::Collation],
) -> Result<()> {
if !sel.order_by.is_empty() {
check_positional_terms(&[], &sel.order_by, result.columns.len())?;
let mut keys = Vec::new();
for (i, term) in sel.order_by.iter().enumerate() {
let idx = resolve_order_index(&term.expr, &result.columns, result.columns.len())
.ok_or_else(|| {
Error::Error(alloc::format!(
"{} ORDER BY term does not match any column in the result set",
ordinal(i + 1),
))
})?;
let coll = colls.get(idx).copied().unwrap_or_default();
keys.push((idx, term.descending, term.nulls_first, coll));
}
result.rows.sort_by(|a, b| {
for (idx, desc, nf, coll) in &keys {
let ord = cmp_order(&a[*idx], &b[*idx], *desc, *nf, *coll);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
});
}
let offset = match &sel.offset {
Some(e) => must_be_int(eval::eval(
e,
&EvalCtx::rowless(params).with_subqueries(self),
)?)?
.max(0) as usize,
None => 0,
};
let limit = match &sel.limit {
Some(e) => {
let n = must_be_int(eval::eval(
e,
&EvalCtx::rowless(params).with_subqueries(self),
)?)?;
if n < 0 {
None
} else {
Some(n as usize)
}
}
None => None,
};
if offset > 0 {
result.rows.drain(0..offset.min(result.rows.len()));
}
if let Some(n) = limit {
result.rows.truncate(n);
}
Ok(())
}
fn apply_windows(
&self,
sel: &Select,
columns: &mut Vec<ColumnInfo>,
rows: &mut [InputRow],
params: &Params,
) -> Result<Select> {
let wins = window::collect_window_exprs(sel);
let mut new_sel = sel.clone();
for (k, wexpr) in wins.iter().enumerate() {
let resolved = resolve_window_ref(wexpr, &sel.window_defs)?;
let values = self.compute_window(&resolved, columns, rows, params)?;
let col_name = alloc::format!("__win{k}");
columns.push(ColumnInfo {
name: col_name.clone(),
table: String::new(),
affinity: eval::Affinity::Blob,
collation: crate::value::Collation::default(),
});
for (row, v) in rows.iter_mut().zip(values) {
row.values.push(v);
}
let repl = Expr::Column {
table: None,
column: col_name,
};
window::replace_window_expr(&mut new_sel, wexpr, &repl);
}
Ok(new_sel)
}
fn compute_window(
&self,
wexpr: &Expr,
columns: &[ColumnInfo],
rows: &[InputRow],
params: &Params,
) -> Result<Vec<Value>> {
let Expr::Function {
name,
distinct,
args,
star,
filter,
over: Some(spec),
..
} = wexpr
else {
return Err(Error::Error("not a window function".into()));
};
if *distinct {
return Err(Error::Error(
"DISTINCT is not supported for window functions".into(),
));
}
let lname = name.to_ascii_lowercase();
let n = rows.len();
let win_arity: Option<(usize, usize)> = match lname.as_str() {
"row_number" | "rank" | "dense_rank" | "percent_rank" | "cume_dist" => Some((0, 0)),
"ntile" | "first_value" | "last_value" => Some((1, 1)),
"nth_value" => Some((2, 2)),
"lag" | "lead" => Some((1, 3)),
_ => None,
};
if let Some((lo, hi)) = win_arity {
if args.len() < lo || args.len() > hi {
return Err(Error::Error(alloc::format!(
"wrong number of arguments to function {lname}()"
)));
}
}
let mut part_keys: Vec<Vec<Value>> = Vec::with_capacity(n);
let mut ord_keys: Vec<Vec<Value>> = Vec::with_capacity(n);
let mut arg_vals: Vec<Vec<Value>> = Vec::with_capacity(n);
let mut passes: Vec<bool> = Vec::with_capacity(n);
for r in rows {
let ctx = r.ctx(columns, params).with_subqueries(self);
part_keys.push(
spec.partition_by
.iter()
.map(|e| eval::eval(e, &ctx))
.collect::<Result<_>>()?,
);
ord_keys.push(
spec.order_by
.iter()
.map(|t| eval::eval(&t.expr, &ctx))
.collect::<Result<_>>()?,
);
arg_vals.push(
args.iter()
.map(|e| eval::eval(e, &ctx))
.collect::<Result<_>>()?,
);
passes.push(match filter {
Some(pred) => eval::truth(&eval::eval(pred, &ctx)?) == Some(true),
None => true,
});
}
let descending: Vec<bool> = spec.order_by.iter().map(|t| t.descending).collect();
let mut partitions: Vec<Vec<usize>> = Vec::new();
let mut part_of: Vec<usize> = Vec::new();
for i in 0..n {
let p = partitions
.iter()
.position(|members| rows_equal(&part_keys[members[0]], &part_keys[i]));
match p {
Some(idx) => {
partitions[idx].push(i);
part_of.push(idx);
}
None => {
part_of.push(partitions.len());
partitions.push(alloc::vec![i]);
}
}
}
let mut result = alloc::vec![Value::Null; n];
for members in &partitions {
let mut ordered = members.clone();
ordered.sort_by(|&a, &b| cmp_keys(&ord_keys[a], &ord_keys[b], &descending));
self.fill_window_partition(
&lname,
*star || (lname == "count" && args.is_empty()),
&ordered,
&ord_keys,
&arg_vals,
&passes,
spec,
&mut result,
)?;
}
Ok(result)
}
#[allow(clippy::too_many_arguments)]
fn fill_window_partition(
&self,
lname: &str,
star: bool,
ordered: &[usize],
ord_keys: &[Vec<Value>],
arg_vals: &[Vec<Value>],
passes: &[bool],
spec: &WindowSpec,
result: &mut [Value],
) -> Result<()> {
let m = ordered.len();
let mut gid = alloc::vec![0usize; m];
for q in 1..m {
gid[q] = gid[q - 1]
+ usize::from(
!cmp_keys(&ord_keys[ordered[q - 1]], &ord_keys[ordered[q]], &[]).is_eq(),
);
}
let ovals: Vec<Value> = if spec.order_by.len() == 1 {
ordered
.iter()
.map(|&i| ord_keys[i].first().cloned().unwrap_or(Value::Null))
.collect()
} else {
Vec::new()
};
let desc = spec.order_by.first().map(|t| t.descending).unwrap_or(false);
let exclude = spec
.frame
.as_ref()
.map(|f| f.exclude)
.unwrap_or(FrameExclude::NoOthers);
for p in 0..m {
let idx = ordered[p];
let (fstart, fend) = frame_bounds(p, m, &gid, spec, &ovals, desc);
let fpos: Vec<usize> = (fstart..fend)
.filter(|&k| match exclude {
FrameExclude::NoOthers => true,
FrameExclude::CurrentRow => k != p,
FrameExclude::Group => gid[k] != gid[p],
FrameExclude::Ties => gid[k] != gid[p] || k == p,
})
.collect();
let val = match lname {
"row_number" => Value::Integer(p as i64 + 1),
"rank" => {
let mut r = p;
while r > 0 && cmp_keys(&ord_keys[ordered[r - 1]], &ord_keys[idx], &[]).is_eq()
{
r -= 1;
}
Value::Integer(r as i64 + 1)
}
"dense_rank" => {
let mut dr = 1i64;
for q in 1..=p {
if !cmp_keys(&ord_keys[ordered[q - 1]], &ord_keys[ordered[q]], &[]).is_eq()
{
dr += 1;
}
}
Value::Integer(dr)
}
"percent_rank" => {
let mut r = p;
while r > 0 && cmp_keys(&ord_keys[ordered[r - 1]], &ord_keys[idx], &[]).is_eq()
{
r -= 1;
}
if m > 1 {
Value::Real(r as f64 / (m - 1) as f64)
} else {
Value::Real(0.0)
}
}
"cume_dist" => {
let mut last = p;
while last + 1 < m
&& cmp_keys(&ord_keys[idx], &ord_keys[ordered[last + 1]], &[]).is_eq()
{
last += 1;
}
Value::Real((last + 1) as f64 / m as f64)
}
"ntile" => {
let buckets = arg_vals[idx].first().map(eval::to_i64).unwrap_or(0);
if buckets < 1 {
return Err(Error::Error(
"argument of ntile must be a positive integer".into(),
));
}
Value::Integer(ntile_bucket(p, m, buckets))
}
"lag" | "lead" => {
let offset = arg_vals[idx].get(1).map(eval::to_i64).unwrap_or(1);
let default = arg_vals[idx].get(2).cloned().unwrap_or(Value::Null);
let target = if lname == "lag" {
p as i64 - offset
} else {
p as i64 + offset
};
if target >= 0 && (target as usize) < m {
arg_vals[ordered[target as usize]]
.first()
.cloned()
.unwrap_or(Value::Null)
} else {
default
}
}
"first_value" => fpos
.first()
.and_then(|&k| arg_vals[ordered[k]].first().cloned())
.unwrap_or(Value::Null),
"last_value" => fpos
.last()
.and_then(|&k| arg_vals[ordered[k]].first().cloned())
.unwrap_or(Value::Null),
"nth_value" => {
let raw = arg_vals[idx].get(1).cloned().unwrap_or(Value::Null);
let nth = match eval::Affinity::Numeric.coerce(raw) {
Value::Integer(n) if n >= 1 => n,
_ => {
return Err(Error::Error(
"second argument to nth_value must be a positive integer".into(),
))
}
};
fpos.get((nth - 1) as usize)
.and_then(|&k| arg_vals[ordered[k]].first().cloned())
.unwrap_or(Value::Null)
}
_ => {
let frame: Vec<&Vec<Value>> = fpos
.iter()
.filter(|&&k| passes[ordered[k]])
.map(|&k| &arg_vals[ordered[k]])
.collect();
window_aggregate(lname, star, &frame)?
}
};
result[idx] = val;
}
Ok(())
}
fn order_collations(
&self,
sel: &Select,
columns: &[ColumnInfo],
params: &Params,
) -> Vec<crate::value::Collation> {
let ctx = row_ctx(&[], columns, None, params);
sel.order_by
.iter()
.map(|t| eval::key_collation(&t.expr, &ctx))
.collect()
}
fn output_collations(
&self,
sel: &Select,
columns: &[ColumnInfo],
params: &Params,
) -> Vec<crate::value::Collation> {
let ctx = row_ctx(&[], columns, None, params);
let mut out = Vec::new();
for col in &sel.columns {
match col {
ResultColumn::Expr { expr, .. } => out.push(eval::key_collation(expr, &ctx)),
ResultColumn::Wildcard => {
out.extend(columns.iter().map(|c| c.collation));
}
ResultColumn::TableWildcard(t) => out.extend(
columns
.iter()
.filter(|c| c.table.eq_ignore_ascii_case(t))
.map(|c| c.collation),
),
}
}
out
}
fn rowid_ordered_scan(&self, sel: &Select) -> Option<bool> {
let from = sel.from.as_ref()?;
if !from.joins.is_empty() {
return None;
}
let t = &from.first;
if t.subquery.is_some() || t.tvf_args.is_some() || t.schema.is_some() {
return None;
}
if sel.where_clause.is_some()
|| !sel.group_by.is_empty()
|| sel.having.is_some()
|| sel.distinct
|| sel.order_by.len() != 1
{
return None;
}
if self.has_aggregate(sel) || window::has_window(sel) {
return None;
}
let term = &sel.order_by[0];
let (tbl, col) = match &term.expr {
Expr::Column { table, column } => (table.as_deref(), column.as_str()),
_ => return None,
};
if self.lookup_cte(&t.name, None).is_some() || self.is_view(&t.name) {
return None;
}
let label = t.alias.as_deref().unwrap_or(&t.name);
if tbl.is_some_and(|tn| !tn.eq_ignore_ascii_case(label)) {
return None;
}
let meta = self.table_meta(&t.name, t.alias.as_deref()).ok()?;
if meta.without_rowid {
return None;
}
let shadowed = meta
.columns
.iter()
.any(|c| c.name.eq_ignore_ascii_case(col));
let is_rowid_alias = matches!(
col.to_ascii_lowercase().as_str(),
"rowid" | "_rowid_" | "oid"
) && !shadowed;
let is_ipk = meta
.ipk
.is_some_and(|i| meta.columns[i].name.eq_ignore_ascii_case(col));
if is_rowid_alias || is_ipk {
Some(term.descending)
} else {
None
}
}
fn order_index_scan(&self, sel: &Select) -> Option<OrderIndexScan> {
let from = sel.from.as_ref()?;
if !from.joins.is_empty() {
return None;
}
let t = &from.first;
if t.subquery.is_some() || t.tvf_args.is_some() || t.schema.is_some() {
return None;
}
if sel.where_clause.is_some()
|| !sel.group_by.is_empty()
|| sel.having.is_some()
|| sel.distinct
|| sel.order_by.is_empty()
{
return None;
}
if self.has_aggregate(sel) || window::has_window(sel) {
return None;
}
if self.lookup_cte(&t.name, None).is_some() || self.is_view(&t.name) {
return None;
}
let label = t.alias.as_deref().unwrap_or(&t.name);
let meta = self.table_meta(&t.name, t.alias.as_deref()).ok()?;
if meta.without_rowid {
return None;
}
let descending = sel.order_by[0].descending;
let mut cols: Vec<usize> = Vec::with_capacity(sel.order_by.len());
let mut uniform_prefix = 0usize;
let mut prefix_open = true;
for term in &sel.order_by {
if term.nulls_first.is_some() {
return None;
}
let (tbl, col_name) = match &term.expr {
Expr::Column { table, column } => (table.as_deref(), column.as_str()),
_ => return None,
};
if tbl.is_some_and(|tn| !tn.eq_ignore_ascii_case(label)) {
return None;
}
let col = meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(col_name))?;
cols.push(col);
if prefix_open && term.descending == descending {
uniform_prefix += 1;
} else {
prefix_open = false;
}
}
let sorted_suffix = sel.order_by.len() - uniform_prefix;
if cols.len() == 1 && meta.ipk == Some(cols[0]) {
return None;
}
for idx in self.indexes_of(&t.name).ok()? {
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
if idx.cols.len() < cols.len() || idx.cols[..cols.len()] != cols[..] {
continue;
}
let coll_ok = cols
.iter()
.enumerate()
.all(|(i, &c)| idx.collations[i] == meta.columns[c].collation);
if !coll_ok {
continue;
}
let covering = self.index_covers_query(sel, &meta, &idx.cols);
if sorted_suffix > 0 && covering {
continue;
}
return Some(OrderIndexScan {
name: idx.name,
root: idx.root,
colls: idx.collations,
cols: idx.cols,
descending,
covering,
sorted_suffix,
});
}
None
}
fn scan_order_prefix(&self, sel: &Select, params: &Params) -> usize {
if sel.order_by.is_empty() {
return 0;
}
let Some(from) = sel.from.as_ref() else {
return 0;
};
if !from.joins.is_empty() {
return 0;
}
let Ok(meta) = self.table_meta(&from.first.name, from.first.alias.as_deref()) else {
return 0;
};
let Some((name, _, _)) = self.covering_scan(sel, &meta, params) else {
return 0;
};
let Ok(indexes) = self.indexes_of(&from.first.name) else {
return 0;
};
let Some(idx) = indexes
.into_iter()
.find(|i| i.name.eq_ignore_ascii_case(&name))
else {
return 0;
};
let label = from.first.alias.as_deref().unwrap_or(&from.first.name);
let backward = sel.order_by[0].descending;
let mut k = 0usize;
for (i, term) in sel.order_by.iter().enumerate() {
if i >= idx.cols.len() || term.nulls_first.is_some() || term.descending != backward {
break;
}
let (tbl, col_name) = match &term.expr {
Expr::Column { table, column } => (table.as_deref(), column.as_str()),
_ => break,
};
if tbl.is_some_and(|tn| !tn.eq_ignore_ascii_case(label)) {
break;
}
let Some(col) = meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(col_name))
else {
break;
};
if col != idx.cols[i] || idx.collations[i] != meta.columns[col].collation {
break;
}
k += 1;
}
k
}
fn seek_index_covers(
&self,
sel: &Select,
meta: &TableMeta,
idx_cols: &[usize],
where_expr: &Expr,
) -> bool {
if !self.index_covers_query(sel, meta, idx_cols) {
return false;
}
where_cols_covered(where_expr, meta, idx_cols)
}
fn covering_seek_rows(
&self,
meta: &TableMeta,
root: u32,
idx_cols: &[usize],
) -> Result<Vec<InputRow>> {
let src = self.backend.source();
let encoding = src.header().text_encoding;
let mut icur = IndexCursor::new(src, root);
let mut out = Vec::new();
while let Some(payload) = icur.next()? {
let rec = decode_record(&payload, encoding)?;
let rowid = match rec.get(idx_cols.len()) {
Some(Value::Integer(r)) => *r,
_ => return Err(Error::Corrupt("index record missing rowid".into())),
};
let mut values = alloc::vec![Value::Null; meta.columns.len()];
for (i, &mc) in idx_cols.iter().enumerate() {
values[mc] = rec[i].clone();
}
if let Some(ipk) = meta.ipk {
values[ipk] = Value::Integer(rowid);
}
out.push(InputRow {
values,
rowid: Some(rowid),
});
}
Ok(out)
}
fn index_covers_query(&self, sel: &Select, meta: &TableMeta, idx_cols: &[usize]) -> bool {
if meta.generated.iter().any(|g| g.is_some()) {
return false;
}
let covered = |ci: usize| idx_cols.contains(&ci) || meta.ipk == Some(ci);
let col_ok = |expr: &Expr| -> bool {
match expr {
Expr::Column { column, .. } => match meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(column))
{
Some(ci) => covered(ci),
None => matches!(
column.to_ascii_lowercase().as_str(),
"rowid" | "_rowid_" | "oid"
),
},
_ => false,
}
};
for rc in &sel.columns {
match rc {
ResultColumn::Wildcard | ResultColumn::TableWildcard(_) => {
if !(0..meta.columns.len()).all(covered) {
return false;
}
}
ResultColumn::Expr { expr, .. } => {
if !col_ok(expr) {
return false;
}
}
}
}
sel.order_by.iter().all(|t| col_ok(&t.expr))
}
fn query_cols_covered(&self, sel: &Select, meta: &TableMeta, idx_cols: &[usize]) -> bool {
if meta.generated.iter().any(|g| g.is_some()) {
return false;
}
let covered_all =
(0..meta.columns.len()).all(|ci| idx_cols.contains(&ci) || meta.ipk == Some(ci));
for rc in &sel.columns {
match rc {
ResultColumn::Wildcard | ResultColumn::TableWildcard(_) => {
if !covered_all {
return false;
}
}
ResultColumn::Expr { expr, .. } => {
if !where_cols_covered(expr, meta, idx_cols) {
return false;
}
}
}
}
sel.group_by
.iter()
.all(|e| where_cols_covered(e, meta, idx_cols))
&& sel
.having
.as_ref()
.is_none_or(|h| where_cols_covered(h, meta, idx_cols))
&& sel
.order_by
.iter()
.all(|t| where_cols_covered(&t.expr, meta, idx_cols))
&& sel
.where_clause
.as_ref()
.is_none_or(|w| where_cols_covered(w, meta, idx_cols))
}
fn covering_scan(
&self,
sel: &Select,
meta: &TableMeta,
params: &Params,
) -> Option<(String, u32, Vec<usize>)> {
let from = sel.from.as_ref()?;
if !from.joins.is_empty() {
return None;
}
let t = &from.first;
if t.subquery.is_some() || t.tvf_args.is_some() || t.schema.is_some() {
return None;
}
if sel.where_clause.is_some() || window::has_window(sel) || meta.without_rowid {
return None;
}
if self.lookup_cte(&t.name, None).is_some() || self.is_view(&t.name) {
return None;
}
if self.order_satisfied_by_scan(sel, params).is_some() {
return None;
}
let mut covering = self.indexes_of(&t.name).ok()?.into_iter().filter(|idx| {
idx.partial.is_none()
&& idx.key_exprs.is_none()
&& self.query_cols_covered(sel, meta, &idx.cols)
});
let chosen = covering.next()?;
if covering.next().is_some() {
return None;
}
Some((chosen.name, chosen.root, chosen.cols))
}
fn count_covering_index(&self, sel: &Select) -> Option<(String, u32)> {
let from = sel.from.as_ref()?;
if !from.joins.is_empty() {
return None;
}
let t = &from.first;
if t.subquery.is_some() || t.tvf_args.is_some() || t.schema.is_some() {
return None;
}
if sel.where_clause.is_some()
|| !sel.group_by.is_empty()
|| sel.having.is_some()
|| sel.distinct
|| !sel.order_by.is_empty()
{
return None;
}
if window::has_window(sel) {
return None;
}
if sel.columns.len() != 1 {
return None;
}
let ResultColumn::Expr { expr, .. } = &sel.columns[0] else {
return None;
};
match expr {
Expr::Function {
name,
distinct: false,
star: true,
filter: None,
over: None,
..
} if name.eq_ignore_ascii_case("count") => {}
_ => return None,
}
if self.lookup_cte(&t.name, None).is_some() || self.is_view(&t.name) {
return None;
}
let meta = self.table_meta(&t.name, t.alias.as_deref()).ok()?;
if meta.without_rowid {
return None;
}
let mut chosen: Option<(String, u32)> = None;
for idx in self.indexes_of(&t.name).ok()? {
if idx.partial.is_some() || idx.key_exprs.is_some() {
continue;
}
if chosen.is_some() {
return None; }
chosen = Some((idx.name, idx.root));
}
chosen
}
fn order_satisfied_by_scan(&self, sel: &Select, params: &Params) -> Option<bool> {
if let Some(d) = self.rowid_ordered_scan(sel) {
return Some(d);
}
if let Some(s) = self.order_index_scan(sel) {
if s.sorted_suffix == 0 {
return Some(s.descending);
}
}
match self.seek_order_prefix(sel, params) {
Some((k, descending)) if k == sel.order_by.len() => Some(descending),
_ => None,
}
}
fn seek_order_prefix(&self, sel: &Select, params: &Params) -> Option<(usize, bool)> {
let from = sel.from.as_ref()?;
if !from.joins.is_empty() {
return None;
}
let t = &from.first;
if t.subquery.is_some()
|| t.tvf_args.is_some()
|| t.schema.is_some()
|| from.first.index_hint.is_some()
{
return None;
}
let where_expr = sel.where_clause.as_ref()?;
if sel.order_by.is_empty()
|| !sel.group_by.is_empty()
|| sel.having.is_some()
|| sel.distinct
|| self.has_aggregate(sel)
|| window::has_window(sel)
{
return None;
}
if self.lookup_cte(&t.name, None).is_some() || self.is_view(&t.name) {
return None;
}
let label = t.alias.as_deref().unwrap_or(&t.name);
let meta = self.table_meta(&t.name, t.alias.as_deref()).ok()?;
if meta.without_rowid {
return None;
}
let indexes = self.indexes_of(&t.name).ok()?;
let mut eqs: Vec<(usize, Value)> = Vec::new();
collect_eq_constraints(where_expr, &meta.columns, params, &mut eqs);
eqs.retain(|(_, v)| !matches!(v, Value::Null));
let (walk_cols, walk_colls): (&[usize], &[crate::value::Collation]) = if !eqs.is_empty() {
if meta
.ipk
.is_some_and(|ipk| eqs.iter().any(|(c, _)| *c == ipk))
{
return None;
}
let mut seekable = indexes.iter().filter(|idx| {
idx.partial.is_none()
&& idx.key_exprs.is_none()
&& idx
.cols
.first()
.is_some_and(|c| eqs.iter().any(|(col, _)| col == c))
});
let idx = seekable.next()?;
if seekable.next().is_some() {
return None;
}
let prefix = idx
.cols
.iter()
.take_while(|&&c| eqs.iter().any(|(col, _)| *col == c))
.count();
(&idx.cols[prefix..], &idx.collations[prefix..])
} else {
let mut ranges: alloc::collections::BTreeMap<usize, RangeBound> =
alloc::collections::BTreeMap::new();
collect_range_constraints(where_expr, &meta.columns, params, &mut ranges);
if ranges.is_empty() {
return None;
}
if meta.ipk.is_some_and(|ipk| ranges.contains_key(&ipk)) {
return None;
}
if indexes
.iter()
.any(|idx| idx.partial.is_some() || idx.key_exprs.is_some())
{
return None;
}
let mut seekable = indexes
.iter()
.filter(|idx| idx.cols.first().is_some_and(|c| ranges.contains_key(c)));
let idx = seekable.next()?;
if seekable.next().is_some() {
return None;
}
(&idx.cols[..], &idx.collations[..])
};
let descending = sel.order_by[0].descending;
let mut k = 0;
for term in &sel.order_by {
if k >= walk_cols.len() || term.descending != descending || term.nulls_first.is_some() {
break;
}
let (tbl, col_name) = match &term.expr {
Expr::Column { table, column } => (table.as_deref(), column.as_str()),
_ => break,
};
if tbl.is_some_and(|tn| !tn.eq_ignore_ascii_case(label)) {
break;
}
let Some(oc) = meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(col_name))
else {
break;
};
if walk_cols[k] != oc || walk_colls[k] != meta.columns[oc].collation {
break;
}
k += 1;
}
Some((k, descending))
}
#[cfg(feature = "fts5")]
fn fts5_match_query(&self, expr: &Expr, params: &Params) -> Option<(String, String)> {
match expr {
Expr::Function { name, args, .. }
if name.eq_ignore_ascii_case("match") && args.len() == 2 =>
{
let v = eval::eval(&args[0], &eval::EvalCtx::rowless(params)).ok()?;
let operand = match &args[1] {
Expr::Column { column, .. } => column.clone(),
_ => return None,
};
Some((eval::to_text(&v), operand))
}
Expr::Binary { left, right, .. } => self
.fts5_match_query(left, params)
.or_else(|| self.fts5_match_query(right, params)),
Expr::Unary { expr, .. } | Expr::Paren(expr) => self.fts5_match_query(expr, params),
_ => None,
}
}
#[cfg(feature = "fts5")]
fn fts5_query_ctx(
&self,
sel: &Select,
columns: &[ColumnInfo],
input_rows: &[InputRow],
params: &Params,
) -> Option<Fts5QueryCtx> {
const AUX: &[&str] = &["rank", "bm25", "highlight", "snippet"];
const RANK: &[&str] = &["rank", "bm25"];
if !select_mentions(sel, AUX) {
return None;
}
let from = sel.from.as_ref()?;
if !from.joins.is_empty()
|| from.first.subquery.is_some()
|| from.first.tvf_args.is_some()
|| from.first.schema.is_some()
{
return None;
}
let (module, vargs, _) = self.vtab_meta(&from.first.name).ok()?;
if !module.eq_ignore_ascii_case("fts5") {
return None;
}
let arg_refs: Vec<&str> = vargs.iter().map(String::as_str).collect();
let all = crate::vtab::fts5_indexed_columns(&arg_refs);
let indexed = (all.len() != columns.len()).then_some(all);
let tok = crate::vtab::fts5_tok_config(&arg_refs);
let (query, operand) = self.fts5_match_query(sel.where_clause.as_ref()?, params)?;
let col_names: Vec<String> = columns.iter().map(|c| c.name.clone()).collect();
let scope = col_names
.iter()
.find(|n| n.eq_ignore_ascii_case(&operand))
.cloned();
let bm25 = select_mentions(sel, RANK).then(|| {
let docs: Vec<Vec<String>> = input_rows
.iter()
.map(|r| r.values.iter().map(eval::to_text).collect())
.collect();
let corpus = crate::vtab::fts5_bm25_corpus(
&query,
&col_names,
&docs,
scope.as_deref(),
indexed.as_deref(),
tok,
);
let index = input_rows
.iter()
.enumerate()
.filter_map(|(i, r)| Some((r.rowid?, i)))
.collect();
(corpus, index)
});
Some(Fts5QueryCtx {
col_names,
query,
scope,
indexed,
tok,
bm25,
})
}
fn run_core(&self, sel: &Select, params: &Params) -> Result<QueryResult> {
if self.use_vdbe.get() && self.outer_scope.borrow().is_empty() {
let substituted;
let vsel = if params.positional.is_empty() && params.named.is_empty() {
Some(sel)
} else {
match substitute_params(sel, params) {
Some(s) => {
substituted = s;
Some(&substituted)
}
None => None,
}
};
if let Some(vsel) = vsel {
if let Ok(result) = self.run_select_vdbe(vsel) {
return Ok(result);
}
}
}
let rewritten;
let sel = match promote_comma_join_ons(sel) {
Some(r) => {
rewritten = r;
&rewritten
}
None => sel,
};
if let Some((_, root)) = self.count_covering_index(sel) {
let mut cur = IndexCursor::new(self.backend.source(), root);
let mut n = 0i64;
while cur.next()?.is_some() {
n += 1;
}
let label = self.output_labels(sel, &[]).pop().unwrap_or_default();
return Ok(QueryResult {
columns: alloc::vec![label],
rows: alloc::vec![alloc::vec![Value::Integer(n)]],
});
}
let (columns, input_rows) = self.scan_source(sel, params)?;
#[cfg(feature = "fts5")]
let _fts5_rank_guard = Fts5RankGuard {
conn: self,
prev: core::mem::replace(
&mut *self.fts5_rank.borrow_mut(),
self.fts5_query_ctx(sel, &columns, &input_rows, params),
),
};
let alias_rewritten;
let sel = match alias_substituted(sel, &columns) {
Some(s) => {
alias_rewritten = s;
&alias_rewritten
}
None => sel,
};
validate_unambiguous_columns(sel, &columns)?;
if self.outer_scope.borrow().is_empty() {
self.validate_nested_ambiguity(sel, &columns)?;
self.validate_columns_exist(sel, &columns)?;
}
let ncols = self.output_labels(sel, &columns).len();
check_positional_terms(&sel.group_by, &sel.order_by, ncols)?;
{
let is_agg = |name: &str, n: usize, star: bool| {
func::is_aggregate_call(name, n, star)
|| self.aggregates.contains_key(&name.to_ascii_lowercase())
};
if sel.group_by.iter().any(|g| expr_contains_agg(g, &is_agg)) {
return Err(Error::Error(
"aggregate functions are not allowed in the GROUP BY clause".into(),
));
}
}
let mut rows: Vec<InputRow> = Vec::new();
for r in input_rows {
if let Some(pred) = &sel.where_clause {
let ctx = r.ctx(&columns, params).with_subqueries(self);
if eval::truth(&eval::eval(pred, &ctx)?) != Some(true) {
continue;
}
}
rows.push(r);
}
self.finish_from_rows(sel, columns, rows, params)
}
fn finish_from_rows(
&self,
sel: &Select,
mut columns: Vec<ColumnInfo>,
mut rows: Vec<InputRow>,
params: &Params,
) -> Result<QueryResult> {
let windowed_agg =
window::has_window(sel) && (!sel.group_by.is_empty() || self.has_aggregate(sel));
let window_labels = if window::has_window(sel) && !windowed_agg {
Some(self.output_labels(sel, &columns))
} else {
None
};
let rewritten;
let sel = if window::has_window(sel) && !windowed_agg {
rewritten = self.apply_windows(sel, &mut columns, &mut rows, params)?;
&rewritten
} else {
sel
};
let aggregated = !sel.group_by.is_empty() || self.has_aggregate(sel);
if sel.having.is_some() && sel.group_by.is_empty() && !self.has_result_aggregate(sel) {
return Err(Error::Error(
"HAVING clause on a non-aggregate query".into(),
));
}
let (mut out_labels, mut out) = if windowed_agg {
self.eval_windowed_aggregate(sel, &columns, rows, params)?
} else if aggregated {
self.eval_aggregated(sel, &columns, rows, params)?
} else {
self.eval_simple(sel, &columns, rows, params)?
};
if let Some(labels) = window_labels {
out_labels = labels;
}
if sel.distinct {
let colls = self.output_collations(sel, &columns, params);
let mut seen: Vec<Vec<Value>> = Vec::new();
out.retain(|row| {
if seen.iter().any(|s| rows_equal_coll(s, &row.values, &colls)) {
false
} else {
seen.push(row.values.clone());
true
}
});
}
if !sel.order_by.is_empty() {
match self.order_satisfied_by_scan(sel, params) {
Some(true) => out.reverse(),
Some(false) => {}
None => {
let colls = self.order_collations(sel, &columns, params);
out.sort_by(|a, b| {
for (i, term) in sel.order_by.iter().enumerate() {
let ord = cmp_order(
&a.sort_keys[i],
&b.sort_keys[i],
term.descending,
term.nulls_first,
colls[i],
);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
});
}
}
}
let offset = match &sel.offset {
Some(e) => must_be_int(eval::eval(
e,
&EvalCtx::rowless(params).with_subqueries(self),
)?)?
.max(0) as usize,
None => 0,
};
let limit = match &sel.limit {
Some(e) => {
let n = must_be_int(eval::eval(
e,
&EvalCtx::rowless(params).with_subqueries(self),
)?)?;
if n < 0 {
None
} else {
Some(n as usize)
}
}
None => None,
};
let mut final_rows: Vec<Vec<Value>> =
out.into_iter().skip(offset).map(|r| r.values).collect();
if let Some(n) = limit {
final_rows.truncate(n);
}
Ok(QueryResult {
columns: out_labels,
rows: final_rows,
})
}
fn run_window_vdbe(&self, sel: &Select) -> Result<QueryResult> {
fn is_rowid_name(n: &str) -> bool {
n.eq_ignore_ascii_case("rowid")
|| n.eq_ignore_ascii_case("_rowid_")
|| n.eq_ignore_ascii_case("oid")
}
fn spec_has_rowid(spec: &WindowSpec) -> bool {
spec.partition_by.iter().any(expr_has_rowid)
|| spec.order_by.iter().any(|t| expr_has_rowid(&t.expr))
}
fn expr_has_rowid(e: &Expr) -> bool {
let mut found = false;
window::visit(e, &mut |node| match node {
Expr::Column { column, .. } if is_rowid_name(column) => found = true,
Expr::Function {
over: Some(spec), ..
} if spec_has_rowid(spec) => found = true,
_ => {}
});
found
}
fn select_mentions_rowid(sel: &Select) -> bool {
sel.columns
.iter()
.any(|c| matches!(c, ResultColumn::Expr { expr, .. } if expr_has_rowid(expr)))
|| sel.where_clause.as_ref().is_some_and(expr_has_rowid)
|| sel.group_by.iter().any(expr_has_rowid)
|| sel.having.as_ref().is_some_and(expr_has_rowid)
|| sel.order_by.iter().any(|t| expr_has_rowid(&t.expr))
|| sel.window_defs.iter().any(|(_, spec)| spec_has_rowid(spec))
}
if !sel.ctes.is_empty() {
return Err(Error::Unsupported("VDBE window: query carries CTEs"));
}
let Some(from) = &sel.from else {
return Err(Error::Unsupported("VDBE window: no FROM"));
};
let is_join = !from.joins.is_empty();
let columns = if is_join {
if select_mentions_rowid(sel) {
return Err(Error::Unsupported("VDBE window: join references rowid"));
}
let Some(cols) = self.static_scope_columns(sel) else {
return Err(Error::Unsupported("VDBE window: non-plain join source"));
};
cols
} else {
let tref = &from.first;
if tref.subquery.is_some()
|| tref.tvf_args.is_some()
|| tref.index_hint.is_some()
|| tref.schema.is_some()
|| self.is_pragma_tvf(tref)
|| self.is_view(&tref.name)
|| self.is_virtual_table(&tref.name)
|| self
.cte_env
.borrow()
.iter()
.any(|b| b.name.eq_ignore_ascii_case(&tref.name))
{
return Err(Error::Unsupported("VDBE window: non-plain source"));
}
self.table_meta(&tref.name, tref.alias.as_deref())?.columns
};
let ncols = columns.len();
let mut base = sel.clone();
base.distinct = false;
base.group_by = Vec::new();
base.having = None;
base.window_defs = Vec::new();
base.order_by = Vec::new();
base.limit = None;
base.offset = None;
base.columns = if is_join {
alloc::vec![ResultColumn::Wildcard]
} else {
alloc::vec![
ResultColumn::Wildcard,
ResultColumn::Expr {
expr: Expr::Column {
table: None,
column: "rowid".into(),
},
alias: Some("__winrowid__".into()),
source: None,
},
]
};
let scanned = self.run_select_vdbe(&base)?;
let mut rows: Vec<InputRow> = Vec::with_capacity(scanned.rows.len());
for mut values in scanned.rows {
let rowid = if is_join {
if values.len() != ncols {
return Err(Error::Unsupported("VDBE window: column count mismatch"));
}
None
} else {
if values.len() != ncols + 1 {
return Err(Error::Unsupported("VDBE window: column count mismatch"));
}
match values.pop() {
Some(Value::Integer(id)) => Some(id),
_ => None,
}
};
rows.push(InputRow { values, rowid });
}
self.finish_from_rows(sel, columns, rows, &Params::default())
}
fn static_scope_columns(&self, sel: &Select) -> Option<Vec<ColumnInfo>> {
let Some(from) = &sel.from else {
return Some(Vec::new());
};
if from.joins.iter().any(|j| j.natural || !j.using.is_empty()) {
return None;
}
let mut cols = Vec::new();
for tref in core::iter::once(&from.first).chain(from.joins.iter().map(|j| &j.table)) {
if tref.subquery.is_some()
|| tref.tvf_args.is_some()
|| tref.schema.is_some()
|| self.is_pragma_tvf(tref)
|| self.is_view(&tref.name)
|| self
.cte_env
.borrow()
.iter()
.any(|b| b.name.eq_ignore_ascii_case(&tref.name))
{
return None;
}
let meta = self.table_meta(&tref.name, tref.alias.as_deref()).ok()?;
cols.extend(meta.columns);
}
Some(cols)
}
fn validate_columns_exist(&self, sel: &Select, columns: &[ColumnInfo]) -> Result<()> {
let Some(from) = &sel.from else { return Ok(()) };
if window::has_window(sel) {
return Ok(());
}
let mut srcs = alloc::vec![&from.first];
for j in &from.joins {
if j.natural || !j.using.is_empty() {
return Ok(());
}
srcs.push(&j.table);
}
let mut labels: Vec<&str> = Vec::new();
for s in &srcs {
let plain = s.subquery.is_none() && s.tvf_args.is_none() && !s.name.is_empty();
if !plain || self.is_virtual_table(&s.name) {
return Ok(());
}
labels.push(s.alias.as_deref().unwrap_or(&s.name));
}
for c in &sel.columns {
if let ResultColumn::TableWildcard(q) = c {
if !labels.iter().any(|l| l.eq_ignore_ascii_case(q)) {
return Err(Error::Error(alloc::format!("no such table: {q}")));
}
}
}
let mut targets: Vec<&Expr> = Vec::new();
for c in &sel.columns {
if let ResultColumn::Expr { expr, .. } = c {
targets.push(expr);
}
}
if let Some(w) = &sel.where_clause {
targets.push(w);
}
for j in &from.joins {
if let Some(on) = &j.on {
targets.push(on);
}
}
let mut qualified_only: Vec<&Expr> = Vec::new();
for g in &sel.group_by {
qualified_only.push(g);
}
if let Some(h) = &sel.having {
qualified_only.push(h);
}
for o in &sel.order_by {
qualified_only.push(&o.expr);
}
let column_missing = |table: Option<&str>, column: &str| -> Option<String> {
if matches!(
column.to_ascii_lowercase().as_str(),
"rowid" | "oid" | "_rowid_" | "current_date" | "current_time" | "current_timestamp"
) {
return None;
}
let n = columns
.iter()
.filter(|c| {
c.name.eq_ignore_ascii_case(column)
&& table.is_none_or(|t| c.table.eq_ignore_ascii_case(t))
})
.count();
(n == 0).then(|| match table {
Some(t) => alloc::format!("no such column: {t}.{column}"),
None => alloc::format!("no such column: {column}"),
})
};
let mut missing: Option<String> = None;
for e in targets {
if missing.is_some() {
break;
}
walk_shallow_columns(e, &mut |table, column| {
if missing.is_none() {
missing = column_missing(table, column);
}
});
}
for e in qualified_only {
if missing.is_some() {
break;
}
walk_shallow_columns(e, &mut |table, column| {
if missing.is_none() && table.is_some() {
missing = column_missing(table, column);
}
});
}
match missing {
Some(m) => Err(Error::Error(m)),
None => Ok(()),
}
}
fn validate_dml_refs(
&self,
table: &str,
columns: &[ColumnInfo],
exprs: &[&Expr],
) -> Result<()> {
let mut missing: Option<String> = None;
for e in exprs {
if missing.is_some() {
break;
}
walk_shallow_columns(e, &mut |tbl, col| {
if missing.is_some() {
return;
}
if matches!(
col.to_ascii_lowercase().as_str(),
"rowid"
| "oid"
| "_rowid_"
| "current_date"
| "current_time"
| "current_timestamp"
) {
return;
}
if let Some(q) = tbl {
if !q.eq_ignore_ascii_case(table) {
return;
}
}
if !columns.iter().any(|c| c.name.eq_ignore_ascii_case(col)) {
missing = Some(match tbl {
Some(t) => alloc::format!("no such column: {t}.{col}"),
None => alloc::format!("no such column: {col}"),
});
}
});
}
match missing {
Some(m) => Err(Error::Error(m)),
None => Ok(()),
}
}
fn validate_nested_ambiguity(&self, sel: &Select, top: &[ColumnInfo]) -> Result<()> {
let scopes = alloc::vec![Some(top.to_vec())];
self.walk_nested_ambiguity(sel, &scopes)
}
fn walk_nested_ambiguity(
&self,
sel: &Select,
scopes: &[Option<Vec<ColumnInfo>>],
) -> Result<()> {
let mut subs: Vec<&Select> = Vec::new();
vdbe_block_exprs(sel, &mut |e| collect_subselects(e, &mut subs));
for sub in subs {
let mut child: Vec<Option<Vec<ColumnInfo>>> =
alloc::vec![self.static_scope_columns(sub)];
child.extend(scopes.iter().cloned());
if let Some(msg) = first_ambiguous_in_scopes(sub, &child) {
return Err(Error::Error(msg));
}
self.walk_nested_ambiguity(sub, &child)?;
}
Ok(())
}
fn scan_source(
&self,
sel: &Select,
params: &Params,
) -> Result<(Vec<ColumnInfo>, Vec<InputRow>)> {
let Some(from) = &sel.from else {
return Ok((
Vec::new(),
alloc::vec![InputRow {
values: Vec::new(),
rowid: None
}],
));
};
for tref in core::iter::once(&from.first).chain(from.joins.iter().map(|j| &j.table)) {
if let Some(IndexHint::IndexedBy(name)) = &tref.index_hint {
if self.schema.table(&tref.name).is_some() {
let auto_prefix =
alloc::format!("sqlite_autoindex_{}_", tref.name.to_ascii_lowercase());
let known = self
.schema
.indexes_on(&tref.name)
.any(|o| o.name.eq_ignore_ascii_case(name))
|| name.to_ascii_lowercase().starts_with(&auto_prefix);
if !known {
return Err(Error::Error(alloc::format!("no such index: {name}")));
}
}
}
}
if from.joins.is_empty() && from.first.subquery.is_none() && from.first.tvf_args.is_none() {
if from.first.schema.is_none() && is_temp_schema_table(&from.first.name) {
let alias = from.first.alias.as_deref();
return match &self.temp_db {
Some(_) => self.scan_db_table(DbRef::Temp, "sqlite_master", alias),
None => Ok((
schema_table_meta(alias.unwrap_or(&from.first.name)).columns,
Vec::new(),
)),
};
}
let lname = from.first.name.to_ascii_lowercase();
if matches!(lname.as_str(), "dbstat" | "sqlite_dbpage") {
let qual_db = match from.first.schema.as_deref() {
None => DbRef::Main,
Some(s) => self.resolve_db(Some(s))?,
};
let shadowed = match qual_db {
DbRef::Main => self.schema.table(&lname).is_some(),
DbRef::Temp => self
.temp_db
.as_ref()
.is_some_and(|t| t.schema.table(&lname).is_some()),
DbRef::Attached(i) => self.attached[i].schema.table(&lname).is_some(),
};
if !shadowed {
let alias = from.first.alias.as_deref();
let src = self.backend.source();
return match lname.as_str() {
"dbstat" => self.scan_dbstat(&self.schema, src, alias),
_ => self.scan_dbpage(src, alias),
};
}
}
let db = match from.first.schema.as_deref() {
Some(_) => self.resolve_db(from.first.schema.as_deref())?,
None if self.lookup_cte(&from.first.name, None).is_none()
&& !self.is_view(&from.first.name) =>
{
self.unqualified_db(&from.first.name)
}
None => DbRef::Main,
};
if db != DbRef::Main {
self.guard_qualified_temp(db, from.first.schema.as_deref(), &from.first.name)?;
let alias = from.first.alias.as_deref();
if let Some(r) = self.scan_db_view(db, &from.first.name, alias, params)? {
return Ok(r);
}
return self.scan_db_table(db, &from.first.name, alias);
}
}
if from.joins.is_empty()
&& (from.first.tvf_args.is_some() || self.is_pragma_tvf(&from.first))
{
let (columns, rows) = self.tvf_rows(&from.first, params)?;
let input = rows
.into_iter()
.map(|values| InputRow {
values,
rowid: None,
})
.collect();
return Ok((columns, input));
}
if from.joins.is_empty() {
if let Some(sub) = &from.first.subquery {
let (columns, rows) =
self.run_subquery_source(sub, from.first.alias.as_deref(), params)?;
let input = rows
.into_iter()
.map(|values| InputRow {
values,
rowid: None,
})
.collect();
return Ok((columns, input));
}
}
if from.joins.is_empty() {
if let Some((columns, rows)) =
self.lookup_cte(&from.first.name, from.first.alias.as_deref())
{
return Ok((columns, rows));
}
}
if from.joins.is_empty() {
if let Some((columns, rows)) =
self.try_view(&from.first.name, from.first.alias.as_deref(), params)?
{
return Ok((columns, rows));
}
}
if from.joins.is_empty() && from.first.schema.is_none() {
if let Some((columns, rows)) = self.try_virtual_table(
&from.first.name,
from.first.alias.as_deref(),
Some((sel, params)),
)? {
return Ok((columns, rows));
}
}
if from.joins.is_empty() {
let first_meta = self.table_meta(&from.first.name, from.first.alias.as_deref())?;
if first_meta.without_rowid {
if let Some(rows) = self.try_without_rowid_pk_seek(&first_meta, sel, params)? {
return Ok((first_meta.columns, rows));
}
if let Some(rows) = self.try_without_rowid_pk_range(&first_meta, sel, params)? {
return Ok((first_meta.columns, rows));
}
if let Some(rows) =
self.try_without_rowid_index_seek(&first_meta, &from.first.name, sel, params)?
{
return Ok((first_meta.columns, rows));
}
if let Some(rows) =
self.try_without_rowid_index_range(&first_meta, &from.first.name, sel, params)?
{
return Ok((first_meta.columns, rows));
}
let input_rows = self
.scan_without_rowid(&first_meta)?
.into_iter()
.map(|values| InputRow {
values,
rowid: None,
})
.collect();
return Ok((first_meta.columns, input_rows));
}
if let Some(rows) = self.try_index_lookup(&first_meta, &from.first.name, sel, params)? {
return Ok((first_meta.columns, rows));
}
if let Some(rows) = self.try_index_range(&first_meta, &from.first.name, sel, params)? {
return Ok((first_meta.columns, rows));
}
if let Some(rows) = self.try_index_in(&first_meta, &from.first.name, sel, params)? {
return Ok((first_meta.columns, rows));
}
if let Some(rows) = self.try_index_or(&first_meta, &from.first.name, sel, params)? {
return Ok((first_meta.columns, rows));
}
if let Some(s) = self.order_index_scan(sel) {
let src = self.backend.source();
let encoding = src.header().text_encoding;
if s.covering {
let mut icur = IndexCursor::new(src, s.root);
let mut input_rows = Vec::new();
while let Some(payload) = icur.next()? {
let rec = decode_record(&payload, encoding)?;
let rowid = match rec.get(s.cols.len()) {
Some(Value::Integer(r)) => *r,
_ => return Err(Error::Corrupt("index record missing rowid".into())),
};
let mut values = alloc::vec![Value::Null; first_meta.columns.len()];
for (i, &mc) in s.cols.iter().enumerate() {
values[mc] = rec[i].clone();
}
if let Some(ipk) = first_meta.ipk {
values[ipk] = Value::Integer(rowid);
}
input_rows.push(InputRow {
values,
rowid: Some(rowid),
});
}
return Ok((first_meta.columns, input_rows));
}
let rowids = crate::btree::index_range_rowids(src, s.root, None, None, &s.colls)?;
let mut cur = TableCursor::new(src, first_meta.root);
let mut input_rows = Vec::with_capacity(rowids.len());
for rid in rowids {
if cur.seek(rid)? {
let values =
self.decode_full_row(&first_meta, rid, &cur.payload()?, encoding)?;
input_rows.push(InputRow {
values,
rowid: Some(rid),
});
}
}
return Ok((first_meta.columns, input_rows));
}
if let Some((_, root, cols)) = self.covering_scan(sel, &first_meta, params) {
return Ok((
first_meta.columns.clone(),
self.covering_seek_rows(&first_meta, root, &cols)?,
));
}
let input_rows = self
.scan_table(&first_meta)?
.into_iter()
.map(|(rowid, values)| InputRow {
values,
rowid: Some(rowid),
})
.collect();
return Ok((first_meta.columns, input_rows));
}
let (mut columns, mut rows) = self.resolve_join_source(&from.first, params)?;
for join in &from.joins {
if let Some((outer_col, inner_meta)) = self.rowid_join_seek(join, &columns) {
let (new_columns, joined) = self.exec_rowid_join_seek(
join,
&columns,
&rows,
outer_col,
&inner_meta,
params,
)?;
columns = new_columns;
rows = joined;
continue;
}
if let Some((outer_col, inner_meta, idx)) = self.index_join_seek(join, &columns) {
let (new_columns, joined) = self.exec_index_join_seek(
join,
&columns,
&rows,
outer_col,
&inner_meta,
&idx,
params,
)?;
columns = new_columns;
rows = joined;
continue;
}
if let Some((outer_col, inner_meta)) = self.without_rowid_pk_join_seek(join, &columns) {
let (new_columns, joined) = self.exec_without_rowid_pk_join_seek(
join,
&columns,
&rows,
outer_col,
&inner_meta,
params,
)?;
columns = new_columns;
rows = joined;
continue;
}
let (jcols, jrows) = self.resolve_join_source(&join.table, params)?;
let left_width = columns.len();
let pairs: Vec<(usize, usize)> = if join.natural {
jcols
.iter()
.enumerate()
.filter_map(|(rl, rc)| {
columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(&rc.name))
.map(|li| (li, rl))
})
.collect()
} else if !join.using.is_empty() {
let mut v = Vec::with_capacity(join.using.len());
for name in &join.using {
let li = columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(name));
let rl = jcols.iter().position(|c| c.name.eq_ignore_ascii_case(name));
match (li, rl) {
(Some(li), Some(rl)) => v.push((li, rl)),
_ => {
return Err(Error::Error(format!(
"cannot join using column {name} - column not present in both tables"
)))
}
}
}
v
} else {
Vec::new()
};
let mut new_columns = columns.clone();
new_columns.extend(jcols.iter().cloned());
let n_jcols = jcols.len();
let mut joined: Vec<Vec<Value>> = Vec::new();
let mut right_matched = alloc::vec![false; jrows.len()];
let equi = if pairs.is_empty() {
join.on
.as_ref()
.and_then(|on| join_equi_cols(on, &new_columns, left_width))
} else {
None
};
let hash: Option<(usize, alloc::collections::BTreeMap<JoinKey, Vec<usize>>)> = equi
.map(|(li, ri_local)| {
let mut map: alloc::collections::BTreeMap<JoinKey, Vec<usize>> =
alloc::collections::BTreeMap::new();
for (ri, right) in jrows.iter().enumerate() {
for k in join_keys_of(&right[ri_local]) {
map.entry(k).or_default().push(ri);
}
}
(li, map)
});
for left in &rows {
let mut matched = false;
let candidates: Vec<usize> = match &hash {
Some((li, map)) => {
let mut c: Vec<usize> = Vec::new();
for k in join_keys_of(&left[*li]) {
if let Some(idxs) = map.get(&k) {
c.extend_from_slice(idxs);
}
}
c.sort_unstable();
c.dedup();
c
}
None => (0..jrows.len()).collect(),
};
for ri in candidates {
let right = &jrows[ri];
let mut combined = left.clone();
combined.extend(right.iter().cloned());
let keep = if !pairs.is_empty() {
pairs.iter().all(|&(li, rl)| {
let coll = new_columns[li].collation;
let (lv, rv) = eval::apply_comparison_affinity(
combined[li].clone(),
Some(new_columns[li].affinity),
combined[left_width + rl].clone(),
Some(new_columns[left_width + rl].affinity),
);
eval::truth(&eval::compare_op(BinaryOp::Eq, &lv, &rv, coll))
== Some(true)
})
} else {
match &join.on {
Some(on) => {
let ctx = row_ctx(&combined, &new_columns, None, params);
eval::truth(&eval::eval(on, &ctx)?) == Some(true)
}
None => true, }
};
if keep {
joined.push(combined);
matched = true;
right_matched[ri] = true;
}
}
if !matched && matches!(join.kind, JoinKind::Left | JoinKind::Full) {
let mut combined = left.clone();
combined.extend(core::iter::repeat_n(Value::Null, n_jcols));
joined.push(combined);
}
}
if matches!(join.kind, JoinKind::Right | JoinKind::Full) {
for (ri, right) in jrows.iter().enumerate() {
if !right_matched[ri] {
let mut combined = alloc::vec![Value::Null; left_width];
combined.extend(right.iter().cloned());
joined.push(combined);
}
}
}
if !pairs.is_empty() {
let mut drop: Vec<usize> = pairs.iter().map(|&(_, rl)| left_width + rl).collect();
drop.sort_unstable();
drop.dedup();
for row in &mut joined {
for &(li, rl) in &pairs {
if matches!(row[li], Value::Null) {
row[li] = row[left_width + rl].clone();
}
}
for &d in drop.iter().rev() {
row.remove(d);
}
}
for &d in drop.iter().rev() {
new_columns.remove(d);
}
}
columns = new_columns;
rows = joined;
}
let input_rows = rows
.into_iter()
.map(|values| InputRow {
values,
rowid: None, })
.collect();
Ok((columns, input_rows))
}
fn is_pragma_tvf(&self, tref: &TableRef) -> bool {
tref.tvf_args.is_none()
&& tref.subquery.is_none()
&& tref.schema.is_none()
&& tref.name.to_ascii_lowercase().starts_with("pragma_")
&& self.lookup_cte(&tref.name, None).is_none()
&& !self.is_view(&tref.name)
&& self.unqualified_db(&tref.name) == DbRef::Main
&& self.schema.table(&tref.name).is_none()
}
fn tvf_rows(
&self,
tref: &TableRef,
params: &Params,
) -> Result<(Vec<ColumnInfo>, Vec<Vec<Value>>)> {
let args = tref.tvf_args.as_deref().unwrap_or(&[]);
let lname = tref.name.to_ascii_lowercase();
let label = tref.alias.clone().unwrap_or_else(|| tref.name.clone());
let ctx = EvalCtx::rowless(params).with_subqueries(self);
let col = |name: &str, affinity| ColumnInfo {
name: String::from(name),
table: label.clone(),
affinity,
collation: crate::value::Collation::default(),
};
match lname.as_str() {
"generate_series" => {
if args.is_empty() {
return Err(Error::Error("generate_series() requires arguments".into()));
}
let nums: Vec<i64> = args
.iter()
.map(|a| eval::eval(a, &ctx).map(|v| eval::to_i64(&v)))
.collect::<Result<_>>()?;
let start = nums[0];
let stop = nums.get(1).copied().unwrap_or(start);
let step = match nums.get(2).copied().unwrap_or(1) {
0 => 1,
s => s,
};
let mut rows = Vec::new();
if step != 0 {
let mut v = start;
loop {
let in_range = if step > 0 { v <= stop } else { v >= stop };
if !in_range {
break;
}
rows.push(alloc::vec![Value::Integer(v)]);
match v.checked_add(step) {
Some(n) => v = n,
None => break, }
}
}
Ok((alloc::vec![col("value", eval::Affinity::Integer)], rows))
}
"json_each" | "json_tree" => {
let columns = alloc::vec![
col("key", eval::Affinity::Blob),
col("value", eval::Affinity::Blob),
col("type", eval::Affinity::Text),
col("atom", eval::Affinity::Blob),
col("id", eval::Affinity::Integer),
col("parent", eval::Affinity::Integer),
col("fullkey", eval::Affinity::Text),
col("path", eval::Affinity::Text),
];
let Some(doc_arg) = args.first() else {
return Err(Error::Error(format!("{lname}() requires a JSON argument")));
};
let doc = eval::eval(doc_arg, &ctx)?;
if matches!(doc, Value::Null) {
return Ok((columns, Vec::new()));
}
let Some(root) = crate::exec::json::parse(&eval::to_text(&doc)) else {
return Err(Error::Error("malformed JSON".into()));
};
let (target, root_path) = match args.get(1) {
Some(path_arg) => {
let p = eval::to_text(&eval::eval(path_arg, &ctx)?);
match crate::exec::json::navigate(&root, &p) {
Some(sub) => (sub, p),
None => return Ok((columns, Vec::new())),
}
}
None => (&root, String::from("$")),
};
let mut rows = Vec::new();
let mut next_id = 0i64;
if lname == "json_tree" {
let (parent_path, key) = split_json_path(&root_path);
json_tree_walk(
target,
key,
&root_path,
&parent_path,
None,
&mut next_id,
&mut rows,
);
} else {
json_each_children(target, &root_path, &mut next_id, &mut rows);
}
Ok((columns, rows))
}
pragma if pragma.starts_with("pragma_") => {
let p = Pragma {
name: String::from(&pragma["pragma_".len()..]),
value: args.first().cloned(),
};
let result = self.run_pragma(&p)?;
let columns = result
.columns
.iter()
.map(|n| col(n, eval::Affinity::Blob))
.collect();
Ok((columns, result.rows))
}
_ => Err(Error::Error(format!(
"no such table-valued function: {}",
tref.name
))),
}
}
fn run_subquery_source(
&self,
select: &Select,
alias: Option<&str>,
params: &Params,
) -> Result<(Vec<ColumnInfo>, Vec<Vec<Value>>)> {
let result = self.run_select(select, params)?;
let label = alias.unwrap_or("").to_string();
let origins = self.subquery_column_origins(select);
let columns = result
.columns
.iter()
.enumerate()
.map(|(i, n)| {
let (affinity, collation) = origins
.as_ref()
.and_then(|o| o.get(i).copied())
.unwrap_or((eval::Affinity::Blob, crate::value::Collation::default()));
ColumnInfo {
name: n.clone(),
table: label.clone(),
affinity,
collation,
}
})
.collect();
Ok((columns, result.rows))
}
fn subquery_column_origins(&self, select: &Select) -> Option<Vec<ColOrigin>> {
if !select.compound.is_empty() {
return None;
}
let from = select.from.as_ref()?;
if !from.joins.is_empty() {
return None;
}
let src = self.named_source_origins(&from.first)?;
let label = from
.first
.alias
.clone()
.unwrap_or_else(|| from.first.name.clone());
let base = |table: Option<&str>, col: &str| -> Option<ColOrigin> {
if table.is_some_and(|t| !t.eq_ignore_ascii_case(&label)) {
return None;
}
src.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(col))
.map(|(_, o)| *o)
};
fn origin(e: &Expr, base: &dyn Fn(Option<&str>, &str) -> Option<ColOrigin>) -> ColOrigin {
match e {
Expr::Paren(inner) => origin(inner, base),
Expr::Column { table, column } => base(table.as_deref(), column)
.unwrap_or((eval::Affinity::Blob, crate::value::Collation::default())),
Expr::Collate { expr, collation } => {
let (aff, base_coll) = origin(expr, base);
(
aff,
crate::value::Collation::parse(collation).unwrap_or(base_coll),
)
}
_ => (eval::Affinity::Blob, crate::value::Collation::default()),
}
}
let mut out = Vec::new();
for rc in &select.columns {
match rc {
ResultColumn::Wildcard => out.extend(src.iter().map(|(_, o)| *o)),
ResultColumn::TableWildcard(t) if t.eq_ignore_ascii_case(&label) => {
out.extend(src.iter().map(|(_, o)| *o))
}
ResultColumn::TableWildcard(_) => return None,
ResultColumn::Expr { expr, .. } => out.push(origin(expr, &base)),
}
}
Some(out)
}
fn named_source_origins(&self, tref: &TableRef) -> Option<Vec<(String, ColOrigin)>> {
if tref.tvf_args.is_some() {
return None;
}
if let Some(sub) = &tref.subquery {
let names = self.resolved_view_columns(sub)?;
let origins = self.subquery_column_origins(sub)?;
if names.len() != origins.len() {
return None;
}
return Some(
names
.into_iter()
.zip(origins)
.map(|((n, _), o)| (n, o))
.collect(),
);
}
self.schema.table(&tref.name)?;
let meta = self.table_meta(&tref.name, tref.alias.as_deref()).ok()?;
Some(
meta.columns
.iter()
.map(|c| (c.name.clone(), (c.affinity, c.collation)))
.collect(),
)
}
fn rowid_join_seek(
&self,
join: &Join,
left_columns: &[ColumnInfo],
) -> Option<(usize, TableMeta)> {
if !matches!(join.kind, JoinKind::Inner | JoinKind::Left)
|| join.natural
|| !join.using.is_empty()
{
return None;
}
let on = join.on.as_ref()?;
let tref = &join.table;
if tref.subquery.is_some()
|| tref.tvf_args.is_some()
|| self.is_pragma_tvf(tref)
|| tref.schema.is_some()
|| self.lookup_cte(&tref.name, tref.alias.as_deref()).is_some()
|| self.is_view(&tref.name)
|| self.unqualified_db(&tref.name) != DbRef::Main
{
return None;
}
let meta = self.table_meta(&tref.name, tref.alias.as_deref()).ok()?;
let ipk = meta.ipk?;
let mut on = on;
while let Expr::Paren(inner) = on {
on = inner;
}
let left_width = left_columns.len();
let mut combined = left_columns.to_vec();
combined.extend(meta.columns.iter().cloned());
let (a, b) = match on {
Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} => (col_index(left, &combined)?, col_index(right, &combined)?),
_ => return None,
};
let inner_ipk = left_width + ipk;
let outer = if a == inner_ipk && b < left_width {
b
} else if b == inner_ipk && a < left_width {
a
} else {
return None;
};
Some((outer, meta))
}
fn index_join_seek(
&self,
join: &Join,
left_columns: &[ColumnInfo],
) -> Option<(usize, TableMeta, IndexMeta)> {
if !matches!(join.kind, JoinKind::Inner | JoinKind::Left)
|| join.natural
|| !join.using.is_empty()
{
return None;
}
let on = join.on.as_ref()?;
let tref = &join.table;
if tref.subquery.is_some()
|| tref.tvf_args.is_some()
|| self.is_pragma_tvf(tref)
|| tref.schema.is_some()
|| self.lookup_cte(&tref.name, tref.alias.as_deref()).is_some()
|| self.is_view(&tref.name)
|| self.unqualified_db(&tref.name) != DbRef::Main
{
return None;
}
let meta = self.table_meta(&tref.name, tref.alias.as_deref()).ok()?;
if meta.without_rowid {
return None;
}
let mut on = on;
while let Expr::Paren(inner) = on {
on = inner;
}
let left_width = left_columns.len();
let mut combined = left_columns.to_vec();
combined.extend(meta.columns.iter().cloned());
let (a, b) = match on {
Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} => (col_index(left, &combined)?, col_index(right, &combined)?),
_ => return None,
};
let (inner_idx, outer) = if a >= left_width && b < left_width {
(a - left_width, b)
} else if b >= left_width && a < left_width {
(b - left_width, a)
} else {
return None;
};
let indexes = self.indexes_of(&tref.name).ok()?;
let idx = indexes.into_iter().find(|i| {
i.partial.is_none() && i.key_exprs.is_none() && i.cols.first() == Some(&inner_idx)
})?;
Some((outer, meta, idx))
}
fn without_rowid_pk_join_seek(
&self,
join: &Join,
left_columns: &[ColumnInfo],
) -> Option<(usize, TableMeta)> {
if !matches!(join.kind, JoinKind::Inner | JoinKind::Left)
|| join.natural
|| !join.using.is_empty()
{
return None;
}
let on = join.on.as_ref()?;
let tref = &join.table;
if tref.subquery.is_some()
|| tref.tvf_args.is_some()
|| self.is_pragma_tvf(tref)
|| tref.schema.is_some()
|| self.lookup_cte(&tref.name, tref.alias.as_deref()).is_some()
|| self.is_view(&tref.name)
|| self.unqualified_db(&tref.name) != DbRef::Main
{
return None;
}
let meta = self.table_meta(&tref.name, tref.alias.as_deref()).ok()?;
if !meta.without_rowid || meta.pk_len == 0 {
return None;
}
let lead_pk = meta.storage_order[0];
let mut on = on;
while let Expr::Paren(inner) = on {
on = inner;
}
let left_width = left_columns.len();
let mut combined = left_columns.to_vec();
combined.extend(meta.columns.iter().cloned());
let (a, b) = match on {
Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} => (col_index(left, &combined)?, col_index(right, &combined)?),
_ => return None,
};
let (inner_idx, outer) = if a >= left_width && b < left_width {
(a - left_width, b)
} else if b >= left_width && a < left_width {
(b - left_width, a)
} else {
return None;
};
if inner_idx != lead_pk {
return None;
}
Some((outer, meta))
}
fn exec_without_rowid_pk_join_seek(
&self,
join: &Join,
columns: &[ColumnInfo],
rows: &[Vec<Value>],
outer_col: usize,
inner_meta: &TableMeta,
params: &Params,
) -> Result<(Vec<ColumnInfo>, Vec<Vec<Value>>)> {
let mut new_columns = columns.to_vec();
new_columns.extend(inner_meta.columns.iter().cloned());
let n_jcols = inner_meta.columns.len();
let on = join.on.as_ref();
let is_left = matches!(join.kind, JoinKind::Left);
let lead = inner_meta.storage_order[0];
let coll = wr_storage_collations(inner_meta)[0];
let src = self.backend.source();
let mut joined: Vec<Vec<Value>> = Vec::new();
for left in rows {
let mut matched = false;
if !matches!(left[outer_col], Value::Null) {
let key = [inner_meta.columns[lead]
.affinity
.coerce(left[outer_col].clone())];
let records =
crate::btree::index_seek_records(src, inner_meta.root, &key, &[coll])?;
for storage in records {
let mut inner = unpermute_row(inner_meta, storage);
self.compute_generated(inner_meta, &mut inner, params)?;
let mut row = left.clone();
row.extend(inner);
let keep = match on {
Some(on) => {
let ctx = row_ctx(&row, &new_columns, None, params);
eval::truth(&eval::eval(on, &ctx)?) == Some(true)
}
None => true,
};
if keep {
joined.push(row);
matched = true;
}
}
}
if !matched && is_left {
let mut combined = left.clone();
combined.extend(core::iter::repeat_n(Value::Null, n_jcols));
joined.push(combined);
}
}
Ok((new_columns, joined))
}
#[allow(clippy::too_many_arguments)]
fn exec_index_join_seek(
&self,
join: &Join,
columns: &[ColumnInfo],
rows: &[Vec<Value>],
outer_col: usize,
inner_meta: &TableMeta,
idx: &IndexMeta,
params: &Params,
) -> Result<(Vec<ColumnInfo>, Vec<Vec<Value>>)> {
let encoding = self.backend.source().header().text_encoding;
let mut new_columns = columns.to_vec();
new_columns.extend(inner_meta.columns.iter().cloned());
let n_jcols = inner_meta.columns.len();
let on = join.on.as_ref();
let is_left = matches!(join.kind, JoinKind::Left);
let lead = idx.cols[0];
let coll = idx.collations[0];
let src = self.backend.source();
let mut cur = TableCursor::new(self.backend.source(), inner_meta.root);
let mut joined: Vec<Vec<Value>> = Vec::new();
for left in rows {
let mut matched = false;
if !matches!(left[outer_col], Value::Null) {
let key = [inner_meta.columns[lead]
.affinity
.coerce(left[outer_col].clone())];
let colls = [coll];
let rowids = crate::btree::index_seek_rowids(src, idx.root, &key, &colls)?;
for rid in rowids {
if cur.seek(rid)? {
let inner =
self.decode_full_row(inner_meta, rid, &cur.payload()?, encoding)?;
let mut row = left.clone();
row.extend(inner);
let keep = match on {
Some(on) => {
let ctx = row_ctx(&row, &new_columns, None, params);
eval::truth(&eval::eval(on, &ctx)?) == Some(true)
}
None => true,
};
if keep {
joined.push(row);
matched = true;
}
}
}
}
if !matched && is_left {
let mut combined = left.clone();
combined.extend(core::iter::repeat_n(Value::Null, n_jcols));
joined.push(combined);
}
}
Ok((new_columns, joined))
}
fn exec_rowid_join_seek(
&self,
join: &Join,
columns: &[ColumnInfo],
rows: &[Vec<Value>],
outer_col: usize,
inner_meta: &TableMeta,
params: &Params,
) -> Result<(Vec<ColumnInfo>, Vec<Vec<Value>>)> {
let encoding = self.backend.source().header().text_encoding;
let mut new_columns = columns.to_vec();
new_columns.extend(inner_meta.columns.iter().cloned());
let n_jcols = inner_meta.columns.len();
let on = join.on.as_ref();
let is_left = matches!(join.kind, JoinKind::Left);
let mut cur = TableCursor::new(self.backend.source(), inner_meta.root);
let mut joined: Vec<Vec<Value>> = Vec::new();
for left in rows {
let key = &left[outer_col];
let candidate = match key {
Value::Integer(i) => Some(*i),
Value::Real(_) | Value::Text(_) => match eval::to_number(key) {
Value::Integer(i) => Some(i),
Value::Real(r) if r == (r as i64) as f64 => Some(r as i64),
_ => None,
},
Value::Null | Value::Blob(_) => None,
};
let mut matched = false;
if let Some(rid) = candidate {
if cur.seek(rid)? {
let inner = self.decode_full_row(inner_meta, rid, &cur.payload()?, encoding)?;
let mut combined = left.clone();
combined.extend(inner);
let keep = match on {
Some(on) => {
let ctx = row_ctx(&combined, &new_columns, None, params);
eval::truth(&eval::eval(on, &ctx)?) == Some(true)
}
None => true,
};
if keep {
joined.push(combined);
matched = true;
}
}
}
if !matched && is_left {
let mut combined = left.clone();
combined.extend(core::iter::repeat_n(Value::Null, n_jcols));
joined.push(combined);
}
}
Ok((new_columns, joined))
}
fn resolve_join_source(
&self,
tref: &TableRef,
params: &Params,
) -> Result<(Vec<ColumnInfo>, Vec<Vec<Value>>)> {
if tref.tvf_args.is_some() || self.is_pragma_tvf(tref) {
return self.tvf_rows(tref, params);
}
if let Some(sub) = &tref.subquery {
return self.run_subquery_source(sub, tref.alias.as_deref(), params);
}
if let Some((cols, rows)) = self.lookup_cte(&tref.name, tref.alias.as_deref()) {
return Ok((cols, rows.into_iter().map(|r| r.values).collect()));
}
if let Some((cols, rows)) = self.try_view(&tref.name, tref.alias.as_deref(), params)? {
return Ok((cols, rows.into_iter().map(|r| r.values).collect()));
}
if tref.schema.is_none() {
if let Some((cols, rows)) =
self.try_virtual_table(&tref.name, tref.alias.as_deref(), None)?
{
return Ok((cols, rows.into_iter().map(|r| r.values).collect()));
}
}
let db = match tref.schema.as_deref() {
Some(_) => self.resolve_db(tref.schema.as_deref())?,
None => self.unqualified_db(&tref.name),
};
if db != DbRef::Main {
self.guard_qualified_temp(db, tref.schema.as_deref(), &tref.name)?;
if let Some((cols, input)) =
self.scan_db_view(db, &tref.name, tref.alias.as_deref(), params)?
{
return Ok((cols, input.into_iter().map(|r| r.values).collect()));
}
let (cols, input) = self.scan_db_table(db, &tref.name, tref.alias.as_deref())?;
return Ok((cols, input.into_iter().map(|r| r.values).collect()));
}
let meta = self.table_meta(&tref.name, tref.alias.as_deref())?;
let rows = if meta.without_rowid {
self.scan_without_rowid(&meta)?
} else {
self.scan_table(&meta)?
.into_iter()
.map(|(_, v)| v)
.collect()
};
Ok((meta.columns, rows))
}
fn scan_without_rowid(&self, meta: &TableMeta) -> Result<Vec<Vec<Value>>> {
let encoding = self.backend.source().header().text_encoding;
let mut cur = IndexCursor::new(self.backend.source(), meta.root);
let params = Params::default();
let mut out = Vec::new();
while let Some(payload) = cur.next()? {
let storage = decode_record(&payload, encoding)?;
let mut row = unpermute_row(meta, storage);
self.compute_generated(meta, &mut row, ¶ms)?;
out.push(row);
}
Ok(out)
}
fn build_insert_row(
&self,
meta: &TableMeta,
target: &[usize],
row_exprs: &[Expr],
params: &Params,
) -> Result<Vec<Value>> {
let ctx = EvalCtx::rowless(params).with_subqueries(self);
let mut values: Vec<Value> = meta
.defaults
.iter()
.map(|d| match d {
Some(e) => eval::eval(e, &ctx),
None => Ok(Value::Null),
})
.collect::<Result<_>>()?;
for (i, e) in row_exprs.iter().enumerate() {
if meta.is_generated(target[i]) {
return Err(Error::Error(format!(
"cannot INSERT into generated column \"{}\"",
meta.columns[target[i]].name
)));
}
values[target[i]] = eval::eval(e, &ctx)?;
}
apply_column_affinity(meta, &mut values);
self.materialize_generated(meta, &mut values, params)?;
self.check_strict_types(meta, &values)?;
Ok(values)
}
fn exec_insert_without_rowid(
&mut self,
ins: &Insert,
meta: &TableMeta,
rows: &[Vec<Expr>],
is_default_values: bool,
params: &Params,
) -> Result<usize> {
let n_cols = meta.columns.len();
let target: Vec<usize> = if ins.columns.is_empty() {
(0..n_cols).filter(|&i| !meta.is_generated(i)).collect()
} else {
ins.columns
.iter()
.map(|name| {
meta.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(name))
.ok_or_else(|| {
Error::Error(format!("table {} has no column named {name}", ins.table))
})
})
.collect::<Result<_>>()?
};
let pk = &meta.storage_order[..meta.pk_len];
let mut affected = 0;
for row_exprs in rows {
if !is_default_values && row_exprs.len() != target.len() {
return Err(insert_count_mismatch(
&ins.table,
!ins.columns.is_empty(),
target.len(),
row_exprs.len(),
));
}
let values = self.build_insert_row(meta, &target, row_exprs, params)?;
{
let r = (|| {
for &c in pk {
if matches!(values[c], Value::Null) {
return Err(Error::Constraint(format!(
"NOT NULL constraint failed: {}.{}",
meta.columns[c].table, meta.columns[c].name
)));
}
}
check_not_null(meta, &values)?;
self.check_constraints(meta, &values, None, params)
})();
match r {
Ok(()) => {}
Err(Error::Constraint(_)) if ins.on_conflict == OnConflict::Ignore => continue,
Err(e) => return Err(e),
}
}
let existing = self.scan_without_rowid(meta)?;
let mut collide = Vec::new();
for (i, r) in existing.iter().enumerate() {
if unique_match(meta, r, &values)
|| self.wr_index_collision(&ins.table, meta, r, &values, params)?
{
collide.push(i);
}
}
if !collide.is_empty() {
match ins.on_conflict {
oc @ (OnConflict::Abort | OnConflict::Fail | OnConflict::Rollback) => {
let m = wr_unique_message(meta, &existing[collide[0]], &values);
return Err(self.conflict_error(oc, &m));
}
OnConflict::Ignore => continue,
OnConflict::Replace => {
let kept: Vec<Vec<Value>> = existing
.into_iter()
.enumerate()
.filter(|(i, _)| !collide.contains(i))
.map(|(_, r)| r)
.collect();
self.rewrite_without_rowid(meta, kept.into_iter())?;
}
}
}
let record = encode_record(&permute_row(meta, &values));
let scolls = wr_storage_collations(meta);
insert_index(self.backend.writer()?, meta.root, &record, &scolls)?;
affected += 1;
}
if affected > 0 {
self.rebuild_wr_indexes(meta, &ins.table)?;
}
Ok(affected)
}
fn exec_delete_without_rowid(
&mut self,
del: &Delete,
meta: &TableMeta,
params: &Params,
) -> Result<usize> {
let all = self.scan_without_rowid(meta)?;
let mut kept = Vec::new();
let mut deleted = 0;
for row in all {
let keep = match &del.where_clause {
Some(p) => {
let ctx = row_ctx(&row, &meta.columns, None, params).with_subqueries(self);
eval::truth(&eval::eval(p, &ctx)?) != Some(true)
}
None => false,
};
if keep {
kept.push(row);
} else {
deleted += 1;
}
}
if deleted > 0 {
self.rewrite_without_rowid(meta, kept.into_iter())?;
self.rebuild_wr_indexes(meta, &del.table)?;
}
Ok(deleted)
}
fn exec_update_without_rowid(
&mut self,
upd: &Update,
meta: &TableMeta,
params: &Params,
) -> Result<usize> {
let all = self.scan_without_rowid(meta)?;
let mut out = Vec::with_capacity(all.len());
let mut affected = 0;
for mut row in all {
let matches = match &upd.where_clause {
Some(p) => {
let ctx = row_ctx(&row, &meta.columns, None, params).with_subqueries(self);
eval::truth(&eval::eval(p, &ctx)?) == Some(true)
}
None => true,
};
if matches {
let original = row.clone();
for (col, expr) in &upd.assignments {
let pos = meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(col))
.ok_or_else(|| Error::Error(format!("no such column: {col}")))?;
if meta.is_generated(pos) {
return Err(Error::Error(format!(
"cannot UPDATE generated column \"{col}\""
)));
}
let ctx = row_ctx(&original, &meta.columns, None, params).with_subqueries(self);
row[pos] = eval::eval(expr, &ctx)?;
}
if !upd.row_assignments.is_empty() {
let ctx = row_ctx(&original, &meta.columns, None, params).with_subqueries(self);
self.apply_row_subquery_assignments(
&upd.row_assignments,
&meta.columns,
Some(meta),
&ctx,
&mut row,
)?;
}
apply_column_affinity(meta, &mut row);
self.materialize_generated(meta, &mut row, params)?;
check_not_null(meta, &row)?;
self.check_strict_types(meta, &row)?;
self.check_constraints(meta, &row, None, params)?;
affected += 1;
}
out.push(row);
}
for i in 0..out.len() {
for j in (i + 1)..out.len() {
if unique_match(meta, &out[i], &out[j])
|| self.wr_index_collision(&upd.table, meta, &out[i], &out[j], params)?
{
return Err(Error::Constraint(wr_unique_message(meta, &out[i], &out[j])));
}
}
}
if affected > 0 {
self.rewrite_without_rowid(meta, out.into_iter())?;
self.rebuild_wr_indexes(meta, &upd.table)?;
}
Ok(affected)
}
fn rewrite_without_rowid(
&mut self,
meta: &TableMeta,
rows: impl Iterator<Item = Vec<Value>>,
) -> Result<()> {
let records: Vec<Vec<u8>> = rows
.map(|r| encode_record(&permute_row(meta, &r)))
.collect();
let scolls = wr_storage_collations(meta);
let w = self.backend.writer()?;
clear_index(w, meta.root)?;
for rec in &records {
insert_index(w, meta.root, rec, &scolls)?;
}
Ok(())
}
fn rebuild_wr_indexes(&mut self, meta: &TableMeta, table: &str) -> Result<()> {
let indexes = self.indexes_of(table)?;
if indexes.is_empty() {
return Ok(());
}
let rows = self.scan_without_rowid(meta)?;
let pk_cols = meta.storage_order[..meta.pk_len].to_vec();
let pk_colls: Vec<crate::value::Collation> =
pk_cols.iter().map(|&c| meta.columns[c].collation).collect();
let mut keep: Vec<Vec<usize>> = Vec::with_capacity(indexes.len());
for idx in &indexes {
let mut ks = Vec::new();
for (i, row) in rows.iter().enumerate() {
if self.row_in_index(idx, meta, row, None, &Params::default())? {
ks.push(i);
}
}
keep.push(ks);
}
let w = self.backend.writer()?;
for (idx, ks) in indexes.iter().zip(&keep) {
let mut key_colls = idx.collations.clone();
key_colls.extend(pk_colls.iter().copied());
clear_index(w, idx.root)?;
for &i in ks {
insert_index(
w,
idx.root,
&wr_index_key(&idx.cols, &pk_cols, &rows[i]),
&key_colls,
)?;
}
}
Ok(())
}
fn table_is_empty(&self, table: &str) -> Result<bool> {
let meta = self.table_meta(table, None)?;
if meta.without_rowid {
Ok(self.scan_without_rowid(&meta)?.is_empty())
} else {
Ok(self.scan_table(&meta)?.is_empty())
}
}
fn resolve_db(&self, schema: Option<&str>) -> Result<DbRef> {
match schema {
None => Ok(DbRef::Main),
Some(s) if s.eq_ignore_ascii_case("main") => Ok(DbRef::Main),
Some(s) if s.eq_ignore_ascii_case("temp") || s.eq_ignore_ascii_case("temporary") => {
Ok(DbRef::Temp)
}
Some(s) => self
.attached
.iter()
.position(|d| d.name.eq_ignore_ascii_case(s))
.map(DbRef::Attached)
.ok_or_else(|| Error::Error(alloc::format!("unknown database {s}"))),
}
}
fn guard_qualified_temp(&self, db: DbRef, qualifier: Option<&str>, name: &str) -> Result<()> {
if db == DbRef::Temp && self.temp_db.is_none() {
return Err(Error::Error(alloc::format!(
"no such table: {}.{}",
qualifier.unwrap_or("temp"),
name
)));
}
Ok(())
}
fn db_parts(&self, db: DbRef) -> (&Schema, &Backend) {
match db {
DbRef::Main => (&self.schema, &self.backend),
DbRef::Temp => {
let t = self.temp_db.as_ref().expect("temp db exists");
(&t.schema, &t.backend)
}
DbRef::Attached(i) => (&self.attached[i].schema, &self.attached[i].backend),
}
}
fn unqualified_db(&self, name: &str) -> DbRef {
if let Some(t) = &self.temp_db {
if t.schema.table(name).is_some() {
return DbRef::Temp;
}
}
let def = self.read_default.get();
if def != DbRef::Main {
let (schema, _) = self.db_parts(def);
if schema.table(name).is_some() {
return def;
}
}
if self.schema.table(name).is_some() {
return DbRef::Main;
}
for (i, d) in self.attached.iter().enumerate() {
if d.schema.table(name).is_some() {
return DbRef::Attached(i);
}
}
DbRef::Main
}
fn ensure_temp(&mut self) -> Result<()> {
if self.temp_db.is_some() {
return Ok(());
}
let vfs = crate::vfs::memory::MemoryVfs::new();
let f = vfs.open("temp", OpenFlags::READ_WRITE_CREATE)?;
let mut db = WritePager::create(f, None, 4096)?;
db.commit()?;
let backend = Backend::Write(Box::new(db));
let schema = Schema::read(backend.source())?;
self.temp_db = Some(AttachedDb {
name: "temp".into(),
file: String::new(),
backend,
schema,
});
Ok(())
}
fn scan_db_table(
&self,
db: DbRef,
name: &str,
alias: Option<&str>,
) -> Result<(Vec<ColumnInfo>, Vec<InputRow>)> {
let (schema, backend) = self.db_parts(db);
let meta = self.table_meta_in(schema, name, alias)?;
let source = backend.source();
let encoding = source.header().text_encoding;
if meta.without_rowid {
let params = Params::default();
let mut rows = Vec::new();
let mut cur = IndexCursor::new(source, meta.root);
while let Some(payload) = cur.next()? {
let storage = decode_record(&payload, encoding)?;
let mut values = unpermute_row(&meta, storage);
self.compute_generated(&meta, &mut values, ¶ms)?;
rows.push(InputRow {
values,
rowid: None,
});
}
return Ok((meta.columns, rows));
}
let mut rows = Vec::new();
let mut cur = TableCursor::new(source, meta.root);
let mut ok = cur.first()?;
while ok {
let rowid = cur.rowid()?;
let values = self.decode_full_row(&meta, rowid, &cur.payload()?, encoding)?;
rows.push(InputRow {
values,
rowid: Some(rowid),
});
ok = cur.next()?;
}
Ok((meta.columns, rows))
}
fn scan_db_view(
&self,
db: DbRef,
name: &str,
alias: Option<&str>,
params: &Params,
) -> Result<Option<(Vec<ColumnInfo>, Vec<InputRow>)>> {
use crate::schema::ObjectType;
let (schema, _) = self.db_parts(db);
let obj = match schema
.objects()
.iter()
.find(|o| o.obj_type == ObjectType::View && o.name.eq_ignore_ascii_case(name))
{
Some(o) => o.clone(),
None => return Ok(None),
};
let sql = obj
.sql
.as_deref()
.ok_or_else(|| Error::Corrupt("view has no CREATE statement".into()))?;
let Statement::CreateView(cv) = sql::parse_one(sql)? else {
return Err(Error::Corrupt("schema sql is not CREATE VIEW".into()));
};
let prev = self.read_default.get();
self.read_default.set(db);
let run = self.run_select(&cv.select, params);
self.read_default.set(prev);
let result = run?;
let label = alias.unwrap_or(name).to_string();
let names = if cv.columns.is_empty() {
result.columns.clone()
} else {
cv.columns.clone()
};
let columns: Vec<ColumnInfo> = names
.into_iter()
.map(|n| ColumnInfo {
name: n,
table: label.clone(),
affinity: eval::Affinity::Blob,
collation: crate::value::Collation::default(),
})
.collect();
let rows = result
.rows
.into_iter()
.map(|values| InputRow {
values,
rowid: None,
})
.collect();
Ok(Some((columns, rows)))
}
fn scan_dbpage(
&self,
src: &dyn PageSource,
alias: Option<&str>,
) -> Result<(Vec<ColumnInfo>, Vec<InputRow>)> {
use eval::Affinity::{Blob, Integer};
let label = alias.unwrap_or("sqlite_dbpage").to_string();
let col = |name: &str, affinity| ColumnInfo {
name: String::from(name),
table: label.clone(),
affinity,
collation: crate::value::Collation::default(),
};
let columns = alloc::vec![col("pgno", Integer), col("data", Blob)];
let count = src.page_count();
let mut rows: Vec<InputRow> = Vec::with_capacity(count as usize);
for pgno in 1..=count {
let page = src.page(pgno)?;
rows.push(InputRow {
values: alloc::vec![
Value::Integer(pgno as i64),
Value::Blob(page.data().to_vec()),
],
rowid: Some(pgno as i64),
});
}
Ok((columns, rows))
}
fn scan_dbstat(
&self,
schema: &Schema,
src: &dyn PageSource,
alias: Option<&str>,
) -> Result<(Vec<ColumnInfo>, Vec<InputRow>)> {
use crate::btree::page::{BtreePage, PageType};
use eval::Affinity::{Integer, Text};
let label = alias.unwrap_or("dbstat").to_string();
let col = |name: &str, affinity| ColumnInfo {
name: String::from(name),
table: label.clone(),
affinity,
collation: crate::value::Collation::default(),
};
let columns = alloc::vec![
col("name", Text),
col("path", Text),
col("pageno", Integer),
col("pagetype", Text),
col("ncell", Integer),
col("payload", Integer),
col("unused", Integer),
col("mx_payload", Integer),
col("pgoffset", Integer),
col("pgsize", Integer),
];
let usable = src.usable_size();
let page_size = src.header().page_size as i64;
let be16 = |d: &[u8], off: usize| u16::from_be_bytes([d[off], d[off + 1]]) as usize;
let mut btrees: Vec<(String, u32)> = alloc::vec![(String::from("sqlite_schema"), 1)];
for obj in schema.objects() {
if obj.rootpage != 0 {
btrees.push((obj.name.clone(), obj.rootpage));
}
}
let mut rows: Vec<InputRow> = Vec::new();
for (name, root) in btrees {
let mut stack = alloc::vec![(root, String::from("/"))];
while let Some((pgno, path)) = stack.pop() {
let page = src.page(pgno)?;
let bp = BtreePage::parse(page)?;
let data = bp.data();
let body = if pgno == 1 { 100 } else { 0 };
let ncell = bp.num_cells();
let is_leaf = bp.page_type().is_leaf();
let nhdr = body + if is_leaf { 8 } else { 12 };
let ptype = if is_leaf { "leaf" } else { "internal" };
let mut payload = 0i64;
let mut mx = 0i64;
let mut prev_pgno = pgno;
for i in 0..ncell {
let pl = match bp.page_type() {
PageType::LeafTable => bp.table_leaf_cell(i, usable)?.payload,
PageType::LeafIndex | PageType::InteriorIndex => {
bp.index_cell(i, usable)?.payload
}
PageType::InteriorTable => continue,
};
payload += pl.local_len as i64;
mx = mx.max(pl.total_len as i64);
let mut ovfl = pl.overflow;
let mut remaining = pl.total_len - pl.local_len;
let mut iovfl = 0usize;
while ovfl != 0 {
let opage = src.page(ovfl)?;
let odata = opage.data();
let next = u32::from_be_bytes([odata[0], odata[1], odata[2], odata[3]]);
let cap = usable - 4;
let (opayload, ounused) = if remaining <= cap {
(remaining as i64, (cap - remaining) as i64)
} else {
(cap as i64, 0)
};
rows.push(InputRow {
values: alloc::vec![
Value::Text(name.clone()),
Value::Text(alloc::format!("{path}{i:03x}+{iovfl:06x}")),
Value::Integer(ovfl as i64),
Value::Text(String::from("overflow")),
Value::Integer(0),
Value::Integer(opayload),
Value::Integer(ounused),
Value::Integer(0),
Value::Integer((prev_pgno as i64 - 1) * page_size),
Value::Integer(page_size),
],
rowid: None,
});
remaining = remaining.saturating_sub(cap);
iovfl += 1;
prev_pgno = ovfl;
ovfl = next;
}
}
let cc = match be16(data, body + 5) {
0 => 65536,
n => n,
};
let mut unused = cc as i64 - nhdr as i64 - 2 * ncell as i64 + data[body + 7] as i64;
let mut fb = be16(data, body + 1);
while fb != 0 && fb + 4 <= data.len() {
unused += be16(data, fb + 2) as i64;
fb = be16(data, fb);
}
rows.push(InputRow {
values: alloc::vec![
Value::Text(name.clone()),
Value::Text(path.clone()),
Value::Integer(pgno as i64),
Value::Text(String::from(ptype)),
Value::Integer(ncell as i64),
Value::Integer(payload),
Value::Integer(unused),
Value::Integer(mx),
Value::Integer((pgno as i64 - 1) * page_size),
Value::Integer(page_size),
],
rowid: None,
});
if !is_leaf {
for i in 0..=ncell {
let child = bp.child_pointer(i)?;
if child != 0 {
stack.push((child, alloc::format!("{path}{i:03x}/")));
}
}
}
}
}
Ok((columns, rows))
}
#[cfg(feature = "fts5")]
fn scan_fts5vocab(
&self,
args: &[String],
vocab_name: &str,
alias: Option<&str>,
) -> Result<(Vec<ColumnInfo>, Vec<InputRow>)> {
use alloc::collections::{BTreeMap, BTreeSet};
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let (ft_name, form) = crate::vtab::fts5vocab_args(&arg_refs)?;
let label = alias.unwrap_or(vocab_name).to_string();
let colnames: &[&str] = match form.as_str() {
"row" => &["term", "doc", "cnt"],
"col" => &["term", "col", "doc", "cnt"],
_ => &["term", "doc", "col", "offset"],
};
let columns: Vec<ColumnInfo> = colnames
.iter()
.map(|n| ColumnInfo {
name: String::from(*n),
table: label.clone(),
affinity: eval::Affinity::Blob,
collation: crate::value::Collation::default(),
})
.collect();
let (ft_module, ft_args, ft_schema) = self.vtab_meta(&ft_name)?;
if !ft_module.eq_ignore_ascii_case("fts5") {
return Err(Error::Error(format!("no such fts5 table: {ft_name}")));
}
let ft_cols = ft_schema.columns;
let ft_refs: Vec<&str> = ft_args.iter().map(String::as_str).collect();
let ft_tok = crate::vtab::fts5_tok_config(&ft_refs);
let bmeta = self.table_meta(&format!("{ft_name}_content"), None)?;
let docs: Vec<(i64, Vec<Value>)> = self
.scan_table(&bmeta)?
.into_iter()
.map(|(rowid, mut vals)| {
if !vals.is_empty() {
vals.remove(0);
}
(rowid, vals)
})
.collect();
let to_text = |v: &Value| -> Option<String> {
match v {
Value::Text(s) => Some(s.clone()),
Value::Integer(i) => Some(i.to_string()),
Value::Real(r) => Some(eval::format_real(*r)),
Value::Null | Value::Blob(_) => None,
}
};
let mut rows: Vec<InputRow> = Vec::new();
match form.as_str() {
"row" => {
let mut map: BTreeMap<String, (BTreeSet<i64>, i64)> = BTreeMap::new();
for (rowid, vals) in &docs {
for v in vals.iter().take(ft_cols.len()) {
if let Some(t) = to_text(v) {
for tok in crate::vtab::fts5_tokenize(&t, ft_tok) {
let e = map.entry(tok).or_default();
e.0.insert(*rowid);
e.1 += 1;
}
}
}
}
for (term, (ds, cnt)) in map {
rows.push(InputRow {
values: alloc::vec![
Value::Text(term),
Value::Integer(ds.len() as i64),
Value::Integer(cnt),
],
rowid: None,
});
}
}
"col" => {
let mut map: BTreeMap<(String, usize), (BTreeSet<i64>, i64)> = BTreeMap::new();
for (rowid, vals) in &docs {
for (ci, v) in vals.iter().take(ft_cols.len()).enumerate() {
if let Some(t) = to_text(v) {
for tok in crate::vtab::fts5_tokenize(&t, ft_tok) {
let e = map.entry((tok, ci)).or_default();
e.0.insert(*rowid);
e.1 += 1;
}
}
}
}
for ((term, ci), (ds, cnt)) in map {
rows.push(InputRow {
values: alloc::vec![
Value::Text(term),
Value::Text(ft_cols[ci].clone()),
Value::Integer(ds.len() as i64),
Value::Integer(cnt),
],
rowid: None,
});
}
}
_ => {
let mut insts: Vec<(String, i64, usize, i64)> = Vec::new();
for (rowid, vals) in &docs {
for (ci, v) in vals.iter().take(ft_cols.len()).enumerate() {
if let Some(t) = to_text(v) {
for (off, tok) in crate::vtab::fts5_tokenize(&t, ft_tok)
.into_iter()
.enumerate()
{
insts.push((tok, *rowid, ci, off as i64));
}
}
}
}
insts.sort();
for (term, rowid, ci, off) in insts {
rows.push(InputRow {
values: alloc::vec![
Value::Text(term),
Value::Integer(rowid),
Value::Text(ft_cols[ci].clone()),
Value::Integer(off),
],
rowid: None,
});
}
}
}
Ok((columns, rows))
}
fn scan_rtree_nodes(
&self,
name: &str,
n_coords: usize,
integer: bool,
bbox: &[(usize, ConstraintOp, f64)],
) -> Result<Vec<InputRow>> {
use alloc::collections::BTreeMap;
let node_meta = self.table_meta(&format!("{name}_node"), None)?;
let mut nodes: BTreeMap<i64, Vec<u8>> = BTreeMap::new();
for (nodeno, vals) in self.scan_table(&node_meta)? {
if let Some(Value::Blob(b)) = vals.into_iter().find(|v| matches!(v, Value::Blob(_))) {
nodes.insert(nodeno, b);
}
}
let cell_size = 8 + n_coords * 4;
let coord_at = |blob: &[u8], off: usize, j: usize| -> f64 {
let p = off + 8 + j * 4;
let b: [u8; 4] = blob[p..p + 4].try_into().expect("4 bytes");
if integer {
f64::from(i32::from_be_bytes(b))
} else {
f64::from(f32::from_be_bytes(b))
}
};
let subtree_matches = |blob: &[u8], off: usize| -> bool {
bbox.iter().all(|&(ci, op, v)| {
let d = ci / 2;
let lo = coord_at(blob, off, 2 * d);
let hi = coord_at(blob, off, 2 * d + 1);
match op {
ConstraintOp::Ge => hi >= v,
ConstraintOp::Gt => hi > v,
ConstraintOp::Le => lo <= v,
ConstraintOp::Lt => lo < v,
ConstraintOp::Eq => lo <= v && v <= hi,
_ => true,
}
})
};
let mut out = Vec::new();
let Some(root) = nodes.get(&1) else {
return Ok(out);
};
if root.len() < 4 {
return Ok(out);
}
let depth = i64::from(u16::from_be_bytes([root[0], root[1]]));
let mut stack: Vec<(i64, i64)> = alloc::vec![(1, depth)];
while let Some((nodeno, level)) = stack.pop() {
let Some(blob) = nodes.get(&nodeno) else {
continue;
};
if blob.len() < 4 {
continue;
}
let ncell = u16::from_be_bytes([blob[2], blob[3]]) as usize;
for i in 0..ncell {
let off = 4 + i * cell_size;
if off + cell_size > blob.len() {
break;
}
let key = i64::from_be_bytes(blob[off..off + 8].try_into().expect("8 bytes"));
if level > 0 {
if !bbox.is_empty() && !subtree_matches(blob, off) {
continue;
}
stack.push((key, level - 1));
continue;
}
let mut row = Vec::with_capacity(1 + n_coords);
row.push(Value::Integer(key));
for c in 0..n_coords {
let p = off + 8 + c * 4;
let b: [u8; 4] = blob[p..p + 4].try_into().expect("4 bytes");
row.push(if integer {
Value::Integer(i64::from(i32::from_be_bytes(b)))
} else {
Value::Real(f64::from(f32::from_be_bytes(b)))
});
}
out.push(InputRow {
values: row,
rowid: Some(key),
});
}
}
Ok(out)
}
fn rtree_node_size_for(&self, n_coord: usize) -> usize {
rtree_node_size(n_coord, self.backend.source().header().page_size as usize)
}
fn rtree_entries(&self, name: &str, n_coord: usize, integer: bool) -> Result<Vec<RtreeCell>> {
Ok(self
.scan_rtree_nodes(name, n_coord, integer, &[])?
.into_iter()
.map(|r| {
let key = match r.values.first() {
Some(Value::Integer(i)) => *i,
_ => 0,
};
let coords = r.values[1..1 + n_coord]
.iter()
.map(|v| match v {
Value::Integer(i) => *i as f64,
Value::Real(f) => *f,
_ => 0.0,
})
.collect();
RtreeCell { key, coords }
})
.collect())
}
fn rtree_write_build(&mut self, name: &str, build: &RtreeBuild) -> Result<()> {
let node_t = sql::print::ident(&format!("{name}_node"));
let rowid_t = sql::print::ident(&format!("{name}_rowid"));
let parent_t = sql::print::ident(&format!("{name}_parent"));
let pv = |vals: Vec<Value>| Params {
positional: vals,
named: Vec::new(),
};
self.execute(&format!("DELETE FROM {node_t}"))?;
self.execute(&format!("DELETE FROM {rowid_t}"))?;
self.execute(&format!("DELETE FROM {parent_t}"))?;
for (nodeno, blob) in &build.nodes {
self.execute_params(
&format!("INSERT INTO {node_t} VALUES(?1,?2)"),
&pv(alloc::vec![
Value::Integer(*nodeno),
Value::Blob(blob.clone())
]),
)?;
}
for (rowid, nodeno) in &build.rowids {
self.execute_params(
&format!("INSERT INTO {rowid_t} VALUES(?1,?2)"),
&pv(alloc::vec![Value::Integer(*rowid), Value::Integer(*nodeno)]),
)?;
}
for (child, parent) in &build.parents {
self.execute_params(
&format!("INSERT INTO {parent_t} VALUES(?1,?2)"),
&pv(alloc::vec![Value::Integer(*child), Value::Integer(*parent)]),
)?;
}
Ok(())
}
fn rtree_create_storage(&mut self, name: &str, n_coord: usize, integer: bool) -> Result<()> {
for (suffix, cols) in [
("_node", "nodeno INTEGER PRIMARY KEY, data"),
("_rowid", "rowid INTEGER PRIMARY KEY, nodeno"),
("_parent", "nodeno INTEGER PRIMARY KEY, parentnode"),
] {
let sql = format!(
"CREATE TABLE {}({cols})",
sql::print::ident(&format!("{name}{suffix}"))
);
let Statement::CreateTable(ct) = sql::parse_one(&sql)? else {
unreachable!("constructed a CREATE TABLE")
};
self.exec_create_table(&ct, &sql)?;
}
let build = rtree_bulk_build(
Vec::new(),
n_coord,
integer,
self.rtree_node_size_for(n_coord),
);
self.rtree_write_build(name, &build)
}
fn rtree_apply(
&mut self,
name: &str,
n_coord: usize,
integer: bool,
inserts: Vec<RtreeCell>,
deletes: &[i64],
) -> Result<()> {
let mut entries = self.rtree_entries(name, n_coord, integer)?;
let removed: alloc::collections::BTreeSet<i64> = deletes
.iter()
.copied()
.chain(inserts.iter().map(|c| c.key))
.collect();
entries.retain(|c| !removed.contains(&c.key));
entries.extend(inserts);
let build = rtree_bulk_build(entries, n_coord, integer, self.rtree_node_size_for(n_coord));
self.rtree_write_build(name, &build)
}
fn fts5_maybe_rebuild(&mut self, module_name: &str, table: &str) -> Result<()> {
#[cfg(feature = "fts5")]
if module_name.eq_ignore_ascii_case("fts5") {
return self.fts5_rebuild_index(table);
}
let _ = (module_name, table);
Ok(())
}
#[cfg(feature = "fts5")]
fn fts5_create_storage(&mut self, name: &str, ncols: usize) -> Result<()> {
let content_cols: Vec<String> = (0..ncols).map(|c| format!("c{c}")).collect();
let q = |s: &str| sql::print::ident(s);
let defs = [
(
format!("{name}_content"),
format!("id INTEGER PRIMARY KEY, {}", content_cols.join(", ")),
"",
),
(
format!("{name}_docsize"),
"id INTEGER PRIMARY KEY, sz BLOB".to_string(),
"",
),
(
format!("{name}_config"),
"k PRIMARY KEY, v".to_string(),
" WITHOUT ROWID",
),
(
format!("{name}_idx"),
"segid, term, pgno, PRIMARY KEY(segid, term)".to_string(),
" WITHOUT ROWID",
),
(
format!("{name}_data"),
"id INTEGER PRIMARY KEY, block BLOB".to_string(),
"",
),
];
for (tname, cols, tail) in &defs {
let sql = format!("CREATE TABLE {}({cols}){tail}", q(tname));
let Statement::CreateTable(ct) = sql::parse_one(&sql)? else {
unreachable!("constructed a CREATE TABLE")
};
self.exec_create_table(&ct, &sql)?;
}
self.execute_params(
&format!(
"INSERT INTO {} VALUES('version', 4)",
q(&format!("{name}_config"))
),
&Params::default(),
)?;
let seg = crate::fts5_index::build_segment(&[], 0, &alloc::vec![0u64; ncols], &[], 4050, 0);
let data_t = q(&format!("{name}_data"));
for (id, block) in &seg.data {
self.execute_params(
&format!("INSERT INTO {data_t} VALUES(?1,?2)"),
&Params {
positional: alloc::vec![Value::Integer(*id), Value::Blob(block.clone())],
named: Vec::new(),
},
)?;
}
Ok(())
}
#[cfg(feature = "fts5")]
fn fts5_rebuild_index(&mut self, name: &str) -> Result<()> {
use crate::fts5_index::{self, IdxRow, Posting};
use alloc::collections::BTreeMap;
let (_module, args, schema) = self.vtab_meta(name)?;
let ncols = schema.columns.len();
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
let tok = crate::vtab::fts5_tok_config(&arg_refs);
let cmeta = self.table_meta(&format!("{name}_content"), None)?;
let docs = self.scan_table(&cmeta)?;
let mut index: BTreeMap<Vec<u8>, BTreeMap<i64, Vec<Vec<u32>>>> = BTreeMap::new();
let mut col_totals = alloc::vec![0u64; ncols];
let mut doc_sizes: Vec<(i64, Vec<u64>)> = Vec::new();
for (rowid, values) in &docs {
let mut sizes = alloc::vec![0u64; ncols];
for c in 0..ncols {
let text = match values.get(c + 1) {
Some(v) if !matches!(v, Value::Null) => eval::to_text(v),
_ => String::new(),
};
let toks = crate::vtab::fts5_tokenize(&text, tok);
sizes[c] = toks.len() as u64;
col_totals[c] += toks.len() as u64;
for (pos, tok) in toks.iter().enumerate() {
index
.entry(tok.as_bytes().to_vec())
.or_default()
.entry(*rowid)
.or_insert_with(|| alloc::vec![Vec::new(); ncols])[c]
.push(pos as u32);
}
}
doc_sizes.push((*rowid, sizes));
}
let terms: Vec<(Vec<u8>, Vec<Posting>)> = index
.into_iter()
.map(|(term, per_doc)| {
let postings = per_doc
.into_iter()
.map(|(rowid, cols)| Posting { rowid, cols })
.collect();
(term, postings)
})
.collect();
let seg =
fts5_index::build_segment(&terms, docs.len() as u64, &col_totals, &doc_sizes, 4050, 0);
let q = |s: &str| sql::print::ident(s);
let pv = |vals: Vec<Value>| Params {
positional: vals,
named: Vec::new(),
};
self.execute(&format!("DELETE FROM {}", q(&format!("{name}_data"))))?;
self.execute(&format!("DELETE FROM {}", q(&format!("{name}_idx"))))?;
self.execute(&format!("DELETE FROM {}", q(&format!("{name}_docsize"))))?;
let data_t = q(&format!("{name}_data"));
for (id, block) in &seg.data {
self.execute_params(
&format!("INSERT INTO {data_t} VALUES(?1,?2)"),
&pv(alloc::vec![Value::Integer(*id), Value::Blob(block.clone())]),
)?;
}
let idx_t = q(&format!("{name}_idx"));
for IdxRow { segid, term, pgno } in &seg.idx {
self.execute_params(
&format!("INSERT INTO {idx_t} VALUES(?1,?2,?3)"),
&pv(alloc::vec![
Value::Integer(*segid),
Value::Blob(term.clone()),
Value::Integer(*pgno)
]),
)?;
}
let docsize_t = q(&format!("{name}_docsize"));
for (rowid, sz) in &seg.docsize {
self.execute_params(
&format!("INSERT INTO {docsize_t} VALUES(?1,?2)"),
&pv(alloc::vec![Value::Integer(*rowid), Value::Blob(sz.clone())]),
)?;
}
Ok(())
}
fn scan_table(&self, meta: &TableMeta) -> Result<Vec<(i64, Vec<Value>)>> {
let encoding = self.backend.source().header().text_encoding;
let mut rows = Vec::new();
let mut cur = TableCursor::new(self.backend.source(), meta.root);
let mut ok = cur.first()?;
while ok {
let rowid = cur.rowid()?;
let values = self.decode_full_row(meta, rowid, &cur.payload()?, encoding)?;
rows.push((rowid, values));
ok = cur.next()?;
}
Ok(rows)
}
fn decode_full_row(
&self,
meta: &TableMeta,
rowid: i64,
payload: &[u8],
encoding: crate::format::TextEncoding,
) -> Result<Vec<Value>> {
let record = decode_record(payload, encoding)?;
let n = meta.columns.len();
let mut values = alloc::vec![Value::Null; n];
let p = Params::default();
let mut ri = 0usize;
for (i, def) in meta.defaults.iter().enumerate() {
if meta.is_virtual(i) {
continue;
}
if ri < record.len() {
values[i] = record[ri].clone();
} else if let Some(e) = def {
values[i] = eval::eval(e, &EvalCtx::rowless(&p))?;
}
ri += 1;
}
if let Some(ipk) = meta.ipk {
values[ipk] = Value::Integer(rowid);
}
self.compute_generated(meta, &mut values, &p)?;
Ok(values)
}
fn compute_generated(
&self,
meta: &TableMeta,
values: &mut [Value],
params: &Params,
) -> Result<()> {
if meta.generated.iter().all(|g| g.is_none()) {
return Ok(());
}
for i in 0..meta.columns.len() {
if let Some((expr, stored)) = &meta.generated[i] {
if !stored {
let ctx = row_ctx(values, &meta.columns, None, params).with_subqueries(self);
let v = eval::eval(expr, &ctx)?;
values[i] = meta.columns[i].affinity.coerce(v);
}
}
}
Ok(())
}
fn materialize_generated(
&self,
meta: &TableMeta,
values: &mut [Value],
params: &Params,
) -> Result<()> {
if meta.generated.iter().all(|g| g.is_none()) {
return Ok(());
}
for i in 0..meta.columns.len() {
if let Some((expr, _)) = &meta.generated[i] {
let ctx = row_ctx(values, &meta.columns, None, params).with_subqueries(self);
let v = eval::eval(expr, &ctx)?;
values[i] = meta.columns[i].affinity.coerce(v);
}
}
Ok(())
}
fn encode_table_record(&self, meta: &TableMeta, values: &[Value]) -> Vec<u8> {
let stored: Vec<Value> = (0..meta.columns.len())
.filter(|&i| !meta.is_virtual(i))
.map(|i| {
if Some(i) == meta.ipk {
Value::Null
} else {
values[i].clone()
}
})
.collect();
encode_record(&stored)
}
fn eval_simple(
&self,
sel: &Select,
columns: &[ColumnInfo],
rows: Vec<InputRow>,
params: &Params,
) -> Result<(Vec<String>, Vec<OutRow>)> {
let labels = self.output_labels(sel, columns);
let mut out = Vec::with_capacity(rows.len());
for r in &rows {
let ctx = r.ctx(columns, params).with_subqueries(self);
let mut values = Vec::new();
for col in &sel.columns {
project_column(col, columns, &ctx, &mut values)?;
}
let mut sort_keys = Vec::new();
for term in &sel.order_by {
match resolve_order_index(&term.expr, &labels, values.len()) {
Some(idx) => sort_keys.push(values[idx].clone()),
None => sort_keys.push(eval::eval(&term.expr, &ctx)?),
}
}
out.push(OutRow { values, sort_keys });
}
Ok((labels, out))
}
fn eval_aggregated(
&self,
sel: &Select,
columns: &[ColumnInfo],
rows: Vec<InputRow>,
params: &Params,
) -> Result<(Vec<String>, Vec<OutRow>)> {
let expanded;
let sel = if sel
.columns
.iter()
.any(|c| matches!(c, ResultColumn::Wildcard | ResultColumn::TableWildcard(_)))
{
expanded = expand_agg_wildcards(sel, columns);
&expanded
} else {
sel
};
let group_by: Vec<Expr> = sel
.group_by
.iter()
.map(|g| {
if let Expr::Literal(Literal::Integer(n)) = g {
if *n >= 1 {
if let Some(ResultColumn::Expr { expr, .. }) =
sel.columns.get((*n - 1) as usize)
{
return expr.clone();
}
}
}
g.clone()
})
.collect();
let group_colls: Vec<crate::value::Collation> = {
let cctx = row_ctx(&[], columns, None, params);
group_by
.iter()
.map(|g| eval::key_collation(g, &cctx))
.collect()
};
let mut group_keys: Vec<Vec<Value>> = Vec::new();
let mut groups: Vec<Vec<usize>> = Vec::new();
for (i, r) in rows.iter().enumerate() {
let ctx = r.ctx(columns, params).with_subqueries(self);
let mut key = Vec::new();
for g in &group_by {
key.push(eval::eval(g, &ctx)?);
}
match group_keys
.iter()
.position(|k| rows_equal_coll(k, &key, &group_colls))
{
Some(idx) => groups[idx].push(i),
None => {
group_keys.push(key);
groups.push(alloc::vec![i]);
}
}
}
if sel.group_by.is_empty() {
groups = alloc::vec![(0..rows.len()).collect()];
} else {
let mut order: Vec<usize> = (0..groups.len()).collect();
order.sort_by(|&i, &j| {
for (k, coll) in group_colls.iter().enumerate() {
let ord =
crate::value::cmp_values_coll(&group_keys[i][k], &group_keys[j][k], *coll);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
});
let mut sorted = Vec::with_capacity(groups.len());
for i in order {
sorted.push(core::mem::take(&mut groups[i]));
}
groups = sorted;
}
let labels = self.output_labels(sel, columns);
let minmax = single_minmax_arg(sel);
let mut out = Vec::new();
for group in &groups {
let repr_idx = match &minmax {
Some((is_max, arg)) => {
self.argextreme_row(group, columns, &rows, arg, *is_max, params)?
}
None => group.first().copied(),
};
let repr = repr_idx.map(|i| &rows[i]);
let empty = InputRow {
values: alloc::vec![Value::Null; columns.len()],
rowid: None,
};
let repr_ctx = repr
.unwrap_or(&empty)
.ctx(columns, params)
.with_subqueries(self);
let mut values = Vec::new();
for col in &sel.columns {
let ResultColumn::Expr { expr, .. } = col else {
unreachable!("wildcards rejected above")
};
let substituted =
self.substitute_aggregates(expr, columns, &rows, group, params)?;
values.push(eval::eval(&substituted, &repr_ctx)?);
}
if let Some(having) = &sel.having {
let h = self.substitute_aggregates(having, columns, &rows, group, params)?;
let mut aug_cols = columns.to_vec();
for label in &labels {
aug_cols.push(ColumnInfo {
name: label.clone(),
table: String::new(),
affinity: eval::Affinity::Blob,
collation: crate::value::Collation::Binary,
});
}
let mut aug_vals = repr.unwrap_or(&empty).values.clone();
aug_vals.extend(values.iter().cloned());
let aug_row = InputRow {
values: aug_vals,
rowid: repr.and_then(|r| r.rowid),
};
let actx = aug_row.ctx(&aug_cols, params).with_subqueries(self);
if eval::truth(&eval::eval(&h, &actx)?) != Some(true) {
continue;
}
}
let mut sort_keys = Vec::new();
for term in &sel.order_by {
if let Some(idx) = resolve_order_index(&term.expr, &labels, values.len()) {
sort_keys.push(values[idx].clone());
} else {
let s =
self.substitute_aggregates(&term.expr, columns, &rows, group, params)?;
sort_keys.push(eval::eval(&s, &repr_ctx)?);
}
}
out.push(OutRow { values, sort_keys });
}
Ok((labels, out))
}
fn extract_aggregates(&self, e: &Expr, aggs: &mut Vec<Expr>) -> Expr {
let is_agg = matches!(e, Expr::Function { name, args, star, over: None, .. }
if func::is_aggregate_call(name, args.len(), *star)
|| self.aggregates.contains_key(&name.to_ascii_lowercase()));
if is_agg {
let idx = aggs.len();
aggs.push(e.clone());
return Expr::Column {
table: None,
column: alloc::format!("__agg{idx}"),
};
}
match e {
Expr::Function {
name,
distinct,
args,
star,
filter,
order_by,
over,
} => {
let new_args = args
.iter()
.map(|a| self.extract_aggregates(a, aggs))
.collect();
let new_filter = filter
.as_ref()
.map(|f| Box::new(self.extract_aggregates(f, aggs)));
let new_over = over.as_ref().map(|spec| {
let mut s = spec.clone();
s.partition_by = spec
.partition_by
.iter()
.map(|p| self.extract_aggregates(p, aggs))
.collect();
s.order_by = spec
.order_by
.iter()
.map(|t| OrderTerm {
expr: self.extract_aggregates(&t.expr, aggs),
descending: t.descending,
nulls_first: t.nulls_first,
})
.collect();
s
});
Expr::Function {
name: name.clone(),
distinct: *distinct,
args: new_args,
star: *star,
filter: new_filter,
order_by: order_by.clone(),
over: new_over,
}
}
Expr::Binary { op, left, right } => Expr::Binary {
op: *op,
left: Box::new(self.extract_aggregates(left, aggs)),
right: Box::new(self.extract_aggregates(right, aggs)),
},
Expr::Unary { op, expr } => Expr::Unary {
op: *op,
expr: Box::new(self.extract_aggregates(expr, aggs)),
},
Expr::Paren(x) => Expr::Paren(Box::new(self.extract_aggregates(x, aggs))),
Expr::Cast { expr, type_name } => Expr::Cast {
expr: Box::new(self.extract_aggregates(expr, aggs)),
type_name: type_name.clone(),
},
Expr::Collate { expr, collation } => Expr::Collate {
expr: Box::new(self.extract_aggregates(expr, aggs)),
collation: collation.clone(),
},
Expr::IsNull { expr, negated } => Expr::IsNull {
expr: Box::new(self.extract_aggregates(expr, aggs)),
negated: *negated,
},
Expr::Between {
expr,
low,
high,
negated,
} => Expr::Between {
expr: Box::new(self.extract_aggregates(expr, aggs)),
low: Box::new(self.extract_aggregates(low, aggs)),
high: Box::new(self.extract_aggregates(high, aggs)),
negated: *negated,
},
Expr::InList {
expr,
list,
negated,
candidate_affinity,
} => Expr::InList {
expr: Box::new(self.extract_aggregates(expr, aggs)),
list: list
.iter()
.map(|x| self.extract_aggregates(x, aggs))
.collect(),
negated: *negated,
candidate_affinity: candidate_affinity.clone(),
},
Expr::Case {
operand,
when_then,
else_result,
} => Expr::Case {
operand: operand
.as_ref()
.map(|o| Box::new(self.extract_aggregates(o, aggs))),
when_then: when_then
.iter()
.map(|(w, t)| {
(
self.extract_aggregates(w, aggs),
self.extract_aggregates(t, aggs),
)
})
.collect(),
else_result: else_result
.as_ref()
.map(|x| Box::new(self.extract_aggregates(x, aggs))),
},
Expr::RowValue(items) => Expr::RowValue(
items
.iter()
.map(|x| self.extract_aggregates(x, aggs))
.collect(),
),
other => other.clone(),
}
}
fn eval_windowed_aggregate(
&self,
sel: &Select,
columns: &[ColumnInfo],
rows: Vec<InputRow>,
params: &Params,
) -> Result<(Vec<String>, Vec<OutRow>)> {
if sel
.columns
.iter()
.any(|c| matches!(c, ResultColumn::Wildcard | ResultColumn::TableWildcard(_)))
{
return Err(Error::Unsupported(
"SELECT * with window functions over GROUP BY",
));
}
let labels = self.output_labels(sel, columns);
let group_by: Vec<Expr> = sel
.group_by
.iter()
.map(|g| {
if let Expr::Literal(Literal::Integer(n)) = g {
if *n >= 1 {
if let Some(ResultColumn::Expr { expr, .. }) =
sel.columns.get((*n - 1) as usize)
{
return expr.clone();
}
}
}
g.clone()
})
.collect();
let group_colls: Vec<crate::value::Collation> = {
let cctx = row_ctx(&[], columns, None, params);
group_by
.iter()
.map(|g| eval::key_collation(g, &cctx))
.collect()
};
let mut group_keys: Vec<Vec<Value>> = Vec::new();
let mut groups: Vec<Vec<usize>> = Vec::new();
for (i, r) in rows.iter().enumerate() {
let ctx = r.ctx(columns, params).with_subqueries(self);
let mut key = Vec::new();
for g in &group_by {
key.push(eval::eval(g, &ctx)?);
}
match group_keys
.iter()
.position(|k| rows_equal_coll(k, &key, &group_colls))
{
Some(idx) => groups[idx].push(i),
None => {
group_keys.push(key);
groups.push(alloc::vec![i]);
}
}
}
if sel.group_by.is_empty() {
groups = alloc::vec![(0..rows.len()).collect()];
} else {
let mut order: Vec<usize> = (0..groups.len()).collect();
order.sort_by(|&i, &j| {
for (k, coll) in group_colls.iter().enumerate() {
let ord =
crate::value::cmp_values_coll(&group_keys[i][k], &group_keys[j][k], *coll);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
});
let mut sorted = Vec::with_capacity(groups.len());
for i in order {
sorted.push(core::mem::take(&mut groups[i]));
}
groups = sorted;
}
let mut aggs: Vec<Expr> = Vec::new();
let mut rsel = sel.clone();
for col in &mut rsel.columns {
if let ResultColumn::Expr { expr, .. } = col {
*expr = self.extract_aggregates(expr, &mut aggs);
}
}
if let Some(h) = rsel.having.take() {
rsel.having = Some(self.extract_aggregates(&h, &mut aggs));
}
for t in &mut rsel.order_by {
t.expr = self.extract_aggregates(&t.expr, &mut aggs);
}
for (_, ws) in &mut rsel.window_defs {
for p in &mut ws.partition_by {
*p = self.extract_aggregates(p, &mut aggs);
}
for t in &mut ws.order_by {
t.expr = self.extract_aggregates(&t.expr, &mut aggs);
}
}
let mut cols: Vec<ColumnInfo> = columns.to_vec();
for i in 0..aggs.len() {
cols.push(ColumnInfo {
name: alloc::format!("__agg{i}"),
table: String::new(),
affinity: eval::Affinity::Blob,
collation: crate::value::Collation::default(),
});
}
let empty = InputRow {
values: alloc::vec![Value::Null; columns.len()],
rowid: None,
};
let mut grows: Vec<InputRow> = Vec::with_capacity(groups.len());
for group in &groups {
let repr_idx = group.first().copied();
let repr = repr_idx.map(|i| &rows[i]).unwrap_or(&empty);
let repr_ctx = repr.ctx(columns, params).with_subqueries(self);
let mut vals = repr.values.clone();
for agg in &aggs {
let sub = self.substitute_aggregates(agg, columns, &rows, group, params)?;
vals.push(eval::eval(&sub, &repr_ctx)?);
}
grows.push(InputRow {
values: vals,
rowid: repr_idx.and_then(|i| rows[i].rowid),
});
}
if let Some(having) = &rsel.having {
let mut kept = Vec::with_capacity(grows.len());
for r in grows {
let ctx = r.ctx(&cols, params).with_subqueries(self);
if eval::truth(&eval::eval(having, &ctx)?) == Some(true) {
kept.push(r);
}
}
grows = kept;
}
let mut wcols = cols;
let win_sel = self.apply_windows(&rsel, &mut wcols, &mut grows, params)?;
let mut out = Vec::with_capacity(grows.len());
for r in &grows {
let ctx = r.ctx(&wcols, params).with_subqueries(self);
let mut values = Vec::new();
for col in &win_sel.columns {
project_column(col, &wcols, &ctx, &mut values)?;
}
let mut sort_keys = Vec::new();
for term in &win_sel.order_by {
match resolve_order_index(&term.expr, &labels, values.len()) {
Some(idx) => sort_keys.push(values[idx].clone()),
None => sort_keys.push(eval::eval(&term.expr, &ctx)?),
}
}
out.push(OutRow { values, sort_keys });
}
Ok((labels, out))
}
fn argextreme_row(
&self,
group: &[usize],
columns: &[ColumnInfo],
rows: &[InputRow],
arg: &Expr,
is_max: bool,
params: &Params,
) -> Result<Option<usize>> {
let mut best: Option<(usize, Value)> = None;
for &i in group {
let ctx = rows[i].ctx(columns, params).with_subqueries(self);
let v = eval::eval(arg, &ctx)?;
if matches!(v, Value::Null) {
continue;
}
let take = match &best {
None => true,
Some((_, bv)) => {
let ord = eval::compare(&v, bv);
if is_max {
ord == core::cmp::Ordering::Greater
} else {
ord == core::cmp::Ordering::Less
}
}
};
if take {
best = Some((i, v));
}
}
Ok(best.map(|(i, _)| i).or_else(|| group.first().copied()))
}
fn substitute_aggregates(
&self,
expr: &Expr,
columns: &[ColumnInfo],
rows: &[InputRow],
group: &[usize],
params: &Params,
) -> Result<Expr> {
Ok(match expr {
Expr::Function {
name,
distinct,
args,
star,
filter,
order_by,
over: None,
} if func::is_aggregate_call(name, args.len(), *star)
|| self.aggregates.contains_key(&name.to_ascii_lowercase()) =>
{
let filtered;
let group = match filter {
Some(pred) => {
filtered = self.filter_group(pred, columns, rows, group, params)?;
&filtered[..]
}
None => group,
};
let v = self.compute_aggregate(
name, *distinct, args, *star, order_by, columns, rows, group, params,
)?;
let lit = Expr::Literal(value_to_literal(v));
if matches!(
name.to_ascii_lowercase().as_str(),
"json_group_array" | "json_group_object"
) {
Expr::Function {
name: String::from("json"),
distinct: false,
args: alloc::vec![lit],
star: false,
filter: None,
order_by: Vec::new(),
over: None,
}
} else {
lit
}
}
Expr::Function {
name,
distinct,
args,
star,
filter,
order_by,
over,
} => {
let mut new_args = Vec::with_capacity(args.len());
for a in args {
new_args.push(self.substitute_aggregates(a, columns, rows, group, params)?);
}
Expr::Function {
name: name.clone(),
distinct: *distinct,
args: new_args,
star: *star,
filter: filter.clone(),
order_by: order_by.clone(),
over: over.clone(),
}
}
Expr::Binary { op, left, right } => Expr::Binary {
op: *op,
left: Box::new(self.substitute_aggregates(left, columns, rows, group, params)?),
right: Box::new(self.substitute_aggregates(right, columns, rows, group, params)?),
},
Expr::Unary { op, expr } => Expr::Unary {
op: *op,
expr: Box::new(self.substitute_aggregates(expr, columns, rows, group, params)?),
},
Expr::Paren(e) => Expr::Paren(Box::new(
self.substitute_aggregates(e, columns, rows, group, params)?,
)),
Expr::Cast { expr, type_name } => Expr::Cast {
expr: Box::new(self.substitute_aggregates(expr, columns, rows, group, params)?),
type_name: type_name.clone(),
},
Expr::IsNull { expr, negated } => Expr::IsNull {
expr: Box::new(self.substitute_aggregates(expr, columns, rows, group, params)?),
negated: *negated,
},
Expr::Between {
expr,
low,
high,
negated,
} => Expr::Between {
expr: Box::new(self.substitute_aggregates(expr, columns, rows, group, params)?),
low: Box::new(self.substitute_aggregates(low, columns, rows, group, params)?),
high: Box::new(self.substitute_aggregates(high, columns, rows, group, params)?),
negated: *negated,
},
Expr::InList {
expr,
list,
negated,
candidate_affinity,
} => {
let mut new_list = Vec::with_capacity(list.len());
for e in list {
new_list.push(self.substitute_aggregates(e, columns, rows, group, params)?);
}
Expr::InList {
expr: Box::new(self.substitute_aggregates(expr, columns, rows, group, params)?),
list: new_list,
negated: *negated,
candidate_affinity: candidate_affinity.clone(),
}
}
Expr::Case {
operand,
when_then,
else_result,
} => {
let operand = match operand {
Some(o) => Some(Box::new(
self.substitute_aggregates(o, columns, rows, group, params)?,
)),
None => None,
};
let mut new_wt = Vec::with_capacity(when_then.len());
for (w, t) in when_then {
new_wt.push((
self.substitute_aggregates(w, columns, rows, group, params)?,
self.substitute_aggregates(t, columns, rows, group, params)?,
));
}
let else_result = match else_result {
Some(e) => Some(Box::new(
self.substitute_aggregates(e, columns, rows, group, params)?,
)),
None => None,
};
Expr::Case {
operand,
when_then: new_wt,
else_result,
}
}
other => other.clone(),
})
}
fn filter_group(
&self,
pred: &Expr,
columns: &[ColumnInfo],
rows: &[InputRow],
group: &[usize],
params: &Params,
) -> Result<Vec<usize>> {
let mut out = Vec::new();
for &i in group {
let ctx = rows[i].ctx(columns, params).with_subqueries(self);
if eval::truth(&eval::eval(pred, &ctx)?) == Some(true) {
out.push(i);
}
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
fn compute_aggregate(
&self,
name: &str,
distinct: bool,
args: &[Expr],
star: bool,
order_by: &[OrderTerm],
columns: &[ColumnInfo],
rows: &[InputRow],
group: &[usize],
params: &Params,
) -> Result<Value> {
let lname = name.to_ascii_lowercase();
if !self.aggregates.contains_key(&lname) {
let max_args = match lname.as_str() {
"group_concat" | "string_agg" | "json_group_object" | "jsonb_group_object" => 2,
_ => 1,
};
if args.len() > max_args {
return Err(Error::Error(format!(
"wrong number of arguments to function {lname}()"
)));
}
if lname == "string_agg" && args.len() < 2 {
return Err(Error::Error(format!(
"wrong number of arguments to function {lname}()"
)));
}
}
if distinct && !star && args.len() != 1 {
return Err(Error::Error(
"DISTINCT aggregates must have exactly one argument".into(),
));
}
if !star && args.is_empty() && lname != "count" {
return Err(Error::Error(format!(
"wrong number of arguments to function {lname}()"
)));
}
if (lname == "json_group_object" || lname == "jsonb_group_object") && args.len() < 2 {
return Err(Error::Error(format!(
"wrong number of arguments to function {lname}()"
)));
}
let ordered_group;
let group = if order_by.is_empty() {
group
} else {
let mut g = group.to_vec();
let mut err = None;
g.sort_by(|&a, &b| {
for term in order_by {
let ca = rows[a].ctx(columns, params).with_subqueries(self);
let cb = rows[b].ctx(columns, params).with_subqueries(self);
let (va, vb) = match (eval::eval(&term.expr, &ca), eval::eval(&term.expr, &cb))
{
(Ok(x), Ok(y)) => (x, y),
(Err(e), _) | (_, Err(e)) => {
err.get_or_insert(e);
return core::cmp::Ordering::Equal;
}
};
let coll = eval::key_collation(&term.expr, &ca);
let ord = cmp_order(&va, &vb, term.descending, term.nulls_first, coll);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
});
if let Some(e) = err {
return Err(e);
}
ordered_group = g;
&ordered_group[..]
};
if lname == "json_group_array" || lname == "jsonb_group_array" {
let mut vals = Vec::new();
for &i in group {
let ctx = rows[i].ctx(columns, params).with_subqueries(self);
vals.push(eval::eval(&args[0], &ctx)?);
}
if distinct {
dedup_values(&mut vals, crate::value::Collation::default());
}
let items: Vec<_> = vals
.iter()
.map(|v| func::arg_to_json(v, args.first()))
.collect();
let arr = json::Json::Array(items);
return Ok(if lname.starts_with("jsonb") {
Value::Blob(arr.to_jsonb())
} else {
Value::Text(arr.serialize())
});
}
if lname == "json_group_object" || lname == "jsonb_group_object" {
let mut pairs = Vec::new();
for &i in group {
let ctx = rows[i].ctx(columns, params).with_subqueries(self);
let k = eval::eval(&args[0], &ctx)?;
let v = eval::eval(&args[1], &ctx)?;
pairs.push((eval::to_text(&k), func::arg_to_json(&v, args.get(1))));
}
let obj = json::Json::Object(pairs);
return Ok(if lname.starts_with("jsonb") {
Value::Blob(obj.to_jsonb())
} else {
Value::Text(obj.serialize())
});
}
let mut vals: Vec<Value> = Vec::new();
let mut count_rows = 0usize; for &i in group {
count_rows += 1;
if star || args.is_empty() {
continue;
}
let ctx = rows[i].ctx(columns, params).with_subqueries(self);
let v = eval::eval(&args[0], &ctx)?;
if !matches!(v, Value::Null) {
vals.push(v);
}
}
if distinct {
let coll = if star || args.is_empty() {
crate::value::Collation::default()
} else {
let cctx = row_ctx(&[], columns, None, params);
eval::key_collation(&args[0], &cctx)
};
dedup_values(&mut vals, coll);
}
let arg_coll = if args.is_empty() {
crate::value::Collation::default()
} else {
let cctx = row_ctx(&[], columns, None, params);
eval::key_collation(&args[0], &cctx)
};
Ok(match lname.as_str() {
"count" => {
if star || args.is_empty() {
Value::Integer(count_rows as i64)
} else {
Value::Integer(vals.len() as i64)
}
}
"sum" => {
if vals.is_empty() {
Value::Null
} else if vals.iter().all(|v| matches!(v, Value::Integer(_))) {
let mut acc: i64 = 0;
let mut overflow = false;
for v in &vals {
if let Value::Integer(i) = v {
match acc.checked_add(*i) {
Some(s) => acc = s,
None => {
overflow = true;
break;
}
}
}
}
if overflow {
return Err(Error::Error("integer overflow".into()));
} else {
Value::Integer(acc)
}
} else {
Value::Real(vals.iter().map(eval::to_f64).sum())
}
}
"total" => Value::Real(vals.iter().map(eval::to_f64).sum()),
"avg" => {
if vals.is_empty() {
Value::Null
} else {
let sum: f64 = vals.iter().map(eval::to_f64).sum();
Value::Real(sum / vals.len() as f64)
}
}
"min" => vals
.into_iter()
.reduce(|a, b| {
if crate::value::cmp_values_coll(&b, &a, arg_coll) == core::cmp::Ordering::Less
{
b
} else {
a
}
})
.unwrap_or(Value::Null),
"max" => vals
.into_iter()
.reduce(|a, b| {
if crate::value::cmp_values_coll(&b, &a, arg_coll)
== core::cmp::Ordering::Greater
{
b
} else {
a
}
})
.unwrap_or(Value::Null),
"group_concat" | "string_agg" => {
if vals.is_empty() {
Value::Null
} else {
let sep = if args.len() >= 2 {
let ctx = EvalCtx::rowless(params);
eval::to_text(&eval::eval(&args[1], &ctx)?)
} else {
",".to_string()
};
let parts: Vec<String> = vals.iter().map(eval::to_text).collect();
Value::Text(parts.join(&sep))
}
}
_ => {
if let Some(factory) = self.aggregates.get(&lname) {
let mut acc = factory();
let mut seen: Vec<Vec<Value>> = Vec::new();
for &i in group {
let ctx = rows[i].ctx(columns, params).with_subqueries(self);
let vals: Vec<Value> = args
.iter()
.map(|a| eval::eval(a, &ctx))
.collect::<Result<_>>()?;
if distinct {
if seen.contains(&vals) {
continue;
}
seen.push(vals.clone());
}
acc.step(&vals)?;
}
return acc.finalize();
}
return Err(Error::Error(format!("no such function: {name}")));
}
})
}
fn has_result_aggregate(&self, sel: &Select) -> bool {
let is_agg = |name: &str, n: usize, star: bool| {
func::is_aggregate_call(name, n, star)
|| self.aggregates.contains_key(&name.to_ascii_lowercase())
};
sel.columns.iter().any(|c| match c {
ResultColumn::Expr { expr, .. } => expr_contains_agg(expr, &is_agg),
_ => false,
})
}
fn has_aggregate(&self, sel: &Select) -> bool {
if self.has_result_aggregate(sel) {
return true;
}
let is_agg = |name: &str, n: usize, star: bool| {
func::is_aggregate_call(name, n, star)
|| self.aggregates.contains_key(&name.to_ascii_lowercase())
};
sel.having
.as_ref()
.is_some_and(|h| expr_contains_agg(h, &is_agg))
}
fn output_labels(&self, sel: &Select, columns: &[ColumnInfo]) -> Vec<String> {
let mut labels = Vec::new();
for col in &sel.columns {
match col {
ResultColumn::Wildcard => {
for c in columns {
labels.push(c.name.clone());
}
}
ResultColumn::TableWildcard(t) => {
for c in columns.iter().filter(|c| c.table.eq_ignore_ascii_case(t)) {
labels.push(c.name.clone());
}
}
ResultColumn::Expr {
expr,
alias,
source,
} => {
labels.push(result_column_label(expr, alias, source));
}
}
}
labels
}
fn table_meta(&self, name: &str, alias: Option<&str>) -> Result<TableMeta> {
self.table_meta_in(&self.schema, name, alias)
}
fn table_meta_in(&self, schema: &Schema, name: &str, alias: Option<&str>) -> Result<TableMeta> {
if is_main_schema_table(name) {
return Ok(schema_table_meta(alias.unwrap_or(name)));
}
let obj = schema
.table(name)
.ok_or_else(|| Error::Error(alloc::format!("no such table: {name}")))?;
let sql = obj
.sql
.as_ref()
.ok_or_else(|| Error::Corrupt("table has no CREATE statement".into()))?;
let Statement::CreateTable(ct) = sql::parse_one(sql)? else {
return Err(Error::Corrupt("schema sql is not CREATE TABLE".into()));
};
let table_label = alias.unwrap_or(name).to_string();
let columns: Vec<ColumnInfo> = ct
.columns
.iter()
.map(|c| ColumnInfo {
name: c.name.clone(),
table: table_label.clone(),
affinity: eval::Affinity::from_type(c.type_name.as_deref()),
collation: column_collation(c),
})
.collect();
let defaults: Vec<Option<Expr>> = ct
.columns
.iter()
.map(|c| {
c.constraints.iter().find_map(|k| match k {
ColumnConstraint::Default(e) => Some(e.clone()),
_ => None,
})
})
.collect();
let ipk = if ct.without_rowid {
None
} else {
find_integer_primary_key(&ct)
};
let not_null: Vec<Option<OnConflict>> = ct
.columns
.iter()
.enumerate()
.map(|(i, c)| {
if Some(i) == ipk {
return Some(OnConflict::Abort);
}
c.constraints.iter().find_map(|k| match k {
ColumnConstraint::NotNull(oc) => Some(*oc),
_ => None,
})
})
.collect();
let generated: Vec<Option<(Expr, bool)>> = ct
.columns
.iter()
.map(|c| {
c.constraints.iter().find_map(|k| match k {
ColumnConstraint::Generated { expr, stored } => Some((expr.clone(), *stored)),
_ => None,
})
})
.collect();
let mut checks: Vec<(Expr, Option<String>)> = Vec::new();
for col in &ct.columns {
for k in &col.constraints {
if let ColumnConstraint::Check(e, label) = k {
checks.push((e.clone(), label.clone()));
}
}
}
for tc in &ct.constraints {
if let TableConstraint::Check(e, label) = tc {
checks.push((e.clone(), label.clone()));
}
}
let unique = collect_unique_sets(&ct, ipk);
let (without_rowid, storage_order, pk_len) = if ct.without_rowid {
let pk = primary_key_positions(&ct);
if pk.is_empty() {
return Err(Error::Error(format!(
"PRIMARY KEY missing on table {}",
ct.name
)));
}
let mut order = pk.clone();
for (i, gen) in generated.iter().enumerate() {
let is_virtual = matches!(gen, Some((_, false)));
if !pk.contains(&i) && !is_virtual {
order.push(i);
}
}
let pk_len = pk.len();
(true, order, pk_len)
} else {
(false, Vec::new(), 0)
};
let strict_types: Option<Vec<(StrictType, String)>> = if ct.strict {
let mut v = Vec::with_capacity(columns.len());
for c in &ct.columns {
let st = strict_column_type(c.type_name.as_deref()).unwrap_or(StrictType::Any);
let decl = c.type_name.clone().unwrap_or_default();
v.push((st, decl));
}
Some(v)
} else {
None
};
let mut columns = columns;
if let Some(st) = &strict_types {
for (col, (ty, _)) in columns.iter_mut().zip(st) {
if *ty == StrictType::Any {
col.affinity = eval::Affinity::Blob; }
}
}
Ok(TableMeta {
root: obj.rootpage,
columns,
defaults,
not_null,
checks,
unique,
ipk,
generated,
without_rowid,
storage_order,
pk_len,
strict_types,
autoincrement: ipk.is_some_and(|i| {
ct.columns[i].constraints.iter().any(|k| {
matches!(
k,
ColumnConstraint::PrimaryKey {
autoincrement: true,
..
}
)
})
}),
})
}
fn check_strict_types(&self, meta: &TableMeta, values: &[Value]) -> Result<()> {
let Some(stypes) = &meta.strict_types else {
return Ok(());
};
for (i, (st, decl)) in stypes.iter().enumerate() {
let v = &values[i];
let ok = matches!(
(st, v),
(_, Value::Null)
| (StrictType::Any, _)
| (StrictType::Int, Value::Integer(_))
| (StrictType::Real, Value::Real(_))
| (StrictType::Text, Value::Text(_))
| (StrictType::Blob, Value::Blob(_))
);
if !ok {
let class = match v {
Value::Integer(_) => "INT",
Value::Real(_) => "REAL",
Value::Text(_) => "TEXT",
Value::Blob(_) => "BLOB",
Value::Null => unreachable!(),
};
return Err(Error::Constraint(format!(
"cannot store {class} value in {decl} column {}.{}",
meta.columns[i].table, meta.columns[i].name
)));
}
}
Ok(())
}
fn check_constraints(
&self,
meta: &TableMeta,
values: &[Value],
rowid: Option<i64>,
params: &Params,
) -> Result<()> {
for (expr, label) in &meta.checks {
let ctx = row_ctx(values, &meta.columns, rowid, params).with_subqueries(self);
if eval::truth(&eval::eval(expr, &ctx)?) == Some(false) {
let msg = match label {
Some(l) => alloc::format!("CHECK constraint failed: {l}"),
None => String::from("CHECK constraint failed"),
};
return Err(Error::Constraint(msg));
}
}
Ok(())
}
}
struct TableMeta {
root: u32,
columns: Vec<ColumnInfo>,
defaults: Vec<Option<Expr>>,
not_null: Vec<Option<OnConflict>>,
checks: Vec<(Expr, Option<String>)>,
unique: Vec<(Vec<usize>, OnConflict)>,
ipk: Option<usize>,
generated: Vec<Option<(Expr, bool)>>,
without_rowid: bool,
storage_order: Vec<usize>,
pk_len: usize,
strict_types: Option<Vec<(StrictType, String)>>,
autoincrement: bool,
}
fn expand_agg_wildcards(sel: &Select, columns: &[ColumnInfo]) -> Select {
let col_ref = |c: &ColumnInfo| ResultColumn::Expr {
expr: Expr::Column {
table: Some(c.table.clone()),
column: c.name.clone(),
},
alias: None,
source: None,
};
let mut new_cols = Vec::new();
for col in &sel.columns {
match col {
ResultColumn::Wildcard => new_cols.extend(columns.iter().map(&col_ref)),
ResultColumn::TableWildcard(t) => new_cols.extend(
columns
.iter()
.filter(|c| c.table.eq_ignore_ascii_case(t))
.map(&col_ref),
),
other => new_cols.push(other.clone()),
}
}
let mut s = sel.clone();
s.columns = new_cols;
s
}
fn alias_substituted(sel: &Select, columns: &[ColumnInfo]) -> Option<Select> {
let mut aliases: Vec<(String, Expr)> = Vec::new();
for c in &sel.columns {
if let ResultColumn::Expr {
expr,
alias: Some(name),
..
} = c
{
if !columns
.iter()
.any(|col| col.name.eq_ignore_ascii_case(name))
&& !aliases.iter().any(|(a, _)| a.eq_ignore_ascii_case(name))
{
aliases.push((name.clone(), expr.clone()));
}
}
}
if aliases.is_empty() {
return None;
}
let mentions = |e: &Expr| -> bool {
let mut found = false;
window::visit(e, &mut |n| {
if let Expr::Column {
table: None,
column,
} = n
{
if aliases.iter().any(|(a, _)| a.eq_ignore_ascii_case(column)) {
found = true;
}
}
});
found
};
let used = sel.where_clause.as_ref().is_some_and(&mentions)
|| sel.group_by.iter().any(&mentions)
|| sel.having.as_ref().is_some_and(&mentions);
if !used {
return None;
}
let mut out = sel.clone();
let apply = |e: &mut Expr| {
for (name, repl) in &aliases {
let target = Expr::Column {
table: None,
column: name.clone(),
};
window::replace_expr(e, &target, repl);
}
};
if let Some(w) = &mut out.where_clause {
apply(w);
}
for g in &mut out.group_by {
apply(g);
}
if let Some(h) = &mut out.having {
apply(h);
}
Some(out)
}
fn insert_count_mismatch(
table: &str,
explicit_columns: bool,
n_cols: usize,
n_vals: usize,
) -> Error {
if explicit_columns {
Error::Error(alloc::format!("{n_vals} values for {n_cols} columns"))
} else {
Error::Error(alloc::format!(
"table {table} has {n_cols} columns but {n_vals} values were supplied"
))
}
}
fn value_to_literal_expr(v: Value) -> Expr {
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),
Value::Blob(b) => Literal::Blob(b),
})
}
fn is_main_schema_table(name: &str) -> bool {
name.eq_ignore_ascii_case("sqlite_schema") || name.eq_ignore_ascii_case("sqlite_master")
}
fn is_temp_schema_table(name: &str) -> bool {
name.eq_ignore_ascii_case("sqlite_temp_schema")
|| name.eq_ignore_ascii_case("sqlite_temp_master")
}
fn reject_schema_write(table: &str) -> Result<()> {
if is_main_schema_table(table) {
return Err(Error::Error(alloc::format!(
"table {table} may not be modified"
)));
}
Ok(())
}
fn schema_table_meta(label: &str) -> TableMeta {
let col = |n: &str, aff: eval::Affinity| ColumnInfo {
name: n.to_string(),
table: label.to_string(),
affinity: aff,
collation: crate::value::Collation::default(),
};
let columns = alloc::vec![
col("type", eval::Affinity::Text),
col("name", eval::Affinity::Text),
col("tbl_name", eval::Affinity::Text),
col("rootpage", eval::Affinity::Integer),
col("sql", eval::Affinity::Text),
];
let n = columns.len();
TableMeta {
root: crate::schema::SCHEMA_ROOT_PAGE,
columns,
defaults: alloc::vec![None; n],
not_null: alloc::vec![None; n],
checks: Vec::new(),
unique: Vec::new(),
ipk: None,
generated: alloc::vec![None; n],
without_rowid: false,
storage_order: Vec::new(),
pk_len: 0,
strict_types: None,
autoincrement: false,
}
}
fn validate_used_collations(sel: &Select) -> Result<()> {
for (_, arm) in &sel.compound {
validate_used_collations(arm)?;
}
if let Some(w) = &sel.where_clause {
consumed_collations(w)?;
}
if let Some(h) = &sel.having {
consumed_collations(h)?;
}
if let Some(from) = &sel.from {
for j in &from.joins {
if let Some(on) = &j.on {
consumed_collations(on)?;
}
}
}
for t in &sel.order_by {
top_collation(&t.expr)?;
consumed_collations(&t.expr)?;
}
for g in &sel.group_by {
top_collation(g)?;
consumed_collations(g)?;
}
for c in &sel.columns {
if let ResultColumn::Expr { expr, .. } = c {
if sel.distinct {
top_collation(expr)?;
}
consumed_collations(expr)?;
}
}
Ok(())
}
fn validate_unambiguous_columns(sel: &Select, columns: &[ColumnInfo]) -> Result<()> {
let mut ambiguous: Option<String> = None;
vdbe_block_exprs(sel, &mut |e| {
window::visit(e, &mut |sub| {
if ambiguous.is_some() {
return;
}
if let Expr::Column { table, column } = sub {
let n = columns
.iter()
.filter(|c| {
c.name.eq_ignore_ascii_case(column)
&& table
.as_deref()
.is_none_or(|t| c.table.eq_ignore_ascii_case(t))
})
.count();
if n >= 2 {
ambiguous = Some(alloc::format!("ambiguous column name: {column}"));
}
}
});
});
if let Some(msg) = ambiguous {
return Err(Error::Error(msg));
}
let has_wildcard = sel
.columns
.iter()
.any(|c| matches!(c, ResultColumn::Wildcard | ResultColumn::TableWildcard(_)));
if has_wildcard {
for (i, a) in columns.iter().enumerate() {
if let Some(b) = columns[i + 1..].iter().find(|b| {
a.name.eq_ignore_ascii_case(&b.name) && a.table.eq_ignore_ascii_case(&b.table)
}) {
return Err(Error::Error(alloc::format!(
"ambiguous column name: {}.{}",
b.table,
b.name
)));
}
}
}
Ok(())
}
fn walk_shallow_columns(e: &Expr, f: &mut impl FnMut(Option<&str>, &str)) {
match e {
Expr::Column { table, column } => f(table.as_deref(), column),
Expr::Unary { expr, .. } => walk_shallow_columns(expr, f),
Expr::Binary { left, right, .. } => {
walk_shallow_columns(left, f);
walk_shallow_columns(right, f);
}
Expr::Function {
args,
filter,
order_by,
..
} => {
for a in args {
walk_shallow_columns(a, f);
}
if let Some(flt) = filter {
walk_shallow_columns(flt, f);
}
for t in order_by {
walk_shallow_columns(&t.expr, f);
}
}
Expr::IsNull { expr, .. } => walk_shallow_columns(expr, f),
Expr::InList { expr, list, .. } => {
walk_shallow_columns(expr, f);
for a in list {
walk_shallow_columns(a, f);
}
}
Expr::Between {
expr, low, high, ..
} => {
walk_shallow_columns(expr, f);
walk_shallow_columns(low, f);
walk_shallow_columns(high, f);
}
Expr::Case {
operand,
when_then,
else_result,
} => {
if let Some(o) = operand {
walk_shallow_columns(o, f);
}
for (w, t) in when_then {
walk_shallow_columns(w, f);
walk_shallow_columns(t, f);
}
if let Some(el) = else_result {
walk_shallow_columns(el, f);
}
}
Expr::Cast { expr, .. } => walk_shallow_columns(expr, f),
Expr::Collate { expr, .. } => walk_shallow_columns(expr, f),
Expr::Paren(inner) => walk_shallow_columns(inner, f),
Expr::RowValue(items) => {
for it in items {
walk_shallow_columns(it, f);
}
}
_ => {}
}
}
fn collect_subselects<'a>(e: &'a Expr, out: &mut Vec<&'a Select>) {
match e {
Expr::Subquery(s) => out.push(s),
Expr::Exists { select, .. } => out.push(select),
Expr::InSelect { select, expr, .. } => {
out.push(select);
collect_subselects(expr, out);
}
Expr::Unary { expr, .. } => collect_subselects(expr, out),
Expr::Binary { left, right, .. } => {
collect_subselects(left, out);
collect_subselects(right, out);
}
Expr::Function { args, .. } => {
for a in args {
collect_subselects(a, out);
}
}
Expr::IsNull { expr, .. } => collect_subselects(expr, out),
Expr::InList { expr, list, .. } => {
collect_subselects(expr, out);
for a in list {
collect_subselects(a, out);
}
}
Expr::Between {
expr, low, high, ..
} => {
collect_subselects(expr, out);
collect_subselects(low, out);
collect_subselects(high, out);
}
Expr::Case {
operand,
when_then,
else_result,
} => {
if let Some(o) = operand {
collect_subselects(o, out);
}
for (w, t) in when_then {
collect_subselects(w, out);
collect_subselects(t, out);
}
if let Some(el) = else_result {
collect_subselects(el, out);
}
}
Expr::Cast { expr, .. } => collect_subselects(expr, out),
Expr::Collate { expr, .. } => collect_subselects(expr, out),
Expr::Paren(inner) => collect_subselects(inner, out),
_ => {}
}
}
fn first_ambiguous_in_scopes(sel: &Select, scopes: &[Option<Vec<ColumnInfo>>]) -> Option<String> {
let mut found: Option<String> = None;
vdbe_block_exprs(sel, &mut |e| {
window::visit(e, &mut |node| {
if found.is_some() {
return;
}
if let Expr::Column { table, column } = node {
for scope in scopes {
let Some(cols) = scope else {
break;
};
let n = cols
.iter()
.filter(|c| {
c.name.eq_ignore_ascii_case(column)
&& table
.as_deref()
.is_none_or(|t| c.table.eq_ignore_ascii_case(t))
})
.count();
if n >= 1 {
if n >= 2 {
found = Some(alloc::format!("ambiguous column name: {column}"));
}
break;
}
}
}
});
});
found
}
fn top_collation(e: &Expr) -> Result<()> {
match e {
Expr::Collate { collation, expr } => {
if crate::value::Collation::parse(collation).is_none() {
return Err(Error::Error(format!(
"no such collation sequence: {collation}"
)));
}
top_collation(expr)
}
Expr::Paren(inner) => top_collation(inner),
_ => Ok(()),
}
}
fn consumed_collations(e: &Expr) -> Result<()> {
match e {
Expr::Binary { op, left, right } => {
if matches!(
op,
BinaryOp::Eq
| BinaryOp::NotEq
| BinaryOp::Lt
| BinaryOp::LtEq
| BinaryOp::Gt
| BinaryOp::GtEq
) {
top_collation(left)?;
top_collation(right)?;
}
consumed_collations(left)?;
consumed_collations(right)?;
}
Expr::Between {
expr, low, high, ..
} => {
top_collation(expr)?;
top_collation(low)?;
top_collation(high)?;
consumed_collations(expr)?;
consumed_collations(low)?;
consumed_collations(high)?;
}
Expr::InList { expr, list, .. } => {
top_collation(expr)?;
consumed_collations(expr)?;
for it in list {
top_collation(it)?;
consumed_collations(it)?;
}
}
Expr::InSelect { expr, .. } => {
top_collation(expr)?;
consumed_collations(expr)?;
}
Expr::Case {
operand,
when_then,
else_result,
} => {
if let Some(o) = operand {
top_collation(o)?;
consumed_collations(o)?;
for (w, t) in when_then {
top_collation(w)?;
consumed_collations(w)?;
consumed_collations(t)?;
}
} else {
for (w, t) in when_then {
consumed_collations(w)?;
consumed_collations(t)?;
}
}
if let Some(er) = else_result {
consumed_collations(er)?;
}
}
Expr::Function { name, args, .. } => {
let lname = name.to_ascii_lowercase();
if matches!(lname.as_str(), "min" | "max") {
for a in args {
top_collation(a)?;
}
}
for a in args {
consumed_collations(a)?;
}
}
Expr::Unary { expr, .. }
| Expr::Paren(expr)
| Expr::IsNull { expr, .. }
| Expr::Cast { expr, .. }
| Expr::Collate { expr, .. } => consumed_collations(expr)?,
Expr::RowValue(items) => {
for it in items {
consumed_collations(it)?;
}
}
_ => {}
}
Ok(())
}
fn unknown_collation(e: &Expr) -> Option<&str> {
match e {
Expr::Collate { expr, collation } => {
if crate::value::Collation::parse(collation).is_none() {
Some(collation)
} else {
unknown_collation(expr)
}
}
Expr::Binary { left, right, .. } => {
unknown_collation(left).or_else(|| unknown_collation(right))
}
Expr::Unary { expr, .. }
| Expr::Paren(expr)
| Expr::Cast { expr, .. }
| Expr::IsNull { expr, .. } => unknown_collation(expr),
Expr::Function { args, .. } => args.iter().find_map(unknown_collation),
_ => None,
}
}
fn unknown_column_ref(
e: &Expr,
known: &[String],
allow_rowid: bool,
self_table: Option<&str>,
) -> Option<String> {
let mut bad: Option<String> = None;
window::visit(e, &mut |n| {
if let Expr::Column { table, column } = n {
if bad.is_some() {
return;
}
let foreign_qualifier = table
.as_ref()
.is_some_and(|q| self_table.is_none_or(|t| !t.eq_ignore_ascii_case(q)));
let resolves = !foreign_qualifier
&& (known.iter().any(|c| c.eq_ignore_ascii_case(column))
|| (allow_rowid && eval::is_rowid_alias(column)));
if !resolves {
bad = Some(match table {
Some(q) => alloc::format!("{q}.{column}"),
None => column.clone(),
});
}
}
});
bad
}
fn has_resolved_dotted_ref(
e: &Expr,
known: &[String],
allow_rowid: bool,
self_table: &str,
) -> bool {
let mut found = false;
window::visit(e, &mut |n| {
if let Expr::Column {
table: Some(q),
column,
} = n
{
if q.eq_ignore_ascii_case(self_table)
&& (known.iter().any(|c| c.eq_ignore_ascii_case(column))
|| (allow_rowid && eval::is_rowid_alias(column)))
{
found = true;
}
}
});
found
}
fn expr_has_subquery(e: &Expr) -> bool {
let mut found = false;
window::visit(e, &mut |n| {
if matches!(
n,
Expr::Subquery(_) | Expr::Exists { .. } | Expr::InSelect { .. }
) {
found = true;
}
});
found
}
fn first_aggregate_call_name(e: &Expr) -> Option<String> {
let mut found: Option<String> = None;
window::visit(e, &mut |n| {
if found.is_some() {
return;
}
if let Expr::Function {
name,
args,
star,
over,
..
} = n
{
if over.is_none() && func::is_aggregate_call(name, args.len(), *star) {
found = Some(name.clone());
}
}
});
found
}
fn expr_is_nondeterministic(e: &Expr) -> bool {
let mut found = false;
window::visit(e, &mut |n| {
if let Expr::Function { name, .. } = n {
if matches!(
name.to_ascii_lowercase().as_str(),
"random" | "randomblob" | "last_insert_rowid" | "changes" | "total_changes"
) {
found = true;
}
}
});
found
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum StrictType {
Int,
Real,
Text,
Blob,
Any,
}
fn strict_column_type(type_name: Option<&str>) -> Option<StrictType> {
let t = type_name?.trim();
if t.eq_ignore_ascii_case("INT") || t.eq_ignore_ascii_case("INTEGER") {
Some(StrictType::Int)
} else if t.eq_ignore_ascii_case("REAL") {
Some(StrictType::Real)
} else if t.eq_ignore_ascii_case("TEXT") {
Some(StrictType::Text)
} else if t.eq_ignore_ascii_case("BLOB") {
Some(StrictType::Blob)
} else if t.eq_ignore_ascii_case("ANY") {
Some(StrictType::Any)
} else {
None
}
}
impl TableMeta {
fn is_virtual(&self, i: usize) -> bool {
matches!(self.generated[i], Some((_, false)))
}
fn is_generated(&self, i: usize) -> bool {
self.generated[i].is_some()
}
}
fn vdbe_block_exprs<'a>(sel: &'a Select, f: &mut impl FnMut(&'a Expr)) {
for c in &sel.columns {
if let ResultColumn::Expr { expr, .. } = c {
f(expr);
}
}
sel.where_clause.iter().for_each(&mut *f);
sel.group_by.iter().for_each(&mut *f);
sel.having.iter().for_each(&mut *f);
for t in &sel.order_by {
f(&t.expr);
}
sel.limit.iter().for_each(&mut *f);
sel.offset.iter().for_each(&mut *f);
if let Some(from) = &sel.from {
for j in &from.joins {
if let Some(on) = &j.on {
f(on);
}
}
}
}
fn vdbe_block_exprs_mut(sel: &mut Select, f: &mut impl FnMut(&mut Expr)) {
for c in &mut sel.columns {
if let ResultColumn::Expr { expr, .. } = c {
f(expr);
}
}
sel.where_clause.iter_mut().for_each(&mut *f);
sel.group_by.iter_mut().for_each(&mut *f);
sel.having.iter_mut().for_each(&mut *f);
for t in &mut sel.order_by {
f(&mut t.expr);
}
sel.limit.iter_mut().for_each(&mut *f);
sel.offset.iter_mut().for_each(&mut *f);
if let Some(from) = &mut sel.from {
for j in &mut from.joins {
if let Some(on) = &mut j.on {
f(on);
}
}
}
}
fn substitute_params(sel: &Select, params: &Params) -> Option<Select> {
use crate::sql::token::Param;
let mut anon = false;
let mut explicit: Vec<Param> = Vec::new();
vdbe_block_exprs(sel, &mut |e| {
window::visit(e, &mut |x| {
if let Expr::Parameter(p) = x {
if matches!(p, Param::Anonymous) {
anon = true;
} else if !explicit.contains(p) {
explicit.push(p.clone());
}
}
});
});
if anon || explicit.is_empty() {
return None;
}
let mut out = sel.clone();
for p in &explicit {
let v = match p {
Param::Numbered(n) => params
.positional
.get((*n as usize).checked_sub(1)?)?
.clone(),
Param::Named(name) => params
.named
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.clone())?,
Param::Anonymous => return None,
};
let target = Expr::Parameter(p.clone());
let repl = Expr::Literal(value_to_literal(v));
vdbe_block_exprs_mut(&mut out, &mut |e| window::replace_expr(e, &target, &repl));
}
Some(out)
}
struct OrderIndexScan {
name: String,
root: u32,
colls: Vec<crate::value::Collation>,
cols: Vec<usize>,
descending: bool,
covering: bool,
sorted_suffix: usize,
}
struct IndexMeta {
name: String,
root: u32,
cols: Vec<usize>,
collations: Vec<crate::value::Collation>,
partial: Option<Expr>,
key_exprs: Option<Vec<Expr>>,
unique: bool,
}
impl Connection {
fn with_outer_frame<T>(
&self,
outer: &EvalCtx,
body: impl FnOnce(&Params) -> Result<T>,
) -> Result<T> {
self.outer_scope.borrow_mut().push(OuterFrame {
columns: outer.columns.to_vec(),
row: outer.row.to_vec(),
rowid: outer.rowid,
});
let params_ptr = outer.params;
let out = body(params_ptr);
self.outer_scope.borrow_mut().pop();
out
}
}
impl eval::Subqueries for Connection {
fn last_insert_rowid(&self) -> i64 {
self.last_insert_rowid.get()
}
fn changes(&self) -> i64 {
self.changes.get()
}
fn total_changes(&self) -> i64 {
self.total_changes.get()
}
fn next_random(&self) -> i64 {
let s = self.rng_state.get().wrapping_add(0x9E37_79B9_7F4A_7C15);
self.rng_state.set(s);
let mut z = s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
z as i64
}
fn call_udf(&self, name: &str, args: &[Value]) -> Option<Result<Value>> {
self.functions.get(name).map(|f| f(args))
}
#[cfg(feature = "fts5")]
fn fts5_bm25(&self, rowid: i64, weights: &[f64]) -> Option<f64> {
let cell = self.fts5_rank.borrow();
let (corpus, index) = cell.as_ref()?.bm25.as_ref()?;
Some(corpus.score(*index.get(&rowid)?, weights))
}
#[cfg(feature = "fts5")]
fn fts5_highlight(&self, col: usize, text: &str, open: &str, close: &str) -> Option<String> {
let cell = self.fts5_rank.borrow();
let ctx = cell.as_ref()?;
if ctx.col_names.get(col).is_some_and(|n| !ctx.col_indexed(n)) {
return Some(String::from(text));
}
Some(crate::vtab::fts5_highlight(
&ctx.query,
&ctx.col_names,
ctx.scope.as_deref(),
col,
text,
ctx.tok,
open,
close,
))
}
#[cfg(feature = "fts5")]
fn fts5_indexed_columns(&self, table: &str) -> Option<Vec<String>> {
let (module, args, _) = self.vtab_meta(table).ok()?;
if !module.eq_ignore_ascii_case("fts5") {
return None;
}
let refs: Vec<&str> = args.iter().map(String::as_str).collect();
Some(crate::vtab::fts5_indexed_columns(&refs))
}
#[cfg(feature = "fts5")]
fn fts5_tok(&self, table: &str) -> crate::vtab::Fts5Tok {
let Ok((module, args, _)) = self.vtab_meta(table) else {
return crate::vtab::Fts5Tok::default();
};
if !module.eq_ignore_ascii_case("fts5") {
return crate::vtab::Fts5Tok::default();
}
let refs: Vec<&str> = args.iter().map(String::as_str).collect();
crate::vtab::fts5_tok_config(&refs)
}
#[cfg(feature = "fts5")]
fn fts5_snippet(
&self,
col: i64,
cols: &[String],
open: &str,
close: &str,
ellipsis: &str,
ntokens: usize,
) -> Option<String> {
let cell = self.fts5_rank.borrow();
let ctx = cell.as_ref()?;
Some(crate::vtab::fts5_snippet(
&ctx.query,
&ctx.col_names,
ctx.scope.as_deref(),
col,
cols,
ctx.indexed.as_deref(),
ctx.tok,
open,
close,
ellipsis,
ntokens,
))
}
fn scalar(&self, select: &Select, outer: &EvalCtx) -> Result<Value> {
self.with_outer_frame(outer, |params| {
let r = self.run_select(select, params)?;
if r.columns.len() > 1 {
return Err(Error::Error(alloc::format!(
"sub-select returns {} columns - expected 1",
r.columns.len()
)));
}
Ok(r.rows
.first()
.and_then(|row| row.first())
.cloned()
.unwrap_or(Value::Null))
})
}
fn column(&self, select: &Select, outer: &EvalCtx) -> Result<Vec<Value>> {
self.with_outer_frame(outer, |params| {
let r = self.run_select(select, params)?;
Ok(r.rows
.into_iter()
.map(|mut row| {
if row.is_empty() {
Value::Null
} else {
row.swap_remove(0)
}
})
.collect())
})
}
fn column_affinity(&self, select: &Select) -> Option<eval::Affinity> {
self.row_column_affinities(select)
.into_iter()
.next()
.flatten()
}
fn row_column_affinities(&self, select: &Select) -> Vec<Option<eval::Affinity>> {
let params = Params::default();
let Ok((columns, _)) = self.scan_source(select, ¶ms) else {
return Vec::new();
};
let ctx = row_ctx(&[], &columns, None, ¶ms);
let mut out = Vec::new();
for col in &select.columns {
match col {
ResultColumn::Expr { expr, .. } => out.push(eval::expr_affinity(expr, &ctx)),
ResultColumn::Wildcard => out.extend(columns.iter().map(|c| Some(c.affinity))),
ResultColumn::TableWildcard(t) => out.extend(
columns
.iter()
.filter(|c| c.table.eq_ignore_ascii_case(t))
.map(|c| Some(c.affinity)),
),
}
}
out
}
fn rows(&self, select: &Select, outer: &EvalCtx) -> Result<Vec<Vec<Value>>> {
self.with_outer_frame(outer, |params| Ok(self.run_select(select, params)?.rows))
}
fn exists(&self, select: &Select, outer: &EvalCtx) -> Result<bool> {
self.with_outer_frame(outer, |params| {
Ok(!self.run_select(select, params)?.rows.is_empty())
})
}
fn resolve_outer(&self, table: Option<&str>, name: &str) -> Option<Value> {
let scope = self.outer_scope.borrow();
for frame in scope.iter().rev() {
if eval::is_rowid_alias(name) {
let qualifies = match table {
None => true,
Some(t) => frame
.columns
.iter()
.any(|c| c.table.eq_ignore_ascii_case(t)),
};
let has_real = frame.columns.iter().any(|c| {
c.name.eq_ignore_ascii_case(name)
&& table.is_none_or(|t| c.table.eq_ignore_ascii_case(t))
});
if qualifies && !has_real {
if let Some(r) = frame.rowid {
return Some(Value::Integer(r));
}
}
}
for (i, col) in frame.columns.iter().enumerate() {
let name_ok = col.name.eq_ignore_ascii_case(name);
let table_ok = table.is_none_or(|t| col.table.eq_ignore_ascii_case(t));
if name_ok && table_ok {
return Some(frame.row[i].clone());
}
}
}
None
}
}
fn is_text(v: &Value, s: &str) -> bool {
matches!(v, Value::Text(t) if t == s)
}
fn references_name(select: &Select, name: &str) -> bool {
if references_name_select(select, name) {
return true;
}
select
.compound
.iter()
.any(|(_, s)| references_name_select(s, name))
}
fn references_name_select(select: &Select, name: &str) -> bool {
let Some(from) = &select.from else {
return false;
};
if from.first.name.eq_ignore_ascii_case(name) {
return true;
}
from.joins
.iter()
.any(|j| j.table.name.eq_ignore_ascii_case(name))
}
fn is_bare_column_expr(e: &Expr) -> bool {
match e {
Expr::Column { .. } => true,
Expr::Paren(inner) | Expr::Collate { expr: inner, .. } => is_bare_column_expr(inner),
_ => false,
}
}
fn affinity_type_name(aff: eval::Affinity) -> alloc::string::String {
match aff {
eval::Affinity::Integer => "INTEGER",
eval::Affinity::Text => "TEXT",
eval::Affinity::Real => "REAL",
eval::Affinity::Numeric => "NUMERIC",
eval::Affinity::Blob => "BLOB",
}
.into()
}
fn expr_is_internal(e: &Expr, quals: &[String], cols: &[String]) -> bool {
let rec = |x: &Expr| expr_is_internal(x, quals, cols);
match e {
Expr::Literal(_) => true,
Expr::Parameter(_) => false,
Expr::Subquery(_) | Expr::Exists { .. } | Expr::InSelect { .. } => false,
Expr::Column { table, column } => match table {
Some(q) => quals.iter().any(|x| x.eq_ignore_ascii_case(q)),
None => {
cols.iter().any(|c| c.eq_ignore_ascii_case(column))
|| column.eq_ignore_ascii_case("rowid")
|| column.eq_ignore_ascii_case("_rowid_")
|| column.eq_ignore_ascii_case("oid")
}
},
Expr::Unary { expr, .. } => rec(expr),
Expr::Binary { left, right, .. } => rec(left) && rec(right),
Expr::IsNull { expr, .. } => rec(expr),
Expr::InList { expr, list, .. } => rec(expr) && list.iter().all(rec),
Expr::Between {
expr, low, high, ..
} => rec(expr) && rec(low) && rec(high),
Expr::Case {
operand,
when_then,
else_result,
} => {
operand.as_deref().map(rec).unwrap_or(true)
&& when_then.iter().all(|(w, t)| rec(w) && rec(t))
&& else_result.as_deref().map(rec).unwrap_or(true)
}
Expr::Cast { expr, .. } => rec(expr),
Expr::Paren(inner) => rec(inner),
Expr::Collate { expr, .. } => rec(expr),
Expr::RowValue(items) => items.iter().all(rec),
Expr::Function {
args,
filter,
order_by,
over,
..
} => {
over.is_none()
&& args.iter().all(rec)
&& filter.as_deref().map(rec).unwrap_or(true)
&& order_by.iter().all(|t| rec(&t.expr))
}
}
}
fn cmp_keys(a: &[Value], b: &[Value], desc: &[bool]) -> core::cmp::Ordering {
use core::cmp::Ordering;
for (i, (x, y)) in a.iter().zip(b).enumerate() {
let o = cmp_order(
x,
y,
desc.get(i).copied().unwrap_or(false),
None,
crate::value::Collation::Binary,
);
if o != Ordering::Equal {
return o;
}
}
Ordering::Equal
}
pub(crate) fn cmp_order(
a: &Value,
b: &Value,
descending: bool,
nulls_first: Option<bool>,
coll: crate::value::Collation,
) -> core::cmp::Ordering {
use core::cmp::Ordering;
let a_null = matches!(a, Value::Null);
let b_null = matches!(b, Value::Null);
let nulls_first = nulls_first.unwrap_or(!descending);
match (a_null, b_null) {
(true, true) => Ordering::Equal,
(true, false) => {
if nulls_first {
Ordering::Less
} else {
Ordering::Greater
}
}
(false, true) => {
if nulls_first {
Ordering::Greater
} else {
Ordering::Less
}
}
(false, false) => {
let ord = crate::value::cmp_values_coll(a, b, coll);
if descending {
ord.reverse()
} else {
ord
}
}
}
}
fn resolve_window_ref(wexpr: &Expr, defs: &[(String, WindowSpec)]) -> Result<Expr> {
let Expr::Function {
name,
distinct,
args,
star,
filter,
order_by,
over: Some(spec),
} = wexpr
else {
return Ok(wexpr.clone());
};
let Some(base) = &spec.base_name else {
return Ok(wexpr.clone());
};
let def = defs
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(base))
.map(|(_, s)| s)
.ok_or_else(|| Error::Error(alloc::format!("no such window: {base}")))?;
let effective = WindowSpec {
partition_by: def.partition_by.clone(),
order_by: if spec.order_by.is_empty() {
def.order_by.clone()
} else {
spec.order_by.clone()
},
frame: spec.frame.clone().or_else(|| def.frame.clone()),
base_name: None,
};
Ok(Expr::Function {
name: name.clone(),
distinct: *distinct,
args: args.clone(),
star: *star,
filter: filter.clone(),
order_by: order_by.clone(),
over: Some(effective),
})
}
fn json_emit_node(
node: &crate::exec::json::Json,
key: Option<Value>,
fullkey: &str,
path: &str,
parent: Option<i64>,
next_id: &mut i64,
rows: &mut Vec<Vec<Value>>,
) -> i64 {
use crate::exec::json::Json;
let id = *next_id;
*next_id += 1;
let is_container = matches!(node, Json::Object(_) | Json::Array(_));
let value = node.to_sql();
let atom = if is_container {
Value::Null
} else {
value.clone()
};
rows.push(alloc::vec![
key.unwrap_or(Value::Null),
value,
Value::Text(String::from(node.type_name())),
atom,
Value::Integer(id),
parent.map(Value::Integer).unwrap_or(Value::Null),
Value::Text(String::from(fullkey)),
Value::Text(String::from(path)),
]);
id
}
fn json_each_children(
root: &crate::exec::json::Json,
root_path: &str,
next_id: &mut i64,
rows: &mut Vec<Vec<Value>>,
) {
use crate::exec::json::Json;
match root {
Json::Object(members) => {
for (k, v) in members {
let fullkey = alloc::format!("{root_path}.{k}");
json_emit_node(
v,
Some(Value::Text(k.clone())),
&fullkey,
root_path,
None,
next_id,
rows,
);
}
}
Json::Array(items) => {
for (i, v) in items.iter().enumerate() {
let fullkey = alloc::format!("{root_path}[{i}]");
json_emit_node(
v,
Some(Value::Integer(i as i64)),
&fullkey,
root_path,
None,
next_id,
rows,
);
}
}
scalar => {
json_emit_node(scalar, None, root_path, root_path, None, next_id, rows);
}
}
}
fn split_json_path(path: &str) -> (alloc::string::String, Option<Value>) {
if path.ends_with(']') {
if let Some(open) = path.rfind('[') {
if let Ok(i) = path[open + 1..path.len() - 1].parse::<i64>() {
return (String::from(&path[..open]), Some(Value::Integer(i)));
}
}
}
if let Some(dot) = path.rfind('.') {
let name = path[dot + 1..].trim_matches('"');
return (
String::from(&path[..dot]),
Some(Value::Text(String::from(name))),
);
}
(String::from(path), None)
}
fn json_tree_walk(
node: &crate::exec::json::Json,
key: Option<Value>,
fullkey: &str,
path: &str,
parent: Option<i64>,
next_id: &mut i64,
rows: &mut Vec<Vec<Value>>,
) {
use crate::exec::json::Json;
let id = json_emit_node(node, key, fullkey, path, parent, next_id, rows);
match node {
Json::Object(members) => {
for (k, v) in members {
let child = alloc::format!("{fullkey}.{k}");
json_tree_walk(
v,
Some(Value::Text(k.clone())),
&child,
fullkey,
Some(id),
next_id,
rows,
);
}
}
Json::Array(items) => {
for (i, v) in items.iter().enumerate() {
let child = alloc::format!("{fullkey}[{i}]");
json_tree_walk(
v,
Some(Value::Integer(i as i64)),
&child,
fullkey,
Some(id),
next_id,
rows,
);
}
}
_ => {}
}
}
fn frame_bounds(
p: usize,
m: usize,
gid: &[usize],
spec: &WindowSpec,
ovals: &[Value],
desc: bool,
) -> (usize, usize) {
let Some(frame) = &spec.frame else {
if spec.order_by.is_empty() {
return (0, m);
}
let mut e = p + 1;
while e < m && gid[e] == gid[p] {
e += 1;
}
return (0, e);
};
let (start, end) = match frame.mode {
FrameMode::Rows => (
row_bound(&frame.start, p, m, true),
row_bound(&frame.end, p, m, false),
),
FrameMode::Range
if !ovals.is_empty()
&& (matches!(
frame.start,
FrameBound::Preceding(_) | FrameBound::Following(_)
) || matches!(
frame.end,
FrameBound::Preceding(_) | FrameBound::Following(_)
)) =>
{
(
range_value_bound(&frame.start, p, m, gid, ovals, desc, true),
range_value_bound(&frame.end, p, m, gid, ovals, desc, false),
)
}
FrameMode::Range | FrameMode::Groups => (
group_bound(&frame.start, p, m, gid, true),
group_bound(&frame.end, p, m, gid, false),
),
};
let start = start.min(m);
(start, end.min(m).max(start))
}
fn range_value_bound(
b: &FrameBound,
p: usize,
m: usize,
gid: &[usize],
ovals: &[Value],
desc: bool,
is_start: bool,
) -> usize {
if matches!(
b,
FrameBound::CurrentRow | FrameBound::Preceding(_) | FrameBound::Following(_)
) && matches!(ovals[p], Value::Null)
{
return group_bound(&FrameBound::CurrentRow, p, m, gid, is_start);
}
let val = eval::to_f64(&ovals[p]);
let threshold = match b {
FrameBound::UnboundedPreceding => return 0,
FrameBound::UnboundedFollowing => return m,
FrameBound::CurrentRow => val,
FrameBound::Preceding(n) => {
if desc {
val + *n as f64
} else {
val - *n as f64
}
}
FrameBound::Following(n) => {
if desc {
val - *n as f64
} else {
val + *n as f64
}
}
};
let inside = |vk: f64, edge: f64| if desc { vk >= edge } else { vk <= edge };
if is_start {
(0..m)
.find(|&k| {
!matches!(ovals[k], Value::Null) && {
let vk = eval::to_f64(&ovals[k]);
if desc {
vk <= threshold
} else {
vk >= threshold
}
}
})
.unwrap_or(m)
} else {
let mut e = 0;
for (k, ov) in ovals.iter().enumerate().take(m) {
if matches!(ov, Value::Null) {
continue;
}
if inside(eval::to_f64(ov), threshold) {
e = k + 1;
} else {
break;
}
}
e
}
}
fn row_bound(b: &FrameBound, p: usize, m: usize, is_start: bool) -> usize {
match (b, is_start) {
(FrameBound::UnboundedPreceding, _) => 0,
(FrameBound::UnboundedFollowing, _) => m,
(FrameBound::CurrentRow, true) => p,
(FrameBound::CurrentRow, false) => p + 1,
(FrameBound::Preceding(n), true) => p.saturating_sub(*n as usize),
(FrameBound::Preceding(n), false) => (p + 1).saturating_sub(*n as usize),
(FrameBound::Following(n), true) => (p + *n as usize).min(m),
(FrameBound::Following(n), false) => (p + 1 + *n as usize).min(m),
}
}
fn group_bound(b: &FrameBound, p: usize, m: usize, gid: &[usize], is_start: bool) -> usize {
let maxg = if m == 0 { 0 } else { gid[m - 1] as i64 };
let target = |g: i64| -> i64 { gid[p] as i64 + g };
let first_of = |g: i64| -> usize {
if g < 0 {
0
} else if g > maxg {
m
} else {
(0..m).find(|&i| gid[i] as i64 == g).unwrap_or(m)
}
};
let after_last_of = |g: i64| -> usize {
if g < 0 {
0
} else if g > maxg {
m
} else {
(0..m)
.rev()
.find(|&i| gid[i] as i64 == g)
.map_or(0, |i| i + 1)
}
};
match (b, is_start) {
(FrameBound::UnboundedPreceding, _) => 0,
(FrameBound::UnboundedFollowing, _) => m,
(FrameBound::CurrentRow, true) => first_of(target(0)),
(FrameBound::CurrentRow, false) => after_last_of(target(0)),
(FrameBound::Preceding(n), true) => first_of(target(-(*n))),
(FrameBound::Preceding(n), false) => after_last_of(target(-(*n))),
(FrameBound::Following(n), true) => first_of(target(*n)),
(FrameBound::Following(n), false) => after_last_of(target(*n)),
}
}
fn ntile_bucket(p: usize, m: usize, buckets: i64) -> i64 {
let buckets = (buckets.max(1) as usize).min(m.max(1));
let size = m / buckets;
let rem = m % buckets;
let big = rem * (size + 1);
if p < big {
(p / (size + 1)) as i64 + 1
} else {
(rem + (p - big) / size.max(1)) as i64 + 1
}
}
fn window_aggregate(lname: &str, star: bool, frame: &[&Vec<Value>]) -> Result<Value> {
let mut vals: Vec<Value> = Vec::new();
for row in frame {
if star {
continue;
}
if let Some(v) = row.first() {
if !matches!(v, Value::Null) {
vals.push(v.clone());
}
}
}
Ok(match lname {
"count" => {
if star {
Value::Integer(frame.len() as i64)
} else {
Value::Integer(vals.len() as i64)
}
}
"sum" => {
if vals.is_empty() {
Value::Null
} else if vals.iter().all(|v| matches!(v, Value::Integer(_))) {
let mut acc: i64 = 0;
let mut overflow = false;
for v in &vals {
if let Value::Integer(i) = v {
match acc.checked_add(*i) {
Some(s) => acc = s,
None => {
overflow = true;
break;
}
}
}
}
if overflow {
Value::Real(vals.iter().map(eval::to_f64).sum())
} else {
Value::Integer(acc)
}
} else {
Value::Real(vals.iter().map(eval::to_f64).sum())
}
}
"total" => Value::Real(vals.iter().map(eval::to_f64).sum()),
"avg" => {
if vals.is_empty() {
Value::Null
} else {
let sum: f64 = vals.iter().map(eval::to_f64).sum();
Value::Real(sum / vals.len() as f64)
}
}
"min" => vals
.into_iter()
.reduce(|a, b| {
if eval::compare(&b, &a) == core::cmp::Ordering::Less {
b
} else {
a
}
})
.unwrap_or(Value::Null),
"max" => vals
.into_iter()
.reduce(|a, b| {
if eval::compare(&b, &a) == core::cmp::Ordering::Greater {
b
} else {
a
}
})
.unwrap_or(Value::Null),
"group_concat" | "string_agg" => {
if vals.is_empty() {
Value::Null
} else {
let sep = frame
.first()
.and_then(|r| r.get(1))
.map(eval::to_text)
.unwrap_or_else(|| String::from(","));
let parts: Vec<String> = vals.iter().map(eval::to_text).collect();
Value::Text(parts.join(&sep))
}
}
_ => return Err(Error::Unsupported("window function")),
})
}
fn dedup_rows(rows: &mut Vec<Vec<Value>>) {
let mut seen: Vec<Vec<Value>> = Vec::new();
rows.retain(|row| {
if seen.iter().any(|s| rows_equal(s, row)) {
false
} else {
seen.push(row.clone());
true
}
});
}
fn pragma_text(e: &Expr) -> String {
match e {
Expr::Column { column, .. } => column.clone(),
Expr::Literal(Literal::Str(s)) => s.clone(),
_ => String::new(),
}
}
fn pragma_truth(e: &Expr, params: &Params) -> bool {
match e {
Expr::Column { column, .. } => {
matches!(column.to_ascii_lowercase().as_str(), "on" | "yes" | "true")
}
Expr::Literal(Literal::Str(s)) => {
matches!(s.to_ascii_lowercase().as_str(), "on" | "yes" | "true" | "1")
}
_ => eval::eval(e, &EvalCtx::rowless(params))
.map(|v| eval::to_i64(&v) != 0)
.unwrap_or(false),
}
}
fn eqp_label(t: &TableRef) -> String {
match &t.alias {
Some(a) => alloc::format!("{} AS {}", t.name, a),
None => t.name.clone(),
}
}
fn partial_pred_guaranteed(idx: &IndexMeta, where_expr: &Expr) -> bool {
match &idx.partial {
None => true,
Some(pred) => {
let mut conjuncts = Vec::new();
and_conjuncts(where_expr, &mut conjuncts);
conjuncts.iter().any(|c| expr_eq_modulo_parens(c, pred))
}
}
}
fn find_expr_in_values(key_expr: &Expr, e: &Expr, params: &Params) -> Option<Vec<Value>> {
match e {
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => find_expr_in_values(key_expr, left, params)
.or_else(|| find_expr_in_values(key_expr, right, params)),
Expr::Paren(inner) => find_expr_in_values(key_expr, inner, params),
Expr::InList {
expr,
list,
negated: false,
..
} => {
if list.is_empty() || !expr_eq_modulo_parens(expr, key_expr) {
return None;
}
let mut vals = Vec::with_capacity(list.len());
for item in list {
vals.push(const_value(item, params)?);
}
Some(vals)
}
_ => None,
}
}
struct ExecVTabStore<'a> {
conn: &'a mut Connection,
backing: &'a str,
ipk_prefix: bool,
}
impl VTabStore for ExecVTabStore<'_> {
fn rows(&self) -> Result<Vec<(i64, Vec<Value>)>> {
let meta = self.conn.table_meta(self.backing, None)?;
let mut rows = self.conn.scan_table(&meta)?;
if self.ipk_prefix {
for (_, values) in &mut rows {
if !values.is_empty() {
values.remove(0);
}
}
}
Ok(rows)
}
fn put(&mut self, rowid: i64, values: &[Value]) -> Result<()> {
let root = self.conn.table_meta(self.backing, None)?.root;
let payload = if self.ipk_prefix {
let mut row = alloc::vec![Value::Null];
row.extend_from_slice(values);
encode_record(&row)
} else {
encode_record(values)
};
let w = self.conn.backend.writer()?;
crate::btree::delete_table(w, root, rowid)?;
crate::btree::insert_table(w, root, rowid, &payload)?;
Ok(())
}
fn delete(&mut self, rowid: i64) -> Result<()> {
let root = self.conn.table_meta(self.backing, None)?.root;
let w = self.conn.backend.writer()?;
crate::btree::delete_table(w, root, rowid)?;
Ok(())
}
}
fn mirror_comparison(op: BinaryOp) -> BinaryOp {
match op {
BinaryOp::Lt => BinaryOp::Gt,
BinaryOp::LtEq => BinaryOp::GtEq,
BinaryOp::Gt => BinaryOp::Lt,
BinaryOp::GtEq => BinaryOp::LtEq,
other => other,
}
}
fn where_cols_covered(e: &Expr, meta: &TableMeta, idx_cols: &[usize]) -> bool {
let covered = |ci: usize| idx_cols.contains(&ci) || meta.ipk == Some(ci);
match e {
Expr::Literal(_) | Expr::Parameter(_) => true,
Expr::Column { column, .. } => match meta
.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(column))
{
Some(ci) => covered(ci),
None => matches!(
column.to_ascii_lowercase().as_str(),
"rowid" | "_rowid_" | "oid"
),
},
Expr::Unary { expr, .. }
| Expr::IsNull { expr, .. }
| Expr::Cast { expr, .. }
| Expr::Collate { expr, .. }
| Expr::Paren(expr) => where_cols_covered(expr, meta, idx_cols),
Expr::Binary { left, right, .. } => {
where_cols_covered(left, meta, idx_cols) && where_cols_covered(right, meta, idx_cols)
}
Expr::Between {
expr, low, high, ..
} => {
where_cols_covered(expr, meta, idx_cols)
&& where_cols_covered(low, meta, idx_cols)
&& where_cols_covered(high, meta, idx_cols)
}
Expr::InList { expr, list, .. } => {
where_cols_covered(expr, meta, idx_cols)
&& list.iter().all(|x| where_cols_covered(x, meta, idx_cols))
}
Expr::RowValue(items) => items.iter().all(|x| where_cols_covered(x, meta, idx_cols)),
Expr::Function {
args, filter, over, ..
} => {
over.is_none()
&& filter.is_none()
&& args.iter().all(|x| where_cols_covered(x, meta, idx_cols))
}
Expr::Case {
operand,
when_then,
else_result,
} => {
operand
.as_deref()
.map(|o| where_cols_covered(o, meta, idx_cols))
.unwrap_or(true)
&& when_then.iter().all(|(w, t)| {
where_cols_covered(w, meta, idx_cols) && where_cols_covered(t, meta, idx_cols)
})
&& else_result
.as_deref()
.map(|x| where_cols_covered(x, meta, idx_cols))
.unwrap_or(true)
}
Expr::Subquery(_) | Expr::Exists { .. } | Expr::InSelect { .. } => false,
}
}
#[cfg(feature = "fts5")]
fn fts5_rowid_eq(expr: &Expr, params: &Params) -> bool {
let is_rowid = |e: &Expr| {
matches!(e, Expr::Column { column, .. }
if matches!(column.to_ascii_lowercase().as_str(), "rowid" | "_rowid_" | "oid"))
};
match expr {
Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} => {
(is_rowid(left) && const_value(right, params).is_some())
|| (is_rowid(right) && const_value(left, params).is_some())
}
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => fts5_rowid_eq(left, params) || fts5_rowid_eq(right, params),
Expr::Paren(e) => fts5_rowid_eq(e, params),
_ => false,
}
}
fn collect_vtab_constraints(
sel: &Select,
columns: &[ColumnInfo],
params: &Params,
) -> (Vec<IndexConstraint>, Vec<Value>) {
let mut constraints = Vec::new();
let mut values = Vec::new();
let Some(where_expr) = &sel.where_clause else {
return (constraints, values);
};
let mut conjuncts = Vec::new();
and_conjuncts(where_expr, &mut conjuncts);
let mut push = |col: usize, op: ConstraintOp, v: Value| {
constraints.push(IndexConstraint {
column: col,
op,
usable: true,
});
values.push(v);
};
for c in conjuncts {
match c {
Expr::Binary { op, left, right }
if matches!(
op,
BinaryOp::Eq | BinaryOp::Lt | BinaryOp::LtEq | BinaryOp::Gt | BinaryOp::GtEq
) =>
{
if let (Some(ci), Some(v)) = (col_index(left, columns), const_value(right, params))
{
if let Some(cop) = binop_to_constraint(*op) {
push(ci, cop, v);
}
} else if let (Some(ci), Some(v)) =
(col_index(right, columns), const_value(left, params))
{
if let Some(cop) = binop_to_constraint(flip_cmp(*op)) {
push(ci, cop, v);
}
}
}
Expr::Between {
expr,
low,
high,
negated: false,
} => {
if let Some(ci) = col_index(expr, columns) {
if let Some(v) = const_value(low, params) {
push(ci, ConstraintOp::Ge, v);
}
if let Some(v) = const_value(high, params) {
push(ci, ConstraintOp::Le, v);
}
}
}
_ => {}
}
}
(constraints, values)
}
fn binop_to_constraint(op: BinaryOp) -> Option<ConstraintOp> {
Some(match op {
BinaryOp::Eq => ConstraintOp::Eq,
BinaryOp::Lt => ConstraintOp::Lt,
BinaryOp::LtEq => ConstraintOp::Le,
BinaryOp::Gt => ConstraintOp::Gt,
BinaryOp::GtEq => ConstraintOp::Ge,
_ => return None,
})
}
fn order_vtab_argv(plan: &IndexPlan, values: &[Value]) -> Vec<Value> {
let mut slots: Vec<(u32, Value)> = plan
.argv_index
.iter()
.zip(values.iter())
.filter(|(pos, _)| **pos != 0)
.map(|(pos, v)| (*pos, v.clone()))
.collect();
slots.sort_by_key(|(pos, _)| *pos);
slots.into_iter().map(|(_, v)| v).collect()
}
fn collect_eq_constraints(
e: &Expr,
columns: &[ColumnInfo],
params: &Params,
out: &mut Vec<(usize, Value)>,
) {
match e {
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => {
collect_eq_constraints(left, columns, params, out);
collect_eq_constraints(right, columns, params, out);
}
Expr::Paren(inner) => collect_eq_constraints(inner, columns, params, out),
Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} => {
if let (Some(ci), Some(v)) = (col_index(left, columns), const_value(right, params)) {
out.push((ci, v));
} else if let (Some(ci), Some(v)) =
(col_index(right, columns), const_value(left, params))
{
out.push((ci, v));
}
}
_ => {}
}
}
fn unparen(e: &Expr) -> &Expr {
let mut cur = e;
while let Expr::Paren(inner) = cur {
cur = inner;
}
cur
}
fn expr_eq_modulo_parens(a: &Expr, b: &Expr) -> bool {
unparen(a) == unparen(b)
}
fn and_conjuncts<'e>(e: &'e Expr, out: &mut Vec<&'e Expr>) {
match unparen(e) {
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => {
and_conjuncts(left, out);
and_conjuncts(right, out);
}
other => out.push(other),
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
enum JoinKey {
Num(u64),
Text(String),
Blob(Vec<u8>),
}
fn num_bits(f: f64) -> u64 {
(if f == 0.0 { 0.0 } else { f }).to_bits()
}
fn join_keys_of(v: &Value) -> Vec<JoinKey> {
match v {
Value::Null => Vec::new(),
Value::Integer(i) => alloc::vec![
JoinKey::Num(num_bits(*i as f64)),
JoinKey::Text(i.to_string())
],
Value::Real(r) => {
alloc::vec![
JoinKey::Num(num_bits(*r)),
JoinKey::Text(eval::format_real(*r))
]
}
Value::Text(s) => {
let mut keys = alloc::vec![JoinKey::Text(s.clone())];
match eval::to_number(&Value::Text(s.clone())) {
Value::Integer(i) => keys.push(JoinKey::Num(num_bits(i as f64))),
Value::Real(r) => keys.push(JoinKey::Num(num_bits(r))),
_ => {}
}
keys
}
Value::Blob(b) => alloc::vec![JoinKey::Blob(b.clone())],
}
}
fn promote_comma_join_ons(sel: &Select) -> Option<Select> {
let from = sel.from.as_ref()?;
let where_clause = sel.where_clause.as_ref()?;
let promotable = |j: &Join| {
j.on.is_none() && !j.natural && j.using.is_empty() && matches!(j.kind, JoinKind::Inner)
};
if !from.joins.iter().any(promotable) {
return None;
}
let mut conjuncts: Vec<&Expr> = Vec::new();
and_conjuncts(where_clause, &mut conjuncts);
let label = |t: &TableRef| t.alias.clone().unwrap_or_else(|| t.name.clone());
let mut available: Vec<String> = alloc::vec![label(&from.first)];
let mut new_joins = from.joins.clone();
let mut changed = false;
for (i, join) in from.joins.iter().enumerate() {
let jlabel = label(&join.table);
if promotable(join) {
if let Some(cond) = conjuncts
.iter()
.find_map(|c| eligible_join_equi(c, &jlabel, &available))
{
new_joins[i].on = Some(cond);
changed = true;
}
}
available.push(jlabel);
}
if !changed {
return None;
}
let mut new_sel = sel.clone();
new_sel.from = Some(FromClause {
first: from.first.clone(),
joins: new_joins,
});
Some(new_sel)
}
fn eligible_join_equi(c: &Expr, jlabel: &str, available: &[String]) -> Option<Expr> {
let mut c = c;
while let Expr::Paren(inner) = c {
c = inner;
}
let (l, r) = match c {
Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} => (left.as_ref(), right.as_ref()),
_ => return None,
};
let lt = column_qualifier(l)?;
let rt = column_qualifier(r)?;
let here = |t: &str| t.eq_ignore_ascii_case(jlabel);
let earlier = |t: &str| available.iter().any(|a| a.eq_ignore_ascii_case(t));
if (here(lt) && earlier(rt)) || (here(rt) && earlier(lt)) {
Some(c.clone())
} else {
None
}
}
fn column_qualifier(e: &Expr) -> Option<&str> {
match e {
Expr::Column { table: Some(t), .. } => Some(t),
Expr::Paren(inner) => column_qualifier(inner),
_ => None,
}
}
fn join_equi_cols(on: &Expr, cols: &[ColumnInfo], left_width: usize) -> Option<(usize, usize)> {
match on {
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => join_equi_cols(left, cols, left_width)
.or_else(|| join_equi_cols(right, cols, left_width)),
Expr::Paren(inner) => join_equi_cols(inner, cols, left_width),
Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} => {
let a = col_index(left, cols)?;
let b = col_index(right, cols)?;
let binary = |i: usize| cols[i].collation == crate::value::Collation::Binary;
let (l, r) = if a < left_width && b >= left_width {
(a, b)
} else if b < left_width && a >= left_width {
(b, a)
} else {
return None;
};
if binary(l) && binary(r) {
Some((l, r - left_width))
} else {
None
}
}
_ => None,
}
}
fn flatten_or<'a>(e: &'a Expr, out: &mut Vec<&'a Expr>) {
match e {
Expr::Binary {
op: BinaryOp::Or,
left,
right,
} => {
flatten_or(left, out);
flatten_or(right, out);
}
Expr::Paren(inner) => flatten_or(inner, out),
other => out.push(other),
}
}
fn find_in_constraint(
e: &Expr,
columns: &[ColumnInfo],
params: &Params,
) -> Option<(usize, Vec<Value>)> {
match e {
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => find_in_constraint(left, columns, params)
.or_else(|| find_in_constraint(right, columns, params)),
Expr::Paren(inner) => find_in_constraint(inner, columns, params),
Expr::InList {
expr,
list,
negated: false,
..
} => {
let ci = col_index(expr, columns)?;
if list.is_empty() {
return None;
}
let mut vals = Vec::with_capacity(list.len());
for item in list {
vals.push(const_value(item, params)?);
}
Some((ci, vals))
}
_ => None,
}
}
#[derive(Clone)]
struct RtreeCell {
key: i64,
coords: Vec<f64>,
}
fn rtree_node_size(n_coord: usize, page_size: usize) -> usize {
let cell = 8 + n_coord * 4;
page_size.saturating_sub(64).min(4 + 51 * cell)
}
fn rtree_encode_node(
cells: &[RtreeCell],
n_coord: usize,
is_root: bool,
depth: u16,
integer: bool,
node_size: usize,
) -> Vec<u8> {
let mut b = alloc::vec![0u8; node_size];
b[0..2].copy_from_slice(&(if is_root { depth } else { 0 }).to_be_bytes());
b[2..4].copy_from_slice(&(cells.len() as u16).to_be_bytes());
let cell_size = 8 + n_coord * 4;
for (i, c) in cells.iter().enumerate() {
let off = 4 + i * cell_size;
b[off..off + 8].copy_from_slice(&c.key.to_be_bytes());
for (d, &v) in c.coords.iter().enumerate() {
let p = off + 8 + d * 4;
let bytes = if integer {
(v as i32).to_be_bytes()
} else {
(v as f32).to_be_bytes()
};
b[p..p + 4].copy_from_slice(&bytes);
}
}
b
}
fn rtree_union(cells: &[RtreeCell], n_coord: usize) -> Vec<f64> {
let mut bb = alloc::vec![0.0f64; n_coord];
for (ci, c) in cells.iter().enumerate() {
for (d, slot) in bb.iter_mut().enumerate() {
let v = c.coords.get(d).copied().unwrap_or(0.0);
if ci == 0 {
*slot = v;
} else if d % 2 == 0 {
*slot = slot.min(v); } else {
*slot = slot.max(v); }
}
}
bb
}
struct RtreeBuild {
nodes: Vec<(i64, Vec<u8>)>,
rowids: Vec<(i64, i64)>,
parents: Vec<(i64, i64)>,
}
fn rtree_bulk_build(
entries: Vec<RtreeCell>,
n_coord: usize,
integer: bool,
node_size: usize,
) -> RtreeBuild {
let max_cells = ((node_size - 4) / (8 + n_coord * 4)).max(1);
if entries.is_empty() {
return RtreeBuild {
nodes: alloc::vec![(
1,
rtree_encode_node(&[], n_coord, true, 0, integer, node_size)
)],
rowids: Vec::new(),
parents: Vec::new(),
};
}
let mut levels: Vec<Vec<Vec<RtreeCell>>> = Vec::new();
levels.push(entries.chunks(max_cells).map(<[_]>::to_vec).collect());
while levels.last().map_or(0, Vec::len) > 1 {
let child_level = levels.len() - 1;
let children = &levels[child_level];
let parent_cells: Vec<RtreeCell> = (0..children.len())
.map(|idx| RtreeCell {
key: idx as i64,
coords: rtree_union(&children[idx], n_coord),
})
.collect();
levels.push(parent_cells.chunks(max_cells).map(<[_]>::to_vec).collect());
}
let root_level = levels.len() - 1;
let depth = root_level as u16;
let mut nodeno_of: alloc::collections::BTreeMap<(usize, usize), i64> =
alloc::collections::BTreeMap::new();
nodeno_of.insert((root_level, 0), 1);
let mut next = 2i64;
for level in (0..levels.len()).rev() {
for idx in 0..levels[level].len() {
nodeno_of.entry((level, idx)).or_insert_with(|| {
let n = next;
next += 1;
n
});
}
}
let mut nodes = Vec::new();
let mut rowids = Vec::new();
let mut parents = Vec::new();
for level in 0..levels.len() {
let is_leaf = level == 0;
for (idx, cells) in levels[level].iter().enumerate() {
let nodeno = nodeno_of[&(level, idx)];
let is_root = level == root_level;
let resolved: Vec<RtreeCell> = cells
.iter()
.map(|c| {
if is_leaf {
rowids.push((c.key, nodeno));
c.clone()
} else {
let child = nodeno_of[&(level - 1, c.key as usize)];
parents.push((child, nodeno));
RtreeCell {
key: child,
coords: c.coords.clone(),
}
}
})
.collect();
nodes.push((
nodeno,
rtree_encode_node(&resolved, n_coord, is_root, depth, integer, node_size),
));
}
}
RtreeBuild {
nodes,
rowids,
parents,
}
}
fn rtree_cell_from_values(
rowid: i64,
values: &[Value],
n_coord: usize,
integer: bool,
) -> Result<RtreeCell> {
for d in 0..n_coord / 2 {
let mn = values.get(1 + 2 * d).map_or(0.0, crate::vtab::coord_f64);
let mx = values.get(2 + 2 * d).map_or(0.0, crate::vtab::coord_f64);
if mn > mx {
return Err(Error::Error("rtree constraint failed".into()));
}
}
let coords = (0..n_coord)
.map(|d| {
let v = values.get(1 + d).map_or(0.0, crate::vtab::coord_f64);
if integer {
(v as i64).clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as f64
} else if d % 2 == 0 {
crate::vtab::round_min_f32(v)
} else {
crate::vtab::round_max_f32(v)
}
})
.collect();
Ok(RtreeCell { key: rowid, coords })
}
fn is_rowid_ref(e: &Expr, columns: &[ColumnInfo]) -> bool {
matches!(e, Expr::Column { column, .. }
if matches!(column.to_ascii_lowercase().as_str(), "rowid" | "_rowid_" | "oid")
&& !columns.iter().any(|c| c.name.eq_ignore_ascii_case(column)))
}
fn rowid_seek_constraint(
where_expr: &Expr,
columns: &[ColumnInfo],
params: &Params,
) -> Option<Vec<i64>> {
match where_expr {
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => rowid_seek_constraint(left, columns, params)
.or_else(|| rowid_seek_constraint(right, columns, params)),
Expr::Paren(inner) => rowid_seek_constraint(inner, columns, params),
Expr::Binary {
op: BinaryOp::Eq,
left,
right,
} => {
let other = if is_rowid_ref(left, columns) {
right
} else if is_rowid_ref(right, columns) {
left
} else {
return None;
};
Some(alloc::vec![eval::to_i64(&const_value(other, params)?)])
}
Expr::InList {
expr,
list,
negated: false,
..
} if is_rowid_ref(expr, columns) && !list.is_empty() => {
let mut out = Vec::with_capacity(list.len());
for item in list {
out.push(eval::to_i64(&const_value(item, params)?));
}
Some(out)
}
_ => None,
}
}
#[derive(Default, Clone)]
struct RangeBound {
lower: Option<(Value, bool)>,
upper: Option<(Value, bool)>,
}
fn apply_bound(b: &mut RangeBound, op: BinaryOp, v: Value) {
match op {
BinaryOp::Gt => b.lower = Some((v, false)),
BinaryOp::GtEq => b.lower = Some((v, true)),
BinaryOp::Lt => b.upper = Some((v, false)),
BinaryOp::LtEq => b.upper = Some((v, true)),
_ => {}
}
}
fn flip_cmp(op: BinaryOp) -> BinaryOp {
match op {
BinaryOp::Lt => BinaryOp::Gt,
BinaryOp::LtEq => BinaryOp::GtEq,
BinaryOp::Gt => BinaryOp::Lt,
BinaryOp::GtEq => BinaryOp::LtEq,
other => other,
}
}
fn collect_range_constraints(
e: &Expr,
columns: &[ColumnInfo],
params: &Params,
out: &mut alloc::collections::BTreeMap<usize, RangeBound>,
) {
match e {
Expr::Binary {
op: BinaryOp::And,
left,
right,
} => {
collect_range_constraints(left, columns, params, out);
collect_range_constraints(right, columns, params, out);
}
Expr::Paren(inner) => collect_range_constraints(inner, columns, params, out),
Expr::Binary { op, left, right }
if matches!(
op,
BinaryOp::Lt | BinaryOp::LtEq | BinaryOp::Gt | BinaryOp::GtEq
) =>
{
if let (Some(ci), Some(v)) = (col_index(left, columns), const_value(right, params)) {
apply_bound(out.entry(ci).or_default(), *op, v);
} else if let (Some(ci), Some(v)) =
(col_index(right, columns), const_value(left, params))
{
apply_bound(out.entry(ci).or_default(), flip_cmp(*op), v);
}
}
Expr::Between {
expr,
low,
high,
negated: false,
} => {
if let Some(ci) = col_index(expr, columns) {
let b = out.entry(ci).or_default();
if let Some(v) = const_value(low, params) {
apply_bound(b, BinaryOp::GtEq, v);
}
if let Some(v) = const_value(high, params) {
apply_bound(b, BinaryOp::LtEq, v);
}
}
}
_ => {}
}
}
fn col_index(e: &Expr, columns: &[ColumnInfo]) -> Option<usize> {
if let Expr::Column { table, column } = e {
columns.iter().position(|c| {
c.name.eq_ignore_ascii_case(column)
&& table
.as_deref()
.is_none_or(|t| c.table.eq_ignore_ascii_case(t))
})
} else {
None
}
}
fn const_value(e: &Expr, params: &Params) -> Option<Value> {
eval::eval(e, &EvalCtx::rowless(params)).ok()
}
fn apply_column_affinity(meta: &TableMeta, values: &mut [Value]) {
for (i, v) in values.iter_mut().enumerate() {
let taken = core::mem::replace(v, Value::Null);
*v = meta.columns[i].affinity.coerce(taken);
}
}
fn check_not_null(meta: &TableMeta, values: &[Value]) -> Result<()> {
for (i, v) in values.iter().enumerate() {
if meta.not_null[i].is_some() && matches!(v, Value::Null) {
return Err(Error::Constraint(format!(
"NOT NULL constraint failed: {}.{}",
meta.columns[i].table, meta.columns[i].name
)));
}
}
Ok(())
}
fn index_key(cols: &[usize], values: &[Value], rowid: i64) -> Vec<u8> {
let mut key: Vec<Value> = cols.iter().map(|&p| values[p].clone()).collect();
key.push(Value::Integer(rowid));
encode_record(&key)
}
fn index_stat_string(
cols: &[usize],
colls: &[crate::value::Collation],
rows: &[Vec<Value>],
) -> String {
let n = rows.len();
let mut tuples: Vec<Vec<Value>> = rows
.iter()
.map(|r| cols.iter().map(|&c| r[c].clone()).collect())
.collect();
tuples.sort_by(|a, b| stat_prefix_cmp(a, b, colls, cols.len()));
let mut s = alloc::format!("{n}");
for k in 1..=cols.len() {
let mut distinct = 1usize; for w in tuples.windows(2) {
if stat_prefix_cmp(&w[0], &w[1], colls, k) != core::cmp::Ordering::Equal {
distinct += 1;
}
}
let avg = (n + distinct / 2) / distinct;
s.push(' ');
s.push_str(&avg.to_string());
}
s
}
fn stat_prefix_cmp(
a: &[Value],
b: &[Value],
colls: &[crate::value::Collation],
len: usize,
) -> core::cmp::Ordering {
for i in 0..len {
let coll = colls.get(i).copied().unwrap_or_default();
let ord = crate::value::cmp_values_coll(&a[i], &b[i], coll);
if ord != core::cmp::Ordering::Equal {
return ord;
}
}
core::cmp::Ordering::Equal
}
fn unique_match(meta: &TableMeta, a: &[Value], b: &[Value]) -> bool {
meta.unique.iter().any(|(set, _)| {
set.iter().all(|&c| {
!matches!(a[c], Value::Null)
&& !matches!(b[c], Value::Null)
&& crate::value::cmp_values_coll(&a[c], &b[c], meta.columns[c].collation).is_eq()
})
})
}
fn wr_unique_message(meta: &TableMeta, a: &[Value], b: &[Value]) -> String {
meta.unique
.iter()
.find(|(set, _)| {
set.iter().all(|&c| {
!matches!(a[c], Value::Null)
&& !matches!(b[c], Value::Null)
&& crate::value::cmp_values_coll(&a[c], &b[c], meta.columns[c].collation)
.is_eq()
})
})
.map(|(set, _)| {
let cols = set
.iter()
.map(|&i| alloc::format!("{}.{}", meta.columns[i].table, meta.columns[i].name))
.collect::<Vec<_>>()
.join(", ");
alloc::format!("UNIQUE constraint failed: {cols}")
})
.unwrap_or_else(|| String::from("UNIQUE constraint failed"))
}
fn wr_index_key(cols: &[usize], pk_cols: &[usize], values: &[Value]) -> Vec<u8> {
let mut key: Vec<Value> = cols.iter().map(|&p| values[p].clone()).collect();
key.extend(pk_cols.iter().map(|&p| values[p].clone()));
encode_record(&key)
}
#[derive(Clone)]
struct InputRow {
values: Vec<Value>,
rowid: Option<i64>,
}
impl InputRow {
fn ctx<'a>(&'a self, columns: &'a [ColumnInfo], params: &'a Params) -> EvalCtx<'a> {
EvalCtx {
row: &self.values,
columns,
rowid: self.rowid,
params,
anon_counter: core::cell::Cell::new(0),
subqueries: None,
}
}
}
fn row_ctx<'a>(
values: &'a [Value],
columns: &'a [ColumnInfo],
rowid: Option<i64>,
params: &'a Params,
) -> EvalCtx<'a> {
EvalCtx {
row: values,
columns,
rowid,
params,
anon_counter: core::cell::Cell::new(0),
subqueries: None,
}
}
fn journal_path(path: &str) -> String {
let mut p = String::from(path);
p.push_str("-journal");
p
}
fn wal_path(path: &str) -> String {
let mut p = String::from(path);
p.push_str("-wal");
p
}
struct OutRow {
values: Vec<Value>,
sort_keys: Vec<Value>,
}
fn returning_labels(returning: &[ResultColumn], columns: &[ColumnInfo]) -> Vec<String> {
let mut labels = Vec::new();
for col in returning {
match col {
ResultColumn::Wildcard => {
for c in columns {
labels.push(c.name.clone());
}
}
ResultColumn::TableWildcard(t) => {
for c in columns {
if c.table.eq_ignore_ascii_case(t) {
labels.push(c.name.clone());
}
}
}
ResultColumn::Expr {
expr,
alias,
source,
} => {
labels.push(result_column_label(expr, alias, source));
}
}
}
labels
}
fn project_column(
col: &ResultColumn,
columns: &[ColumnInfo],
ctx: &EvalCtx,
out: &mut Vec<Value>,
) -> Result<()> {
match col {
ResultColumn::Wildcard => {
for v in ctx.row {
out.push(v.clone());
}
}
ResultColumn::TableWildcard(table) => {
for (i, c) in columns.iter().enumerate() {
if c.table.eq_ignore_ascii_case(table) {
out.push(ctx.row[i].clone());
}
}
}
ResultColumn::Expr { expr, .. } => {
out.push(eval::eval(expr, ctx)?);
}
}
Ok(())
}
fn positional_int(expr: &Expr) -> Option<i64> {
match expr {
Expr::Literal(Literal::Integer(n)) => Some(*n),
Expr::Unary {
op: UnaryOp::Negate,
expr,
} => match expr.as_ref() {
Expr::Literal(Literal::Integer(n)) => Some(n.wrapping_neg()),
_ => None,
},
Expr::Unary {
op: UnaryOp::Identity,
expr,
} => positional_int(expr),
Expr::Collate { expr, .. } | Expr::Paren(expr) => positional_int(expr),
_ => None,
}
}
fn is_values_projection(cols: &[ResultColumn]) -> bool {
!cols.is_empty()
&& cols.iter().enumerate().all(|(i, c)| {
matches!(
c,
ResultColumn::Expr { alias: Some(a), source: None, .. }
if *a == alloc::format!("column{}", i + 1)
)
})
}
fn ordinal(n: usize) -> alloc::string::String {
let suffix = if (11..=13).contains(&(n % 100)) {
"th"
} else {
match n % 10 {
1 => "st",
2 => "nd",
3 => "rd",
_ => "th",
}
};
alloc::format!("{n}{suffix}")
}
fn check_positional_terms(group_by: &[Expr], order_by: &[OrderTerm], ncols: usize) -> Result<()> {
for (i, t) in order_by.iter().enumerate() {
if let Some(n) = positional_int(&t.expr) {
if n < 1 || (n as u64) > ncols as u64 {
return Err(Error::Error(alloc::format!(
"{} ORDER BY term out of range - should be between 1 and {ncols}",
ordinal(i + 1),
)));
}
}
}
for (i, g) in group_by.iter().enumerate() {
if let Some(n) = positional_int(g) {
if n < 1 || (n as u64) > ncols as u64 {
return Err(Error::Error(alloc::format!(
"{} GROUP BY term out of range - should be between 1 and {ncols}",
ordinal(i + 1),
)));
}
}
}
Ok(())
}
fn must_be_int(v: Value) -> Result<i64> {
fn real_exact(r: f64) -> Result<i64> {
if r.is_finite()
&& r == crate::util::float::trunc(r)
&& r >= i64::MIN as f64
&& r < 9_223_372_036_854_775_808.0
{
Ok(r as i64)
} else {
Err(Error::Error("datatype mismatch".into()))
}
}
match v {
Value::Integer(i) => Ok(i),
Value::Real(r) => real_exact(r),
Value::Text(s) => {
let t = s.trim();
if let Ok(i) = t.parse::<i64>() {
Ok(i)
} else if let Ok(r) = t.parse::<f64>() {
real_exact(r)
} else {
Err(Error::Error("datatype mismatch".into()))
}
}
Value::Null | Value::Blob(_) => Err(Error::Error("datatype mismatch".into())),
}
}
fn resolve_order_index(expr: &Expr, labels: &[String], ncols: usize) -> Option<usize> {
match expr {
Expr::Literal(Literal::Integer(n)) => {
let idx = (*n as usize).checked_sub(1)?;
(idx < ncols).then_some(idx)
}
Expr::Column {
table: None,
column,
} => labels.iter().position(|l| l.eq_ignore_ascii_case(column)),
Expr::Collate { expr, .. } | Expr::Paren(expr) => resolve_order_index(expr, labels, ncols),
_ => None,
}
}
fn for_each_minmax(expr: &Expr, f: &mut dyn FnMut(bool, &Expr)) {
match expr {
Expr::Function {
over: Some(_),
args,
..
} => {
for a in args {
for_each_minmax(a, f);
}
}
Expr::Function {
name,
args,
star: false,
..
} => {
if args.len() == 1 {
let l = name.to_ascii_lowercase();
if l == "min" || l == "max" {
f(l == "max", &args[0]);
}
}
for a in args {
for_each_minmax(a, f);
}
}
Expr::Function { args, .. } => {
for a in args {
for_each_minmax(a, f);
}
}
Expr::Binary { left, right, .. } => {
for_each_minmax(left, f);
for_each_minmax(right, f);
}
Expr::Unary { expr, .. }
| Expr::Paren(expr)
| Expr::IsNull { expr, .. }
| Expr::Cast { expr, .. }
| Expr::Collate { expr, .. } => for_each_minmax(expr, f),
Expr::Between {
expr, low, high, ..
} => {
for_each_minmax(expr, f);
for_each_minmax(low, f);
for_each_minmax(high, f);
}
Expr::InList { expr, list, .. } => {
for_each_minmax(expr, f);
for l in list {
for_each_minmax(l, f);
}
}
Expr::Case {
operand,
when_then,
else_result,
} => {
if let Some(o) = operand {
for_each_minmax(o, f);
}
for (w, t) in when_then {
for_each_minmax(w, f);
for_each_minmax(t, f);
}
if let Some(e) = else_result {
for_each_minmax(e, f);
}
}
_ => {}
}
}
fn single_minmax_arg(sel: &Select) -> Option<(bool, Expr)> {
let mut hits: Vec<(bool, Expr)> = Vec::new();
let mut collect =
|e: &Expr| for_each_minmax(e, &mut |is_max, arg| hits.push((is_max, arg.clone())));
for col in &sel.columns {
if let ResultColumn::Expr { expr, .. } = col {
collect(expr);
}
}
if let Some(h) = &sel.having {
collect(h);
}
for term in &sel.order_by {
collect(&term.expr);
}
if hits.len() == 1 {
hits.pop()
} else {
None
}
}
#[cfg(feature = "fts5")]
struct Fts5QueryCtx {
col_names: Vec<String>,
query: String,
scope: Option<String>,
indexed: Option<Vec<String>>,
tok: crate::vtab::Fts5Tok,
bm25: Option<(
crate::vtab::Fts5Bm25,
alloc::collections::BTreeMap<i64, usize>,
)>,
}
#[cfg(feature = "fts5")]
impl Fts5QueryCtx {
fn col_indexed(&self, col: &str) -> bool {
self.indexed
.as_ref()
.is_none_or(|cols| cols.iter().any(|n| n.eq_ignore_ascii_case(col)))
}
}
#[cfg(feature = "fts5")]
struct Fts5RankGuard<'a> {
conn: &'a Connection,
prev: Option<Fts5QueryCtx>,
}
#[cfg(feature = "fts5")]
impl core::ops::Drop for Fts5RankGuard<'_> {
fn drop(&mut self) {
*self.conn.fts5_rank.borrow_mut() = self.prev.take();
}
}
#[cfg(feature = "fts5")]
fn expr_mentions_any(expr: &Expr, names: &[&str]) -> bool {
let rec = |e: &Expr| expr_mentions_any(e, names);
match expr {
Expr::Column {
table: None,
column,
} => names.iter().any(|n| column.eq_ignore_ascii_case(n)),
Expr::Function { name, args, .. } => {
names.iter().any(|n| name.eq_ignore_ascii_case(n)) || args.iter().any(rec)
}
Expr::Binary { left, right, .. } => rec(left) || rec(right),
Expr::Unary { expr, .. } | Expr::Paren(expr) => rec(expr),
Expr::IsNull { expr, .. } => rec(expr),
Expr::Between {
expr, low, high, ..
} => rec(expr) || rec(low) || rec(high),
Expr::InList { expr, list, .. } => rec(expr) || list.iter().any(rec),
Expr::Case {
operand,
when_then,
else_result,
} => {
operand.as_deref().is_some_and(rec)
|| when_then.iter().any(|(w, t)| rec(w) || rec(t))
|| else_result.as_deref().is_some_and(rec)
}
Expr::Cast { expr, .. } => rec(expr),
_ => false,
}
}
#[cfg(feature = "fts5")]
fn select_mentions(sel: &Select, names: &[&str]) -> bool {
sel.columns
.iter()
.any(|c| matches!(c, ResultColumn::Expr { expr, .. } if expr_mentions_any(expr, names)))
|| sel
.order_by
.iter()
.any(|t| expr_mentions_any(&t.expr, names))
|| sel
.having
.as_ref()
.is_some_and(|h| expr_mentions_any(h, names))
}
fn expr_contains_agg(expr: &Expr, is_agg: &dyn Fn(&str, usize, bool) -> bool) -> bool {
let rec = |e: &Expr| expr_contains_agg(e, is_agg);
match expr {
Expr::Function {
over: Some(_),
args,
..
} => args.iter().any(rec),
Expr::Function {
name, args, star, ..
} => is_agg(name, args.len(), *star) || args.iter().any(rec),
Expr::Binary { left, right, .. } => rec(left) || rec(right),
Expr::Unary { expr, .. } | Expr::Paren(expr) => rec(expr),
Expr::IsNull { expr, .. } => rec(expr),
Expr::Between {
expr, low, high, ..
} => rec(expr) || rec(low) || rec(high),
Expr::InList { expr, list, .. } => rec(expr) || list.iter().any(rec),
Expr::Case {
operand,
when_then,
else_result,
} => {
operand.as_deref().is_some_and(rec)
|| when_then.iter().any(|(w, t)| rec(w) || rec(t))
|| else_result.as_deref().is_some_and(rec)
}
Expr::Cast { expr, .. } => rec(expr),
_ => false,
}
}
fn apply_compound(
op: CompoundOp,
left: Vec<Vec<Value>>,
right: Vec<Vec<Value>>,
colls: &[crate::value::Collation],
) -> Vec<Vec<Value>> {
let eq = |a: &[Value], b: &[Value]| rows_equal_coll(a, b, colls);
let dedup = |rows: Vec<Vec<Value>>| -> Vec<Vec<Value>> {
let mut seen: Vec<Vec<Value>> = Vec::new();
for r in rows {
match seen.iter().position(|s| eq(s, &r)) {
Some(i) => seen[i] = r,
None => seen.push(r),
}
}
seen
};
match op {
CompoundOp::UnionAll => {
let mut out = left;
out.extend(right);
out
}
CompoundOp::Union => {
let mut out = left;
out.extend(right);
dedup(out)
}
CompoundOp::Intersect => dedup(
left.into_iter()
.filter(|l| right.iter().any(|r| eq(l, r)))
.collect(),
),
CompoundOp::Except => dedup(
left.into_iter()
.filter(|l| !right.iter().any(|r| eq(l, r)))
.collect(),
),
}
}
fn rows_equal(a: &[Value], b: &[Value]) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b)
.all(|(x, y)| eval::compare(x, y) == core::cmp::Ordering::Equal)
}
fn rows_equal_coll(a: &[Value], b: &[Value], colls: &[crate::value::Collation]) -> bool {
a.len() == b.len()
&& a.iter().zip(b).enumerate().all(|(i, (x, y))| {
let c = colls.get(i).copied().unwrap_or_default();
crate::value::cmp_values_coll(x, y, c) == core::cmp::Ordering::Equal
})
}
fn dedup_values(vals: &mut Vec<Value>, coll: crate::value::Collation) {
let mut seen: Vec<Value> = Vec::new();
vals.retain(|v| {
if seen
.iter()
.any(|s| crate::value::cmp_values_coll(s, v, coll) == core::cmp::Ordering::Equal)
{
false
} else {
seen.push(v.clone());
true
}
});
}
fn value_to_literal(v: Value) -> 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),
Value::Blob(b) => Literal::Blob(b),
}
}
type NamedColumns = Vec<(String, Option<String>)>;
type ColOrigin = (eval::Affinity, crate::value::Collation);
fn table_info_columns(extended: bool) -> Vec<String> {
let mut c: Vec<String> = ["cid", "name", "type", "notnull", "dflt_value", "pk"]
.iter()
.map(|s| String::from(*s))
.collect();
if extended {
c.push(String::from("hidden"));
}
c
}
fn rename_column_ref(e: &mut Expr, table: &str, old: &str, new: &str) {
window::replace_expr(
e,
&Expr::Column {
table: None,
column: String::from(old),
},
&Expr::Column {
table: None,
column: String::from(new),
},
);
window::replace_expr(
e,
&Expr::Column {
table: Some(String::from(table)),
column: String::from(old),
},
&Expr::Column {
table: Some(String::from(table)),
column: String::from(new),
},
);
}
fn rename_table_in_select(sel: &mut Select, old: &str, new: &str) {
let shadowed = sel.ctes.iter().any(|c| c.name.eq_ignore_ascii_case(old));
for cte in &mut sel.ctes {
rename_table_in_select(&mut cte.select, old, new);
}
if let Some(from) = &mut sel.from {
rename_table_in_ref(&mut from.first, old, new, shadowed);
for j in &mut from.joins {
rename_table_in_ref(&mut j.table, old, new, shadowed);
if let Some(on) = &mut j.on {
rename_table_in_expr(on, old, new);
}
}
}
for rc in &mut sel.columns {
match rc {
ResultColumn::Expr { expr, .. } => rename_table_in_expr(expr, old, new),
ResultColumn::TableWildcard(t) if !shadowed && t.eq_ignore_ascii_case(old) => {
*t = String::from(new);
}
_ => {}
}
}
if let Some(w) = &mut sel.where_clause {
rename_table_in_expr(w, old, new);
}
for e in &mut sel.group_by {
rename_table_in_expr(e, old, new);
}
if let Some(h) = &mut sel.having {
rename_table_in_expr(h, old, new);
}
for t in &mut sel.order_by {
rename_table_in_expr(&mut t.expr, old, new);
}
for (_, ws) in &mut sel.window_defs {
rename_table_in_window(ws, old, new);
}
if let Some(e) = &mut sel.limit {
rename_table_in_expr(e, old, new);
}
if let Some(e) = &mut sel.offset {
rename_table_in_expr(e, old, new);
}
for (_, comp) in &mut sel.compound {
rename_table_in_select(comp, old, new);
}
}
fn rename_table_in_ref(tref: &mut TableRef, old: &str, new: &str, shadowed: bool) {
if let Some(sub) = &mut tref.subquery {
rename_table_in_select(sub, old, new);
} else if tref.schema.is_none() && !shadowed && tref.name.eq_ignore_ascii_case(old) {
tref.name = String::from(new);
}
}
fn rename_table_in_window(ws: &mut WindowSpec, old: &str, new: &str) {
for e in &mut ws.partition_by {
rename_table_in_expr(e, old, new);
}
for t in &mut ws.order_by {
rename_table_in_expr(&mut t.expr, old, new);
}
}
fn rename_table_in_expr(e: &mut Expr, old: &str, new: &str) {
match e {
Expr::Column { table: Some(t), .. } if t.eq_ignore_ascii_case(old) => {
*t = String::from(new)
}
Expr::Column { .. } | Expr::Literal(_) | Expr::Parameter(_) => {}
Expr::Unary { expr, .. }
| Expr::IsNull { expr, .. }
| Expr::Cast { expr, .. }
| Expr::Paren(expr)
| Expr::Collate { expr, .. } => rename_table_in_expr(expr, old, new),
Expr::Binary { left, right, .. } => {
rename_table_in_expr(left, old, new);
rename_table_in_expr(right, old, new);
}
Expr::Function {
args,
filter,
order_by,
over,
..
} => {
for a in args {
rename_table_in_expr(a, old, new);
}
if let Some(f) = filter {
rename_table_in_expr(f, old, new);
}
for t in order_by {
rename_table_in_expr(&mut t.expr, old, new);
}
if let Some(w) = over {
rename_table_in_window(w, old, new);
}
}
Expr::InList { expr, list, .. } => {
rename_table_in_expr(expr, old, new);
for a in list {
rename_table_in_expr(a, old, new);
}
}
Expr::Between {
expr, low, high, ..
} => {
rename_table_in_expr(expr, old, new);
rename_table_in_expr(low, old, new);
rename_table_in_expr(high, old, new);
}
Expr::Case {
operand,
when_then,
else_result,
} => {
if let Some(o) = operand {
rename_table_in_expr(o, old, new);
}
for (w, t) in when_then {
rename_table_in_expr(w, old, new);
rename_table_in_expr(t, old, new);
}
if let Some(el) = else_result {
rename_table_in_expr(el, old, new);
}
}
Expr::RowValue(items) => {
for i in items {
rename_table_in_expr(i, old, new);
}
}
Expr::Subquery(s) => rename_table_in_select(s, old, new),
Expr::Exists { select, .. } => rename_table_in_select(select, old, new),
Expr::InSelect { expr, select, .. } => {
rename_table_in_expr(expr, old, new);
rename_table_in_select(select, old, new);
}
}
}
fn split_sql_script(sql: &str) -> Vec<&str> {
let toks = match sql::token::tokenize(sql) {
Ok(t) => t,
Err(_) => return alloc::vec![sql.trim()],
};
let mut out = Vec::new();
let mut depth: u32 = 0;
let mut seg_start = 0usize;
let mut seen = false;
for sp in &toks {
match &sp.token {
sql::token::Token::Semicolon if depth == 0 => {
if seen {
out.push(sql[seg_start..sp.start].trim());
}
seg_start = sp.end;
seen = false;
}
sql::token::Token::Word(w) => {
match w.to_ascii_uppercase().as_str() {
"BEGIN" if seen => depth += 1,
"CASE" => depth += 1,
"END" => depth = depth.saturating_sub(1),
_ => {}
}
seen = true;
}
_ => seen = true,
}
}
if seen {
out.push(sql[seg_start..].trim());
}
out
}
fn ddl_text(sql: &str) -> &str {
match sql::token::tokenize(sql) {
Ok(toks) if !toks.is_empty() => sql[toks[0].start..].trim_end(),
_ => sql.trim(),
}
}
fn reject_reserved_name(name: &str) -> Result<()> {
if name.len() >= 7 && name[..7].eq_ignore_ascii_case("sqlite_") {
return Err(Error::Error(format!(
"object name reserved for internal use: {name}"
)));
}
Ok(())
}
fn select_reads_table(sel: &Select, name: &str) -> bool {
let mut probe = sel.clone();
rename_table_in_select(
&mut probe,
name,
"\u{1}\u{1}graphite_rename_probe\u{1}\u{1}",
);
probe != *sel
}
fn from_refs_table(f: &FromClause, name: &str) -> bool {
f.first.name.eq_ignore_ascii_case(name)
|| f.joins
.iter()
.any(|j| j.table.name.eq_ignore_ascii_case(name))
}
fn trigger_uses_table(trigger_sql: &str, name: &str) -> bool {
let Ok(Statement::CreateTrigger(ct)) = sql::parse_one(trigger_sql) else {
return false;
};
if ct.table.eq_ignore_ascii_case(name) {
return true;
}
ct.body.iter().any(|s| match s {
Statement::Select(sel) => select_reads_table(sel, name),
Statement::Insert(i) => {
i.table.eq_ignore_ascii_case(name)
|| matches!(&i.source, InsertSource::Select(sel) if select_reads_table(sel, name))
}
Statement::Update(u) => {
u.table.eq_ignore_ascii_case(name)
|| u.from.as_ref().is_some_and(|f| from_refs_table(f, name))
}
Statement::Delete(d) => d.table.eq_ignore_ascii_case(name),
_ => false,
})
}
fn view_uses_table(view_sql: &str, name: &str) -> bool {
match sql::parse_one(view_sql) {
Ok(Statement::CreateView(cv)) => {
let mut probe = cv.select.clone();
rename_table_in_select(
&mut probe,
name,
"\u{1}\u{1}graphite_rename_probe\u{1}\u{1}",
);
*probe != *cv.select
}
_ => false,
}
}
fn rewrite_fk_parent_column(sql: &str, parent: &str, old: &str, rendered: &str) -> String {
use sql::token::Token;
let toks = match sql::token::tokenize(sql) {
Ok(t) => t,
Err(_) => return String::from(sql),
};
let is_word = |t: &Token, w: &str| matches!(t, Token::Word(x) | Token::Ident(x) if x.eq_ignore_ascii_case(w));
let mut spans: Vec<(usize, usize)> = Vec::new();
let mut i = 0;
while i < toks.len() {
if is_word(&toks[i].token, "references")
&& toks.get(i + 1).is_some_and(|p| is_word(&p.token, parent))
&& toks
.get(i + 2)
.is_some_and(|l| matches!(l.token, Token::LParen))
{
let mut m = i + 3;
while m < toks.len() && !matches!(toks[m].token, Token::RParen) {
if is_word(&toks[m].token, old) {
spans.push((toks[m].start, toks[m].end));
}
m += 1;
}
i = m;
continue;
}
i += 1;
}
if spans.is_empty() {
return String::from(sql);
}
let mut out = String::new();
let mut cursor = 0;
for (s, e) in spans {
out.push_str(&sql[cursor..s]);
out.push_str(rendered);
cursor = e;
}
out.push_str(&sql[cursor..]);
out
}
fn view_single_source_column_quals(view_sql: &str, table: &str, old: &str) -> Option<Vec<String>> {
let Ok(Statement::CreateView(cv)) = sql::parse_one(view_sql) else {
return None;
};
let sel = &cv.select;
if !sel.ctes.is_empty() || !sel.compound.is_empty() {
return None;
}
if old.eq_ignore_ascii_case(table) {
return None;
}
let from = sel.from.as_ref()?;
if !from.joins.is_empty() || from.first.subquery.is_some() || from.first.tvf_args.is_some() {
return None;
}
if !from.first.name.eq_ignore_ascii_case(table) {
return None;
}
let mut quals = alloc::vec![table.to_string()];
if let Some(a) = &from.first.alias {
if a.eq_ignore_ascii_case(old) {
return None; }
quals.push(a.clone());
}
for rc in &sel.columns {
if let ResultColumn::Expr { expr, alias, .. } = rc {
if expr_has_subquery(expr) {
return None;
}
if alias
.as_deref()
.is_some_and(|a| a.eq_ignore_ascii_case(old))
{
return None;
}
}
}
let mut clean = true;
for e in sel
.where_clause
.iter()
.chain(sel.group_by.iter())
.chain(sel.having.iter())
{
clean &= !expr_has_subquery(e);
}
for t in &sel.order_by {
clean &= !expr_has_subquery(&t.expr);
}
if !clean {
return None;
}
Some(quals)
}
fn view_multi_source_quals(
view_sql: &str,
table: &str,
old: &str,
table_cols: &alloc::collections::BTreeMap<String, Vec<String>>,
) -> Option<(Vec<String>, bool)> {
let Ok(Statement::CreateView(cv)) = sql::parse_one(view_sql) else {
return None;
};
let sel = &cv.select;
if !sel.ctes.is_empty() || !sel.compound.is_empty() || old.eq_ignore_ascii_case(table) {
return None;
}
let from = sel.from.as_ref()?;
if from.joins.is_empty() {
return None; }
let mut srcs: Vec<(String, Option<String>)> = Vec::new();
let mut push = |tr: &crate::sql::ast::TableRef| -> bool {
if tr.subquery.is_some() || tr.tvf_args.is_some() || tr.schema.is_some() {
return false;
}
srcs.push((tr.name.clone(), tr.alias.clone()));
true
};
if !push(&from.first) {
return None;
}
for j in &from.joins {
if j.natural || !j.using.is_empty() || !push(&j.table) {
return None;
}
}
let renamed: Vec<&(String, Option<String>)> = srcs
.iter()
.filter(|(n, _)| n.eq_ignore_ascii_case(table))
.collect();
if renamed.len() != 1 {
return None;
}
let mut quals = alloc::vec![renamed[0].0.clone()];
if let Some(a) = &renamed[0].1 {
if a.eq_ignore_ascii_case(old) {
return None;
}
quals.push(a.clone());
}
let has_old = |name: &str| -> Option<bool> {
let cols = table_cols
.iter()
.find(|(t, _)| t.eq_ignore_ascii_case(name))
.map(|(_, c)| c)?;
Some(cols.iter().any(|c| c.eq_ignore_ascii_case(old)))
};
let mut count = 0usize;
for (n, _) in &srcs {
if has_old(n)? {
count += 1;
}
}
let rewrite_bare = count == 1;
for rc in &sel.columns {
if let ResultColumn::Expr { expr, alias, .. } = rc {
if expr_has_subquery(expr)
|| alias
.as_deref()
.is_some_and(|a| a.eq_ignore_ascii_case(old))
{
return None;
}
}
}
for e in sel
.where_clause
.iter()
.chain(sel.group_by.iter())
.chain(sel.having.iter())
{
if expr_has_subquery(e) {
return None;
}
}
for t in &sel.order_by {
if expr_has_subquery(&t.expr) {
return None;
}
}
Some((quals, rewrite_bare))
}
fn select_single_source_ok(sel: &Select, table: &str) -> bool {
if !sel.ctes.is_empty() || !sel.compound.is_empty() {
return false;
}
if let Some(from) = &sel.from {
if !from.joins.is_empty()
|| from.first.subquery.is_some()
|| from.first.tvf_args.is_some()
|| from.first.alias.is_some()
|| !from.first.name.eq_ignore_ascii_case(table)
{
return false;
}
}
let mut ok = true;
for rc in &sel.columns {
if let ResultColumn::Expr { expr, alias, .. } = rc {
ok &= !expr_has_subquery(expr) && alias.is_none();
}
}
for e in sel
.where_clause
.iter()
.chain(sel.group_by.iter())
.chain(sel.having.iter())
{
ok &= !expr_has_subquery(e);
}
for t in &sel.order_by {
ok &= !expr_has_subquery(&t.expr);
}
ok
}
fn trigger_single_source_quals(trigger_sql: &str, table: &str, old: &str) -> Option<Vec<String>> {
let Ok(Statement::CreateTrigger(ct)) = sql::parse_one(trigger_sql) else {
return None;
};
if !ct.table.eq_ignore_ascii_case(table)
|| old.eq_ignore_ascii_case(table)
|| old.eq_ignore_ascii_case("new")
|| old.eq_ignore_ascii_case("old")
{
return None;
}
if ct.when.as_ref().is_some_and(expr_has_subquery) {
return None;
}
for stmt in &ct.body {
let safe = match stmt {
Statement::Select(sel) => select_single_source_ok(sel, table),
Statement::Insert(i) => {
i.schema.is_none()
&& i.returning.is_empty()
&& i.upsert.is_empty()
&& i.table.eq_ignore_ascii_case(table)
&& match &i.source {
InsertSource::DefaultValues => true,
InsertSource::Values(rows) => {
!rows.iter().any(|r| r.iter().any(expr_has_subquery))
}
InsertSource::Select(sel) => select_single_source_ok(sel, table),
}
}
Statement::Update(u) => {
u.schema.is_none()
&& u.from.is_none()
&& u.returning.is_empty()
&& u.table.eq_ignore_ascii_case(table)
&& u.row_assignments.is_empty()
&& !u.assignments.iter().any(|(_, e)| expr_has_subquery(e))
&& !u.where_clause.as_ref().is_some_and(expr_has_subquery)
&& !u.order_by.iter().any(|t| expr_has_subquery(&t.expr))
&& !u.limit.as_ref().is_some_and(expr_has_subquery)
&& !u.offset.as_ref().is_some_and(expr_has_subquery)
}
Statement::Delete(d) => {
d.schema.is_none()
&& d.returning.is_empty()
&& d.table.eq_ignore_ascii_case(table)
&& !d.where_clause.as_ref().is_some_and(expr_has_subquery)
&& !d.order_by.iter().any(|t| expr_has_subquery(&t.expr))
&& !d.limit.as_ref().is_some_and(expr_has_subquery)
&& !d.offset.as_ref().is_some_and(expr_has_subquery)
}
_ => false,
};
if !safe {
return None;
}
}
Some(alloc::vec![
table.to_string(),
String::from("NEW"),
String::from("OLD"),
])
}
fn trigger_body_single_source_over(trigger_sql: &str, table: &str, old: &str) -> bool {
let Ok(Statement::CreateTrigger(ct)) = sql::parse_one(trigger_sql) else {
return false;
};
if old.eq_ignore_ascii_case("new") || old.eq_ignore_ascii_case("old") {
return false;
}
if ct.when.as_ref().is_some_and(expr_has_subquery) {
return false;
}
for stmt in &ct.body {
let safe = match stmt {
Statement::Select(sel) => select_single_source_ok(sel, table),
Statement::Insert(i) => {
i.schema.is_none()
&& i.returning.is_empty()
&& i.upsert.is_empty()
&& i.table.eq_ignore_ascii_case(table)
&& match &i.source {
InsertSource::DefaultValues => true,
InsertSource::Values(rows) => {
!rows.iter().any(|r| r.iter().any(expr_has_subquery))
}
InsertSource::Select(sel) => select_single_source_ok(sel, table),
}
}
Statement::Update(u) => {
u.schema.is_none()
&& u.from.is_none()
&& u.returning.is_empty()
&& u.table.eq_ignore_ascii_case(table)
&& u.row_assignments.is_empty()
&& !u.assignments.iter().any(|(_, e)| expr_has_subquery(e))
&& !u.where_clause.as_ref().is_some_and(expr_has_subquery)
}
Statement::Delete(d) => {
d.schema.is_none()
&& d.returning.is_empty()
&& d.table.eq_ignore_ascii_case(table)
&& !d.where_clause.as_ref().is_some_and(expr_has_subquery)
}
_ => false,
};
if !safe {
return false;
}
}
!ct.body.is_empty()
}
fn trigger_on_renamed_table(trigger_sql: &str, table: &str, old: &str) -> bool {
matches!(sql::parse_one(trigger_sql), Ok(Statement::CreateTrigger(ct))
if ct.table.eq_ignore_ascii_case(table)
&& !old.eq_ignore_ascii_case(table)
&& !old.eq_ignore_ascii_case("new")
&& !old.eq_ignore_ascii_case("old"))
}
fn rewrite_column_tokens(
sql: &str,
quals: &[String],
old: &str,
rendered: &str,
rewrite_bare: bool,
) -> String {
use sql::token::Token;
let toks = match sql::token::tokenize(sql) {
Ok(t) => t,
Err(_) => return String::from(sql),
};
let mut out = String::new();
let mut cursor = 0usize;
for (i, sp) in toks.iter().enumerate() {
let hit =
matches!(&sp.token, Token::Word(w) | Token::Ident(w) if w.eq_ignore_ascii_case(old));
if !hit {
continue;
}
if toks
.get(i + 1)
.is_some_and(|n| matches!(n.token, Token::LParen))
{
continue;
}
let after_dot = i > 0 && matches!(toks[i - 1].token, Token::Dot);
if after_dot {
let qual_ok = i >= 2
&& matches!(&toks[i - 2].token, Token::Word(q) | Token::Ident(q)
if quals.iter().any(|t| t.eq_ignore_ascii_case(q)));
if !qual_ok {
continue;
}
} else if !rewrite_bare {
continue;
}
out.push_str(&sql[cursor..sp.start]);
out.push_str(rendered);
cursor = sp.end;
}
out.push_str(&sql[cursor..]);
out
}
fn rewrite_ident_tokens(sql: &str, old: &str, rendered: &str) -> String {
let toks = match sql::token::tokenize(sql) {
Ok(t) => t,
Err(_) => return String::from(sql),
};
let mut out = String::new();
let mut cursor = 0usize;
for (i, sp) in toks.iter().enumerate() {
let hit = matches!(
&sp.token,
sql::token::Token::Word(w) | sql::token::Token::Ident(w) if w.eq_ignore_ascii_case(old)
);
if !hit {
continue;
}
let after_dot = i > 0 && matches!(toks[i - 1].token, sql::token::Token::Dot);
let before_lparen = toks
.get(i + 1)
.is_some_and(|n| matches!(n.token, sql::token::Token::LParen));
if after_dot || before_lparen {
continue;
}
out.push_str(&sql[cursor..sp.start]);
out.push_str(rendered);
cursor = sp.end;
}
out.push_str(&sql[cursor..]);
out
}
fn rename_table_token_after(sql: &str, anchor: &str, new: &str) -> String {
use sql::token::Token;
let toks = match sql::token::tokenize(sql) {
Ok(t) => t,
Err(_) => return String::from(sql),
};
let kw = |t: &Token, k: &str| matches!(t, Token::Word(w) if w.eq_ignore_ascii_case(k));
let mut i = 0;
while i < toks.len() && !kw(&toks[i].token, anchor) {
i += 1;
}
i += 1;
if i + 2 < toks.len()
&& kw(&toks[i].token, "if")
&& kw(&toks[i + 1].token, "not")
&& kw(&toks[i + 2].token, "exists")
{
i += 3;
}
if i + 1 < toks.len() && matches!(toks[i + 1].token, Token::Dot) {
i += 2;
}
let Some(sp) = toks.get(i) else {
return String::from(sql);
};
let mut out = String::with_capacity(sql.len() + new.len());
out.push_str(&sql[..sp.start]);
out.push_str(&sql::print::ident(new));
out.push_str(&sql[sp.end..]);
out
}
fn rewrite_fk_references(sql: &str, old: &str, new: &str) -> String {
use sql::token::Token;
let toks = match sql::token::tokenize(sql) {
Ok(t) => t,
Err(_) => return String::from(sql),
};
let mut out = String::new();
let mut cursor = 0usize;
for (i, sp) in toks.iter().enumerate() {
if !matches!(&sp.token, Token::Word(w) if w.eq_ignore_ascii_case("references")) {
continue;
}
let Some(target) = toks.get(i + 1) else {
continue;
};
if matches!(&target.token, Token::Word(w) | Token::Ident(w) if w.eq_ignore_ascii_case(old))
{
out.push_str(&sql[cursor..target.start]);
out.push_str(&sql::print::ident(new));
cursor = target.end;
}
}
out.push_str(&sql[cursor..]);
out
}
fn append_column_to_create(sql: &str, col_text: &str) -> Option<String> {
use sql::token::Token;
let toks = sql::token::tokenize(sql).ok()?;
let open = toks.iter().position(|t| matches!(t.token, Token::LParen))?;
let mut depth = 0i32;
let mut close = None;
for (i, sp) in toks.iter().enumerate().skip(open) {
match sp.token {
Token::LParen => depth += 1,
Token::RParen => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let pos = toks[close?].start;
let mut out = String::with_capacity(sql.len() + col_text.len() + 2);
out.push_str(&sql[..pos]);
out.push_str(", ");
out.push_str(col_text.trim());
out.push_str(&sql[pos..]);
Some(out)
}
fn drop_column_from_create(sql: &str, col: &str) -> Option<String> {
use sql::token::Token;
let toks = sql::token::tokenize(sql).ok()?;
let open = toks.iter().position(|t| matches!(t.token, Token::LParen))?;
let mut depth = 0i32;
let mut close = None;
let mut seps = Vec::new();
for (i, sp) in toks.iter().enumerate().skip(open) {
match sp.token {
Token::LParen => depth += 1,
Token::RParen => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
Token::Comma if depth == 1 => seps.push(i),
_ => {}
}
}
let close = close?;
let mut bounds = alloc::vec![open];
bounds.extend_from_slice(&seps);
bounds.push(close);
let n = bounds.len() - 1; let is_named = |i: usize| {
matches!(&toks.get(i).map(|t| &t.token),
Some(Token::Word(w) | Token::Ident(w)) if w.eq_ignore_ascii_case(col))
};
let j = (0..n).find(|&j| bounds[j] + 1 < bounds[j + 1] && is_named(bounds[j] + 1))?;
let (del_start, del_end) = if j < n - 1 {
(toks[bounds[j] + 1].start, toks[bounds[j + 1] + 1].start)
} else {
(toks[bounds[j]].start, toks[close - 1].end)
};
let mut out = String::with_capacity(sql.len());
out.push_str(&sql[..del_start]);
out.push_str(&sql[del_end..]);
Some(out)
}
fn expr_label(expr: &Expr) -> String {
match expr {
Expr::Column { column, .. } => column.clone(),
Expr::Literal(Literal::Integer(i)) => i.to_string(),
Expr::Literal(Literal::Str(s)) => s.clone(),
Expr::Function { name, .. } => name.clone(),
Expr::Paren(e) => expr_label(e),
_ => "expr".to_string(),
}
}
fn result_column_label(expr: &Expr, alias: &Option<String>, source: &Option<String>) -> String {
if let Some(a) = alias {
return a.clone();
}
match expr {
Expr::Column { column, .. } => column.clone(),
_ => source.clone().unwrap_or_else(|| expr_label(expr)),
}
}
fn wr_storage_collations(meta: &TableMeta) -> Vec<crate::value::Collation> {
meta.storage_order
.iter()
.map(|&c| meta.columns[c].collation)
.collect()
}
fn column_collation(col: &ColumnDef) -> crate::value::Collation {
col.constraints
.iter()
.find_map(|c| match c {
ColumnConstraint::Collate(name) => crate::value::Collation::parse(name),
_ => None,
})
.unwrap_or_default()
}
fn collect_unique_sets(ct: &CreateTable, ipk: Option<usize>) -> Vec<(Vec<usize>, OnConflict)> {
let col_pos = |name: &str| {
ct.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(name))
};
let mut unique: Vec<(Vec<usize>, OnConflict)> = Vec::new();
for (i, c) in ct.columns.iter().enumerate() {
for k in &c.constraints {
match k {
ColumnConstraint::Unique(oc) => unique.push((alloc::vec![i], *oc)),
ColumnConstraint::PrimaryKey { on_conflict, .. } if Some(i) != ipk => {
unique.push((alloc::vec![i], *on_conflict))
}
_ => {}
}
}
}
for tc in &ct.constraints {
let (names, oc) = match tc {
TableConstraint::Unique(n, oc) | TableConstraint::PrimaryKey(n, oc) => (n, *oc),
_ => continue,
};
let idxs: Option<Vec<usize>> = names.iter().map(|n| col_pos(n)).collect();
if let Some(set) = idxs {
if !(set.len() == 1 && Some(set[0]) == ipk) {
unique.push((set, oc));
}
}
}
unique
}
fn permute_row(meta: &TableMeta, declared: &[Value]) -> Vec<Value> {
meta.storage_order
.iter()
.map(|&i| declared[i].clone())
.collect()
}
fn auto_vacuum_mode(header: &crate::format::DatabaseHeader) -> u32 {
if header.largest_root_page == 0 {
0
} else if header.incremental_vacuum == 0 {
1
} else {
2
}
}
fn strip_schema_qualifier(sql: &str, schema: &str) -> Result<String> {
use crate::sql::token::Token;
let toks = crate::sql::token::tokenize(sql)?;
for (i, t) in toks.iter().enumerate() {
if i == 0 || !matches!(t.token, Token::Dot) {
continue;
}
let lead = match &toks[i - 1].token {
Token::Word(s) | Token::Ident(s) => Some(s.as_str()),
_ => None,
};
if lead.is_some_and(|s| s.eq_ignore_ascii_case(schema)) {
let schema_start = toks[i - 1].start;
let name_start = toks.get(i + 1).map_or(sql.len(), |s| s.start);
let mut out = String::with_capacity(sql.len());
out.push_str(&sql[..schema_start]);
out.push_str(&sql[name_start..]);
return Ok(out);
}
break;
}
Ok(sql.into())
}
fn unpermute_row(meta: &TableMeta, storage: Vec<Value>) -> Vec<Value> {
let mut row = alloc::vec![Value::Null; meta.columns.len()];
for (k, &col) in meta.storage_order.iter().enumerate() {
if let Some(v) = storage.get(k) {
row[col] = v.clone();
}
}
row
}
fn primary_key_positions(ct: &CreateTable) -> Vec<usize> {
for (i, c) in ct.columns.iter().enumerate() {
if c.constraints
.iter()
.any(|k| matches!(k, ColumnConstraint::PrimaryKey { .. }))
{
return alloc::vec![i];
}
}
for tc in &ct.constraints {
if let TableConstraint::PrimaryKey(names, _) = tc {
let pos: Option<Vec<usize>> = names
.iter()
.map(|n| {
ct.columns
.iter()
.position(|c| c.name.eq_ignore_ascii_case(n))
})
.collect();
if let Some(pos) = pos {
return pos;
}
}
}
Vec::new()
}
fn autoindex_number(name: &str, table: &str) -> Option<usize> {
let prefix = alloc::format!("sqlite_autoindex_{table}_");
name.strip_prefix(&prefix)?.parse::<usize>().ok()
}
fn find_integer_primary_key(ct: &CreateTable) -> Option<usize> {
for (i, c) in ct.columns.iter().enumerate() {
let is_integer = c
.type_name
.as_deref()
.is_some_and(|t| t.eq_ignore_ascii_case("integer"));
let is_pk_alias = c.constraints.iter().any(|k| {
matches!(
k,
ColumnConstraint::PrimaryKey {
descending: false,
..
}
)
});
if is_integer && is_pk_alias {
return Some(i);
}
}
for tc in &ct.constraints {
if let TableConstraint::PrimaryKey(cols, _) = tc {
if cols.len() == 1 {
if let Some(i) = ct.columns.iter().position(|c| c.name == cols[0]) {
if ct.columns[i]
.type_name
.as_deref()
.is_some_and(|t| t.eq_ignore_ascii_case("integer"))
{
return Some(i);
}
}
}
}
}
None
}
#[cfg(all(test, feature = "fts5", feature = "std"))]
mod fts5_index_route_tests {
use super::Connection;
use crate::fts5_index::INDEX_ROUTE_HITS;
use crate::value::Value;
use core::sync::atomic::Ordering;
use std::sync::Mutex;
static SERIALIZE: Mutex<()> = Mutex::new(());
fn texts(c: &mut Connection, sql: &str) -> alloc::vec::Vec<alloc::string::String> {
c.query(sql)
.unwrap()
.rows
.into_iter()
.map(|r| match &r[0] {
Value::Text(s) => s.clone(),
other => alloc::format!("{other:?}"),
})
.collect()
}
#[test]
fn bare_term_match_takes_index_route() {
let _guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(body)")
.unwrap();
for (i, body) in [
"the quick brown fox",
"lazy dog sleeps",
"fox and hound",
"nothing relevant here",
"a quick test",
]
.iter()
.enumerate()
{
c.execute(&alloc::format!(
"INSERT INTO t(rowid, body) VALUES({}, '{}')",
i + 1,
body
))
.unwrap();
}
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows = texts(&mut c, "SELECT body FROM t WHERE t MATCH 'fox'");
let after = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
assert!(after > before, "bare-term MATCH must take the index route");
assert_eq!(rows, ["the quick brown fox", "fox and hound"]);
assert_eq!(
texts(&mut c, "SELECT body FROM t WHERE t MATCH 'quick'"),
["the quick brown fox", "a quick test"]
);
assert!(texts(&mut c, "SELECT body FROM t WHERE t MATCH 'zebra'").is_empty());
}
#[test]
fn non_bare_shapes_stay_on_scan() {
let _guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(body)")
.unwrap();
for (i, body) in ["quick brown fox", "slow brown bear", "quick red fox"]
.iter()
.enumerate()
{
c.execute(&alloc::format!(
"INSERT INTO t(rowid, body) VALUES({}, '{}')",
i + 1,
body
))
.unwrap();
}
for q in [
"\"quick brown\" OR fox",
"^\"quick brown\"",
"^qui*", "qui* AND fox", "NEAR(quick brown fox, 3)", "NEAR(\"quick brown\" fox)", "NEAR(quick fo*)", "fox AND NEAR(quick brown)", "\"quick brown\" OR bear", "title : quick OR fox", "quick AND brown AND qui*", "(quick OR brown) AND fox*", "quick AND NEAR(brown fox, 2)", ] {
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let sql = alloc::format!("SELECT body FROM t WHERE t MATCH '{q}'");
let _ = c.query(&sql).unwrap();
let after = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
assert_eq!(after, before, "query {q:?} must stay on the scan");
}
}
#[test]
fn two_term_phrase_match_takes_index_route() {
let _guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(title, body)")
.unwrap();
let docs = [
("the quick brown fox", "nothing here"),
("quick red brown fox", "all separate words"),
("brown then quick", "reversed order only"),
("plain title text", "a quick brown hare"),
("quick", "brown"), ];
for (i, (title, body)) in docs.iter().enumerate() {
c.execute(&alloc::format!(
"INSERT INTO t(rowid, title, body) VALUES({}, '{}', '{}')",
i + 1,
title,
body
))
.unwrap();
}
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH '\"quick brown\"' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"table-wide phrase must take the index route"
);
assert_eq!(rows, [1, 4]);
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH 'body : \"quick brown\"' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"column-scoped phrase must take the index route"
);
assert_eq!(rows, [4]);
}
#[test]
fn k_term_phrase_match_takes_index_route() {
let _guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(title, body)")
.unwrap();
let docs = [
("the quick brown fox runs", "nothing here at all"),
("quick red brown gray fox", "all separate words here"),
("fox brown quick reversed", "still reversed only here"),
("plain title text here", "a quick brown fox hops"),
("ends with quick brown", "fox starts the body now"),
("na na na batman here", "plain body without it now"),
];
for (i, (title, body)) in docs.iter().enumerate() {
c.execute(&alloc::format!(
"INSERT INTO t(rowid, title, body) VALUES({}, '{}', '{}')",
i + 1,
title,
body
))
.unwrap();
}
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH '\"quick brown fox\"' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"table-wide K-term phrase must take the index route"
);
assert_eq!(rows, [1, 4]);
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH 'body : \"quick brown fox\"' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"column-scoped K-term phrase must take the index route"
);
assert_eq!(rows, [4]);
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH '\"na na na\"' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"repeated-word K-term phrase must take the index route"
);
assert_eq!(rows, [6]);
}
#[test]
fn two_term_near_match_takes_index_route() {
let _guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(title, body)")
.unwrap();
let docs = [
("alpha beta", "nothing here"), ("alpha x beta", "irrelevant"), ("alpha x y beta", "irrelevant"), ("alpha only here", "no second term"), ("alpha here", "beta there"), ("plain title", "alpha beta close"), ];
for (i, (title, body)) in docs.iter().enumerate() {
c.execute(&alloc::format!(
"INSERT INTO t(rowid, title, body) VALUES({}, '{}', '{}')",
i + 1,
title,
body
))
.unwrap();
}
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH 'NEAR(alpha beta, 1)' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"two-term NEAR must take the index route"
);
assert_eq!(rows, [1, 2, 6]);
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH 'NEAR(alpha beta, 0)' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"two-term NEAR(.,0) must take the index route"
);
assert_eq!(rows, [1, 6]);
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH 'NEAR(alpha beta)' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"default-distance NEAR must take the index route"
);
assert_eq!(rows, [1, 2, 3, 6]);
}
#[test]
fn prefix_term_match_takes_index_route() {
let _guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(title, body)")
.unwrap();
let docs = [
("quick brown fox", "nothing here"),
("calm title", "quiet body now"),
("quick red fox", "all separate"),
("plain title", "no match in body"),
];
for (i, (title, body)) in docs.iter().enumerate() {
c.execute(&alloc::format!(
"INSERT INTO t(rowid, title, body) VALUES({}, '{}', '{}')",
i + 1,
title,
body
))
.unwrap();
}
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH 'qu*' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"table-wide prefix must take the index route"
);
assert_eq!(rows, [1, 2, 3]);
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows: alloc::vec::Vec<i64> = c
.query("SELECT rowid FROM t WHERE t MATCH 'title : qu*' ORDER BY rowid")
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect();
assert!(
INDEX_ROUTE_HITS.load(Ordering::Relaxed) > before,
"column-scoped prefix must take the index route"
);
assert_eq!(rows, [1, 3]);
}
#[test]
fn bare_term_boolean_tree_match_takes_index_route() {
let _guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(body)")
.unwrap();
let docs = [
"the quick brown fox", "lazy brown dog", "fox in the henhouse", "brown fox runs", "a fox and a dog", ];
for (i, body) in docs.iter().enumerate() {
c.execute(&alloc::format!(
"INSERT INTO t(rowid, body) VALUES({}, '{}')",
i + 1,
body
))
.unwrap();
}
let ids = |c: &mut Connection, sql: &str| -> alloc::vec::Vec<i64> {
c.query(sql)
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect()
};
for (q, want) in [
("fox AND brown", alloc::vec![1i64, 4]),
("fox OR dog", alloc::vec![1, 2, 3, 4, 5]),
("fox NOT brown", alloc::vec![3, 5]),
("brown NOT fox", alloc::vec![2]),
("fox brown", alloc::vec![1, 4]), ("zebra AND fox", alloc::vec![]), ("zebra OR dog", alloc::vec![2, 5]),
("fox AND brown AND dog", alloc::vec![]), ("fox OR brown OR dog", alloc::vec![1, 2, 3, 4, 5]), ("fox brown dog", alloc::vec![]), ("fox OR brown AND dog", alloc::vec![1, 2, 3, 4, 5]),
("(fox OR brown) AND dog", alloc::vec![2, 5]),
("(fox OR brown) NOT dog", alloc::vec![1, 3, 4]),
("fox AND brown NOT dog", alloc::vec![1, 4]),
] {
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows = ids(
&mut c,
&alloc::format!("SELECT rowid FROM t WHERE t MATCH '{q}' ORDER BY rowid"),
);
let after = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
assert!(after > before, "boolean {q:?} must take the index route");
assert_eq!(rows, want, "boolean {q:?}");
}
}
#[test]
fn column_scoped_bare_term_match_takes_index_route() {
let _guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
let mut c = Connection::open_memory().unwrap();
c.execute("CREATE VIRTUAL TABLE t USING fts5(title, body)")
.unwrap();
let docs = [
("the fox", "sleeps soundly"),
("a lazy dog", "chases a fox"),
("quiet night", "fox runs past"),
("fox tracks", "across the snow"),
("fox tale", "the fox returns"),
];
for (i, (title, body)) in docs.iter().enumerate() {
c.execute(&alloc::format!(
"INSERT INTO t(rowid, title, body) VALUES({}, '{}', '{}')",
i + 1,
title,
body
))
.unwrap();
}
let ids = |c: &mut Connection, sql: &str| -> alloc::vec::Vec<i64> {
c.query(sql)
.unwrap()
.rows
.into_iter()
.map(|r| match r[0] {
Value::Integer(i) => i,
ref o => panic!("non-integer rowid: {o:?}"),
})
.collect()
};
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows = ids(
&mut c,
"SELECT rowid FROM t WHERE t MATCH 'title : fox' ORDER BY rowid",
);
let after = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
assert!(
after > before,
"column-scoped MATCH must take the index route"
);
assert_eq!(rows, [1, 4, 5]);
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows = ids(
&mut c,
"SELECT rowid FROM t WHERE t MATCH 'body:fox' ORDER BY rowid",
);
let after = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
assert!(
after > before,
"compact `body:fox` must take the index route"
);
assert_eq!(rows, [2, 3, 5]);
let before = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
let rows = ids(
&mut c,
"SELECT rowid FROM t WHERE t MATCH 'nope:fox' ORDER BY rowid",
);
let after = INDEX_ROUTE_HITS.load(Ordering::Relaxed);
assert_eq!(after, before, "unknown-column filter must stay on the scan");
assert!(rows.is_empty());
}
}