use super::json::JsonBuilder;
use crate::ast::{Expr, JsonPathSegment};
const INVALID_JSON_SOURCE_IDENT: &str = "__qail_invalid_json_source__";
fn json_source_or_invalid(expr: Expr, _method: &str) -> String {
match expr {
Expr::Named(name) => name,
Expr::Aliased { name, .. } => name,
Expr::JsonAccess { column, .. } => column,
_other => {
#[cfg(debug_assertions)]
eprintln!(
"QAIL: {}() expects a column-like expression, got {:?}; using fallback identifier {}",
_method, _other, INVALID_JSON_SOURCE_IDENT
);
INVALID_JSON_SOURCE_IDENT.to_string()
}
}
}
pub trait ExprExt {
fn with_alias(self, alias: &str) -> Expr;
fn or_default(self, default: impl Into<Expr>) -> Expr;
fn json(self, key: &str) -> JsonBuilder;
fn path(self, dotted_path: &str) -> JsonBuilder;
fn cast(self, target_type: &str) -> Expr;
fn upper(self) -> Expr;
fn lower(self) -> Expr;
fn trim(self) -> Expr;
fn length(self) -> Expr;
fn abs(self) -> Expr;
}
impl ExprExt for Expr {
fn with_alias(self, alias: &str) -> Expr {
let mut expr = self;
expr.set_alias(alias);
expr
}
fn or_default(self, default: impl Into<Expr>) -> Expr {
Expr::FunctionCall {
name: "COALESCE".to_string(),
args: vec![self, default.into()],
alias: None,
}
}
fn json(self, key: &str) -> JsonBuilder {
let column = json_source_or_invalid(self, "json");
JsonBuilder {
column,
path_segments: vec![(key.into(), true)], alias: None,
}
}
fn path(self, dotted_path: &str) -> JsonBuilder {
let column = json_source_or_invalid(self, "path");
let segments: Vec<&str> = dotted_path.split('.').collect();
let len = segments.len();
let path_segments: Vec<(JsonPathSegment, bool)> = segments
.into_iter()
.enumerate()
.map(|(i, segment)| (JsonPathSegment::from_path_text(segment), i == len - 1)) .collect();
JsonBuilder {
column,
path_segments,
alias: None,
}
}
fn cast(self, target_type: &str) -> Expr {
Expr::Cast {
expr: Box::new(self),
target_type: target_type.to_string(),
alias: None,
}
}
fn upper(self) -> Expr {
Expr::FunctionCall {
name: "UPPER".to_string(),
args: vec![self],
alias: None,
}
}
fn lower(self) -> Expr {
Expr::FunctionCall {
name: "LOWER".to_string(),
args: vec![self],
alias: None,
}
}
fn trim(self) -> Expr {
Expr::FunctionCall {
name: "TRIM".to_string(),
args: vec![self],
alias: None,
}
}
fn length(self) -> Expr {
Expr::FunctionCall {
name: "LENGTH".to_string(),
args: vec![self],
alias: None,
}
}
fn abs(self) -> Expr {
Expr::FunctionCall {
name: "ABS".to_string(),
args: vec![self],
alias: None,
}
}
}
impl ExprExt for &str {
fn with_alias(self, alias: &str) -> Expr {
Expr::Aliased {
name: self.to_string(),
alias: alias.to_string(),
}
}
fn or_default(self, default: impl Into<Expr>) -> Expr {
Expr::FunctionCall {
name: "COALESCE".to_string(),
args: vec![Expr::Named(self.to_string()), default.into()],
alias: None,
}
}
fn json(self, key: &str) -> JsonBuilder {
JsonBuilder {
column: self.to_string(),
path_segments: vec![(key.into(), true)],
alias: None,
}
}
fn path(self, dotted_path: &str) -> JsonBuilder {
let segments: Vec<&str> = dotted_path.split('.').collect();
let len = segments.len();
let path_segments: Vec<(JsonPathSegment, bool)> = segments
.into_iter()
.enumerate()
.map(|(i, segment)| (JsonPathSegment::from_path_text(segment), i == len - 1))
.collect();
JsonBuilder {
column: self.to_string(),
path_segments,
alias: None,
}
}
fn cast(self, target_type: &str) -> Expr {
Expr::Cast {
expr: Box::new(Expr::Named(self.to_string())),
target_type: target_type.to_string(),
alias: None,
}
}
fn upper(self) -> Expr {
Expr::FunctionCall {
name: "UPPER".to_string(),
args: vec![Expr::Named(self.to_string())],
alias: None,
}
}
fn lower(self) -> Expr {
Expr::FunctionCall {
name: "LOWER".to_string(),
args: vec![Expr::Named(self.to_string())],
alias: None,
}
}
fn trim(self) -> Expr {
Expr::FunctionCall {
name: "TRIM".to_string(),
args: vec![Expr::Named(self.to_string())],
alias: None,
}
}
fn length(self) -> Expr {
Expr::FunctionCall {
name: "LENGTH".to_string(),
args: vec![Expr::Named(self.to_string())],
alias: None,
}
}
fn abs(self) -> Expr {
Expr::FunctionCall {
name: "ABS".to_string(),
args: vec![Expr::Named(self.to_string())],
alias: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::builders::col;
#[test]
fn test_or_default() {
let expr = col("name").or_default("Unknown");
assert!(matches!(expr, Expr::FunctionCall { name, .. } if name == "COALESCE"));
}
#[test]
fn test_json_fluent() {
let expr: Expr = col("info").json("phone").into();
assert!(matches!(expr, Expr::JsonAccess { column, .. } if column == "info"));
}
#[test]
fn test_path_fluent() {
let expr: Expr = col("metadata").path("vessel.0.port").into();
if let Expr::JsonAccess { path_segments, .. } = expr {
assert_eq!(path_segments.len(), 3);
assert_eq!(path_segments[0], ("vessel".into(), false)); assert_eq!(path_segments[1], (JsonPathSegment::Index(0), false)); assert_eq!(path_segments[2], ("port".into(), true)); } else {
panic!("Expected JsonAccess");
}
}
#[test]
fn test_str_or_default() {
let expr = "name".or_default("N/A");
assert!(matches!(expr, Expr::FunctionCall { name, .. } if name == "COALESCE"));
}
#[test]
fn test_cast_fluent() {
let expr = col("value").cast("int4");
assert!(matches!(expr, Expr::Cast { target_type, .. } if target_type == "int4"));
}
#[test]
fn test_upper_fluent() {
let expr = col("name").upper();
assert!(matches!(expr, Expr::FunctionCall { name, .. } if name == "UPPER"));
}
#[test]
fn test_lower_fluent() {
let expr = "email".lower();
assert!(matches!(expr, Expr::FunctionCall { name, .. } if name == "LOWER"));
}
#[test]
fn test_trim_fluent() {
let expr = col("text").trim();
assert!(matches!(expr, Expr::FunctionCall { name, .. } if name == "TRIM"));
}
}