use crate::error::{Error, Result};
use crate::sql::ast::{BinaryOp, Expr, Literal, Select, UnaryOp};
use crate::sql::token::Param;
use crate::value::Value;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cmp::Ordering;
pub trait Subqueries {
fn scalar(&self, select: &Select, outer: &EvalCtx) -> Result<Value>;
fn column(&self, select: &Select, outer: &EvalCtx) -> Result<Vec<Value>>;
fn column_affinity(&self, select: &Select) -> Option<Affinity>;
fn row_column_affinities(&self, select: &Select) -> alloc::vec::Vec<Option<Affinity>>;
fn rows(&self, select: &Select, outer: &EvalCtx) -> Result<Vec<Vec<Value>>>;
fn exists(&self, select: &Select, outer: &EvalCtx) -> Result<bool>;
fn resolve_outer(&self, table: Option<&str>, name: &str) -> Option<Value>;
fn last_insert_rowid(&self) -> i64 {
0
}
fn changes(&self) -> i64 {
0
}
fn case_sensitive_like(&self) -> bool {
false
}
fn total_changes(&self) -> i64 {
0
}
fn next_random(&self) -> i64 {
0
}
fn call_udf(&self, _name: &str, _args: &[Value]) -> Option<Result<Value>> {
None
}
fn fts5_bm25(&self, _rowid: i64, _weights: &[f64]) -> Option<f64> {
None
}
fn fts5_highlight(
&self,
_col: usize,
_text: &str,
_open: &str,
_close: &str,
) -> Option<String> {
None
}
#[allow(clippy::too_many_arguments)]
fn fts5_snippet(
&self,
_col: i64,
_cols: &[String],
_open: &str,
_close: &str,
_ellipsis: &str,
_ntokens: usize,
) -> Option<String> {
None
}
fn fts5_indexed_columns(&self, _table: &str) -> Option<Vec<String>> {
None
}
#[cfg(feature = "fts5")]
fn fts5_tok(&self, _table: &str) -> crate::vtab::Fts5Tok {
crate::vtab::Fts5Tok::default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Affinity {
Blob,
Text,
Numeric,
Integer,
Real,
}
impl Affinity {
pub fn from_type(type_name: Option<&str>) -> Affinity {
let Some(t) = type_name else {
return Affinity::Blob; };
let t = t.to_ascii_uppercase();
if t.contains("INT") {
Affinity::Integer
} else if t.contains("CHAR") || t.contains("CLOB") || t.contains("TEXT") {
Affinity::Text
} else if t.contains("BLOB") {
Affinity::Blob
} else if t.contains("REAL") || t.contains("FLOA") || t.contains("DOUB") {
Affinity::Real
} else {
Affinity::Numeric
}
}
pub fn coerce(self, v: Value) -> Value {
match self {
Affinity::Blob => v,
Affinity::Text => match v {
Value::Integer(_) | Value::Real(_) => Value::Text(to_text(&v)),
other => other,
},
Affinity::Real => match v {
Value::Null | Value::Blob(_) => v,
_ => match to_number_strict(&v) {
Some(n) => Value::Real(number_as_f64(&n)),
None => v,
},
},
Affinity::Integer | Affinity::Numeric => match v {
Value::Null | Value::Blob(_) => v,
Value::Real(r) => integral_real_to_int(r).unwrap_or(Value::Real(r)),
Value::Integer(_) => v,
Value::Text(_) => match to_number_strict(&v) {
Some(Value::Real(r)) => integral_real_to_int(r).unwrap_or(Value::Real(r)),
Some(n) => n,
None => v,
},
},
}
}
}
fn integral_real_to_int(r: f64) -> Option<Value> {
if r.is_finite()
&& r == crate::util::float::trunc(r)
&& r >= i64::MIN as f64
&& r < 9_223_372_036_854_775_808.0
{
Some(Value::Integer(r as i64))
} else {
None
}
}
fn to_number_strict(v: &Value) -> Option<Value> {
match v {
Value::Integer(_) | Value::Real(_) => Some(v.clone()),
Value::Text(s) => {
let t = s.trim();
if let Ok(i) = t.parse::<i64>() {
Some(Value::Integer(i))
} else {
parse_decimal_f64(t).map(Value::Real)
}
}
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct ColumnInfo {
pub name: String,
pub table: String,
pub affinity: Affinity,
pub collation: crate::value::Collation,
pub hidden: bool,
}
#[derive(Debug, Default, Clone)]
pub struct Params {
pub positional: Vec<Value>,
pub named: Vec<(String, Value)>,
}
impl Params {
fn get(&self, p: &Param, anon_index: usize) -> Result<Value> {
match p {
Param::Anonymous => self
.positional
.get(anon_index)
.cloned()
.ok_or_else(|| Error::Error(format_args_unbound(anon_index + 1))),
Param::Numbered(n) => self
.positional
.get((*n as usize).wrapping_sub(1))
.cloned()
.ok_or_else(|| Error::Error(format_args_unbound(*n as usize))),
Param::Named(name) => self
.named
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.clone())
.ok_or_else(|| Error::Error(alloc::format!("unbound parameter {name}"))),
}
}
}
fn format_args_unbound(n: usize) -> String {
alloc::format!("unbound parameter ?{n}")
}
pub struct EvalCtx<'a> {
pub row: &'a [Value],
pub columns: &'a [ColumnInfo],
pub rowid: Option<i64>,
pub params: &'a Params,
pub anon_counter: core::cell::Cell<usize>,
pub subqueries: Option<&'a dyn Subqueries>,
}
impl<'a> EvalCtx<'a> {
pub fn rowless(params: &'a Params) -> EvalCtx<'a> {
EvalCtx {
row: &[],
columns: &[],
rowid: None,
params,
anon_counter: core::cell::Cell::new(0),
subqueries: None,
}
}
pub fn with_subqueries(mut self, s: &'a dyn Subqueries) -> EvalCtx<'a> {
self.subqueries = Some(s);
self
}
fn column_collation(&self, table: Option<&str>, name: &str) -> Option<Collation> {
self.columns.iter().find_map(|col| {
let name_ok = col.name.eq_ignore_ascii_case(name);
let table_ok = table.is_none_or(|t| col.table.eq_ignore_ascii_case(t));
(name_ok && table_ok).then_some(col.collation)
})
}
fn resolve_column(
&self,
schema: Option<&str>,
table: Option<&str>,
name: &str,
quoted: bool,
) -> Result<Value> {
if is_rowid_alias(name)
&& !self.columns.iter().any(|col| {
col.name.eq_ignore_ascii_case(name)
&& table.is_none_or(|t| col.table.eq_ignore_ascii_case(t))
})
{
let qualifies = match table {
None => true,
Some(t) => self.columns.iter().any(|c| c.table.eq_ignore_ascii_case(t)),
};
if qualifies {
if let Some(r) = self.rowid {
return Ok(Value::Integer(r));
}
}
}
for (i, col) in self.columns.iter().enumerate() {
let name_ok = col.name.eq_ignore_ascii_case(name);
let table_ok = table.is_none_or(|t| col.table.eq_ignore_ascii_case(t));
if name_ok && table_ok {
return Ok(self.row[i].clone());
}
}
if let Some(s) = self.subqueries {
if let Some(v) = s.resolve_outer(table, name) {
return Ok(v);
}
}
if table.is_none() && name.eq_ignore_ascii_case("rank") {
if let Some(score) = self.rowid.and_then(|r| self.subqueries?.fts5_bm25(r, &[])) {
return Ok(Value::Real(score));
}
}
Err(no_such_column(schema, table, name, quoted))
}
}
pub(crate) fn no_such_column(
schema: Option<&str>,
table: Option<&str>,
name: &str,
quoted: bool,
) -> Error {
Error::Error(match (schema, table) {
(Some(s), Some(t)) => alloc::format!("no such column: {s}.{t}.{name}"),
(_, Some(t)) => alloc::format!("no such column: {t}.{name}"),
(_, None) if quoted => alloc::format!(
"no such column: \"{name}\" - should this be a string literal in single-quotes?"
),
(_, None) => alloc::format!("no such column: {name}"),
})
}
pub(crate) fn expr_affinity(expr: &Expr, ctx: &EvalCtx) -> Option<Affinity> {
match expr {
Expr::Column { table, column, .. } => {
for col in ctx.columns {
let name_ok = col.name.eq_ignore_ascii_case(column);
let table_ok = table
.as_deref()
.is_none_or(|t| col.table.eq_ignore_ascii_case(t));
if name_ok && table_ok {
return Some(col.affinity);
}
}
if is_rowid_alias(column) && ctx.rowid.is_some() {
return Some(Affinity::Integer);
}
None
}
Expr::Cast { type_name, .. } => Some(Affinity::from_type(Some(type_name))),
Expr::Paren(e) => expr_affinity(e, ctx),
Expr::Subquery(select) => ctx.subqueries.and_then(|sq| sq.column_affinity(select)),
_ => None,
}
}
pub fn apply_comparison_affinity(
l: Value,
la: Option<Affinity>,
r: Value,
ra: Option<Affinity>,
) -> (Value, Value) {
let numeric = |a: Option<Affinity>| {
matches!(
a,
Some(Affinity::Integer | Affinity::Real | Affinity::Numeric)
)
};
if numeric(la) && !numeric(ra) {
(l, Affinity::Numeric.coerce(r))
} else if numeric(ra) && !numeric(la) {
(Affinity::Numeric.coerce(l), r)
} else if la == Some(Affinity::Text) && ra.is_none() {
(l, Affinity::Text.coerce(r))
} else if ra == Some(Affinity::Text) && la.is_none() {
(Affinity::Text.coerce(l), r)
} else {
(l, r)
}
}
pub(crate) fn is_rowid_alias(name: &str) -> bool {
name.eq_ignore_ascii_case("rowid")
|| name.eq_ignore_ascii_case("_rowid_")
|| name.eq_ignore_ascii_case("oid")
}
use crate::value::Collation;
pub fn compare_op(op: BinaryOp, l: &Value, r: &Value, coll: Collation) -> Value {
use BinaryOp::*;
if matches!(l, Value::Null) || matches!(r, Value::Null) {
return Value::Null;
}
let ord = crate::value::cmp_values_coll(l, r, coll);
let res = match op {
Eq => ord == Ordering::Equal,
NotEq => ord != Ordering::Equal,
Lt => ord == Ordering::Less,
LtEq => ord != Ordering::Greater,
Gt => ord == Ordering::Greater,
GtEq => ord != Ordering::Less,
_ => unreachable!("compare_op on non-comparison operator"),
};
bool_value(res)
}
pub(crate) fn resolve_collation(left: &Expr, right: &Expr, ctx: &EvalCtx) -> Collation {
explicit_collation(left)
.or_else(|| explicit_collation(right))
.or_else(|| column_collation_of(left, ctx))
.or_else(|| column_collation_of(right, ctx))
.unwrap_or_default()
}
fn explicit_collation(e: &Expr) -> Option<Collation> {
match e {
Expr::Collate { collation, .. } => Collation::parse(collation),
Expr::Paren(inner) => explicit_collation(inner),
_ => None,
}
}
fn column_collation_of(e: &Expr, ctx: &EvalCtx) -> Option<Collation> {
match e {
Expr::Column { table, column, .. } => ctx.column_collation(table.as_deref(), column),
Expr::Paren(inner) | Expr::Collate { expr: inner, .. } => column_collation_of(inner, ctx),
_ => None,
}
}
pub fn key_collation(e: &Expr, ctx: &EvalCtx) -> Collation {
explicit_collation(e)
.or_else(|| column_collation_of(e, ctx))
.unwrap_or_default()
}
pub fn eval(expr: &Expr, ctx: &EvalCtx) -> Result<Value> {
match expr {
Expr::Literal(lit) => Ok(literal_value(lit)),
Expr::Parameter(p) => {
let idx = ctx.anon_counter.get();
if matches!(p, Param::Anonymous) {
ctx.anon_counter.set(idx + 1);
}
ctx.params.get(p, idx)
}
Expr::Column {
schema,
table,
column,
quoted,
} => ctx.resolve_column(schema.as_deref(), table.as_deref(), column, *quoted),
Expr::Paren(e) => eval(e, ctx),
Expr::Unary { op, expr } => eval_unary(*op, eval(expr, ctx)?),
Expr::Binary { op, left, right } => {
use BinaryOp::*;
match op {
And => eval_and(left, right, ctx),
Or => eval_or(left, right, ctx),
Eq | NotEq | Lt | LtEq | Gt | GtEq => {
let la = operand_arity(left, ctx);
let ra = operand_arity(right, ctx);
if la > 1 || ra > 1 {
if la != ra {
return Err(Error::Error("row value misused".into()));
}
return match (operand_row(left, ctx)?, operand_row(right, ctx)?) {
(Some(ls), Some(rs)) => compare_row_values(*op, &ls, &rs, ctx),
_ => Ok(Value::Null),
};
}
let l = eval(left, ctx)?;
let r = eval(right, ctx)?;
let (l, r) = apply_comparison_affinity(
l,
expr_affinity(left, ctx),
r,
expr_affinity(right, ctx),
);
let coll = resolve_collation(left, right, ctx);
Ok(compare_op(*op, &l, &r, coll))
}
Is | IsNot if matches!(unparen(right), Expr::Literal(Literal::Boolean(_))) => {
let want = matches!(unparen(right), Expr::Literal(Literal::Boolean(true)));
let is_truthy = truth(&eval(left, ctx)?) == Some(want);
let res = if matches!(op, IsNot) {
!is_truthy
} else {
is_truthy
};
Ok(Value::Integer(res as i64))
}
Is | IsNot => {
let la = operand_arity(left, ctx);
let ra = operand_arity(right, ctx);
if la > 1 || ra > 1 {
if la != ra {
return Err(Error::Error("row value misused".into()));
}
let ls = operand_row(left, ctx)?
.unwrap_or_else(|| alloc::vec![Expr::Literal(Literal::Null); la]);
let rs = operand_row(right, ctx)?
.unwrap_or_else(|| alloc::vec![Expr::Literal(Literal::Null); ra]);
return compare_row_values_is(matches!(op, IsNot), &ls, &rs, ctx);
}
let l = eval(left, ctx)?;
let r = eval(right, ctx)?;
let (l, r) = apply_comparison_affinity(
l,
expr_affinity(left, ctx),
r,
expr_affinity(right, ctx),
);
let eq = match (&l, &r) {
(Value::Null, Value::Null) => true,
(Value::Null, _) | (_, Value::Null) => false,
_ => {
crate::value::cmp_values_coll(
&l,
&r,
resolve_collation(left, right, ctx),
) == Ordering::Equal
}
};
Ok(bool_value(eq == matches!(op, Is)))
}
_ => eval_binary(
*op,
eval(left, ctx)?,
eval(right, ctx)?,
ctx.subqueries.is_some_and(|s| s.case_sensitive_like()),
),
}
}
Expr::IsNull { expr, negated } => {
let v = eval(expr, ctx)?;
let is_null = matches!(v, Value::Null);
Ok(bool_value(is_null != *negated))
}
Expr::InList {
expr,
list,
negated,
candidate_affinity,
} => {
if let Some(ls) = as_row_value(expr) {
return eval_row_in(ls, list, *negated, ctx);
}
eval_in(expr, list, *negated, candidate_affinity.as_deref(), ctx)
}
Expr::Between {
expr,
low,
high,
negated,
} => {
let ea = operand_arity(expr, ctx);
let la = operand_arity(low, ctx);
let ha = operand_arity(high, ctx);
if ea > 1 || la > 1 || ha > 1 {
if ea != la || ea != ha {
return Err(Error::Error("row value misused".into()));
}
let (Some(ev), Some(lv), Some(hv)) = (
operand_row(expr, ctx)?,
operand_row(low, ctx)?,
operand_row(high, ctx)?,
) else {
return Ok(Value::Null);
};
let ge = compare_row_values(BinaryOp::GtEq, &ev, &lv, ctx)?;
let le = compare_row_values(BinaryOp::LtEq, &ev, &hv, ctx)?;
let within = and3(&ge, &le);
return Ok(if *negated { not3(within) } else { within });
}
let v = eval(expr, ctx)?;
let lo = eval(low, ctx)?;
let hi = eval(high, ctx)?;
if matches!(v, Value::Null) {
return Ok(Value::Null);
}
let ea = expr_affinity(expr, ctx);
let (vl, lo) = apply_comparison_affinity(v.clone(), ea, lo, expr_affinity(low, ctx));
let (vh, hi) = apply_comparison_affinity(v, ea, hi, expr_affinity(high, ctx));
let ge = matches!(
crate::value::cmp_values_coll(&vl, &lo, resolve_collation(expr, low, ctx)),
Ordering::Greater | Ordering::Equal
);
let le = matches!(
crate::value::cmp_values_coll(&vh, &hi, resolve_collation(expr, high, ctx)),
Ordering::Less | Ordering::Equal
);
let within = ge && le;
Ok(bool_value(within != *negated))
}
Expr::Case {
operand,
when_then,
else_result,
} => eval_case(operand.as_deref(), when_then, else_result.as_deref(), ctx),
Expr::Cast { expr, type_name } => Ok(cast(eval(expr, ctx)?, type_name)),
Expr::Collate { expr, .. } => eval(expr, ctx),
Expr::Function {
name,
args,
star,
over,
..
} => {
if over.is_some() {
return Err(Error::Error(alloc::format!(
"misuse of window function {}()",
name.to_ascii_lowercase()
)));
}
super::func::eval_scalar(name, args, *star, ctx)
}
Expr::Subquery(select) => match ctx.subqueries {
Some(s) => s.scalar(select, ctx),
None => Err(Error::Unsupported("subquery in this context")),
},
Expr::Exists { select, negated } => match ctx.subqueries {
Some(s) => Ok(bool_value(s.exists(select, ctx)? != *negated)),
None => Err(Error::Unsupported("EXISTS in this context")),
},
Expr::InSelect {
expr,
select,
negated,
} => {
if let Some(ls) = as_row_value(expr) {
return eval_row_in_select(ls, select, *negated, ctx);
}
let v = eval(expr, ctx)?;
if matches!(v, Value::Null) {
return Ok(Value::Null);
}
let rows = match ctx.subqueries {
Some(s) => s.rows(select, ctx)?,
None => return Err(Error::Unsupported("IN (SELECT …) in this context")),
};
let mut set = Vec::with_capacity(rows.len());
for mut row in rows {
if row.len() != 1 {
return Err(Error::Error(alloc::format!(
"sub-select returns {} columns - expected 1",
row.len()
)));
}
set.push(row.swap_remove(0));
}
let coll = key_collation(expr, ctx);
let la = expr_affinity(expr, ctx);
let ra = ctx.subqueries.and_then(|s| s.column_affinity(select));
let mut saw_null = false;
for iv in &set {
if matches!(iv, Value::Null) {
saw_null = true;
continue;
}
let (lv, rv) = apply_comparison_affinity(v.clone(), la, iv.clone(), ra);
if crate::value::cmp_values_coll(&lv, &rv, coll) == Ordering::Equal {
return Ok(bool_value(!negated));
}
}
if saw_null {
Ok(Value::Null)
} else {
Ok(bool_value(*negated))
}
}
Expr::RowValue(_) => Err(Error::Error("row value misused".into())),
}
}
fn unparen(e: &Expr) -> &Expr {
match e {
Expr::Paren(inner) => unparen(inner),
other => other,
}
}
fn as_row_value(e: &Expr) -> Option<&[Expr]> {
match e {
Expr::RowValue(items) => Some(items),
Expr::Paren(inner) => as_row_value(inner),
_ => None,
}
}
fn row_subquery_first(select: &Select, ctx: &EvalCtx) -> Result<Option<Vec<Expr>>> {
let Some(s) = ctx.subqueries else {
return Err(Error::Unsupported("subquery in this context"));
};
Ok(s.rows(select, ctx)?.into_iter().next().map(|vals| {
vals.into_iter()
.map(|v| {
Expr::Literal(match v {
Value::Null => Literal::Null,
Value::Integer(i) => Literal::Integer(i),
Value::Real(r) => Literal::Real(r),
Value::Text(t) => Literal::Str(t),
Value::Blob(b) => Literal::Blob(b),
})
})
.collect()
}))
}
fn operand_arity(e: &Expr, ctx: &EvalCtx) -> usize {
match unparen(e) {
Expr::RowValue(items) => items.len(),
Expr::Subquery(select) => ctx
.subqueries
.map(|s| s.row_column_affinities(select).len())
.filter(|&n| n > 0)
.unwrap_or(1),
_ => 1,
}
}
fn operand_row(e: &Expr, ctx: &EvalCtx) -> Result<Option<Vec<Expr>>> {
match unparen(e) {
Expr::RowValue(items) => Ok(Some(items.clone())),
Expr::Subquery(select) => row_subquery_first(select, ctx),
other => Ok(Some(alloc::vec![other.clone()])),
}
}
fn and3(a: &Value, b: &Value) -> Value {
if matches!(a, Value::Integer(0)) || matches!(b, Value::Integer(0)) {
bool_value(false)
} else if matches!(a, Value::Null) || matches!(b, Value::Null) {
Value::Null
} else {
bool_value(true)
}
}
fn not3(a: Value) -> Value {
match a {
Value::Null => Value::Null,
Value::Integer(0) => bool_value(true),
_ => bool_value(false),
}
}
fn row_element_cmps(
lefts: &[Expr],
rights: &[Expr],
ctx: &EvalCtx,
) -> Result<Vec<Option<Ordering>>> {
if lefts.len() != rights.len() {
return Err(Error::Error(alloc::format!(
"row values have a different number of columns ({} vs {})",
lefts.len(),
rights.len()
)));
}
let mut out = Vec::with_capacity(lefts.len());
for (le, re) in lefts.iter().zip(rights) {
let l = eval(le, ctx)?;
let r = eval(re, ctx)?;
if matches!(l, Value::Null) || matches!(r, Value::Null) {
out.push(None);
continue;
}
let (l, r) =
apply_comparison_affinity(l, expr_affinity(le, ctx), r, expr_affinity(re, ctx));
let coll = resolve_collation(le, re, ctx);
out.push(Some(crate::value::cmp_values_coll(&l, &r, coll)));
}
Ok(out)
}
fn compare_row_values(
op: BinaryOp,
lefts: &[Expr],
rights: &[Expr],
ctx: &EvalCtx,
) -> Result<Value> {
let cmps = row_element_cmps(lefts, rights, ctx)?;
Ok(fold_row_comparison(op, &cmps))
}
fn compare_row_values_is(
is_not: bool,
lefts: &[Expr],
rights: &[Expr],
ctx: &EvalCtx,
) -> Result<Value> {
let mut all_eq = true;
for (le, re) in lefts.iter().zip(rights) {
let l = eval(le, ctx)?;
let r = eval(re, ctx)?;
let eq = match (&l, &r) {
(Value::Null, Value::Null) => true,
(Value::Null, _) | (_, Value::Null) => false,
_ => {
let (l, r) =
apply_comparison_affinity(l, expr_affinity(le, ctx), r, expr_affinity(re, ctx));
crate::value::cmp_values_coll(&l, &r, resolve_collation(le, re, ctx))
== Ordering::Equal
}
};
if !eq {
all_eq = false;
}
}
Ok(bool_value(all_eq != is_not))
}
fn fold_row_comparison(op: BinaryOp, cmps: &[Option<Ordering>]) -> Value {
use BinaryOp::*;
match op {
Eq | NotEq => {
let mut unknown = false;
for c in cmps {
match c {
None => unknown = true,
Some(Ordering::Equal) => {}
Some(_) => return bool_value(matches!(op, NotEq)), }
}
if unknown {
Value::Null
} else {
bool_value(matches!(op, Eq)) }
}
Lt | LtEq | Gt | GtEq => {
for c in cmps {
match c {
None => return Value::Null, Some(Ordering::Equal) => continue,
Some(Ordering::Less) => return bool_value(matches!(op, Lt | LtEq)),
Some(Ordering::Greater) => return bool_value(matches!(op, Gt | GtEq)),
}
}
bool_value(matches!(op, LtEq | GtEq))
}
_ => Value::Null,
}
}
fn eval_row_in_select(
lefts: &[Expr],
select: &Select,
negated: bool,
ctx: &EvalCtx,
) -> Result<Value> {
let lvals: Vec<Value> = lefts.iter().map(|e| eval(e, ctx)).collect::<Result<_>>()?;
let lafs: Vec<Option<Affinity>> = lefts.iter().map(|e| expr_affinity(e, ctx)).collect();
let lcolls: Vec<Collation> = lefts.iter().map(|e| key_collation(e, ctx)).collect();
let rafs = ctx
.subqueries
.map(|s| s.row_column_affinities(select))
.unwrap_or_default();
let rows = match ctx.subqueries {
Some(s) => s.rows(select, ctx)?,
None => return Err(Error::Unsupported("IN (SELECT …) in this context")),
};
let mut saw_null = false;
for row in &rows {
if row.len() != lvals.len() {
return Err(Error::Error(alloc::format!(
"sub-select returns {} columns - expected {}",
row.len(),
lvals.len()
)));
}
let cmps: Vec<Option<Ordering>> = lvals
.iter()
.zip(row)
.enumerate()
.map(|(i, (l, r))| {
if matches!(l, Value::Null) || matches!(r, Value::Null) {
None
} else {
let (lv, rv) = apply_comparison_affinity(
l.clone(),
lafs[i],
r.clone(),
rafs.get(i).copied().flatten(),
);
Some(crate::value::cmp_values_coll(&lv, &rv, lcolls[i]))
}
})
.collect();
match fold_row_comparison(BinaryOp::Eq, &cmps) {
Value::Integer(1) => return Ok(bool_value(!negated)),
Value::Null => saw_null = true,
_ => {}
}
}
if saw_null {
Ok(Value::Null)
} else {
Ok(bool_value(negated))
}
}
fn eval_row_in(lefts: &[Expr], list: &[Expr], negated: bool, ctx: &EvalCtx) -> Result<Value> {
let mut saw_null = false;
for item in list {
let Some(rights) = as_row_value(item) else {
return Err(Error::Error(
"row value IN list must contain row values".into(),
));
};
match fold_row_comparison(BinaryOp::Eq, &row_element_cmps(lefts, rights, ctx)?) {
Value::Integer(1) => return Ok(bool_value(!negated)),
Value::Null => saw_null = true,
_ => {}
}
}
if saw_null {
Ok(Value::Null)
} else {
Ok(bool_value(negated))
}
}
fn literal_value(lit: &Literal) -> Value {
match lit {
Literal::Null => Value::Null,
Literal::Integer(i) => Value::Integer(*i),
Literal::Real(r) => Value::Real(*r),
Literal::Str(s) => Value::Text(s.clone()),
Literal::Blob(b) => Value::Blob(b.clone()),
Literal::Boolean(b) => Value::Integer(*b as i64),
}
}
fn eval_unary(op: UnaryOp, v: Value) -> Result<Value> {
Ok(match op {
UnaryOp::Identity => v,
UnaryOp::Negate => match to_number(&v) {
Value::Integer(i) => i
.checked_neg()
.map(Value::Integer)
.unwrap_or(Value::Real(-(i as f64))),
Value::Real(r) => Value::Real(-r),
_ => Value::Null,
},
UnaryOp::Not => match truth(&v) {
None => Value::Null,
Some(b) => bool_value(!b),
},
UnaryOp::BitNot => match &v {
Value::Null => Value::Null,
_ => Value::Integer(!to_i64(&v)),
},
})
}
fn eval_and(left: &Expr, right: &Expr, ctx: &EvalCtx) -> Result<Value> {
let l = truth(&eval(left, ctx)?);
if l == Some(false) {
return Ok(bool_value(false)); }
let r = truth(&eval(right, ctx)?);
Ok(match (l, r) {
(Some(true), Some(true)) => bool_value(true),
(_, Some(false)) => bool_value(false),
_ => Value::Null,
})
}
fn eval_or(left: &Expr, right: &Expr, ctx: &EvalCtx) -> Result<Value> {
let l = truth(&eval(left, ctx)?);
if l == Some(true) {
return Ok(bool_value(true)); }
let r = truth(&eval(right, ctx)?);
Ok(match (l, r) {
(Some(false), Some(false)) => bool_value(false),
(_, Some(true)) => bool_value(true),
_ => Value::Null,
})
}
fn eval_in(
expr: &Expr,
list: &[Expr],
negated: bool,
candidate_affinity: Option<&str>,
ctx: &EvalCtx,
) -> Result<Value> {
if list.is_empty() {
return Ok(bool_value(negated));
}
let v = eval(expr, ctx)?;
if matches!(v, Value::Null) {
return Ok(Value::Null);
}
let coll = if list.len() == 1 {
resolve_collation(expr, &list[0], ctx)
} else {
key_collation(expr, ctx)
};
let ea = expr_affinity(expr, ctx);
let cand_aff = candidate_affinity.map(|t| Affinity::from_type(Some(t)));
let mut saw_null = false;
for item in list {
let iv = eval(item, ctx)?;
if matches!(iv, Value::Null) {
saw_null = true;
continue;
}
let ra = cand_aff;
let (lv, iv) = apply_comparison_affinity(v.clone(), ea, iv, ra);
if crate::value::cmp_values_coll(&lv, &iv, coll) == Ordering::Equal {
return Ok(bool_value(!negated));
}
}
if saw_null {
Ok(Value::Null)
} else {
Ok(bool_value(negated))
}
}
fn eval_case(
operand: Option<&Expr>,
when_then: &[(Expr, Expr)],
else_result: Option<&Expr>,
ctx: &EvalCtx,
) -> Result<Value> {
let base = match operand {
Some(e) => Some(eval(e, ctx)?),
None => None,
};
for (when, then) in when_then {
let matched = match (&base, operand) {
(Some(b), Some(op_expr)) => {
let w = eval(when, ctx)?;
let coll = resolve_collation(op_expr, when, ctx);
let (bv, wv) = apply_comparison_affinity(
b.clone(),
expr_affinity(op_expr, ctx),
w,
expr_affinity(when, ctx),
);
!matches!(bv, Value::Null)
&& !matches!(wv, Value::Null)
&& crate::value::cmp_values_coll(&bv, &wv, coll) == Ordering::Equal
}
_ => truth(&eval(when, ctx)?) == Some(true),
};
if matched {
return eval(then, ctx);
}
}
match else_result {
Some(e) => eval(e, ctx),
None => Ok(Value::Null),
}
}
fn eval_binary(op: BinaryOp, l: Value, r: Value, case_sensitive_like: bool) -> Result<Value> {
use BinaryOp::*;
Ok(match op {
Eq | NotEq | Lt | LtEq | Gt | GtEq => {
if matches!(l, Value::Null) || matches!(r, Value::Null) {
Value::Null
} else {
let ord = compare(&l, &r);
let res = match op {
Eq => ord == Ordering::Equal,
NotEq => ord != Ordering::Equal,
Lt => ord == Ordering::Less,
LtEq => ord != Ordering::Greater,
Gt => ord == Ordering::Greater,
GtEq => ord != Ordering::Less,
_ => unreachable!(),
};
bool_value(res)
}
}
Is | IsNot => {
let eq = match (&l, &r) {
(Value::Null, Value::Null) => true,
(Value::Null, _) | (_, Value::Null) => false,
_ => compare(&l, &r) == Ordering::Equal,
};
bool_value(eq == matches!(op, Is))
}
Add | Sub | Mul | Div | Mod => arithmetic(op, l, r),
Concat => {
if matches!(l, Value::Null) || matches!(r, Value::Null) {
Value::Null
} else {
let mut bytes = text_bytes(&l);
bytes.extend_from_slice(&text_bytes(&r));
match String::from_utf8(bytes) {
Ok(s) => Value::Text(s),
Err(e) => Value::Blob(e.into_bytes()),
}
}
}
BitAnd | BitOr | LShift | RShift => {
if matches!(l, Value::Null) || matches!(r, Value::Null) {
Value::Null
} else {
let a = to_i64(&l);
let b = to_i64(&r);
Value::Integer(match op {
BitAnd => a & b,
BitOr => a | b,
LShift => shift_left(a, b),
RShift => shift_right(a, b),
_ => unreachable!(),
})
}
}
Like | Glob => {
if matches!(l, Value::Null) || matches!(r, Value::Null) {
Value::Null
} else {
let text = to_text(&l);
let pat = to_text(&r);
let m = if matches!(op, Like) {
like_match_escape(&pat, &text, None, case_sensitive_like)
} else {
glob_match(&pat, &text)
};
bool_value(m)
}
}
JsonExtract => crate::exec::json::arrow(&l, &r, false)?,
JsonExtractText => crate::exec::json::arrow(&l, &r, true)?,
And | Or => unreachable!("handled with short-circuiting"),
})
}
pub fn arithmetic_values(op: BinaryOp, l: &Value, r: &Value) -> Value {
arithmetic(op, l.clone(), r.clone())
}
pub fn like_glob_values(glob: bool, l: &Value, r: &Value) -> Value {
if matches!(l, Value::Null) || matches!(r, Value::Null) {
return Value::Null;
}
let text = to_text(l);
let pat = to_text(r);
let m = if glob {
glob_match(&pat, &text)
} else {
like_match(&pat, &text)
};
bool_value(m)
}
pub fn concat_values(l: &Value, r: &Value) -> Value {
if matches!(l, Value::Null) || matches!(r, Value::Null) {
return Value::Null;
}
let mut bytes = text_bytes(l);
bytes.extend_from_slice(&text_bytes(r));
match String::from_utf8(bytes) {
Ok(s) => Value::Text(s),
Err(e) => Value::Blob(e.into_bytes()),
}
}
pub fn is_values(is: bool, l: &Value, r: &Value) -> Value {
let eq = match (l, r) {
(Value::Null, Value::Null) => true,
(Value::Null, _) | (_, Value::Null) => false,
_ => compare(l, r) == core::cmp::Ordering::Equal,
};
bool_value(eq == is)
}
pub fn bitwise_values(op: BinaryOp, l: &Value, r: &Value) -> Value {
use BinaryOp::*;
if matches!(l, Value::Null) || matches!(r, Value::Null) {
return Value::Null;
}
let a = to_i64(l);
let b = to_i64(r);
Value::Integer(match op {
BitAnd => a & b,
BitOr => a | b,
LShift => shift_left(a, b),
RShift => shift_right(a, b),
_ => return Value::Null,
})
}
fn arithmetic(op: BinaryOp, l: Value, r: Value) -> Value {
use BinaryOp::*;
if matches!(l, Value::Null) || matches!(r, Value::Null) {
return Value::Null;
}
let ln = to_number(&l);
let rn = to_number(&r);
if let (Value::Integer(a), Value::Integer(b)) = (&ln, &rn) {
let (a, b) = (*a, *b);
return match op {
Add => a
.checked_add(b)
.map(Value::Integer)
.unwrap_or(Value::Real(a as f64 + b as f64)),
Sub => a
.checked_sub(b)
.map(Value::Integer)
.unwrap_or(Value::Real(a as f64 - b as f64)),
Mul => a
.checked_mul(b)
.map(Value::Integer)
.unwrap_or(Value::Real(a as f64 * b as f64)),
Div => {
if b == 0 {
Value::Null
} else {
a.checked_div(b)
.map(Value::Integer)
.unwrap_or(Value::Real(a as f64 / b as f64))
}
}
Mod => {
if b == 0 {
Value::Null
} else {
Value::Integer(a.wrapping_rem(b))
}
}
_ => unreachable!(),
};
}
let a = number_as_f64(&ln);
let b = number_as_f64(&rn);
let real = |r: f64| {
if r.is_nan() {
Value::Null
} else {
Value::Real(r)
}
};
match op {
Add => real(a + b),
Sub => real(a - b),
Mul => real(a * b),
Div => {
if b == 0.0 {
Value::Null
} else {
real(a / b)
}
}
Mod => {
let bi = b as i64;
if bi == 0 {
Value::Null
} else {
Value::Real((a as i64).wrapping_rem(bi) as f64)
}
}
_ => unreachable!(),
}
}
fn shift_left(a: i64, b: i64) -> i64 {
if b <= -64 || b >= 64 {
0
} else if b < 0 {
((a as u64) >> (-b) as u32) as i64
} else {
((a as u64) << b as u32) as i64
}
}
fn shift_right(a: i64, b: i64) -> i64 {
if b <= -64 {
0
} else if b < 0 {
shift_left(a, -b)
} else if b >= 64 {
if a < 0 {
-1
} else {
0
}
} else {
a >> b as u32
}
}
pub fn cast(v: Value, type_name: &str) -> Value {
if matches!(v, Value::Null) {
return Value::Null;
}
let aff = type_name.to_ascii_uppercase();
let v = if matches!(v, Value::Blob(_)) && !aff.contains("BLOB") {
Value::Text(to_text(&v))
} else {
v
};
if aff.contains("INT") {
Value::Integer(match &v {
Value::Text(s) => parse_int_prefix(s),
_ => to_i64(&v),
})
} else if aff.contains("CHAR") || aff.contains("CLOB") || aff.contains("TEXT") {
Value::Text(to_text(&v))
} else if aff.contains("REAL") || aff.contains("FLOA") || aff.contains("DOUB") {
Value::Real(number_as_f64(&to_number(&v)))
} else if aff.contains("BLOB") {
match v {
Value::Blob(_) => v,
other => Value::Blob(to_text(&other).into_bytes()),
}
} else if aff.is_empty() {
v
} else {
match v {
Value::Text(_) => match to_number(&v) {
Value::Real(r) => integral_real_to_int(r).unwrap_or(Value::Real(r)),
other => other,
},
other => to_number(&other),
}
}
}
pub fn compare(a: &Value, b: &Value) -> Ordering {
crate::value::cmp_values(a, b)
}
pub fn truth(v: &Value) -> Option<bool> {
match v {
Value::Null => None,
Value::Integer(i) => Some(*i != 0),
Value::Real(r) => Some(*r != 0.0),
Value::Text(_) | Value::Blob(_) => Some(number_as_f64(&to_number(v)) != 0.0),
}
}
fn bool_value(b: bool) -> Value {
Value::Integer(b as i64)
}
pub fn to_number(v: &Value) -> Value {
match v {
Value::Integer(_) | Value::Real(_) => v.clone(),
Value::Null => Value::Null,
Value::Text(s) => parse_number(s),
Value::Blob(_) => Value::Integer(0),
}
}
pub(crate) fn parse_decimal_f64(t: &str) -> Option<f64> {
let body = t.strip_prefix(['+', '-']).unwrap_or(t);
match body.as_bytes().first() {
Some(c) if c.is_ascii_digit() || *c == b'.' => t.parse::<f64>().ok(),
_ => None,
}
}
fn parse_int_prefix(s: &str) -> i64 {
let b = s.as_bytes();
let mut i = 0;
while i < b.len() && b[i].is_ascii_whitespace() {
i += 1;
}
let neg = match b.get(i) {
Some(b'-') => {
i += 1;
true
}
Some(b'+') => {
i += 1;
false
}
_ => false,
};
let start = i;
let mut acc: i64 = 0;
let mut overflow = false;
while i < b.len() && b[i].is_ascii_digit() {
let d = (b[i] - b'0') as i64;
match acc.checked_mul(10).and_then(|x| x.checked_sub(d)) {
Some(v) => acc = v,
None => {
overflow = true;
break;
}
}
i += 1;
}
if i == start {
return 0;
}
if overflow {
return if neg { i64::MIN } else { i64::MAX };
}
if neg {
acc
} else {
acc.checked_neg().unwrap_or(i64::MAX)
}
}
fn parse_number(s: &str) -> Value {
let t = s.trim();
if let Ok(i) = t.parse::<i64>() {
return Value::Integer(i);
}
if let Some(f) = parse_decimal_f64(t) {
return Value::Real(f);
}
let mut end = 0;
let bytes = t.as_bytes();
let mut seen_dot = false;
let mut seen_digit = false;
while end < bytes.len() {
let c = bytes[end];
if c.is_ascii_digit() {
seen_digit = true;
end += 1;
} else if c == b'.' && !seen_dot {
seen_dot = true;
end += 1;
} else if (c == b'-' || c == b'+') && end == 0 {
end += 1;
} else if (c == b'e' || c == b'E') && seen_digit {
let mut k = end + 1;
if k < bytes.len() && (bytes[k] == b'+' || bytes[k] == b'-') {
k += 1;
}
if k < bytes.len() && bytes[k].is_ascii_digit() {
while k < bytes.len() && bytes[k].is_ascii_digit() {
k += 1;
}
end = k;
}
break;
} else {
break;
}
}
if !seen_digit {
return Value::Integer(0);
}
let prefix = &t[..end];
if let Ok(i) = prefix.parse::<i64>() {
Value::Integer(i)
} else if let Ok(f) = prefix.parse::<f64>() {
Value::Real(f)
} else {
Value::Integer(0)
}
}
fn number_as_f64(v: &Value) -> f64 {
match v {
Value::Integer(i) => *i as f64,
Value::Real(r) => *r,
_ => 0.0,
}
}
pub fn to_i64(v: &Value) -> i64 {
match to_number(v) {
Value::Integer(i) => i,
Value::Real(r) => r as i64,
_ => 0,
}
}
pub fn to_f64(v: &Value) -> f64 {
number_as_f64(&to_number(v))
}
fn text_bytes(v: &Value) -> Vec<u8> {
match v {
Value::Blob(b) => b.clone(),
other => to_text(other).into_bytes(),
}
}
pub fn to_text(v: &Value) -> String {
match v {
Value::Null => String::new(),
Value::Integer(i) => i.to_string(),
Value::Real(r) => format_real(*r),
Value::Text(s) => s.clone(),
Value::Blob(b) => String::from_utf8_lossy(b).into_owned(),
}
}
pub fn format_real(r: f64) -> String {
if r.is_nan() {
return String::new();
}
if !r.is_finite() {
return if r < 0.0 {
String::from("-Inf")
} else {
String::from("Inf")
};
}
if r == 0.0 {
return String::from("0.0");
}
let neg = r < 0.0;
let a = crate::util::float::abs(r);
let sci = alloc::format!("{:.14e}", a);
let (mant, exp_str) = sci.split_once('e').expect("scientific format has 'e'");
let exp: i32 = exp_str.parse().expect("valid exponent");
let digits: String = mant.chars().filter(|c| *c != '.').collect();
let body = if !(-4..15).contains(&exp) {
let frac = digits[1..].trim_end_matches('0');
let mantissa = if frac.is_empty() {
alloc::format!("{}.0", &digits[0..1])
} else {
alloc::format!("{}.{}", &digits[0..1], frac)
};
let sign = if exp < 0 { '-' } else { '+' };
alloc::format!("{mantissa}e{sign}{:02}", exp.abs())
} else if exp >= 0 {
let int_len = (exp + 1) as usize;
let int_part = &digits[..int_len];
let frac = digits[int_len..].trim_end_matches('0');
if frac.is_empty() {
alloc::format!("{int_part}.0")
} else {
alloc::format!("{int_part}.{frac}")
}
} else {
let lead = (-(exp + 1)) as usize;
let mut frac = String::new();
for _ in 0..lead {
frac.push('0');
}
frac.push_str(&digits);
let frac = frac.trim_end_matches('0');
alloc::format!("0.{frac}")
};
if neg {
alloc::format!("-{body}")
} else {
body
}
}
fn like_match(pattern: &str, text: &str) -> bool {
like_match_escape(pattern, text, None, false)
}
pub fn like_match_escape(
pattern: &str,
text: &str,
escape: Option<char>,
case_sensitive: bool,
) -> bool {
let p: Vec<char> = pattern.chars().collect();
let t: Vec<char> = text.chars().collect();
like_rec(&p, &t, escape, case_sensitive)
}
fn like_char_eq(pc: char, tc: char, case_sensitive: bool) -> bool {
if case_sensitive {
pc == tc
} else {
pc.eq_ignore_ascii_case(&tc)
}
}
fn like_rec(p: &[char], t: &[char], esc: Option<char>, cs: bool) -> bool {
if p.is_empty() {
return t.is_empty();
}
if let Some(e) = esc {
if p[0] == e {
return match p.get(1) {
Some(&lit) => {
!t.is_empty()
&& like_char_eq(lit, t[0], cs)
&& like_rec(&p[2..], &t[1..], esc, cs)
}
None => t.is_empty(),
};
}
}
match p[0] {
'%' => {
let rest = &p[1..];
if like_rec(rest, t, esc, cs) {
return true;
}
for i in 0..t.len() {
if like_rec(rest, &t[i + 1..], esc, cs) {
return true;
}
}
false
}
'_' => !t.is_empty() && like_rec(&p[1..], &t[1..], esc, cs),
pc => !t.is_empty() && like_char_eq(pc, t[0], cs) && like_rec(&p[1..], &t[1..], esc, cs),
}
}
pub fn glob_match(pattern: &str, text: &str) -> bool {
let p: Vec<char> = pattern.chars().collect();
let t: Vec<char> = text.chars().collect();
glob_rec(&p, &t)
}
fn glob_rec(p: &[char], t: &[char]) -> bool {
if p.is_empty() {
return t.is_empty();
}
match p[0] {
'*' => {
let rest = &p[1..];
if glob_rec(rest, t) {
return true;
}
for i in 0..t.len() {
if glob_rec(rest, &t[i + 1..]) {
return true;
}
}
false
}
'?' => !t.is_empty() && glob_rec(&p[1..], &t[1..]),
'[' => {
if t.is_empty() {
return false;
}
let mut i = 1;
let mut negate = false;
if i < p.len() && (p[i] == '^') {
negate = true;
i += 1;
}
let mut matched = false;
if i < p.len() && p[i] == ']' {
if t[0] == ']' {
matched = true;
}
i += 1;
}
while i < p.len() && p[i] != ']' {
if i + 2 < p.len() && p[i + 1] == '-' && p[i + 2] != ']' {
if t[0] >= p[i] && t[0] <= p[i + 2] {
matched = true;
}
i += 3;
} else {
if t[0] == p[i] {
matched = true;
}
i += 1;
}
}
if i >= p.len() {
return false; }
(matched != negate) && glob_rec(&p[i + 1..], &t[1..])
}
pc => !t.is_empty() && pc == t[0] && glob_rec(&p[1..], &t[1..]),
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn comparison_class_order() {
assert_eq!(compare(&Value::Null, &Value::Integer(0)), Ordering::Less);
assert_eq!(
compare(&Value::Integer(5), &Value::Text("a".into())),
Ordering::Less
);
assert_eq!(
compare(&Value::Integer(2), &Value::Real(2.0)),
Ordering::Equal
);
assert_eq!(
compare(&Value::Text("abc".into()), &Value::Text("abd".into())),
Ordering::Less
);
}
#[test]
fn like_and_glob() {
assert!(like_match("a%", "apple"));
assert!(like_match("%ple", "apple"));
assert!(like_match("a_ple", "apple"));
assert!(like_match("APPLE", "apple")); assert!(!like_match("a%", "banana"));
assert!(glob_match("a*", "apple"));
assert!(glob_match("a[pq]ple", "apple"));
assert!(!glob_match("A*", "apple")); assert!(glob_match("a[]]c", "a]c"));
assert!(glob_match("[]]", "]"));
assert!(glob_match("[^]]", "x"));
assert!(!glob_match("[^]]", "]"));
assert!(glob_match("[]a]", "a"));
assert!(!glob_match("[^]a]", "a"));
}
#[test]
fn concat_preserves_blob_bytes() {
assert_eq!(
eval_binary(
BinaryOp::Concat,
Value::Blob(vec![0x00]),
Value::Blob(vec![0xff]),
false
)
.unwrap(),
Value::Blob(vec![0x00, 0xff])
);
assert_eq!(
eval_binary(
BinaryOp::Concat,
Value::Blob(vec![0x61]),
Value::Text("b".into()),
false
)
.unwrap(),
Value::Text("ab".into())
);
}
#[test]
fn shift_and_negate_overflow_do_not_panic() {
assert_eq!(shift_right(-1, i64::MIN), 0);
assert_eq!(shift_left(1, i64::MIN), 0);
assert_eq!(shift_right(1, i64::MIN), 0);
assert_eq!(
eval_unary(UnaryOp::Negate, Value::Integer(i64::MIN)).unwrap(),
Value::Real(-(i64::MIN as f64))
);
assert_eq!(
arithmetic(BinaryOp::Div, Value::Integer(i64::MIN), Value::Integer(-1)),
Value::Real(i64::MIN as f64 / -1.0)
);
assert_eq!(
arithmetic(BinaryOp::Mod, Value::Integer(i64::MIN), Value::Integer(-1)),
Value::Integer(0)
);
}
#[test]
fn real_formatting() {
assert_eq!(format_real(3.0), "3.0");
assert_eq!(format_real(2.5), "2.5");
assert_eq!(format_real(35.0 / 3.0), "11.6666666666667");
assert_eq!(format_real(1.0 / 3.0), "0.333333333333333");
assert_eq!(format_real(0.1), "0.1");
assert_eq!(format_real(0.1 + 0.2), "0.3");
assert_eq!(format_real(1e20), "1.0e+20");
assert_eq!(format_real(1e-10), "1.0e-10");
assert_eq!(format_real(1e15), "1.0e+15");
assert_eq!(format_real(1e14), "100000000000000.0");
assert_eq!(format_real(0.0001), "0.0001");
assert_eq!(format_real(0.00001), "1.0e-05");
assert_eq!(format_real(-2.5), "-2.5");
assert_eq!(format_real(0.0), "0.0");
}
}