use fsqlite_types::{
opcode::{Opcode, P4, VdbeOp},
record::PrecomputedRecordHeader,
value::SqliteValue,
};
#[derive(Debug, Clone)]
pub enum CompiledProgram {
ConstantResultRow(ConstantResultRowTemplate),
SimpleInsert(SimpleInsertTemplate),
RowidLookupSelect(RowidLookupSelectTemplate),
FullScanSelect(FullScanSelectTemplate),
}
#[derive(Debug, Clone)]
pub struct RowidLookupSelectTemplate {
pub cursor_id: i32,
pub root_page: i32,
pub column_indices: Vec<i32>,
pub rowid_source: InsertValueSource,
}
#[derive(Debug, Clone)]
pub struct FullScanSelectTemplate {
pub cursor_id: i32,
pub root_page: i32,
pub column_indices: Vec<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InsertValueSource {
Binding(usize),
Constant(SqliteValue),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompiledRecordBuilder {
Generic,
PrecomputedHeader(PrecomputedRecordHeader),
}
impl CompiledRecordBuilder {
fn from_make_record_p4(p4: &P4, num_cols: i32) -> Self {
let Ok(num_cols) = usize::try_from(num_cols) else {
return Self::Generic;
};
match p4 {
P4::PrecomputedHeader(header) if header.column_count() == num_cols => {
Self::PrecomputedHeader(header.clone())
}
_ => Self::Generic,
}
}
}
#[derive(Debug, Clone)]
pub struct ConstantResultRowTemplate {
pub values: Vec<SqliteValue>,
}
#[derive(Debug, Clone)]
pub struct SimpleInsertTemplate {
pub cursor_id: i32,
pub root_page: i32,
pub num_cols: i32,
pub value_sources: Vec<InsertValueSource>,
pub affinity: Option<String>,
pub record_p4: P4,
pub record_builder: CompiledRecordBuilder,
pub insert_flags: u16,
}
pub fn try_compile_program(ops: &[VdbeOp]) -> Option<CompiledProgram> {
try_compile_constant_result_row(ops)
.map(CompiledProgram::ConstantResultRow)
.or_else(|| try_compile_insert(ops).map(CompiledProgram::SimpleInsert))
.or_else(|| try_compile_rowid_lookup_select(ops).map(CompiledProgram::RowidLookupSelect))
.or_else(|| try_compile_full_scan_select(ops).map(CompiledProgram::FullScanSelect))
}
fn ensure_const_register(registers: &mut Vec<Option<SqliteValue>>, reg: i32, value: SqliteValue) {
if reg <= 0 {
return;
}
let idx = usize::try_from(reg).unwrap_or(0);
if registers.len() <= idx {
registers.resize(idx + 1, None);
}
registers[idx] = Some(value);
}
fn set_register_source(
sources: &mut Vec<Option<InsertValueSource>>,
reg: i32,
source: Option<InsertValueSource>,
) {
if reg <= 0 {
return;
}
let idx = usize::try_from(reg).unwrap_or(0);
if sources.len() <= idx {
sources.resize(idx + 1, None);
}
sources[idx] = source;
}
fn register_source(sources: &[Option<InsertValueSource>], reg: i32) -> Option<InsertValueSource> {
let idx = usize::try_from(reg).ok()?;
sources.get(idx).cloned().flatten()
}
fn try_compile_constant_result_row(ops: &[VdbeOp]) -> Option<ConstantResultRowTemplate> {
let mut registers: Vec<Option<SqliteValue>> = Vec::new();
let mut result_row = None;
for op in ops {
match op.opcode {
Opcode::Init
| Opcode::Goto
| Opcode::Noop
| Opcode::Transaction
| Opcode::Halt
| Opcode::Close => {}
Opcode::Integer => {
ensure_const_register(
&mut registers,
op.p2,
SqliteValue::Integer(i64::from(op.p1)),
);
}
Opcode::Int64 => {
let value = match &op.p4 {
P4::Int64(value) => *value,
P4::Int(value) => i64::from(*value),
_ => return None,
};
ensure_const_register(&mut registers, op.p2, SqliteValue::Integer(value));
}
Opcode::Real => {
let value = match &op.p4 {
P4::Real(value) => *value,
_ => return None,
};
ensure_const_register(&mut registers, op.p2, SqliteValue::Float(value));
}
Opcode::String | Opcode::String8 => {
let value = match &op.p4 {
P4::Str(value) => SqliteValue::Text(value.clone().into()),
_ => return None,
};
ensure_const_register(&mut registers, op.p2, value);
}
Opcode::Blob => {
let value = match &op.p4 {
P4::Blob(value) => SqliteValue::Blob(value.clone().into()),
_ => return None,
};
ensure_const_register(&mut registers, op.p2, value);
}
Opcode::Null => {
let end_reg = if op.p3 > 0 { op.p3 } else { op.p2 };
if end_reg < op.p2 {
return None;
}
for reg in op.p2..=end_reg {
ensure_const_register(&mut registers, reg, SqliteValue::Null);
}
}
Opcode::ResultRow => {
if result_row.is_some() {
return None;
}
let count = usize::try_from(op.p2).ok()?;
let mut values = Vec::with_capacity(count);
for offset in 0..count {
let reg = op.p1.checked_add(i32::try_from(offset).ok()?)?;
let value = registers
.get(usize::try_from(reg).ok()?)
.and_then(|value| value.clone())?;
values.push(value);
}
result_row = Some(ConstantResultRowTemplate { values });
}
_ => return None,
}
}
result_row
}
fn find_value_sources(
ops: &[VdbeOp],
first_col_reg: i32,
num_cols: i32,
) -> Option<Vec<InsertValueSource>> {
if first_col_reg <= 0 || num_cols <= 0 {
return None;
}
let num_cols_usize = usize::try_from(num_cols).ok()?;
let mut sources_by_reg = Vec::new();
for op in ops {
match op.opcode {
Opcode::Variable => {
let binding_index = usize::try_from(op.p1).ok()?.checked_sub(1)?;
set_register_source(
&mut sources_by_reg,
op.p2,
Some(InsertValueSource::Binding(binding_index)),
);
}
Opcode::Copy => {
for offset in 0..=op.p3 {
let source = register_source(&sources_by_reg, op.p1 + offset);
set_register_source(&mut sources_by_reg, op.p2 + offset, source);
}
}
Opcode::SCopy => {
let source = register_source(&sources_by_reg, op.p1);
set_register_source(&mut sources_by_reg, op.p2, source);
}
Opcode::Integer => {
set_register_source(
&mut sources_by_reg,
op.p2,
Some(InsertValueSource::Constant(SqliteValue::Integer(
i64::from(op.p1),
))),
);
}
Opcode::Int64 => {
let value = match &op.p4 {
P4::Int64(value) => *value,
P4::Int(value) => i64::from(*value),
_ => return None,
};
set_register_source(
&mut sources_by_reg,
op.p2,
Some(InsertValueSource::Constant(SqliteValue::Integer(value))),
);
}
Opcode::Real => {
let value = match &op.p4 {
P4::Real(value) => *value,
_ => return None,
};
set_register_source(
&mut sources_by_reg,
op.p2,
Some(InsertValueSource::Constant(SqliteValue::Float(value))),
);
}
Opcode::String | Opcode::String8 => {
let value = match &op.p4 {
P4::Str(value) => SqliteValue::Text(value.clone().into()),
_ => return None,
};
set_register_source(
&mut sources_by_reg,
op.p2,
Some(InsertValueSource::Constant(value)),
);
}
Opcode::Blob => {
let value = match &op.p4 {
P4::Blob(value) => SqliteValue::Blob(value.clone().into()),
_ => return None,
};
set_register_source(
&mut sources_by_reg,
op.p2,
Some(InsertValueSource::Constant(value)),
);
}
Opcode::Null => {
let end_reg = if op.p3 > 0 { op.p3 } else { op.p2 };
if end_reg < op.p2 {
return None;
}
for reg in op.p2..=end_reg {
set_register_source(
&mut sources_by_reg,
reg,
Some(InsertValueSource::Constant(SqliteValue::Null)),
);
}
}
_ => {}
}
}
(0..num_cols_usize)
.map(|offset| register_source(&sources_by_reg, first_col_reg + i32::try_from(offset).ok()?))
.collect()
}
fn find_affinity(ops: &[VdbeOp], first_col_reg: i32, num_cols: i32) -> Option<String> {
ops.iter().find_map(|op| {
if op.opcode != Opcode::Affinity || op.p1 != first_col_reg || op.p2 != num_cols {
return None;
}
match &op.p4 {
P4::Affinity(affinity) => Some(affinity.clone()),
_ => None,
}
})
}
pub fn try_compile_insert(ops: &[VdbeOp]) -> Option<SimpleInsertTemplate> {
for op in ops {
match op.opcode {
Opcode::Init
| Opcode::Transaction
| Opcode::OpenWrite
| Opcode::Variable
| Opcode::Copy
| Opcode::SCopy
| Opcode::Null
| Opcode::Integer
| Opcode::Int64
| Opcode::Real
| Opcode::String
| Opcode::String8
| Opcode::Blob
| Opcode::NewRowid
| Opcode::Affinity
| Opcode::MakeRecord
| Opcode::Insert
| Opcode::FusedAppendInsert
| Opcode::Close
| Opcode::Halt
| Opcode::Goto
| Opcode::Noop => {}
_ => return None,
}
}
if let Some(fused) = ops.iter().find(|op| op.opcode == Opcode::FusedAppendInsert) {
let value_sources = find_value_sources(ops, fused.p2, fused.p3)?;
return Some(SimpleInsertTemplate {
cursor_id: fused.p1,
root_page: find_root_page(ops, fused.p1)?,
num_cols: fused.p3,
value_sources,
affinity: find_affinity(ops, fused.p2, fused.p3),
record_p4: fused.p4.clone(),
record_builder: CompiledRecordBuilder::from_make_record_p4(&fused.p4, fused.p3),
insert_flags: fused.p5,
});
}
let new_rowid = ops.iter().find(|op| op.opcode == Opcode::NewRowid)?;
let make_record = ops.iter().find(|op| op.opcode == Opcode::MakeRecord)?;
let insert = ops.iter().find(|op| op.opcode == Opcode::Insert)?;
if new_rowid.p1 != insert.p1 {
return None; }
if make_record.p3 != insert.p2 {
return None; }
let oe_flag = insert.p5 & 0x0F;
if oe_flag != 2 {
return None; }
let value_sources = find_value_sources(ops, make_record.p1, make_record.p2)?;
Some(SimpleInsertTemplate {
cursor_id: new_rowid.p1,
root_page: find_root_page(ops, new_rowid.p1)?,
num_cols: make_record.p2,
value_sources,
affinity: find_affinity(ops, make_record.p1, make_record.p2),
record_p4: make_record.p4.clone(),
record_builder: CompiledRecordBuilder::from_make_record_p4(&make_record.p4, make_record.p2),
insert_flags: insert.p5,
})
}
fn find_root_page(ops: &[VdbeOp], cursor_id: i32) -> Option<i32> {
ops.iter()
.find(|op| {
(op.opcode == Opcode::OpenWrite || op.opcode == Opcode::FusedOpenWriteLast)
&& op.p1 == cursor_id
})
.map(|op| op.p2)
}
fn find_read_root_page(ops: &[VdbeOp], cursor_id: i32) -> Option<i32> {
ops.iter()
.find(|op| op.opcode == Opcode::OpenRead && op.p1 == cursor_id)
.map(|op| op.p2)
}
fn try_compile_rowid_lookup_select(ops: &[VdbeOp]) -> Option<RowidLookupSelectTemplate> {
for op in ops {
match op.opcode {
Opcode::Init
| Opcode::Transaction
| Opcode::Goto
| Opcode::Noop
| Opcode::OpenRead
| Opcode::Close
| Opcode::Halt
| Opcode::Integer
| Opcode::Int64
| Opcode::Real
| Opcode::String
| Opcode::String8
| Opcode::Null
| Opcode::Variable
| Opcode::Copy
| Opcode::SCopy
| Opcode::Move
| Opcode::SeekRowid
| Opcode::NotExists
| Opcode::Column
| Opcode::ResultRow
| Opcode::Affinity
| Opcode::ReadCookie
| Opcode::TableLock
| Opcode::RealAffinity => {}
_ => return None,
}
}
let seek = ops
.iter()
.find(|op| op.opcode == Opcode::SeekRowid || op.opcode == Opcode::NotExists)?;
let cursor_id = seek.p1;
let rowid_reg = seek.p3;
let root_page = find_read_root_page(ops, cursor_id)?;
let rowid_source = resolve_register_source(ops, rowid_reg)?;
let result_row = ops.iter().find(|op| op.opcode == Opcode::ResultRow)?;
let first_result_reg = result_row.p1;
let result_count = usize::try_from(result_row.p2).ok()?;
let mut column_indices = Vec::with_capacity(result_count);
for offset in 0..i32::try_from(result_count).ok()? {
let dest_reg = first_result_reg + offset;
let col_op = ops
.iter()
.find(|op| op.opcode == Opcode::Column && op.p1 == cursor_id && op.p3 == dest_reg)?;
column_indices.push(col_op.p2);
}
Some(RowidLookupSelectTemplate {
cursor_id,
root_page,
column_indices,
rowid_source,
})
}
fn resolve_register_source(ops: &[VdbeOp], reg: i32) -> Option<InsertValueSource> {
for op in ops.iter().rev() {
match op.opcode {
Opcode::Variable if op.p2 == reg => {
let binding_index = usize::try_from(op.p1).ok()?.checked_sub(1)?;
return Some(InsertValueSource::Binding(binding_index));
}
Opcode::Integer if op.p2 == reg => {
return Some(InsertValueSource::Constant(SqliteValue::Integer(
i64::from(op.p1),
)));
}
Opcode::Int64 if op.p2 == reg => {
let value = match &op.p4 {
P4::Int64(v) => *v,
P4::Int(v) => i64::from(*v),
_ => return None,
};
return Some(InsertValueSource::Constant(SqliteValue::Integer(value)));
}
Opcode::SCopy if op.p2 == reg => {
return resolve_register_source(ops, op.p1);
}
Opcode::Copy if op.p2 == reg && op.p3 == 0 => {
return resolve_register_source(ops, op.p1);
}
_ => {}
}
}
None
}
fn try_compile_full_scan_select(ops: &[VdbeOp]) -> Option<FullScanSelectTemplate> {
for op in ops {
match op.opcode {
Opcode::Init
| Opcode::Transaction
| Opcode::Goto
| Opcode::Noop
| Opcode::OpenRead
| Opcode::Close
| Opcode::Halt
| Opcode::Rewind
| Opcode::Next
| Opcode::Column
| Opcode::ResultRow
| Opcode::Affinity
| Opcode::ReadCookie
| Opcode::TableLock
| Opcode::RealAffinity
| Opcode::Integer
| Opcode::Null => {}
_ => return None,
}
}
let rewind = ops.iter().find(|op| op.opcode == Opcode::Rewind)?;
let cursor_id = rewind.p1;
let root_page = find_read_root_page(ops, cursor_id)?;
ops.iter()
.find(|op| op.opcode == Opcode::Next && op.p1 == cursor_id)?;
let result_row = ops.iter().find(|op| op.opcode == Opcode::ResultRow)?;
let first_result_reg = result_row.p1;
let result_count = usize::try_from(result_row.p2).ok()?;
let mut column_indices = Vec::with_capacity(result_count);
for offset in 0..i32::try_from(result_count).ok()? {
let dest_reg = first_result_reg + offset;
let col_op = ops
.iter()
.find(|op| op.opcode == Opcode::Column && op.p1 == cursor_id && op.p3 == dest_reg)?;
column_indices.push(col_op.p2);
}
Some(FullScanSelectTemplate {
cursor_id,
root_page,
column_indices,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_compile_insert_accepts_copy_remapped_bindings() {
let ops = vec![
VdbeOp {
opcode: Opcode::Init,
p1: 0,
p2: 13,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Transaction,
p1: 0,
p2: 1,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::OpenWrite,
p1: 0,
p2: 2,
p3: 0,
p4: P4::Table("jit_hot_query".to_owned()),
p5: 0,
},
VdbeOp {
opcode: Opcode::Variable,
p1: 1,
p2: 2,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Variable,
p1: 2,
p2: 3,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Copy,
p1: 2,
p2: 5,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Copy,
p1: 3,
p2: 6,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::NewRowid,
p1: 0,
p2: 1,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Affinity,
p1: 5,
p2: 2,
p3: 0,
p4: P4::Affinity("DB".to_owned()),
p5: 0,
},
VdbeOp {
opcode: Opcode::MakeRecord,
p1: 5,
p2: 2,
p3: 4,
p4: P4::Affinity("DB".to_owned()),
p5: 0,
},
VdbeOp {
opcode: Opcode::Insert,
p1: 0,
p2: 4,
p3: 1,
p4: P4::Table("jit_hot_query".to_owned()),
p5: 2,
},
VdbeOp {
opcode: Opcode::Close,
p1: 0,
p2: 0,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Halt,
p1: 0,
p2: 0,
p3: 0,
p4: P4::None,
p5: 0,
},
];
let template = try_compile_insert(&ops).expect("copy-remapped insert should compile");
assert_eq!(template.cursor_id, 0);
assert_eq!(template.root_page, 2);
assert_eq!(template.num_cols, 2);
assert_eq!(
template.value_sources,
vec![InsertValueSource::Binding(0), InsertValueSource::Binding(1)]
);
assert_eq!(template.affinity.as_deref(), Some("DB"));
assert_eq!(template.record_p4, P4::Affinity("DB".to_owned()));
assert_eq!(template.record_builder, CompiledRecordBuilder::Generic);
assert_eq!(template.insert_flags, 2);
}
#[test]
fn try_compile_insert_keeps_precomputed_record_builder() {
let header = PrecomputedRecordHeader::new(&[
fsqlite_types::record::PrecomputedSerialTypeKind::NullPlaceholder,
fsqlite_types::record::PrecomputedSerialTypeKind::IntegerOrNull,
fsqlite_types::record::PrecomputedSerialTypeKind::RealOrNull,
]);
let ops = vec![
VdbeOp {
opcode: Opcode::OpenWrite,
p1: 0,
p2: 2,
p3: 0,
p4: P4::Table("jit_hot_query".to_owned()),
p5: 0,
},
VdbeOp {
opcode: Opcode::Variable,
p1: 1,
p2: 2,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Variable,
p1: 2,
p2: 3,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Null,
p1: 0,
p2: 1,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::NewRowid,
p1: 0,
p2: 1,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::MakeRecord,
p1: 1,
p2: 3,
p3: 4,
p4: P4::PrecomputedHeader(header.clone()),
p5: 0,
},
VdbeOp {
opcode: Opcode::Insert,
p1: 0,
p2: 4,
p3: 1,
p4: P4::Table("jit_hot_query".to_owned()),
p5: 2,
},
VdbeOp {
opcode: Opcode::Halt,
p1: 0,
p2: 0,
p3: 0,
p4: P4::None,
p5: 0,
},
];
let template = try_compile_insert(&ops).expect("precomputed INSERT should compile");
assert_eq!(
template.record_builder,
CompiledRecordBuilder::PrecomputedHeader(header)
);
}
#[test]
fn try_compile_rowid_lookup_select_from_seek_pattern() {
let ops = vec![
VdbeOp {
opcode: Opcode::Init,
p1: 0,
p2: 8,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Transaction,
p1: 0,
p2: 0,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::OpenRead,
p1: 0,
p2: 5,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Variable,
p1: 1,
p2: 1,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::SeekRowid,
p1: 0,
p2: 7,
p3: 1,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Column,
p1: 0,
p2: 0,
p3: 2,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Column,
p1: 0,
p2: 1,
p3: 3,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::ResultRow,
p1: 2,
p2: 2,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Close,
p1: 0,
p2: 0,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Halt,
p1: 0,
p2: 0,
p3: 0,
p4: P4::None,
p5: 0,
},
];
let compiled = try_compile_program(&ops);
let template = match compiled {
Some(CompiledProgram::RowidLookupSelect(t)) => t,
other => panic!("expected RowidLookupSelect, got {other:?}"),
};
assert_eq!(template.cursor_id, 0);
assert_eq!(template.root_page, 5);
assert_eq!(template.column_indices, vec![0, 1]);
assert_eq!(template.rowid_source, InsertValueSource::Binding(0));
}
#[test]
fn try_compile_full_scan_select_from_rewind_next_pattern() {
let ops = vec![
VdbeOp {
opcode: Opcode::Init,
p1: 0,
p2: 9,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Transaction,
p1: 0,
p2: 0,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::OpenRead,
p1: 0,
p2: 3,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Rewind,
p1: 0,
p2: 8,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Column,
p1: 0,
p2: 0,
p3: 1,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Column,
p1: 0,
p2: 1,
p3: 2,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::ResultRow,
p1: 1,
p2: 2,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Next,
p1: 0,
p2: 4,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Close,
p1: 0,
p2: 0,
p3: 0,
p4: P4::None,
p5: 0,
},
VdbeOp {
opcode: Opcode::Halt,
p1: 0,
p2: 0,
p3: 0,
p4: P4::None,
p5: 0,
},
];
let compiled = try_compile_program(&ops);
let template = match compiled {
Some(CompiledProgram::FullScanSelect(t)) => t,
other => panic!("expected FullScanSelect, got {other:?}"),
};
assert_eq!(template.cursor_id, 0);
assert_eq!(template.root_page, 3);
assert_eq!(template.column_indices, vec![0, 1]);
}
}