drop in replacement for predicate that uses ast::Expr under the hood

This commit is contained in:
pedrocarlo 2025-06-03 01:24:36 -03:00
parent f230703279
commit 3e369b9dde
6 changed files with 291 additions and 9 deletions

View file

@ -1,5 +0,0 @@
use limbo_sqlite3_parser::ast;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct Predicate(ast::Expr);

View file

@ -5,6 +5,7 @@ pub(crate) use create_index::CreateIndex;
pub(crate) use delete::Delete;
pub(crate) use drop::Drop;
pub(crate) use insert::Insert;
use limbo_sqlite3_parser::to_sql_string::ToSqlContext;
pub(crate) use select::Select;
use serde::{Deserialize, Serialize};
use update::Update;
@ -15,8 +16,8 @@ pub mod create;
pub mod create_index;
pub mod delete;
pub mod drop;
pub mod expr;
pub mod insert;
pub mod predicate;
pub mod select;
pub mod update;
@ -86,3 +87,20 @@ impl Display for Query {
}
}
}
/// Used to print sql strings that already have all the context it needs
struct EmptyContext;
impl ToSqlContext for EmptyContext {
fn get_column_name(
&self,
_table_id: limbo_sqlite3_parser::ast::TableInternalId,
_col_idx: usize,
) -> &str {
unreachable!()
}
fn get_table_name(&self, _id: limbo_sqlite3_parser::ast::TableInternalId) -> &str {
unreachable!()
}
}

View file

@ -0,0 +1,92 @@
use std::fmt::Display;
use limbo_sqlite3_parser::{ast, to_sql_string::ToSqlString};
use serde::{Deserialize, Serialize};
use crate::model::{
query::EmptyContext,
table::{Table, Value},
};
macro_rules! assert_implemented_predicate_expr {
($val:expr) => {
assert!(matches!(
$val,
ast::Expr::DoublyQualified(..) | ast::Expr::Qualified(..) | ast::Expr::Literal(..)
))
};
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Predicate(ast::Expr);
impl Predicate {
pub(crate) fn true_() -> Self {
Self(ast::Expr::Literal(ast::Literal::Numeric("1".to_string())))
}
pub(crate) fn false_() -> Self {
Self(ast::Expr::Literal(ast::Literal::Numeric("0".to_string())))
}
pub(crate) fn test(&self, row: &[Value], table: &Table) -> bool {
match &self.0 {
ast::Expr::Binary(lhs, operator, rhs) => {
let lhs = expr_to_value(lhs, row, table);
let rhs = expr_to_value(rhs, row, table);
match (lhs, rhs) {
(Some(lhs), Some(rhs)) => lhs.binary_compare(&rhs, *operator),
_ => false,
}
}
ast::Expr::Like {
lhs,
not,
op,
rhs,
escape: _, // TODO: support escape
} => {
let lhs = expr_to_value(lhs, row, table);
let rhs = expr_to_value(rhs, row, table);
let res = match (lhs, rhs) {
(Some(lhs), Some(rhs)) => lhs.like_compare(&rhs, *op),
_ => false,
};
if *not {
!res
} else {
res
}
}
ast::Expr::Literal(literal) => Value::from(literal).into_bool(),
ast::Expr::Unary(unary_operator, expr) => todo!(),
expr => unimplemented!("{:?}", expr),
}
}
}
// TODO: In the future pass a Vec<Table> to support resolving a value from another table
// This function attempts to convert an simpler easily computable expression into values
// TODO: In the future, we can try to expand this computation if we want to support harder properties that require us
// to already know more values before hand
fn expr_to_value(expr: &ast::Expr, row: &[Value], table: &Table) -> Option<Value> {
assert_implemented_predicate_expr!(expr);
match expr {
ast::Expr::DoublyQualified(_, _, col_name) | ast::Expr::Qualified(_, col_name) => table
.columns
.iter()
.zip(row.iter())
.find(|(column, _)| column.name == col_name.0)
.map(|(_, value)| value)
.cloned(),
ast::Expr::Literal(literal) => Some(literal.into()),
// TODO: add binary and unary
_ => unreachable!("{:?}", expr),
}
}
impl Display for Predicate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.to_sql_string(&EmptyContext))
}
}

View file

@ -1,5 +1,8 @@
use std::{fmt::Display, ops::Deref};
use limbo_core::numeric::{nonnan::NonNan, Numeric};
use limbo_sqlite3_parser::ast;
use regex::{Regex, RegexBuilder};
use serde::{Deserialize, Serialize};
pub(crate) struct Name(pub(crate) String);
@ -111,3 +114,177 @@ impl Display for Value {
}
}
}
impl Value {
pub const FALSE: Self = Value::Integer(0);
pub const TRUE: Self = Value::Integer(1);
pub fn into_bool(&self) -> bool {
match Numeric::from(self).try_into_bool() {
None => false, // Value::Null
Some(v) => v,
}
}
// TODO: support more predicates
pub fn binary_compare(&self, other: &Self, operator: ast::Operator) -> bool {
match operator {
ast::Operator::Add => todo!(),
ast::Operator::And => self.into_bool() && other.into_bool(),
ast::Operator::ArrowRight => todo!(),
ast::Operator::ArrowRightShift => todo!(),
ast::Operator::BitwiseAnd => todo!(),
ast::Operator::BitwiseOr => todo!(),
ast::Operator::BitwiseNot => todo!(),
ast::Operator::Concat => todo!(),
ast::Operator::Equals => self == other,
ast::Operator::Divide => todo!(),
ast::Operator::Greater => self > other,
ast::Operator::GreaterEquals => self >= other,
ast::Operator::Is => todo!(),
ast::Operator::IsNot => todo!(),
ast::Operator::LeftShift => todo!(),
ast::Operator::Less => self < other,
ast::Operator::LessEquals => self <= other,
ast::Operator::Modulus => todo!(),
ast::Operator::Multiply => todo!(),
ast::Operator::NotEquals => self != other,
ast::Operator::Or => self.into_bool() || other.into_bool(),
ast::Operator::RightShift => todo!(),
ast::Operator::Subtract => todo!(),
}
}
// TODO: support more operators. Copy the implementation for exec_glob
pub fn like_compare(&self, other: &Self, operator: ast::LikeOperator) -> bool {
match operator {
ast::LikeOperator::Glob => todo!(),
ast::LikeOperator::Like => {
// TODO: support ESCAPE `expr` option in AST
exec_like(self.to_string().as_str(), other.to_string().as_str())
}
ast::LikeOperator::Match => todo!(),
ast::LikeOperator::Regexp => todo!(),
}
}
}
/// This function is a duplication of the exec_like function in core/vdbe/mod.rs at commit 9b9d5f9b4c9920e066ef1237c80878f4c3968524
/// Any updates to the original function should be reflected here, otherwise the test will be incorrect.
fn construct_like_regex(pattern: &str) -> Regex {
let mut regex_pattern = String::with_capacity(pattern.len() * 2);
regex_pattern.push('^');
for c in pattern.chars() {
match c {
'\\' => regex_pattern.push_str("\\\\"),
'%' => regex_pattern.push_str(".*"),
'_' => regex_pattern.push('.'),
ch => {
if regex_syntax::is_meta_character(c) {
regex_pattern.push('\\');
}
regex_pattern.push(ch);
}
}
}
regex_pattern.push('$');
RegexBuilder::new(&regex_pattern)
.case_insensitive(true)
.dot_matches_new_line(true)
.build()
.unwrap()
}
fn exec_like(pattern: &str, text: &str) -> bool {
let re = construct_like_regex(pattern);
re.is_match(text)
}
impl From<&ast::Literal> for Value {
fn from(value: &ast::Literal) -> Self {
match value {
ast::Literal::Null => Self::Null,
ast::Literal::Numeric(number) => Numeric::from(number).into(),
ast::Literal::String(string) => Self::Text(string.clone()),
ast::Literal::Blob(blob) => Self::Blob(
blob.as_bytes()
.chunks_exact(2)
.map(|pair| {
// We assume that sqlite3-parser has already validated that
// the input is valid hex string, thus unwrap is safe.
let hex_byte = std::str::from_utf8(pair).unwrap();
u8::from_str_radix(hex_byte, 16).unwrap()
})
.collect(),
),
lit => unimplemented!("{:?}", lit),
}
}
}
impl From<ast::Literal> for Value {
fn from(value: ast::Literal) -> Self {
match value {
ast::Literal::Null => Self::Null,
ast::Literal::Numeric(number) => Numeric::from(number).into(),
ast::Literal::String(string) => Self::Text(string),
ast::Literal::Blob(blob) => Self::Blob(
blob.as_bytes()
.chunks_exact(2)
.map(|pair| {
// We assume that sqlite3-parser has already validated that
// the input is valid hex string, thus unwrap is safe.
let hex_byte = std::str::from_utf8(pair).unwrap();
u8::from_str_radix(hex_byte, 16).unwrap()
})
.collect(),
),
lit => unimplemented!("{:?}", lit),
}
}
}
impl From<Numeric> for Value {
fn from(value: Numeric) -> Self {
match value {
Numeric::Null => Value::Null,
Numeric::Integer(i) => Value::Integer(i),
Numeric::Float(f) => Value::Float(f.into()),
}
}
}
// Copied from numeric in Core
impl From<Value> for Numeric {
fn from(value: Value) -> Self {
Self::from(&value)
}
}
impl From<&Value> for Numeric {
fn from(value: &Value) -> Self {
match value {
Value::Null => Self::Null,
Value::Integer(v) => Self::Integer(*v),
Value::Float(v) => match NonNan::new(*v) {
Some(v) => Self::Float(v),
None => Self::Null,
},
Value::Text(text) => Numeric::from(text.as_str()),
Value::Blob(blob) => {
let text = String::from_utf8_lossy(blob.as_slice());
Numeric::from(&text)
}
}
}
}
impl From<bool> for Value {
fn from(value: bool) -> Self {
value.then_some(Value::TRUE).unwrap_or(Value::FALSE)
}
}