diff --git a/lykiadb-lang/src/parser/mod.rs b/lykiadb-lang/src/parser/mod.rs index 263cf10..08b65db 100644 --- a/lykiadb-lang/src/parser/mod.rs +++ b/lykiadb-lang/src/parser/mod.rs @@ -585,7 +585,7 @@ impl<'a> Parser<'a> { fn expect_get_path(&mut self, initial: Box, tok: TokenType) -> ParseResult> { let mut expr = initial; - + loop { if self.match_next(&sym!(LeftParen)) { expr = self.finish_call(expr)?; @@ -600,7 +600,7 @@ impl<'a> Parser<'a> { } else { break; } - }; + } Ok(expr) } @@ -609,11 +609,12 @@ impl<'a> Parser<'a> { let expr = self.primary()?; if let Expr::Variable { name, span, id } = expr.as_ref() { - if !name.dollar - { + if !name.dollar { let next_tok = &self.peek_bw(0).tok_type; - if (next_tok == &sym!(Dot) || next_tok != &sym!(LeftParen)) && self.in_select_depth > 0 { + if (next_tok == &sym!(Dot) || next_tok != &sym!(LeftParen)) + && self.in_select_depth > 0 + { let head = name.clone(); let mut tail: Vec = vec![]; while self.match_next(&sym!(Dot)) { @@ -628,11 +629,11 @@ impl<'a> Parser<'a> { })); } - return Ok(self.expect_get_path(expr, sym!(DoubleColon))?); + return self.expect_get_path(expr, sym!(DoubleColon)); } } - Ok(self.expect_get_path(expr, sym!(Dot))?) + self.expect_get_path(expr, sym!(Dot)) } fn finish_call(&mut self, callee: Box) -> ParseResult> { @@ -777,7 +778,7 @@ impl<'a> Parser<'a> { } fn expected(&mut self, expected_tok_type: &TokenType) -> ParseResult<&Token> { - if self.cmp_tok(&expected_tok_type) { + if self.cmp_tok(expected_tok_type) { return Ok(self.advance()); }; let prev_token = self.peek_bw(1); diff --git a/lykiadb-lang/src/tokenizer/scanner.rs b/lykiadb-lang/src/tokenizer/scanner.rs index 7f72f3a..83d2c9b 100644 --- a/lykiadb-lang/src/tokenizer/scanner.rs +++ b/lykiadb-lang/src/tokenizer/scanner.rs @@ -231,7 +231,7 @@ impl<'a> Scanner<'a> { fn scan_double_token(&mut self, start: usize, c: char) -> Token { self.advance(); - + if self.match_next(':') && c == ':' { Token { tok_type: sym!(DoubleColon), diff --git a/lykiadb-server/src/engine/interpreter.rs b/lykiadb-server/src/engine/interpreter.rs index 75f259f..f7c9f1a 100644 --- a/lykiadb-server/src/engine/interpreter.rs +++ b/lykiadb-server/src/engine/interpreter.rs @@ -663,6 +663,12 @@ pub struct Output { out: Vec, } +impl Default for Output { + fn default() -> Self { + Self::new() + } +} + impl Output { pub fn new() -> Output { Output { out: Vec::new() } @@ -685,7 +691,13 @@ impl Output { } // TODO(vck): Remove this pub fn expect_str(&mut self, rv: Vec) { - assert_eq!(self.out.iter().map(|x| x.to_string()).collect::>(), rv) + assert_eq!( + self.out + .iter() + .map(|x| x.to_string()) + .collect::>(), + rv + ) } } diff --git a/lykiadb-server/src/engine/mod.rs b/lykiadb-server/src/engine/mod.rs index 71a7412..e502fb9 100644 --- a/lykiadb-server/src/engine/mod.rs +++ b/lykiadb-server/src/engine/mod.rs @@ -45,11 +45,11 @@ impl Runtime { } pub mod test_helpers { + use pretty_assertions::assert_eq; use std::collections::HashMap; use std::sync::Arc; - use pretty_assertions::assert_eq; - use crate::engine::{Runtime, RuntimeMode, Interpreter, error::ExecutionError}; + use crate::engine::{error::ExecutionError, Interpreter, Runtime, RuntimeMode}; use crate::util::{alloc_shared, Shared}; use crate::value::RV; @@ -60,13 +60,19 @@ pub mod test_helpers { runtime: Runtime, } + impl Default for RuntimeTester { + fn default() -> Self { + Self::new() + } + } + impl RuntimeTester { pub fn new() -> RuntimeTester { let out = alloc_shared(Output::new()); - RuntimeTester { + RuntimeTester { out: out.clone(), - runtime: Runtime::new(RuntimeMode::File, Interpreter::new(Some(out), true)), + runtime: Runtime::new(RuntimeMode::File, Interpreter::new(Some(out), true)), } } @@ -74,7 +80,6 @@ pub mod test_helpers { let parts: Vec<&str> = input.split("#[").collect(); for part in parts[1..].iter() { - let mut tester = RuntimeTester::new(); let directives_and_input = part.trim(); @@ -111,37 +116,50 @@ pub mod test_helpers { } fn run_case(&mut self, case_parts: Vec, flags: HashMap<&str, &str>) { - - assert!(case_parts.len() > 1, "Expected at least one input/output pair"); - + assert!( + case_parts.len() > 1, + "Expected at least one input/output pair" + ); + let mut errors: Vec = vec![]; - + let result = self.runtime.interpret(&case_parts[0]); - + if let Err(err) = result { errors.push(err); } - + for part in &case_parts[1..] { if part.starts_with("err") { - assert_eq!(errors.iter().map(|x| x.to_string()).collect::>().join("\n"), part[3..].trim()); - } - - else if part.starts_with(">") { + assert_eq!( + errors + .iter() + .map(|x| x.to_string()) + .collect::>() + .join("\n"), + part[3..].trim() + ); + } else if part.starts_with('>') { let result = self.runtime.interpret(part[1..].trim()); - + if let Err(err) = result { errors.push(err); } - } - else if flags.get("run") == Some(&"plan") { + } else if flags.get("run") == Some(&"plan") { // TODO(vck): Remove this - self.out.write().unwrap().expect(vec![RV::Str(Arc::new(part.to_string()))]); + self.out + .write() + .unwrap() + .expect(vec![RV::Str(Arc::new(part.to_string()))]); + } else { + self.out.write().unwrap().expect_str( + part.to_string() + .split('\n') + .map(|x| x.to_string()) + .collect(), + ); } - else { - self.out.write().unwrap().expect_str(part.to_string().split("\n").map(|x| x.to_string()).collect()); - } } } } -} \ No newline at end of file +} diff --git a/lykiadb-server/src/engine/stdlib/fib.rs b/lykiadb-server/src/engine/stdlib/fib.rs index 5eb6658..e58b183 100644 --- a/lykiadb-server/src/engine/stdlib/fib.rs +++ b/lykiadb-server/src/engine/stdlib/fib.rs @@ -21,3 +21,79 @@ pub fn nt_fib(_interpreter: &mut Interpreter, args: &[RV]) -> Result { + assert!(e.to_string().contains("Unexpected argument")); + } + _ => panic!("Expected InterpretError"), + } + } + + #[test] + fn test_negative_input() { + let mut interpreter = Interpreter::new(None, true); + + // Negative numbers should return themselves as per implementation + assert_eq!( + nt_fib(&mut interpreter, &[RV::Num(-1.0)]).unwrap(), + RV::Num(-1.0) + ); + assert_eq!( + nt_fib(&mut interpreter, &[RV::Num(-5.0)]).unwrap(), + RV::Num(-5.0) + ); + } +} diff --git a/lykiadb-server/src/engine/stdlib/json.rs b/lykiadb-server/src/engine/stdlib/json.rs index 222d2a3..0f65eb0 100644 --- a/lykiadb-server/src/engine/stdlib/json.rs +++ b/lykiadb-server/src/engine/stdlib/json.rs @@ -36,3 +36,145 @@ pub fn nt_json_decode(_interpreter: &mut Interpreter, args: &[RV]) -> Result Interpreter { + Interpreter::new(Some(alloc_shared(Output::new())), true) + } + + #[test] + fn test_json_encode() { + let mut interpreter = setup(); + + // Test primitive values + assert_eq!( + nt_json_encode(&mut interpreter, &[RV::Num(42.0)]).unwrap(), + RV::Str(Arc::new("42.0".to_string())) + ); + + assert_eq!( + nt_json_encode(&mut interpreter, &[RV::Bool(true)]).unwrap(), + RV::Str(Arc::new("true".to_string())) + ); + + assert_eq!( + nt_json_encode(&mut interpreter, &[RV::Str(Arc::new("hello".to_string()))]).unwrap(), + RV::Str(Arc::new("\"hello\"".to_string())) + ); + + assert_eq!( + nt_json_encode(&mut interpreter, &[RV::Null]).unwrap(), + RV::Str(Arc::new("null".to_string())) + ); + + // Test array + let mut arr = Vec::new(); + arr.push(RV::Num(1.0)); + arr.push(RV::Str(Arc::new("test".to_string()))); + let array_rv = RV::Array(alloc_shared(arr)); + + assert_eq!( + nt_json_encode(&mut interpreter, &[array_rv]).unwrap(), + RV::Str(Arc::new("[1.0,\"test\"]".to_string())) + ); + + // Test object + let mut map = FxHashMap::default(); + map.insert("key".to_string(), RV::Num(123.0)); + map.insert("msg".to_string(), RV::Str(Arc::new("value".to_string()))); + let object_rv = RV::Object(alloc_shared(map)); + + assert_eq!( + nt_json_encode(&mut interpreter, &[object_rv]).unwrap(), + RV::Str(Arc::new("{\"key\":123.0,\"msg\":\"value\"}".to_string())) + ); + } + + #[test] + fn test_json_decode() { + let mut interpreter = setup(); + + // Test primitive values + assert_eq!( + nt_json_decode(&mut interpreter, &[RV::Str(Arc::new("42.0".to_string()))]).unwrap(), + RV::Num(42.0) + ); + + assert_eq!( + nt_json_decode(&mut interpreter, &[RV::Str(Arc::new("true".to_string()))]).unwrap(), + RV::Bool(true) + ); + + assert_eq!( + nt_json_decode( + &mut interpreter, + &[RV::Str(Arc::new("\"hello\"".to_string()))] + ) + .unwrap(), + RV::Str(Arc::new("hello".to_string())) + ); + + assert_eq!( + nt_json_decode(&mut interpreter, &[RV::Str(Arc::new("null".to_string()))]).unwrap(), + RV::Null + ); + + // Test array + let array_result = nt_json_decode( + &mut interpreter, + &[RV::Str(Arc::new("[1.0, \"test\"]".to_string()))], + ) + .unwrap(); + + if let RV::Array(arr) = array_result { + let arr = arr.read().unwrap(); + assert_eq!(arr.len(), 2); + assert_eq!(arr[0], RV::Num(1.0)); + assert_eq!(arr[1], RV::Str(Arc::new("test".to_string()))); + } else { + panic!("Expected array result"); + } + + // Test object + let object_result = nt_json_decode( + &mut interpreter, + &[RV::Str(Arc::new( + "{\"key\": 123.0, \"msg\": \"value\"}".to_string(), + ))], + ) + .unwrap(); + + if let RV::Object(obj) = object_result { + let obj = obj.read().unwrap(); + assert_eq!(obj.len(), 2); + assert_eq!(obj.get("key").unwrap(), &RV::Num(123.0)); + assert_eq!( + obj.get("msg").unwrap(), + &RV::Str(Arc::new("value".to_string())) + ); + } else { + panic!("Expected object result"); + } + + // Test error cases + assert!(matches!( + nt_json_decode(&mut interpreter, &[RV::Num(42.0)]), + Err(HaltReason::Error(_)) + )); + + assert!(matches!( + nt_json_decode( + &mut interpreter, + &[RV::Str(Arc::new("invalid json".to_string()))] + ), + Err(HaltReason::Error(_)) + )); + } +} diff --git a/lykiadb-server/src/engine/stdlib/time.rs b/lykiadb-server/src/engine/stdlib/time.rs index c8dbec0..3d5fe50 100644 --- a/lykiadb-server/src/engine/stdlib/time.rs +++ b/lykiadb-server/src/engine/stdlib/time.rs @@ -8,3 +8,29 @@ pub fn nt_clock(_interpreter: &mut Interpreter, _args: &[RV]) -> Result Interpreter { + Interpreter::new(Some(alloc_shared(Output::new())), true) + } + + #[test] + fn test_nt_clock() { + let mut interpreter = setup(); + + // Test clock function + let result = nt_clock(&mut interpreter, &[]); + assert!(result.is_ok()); + + let clock = result.unwrap(); + if let RV::Num(_) = clock { + // Clock function returns a number + } else { + panic!("Expected number result from clock function"); + } + } +} diff --git a/lykiadb-server/src/plan/mod.rs b/lykiadb-server/src/plan/mod.rs index 69cf058..4c9fed8 100644 --- a/lykiadb-server/src/plan/mod.rs +++ b/lykiadb-server/src/plan/mod.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt::Display}; +use std::fmt::Display; use lykiadb_lang::{ ast::{ @@ -39,9 +39,7 @@ pub enum Aggregate { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum IntermediateExpr { Constant(RV), - Expr { - expr: Expr, - }, + Expr { expr: Expr }, } impl Display for IntermediateExpr { diff --git a/lykiadb-server/src/plan/planner.rs b/lykiadb-server/src/plan/planner.rs index 29e88c9..c42311c 100644 --- a/lykiadb-server/src/plan/planner.rs +++ b/lykiadb-server/src/plan/planner.rs @@ -15,8 +15,7 @@ use lykiadb_lang::{ Spanned, }; -use super::{scope::Scope, - IntermediateExpr, Node, Plan, PlannerError}; +use super::{scope::Scope, IntermediateExpr, Node, Plan, PlannerError}; pub struct Planner<'a> { interpreter: &'a mut Interpreter, @@ -49,8 +48,7 @@ impl<'a> Planner<'a> { // WHERE if let Some(predicate) = &core.r#where { - let (expr, subqueries): ( - IntermediateExpr, Vec) = + let (expr, subqueries): (IntermediateExpr, Vec) = self.build_expr(predicate.as_ref(), true, false)?; node = Node::Filter { source: Box::new(node), @@ -67,7 +65,7 @@ impl<'a> Planner<'a> { if core.projection.as_slice() != [SqlProjection::All { collection: None }] { for projection in &core.projection { if let SqlProjection::Expr { expr, .. } = projection { - self.build_expr(&expr, false, true)?; + self.build_expr(expr, false, true)?; } } node = Node::Projection { @@ -98,8 +96,7 @@ impl<'a> Planner<'a> { expr: &Expr, allow_subqueries: bool, allow_aggregates: bool, - ) -> Result<( - IntermediateExpr, Vec), HaltReason> { + ) -> Result<(IntermediateExpr, Vec), HaltReason> { // TODO(vck): Implement this let mut subqueries: Vec = vec![]; @@ -110,7 +107,11 @@ impl<'a> Planner<'a> { None } Expr::FieldPath { head, tail, .. } => { - println!("FieldPath {} {}", head.to_string(), tail.iter().map(|x| x.to_string()).collect::()); + println!( + "FieldPath {} {}", + head, + tail.iter().map(|x| x.to_string()).collect::() + ); None } Expr::Call { callee, args, .. } => { @@ -134,12 +135,7 @@ impl<'a> Planner<'a> { return Err(err); } - Ok(( - IntermediateExpr::Expr { - expr: expr.clone(), - }, - subqueries, - )) + Ok((IntermediateExpr::Expr { expr: expr.clone() }, subqueries)) } fn build_select(&mut self, query: &SqlSelect) -> Result { diff --git a/lykiadb-server/src/value/mod.rs b/lykiadb-server/src/value/mod.rs index bf0d1a7..507af14 100644 --- a/lykiadb-server/src/value/mod.rs +++ b/lykiadb-server/src/value/mod.rs @@ -185,3 +185,142 @@ impl<'de> Deserialize<'de> for RV { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + #[test] + fn test_rv_as_bool() { + // Test numeric values + assert!(!RV::Num(0.0).as_bool()); + assert!(RV::Num(1.0).as_bool()); + assert!(RV::Num(-1.0).as_bool()); + assert!(!RV::Num(f64::NAN).as_bool()); + + // Test strings + assert!(!RV::Str(Arc::new("".to_string())).as_bool()); + assert!(RV::Str(Arc::new("hello".to_string())).as_bool()); + + // Test booleans + assert!(RV::Bool(true).as_bool()); + assert!(!RV::Bool(false).as_bool()); + + // Test special values + assert!(!RV::Null.as_bool()); + assert!(!RV::Undefined.as_bool()); + assert!(!RV::NaN.as_bool()); + + // Test collections + let empty_array = RV::Array(alloc_shared(Vec::new())); + let empty_object = RV::Object(alloc_shared(FxHashMap::default())); + assert!(empty_array.as_bool()); + assert!(empty_object.as_bool()); + } + + #[test] + fn test_rv_as_number() { + // Test numeric values + assert_eq!(RV::Num(42.0).as_number(), Some(42.0)); + assert_eq!(RV::Num(-42.0).as_number(), Some(-42.0)); + assert_eq!(RV::Num(0.0).as_number(), Some(0.0)); + + // Test booleans + assert_eq!(RV::Bool(true).as_number(), Some(1.0)); + assert_eq!(RV::Bool(false).as_number(), Some(0.0)); + + // Test strings + assert_eq!(RV::Str(Arc::new("42".to_string())).as_number(), Some(42.0)); + assert_eq!( + RV::Str(Arc::new("-42".to_string())).as_number(), + Some(-42.0) + ); + assert_eq!(RV::Str(Arc::new("invalid".to_string())).as_number(), None); + assert_eq!(RV::Str(Arc::new("".to_string())).as_number(), None); + + // Test other types + assert_eq!(RV::Null.as_number(), None); + assert_eq!(RV::Undefined.as_number(), None); + assert_eq!(RV::NaN.as_number(), None); + assert_eq!(RV::Array(alloc_shared(Vec::new())).as_number(), None); + assert_eq!( + RV::Object(alloc_shared(FxHashMap::default())).as_number(), + None + ); + } + + #[test] + fn test_rv_is_in() { + // Test string contains + let haystack = RV::Str(Arc::new("hello world".to_string())); + let needle = RV::Str(Arc::new("world".to_string())); + assert_eq!(needle.is_in(&haystack), RV::Bool(true)); + + let not_found = RV::Str(Arc::new("xyz".to_string())); + assert_eq!(not_found.is_in(&haystack), RV::Bool(false)); + + // Test array contains + let mut arr = Vec::new(); + arr.push(RV::Num(1.0)); + arr.push(RV::Str(Arc::new("test".to_string()))); + let array = RV::Array(alloc_shared(arr)); + + assert_eq!(RV::Num(1.0).is_in(&array), RV::Bool(true)); + assert_eq!(RV::Num(2.0).is_in(&array), RV::Bool(false)); + assert_eq!( + RV::Str(Arc::new("test".to_string())).is_in(&array), + RV::Bool(true) + ); + + // Test object key contains + let mut map = FxHashMap::default(); + map.insert("key".to_string(), RV::Num(1.0)); + let object = RV::Object(alloc_shared(map)); + + assert_eq!( + RV::Str(Arc::new("key".to_string())).is_in(&object), + RV::Bool(true) + ); + assert_eq!( + RV::Str(Arc::new("missing".to_string())).is_in(&object), + RV::Bool(false) + ); + } + + #[test] + fn test_rv_not() { + assert_eq!(RV::Bool(true).not(), RV::Bool(false)); + assert_eq!(RV::Bool(false).not(), RV::Bool(true)); + assert_eq!(RV::Num(0.0).not(), RV::Bool(true)); + assert_eq!(RV::Num(1.0).not(), RV::Bool(false)); + assert_eq!(RV::Str(Arc::new("".to_string())).not(), RV::Bool(true)); + assert_eq!( + RV::Str(Arc::new("hello".to_string())).not(), + RV::Bool(false) + ); + assert_eq!(RV::Null.not(), RV::Bool(true)); + assert_eq!(RV::Undefined.not(), RV::Bool(true)); + assert_eq!(RV::NaN.not(), RV::Bool(true)); + } + + #[test] + fn test_rv_display() { + assert_eq!(RV::Str(Arc::new("hello".to_string())).to_string(), "hello"); + assert_eq!(RV::Num(42.0).to_string(), "42"); + assert_eq!(RV::Bool(true).to_string(), "true"); + assert_eq!(RV::Bool(false).to_string(), "false"); + assert_eq!(RV::Undefined.to_string(), "undefined"); + assert_eq!(RV::NaN.to_string(), "NaN"); + assert_eq!(RV::Null.to_string(), "null"); + + let mut arr = Vec::new(); + arr.push(RV::Num(1.0)); + arr.push(RV::Str(Arc::new("test".to_string()))); + assert_eq!(RV::Array(alloc_shared(arr)).to_string(), "[1, test]"); + + let mut map = FxHashMap::default(); + map.insert("key".to_string(), RV::Num(42.0)); + assert_eq!(RV::Object(alloc_shared(map)).to_string(), "{key: 42}"); + } +} diff --git a/lykiadb-server/tests/tests.rs b/lykiadb-server/tests/tests.rs index 72f0337..50ecb15 100644 --- a/lykiadb-server/tests/tests.rs +++ b/lykiadb-server/tests/tests.rs @@ -10,4 +10,4 @@ mod interpreter { use test_each_file::test_each_file; test_each_file! { in "lykiadb-server/tests/interpreter" => RuntimeTester::test_file } -} \ No newline at end of file +}