Merge 'Add BeginSubrtn, NotFound and Affinity bytecodes' from Diego Reis

I'm working on an optimization of `WHERE .. IN (..)` statements that
requires these bytecodes.
```sh
sqlite> explain select * from users where first_name in ('alice', 'bob', 'charlie');
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     35    0                    0   Start at 35
1     OpenRead       0     2     0     10             0   root=2 iDb=0; users
2     Rewind         0     34    0                    0
3       Noop           0     0     0                    0   begin IN expr
4       BeginSubrtn    0     1     0                    0   r[1]=NULL <---- Here
5         Once           0     17    0                    0
6         OpenEphemeral  1     1     0     k(1,B)         0   nColumn=1; RHS of IN operator
7         String8        0     2     0     alice          0   r[2]='alice'
8         MakeRecord     2     1     3     B              0   r[3]=mkrec(r[2])
9         IdxInsert      1     3     2     1              0   key=r[3]
10        String8        0     2     0     bob            0   r[2]='bob'
11        MakeRecord     2     1     3     B              0   r[3]=mkrec(r[2])
12        IdxInsert      1     3     2     1              0   key=r[3]
13        String8        0     2     0     charlie        0   r[2]='charlie'
14        MakeRecord     2     1     3     B              0   r[3]=mkrec(r[2])
15        IdxInsert      1     3     2     1              0   key=r[3]
16        NullRow        1     0     0                    0
17      Return         1     5     1                    0
18      Column         0     1     4                    0   r[4]= cursor 0 column 1
19      IsNull         4     33    0                    0   if r[4]==NULL goto 33
20      Affinity       4     1     0     B              0   affinity(r[4]) <---- Here
21      NotFound       1     33    4     1              0   key=r[4]; end IN expr <---- Here
22      Rowid          0     5     0                    0   r[5]=users.rowid
23      Column         0     1     6                    0   r[6]= cursor 0 column 1
24      Column         0     2     7                    0   r[7]= cursor 0 column 2
25      Column         0     3     8                    0   r[8]= cursor 0 column 3
26      Column         0     4     9                    0   r[9]= cursor 0 column 4
27      Column         0     5     10                   0   r[10]= cursor 0 column 5
28      Column         0     6     11                   0   r[11]= cursor 0 column 6
29      Column         0     7     12                   0   r[12]= cursor 0 column 7
30      Column         0     8     13                   0   r[13]= cursor 0 column 8
31      Column         0     9     14                   0   r[14]= cursor 0 column 9
32      ResultRow      5     10    0                    0   output=r[5..14]
33    Next           0     3     0                    1
34    Halt           0     0     0                    0
35    Transaction    0     0     3     0              1   usesStmtJournal=0
36    Goto           0     1     0                    0
```
EDIT: [Found](https://sqlite.org/opcode.html#Found) and
[NoConflict](https://sqlite.org/opcode.html#NoConflict) can be easily
derived from `NotFound` but I wanted to be concise, I could do it in
another PR :)

Closes #1345
This commit is contained in:
Jussi Saurio
2025-04-15 20:25:55 +03:00
5 changed files with 180 additions and 2 deletions

View File

@@ -427,6 +427,7 @@ Modifiers:
| BitNot | Yes | |
| BitOr | Yes | |
| Blob | Yes | |
| BeginSubrtn | Yes | |
| Checkpoint | No | |
| Clear | No | |
| Close | No | |

View File

@@ -1,5 +1,5 @@
use crate::VirtualTable;
use crate::{util::normalize_ident, Result};
use crate::{LimboError, VirtualTable};
use core::fmt;
use fallible_iterator::FallibleIterator;
use limbo_sqlite3_parser::ast::{Expr, Literal, SortOrder, TableOptions};
@@ -585,6 +585,20 @@ impl Affinity {
Affinity::Numeric => SQLITE_AFF_NUMERIC,
}
}
pub fn from_char(char: char) -> Result<Self> {
match char {
SQLITE_AFF_INTEGER => Ok(Affinity::Integer),
SQLITE_AFF_TEXT => Ok(Affinity::Text),
SQLITE_AFF_NONE => Ok(Affinity::Blob),
SQLITE_AFF_REAL => Ok(Affinity::Real),
SQLITE_AFF_NUMERIC => Ok(Affinity::Numeric),
_ => Err(LimboError::InternalError(format!(
"Invalid affinity character: {}",
char
))),
}
}
}
impl fmt::Display for Type {

View File

@@ -4494,6 +4494,87 @@ pub fn op_once(
Ok(InsnFunctionStepResult::Step)
}
pub fn op_not_found(
program: &Program,
state: &mut ProgramState,
insn: &Insn,
pager: &Rc<Pager>,
mv_store: Option<&Rc<MvStore>>,
) -> Result<InsnFunctionStepResult> {
let Insn::NotFound {
cursor_id,
target_pc,
record_reg,
num_regs,
} = insn
else {
unreachable!("unexpected Insn {:?}", insn)
};
let found = {
let mut cursor = state.get_cursor(*cursor_id);
let cursor = cursor.as_btree_mut();
if *num_regs == 0 {
let record = match &state.registers[*record_reg] {
Register::Record(r) => r,
_ => {
return Err(LimboError::InternalError(
"NotFound: exepected a record in the register".into(),
));
}
};
return_if_io!(cursor.seek(SeekKey::IndexKey(&record), SeekOp::EQ))
} else {
let record = make_record(&state.registers, record_reg, num_regs);
return_if_io!(cursor.seek(SeekKey::IndexKey(&record), SeekOp::EQ))
}
};
if found {
state.pc += 1;
} else {
state.pc = target_pc.to_offset_int();
}
Ok(InsnFunctionStepResult::Step)
}
pub fn op_affinity(
program: &Program,
state: &mut ProgramState,
insn: &Insn,
pager: &Rc<Pager>,
mv_store: Option<&Rc<MvStore>>,
) -> Result<InsnFunctionStepResult> {
let Insn::Affinity {
start_reg,
count,
affinities,
} = insn
else {
unreachable!("unexpected Insn {:?}", insn)
};
if affinities.len() != count.get() {
return Err(LimboError::InternalError(
"Affinity: the length of affinities does not match the count".into(),
));
}
for (i, affinity_char) in affinities.chars().enumerate().take(count.get()) {
let reg_index = *start_reg + i;
let affinity = Affinity::from_char(affinity_char)?;
apply_affinity_char(&mut state.registers[reg_index], affinity);
}
state.pc += 1;
Ok(InsnFunctionStepResult::Step)
}
fn exec_lower(reg: &OwnedValue) -> Option<OwnedValue> {
match reg {
OwnedValue::Text(t) => Some(OwnedValue::build_text(&t.as_str().to_lowercase())),

View File

@@ -1366,6 +1366,57 @@ pub fn insn_to_str(
0,
format!("goto {}", target_pc_when_reentered.to_debug_int()),
),
Insn::BeginSubrtn { dest, dest_end } => (
"BeginSubrtn",
*dest as i32,
dest_end.map_or(0, |end| end as i32),
0,
OwnedValue::build_text(""),
0,
dest_end.map_or(format!("r[{}]=NULL", dest), |end| {
format!("r[{}..{}]=NULL", dest, end)
}),
),
Insn::NotFound {
cursor_id,
target_pc,
record_reg,
..
} => (
"NotFound",
*cursor_id as i32,
target_pc.to_debug_int(),
*record_reg as i32,
OwnedValue::build_text(""),
0,
format!(
"if (r[{}] != NULL) goto {}",
record_reg,
target_pc.to_debug_int()
),
),
Insn::Affinity {
start_reg,
count,
affinities,
} => (
"Affinity",
*start_reg as i32,
count.get() as i32,
0,
OwnedValue::build_text(""),
0,
format!(
"r[{}..{}] = {}",
start_reg,
start_reg + count.get(),
affinities
.chars()
.map(|a| a.to_string())
.collect::<Vec<_>>()
.join(", ")
),
),
};
format!(
"{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}",

View File

@@ -1,4 +1,7 @@
use std::{num::NonZero, rc::Rc};
use std::{
num::{NonZero, NonZeroUsize},
rc::Rc,
};
use super::{execute, AggFunc, BranchOffset, CursorID, FuncCtx, InsnFunction, PageIdx};
use crate::{
@@ -100,6 +103,12 @@ pub enum Insn {
dest: usize,
dest_end: Option<usize>,
},
/// Mark the beginning of a subroutine tha can be entered in-line. This opcode is identical to Null
/// it has a different name only to make the byte code easier to read and verify
BeginSubrtn {
dest: usize,
dest_end: Option<usize>,
},
/// Move the cursor P1 to a null row. Any Column operations that occur while the cursor is on the null row will always write a NULL.
NullRow {
cursor_id: CursorID,
@@ -801,6 +810,25 @@ pub enum Insn {
Once {
target_pc_when_reentered: BranchOffset,
},
/// Search for record in the index cusor, if any entry for which the key is a prefix exists
/// is a no-op, otherwise go to target_pc
/// Example =>
/// For a index key (1,2,3):
/// NotFound((1,2,3)) => No-op
/// NotFound((1,2)) => No-op
/// NotFound((2,2, 1)) => Jump
NotFound {
cursor_id: CursorID,
target_pc: BranchOffset,
record_reg: usize,
num_regs: usize,
},
/// Apply affinities to a range of registers. Affinities must have the same size of count
Affinity {
start_reg: usize,
count: NonZeroUsize,
affinities: String,
},
}
impl Insn {
@@ -808,6 +836,7 @@ impl Insn {
match self {
Insn::Init { .. } => execute::op_init,
Insn::Null { .. } => execute::op_null,
Insn::BeginSubrtn { .. } => execute::op_null,
Insn::NullRow { .. } => execute::op_null_row,
Insn::Add { .. } => execute::op_add,
Insn::Subtract { .. } => execute::op_subtract,
@@ -915,6 +944,8 @@ impl Insn {
Insn::ReadCookie { .. } => execute::op_read_cookie,
Insn::OpenEphemeral { .. } | Insn::OpenAutoindex { .. } => execute::op_open_ephemeral,
Insn::Once { .. } => execute::op_once,
Insn::NotFound { .. } => execute::op_not_found,
Insn::Affinity { .. } => execute::op_affinity,
}
}
}