use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::value::Value;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct VTabSchema {
pub columns: Vec<String>,
pub types: Vec<String>,
}
impl VTabSchema {
pub fn new<I, S>(columns: I) -> VTabSchema
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let columns: Vec<String> = columns.into_iter().map(Into::into).collect();
let types = alloc::vec![String::new(); columns.len()];
VTabSchema { columns, types }
}
pub fn typed<I, S, T>(columns: I) -> VTabSchema
where
I: IntoIterator<Item = (S, T)>,
S: Into<String>,
T: Into<String>,
{
let mut names = Vec::new();
let mut types = Vec::new();
for (n, t) in columns {
names.push(n.into());
types.push(t.into());
}
VTabSchema {
columns: names,
types,
}
}
pub fn len(&self) -> usize {
self.columns.len()
}
pub fn is_empty(&self) -> bool {
self.columns.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintOp {
Eq,
Gt,
Le,
Lt,
Ge,
Match,
Like,
Glob,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexConstraint {
pub column: usize,
pub op: ConstraintOp,
pub usable: bool,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct IndexPlan {
pub idx_num: i32,
pub idx_str: Option<String>,
pub estimated_cost: f64,
pub argv_index: Vec<u32>,
pub omit: Vec<bool>,
pub order_by_consumed: bool,
}
pub trait VTabRow {
fn column(&self, i: usize) -> Value;
fn rowid(&self) -> i64;
}
pub trait VTabCursor {
type Row: VTabRow;
fn next(&mut self) -> Result<Option<Self::Row>>;
}
#[derive(Debug, Clone)]
pub enum VTabChange<'a> {
Delete {
rowid: i64,
},
Insert {
rowid: Option<i64>,
values: &'a [Value],
},
Update {
rowid: i64,
new_rowid: i64,
values: &'a [Value],
},
}
pub trait VTabStore {
fn rows(&self) -> Result<alloc::vec::Vec<(i64, alloc::vec::Vec<Value>)>>;
fn put(&mut self, rowid: i64, values: &[Value]) -> Result<()>;
fn delete(&mut self, rowid: i64) -> Result<()>;
}
pub trait VTabModule {
type Cursor: VTabCursor;
fn connect(&self, args: &[&str]) -> Result<VTabSchema>;
fn best_index(&self, _constraints: &[IndexConstraint]) -> Result<IndexPlan> {
Ok(IndexPlan {
estimated_cost: f64::from(u32::MAX),
..IndexPlan::default()
})
}
fn open(&self, args: &[&str], plan: &IndexPlan) -> Result<Self::Cursor>;
fn filter(
&self,
cursor: Self::Cursor,
_plan: &IndexPlan,
_argv: &[Value],
) -> Result<Self::Cursor> {
Ok(cursor)
}
fn persistent(&self) -> bool {
false
}
fn update(
&self,
_args: &[&str],
_change: VTabChange,
_store: &mut dyn VTabStore,
) -> Result<i64> {
Err(Error::Error(alloc::string::String::from(
"table is read-only",
)))
}
}
pub trait DynVTabModule {
fn dyn_connect(&self, args: &[&str]) -> Result<VTabSchema>;
fn dyn_best_index(&self, constraints: &[IndexConstraint]) -> Result<IndexPlan>;
fn dyn_open(
&self,
args: &[&str],
plan: &IndexPlan,
argv: &[Value],
) -> Result<Box<dyn DynCursor>>;
fn dyn_persistent(&self) -> bool;
fn dyn_update(
&self,
args: &[&str],
change: VTabChange,
store: &mut dyn VTabStore,
) -> Result<i64>;
}
pub trait DynRow {
fn dyn_column(&self, i: usize) -> Value;
fn dyn_rowid(&self) -> i64;
}
impl<R: VTabRow> DynRow for R {
fn dyn_column(&self, i: usize) -> Value {
VTabRow::column(self, i)
}
fn dyn_rowid(&self) -> i64 {
VTabRow::rowid(self)
}
}
pub trait DynCursor {
fn dyn_next(&mut self) -> Result<Option<Box<dyn DynRow>>>;
}
impl<C: VTabCursor> DynCursor for C
where
C::Row: 'static,
{
fn dyn_next(&mut self) -> Result<Option<Box<dyn DynRow>>> {
Ok(VTabCursor::next(self)?.map(|r| Box::new(r) as Box<dyn DynRow>))
}
}
impl<M> DynVTabModule for M
where
M: VTabModule,
M::Cursor: 'static,
{
fn dyn_connect(&self, args: &[&str]) -> Result<VTabSchema> {
VTabModule::connect(self, args)
}
fn dyn_best_index(&self, constraints: &[IndexConstraint]) -> Result<IndexPlan> {
VTabModule::best_index(self, constraints)
}
fn dyn_open(
&self,
args: &[&str],
plan: &IndexPlan,
argv: &[Value],
) -> Result<Box<dyn DynCursor>> {
let cursor = VTabModule::open(self, args, plan)?;
let cursor = VTabModule::filter(self, cursor, plan, argv)?;
Ok(Box::new(cursor) as Box<dyn DynCursor>)
}
fn dyn_persistent(&self) -> bool {
VTabModule::persistent(self)
}
fn dyn_update(
&self,
args: &[&str],
change: VTabChange,
store: &mut dyn VTabStore,
) -> Result<i64> {
VTabModule::update(self, args, change, store)
}
}
#[derive(Default)]
pub struct VTabRegistry {
modules: BTreeMap<String, Box<dyn DynVTabModule>>,
}
impl VTabRegistry {
pub fn new() -> VTabRegistry {
VTabRegistry {
modules: BTreeMap::new(),
}
}
pub fn register(&mut self, name: &str, module: Box<dyn DynVTabModule>) -> Result<()> {
let key = name.to_ascii_lowercase();
if self.modules.contains_key(&key) {
return Err(Error::Constraint(alloc::format!(
"virtual table module \"{name}\" is already registered"
)));
}
self.modules.insert(key, module);
Ok(())
}
pub fn get(&self, name: &str) -> Option<&dyn DynVTabModule> {
self.modules
.get(&name.to_ascii_lowercase())
.map(AsRef::as_ref)
}
pub fn unregister(&mut self, name: &str) -> Option<Box<dyn DynVTabModule>> {
self.modules.remove(&name.to_ascii_lowercase())
}
pub fn len(&self) -> usize {
self.modules.len()
}
pub fn is_empty(&self) -> bool {
self.modules.is_empty()
}
}
impl VTabRegistry {
pub fn with_builtins() -> VTabRegistry {
let mut reg = VTabRegistry::new();
reg.register("series", Box::new(SeriesModule))
.expect("fresh registry has no name collisions");
reg.register("rtree", Box::new(RTreeModule))
.expect("fresh registry has no name collisions");
reg.register("fts5", Box::new(Fts5Module))
.expect("fresh registry has no name collisions");
reg
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SeriesModule;
mod series_plan {
pub const SCAN: i32 = 0;
pub const LOWER: i32 = 1 << 0;
pub const UPPER: i32 = 1 << 1;
}
#[derive(Debug)]
pub struct SeriesCursor {
next: i64,
stop: i64,
step: i64,
next_rowid: i64,
done: bool,
generated: usize,
}
impl SeriesCursor {
pub fn generated(&self) -> usize {
self.generated
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SeriesRow {
value: i64,
rowid: i64,
}
impl VTabRow for SeriesRow {
fn column(&self, i: usize) -> Value {
match i {
0 => Value::Integer(self.value),
_ => Value::Null,
}
}
fn rowid(&self) -> i64 {
self.rowid
}
}
impl VTabCursor for SeriesCursor {
type Row = SeriesRow;
fn next(&mut self) -> Result<Option<SeriesRow>> {
if self.done {
return Ok(None);
}
let in_range = if self.step > 0 {
self.next <= self.stop
} else {
self.next >= self.stop
};
if !in_range {
self.done = true;
return Ok(None);
}
let row = SeriesRow {
value: self.next,
rowid: self.next_rowid,
};
self.generated += 1;
self.next_rowid += 1;
match self.next.checked_add(self.step) {
Some(n) => self.next = n,
None => self.done = true, }
Ok(Some(row))
}
}
fn advance_to(start: i64, step: i64, target: i64) -> i64 {
debug_assert!(step != 0);
let delta = i128::from(target) - i128::from(start);
let step128 = i128::from(step);
if (step > 0 && delta <= 0) || (step < 0 && delta >= 0) {
return start;
}
let k = {
let q = delta / step128;
let r = delta % step128;
if r != 0 {
q + 1
} else {
q
}
};
match i128::from(start).checked_add(k * step128) {
Some(v) if v >= i128::from(i64::MIN) && v <= i128::from(i64::MAX) => v as i64,
_ => start,
}
}
impl SeriesModule {
fn parse_arg(s: &str) -> Result<i64> {
s.trim()
.parse::<i64>()
.map_err(|_| Error::Error(alloc::format!("series(): invalid integer argument {s:?}")))
}
}
impl VTabModule for SeriesModule {
type Cursor = SeriesCursor;
fn connect(&self, args: &[&str]) -> Result<VTabSchema> {
if args.is_empty() {
return Err(Error::Error(
"series() requires at least a start argument".into(),
));
}
if args.len() > 3 {
return Err(Error::Error("series() takes at most 3 arguments".into()));
}
for a in args {
SeriesModule::parse_arg(a)?;
}
Ok(VTabSchema::new(["value"]))
}
fn best_index(&self, constraints: &[IndexConstraint]) -> Result<IndexPlan> {
let mut argv_index = alloc::vec![0u32; constraints.len()];
let mut idx_num = series_plan::SCAN;
let mut next_arg = 1u32;
for (i, c) in constraints.iter().enumerate() {
if c.column != 0 || !c.usable {
continue;
}
let bound = match c.op {
ConstraintOp::Eq => series_plan::LOWER | series_plan::UPPER,
ConstraintOp::Ge | ConstraintOp::Gt => series_plan::LOWER,
ConstraintOp::Le | ConstraintOp::Lt => series_plan::UPPER,
_ => continue,
};
argv_index[i] = next_arg;
next_arg += 1;
idx_num |= bound;
}
if idx_num == series_plan::SCAN {
return Ok(IndexPlan {
estimated_cost: f64::from(u32::MAX),
..IndexPlan::default()
});
}
let mut sides = String::new();
for c in constraints {
if c.column != 0 || !c.usable {
continue;
}
match c.op {
ConstraintOp::Eq => sides.push('='),
ConstraintOp::Ge | ConstraintOp::Gt => sides.push('>'),
ConstraintOp::Le | ConstraintOp::Lt => sides.push('<'),
_ => {}
}
}
Ok(IndexPlan {
idx_num,
idx_str: Some(sides),
estimated_cost: 100.0,
argv_index,
omit: Vec::new(),
order_by_consumed: false,
})
}
fn filter(
&self,
mut cursor: SeriesCursor,
plan: &IndexPlan,
argv: &[Value],
) -> Result<SeriesCursor> {
if plan.idx_num == series_plan::SCAN {
return Ok(cursor);
}
let sides = plan.idx_str.as_deref().unwrap_or("");
let mut lower: Option<i64> = None;
let mut upper: Option<i64> = None;
for (side, v) in sides.chars().zip(argv.iter()) {
let n = match v {
Value::Integer(i) => *i,
_ => continue,
};
match side {
'>' => lower = Some(lower.map_or(n, |cur| cur.max(n))),
'<' => upper = Some(upper.map_or(n, |cur| cur.min(n))),
'=' => {
lower = Some(lower.map_or(n, |cur| cur.max(n)));
upper = Some(upper.map_or(n, |cur| cur.min(n)));
}
_ => {}
}
}
let ascending = cursor.step > 0;
if ascending {
if let Some(lo) = lower {
cursor.next = advance_to(cursor.next, cursor.step, lo);
}
if let Some(hi) = upper {
cursor.stop = cursor.stop.min(hi);
}
} else {
if let Some(hi) = upper {
cursor.next = advance_to(cursor.next, cursor.step, hi);
}
if let Some(lo) = lower {
cursor.stop = cursor.stop.max(lo);
}
}
Ok(cursor)
}
fn open(&self, args: &[&str], plan: &IndexPlan) -> Result<SeriesCursor> {
let _ = plan;
let start = args
.first()
.map(|a| SeriesModule::parse_arg(a))
.transpose()?;
let Some(start) = start else {
return Err(Error::Error(
"series() requires at least a start argument".into(),
));
};
let stop = match args.get(1) {
Some(a) => SeriesModule::parse_arg(a)?,
None => start,
};
let step = match args.get(2) {
Some(a) => SeriesModule::parse_arg(a)?,
None => 1,
};
SeriesModule::scan(start, stop, step)
}
}
impl SeriesModule {
pub fn scan(start: i64, stop: i64, step: i64) -> Result<SeriesCursor> {
if step == 0 {
return Err(Error::Error("series(): step must be non-zero".into()));
}
Ok(SeriesCursor {
next: start,
stop,
step,
next_rowid: 1,
done: false,
generated: 0,
})
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RTreeModule;
pub struct RTreeCursor;
pub struct RTreeRow;
impl VTabRow for RTreeRow {
fn column(&self, _i: usize) -> Value {
Value::Null
}
fn rowid(&self) -> i64 {
0
}
}
impl VTabCursor for RTreeCursor {
type Row = RTreeRow;
fn next(&mut self) -> Result<Option<RTreeRow>> {
Ok(None)
}
}
fn rtree_i64(v: &Value) -> i64 {
match v {
Value::Integer(i) => *i,
Value::Real(r) => *r as i64,
Value::Text(t) => t.parse().unwrap_or(0),
_ => 0,
}
}
fn coord_f64(v: &Value) -> f64 {
match v {
Value::Integer(i) => *i as f64,
Value::Real(r) => *r,
Value::Text(t) => t.parse().unwrap_or(0.0),
_ => 0.0,
}
}
const RTREE_RND_TOWARDS: f64 = 1.0 - 1.0 / 8388608.0;
const RTREE_RND_AWAY: f64 = 1.0 + 1.0 / 8388608.0;
fn round_min_f32(d: f64) -> f64 {
let f = d as f32;
let f = if f64::from(f) > d {
(d * if d < 0.0 {
RTREE_RND_AWAY
} else {
RTREE_RND_TOWARDS
}) as f32
} else {
f
};
f64::from(f)
}
fn round_max_f32(d: f64) -> f64 {
let f = d as f32;
let f = if f64::from(f) < d {
(d * if d < 0.0 {
RTREE_RND_TOWARDS
} else {
RTREE_RND_AWAY
}) as f32
} else {
f
};
f64::from(f)
}
impl RTreeModule {
fn record(values: &[Value]) -> Result<Vec<Value>> {
let mut rec = Vec::with_capacity(values.len());
rec.push(Value::Integer(rtree_i64(
values.first().unwrap_or(&Value::Null),
)));
for (i, v) in values.iter().enumerate().skip(1) {
let x = coord_f64(v);
rec.push(Value::Real(if i % 2 == 1 {
round_min_f32(x)
} else {
round_max_f32(x)
}));
}
let mut k = 1;
while k + 1 < rec.len() {
if let (Value::Real(lo), Value::Real(hi)) = (&rec[k], &rec[k + 1]) {
if lo > hi {
return Err(Error::Error(alloc::string::String::from(
"rtree constraint failed",
)));
}
}
k += 2;
}
Ok(rec)
}
}
impl VTabModule for RTreeModule {
type Cursor = RTreeCursor;
fn connect(&self, args: &[&str]) -> Result<VTabSchema> {
if args.len() < 3 || args.len().is_multiple_of(2) {
return Err(Error::Error(alloc::string::String::from(
"rtree requires an odd number of columns (id + 2N coordinates), \
at least 3",
)));
}
Ok(VTabSchema::typed(args.iter().enumerate().map(|(i, s)| {
let ty = if i == 0 { "INT" } else { "REAL" };
(String::from(*s), ty)
})))
}
fn open(&self, _args: &[&str], _plan: &IndexPlan) -> Result<RTreeCursor> {
Ok(RTreeCursor)
}
fn persistent(&self) -> bool {
true
}
fn update(&self, _args: &[&str], change: VTabChange, store: &mut dyn VTabStore) -> Result<i64> {
match change {
VTabChange::Insert { values, .. } => {
let mut rec = RTreeModule::record(values)?;
let id = if matches!(values.first(), Some(Value::Null) | None) {
store.rows()?.iter().map(|(r, _)| *r).max().unwrap_or(0) + 1
} else {
rtree_i64(&values[0])
};
rec[0] = Value::Integer(id);
store.put(id, &rec)?;
Ok(id)
}
VTabChange::Delete { rowid } => {
store.delete(rowid)?;
Ok(rowid)
}
VTabChange::Update { rowid, values, .. } => {
let mut rec = RTreeModule::record(values)?;
let id = rtree_i64(&values[0]);
rec[0] = Value::Integer(id);
if id != rowid {
store.delete(rowid)?;
}
store.put(id, &rec)?;
Ok(id)
}
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Fts5Module;
pub struct Fts5Cursor;
pub struct Fts5Row;
impl VTabRow for Fts5Row {
fn column(&self, _i: usize) -> Value {
Value::Null
}
fn rowid(&self) -> i64 {
0
}
}
impl VTabCursor for Fts5Cursor {
type Row = Fts5Row;
fn next(&mut self) -> Result<Option<Fts5Row>> {
Ok(None)
}
}
pub(crate) fn fts5_tokenize(text: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut cur = String::new();
for ch in text.chars() {
if ch.is_alphanumeric() {
cur.extend(ch.to_lowercase());
} else if !cur.is_empty() {
tokens.push(core::mem::take(&mut cur));
}
}
if !cur.is_empty() {
tokens.push(cur);
}
tokens
}
fn fts5_tokenize_spans(text: &str) -> Vec<(String, usize, usize)> {
let mut out = Vec::new();
let mut cur = String::new();
let mut start = 0;
for (i, ch) in text.char_indices() {
if ch.is_alphanumeric() {
if cur.is_empty() {
start = i;
}
cur.extend(ch.to_lowercase());
} else if !cur.is_empty() {
out.push((core::mem::take(&mut cur), start, i));
}
}
if !cur.is_empty() {
out.push((cur, start, text.len()));
}
out
}
pub(crate) fn fts5_highlight(
query: &str,
col_names: &[String],
scope: Option<&str>,
col: usize,
text: &str,
open: &str,
close: &str,
) -> String {
if scope.is_some_and(|s| {
col_names
.get(col)
.is_none_or(|n| !n.eq_ignore_ascii_case(s))
}) {
return String::from(text);
}
let toks = fts5_lex(query);
let parsed = match (Fts5Parser {
toks: &toks,
pos: 0,
})
.parse()
{
Some(q) => q,
None => return String::from(text),
};
let mut terms = Vec::new();
fts5_collect_terms(&parsed, &mut terms);
let spans = fts5_tokenize_spans(text);
let col_tokens: Vec<String> = spans.iter().map(|(t, _, _)| t.clone()).collect();
let mut hits: Vec<(usize, usize)> = Vec::new();
for term in &terms {
if term.column.as_deref().is_some_and(|c| {
col_names
.get(col)
.is_none_or(|n| !n.eq_ignore_ascii_case(c))
}) {
continue;
}
for start in fts5_term_starts(term, &col_tokens) {
hits.push((start, (start + term.phrase.len()).min(spans.len())));
}
}
hits.sort_unstable();
let mut merged: Vec<(usize, usize)> = Vec::new();
for (s, e) in hits {
match merged.last_mut() {
Some(last) if s < last.1 => last.1 = last.1.max(e),
_ => merged.push((s, e)),
}
}
let mut out = String::new();
let mut last = 0;
for (s, e) in merged {
out.push_str(&text[last..spans[s].1]);
out.push_str(open);
out.push_str(&text[spans[s].1..spans[e - 1].2]);
out.push_str(close);
last = spans[e - 1].2;
}
out.push_str(&text[last..]);
out
}
#[derive(Clone)]
struct Fts5Term {
column: Option<String>,
phrase: Vec<String>,
prefix: bool,
anchored: bool,
}
fn fts5_term_starts(term: &Fts5Term, tokens: &[String]) -> Vec<usize> {
let mut starts = fts5_phrase_starts(&term.phrase, term.prefix, tokens);
if term.anchored {
starts.retain(|&s| s == 0);
}
starts
}
fn fts5_phrase_starts(phrase: &[String], prefix: bool, doc: &[String]) -> Vec<usize> {
if phrase.is_empty() || doc.len() < phrase.len() {
return Vec::new();
}
let last = phrase.len() - 1;
(0..=doc.len() - phrase.len())
.filter(|&start| {
phrase.iter().enumerate().all(|(k, want)| {
let got = &doc[start + k];
if k == last && prefix {
got.starts_with(want.as_str())
} else {
got == want
}
})
})
.collect()
}
fn fts5_near_matches(phrases: &[(Vec<usize>, usize)], n: usize) -> bool {
if phrases.iter().any(|(starts, _)| starts.is_empty()) {
return false;
}
let total_len: usize = phrases.iter().map(|(_, len)| *len).sum();
let mut ptr = alloc::vec![0usize; phrases.len()];
loop {
let mut min_start = usize::MAX;
let mut max_end = 0;
let mut min_phrase = 0;
for (i, (starts, len)) in phrases.iter().enumerate() {
let s = starts[ptr[i]];
let e = s + len - 1;
if s < min_start {
min_start = s;
min_phrase = i;
}
max_end = max_end.max(e);
}
if max_end - min_start < n + total_len {
return true;
}
ptr[min_phrase] += 1;
if ptr[min_phrase] >= phrases[min_phrase].0.len() {
return false;
}
}
}
enum Fts5Lex {
Or,
And,
Not,
LParen,
RParen,
Term(Fts5Term),
Near(Vec<Fts5Term>, usize),
}
fn fts5_lex(pattern: &str) -> Vec<Fts5Lex> {
let chars: Vec<char> = pattern.chars().collect();
let n = chars.len();
let mut i = 0;
let mut out = Vec::new();
while i < n {
let ch = chars[i];
if ch.is_whitespace() {
i += 1;
continue;
}
if ch == '(' {
out.push(Fts5Lex::LParen);
i += 1;
continue;
}
if ch == ')' {
out.push(Fts5Lex::RParen);
i += 1;
continue;
}
let mut column = None;
let mut j = i;
while j < n && (chars[j].is_alphanumeric() || chars[j] == '_') {
j += 1;
}
if j > i && j < n && chars[j] == ':' {
column = Some(chars[i..j].iter().collect());
i = j + 1;
}
let anchored = i < n && chars[i] == '^';
if anchored {
i += 1;
}
let (text, prefix) = if i < n && chars[i] == '"' {
i += 1;
let start = i;
while i < n && chars[i] != '"' {
i += 1;
}
let body: String = chars[start..i].iter().collect();
if i < n {
i += 1; }
(body, false)
} else {
let start = i;
while i < n && !chars[i].is_whitespace() && chars[i] != '(' && chars[i] != ')' {
i += 1;
}
let raw: String = chars[start..i].iter().collect();
if column.is_none() {
match raw.as_str() {
"OR" => {
out.push(Fts5Lex::Or);
continue;
}
"AND" => {
out.push(Fts5Lex::And);
continue;
}
"NOT" => {
out.push(Fts5Lex::Not);
continue;
}
"NEAR" => {
let mut k = i;
while k < n && chars[k].is_whitespace() {
k += 1;
}
if k < n && chars[k] == '(' {
let start = k + 1;
let mut depth = 1;
let mut e = start;
while e < n && depth > 0 {
match chars[e] {
'(' => depth += 1,
')' => depth -= 1,
_ => {}
}
if depth == 0 {
break;
}
e += 1;
}
let inside: String = chars[start..e].iter().collect();
i = if e < n { e + 1 } else { e };
let (phrases, dist) = fts5_parse_near(&inside);
if !phrases.is_empty() {
out.push(Fts5Lex::Near(phrases, dist));
}
continue;
}
}
_ => {}
}
}
let mut body = raw;
let prefix = body.ends_with('*');
if prefix {
body.pop();
}
(body, prefix)
};
let phrase = fts5_tokenize(&text);
if !phrase.is_empty() {
out.push(Fts5Lex::Term(Fts5Term {
column,
phrase,
prefix,
anchored,
}));
}
}
out
}
fn fts5_parse_near(inside: &str) -> (Vec<Fts5Term>, usize) {
let (phrases_part, distance) = match inside.rsplit_once(',') {
Some((left, right))
if !right.trim().is_empty() && right.trim().bytes().all(|b| b.is_ascii_digit()) =>
{
(left, right.trim().parse::<usize>().unwrap_or(10))
}
_ => (inside, 10),
};
let phrases = fts5_lex(phrases_part)
.into_iter()
.filter_map(|t| match t {
Fts5Lex::Term(term) => Some(term),
_ => None,
})
.collect();
(phrases, distance)
}
enum Fts5Query {
Term(Fts5Term),
Near(Vec<Fts5Term>, usize),
And(Box<Fts5Query>, Box<Fts5Query>),
Or(Box<Fts5Query>, Box<Fts5Query>),
Not(Box<Fts5Query>, Box<Fts5Query>),
}
struct Fts5Parser<'a> {
toks: &'a [Fts5Lex],
pos: usize,
}
impl Fts5Parser<'_> {
fn parse(&mut self) -> Option<Fts5Query> {
let q = self.parse_or();
q
}
fn parse_or(&mut self) -> Option<Fts5Query> {
let mut left = self.parse_and()?;
while matches!(self.toks.get(self.pos), Some(Fts5Lex::Or)) {
self.pos += 1;
match self.parse_and() {
Some(right) => left = Fts5Query::Or(Box::new(left), Box::new(right)),
None => break,
}
}
Some(left)
}
fn parse_and(&mut self) -> Option<Fts5Query> {
let mut left = self.parse_not()?;
loop {
match self.toks.get(self.pos) {
Some(Fts5Lex::And) => self.pos += 1,
Some(Fts5Lex::Term(_) | Fts5Lex::Near(..) | Fts5Lex::LParen) => {}
_ => break,
}
match self.parse_not() {
Some(right) => left = Fts5Query::And(Box::new(left), Box::new(right)),
None => break,
}
}
Some(left)
}
fn parse_not(&mut self) -> Option<Fts5Query> {
let mut left = self.parse_primary()?;
while matches!(self.toks.get(self.pos), Some(Fts5Lex::Not)) {
self.pos += 1;
match self.parse_primary() {
Some(right) => left = Fts5Query::Not(Box::new(left), Box::new(right)),
None => break,
}
}
Some(left)
}
fn parse_primary(&mut self) -> Option<Fts5Query> {
match self.toks.get(self.pos) {
Some(Fts5Lex::LParen) => {
self.pos += 1;
let inner = self.parse_or();
if matches!(self.toks.get(self.pos), Some(Fts5Lex::RParen)) {
self.pos += 1;
}
inner
}
Some(Fts5Lex::Term(t)) => {
let t = t.clone();
self.pos += 1;
Some(Fts5Query::Term(t))
}
Some(Fts5Lex::Near(phrases, dist)) => {
let q = Fts5Query::Near(phrases.clone(), *dist);
self.pos += 1;
Some(q)
}
_ => None,
}
}
}
fn fts5_term_matches(term: &Fts5Term, cols: &[(&str, Vec<String>)]) -> bool {
cols.iter().any(|(name, tokens)| {
term.column
.as_deref()
.is_none_or(|c| name.eq_ignore_ascii_case(c))
&& !fts5_term_starts(term, tokens).is_empty()
})
}
fn fts5_near_group_matches(
phrases: &[Fts5Term],
dist: usize,
cols: &[(&str, Vec<String>)],
) -> bool {
cols.iter().any(|(_, tokens)| {
let positioned: Vec<(Vec<usize>, usize)> = phrases
.iter()
.map(|p| (fts5_term_starts(p, tokens), p.phrase.len()))
.collect();
fts5_near_matches(&positioned, dist)
})
}
fn fts5_eval(query: &Fts5Query, cols: &[(&str, Vec<String>)]) -> bool {
match query {
Fts5Query::Term(t) => fts5_term_matches(t, cols),
Fts5Query::Near(phrases, dist) => fts5_near_group_matches(phrases, *dist, cols),
Fts5Query::And(a, b) => fts5_eval(a, cols) && fts5_eval(b, cols),
Fts5Query::Or(a, b) => fts5_eval(a, cols) || fts5_eval(b, cols),
Fts5Query::Not(a, b) => fts5_eval(a, cols) && !fts5_eval(b, cols),
}
}
pub(crate) fn fts5_query_matches(pattern: &str, cols: &[(String, String)]) -> bool {
let toks = fts5_lex(pattern);
let query = match (Fts5Parser {
toks: &toks,
pos: 0,
})
.parse()
{
Some(q) => q,
None => return false,
};
let tokenized: Vec<(&str, Vec<String>)> = cols
.iter()
.map(|(name, text)| (name.as_str(), fts5_tokenize(text)))
.collect();
fts5_eval(&query, &tokenized)
}
fn fts5_collect_terms<'a>(q: &'a Fts5Query, out: &mut Vec<&'a Fts5Term>) {
match q {
Fts5Query::Term(t) => out.push(t),
Fts5Query::Near(phrases, _) => out.extend(phrases.iter()),
Fts5Query::And(a, b) | Fts5Query::Or(a, b) | Fts5Query::Not(a, b) => {
fts5_collect_terms(a, out);
fts5_collect_terms(b, out);
}
}
}
pub(crate) fn fts5_bm25_corpus(
query: &str,
col_names: &[String],
docs: &[Vec<String>],
scope: Option<&str>,
) -> Fts5Bm25 {
let n = docs.len();
let toks = fts5_lex(query);
let parsed = (Fts5Parser {
toks: &toks,
pos: 0,
})
.parse();
let terms: Vec<&Fts5Term> = match &parsed {
Some(q) => {
let mut t = Vec::new();
fts5_collect_terms(q, &mut t);
t
}
None => Vec::new(),
};
let nterms = terms.len();
let tok_docs: Vec<Vec<Vec<String>>> = docs
.iter()
.map(|cols| cols.iter().map(|t| fts5_tokenize(t)).collect())
.collect();
let dl: Vec<f64> = tok_docs
.iter()
.map(|cols| cols.iter().map(Vec::len).sum::<usize>() as f64)
.collect();
let avgdl = if n == 0 {
0.0
} else {
dl.iter().sum::<f64>() / n as f64
};
let mut occ: Vec<Vec<Vec<f64>>> =
alloc::vec![alloc::vec![alloc::vec![0.0; col_names.len()]; nterms]; n];
let mut idf = alloc::vec![0.0f64; nterms];
for (t, term) in terms.iter().enumerate() {
let mut docfreq = 0usize;
for (i, cols) in tok_docs.iter().enumerate() {
let mut any = false;
for (ci, ctoks) in cols.iter().enumerate() {
let name = col_names.get(ci);
if scope.is_some_and(|s| name.is_none_or(|nm| !nm.eq_ignore_ascii_case(s)))
|| term
.column
.as_deref()
.is_some_and(|c| name.is_none_or(|nm| !nm.eq_ignore_ascii_case(c)))
{
continue;
}
let c = fts5_term_starts(term, ctoks).len();
if c > 0 {
occ[i][t][ci] = c as f64;
any = true;
}
}
if any {
docfreq += 1;
}
}
let raw = crate::util::float::ln(((n - docfreq) as f64 + 0.5) / (docfreq as f64 + 0.5));
idf[t] = if raw <= 0.0 { 1e-6 } else { raw };
}
Fts5Bm25 {
avgdl,
idf,
docs: dl
.into_iter()
.zip(occ)
.map(|(dl, occ)| Fts5Bm25Doc { dl, occ })
.collect(),
}
}
pub(crate) struct Fts5Bm25 {
avgdl: f64,
idf: Vec<f64>,
docs: Vec<Fts5Bm25Doc>,
}
struct Fts5Bm25Doc {
dl: f64,
occ: Vec<Vec<f64>>,
}
impl Fts5Bm25 {
pub(crate) fn score(&self, i: usize, weights: &[f64]) -> f64 {
const K1: f64 = 1.2;
const B: f64 = 0.75;
let doc = &self.docs[i];
let mut s = 0.0;
for (t, occ_cols) in doc.occ.iter().enumerate() {
let f: f64 = occ_cols
.iter()
.enumerate()
.map(|(c, &o)| weights.get(c).copied().unwrap_or(1.0) * o)
.sum();
if f == 0.0 {
continue;
}
let norm = 1.0 - B + B * doc.dl / self.avgdl;
s += self.idf[t] * (f * (K1 + 1.0)) / (f + K1 * norm);
}
-s
}
}
impl Fts5Module {
fn column_name(arg: &str) -> Option<String> {
let arg = arg.trim();
if arg.is_empty() || arg.contains('=') {
return None;
}
let first = arg.split_whitespace().next().unwrap_or(arg);
let name = first.trim_matches(|c| c == '"' || c == '\'' || c == '`');
let name = name.strip_prefix('[').unwrap_or(name);
let name = name.strip_suffix(']').unwrap_or(name);
Some(String::from(name))
}
}
impl VTabModule for Fts5Module {
type Cursor = Fts5Cursor;
fn connect(&self, args: &[&str]) -> Result<VTabSchema> {
let columns: Vec<String> = args
.iter()
.filter_map(|a| Fts5Module::column_name(a))
.collect();
if columns.is_empty() {
return Err(Error::Error(alloc::string::String::from(
"fts5: no columns specified",
)));
}
Ok(VTabSchema::new(columns))
}
fn open(&self, _args: &[&str], _plan: &IndexPlan) -> Result<Fts5Cursor> {
Ok(Fts5Cursor)
}
fn persistent(&self) -> bool {
true
}
fn update(&self, _args: &[&str], change: VTabChange, store: &mut dyn VTabStore) -> Result<i64> {
match change {
VTabChange::Insert { rowid, values } => {
let id = match rowid {
Some(r) => r,
None => store.rows()?.iter().map(|(r, _)| *r).max().unwrap_or(0) + 1,
};
store.put(id, values)?;
Ok(id)
}
VTabChange::Delete { rowid } => {
store.delete(rowid)?;
Ok(rowid)
}
VTabChange::Update {
rowid,
new_rowid,
values,
} => {
if new_rowid != rowid {
store.delete(rowid)?;
}
store.put(new_rowid, values)?;
Ok(new_rowid)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn fts5_tokenizer_splits_and_folds() {
assert_eq!(
fts5_tokenize("The quick-brown Fox!"),
vec![
String::from("the"),
String::from("quick"),
String::from("brown"),
String::from("fox"),
]
);
assert_eq!(
fts5_tokenize(" a1 b2,c3 "),
vec![String::from("a1"), String::from("b2"), String::from("c3")]
);
assert!(fts5_tokenize(" ,. !").is_empty());
}
#[test]
fn fts5_query_matches_are_token_anded() {
let doc = [(String::from("body"), String::from("the quick brown fox"))];
assert!(fts5_query_matches("fox", &doc));
assert!(fts5_query_matches("QUICK fox", &doc)); assert!(!fts5_query_matches("quick zebra", &doc)); assert!(!fts5_query_matches("", &doc)); }
#[test]
fn fts5_column_filters_scope_tokens() {
let cols = [
(String::from("title"), String::from("Mixed Fox")),
(String::from("body"), String::from("and the dog")),
];
assert!(fts5_query_matches("fox", &cols));
assert!(fts5_query_matches("title:fox", &cols));
assert!(!fts5_query_matches("body:fox", &cols)); assert!(fts5_query_matches("title:mixed body:dog", &cols)); assert!(!fts5_query_matches("title:dog", &cols)); }
#[test]
fn fts5_phrase_and_prefix_queries() {
let doc = [(
String::from("body"),
String::from("the quick brown fox runs"),
)];
assert!(fts5_query_matches("\"quick brown\"", &doc));
assert!(!fts5_query_matches("\"brown quick\"", &doc)); assert!(!fts5_query_matches("\"quick fox\"", &doc)); assert!(fts5_query_matches("fo*", &doc)); assert!(fts5_query_matches("run*", &doc)); assert!(!fts5_query_matches("cat*", &doc));
assert!(fts5_query_matches("body:\"quick brown\"", &doc));
assert!(fts5_query_matches("body:ru*", &doc));
}
#[test]
fn fts5_boolean_operators_and_precedence() {
let doc = |s: &str| [(String::from("body"), String::from(s))];
assert!(fts5_query_matches("apple OR cherry", &doc("apple banana")));
assert!(!fts5_query_matches("apple AND date", &doc("apple banana")));
assert!(fts5_query_matches("apple AND date", &doc("apple date")));
assert!(fts5_query_matches(
"banana NOT cherry",
&doc("apple banana")
));
assert!(!fts5_query_matches(
"banana NOT cherry",
&doc("banana cherry")
));
assert!(fts5_query_matches(
"apple OR banana AND cherry",
&doc("apple only")
));
assert!(fts5_query_matches(
"apple OR banana AND cherry",
&doc("banana cherry")
));
assert!(!fts5_query_matches(
"apple OR banana AND cherry",
&doc("banana only")
));
assert!(fts5_query_matches(
"(apple OR banana) AND date",
&doc("apple date")
));
assert!(!fts5_query_matches(
"(apple OR banana) AND date",
&doc("apple only")
));
}
#[test]
fn fts5_near_proximity_groups() {
let doc = |s: &str| [(String::from("body"), String::from(s))];
let adjacent = doc("the quick brown fox");
let gap2 = doc("quick the lazy brown");
let gap4 = doc("brown a b c d quick");
assert!(fts5_query_matches("NEAR(quick brown)", &adjacent));
assert!(fts5_query_matches("NEAR(quick brown)", &gap4));
assert!(fts5_query_matches("NEAR(quick brown, 2)", &gap2));
assert!(!fts5_query_matches("NEAR(quick brown, 1)", &gap2));
assert!(fts5_query_matches("NEAR(quick brown, 0)", &adjacent));
assert!(!fts5_query_matches("NEAR(quick brown, 0)", &gap2));
assert!(!fts5_query_matches("NEAR(quick zebra, 5)", &adjacent));
assert!(fts5_query_matches(
"NEAR(quick brown, 2) AND fox",
&adjacent
));
}
#[test]
fn fts5_bm25_matches_sqlite() {
let names = [String::from("body")];
let doc = |s: &str| alloc::vec![String::from(s)];
let docs = [
doc("apple apple banana"),
doc("apple cherry"),
doc("banana date elderberry"),
doc("fig grape"),
doc("apple banana cherry date"),
];
let close = |a: f64, b: f64| (a - b).abs() < 1e-12;
let score = |q: &str, i: usize| fts5_bm25_corpus(q, &names, &docs, None).score(i, &[]);
assert!(close(score("apple", 0), -1.347_921_225_382_93e-6));
assert!(close(score("apple", 1), -1.132_352_941_176_47e-6));
assert!(close(score("apple", 4), -8.508_287_292_817_68e-7));
assert_eq!(score("apple", 3), 0.0);
assert!(close(score("elderberry", 2), -1.067_421_403_500_88));
assert!(close(score("apple banana", 0), -2.319_530_058_190_5e-6));
assert!(close(score("apple banana", 4), -1.701_657_458_563_54e-6));
let corpus = fts5_bm25_corpus("apple", &names, &docs, None);
assert!(corpus.score(0, &[10.0]) < corpus.score(0, &[]));
assert_eq!(corpus.score(3, &[10.0]), 0.0); }
#[test]
fn fts5_highlight_wraps_matched_tokens() {
let names = [String::from("body")];
let hl = |q: &str, text: &str| fts5_highlight(q, &names, None, 0, text, "[", "]");
assert_eq!(
hl("fox", "the quick brown fox jumps"),
"the quick brown [fox] jumps"
);
assert_eq!(
hl("quick dog", "the quick brown fox and the lazy dog"),
"the [quick] brown fox and the lazy [dog]"
);
assert_eq!(
hl("\"quick brown\"", "a quick brown fox"),
"a [quick brown] fox"
);
assert_eq!(hl("fox", "fox fox"), "[fox] [fox]");
assert_eq!(hl("hello", "Hello World"), "[Hello] World");
assert_eq!(hl("zebra", "the quick fox"), "the quick fox");
}
#[test]
fn fts5_anchor_requires_first_token() {
let doc = |s: &str| [(String::from("body"), String::from(s))];
assert!(fts5_query_matches("^quick", &doc("quick brown fox")));
assert!(!fts5_query_matches("^quick", &doc("the quick fox")));
assert!(fts5_query_matches(
"^\"quick brown\"",
&doc("quick brown fox")
));
assert!(!fts5_query_matches(
"^\"quick brown\"",
&doc("a quick brown")
));
assert!(fts5_query_matches("quick", &doc("the quick fox")));
}
#[test]
fn fts5_column_name_skips_options_and_modifiers() {
assert_eq!(
Fts5Module::column_name("title"),
Some(String::from("title"))
);
assert_eq!(
Fts5Module::column_name("body UNINDEXED"),
Some(String::from("body"))
);
assert_eq!(Fts5Module::column_name("tokenize = 'porter'"), None);
assert_eq!(
Fts5Module::column_name("\"quoted\""),
Some(String::from("quoted"))
);
}
fn drain(mut cur: SeriesCursor) -> Vec<(i64, i64)> {
let mut out = Vec::new();
while let Some(row) = cur.next().unwrap() {
assert_eq!(row.column(0), Value::Integer(row.value));
assert_eq!(row.column(1), Value::Null);
out.push((row.rowid(), row.value));
}
out
}
#[test]
fn connect_declares_value_column() {
let m = SeriesModule;
let schema = m.connect(&["1", "5"]).unwrap();
assert_eq!(schema.columns, vec![String::from("value")]);
assert_eq!(schema.len(), 1);
assert!(!schema.is_empty());
}
#[test]
fn connect_validates_arguments() {
let m = SeriesModule;
assert!(m.connect(&[]).is_err()); assert!(m.connect(&["1", "2", "3", "4"]).is_err()); assert!(m.connect(&["notanint"]).is_err()); assert!(m.connect(&["10"]).is_ok());
}
#[test]
fn cursor_iterates_ascending() {
let cur = SeriesModule::scan(1, 5, 1).unwrap();
assert_eq!(drain(cur), vec![(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]);
}
#[test]
fn cursor_iterates_with_step() {
let cur = SeriesModule::scan(0, 10, 3).unwrap();
assert_eq!(drain(cur), vec![(1, 0), (2, 3), (3, 6), (4, 9)]);
}
#[test]
fn cursor_iterates_descending() {
let cur = SeriesModule::scan(3, 1, -1).unwrap();
assert_eq!(drain(cur), vec![(1, 3), (2, 2), (3, 1)]);
}
#[test]
fn empty_range_yields_no_rows() {
let cur = SeriesModule::scan(5, 1, 1).unwrap();
assert_eq!(drain(cur), vec![]);
}
#[test]
fn step_zero_is_rejected() {
assert!(SeriesModule::scan(1, 5, 0).is_err());
}
#[test]
fn next_keeps_returning_none_after_end() {
let mut cur = SeriesModule::scan(1, 1, 1).unwrap();
assert!(cur.next().unwrap().is_some());
assert!(cur.next().unwrap().is_none());
assert!(cur.next().unwrap().is_none());
}
#[test]
fn default_best_index_is_a_full_scan_plan() {
let m = SeriesModule;
let plan = m.best_index(&[]).unwrap();
assert_eq!(plan.idx_num, 0);
assert_eq!(plan.idx_str, None);
assert!(plan.argv_index.is_empty());
assert!(plan.estimated_cost > 1.0);
}
#[test]
fn advance_to_aligns_to_grid() {
assert_eq!(advance_to(0, 2, 3), 4);
assert_eq!(advance_to(0, 2, 4), 4);
assert_eq!(advance_to(5, 1, 2), 5);
assert_eq!(advance_to(10, -2, 7), 6);
assert_eq!(advance_to(10, -2, 8), 8);
assert_eq!(advance_to(1, 1, 3), 3);
}
#[test]
fn best_index_pushes_value_constraints() {
let m = SeriesModule;
let cons = [
IndexConstraint {
column: 0,
op: ConstraintOp::Ge,
usable: true,
},
IndexConstraint {
column: 0,
op: ConstraintOp::Le,
usable: true,
},
];
let plan = m.best_index(&cons).unwrap();
assert_eq!(plan.idx_num, series_plan::LOWER | series_plan::UPPER);
assert_eq!(plan.argv_index, vec![1, 2]);
assert_eq!(plan.idx_str.as_deref(), Some("><"));
assert!(plan.estimated_cost < f64::from(u32::MAX));
let unusable = [IndexConstraint {
column: 0,
op: ConstraintOp::Ge,
usable: false,
}];
let plan = m.best_index(&unusable).unwrap();
assert_eq!(plan.idx_num, series_plan::SCAN);
assert_eq!(plan.argv_index, Vec::<u32>::new());
}
#[test]
fn filter_narrows_generation() {
let m = SeriesModule;
let cons = [
IndexConstraint {
column: 0,
op: ConstraintOp::Ge,
usable: true,
},
IndexConstraint {
column: 0,
op: ConstraintOp::Le,
usable: true,
},
];
let plan = m.best_index(&cons).unwrap();
let cur = SeriesModule::scan(0, 100, 1).unwrap();
let mut cur = m
.filter(cur, &plan, &[Value::Integer(3), Value::Integer(5)])
.unwrap();
let mut out = Vec::new();
while let Some(row) = cur.next().unwrap() {
out.push((row.rowid(), row.value));
}
assert_eq!(out, vec![(1, 3), (2, 4), (3, 5)]);
assert_eq!(cur.generated(), 3, "only 3..=5 generated, not 0..=100");
}
#[test]
fn filter_equality_stays_on_grid() {
let m = SeriesModule;
let cons = [IndexConstraint {
column: 0,
op: ConstraintOp::Eq,
usable: true,
}];
let plan = m.best_index(&cons).unwrap();
let cur = SeriesModule::scan(0, 10, 2).unwrap();
let cur = m.filter(cur, &plan, &[Value::Integer(3)]).unwrap();
assert_eq!(drain(cur), vec![]);
let cur = SeriesModule::scan(0, 10, 2).unwrap();
let cur = m.filter(cur, &plan, &[Value::Integer(6)]).unwrap();
assert_eq!(drain(cur), vec![(1, 6)]);
}
#[test]
fn registry_register_get_roundtrip() {
let mut reg = VTabRegistry::new();
assert!(reg.is_empty());
reg.register("series", Box::new(SeriesModule)).unwrap();
assert_eq!(reg.len(), 1);
assert!(!reg.is_empty());
assert!(reg.get("series").is_some());
assert!(reg.get("SERIES").is_some());
assert!(reg.get("missing").is_none());
}
#[test]
fn registry_rejects_duplicate_names() {
let mut reg = VTabRegistry::new();
reg.register("series", Box::new(SeriesModule)).unwrap();
let err = reg.register("SERIES", Box::new(SeriesModule)).unwrap_err();
assert!(matches!(err, Error::Constraint(_)));
}
#[test]
fn registry_unregister() {
let mut reg = VTabRegistry::new();
reg.register("series", Box::new(SeriesModule)).unwrap();
assert!(reg.unregister("Series").is_some());
assert!(reg.is_empty());
assert!(reg.unregister("series").is_none());
}
#[test]
fn dyn_module_end_to_end() {
let mut reg = VTabRegistry::new();
reg.register("series", Box::new(SeriesModule)).unwrap();
let module = reg.get("series").expect("registered");
let schema = module.dyn_connect(&["2", "8", "2"]).unwrap();
assert_eq!(schema.columns, vec![String::from("value")]);
let plan = module.dyn_best_index(&[]).unwrap();
let mut cur = module.dyn_open(&["2", "8", "2"], &plan, &[]).unwrap();
let mut seen = Vec::new();
while let Some(row) = cur.dyn_next().unwrap() {
seen.push(row.dyn_column(0));
}
assert_eq!(
seen,
vec![
Value::Integer(2),
Value::Integer(4),
Value::Integer(6),
Value::Integer(8),
]
);
}
#[test]
fn dyn_cursor_yields_rows() {
let cur = SeriesModule::scan(10, 12, 1).unwrap();
let mut dyn_cur: Box<dyn DynCursor> = Box::new(cur);
let mut seen = Vec::new();
while let Some(row) = dyn_cur.dyn_next().unwrap() {
seen.push((row.dyn_rowid(), row.dyn_column(0)));
}
assert_eq!(
seen,
vec![
(1, Value::Integer(10)),
(2, Value::Integer(11)),
(3, Value::Integer(12)),
]
);
}
}