From 05d241b97a0000736a5109129051d83b623d8a5e Mon Sep 17 00:00:00 2001 From: Shautvast Date: Tue, 6 Jan 2026 22:14:14 +0100 Subject: [PATCH] added first step for db queries --- Cargo.lock | 82 +++++++++++++++--- Cargo.toml | 6 +- README.md | 8 +- examples/web/api/customer/db.tp | 7 +- examples/web/api/customer/service.tp | 8 +- examples/web/api/customer/web.tp | 12 +-- {examples/web => source}/model/customers.tp | 0 src/builtins/globals.rs | 94 +++++++++++++++++++-- src/builtins/mod.rs | 29 ++++--- src/compiler/assembly_pass.rs | 9 +- src/compiler/ast_pass.rs | 13 ++- src/compiler/compiler_tests.rs | 35 +++++--- src/compiler/scan_pass.rs | 9 +- src/compiler/tokens.rs | 2 + src/value.rs | 2 + 15 files changed, 246 insertions(+), 70 deletions(-) rename {examples/web => source}/model/customers.tp (100%) diff --git a/Cargo.lock b/Cargo.lock index 5fccc55..533ae07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,9 +304,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -492,9 +492,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -935,13 +935,13 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.9.4", "libc", - "redox_syscall", + "redox_syscall 0.7.0", ] [[package]] @@ -1207,7 +1207,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -1255,6 +1255,20 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "postgres" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c48ece1c6cda0db61b058c1721378da76855140e9214339fa1317decacb176" +dependencies = [ + "bytes", + "fallible-iterator", + "futures-util", + "log", + "tokio", + "tokio-postgres", +] + [[package]] name = "postgres-protocol" version = "0.6.9" @@ -1280,6 +1294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4605b7c057056dd35baeb6ac0c0338e4975b1f2bef0f65da953285eb007095" dependencies = [ "bytes", + "chrono", "fallible-iterator", "postgres-protocol", ] @@ -1326,6 +1341,27 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_postgres" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd4b47636dbca581cd057e2f27a5d39be741ea4f85fd3c29e415c55f71c7595" +dependencies = [ + "postgres", + "r2d2", +] + [[package]] name = "rand" version = "0.9.2" @@ -1364,6 +1400,15 @@ dependencies = [ "bitflags 2.9.4", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.9.4", +] + [[package]] name = "regex" version = "1.12.2" @@ -1525,6 +1570,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1876,12 +1930,14 @@ dependencies = [ "log", "log4rs", "notify", + "postgres", + "r2d2", + "r2d2_postgres", "regex", "reqwest", "serde", "thiserror", "tokio", - "tokio-postgres", "tower", "tower-http", "tower-livereload", @@ -2147,18 +2203,18 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" diff --git a/Cargo.toml b/Cargo.toml index a94efbd..3113685 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,7 @@ axum = "0.8.6" log4rs = "1.4.0" serde = { version = "1.0.228", features = ["derive"] } tokio = { version = "1.47", features = ["full"] } -tokio-postgres = "0.7" -chrono = "0.4.42" +chrono = "0.4" dotenv = "0.15.0" reqwest = { version = "0.12", features = ["json", "multipart"] } tower-http = { version = "0.6", features = ["fs"] } @@ -26,3 +25,6 @@ clap = { version = "4.5.51", features = ["derive"] } notify = "8.2.0" arc-swap = "1.7.1" regex = "1.12.2" +r2d2 = "0.8.10" +postgres = { version = "0.19", features = ["with-chrono-0_4"] } +r2d2_postgres = "0.18.2" \ No newline at end of file diff --git a/README.md b/README.md index fce769a..b49b35c 100644 --- a/README.md +++ b/README.md @@ -231,4 +231,10 @@ fn add(a: string, b: string) -> string: ISSUES * Make everything an expression. If is a statement and so it can not be type checked -* improve indenting \ No newline at end of file +* improve indenting + +WIP guards +* | /$uuid -> service.get(uuid)? + | /&first_name -> service.get_by_firstname(first_name)? + | /&last_name -> service.get_by_lastname(last_name)? + | 404 \ No newline at end of file diff --git a/examples/web/api/customer/db.tp b/examples/web/api/customer/db.tp index 332397f..eacb294 100644 --- a/examples/web/api/customer/db.tp +++ b/examples/web/api/customer/db.tp @@ -1,5 +1,2 @@ -fn get(id: u32) -> Customer: - select id, first_name, last_name from customers where id = :id - -fn save(c: Customer): - insert into customers values(id, first_name, last_name) values(:c.id, :c.first_name, :c.last_name) +fn get_all() -> list: + sql("select id, first_name, last_name from customers") diff --git a/examples/web/api/customer/service.tp b/examples/web/api/customer/service.tp index 4ffab17..a08ffd0 100644 --- a/examples/web/api/customer/service.tp +++ b/examples/web/api/customer/service.tp @@ -1,6 +1,2 @@ -fn get(id: u32) -> Customer: - let customer = dao.get(id) - customer.date_fetched = current_date - -fn add(customer: Customer): - dao.save(customer) +fn get_all() -> list: + db::get_all() diff --git a/examples/web/api/customer/web.tp b/examples/web/api/customer/web.tp index f85634e..fc64299 100644 --- a/examples/web/api/customer/web.tp +++ b/examples/web/api/customer/web.tp @@ -1,13 +1,5 @@ -fn get(): - | path == "/" -> list: - service.get_all() - | path == "/{uuid}" -> Customer?: - service.get(uuid)? - | path == "/" && query.firstname -> Customer?: - service.get_by_firstname(fname)? - | path == "/" && query.last_name -> Customer? - service.get_by_lastname(lname)? - | 404 +fn get() -> list: + service::get_all() fn post(customer: Customer): service.add(customer) diff --git a/examples/web/model/customers.tp b/source/model/customers.tp similarity index 100% rename from examples/web/model/customers.tp rename to source/model/customers.tp diff --git a/src/builtins/globals.rs b/src/builtins/globals.rs index 371feb6..0a3a806 100644 --- a/src/builtins/globals.rs +++ b/src/builtins/globals.rs @@ -1,18 +1,55 @@ -use std::cell::{RefMut}; use crate::builtins::{FunctionMap, Signature, add}; -use crate::compiler::tokens::TokenType::{DateTime, StringType, Void}; +use crate::compiler::ast_pass::Parameter; +use crate::compiler::tokens::TokenType; +use crate::compiler::tokens::TokenType::{DateTime, ListType, StringType, Void}; use crate::errors::RuntimeError; -use crate::value::Value; +use crate::value::{Value, string}; +use r2d2_postgres::PostgresConnectionManager; +use r2d2_postgres::postgres::NoTls; +use r2d2_postgres::postgres::types::Type; +use std::cell::RefMut; use std::collections::HashMap; use std::sync::LazyLock; -use crate::compiler::ast_pass::Parameter; + +pub(crate) static DB_POOL: LazyLock>> = + LazyLock::new(|| { + let manager = PostgresConnectionManager::new( + "host=localhost user=postgres dbname=postgres password=boompje".parse().unwrap(), + NoTls, + ); + + r2d2::Pool::builder() + .max_size(15) + .build(manager) + .expect("Failed to create pool") + }); pub(crate) static GLOBAL_FUNCTIONS: LazyLock = LazyLock::new(|| { let mut global_functions: FunctionMap = HashMap::new(); let functions = &mut global_functions; add(functions, "now", Signature::new(vec![], DateTime, now)); - add(functions, "print", Signature::new(vec![Parameter::new("text", StringType)], Void, print)); - add(functions, "println", Signature::new(vec![Parameter::new("text", StringType)], Void, println)); + add( + functions, + "print", + Signature::new(vec![Parameter::new("text", StringType)], Void, print), + ); + add( + functions, + "println", + Signature::new(vec![Parameter::new("text", StringType)], Void, println), + ); + add( + functions, + "sql", + Signature::new( + vec![ + Parameter::new("query", StringType), + Parameter::varargs(TokenType::Any), + ], + ListType, + sql, + ), + ); global_functions }); @@ -31,7 +68,46 @@ fn print(_self_val: RefMut, args: Vec) -> Result, _args: Vec) -> Result { - Ok(Value::DateTime(Box::new( - chrono::Utc::now(), - ))) + Ok(Value::DateTime(Box::new(chrono::Utc::now()))) +} + +fn sql(_self_val: RefMut, args: Vec) -> Result { + let mut conn = DB_POOL.get().unwrap(); + let result = conn.query(&args[0].to_string(), &[]).unwrap(); + let mut columns = vec![]; + + if let Some(first_row) = result.first() { + let retrieved_columns = first_row.columns(); + for column in retrieved_columns { + columns.push((column.name(), column.type_())); + } + } + + let mut rows = vec![]; + for result_row in &result { + let mut row: HashMap = HashMap::new(); + for (index, column) in result_row.columns().iter().enumerate() { + let column_name = columns.get(index).unwrap().0; + let column_type = columns.get(index).unwrap().1; + let value = match column_type { + &Type::BOOL => Value::Bool(result_row.get(index)), + &Type::INT2 | &Type::INT4 => Value::I32(result_row.get(index)), + &Type::INT8 => Value::I64(result_row.get(index)), + &Type::FLOAT4 => Value::F32(result_row.get(index)), + &Type::FLOAT8 | &Type::NUMERIC => Value::F64(result_row.get(index)), + &Type::CHAR | &Type::VARCHAR | &Type::TEXT | &Type::BPCHAR | &Type::NAME => { + Value::String(result_row.get(index)) + } + &Type::TIMESTAMP | &Type::TIMESTAMPTZ => { + Value::DateTime(Box::new(result_row.get(index))) + } + &Type::UUID => Value::Uuid(result_row.get(index)), + _ => unimplemented!("database type {:?}", column_type), + }; + + row.insert(string(column_name), value); + } + rows.push(Value::Map(row)); + } + Ok(Value::List(rows)) } diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index c98c450..a08a497 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -1,19 +1,19 @@ -mod string; -mod list; pub(crate) mod globals; +mod list; mod map; +mod string; -use std::cell::{RefCell, RefMut}; +use crate::builtins::list::list_functions; +use crate::builtins::map::map_functions; use crate::builtins::string::string_functions; -use crate::errors::{CompilerError, RuntimeError}; +use crate::compiler::ast_pass::Parameter; use crate::compiler::tokens::TokenType; +use crate::errors::{CompilerError, RuntimeError}; use crate::value::Value; +use std::cell::{RefCell, RefMut}; use std::collections::HashMap; use std::rc::Rc; use std::sync::LazyLock; -use crate::compiler::ast_pass::Parameter; -use crate::builtins::list::list_functions; -use crate::builtins::map::map_functions; pub(crate) struct Signature { pub(crate) parameters: Vec, @@ -37,6 +37,10 @@ impl Signature { pub(crate) fn arity(&self) -> u8 { self.parameters.len() as u8 } + + pub(crate) fn has_varargs(&self) -> bool { + self.parameters.last().unwrap().name.lexeme == "varargs" + } } pub(crate) type FunctionFn = fn(RefMut, Vec) -> Result; @@ -58,8 +62,11 @@ pub(crate) fn add(m: &mut FunctionMap, name: &str, method: Signature) { m.insert(name.to_string(), method); } -pub(crate) fn lookup(type_name: &str, method_name: &str) -> Result<&'static Signature, CompilerError> { - FUNCTIONS +pub(crate) fn lookup( + type_name: &str, + method_name: &str, +) -> Result<&'static Signature, CompilerError> { + FUNCTIONS .get(type_name) .and_then(|methods| methods.get(method_name)) .ok_or_else(|| CompilerError::FunctionNotFound(format!("{}.{}", type_name, method_name))) @@ -71,7 +78,9 @@ pub(crate) fn call( self_val: Rc>, args: Vec, ) -> Result { - (lookup(type_name,method_name).map_err(|e|RuntimeError::FunctionNotFound(e.to_string()))?.function)(self_val.borrow_mut(), args) + (lookup(type_name, method_name) + .map_err(|e| RuntimeError::FunctionNotFound(e.to_string()))? + .function)(self_val.borrow_mut(), args) } pub(crate) fn expected(expected_type: &str) -> RuntimeError { diff --git a/src/compiler/assembly_pass.rs b/src/compiler/assembly_pass.rs index b8dfb1b..4d0400d 100644 --- a/src/compiler/assembly_pass.rs +++ b/src/compiler/assembly_pass.rs @@ -295,7 +295,12 @@ impl AsmPass { let name_index = self.chunk.find_constant(name).unwrap_or_else(|| { self.chunk.add_constant(Value::String(name.to_string())) }); - self.emit(Call(name_index, fun.arity())); + let arity = if fun.has_varargs() { + arguments.len() as u8 + } else { + fun.arity() + }; + self.emit(Call(name_index, arity)); } else { return Err(self .error_at_line(CompilerError::FunctionNotFound(name.to_string()))); @@ -325,7 +330,7 @@ impl AsmPass { }); let signature = lookup(&receiver_type, method_name).map_err(|e| self.error_at_line(e))?; - if signature.arity() != arguments.len() as u8 { + if signature.arity() != arguments.len() as u8 && !signature.has_varargs() { return Err(self.error_at_line(CompilerError::IllegalArgumentsException( format!("{}.{}", receiver_type, method_name), signature.parameters.len(), diff --git a/src/compiler/ast_pass.rs b/src/compiler/ast_pass.rs index 015fd15..07bde5c 100644 --- a/src/compiler/ast_pass.rs +++ b/src/compiler/ast_pass.rs @@ -891,13 +891,24 @@ impl Parameter { pub fn new(name: impl Into, value_type: TokenType) -> Self { Self { name: Token { - token_type: TokenType::StringType, + token_type: StringType, lexeme: name.into(), line: 0, }, var_type: value_type, } } + + pub fn varargs(token_type: TokenType) -> Self { + Self { + name: Token { + token_type: StringType, + lexeme: "varargs".into(), + line: 0, + }, + var_type: token_type, + } + } } #[derive(Debug, Clone)] diff --git a/src/compiler/compiler_tests.rs b/src/compiler/compiler_tests.rs index 5a1fed6..265c243 100644 --- a/src/compiler/compiler_tests.rs +++ b/src/compiler/compiler_tests.rs @@ -6,7 +6,7 @@ mod tests { use crate::errors::CompilerErrorAtLine; use crate::errors::RuntimeError::{IllegalArgumentException, IndexOutOfBounds, NotSortable}; use crate::errors::TipiLangError::{Compiler, Runtime}; - use crate::value::{Value, string, i64}; + use crate::value::{Value, i64, string}; use chrono::DateTime; #[test] @@ -476,15 +476,30 @@ a"#) } // #[test] // fn package() { - // assert_eq!(run(r#"a.b.c()"#), Ok(Value::U32(48))); + // assert_eq!(run(r#"a::b.c()"#), Ok(Value::U32(48))); // } - // #[test] - // fn guards() { - // assert_eq!( - // run(r#"fn get_all_users() -> list: - // | /{uuid} -> service.get_by_uuid(uuid)?"#), - // Ok(Value::Void) - // ); - // } + #[test] + fn guards() { + assert_eq!( + run(r#"fn get_all_users() -> list: + | /{uuid} -> service.get_by_uuid(uuid)?"#), + Ok(Value::Void) + ); + } + + #[test] + fn query() { + let result = + run(r#"sql("select id, first_name, last_name from customers where id = 1")"#).unwrap(); + if let Value::List(records) = result { + assert_eq!(records.len(), 1); + if let Value::Map(record) = records.get(0).unwrap() { + assert_eq!(record.get(&string("id")).unwrap(), &i64(1)); + assert_eq!(record.get(&string("first_name")).unwrap(), &string("sander")); + assert_eq!(record.get(&string("last_name")).unwrap(), &string("hautvast")); + } + } + } + } diff --git a/src/compiler/scan_pass.rs b/src/compiler/scan_pass.rs index b5b40e6..c822f2e 100644 --- a/src/compiler/scan_pass.rs +++ b/src/compiler/scan_pass.rs @@ -87,7 +87,14 @@ impl Scanner { } '#' => self.add_token(TokenType::Hash), '+' => self.add_token(TokenType::Plus), - ':' => self.add_token(TokenType::Colon), + ':' => { + let t = if self.match_next(':') { + TokenType::ColonColon + } else { + TokenType::Colon + }; + self.add_token(t); + } ';' => println!("Warning: Ignoring semicolon at line {}", self.line), '*' => self.add_token(TokenType::Star), '!' => { diff --git a/src/compiler/tokens.rs b/src/compiler/tokens.rs index 872c2c5..e6f6a65 100644 --- a/src/compiler/tokens.rs +++ b/src/compiler/tokens.rs @@ -28,6 +28,7 @@ pub enum TokenType { Bool, Char, Colon, + ColonColon, Comma, DateTime, Dot, @@ -113,6 +114,7 @@ impl fmt::Display for TokenType { TokenType::Pipe => write!(f, "|"), TokenType::BitXor => write!(f, "^"), TokenType::Colon => write!(f, ":"), + TokenType::ColonColon => write!(f, "::"), TokenType::Comma => write!(f, ","), TokenType::FloatingPoint => write!(f, "float"), TokenType::MapType => write!(f, "map"), diff --git a/src/value.rs b/src/value.rs index 5d3919a..473131c 100644 --- a/src/value.rs +++ b/src/value.rs @@ -31,6 +31,7 @@ pub enum Value { ObjectType(Box), Error(String), Void, + Uuid(String), } pub(crate) fn string(v: impl Into) -> Value { @@ -217,6 +218,7 @@ impl Display for Value { Value::Map(map) => to_string(f, map), Value::Error(v) => write!(f, "{}", v), Value::Void => write!(f, "()"), + Value::Uuid(v) => write!(f, "{}", v), } } }