refatored vault

This commit is contained in:
2025-09-24 11:32:11 +02:00
parent d5670ca470
commit 1a40f9d2aa
16 changed files with 2364 additions and 1481 deletions

View File

@ -1,246 +1,536 @@
// database/core.rs
// src-tauri/src/database/core.rs
use std::collections::HashMap;
use crate::database::error::DatabaseError;
use crate::database::DbConnection;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use rusqlite::types::Value as SqlValue;
use rusqlite::{
types::{Value as RusqliteValue, ValueRef},
Connection, OpenFlags, ToSql,
};
use serde_json::Value as JsonValue;
use tauri::State;
// --- Hilfsfunktion: Konvertiert JSON Value zu etwas, das rusqlite versteht ---
// Diese Funktion ist etwas knifflig wegen Ownership und Lifetimes.
// Eine einfachere Variante ist oft, direkt rusqlite::types::Value zu erstellen.
// Hier ein Beispiel, das owned Values erstellt (braucht evtl. Anpassung je nach rusqlite-Version/Nutzung)
fn json_to_rusqlite_value(json_val: &JsonValue) -> Result<RusqliteValue, String> {
match json_val {
JsonValue::Null => Ok(RusqliteValue::Null),
JsonValue::Bool(b) => Ok(RusqliteValue::Integer(*b as i64)), // SQLite hat keinen BOOLEAN
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(RusqliteValue::Integer(i))
} else if let Some(f) = n.as_f64() {
Ok(RusqliteValue::Real(f))
} else {
Err("Ungültiger Zahlenwert".to_string())
}
}
JsonValue::String(s) => Ok(RusqliteValue::Text(s.clone())),
JsonValue::Array(_) | JsonValue::Object(_) => {
// SQLite kann Arrays/Objects nicht direkt speichern (außer als TEXT/BLOB)
// Konvertiere sie zu JSON-Strings, wenn das gewünscht ist
Ok(RusqliteValue::Text(
serde_json::to_string(json_val).map_err(|e| e.to_string())?,
))
// Oder gib einen Fehler zurück, wenn Arrays/Objekte nicht erlaubt sind
// Err("Arrays oder Objekte werden nicht direkt als Parameter unterstützt".to_string())
}
}
}
pub async fn execute(
sql: String,
params: Vec<JsonValue>,
state: &State<'_, DbConnection>,
) -> Result<usize, String> {
// Gibt Anzahl betroffener Zeilen zurück
let params_converted: Vec<RusqliteValue> = params
.iter()
.map(json_to_rusqlite_value)
.collect::<Result<Vec<_>, _>>()?;
let params_sql: Vec<&dyn ToSql> = params_converted.iter().map(|v| v as &dyn ToSql).collect();
let db_lock = state
.0
.lock()
.map_err(|e| format!("Mutex Lock Fehler: {}", e))?;
let conn = db_lock.as_ref().ok_or("Keine Datenbankverbindung")?;
let affected_rows = conn
.execute(&sql, &params_sql[..])
.map_err(|e| format!("SQL Execute Fehler: {}", e))?;
Ok(affected_rows)
}
pub async fn select(
sql: String,
params: Vec<JsonValue>,
state: &State<'_, DbConnection>,
) -> Result<Vec<Vec<JsonValue>>, String> {
// Ergebnis als Vec<RowObject>
// Konvertiere JSON Params zu rusqlite Values für die Abfrage
// Wir sammeln sie als owned Values, da `params_from_iter` Referenzen braucht,
// was mit lokalen Konvertierungen schwierig ist.
let params_converted: Vec<RusqliteValue> = params
.iter()
.map(json_to_rusqlite_value)
.collect::<Result<Vec<_>, _>>()?; // Sammle Ergebnisse, gibt Fehler weiter
// Konvertiere zu Slice von ToSql-Referenzen (erfordert, dass die Values leben)
let params_sql: Vec<&dyn ToSql> = params_converted.iter().map(|v| v as &dyn ToSql).collect();
// Zugriff auf die Verbindung (blockierend, okay für SQLite in vielen Fällen)
let db_lock = state
.0
.lock()
.map_err(|e| format!("Mutex Lock Fehler: {}", e))?;
let conn = db_lock.as_ref().ok_or("Keine Datenbankverbindung")?;
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("SQL Prepare Fehler: {}", e))?;
let column_names: Vec<String> = stmt
.column_names()
.into_iter()
.map(|s| s.to_string())
.collect();
let num_columns = column_names.len();
let mut rows = stmt
.query(&params_sql[..])
.map_err(|e| format!("SQL Query Fehler: {}", e))?;
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
println!();
println!();
println!();
println!();
while let Some(row) = rows.next().map_err(|e| format!("Row Next Fehler: {}", e))? {
//let mut row_map = HashMap::new();
let mut row_data: Vec<JsonValue> = Vec::with_capacity(num_columns);
for i in 0..num_columns {
let col_name = &column_names[i];
println!(
"--- Processing Column --- Index: {}, Name: '{}'",
i, col_name
);
let value_ref = row
.get_ref(i)
.map_err(|e| format!("Get Ref Fehler Spalte {}: {}", i, e))?;
// Wandle rusqlite ValueRef zurück zu serde_json Value
let json_val = match value_ref {
ValueRef::Null => JsonValue::Null,
ValueRef::Integer(i) => JsonValue::Number(i.into()),
ValueRef::Real(f) => JsonValue::Number(
serde_json::Number::from_f64(f).unwrap_or(serde_json::Number::from(0)),
), // Fallback für NaN/Infinity
ValueRef::Text(t) => {
let s = String::from_utf8_lossy(t).to_string();
// Versuche, als JSON zu parsen, falls es ursprünglich ein Array/Objekt war
//serde_json::from_str(&s).unwrap_or(JsonValue::String(s))
JsonValue::String(s)
}
ValueRef::Blob(b) => {
// BLOBs z.B. als Base64-String zurückgeben
JsonValue::String(STANDARD.encode(b))
}
};
println!(
"new row: name: {} with value: {}",
column_names[i].clone(),
json_val,
);
row_data.push(json_val);
//row_map.insert(column_names[i].clone(), json_val);
}
//result_vec.push(row_map);
result_vec.push(row_data);
}
Ok(result_vec)
}
use sqlparser::ast::{Query, Select, SetExpr, Statement, TableFactor, TableObject};
use sqlparser::dialect::SQLiteDialect;
use sqlparser::parser::Parser;
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, String> {
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, DatabaseError> {
let flags = if create {
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE
} else {
OpenFlags::SQLITE_OPEN_READ_WRITE
};
let conn = Connection::open_with_flags(path, flags).map_err(|e| {
format!(
"Datei gibt es nicht: {}. Habe nach {} gesucht",
e.to_string(),
path
)
})?;
conn.pragma_update(None, "key", key)
.map_err(|e| e.to_string())?;
let conn =
Connection::open_with_flags(path, flags).map_err(|e| DatabaseError::ConnectionFailed {
path: path.to_string(),
reason: e.to_string(),
})?;
/* conn.execute_batch("SELECT count(*) from haex_extensions")
.map_err(|e| e.to_string())?; */
conn.pragma_update(None, "key", key)
.map_err(|e| DatabaseError::PragmaError {
pragma: "key".to_string(),
reason: e.to_string(),
})?;
let journal_mode: String = conn
.query_row("PRAGMA journal_mode=WAL;", [], |row| row.get(0))
.map_err(|e| e.to_string())?;
.map_err(|e| DatabaseError::PragmaError {
pragma: "journal_mode=WAL".to_string(),
reason: e.to_string(),
})?;
if journal_mode.eq_ignore_ascii_case("wal") {
println!("WAL mode successfully enabled.");
} else {
eprintln!("Failed to enable WAL mode.");
eprintln!(
"Failed to enable WAL mode, journal_mode is '{}'.",
journal_mode
);
}
Ok(conn)
}
// Hilfsfunktionen für SQL-Parsing
pub fn extract_tables_from_query(query: &sqlparser::ast::Query) -> Vec<String> {
/// Utility für SQL-Parsing - parst ein einzelnes SQL-Statement
pub fn parse_single_statement(sql: &str) -> Result<Statement, DatabaseError> {
let dialect = SQLiteDialect {};
let statements = Parser::parse_sql(&dialect, sql).map_err(|e| DatabaseError::ParseError {
reason: e.to_string(),
sql: sql.to_string(),
})?;
statements
.into_iter()
.next()
.ok_or(DatabaseError::ParseError {
reason: "No SQL statement found".to_string(),
sql: sql.to_string(),
})
}
/// Utility für SQL-Parsing - parst mehrere SQL-Statements
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
let dialect = SQLiteDialect {};
Parser::parse_sql(&dialect, sql).map_err(|e| DatabaseError::ParseError {
reason: e.to_string(),
sql: sql.to_string(),
})
}
pub struct ValueConverter;
impl ValueConverter {
pub fn json_to_rusqlite_value(json_val: &JsonValue) -> Result<SqlValue, DatabaseError> {
match json_val {
JsonValue::Null => Ok(SqlValue::Null),
JsonValue::Bool(b) => {
// SQLite hat keinen Bool-Typ; verwende Integer 0/1
Ok(SqlValue::Integer(if *b { 1 } else { 0 }))
}
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(SqlValue::Integer(i))
} else if let Some(f) = n.as_f64() {
Ok(SqlValue::Real(f))
} else {
// Fallback: als Text
Ok(SqlValue::Text(n.to_string()))
}
}
JsonValue::String(s) => Ok(SqlValue::Text(s.clone())),
JsonValue::Array(_) | JsonValue::Object(_) => {
// Arrays/Objects als JSON-Text speichern
serde_json::to_string(json_val)
.map(SqlValue::Text)
.map_err(|e| DatabaseError::SerializationError {
reason: format!("Failed to serialize JSON param: {}", e),
})
}
}
}
pub fn convert_params(params: &[JsonValue]) -> Result<Vec<SqlValue>, DatabaseError> {
params.iter().map(Self::json_to_rusqlite_value).collect()
}
}
pub fn execute(
sql: String,
params: Vec<JsonValue>,
connection: &DbConnection,
) -> Result<usize, DatabaseError> {
// Konvertiere Parameter
let params_converted: Vec<RusqliteValue> = params
.iter()
.map(ValueConverter::json_to_rusqlite_value)
.collect::<Result<Vec<_>, _>>()?;
let params_sql: Vec<&dyn ToSql> = params_converted.iter().map(|v| v as &dyn ToSql).collect();
with_connection(connection, |conn| {
let affected_rows = conn.execute(&sql, &params_sql[..]).map_err(|e| {
// "Lazy Parsing": Extrahiere den Tabellennamen nur, wenn ein Fehler auftritt,
// um den Overhead bei erfolgreichen Operationen zu vermeiden.
let table_name = extract_primary_table_name_from_sql(&sql).unwrap_or(None);
DatabaseError::ExecutionError {
sql: sql.clone(),
reason: e.to_string(),
table: table_name,
}
})?;
Ok(affected_rows)
})
}
pub fn select(
sql: String,
params: Vec<JsonValue>,
connection: &DbConnection,
) -> Result<Vec<HashMap<String, JsonValue>>, DatabaseError> {
// Validiere SQL-Statement
let statement = parse_single_statement(&sql)?;
// Stelle sicher, dass es eine Query ist
if !matches!(statement, Statement::Query(_)) {
return Err(DatabaseError::UnsupportedStatement {
statement_type: "Non-Query".to_string(),
description: "Only SELECT statements are allowed in select function".to_string(),
});
}
// Konvertiere Parameter
let params_converted: Vec<RusqliteValue> = params
.iter()
.map(ValueConverter::json_to_rusqlite_value)
.collect::<Result<Vec<_>, _>>()?;
let params_sql: Vec<&dyn ToSql> = params_converted.iter().map(|v| v as &dyn ToSql).collect();
with_connection(connection, |conn| {
let mut stmt = conn
.prepare(&sql)
.map_err(|e| DatabaseError::PrepareError {
reason: e.to_string(),
})?;
let column_names: Vec<String> = stmt
.column_names()
.into_iter()
.map(|s| s.to_string())
.collect();
let num_columns = column_names.len();
let mut rows = stmt
.query(&params_sql[..])
.map_err(|e| DatabaseError::QueryError {
reason: e.to_string(),
})?;
let mut result_vec: Vec<HashMap<String, JsonValue>> = Vec::new();
while let Some(row) = rows.next().map_err(|e| DatabaseError::RowProcessingError {
reason: format!("Row iteration error: {}", e),
})? {
let mut row_map: HashMap<String, JsonValue> = HashMap::with_capacity(num_columns);
for i in 0..num_columns {
let col_name = &column_names[i];
/* println!(
"--- Processing Column --- Index: {}, Name: '{}'",
i, col_name
); */
let value_ref = row
.get_ref(i)
.map_err(|e| DatabaseError::RowProcessingError {
reason: format!("Failed to get column {} ('{}'): {}", i, col_name, e),
})?;
let json_val = convert_value_ref_to_json(value_ref)?;
//println!("Column: {} = {}", column_names[i], json_val);
row_map.insert(col_name.clone(), json_val);
}
result_vec.push(row_map);
}
Ok(result_vec)
})
}
/// Konvertiert rusqlite ValueRef zu JSON
fn convert_value_ref_to_json(value_ref: ValueRef) -> Result<JsonValue, DatabaseError> {
let json_val = match value_ref {
ValueRef::Null => JsonValue::Null,
ValueRef::Integer(i) => JsonValue::Number(i.into()),
ValueRef::Real(f) => JsonValue::Number(
serde_json::Number::from_f64(f).unwrap_or_else(|| serde_json::Number::from(0)),
),
ValueRef::Text(t) => {
let s = String::from_utf8_lossy(t).to_string();
JsonValue::String(s)
}
ValueRef::Blob(b) => {
// BLOBs als Base64-String zurückgeben
JsonValue::String(STANDARD.encode(b))
}
};
Ok(json_val)
}
// Extrahiert alle Tabellennamen aus einem SQL-Statement über AST-Parsing
pub fn extract_table_names_from_sql(sql: &str) -> Result<Vec<String>, DatabaseError> {
let statement = parse_single_statement(sql)?;
Ok(extract_table_names_from_statement(&statement))
}
/// Extrahiert den ersten/primären Tabellennamen aus einem SQL-Statement
pub fn extract_primary_table_name_from_sql(sql: &str) -> Result<Option<String>, DatabaseError> {
let table_names = extract_table_names_from_sql(sql)?;
Ok(table_names.into_iter().next())
}
/// Extrahiert alle Tabellennamen aus einem AST Statement
pub fn extract_table_names_from_statement(statement: &Statement) -> Vec<String> {
let mut tables = Vec::new();
extract_tables_from_set_expr(&query.body, &mut tables);
match statement {
Statement::Query(query) => {
extract_tables_from_query_recursive(query, &mut tables);
}
Statement::Insert(insert) => {
if let TableObject::TableName(name) = &insert.table {
tables.push(name.to_string());
}
}
Statement::Update { table, .. } => {
extract_tables_from_table_factor(&table.relation, &mut tables);
}
Statement::Delete(delete) => {
use sqlparser::ast::FromTable;
match &delete.from {
FromTable::WithFromKeyword(table_refs) | FromTable::WithoutKeyword(table_refs) => {
for table_ref in table_refs {
extract_tables_from_table_factor(&table_ref.relation, &mut tables);
}
}
}
// Fallback für DELETE-Syntax ohne FROM
for table_name in &delete.tables {
tables.push(table_name.to_string());
}
}
Statement::CreateTable(create) => {
tables.push(create.name.to_string());
}
Statement::AlterTable { name, .. } => {
tables.push(name.to_string());
}
Statement::Drop { names, .. } => {
for name in names {
tables.push(name.to_string());
}
}
Statement::CreateIndex(create_index) => {
tables.push(create_index.table_name.to_string());
}
Statement::Truncate { table_names, .. } => {
for table_name in table_names {
tables.push(table_name.to_string());
}
}
// Weitere Statement-Typen können hier hinzugefügt werden
_ => {
// Für unbekannte Statement-Typen geben wir eine leere Liste zurück
}
}
tables
}
fn extract_tables_from_set_expr(set_expr: &sqlparser::ast::SetExpr, tables: &mut Vec<String>) {
match set_expr {
sqlparser::ast::SetExpr::Select(select) => {
for from in &select.from {
extract_tables_from_table_with_joins(from, tables);
}
/// Extrahiert Tabellennamen rekursiv aus Query-Strukturen
fn extract_tables_from_query_recursive(query: &Query, tables: &mut Vec<String>) {
extract_tables_from_set_expr_recursive(&query.body, tables);
}
/// Extrahiert Tabellennamen aus SELECT-Statements
fn extract_tables_from_select(select: &Select, tables: &mut Vec<String>) {
// FROM clause
for table_ref in &select.from {
extract_tables_from_table_factor(&table_ref.relation, tables);
// JOINs
for join in &table_ref.joins {
extract_tables_from_table_factor(&join.relation, tables);
}
sqlparser::ast::SetExpr::Query(query) => {
extract_tables_from_set_expr(&query.body, tables);
}
sqlparser::ast::SetExpr::SetOperation { left, right, .. } => {
extract_tables_from_set_expr(left, tables);
extract_tables_from_set_expr(right, tables);
}
_ => (), // Andere Fälle wie Values oder Insert ignorieren
}
}
fn extract_tables_from_table_with_joins(
table_with_joins: &sqlparser::ast::TableWithJoins,
tables: &mut Vec<String>,
) {
extract_tables_from_table_factor(&table_with_joins.relation, tables);
for join in &table_with_joins.joins {
extract_tables_from_table_factor(&join.relation, tables);
}
}
fn extract_tables_from_table_factor(
table_factor: &sqlparser::ast::TableFactor,
tables: &mut Vec<String>,
) {
/// Extrahiert Tabellennamen aus TableFactor-Strukturen
fn extract_tables_from_table_factor(table_factor: &TableFactor, tables: &mut Vec<String>) {
match table_factor {
sqlparser::ast::TableFactor::Table { name, .. } => {
TableFactor::Table { name, .. } => {
tables.push(name.to_string());
}
sqlparser::ast::TableFactor::Derived { subquery, .. } => {
extract_tables_from_set_expr(&subquery.body, tables);
TableFactor::Derived { subquery, .. } => {
extract_tables_from_query_recursive(subquery, tables);
}
sqlparser::ast::TableFactor::NestedJoin {
TableFactor::TableFunction { .. } => {
// Table functions haben normalerweise keine direkten Tabellennamen
}
TableFactor::NestedJoin {
table_with_joins, ..
} => {
extract_tables_from_table_with_joins(table_with_joins, tables);
extract_tables_from_table_factor(&table_with_joins.relation, tables);
for join in &table_with_joins.joins {
extract_tables_from_table_factor(&join.relation, tables);
}
}
_ => {
// TableFunction, UNNEST, JsonTable, etc. haben normalerweise keine direkten Tabellennamen
// oder sind nicht relevant für SQLite
}
_ => (), // Andere Fälle wie TableFunction ignorieren
}
}
/// Extrahiert Tabellennamen rekursiv aus SetExpr-Strukturen.
/// Diese Funktion enthält die eigentliche rekursive Logik.
fn extract_tables_from_set_expr_recursive(set_expr: &SetExpr, tables: &mut Vec<String>) {
match set_expr {
SetExpr::Select(select) => {
extract_tables_from_select(select, tables);
}
SetExpr::Query(sub_query) => {
extract_tables_from_set_expr_recursive(&sub_query.body, tables);
}
SetExpr::SetOperation { left, right, .. } => {
extract_tables_from_set_expr_recursive(left, tables);
extract_tables_from_set_expr_recursive(right, tables);
}
SetExpr::Values(_)
| SetExpr::Table(_)
| SetExpr::Insert(_)
| SetExpr::Update(_)
| SetExpr::Delete(_) => {}
}
}
pub fn with_connection<T, F>(connection: &DbConnection, f: F) -> Result<T, DatabaseError>
where
F: FnOnce(&mut Connection) -> Result<T, DatabaseError>,
{
let mut db_lock = connection
.0
.lock()
.map_err(|e| DatabaseError::MutexPoisoned {
reason: e.to_string(),
})?;
let conn = db_lock.as_mut().ok_or(DatabaseError::ConnectionError {
reason: "Connection to vault failed".to_string(),
})?;
f(conn)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_simple_select() {
let sql = "SELECT * FROM users";
let tables = extract_table_names_from_sql(sql).unwrap();
assert_eq!(tables, vec!["users"]);
}
#[test]
fn test_extract_select_with_join() {
let sql = "SELECT u.name, p.title FROM users u JOIN posts p ON u.id = p.user_id";
let tables = extract_table_names_from_sql(sql).unwrap();
assert_eq!(tables, vec!["users", "posts"]);
}
#[test]
fn test_extract_insert() {
let sql = "INSERT INTO users (name, email) VALUES (?, ?)";
let tables = extract_table_names_from_sql(sql).unwrap();
assert_eq!(tables, vec!["users"]);
}
#[test]
fn test_extract_update() {
let sql = "UPDATE users SET name = ? WHERE id = ?";
let tables = extract_table_names_from_sql(sql).unwrap();
assert_eq!(tables, vec!["users"]);
}
#[test]
fn test_extract_delete() {
let sql = "DELETE FROM users WHERE id = ?";
let tables = extract_table_names_from_sql(sql).unwrap();
assert_eq!(tables, vec!["users"]);
}
#[test]
fn test_extract_create_table() {
let sql = "CREATE TABLE new_table (id INTEGER, name TEXT)";
let tables = extract_table_names_from_sql(sql).unwrap();
assert_eq!(tables, vec!["new_table"]);
}
#[test]
fn test_extract_subquery() {
let sql = "SELECT * FROM (SELECT id FROM users) AS sub";
let tables = extract_table_names_from_sql(sql).unwrap();
assert_eq!(tables, vec!["users"]);
}
#[test]
fn test_extract_primary_table() {
let sql = "SELECT u.name FROM users u JOIN posts p ON u.id = p.user_id";
let primary_table = extract_primary_table_name_from_sql(sql).unwrap();
assert_eq!(primary_table, Some("users".to_string()));
}
#[test]
fn test_extract_complex_query() {
let sql = r#"
SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.created_at > (SELECT MIN(created_at) FROM sessions)
GROUP BY u.id
"#;
let tables = extract_table_names_from_sql(sql).unwrap();
assert_eq!(tables, vec!["users", "posts", "sessions"]);
}
#[test]
fn test_invalid_sql() {
let sql = "INVALID SQL";
let result = extract_table_names_from_sql(sql);
assert!(result.is_err());
}
#[test]
fn test_parse_single_statement() {
let sql = "SELECT * FROM users WHERE id = ?";
let result = parse_single_statement(sql);
assert!(result.is_ok());
assert!(matches!(result.unwrap(), Statement::Query(_)));
}
#[test]
fn test_parse_invalid_sql() {
let sql = "INVALID SQL STATEMENT";
let result = parse_single_statement(sql);
assert!(matches!(result, Err(DatabaseError::ParseError { .. })));
}
#[test]
fn test_convert_value_ref_to_json() {
use rusqlite::types::ValueRef;
assert_eq!(
convert_value_ref_to_json(ValueRef::Null).unwrap(),
JsonValue::Null
);
assert_eq!(
convert_value_ref_to_json(ValueRef::Integer(42)).unwrap(),
JsonValue::Number(42.into())
);
assert_eq!(
convert_value_ref_to_json(ValueRef::Text(b"hello")).unwrap(),
JsonValue::String("hello".to_string())
);
}
// Test für die neuen AST-basierten Funktionen
#[test]
fn test_extract_table_names_comprehensive() {
// Test verschiedene SQL-Statement-Typen
assert_eq!(
extract_primary_table_name_from_sql("SELECT * FROM users WHERE id = 1").unwrap(),
Some("users".to_string())
);
assert_eq!(
extract_primary_table_name_from_sql("INSERT INTO products (name) VALUES ('test')")
.unwrap(),
Some("products".to_string())
);
assert_eq!(
extract_primary_table_name_from_sql("UPDATE orders SET status = 'completed'").unwrap(),
Some("orders".to_string())
);
assert_eq!(
extract_primary_table_name_from_sql("DELETE FROM customers").unwrap(),
Some("customers".to_string())
);
}
}

View File

@ -0,0 +1,159 @@
// src-tauri/src/database/error.rs
use serde::{Deserialize, Serialize};
use thiserror::Error;
use ts_rs::TS;
use crate::crdt::trigger::CrdtSetupError;
#[derive(Error, Debug, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(tag = "type", content = "details")]
pub enum DatabaseError {
/// Der SQL-Code konnte nicht geparst werden.
#[error("Failed to parse SQL: {reason} - SQL: {sql}")]
ParseError { reason: String, sql: String },
/// Parameter-Fehler (falsche Anzahl, ungültiger Typ, etc.)
#[error("Parameter error: {reason} (expected: {expected}, provided: {provided})")]
ParamError {
reason: String,
expected: usize,
provided: usize,
},
#[error("Failed to prepare statement: {reason}")]
PrepareError { reason: String },
#[error("Database error: {reason}")]
DatabaseError { reason: String },
/// Ein Fehler ist während der Ausführung in der Datenbank aufgetreten.
#[error("Execution error on table {}: {} - SQL: {}", table.as_deref().unwrap_or("unknown"), reason, sql)]
ExecutionError {
sql: String,
reason: String,
table: Option<String>,
},
/// Ein Fehler ist beim Verwalten der Transaktion aufgetreten.
#[error("Transaction error: {reason}")]
TransactionError { reason: String },
/// Ein SQL-Statement wird vom Proxy nicht unterstützt.
#[error("Unsupported statement type '{statement_type}': {description}")]
UnsupportedStatement {
statement_type: String,
description: String,
},
/// Fehler im HLC-Service
#[error("HLC error: {reason}")]
HlcError { reason: String },
/// Fehler beim Sperren der Datenbankverbindung
#[error("Lock error: {reason}")]
LockError { reason: String },
/// Fehler bei der Datenbankverbindung
#[error("Connection error: {reason}")]
ConnectionError { reason: String },
/// Fehler bei der JSON-Serialisierung
#[error("Serialization error: {reason}")]
SerializationError { reason: String },
#[error("Permission error for extension '{extension_id}': {reason} (operation: {}, resource: {})",
operation.as_deref().unwrap_or("unknown"),
resource.as_deref().unwrap_or("unknown"))]
PermissionError {
extension_id: String,
operation: Option<String>,
resource: Option<String>,
reason: String,
},
#[error("Query error: {reason}")]
QueryError { reason: String },
#[error("Row processing error: {reason}")]
RowProcessingError { reason: String },
#[error("Mutex Poisoned error: {reason}")]
MutexPoisoned { reason: String },
#[error("Datenbankverbindung fehlgeschlagen für Pfad '{path}': {reason}")]
ConnectionFailed { path: String, reason: String },
#[error("PRAGMA-Befehl '{pragma}' konnte nicht gesetzt werden: {reason}")]
PragmaError { pragma: String, reason: String },
#[error("Fehler beim Auflösen des Dateipfads: {reason}")]
PathResolutionError { reason: String },
#[error("Datei-I/O-Fehler für Pfad '{path}': {reason}")]
IoError { path: String, reason: String },
#[error("CRDT setup failed: {0}")]
CrdtSetup(String),
}
impl From<rusqlite::Error> for DatabaseError {
fn from(err: rusqlite::Error) -> Self {
DatabaseError::DatabaseError {
reason: err.to_string(),
}
}
}
impl From<String> for DatabaseError {
fn from(reason: String) -> Self {
DatabaseError::DatabaseError { reason }
}
}
impl From<CrdtSetupError> for DatabaseError {
fn from(err: CrdtSetupError) -> Self {
// Wir konvertieren den Fehler in einen String, um ihn einfach zu halten.
DatabaseError::CrdtSetup(err.to_string())
}
}
impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError {
fn from(err: crate::extension::database::ExtensionDatabaseError) -> Self {
match err {
crate::extension::database::ExtensionDatabaseError::Permission { source } => {
// Konvertiere PermissionError zu DatabaseError
match source {
crate::extension::database::permissions::PermissionError::AccessDenied {
extension_id,
operation,
resource,
reason,
} => DatabaseError::PermissionError {
extension_id,
operation: Some(operation),
resource: Some(resource),
reason,
},
crate::extension::database::permissions::PermissionError::Database {
source,
} => source,
other => DatabaseError::PermissionError {
extension_id: "unknown".to_string(),
operation: None,
resource: None,
reason: other.to_string(),
},
}
}
crate::extension::database::ExtensionDatabaseError::Database { source } => source,
crate::extension::database::ExtensionDatabaseError::ParameterValidation { reason } => {
DatabaseError::ParamError {
reason: reason.clone(),
expected: 0, // Kann nicht aus dem Grund extrahiert werden
provided: 0, // Kann nicht aus dem Grund extrahiert werden
}
}
crate::extension::database::ExtensionDatabaseError::StatementExecution { reason } => {
DatabaseError::ExecutionError {
sql: "unknown".to_string(),
reason,
table: None,
}
}
}
}
}

View File

@ -1,48 +1,41 @@
// database/mod.rs
// src-tauri/src/database/mod.rs
pub mod core;
pub mod error;
use rusqlite::Connection;
use serde_json::Value as JsonValue;
use std::fs;
use std::collections::HashMap;
use std::path::Path;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::sync::Mutex;
use std::{fs, sync::Arc};
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
use crate::crdt::trigger;
use crate::database::core::open_and_init_db;
pub struct HlcService(pub Mutex<uhlc::HLC>);
use crate::crdt::hlc::HlcService;
use crate::database::error::DatabaseError;
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
#[tauri::command]
pub async fn sql_select(
sql: String,
params: Vec<JsonValue>,
state: State<'_, DbConnection>,
) -> Result<Vec<Vec<JsonValue>>, String> {
core::select(sql, params, &state).await
pub struct AppState {
pub db: DbConnection,
pub hlc: Mutex<HlcService>, // Kein Arc hier nötig, da der ganze AppState von Tauri in einem Arc verwaltet wird.
}
#[tauri::command]
pub async fn sql_execute(
pub fn sql_select(
sql: String,
params: Vec<JsonValue>,
state: State<'_, DbConnection>,
) -> Result<usize, String> {
core::execute(sql, params, &state).await
state: State<'_, AppState>,
) -> Result<Vec<HashMap<String, JsonValue>>, DatabaseError> {
core::select(sql, params, &state.db)
}
#[tauri::command]
pub fn test(app_handle: AppHandle) -> Result<String, String> {
let resource_path = app_handle
.path()
.resolve("database/vault.db", BaseDirectory::Resource)
.map_err(|e| format!("Fehler {}", e));
//let file = app_handle.fs().open(resource_path, {}).unwrap().read();
Ok(String::from(resource_path.unwrap().to_string_lossy()))
/* std::fs::exists(String::from(resource_path.unwrap().to_string_lossy()))
.map_err(|e| format!("Fehler: {}", e)) */
pub fn sql_execute(
sql: String,
params: Vec<JsonValue>,
state: State<'_, AppState>,
) -> Result<usize, DatabaseError> {
core::execute(sql, params, &state.db)
}
#[tauri::command]
@ -50,8 +43,8 @@ pub fn create_encrypted_database(
app_handle: AppHandle,
path: String,
key: String,
state: State<'_, DbConnection>,
) -> Result<String, String> {
state: State<'_, AppState>,
) -> Result<String, DatabaseError> {
// Ressourcenpfad zur eingebundenen Datenbank auflösen
println!("Arbeitsverzeichnis: {:?}", std::env::current_dir());
@ -68,14 +61,16 @@ pub fn create_encrypted_database(
let resource_path = app_handle
.path()
.resolve("temp_vault.db", BaseDirectory::AppLocalData)
.map_err(|e| format!("Fehler beim Auflösen des Ressourcenpfads: {}", e))?;
.map_err(|e| DatabaseError::PathResolutionError {
reason: e.to_string(),
})?;
// Prüfen, ob die Ressourcendatei existiert
if !resource_path.exists() {
return Err(format!(
"Ressourcendatenbank wurde nicht gefunden: {}",
resource_path.display()
));
return Err(DatabaseError::IoError {
path: resource_path.display().to_string(),
reason: "Ressourcendatenbank wurde nicht gefunden.".to_string(),
});
}
// Sicherstellen, dass das Zielverzeichnis existiert
@ -92,15 +87,10 @@ pub fn create_encrypted_database(
let target = Path::new(&path);
if target.exists() & target.is_file() {
println!(
"Datei '{}' existiert bereits. Sie wird gelöscht.",
target.display()
);
fs::remove_file(target)
.map_err(|e| format!("Kann Vault {} nicht löschen. \n {}", target.display(), e))?;
} else {
println!("Datei '{}' existiert nicht.", target.display());
fs::remove_file(target).map_err(|e| DatabaseError::IoError {
path: target.display().to_string(),
reason: format!("Bestehende Zieldatei konnte nicht gelöscht werden: {}", e),
})?;
}
println!(
@ -108,37 +98,43 @@ pub fn create_encrypted_database(
resource_path.as_path().display()
);
let conn = Connection::open(&resource_path).map_err(|e| {
format!(
"Fehler beim Öffnen der kopierten Datenbank: {}",
e.to_string()
)
let conn = Connection::open(&resource_path).map_err(|e| DatabaseError::ConnectionFailed {
path: resource_path.display().to_string(),
reason: format!(
"Fehler beim Öffnen der unverschlüsselten Quelldatenbank: {}",
e
),
})?;
println!("Hänge neue, verschlüsselte Datenbank an unter '{}'", &path);
// ATTACH DATABASE 'Dateiname' AS Alias KEY 'Passwort';
conn.execute("ATTACH DATABASE ?1 AS encrypted KEY ?2;", [&path, &key])
.map_err(|e| format!("Fehler bei ATTACH DATABASE: {}", e.to_string()))?;
.map_err(|e| DatabaseError::ExecutionError {
sql: "ATTACH DATABASE ...".to_string(),
reason: e.to_string(),
table: None,
})?;
println!(
"Exportiere Daten von 'main' nach 'encrypted' mit password {} ...",
&key
);
match conn.query_row("SELECT sqlcipher_export('encrypted');", [], |_row| Ok(())) {
Ok(_) => {
println!(">>> sqlcipher_export erfolgreich ausgeführt (Rückgabewert ignoriert).");
}
Err(e) => {
eprintln!("!!! FEHLER während sqlcipher_export: {}", e);
conn.execute("DETACH DATABASE encrypted;", []).ok(); // Versuche zu detachen
return Err(e.to_string()); // Gib den Fehler zurück
}
if let Err(e) = conn.query_row("SELECT sqlcipher_export('encrypted');", [], |_| Ok(())) {
// Versuche aufzuräumen, ignoriere Fehler dabei
let _ = conn.execute("DETACH DATABASE encrypted;", []);
return Err(DatabaseError::QueryError {
reason: format!("Fehler während sqlcipher_export: {}", e),
});
}
println!("Löse die verschlüsselte Datenbank vom Handle...");
conn.execute("DETACH DATABASE encrypted;", [])
.map_err(|e| format!("Fehler bei DETACH DATABASE: {}", e.to_string()))?;
.map_err(|e| DatabaseError::ExecutionError {
sql: "DETACH DATABASE ...".to_string(),
reason: e.to_string(),
table: None,
})?;
println!("Datenbank erfolgreich nach '{}' verschlüsselt.", &path);
println!(
@ -164,17 +160,19 @@ pub fn create_encrypted_database(
println!("resource_path: {}", resource_path.display());
// erstelle Trigger für haex_tables
conn.close()
.map_err(|(_, e)| DatabaseError::ConnectionFailed {
path: resource_path.display().to_string(),
reason: format!("Fehler beim Schließen der Quelldatenbank: {}", e),
})?;
conn.close().unwrap();
let new_conn = open_and_init_db(&path, &key, false)?;
let new_conn = core::open_and_init_db(&path, &key, false)?;
// Aktualisieren der Datenbankverbindung im State
let mut db = state
.0
.lock()
.map_err(|e| format!("Mutex-Fehler: {}", e.to_string()))?;
let mut db = state.db.0.lock().map_err(|e| DatabaseError::LockError {
reason: e.to_string(),
})?;
*db = Some(new_conn);
Ok(format!("Verschlüsselte CRDT-Datenbank erstellt",))
@ -182,11 +180,11 @@ pub fn create_encrypted_database(
#[tauri::command]
pub fn open_encrypted_database(
app_handle: AppHandle,
//app_handle: AppHandle,
path: String,
key: String,
state: State<'_, DbConnection>,
) -> Result<String, String> {
state: State<'_, AppState>,
) -> Result<String, DatabaseError> {
/* let vault_path = app_handle
.path()
.resolve(format!("vaults/{}", path), BaseDirectory::AppLocalData)
@ -194,92 +192,16 @@ pub fn open_encrypted_database(
.into_os_string()
.into_string()
.unwrap(); */
if !std::path::Path::new(&path).exists() {
/* if !std::path::Path::new(&path).exists() {
return Err(format!("File not found {}", path).into());
}
} */
let conn =
core::open_and_init_db(&path, &key, false).map_err(|e| format!("Error during open: {}", e));
let conn = core::open_and_init_db(&path, &key, false)
.map_err(|e| format!("Error during open: {}", e))?;
let mut db = state.0.lock().map_err(|e| e.to_string())?;
let mut db = state.db.0.lock().map_err(|e| e.to_string())?;
*db = Some(conn.unwrap());
*db = Some(conn);
Ok(format!("success"))
}
fn get_target_triple() -> Result<String, String> {
let target_triple = if cfg!(target_os = "linux") {
if cfg!(target_arch = "x86_64") {
"x86_64-unknown-linux-gnu".to_string()
} else if cfg!(target_arch = "aarch64") {
"aarch64-unknown-linux-gnu".to_string()
} else {
return Err(format!(
"Unbekannte Linux-Architektur: {}",
std::env::consts::ARCH
));
}
} else if cfg!(target_os = "macos") {
if cfg!(target_arch = "x86_64") {
"x86_64-apple-darwin".to_string()
} else if cfg!(target_arch = "aarch64") {
"aarch64-apple-darwin".to_string()
} else {
return Err(format!(
"Unbekannte macOS-Architektur: {}",
std::env::consts::ARCH
));
}
} else if cfg!(target_os = "windows") {
if cfg!(target_arch = "x86_64") {
"x86_64-pc-windows-msvc".to_string()
} else if cfg!(target_arch = "x86") {
"i686-pc-windows-msvc".to_string()
} else {
return Err(format!(
"Unbekannte Windows-Architektur: {}",
std::env::consts::ARCH
));
}
} else if cfg!(target_os = "android") {
if cfg!(target_arch = "aarch64") {
"aarch64-linux-android".to_string()
} else {
return Err(format!(
"Unbekannte Android-Architektur: {}",
std::env::consts::ARCH
));
}
} else if cfg!(target_os = "ios") {
if cfg!(target_arch = "aarch64") {
"aarch64-apple-ios".to_string()
} else {
return Err(format!(
"Unbekannte iOS-Architektur: {}",
std::env::consts::ARCH
));
}
} else {
return Err("Unbekanntes Zielsystem".to_string());
};
Ok(target_triple)
}
pub fn get_hlc_timestamp(state: tauri::State<HlcService>) -> String {
let hlc = state.0.lock().unwrap();
hlc.new_timestamp().to_string()
}
#[tauri::command]
pub fn update_hlc_from_remote(
remote_timestamp_str: String,
state: tauri::State<HlcService>,
) -> Result<(), String> {
let remote_ts =
uhlc::Timestamp::from_str(&remote_timestamp_str).map_err(|e| e.cause.to_string())?;
let hlc = state.0.lock().unwrap();
hlc.update_with_timestamp(&remote_ts)
.map_err(|e| format!("HLC update failed: {:?}", e))
}