diff --git a/src-tauri/src/database/core.rs b/src-tauri/src/database/core.rs index 0078c5b..3a29c00 100644 --- a/src-tauri/src/database/core.rs +++ b/src-tauri/src/database/core.rs @@ -236,7 +236,7 @@ pub fn select_with_crdt( connection: &DbConnection, ) -> Result>, DatabaseError> { with_connection(&connection, |conn| { - SqlExecutor::select_internal(conn, &sql, ¶ms) + SqlExecutor::query_select(conn, &sql, ¶ms) }) } diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index df09980..eac7456 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -76,7 +76,7 @@ pub fn sql_query_with_crdt( core::with_connection(&state.db, |conn| { let tx = conn.transaction().map_err(DatabaseError::from)?; - let result = SqlExecutor::query_internal(&tx, &hlc_service, &sql, ¶ms)?; + let (_modified_tables, result) = SqlExecutor::query_internal(&tx, &hlc_service, &sql, ¶ms)?; tx.commit().map_err(DatabaseError::from)?; Ok(result) }) diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs index 9307fb2..8a43722 100644 --- a/src-tauri/src/extension/core/manager.rs +++ b/src-tauri/src/extension/core/manager.rs @@ -4,7 +4,7 @@ use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, E use crate::extension::core::types::{copy_directory, Extension, ExtensionSource}; use crate::extension::core::ExtensionPermissions; use crate::extension::crypto::ExtensionCrypto; -use crate::extension::database::executor::{PkRemappingContext, SqlExecutor}; +use crate::extension::database::executor::SqlExecutor; use crate::extension::error::ExtensionError; use crate::extension::permissions::manager::PermissionManager; use crate::extension::permissions::types::ExtensionPermission; @@ -478,19 +478,13 @@ impl ExtensionManager { let hlc_service = hlc_service_guard.clone(); drop(hlc_service_guard); - // Erstelle PK-Remapping Context für die gesamte Transaktion - // Dies ermöglicht automatisches FK-Remapping wenn ON CONFLICT bei Extension auftritt - let mut pk_context = PkRemappingContext::new(); - // 1. Extension-Eintrag erstellen mit generierter UUID - // WICHTIG: RETURNING wird vom CRDT-Transformer automatisch hinzugefügt let insert_ext_sql = format!( - "INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id", + "INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", TABLE_EXTENSIONS ); - let (_tables, returning_results): (HashSet, Vec>) = - SqlExecutor::query_internal_typed_with_context( + SqlExecutor::execute_internal_typed( &tx, &hlc_service, &insert_ext_sql, @@ -507,26 +501,9 @@ impl ExtensionManager { extracted.manifest.description, true, // enabled ], - &mut pk_context, )?; - // Nutze die tatsächliche ID aus der Datenbank (wichtig bei ON CONFLICT) - // Die haex_extensions Tabelle hat einen single-column PK namens "id" - let actual_extension_id = returning_results - .first() // Holt die erste Zeile (das innere Vec, z.B. Some(&["uuid-string"])) - .and_then(|row_array| row_array.first()) // Holt das erste Element daraus (z.B. Some(&JsonValue::String("uuid-string"))) - .and_then(|val| val.as_str()) // Konvertiert zu &str (z.B. Some("uuid-string")) - .map(|s| s.to_string()) // Konvertiert zu String - .unwrap_or_else(|| extension_id.clone()); // Fallback - - eprintln!( - "DEBUG: Extension UUID - Generated: {}, Actual from DB: {}", - extension_id, actual_extension_id - ); - - // 2. Permissions speichern (oder aktualisieren falls schon vorhanden) - // Nutze einfaches INSERT - die CRDT-Transformation fügt automatisch ON CONFLICT hinzu - // FK-Werte (extension_id) werden automatisch remapped wenn Extension ON CONFLICT hatte + // 2. Permissions speichern let insert_perm_sql = format!( "INSERT INTO {} (id, extension_id, resource_type, action, target, constraints, status) VALUES (?, ?, ?, ?, ?, ?, ?)", TABLE_EXTENSION_PERMISSIONS @@ -536,7 +513,7 @@ impl ExtensionManager { use crate::database::generated::HaexExtensionPermissions; let db_perm: HaexExtensionPermissions = perm.into(); - SqlExecutor::execute_internal_typed_with_context( + SqlExecutor::execute_internal_typed( &tx, &hlc_service, &insert_perm_sql, @@ -549,16 +526,15 @@ impl ExtensionManager { db_perm.constraints, db_perm.status, ], - &mut pk_context, )?; } tx.commit().map_err(DatabaseError::from)?; - Ok(actual_extension_id.clone()) + Ok(extension_id.clone()) })?; let extension = Extension { - id: actual_extension_id.clone(), // Nutze die actual_extension_id aus der Transaktion + id: extension_id.clone(), source: ExtensionSource::Production { path: extensions_dir.clone(), version: extracted.manifest.version.clone(), @@ -607,8 +583,7 @@ impl ExtensionManager { ); eprintln!("DEBUG: SQL Query before transformation: {}", sql); - // select_internal gibt jetzt Vec> zurück - let results = SqlExecutor::select_internal(conn, &sql, &[])?; + let results = SqlExecutor::query_select(conn, &sql, &[])?; eprintln!("DEBUG: Query returned {} results", results.len()); let mut data = Vec::new(); diff --git a/src-tauri/src/extension/database/executor.rs b/src-tauri/src/extension/database/executor.rs index a74290e..66d4ced 100644 --- a/src-tauri/src/extension/database/executor.rs +++ b/src-tauri/src/extension/database/executor.rs @@ -1,121 +1,33 @@ -// src-tauri/src/extension/database/executor.rs (neu) +// src-tauri/src/extension/database/executor.rs use crate::crdt::hlc::HlcService; use crate::crdt::transformer::CrdtTransformer; use crate::crdt::trigger; use crate::database::core::{convert_value_ref_to_json, parse_sql_statements, ValueConverter}; use crate::database::error::DatabaseError; -use rusqlite::Connection; use rusqlite::{params_from_iter, types::Value as SqliteValue, ToSql, Transaction}; -use serde_json::{Map, Value as JsonValue}; -use sqlparser::ast::{Insert, Statement, TableObject}; -use std::collections::{HashMap, HashSet}; - -/// Repräsentiert PK-Werte für eine Zeile (kann single oder composite key sein) -#[derive(Debug, Clone, PartialEq, Eq)] -struct PkValues { - /// column_name -> value - values: HashMap, -} - -impl PkValues { - fn new() -> Self { - Self { - values: HashMap::new(), - } - } - - fn insert(&mut self, column: String, value: String) { - self.values.insert(column, value); - } - - fn get(&self, column: &str) -> Option<&String> { - self.values.get(column) - } -} - -/// Context für PK-Remapping während einer Transaktion -/// Trackt für jede Tabelle: welche PKs sollten eingefügt werden vs. welche sind tatsächlich in der DB -#[derive(Debug, Default)] -pub struct PkRemappingContext { - /// Für jede Tabelle: Liste von (original_pk_values, actual_pk_values) Mappings - /// Wird nur gespeichert wenn original != actual (d.h. ON CONFLICT hat PK geändert) - mappings: HashMap>, -} - -impl PkRemappingContext { - pub fn new() -> Self { - Self::default() - } - - /// Fügt ein Mapping für eine Tabelle hinzu, aber nur wenn original != actual - /// original und actual sind die PK-Werte vor und nach dem INSERT - fn add_mapping(&mut self, table: String, original: PkValues, actual: PkValues) { - // Nur speichern wenn tatsächlich unterschiedlich (ON CONFLICT hat stattgefunden) - if original != actual { - eprintln!( - "DEBUG: PK Remapping for table '{}': {:?} -> {:?}", - table, original.values, actual.values - ); - self.mappings - .entry(table) - .or_insert_with(Vec::new) - .push((original, actual)); - } - } - - /// Versucht einen FK-Wert zu remappen - /// referenced_table: Die Tabelle auf die der FK zeigt - /// referenced_column: Die PK-Spalte in der referenced_table - /// value: Der FK-Wert der ersetzt werden soll - fn remap_fk_value( - &self, - referenced_table: &str, - referenced_column: &str, - value: &str, - ) -> String { - self.mappings - .get(referenced_table) - .and_then(|mappings| { - mappings.iter().find_map(|(original, actual)| { - if original.get(referenced_column)? == value { - let actual_val = actual.get(referenced_column)?.clone(); - eprintln!( - "DEBUG: FK Remapping for {}.{}: {} -> {}", - referenced_table, referenced_column, value, actual_val - ); - Some(actual_val) - } else { - None - } - }) - }) - .unwrap_or_else(|| value.to_string()) - } -} +use serde_json::Value as JsonValue; +use sqlparser::ast::Statement; +use std::collections::HashSet; /// SQL-Executor OHNE Berechtigungsprüfung - für interne Nutzung pub struct SqlExecutor; impl SqlExecutor { - /// Führt ein SQL Statement OHNE RETURNING aus (mit CRDT und PK-Remapping) - /// Unterstützt automatisches FK-Remapping wenn vorherige INSERTs ON CONFLICT getriggert haben - /// - /// Diese Variante akzeptiert &[&dyn ToSql] direkt (wie von rusqlite::params![] erzeugt) + /// Führt ein SQL Statement OHNE RETURNING aus (mit CRDT) /// Returns: modified_schema_tables - pub fn execute_internal_typed_with_context( + pub fn execute_internal_typed( tx: &Transaction, hlc_service: &HlcService, sql: &str, params: &[&dyn ToSql], - pk_context: &mut PkRemappingContext, ) -> Result, DatabaseError> { let mut ast_vec = parse_sql_statements(sql)?; if ast_vec.len() != 1 { return Err(DatabaseError::ExecutionError { sql: sql.to_string(), - reason: "execute_internal_typed_with_context sollte nur ein einzelnes SQL-Statement erhalten" + reason: "execute_internal_typed should only receive a single SQL statement" .to_string(), table: None, }); @@ -140,56 +52,15 @@ impl SqlExecutor { } let sql_str = statement.to_string(); - eprintln!("DEBUG: Transformed SQL (execute path): {}", sql_str); + eprintln!("DEBUG: Transformed SQL: {}", sql_str); - // Spezielle Behandlung für INSERT Statements (mit FK-Remapping, OHNE RETURNING) - if let Statement::Insert(ref insert_stmt) = statement { - if let TableObject::TableName(ref table_name) = insert_stmt.table { - let table_name_str = table_name - .to_string() - .trim_matches('`') - .trim_matches('"') - .to_string(); - - // Konvertiere Params zu Vec für Manipulation - let mut param_vec = params_to_vec(params, tx)?; - - // Hole Foreign Key Informationen - let fk_info = get_fk_info(tx, &table_name_str)?; - - // Remap FK-Werte in params (falls Mappings existieren) - remap_fk_params(insert_stmt, &mut param_vec, &fk_info, pk_context)?; - - let param_refs: Vec<&dyn ToSql> = - param_vec.iter().map(|v| v as &dyn ToSql).collect(); - - let mut stmt = tx - .prepare(&sql_str) - .map_err(|e| DatabaseError::ExecutionError { - sql: sql_str.clone(), - table: Some(table_name_str.clone()), - reason: format!("Prepare failed: {}", e), - })?; - - let mut rows = stmt - .query(params_from_iter(param_refs.iter())) - .map_err(|e| DatabaseError::ExecutionError { - sql: sql_str.clone(), - table: Some(table_name_str.clone()), - reason: format!("INSERT query execution failed: {}", e), - })?; - - let _ = rows.next()?; - } - } else { - // Nicht-INSERT Statements normal ausführen - tx.execute(&sql_str, params) - .map_err(|e| DatabaseError::ExecutionError { - sql: sql_str.clone(), - table: None, - reason: format!("Execute failed: {}", e), - })?; - } + // Führe Statement aus + tx.execute(&sql_str, params) + .map_err(|e| DatabaseError::ExecutionError { + sql: sql_str.clone(), + table: None, + reason: format!("Execute failed: {}", e), + })?; // Trigger-Logik für CREATE TABLE if let Statement::CreateTable(create_table_details) = statement { @@ -200,25 +71,20 @@ impl SqlExecutor { Ok(modified_schema_tables) } - /// Führt ein SQL Statement MIT RETURNING aus (mit CRDT und PK-Remapping) - /// Unterstützt automatisches FK-Remapping wenn vorherige INSERTs ON CONFLICT getriggert haben - /// - /// Diese Variante akzeptiert &[&dyn ToSql] direkt (wie von rusqlite::params![] erzeugt) + /// Führt ein SQL Statement MIT RETURNING aus (mit CRDT) /// Returns: (modified_schema_tables, returning_results) - /// returning_results enthält ALLE RETURNING-Spalten für INSERT/UPDATE/DELETE mit RETURNING - pub fn query_internal_typed_with_context( + pub fn query_internal_typed( tx: &Transaction, hlc_service: &HlcService, sql: &str, params: &[&dyn ToSql], - pk_context: &mut PkRemappingContext, ) -> Result<(HashSet, Vec>), DatabaseError> { let mut ast_vec = parse_sql_statements(sql)?; if ast_vec.len() != 1 { return Err(DatabaseError::ExecutionError { sql: sql.to_string(), - reason: "query_internal_typed_with_context sollte nur ein einzelnes SQL-Statement erhalten" + reason: "query_internal_typed should only receive a single SQL statement" .to_string(), table: None, }); @@ -245,493 +111,162 @@ impl SqlExecutor { let sql_str = statement.to_string(); eprintln!("DEBUG: Transformed SQL (with RETURNING): {}", sql_str); - // Spezielle Behandlung für INSERT Statements (mit PK-Remapping + RETURNING) - if let Statement::Insert(ref insert_stmt) = statement { - if let TableObject::TableName(ref table_name) = insert_stmt.table { - let table_name_str = table_name - .to_string() - .trim_matches('`') - .trim_matches('"') - .to_string(); - - // Konvertiere Params zu Vec für Manipulation - let mut param_vec = params_to_vec(params, tx)?; - - // Hole Table Schema um PKs und FKs zu identifizieren - let table_columns = - trigger::get_table_schema(tx, &table_name_str).map_err(|e| { - DatabaseError::ExecutionError { - sql: format!("PRAGMA table_info('{}')", table_name_str), - reason: e.to_string(), - table: Some(table_name_str.clone()), - } - })?; - - let pk_columns: Vec = table_columns - .iter() - .filter(|c| c.is_pk) - .map(|c| c.name.clone()) - .collect(); - - // Hole Foreign Key Informationen - let fk_info = get_fk_info(tx, &table_name_str)?; - - // 1. Extrahiere Original PK-Werte aus params (vor FK-Remapping) - let original_pk = - extract_pk_values_from_params(insert_stmt, ¶m_vec, &pk_columns)?; - - // 2. Remap FK-Werte in params (falls Mappings existieren) - remap_fk_params(insert_stmt, &mut param_vec, &fk_info, pk_context)?; - - // 3. Führe INSERT mit query() aus um RETURNING zu lesen - let mut stmt = tx - .prepare(&sql_str) - .map_err(|e| DatabaseError::ExecutionError { - sql: sql_str.clone(), - table: Some(table_name_str.clone()), - reason: e.to_string(), - })?; - - let column_names: Vec = stmt - .column_names() - .into_iter() - .map(|s| s.to_string()) - .collect(); - let num_columns = column_names.len(); - - let param_refs: Vec<&dyn ToSql> = - param_vec.iter().map(|v| v as &dyn ToSql).collect(); - - let mut rows = stmt - .query(params_from_iter(param_refs.iter())) - .map_err(|e| DatabaseError::ExecutionError { - sql: sql_str.clone(), - table: Some(table_name_str.clone()), - reason: e.to_string(), - })?; - - let mut result_vec: Vec> = Vec::new(); - - // 4. Lese ALLE RETURNING Werte und speichere PK-Mapping - while let Some(row) = rows.next().map_err(|e| DatabaseError::ExecutionError { - sql: sql_str.clone(), - table: Some(table_name_str.clone()), - reason: e.to_string(), - })? { - // Extrahiere PK-Werte für PK-Remapping - let actual_pk = extract_pk_values_from_row(&row, &pk_columns)?; - pk_context.add_mapping( - table_name_str.clone(), - original_pk.clone(), - actual_pk.clone(), - ); - - // Extrahiere ALLE Spalten für RETURNING-Ergebnis - let mut row_values: Vec = Vec::with_capacity(num_columns); - - for i in 0..num_columns { - let value_ref = - row.get_ref(i) - .map_err(|e| DatabaseError::RowProcessingError { - reason: format!("Failed to get column {}: {}", i, e), - })?; - let json_val = convert_value_ref_to_json(value_ref)?; - row_values.push(json_val); - } - result_vec.push(row_values); - } - - return Ok((modified_schema_tables, result_vec)); - } - } - - // Für UPDATE/DELETE mit RETURNING: query() verwenden (kein PK-Remapping nötig) + // Prepare und query ausführen let mut stmt = tx .prepare(&sql_str) - .map_err(|e| DatabaseError::PrepareError { + .map_err(|e| DatabaseError::ExecutionError { + sql: sql_str.clone(), + table: None, reason: e.to_string(), })?; - let num_columns = stmt.column_count(); + let column_names: Vec = stmt + .column_names() + .into_iter() + .map(|s| s.to_string()) + .collect(); + let num_columns = column_names.len(); - let mut rows = stmt.query(params).map_err(|e| DatabaseError::QueryError { - reason: e.to_string(), - })?; + let mut rows = stmt + .query(params_from_iter(params.iter())) + .map_err(|e| DatabaseError::ExecutionError { + sql: sql_str.clone(), + table: None, + reason: e.to_string(), + })?; let mut result_vec: Vec> = Vec::new(); - while let Some(row) = rows.next().map_err(|e| DatabaseError::RowProcessingError { - reason: format!("Row iteration error: {}", e), + // Lese alle RETURNING Zeilen + while let Some(row) = rows.next().map_err(|e| DatabaseError::ExecutionError { + sql: sql_str.clone(), + table: None, + reason: e.to_string(), })? { - let mut row_values: Vec = Vec::with_capacity(num_columns); - + let mut row_values: Vec = Vec::new(); for i in 0..num_columns { - let value_ref = row - .get_ref(i) - .map_err(|e| DatabaseError::RowProcessingError { - reason: format!("Failed to get column {}: {}", i, e), - })?; - - let json_val = convert_value_ref_to_json(value_ref)?; - row_values.push(json_val); + let value_ref = row.get_ref(i).map_err(|e| DatabaseError::ExecutionError { + sql: sql_str.clone(), + table: None, + reason: e.to_string(), + })?; + let json_value = convert_value_ref_to_json(value_ref)?; + row_values.push(json_value); } result_vec.push(row_values); } + // Trigger-Logik für CREATE TABLE + if let Statement::CreateTable(create_table_details) = statement { + let table_name_str = create_table_details.name.to_string(); + trigger::setup_triggers_for_table(tx, &table_name_str, false)?; + } + Ok((modified_schema_tables, result_vec)) } - /// Legacy-Methode ohne PK-Remapping Context - pub fn execute_internal_typed( - tx: &Transaction, - hlc_service: &HlcService, - sql: &str, - params: &[&dyn ToSql], - ) -> Result, DatabaseError> { - let mut context = PkRemappingContext::new(); - Self::execute_internal_typed_with_context(tx, hlc_service, sql, params, &mut context) - } - /// Führt SQL aus (mit CRDT-Transformation) - OHNE Permission-Check - /// Wrapper um execute_internal_typed für JsonValue-Parameter - /// Nutzt PK-Remapping Logik für INSERT mit ON CONFLICT + /// Führt ein einzelnes SQL Statement OHNE Typinformationen aus (JSON params) pub fn execute_internal( tx: &Transaction, hlc_service: &HlcService, sql: &str, params: &[JsonValue], ) -> Result, DatabaseError> { - // Parameter validation - let total_placeholders = sql.matches('?').count(); - if total_placeholders != params.len() { - return Err(DatabaseError::ParameterMismatchError { - expected: total_placeholders, - provided: params.len(), - sql: sql.to_string(), - }); - } - - // Convert JsonValue params to SqliteValue - let params_converted: Vec = params + let sql_params: Vec = params .iter() - .map(ValueConverter::json_to_rusqlite_value) + .map(|v| crate::database::core::ValueConverter::json_to_rusqlite_value(v)) .collect::, _>>()?; - - // Convert to &dyn ToSql references - let param_refs: Vec<&dyn ToSql> = - params_converted.iter().map(|v| v as &dyn ToSql).collect(); - - // Call execute_internal_typed (mit PK-Remapping!) + let param_refs: Vec<&dyn ToSql> = sql_params.iter().map(|p| p as &dyn ToSql).collect(); Self::execute_internal_typed(tx, hlc_service, sql, ¶m_refs) } - /// Führt SELECT aus (mit CRDT-Transformation) - OHNE Permission-Check - pub fn select_internal( - conn: &Connection, + /// Query-Variante (mit RETURNING) OHNE Typinformationen (JSON params) + pub fn query_internal( + tx: &Transaction, + hlc_service: &HlcService, sql: &str, params: &[JsonValue], - ) -> Result>, DatabaseError> { - // Parameter validation - let total_placeholders = sql.matches('?').count(); - if total_placeholders != params.len() { - return Err(DatabaseError::ParameterMismatchError { - expected: total_placeholders, - provided: params.len(), - sql: sql.to_string(), + ) -> Result<(HashSet, Vec>), DatabaseError> { + let sql_params: Vec = params + .iter() + .map(|v| crate::database::core::ValueConverter::json_to_rusqlite_value(v)) + .collect::, _>>()?; + let param_refs: Vec<&dyn ToSql> = sql_params.iter().map(|p| p as &dyn ToSql).collect(); + Self::query_internal_typed(tx, hlc_service, sql, ¶m_refs) + } + + /// Führt mehrere SQL Statements als Batch aus + pub fn execute_batch_internal( + tx: &Transaction, + hlc_service: &HlcService, + sqls: &[String], + params: &[Vec], + ) -> Result, DatabaseError> { + if sqls.len() != params.len() { + return Err(DatabaseError::ExecutionError { + sql: format!("{} statements but {} param sets", sqls.len(), params.len()), + reason: "Statement count and parameter count mismatch".to_string(), + table: None, }); } + let mut all_modified_tables = HashSet::new(); + + for (sql, param_set) in sqls.iter().zip(params.iter()) { + let modified_tables = Self::execute_internal(tx, hlc_service, sql, param_set)?; + all_modified_tables.extend(modified_tables); + } + + Ok(all_modified_tables) + } + + /// Query für SELECT-Statements (read-only, kein CRDT nötig außer Filter) + pub fn query_select( + conn: &rusqlite::Connection, + sql: &str, + params: &[JsonValue], + ) -> Result>, DatabaseError> { let mut ast_vec = parse_sql_statements(sql)?; - if ast_vec.is_empty() { - return Ok(vec![]); + if ast_vec.len() != 1 { + return Err(DatabaseError::ExecutionError { + sql: sql.to_string(), + reason: "query_select should only receive a single SELECT statement".to_string(), + table: None, + }); } - // Validate that all statements are queries - for stmt in &ast_vec { - if !matches!(stmt, Statement::Query(_)) { - return Err(DatabaseError::ExecutionError { - sql: sql.to_string(), - reason: "Only SELECT statements are allowed".to_string(), - table: None, - }); - } - } - - let sql_params = ValueConverter::convert_params(params)?; - // Hard Delete: Keine SELECT-Transformation mehr nötig let stmt_to_execute = ast_vec.pop().unwrap(); let transformed_sql = stmt_to_execute.to_string(); eprintln!("DEBUG: SELECT (no transformation): {}", transformed_sql); + // Convert JSON params to SQLite values + let sql_params: Vec = params + .iter() + .map(|v| crate::database::core::ValueConverter::json_to_rusqlite_value(v)) + .collect::, _>>()?; + let mut prepared_stmt = conn.prepare(&transformed_sql)?; let num_columns = prepared_stmt.column_count(); - let mut rows = prepared_stmt - .query(params_from_iter(&sql_params[..])) - .map_err(|e| DatabaseError::QueryError { - reason: e.to_string(), - })?; + let param_refs: Vec<&dyn ToSql> = sql_params.iter().map(|p| p as &dyn ToSql).collect(); - let mut result_vec: Vec> = Vec::new(); - - while let Some(row) = rows.next().map_err(|e| DatabaseError::RowProcessingError { - reason: format!("Row iteration error: {}", e), - })? { - let mut row_values: Vec = Vec::with_capacity(num_columns); + let mut rows = prepared_stmt.query(params_from_iter(param_refs.iter()))?; + let mut result: Vec> = Vec::new(); + while let Some(row) = rows.next()? { + let mut row_values: Vec = Vec::new(); for i in 0..num_columns { - let value_ref = row - .get_ref(i) - .map_err(|e| DatabaseError::RowProcessingError { - reason: format!("Failed to get column {}: {}", i, e), - })?; - - let json_val = convert_value_ref_to_json(value_ref)?; - row_values.push(json_val); + let value_ref = row.get_ref(i)?; + let json_value = convert_value_ref_to_json(value_ref)?; + row_values.push(json_value); } - result_vec.push(row_values); + result.push(row_values); } - Ok(result_vec) - } - - /// Führt SQL mit CRDT-Transformation aus und gibt RETURNING-Ergebnisse zurück - /// Speziell für INSERT/UPDATE/DELETE mit RETURNING (Drizzle-Integration) - /// Nutzt PK-Remapping für INSERT-Operationen - pub fn query_internal( - tx: &Transaction, - hlc_service: &HlcService, - sql: &str, - params: &[JsonValue], - ) -> Result>, DatabaseError> { - // Parameter validation - let total_placeholders = sql.matches('?').count(); - if total_placeholders != params.len() { - return Err(DatabaseError::ParameterMismatchError { - expected: total_placeholders, - provided: params.len(), - sql: sql.to_string(), - }); - } - - // Parameter konvertieren - let params_converted: Vec = params - .iter() - .map(ValueConverter::json_to_rusqlite_value) - .collect::, _>>()?; - - // Convert to &dyn ToSql references - let param_refs: Vec<&dyn ToSql> = - params_converted.iter().map(|v| v as &dyn ToSql).collect(); - - // Call query_internal_typed_with_context (mit PK-Remapping!) - let mut context = PkRemappingContext::new(); - let (_tables, results) = Self::query_internal_typed_with_context( - tx, - hlc_service, - sql, - ¶m_refs, - &mut context, - )?; - - Ok(results) + Ok(result) } } - -// ========================= -// Helper-Funktionen für FK-Remapping -// ========================= - -/// Strukturiert FK-Informationen für einfache Lookups -#[derive(Debug)] -struct FkInfo { - /// column_name -> (referenced_table, referenced_column) - mappings: HashMap, -} - -/// Hole Foreign Key Informationen für eine Tabelle -fn get_fk_info(tx: &Transaction, table_name: &str) -> Result { - // Nutze PRAGMA foreign_key_list um FK-Beziehungen zu holen - let sql = format!("PRAGMA foreign_key_list('{}');", table_name); - let mut stmt = tx - .prepare(&sql) - .map_err(|e| DatabaseError::ExecutionError { - sql: sql.clone(), - reason: e.to_string(), - table: Some(table_name.to_string()), - })?; - - let mut mappings = HashMap::new(); - let rows = stmt - .query_map([], |row| { - Ok(( - row.get::<_, String>("from")?, // FK column in this table - row.get::<_, String>("table")?, // referenced table - row.get::<_, String>("to")?, // referenced column - )) - }) - .map_err(|e| DatabaseError::ExecutionError { - sql, - reason: e.to_string(), - table: Some(table_name.to_string()), - })?; - - for row in rows { - let (from_col, ref_table, ref_col) = row.map_err(|e| DatabaseError::ExecutionError { - sql: format!("PRAGMA foreign_key_list('{}')", table_name), - reason: e.to_string(), - table: Some(table_name.to_string()), - })?; - mappings.insert(from_col, (ref_table, ref_col)); - } - - Ok(FkInfo { mappings }) -} - -/// Konvertiert &[&dyn ToSql] zu Vec für Manipulation -/// Nutzt einen Dummy-Query um die Parameter-Werte zu extrahieren -fn params_to_vec( - params: &[&dyn ToSql], - tx: &Transaction, -) -> Result, DatabaseError> { - let mut values = Vec::new(); - - // Erstelle eine Dummy-Query mit genau so vielen Platzhaltern wie wir Parameter haben - // z.B. "SELECT ?, ?, ?" - if params.is_empty() { - return Ok(values); - } - - let placeholders = vec!["?"; params.len()].join(", "); - let dummy_sql = format!("SELECT {}", placeholders); - - let mut stmt = tx - .prepare(&dummy_sql) - .map_err(|e| DatabaseError::ExecutionError { - sql: dummy_sql.clone(), - reason: format!("Failed to prepare dummy query: {}", e), - table: None, - })?; - - // Führe die Query aus und extrahiere die Werte aus der Row - let mut rows = stmt - .query(params) - .map_err(|e| DatabaseError::ExecutionError { - sql: dummy_sql.clone(), - reason: format!("Failed to execute dummy query: {}", e), - table: None, - })?; - - if let Some(row) = rows.next().map_err(|e| DatabaseError::ExecutionError { - sql: dummy_sql, - reason: format!("Failed to read dummy query result: {}", e), - table: None, - })? { - // Extrahiere alle Spalten-Werte - for i in 0..params.len() { - let value: SqliteValue = row.get(i).map_err(|e| DatabaseError::ExecutionError { - sql: format!("SELECT ..."), - reason: format!("Failed to extract value at index {}: {}", i, e), - table: None, - })?; - values.push(value); - } - } - - Ok(values) -} - -/// Extrahiert PK-Werte aus den INSERT-Parametern -fn extract_pk_values_from_params( - insert_stmt: &Insert, - params: &[SqliteValue], - pk_columns: &[String], -) -> Result { - let mut pk_values = PkValues::new(); - - // Finde die Positionen der PK-Spalten in der INSERT column list - for pk_col in pk_columns { - if let Some(pos) = insert_stmt.columns.iter().position(|c| &c.value == pk_col) { - // Hole den Parameter-Wert an dieser Position - if pos < params.len() { - // Konvertiere SqliteValue zu String - let value_str = value_to_string(¶ms[pos]); - pk_values.insert(pk_col.clone(), value_str); - } - } - } - - Ok(pk_values) -} - -/// Remapped FK-Werte in den Parametern basierend auf dem PK-Remapping Context -fn remap_fk_params( - insert_stmt: &Insert, - params: &mut Vec, - fk_info: &FkInfo, - pk_context: &PkRemappingContext, -) -> Result<(), DatabaseError> { - // Für jede FK-Spalte: prüfe ob Remapping nötig ist - for (col_name, (ref_table, ref_col)) in &fk_info.mappings { - // Finde Position der FK-Spalte in der INSERT column list - if let Some(pos) = insert_stmt - .columns - .iter() - .position(|c| &c.value == col_name) - { - if pos < params.len() { - // Hole aktuellen FK-Wert (als String) - let current_value = value_to_string(¶ms[pos]); - - // Versuche zu remappen - let new_value = pk_context.remap_fk_value(ref_table, ref_col, ¤t_value); - - if new_value != current_value { - // Ersetze den Parameter-Wert - params[pos] = SqliteValue::Text(new_value); - eprintln!( - "DEBUG: Remapped FK {}={} to {:?}", - col_name, current_value, params[pos] - ); - } - } - } - } - - Ok(()) -} - -/// Hilfsfunktion: Konvertiert SqliteValue zu String für Vergleiche -fn value_to_string(value: &SqliteValue) -> String { - match value { - SqliteValue::Null => "NULL".to_string(), - SqliteValue::Integer(i) => i.to_string(), - SqliteValue::Real(r) => r.to_string(), - SqliteValue::Text(s) => s.clone(), - SqliteValue::Blob(b) => format!("BLOB({} bytes)", b.len()), - } -} - -/// Extrahiert PK-Werte aus einer RETURNING Row -fn extract_pk_values_from_row( - row: &rusqlite::Row, - pk_columns: &[String], -) -> Result { - let mut pk_values = PkValues::new(); - - for pk_col in pk_columns.iter() { - let value: String = - row.get(pk_col.as_str()) - .map_err(|e| DatabaseError::ExecutionError { - sql: "RETURNING clause".to_string(), - reason: format!("Failed to extract PK column '{}': {}", pk_col, e), - table: None, - })?; - pk_values.insert(pk_col.clone(), value); - } - - Ok(pk_values) -}