use crate::{
db::{
QueryError,
executor::projection::eval_value_projection_expr_with_value,
query::{
builder::{
ValueProjectionExpr, scalar_projection::render_scalar_projection_expr_sql_label,
},
plan::expr::{Expr, FieldId, Function},
},
},
traits::FieldValue,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TextProjectionExpr {
field: String,
expr: Expr,
}
impl TextProjectionExpr {
pub(in crate::db) fn unary(field: impl Into<String>, function: Function) -> Self {
let field = field.into();
Self {
expr: Expr::FunctionCall {
function,
args: vec![Expr::Field(FieldId::new(field.clone()))],
},
field,
}
}
pub(in crate::db) fn with_literal(
field: impl Into<String>,
function: Function,
literal: impl FieldValue,
) -> Self {
let field = field.into();
Self {
expr: Expr::FunctionCall {
function,
args: vec![
Expr::Field(FieldId::new(field.clone())),
Expr::Literal(literal.to_value()),
],
},
field,
}
}
pub(in crate::db) fn with_two_literals(
field: impl Into<String>,
function: Function,
literal: impl FieldValue,
literal2: impl FieldValue,
) -> Self {
let field = field.into();
Self {
expr: Expr::FunctionCall {
function,
args: vec![
Expr::Field(FieldId::new(field.clone())),
Expr::Literal(literal.to_value()),
Expr::Literal(literal2.to_value()),
],
},
field,
}
}
pub(in crate::db) fn position(field: impl Into<String>, literal: impl FieldValue) -> Self {
let field = field.into();
Self {
expr: Expr::FunctionCall {
function: Function::Position,
args: vec![
Expr::Literal(literal.to_value()),
Expr::Field(FieldId::new(field.clone())),
],
},
field,
}
}
#[must_use]
pub(in crate::db) const fn expr(&self) -> &Expr {
&self.expr
}
}
impl ValueProjectionExpr for TextProjectionExpr {
fn field(&self) -> &str {
self.field.as_str()
}
fn sql_label(&self) -> String {
render_scalar_projection_expr_sql_label(&self.expr)
}
fn apply_value(&self, value: crate::value::Value) -> Result<crate::value::Value, QueryError> {
eval_value_projection_expr_with_value(&self.expr, self.field.as_str(), &value)
}
}
#[must_use]
pub fn trim(field: impl AsRef<str>) -> TextProjectionExpr {
TextProjectionExpr::unary(field.as_ref().to_string(), Function::Trim)
}
#[must_use]
pub fn ltrim(field: impl AsRef<str>) -> TextProjectionExpr {
TextProjectionExpr::unary(field.as_ref().to_string(), Function::Ltrim)
}
#[must_use]
pub fn rtrim(field: impl AsRef<str>) -> TextProjectionExpr {
TextProjectionExpr::unary(field.as_ref().to_string(), Function::Rtrim)
}
#[must_use]
pub fn lower(field: impl AsRef<str>) -> TextProjectionExpr {
TextProjectionExpr::unary(field.as_ref().to_string(), Function::Lower)
}
#[must_use]
pub fn upper(field: impl AsRef<str>) -> TextProjectionExpr {
TextProjectionExpr::unary(field.as_ref().to_string(), Function::Upper)
}
#[must_use]
pub fn length(field: impl AsRef<str>) -> TextProjectionExpr {
TextProjectionExpr::unary(field.as_ref().to_string(), Function::Length)
}
#[must_use]
pub fn left(field: impl AsRef<str>, length: impl FieldValue) -> TextProjectionExpr {
TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Left, length)
}
#[must_use]
pub fn right(field: impl AsRef<str>, length: impl FieldValue) -> TextProjectionExpr {
TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Right, length)
}
#[must_use]
pub fn starts_with(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::StartsWith, literal)
}
#[must_use]
pub fn ends_with(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::EndsWith, literal)
}
#[must_use]
pub fn contains(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Contains, literal)
}
#[must_use]
pub fn position(field: impl AsRef<str>, literal: impl FieldValue) -> TextProjectionExpr {
TextProjectionExpr::position(field.as_ref().to_string(), literal)
}
#[must_use]
pub fn replace(
field: impl AsRef<str>,
from: impl FieldValue,
to: impl FieldValue,
) -> TextProjectionExpr {
TextProjectionExpr::with_two_literals(field.as_ref().to_string(), Function::Replace, from, to)
}
#[must_use]
pub fn substring(field: impl AsRef<str>, start: impl FieldValue) -> TextProjectionExpr {
TextProjectionExpr::with_literal(field.as_ref().to_string(), Function::Substring, start)
}
#[must_use]
pub fn substring_with_length(
field: impl AsRef<str>,
start: impl FieldValue,
length: impl FieldValue,
) -> TextProjectionExpr {
TextProjectionExpr::with_two_literals(
field.as_ref().to_string(),
Function::Substring,
start,
length,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Value;
#[test]
fn lower_text_projection_renders_sql_label() {
assert_eq!(lower("name").sql_label(), "LOWER(name)");
}
#[test]
fn replace_text_projection_applies_shared_transform() {
let value = replace("name", "Ada", "Eve")
.apply_value(Value::Text("Ada Ada".to_string()))
.expect("replace projection should apply");
assert_eq!(value, Value::Text("Eve Eve".to_string()));
}
}