//! BaraDB Rust Client //! //! Binary protocol client for BaraDB database. //! //! # Example //! ```no_run //! use baradb::Client; //! //! let mut client = Client::connect("localhost", 9472).unwrap(); //! let result = client.query("SELECT name FROM users WHERE age > 18").unwrap(); //! for row in result.rows() { //! println!("{}", row["name"]); //! } //! client.close(); //! ``` use std::collections::HashMap; use std::io::{Read, Write}; use std::net::TcpStream; // Wire protocol constants const MK_QUERY: u32 = 0x02; const MK_READY: u32 = 0x81; const MK_DATA: u32 = 0x82; const MK_COMPLETE: u32 = 0x83; const MK_ERROR: u32 = 0x84; const MK_PING: u32 = 0x08; const FK_NULL: u8 = 0x00; const FK_BOOL: u8 = 0x01; const FK_INT8: u8 = 0x02; const FK_INT16: u8 = 0x03; const FK_INT32: u8 = 0x04; const FK_INT64: u8 = 0x05; const FK_FLOAT32: u8 = 0x06; const FK_FLOAT64: u8 = 0x07; const FK_STRING: u8 = 0x08; const FK_BYTES: u8 = 0x09; const FK_ARRAY: u8 = 0x0A; const FK_OBJECT: u8 = 0x0B; const FK_VECTOR: u8 = 0x0C; /// Connection configuration #[derive(Debug, Clone)] pub struct Config { pub host: String, pub port: u16, pub database: String, pub username: String, pub password: String, } impl Default for Config { fn default() -> Self { Config { host: "127.0.0.1".to_string(), port: 9472, database: "default".to_string(), username: "admin".to_string(), password: String::new(), } } } /// Query result #[derive(Debug)] pub struct QueryResult { columns: Vec, rows: Vec>, affected_rows: usize, } impl QueryResult { pub fn columns(&self) -> &[String] { &self.columns } pub fn rows(&self) -> &[HashMap] { &self.rows } pub fn row_count(&self) -> usize { self.rows.len() } pub fn affected_rows(&self) -> usize { self.affected_rows } } /// BaraDB client pub struct Client { config: Config, stream: TcpStream, connected: bool, request_id: u32, } impl Client { /// Connect to BaraDB server pub fn connect(host: &str, port: u16) -> Result> { let config = Config { host: host.to_string(), port, ..Default::default() }; Self::connect_with_config(config) } /// Connect with custom configuration pub fn connect_with_config(config: Config) -> Result> { let addr = format!("{}:{}", config.host, config.port); let stream = TcpStream::connect(&addr)?; Ok(Client { config, stream, connected: true, request_id: 0, }) } /// Close the connection pub fn close(&mut self) { self.connected = false; } /// Check if connected pub fn is_connected(&self) -> bool { self.connected } fn next_id(&mut self) -> u32 { self.request_id += 1; self.request_id } /// Execute a BaraQL query pub fn query(&mut self, sql: &str) -> Result> { if !self.connected { return Err("Not connected".into()); } let payload = encode_string(sql); let msg = build_message(MK_QUERY, self.next_id(), &payload); self.stream.write_all(&msg)?; // Read header let mut header = [0u8; 12]; self.stream.read_exact(&mut header)?; let kind = u32::from_be_bytes([header[0], header[1], header[2], header[3]]); let length = u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize; // Read payload let mut payload = vec![0u8; length]; if length > 0 { self.stream.read_exact(&mut payload)?; } match kind { MK_READY => Ok(QueryResult { columns: vec![], rows: vec![], affected_rows: 0 }), MK_DATA => { let mut pos = 0usize; let col_count = read_u32(&payload, &mut pos) as usize; let mut columns = Vec::with_capacity(col_count); for _ in 0..col_count { columns.push(read_string(&payload, &mut pos)); } let row_count = read_u32(&payload, &mut pos) as usize; let mut rows = Vec::with_capacity(row_count); for _ in 0..row_count { let mut row = HashMap::new(); for c in 0..col_count { let val = read_value(&payload, &mut pos); row.insert(columns[c].clone(), val); } rows.push(row); } // Read following COMPLETE message let mut comp_header = [0u8; 12]; self.stream.read_exact(&mut comp_header)?; let comp_kind = u32::from_be_bytes([comp_header[0], comp_header[1], comp_header[2], comp_header[3]]); let comp_len = u32::from_be_bytes([comp_header[4], comp_header[5], comp_header[6], comp_header[7]]) as usize; let mut comp_payload = vec![0u8; comp_len]; if comp_len > 0 { self.stream.read_exact(&mut comp_payload)?; } let affected = if comp_kind == MK_COMPLETE && comp_payload.len() >= 4 { u32::from_be_bytes([comp_payload[0], comp_payload[1], comp_payload[2], comp_payload[3]]) as usize } else { 0 }; Ok(QueryResult { columns, rows, affected_rows: affected }) } MK_COMPLETE => { let affected = if payload.len() >= 4 { u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize } else { 0 }; Ok(QueryResult { columns: vec![], rows: vec![], affected_rows: affected }) } MK_ERROR => Err("Query error".into()), _ => Err(format!("Unknown response kind: {}", kind).into()), } } /// Execute a statement pub fn execute(&mut self, sql: &str) -> Result> { let result = self.query(sql)?; Ok(result.affected_rows()) } /// Ping the server pub fn ping(&mut self) -> Result<(), Box> { let msg = build_message(MK_PING, self.next_id(), &[]); self.stream.write_all(&msg)?; Ok(()) } } /// Query builder for fluent API pub struct QueryBuilder<'a> { client: &'a mut Client, select_cols: Vec, from_table: String, where_clauses: Vec, joins: Vec, group_by: Vec, having: String, order_by: Vec, limit: usize, offset: usize, } impl<'a> QueryBuilder<'a> { pub fn new(client: &'a mut Client) -> Self { QueryBuilder { client, select_cols: vec![], from_table: String::new(), where_clauses: vec![], joins: vec![], group_by: vec![], having: String::new(), order_by: vec![], limit: 0, offset: 0, } } pub fn select(mut self, cols: &[&str]) -> Self { self.select_cols.extend(cols.iter().map(|s| s.to_string())); self } pub fn from(mut self, table: &str) -> Self { self.from_table = table.to_string(); self } pub fn where_clause(mut self, clause: &str) -> Self { self.where_clauses.push(clause.to_string()); self } pub fn join(mut self, table: &str, on: &str) -> Self { self.joins.push(format!("JOIN {} ON {}", table, on)); self } pub fn left_join(mut self, table: &str, on: &str) -> Self { self.joins.push(format!("LEFT JOIN {} ON {}", table, on)); self } pub fn group_by(mut self, cols: &[&str]) -> Self { self.group_by.extend(cols.iter().map(|s| s.to_string())); self } pub fn having(mut self, clause: &str) -> Self { self.having = clause.to_string(); self } pub fn order_by(mut self, col: &str, dir: &str) -> Self { self.order_by.push(format!("{} {}", col, dir)); self } pub fn limit(mut self, n: usize) -> Self { self.limit = n; self } pub fn offset(mut self, n: usize) -> Self { self.offset = n; self } pub fn build(&self) -> String { let mut sql = String::from("SELECT "); if self.select_cols.is_empty() { sql.push('*'); } else { sql.push_str(&self.select_cols.join(", ")); } sql.push_str(&format!(" FROM {}", self.from_table)); for j in &self.joins { sql.push(' '); sql.push_str(j); } if !self.where_clauses.is_empty() { sql.push_str(&format!(" WHERE {}", self.where_clauses.join(" AND "))); } if !self.group_by.is_empty() { sql.push_str(&format!(" GROUP BY {}", self.group_by.join(", "))); } if !self.having.is_empty() { sql.push_str(&format!(" HAVING {}", self.having)); } if !self.order_by.is_empty() { sql.push_str(&format!(" ORDER BY {}", self.order_by.join(", "))); } if self.limit > 0 { sql.push_str(&format!(" LIMIT {}", self.limit)); } if self.offset > 0 { sql.push_str(&format!(" OFFSET {}", self.offset)); } sql } pub fn exec(self) -> Result> { let sql = self.build(); self.client.query(&sql) } } fn build_message(kind: u32, req_id: u32, payload: &[u8]) -> Vec { let mut msg = Vec::with_capacity(12 + payload.len()); msg.extend_from_slice(&kind.to_be_bytes()); msg.extend_from_slice(&(payload.len() as u32).to_be_bytes()); msg.extend_from_slice(&req_id.to_be_bytes()); msg.extend_from_slice(payload); msg } fn encode_string(s: &str) -> Vec { let bytes = s.as_bytes(); let mut result = Vec::with_capacity(4 + bytes.len()); result.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); result.extend_from_slice(bytes); result } fn read_u32(data: &[u8], pos: &mut usize) -> u32 { let val = u32::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]); *pos += 4; val } fn read_string(data: &[u8], pos: &mut usize) -> String { let len = read_u32(data, pos) as usize; let s = String::from_utf8_lossy(&data[*pos..*pos + len]).to_string(); *pos += len; s } fn read_value(data: &[u8], pos: &mut usize) -> String { let kind = data[*pos]; *pos += 1; match kind { FK_NULL => String::new(), FK_BOOL => { let val = data[*pos] != 0; *pos += 1; val.to_string() } FK_INT8 => { let val = data[*pos] as i8; *pos += 1; val.to_string() } FK_INT16 => { let val = i16::from_be_bytes([data[*pos], data[*pos + 1]]); *pos += 2; val.to_string() } FK_INT32 => { let val = i32::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]); *pos += 4; val.to_string() } FK_INT64 => { let val = i64::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3], data[*pos + 4], data[*pos + 5], data[*pos + 6], data[*pos + 7]]); *pos += 8; val.to_string() } FK_FLOAT32 => { let val = f32::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]); *pos += 4; val.to_string() } FK_FLOAT64 => { let val = f64::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3], data[*pos + 4], data[*pos + 5], data[*pos + 6], data[*pos + 7]]); *pos += 8; val.to_string() } FK_STRING => read_string(data, pos), FK_BYTES => { let len = read_u32(data, pos) as usize; *pos += len; format!("", len) } FK_ARRAY => { let count = read_u32(data, pos); for _ in 0..count { let _ = read_value(data, pos); } format!("", count) } FK_OBJECT => { let count = read_u32(data, pos); for _ in 0..count { let _ = read_string(data, pos); let _ = read_value(data, pos); } format!("", count) } FK_VECTOR => { let dim = read_u32(data, pos); *pos += (dim * 4) as usize; format!("", dim) } _ => String::new(), } }