From cbef2046fd8075088db68e3ea96e309044e11517 Mon Sep 17 00:00:00 2001 From: polv Date: Tue, 3 Mar 2026 14:02:53 +0700 Subject: [PATCH] (feat) sql runner for the frontend invoke --- src-core/src/lib.rs | 2 +- src-core/src/service.rs | 68 ++++++++++++++++++++++++++++-- src-react/src/pages/SearchPage.tsx | 4 +- src-tauri/src/command.rs | 11 ++++- src-tauri/src/lib.rs | 6 ++- 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src-core/src/lib.rs b/src-core/src/lib.rs index cb368c1..84a79d9 100644 --- a/src-core/src/lib.rs +++ b/src-core/src/lib.rs @@ -5,7 +5,7 @@ mod util; pub use db::{YomitanRow, YomitanZipImportProgress, YomitanZipImportResult, ZipSource}; pub use error::CJDicError; -pub use service::AppService; +pub use service::{AppService, SqlParam}; pub use util::Timer; pub fn add(left: u64, right: u64) -> u64 { diff --git a/src-core/src/service.rs b/src-core/src/service.rs index 48ad2e2..26add51 100644 --- a/src-core/src/service.rs +++ b/src-core/src/service.rs @@ -3,11 +3,11 @@ use std::{ path::Path, }; -use rusqlite::Connection; -use serde::Serialize; +use rusqlite::{Connection, params_from_iter}; +use serde::{Deserialize, Serialize}; use crate::{ - ZipSource, + Timer, ZipSource, db::{ Database, YOMITAN_DBFILE, YomitanRow, YomitanWriter, YomitanZipImportProgress, YomitanZipImportResult, @@ -21,6 +21,16 @@ pub struct LoadYomitanZipDirResult { pub to_be_removed_dicts: Vec, } +#[derive(Deserialize)] +#[serde(untagged)] +pub enum SqlParam { + Null, + Integer(i64), + Real(f64), + Text(String), + Bool(bool), +} + pub struct AppService { db: Database, } @@ -31,6 +41,58 @@ impl AppService { Ok(Self { db }) } + pub fn execute_sql( + &self, + sql: String, + params: Vec, + ) -> Result { + let _timer = Timer::new(sql.to_string()); + + let conn = self.db.conn.lock().unwrap(); + let converted_params: Vec<_> = params + .into_iter() + .map(|p| match p { + SqlParam::Null => rusqlite::types::Value::Null, + SqlParam::Integer(i) => rusqlite::types::Value::Integer(i), + SqlParam::Real(n) => rusqlite::types::Value::Real(n), + SqlParam::Text(s) => rusqlite::types::Value::Text(s), + SqlParam::Bool(b) => rusqlite::types::Value::Integer(b as i64), + }) + .collect(); + + let mut stmt = conn.prepare(&sql)?; + + if stmt.column_count() == 0 { + let affected = stmt.execute(params_from_iter(converted_params))?; + return Ok(serde_json::Value::Number(affected.into())); + } + + let column_names: Vec = stmt.column_names().iter().map(|s| s.to_string()).collect(); + let rows = stmt.query_map(params_from_iter(converted_params), |row| { + let mut obj = serde_json::Map::new(); + + for (i, name) in column_names.iter().enumerate() { + let value = match row.get_ref(i)? { + rusqlite::types::ValueRef::Null => serde_json::Value::Null, + rusqlite::types::ValueRef::Integer(i) => serde_json::Value::Number(i.into()), + // Boolean are also rendered as an integer. + rusqlite::types::ValueRef::Real(n) => serde_json::Number::from_f64(n) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + rusqlite::types::ValueRef::Text(t) => { + serde_json::Value::String(String::from_utf8_lossy(t).into()) + } + _ => serde_json::Value::Null, + }; + obj.insert(name.clone(), value); + } + return Ok(serde_json::Value::Object(obj)); + })?; + + let result: Result, _> = rows.collect(); + Ok(serde_json::Value::Array(result?)) + } + pub fn search_yomitan( &self, q_term: &str, diff --git a/src-react/src/pages/SearchPage.tsx b/src-react/src/pages/SearchPage.tsx index ce95179..4e6b168 100644 --- a/src-react/src/pages/SearchPage.tsx +++ b/src-react/src/pages/SearchPage.tsx @@ -5,7 +5,7 @@ import { useRef, useState, } from "react"; -import { toHiragana } from "wanakana"; +import { toKana } from "wanakana"; import { Box, @@ -145,7 +145,7 @@ function SearchPage() { function onSearchboxChange(q: string) { if (isAutoKana) { - q = toHiragana(q, { useObsoleteKana: true, IMEMode: true }); + q = toKana(q, { useObsoleteKana: true, IMEMode: true }); } setQ(q); diff --git a/src-tauri/src/command.rs b/src-tauri/src/command.rs index 7347763..d87f3e1 100644 --- a/src-tauri/src/command.rs +++ b/src-tauri/src/command.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, path::PathBuf}; -use cjdic2_core::{AppService, CJDicError, YomitanRow, ZipSource}; +use cjdic2_core::{AppService, CJDicError, SqlParam, YomitanRow, ZipSource}; use tauri::{AppHandle, Emitter, Manager, Runtime, path::BaseDirectory}; use tauri_plugin_fs::FsExt; @@ -104,3 +104,12 @@ pub async fn search_yomitan( ) -> Result, CJDicError> { state.search_yomitan(q_term, q_reading, limit, offset) } + +#[tauri::command] +pub async fn execute_sql( + sql: String, + params: Vec, + state: tauri::State<'_, AppService>, +) -> Result { + state.execute_sql(sql, params) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e6301cb..e00db29 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -25,7 +25,11 @@ pub fn run() { Ok(()) }) - .invoke_handler(tauri::generate_handler![init_yomitan, search_yomitan]) + .invoke_handler(tauri::generate_handler![ + init_yomitan, + search_yomitan, + execute_sql + ]) .build(tauri::generate_context!()) .expect("error while running tauri application") .run(|_app_handle, event| match event {