diff --git a/partiql-eval/src/eval.rs b/partiql-eval/src/eval.rs index abcb6a74..d066fcc6 100644 --- a/partiql-eval/src/eval.rs +++ b/partiql-eval/src/eval.rs @@ -1099,3 +1099,217 @@ impl EvalContext for BasicContext { &self.bindings } } + +#[inline] +#[track_caller] +fn string_transform(value: Value, transform_fn: FnTransform) -> Value +where + FnTransform: Fn(&str) -> Value, +{ + match value { + Null => Value::Null, + Value::String(s) => transform_fn(s.as_ref()), + _ => Value::Missing, + } +} + +#[derive(Debug)] +pub struct EvalFnLower { + pub value: Box, +} + +impl EvalExpr for EvalFnLower { + #[inline] + fn evaluate(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> Value { + string_transform(self.value.evaluate(bindings, ctx), |s| { + s.to_lowercase().into() + }) + } +} + +#[derive(Debug)] +pub struct EvalFnUpper { + pub value: Box, +} + +impl EvalExpr for EvalFnUpper { + #[inline] + fn evaluate(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> Value { + string_transform(self.value.evaluate(bindings, ctx), |s| { + s.to_uppercase().into() + }) + } +} + +#[derive(Debug)] +pub struct EvalFnCharLength { + pub value: Box, +} + +impl EvalExpr for EvalFnCharLength { + #[inline] + fn evaluate(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> Value { + string_transform(self.value.evaluate(bindings, ctx), |s| { + s.chars().count().into() + }) + } +} + +#[derive(Debug)] +pub struct EvalFnSubstring { + pub value: Box, + pub offset: Box, + pub length: Option>, +} + +impl EvalExpr for EvalFnSubstring { + #[inline] + fn evaluate(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> Value { + let value = match self.value.evaluate(bindings, ctx) { + Null => None, + Value::String(s) => Some(s), + _ => return Value::Missing, + }; + let offset = match self.offset.evaluate(bindings, ctx) { + Null => None, + Value::Integer(i) => Some(i), + _ => return Value::Missing, + }; + + if let Some(length) = &self.length { + let length = match length.evaluate(bindings, ctx) { + Value::Integer(i) => i as usize, + Value::Null => return Value::Null, + _ => return Value::Missing, + }; + if let (Some(value), Some(offset)) = (value, offset) { + let (offset, length) = if length < 1 { + (0, 0) + } else if offset < 1 { + let length = std::cmp::max(offset + (length - 1) as i64, 0) as usize; + let offset = std::cmp::max(offset, 0) as usize; + (offset, length) + } else { + ((offset - 1) as usize, length) + }; + value + .chars() + .skip(offset) + .take(length) + .collect::() + .into() + } else { + // either value or offset was NULL; return NULL + Value::Null + } + } else if let (Some(value), Some(offset)) = (value, offset) { + let offset = (std::cmp::max(offset, 1) - 1) as usize; + value.chars().skip(offset).collect::().into() + } else { + // either value or offset was NULL; return NULL + Value::Null + } + } +} + +#[inline] +#[track_caller] +fn trim(value: Value, to_trim: Value, trim_fn: FnTrim) -> Value +where + FnTrim: Fn(&str, &str) -> Value, +{ + let value = match value { + Value::String(s) => Some(s), + Null => None, + _ => return Value::Missing, + }; + let to_trim = match to_trim { + Value::String(s) => s, + Null => return Value::Null, + _ => return Value::Missing, + }; + if let Some(s) = value { + trim_fn(&s, &to_trim) + } else { + Value::Null + } +} + +#[derive(Debug)] +pub struct EvalFnBtrim { + pub value: Box, + pub to_trim: Box, +} + +impl EvalExpr for EvalFnBtrim { + #[inline] + fn evaluate(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> Value { + trim( + self.value.evaluate(bindings, ctx), + self.to_trim.evaluate(bindings, ctx), + |s, to_trim| { + let to_trim = to_trim.chars().collect_vec(); + s.trim_matches(&to_trim[..]).into() + }, + ) + } +} + +#[derive(Debug)] +pub struct EvalFnRtrim { + pub value: Box, + pub to_trim: Box, +} + +impl EvalExpr for EvalFnRtrim { + #[inline] + fn evaluate(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> Value { + trim( + self.value.evaluate(bindings, ctx), + self.to_trim.evaluate(bindings, ctx), + |s, to_trim| { + let to_trim = to_trim.chars().collect_vec(); + s.trim_end_matches(&to_trim[..]).into() + }, + ) + } +} + +#[derive(Debug)] +pub struct EvalFnLtrim { + pub value: Box, + pub to_trim: Box, +} + +impl EvalExpr for EvalFnLtrim { + #[inline] + fn evaluate(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> Value { + trim( + self.value.evaluate(bindings, ctx), + self.to_trim.evaluate(bindings, ctx), + |s, to_trim| { + let to_trim = to_trim.chars().collect_vec(); + s.trim_start_matches(&to_trim[..]).into() + }, + ) + } +} + +#[derive(Debug)] +pub struct EvalFnExists { + pub value: Box, +} + +impl EvalExpr for EvalFnExists { + #[inline] + fn evaluate(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> Value { + let value = self.value.evaluate(bindings, ctx); + let exists = match value { + Value::Bag(b) => !b.is_empty(), + Value::List(l) => !l.is_empty(), + Value::Tuple(t) => !t.is_empty(), + _ => false, + }; + Value::Boolean(exists) + } +} diff --git a/partiql-eval/src/plan.rs b/partiql-eval/src/plan.rs index caa8b959..cc379566 100644 --- a/partiql-eval/src/plan.rs +++ b/partiql-eval/src/plan.rs @@ -4,16 +4,17 @@ use std::collections::HashMap; use partiql_logical as logical; use partiql_logical::{ - BinaryOp, BindingsOp, IsTypeExpr, JoinKind, LogicalPlan, OpId, PathComponent, Pattern, - PatternMatchExpr, SearchedCase, Type, UnaryOp, ValueExpr, + BinaryOp, BindingsOp, CallName, IsTypeExpr, JoinKind, LogicalPlan, OpId, PathComponent, + Pattern, PatternMatchExpr, SearchedCase, Type, UnaryOp, ValueExpr, }; use crate::eval; use crate::eval::{ EvalBagExpr, EvalBetweenExpr, EvalBinOp, EvalBinOpExpr, EvalDynamicLookup, EvalExpr, - EvalIsTypeExpr, EvalJoinKind, EvalLikeMatch, EvalListExpr, EvalLitExpr, EvalPath, EvalPlan, - EvalSearchedCaseExpr, EvalSubQueryExpr, EvalTupleExpr, EvalUnaryOp, EvalUnaryOpExpr, - EvalVarRef, Evaluable, + EvalFnBtrim, EvalFnCharLength, EvalFnExists, EvalFnLower, EvalFnLtrim, EvalFnRtrim, + EvalFnSubstring, EvalFnUpper, EvalIsTypeExpr, EvalJoinKind, EvalLikeMatch, EvalListExpr, + EvalLitExpr, EvalPath, EvalPlan, EvalSearchedCaseExpr, EvalSubQueryExpr, EvalTupleExpr, + EvalUnaryOp, EvalUnaryOpExpr, EvalVarRef, Evaluable, }; use crate::pattern_match::like_to_re_pattern; use partiql_value::Value::Null; @@ -337,6 +338,73 @@ impl EvaluatorPlanner { Box::new(EvalDynamicLookup { lookups }) } + ValueExpr::Call(logical::CallExpr { name, arguments }) => { + let mut args = arguments + .into_iter() + .map(|arg| self.plan_values(arg)) + .collect_vec(); + match name { + CallName::Lower => { + assert_eq!(args.len(), 1); + Box::new(EvalFnLower { + value: args.pop().unwrap(), + }) + } + CallName::Upper => { + assert_eq!(args.len(), 1); + Box::new(EvalFnUpper { + value: args.pop().unwrap(), + }) + } + CallName::CharLength => { + assert_eq!(args.len(), 1); + Box::new(EvalFnCharLength { + value: args.pop().unwrap(), + }) + } + CallName::LTrim => { + assert_eq!(args.len(), 2); + let value = args.pop().unwrap(); + let to_trim = args.pop().unwrap(); + Box::new(EvalFnLtrim { value, to_trim }) + } + CallName::BTrim => { + assert_eq!(args.len(), 2); + let value = args.pop().unwrap(); + let to_trim = args.pop().unwrap(); + Box::new(EvalFnBtrim { value, to_trim }) + } + CallName::RTrim => { + assert_eq!(args.len(), 2); + let value = args.pop().unwrap(); + let to_trim = args.pop().unwrap(); + Box::new(EvalFnRtrim { value, to_trim }) + } + CallName::Substring => { + assert!((2usize..=3).contains(&args.len())); + + let length = if args.len() == 3 { + Some(args.pop().unwrap()) + } else { + None + }; + let offset = args.pop().unwrap(); + let value = args.pop().unwrap(); + + Box::new(EvalFnSubstring { + value, + offset, + length, + }) + } + CallName::Exists => { + assert_eq!(args.len(), 1); + Box::new(EvalFnExists { + value: args.pop().unwrap(), + }) + } + } + } } } } diff --git a/partiql-logical-planner/src/call_defs.rs b/partiql-logical-planner/src/call_defs.rs new file mode 100644 index 00000000..56b499c5 --- /dev/null +++ b/partiql-logical-planner/src/call_defs.rs @@ -0,0 +1,357 @@ +use itertools::Itertools; +use partiql_logical as logical; +use partiql_logical::ValueExpr; +use partiql_value::Value; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use unicase::UniCase; + +#[derive(Debug, Eq, PartialEq)] +pub enum CallArgument { + Positional(ValueExpr), + Named(String, ValueExpr), +} + +#[derive(Debug)] +pub struct CallDef { + names: Vec<&'static str>, + overloads: Vec, +} + +impl CallDef { + pub(crate) fn lookup(&self, args: &Vec) -> ValueExpr { + 'overload: for overload in &self.overloads { + let formals = &overload.input; + if formals.len() != args.len() { + continue 'overload; + } + + let mut actuals = vec![]; + for i in 0..formals.len() { + let formal = &formals[i]; + let actual = &args[i]; + if let Some(vexpr) = formal.transform(actual) { + actuals.push(vexpr); + } else { + continue 'overload; + } + } + + return (overload.output)(actuals); + } + + todo!("mismatched formal/actual arguments to {}", &self.names[0]) + } +} + +impl CallDef {} + +#[derive(Debug, Copy, Clone)] +pub enum CallSpecArg { + Positional, + Named(UniCase<&'static str>), +} + +impl CallSpecArg { + pub(crate) fn transform(&self, arg: &CallArgument) -> Option { + match (self, arg) { + (CallSpecArg::Positional, CallArgument::Positional(ve)) => Some(ve.clone()), + (CallSpecArg::Named(formal_name), CallArgument::Named(arg_name, ve)) => { + if formal_name == &UniCase::new(arg_name.as_str()) { + Some(ve.clone()) + } else { + None + } + } + _ => None, + } + } +} + +impl CallSpecArg {} + +pub struct CallSpec { + input: Vec, + output: Box) -> logical::ValueExpr>, +} + +impl Debug for CallSpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "CallSpec [{:?}]", &self.input) + } +} + +fn function_call_def_char_len() -> CallDef { + CallDef { + names: vec!["char_length", "character_length"], + overloads: vec![CallSpec { + input: vec![CallSpecArg::Positional], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::CharLength, + arguments: args, + }) + }), + }], + } +} + +fn function_call_def_lower() -> CallDef { + CallDef { + names: vec!["lower"], + overloads: vec![CallSpec { + input: vec![CallSpecArg::Positional], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::Lower, + arguments: args, + }) + }), + }], + } +} + +fn function_call_def_upper() -> CallDef { + CallDef { + names: vec!["upper"], + overloads: vec![CallSpec { + input: vec![CallSpecArg::Positional], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::Upper, + arguments: args, + }) + }), + }], + } +} + +fn function_call_def_substring() -> CallDef { + CallDef { + names: vec!["substring"], + overloads: vec![ + CallSpec { + input: vec![ + CallSpecArg::Positional, + CallSpecArg::Positional, + CallSpecArg::Positional, + ], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::Substring, + arguments: args, + }) + }), + }, + CallSpec { + input: vec![CallSpecArg::Positional, CallSpecArg::Positional], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::Substring, + arguments: args, + }) + }), + }, + CallSpec { + input: vec![ + CallSpecArg::Positional, + CallSpecArg::Named("from".into()), + CallSpecArg::Named("for".into()), + ], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::Substring, + arguments: args, + }) + }), + }, + CallSpec { + input: vec![CallSpecArg::Positional, CallSpecArg::Named("from".into())], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::Substring, + arguments: args, + }) + }), + }, + CallSpec { + input: vec![CallSpecArg::Positional, CallSpecArg::Named("for".into())], + output: Box::new(|mut args| { + args.insert(1, ValueExpr::Lit(Box::new(Value::Integer(0)))); + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::Substring, + arguments: args, + }) + }), + }, + ], + } +} + +fn function_call_def_trim() -> CallDef { + CallDef { + names: vec!["trim"], + overloads: vec![ + CallSpec { + input: vec![ + CallSpecArg::Named("leading".into()), + CallSpecArg::Named("from".into()), + ], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::LTrim, + arguments: args, + }) + }), + }, + CallSpec { + input: vec![ + CallSpecArg::Named("trailing".into()), + CallSpecArg::Named("from".into()), + ], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::RTrim, + arguments: args, + }) + }), + }, + CallSpec { + input: vec![ + CallSpecArg::Named("both".into()), + CallSpecArg::Named("from".into()), + ], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::BTrim, + arguments: args, + }) + }), + }, + CallSpec { + input: vec![CallSpecArg::Named("from".into())], + output: Box::new(|mut args| { + args.insert( + 0, + ValueExpr::Lit(Box::new(Value::String(" ".to_string().into()))), + ); + + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::BTrim, + arguments: args, + }) + }), + }, + CallSpec { + input: vec![CallSpecArg::Positional], + output: Box::new(|mut args| { + args.insert( + 0, + ValueExpr::Lit(Box::new(Value::String(" ".to_string().into()))), + ); + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::BTrim, + arguments: args, + }) + }), + }, + CallSpec { + input: vec![CallSpecArg::Positional, CallSpecArg::Named("from".into())], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::BTrim, + arguments: args, + }) + }), + }, + ], + } +} + +fn function_call_def_coalesce() -> CallDef { + CallDef { + names: vec!["coalesce"], + overloads: (0..15) + .map(|n| CallSpec { + input: std::iter::repeat(CallSpecArg::Positional) + .take(n) + .collect_vec(), + output: Box::new(|args| { + logical::ValueExpr::CoalesceExpr(logical::CoalesceExpr { elements: args }) + }), + }) + .collect_vec(), + } +} + +fn function_call_def_nullif() -> CallDef { + CallDef { + names: vec!["nullif"], + overloads: vec![CallSpec { + input: vec![CallSpecArg::Positional, CallSpecArg::Positional], + output: Box::new(|mut args| { + assert_eq!(args.len(), 2); + let rhs = Box::new(args.pop().unwrap()); + let lhs = Box::new(args.pop().unwrap()); + logical::ValueExpr::NullIfExpr(logical::NullIfExpr { lhs, rhs }) + }), + }], + } +} + +fn function_call_def_exists() -> CallDef { + CallDef { + names: vec!["exists"], + overloads: vec![CallSpec { + input: vec![CallSpecArg::Positional], + output: Box::new(|args| { + logical::ValueExpr::Call(logical::CallExpr { + name: logical::CallName::Exists, + arguments: args, + }) + }), + }], + } +} + +/// Function symbol table +#[derive(Debug)] +pub struct FnSymTab { + calls: HashMap, CallDef>, + synonyms: HashMap, UniCase>, +} + +impl FnSymTab { + pub fn lookup(&self, fn_name: &str) -> Option<&CallDef> { + self.synonyms + .get(&fn_name.into()) + .and_then(|name| self.calls.get(name)) + } +} + +pub fn function_call_def() -> FnSymTab { + let mut calls: HashMap, CallDef> = HashMap::new(); + let mut synonyms: HashMap, UniCase> = HashMap::new(); + + for def in [ + function_call_def_char_len(), + function_call_def_lower(), + function_call_def_upper(), + function_call_def_substring(), + function_call_def_trim(), + function_call_def_coalesce(), + function_call_def_nullif(), + function_call_def_exists(), + ] { + assert!(!def.names.is_empty()); + let primary = def.names[0]; + synonyms.insert(primary.into(), primary.into()); + for &name in &def.names[1..] { + synonyms.insert(name.into(), primary.into()); + } + + calls.insert(primary.into(), def); + } + + FnSymTab { calls, synonyms } +} diff --git a/partiql-logical-planner/src/lib.rs b/partiql-logical-planner/src/lib.rs index 7e5ae425..f132e86d 100644 --- a/partiql-logical-planner/src/lib.rs +++ b/partiql-logical-planner/src/lib.rs @@ -4,6 +4,7 @@ use partiql_ast::ast; use partiql_logical as logical; use partiql_parser::Parsed; +mod call_defs; mod lower; mod name_resolver; diff --git a/partiql-logical-planner/src/lower.rs b/partiql-logical-planner/src/lower.rs index 9a25fc16..7799ca0c 100644 --- a/partiql-logical-planner/src/lower.rs +++ b/partiql-logical-planner/src/lower.rs @@ -4,12 +4,12 @@ use num::Integer; use ordered_float::OrderedFloat; use partiql_ast::ast; use partiql_ast::ast::{ - Assignment, Bag, Between, BinOp, BinOpKind, Call, CallAgg, CaseSensitivity, CreateIndex, - CreateTable, Ddl, DdlOp, Delete, Dml, DmlOp, DropIndex, DropTable, FromClause, FromLet, - FromLetKind, GroupByExpr, Insert, InsertValue, Item, Join, JoinKind, JoinSpec, Like, List, Lit, - NodeId, OnConflict, OrderByExpr, Path, PathStep, ProjectExpr, Projection, ProjectionKind, - Query, QuerySet, Remove, Select, Set, SetExpr, SetQuantifier, Sexp, Struct, SymbolPrimitive, - UniOp, UniOpKind, VarRef, + Assignment, Bag, Between, BinOp, BinOpKind, Call, CallAgg, CallArg, CallArgNamed, + CaseSensitivity, CreateIndex, CreateTable, Ddl, DdlOp, Delete, Dml, DmlOp, DropIndex, + DropTable, FromClause, FromLet, FromLetKind, GroupByExpr, Insert, InsertValue, Item, Join, + JoinKind, JoinSpec, Like, List, Lit, NodeId, OnConflict, OrderByExpr, Path, PathStep, + ProjectExpr, Projection, ProjectionKind, Query, QuerySet, Remove, Select, Set, SetExpr, + SetQuantifier, Sexp, Struct, SymbolPrimitive, UniOp, UniOpKind, VarRef, }; use partiql_ast::visit::{Visit, Visitor}; use partiql_logical as logical; @@ -22,6 +22,7 @@ use partiql_value::{BindingsName, Value}; use std::collections::{HashMap, HashSet}; +use crate::call_defs::{function_call_def, CallArgument, FnSymTab}; use crate::name_resolver; use std::sync::atomic::{AtomicU32, Ordering}; @@ -101,6 +102,7 @@ pub struct AstToLogical { ctx_stack: Vec, bexpr_stack: Vec>, vexpr_stack: Vec>, + arg_stack: Vec>, path_stack: Vec>, from_lets: HashSet, @@ -116,6 +118,7 @@ pub struct AstToLogical { plan: LogicalPlan, key_registry: name_resolver::KeyRegistry, + fnsym_tab: FnSymTab, } /// Attempt to infer an alias for a simple variable reference expression. @@ -175,6 +178,7 @@ impl AstToLogical { ctx_stack: Default::default(), bexpr_stack: Default::default(), vexpr_stack: Default::default(), + arg_stack: Default::default(), path_stack: Default::default(), from_lets: Default::default(), @@ -190,6 +194,7 @@ impl AstToLogical { plan: Default::default(), key_registry: registry, + fnsym_tab: function_call_def(), } } @@ -384,6 +389,21 @@ impl AstToLogical { self.push_vexpr(ValueExpr::Lit(Box::new(val))); } + #[inline] + fn enter_call(&mut self) { + self.arg_stack.push(vec![]); + } + + #[inline] + fn exit_call(&mut self) -> Vec { + self.arg_stack.pop().expect("environment level") + } + + #[inline] + fn push_call_arg(&mut self, arg: CallArgument) { + self.arg_stack.last_mut().unwrap().push(arg); + } + #[inline] fn enter_path(&mut self) { self.path_stack.push(vec![]); @@ -724,6 +744,44 @@ impl<'ast> Visitor<'ast> for AstToLogical { })); } + fn enter_call(&mut self, _call: &'ast Call) { + self.enter_call(); + } + + fn exit_call(&mut self, _call: &'ast Call) { + // TODO better argument validation/error messaging + let env = self.exit_call(); + let name = _call.func_name.value.to_lowercase(); + + if let Some(call_def) = self.fnsym_tab.lookup(name.as_str()) { + self.push_vexpr(call_def.lookup(&env)); + } else { + todo!("Unsupported function name") + } + } + + fn enter_call_arg(&mut self, _call_arg: &'ast CallArg) { + self.enter_env(); + } + + fn exit_call_arg(&mut self, _call_arg: &'ast CallArg) { + let mut env = self.exit_env(); + match _call_arg { + CallArg::Star() => todo!(), + CallArg::Positional(_) => { + assert_eq!(env.len(), 1); + self.push_call_arg(CallArgument::Positional(env.pop().unwrap())); + } + CallArg::Named(CallArgNamed { name, .. }) => { + assert_eq!(env.len(), 1); + let name = name.value.to_lowercase(); + self.push_call_arg(CallArgument::Named(name, env.pop().unwrap())); + } + CallArg::PositionalType(_) => todo!("CallArg::PositionalType"), + CallArg::NamedType(_) => todo!("CallArg::NamedType"), + } + } + // Values & Value Constructors fn enter_lit(&mut self, _lit: &'ast Lit) { @@ -799,10 +857,6 @@ impl<'ast> Visitor<'ast> for AstToLogical { todo!("exit_sexp") } - fn enter_call(&mut self, _call: &'ast Call) { - todo!("call") - } - fn enter_call_agg(&mut self, _call_agg: &'ast CallAgg) { todo!("call_agg") } diff --git a/partiql-logical/src/lib.rs b/partiql-logical/src/lib.rs index f29bd7e9..f890bbe3 100644 --- a/partiql-logical/src/lib.rs +++ b/partiql-logical/src/lib.rs @@ -261,6 +261,7 @@ pub enum ValueExpr { IsTypeExpr(IsTypeExpr), NullIfExpr(NullIfExpr), CoalesceExpr(CoalesceExpr), + Call(CallExpr), } // TODO we should replace this enum with some identifier that can be looked up in a symtab/funcregistry? @@ -452,6 +453,26 @@ pub struct CoalesceExpr { pub elements: Vec, } +/// Represents a `CALL` expression (i.e., a function call), e.g. `LOWER("ALL CAPS")`. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct CallExpr { + pub name: CallName, + pub arguments: Vec, +} + +/// Represents a known function. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum CallName { + Lower, + Upper, + CharLength, + LTrim, + BTrim, + RTrim, + Substring, + Exists, +} + #[cfg(test)] mod tests { use super::*; diff --git a/partiql-parser/src/preprocessor.rs b/partiql-parser/src/preprocessor.rs index a012ad69..fb6d7390 100644 --- a/partiql-parser/src/preprocessor.rs +++ b/partiql-parser/src/preprocessor.rs @@ -15,7 +15,7 @@ use partiql_source_map::line_offset_tracker::LineOffsetTracker; /// A single "function expression" argument match. #[derive(Debug, Clone)] pub(crate) enum FnExprArgMatch<'a> { - /// Any 1 [`Token`] that is not function punctuation (i.e., '(', ')', ',') and not a keyword. + /// Any 1 [`Token`] that is not function punctuation (i.e., '(', ')', ',') and potentially not a keyword. /// /// Generally this will be followed by a [`AnyZeroOrMore`] match, in order to match 1 or more [`Token`]s /// `bool` tuple value denotes if keyword is allowed to be considered as match. @@ -72,13 +72,13 @@ mod built_ins { #[rustfmt::skip] patterns: vec![ // e.g., trim(leading 'tt' from x) => trim("leading": 'tt', "from": x) - vec![Id(re.clone()), AnyOne(false), AnyStar(false), Kw(Token::From), AnyOne(false), AnyStar(false)], + vec![Id(re.clone()), AnyOne(true), AnyStar(false), Kw(Token::From), AnyOne(true), AnyStar(false)], // e.g., trim(trailing from x) => trim("trailing": ' ', "from": x) - vec![Id(re), Syn(Token::String(" ")), Kw(Token::From), AnyOne(false), AnyStar(false)], + vec![Id(re), Syn(Token::String(" ")), Kw(Token::From), AnyOne(true), AnyStar(false)], // e.g., trim(' ' from x) => trim(' ', "from": x) - vec![AnyOne(false), AnyStar(false), Kw(Token::From), AnyOne(false), AnyStar(false)], + vec![AnyOne(true), AnyStar(false), Kw(Token::From), AnyOne(true), AnyStar(false)], // e.g., trim(from x) => trim("from": x) - vec![Kw(Token::From), AnyOne(false), AnyStar(false)], + vec![Kw(Token::From), AnyOne(true), AnyStar(false)], ], } } @@ -93,7 +93,7 @@ mod built_ins { #[rustfmt::skip] patterns: vec![ // e.g., extract(day from x) => extract("day":true, "from": x) - vec![Id(re), Syn(Token::True), Kw(Token::From), AnyOne(false), AnyStar(false)] + vec![Id(re), Syn(Token::True), Kw(Token::From), AnyOne(true), AnyStar(false)] ], } } @@ -104,7 +104,7 @@ mod built_ins { #[rustfmt::skip] patterns: vec![ // e.g. position('foo' in 'xyzfooxyz') => position('foo', in: 'xyzfooxyz') - vec![AnyOne(false), AnyStar(false), Kw(Token::In), AnyOne(false), AnyStar(false)] + vec![AnyOne(false), AnyStar(false), Kw(Token::In), AnyOne(true), AnyStar(false)] ], } } @@ -115,9 +115,9 @@ mod built_ins { #[rustfmt::skip] patterns: vec![ // e.g., count(all x) => count("all": x) - vec![Kw(Token::All), AnyOne(false), AnyStar(false)], + vec![Kw(Token::All), AnyOne(true), AnyStar(false)], // e.g., count(distinct x) => count("distinct": x) - vec![Kw(Token::Distinct), AnyOne(false), AnyStar(false)], + vec![Kw(Token::Distinct), AnyOne(true), AnyStar(false)], ], } } @@ -128,11 +128,11 @@ mod built_ins { #[rustfmt::skip] patterns: vec![ // e.g. substring(x from 2 for 3) => substring(x, "from":2, "for":3) - vec![AnyOne(false), AnyStar(false), Kw(Token::From), AnyOne(false), AnyStar(false), Kw(Token::For), AnyOne(false), AnyStar(false)], + vec![AnyOne(true), AnyStar(false), Kw(Token::From), AnyOne(true), AnyStar(false), Kw(Token::For), AnyOne(true), AnyStar(false)], // e.g. substring(x from 2) => substring(x, "from":2) - vec![AnyOne(false), AnyStar(false), Kw(Token::From), AnyOne(false), AnyStar(false)], + vec![AnyOne(true), AnyStar(false), Kw(Token::From), AnyOne(true), AnyStar(false)], // e.g. substring(x for 3) => substring(x, "for":3) - vec![AnyOne(false), AnyStar(false), Kw(Token::For), AnyOne(false), AnyStar(false)], + vec![AnyOne(true), AnyStar(false), Kw(Token::For), AnyOne(true), AnyStar(false)], ], } } @@ -144,7 +144,7 @@ mod built_ins { patterns: vec![ // e.g., cast(9 as VARCHAR(5)) => cast(9 "as": VARCHAR(5)) // Note the `true` passed to Any* as we need to support type-related keywords after `AS` - vec![AnyOne(false), AnyStar(false), Kw(Token::As), AnyOne(true), AnyStar(true)] + vec![AnyOne(true), AnyStar(false), Kw(Token::As), AnyOne(true), AnyStar(true)] ], } } @@ -695,6 +695,10 @@ mod tests { let lexer = PreprocessingPartiqlLexer::new(query, &mut offset_tracker, &BUILT_INS); to_tokens(lexer) } + assert_eq!( + preprocess(r#"trim(both from missing)"#)?, + lex(r#"trim(both: ' ', "from": missing)"#)? + ); // Valid, but missing final paren assert_eq!( diff --git a/partiql-value/src/lib.rs b/partiql-value/src/lib.rs index 08f6551e..2e78394c 100644 --- a/partiql-value/src/lib.rs +++ b/partiql-value/src/lib.rs @@ -799,6 +799,21 @@ impl From for Value { } } +impl From for Value { + #[inline] + fn from(n: i32) -> Self { + (n as i64).into() + } +} + +impl From for Value { + #[inline] + fn from(n: usize) -> Self { + // TODO overflow to bigint/decimal + Value::Integer(n as i64) + } +} + impl From for Value { #[inline] fn from(f: f64) -> Self { @@ -851,7 +866,7 @@ impl List { #[inline] pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.len() == 0 } #[inline] @@ -1002,7 +1017,7 @@ impl Bag { #[inline] pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.len() == 0 } #[inline] @@ -1162,6 +1177,16 @@ impl Tuple { self.vals.push(val); } + #[inline] + pub fn len(&self) -> usize { + self.attrs.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + #[inline] /// Creates a `Tuple` containing all the attributes and values provided by `self` and `other` /// removing duplicate attributes. Assumes that `self` contains unique attributes and `other`