/* DrCr: Web-based double-entry bookkeeping framework Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use indexmap::IndexMap; use serde_json::Value as JsonValue; use sqlx::{Column, Executor, Row, Sqlite, Transaction, TypeInfo, Value, ValueRef}; use sqlx::query::Query; use sqlx::sqlite::{SqliteArguments, SqliteRow, SqliteValueRef}; use sqlx::types::time::{Date, PrimitiveDateTime, Time}; use tokio::sync::Mutex; use tauri::State; use tauri_plugin_sql::{DbInstances, DbPool, Error}; use crate::AppState; pub type SqliteTransaction = Transaction<'static, Sqlite>; #[tauri::command] pub async fn sql_transaction_begin(state: State<'_, Mutex>, db_instances: State<'_, DbInstances>, db: String) -> Result { let instances = db_instances.0.read().await; let db = instances.get(&db).ok_or(Error::DatabaseNotLoaded(db))?; let pool = match db { DbPool::Sqlite(pool) => pool, //_ => panic!("Unexpected non-SQLite backend"), }; // Open transaction let transaction = pool.begin().await?; // Store transaction in state let mut state = state.lock().await; let available_index = state.sql_transactions.iter().position(|t| t.is_none()); match available_index { Some(i) => { state.sql_transactions[i] = Some(transaction); Ok(i) } None => { state.sql_transactions.push(Some(transaction)); Ok(state.sql_transactions.len() - 1) } } } #[tauri::command] pub async fn sql_transaction_execute(state: State<'_, Mutex>, transaction_instance_id: usize, query: String, values: Vec) -> Result<(u64, i64), Error> { let mut state = state.lock().await; let transaction = state.sql_transactions.get_mut(transaction_instance_id) .expect("Invalid database transaction ID") .as_mut() // Take reference to transaction rather than moving out of the Vec .expect("Database transaction ID used after closed"); let query = prepare_query(&query, values); let result = transaction.execute(query).await?; Ok(( result.rows_affected(), result.last_insert_rowid(), )) } #[tauri::command] pub async fn sql_transaction_select(state: State<'_, Mutex>, transaction_instance_id: usize, query: String, values: Vec) -> Result>, Error> { let mut state = state.lock().await; let transaction = state.sql_transactions.get_mut(transaction_instance_id) .expect("Invalid database transaction ID") .as_mut() // Take reference to transaction rather than moving out of the Vec .expect("Database transaction ID used after closed"); let query = prepare_query(&query, values); let rows = transaction.fetch_all(query).await?; rows_to_vec(rows) } #[tauri::command] pub async fn sql_transaction_rollback(state: State<'_, Mutex>, transaction_instance_id: usize) -> Result<(), Error> { let mut state = state.lock().await; let transaction = state.sql_transactions.get_mut(transaction_instance_id) .expect("Invalid database transaction ID") .take() // Remove from Vec .expect("Database transaction ID used after closed"); transaction.rollback().await?; Ok(()) } #[tauri::command] pub async fn sql_transaction_commit(state: State<'_, Mutex>, transaction_instance_id: usize) -> Result<(), Error> { let mut state = state.lock().await; let transaction = state.sql_transactions.get_mut(transaction_instance_id) .expect("Invalid database transaction ID") .take() // Remove from Vec .expect("Database transaction ID used after closed"); transaction.commit().await?; Ok(()) } fn prepare_query<'a, 'b: 'a>(_query: &'b str, _values: Vec) -> Query<'b, Sqlite, SqliteArguments<'a>> { // Copied from tauri_plugin_sql/src/commands.rs // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Licensed under MIT/Apache 2.0 let mut query = sqlx::query(_query); for value in _values { if value.is_null() { query = query.bind(None::); } else if value.is_string() { query = query.bind(value.as_str().unwrap().to_owned()) } else if let Some(number) = value.as_number() { query = query.bind(number.as_f64().unwrap_or_default()) } else { query = query.bind(value); } } query } fn rows_to_vec(rows: Vec) -> Result>, Error> { // Copied from tauri_plugin_sql/src/commands.rs // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Licensed under MIT/Apache 2.0 let mut values = Vec::new(); for row in rows { let mut value = IndexMap::default(); for (i, column) in row.columns().iter().enumerate() { let v = row.try_get_raw(i)?; let v = decode_sqlite_to_json(v)?; value.insert(column.name().to_string(), v); } values.push(value); } Ok(values) } fn decode_sqlite_to_json(v: SqliteValueRef) -> Result { // Copied from tauri_plugin_sql/src/decode/sqlite.rs // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Licensed under MIT/Apache 2.0 // Same as tauri_plugin_sql::decode::sqlite::to_json but that function is not exposed if v.is_null() { return Ok(JsonValue::Null); } let res = match v.type_info().name() { "TEXT" => { if let Ok(v) = v.to_owned().try_decode() { JsonValue::String(v) } else { JsonValue::Null } } "REAL" => { if let Ok(v) = v.to_owned().try_decode::() { JsonValue::from(v) } else { JsonValue::Null } } "INTEGER" | "NUMERIC" => { if let Ok(v) = v.to_owned().try_decode::() { JsonValue::Number(v.into()) } else { JsonValue::Null } } "BOOLEAN" => { if let Ok(v) = v.to_owned().try_decode() { JsonValue::Bool(v) } else { JsonValue::Null } } "DATE" => { if let Ok(v) = v.to_owned().try_decode::() { JsonValue::String(v.to_string()) } else { JsonValue::Null } } "TIME" => { if let Ok(v) = v.to_owned().try_decode::