diff --git a/lykiadb-lang/src/ast/expr.rs b/lykiadb-lang/src/ast/expr.rs index e1a8442..f9ce980 100644 --- a/lykiadb-lang/src/ast/expr.rs +++ b/lykiadb-lang/src/ast/expr.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::sync::Arc; +use std::{fmt::Display, sync::Arc}; use crate::{Identifier, Span, Spanned}; @@ -396,3 +396,51 @@ impl AstNode for Expr { } } } + +impl Display for Expr { + // A basic display function for Expr + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Expr::Select { .. } => write!(f, ""), + Expr::Insert { .. } => write!(f, ""), + Expr::Update { .. } => write!(f, ""), + Expr::Delete { .. } => write!(f, ""), + Expr::Variable { name, .. } => write!(f, "{}", name), + Expr::Grouping { expr, .. } => write!(f, "({})", expr), + Expr::Literal { value, .. } => write!(f, "{:?}", value), + Expr::Function { name, parameters, .. } => { + write!(f, "fn {}({})", name.as_ref().unwrap(), parameters.iter().map(|x| x.to_string()).collect::>().join(", ")) + } + Expr::Between { + lower, + upper, + subject, + kind, + .. + } => write!( + f, + "{} {} {} AND {}", + subject, + match kind { + RangeKind::Between => "BETWEEN", + RangeKind::NotBetween => "NOT BETWEEN", + }, + lower, + upper + ), + Expr::Binary { left, operation, right, .. } => { + write!(f, "({} {:?} {})", left, operation, right) + } + Expr::Unary { operation, expr, .. } => write!(f, "{:?}{}", operation, expr), + Expr::Assignment { dst, expr, .. } => write!(f, "{} = {}", dst, expr), + Expr::Logical { left, operation, right, .. } => { + write!(f, "{} {:?} {}", left, operation, right) + } + Expr::Call { callee, args, .. } => { + write!(f, "{}({})", callee, args.iter().map(|x| x.to_string()).collect::>().join(", ")) + } + Expr::Get { object, name, .. } => write!(f, "{}.{}", object, name), + Expr::Set { object, name, value, .. } => write!(f, "{}.{} = {}", object, name, value), + } + } +} \ No newline at end of file diff --git a/lykiadb-lang/src/lib.rs b/lykiadb-lang/src/lib.rs index 80062eb..34f8e0c 100644 --- a/lykiadb-lang/src/lib.rs +++ b/lykiadb-lang/src/lib.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{fmt::{Display, Formatter, Result}, sync::Arc}; use ast::expr::Expr; use rustc_hash::FxHashMap; @@ -71,3 +71,9 @@ pub struct Identifier { #[serde(skip)] pub span: Span, } + +impl Display for Identifier { + fn fmt(&self, f: &mut Formatter) -> Result { + write!(f, "{}", self.name) + } +} \ No newline at end of file diff --git a/lykiadb-lang/src/parser/program.rs b/lykiadb-lang/src/parser/program.rs index ede128b..1237720 100644 --- a/lykiadb-lang/src/parser/program.rs +++ b/lykiadb-lang/src/parser/program.rs @@ -1,10 +1,13 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::{ - ast::{expr::Expr, stmt::Stmt}, - Locals, + ast::{expr::Expr, stmt::Stmt}, tokenizer::scanner::Scanner, Locals, Scopes }; + +use super::{resolver::Resolver, ParseError, ParseResult, Parser}; #[derive(Serialize, Deserialize)] pub struct Program { root: Box, @@ -47,3 +50,20 @@ impl Program { serde_json::to_value(self.root.clone()).unwrap() } } + +impl FromStr for Program { + type Err = ParseError; + + fn from_str(s: &str) -> ParseResult { + let tokens = Scanner::scan(s).unwrap(); + let parse_result = Parser::parse(&tokens); + + if let Ok(mut program) = parse_result { + let mut resolver = Resolver::new(Scopes::default(), &program, None); + let (_, locals) = resolver.resolve().unwrap(); + program.set_locals(locals); + return Ok(program); + } + panic!("Failed to parse program."); + } +} diff --git a/lykiadb-playground/app/editor.tsx b/lykiadb-playground/app/editor.tsx index fbba791..d44a1db 100644 --- a/lykiadb-playground/app/editor.tsx +++ b/lykiadb-playground/app/editor.tsx @@ -13,29 +13,12 @@ await init(); const EditorView = () => { const [code, setCode] = React.useState( -`var $calc = { - add: function ($a, $b) { - return $a + $b; - }, - sub: function ($a, $b) { - return $a - $b; - }, - mul: function ($a, $b) { - return $a * $b; - }, - div: function ($a, $b) { - return $a / $b; - }, -}; -print($calc.add(4, 5)); -print($calc.sub(4, 5)); -print($calc.mul(4, 5)); -print($calc.div(4, 5)); +`SELECT { 'name': user.name } from users; `); const [ast, setAst] = React.useState({}); - const [sizes, setSizes] = React.useState([100, '30%', 'auto']); + const [sizes, setSizes] = React.useState(['50%', '50%']); function updateCode(code: string) { setCode(code) @@ -49,25 +32,25 @@ print($calc.div(4, 5)); } return ( - - -
Script
+ + +
updateCode(value)} />
-
+
- -
Syntax tree
-
+ +
AST
+
-
+
); diff --git a/lykiadb-playground/app/globals.css b/lykiadb-playground/app/globals.css index 875c01e..dea4298 100644 --- a/lykiadb-playground/app/globals.css +++ b/lykiadb-playground/app/globals.css @@ -11,8 +11,8 @@ @media (prefers-color-scheme: dark) { :root { --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background-start-rgb: 56, 66, 96; + --background-end-rgb: 56, 66, 96; } } diff --git a/lykiadb-playground/app/lyqlSyntax.scss b/lykiadb-playground/app/lyqlSyntax.scss index bdda0fb..b22d31b 100644 --- a/lykiadb-playground/app/lyqlSyntax.scss +++ b/lykiadb-playground/app/lyqlSyntax.scss @@ -1,10 +1,10 @@ .cm-{ - $font-size: 14px; + $font-size: 16px; $font-family: 'JetBrains Mono', monospace; &gutters, &editor { font-size: $font-size; font-family: $font-family; - background-color: #222226!important; + background-color: #1c2130!important; border: none!important; } @@ -26,15 +26,15 @@ color:rgb(94, 145, 255) } &identifier { - color:#FB773C; + color:#bbdf06; text-decoration: underline } &keyword{ - color: rgb(74, 216, 255); + color: rgb(122, 255, 228); font-weight: bold; } &sqlkeyword{ - color: #EB3678; + color: #027e44; font-weight: bold; } &symbol { diff --git a/lykiadb-playground/styles/fonts.ts b/lykiadb-playground/styles/fonts.ts index f072291..5a4290d 100644 --- a/lykiadb-playground/styles/fonts.ts +++ b/lykiadb-playground/styles/fonts.ts @@ -1,7 +1,6 @@ -import { Inter, JetBrains_Mono } from 'next/font/google' +import { JetBrains_Mono, Inter } from 'next/font/google' const defaultFont = Inter({ weight: '400', subsets: ['latin'] }) const jetBrainsMono = JetBrains_Mono({ weight: '400', subsets: ['latin'] }) - export { defaultFont, jetBrainsMono } \ No newline at end of file diff --git a/lykiadb-server/src/lib.rs b/lykiadb-server/src/lib.rs index 830f2c8..f9cdea5 100644 --- a/lykiadb-server/src/lib.rs +++ b/lykiadb-server/src/lib.rs @@ -3,3 +3,15 @@ pub mod engine; pub mod plan; pub mod util; pub mod value; + +#[macro_export] +macro_rules! assert_plan { + ($($name:ident: {$field:literal => $value:literal}),*) => { + $( + #[test] + fn $name() { + expect_plan($field, $value); + } + )* + }; +} diff --git a/lykiadb-server/src/plan/mod.rs b/lykiadb-server/src/plan/mod.rs index d0466bf..a341021 100644 --- a/lykiadb-server/src/plan/mod.rs +++ b/lykiadb-server/src/plan/mod.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use lykiadb_lang::{ ast::{ expr::Expr, @@ -84,3 +86,46 @@ pub enum Node { Nothing, } + +impl Display for Plan { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Plan::Select(node) => write!(f, "{}", node), + } + } +} + +impl Node { + const TAB: &'static str = " "; + const NEWLINE: &'static str = "\n"; + + fn _fmt_recursive(&self, f: &mut std::fmt::Formatter<'_>, indent: usize) -> std::fmt::Result { + let indent_str = Self::TAB.repeat(indent); + match self { + Node::Filter { source, predicate } => { + write!(f, "{}- filter {}:{}", indent_str, predicate, Self::NEWLINE)?; + source._fmt_recursive(f, indent + 1) + } + Node::Scan { source, filter } => { + write!(f, "{}- scan [{} as {}]{}", indent_str, source.name, source.alias.as_ref().unwrap_or(&source.name), Self::NEWLINE) + } + Node::Join { + left, + join_type, + right, + constraint, + } => { + write!(f, "{}- join [{:?}, {}]:{}", indent_str, join_type, constraint.as_ref().unwrap(), Self::NEWLINE)?; + left._fmt_recursive(f, indent + 1)?; + right._fmt_recursive(f, indent + 1) + } + _ => "".fmt(f), + } + } +} + +impl Display for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self._fmt_recursive(f, 0) + } +} \ No newline at end of file diff --git a/lykiadb-server/src/plan/planner.rs b/lykiadb-server/src/plan/planner.rs index 3e1c4e0..b12a8b1 100644 --- a/lykiadb-server/src/plan/planner.rs +++ b/lykiadb-server/src/plan/planner.rs @@ -26,7 +26,6 @@ impl Planner { id: _, } => { let plan = Plan::Select(self.build_select(query)?); - println!("{}", serde_json::to_value(&plan).unwrap()); Ok(plan) } _ => panic!("Not implemented yet."), @@ -40,11 +39,11 @@ impl Planner { node = self.build_from(from)?; } // WHERE - if let Some(where_clause) = &query.core.r#where { + if let Some(predicate) = &query.core.r#where { // TODO: Traverse expression node = Node::Filter { source: Box::new(node), - predicate: *where_clause.clone(), + predicate: *predicate.clone(), } } // GROUP BY @@ -95,3 +94,24 @@ impl Planner { } } } + + +pub mod test_helpers { + use lykiadb_lang::{ast::stmt::Stmt, parser::program::Program}; + + use super::Planner; + + pub fn expect_plan(query: &str, expected_plan: &str) { + let mut planner = Planner::new(); + let program = query.parse::().unwrap(); + match *program.get_root() { + Stmt::Program { body, .. } if matches!(body.get(0), Some(Stmt::Expression { .. })) => { + if let Some(Stmt::Expression { expr, .. }) = body.get(0) { + let generated_plan = planner.build(&expr).unwrap(); + assert_eq!(expected_plan, generated_plan.to_string()); + } + } + _ => panic!("Expected expression statement."), + } + } +} diff --git a/lykiadb-server/tests/planner/join.rs b/lykiadb-server/tests/planner/join.rs new file mode 100644 index 0000000..0638b19 --- /dev/null +++ b/lykiadb-server/tests/planner/join.rs @@ -0,0 +1,19 @@ +use lykiadb_server::{assert_plan, plan::planner::test_helpers::expect_plan}; + + +assert_plan! { + three_way_simple: { + "SELECT * FROM books b + INNER JOIN categories c ON b.category_id = c.id + INNER JOIN publishers AS p ON b.publisher_id = p.id + WHERE p.name = 'Springer';" => + +"- filter (p.name IsEqual Str(\"Springer\")): + - join [Inner, (b.publisher_id IsEqual p.id)]: + - join [Inner, (b.category_id IsEqual c.id)]: + - scan [books as b] + - scan [categories as c] + - scan [publishers as p] +" + } +} diff --git a/lykiadb-server/tests/planner/mod.rs b/lykiadb-server/tests/planner/mod.rs new file mode 100644 index 0000000..76e7a06 --- /dev/null +++ b/lykiadb-server/tests/planner/mod.rs @@ -0,0 +1 @@ +mod join; \ No newline at end of file diff --git a/lykiadb-server/tests/tests.rs b/lykiadb-server/tests/tests.rs index cd5f5e7..7f033b5 100644 --- a/lykiadb-server/tests/tests.rs +++ b/lykiadb-server/tests/tests.rs @@ -1,3 +1,4 @@ #![recursion_limit = "192"] mod runtime; +mod planner; \ No newline at end of file