#![allow(
clippy::unnecessary_literal_bound,
clippy::too_many_lines,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::fn_params_excessive_bools,
clippy::items_after_statements,
clippy::match_same_arms,
clippy::single_match_else,
clippy::manual_let_else,
clippy::comparison_chain,
clippy::suboptimal_flops,
clippy::unnecessary_wraps,
clippy::useless_let_if_seq,
clippy::redundant_closure_for_method_calls,
clippy::manual_ignore_case_cmp
)]
use std::borrow::Cow;
use std::fmt::Write as _;
use std::sync::Arc;
use fsqlite_error::{FrankenError, Result};
use fsqlite_types::value::{format_sqlite_float, sql_like};
use fsqlite_types::{SmallText, SqliteValue};
use crate::agg_builtins::register_aggregate_builtins;
use crate::datetime::register_datetime_builtins;
use crate::math::register_math_builtins;
use crate::{FunctionRegistry, ScalarFunction};
thread_local! {
static LAST_INSERT_ROWID: std::cell::Cell<i64> = const { std::cell::Cell::new(0) };
static LAST_CHANGES: std::cell::Cell<i64> = const { std::cell::Cell::new(0) };
static TOTAL_CHANGES: std::cell::Cell<i64> = const { std::cell::Cell::new(0) };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChangeTrackingState {
pub last_insert_rowid: i64,
pub last_changes: i64,
pub total_changes: i64,
}
pub fn set_change_tracking_state(state: ChangeTrackingState) {
LAST_INSERT_ROWID.set(state.last_insert_rowid);
LAST_CHANGES.set(state.last_changes);
TOTAL_CHANGES.set(state.total_changes);
}
#[must_use]
pub fn get_change_tracking_state() -> ChangeTrackingState {
ChangeTrackingState {
last_insert_rowid: LAST_INSERT_ROWID.get(),
last_changes: LAST_CHANGES.get(),
total_changes: TOTAL_CHANGES.get(),
}
}
pub fn set_last_insert_rowid(rowid: i64) {
LAST_INSERT_ROWID.set(rowid);
}
pub fn get_last_insert_rowid() -> i64 {
LAST_INSERT_ROWID.get()
}
pub fn set_last_changes(count: i64) {
LAST_CHANGES.set(count);
TOTAL_CHANGES.set(TOTAL_CHANGES.get().saturating_add(count));
}
pub fn get_last_changes() -> i64 {
LAST_CHANGES.get()
}
pub fn get_total_changes() -> i64 {
TOTAL_CHANGES.get()
}
pub fn reset_total_changes() {
TOTAL_CHANGES.set(0);
}
const SQLITE_COMPILE_OPTIONS: &[&str] = &[
"COMPILER=rustc",
#[cfg(feature = "ext-fts5")]
"ENABLE_FTS5",
#[cfg(feature = "ext-geopoly")]
"ENABLE_GEOPOLY",
#[cfg(feature = "ext-icu")]
"ENABLE_ICU",
#[cfg(feature = "ext-json")]
"ENABLE_JSON1",
#[cfg(feature = "ext-rtree")]
"ENABLE_RTREE",
"FRANKENSQLITE",
"OMIT_LOAD_EXTENSION",
"THREADSAFE=1",
];
#[must_use]
pub fn sqlite_compile_options() -> &'static [&'static str] {
SQLITE_COMPILE_OPTIONS
}
fn is_sqlite_compile_option_match(query: &str, option: &str) -> bool {
let trimmed = query.trim();
let normalized = if trimmed
.get(..7)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("SQLITE_"))
{
&trimmed[7..]
} else {
trimmed
};
if normalized.is_empty() {
return false;
}
if option.eq_ignore_ascii_case(normalized) {
return true;
}
option
.get(..normalized.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(normalized))
&& option
.as_bytes()
.get(normalized.len())
.is_none_or(|next| !next.is_ascii_alphanumeric() && *next != b'_')
}
#[must_use]
pub fn sqlite_compileoption_used(query: &str) -> bool {
sqlite_compile_options()
.iter()
.any(|option| is_sqlite_compile_option_match(query, option))
}
fn null_propagate(args: &[SqliteValue]) -> Option<SqliteValue> {
if args.iter().any(SqliteValue::is_null) {
Some(SqliteValue::Null)
} else {
None
}
}
pub struct AbsFunc;
impl ScalarFunction for AbsFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
match &args[0] {
SqliteValue::Integer(i) => {
if *i == i64::MIN {
return Err(FrankenError::IntegerOverflow);
}
Ok(SqliteValue::Integer(i.abs()))
}
other => {
let f = other.to_float();
Ok(SqliteValue::Float(if f < 0.0 { -f } else { f }))
}
}
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"abs"
}
}
pub struct CharFunc;
impl ScalarFunction for CharFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
let mut result = String::new();
for arg in args {
let ch = u32::try_from(arg.to_integer())
.ok()
.and_then(char::from_u32)
.unwrap_or(char::REPLACEMENT_CHARACTER);
result.push(ch);
}
Ok(SqliteValue::Text(SmallText::from_string(result)))
}
fn is_deterministic(&self) -> bool {
true
}
fn num_args(&self) -> i32 {
-1 }
fn name(&self) -> &str {
"char"
}
}
pub struct CoalesceFunc;
impl ScalarFunction for CoalesceFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
for arg in args {
if !arg.is_null() {
return Ok(arg.clone());
}
}
Ok(SqliteValue::Null)
}
fn num_args(&self) -> i32 {
-1
}
fn min_args(&self) -> i32 {
2
}
fn name(&self) -> &str {
"coalesce"
}
}
pub struct ConcatFunc;
impl ScalarFunction for ConcatFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
let mut result = String::new();
for arg in args {
if !arg.is_null() {
result.push_str(text_arg(arg).as_ref());
}
}
Ok(SqliteValue::Text(SmallText::from_string(result)))
}
fn num_args(&self) -> i32 {
-1
}
fn min_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"concat"
}
}
pub struct ConcatWsFunc;
impl ScalarFunction for ConcatWsFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args.is_empty() {
return Ok(SqliteValue::Text(SmallText::new("")));
}
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let sep = text_arg(&args[0]);
let mut result = String::new();
let mut has_part = false;
for arg in &args[1..] {
if !arg.is_null() {
if has_part {
result.push_str(sep.as_ref());
}
result.push_str(text_arg(arg).as_ref());
has_part = true;
}
}
Ok(SqliteValue::Text(SmallText::from_string(result)))
}
fn num_args(&self) -> i32 {
-1
}
fn min_args(&self) -> i32 {
2
}
fn name(&self) -> &str {
"concat_ws"
}
}
pub struct HexFunc;
impl ScalarFunction for HexFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Text(SmallText::new("")));
}
let bytes: Cow<'_, [u8]> = match &args[0] {
SqliteValue::Blob(b) => Cow::Borrowed(b.as_ref()),
SqliteValue::Text(text) => Cow::Borrowed(text.as_bytes_direct()),
other => Cow::Owned(other.to_text().into_bytes()),
};
let mut hex = String::with_capacity(bytes.len() * 2);
for b in bytes.as_ref() {
let _ = write!(hex, "{b:02X}");
}
Ok(SqliteValue::Text(SmallText::from_string(hex)))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"hex"
}
}
pub struct IfnullFunc;
impl ScalarFunction for IfnullFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
Ok(args[1].clone())
} else {
Ok(args[0].clone())
}
}
fn num_args(&self) -> i32 {
2
}
fn name(&self) -> &str {
"ifnull"
}
}
pub struct IifFunc;
impl ScalarFunction for IifFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
let cond = &args[0];
let is_true = match cond {
SqliteValue::Null => false,
SqliteValue::Integer(n) => *n != 0,
SqliteValue::Float(f) => *f != 0.0,
SqliteValue::Text(_) | SqliteValue::Blob(_) => {
let i = cond.to_integer();
if i != 0 { true } else { cond.to_float() != 0.0 }
}
};
if is_true {
Ok(args[1].clone())
} else {
Ok(args[2].clone())
}
}
fn num_args(&self) -> i32 {
3
}
fn name(&self) -> &str {
"iif"
}
}
pub struct InstrFunc;
impl ScalarFunction for InstrFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if let Some(null) = null_propagate(args) {
return Ok(null);
}
match (&args[0], &args[1]) {
(SqliteValue::Blob(haystack), SqliteValue::Blob(needle)) => {
if needle.is_empty() {
return Ok(SqliteValue::Integer(1));
}
if haystack.is_empty() {
return Ok(SqliteValue::Integer(0));
}
let pos = find_bytes(haystack, needle).map_or(0, |p| p + 1);
Ok(SqliteValue::Integer(i64::try_from(pos).unwrap_or(0)))
}
_ => {
let haystack = text_arg(&args[0]);
let needle = text_arg(&args[1]);
let haystack = haystack.as_ref();
let needle = needle.as_ref();
if needle.is_empty() {
return Ok(SqliteValue::Integer(1));
}
if haystack.is_empty() {
return Ok(SqliteValue::Integer(0));
}
let pos = haystack
.find(needle)
.map_or(0, |byte_pos| haystack[..byte_pos].chars().count() + 1);
Ok(SqliteValue::Integer(i64::try_from(pos).unwrap_or(0)))
}
}
}
fn num_args(&self) -> i32 {
2
}
fn name(&self) -> &str {
"instr"
}
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() {
return Some(0);
}
haystack.windows(needle.len()).position(|w| w == needle)
}
fn sqlite_text_until_nul(text: &str) -> &str {
text.split_once('\0').map_or(text, |(prefix, _)| prefix)
}
pub struct LengthFunc;
impl ScalarFunction for LengthFunc {
#[allow(clippy::cast_possible_wrap)]
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let len = match &args[0] {
SqliteValue::Text(s) => {
let text = sqlite_text_until_nul(s.as_str());
if text.is_ascii() {
text.len()
} else {
text.chars().count()
}
}
SqliteValue::Blob(b) => b.len(),
other => {
let text = other.to_text();
let text = sqlite_text_until_nul(&text);
if text.is_ascii() {
text.len()
} else {
text.chars().count()
}
}
};
Ok(SqliteValue::Integer(len as i64))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"length"
}
}
pub struct OctetLengthFunc;
impl ScalarFunction for OctetLengthFunc {
#[allow(clippy::cast_possible_wrap)]
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let len = match &args[0] {
SqliteValue::Text(s) => s.len(),
SqliteValue::Blob(b) => b.len(),
other => other.to_text().len(),
};
Ok(SqliteValue::Integer(len as i64))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"octet_length"
}
}
pub struct LowerFunc;
impl ScalarFunction for LowerFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let lowered = text_arg(&args[0]).as_ref().to_ascii_lowercase();
Ok(SqliteValue::Text(SmallText::from_string(lowered)))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"lower"
}
}
pub struct UpperFunc;
impl ScalarFunction for UpperFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let upper = text_arg(&args[0]).as_ref().to_ascii_uppercase();
Ok(SqliteValue::Text(SmallText::from_string(upper)))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"upper"
}
}
pub struct TrimFunc;
pub struct LtrimFunc;
pub struct RtrimFunc;
fn trim_chars(s: &str, chars: &str) -> String {
let char_set: Vec<char> = chars.chars().collect();
s.trim_matches(|c: char| char_set.contains(&c)).to_owned()
}
fn ltrim_chars(s: &str, chars: &str) -> String {
let char_set: Vec<char> = chars.chars().collect();
s.trim_start_matches(|c: char| char_set.contains(&c))
.to_owned()
}
fn rtrim_chars(s: &str, chars: &str) -> String {
let char_set: Vec<char> = chars.chars().collect();
s.trim_end_matches(|c: char| char_set.contains(&c))
.to_owned()
}
impl ScalarFunction for TrimFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let s = text_arg(&args[0]);
let chars = if args.len() > 1 && !args[1].is_null() {
text_arg(&args[1])
} else {
Cow::Borrowed(" ")
};
Ok(SqliteValue::Text(SmallText::new(
trim_chars(s.as_ref(), chars.as_ref()).as_str(),
)))
}
fn num_args(&self) -> i32 {
-1 }
fn min_args(&self) -> i32 {
1
}
fn max_args(&self) -> Option<i32> {
Some(2)
}
fn name(&self) -> &str {
"trim"
}
}
impl ScalarFunction for LtrimFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let s = text_arg(&args[0]);
let chars = if args.len() > 1 && !args[1].is_null() {
text_arg(&args[1])
} else {
Cow::Borrowed(" ")
};
Ok(SqliteValue::Text(SmallText::new(
ltrim_chars(s.as_ref(), chars.as_ref()).as_str(),
)))
}
fn num_args(&self) -> i32 {
-1
}
fn min_args(&self) -> i32 {
1
}
fn max_args(&self) -> Option<i32> {
Some(2)
}
fn name(&self) -> &str {
"ltrim"
}
}
impl ScalarFunction for RtrimFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let s = text_arg(&args[0]);
let chars = if args.len() > 1 && !args[1].is_null() {
text_arg(&args[1])
} else {
Cow::Borrowed(" ")
};
Ok(SqliteValue::Text(SmallText::new(
rtrim_chars(s.as_ref(), chars.as_ref()).as_str(),
)))
}
fn num_args(&self) -> i32 {
-1
}
fn min_args(&self) -> i32 {
1
}
fn max_args(&self) -> Option<i32> {
Some(2)
}
fn name(&self) -> &str {
"rtrim"
}
}
pub struct NullifFunc;
impl ScalarFunction for NullifFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0] == args[1] {
Ok(SqliteValue::Null)
} else {
Ok(args[0].clone())
}
}
fn num_args(&self) -> i32 {
2
}
fn name(&self) -> &str {
"nullif"
}
}
pub struct TypeofFunc;
impl ScalarFunction for TypeofFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
let type_name = match &args[0] {
SqliteValue::Null => "null",
SqliteValue::Integer(_) => "integer",
SqliteValue::Float(_) => "real",
SqliteValue::Text(_) => "text",
SqliteValue::Blob(_) => "blob",
};
Ok(SqliteValue::Text(SmallText::new(type_name)))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"typeof"
}
}
pub struct SubtypeFunc;
impl ScalarFunction for SubtypeFunc {
fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
Ok(SqliteValue::Integer(0))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"subtype"
}
}
pub struct ReplaceFunc;
impl ScalarFunction for ReplaceFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if let Some(null) = null_propagate(args) {
return Ok(null);
}
let x = text_arg(&args[0]);
let y = text_arg(&args[1]);
let z = text_arg(&args[2]);
if y.is_empty() {
return Ok(SqliteValue::Text(SmallText::from_string(x)));
}
if z.len() > y.len() {
let occurrences = x.matches(y.as_ref()).count();
let final_len = x.len() + occurrences * (z.len() - y.len());
if final_len > 1_000_000_000 {
return Err(FrankenError::TooBig);
}
}
Ok(SqliteValue::Text(SmallText::from_string(
x.replace(y.as_ref(), z.as_ref()),
)))
}
fn num_args(&self) -> i32 {
3
}
fn name(&self) -> &str {
"replace"
}
}
pub struct RoundFunc;
impl ScalarFunction for RoundFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let x = args[0].to_float();
let n = if args.len() > 1 && !args[1].is_null() {
args[1].to_integer().clamp(0, 30)
} else {
0
};
if !(-4_503_599_627_370_496.0..=4_503_599_627_370_496.0).contains(&x) {
return Ok(SqliteValue::Float(x));
}
#[allow(clippy::cast_possible_truncation)]
let rounded = {
let prec = (n as usize) + 15;
let full = format!("{x:.prec$}");
let dot = full.find('.').unwrap_or(full.len());
let rd_idx = dot + 1 + n as usize;
if rd_idx >= full.len() {
format!("{x:.prec$}", prec = n as usize)
.parse::<f64>()
.unwrap_or(x)
} else {
let rd = full.as_bytes()[rd_idx] - b'0';
if rd != 5 || !full[rd_idx + 1..].bytes().all(|b| b == b'0') {
format!("{x:.prec$}", prec = n as usize)
.parse::<f64>()
.unwrap_or(x)
} else {
let mut trunc = full.as_bytes()[..rd_idx].to_vec();
if trunc.last() == Some(&b'.') {
trunc.pop();
}
let start = usize::from(trunc.first() == Some(&b'-'));
let mut carry = true;
for b in trunc[start..].iter_mut().rev() {
if *b == b'.' {
continue;
}
if carry {
if *b == b'9' {
*b = b'0';
} else {
*b += 1;
carry = false;
break;
}
}
}
if carry {
trunc.insert(start, b'1');
}
String::from_utf8(trunc)
.ok()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(x)
}
}
};
Ok(SqliteValue::Float(rounded))
}
fn num_args(&self) -> i32 {
-1 }
fn min_args(&self) -> i32 {
1
}
fn max_args(&self) -> Option<i32> {
Some(2)
}
fn name(&self) -> &str {
"round"
}
}
pub struct SignFunc;
impl ScalarFunction for SignFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
match &args[0] {
SqliteValue::Null => Ok(SqliteValue::Null),
SqliteValue::Integer(i) => Ok(SqliteValue::Integer(i.signum())),
SqliteValue::Float(f) => {
if f.is_nan() {
Ok(SqliteValue::Null)
} else if *f > 0.0 {
Ok(SqliteValue::Integer(1))
} else if *f < 0.0 {
Ok(SqliteValue::Integer(-1))
} else {
Ok(SqliteValue::Integer(0))
}
}
SqliteValue::Text(s) => {
let trimmed = s.trim_matches(|ch: char| ch.is_ascii_whitespace());
if trimmed.is_empty() {
return Ok(SqliteValue::Null);
}
let stripped = trimmed.strip_prefix(['+', '-']).unwrap_or(trimmed);
if stripped.eq_ignore_ascii_case("nan")
|| stripped.eq_ignore_ascii_case("inf")
|| stripped.eq_ignore_ascii_case("infinity")
{
return Ok(SqliteValue::Null);
}
if let Ok(f) = trimmed.parse::<f64>() {
if f > 0.0 {
Ok(SqliteValue::Integer(1))
} else if f < 0.0 {
Ok(SqliteValue::Integer(-1))
} else {
Ok(SqliteValue::Integer(0))
}
} else if let Ok(i) = trimmed.parse::<i64>() {
Ok(SqliteValue::Integer(i.signum()))
} else {
Ok(SqliteValue::Null)
}
}
SqliteValue::Blob(_) => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"sign"
}
}
pub struct RandomFunc;
impl ScalarFunction for RandomFunc {
fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
let val = simple_random_i64();
Ok(SqliteValue::Integer(val))
}
fn is_deterministic(&self) -> bool {
false
}
fn num_args(&self) -> i32 {
0
}
fn name(&self) -> &str {
"random"
}
}
fn simple_random_i64() -> i64 {
use std::sync::atomic::{AtomicU64, Ordering};
static STATE: AtomicU64 = AtomicU64::new(0xD1B5_4A32_D192_ED03);
let mut x = STATE.fetch_add(0x9E37_79B9_7F4A_7C15, Ordering::Relaxed);
x ^= x >> 30;
x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
x ^= x >> 27;
x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
x ^= x >> 31;
x as i64
}
pub struct RandomblobFunc;
impl ScalarFunction for RandomblobFunc {
#[allow(clippy::cast_sign_loss)]
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
let n_i64 = if args[0].is_null() {
1
} else {
args[0].to_integer().max(1)
};
if n_i64 > 1_000_000_000 {
return Err(FrankenError::TooBig);
}
let n = n_i64 as usize;
let mut buf = vec![0u8; n];
let mut i = 0;
while i < n {
let rnd = simple_random_i64().to_ne_bytes();
let to_copy = (n - i).min(8);
buf[i..i + to_copy].copy_from_slice(&rnd[..to_copy]);
i += to_copy;
}
Ok(SqliteValue::Blob(Arc::from(buf.as_slice())))
}
fn is_deterministic(&self) -> bool {
false
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"randomblob"
}
}
pub struct ZeroblobFunc;
impl ScalarFunction for ZeroblobFunc {
#[allow(clippy::cast_sign_loss)]
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Blob(Arc::from([] as [u8; 0])));
}
let n_i64 = args[0].to_integer().max(0);
if n_i64 > 1_000_000_000 {
return Err(FrankenError::TooBig);
}
let n = n_i64 as usize;
Ok(SqliteValue::Blob(Arc::from(vec![0u8; n].as_slice())))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"zeroblob"
}
}
pub struct QuoteFunc;
impl ScalarFunction for QuoteFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
let result = quote_sql_value(&args[0], false);
Ok(SqliteValue::Text(SmallText::from_string(result)))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"quote"
}
}
pub struct UnistrQuoteFunc;
impl ScalarFunction for UnistrQuoteFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
let result = quote_sql_value(&args[0], true);
Ok(SqliteValue::Text(SmallText::from_string(result)))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"unistr_quote"
}
}
fn quote_sql_value(value: &SqliteValue, use_unistr_quote: bool) -> String {
match value {
SqliteValue::Null => "NULL".to_owned(),
SqliteValue::Integer(i) => i.to_string(),
SqliteValue::Float(f) => format_sqlite_float(*f),
SqliteValue::Text(s) => quote_sql_text_literal(s.as_str(), use_unistr_quote),
SqliteValue::Blob(b) => {
let mut hex = String::with_capacity(3 + b.len() * 2);
hex.push_str("X'");
for byte in b.iter() {
let _ = write!(hex, "{byte:02X}");
}
hex.push('\'');
hex
}
}
}
fn quote_sql_text_literal(text: &str, use_unistr_quote: bool) -> String {
let text = sqlite_text_until_nul(text);
if use_unistr_quote && text.chars().any(is_unistr_control_char) {
return unistr_quote_sql_text_literal(text);
}
let mut quoted = String::with_capacity(text.len() + 2);
quoted.push('\'');
append_sql_string_literal_body(&mut quoted, text);
quoted.push('\'');
quoted
}
fn unistr_quote_sql_text_literal(text: &str) -> String {
let mut quoted = String::with_capacity(text.len() + 12);
quoted.push_str("unistr('");
for ch in text.chars() {
match ch {
'\'' => quoted.push_str("''"),
'\\' => quoted.push_str("\\\\"),
_ if is_unistr_control_char(ch) => {
let _ = write!(quoted, "\\u{:04x}", ch as u32);
}
_ => quoted.push(ch),
}
}
quoted.push_str("')");
quoted
}
fn append_sql_string_literal_body(out: &mut String, text: &str) {
for ch in text.chars() {
if ch == '\'' {
out.push_str("''");
} else {
out.push(ch);
}
}
}
fn is_unistr_control_char(ch: char) -> bool {
matches!(ch, '\u{0001}'..='\u{001F}')
}
pub struct UnhexFunc;
impl ScalarFunction for UnhexFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
if args.len() > 1 && args[1].is_null() {
return Ok(SqliteValue::Null);
}
let input = text_arg(&args[0]);
let ignore_chars: Vec<char> = if args.len() > 1 {
text_arg(&args[1])
.chars()
.filter(|&c| hex_digit(c).is_none())
.collect()
} else {
Vec::new()
};
let mut bytes = Vec::with_capacity(input.len() / 2);
let mut hi_nibble = None;
for c in input.as_ref().chars() {
if ignore_chars.contains(&c) {
if hi_nibble.is_some() {
return Ok(SqliteValue::Null);
}
continue;
}
let digit = match hex_digit(c) {
Some(v) => v,
None => return Ok(SqliteValue::Null),
};
if let Some(hi) = hi_nibble.take() {
bytes.push(hi << 4 | digit);
} else {
hi_nibble = Some(digit);
}
}
if hi_nibble.is_some() {
return Ok(SqliteValue::Null);
}
Ok(SqliteValue::Blob(Arc::from(bytes.as_slice())))
}
fn num_args(&self) -> i32 {
-1 }
fn min_args(&self) -> i32 {
1
}
fn max_args(&self) -> Option<i32> {
Some(2)
}
fn name(&self) -> &str {
"unhex"
}
}
fn hex_digit(c: char) -> Option<u8> {
match c {
'0'..='9' => Some(c as u8 - b'0'),
'a'..='f' => Some(c as u8 - b'a' + 10),
'A'..='F' => Some(c as u8 - b'A' + 10),
_ => None,
}
}
pub struct UnicodeFunc;
impl ScalarFunction for UnicodeFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
if let SqliteValue::Blob(bytes) = &args[0] {
return Ok(
sqlite_blob_first_codepoint(bytes).map_or(SqliteValue::Null, SqliteValue::Integer)
);
}
let s = text_arg(&args[0]);
match sqlite_text_until_nul(s.as_ref()).chars().next() {
Some(c) => Ok(SqliteValue::Integer(i64::from(c as u32))),
None => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"unicode"
}
}
fn sqlite_blob_first_codepoint(bytes: &[u8]) -> Option<i64> {
let first = *bytes.first()?;
if first == 0 {
return None;
}
let mut codepoint = match first {
0x00..=0xBF => u32::from(first),
0xC0..=0xDF => u32::from(first & 0x1F),
0xE0..=0xEF => u32::from(first & 0x0F),
0xF0..=0xF7 => u32::from(first & 0x07),
_ => 0xFFFD,
};
if first >= 0xC0 && first <= 0xF7 {
for byte in bytes
.iter()
.copied()
.skip(1)
.take_while(|byte| byte & 0xC0 == 0x80)
{
codepoint = codepoint
.wrapping_shl(6)
.wrapping_add(u32::from(byte & 0x3F));
}
if codepoint < 0x80
|| (codepoint & 0xFFFF_F800) == 0xD800
|| (codepoint & 0xFFFF_FFFE) == 0xFFFE
{
codepoint = 0xFFFD;
}
}
Some(i64::from(codepoint))
}
pub struct SubstrFunc;
impl ScalarFunction for SubstrFunc {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() || args[1].is_null() {
return Ok(SqliteValue::Null);
}
let is_blob = matches!(&args[0], SqliteValue::Blob(_));
if is_blob {
return self.invoke_blob(args);
}
let text = text_arg(&args[0]);
let s = text.as_ref();
let ascii_fast_path = s.is_ascii();
let len = if ascii_fast_path {
s.len() as i64
} else {
s.chars().count() as i64
};
let has_length = args.len() > 2 && !args[2].is_null();
let mut p1 = args[1].to_integer();
let mut p2 = if has_length {
args[2].to_integer()
} else {
1_000_000_000
};
let neg_p2 = p2 < 0;
if neg_p2 {
p2 = p2.saturating_neg();
}
if p1 < 0 {
p1 = p1.saturating_add(len);
if p1 < 0 {
p2 = p2.saturating_add(p1);
p1 = 0;
}
} else if p1 > 0 {
p1 -= 1;
} else if p2 > 0 {
p2 -= 1; }
if neg_p2 {
p1 = p1.saturating_sub(p2);
if p1 < 0 {
p2 = p2.saturating_add(p1);
p1 = 0;
}
}
if p1.saturating_add(p2) > len {
p2 = len.saturating_sub(p1);
}
if p2 <= 0 {
return Ok(SqliteValue::Text(SmallText::new("")));
}
if ascii_fast_path {
let start = p1 as usize;
let end = (p1 + p2) as usize;
return Ok(SqliteValue::Text(SmallText::new(&s[start..end])));
}
let chars: Vec<char> = s.chars().collect();
let result: String = chars[p1 as usize..(p1 + p2) as usize].iter().collect();
Ok(SqliteValue::Text(SmallText::from_string(result)))
}
fn num_args(&self) -> i32 {
-1 }
fn min_args(&self) -> i32 {
2
}
fn max_args(&self) -> Option<i32> {
Some(3)
}
fn name(&self) -> &str {
"substr"
}
}
impl SubstrFunc {
#[allow(
clippy::unused_self,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
fn invoke_blob(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
let blob = match &args[0] {
SqliteValue::Blob(b) => b,
_ => return Ok(SqliteValue::Null),
};
let len = blob.len() as i64;
let has_length = args.len() > 2 && !args[2].is_null();
let mut p1 = args[1].to_integer();
let mut p2 = if has_length {
args[2].to_integer()
} else {
1_000_000_000
};
let neg_p2 = p2 < 0;
if neg_p2 {
p2 = p2.saturating_neg();
}
if p1 < 0 {
p1 = p1.saturating_add(len);
if p1 < 0 {
p2 = p2.saturating_add(p1);
p1 = 0;
}
} else if p1 > 0 {
p1 -= 1;
} else if p2 > 0 {
p2 -= 1;
}
if neg_p2 {
p1 = p1.saturating_sub(p2);
if p1 < 0 {
p2 = p2.saturating_add(p1);
p1 = 0;
}
}
if p1.saturating_add(p2) > len {
p2 = len.saturating_sub(p1);
}
if p2 <= 0 {
return Ok(SqliteValue::Blob(Arc::from([] as [u8; 0])));
}
Ok(SqliteValue::Blob(Arc::from(
&blob[p1 as usize..(p1 + p2) as usize],
)))
}
}
pub struct SoundexFunc;
impl ScalarFunction for SoundexFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Text(SmallText::new("?000")));
}
let s = text_arg(&args[0]);
Ok(SqliteValue::Text(SmallText::from_string(soundex(
s.as_ref(),
))))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"soundex"
}
}
fn soundex(s: &str) -> String {
let mut chars = s.chars().filter(|c| c.is_ascii_alphabetic());
let first = match chars.next() {
Some(c) => c.to_ascii_uppercase(),
None => return "?000".to_owned(),
};
let code = |c: char| -> Option<char> {
match c.to_ascii_uppercase() {
'B' | 'F' | 'P' | 'V' => Some('1'),
'C' | 'G' | 'J' | 'K' | 'Q' | 'S' | 'X' | 'Z' => Some('2'),
'D' | 'T' => Some('3'),
'L' => Some('4'),
'M' | 'N' => Some('5'),
'R' => Some('6'),
_ => None, }
};
let mut result = String::with_capacity(4);
result.push(first);
let mut last_code = code(first);
for c in chars {
if result.len() >= 4 {
break;
}
let current = code(c);
if let Some(digit) = current {
if current != last_code {
result.push(digit);
}
}
last_code = current;
}
while result.len() < 4 {
result.push('0');
}
result
}
pub struct ScalarMaxFunc;
impl ScalarFunction for ScalarMaxFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if let Some(null) = null_propagate(args) {
return Ok(null);
}
let mut max = &args[0];
for arg in &args[1..] {
if arg.partial_cmp(max) == Some(std::cmp::Ordering::Greater) {
max = arg;
}
}
Ok(max.clone())
}
fn num_args(&self) -> i32 {
-1
}
fn min_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"max"
}
}
pub struct ScalarMinFunc;
impl ScalarFunction for ScalarMinFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if let Some(null) = null_propagate(args) {
return Ok(null);
}
let mut min = &args[0];
for arg in &args[1..] {
if arg.partial_cmp(min) == Some(std::cmp::Ordering::Less) {
min = arg;
}
}
Ok(min.clone())
}
fn num_args(&self) -> i32 {
-1
}
fn min_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"min"
}
}
pub struct LikelihoodFunc;
impl ScalarFunction for LikelihoodFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
Ok(args[0].clone())
}
fn num_args(&self) -> i32 {
2
}
fn name(&self) -> &str {
"likelihood"
}
}
pub struct LikelyFunc;
impl ScalarFunction for LikelyFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
Ok(args[0].clone())
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"likely"
}
}
pub struct UnlikelyFunc;
impl ScalarFunction for UnlikelyFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
Ok(args[0].clone())
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"unlikely"
}
}
pub struct SqliteVersionFunc;
impl ScalarFunction for SqliteVersionFunc {
fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
Ok(SqliteValue::Text(SmallText::new(
fsqlite_types::FRANKENSQLITE_SQLITE_VERSION,
)))
}
fn num_args(&self) -> i32 {
0
}
fn name(&self) -> &str {
"sqlite_version"
}
}
pub struct SqliteSourceIdFunc;
impl ScalarFunction for SqliteSourceIdFunc {
fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
Ok(SqliteValue::Text(SmallText::new(
fsqlite_types::FRANKENSQLITE_SOURCE_ID,
)))
}
fn num_args(&self) -> i32 {
0
}
fn name(&self) -> &str {
"sqlite_source_id"
}
}
pub struct SqliteCompileoptionUsedFunc;
impl ScalarFunction for SqliteCompileoptionUsedFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let query = text_arg(&args[0]);
Ok(SqliteValue::Integer(i64::from(sqlite_compileoption_used(
query.as_ref(),
))))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"sqlite_compileoption_used"
}
}
pub struct SqliteCompileoptionGetFunc;
impl ScalarFunction for SqliteCompileoptionGetFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let n = args[0].to_integer();
#[allow(clippy::cast_sign_loss)]
match sqlite_compile_options().get(n as usize) {
Some(opt) => Ok(SqliteValue::Text(SmallText::new(opt))),
None => Ok(SqliteValue::Null),
}
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"sqlite_compileoption_get"
}
}
pub struct LikeFunc;
impl ScalarFunction for LikeFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if let Some(null) = null_propagate(args) {
return Ok(null);
}
let pattern = text_arg(&args[0]);
let string = text_arg(&args[1]);
let escape = if args.len() > 2 && !args[2].is_null() {
Some(single_char_escape(text_arg(&args[2]).as_ref())?)
} else {
None
};
let matched = like_match(pattern.as_ref(), string.as_ref(), escape);
Ok(SqliteValue::Integer(i64::from(matched)))
}
fn num_args(&self) -> i32 {
-1 }
fn name(&self) -> &str {
"like"
}
}
fn single_char_escape(escape: &str) -> Result<char> {
let mut chars = escape.chars();
match (chars.next(), chars.next()) {
(Some(ch), None) => Ok(ch),
_ => Err(FrankenError::function_error(
"ESCAPE expression must be a single character",
)),
}
}
fn like_match(pattern: &str, string: &str, escape: Option<char>) -> bool {
sql_like(pattern, string, escape)
}
pub struct GlobFunc;
impl ScalarFunction for GlobFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if let Some(null) = null_propagate(args) {
return Ok(null);
}
let pattern = text_arg(&args[0]);
let string = text_arg(&args[1]);
let matched = glob_match(pattern.as_ref(), string.as_ref());
Ok(SqliteValue::Integer(i64::from(matched)))
}
fn num_args(&self) -> i32 {
2
}
fn name(&self) -> &str {
"glob"
}
}
fn glob_match(pattern: &str, string: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let txt: Vec<char> = string.chars().collect();
glob_match_inner(&pat, &txt, 0, 0)
}
fn text_arg(value: &SqliteValue) -> Cow<'_, str> {
match value.as_text_str() {
Some(text) => Cow::Borrowed(text),
None => Cow::Owned(value.to_text()),
}
}
fn glob_match_inner(pat: &[char], txt: &[char], mut pi: usize, mut ti: usize) -> bool {
while pi < pat.len() {
match pat[pi] {
'*' => {
while pi < pat.len() && pat[pi] == '*' {
pi += 1;
}
if pi >= pat.len() {
return true;
}
for start in ti..=txt.len() {
if glob_match_inner(pat, txt, pi, start) {
return true;
}
}
return false;
}
'?' => {
if ti >= txt.len() {
return false;
}
pi += 1;
ti += 1;
}
'[' => {
if ti >= txt.len() {
return false;
}
pi += 1;
let negate = pi < pat.len() && pat[pi] == '^';
if negate {
pi += 1;
}
let mut found = false;
let mut first = true;
while pi < pat.len() && (first || pat[pi] != ']') {
first = false;
if pi + 2 < pat.len() && pat[pi + 1] == '-' {
let lo = pat[pi];
let hi = pat[pi + 2];
if txt[ti] >= lo && txt[ti] <= hi {
found = true;
}
pi += 3;
} else {
if txt[ti] == pat[pi] {
found = true;
}
pi += 1;
}
}
if pi < pat.len() && pat[pi] == ']' {
pi += 1;
}
if found == negate {
return false;
}
ti += 1;
}
c => {
if ti >= txt.len() || txt[ti] != c {
return false;
}
pi += 1;
ti += 1;
}
}
}
ti >= txt.len()
}
pub struct UnistrFunc;
const INVALID_UNISTR_ESCAPE: &str = "invalid Unicode escape";
fn decode_unistr_escape(chars: &mut std::str::Chars<'_>, digits: usize) -> Result<char> {
let mut lookahead = chars.clone();
let mut codepoint = 0u32;
for _ in 0..digits {
let Some(ch) = lookahead.next() else {
return Err(FrankenError::function_error(INVALID_UNISTR_ESCAPE));
};
let Some(digit) = hex_digit(ch) else {
return Err(FrankenError::function_error(INVALID_UNISTR_ESCAPE));
};
codepoint = (codepoint << 4) | u32::from(digit);
}
for _ in 0..digits {
let _digit = chars.next();
}
char::from_u32(codepoint).ok_or_else(|| FrankenError::function_error(INVALID_UNISTR_ESCAPE))
}
impl ScalarFunction for UnistrFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args[0].is_null() {
return Ok(SqliteValue::Null);
}
let input = text_arg(&args[0]);
let mut result = String::with_capacity(input.len());
let mut chars = input.as_ref().chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
if chars.as_str().starts_with('\\') {
let _ = chars.next();
result.push('\\');
continue;
}
let digits = if chars.as_str().starts_with('+') {
let _plus = chars.next();
6
} else if chars.as_str().starts_with('u') {
let _marker = chars.next();
4
} else if chars.as_str().starts_with('U') {
let _marker = chars.next();
8
} else {
4
};
result.push(decode_unistr_escape(&mut chars, digits)?);
continue;
}
result.push(ch);
}
Ok(SqliteValue::Text(SmallText::from_string(result)))
}
fn num_args(&self) -> i32 {
1
}
fn name(&self) -> &str {
"unistr"
}
}
pub struct ChangesFunc;
impl ScalarFunction for ChangesFunc {
fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
Ok(SqliteValue::Integer(LAST_CHANGES.get()))
}
fn is_deterministic(&self) -> bool {
false
}
fn num_args(&self) -> i32 {
0
}
fn name(&self) -> &str {
"changes"
}
}
pub struct TotalChangesFunc;
impl ScalarFunction for TotalChangesFunc {
fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
Ok(SqliteValue::Integer(TOTAL_CHANGES.get()))
}
fn is_deterministic(&self) -> bool {
false
}
fn num_args(&self) -> i32 {
0
}
fn name(&self) -> &str {
"total_changes"
}
}
pub struct LastInsertRowidFunc;
impl ScalarFunction for LastInsertRowidFunc {
fn invoke(&self, _args: &[SqliteValue]) -> Result<SqliteValue> {
Ok(SqliteValue::Integer(LAST_INSERT_ROWID.get()))
}
fn is_deterministic(&self) -> bool {
false
}
fn num_args(&self) -> i32 {
0
}
fn name(&self) -> &str {
"last_insert_rowid"
}
}
#[allow(clippy::too_many_lines)]
pub fn register_builtins(registry: &mut FunctionRegistry) {
registry.register_scalar(AbsFunc);
registry.register_scalar(SignFunc);
registry.register_scalar(RoundFunc);
registry.register_scalar(RandomFunc);
registry.register_scalar(RandomblobFunc);
registry.register_scalar(ZeroblobFunc);
registry.register_scalar(LowerFunc);
registry.register_scalar(UpperFunc);
registry.register_scalar(LengthFunc);
registry.register_scalar(OctetLengthFunc);
registry.register_scalar(TrimFunc);
registry.register_scalar(LtrimFunc);
registry.register_scalar(RtrimFunc);
registry.register_scalar(ReplaceFunc);
registry.register_scalar(SubstrFunc);
registry.register_scalar(InstrFunc);
registry.register_scalar(CharFunc);
registry.register_scalar(UnicodeFunc);
registry.register_scalar(UnistrFunc);
registry.register_scalar(HexFunc);
registry.register_scalar(UnhexFunc);
registry.register_scalar(QuoteFunc);
registry.register_scalar(UnistrQuoteFunc);
registry.register_scalar(SoundexFunc);
registry.register_scalar(TypeofFunc);
registry.register_scalar(SubtypeFunc);
registry.register_scalar(CoalesceFunc);
registry.register_scalar(IfnullFunc);
registry.register_scalar(NullifFunc);
registry.register_scalar(IifFunc);
registry.register_scalar(ConcatFunc);
registry.register_scalar(ConcatWsFunc);
registry.register_scalar(ScalarMaxFunc);
registry.register_scalar(ScalarMinFunc);
registry.register_scalar(LikelihoodFunc);
registry.register_scalar(LikelyFunc);
registry.register_scalar(UnlikelyFunc);
registry.register_scalar(LikeFunc);
registry.register_scalar(GlobFunc);
registry.register_scalar(SqliteVersionFunc);
registry.register_scalar(SqliteSourceIdFunc);
registry.register_scalar(SqliteCompileoptionUsedFunc);
registry.register_scalar(SqliteCompileoptionGetFunc);
registry.register_scalar(ChangesFunc);
registry.register_scalar(TotalChangesFunc);
registry.register_scalar(LastInsertRowidFunc);
struct IfFunc;
impl ScalarFunction for IfFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
IifFunc.invoke(args)
}
fn num_args(&self) -> i32 {
3
}
fn name(&self) -> &str {
"if"
}
}
registry.register_scalar(IfFunc);
struct SubstringFunc;
impl ScalarFunction for SubstringFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
SubstrFunc.invoke(args)
}
fn num_args(&self) -> i32 {
-1
}
fn min_args(&self) -> i32 {
2
}
fn max_args(&self) -> Option<i32> {
Some(3)
}
fn name(&self) -> &str {
"substring"
}
}
registry.register_scalar(SubstringFunc);
struct PrintfFunc;
impl ScalarFunction for PrintfFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
FormatFunc.invoke(args)
}
fn num_args(&self) -> i32 {
-1
}
fn name(&self) -> &str {
"printf"
}
}
registry.register_scalar(FormatFunc);
registry.register_scalar(PrintfFunc);
register_math_builtins(registry);
register_datetime_builtins(registry);
register_aggregate_builtins(registry);
}
pub struct FormatFunc;
impl ScalarFunction for FormatFunc {
fn invoke(&self, args: &[SqliteValue]) -> Result<SqliteValue> {
if args.is_empty() || args[0].is_null() {
return Ok(SqliteValue::Null);
}
let fmt_str = args[0].to_text();
let params = &args[1..];
let result = sqlite_format(&fmt_str, params)?;
Ok(SqliteValue::Text(SmallText::from_string(result)))
}
fn num_args(&self) -> i32 {
-1
}
fn name(&self) -> &str {
"format"
}
}
fn sqlite_format(fmt: &str, params: &[SqliteValue]) -> Result<String> {
let mut result = String::new();
let chars: Vec<char> = fmt.chars().collect();
let mut i = 0;
let mut param_idx = 0;
while i < chars.len() {
if chars[i] != '%' {
result.push(chars[i]);
i += 1;
continue;
}
i += 1;
if i >= chars.len() {
break;
}
let mut left_align = false;
let mut show_sign = false;
let mut space_sign = false;
let mut zero_pad = false;
loop {
if i >= chars.len() {
break;
}
match chars[i] {
'-' => left_align = true,
'+' => show_sign = true,
' ' => space_sign = true,
'0' => zero_pad = true,
_ => break,
}
i += 1;
}
let mut width = 0usize;
while i < chars.len() && chars[i].is_ascii_digit() {
width = width
.saturating_mul(10)
.saturating_add(chars[i] as usize - '0' as usize)
.min(100_000_000); i += 1;
}
let mut precision = None;
if i < chars.len() && chars[i] == '.' {
i += 1;
let mut prec = 0usize;
while i < chars.len() && chars[i].is_ascii_digit() {
prec = prec
.saturating_mul(10)
.saturating_add(chars[i] as usize - '0' as usize)
.min(100_000_000); i += 1;
}
precision = Some(prec);
}
if i >= chars.len() {
break;
}
let spec = chars[i];
i += 1;
match spec {
'%' => result.push('%'),
'n' => {} 'd' | 'i' => {
let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
param_idx += 1;
let formatted =
format_integer(val, width, left_align, show_sign, space_sign, zero_pad);
result.push_str(&formatted);
}
'f' => {
let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
param_idx += 1;
let prec = precision.unwrap_or(6);
let formatted = format_float_f(
val, prec, width, left_align, show_sign, space_sign, zero_pad,
);
result.push_str(&formatted);
}
'e' | 'E' => {
let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
param_idx += 1;
let prec = precision.unwrap_or(6);
let raw = if spec == 'e' {
format!("{val:.prec$e}")
} else {
format!("{val:.prec$E}")
};
let formatted = normalize_exponent(&raw);
result.push_str(&pad_string(&formatted, width, left_align));
}
'g' | 'G' => {
let val = params.get(param_idx).map_or(0.0, SqliteValue::to_float);
param_idx += 1;
let prec = precision.unwrap_or(6);
let sig = prec.max(1);
let formatted = format_float_g(val, sig, spec == 'G');
result.push_str(&pad_string(&formatted, width, left_align));
}
's' | 'z' => {
let param = params.get(param_idx);
param_idx += 1;
let val = match param {
Some(SqliteValue::Null) | None => String::new(),
Some(v) => v.to_text(),
};
let truncated = if let Some(prec) = precision {
val.chars().take(prec).collect::<String>()
} else {
val
};
result.push_str(&pad_string(&truncated, width, left_align));
}
'q' => {
let param = params.get(param_idx);
param_idx += 1;
match param {
Some(SqliteValue::Null) | None => {
result.push_str("(NULL)");
}
Some(v) => {
let val = v.to_text();
let escaped = val.replace('\'', "''");
result.push_str(&escaped);
}
}
}
'Q' => {
let param = params.get(param_idx);
param_idx += 1;
match param {
Some(SqliteValue::Null) | None => result.push_str("NULL"),
Some(v) => {
let val = v.to_text();
let escaped = val.replace('\'', "''");
result.push('\'');
result.push_str(&escaped);
result.push('\'');
}
}
}
'w' => {
let param = params.get(param_idx);
param_idx += 1;
if matches!(param, Some(SqliteValue::Null) | None) {
} else {
let val = param.map(SqliteValue::to_text).unwrap_or_default();
let escaped = val.replace('"', "\"\"");
result.push_str(&escaped);
}
}
'x' | 'X' => {
let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
param_idx += 1;
#[allow(clippy::cast_sign_loss)]
let formatted = if spec == 'x' {
format!("{:x}", val as u64)
} else {
format!("{:X}", val as u64)
};
let padded = if zero_pad && width > formatted.len() {
let pad = "0".repeat(width - formatted.len());
format!("{pad}{formatted}")
} else {
pad_string(&formatted, width, left_align)
};
result.push_str(&padded);
}
'o' => {
let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
param_idx += 1;
#[allow(clippy::cast_sign_loss)]
let formatted = format!("{:o}", val as u64);
let padded = if zero_pad && width > formatted.len() {
let pad = "0".repeat(width - formatted.len());
format!("{pad}{formatted}")
} else {
pad_string(&formatted, width, left_align)
};
result.push_str(&padded);
}
'c' => {
let val = params.get(param_idx).map_or(0, SqliteValue::to_integer);
param_idx += 1;
#[allow(clippy::cast_sign_loss)]
if let Some(c) = char::from_u32(val as u32) {
result.push(c);
}
}
_ => {
result.push('%');
result.push(spec);
}
}
let _ = (left_align, show_sign, space_sign, zero_pad);
}
Ok(result)
}
fn format_integer(
val: i64,
width: usize,
left_align: bool,
show_sign: bool,
space_sign: bool,
zero_pad: bool,
) -> String {
let sign = if val < 0 {
"-".to_owned()
} else if show_sign {
"+".to_owned()
} else if space_sign {
" ".to_owned()
} else {
String::new()
};
let digits = format!("{}", val.unsigned_abs());
let body = format!("{sign}{digits}");
if body.len() >= width {
return body;
}
let pad = width - body.len();
if left_align {
format!("{body}{}", " ".repeat(pad))
} else if zero_pad {
format!("{sign}{}{digits}", "0".repeat(pad))
} else {
format!("{}{body}", " ".repeat(pad))
}
}
fn format_float_f(
val: f64,
prec: usize,
width: usize,
left_align: bool,
show_sign: bool,
space_sign: bool,
zero_pad: bool,
) -> String {
let sign = if val.is_sign_negative() {
"-".to_owned()
} else if show_sign {
"+".to_owned()
} else if space_sign {
" ".to_owned()
} else {
String::new()
};
let digits = format!("{:.prec$}", val.abs());
let body = format!("{sign}{digits}");
if body.len() >= width {
return body;
}
let pad = width - body.len();
if left_align {
format!("{body}{}", " ".repeat(pad))
} else if zero_pad {
format!("{sign}{}{digits}", "0".repeat(pad))
} else {
format!("{}{body}", " ".repeat(pad))
}
}
fn pad_string(s: &str, width: usize, left_align: bool) -> String {
if s.len() >= width {
return s.to_owned();
}
let pad = width - s.len();
if left_align {
format!("{s}{}", " ".repeat(pad))
} else {
format!("{}{s}", " ".repeat(pad))
}
}
fn normalize_exponent(s: &str) -> String {
let (prefix, e_char, exp_part) = if let Some(pos) = s.find('e') {
(&s[..pos], 'e', &s[pos + 1..])
} else if let Some(pos) = s.find('E') {
(&s[..pos], 'E', &s[pos + 1..])
} else {
return s.to_owned();
};
let (sign, digits) = if let Some(rest) = exp_part.strip_prefix('-') {
("-", rest)
} else if let Some(rest) = exp_part.strip_prefix('+') {
("+", rest)
} else {
("+", exp_part)
};
let padded = if digits.len() < 2 {
format!("0{digits}")
} else {
digits.to_owned()
};
format!("{prefix}{e_char}{sign}{padded}")
}
fn format_float_g(val: f64, sig: usize, upper: bool) -> String {
if !val.is_finite() {
return format!("{val}");
}
let e_str = format!("{val:.prec$e}", prec = sig.saturating_sub(1));
let exp: i32 = e_str
.rsplit_once('e')
.and_then(|(_, e)| e.parse().ok())
.unwrap_or(0);
#[allow(clippy::cast_possible_wrap)]
let formatted = if exp < -4 || exp >= sig as i32 {
let s = format!("{val:.prec$e}", prec = sig.saturating_sub(1));
let s = if upper { s.replace('e', "E") } else { s };
let trimmed = if s.contains('.') {
if let Some(e_pos) = s.find('e').or_else(|| s.find('E')) {
let mantissa = s[..e_pos].trim_end_matches('0').trim_end_matches('.');
format!("{mantissa}{}", &s[e_pos..])
} else {
s.trim_end_matches('0').trim_end_matches('.').to_owned()
}
} else {
s
};
normalize_exponent(&trimmed)
} else {
let decimal_places = if exp >= 0 {
sig.saturating_sub((exp + 1) as usize)
} else {
sig + exp.unsigned_abs() as usize - 1
};
let s = format!("{val:.decimal_places$}");
s.trim_end_matches('0').trim_end_matches('.').to_owned()
};
formatted
}
#[cfg(test)]
#[allow(clippy::too_many_lines)]
mod tests {
use super::*;
fn invoke1(f: &dyn ScalarFunction, v: SqliteValue) -> Result<SqliteValue> {
f.invoke(&[v])
}
fn invoke2(f: &dyn ScalarFunction, a: SqliteValue, b: SqliteValue) -> Result<SqliteValue> {
f.invoke(&[a, b])
}
fn assert_wrong_arg_count(registry: &FunctionRegistry, name: &str, arity: i32) {
let function = registry
.find_scalar(name, arity)
.expect("known scalar name with bad arity returns erroring scalar");
let args = vec![SqliteValue::Null; arity.max(0) as usize];
let err = function
.invoke(&args)
.expect_err("wrong arity should return function error");
let expected = format!("wrong number of arguments to function {name}()");
assert!(
matches!(&err, FrankenError::FunctionError(message) if message == &expected),
"expected {expected:?}, got {err:?}"
);
}
#[test]
fn test_get_change_tracking_state_returns_thread_local_snapshot() {
let original = get_change_tracking_state();
let expected = ChangeTrackingState {
last_insert_rowid: 17,
last_changes: 23,
total_changes: 42,
};
set_change_tracking_state(expected);
assert_eq!(get_change_tracking_state(), expected);
set_change_tracking_state(original);
}
#[test]
fn test_abs_positive() {
assert_eq!(
invoke1(&AbsFunc, SqliteValue::Integer(42)).unwrap(),
SqliteValue::Integer(42)
);
}
#[test]
fn test_abs_negative() {
assert_eq!(
invoke1(&AbsFunc, SqliteValue::Integer(-42)).unwrap(),
SqliteValue::Integer(42)
);
}
#[test]
fn test_abs_null() {
assert_eq!(
invoke1(&AbsFunc, SqliteValue::Null).unwrap(),
SqliteValue::Null
);
}
#[test]
fn test_abs_min_i64_overflow() {
let err = invoke1(&AbsFunc, SqliteValue::Integer(i64::MIN)).unwrap_err();
assert!(matches!(err, FrankenError::IntegerOverflow));
}
#[test]
fn test_abs_string_coercion() {
assert_eq!(
invoke1(&AbsFunc, SqliteValue::Text(SmallText::from_string("-7.5"))).unwrap(),
SqliteValue::Float(7.5)
);
}
#[test]
fn test_abs_whitespace_padded_text() {
assert_eq!(
invoke1(
&AbsFunc,
SqliteValue::Text(SmallText::from_string(" 42 "))
)
.unwrap(),
SqliteValue::Float(42.0)
);
assert_eq!(
invoke1(
&AbsFunc,
SqliteValue::Text(SmallText::from_string(" -7.5 "))
)
.unwrap(),
SqliteValue::Float(7.5)
);
assert_eq!(
invoke1(&AbsFunc, SqliteValue::Text(SmallText::from_string("abc"))).unwrap(),
SqliteValue::Float(0.0)
);
}
#[test]
#[allow(clippy::approx_constant)]
fn test_abs_float() {
assert_eq!(
invoke1(&AbsFunc, SqliteValue::Float(-3.14)).unwrap(),
SqliteValue::Float(3.14)
);
}
#[test]
fn test_char_basic() {
let f = CharFunc;
let result = f
.invoke(&[
SqliteValue::Integer(72),
SqliteValue::Integer(101),
SqliteValue::Integer(108),
SqliteValue::Integer(108),
SqliteValue::Integer(111),
])
.unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("Hello")));
}
#[test]
fn test_char_null_skipped() {
let f = CharFunc;
let result = f
.invoke(&[
SqliteValue::Integer(65),
SqliteValue::Null,
SqliteValue::Integer(66),
])
.unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("A\0B")));
}
#[test]
fn test_char_invalid_scalar_values_use_replacement_character() {
let f = CharFunc;
let result = f
.invoke(&[
SqliteValue::Integer(-1),
SqliteValue::Integer(65),
SqliteValue::Integer(1_114_112),
])
.unwrap();
assert_eq!(
result,
SqliteValue::Text(SmallText::from_string("\u{fffd}A\u{fffd}"))
);
}
#[test]
fn test_coalesce_first_non_null() {
let f = CoalesceFunc;
let result = f
.invoke(&[
SqliteValue::Null,
SqliteValue::Null,
SqliteValue::Integer(3),
SqliteValue::Integer(4),
])
.unwrap();
assert_eq!(result, SqliteValue::Integer(3));
}
#[test]
fn test_concat_null_as_empty() {
let f = ConcatFunc;
let result = f
.invoke(&[
SqliteValue::Null,
SqliteValue::Text(SmallText::from_string("hello")),
SqliteValue::Null,
])
.unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("hello")));
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_concat_text_args() {
use std::hint::black_box;
use std::time::Instant;
const TEXT_ARGS: usize = 24;
const INVOCATIONS: usize = 50_000;
const REPEATS: usize = 5;
let f = ConcatFunc;
let mut args = Vec::with_capacity(TEXT_ARGS);
for _ in 0..TEXT_ARGS {
args.push(SqliteValue::Text(SmallText::from_string("payload")));
}
let mut best_ns = u128::MAX;
let mut result_len = 0usize;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(args.as_slice()))
.expect("concat benchmark invocation must succeed"),
);
result_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
best_ns = best_ns.min(started.elapsed().as_nanos());
}
println!(
"concat_text_args text_args={TEXT_ARGS} invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
);
}
#[test]
fn test_concat_ws_null_skipped() {
let f = ConcatWsFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string(",")),
SqliteValue::Text(SmallText::from_string("a")),
SqliteValue::Null,
SqliteValue::Text(SmallText::from_string("b")),
])
.unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("a,b")));
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_concat_ws_text_args() {
use std::hint::black_box;
use std::time::Instant;
const TEXT_ARGS: usize = 24;
const INVOCATIONS: usize = 50_000;
const REPEATS: usize = 5;
let f = ConcatWsFunc;
let mut args = Vec::with_capacity(TEXT_ARGS + 1);
args.push(SqliteValue::Text(SmallText::from_string(",")));
for _ in 0..TEXT_ARGS {
args.push(SqliteValue::Text(SmallText::from_string("payload")));
}
let mut best_ns = u128::MAX;
let mut result_len = 0usize;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(args.as_slice()))
.expect("concat_ws benchmark invocation must succeed"),
);
result_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
best_ns = best_ns.min(started.elapsed().as_nanos());
}
println!(
"concat_ws_text_args text_args={TEXT_ARGS} invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
);
}
#[test]
fn test_hex_blob() {
let result = invoke1(
&HexFunc,
SqliteValue::Blob(Arc::from([0xDE, 0xAD, 0xBE, 0xEF].as_slice())),
)
.unwrap();
assert_eq!(
result,
SqliteValue::Text(SmallText::from_string("DEADBEEF"))
);
}
#[test]
fn test_hex_number_via_text() {
let result = invoke1(&HexFunc, SqliteValue::Integer(42)).unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("3432")));
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_hex_text_blob_args() {
use std::hint::black_box;
use std::time::Instant;
const BYTES: usize = 24;
const INVOCATIONS: usize = 100_000;
const REPEATS: usize = 5;
let f = HexFunc;
let text_args = [SqliteValue::Text(SmallText::from_string(
"payload payload sentinel",
))];
let blob_args = [SqliteValue::Blob(Arc::from([0xAB; BYTES].as_slice()))];
let mut text_best_ns = u128::MAX;
let mut blob_best_ns = u128::MAX;
let mut text_result_len = 0usize;
let mut blob_result_len = 0usize;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(text_args.as_slice()))
.expect("hex text benchmark invocation must succeed"),
);
text_result_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(blob_args.as_slice()))
.expect("hex blob benchmark invocation must succeed"),
);
blob_result_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
blob_best_ns = blob_best_ns.min(started.elapsed().as_nanos());
}
println!(
"hex_text_blob_args bytes={BYTES} invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} blob_best_ns={blob_best_ns} text_result_len={text_result_len} blob_result_len={blob_result_len}"
);
}
#[test]
fn test_iif_true() {
let f = IifFunc;
let result = f
.invoke(&[
SqliteValue::Integer(1),
SqliteValue::Text(SmallText::from_string("yes")),
SqliteValue::Text(SmallText::from_string("no")),
])
.unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("yes")));
}
#[test]
fn test_iif_false() {
let f = IifFunc;
let result = f
.invoke(&[
SqliteValue::Integer(0),
SqliteValue::Text(SmallText::from_string("yes")),
SqliteValue::Text(SmallText::from_string("no")),
])
.unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("no")));
}
#[test]
fn test_iif_whitespace_padded_text_truthy() {
let f = IifFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string(" 5 ")),
SqliteValue::Text(SmallText::from_string("yes")),
SqliteValue::Text(SmallText::from_string("no")),
])
.unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("yes")));
}
#[test]
fn test_ifnull_non_null() {
assert_eq!(
invoke2(
&IfnullFunc,
SqliteValue::Integer(5),
SqliteValue::Integer(10)
)
.unwrap(),
SqliteValue::Integer(5)
);
}
#[test]
fn test_ifnull_null() {
assert_eq!(
invoke2(&IfnullFunc, SqliteValue::Null, SqliteValue::Integer(10)).unwrap(),
SqliteValue::Integer(10)
);
}
#[test]
fn test_instr_found() {
assert_eq!(
invoke2(
&InstrFunc,
SqliteValue::Text(SmallText::from_string("hello world")),
SqliteValue::Text(SmallText::from_string("world"))
)
.unwrap(),
SqliteValue::Integer(7)
);
}
#[test]
fn test_instr_not_found() {
assert_eq!(
invoke2(
&InstrFunc,
SqliteValue::Text(SmallText::from_string("hello")),
SqliteValue::Text(SmallText::from_string("xyz"))
)
.unwrap(),
SqliteValue::Integer(0)
);
}
#[test]
fn test_instr_empty_needle_returns_one() {
assert_eq!(
invoke2(
&InstrFunc,
SqliteValue::Text(SmallText::from_string("hello")),
SqliteValue::Text(SmallText::new(""))
)
.unwrap(),
SqliteValue::Integer(1)
);
}
#[test]
fn test_instr_empty_haystack_returns_zero() {
assert_eq!(
invoke2(
&InstrFunc,
SqliteValue::Text(SmallText::new("")),
SqliteValue::Text(SmallText::from_string("x"))
)
.unwrap(),
SqliteValue::Integer(0)
);
}
#[test]
fn test_instr_blob_empty_needle_returns_one() {
assert_eq!(
invoke2(
&InstrFunc,
SqliteValue::Blob(Arc::from([1, 2, 3].as_slice())),
SqliteValue::Blob(Arc::from([].as_slice()))
)
.unwrap(),
SqliteValue::Integer(1)
);
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_instr_text_args() {
use std::hint::black_box;
use std::time::Instant;
const INVOCATIONS: usize = 100_000;
const REPEATS: usize = 5;
let f = InstrFunc;
let args = [
SqliteValue::Text(SmallText::from_string("payload payload sentinel")),
SqliteValue::Text(SmallText::from_string("sentinel")),
];
let mut best_ns = u128::MAX;
let mut result_value = 0i64;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(args.as_slice()))
.expect("instr benchmark invocation must succeed"),
);
result_value = match result {
SqliteValue::Integer(value) => value,
SqliteValue::Null
| SqliteValue::Float(_)
| SqliteValue::Text(_)
| SqliteValue::Blob(_) => 0,
};
}
best_ns = best_ns.min(started.elapsed().as_nanos());
}
println!(
"instr_text_args invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_value={result_value}"
);
}
#[test]
fn test_length_text_chars() {
assert_eq!(
invoke1(
&LengthFunc,
SqliteValue::Text(SmallText::from_string("café"))
)
.unwrap(),
SqliteValue::Integer(4)
);
}
#[test]
fn test_length_text_stops_at_nul() {
assert_eq!(
invoke1(
&LengthFunc,
SqliteValue::Text(SmallText::from_string("A\0B"))
)
.unwrap(),
SqliteValue::Integer(1)
);
assert_eq!(
invoke1(
&LengthFunc,
SqliteValue::Text(SmallText::from_string("\0A"))
)
.unwrap(),
SqliteValue::Integer(0)
);
}
#[test]
fn test_length_blob_bytes() {
assert_eq!(
invoke1(&LengthFunc, SqliteValue::Blob(Arc::from([1, 2].as_slice()))).unwrap(),
SqliteValue::Integer(2)
);
}
#[test]
fn test_octet_length_multibyte() {
assert_eq!(
invoke1(
&OctetLengthFunc,
SqliteValue::Text(SmallText::from_string("café"))
)
.unwrap(),
SqliteValue::Integer(5)
);
}
#[test]
fn test_lower_ascii() {
assert_eq!(
invoke1(
&LowerFunc,
SqliteValue::Text(SmallText::from_string("HELLO"))
)
.unwrap(),
SqliteValue::Text(SmallText::from_string("hello"))
);
}
#[test]
fn test_upper_ascii() {
assert_eq!(
invoke1(
&UpperFunc,
SqliteValue::Text(SmallText::from_string("hello"))
)
.unwrap(),
SqliteValue::Text(SmallText::from_string("HELLO"))
);
}
#[test]
fn test_trim_default() {
let f = TrimFunc;
assert_eq!(
f.invoke(&[SqliteValue::Text(SmallText::from_string(" hello "))])
.unwrap(),
SqliteValue::Text(SmallText::from_string("hello"))
);
}
#[test]
fn test_ltrim_default() {
let f = LtrimFunc;
assert_eq!(
f.invoke(&[SqliteValue::Text(SmallText::from_string(" hello"))])
.unwrap(),
SqliteValue::Text(SmallText::from_string("hello"))
);
}
#[test]
fn test_ltrim_custom() {
let f = LtrimFunc;
assert_eq!(
f.invoke(&[
SqliteValue::Text(SmallText::from_string("xxhello")),
SqliteValue::Text(SmallText::from_string("x")),
])
.unwrap(),
SqliteValue::Text(SmallText::from_string("hello"))
);
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_trim_text_args() {
use std::hint::black_box;
use std::time::Instant;
const INVOCATIONS: usize = 100_000;
const REPEATS: usize = 5;
let trim = TrimFunc;
let ltrim = LtrimFunc;
let rtrim = RtrimFunc;
let default_args = [SqliteValue::Text(SmallText::from_string(" payload "))];
let custom_args = [
SqliteValue::Text(SmallText::from_string("xxxpayloadxxx")),
SqliteValue::Text(SmallText::from_string("x")),
];
let mut trim_best_ns = u128::MAX;
let mut ltrim_best_ns = u128::MAX;
let mut rtrim_best_ns = u128::MAX;
let mut custom_best_ns = u128::MAX;
let mut result_len = 0usize;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
trim.invoke(black_box(default_args.as_slice()))
.expect("trim benchmark invocation must succeed"),
);
result_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
trim_best_ns = trim_best_ns.min(started.elapsed().as_nanos());
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
ltrim
.invoke(black_box(default_args.as_slice()))
.expect("ltrim benchmark invocation must succeed"),
);
result_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
ltrim_best_ns = ltrim_best_ns.min(started.elapsed().as_nanos());
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
rtrim
.invoke(black_box(default_args.as_slice()))
.expect("rtrim benchmark invocation must succeed"),
);
result_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
rtrim_best_ns = rtrim_best_ns.min(started.elapsed().as_nanos());
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
trim.invoke(black_box(custom_args.as_slice()))
.expect("custom trim benchmark invocation must succeed"),
);
result_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
custom_best_ns = custom_best_ns.min(started.elapsed().as_nanos());
}
println!(
"trim_text_args invocations={INVOCATIONS} repeats={REPEATS} trim_best_ns={trim_best_ns} ltrim_best_ns={ltrim_best_ns} rtrim_best_ns={rtrim_best_ns} custom_best_ns={custom_best_ns} result_len={result_len}"
);
}
#[test]
fn test_nullif_equal() {
assert_eq!(
invoke2(
&NullifFunc,
SqliteValue::Integer(5),
SqliteValue::Integer(5)
)
.unwrap(),
SqliteValue::Null
);
}
#[test]
fn test_nullif_different() {
assert_eq!(
invoke2(
&NullifFunc,
SqliteValue::Integer(5),
SqliteValue::Integer(3)
)
.unwrap(),
SqliteValue::Integer(5)
);
}
#[test]
fn test_typeof_each() {
assert_eq!(
invoke1(&TypeofFunc, SqliteValue::Null).unwrap(),
SqliteValue::Text(SmallText::from_string("null"))
);
assert_eq!(
invoke1(&TypeofFunc, SqliteValue::Integer(1)).unwrap(),
SqliteValue::Text(SmallText::from_string("integer"))
);
assert_eq!(
invoke1(&TypeofFunc, SqliteValue::Float(1.0)).unwrap(),
SqliteValue::Text(SmallText::from_string("real"))
);
assert_eq!(
invoke1(&TypeofFunc, SqliteValue::Text(SmallText::from_string("x"))).unwrap(),
SqliteValue::Text(SmallText::from_string("text"))
);
assert_eq!(
invoke1(&TypeofFunc, SqliteValue::Blob(Arc::from([0].as_slice()))).unwrap(),
SqliteValue::Text(SmallText::from_string("blob"))
);
}
#[test]
fn test_subtype_null_returns_zero() {
assert_eq!(
invoke1(&SubtypeFunc, SqliteValue::Null).unwrap(),
SqliteValue::Integer(0)
);
}
#[test]
fn test_replace_basic() {
let f = ReplaceFunc;
assert_eq!(
f.invoke(&[
SqliteValue::Text(SmallText::from_string("hello world")),
SqliteValue::Text(SmallText::from_string("world")),
SqliteValue::Text(SmallText::from_string("earth")),
])
.unwrap(),
SqliteValue::Text(SmallText::from_string("hello earth"))
);
}
#[test]
fn test_replace_empty_y() {
let f = ReplaceFunc;
assert_eq!(
f.invoke(&[
SqliteValue::Text(SmallText::from_string("hello")),
SqliteValue::Text(SmallText::new("")),
SqliteValue::Text(SmallText::from_string("x")),
])
.unwrap(),
SqliteValue::Text(SmallText::from_string("hello"))
);
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_replace_text_args() {
use std::hint::black_box;
use std::time::Instant;
const INVOCATIONS: usize = 100_000;
const REPEATS: usize = 5;
let f = ReplaceFunc;
let args = [
SqliteValue::Text(SmallText::from_string("payload payload payload")),
SqliteValue::Text(SmallText::from_string("zz")),
SqliteValue::Text(SmallText::from_string("replacement")),
];
let mut best_ns = u128::MAX;
let mut result_len = 0usize;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(args.as_slice()))
.expect("replace benchmark invocation must succeed"),
);
result_len = match result {
SqliteValue::Text(text) => text.len(),
SqliteValue::Null
| SqliteValue::Integer(_)
| SqliteValue::Float(_)
| SqliteValue::Blob(_) => 0,
};
}
best_ns = best_ns.min(started.elapsed().as_nanos());
}
println!(
"replace_text_args invocations={INVOCATIONS} repeats={REPEATS} best_ns={best_ns} result_len={result_len}"
);
}
#[test]
#[allow(clippy::float_cmp)]
fn test_round_half_away() {
assert_eq!(
RoundFunc.invoke(&[SqliteValue::Float(2.5)]).unwrap(),
SqliteValue::Float(3.0)
);
assert_eq!(
RoundFunc.invoke(&[SqliteValue::Float(-2.5)]).unwrap(),
SqliteValue::Float(-3.0)
);
}
#[test]
#[allow(clippy::float_cmp, clippy::approx_constant)]
fn test_round_precision() {
assert_eq!(
RoundFunc
.invoke(&[SqliteValue::Float(3.14159), SqliteValue::Integer(2)])
.unwrap(),
SqliteValue::Float(3.14)
);
}
#[test]
#[allow(clippy::float_cmp)]
fn test_round_extreme_n_clamped() {
assert_eq!(
RoundFunc
.invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(400)])
.unwrap(),
RoundFunc
.invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(30)])
.unwrap(),
);
assert_eq!(
RoundFunc
.invoke(&[SqliteValue::Float(2.5), SqliteValue::Integer(-5)])
.unwrap(),
SqliteValue::Float(3.0)
);
let result = RoundFunc
.invoke(&[SqliteValue::Float(1.5), SqliteValue::Integer(i64::MAX)])
.unwrap();
if let SqliteValue::Float(v) = result {
assert!(!v.is_nan(), "round must never return NaN");
}
}
#[test]
#[allow(clippy::float_cmp)]
fn test_round_large_value_no_fractional() {
let big = 9_007_199_254_740_993.0_f64;
assert_eq!(
RoundFunc.invoke(&[SqliteValue::Float(big)]).unwrap(),
SqliteValue::Float(big)
);
assert_eq!(
RoundFunc.invoke(&[SqliteValue::Float(-big)]).unwrap(),
SqliteValue::Float(-big)
);
}
#[test]
fn test_sign_positive() {
assert_eq!(
invoke1(&SignFunc, SqliteValue::Integer(42)).unwrap(),
SqliteValue::Integer(1)
);
}
#[test]
fn test_sign_negative() {
assert_eq!(
invoke1(&SignFunc, SqliteValue::Integer(-42)).unwrap(),
SqliteValue::Integer(-1)
);
}
#[test]
fn test_sign_zero() {
assert_eq!(
invoke1(&SignFunc, SqliteValue::Integer(0)).unwrap(),
SqliteValue::Integer(0)
);
}
#[test]
fn test_sign_null() {
assert_eq!(
invoke1(&SignFunc, SqliteValue::Null).unwrap(),
SqliteValue::Null
);
}
#[test]
fn test_sign_non_numeric() {
assert_eq!(
invoke1(&SignFunc, SqliteValue::Text(SmallText::from_string("abc"))).unwrap(),
SqliteValue::Null
);
}
#[test]
fn test_sign_whitespace_padded_text() {
assert_eq!(
invoke1(
&SignFunc,
SqliteValue::Text(SmallText::from_string(" 5 "))
)
.unwrap(),
SqliteValue::Integer(1)
);
assert_eq!(
invoke1(
&SignFunc,
SqliteValue::Text(SmallText::from_string(" -3.14 "))
)
.unwrap(),
SqliteValue::Integer(-1)
);
}
#[test]
fn test_sign_unicode_space_and_blob_return_null() {
assert_eq!(
invoke1(
&SignFunc,
SqliteValue::Text(SmallText::from_string("\u{00a0}123"))
)
.unwrap(),
SqliteValue::Null
);
assert_eq!(
invoke1(&SignFunc, SqliteValue::Blob(Arc::from(b"123".as_slice()))).unwrap(),
SqliteValue::Null
);
}
#[test]
fn test_sign_nan_inf_text_returns_null() {
for s in &[
"NaN",
"nan",
"inf",
"-inf",
"Infinity",
"-Infinity",
"INF",
"+nan",
"+inf",
] {
assert_eq!(
invoke1(&SignFunc, SqliteValue::Text(SmallText::from_string(*s))).unwrap(),
SqliteValue::Null,
"sign('{s}') should be NULL"
);
}
}
#[test]
fn test_sign_numeric_overflow_to_infinity() {
assert_eq!(
invoke1(
&SignFunc,
SqliteValue::Text(SmallText::from_string("1e999"))
)
.unwrap(),
SqliteValue::Integer(1)
);
assert_eq!(
invoke1(
&SignFunc,
SqliteValue::Text(SmallText::from_string("-1e999"))
)
.unwrap(),
SqliteValue::Integer(-1)
);
assert_eq!(
invoke1(
&SignFunc,
SqliteValue::Text(SmallText::from_string("1e-999"))
)
.unwrap(),
SqliteValue::Integer(0)
);
}
#[test]
fn test_sign_float_nan_returns_null() {
assert_eq!(
invoke1(&SignFunc, SqliteValue::Float(f64::NAN)).unwrap(),
SqliteValue::Null
);
}
#[test]
fn test_scalar_max_null() {
let f = ScalarMaxFunc;
let result = f
.invoke(&[
SqliteValue::Integer(1),
SqliteValue::Null,
SqliteValue::Integer(3),
])
.unwrap();
assert_eq!(result, SqliteValue::Null);
}
#[test]
fn test_scalar_max_values() {
let f = ScalarMaxFunc;
let result = f
.invoke(&[
SqliteValue::Integer(3),
SqliteValue::Integer(1),
SqliteValue::Integer(2),
])
.unwrap();
assert_eq!(result, SqliteValue::Integer(3));
}
#[test]
fn test_scalar_min_null() {
let f = ScalarMinFunc;
let result = f
.invoke(&[
SqliteValue::Integer(1),
SqliteValue::Null,
SqliteValue::Integer(3),
])
.unwrap();
assert_eq!(result, SqliteValue::Null);
}
#[test]
fn test_quote_text() {
assert_eq!(
invoke1(
&QuoteFunc,
SqliteValue::Text(SmallText::from_string("it's"))
)
.unwrap(),
SqliteValue::Text(SmallText::from_string("'it''s'"))
);
}
#[test]
fn test_quote_null() {
assert_eq!(
invoke1(&QuoteFunc, SqliteValue::Null).unwrap(),
SqliteValue::Text(SmallText::from_string("NULL"))
);
}
#[test]
fn test_quote_blob() {
assert_eq!(
invoke1(&QuoteFunc, SqliteValue::Blob(Arc::from([0xAB].as_slice()))).unwrap(),
SqliteValue::Text(SmallText::from_string("X'AB'"))
);
}
#[test]
fn test_quote_text_truncates_at_first_nul() {
assert_eq!(
invoke1(
&QuoteFunc,
SqliteValue::Text(SmallText::from_string("A\0B"))
)
.unwrap(),
SqliteValue::Text(SmallText::from_string("'A'"))
);
}
#[test]
fn test_unistr_quote_plain_text_matches_quote() {
assert_eq!(
invoke1(
&UnistrQuoteFunc,
SqliteValue::Text(SmallText::from_string("it's"))
)
.unwrap(),
SqliteValue::Text(SmallText::from_string("'it''s'"))
);
}
#[test]
fn test_unistr_quote_escapes_control_chars_and_backslashes() {
assert_eq!(
invoke1(
&UnistrQuoteFunc,
SqliteValue::Text(SmallText::from_string("a\nb\\c\x01d"))
)
.unwrap(),
SqliteValue::Text(SmallText::from_string("unistr('a\\u000ab\\\\c\\u0001d')"))
);
}
#[test]
fn test_unistr_quote_truncates_at_first_nul_before_wrapping() {
assert_eq!(
invoke1(
&UnistrQuoteFunc,
SqliteValue::Text(SmallText::from_string("A\0\nB"))
)
.unwrap(),
SqliteValue::Text(SmallText::from_string("'A'"))
);
}
#[test]
fn test_unistr_decodes_backslash_and_unicode_escapes() {
assert_eq!(
invoke1(
&UnistrFunc,
SqliteValue::Text(SmallText::from_string(
"a\\\\b\\u0020\\U0001f600\\0041\\+000042"
))
)
.unwrap(),
SqliteValue::Text(SmallText::from_string("a\\b \u{1f600}AB"))
);
}
#[test]
fn test_unistr_invalid_escape_returns_error() {
for input in [
"\\u12xz",
"\\12xz",
"\\+00xz",
"\\",
"\\x",
"\\U00110000",
"\\D800",
] {
let err = invoke1(
&UnistrFunc,
SqliteValue::Text(SmallText::from_string(input)),
)
.unwrap_err();
assert_eq!(err.to_string(), INVALID_UNISTR_ESCAPE);
}
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_unistr_text_args() {
use std::hint::black_box;
use std::time::Instant;
const INVOCATIONS: usize = 500_000;
const REPEATS: usize = 7;
let f = UnistrFunc;
let plain_args = [SqliteValue::Text(SmallText::from_string(
"plain unicode payload",
))];
let escaped_args = [SqliteValue::Text(SmallText::from_string(
"a\\\\b\\u0020\\u0048\\u0069\\U0001f600",
))];
let mut plain_best_ns = u128::MAX;
let mut escaped_best_ns = u128::MAX;
let mut checksum = 0usize;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(plain_args.as_slice()))
.expect("unistr plain benchmark invocation must succeed"),
);
if let SqliteValue::Text(text) = result {
checksum = checksum.wrapping_add(text.len());
}
}
plain_best_ns = plain_best_ns.min(started.elapsed().as_nanos());
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(escaped_args.as_slice()))
.expect("unistr escaped benchmark invocation must succeed"),
);
if let SqliteValue::Text(text) = result {
checksum = checksum.wrapping_add(text.len());
}
}
escaped_best_ns = escaped_best_ns.min(started.elapsed().as_nanos());
}
println!(
"unistr_text_args invocations={INVOCATIONS} repeats={REPEATS} plain_best_ns={plain_best_ns} escaped_best_ns={escaped_best_ns} checksum={checksum}"
);
}
#[test]
fn test_random_range() {
let f = RandomFunc;
let result = f.invoke(&[]).unwrap();
assert!(matches!(result, SqliteValue::Integer(_)));
}
#[test]
fn test_randomblob_length() {
let result = invoke1(&RandomblobFunc, SqliteValue::Integer(16)).unwrap();
match result {
SqliteValue::Blob(b) => assert_eq!(b.len(), 16),
other => unreachable!("expected blob, got {other:?}"),
}
}
#[test]
fn test_randomblob_null_zero_and_negative_lengths_are_one_byte() {
for arg in [
SqliteValue::Null,
SqliteValue::Integer(0),
SqliteValue::Integer(-5),
] {
let result = invoke1(&RandomblobFunc, arg).unwrap();
match result {
SqliteValue::Blob(b) => assert_eq!(b.len(), 1),
other => unreachable!("expected one-byte blob, got {other:?}"),
}
}
}
#[test]
fn test_zeroblob_length() {
let result = invoke1(&ZeroblobFunc, SqliteValue::Integer(100)).unwrap();
match result {
SqliteValue::Blob(b) => {
assert_eq!(b.len(), 100);
assert!(b.iter().all(|&x| x == 0));
}
other => unreachable!("expected blob, got {other:?}"),
}
}
#[test]
fn test_unhex_valid() {
let result = invoke1(
&UnhexFunc,
SqliteValue::Text(SmallText::from_string("48656C6C6F")),
)
.unwrap();
assert_eq!(result, SqliteValue::Blob(Arc::from(b"Hello".as_slice())));
}
#[test]
fn test_unhex_invalid() {
let result = invoke1(
&UnhexFunc,
SqliteValue::Text(SmallText::from_string("ZZZZ")),
)
.unwrap();
assert_eq!(result, SqliteValue::Null);
}
#[test]
fn test_unhex_ignore_chars() {
let f = UnhexFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string("48-65-6C")),
SqliteValue::Text(SmallText::from_string("-")),
])
.unwrap();
assert_eq!(result, SqliteValue::Blob(Arc::from(b"Hel".as_slice())));
}
#[test]
fn test_unhex_ignore_chars_only_between_byte_pairs() {
let f = UnhexFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string("AB CD")),
SqliteValue::Text(SmallText::from_string(" ")),
])
.unwrap();
assert_eq!(result, SqliteValue::Blob(Arc::from([0xAB, 0xCD])));
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string("A BCD")),
SqliteValue::Text(SmallText::from_string(" ")),
])
.unwrap();
assert_eq!(result, SqliteValue::Null);
}
#[test]
fn test_unhex_null_ignore_argument_returns_null() {
let f = UnhexFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string("41")),
SqliteValue::Null,
])
.unwrap();
assert_eq!(result, SqliteValue::Null);
}
#[test]
fn test_unhex_hex_digits_in_ignore_argument_do_not_ignore_digits() {
let f = UnhexFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string("41")),
SqliteValue::Text(SmallText::from_string("4")),
])
.unwrap();
assert_eq!(result, SqliteValue::Blob(Arc::from(b"A".as_slice())));
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_unhex_text_args() {
use std::hint::black_box;
use std::time::Instant;
const INVOCATIONS: usize = 300_000;
const REPEATS: usize = 7;
let f = UnhexFunc;
let plain_args = [SqliteValue::Text(SmallText::from_string(
"48656C6C6F776F726C64",
))];
let ignore_args = [
SqliteValue::Text(SmallText::from_string("48-65-6C-6C-6F")),
SqliteValue::Text(SmallText::from_string("-")),
];
let mut plain_best_ns = u128::MAX;
let mut ignore_best_ns = u128::MAX;
let mut checksum = 0usize;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(plain_args.as_slice()))
.expect("unhex benchmark invocation must succeed"),
);
if let SqliteValue::Blob(blob) = result {
checksum = checksum.wrapping_add(blob.len());
}
}
plain_best_ns = plain_best_ns.min(started.elapsed().as_nanos());
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(ignore_args.as_slice()))
.expect("unhex ignore benchmark invocation must succeed"),
);
if let SqliteValue::Blob(blob) = result {
checksum = checksum.wrapping_add(blob.len());
}
}
ignore_best_ns = ignore_best_ns.min(started.elapsed().as_nanos());
}
println!(
"unhex_text_args invocations={INVOCATIONS} repeats={REPEATS} plain_best_ns={plain_best_ns} ignore_best_ns={ignore_best_ns} checksum={checksum}"
);
}
#[test]
fn test_unicode_first_char() {
assert_eq!(
invoke1(&UnicodeFunc, SqliteValue::Text(SmallText::from_string("A"))).unwrap(),
SqliteValue::Integer(65)
);
}
#[test]
fn test_unicode_text_stops_at_nul() {
assert_eq!(
invoke1(
&UnicodeFunc,
SqliteValue::Text(SmallText::from_string("\0A"))
)
.unwrap(),
SqliteValue::Null
);
assert_eq!(
invoke1(
&UnicodeFunc,
SqliteValue::Text(SmallText::from_string("A\0"))
)
.unwrap(),
SqliteValue::Integer(65)
);
}
#[test]
fn test_unicode_blob_uses_sqlite_utf8_reader() {
let cases: &[(&[u8], SqliteValue)] = &[
(&[0x00, 0x41], SqliteValue::Null),
(&[0x80], SqliteValue::Integer(128)),
(&[0xC2, 0x80], SqliteValue::Integer(128)),
(&[0xC2, 0x80, 0x80], SqliteValue::Integer(8192)),
(&[0xED, 0xA0, 0x80], SqliteValue::Integer(65_533)),
(&[0xF4, 0x90, 0x80, 0x80], SqliteValue::Integer(1_114_112)),
];
for (bytes, expected) in cases {
assert_eq!(
invoke1(&UnicodeFunc, SqliteValue::Blob(Arc::from(*bytes))).unwrap(),
expected.clone()
);
}
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_unicode_text_arg() {
use std::hint::black_box;
use std::time::Instant;
const INVOCATIONS: usize = 1_000_000;
const REPEATS: usize = 7;
let f = UnicodeFunc;
let args = [SqliteValue::Text(SmallText::from_string("Alphabet soup"))];
let mut text_best_ns = u128::MAX;
let mut checksum = 0i64;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(args.as_slice()))
.expect("unicode benchmark invocation must succeed"),
);
if let SqliteValue::Integer(codepoint) = result {
checksum = checksum.wrapping_add(codepoint);
}
}
text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
}
println!(
"unicode_text_arg invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} checksum={checksum}"
);
}
#[test]
fn test_soundex_basic() {
assert_eq!(
invoke1(
&SoundexFunc,
SqliteValue::Text(SmallText::from_string("Robert"))
)
.unwrap(),
SqliteValue::Text(SmallText::from_string("R163"))
);
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_soundex_text_arg() {
use std::hint::black_box;
use std::time::Instant;
const INVOCATIONS: usize = 1_000_000;
const REPEATS: usize = 7;
let f = SoundexFunc;
let args = [SqliteValue::Text(SmallText::from_string("Robert"))];
let mut text_best_ns = u128::MAX;
let mut checksum = 0usize;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(args.as_slice()))
.expect("soundex benchmark invocation must succeed"),
);
if let SqliteValue::Text(text) = result {
checksum = checksum.wrapping_add(text.len());
}
}
text_best_ns = text_best_ns.min(started.elapsed().as_nanos());
}
println!(
"soundex_text_arg invocations={INVOCATIONS} repeats={REPEATS} text_best_ns={text_best_ns} checksum={checksum}"
);
}
#[test]
fn test_substr_basic() {
let f = SubstrFunc;
assert_eq!(
f.invoke(&[
SqliteValue::Text(SmallText::from_string("hello")),
SqliteValue::Integer(2),
SqliteValue::Integer(3),
])
.unwrap(),
SqliteValue::Text(SmallText::from_string("ell"))
);
}
#[test]
fn test_substr_start_zero_quirk() {
let f = SubstrFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string("hello")),
SqliteValue::Integer(0),
SqliteValue::Integer(3),
])
.unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("he")));
}
#[test]
fn test_substr_negative_start() {
let f = SubstrFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string("hello")),
SqliteValue::Integer(-2),
])
.unwrap();
assert_eq!(result, SqliteValue::Text(SmallText::from_string("lo")));
}
#[test]
fn test_substr_negative_length() {
let f = SubstrFunc;
let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
let i = SqliteValue::Integer;
assert_eq!(f.invoke(&[t("hello"), i(3), i(-2)]).unwrap(), t("he"));
assert_eq!(f.invoke(&[t("hello"), i(3), i(-5)]).unwrap(), t("he"));
assert_eq!(f.invoke(&[t("hello"), i(1), i(-1)]).unwrap(), t(""));
}
#[test]
fn test_substr_negative_start_negative_length() {
let f = SubstrFunc;
let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
let i = SqliteValue::Integer;
assert_eq!(f.invoke(&[t("hello"), i(-2), i(-2)]).unwrap(), t("el"));
}
#[test]
fn test_substr_edge_cases() {
let f = SubstrFunc;
let t = |s: &str| SqliteValue::Text(SmallText::from_string(s));
let i = SqliteValue::Integer;
assert_eq!(f.invoke(&[t("hello"), i(6), i(2)]).unwrap(), t(""));
assert_eq!(f.invoke(&[t("hello"), i(-10), i(3)]).unwrap(), t(""));
assert_eq!(f.invoke(&[t("hello"), i(-5), i(6)]).unwrap(), t("hello"));
assert_eq!(f.invoke(&[t("hello"), i(0), i(1)]).unwrap(), t(""));
assert_eq!(f.invoke(&[t("hello"), i(0), i(-1)]).unwrap(), t(""));
assert_eq!(f.invoke(&[t(""), i(1), i(1)]).unwrap(), t(""));
}
#[test]
fn test_substr_blob_negative_length() {
let f = SubstrFunc;
let i = SqliteValue::Integer;
let blob = SqliteValue::Blob(Arc::from([1, 2, 3, 4, 5].as_slice()));
assert_eq!(
f.invoke(&[blob, i(-2), i(-2)]).unwrap(),
SqliteValue::Blob(Arc::from([2, 3].as_slice()))
);
}
#[test]
fn test_like_case_insensitive() {
assert_eq!(
invoke2(
&LikeFunc,
SqliteValue::Text(SmallText::from_string("ABC")),
SqliteValue::Text(SmallText::from_string("abc"))
)
.unwrap(),
SqliteValue::Integer(1)
);
}
#[test]
fn test_like_escape() {
let f = LikeFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string("10\\%")),
SqliteValue::Text(SmallText::from_string("10%")),
SqliteValue::Text(SmallText::from_string("\\")),
])
.unwrap();
assert_eq!(result, SqliteValue::Integer(1));
}
#[test]
fn test_like_escape_rejects_empty_string() {
let err = LikeFunc
.invoke(&[
SqliteValue::Text(SmallText::from_string("a")),
SqliteValue::Text(SmallText::from_string("a")),
SqliteValue::Text(SmallText::new("")),
])
.unwrap_err();
assert!(
err.to_string()
.contains("ESCAPE expression must be a single character")
);
}
#[test]
fn test_like_escape_rejects_multi_character_string() {
let err = LikeFunc
.invoke(&[
SqliteValue::Text(SmallText::from_string("a")),
SqliteValue::Text(SmallText::from_string("a")),
SqliteValue::Text(SmallText::from_string("xx")),
])
.unwrap_err();
assert!(
err.to_string()
.contains("ESCAPE expression must be a single character")
);
}
#[test]
fn test_like_percent() {
assert_eq!(
invoke2(
&LikeFunc,
SqliteValue::Text(SmallText::from_string("%ell%")),
SqliteValue::Text(SmallText::from_string("Hello"))
)
.unwrap(),
SqliteValue::Integer(1)
);
}
#[test]
fn test_glob_star() {
assert_eq!(
invoke2(
&GlobFunc,
SqliteValue::Text(SmallText::from_string("*.txt")),
SqliteValue::Text(SmallText::from_string("file.txt"))
)
.unwrap(),
SqliteValue::Integer(1)
);
}
#[test]
fn test_glob_case_sensitive() {
assert_eq!(
invoke2(
&GlobFunc,
SqliteValue::Text(SmallText::from_string("ABC")),
SqliteValue::Text(SmallText::from_string("abc"))
)
.unwrap(),
SqliteValue::Integer(0)
);
}
#[test]
fn test_format_specifiers() {
let f = FormatFunc;
let result = f
.invoke(&[
SqliteValue::Text(SmallText::from_string("%d %s")),
SqliteValue::Integer(42),
SqliteValue::Text(SmallText::from_string("hello")),
])
.unwrap();
assert_eq!(
result,
SqliteValue::Text(SmallText::from_string("42 hello"))
);
}
#[test]
fn test_format_n_noop() {
let f = FormatFunc;
let result = f
.invoke(&[SqliteValue::Text(SmallText::from_string("before%nafter"))])
.unwrap();
assert_eq!(
result,
SqliteValue::Text(SmallText::from_string("beforeafter"))
);
}
#[test]
fn test_sqlite_version_format() {
let result = SqliteVersionFunc.invoke(&[]).unwrap();
match result {
SqliteValue::Text(v) => {
assert_eq!(v.split('.').count(), 3, "version must be N.N.N format");
}
other => unreachable!("expected text, got {other:?}"),
}
}
#[test]
fn test_sqlite_compileoption_used_matches_sqlite_prefix_and_value_options() {
let func = SqliteCompileoptionUsedFunc;
assert_eq!(
invoke1(
&func,
SqliteValue::Text(SmallText::from_string("THREADSAFE"))
)
.unwrap(),
SqliteValue::Integer(1)
);
let expected_icu_enabled = i64::from(cfg!(feature = "ext-icu"));
assert_eq!(
invoke1(
&func,
SqliteValue::Text(SmallText::from_string("SQLITE_ENABLE_ICU"))
)
.unwrap(),
SqliteValue::Integer(expected_icu_enabled)
);
assert_eq!(
invoke1(
&func,
SqliteValue::Text(SmallText::from_string("sqlite_enable_icu"))
)
.unwrap(),
SqliteValue::Integer(expected_icu_enabled)
);
assert_eq!(
invoke1(
&func,
SqliteValue::Text(SmallText::from_string("OMIT_LOAD_EXTENSION"))
)
.unwrap(),
SqliteValue::Integer(1)
);
assert_eq!(
invoke1(
&func,
SqliteValue::Text(SmallText::from_string("ENABLE_FTS3"))
)
.unwrap(),
SqliteValue::Integer(0)
);
assert_eq!(
invoke1(&func, SqliteValue::Null).unwrap(),
SqliteValue::Null
);
}
#[test]
#[ignore = "perf-only benchmark"]
fn perf_compileoption_used_text_args() {
use std::hint::black_box;
use std::time::Instant;
const INVOCATIONS: usize = 1_000_000;
const REPEATS: usize = 7;
let f = SqliteCompileoptionUsedFunc;
let present_args = [SqliteValue::Text(SmallText::from_string(
"SQLITE_ENABLE_ICU",
))];
let absent_args = [SqliteValue::Text(SmallText::from_string(
"ENABLE_NOT_PRESENT",
))];
let mut present_best_ns = u128::MAX;
let mut absent_best_ns = u128::MAX;
let mut checksum = 0i64;
for _ in 0..REPEATS {
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(present_args.as_slice()))
.expect("compileoption present benchmark invocation must succeed"),
);
if let SqliteValue::Integer(value) = result {
checksum = checksum.wrapping_add(value);
}
}
present_best_ns = present_best_ns.min(started.elapsed().as_nanos());
let started = Instant::now();
for _ in 0..INVOCATIONS {
let result = black_box(
f.invoke(black_box(absent_args.as_slice()))
.expect("compileoption absent benchmark invocation must succeed"),
);
if let SqliteValue::Integer(value) = result {
checksum = checksum.wrapping_add(value);
}
}
absent_best_ns = absent_best_ns.min(started.elapsed().as_nanos());
}
println!(
"compileoption_used_text_args invocations={INVOCATIONS} repeats={REPEATS} present_best_ns={present_best_ns} absent_best_ns={absent_best_ns} checksum={checksum}"
);
}
#[test]
fn test_sqlite_compileoption_get_enumerates_canonical_option_list() {
let func = SqliteCompileoptionGetFunc;
for (index, option) in sqlite_compile_options().iter().enumerate() {
assert_eq!(
invoke1(&func, SqliteValue::Integer(index as i64)).unwrap(),
SqliteValue::Text(SmallText::new(option))
);
}
assert_eq!(
invoke1(&func, SqliteValue::Integer(-1)).unwrap(),
SqliteValue::Null
);
assert_eq!(
invoke1(
&func,
SqliteValue::Integer(sqlite_compile_options().len() as i64)
)
.unwrap(),
SqliteValue::Null
);
}
#[test]
fn test_register_builtins_all_present() {
let mut registry = FunctionRegistry::new();
register_builtins(&mut registry);
assert!(registry.find_scalar("abs", 1).is_some());
assert!(registry.find_scalar("typeof", 1).is_some());
assert!(registry.find_scalar("length", 1).is_some());
assert!(registry.find_scalar("lower", 1).is_some());
assert!(registry.find_scalar("upper", 1).is_some());
assert!(registry.find_scalar("hex", 1).is_some());
assert!(registry.find_scalar("coalesce", 3).is_some());
assert!(registry.find_scalar("concat", 2).is_some());
assert!(registry.find_scalar("like", 2).is_some());
assert!(registry.find_scalar("glob", 2).is_some());
assert!(registry.find_scalar("round", 1).is_some());
assert!(registry.find_scalar("substr", 2).is_some());
assert!(registry.find_scalar("substring", 3).is_some());
assert!(registry.find_scalar("sqlite_version", 0).is_some());
assert!(registry.find_scalar("iif", 3).is_some());
assert!(registry.find_scalar("if", 3).is_some());
assert!(registry.find_scalar("format", 1).is_some());
assert!(registry.find_scalar("printf", 1).is_some());
assert!(registry.find_scalar("max", 2).is_some());
assert!(registry.find_scalar("min", 2).is_some());
assert!(registry.find_scalar("sign", 1).is_some());
assert!(registry.find_scalar("random", 0).is_some());
assert!(registry.find_scalar("concat_ws", 3).is_some());
assert!(registry.find_scalar("octet_length", 1).is_some());
assert!(registry.find_scalar("unhex", 1).is_some());
assert!(registry.find_scalar("timediff", 2).is_some());
assert!(registry.find_scalar("unistr", 1).is_some());
assert!(registry.find_scalar("unistr_quote", 1).is_some());
assert!(registry.find_aggregate("median", 1).is_some());
assert!(registry.find_aggregate("percentile", 2).is_some());
assert!(registry.find_aggregate("percentile_cont", 2).is_some());
assert!(registry.find_aggregate("percentile_disc", 2).is_some());
assert!(registry.find_scalar("load_extension", 1).is_none());
assert!(registry.find_scalar("load_extension", 2).is_none());
}
#[test]
fn test_register_builtins_rejects_invalid_variadic_arities() {
let mut registry = FunctionRegistry::new();
register_builtins(&mut registry);
for (name, too_few, valid, too_many) in [
("coalesce", 1, 2, None),
("concat", 0, 1, None),
("concat_ws", 1, 2, None),
("trim", 0, 1, Some(3)),
("ltrim", 0, 1, Some(3)),
("rtrim", 0, 1, Some(3)),
("round", 0, 1, Some(3)),
("unhex", 0, 1, Some(3)),
("substr", 1, 2, Some(4)),
("substring", 1, 2, Some(4)),
("max", 0, 1, None),
("min", 0, 1, None),
] {
assert_wrong_arg_count(®istry, name, too_few);
assert!(
registry.find_scalar(name, valid).is_some(),
"{name}/{valid} should resolve"
);
if let Some(arity) = too_many {
assert_wrong_arg_count(®istry, name, arity);
}
}
assert!(registry.find_scalar("char", 0).is_some());
assert!(registry.find_scalar("format", 0).is_some());
assert!(registry.find_scalar("printf", 0).is_some());
}
#[test]
fn test_e2e_registry_invoke_through_lookup() {
let mut registry = FunctionRegistry::new();
register_builtins(&mut registry);
let abs = registry.find_scalar("ABS", 1).unwrap();
assert_eq!(
abs.invoke(&[SqliteValue::Integer(-42)]).unwrap(),
SqliteValue::Integer(42)
);
let typeof_fn = registry.find_scalar("typeof", 1).unwrap();
assert_eq!(
typeof_fn
.invoke(&[SqliteValue::Text(SmallText::from_string("hello"))])
.unwrap(),
SqliteValue::Text(SmallText::from_string("text"))
);
let coalesce = registry.find_scalar("COALESCE", 4).unwrap();
assert_eq!(
coalesce
.invoke(&[
SqliteValue::Null,
SqliteValue::Null,
SqliteValue::Integer(42),
SqliteValue::Integer(99),
])
.unwrap(),
SqliteValue::Integer(42)
);
}
#[test]
fn test_nondeterministic_functions_flagged() {
assert!(!RandomFunc.is_deterministic());
assert!(!RandomblobFunc.is_deterministic());
assert!(!ChangesFunc.is_deterministic());
assert!(!TotalChangesFunc.is_deterministic());
assert!(!LastInsertRowidFunc.is_deterministic());
}
#[test]
fn test_deterministic_functions_flagged() {
assert!(AbsFunc.is_deterministic());
assert!(LengthFunc.is_deterministic());
assert!(TypeofFunc.is_deterministic());
assert!(UpperFunc.is_deterministic());
assert!(LowerFunc.is_deterministic());
assert!(HexFunc.is_deterministic());
assert!(CoalesceFunc.is_deterministic());
assert!(IifFunc.is_deterministic());
}
#[test]
fn test_random_produces_different_values() {
let a = RandomFunc.invoke(&[]).unwrap();
let b = RandomFunc.invoke(&[]).unwrap();
assert_ne!(a.as_integer(), b.as_integer());
}
#[test]
fn test_registry_nondeterministic_lookup() {
let mut registry = FunctionRegistry::default();
register_builtins(&mut registry);
let random = registry.find_scalar("random", 0).unwrap();
assert!(!random.is_deterministic());
let changes = registry.find_scalar("changes", 0).unwrap();
assert!(!changes.is_deterministic());
let lir = registry.find_scalar("last_insert_rowid", 0).unwrap();
assert!(!lir.is_deterministic());
let abs = registry.find_scalar("abs", 1).unwrap();
assert!(abs.is_deterministic());
}
}