use crate::connection::SochConnection;
use crate::error::Result;
use crate::transaction::ClientTransaction;
use std::collections::HashMap;
use sochdb_core::soch::SochValue;
#[derive(Debug, Clone)]
pub struct InsertResult {
pub rows_inserted: usize,
pub last_id: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct UpdateResult {
pub rows_updated: usize,
}
#[derive(Debug, Clone)]
pub struct DeleteResult {
pub rows_deleted: usize,
}
#[derive(Debug, Clone)]
pub struct UpsertResult {
pub rows_upserted: usize,
pub rows_inserted: usize,
pub rows_updated: usize,
}
pub struct RowBuilder<'a> {
conn: &'a SochConnection,
table: String,
rows: Vec<HashMap<String, SochValue>>,
current: HashMap<String, SochValue>,
}
impl<'a> RowBuilder<'a> {
pub fn new(conn: &'a SochConnection, table: &str) -> Self {
Self {
conn,
table: table.to_string(),
rows: Vec::new(),
current: HashMap::new(),
}
}
pub fn set(mut self, field: &str, value: impl Into<SochValue>) -> Self {
self.current.insert(field.to_string(), value.into());
self
}
pub fn row(mut self) -> Self {
if !self.current.is_empty() {
self.rows.push(std::mem::take(&mut self.current));
}
self
}
pub fn execute(mut self) -> Result<InsertResult> {
if !self.current.is_empty() {
self.rows.push(std::mem::take(&mut self.current));
}
let rows_inserted = self.rows.len();
let mut last_id = None;
{
let mut tch = self.conn.tch.write();
for row in &self.rows {
let id = tch.insert_row(&self.table, row);
last_id = Some(id);
if let Ok(value) = bincode::serialize(row) {
let key = format!("{}:{}", self.table, id);
let _ = self.conn.put(key.as_bytes().to_vec(), value);
}
}
}
Ok(InsertResult {
rows_inserted,
last_id,
})
}
}
pub struct UpdateBuilder<'a> {
conn: &'a SochConnection,
table: String,
updates: HashMap<String, SochValue>,
where_clause: Option<crate::connection::WhereClause>,
}
pub use crate::connection::{CompareOp, UpsertAction, WhereClause};
impl<'a> UpdateBuilder<'a> {
pub fn new(conn: &'a SochConnection, table: &str) -> Self {
Self {
conn,
table: table.to_string(),
updates: HashMap::new(),
where_clause: None,
}
}
pub fn set(mut self, field: &str, value: impl Into<SochValue>) -> Self {
self.updates.insert(field.to_string(), value.into());
self
}
pub fn where_eq(mut self, field: &str, value: impl Into<SochValue>) -> Self {
self.where_clause = Some(WhereClause::Simple {
field: field.to_string(),
op: CompareOp::Eq,
value: value.into(),
});
self
}
pub fn where_cond(mut self, field: &str, op: CompareOp, value: impl Into<SochValue>) -> Self {
self.where_clause = Some(WhereClause::Simple {
field: field.to_string(),
op,
value: value.into(),
});
self
}
pub fn execute(self) -> Result<UpdateResult> {
let mut tch = self.conn.tch.write();
let mutation_result = tch.update_rows(&self.table, &self.updates, self.where_clause.as_ref());
Ok(UpdateResult { rows_updated: mutation_result.affected_count })
}
}
pub struct DeleteBuilder<'a> {
conn: &'a SochConnection,
table: String,
where_clause: Option<WhereClause>,
}
impl<'a> DeleteBuilder<'a> {
pub fn new(conn: &'a SochConnection, table: &str) -> Self {
Self {
conn,
table: table.to_string(),
where_clause: None,
}
}
pub fn where_eq(mut self, field: &str, value: impl Into<SochValue>) -> Self {
self.where_clause = Some(WhereClause::Simple {
field: field.to_string(),
op: CompareOp::Eq,
value: value.into(),
});
self
}
pub fn where_cond(mut self, field: &str, op: CompareOp, value: impl Into<SochValue>) -> Self {
self.where_clause = Some(WhereClause::Simple {
field: field.to_string(),
op,
value: value.into(),
});
self
}
pub fn execute(self) -> Result<DeleteResult> {
let mut tch = self.conn.tch.write();
let mutation_result = tch.delete_rows(&self.table, self.where_clause.as_ref());
Ok(DeleteResult { rows_deleted: mutation_result.affected_count })
}
}
pub struct FindBuilder<'a> {
conn: &'a SochConnection,
table: String,
columns: Vec<String>,
where_clause: Option<WhereClause>,
order_by: Option<(String, bool)>,
limit: Option<usize>,
offset: Option<usize>,
}
impl<'a> FindBuilder<'a> {
pub fn new(conn: &'a SochConnection, table: &str) -> Self {
Self {
conn,
table: table.to_string(),
columns: Vec::new(),
where_clause: None,
order_by: None,
limit: None,
offset: None,
}
}
pub fn select(mut self, columns: &[&str]) -> Self {
self.columns = columns.iter().map(|s| s.to_string()).collect();
self
}
pub fn where_eq(mut self, field: &str, value: impl Into<SochValue>) -> Self {
self.where_clause = Some(WhereClause::Simple {
field: field.to_string(),
op: CompareOp::Eq,
value: value.into(),
});
self
}
pub fn where_cond(mut self, field: &str, op: CompareOp, value: impl Into<SochValue>) -> Self {
self.where_clause = Some(WhereClause::Simple {
field: field.to_string(),
op,
value: value.into(),
});
self
}
pub fn order_by(mut self, column: &str, ascending: bool) -> Self {
self.order_by = Some((column.to_string(), ascending));
self
}
pub fn limit(mut self, n: usize) -> Self {
self.limit = Some(n);
self
}
pub fn offset(mut self, n: usize) -> Self {
self.offset = Some(n);
self
}
pub fn execute(self) -> Result<Vec<HashMap<String, SochValue>>> {
let tch = self.conn.tch.read();
let mut cursor = tch.select(
&self.table,
&self.columns,
self.where_clause.as_ref(),
self.order_by.as_ref(),
self.limit,
self.offset,
);
let mut rows = Vec::new();
while let Some(row) = cursor.next() {
rows.push(row);
}
Ok(rows)
}
pub fn first(mut self) -> Result<Option<HashMap<String, SochValue>>> {
self.limit = Some(1);
let rows = self.execute()?;
Ok(rows.into_iter().next())
}
pub fn all(self) -> Result<Vec<HashMap<String, SochValue>>> {
self.execute()
}
}
pub struct UpsertBuilder<'a> {
conn: &'a SochConnection,
table: String,
conflict_key: String,
rows: Vec<HashMap<String, SochValue>>,
current: HashMap<String, SochValue>,
}
impl<'a> UpsertBuilder<'a> {
pub fn new(conn: &'a SochConnection, table: &str, conflict_key: &str) -> Self {
Self {
conn,
table: table.to_string(),
conflict_key: conflict_key.to_string(),
rows: Vec::new(),
current: HashMap::new(),
}
}
pub fn set(mut self, field: &str, value: impl Into<SochValue>) -> Self {
self.current.insert(field.to_string(), value.into());
self
}
pub fn row(mut self) -> Self {
if !self.current.is_empty() {
self.rows.push(std::mem::take(&mut self.current));
}
self
}
pub fn execute(mut self) -> Result<UpsertResult> {
if !self.current.is_empty() {
self.rows.push(std::mem::take(&mut self.current));
}
let mut rows_inserted = 0;
let mut rows_updated = 0;
{
let mut tch = self.conn.tch.write();
for row in self.rows {
match tch.upsert_row(&self.table, &self.conflict_key, &row) {
UpsertAction::Inserted => rows_inserted += 1,
UpsertAction::Updated => rows_updated += 1,
}
}
}
Ok(UpsertResult {
rows_upserted: rows_inserted + rows_updated,
rows_inserted,
rows_updated,
})
}
}
impl SochConnection {
pub fn insert_into<'a>(&'a self, table: &str) -> RowBuilder<'a> {
RowBuilder::new(self, table)
}
pub fn update<'a>(&'a self, table: &str) -> UpdateBuilder<'a> {
UpdateBuilder::new(self, table)
}
pub fn delete_from<'a>(&'a self, table: &str) -> DeleteBuilder<'a> {
DeleteBuilder::new(self, table)
}
pub fn find<'a>(&'a self, table: &str) -> FindBuilder<'a> {
FindBuilder::new(self, table)
}
pub fn upsert<'a>(&'a self, table: &str, conflict_key: &str) -> UpsertBuilder<'a> {
UpsertBuilder::new(self, table, conflict_key)
}
pub fn insert_one(
&self,
table: &str,
values: HashMap<String, SochValue>,
) -> Result<InsertResult> {
let mut builder = RowBuilder::new(self, table);
for (k, v) in values {
builder = builder.set(&k, v);
}
builder.execute()
}
pub fn find_by_id(
&self,
table: &str,
id_field: &str,
id: impl Into<SochValue>,
) -> Result<Option<HashMap<String, SochValue>>> {
self.find(table).where_eq(id_field, id).first()
}
pub fn delete_by_id(
&self,
table: &str,
id_field: &str,
id: impl Into<SochValue>,
) -> Result<DeleteResult> {
self.delete_from(table).where_eq(id_field, id).execute()
}
pub fn count(&self, table: &str) -> Result<u64> {
let tch = self.tch.read();
Ok(tch.count_rows(table))
}
pub fn exists(&self, table: &str, field: &str, value: impl Into<SochValue>) -> Result<bool> {
let row = self.find(table).where_eq(field, value).first()?;
Ok(row.is_some())
}
}
impl<'a> ClientTransaction<'a> {
pub fn insert(&mut self, table: &str, values: HashMap<String, SochValue>) -> Result<u64> {
let mut tch = self.conn.tch.write();
let id = tch.insert_row(table, &values);
Ok(id)
}
pub fn update_where(
&mut self,
table: &str,
updates: HashMap<String, SochValue>,
field: &str,
value: impl Into<SochValue>,
) -> Result<usize> {
let mut tch = self.conn.tch.write();
let where_clause = WhereClause::Simple {
field: field.to_string(),
op: CompareOp::Eq,
value: value.into(),
};
let mutation_result = tch.update_rows(table, &updates, Some(&where_clause));
Ok(mutation_result.affected_count)
}
pub fn delete_where(
&mut self,
table: &str,
field: &str,
value: impl Into<SochValue>,
) -> Result<usize> {
let mut tch = self.conn.tch.write();
let where_clause = WhereClause::Simple {
field: field.to_string(),
op: CompareOp::Eq,
value: value.into(),
};
let mutation_result = tch.delete_rows(table, Some(&where_clause));
Ok(mutation_result.affected_count)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_builder() {
let conn = SochConnection::open("./test").unwrap();
let result = conn
.insert_into("users")
.set("id", SochValue::Int(1))
.set("name", SochValue::Text("Alice".to_string()))
.execute()
.unwrap();
assert_eq!(result.rows_inserted, 1);
}
#[test]
fn test_multi_row_insert() {
let conn = SochConnection::open("./test").unwrap();
let result = conn
.insert_into("users")
.set("id", SochValue::Int(1))
.set("name", SochValue::Text("Alice".to_string()))
.row()
.set("id", SochValue::Int(2))
.set("name", SochValue::Text("Bob".to_string()))
.execute()
.unwrap();
assert_eq!(result.rows_inserted, 2);
}
#[test]
fn test_update_builder() {
let conn = SochConnection::open("./test").unwrap();
let result = conn
.update("users")
.set("name", SochValue::Text("New Name".to_string()))
.where_eq("id", SochValue::Int(1))
.execute()
.unwrap();
let _ = result.rows_updated; }
#[test]
fn test_delete_builder() {
let conn = SochConnection::open("./test").unwrap();
let result = conn
.delete_from("users")
.where_eq("id", SochValue::Int(999))
.execute()
.unwrap();
let _ = result.rows_deleted; }
#[test]
fn test_find_builder() {
let conn = SochConnection::open("./test").unwrap();
let result = conn
.find("users")
.select(&["id", "name"])
.where_eq("active", SochValue::Bool(true))
.order_by("name", true)
.limit(10)
.execute();
assert!(result.is_ok());
}
#[test]
fn test_upsert() {
let conn = SochConnection::open("./test").unwrap();
let result = conn
.upsert("users", "id")
.set("id", SochValue::Int(1))
.set("name", SochValue::Text("Alice".to_string()))
.execute()
.unwrap();
assert!(result.rows_upserted >= 1);
}
#[test]
fn test_convenience_methods() {
let conn = SochConnection::open("./test").unwrap();
let _user = conn.find_by_id("users", "id", SochValue::Int(1));
let _exists = conn.exists(
"users",
"email",
SochValue::Text("test@test.com".to_string()),
);
let _count = conn.count("users");
}
}