diff --git a/stdlib/src/sqlite.rs b/stdlib/src/sqlite.rs index e8bab60d..20fd035d 100644 --- a/stdlib/src/sqlite.rs +++ b/stdlib/src/sqlite.rs @@ -39,6 +39,205 @@ impl<'a> rusqlite::types::ToSql for SqlParam<'a> { } } +fn sqlite_open( + _ctx: &mut techscript_runtime::RuntimeContext, + args: Vec, +) -> Result { + let path = args[0].to_string(); + let conn = rusqlite::Connection::open(&path).map_err(|e| { + RuntimeError::new( + RuntimeErrorKind::InvalidOperation(e.to_string()), + None, + None, + ) + })?; + let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); + CONNECTIONS.with(|m| m.borrow_mut().insert(id, conn)); + Ok(RuntimeValue::Int(id)) +} + +fn sqlite_execute( + _ctx: &mut techscript_runtime::RuntimeContext, + args: Vec, +) -> Result { + let id = args[0].try_into_int().map_err(|_| { + RuntimeError::new( + RuntimeErrorKind::InvalidOperation("expected int handle".to_string()), + None, + None, + ) + })?; + let sql = args[1].to_string(); + let _items_borrow; + let params: Vec = if let Some(arg) = args.get(2) { + if let RuntimeValue::List { items, .. } = arg { + _items_borrow = items.borrow(); + _items_borrow.iter().map(SqlParam).collect() + } else { + return Err(RuntimeError::new( + RuntimeErrorKind::TypeMismatch { + expected: "list".to_string(), + found: arg.runtime_type().to_string(), + }, + None, + None, + )); + } + } else { + Vec::new() + }; + + CONNECTIONS.with(|m| { + let mut map = m.borrow_mut(); + if let Some(conn) = map.get_mut(&id) { + let mut stmt = conn.prepare_cached(&sql).map_err(|e| { + RuntimeError::new( + RuntimeErrorKind::InvalidOperation(e.to_string()), + None, + None, + ) + })?; + stmt.execute(rusqlite::params_from_iter(params)) + .map_err(|e| { + RuntimeError::new( + RuntimeErrorKind::InvalidOperation(e.to_string()), + None, + None, + ) + })?; + Ok(()) + } else { + Err(RuntimeError::new( + RuntimeErrorKind::InvalidOperation("Invalid connection handle".to_string()), + None, + None, + )) + } + })?; + Ok(RuntimeValue::Null) +} + +fn sqlite_query( + _ctx: &mut techscript_runtime::RuntimeContext, + args: Vec, +) -> Result { + let id = args[0].try_into_int().map_err(|_| { + RuntimeError::new( + RuntimeErrorKind::InvalidOperation("expected int handle".to_string()), + None, + None, + ) + })?; + let sql = args[1].to_string(); + let _items_borrow; + let params: Vec = if let Some(arg) = args.get(2) { + if let RuntimeValue::List { items, .. } = arg { + _items_borrow = items.borrow(); + _items_borrow.iter().map(SqlParam).collect() + } else { + return Err(RuntimeError::new( + RuntimeErrorKind::TypeMismatch { + expected: "list".to_string(), + found: arg.runtime_type().to_string(), + }, + None, + None, + )); + } + } else { + Vec::new() + }; + + let rows = CONNECTIONS.with(|m| { + let mut map = m.borrow_mut(); + if let Some(conn) = map.get_mut(&id) { + let mut stmt = conn.prepare_cached(&sql).map_err(|e| { + RuntimeError::new( + RuntimeErrorKind::InvalidOperation(e.to_string()), + None, + None, + ) + })?; + let col_count = stmt.column_count(); + let col_names: Vec = (0..col_count) + .map(|i| stmt.column_name(i).unwrap_or("?").to_string()) + .collect(); + + let mut rows = Vec::new(); + + let mut template = IndexMap::with_capacity(col_count); + for name in &col_names { + template.insert(name.clone(), RuntimeValue::Null); + } + + let row_iter = stmt + .query_map(rusqlite::params_from_iter(params), |row| { + let mut map = template.clone(); + for i in 0..col_count { + let val = row.get_ref_unwrap(i); + let rt_val = match val { + rusqlite::types::ValueRef::Null => RuntimeValue::Null, + rusqlite::types::ValueRef::Integer(v) => RuntimeValue::Int(v), + rusqlite::types::ValueRef::Real(v) => RuntimeValue::Float(v), + rusqlite::types::ValueRef::Text(v) => RuntimeValue::Str( + std::str::from_utf8(v).unwrap_or_default().to_string(), + ), + rusqlite::types::ValueRef::Blob(v) => { + RuntimeValue::Str(String::from_utf8_lossy(v).into_owned()) + } + }; + if let Some((_, v)) = map.get_index_mut(i) { + *v = rt_val; + } + } + Ok(map) + }) + .map_err(|e| { + RuntimeError::new( + RuntimeErrorKind::InvalidOperation(e.to_string()), + None, + None, + ) + })?; + for row in row_iter { + if let Ok(map) = row { + rows.push(RuntimeValue::Map { + entries: Rc::new(RefCell::new(map)), + is_const: false, + }); + } + } + Ok(rows) + } else { + Err(RuntimeError::new( + RuntimeErrorKind::InvalidOperation("Invalid connection handle".to_string()), + None, + None, + )) + } + })?; + + Ok(RuntimeValue::List { + items: Rc::new(RefCell::new(rows)), + is_const: false, + }) +} + +fn sqlite_close( + _ctx: &mut techscript_runtime::RuntimeContext, + args: Vec, +) -> Result { + let id = args[0].try_into_int().map_err(|_| { + RuntimeError::new( + RuntimeErrorKind::InvalidOperation("expected int handle".to_string()), + None, + None, + ) + })?; + CONNECTIONS.with(|m| m.borrow_mut().remove(&id)); + Ok(RuntimeValue::Null) +} + impl StdlibRegistry { pub fn register_sqlite(&mut self) { let mut exports: HashMap> = @@ -49,19 +248,7 @@ impl StdlibRegistry { Rc::new(StdFunction { name: "open".to_string(), arity: 1, - callback: |_ctx, args| { - let path = args[0].to_string(); - let conn = rusqlite::Connection::open(&path).map_err(|e| { - RuntimeError::new( - RuntimeErrorKind::InvalidOperation(e.to_string()), - None, - None, - ) - })?; - let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); - CONNECTIONS.with(|m| m.borrow_mut().insert(id, conn)); - Ok(RuntimeValue::Int(id)) - }, + callback: sqlite_open, }), ); @@ -70,65 +257,7 @@ impl StdlibRegistry { Rc::new(StdFunction { name: "execute".to_string(), arity: 2, - callback: |_ctx, args| { - let id = args[0].try_into_int().map_err(|_| { - RuntimeError::new( - RuntimeErrorKind::InvalidOperation("expected int handle".to_string()), - None, - None, - ) - })?; - let sql = args[1].to_string(); - let _items_borrow; - let params: Vec = if let Some(arg) = args.get(2) { - if let RuntimeValue::List { items, .. } = arg { - _items_borrow = items.borrow(); - _items_borrow.iter().map(SqlParam).collect() - } else { - return Err(RuntimeError::new( - RuntimeErrorKind::TypeMismatch { - expected: "list".to_string(), - found: arg.runtime_type().to_string(), - }, - None, - None, - )); - } - } else { - Vec::new() - }; - - CONNECTIONS.with(|m| { - let mut map = m.borrow_mut(); - if let Some(conn) = map.get_mut(&id) { - let mut stmt = conn.prepare_cached(&sql).map_err(|e| { - RuntimeError::new( - RuntimeErrorKind::InvalidOperation(e.to_string()), - None, - None, - ) - })?; - stmt.execute(rusqlite::params_from_iter(params)) - .map_err(|e| { - RuntimeError::new( - RuntimeErrorKind::InvalidOperation(e.to_string()), - None, - None, - ) - })?; - Ok(()) - } else { - Err(RuntimeError::new( - RuntimeErrorKind::InvalidOperation( - "Invalid connection handle".to_string(), - ), - None, - None, - )) - } - })?; - Ok(RuntimeValue::Null) - }, + callback: sqlite_execute, }), ); @@ -137,120 +266,7 @@ impl StdlibRegistry { Rc::new(StdFunction { name: "query".to_string(), arity: 2, - callback: |_ctx, args| { - let id = args[0].try_into_int().map_err(|_| { - RuntimeError::new( - RuntimeErrorKind::InvalidOperation("expected int handle".to_string()), - None, - None, - ) - })?; - let sql = args[1].to_string(); - let _items_borrow; - let params: Vec = if let Some(arg) = args.get(2) { - if let RuntimeValue::List { items, .. } = arg { - _items_borrow = items.borrow(); - _items_borrow.iter().map(SqlParam).collect() - } else { - return Err(RuntimeError::new( - RuntimeErrorKind::TypeMismatch { - expected: "list".to_string(), - found: arg.runtime_type().to_string(), - }, - None, - None, - )); - } - } else { - Vec::new() - }; - - let rows = CONNECTIONS.with(|m| { - let mut map = m.borrow_mut(); - if let Some(conn) = map.get_mut(&id) { - let mut stmt = conn.prepare_cached(&sql).map_err(|e| { - RuntimeError::new( - RuntimeErrorKind::InvalidOperation(e.to_string()), - None, - None, - ) - })?; - let col_count = stmt.column_count(); - let col_names: Vec = (0..col_count) - .map(|i| stmt.column_name(i).unwrap_or("?").to_string()) - .collect(); - - let mut rows = Vec::new(); - - let mut template = IndexMap::with_capacity(col_count); - for name in &col_names { - template.insert(name.clone(), RuntimeValue::Null); - } - - let row_iter = stmt - .query_map(rusqlite::params_from_iter(params), |row| { - let mut map = template.clone(); - for i in 0..col_count { - let val = row.get_ref_unwrap(i); - let rt_val = match val { - rusqlite::types::ValueRef::Null => RuntimeValue::Null, - rusqlite::types::ValueRef::Integer(v) => { - RuntimeValue::Int(v) - } - rusqlite::types::ValueRef::Real(v) => { - RuntimeValue::Float(v) - } - rusqlite::types::ValueRef::Text(v) => { - RuntimeValue::Str( - std::str::from_utf8(v) - .unwrap_or_default() - .to_string(), - ) - } - rusqlite::types::ValueRef::Blob(v) => { - RuntimeValue::Str( - String::from_utf8_lossy(v).into_owned(), - ) - } - }; - if let Some((_, v)) = map.get_index_mut(i) { - *v = rt_val; - } - } - Ok(map) - }) - .map_err(|e| { - RuntimeError::new( - RuntimeErrorKind::InvalidOperation(e.to_string()), - None, - None, - ) - })?; - for row in row_iter { - if let Ok(map) = row { - rows.push(RuntimeValue::Map { - entries: Rc::new(RefCell::new(map)), - is_const: false, - }); - } - } - Ok(rows) - } else { - Err(RuntimeError::new( - RuntimeErrorKind::InvalidOperation( - "Invalid connection handle".to_string(), - ), - None, - None, - )) - } - })?; - - Ok(RuntimeValue::List { - items: Rc::new(RefCell::new(rows)), - is_const: false, - }) - }, + callback: sqlite_query, }), ); @@ -259,17 +275,7 @@ impl StdlibRegistry { Rc::new(StdFunction { name: "close".to_string(), arity: 1, - callback: |_ctx, args| { - let id = args[0].try_into_int().map_err(|_| { - RuntimeError::new( - RuntimeErrorKind::InvalidOperation("expected int handle".to_string()), - None, - None, - ) - })?; - CONNECTIONS.with(|m| m.borrow_mut().remove(&id)); - Ok(RuntimeValue::Null) - }, + callback: sqlite_close, }), ); @@ -284,3 +290,110 @@ impl StdlibRegistry { ); } } + +#[cfg(test)] +mod tests { + use super::*; + use techscript_runtime::context::{RuntimeConfig, RuntimeContext}; + + #[test] + fn test_register_sqlite() { + let mut registry = StdlibRegistry::default(); + registry.register_sqlite(); + + assert!(registry.has_module("std.sqlite")); + let module = registry.get_module("std.sqlite").unwrap(); + assert_eq!(module.name, "std.sqlite"); + assert_eq!(module.version, "1.0.0"); + assert!(module.exports.contains_key("open")); + assert!(module.exports.contains_key("execute")); + assert!(module.exports.contains_key("query")); + assert!(module.exports.contains_key("close")); + assert_eq!(module.exports.len(), 4); + } + + #[test] + fn test_sqlite_flow() { + let mut registry = StdlibRegistry::default(); + registry.register_sqlite(); + + let module = registry.get_module("std.sqlite").unwrap(); + let open_fn = module.exports.get("open").unwrap(); + let execute_fn = module.exports.get("execute").unwrap(); + let query_fn = module.exports.get("query").unwrap(); + let close_fn = module.exports.get("close").unwrap(); + + let mut ctx = RuntimeContext::new(RuntimeConfig::default()); + + // Open in-memory sqlite connection + let handle = open_fn + .call(&mut ctx, vec![RuntimeValue::Str(":memory:".to_string())]) + .unwrap(); + assert!(handle.as_int().is_some()); + + // Create table + let res = execute_fn.call( + &mut ctx, + vec![ + handle.clone(), + RuntimeValue::Str( + "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)".to_string(), + ), + ], + ); + assert!(res.is_ok()); + + // Insert row with parameters + let res = execute_fn.call( + &mut ctx, + vec![ + handle.clone(), + RuntimeValue::Str("INSERT INTO users (id, name) VALUES (?, ?)".to_string()), + RuntimeValue::List { + items: Rc::new(RefCell::new(vec![ + RuntimeValue::Int(1), + RuntimeValue::Str("Alice".to_string()), + ])), + is_const: false, + }, + ], + ); + assert!(res.is_ok()); + + // Query row + let query_res = query_fn + .call( + &mut ctx, + vec![ + handle.clone(), + RuntimeValue::Str("SELECT id, name FROM users WHERE id = ?".to_string()), + RuntimeValue::List { + items: Rc::new(RefCell::new(vec![RuntimeValue::Int(1)])), + is_const: false, + }, + ], + ) + .unwrap(); + + if let RuntimeValue::List { items, .. } = query_res { + let list = items.borrow(); + assert_eq!(list.len(), 1); + if let RuntimeValue::Map { entries, .. } = &list[0] { + let map = entries.borrow(); + assert_eq!(map.get("id"), Some(&RuntimeValue::Int(1))); + assert_eq!( + map.get("name"), + Some(&RuntimeValue::Str("Alice".to_string())) + ); + } else { + panic!("Expected row map"); + } + } else { + panic!("Expected list result"); + } + + // Close connection + let close_res = close_fn.call(&mut ctx, vec![handle]); + assert!(close_res.is_ok()); + } +}