diff --git a/Cargo.lock b/Cargo.lock index 91c81416..0afcaaca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,6 +230,25 @@ dependencies = [ "syn", ] +[[package]] +name = "cbindgen" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" +dependencies = [ + "clap", + "heck", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", + "tempfile", + "toml", +] + [[package]] name = "cc" version = "1.0.74" @@ -367,7 +386,7 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cozo" -version = "0.1.2" +version = "0.1.3" dependencies = [ "approx", "base64", @@ -412,23 +431,20 @@ dependencies = [ [[package]] name = "cozo_c" -version = "0.1.1" +version = "0.1.3" dependencies = [ + "cbindgen", "cozo", "lazy_static", - "miette", - "serde_json", ] [[package]] name = "cozo_java" -version = "0.1.1" +version = "0.1.3" dependencies = [ "cozo", "lazy_static", - "miette", "robusta_jni", - "serde_json", ] [[package]] @@ -1684,6 +1700,9 @@ name = "serde" version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_bytes" @@ -2002,6 +2021,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + [[package]] name = "twoway" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 074cfc3c..297271c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cozo" -version = "0.1.2" +version = "0.1.3" edition = "2021" description = "A general-purpose, transactional, relational database that uses Datalog and focuses on graph data and algorithms" authors = ["Ziyang Hu"] diff --git a/cozo-lib-c/Cargo.toml b/cozo-lib-c/Cargo.toml index e270320b..9a5d8045 100644 --- a/cozo-lib-c/Cargo.toml +++ b/cozo-lib-c/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cozo_c" -version = "0.1.1" +version = "0.1.3" edition = "2021" license = "AGPL-3.0-or-later" homepage = "https://github.com/cozodb/cozo" @@ -19,6 +19,7 @@ io-uring = ["cozo/io-uring"] [dependencies] cozo = { version = "0.1.2", path = ".." } -miette = { version = "=5.3.0", features = ["fancy"] } -serde_json = "1.0.81" lazy_static = "1.4.0" + +[build-dependencies] +cbindgen = "0.24.3" \ No newline at end of file diff --git a/cozo-lib-c/build.rs b/cozo-lib-c/build.rs new file mode 100644 index 00000000..1e447966 --- /dev/null +++ b/cozo-lib-c/build.rs @@ -0,0 +1,27 @@ +/* + * Copyright 2022, The Cozo Project Authors. Licensed under MIT/Apache-2.0/BSD-3-Clause. + */ + +use std::env; + +use cbindgen::{Config, Language}; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let mut config = Config::default(); + config.cpp_compat = true; + cbindgen::Builder::new() + .with_config(config) + .with_crate(crate_dir) + .with_language(Language::C) + .with_include_guard("COZO_C_H") + .with_autogen_warning( + "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */", + ) + .with_header("/* Copyright 2022, The Cozo Project Authors. Licensed under MIT/Apache-2.0/BSD-3-Clause. */") + .with_documentation(true) + .generate() + .expect("Unable to generate bindings") + .write_to_file("cozo_c.h"); +} diff --git a/cozo-lib-c/cbindgen.toml b/cozo-lib-c/cbindgen.toml deleted file mode 100644 index e3237f7e..00000000 --- a/cozo-lib-c/cbindgen.toml +++ /dev/null @@ -1,6 +0,0 @@ -language = "C" -include_guard = "cozo_c_h" -autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" -cpp_compat = true -documentation = true -header = "/* Copyright 2022, The Cozo Project Authors. Licensed under MIT/Apache-2.0/BSD-3-Clause. */" diff --git a/cozo-lib-c/cozo_c.h b/cozo-lib-c/cozo_c.h index 85c5b42a..ceb4ddbc 100644 --- a/cozo-lib-c/cozo_c.h +++ b/cozo-lib-c/cozo_c.h @@ -1,7 +1,7 @@ /* Copyright 2022, The Cozo Project Authors. Licensed under MIT/Apache-2.0/BSD-3-Clause. */ -#ifndef cozo_c_h -#define cozo_c_h +#ifndef COZO_C_H +#define COZO_C_H /* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ @@ -49,10 +49,9 @@ bool cozo_close_db(int32_t id); * `true` if an error occurred. * * Returns a UTF-8-encoded C-string that **must** be freed with `cozo_free_str`. - * If `*errored` is false, then the string contains the JSON return value of the query. - * If `*errored` is true, then the string contains the error message. + * The string contains the JSON return value of the query. */ -char *cozo_run_query(int32_t db_id, const char *script_raw, const char *params_raw, bool *errored); +char *cozo_run_query(int32_t db_id, const char *script_raw, const char *params_raw); /** * Free any C-string returned from the Cozo C API. @@ -66,4 +65,4 @@ void cozo_free_str(char *s); } // extern "C" #endif // __cplusplus -#endif /* cozo_c_h */ +#endif /* COZO_C_H */ diff --git a/cozo-lib-c/example.c b/cozo-lib-c/example.c index dab74943..f2cbeb31 100644 --- a/cozo-lib-c/example.c +++ b/cozo-lib-c/example.c @@ -3,23 +3,17 @@ #include #include "cozo_c.h" -void run_query(int32_t db_id, const char* query) { +void run_query(int32_t db_id, const char *query) { const char *empty_params = "{}"; - bool errored; char *res; - res = cozo_run_query(db_id, query, empty_params, &errored); - - if (errored) { - printf("encountered an error:\n%s\n\n", res); - } else { - printf("query is successful with result:\n%s\n\n", res); - } + res = cozo_run_query(db_id, query, empty_params); + printf("%s\n", res); cozo_free_str(res); } int main() { int32_t db_id; - char* err = cozo_open_db("_test_db", &db_id); + char *err = cozo_open_db("_test_db", &db_id); if (err) { printf("%s", err); @@ -27,8 +21,7 @@ int main() { return -1; } - run_query(db_id, "?[a, b, c] <- [[1, 2, 3]]"); - run_query(db_id, "?[a] <- [[1, 2, 3]]"); + run_query(db_id, "?[] <- [[1, 2, 3]]"); cozo_close_db(db_id); diff --git a/cozo-lib-c/src/lib.rs b/cozo-lib-c/src/lib.rs index 5adeff3a..1bb27399 100644 --- a/cozo-lib-c/src/lib.rs +++ b/cozo-lib-c/src/lib.rs @@ -77,20 +77,19 @@ pub unsafe extern "C" fn cozo_close_db(id: i32) -> bool { /// `true` if an error occurred. /// /// Returns a UTF-8-encoded C-string that **must** be freed with `cozo_free_str`. -/// If `*errored` is false, then the string contains the JSON return value of the query. -/// If `*errored` is true, then the string contains the error message. +/// The string contains the JSON return value of the query. #[no_mangle] pub unsafe extern "C" fn cozo_run_query( db_id: i32, script_raw: *const c_char, params_raw: *const c_char, - errored: &mut bool, ) -> *mut c_char { let script = match CStr::from_ptr(script_raw).to_str() { Ok(p) => p, - Err(err) => { - *errored = true; - return CString::new(format!("{}", err)).unwrap().into_raw(); + Err(_) => { + return CString::new(r##"{"ok":false,"message":"script is not UTF-8 encoded"}"##) + .unwrap() + .into_raw(); } }; let db = { @@ -100,52 +99,26 @@ pub unsafe extern "C" fn cozo_run_query( }; match db_ref { None => { - *errored = true; - return CString::new("database already closed").unwrap().into_raw(); + return CString::new(r##"{"ok":false,"message":"database closed"}"##) + .unwrap() + .into_raw(); } Some(db) => db, } }; let params_str = match CStr::from_ptr(params_raw).to_str() { Ok(p) => p, - Err(err) => { - *errored = true; - return CString::new(format!("{}", err)).unwrap().into_raw(); - } - }; - - let params_map: serde_json::Value = match serde_json::from_str(¶ms_str) { - Ok(m) => m, Err(_) => { - *errored = true; - return CString::new("the given params argument is not valid JSON") - .unwrap() - .into_raw(); + return CString::new( + r##"{"ok":false,"message":"params argument is not UTF-8 encoded"}"##, + ) + .unwrap() + .into_raw(); } }; - let params_arg: BTreeMap<_, _> = match params_map { - serde_json::Value::Object(m) => m.into_iter().collect(), - _ => { - *errored = true; - return CString::new("the given params argument is not a JSON map") - .unwrap() - .into_raw(); - } - }; - let result = db.run_script(script, ¶ms_arg); - match result { - Ok(json) => { - *errored = false; - let json_str = json.to_string(); - CString::new(json_str).unwrap().into_raw() - } - Err(err) => { - let err_str = format!("{:?}", err); - *errored = true; - CString::new(err_str).unwrap().into_raw() - } - } + let result = db.run_script_str(script, ¶ms_str); + CString::new(result).unwrap().into_raw() } /// Free any C-string returned from the Cozo C API. diff --git a/cozo-lib-java/Cargo.toml b/cozo-lib-java/Cargo.toml index 06693393..7b5ff5d9 100644 --- a/cozo-lib-java/Cargo.toml +++ b/cozo-lib-java/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cozo_java" -version = "0.1.1" +version = "0.1.3" edition = "2021" license = "AGPL-3.0-or-later" homepage = "https://github.com/cozodb/cozo" @@ -19,7 +19,5 @@ io-uring = ["cozo/io-uring"] [dependencies] robusta_jni = "0.2.0" -cozo = { version = "0.1.2", path = ".." } -miette = { version = "=5.3.0", features = ["fancy"] } -serde_json = "1.0.81" +cozo = { version = "0.1.3", path = ".." } lazy_static = "1.4.0" diff --git a/cozo-lib-java/src/lib.rs b/cozo-lib-java/src/lib.rs index 46fbd8cb..1fd85142 100644 --- a/cozo-lib-java/src/lib.rs +++ b/cozo-lib-java/src/lib.rs @@ -22,7 +22,6 @@ lazy_static! { #[bridge] mod jni { - use std::collections::BTreeMap; use std::sync::atomic::Ordering; use robusta_jni::convert::{IntoJavaValue, Signature, TryFromJavaValue, TryIntoJavaValue}; @@ -73,22 +72,7 @@ mod jni { let db = db_ref.ok_or_else(|| JniError::from("database already closed"))?; db }; - let params_map: serde_json::Value = serde_json::from_str(¶ms_str) - .map_err(|_| JniError::from("the given params argument is not valid JSON"))?; - - let params_arg: BTreeMap<_, _> = match params_map { - serde_json::Value::Object(m) => m.into_iter().collect(), - _ => { - return Err(JniError::from( - "the given params argument is not a JSON map", - )) - } - }; - let result = db.run_script(&script, ¶ms_arg); - match result { - Ok(json) => Ok(json.to_string()), - Err(err) => Err(JniError::from(format!("{:?}", err))), - } + Ok(db.run_script_str(&script, ¶ms_str)) } } } diff --git a/src/bin/cozoserver.rs b/src/bin/cozoserver.rs index 2ec28011..7400596f 100644 --- a/src/bin/cozoserver.rs +++ b/src/bin/cozoserver.rs @@ -2,20 +2,17 @@ * Copyright 2022, The Cozo Project Authors. Licensed under AGPL-3 or later. */ -use std::collections::BTreeMap; use std::fmt::Debug; use std::fs; use std::net::Ipv6Addr; use std::path::PathBuf; use std::str::FromStr; -use std::time::Instant; use clap::Parser; use env_logger::Env; use log::{error, info}; use rand::Rng; use rouille::{router, try_or_400, Request, Response}; -use serde_json::json; use cozo::Db; @@ -96,24 +93,30 @@ fn main() { #[derive(serde_derive::Serialize, serde_derive::Deserialize)] struct QueryPayload { script: String, - params: BTreeMap, + params: serde_json::Map, } let payload: QueryPayload = try_or_400!(rouille::input::json_input(request)); - let start = Instant::now(); - - match db.run_script(&payload.script, &payload.params) { - Ok(mut result) => { - if let Some(obj) = result.as_object_mut() { - obj.insert( - "time_taken".to_string(), - json!(start.elapsed().as_millis() as u64), - ); - } - Response::json(&result) - } - Err(e) => Response::text(format!("{:?}", e)).with_status_code(400), + let result = db.run_script_fold_err(&payload.script, &payload.params); + let response = Response::json(&result); + if let Some(serde_json::Value::Bool(true)) = result.get("ok") { + response + } else { + response.with_status_code(400) } + // { + // + // Ok(mut result) => { + // if let Some(obj) = result.as_object_mut() { + // obj.insert( + // "time_taken".to_string(), + // json!(start.elapsed().as_millis() as u64), + // ); + // } + // Response::json(&result) + // } + // _ => Response::json(&result).with_status_code(400) + // } }, (GET) (/) => { Response::html(HTML_CONTENT) @@ -163,7 +166,7 @@ const HTML_CONTENT: &str = r##" })) } } else { - console.error(await resp.text()) + console.error((await resp.json()).display) } } console.log( diff --git a/src/runtime/db.rs b/src/runtime/db.rs index 29c9ef21..abf558cf 100644 --- a/src/runtime/db.rs +++ b/src/runtime/db.rs @@ -7,13 +7,17 @@ use std::fmt::{Debug, Formatter}; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use std::{fs, thread}; use either::{Left, Right}; use itertools::Itertools; -use miette::{bail, ensure, miette, Diagnostic, IntoDiagnostic, Result, WrapErr}; -use serde_json::json; +use lazy_static::lazy_static; +use miette::{ + bail, ensure, miette, Diagnostic, GraphicalReportHandler, IntoDiagnostic, JSONReportHandler, + Result, WrapErr, +}; +use serde_json::{json, Map}; use smartstring::SmartString; use thiserror::Error; @@ -79,6 +83,11 @@ impl Debug for Db { #[diagnostic(code(db::init))] struct BadDbInit(#[help] String); +lazy_static! { + static ref TEXT_ERR_HANDLER: GraphicalReportHandler = miette::GraphicalReportHandler::new(); + static ref JSON_ERR_HANDLER: JSONReportHandler = miette::JSONReportHandler::new(); +} + impl Db { /// Creates a database object. pub fn new(path: impl AsRef) -> Result { @@ -175,24 +184,65 @@ impl Db { Ok(ret) } /// Run the CozoScript passed in. The `params` argument is a map of parameters. - pub fn run_script( - &self, - payload: &str, - params: &BTreeMap, - ) -> Result { - self.do_run_script(payload, params).map_err(|err| { - if err.source_code().is_some() { - err - } else { - err.with_source_code(payload.to_string()) + pub fn run_script(&self, payload: &str, params: &Map) -> Result { + let start = Instant::now(); + match self.do_run_script(payload, params) { + Ok(mut json) => { + let took = start.elapsed().as_secs_f64(); + let map = json.as_object_mut().unwrap(); + map.insert("ok".to_string(), json!(true)); + map.insert("took".to_string(), json!(took)); + Ok(json) } - }) + err => err, + } } - fn do_run_script( - &self, - payload: &str, - params: &BTreeMap, - ) -> Result { + /// Run the CozoScript passed in. The `params` argument is a map of parameters. + /// Fold any error into the return JSON itself. + pub fn run_script_fold_err(&self, payload: &str, params: &Map) -> JsonValue { + match self.run_script(payload, params) { + Ok(json) => json, + Err(mut err) => { + if err.source_code().is_none() { + err = err.with_source_code(payload.to_string()); + } + let mut text_err = String::new(); + let mut json_err = String::new(); + TEXT_ERR_HANDLER + .render_report(&mut text_err, err.as_ref()) + .expect("render text error failed"); + JSON_ERR_HANDLER + .render_report(&mut json_err, err.as_ref()) + .expect("render json error failed"); + let mut json: serde_json::Value = + serde_json::from_str(&json_err).expect("parse rendered json error failed"); + let map = json.as_object_mut().unwrap(); + map.insert("ok".to_string(), json!(false)); + map.insert("display".to_string(), json!(text_err)); + json + } + } + } + /// Run the CozoScript passed in. The `params` argument is a map of parameters formatted as JSON. + pub fn run_script_str(&self, payload: &str, params: &str) -> String { + let params_json = if params.is_empty() { + Map::default() + } else { + match serde_json::from_str::(params) { + Ok(serde_json::Value::Object(map)) => map, + Ok(_) => { + return json!({"ok": false, "message": "params argument is not valid JSON"}) + .to_string() + } + Err(_) => { + return json!({"ok": false, "message": "params argument is not a JSON map"}) + .to_string() + } + } + }; + self.run_script_fold_err(payload, ¶ms_json).to_string() + } + fn do_run_script(&self, payload: &str, params: &Map) -> Result { let param_pool = params .iter() .map(|(k, v)| (k.clone(), DataValue::from(v)))