use alloc::format;
use alloc::vec::Vec;
use spg_sql::ast::{BinOp, ColumnName, Expr, Literal, UnOp};
use spg_storage::{Row, Value};
use super::{
EvalContext, EvalError, apply_binary, apply_unary, column_collation, composite_eq, eval_expr,
like_match_str, literal_to_value,
};
pub(crate) enum Step {
Column(usize),
Lit(Value<'static>),
Binary(BinOp),
Coalesce {
n_args: usize,
},
NullIf,
Extremum {
n_args: usize,
max: bool,
},
Connective {
op: BinOp,
rhs: Vec<Step>,
},
BinaryCi(BinOp),
Unary(UnOp),
IsNull {
negated: bool,
},
AnyTextMatch {
negated: bool,
},
InSet {
set: crate::memoize::InListSet,
has_null: bool,
negated: bool,
fallback: Expr,
},
Like {
pattern: alloc::vec::Vec<char>,
negated: bool,
case_insensitive: bool,
},
AnyAll {
op: spg_sql::ast::BinOp,
is_any: bool,
arr: Value<'static>,
},
Extract {
field: spg_sql::ast::ExtractField,
fallback: Expr,
},
Regex {
re: crate::eval::CompiledRe,
fallback: Expr,
},
LikeSubstring {
needle: alloc::string::String,
k_before: usize,
m_after: usize,
negated: bool,
case_insensitive: bool,
},
Function {
name_lower: alloc::string::String,
n_args: usize,
},
ColumnLength {
pos: usize,
},
ColumnOctetLength {
pos: usize,
},
Cast {
target: spg_sql::ast::CastTarget,
},
CastPlain {
dt: spg_storage::DataType,
name: alloc::string::String,
},
Case {
operand: Option<CompiledExpr>,
branches: alloc::vec::Vec<(CompiledExpr, CompiledExpr)>,
else_branch: Option<CompiledExpr>,
},
CoerceCommon(spg_storage::DataType),
Subtree(Expr),
}
pub(crate) struct CompiledExpr {
steps: Vec<Step>,
pred_shape: PredShape,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum PredShape {
Other,
ColumnCmpLit,
ColumnInSet,
ColumnLike,
}
impl CompiledExpr {
pub(crate) fn as_column_cmp_literal(&self) -> Option<(usize, BinOp, &Value<'static>)> {
let [Step::Column(pos), Step::Lit(lit), Step::Binary(op)] = &self.steps[..] else {
return None;
};
if !matches!(
op,
BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
) {
return None;
}
Some((*pos, *op, lit))
}
pub(crate) fn as_column_in_set(
&self,
) -> Option<(usize, &crate::memoize::InListSet, bool, bool)> {
let [
Step::Column(pos),
Step::InSet {
set,
has_null,
negated,
..
},
] = &self.steps[..]
else {
return None;
};
Some((*pos, set, *has_null, *negated))
}
pub(crate) fn as_column_like(&self) -> Option<(usize, &Step)> {
let [
Step::Column(pos),
step @ (Step::Like { .. } | Step::LikeSubstring { .. }),
] = &self.steps[..]
else {
return None;
};
Some((*pos, step))
}
pub(crate) fn as_single_column_length(&self) -> Option<usize> {
if self.steps.len() == 1
&& let Step::ColumnLength { pos } = &self.steps[0]
{
Some(*pos)
} else {
None
}
}
}
fn unparseable_numeric_literal_cmp(lhs: &Expr, rhs: &Expr, ctx: &EvalContext<'_>) -> bool {
let check = |lit: &Expr, other: &Expr| -> bool {
let Expr::Literal(spg_sql::ast::Literal::String(text)) = lit else {
return false;
};
let Some(desc) = crate::describe::describe_expr(other, ctx.columns) else {
return false;
};
if !matches!(
desc.ty,
spg_storage::DataType::SmallInt
| spg_storage::DataType::Int
| spg_storage::DataType::BigInt
| spg_storage::DataType::Float
| spg_storage::DataType::Real
| spg_storage::DataType::Numeric { .. }
) {
return false;
}
crate::conversions::coerce_value(spg_storage::Value::text(text.as_str()), desc.ty, "", 0)
.is_err()
};
check(lhs, rhs) || check(rhs, lhs)
}
fn operand_declares_a_collation(e: &Expr, ctx: &EvalContext<'_>) -> bool {
let derived = crate::collate_derive::derive(e, &|c: &ColumnName| {
let pos = crate::eval::find_column_pos(c, ctx)?;
ctx.columns.get(pos)?.collation_name.clone()
});
derived.conflict().is_some()
|| derived
.name()
.is_some_and(|n| crate::collate::is_supported(n))
}
pub(crate) fn compile_column_pos(c: &ColumnName, ctx: &EvalContext<'_>) -> Option<usize> {
if let Some(q) = &c.qualifier {
if let Some(pos) = ctx
.columns
.iter()
.position(|s| composite_eq(&s.name, q, &c.name))
{
return Some(pos);
}
let prefix_exists = ctx.columns.iter().any(|s| {
s.name.starts_with(q.as_str()) && s.name.as_bytes().get(q.len()) == Some(&b'.')
});
if prefix_exists {
return None;
}
match ctx.table_alias {
Some(a) if a == q => {}
_ => return None,
}
}
if let Some(pos) = ctx.columns.iter().position(|s| s.name == c.name) {
return Some(pos);
}
let mut matches = ctx.columns.iter().enumerate().filter(|(_, s)| {
s.name.len() > c.name.len()
&& s.name.ends_with(c.name.as_str())
&& s.name.as_bytes()[s.name.len() - c.name.len() - 1] == b'.'
});
let first = matches.next();
if matches.next().is_some() {
return None; }
first.map(|(i, _)| i)
}
fn can_raise_at_run_time(e: &Expr) -> bool {
match e {
Expr::Literal(_) | Expr::Column(_) => false,
Expr::Binary { op, lhs, rhs } => {
!matches!(
op,
BinOp::Eq
| BinOp::NotEq
| BinOp::Lt
| BinOp::LtEq
| BinOp::Gt
| BinOp::GtEq
| BinOp::And
| BinOp::Or
) || can_raise_at_run_time(lhs)
|| can_raise_at_run_time(rhs)
}
Expr::Unary { op, expr } => !matches!(op, UnOp::Not) || can_raise_at_run_time(expr),
Expr::IsNull { expr, .. } | Expr::BoolTest { expr, .. } => can_raise_at_run_time(expr),
Expr::Like { expr, pattern, .. } => {
can_raise_at_run_time(expr) || can_raise_at_run_time(pattern)
}
Expr::InList { expr, list, .. } => {
can_raise_at_run_time(expr) || list.iter().any(can_raise_at_run_time)
}
_ => true,
}
}
fn compile_into(e: &Expr, ctx: &EvalContext<'_>, steps: &mut Vec<Step>) {
match e {
Expr::Literal(l) => steps.push(Step::Lit(literal_to_value(l))),
Expr::Column(c) => match compile_column_pos(c, ctx) {
Some(pos)
if ctx
.columns
.get(pos)
.is_some_and(|sc| sc.user_composite_type.is_some()) =>
{
steps.push(Step::Subtree(e.clone()));
}
Some(pos) => steps.push(Step::Column(pos)),
None => steps.push(Step::Subtree(e.clone())),
},
Expr::Binary { lhs, op, rhs } => {
if ctx.mysql_dialect
&& matches!(
op,
BinOp::BitAnd
| BinOp::BitOr
| BinOp::BitXor
| BinOp::InetContainedBy
| BinOp::InetContains
)
{
steps.push(Step::Subtree(e.clone()));
return;
}
if ctx.mysql_dialect && matches!(op, BinOp::LogicalXor) {
steps.push(Step::Subtree(e.clone()));
return;
}
if ctx.mysql_dialect
&& matches!(
op,
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod
)
&& (crate::eval::expr_set_variants(lhs, ctx.columns).is_some()
|| crate::eval::expr_set_variants(rhs, ctx.columns).is_some()
|| crate::eval::expr_inline_enum_variants(lhs, ctx.columns).is_some()
|| crate::eval::expr_inline_enum_variants(rhs, ctx.columns).is_some())
{
steps.push(Step::Subtree(e.clone()));
return;
}
if matches!(op, BinOp::And | BinOp::Or) {
if matches!(rhs.as_ref(), Expr::Literal(_)) {
steps.push(Step::Subtree(e.clone()));
return;
}
if !can_raise_at_run_time(rhs) {
compile_into(lhs, ctx, steps);
compile_into(rhs, ctx, steps);
steps.push(Step::Binary(*op));
return;
}
compile_into(lhs, ctx, steps);
let mut rhs_steps = Vec::new();
compile_into(rhs, ctx, &mut rhs_steps);
steps.push(Step::Connective {
op: *op,
rhs: rhs_steps,
});
return;
}
let cmp = matches!(
op,
BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
);
if cmp
&& ctx.catalog.is_some_and(|cat| !cat.enum_types().is_empty())
&& (crate::eval::expr_enum_labels(lhs, ctx.columns, ctx.catalog).is_some()
|| crate::eval::expr_enum_labels(rhs, ctx.columns, ctx.catalog).is_some())
{
steps.push(Step::Subtree(e.clone()));
return;
}
if matches!(op, BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq)
&& operand_declares_a_collation(lhs, ctx) | operand_declares_a_collation(rhs, ctx)
{
steps.push(Step::Subtree(e.clone()));
return;
}
if cmp && unparseable_numeric_literal_cmp(lhs, rhs, ctx) {
steps.push(Step::Subtree(e.clone()));
return;
}
compile_into(lhs, ctx, steps);
compile_into(rhs, ctx, steps);
let ci = cmp
&& (matches!(
column_collation(lhs, ctx),
Some(spg_storage::Collation::CaseInsensitive)
) || matches!(
column_collation(rhs, ctx),
Some(spg_storage::Collation::CaseInsensitive)
));
let ci = ci || (cmp && super::resolve::mysql_text_fold_applies(lhs, rhs, ctx));
steps.push(if ci {
Step::BinaryCi(*op)
} else {
Step::Binary(*op)
});
}
Expr::Unary { op, expr } => {
if ctx.mysql_dialect && matches!(op, UnOp::BitNot) {
steps.push(Step::Subtree(e.clone()));
return;
}
compile_into(expr, ctx, steps);
steps.push(Step::Unary(*op));
}
Expr::IsNull { expr, negated } => {
compile_into(expr, ctx, steps);
steps.push(Step::IsNull { negated: *negated });
}
Expr::InList {
expr,
list,
negated,
} => {
if ctx.mysql_dialect {
steps.push(Step::Subtree(e.clone()));
return;
}
match crate::build_in_list_set(list) {
Some(entry) if fully_compilable(expr) => {
compile_into(expr, ctx, steps);
steps.push(Step::InSet {
set: entry.set,
has_null: entry.has_null,
negated: *negated,
fallback: e.clone(),
});
}
_ => steps.push(Step::Subtree(e.clone())),
}
}
Expr::Like {
expr,
pattern,
negated,
case_insensitive,
} => {
if ctx.mysql_dialect {
steps.push(Step::Subtree(e.clone()));
return;
}
match literal_text_pattern(pattern) {
Some(pat) if fully_compilable(expr) => {
if !pat.is_empty() && pat.chars().all(|c| c == '%') {
compile_into(expr, ctx, steps);
steps.push(Step::AnyTextMatch { negated: *negated });
return;
}
compile_into(expr, ctx, steps);
let chars: alloc::vec::Vec<char> = if *case_insensitive {
pat.to_lowercase().chars().collect()
} else {
pat.chars().collect()
};
if let Some((k, needle, m)) = like_substring_shape(&chars) {
steps.push(Step::LikeSubstring {
needle,
k_before: k,
m_after: m,
negated: *negated,
case_insensitive: *case_insensitive,
});
return;
}
steps.push(Step::Like {
pattern: chars,
negated: *negated,
case_insensitive: *case_insensitive,
});
}
_ => steps.push(Step::Subtree(e.clone())),
}
}
Expr::FunctionCall { name, args }
if name.eq_ignore_ascii_case("regexp_like")
&& matches!(args.len(), 2 | 3)
&& regex_literal_parts(args.as_slice()).is_some()
&& fully_compilable(&args[0]) =>
{
let (pat, ci) = regex_literal_parts(args.as_slice()).expect("checked above");
match crate::eval::compile_re(pat, ci) {
Ok(re) => {
compile_into(&args[0], ctx, steps);
steps.push(Step::Regex {
re,
fallback: e.clone(),
});
}
Err(_) => steps.push(Step::Subtree(e.clone())),
}
}
Expr::FunctionCall { name, args } if is_pure_scalar_function(name) => {
let lower = name.to_ascii_lowercase();
if args.len() == 1 {
if let Expr::Column(c) = &args[0]
&& let Some(pos) = compile_column_pos(c, ctx)
{
match lower.as_str() {
"length" | "char_length" | "character_length" => {
steps.push(Step::ColumnLength { pos });
return;
}
"octet_length" => {
steps.push(Step::ColumnOctetLength { pos });
return;
}
_ => {}
}
}
}
for a in args {
compile_into(a, ctx, steps);
}
if name.eq_ignore_ascii_case("coalesce") && !args.is_empty() {
steps.push(Step::Coalesce { n_args: args.len() });
return;
}
if name.eq_ignore_ascii_case("nullif") && args.len() == 2 {
steps.push(Step::NullIf);
return;
}
if (lower == "greatest" || lower == "least") && !args.is_empty() {
steps.push(Step::Extremum {
n_args: args.len(),
max: lower == "greatest",
});
return;
}
steps.push(Step::Function {
name_lower: lower,
n_args: args.len(),
});
}
e if !matches!(e, Expr::Literal(_)) && constant_expr(e) => {
match eval_expr(e, &Row::new(alloc::vec::Vec::new()), ctx) {
Ok(v) => steps.push(Step::Lit(v)),
Err(_) => steps.push(Step::Subtree(e.clone())),
}
}
Expr::AnyAll {
expr,
op,
array,
is_any,
} if !ctx.mysql_dialect
&& ((matches!(op, spg_sql::ast::BinOp::Eq) && *is_any)
|| (matches!(op, spg_sql::ast::BinOp::NotEq) && !*is_any))
&& array_literal_items(array)
.is_some_and(|it| !it.is_empty() && crate::build_in_list_set(it).is_some())
&& fully_compilable(expr) =>
{
let items = array_literal_items(array).expect("checked above");
let entry = crate::build_in_list_set(items).expect("checked above");
compile_into(expr, ctx, steps);
steps.push(Step::InSet {
set: entry.set,
has_null: entry.has_null,
negated: !*is_any,
fallback: e.clone(),
});
}
Expr::AnyAll {
expr,
op,
array,
is_any,
} if constant_expr(array) => {
match eval_expr(array, &Row::new(alloc::vec::Vec::new()), ctx) {
Ok(arr) => {
compile_into(expr, ctx, steps);
if !ctx.mysql_dialect
&& ((matches!(op, spg_sql::ast::BinOp::Eq) && *is_any)
|| (matches!(op, spg_sql::ast::BinOp::NotEq) && !*is_any))
&& let Some(entry) = value_array_in_list_set(&arr)
{
steps.push(Step::InSet {
set: entry.set,
has_null: entry.has_null,
negated: !*is_any,
fallback: e.clone(),
});
return;
}
steps.push(Step::AnyAll {
op: *op,
is_any: *is_any,
arr,
});
}
Err(_) => steps.push(Step::Subtree(e.clone())),
}
}
Expr::Extract { field, source } => {
compile_into(source, ctx, steps);
steps.push(Step::Extract {
field: field.clone(),
fallback: e.clone(),
});
}
Expr::Cast { expr, target } => {
let named_text_family = match target {
spg_sql::ast::CastTarget::Named(n) => named_varchar_family(n),
_ => false,
};
if let spg_sql::ast::CastTarget::Named(n) = target
&& !named_text_family
&& let Some(dt) = super::cast::plain_named_target(n)
{
compile_into(expr, ctx, steps);
steps.push(Step::CastPlain {
dt,
name: n.clone(),
});
return;
}
if matches!(target, spg_sql::ast::CastTarget::RegClass)
|| (matches!(target, spg_sql::ast::CastTarget::Named(_)) && !named_text_family)
{
steps.push(Step::Subtree(e.clone()));
return;
}
if (matches!(target, spg_sql::ast::CastTarget::Text) || named_text_family)
&& crate::describe::describe_expr(expr, ctx.columns)
.is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
{
steps.push(Step::Subtree(e.clone()));
return;
}
compile_into(expr, ctx, steps);
steps.push(Step::Cast {
target: target.clone(),
});
}
Expr::Case {
operand,
branches,
else_branch,
} => {
let all_ok = operand.as_deref().is_none_or(fully_compilable)
&& branches
.iter()
.all(|(w, t)| fully_compilable(w) && fully_compilable(t))
&& else_branch.as_deref().is_none_or(fully_compilable);
if !all_ok {
steps.push(Step::Subtree(e.clone()));
return;
}
let op_c = operand.as_deref().map(|o| compile_expr(o, ctx));
let branches_c: alloc::vec::Vec<(CompiledExpr, CompiledExpr)> = branches
.iter()
.map(|(w, t)| (compile_expr(w, ctx), compile_expr(t, ctx)))
.collect();
let else_c = else_branch.as_deref().map(|el| compile_expr(el, ctx));
steps.push(Step::Case {
operand: op_c,
branches: branches_c,
else_branch: else_c,
});
let branch_types: Vec<spg_storage::DataType> = branches
.iter()
.map(|(_, t)| t)
.chain(else_branch.iter().map(|b| b.as_ref()))
.filter_map(|e| crate::describe::describe_expr(e, ctx.columns).map(|s| s.ty))
.collect();
if let Some(common) = crate::describe::common_type(&branch_types) {
steps.push(Step::CoerceCommon(common));
}
}
other => steps.push(Step::Subtree(other.clone())),
}
}
fn like_substring_shape(pat: &[char]) -> Option<(usize, alloc::string::String, usize)> {
let mut lo = 0;
while lo < pat.len() && pat[lo] == '%' {
lo += 1;
}
if lo == 0 {
return None; }
let mut hi = pat.len();
while hi > lo && pat[hi - 1] == '%' {
hi -= 1;
}
if hi == pat.len() {
return None; }
let inner = &pat[lo..hi];
let mut i = 0;
while i < inner.len() && inner[i] == '_' {
i += 1;
}
let mut j = inner.len();
while j > i && inner[j - 1] == '_' {
j -= 1;
}
let lit = &inner[i..j];
if lit.is_empty() || lit.iter().any(|&c| c == '%' || c == '_' || c == '\\') {
return None;
}
Some((i, lit.iter().collect(), inner.len() - j))
}
fn like_find_from(hay: &str, needle: &str, start: usize) -> Option<usize> {
if needle.is_empty() {
return Some(start);
}
if !needle.is_ascii() {
return hay[start..].find(needle).map(|rel| start + rel);
}
let h = hay.as_bytes();
let n = needle.as_bytes();
if h.len() < n.len() {
return None;
}
let last = h.len() - n.len();
let mut i = start;
while i <= last {
let off = h[i..=last].iter().position(|&b| b == n[0])?;
let at = i + off;
if &h[at..at + n.len()] == n {
return Some(at);
}
i = at + 1;
}
None
}
fn like_substring_match(hay: &str, needle: &str, k: usize, m: usize) -> bool {
let mut start = 0;
while let Some(off) = like_find_from(hay, needle, start) {
let before_ok = k == 0 || hay[..off].chars().take(k).count() == k;
let after_ok = m == 0 || hay[off + needle.len()..].chars().take(m).count() == m;
if before_ok && after_ok {
return true;
}
match hay[off..].chars().next() {
Some(c) => start = off + c.len_utf8(),
None => return false,
}
}
false
}
fn literal_text_pattern(pattern: &Expr) -> Option<&str> {
match pattern {
Expr::Literal(Literal::String(s)) => Some(s.as_str()),
_ => None,
}
}
fn named_varchar_family(n: &str) -> bool {
let base = n.split('(').next().unwrap_or(n);
base.eq_ignore_ascii_case("varchar")
|| base.eq_ignore_ascii_case("text")
|| base.eq_ignore_ascii_case("char")
|| base.eq_ignore_ascii_case("bpchar")
|| base.eq_ignore_ascii_case("character")
}
fn varchar_limit(n: &str) -> Option<usize> {
let base = n.split('(').next().unwrap_or(n);
if !base.eq_ignore_ascii_case("varchar") {
return None;
}
let inner = n.split('(').nth(1)?.strip_suffix(')')?;
inner.trim().parse().ok()
}
fn cast_is_identity_for(v: &Value<'_>, target: &spg_sql::ast::CastTarget) -> bool {
match (v, target) {
(Value::Text(_), spg_sql::ast::CastTarget::Text) => true,
(Value::Text(t), spg_sql::ast::CastTarget::Named(n)) => {
n.eq_ignore_ascii_case("text")
|| n.eq_ignore_ascii_case("varchar")
|| varchar_limit(n).is_some_and(|k| t.chars().take(k + 1).count() <= k)
}
(Value::Int(_), spg_sql::ast::CastTarget::Int) => true,
(Value::BigInt(_), spg_sql::ast::CastTarget::BigInt) => true,
(Value::Float(_), spg_sql::ast::CastTarget::Float) => true,
(Value::Bool(_), spg_sql::ast::CastTarget::Bool) => true,
_ => false,
}
}
pub(crate) fn fully_compilable(e: &Expr) -> bool {
match e {
Expr::Literal(_) | Expr::Column(_) => true,
Expr::Binary { lhs, rhs, .. } => fully_compilable(lhs) && fully_compilable(rhs),
Expr::Unary { expr, .. } | Expr::IsNull { expr, .. } => fully_compilable(expr),
Expr::InList { expr, list, .. } => {
fully_compilable(expr) && crate::build_in_list_set(list).is_some()
}
Expr::Like { expr, pattern, .. } => {
fully_compilable(expr) && literal_text_pattern(pattern).is_some()
}
Expr::FunctionCall { name, args }
if name.eq_ignore_ascii_case("regexp_like")
&& matches!(args.len(), 2 | 3)
&& regex_literal_parts(args.as_slice()).is_some() =>
{
fully_compilable(&args[0])
}
Expr::FunctionCall { name, args } => {
is_pure_scalar_function(name) && args.iter().all(fully_compilable)
}
Expr::AnyAll { expr, array, .. } if constant_expr(array) => fully_compilable(expr),
Expr::Extract { source, .. } => fully_compilable(source),
Expr::Cast { expr, target } => {
let target_ok = match target {
spg_sql::ast::CastTarget::RegClass => false,
spg_sql::ast::CastTarget::Named(n) => {
named_varchar_family(n) || super::cast::plain_named_target(n).is_some()
}
_ => true,
};
target_ok && fully_compilable(expr)
}
Expr::Case {
operand,
branches,
else_branch,
} => {
operand.as_deref().is_none_or(fully_compilable)
&& branches
.iter()
.all(|(w, t)| fully_compilable(w) && fully_compilable(t))
&& else_branch.as_deref().is_none_or(fully_compilable)
}
_ => false,
}
}
fn is_session_deterministic_function(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"date_trunc" | "date_part" | "to_char" | "age" | "format"
)
}
fn is_pure_scalar_function(name: &str) -> bool {
is_session_deterministic_function(name)
|| matches!(
name.to_ascii_lowercase().as_str(),
"length"
| "char_length"
| "character_length"
| "octet_length"
| "upper"
| "lower"
| "trim"
| "ltrim"
| "rtrim"
| "btrim"
| "left"
| "right"
| "substring"
| "substr"
| "replace"
| "position"
| "strpos"
| "concat"
| "concat_ws"
| "reverse"
| "repeat"
| "lpad"
| "rpad"
| "split_part"
| "md5"
| "sha224"
| "sha256"
| "sha384"
| "sha512"
| "to_json"
| "to_jsonb"
| "jsonb_build_object"
| "json_build_object"
| "jsonb_build_array"
| "json_build_array"
| "coalesce"
| "nullif"
| "greatest"
| "least"
| "ifnull"
| "isnull"
| "nvl"
| "abs"
| "ceil"
| "ceiling"
| "floor"
| "round"
| "trunc"
| "sqrt"
| "power"
| "pow"
| "mod"
| "sign"
| "log"
| "log10"
| "exp"
| "ln"
| "cast"
)
}
pub(crate) fn compile_expr(e: &Expr, ctx: &EvalContext<'_>) -> CompiledExpr {
let mut steps = Vec::new();
compile_into(e, ctx, &mut steps);
let mut c = CompiledExpr {
steps,
pred_shape: PredShape::Other,
};
c.pred_shape = if c.as_column_cmp_literal().is_some() {
PredShape::ColumnCmpLit
} else if c.as_column_in_set().is_some() {
PredShape::ColumnInSet
} else if c.as_column_like().is_some() {
PredShape::ColumnLike
} else {
PredShape::Other
};
c
}
pub(crate) fn eval_compiled(
c: &CompiledExpr,
row: &Row<'static>,
ctx: &EvalContext<'_>,
stack: &mut Vec<Value<'static>>,
) -> Result<Value<'static>, EvalError> {
let rowref = crate::join::RowRef::Owned(row);
let mut local_stack: Vec<Value<'_>> = core::mem::take(stack);
let result = eval_compiled_ref(c, rowref, ctx, &mut local_stack);
let owned = result.map(Value::into_owned);
*stack = recycle_stack(local_stack);
owned
}
pub(crate) fn eval_compiled_pred(
c: &CompiledExpr,
row: &Row<'static>,
ctx: &EvalContext<'_>,
stack: &mut Vec<Value<'static>>,
mysql: bool,
) -> Result<bool, EvalError> {
match c.pred_shape {
PredShape::ColumnCmpLit => {
if let Some((pos, op, lit)) = c.as_column_cmp_literal() {
crate::bump_counter!(STEP_VM_FASTPRED_FIRE);
let cell = row.values.get(pos).unwrap_or(&Value::Null);
if let Some(res) = super::apply_binary_by_ref(op, cell, lit)? {
return crate::eval::predicate_is_true(&res, "WHERE", mysql);
}
}
}
PredShape::ColumnInSet => {
if let Some((pos, set, has_null, negated)) = c.as_column_in_set() {
let cell = row.values.get(pos).unwrap_or(&Value::Null);
if let Some(v) = in_set_verdict(cell, set, has_null, negated) {
crate::bump_counter!(STEP_VM_FASTPRED_FIRE);
return crate::eval::predicate_is_true(&v, "WHERE", mysql);
}
}
}
PredShape::ColumnLike => {
if let Some((pos, step)) = c.as_column_like() {
let cell = row.values.get(pos).unwrap_or(&Value::Null);
if let Some(v) = like_verdict(cell, step) {
crate::bump_counter!(STEP_VM_FASTPRED_FIRE);
return crate::eval::predicate_is_true(&v?, "WHERE", mysql);
}
}
}
PredShape::Other => {}
}
let rowref = crate::join::RowRef::Owned(row);
let mut local_stack: Vec<Value<'_>> = core::mem::take(stack);
let verdict = eval_compiled_ref(c, rowref, ctx, &mut local_stack)
.and_then(|v| crate::eval::predicate_is_true(&v, "WHERE", mysql));
*stack = recycle_stack(local_stack);
verdict
}
#[allow(clippy::inline_always)] #[inline(always)]
fn in_set_verdict(
needle: &Value<'_>,
set: &crate::memoize::InListSet,
has_null: bool,
negated: bool,
) -> Option<Value<'static>> {
let contained = match (needle, set) {
(Value::Null, _) => return Some(Value::Null),
(Value::SmallInt(n), crate::memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
(Value::Int(n), crate::memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
(Value::BigInt(n), crate::memoize::InListSet::Int(s)) => s.contains(n),
(Value::Text(t), crate::memoize::InListSet::Text(s)) => s.contains(t.as_ref()),
_ => return None,
};
let inner = if contained {
Value::Bool(true)
} else if has_null {
Value::Null
} else {
Value::Bool(false)
};
Some(match (negated, inner) {
(true, Value::Bool(b)) => Value::Bool(!b),
(_, v) => v,
})
}
fn value_array_in_list_set(arr: &Value<'_>) -> Option<crate::memoize::InListSetEntry> {
let len = crate::eval::values::array_len(arr)?;
if len == 0 {
return None;
}
let mut ints: hashbrown::HashSet<i64> = hashbrown::HashSet::with_capacity(len);
let mut texts: hashbrown::HashSet<alloc::string::String> =
hashbrown::HashSet::with_capacity(len);
let mut has_null = false;
for i in 0..len {
match crate::eval::values::array_element_at(arr, i) {
None | Some(Value::Null) => has_null = true,
Some(Value::SmallInt(n)) => {
ints.insert(i64::from(n));
}
Some(Value::Int(n)) => {
ints.insert(i64::from(n));
}
Some(Value::BigInt(n)) => {
ints.insert(n);
}
Some(Value::Text(s) | Value::BpChar(s)) => {
texts.insert(s.into_owned());
}
_ => return None,
}
if !ints.is_empty() && !texts.is_empty() {
return None;
}
}
let set = if !ints.is_empty() {
crate::memoize::InListSet::Int(ints)
} else if !texts.is_empty() {
crate::memoize::InListSet::Text(texts)
} else {
return None;
};
Some(crate::memoize::InListSetEntry { set, has_null })
}
fn array_literal_items(e: &Expr) -> Option<&[Expr]> {
match e {
Expr::Array(items) if items.iter().all(constant_expr) => Some(items.as_slice()),
_ => None,
}
}
pub(crate) fn constant_projection_value(e: &Expr, ctx: &EvalContext<'_>) -> Option<Value<'static>> {
if matches!(e, Expr::Literal(_)) || !constant_expr(e) {
return None;
}
eval_expr(e, &Row::new(alloc::vec::Vec::new()), ctx).ok()
}
fn constant_expr(e: &Expr) -> bool {
match e {
Expr::Literal(_) => true,
Expr::Array(items) => items.iter().all(constant_expr),
Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => constant_expr(expr),
Expr::Binary { lhs, rhs, .. } => constant_expr(lhs) && constant_expr(rhs),
_ => false,
}
}
fn source_of_extract(node: &Expr) -> &Expr {
match node {
Expr::Extract { source, .. } => source,
other => other,
}
}
fn regex_literal_parts(args: &[Expr]) -> Option<(&str, bool)> {
let Expr::Literal(spg_sql::ast::Literal::String(pat)) = &args[1] else {
return None;
};
let ci = match args.get(2) {
None => false,
Some(Expr::Literal(spg_sql::ast::Literal::String(f))) => f.contains('i'),
Some(_) => return None,
};
Some((pat.as_str(), ci))
}
fn regex_verdict(
cell: &Value<'_>,
re: &crate::eval::CompiledRe,
) -> Option<Result<Value<'static>, EvalError>> {
let text = match cell {
Value::Null => return Some(Ok(Value::Null)),
Value::Text(t) | Value::BpChar(t) => t.as_ref(),
_ => return None,
};
Some(crate::eval::compiled_is_match(re, text).map(Value::Bool))
}
#[allow(clippy::inline_always)] #[inline(always)]
fn like_verdict(cell: &Value<'_>, step: &Step) -> Option<Result<Value<'static>, EvalError>> {
let (text, negated) = match (cell, step) {
(Value::Null, _) => return Some(Ok(Value::Null)),
(
Value::Text(t) | Value::BpChar(t),
Step::Like { negated, .. } | Step::LikeSubstring { negated, .. },
) => (t.as_ref(), *negated),
_ => return None,
};
let matched = match step {
Step::Like {
pattern,
case_insensitive,
..
} => {
let r = if *case_insensitive {
like_match_str(&text.to_lowercase(), pattern, 0)
} else {
like_match_str(text, pattern, 0)
};
match r {
Ok(m) => m,
Err(e) => return Some(Err(e)),
}
}
Step::LikeSubstring {
needle,
k_before,
m_after,
case_insensitive,
..
} => {
if *case_insensitive {
like_substring_match(&text.to_lowercase(), needle, *k_before, *m_after)
} else {
like_substring_match(text, needle, *k_before, *m_after)
}
}
_ => return None,
};
Some(Ok(Value::Bool(if negated { !matched } else { matched })))
}
#[allow(unsafe_code)] fn recycle_stack(mut v: Vec<Value<'_>>) -> Vec<Value<'static>> {
crate::bump_counter!(STEP_VM_STACK_LEFTOVER, v.len() as u64);
#[cfg(feature = "perf-counters")]
{
let heap = v
.iter()
.filter(|x| {
matches!(
x,
Value::Text(_) | Value::Bytes(_) | Value::Json(_) | Value::Vector(_)
)
})
.count();
crate::bump_counter!(STEP_VM_STACK_LEFTOVER_HEAP, heap as u64);
}
v.clear();
debug_assert!(v.is_empty());
unsafe { core::mem::transmute::<Vec<Value<'_>>, Vec<Value<'static>>>(v) }
}
pub(crate) fn eval_compiled_ref<'row, 'val>(
c: &'val CompiledExpr,
row: crate::join::RowRef<'row>,
ctx: &EvalContext<'_>,
stack: &mut Vec<Value<'val>>,
) -> Result<Value<'val>, EvalError>
where
'row: 'val,
{
stack.clear();
run_compiled_steps(&c.steps, row, ctx, stack)?;
Ok(stack.pop().unwrap_or(Value::Null))
}
fn eval_compiled_ref_into<'row, 'val>(
c: &'val CompiledExpr,
row: crate::join::RowRef<'row>,
ctx: &EvalContext<'_>,
stack: &mut Vec<Value<'val>>,
_mark: usize,
) -> Result<(), EvalError>
where
'row: 'val,
{
run_compiled_steps(&c.steps, row, ctx, stack)
}
#[inline]
fn run_compiled_steps<'row, 'val>(
steps: &'val [Step],
row: crate::join::RowRef<'row>,
ctx: &EvalContext<'_>,
stack: &mut Vec<Value<'val>>,
) -> Result<(), EvalError>
where
'row: 'val,
{
crate::bump_counter!(STEP_VM_CALL_COUNT);
crate::bump_counter!(STEP_VM_STEPS_TOTAL, steps.len() as u64);
for step in steps {
match step {
Step::Column(pos) => {
crate::bump_counter!(STEP_VM_COLUMN_FIRE);
let cell: Value<'val> = match row.get(*pos) {
Some(spg_storage::Value::Text(s)) => {
spg_storage::Value::Text(alloc::borrow::Cow::Borrowed(s.as_ref()))
}
Some(spg_storage::Value::Bytes(b)) => {
spg_storage::Value::Bytes(alloc::borrow::Cow::Borrowed(b.as_ref()))
}
Some(spg_storage::Value::Json(s)) => {
spg_storage::Value::Json(alloc::borrow::Cow::Borrowed(s.as_ref()))
}
Some(spg_storage::Value::Vector(v)) => {
spg_storage::Value::Vector(alloc::borrow::Cow::Borrowed(v.as_ref()))
}
Some(v) => v.clone(),
None => Value::Null,
};
if matches!(
&cell,
spg_storage::Value::Text(_)
| spg_storage::Value::Bytes(_)
| spg_storage::Value::Json(_)
| spg_storage::Value::Vector(_)
) {
crate::bump_counter!(STEP_VM_COLUMN_HEAP_ALLOC);
}
stack.push(cell);
}
Step::Lit(v) => {
crate::bump_counter!(STEP_VM_LIT_FIRE);
if matches!(
v,
spg_storage::Value::Text(_)
| spg_storage::Value::Bytes(_)
| spg_storage::Value::Json(_)
| spg_storage::Value::Vector(_)
) {
crate::bump_counter!(STEP_VM_LIT_HEAP_ALLOC);
}
let pushed: Value<'val> = match v {
spg_storage::Value::Text(s) => {
spg_storage::Value::Text(alloc::borrow::Cow::Borrowed(s.as_ref()))
}
spg_storage::Value::Bytes(b) => {
spg_storage::Value::Bytes(alloc::borrow::Cow::Borrowed(b.as_ref()))
}
spg_storage::Value::Json(s) => {
spg_storage::Value::Json(alloc::borrow::Cow::Borrowed(s.as_ref()))
}
spg_storage::Value::Vector(vec) => {
spg_storage::Value::Vector(alloc::borrow::Cow::Borrowed(vec.as_ref()))
}
other => other.clone(),
};
stack.push(pushed);
}
Step::Binary(op) => {
crate::bump_counter!(STEP_VM_BINARY_FIRE);
if ctx.mysql_dialect && matches!(op, BinOp::And | BinOp::Or) {
let r = super::as_mysql_truth(stack.pop().unwrap_or(Value::Null).into_owned())?;
let l = super::as_mysql_truth(stack.pop().unwrap_or(Value::Null).into_owned())?;
stack.push(apply_binary(*op, l, r)?);
continue;
}
let n = stack.len();
if n >= 2 {
if let Some(result) =
super::apply_binary_by_ref(*op, &stack[n - 2], &stack[n - 1])?
{
stack.truncate(n - 2);
stack.push(result);
continue;
}
}
let r = stack.pop().unwrap_or(Value::Null).into_owned();
let l = stack.pop().unwrap_or(Value::Null).into_owned();
stack.push(apply_binary(*op, l, r)?);
}
Step::Connective { op, rhs } => {
crate::bump_counter!(STEP_VM_BINARY_FIRE);
let l = stack.pop().unwrap_or(Value::Null).into_owned();
match (op, &l) {
(BinOp::And, Value::Bool(false)) => {
stack.push(Value::Bool(false));
continue;
}
(BinOp::Or, Value::Bool(true)) => {
stack.push(Value::Bool(true));
continue;
}
_ => {}
}
run_compiled_steps(rhs, row, ctx, stack)?;
let r = stack.pop().unwrap_or(Value::Null).into_owned();
stack.push(apply_binary(*op, l, r)?);
}
Step::BinaryCi(op) => {
let fold = |v: Value<'static>| match v {
Value::Text(s) if ctx.mysql_dialect => {
Value::text(spg_storage::mysql_compare_fold(&s))
}
Value::Text(s) => Value::text(s.to_ascii_lowercase()),
other => other,
};
let r = fold(stack.pop().unwrap_or(Value::Null).into_owned());
let l = fold(stack.pop().unwrap_or(Value::Null).into_owned());
stack.push(apply_binary(*op, l, r)?);
}
Step::Unary(op) => {
let v = stack.pop().unwrap_or(Value::Null).into_owned();
if ctx.mysql_dialect
&& matches!(op, UnOp::Not)
&& !matches!(v, Value::Bool(_) | Value::Null)
{
stack.push(Value::Bool(!super::predicate_is_true(&v, "NOT", true)?));
continue;
}
stack.push(apply_unary(*op, v)?);
}
Step::IsNull { negated } => {
let v = stack.pop().unwrap_or(Value::Null);
let is_null = matches!(v, Value::Null);
stack.push(Value::Bool(if *negated { !is_null } else { is_null }));
}
Step::AnyTextMatch { negated } => {
let v = stack.pop().unwrap_or(Value::Null);
stack.push(match v {
Value::Null => Value::Null,
_ => Value::Bool(!*negated),
});
}
Step::InSet {
set,
has_null,
negated,
fallback,
} => {
let needle = stack.pop().unwrap_or(Value::Null);
match in_set_verdict(&needle, set, *has_null, *negated) {
Some(v) => stack.push(v),
None => stack.push(eval_expr(fallback, &row.as_row(), ctx)?),
}
}
Step::AnyAll { op, is_any, arr } => {
let lhs = stack.pop().unwrap_or(Value::Null).into_owned();
stack.push(crate::eval::any_all_over(lhs, arr.clone(), op, *is_any)?);
}
Step::Extract { field, fallback } => {
let v = stack.pop().unwrap_or(Value::Null).into_owned();
stack.push(crate::eval::extract_from_value(
field,
v,
source_of_extract(fallback),
ctx,
)?);
}
Step::Regex { re, fallback } => {
let v = stack.pop().unwrap_or(Value::Null);
match regex_verdict(&v, re) {
Some(r) => stack.push(r?),
None => stack.push(eval_expr(fallback, &row.as_row(), ctx)?),
}
}
step @ (Step::Like { .. } | Step::LikeSubstring { .. }) => {
let v = stack.pop().unwrap_or(Value::Null);
match like_verdict(&v, step) {
Some(r) => stack.push(r?),
None => {
return Err(EvalError::TypeMismatch {
detail: format!(
"LIKE requires text operands, got {}",
crate::conversions::pg_type_name_for_error_opt(v.data_type())
),
});
}
}
}
Step::ColumnLength { pos } => {
let v = row.get(*pos).unwrap_or(&Value::Null);
let pushed = match v {
Value::Null => Value::Null,
Value::Text(s) => {
let n = if s.is_ascii() {
i32::try_from(s.len()).unwrap_or(i32::MAX)
} else {
i32::try_from(s.chars().count()).unwrap_or(i32::MAX)
};
Value::Int(n)
}
Value::BpChar(s) => {
let t = s.trim_end_matches(' ');
let n = if t.is_ascii() {
i32::try_from(t.len()).unwrap_or(i32::MAX)
} else {
i32::try_from(t.chars().count()).unwrap_or(i32::MAX)
};
Value::Int(n)
}
Value::Bytes(b) => Value::Int(i32::try_from(b.len()).unwrap_or(i32::MAX)),
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"length() needs text or bytea, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
stack.push(pushed);
}
Step::ColumnOctetLength { pos } => {
let v = row.get(*pos).unwrap_or(&Value::Null);
let pushed = match v {
Value::Null => Value::Null,
Value::Text(s) | Value::BpChar(s) => {
Value::Int(i32::try_from(s.len()).unwrap_or(i32::MAX))
}
Value::Bytes(b) => Value::Int(i32::try_from(b.len()).unwrap_or(i32::MAX)),
other => {
return Err(EvalError::TypeMismatch {
detail: format!(
"octet_length() needs text or bytea, got {}",
crate::conversions::pg_type_name_for_error_opt(other.data_type())
),
});
}
};
stack.push(pushed);
}
Step::Function { name_lower, n_args } => {
crate::bump_counter!(STEP_VM_FUNCTION_FIRE);
let start = stack.len().saturating_sub(*n_args);
let result =
super::functions::apply_function_lower(name_lower, &stack[start..], ctx)?;
stack.truncate(start);
stack.push(result);
}
Step::Coalesce { n_args } => {
let start = stack.len().saturating_sub(*n_args);
let mut mixed = false;
let mut seen: Option<spg_storage::DataType> = None;
for v in &stack[start..] {
if let Some(t) = v.data_type() {
match seen {
None => seen = Some(t),
Some(prev) if prev != t => {
mixed = true;
break;
}
Some(_) => {}
}
}
}
if mixed {
let result =
super::functions::apply_function_lower("coalesce", &stack[start..], ctx)?;
stack.truncate(start);
stack.push(result);
} else {
let chosen = stack[start..]
.iter()
.position(|v| !matches!(v, Value::Null));
match chosen {
Some(k) => {
let v = stack.swap_remove(start + k);
stack.truncate(start);
stack.push(v);
}
None => {
stack.truncate(start);
stack.push(Value::Null);
}
}
}
}
Step::Extremum { n_args, max } => {
let start = stack.len().saturating_sub(*n_args);
let mut uniform: Option<spg_storage::DataType> = None;
let mut any_null = false;
let mut fall_back = false;
for v in &stack[start..] {
if matches!(v, Value::Null) {
any_null = true;
continue;
}
if matches!(v, Value::Xid(_)) {
fall_back = true;
break;
}
match (v.data_type(), uniform) {
(Some(t), None) => uniform = Some(t),
(Some(t), Some(prev)) if t != prev => {
fall_back = true;
break;
}
(Some(_), Some(_)) => {}
(None, _) => {
fall_back = true;
break;
}
}
}
if fall_back || (ctx.mysql_dialect && any_null) {
let name = if *max { "greatest" } else { "least" };
let result =
super::functions::apply_function_lower(name, &stack[start..], ctx)?;
stack.truncate(start);
stack.push(result);
} else {
let mut best: Option<usize> = None;
for k in start..stack.len() {
if matches!(&stack[k], Value::Null) {
continue;
}
match best {
None => best = Some(k),
Some(b) => {
let ord = super::values::value_cmp_for_min_max(
&stack[b],
&stack[k],
ctx.mysql_dialect,
);
let take = if *max {
ord == core::cmp::Ordering::Less
} else {
ord == core::cmp::Ordering::Greater
};
if take {
best = Some(k);
}
}
}
}
match best {
Some(k) => {
let v = stack.swap_remove(k);
stack.truncate(start);
stack.push(v);
}
None => {
stack.truncate(start);
stack.push(Value::Null);
}
}
}
}
Step::NullIf => {
let n = stack.len();
let verdict = match (&stack[n - 2], &stack[n - 1]) {
(Value::Null, _) => Some(true),
(_, Value::Null) => Some(false),
(a, b) => {
super::binop::require_comparable(spg_sql::ast::BinOp::Eq, a, b)?;
match super::apply_binary_by_ref(spg_sql::ast::BinOp::Eq, a, b)? {
Some(Value::Bool(eq)) => Some(eq),
_ => None,
}
}
};
match verdict {
Some(true) => {
stack.truncate(n - 2);
stack.push(Value::Null);
}
Some(false) => {
let a = stack.swap_remove(n - 2);
stack.truncate(n - 2);
stack.push(a);
}
None => {
let result =
super::functions::apply_function_lower("nullif", &stack[n - 2..], ctx)?;
stack.truncate(n - 2);
stack.push(result);
}
}
}
Step::Cast { target } => {
crate::bump_counter!(STEP_VM_CAST_FIRE);
let v = stack.pop().unwrap_or(Value::Null);
if cast_is_identity_for(&v, target) {
stack.push(v);
} else {
stack.push(super::cast::cast_value_ref_in(
v.into_owned(),
target,
ctx.mysql_dialect,
)?);
}
}
Step::CastPlain { dt, name } => {
let v = stack.pop().unwrap_or(Value::Null);
let identity = matches!(
(&v, dt),
(Value::Null, _)
| (Value::Int(_), spg_storage::DataType::Int)
| (Value::BigInt(_), spg_storage::DataType::BigInt)
| (Value::SmallInt(_), spg_storage::DataType::SmallInt)
| (Value::Real(_), spg_storage::DataType::Real)
| (Value::Float(_), spg_storage::DataType::Float)
| (Value::Bool(_), spg_storage::DataType::Bool)
| (Value::Date(_), spg_storage::DataType::Date)
| (Value::Uuid(_), spg_storage::DataType::Uuid)
);
if identity {
stack.push(v);
} else {
stack.push(super::cast::finish_named_cast_plain(
v.into_owned(),
*dt,
name,
ctx.mysql_dialect,
)?);
}
}
Step::Case {
operand,
branches,
else_branch,
} => {
crate::bump_counter!(STEP_VM_CASE_FIRE);
let mark = stack.len();
let operand_value: Option<Value<'val>> = if let Some(op) = operand {
eval_compiled_ref_into(op, row, ctx, stack, mark)?;
Some(stack.pop().unwrap_or(Value::Null))
} else {
None
};
stack.truncate(mark);
let mut matched_value: Option<Value<'val>> = None;
for (when_c, then_c) in branches {
eval_compiled_ref_into(when_c, row, ctx, stack, mark)?;
let when_v = stack.pop().unwrap_or(Value::Null);
stack.truncate(mark);
let matched = match &operand_value {
None => matches!(when_v, Value::Bool(true)),
Some(op_v) => {
let eq_result =
match super::apply_binary_by_ref(BinOp::Eq, op_v, &when_v)? {
Some(v) => v,
None => apply_binary(
BinOp::Eq,
op_v.clone().into_owned(),
when_v.clone().into_owned(),
)?,
};
matches!(eq_result, Value::Bool(true))
}
};
if matched {
eval_compiled_ref_into(then_c, row, ctx, stack, mark)?;
matched_value = Some(stack.pop().unwrap_or(Value::Null));
stack.truncate(mark);
break;
}
}
let v: Value<'val> = match matched_value {
Some(v) => v,
None => match else_branch {
Some(el) => {
eval_compiled_ref_into(el, row, ctx, stack, mark)?;
let v = stack.pop().unwrap_or(Value::Null);
stack.truncate(mark);
v
}
None => Value::Null,
},
};
stack.push(v);
}
Step::CoerceCommon(target) => {
let v = stack.pop().unwrap_or(Value::Null).into_owned();
stack.push(super::widen_value_to(v, *target));
}
Step::Subtree(e) => stack.push(eval_expr(e, &row.as_row(), ctx)?),
}
}
Ok(())
}
pub static STEP_VM_CALL_COUNT: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_STEPS_TOTAL: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_COLUMN_FIRE: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_LIT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_BINARY_FIRE: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_FUNCTION_FIRE: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_CAST_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_CASE_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_COLUMN_HEAP_ALLOC: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_LIT_HEAP_ALLOC: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_FASTPRED_FIRE: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_STACK_LEFTOVER: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static STEP_VM_STACK_LEFTOVER_HEAP: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
#[cfg(test)]
mod like_substring_tests {
use super::{like_substring_match, like_substring_shape};
fn shape(p: &str) -> Option<(usize, alloc::string::String, usize)> {
let chars: alloc::vec::Vec<char> = p.chars().collect();
like_substring_shape(&chars)
}
#[test]
fn shape_recognition() {
assert_eq!(shape("%_05%"), Some((1, "05".into(), 0)));
assert_eq!(shape("%abc%"), Some((0, "abc".into(), 0)));
assert_eq!(shape("%ab_%"), Some((0, "ab".into(), 1)));
assert_eq!(shape("%%x%%"), Some((0, "x".into(), 0)));
assert_eq!(shape("%__a__%"), Some((2, "a".into(), 2)));
assert_eq!(shape("ab%"), None);
assert_eq!(shape("%ab"), None);
assert_eq!(shape("%a%b%"), None);
assert_eq!(shape("%___%"), None);
assert_eq!(shape("%a\\%b%"), None);
assert_eq!(shape("%"), None);
}
#[test]
fn matcher_semantics() {
assert!(like_substring_match("x05", "05", 1, 0));
assert!(!like_substring_match("05", "05", 1, 0));
assert!(like_substring_match("ab05cd", "05", 1, 0));
assert!(like_substring_match("05x05", "05", 1, 0));
assert!(like_substring_match("abz", "ab", 0, 1));
assert!(!like_substring_match("ab", "ab", 0, 1));
assert!(like_substring_match("hello", "ell", 0, 0));
assert!(!like_substring_match("hello", "xyz", 0, 0));
assert!(like_substring_match("é05", "05", 1, 0));
assert!(!like_substring_match("é5", "05", 1, 0));
}
}