use serde_json::Value;
use super::record_id::RecordIdValue;
use super::record_ref::{record_ref, RecordRef};
use super::surreal_fn::SurrealFn;
use crate::query::expressions::Expression;
pub trait OperatorExpr {
fn to_surql(&self) -> String;
}
#[derive(Debug, Clone, PartialEq)]
pub enum Operator {
Eq(Eq),
Ne(Ne),
Gt(Gt),
Gte(Gte),
Lt(Lt),
Lte(Lte),
Contains(Contains),
ContainsNot(ContainsNot),
ContainsAll(ContainsAll),
ContainsAny(ContainsAny),
Inside(Inside),
NotInside(NotInside),
IsNull(IsNull),
IsNotNull(IsNotNull),
And(And),
Or(Or),
Not(Not),
}
impl OperatorExpr for Operator {
fn to_surql(&self) -> String {
match self {
Self::Eq(x) => x.to_surql(),
Self::Ne(x) => x.to_surql(),
Self::Gt(x) => x.to_surql(),
Self::Gte(x) => x.to_surql(),
Self::Lt(x) => x.to_surql(),
Self::Lte(x) => x.to_surql(),
Self::Contains(x) => x.to_surql(),
Self::ContainsNot(x) => x.to_surql(),
Self::ContainsAll(x) => x.to_surql(),
Self::ContainsAny(x) => x.to_surql(),
Self::Inside(x) => x.to_surql(),
Self::NotInside(x) => x.to_surql(),
Self::IsNull(x) => x.to_surql(),
Self::IsNotNull(x) => x.to_surql(),
Self::And(x) => x.to_surql(),
Self::Or(x) => x.to_surql(),
Self::Not(x) => x.to_surql(),
}
}
}
macro_rules! binary_comparison {
($(#[$meta:meta])* $name:ident, $sql:literal) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
pub struct $name {
/// Field name.
pub field: String,
pub value: Value,
}
impl $name {
pub fn new(field: impl Into<String>, value: impl Into<Value>) -> Self {
Self {
field: field.into(),
value: value.into(),
}
}
}
impl OperatorExpr for $name {
fn to_surql(&self) -> String {
format!("{} {} {}", self.field, $sql, quote_value(&self.value))
}
}
};
}
binary_comparison!(
Eq,
"="
);
binary_comparison!(
Ne,
"!="
);
binary_comparison!(
Gt,
">"
);
binary_comparison!(
Gte,
">="
);
binary_comparison!(
Lt,
"<"
);
binary_comparison!(
Lte,
"<="
);
binary_comparison!(
Contains,
"CONTAINS"
);
binary_comparison!(
ContainsNot,
"CONTAINSNOT"
);
macro_rules! array_comparison {
($(#[$meta:meta])* $name:ident, $sql:literal) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
pub struct $name {
/// Field name.
pub field: String,
pub values: Vec<Value>,
}
impl $name {
pub fn new(field: impl Into<String>, values: impl IntoIterator<Item = Value>) -> Self {
Self {
field: field.into(),
values: values.into_iter().collect(),
}
}
}
impl OperatorExpr for $name {
fn to_surql(&self) -> String {
let rendered = self
.values
.iter()
.map(quote_value)
.collect::<Vec<_>>()
.join(", ");
format!("{} {} [{}]", self.field, $sql, rendered)
}
}
};
}
array_comparison!(
ContainsAll,
"CONTAINSALL"
);
array_comparison!(
ContainsAny,
"CONTAINSANY"
);
array_comparison!(
Inside,
"INSIDE"
);
array_comparison!(
NotInside,
"NOTINSIDE"
);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IsNull {
pub field: String,
}
impl IsNull {
pub fn new(field: impl Into<String>) -> Self {
Self {
field: field.into(),
}
}
}
impl OperatorExpr for IsNull {
fn to_surql(&self) -> String {
format!("{} IS NULL", self.field)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IsNotNull {
pub field: String,
}
impl IsNotNull {
pub fn new(field: impl Into<String>) -> Self {
Self {
field: field.into(),
}
}
}
impl OperatorExpr for IsNotNull {
fn to_surql(&self) -> String {
format!("{} IS NOT NULL", self.field)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct And {
pub left: Box<Operator>,
pub right: Box<Operator>,
}
impl OperatorExpr for And {
fn to_surql(&self) -> String {
format!("({}) AND ({})", self.left.to_surql(), self.right.to_surql())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Or {
pub left: Box<Operator>,
pub right: Box<Operator>,
}
impl OperatorExpr for Or {
fn to_surql(&self) -> String {
format!("({}) OR ({})", self.left.to_surql(), self.right.to_surql())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Not {
pub operand: Box<Operator>,
}
impl OperatorExpr for Not {
fn to_surql(&self) -> String {
format!("NOT ({})", self.operand.to_surql())
}
}
pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Operator {
Operator::Eq(Eq::new(field, value))
}
pub fn ne(field: impl Into<String>, value: impl Into<Value>) -> Operator {
Operator::Ne(Ne::new(field, value))
}
pub fn gt(field: impl Into<String>, value: impl Into<Value>) -> Operator {
Operator::Gt(Gt::new(field, value))
}
pub fn gte(field: impl Into<String>, value: impl Into<Value>) -> Operator {
Operator::Gte(Gte::new(field, value))
}
pub fn lt(field: impl Into<String>, value: impl Into<Value>) -> Operator {
Operator::Lt(Lt::new(field, value))
}
pub fn lte(field: impl Into<String>, value: impl Into<Value>) -> Operator {
Operator::Lte(Lte::new(field, value))
}
pub fn contains(field: impl Into<String>, value: impl Into<Value>) -> Operator {
Operator::Contains(Contains::new(field, value))
}
pub fn contains_not(field: impl Into<String>, value: impl Into<Value>) -> Operator {
Operator::ContainsNot(ContainsNot::new(field, value))
}
pub fn contains_all(field: impl Into<String>, values: impl IntoIterator<Item = Value>) -> Operator {
Operator::ContainsAll(ContainsAll::new(field, values))
}
pub fn contains_any(field: impl Into<String>, values: impl IntoIterator<Item = Value>) -> Operator {
Operator::ContainsAny(ContainsAny::new(field, values))
}
pub fn inside(field: impl Into<String>, values: impl IntoIterator<Item = Value>) -> Operator {
Operator::Inside(Inside::new(field, values))
}
pub fn not_inside(field: impl Into<String>, values: impl IntoIterator<Item = Value>) -> Operator {
Operator::NotInside(NotInside::new(field, values))
}
pub fn is_null(field: impl Into<String>) -> Operator {
Operator::IsNull(IsNull::new(field))
}
pub fn is_not_null(field: impl Into<String>) -> Operator {
Operator::IsNotNull(IsNotNull::new(field))
}
pub fn and_(left: Operator, right: Operator) -> Operator {
Operator::And(And {
left: Box::new(left),
right: Box::new(right),
})
}
pub fn or_(left: Operator, right: Operator) -> Operator {
Operator::Or(Or {
left: Box::new(left),
right: Box::new(right),
})
}
pub fn not_(operand: Operator) -> Operator {
Operator::Not(Not {
operand: Box::new(operand),
})
}
pub fn quote_value_public(value: &Value) -> String {
quote_value(value)
}
pub(crate) fn quote_value(value: &Value) -> String {
match value {
Value::Null => "NULL".to_string(),
Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => {
let escaped = s.replace('\\', "\\\\").replace('\'', "\\'");
format!("'{escaped}'")
}
Value::Array(arr) => {
let inner = arr.iter().map(quote_value).collect::<Vec<_>>().join(", ");
format!("[{inner}]")
}
Value::Object(obj) => {
if let Some(raw) = try_wrapped_raw(obj) {
return raw;
}
let inner = obj
.iter()
.map(|(k, v)| format!("{}: {}", quote_key(k), quote_value(v)))
.collect::<Vec<_>>()
.join(", ");
format!("{{ {inner} }}")
}
}
}
fn quote_key(key: &str) -> String {
if key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
key.to_owned()
} else {
let escaped = key.replace('\\', "\\\\").replace('\'', "\\'");
format!("'{escaped}'")
}
}
pub fn type_record(table: impl Into<String>, record_id: impl Into<RecordIdValue>) -> Expression {
Expression::function(record_ref(table, record_id).to_surql())
}
pub fn type_thing(table: impl Into<String>, record_id: impl Into<RecordIdValue>) -> Expression {
let rendered = match record_id.into() {
RecordIdValue::Int(n) => format!("type::thing('{}', {n})", table.into()),
RecordIdValue::String(s) => {
let escaped = s.replace('\\', "\\\\").replace('\'', "\\'");
format!("type::thing('{}', '{escaped}')", table.into())
}
};
Expression::function(rendered)
}
fn try_wrapped_raw(obj: &serde_json::Map<String, Value>) -> Option<String> {
if let Ok(fnv) = serde_json::from_value::<SurrealFn>(Value::Object(obj.clone())) {
return Some(fnv.to_surql());
}
if let Ok(rr) = serde_json::from_value::<RecordRef>(Value::Object(obj.clone())) {
return Some(rr.to_surql());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn eq_renders() {
assert_eq!(eq("name", "Alice").to_surql(), "name = 'Alice'");
}
#[test]
fn ne_renders() {
assert_eq!(ne("status", "deleted").to_surql(), "status != 'deleted'");
}
#[test]
fn gt_renders_integer() {
assert_eq!(gt("age", 18).to_surql(), "age > 18");
}
#[test]
fn lt_renders_float() {
assert_eq!(lt("price", 50.0).to_surql(), "price < 50.0");
}
#[test]
fn gte_and_lte() {
assert_eq!(gte("score", 100).to_surql(), "score >= 100");
assert_eq!(lte("quantity", 10).to_surql(), "quantity <= 10");
}
#[test]
fn contains_renders() {
assert_eq!(
contains("email", "@example.com").to_surql(),
"email CONTAINS '@example.com'"
);
}
#[test]
fn contains_not_renders() {
assert_eq!(
contains_not("tags", "spam").to_surql(),
"tags CONTAINSNOT 'spam'"
);
}
#[test]
fn contains_all_renders() {
let op = contains_all("tags", [json!("python"), json!("database")]);
assert_eq!(op.to_surql(), "tags CONTAINSALL ['python', 'database']");
}
#[test]
fn contains_any_renders() {
let op = contains_any("tags", [json!("python"), json!("javascript")]);
assert_eq!(op.to_surql(), "tags CONTAINSANY ['python', 'javascript']");
}
#[test]
fn inside_renders() {
let op = inside("status", [json!("active"), json!("pending")]);
assert_eq!(op.to_surql(), "status INSIDE ['active', 'pending']");
}
#[test]
fn not_inside_renders() {
let op = not_inside("status", [json!("deleted"), json!("archived")]);
assert_eq!(op.to_surql(), "status NOTINSIDE ['deleted', 'archived']");
}
#[test]
fn is_null_and_not_null() {
assert_eq!(is_null("deleted_at").to_surql(), "deleted_at IS NULL");
assert_eq!(
is_not_null("created_at").to_surql(),
"created_at IS NOT NULL"
);
}
#[test]
fn and_renders() {
let op = and_(gt("age", 18), eq("status", "active"));
assert_eq!(op.to_surql(), "(age > 18) AND (status = 'active')");
}
#[test]
fn or_renders() {
let op = or_(eq("type", "admin"), eq("type", "moderator"));
assert_eq!(op.to_surql(), "(type = 'admin') OR (type = 'moderator')");
}
#[test]
fn not_renders() {
let op = not_(eq("status", "deleted"));
assert_eq!(op.to_surql(), "NOT (status = 'deleted')");
}
#[test]
fn null_quoted_as_keyword() {
assert_eq!(
eq("deleted_at", Value::Null).to_surql(),
"deleted_at = NULL"
);
}
#[test]
fn bool_quoted_lowercase() {
assert_eq!(eq("active", true).to_surql(), "active = true");
assert_eq!(eq("active", false).to_surql(), "active = false");
}
#[test]
fn string_escapes_single_quote() {
assert_eq!(eq("name", "O'Brien").to_surql(), "name = 'O\\'Brien'");
}
#[test]
fn string_escapes_backslash() {
assert_eq!(eq("path", "a\\b").to_surql(), "path = 'a\\\\b'");
}
#[test]
fn surrealfn_value_renders_raw() {
let fnv =
serde_json::to_value(super::super::surreal_fn::surql_fn("time::now", &[])).unwrap();
assert_eq!(eq("created_at", fnv).to_surql(), "created_at = time::now()");
}
#[test]
fn record_ref_value_renders_raw() {
let rr =
serde_json::to_value(super::super::record_ref::record_ref("user", "alice")).unwrap();
assert_eq!(
eq("author", rr).to_surql(),
"author = type::record('user', 'alice')"
);
}
#[test]
fn type_record_string_id_renders() {
assert_eq!(
type_record("task", "abc-123").to_surql(),
"type::record('task', 'abc-123')"
);
}
#[test]
fn type_record_int_id_renders() {
assert_eq!(
type_record("post", 42_i64).to_surql(),
"type::record('post', 42)"
);
}
#[test]
fn type_record_escapes_single_quote() {
assert_eq!(
type_record("user", "o'brien").to_surql(),
"type::record('user', 'o\\'brien')"
);
}
#[test]
fn type_record_is_function_expression() {
let expr = type_record("task", "abc");
assert_eq!(
expr.kind,
crate::query::expressions::ExpressionKind::Function
);
}
#[test]
fn type_thing_string_id_renders() {
assert_eq!(
type_thing("user", "alice").to_surql(),
"type::thing('user', 'alice')"
);
}
#[test]
fn type_thing_int_id_renders() {
assert_eq!(
type_thing("post", 123_i64).to_surql(),
"type::thing('post', 123)"
);
}
#[test]
fn type_thing_escapes_backslash() {
assert_eq!(
type_thing("path", "a\\b").to_surql(),
"type::thing('path', 'a\\\\b')"
);
}
#[test]
fn type_thing_is_function_expression() {
let expr = type_thing("user", "alice");
assert_eq!(
expr.kind,
crate::query::expressions::ExpressionKind::Function
);
}
}