use std::cell::RefCell;
use wasm_bindgen::prelude::*;
use crate::api::{Database, Transaction as ApiTransaction};
use crate::common::version::{GIT_COMMIT, VERSION};
use crate::core::types::DataType;
use crate::core::Value;
#[wasm_bindgen]
pub struct StoolapDB {
db: Database,
tx: RefCell<Option<ApiTransaction>>,
}
#[wasm_bindgen]
impl StoolapDB {
#[wasm_bindgen(constructor)]
pub fn new() -> Result<StoolapDB, JsValue> {
use std::sync::Once;
static SET_HOOK: Once = Once::new();
SET_HOOK.call_once(|| {
std::panic::set_hook(Box::new(|info| {
let msg = format!("stoolap panic: {}", info);
web_sys::console::error_1(&msg.into());
}));
});
let db = Database::open_in_memory()
.map_err(|e| JsValue::from_str(&format!("Failed to open database: {}", e)))?;
Ok(StoolapDB {
db,
tx: RefCell::new(None),
})
}
pub fn version(&self) -> String {
if GIT_COMMIT != "unknown" {
let short = if GIT_COMMIT.len() > 7 {
&GIT_COMMIT[..7]
} else {
GIT_COMMIT
};
format!("{}-{}", VERSION, short)
} else {
VERSION.to_string()
}
}
pub fn execute(&self, sql: &str) -> String {
self.execute_inner(sql)
}
pub fn execute_batch(&self, sql: &str) -> String {
let mut last_result = String::from(r#"{"type":"affected","affected":0}"#);
for stmt in split_sql_statements(sql) {
let trimmed = stmt.trim();
if trimmed.is_empty() {
continue;
}
last_result = self.execute_inner(trimmed);
if last_result.starts_with(r#"{"type":"error"#) {
return last_result;
}
}
last_result
}
}
impl StoolapDB {
fn execute_inner(&self, sql: &str) -> String {
let trimmed = sql.trim();
if trimmed.is_empty() {
return r#"{"type":"affected","affected":0}"#.to_string();
}
let upper = trimmed.to_uppercase();
let upper = upper.trim();
if upper.starts_with("BEGIN") {
return self.begin_transaction();
} else if upper == "COMMIT" {
return self.commit_transaction();
} else if upper == "ROLLBACK" {
return self.rollback_transaction();
}
let is_query = upper.starts_with("SELECT")
|| upper.starts_with("WITH")
|| upper.starts_with("SHOW")
|| upper.starts_with("DESCRIBE")
|| upper.starts_with("DESC ")
|| upper.starts_with("EXPLAIN")
|| upper.starts_with("VACUUM")
|| (upper.starts_with("PRAGMA") && !upper.contains('='))
|| upper.contains(" RETURNING ")
|| upper.ends_with(" RETURNING");
if is_query {
self.execute_read_query(trimmed)
} else {
self.execute_write_query(trimmed)
}
}
fn begin_transaction(&self) -> String {
let mut tx_ref = self.tx.borrow_mut();
if tx_ref.is_some() {
return error_json("already in a transaction");
}
match self.db.begin() {
Ok(tx) => {
*tx_ref = Some(tx);
r#"{"type":"affected","affected":0}"#.to_string()
}
Err(e) => error_json(&e.to_string()),
}
}
fn commit_transaction(&self) -> String {
let mut tx_ref = self.tx.borrow_mut();
match tx_ref.take() {
Some(mut tx) => match tx.commit() {
Ok(_) => r#"{"type":"affected","affected":0}"#.to_string(),
Err(e) => error_json(&e.to_string()),
},
None => error_json("not in a transaction"),
}
}
fn rollback_transaction(&self) -> String {
let mut tx_ref = self.tx.borrow_mut();
match tx_ref.take() {
Some(mut tx) => match tx.rollback() {
Ok(_) => r#"{"type":"affected","affected":0}"#.to_string(),
Err(e) => error_json(&e.to_string()),
},
None => error_json("not in a transaction"),
}
}
fn execute_read_query(&self, query: &str) -> String {
let mut tx_ref = self.tx.borrow_mut();
let rows_result = if let Some(ref mut tx) = *tx_ref {
match tx.query(query, ()) {
Ok(r) => r,
Err(e) => return error_json(&e.to_string()),
}
} else {
drop(tx_ref);
match self.db.query(query, ()) {
Ok(r) => r,
Err(e) => return error_json(&e.to_string()),
}
};
let columns: Vec<String> = rows_result.columns().to_vec();
let mut result_rows: Vec<Vec<serde_json::Value>> = Vec::new();
for row_result in rows_result {
match row_result {
Ok(row) => {
let mut json_row = Vec::with_capacity(columns.len());
for i in 0..columns.len() {
json_row.push(match row.get_value(i) {
Some(v) => value_to_json(v),
None => serde_json::Value::Null,
});
}
result_rows.push(json_row);
}
Err(e) => return error_json(&e.to_string()),
}
}
let row_count = result_rows.len();
serde_json::json!({
"type": "rows",
"columns": columns,
"rows": result_rows,
"count": row_count
})
.to_string()
}
fn execute_write_query(&self, query: &str) -> String {
let mut tx_ref = self.tx.borrow_mut();
let rows_affected = if let Some(ref mut tx) = *tx_ref {
match tx.execute(query, ()) {
Ok(n) => n,
Err(e) => return error_json(&e.to_string()),
}
} else {
drop(tx_ref);
match self.db.execute(query, ()) {
Ok(n) => n,
Err(e) => return error_json(&e.to_string()),
}
};
serde_json::json!({
"type": "affected",
"affected": rows_affected
})
.to_string()
}
}
fn error_json(msg: &str) -> String {
serde_json::json!({
"type": "error",
"message": msg
})
.to_string()
}
fn value_to_json(val: &Value) -> serde_json::Value {
match val {
Value::Null(_) => serde_json::Value::Null,
Value::Boolean(b) => serde_json::json!(b),
Value::Integer(i) => serde_json::json!(i),
Value::Float(f) => {
if f.is_finite() {
serde_json::json!(f)
} else {
serde_json::Value::Null
}
}
Value::Text(s) => serde_json::json!(s.as_str()),
Value::Timestamp(ts) => {
serde_json::json!(ts.format("%Y-%m-%dT%H:%M:%SZ").to_string())
}
Value::Extension(data) if data.first() == Some(&(DataType::Json as u8)) => {
serde_json::json!(std::str::from_utf8(&data[1..]).unwrap_or(""))
}
Value::Extension(data) if data.first() == Some(&(DataType::Vector as u8)) => {
serde_json::json!(crate::core::value::format_vector_bytes(&data[1..]))
}
Value::Extension(_) => serde_json::Value::Null,
}
}
fn split_sql_statements(input: &str) -> Vec<String> {
let mut statements = Vec::new();
let mut current_statement = String::new();
let mut in_single_quotes = false;
let mut in_double_quotes = false;
let mut in_line_comment = false;
let mut in_block_comment = false;
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
let char = chars[i];
if in_line_comment {
if char == '\n' {
in_line_comment = false;
current_statement.push(char);
}
i += 1;
continue;
}
if !in_single_quotes
&& !in_double_quotes
&& !in_block_comment
&& char == '-'
&& i + 1 < chars.len()
&& chars[i + 1] == '-'
{
let after_second_dash = if i + 2 < chars.len() {
chars[i + 2]
} else {
'\0'
};
if after_second_dash == '\0'
|| after_second_dash == ' '
|| after_second_dash == '\t'
|| after_second_dash == '\n'
|| after_second_dash == '\r'
{
in_line_comment = true;
i += 2;
continue;
}
}
if in_block_comment {
if char == '*' && i + 1 < chars.len() && chars[i + 1] == '/' {
in_block_comment = false;
i += 2;
continue;
}
i += 1;
continue;
}
if !in_single_quotes
&& !in_double_quotes
&& char == '/'
&& i + 1 < chars.len()
&& chars[i + 1] == '*'
{
in_block_comment = true;
i += 2;
continue;
}
if !in_block_comment && !in_line_comment {
if char == '\'' && (i == 0 || chars[i - 1] != '\\') {
in_single_quotes = !in_single_quotes;
} else if char == '"' && (i == 0 || chars[i - 1] != '\\') {
in_double_quotes = !in_double_quotes;
}
}
if char == ';'
&& !in_single_quotes
&& !in_double_quotes
&& !in_block_comment
&& !in_line_comment
{
statements.push(current_statement.clone());
current_statement.clear();
} else {
current_statement.push(char);
}
i += 1;
}
if !current_statement.is_empty() {
statements.push(current_statement);
}
statements
}