query mutability

main
Ziyang Hu 1 year ago
parent a164a3919c
commit 6b8438e823

@ -20,7 +20,7 @@ use rustyline::history::DefaultHistory;
use rustyline::Changeset; use rustyline::Changeset;
use serde_json::{json, Value}; use serde_json::{json, Value};
use cozo::{DataValue, DbInstance, evaluate_expressions, NamedRows}; use cozo::{evaluate_expressions, DataValue, DbInstance, NamedRows, ScriptMutability};
struct Indented; struct Indented;
@ -83,13 +83,17 @@ pub(crate) fn repl_main(args: ReplArgs) -> Result<(), Box<dyn Error>> {
let db_copy = db.clone(); let db_copy = db.clone();
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
let running = db_copy let running = db_copy
.run_script("::running", Default::default()) .run_default("::running")
.expect("Cannot determine running queries"); .expect("Cannot determine running queries");
for row in running.rows { for row in running.rows {
let id = row.into_iter().next().unwrap(); let id = row.into_iter().next().unwrap();
eprintln!("Killing running query {id}"); eprintln!("Killing running query {id}");
db_copy db_copy
.run_script("::kill $id", BTreeMap::from([("id".to_string(), id)])) .run_script(
"::kill $id",
BTreeMap::from([("id".to_string(), id)]),
ScriptMutability::Mutable,
)
.expect("Cannot kill process"); .expect("Cannot kill process");
} }
}) })
@ -250,7 +254,7 @@ fn process_line(
bail!("Run requires path to a script"); bail!("Run requires path to a script");
} }
let content = fs::read_to_string(path).into_diagnostic()?; let content = fs::read_to_string(path).into_diagnostic()?;
let out = db.run_script(&content, params.clone())?; let out = db.run_script(&content, params.clone(), ScriptMutability::Mutable)?;
process_out(out)?; process_out(out)?;
} }
"restore" => { "restore" => {
@ -289,7 +293,7 @@ fn process_line(
op => bail!("Unknown op: {}", op), op => bail!("Unknown op: {}", op),
} }
} else { } else {
let out = db.run_script(line, params.clone())?; let out = db.run_script(line, params.clone(), ScriptMutability::Mutable)?;
process_out(out)?; process_out(out)?;
} }
Ok(()) Ok(())

@ -35,7 +35,8 @@ use tower_http::compression::CompressionLayer;
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
use cozo::{ use cozo::{
format_error_as_json, DataValue, DbInstance, MultiTransaction, NamedRows, SimpleFixedRule, format_error_as_json, DataValue, DbInstance, MultiTransaction, NamedRows, ScriptMutability,
SimpleFixedRule,
}; };
#[derive(Args, Debug)] #[derive(Args, Debug)]
@ -309,6 +310,7 @@ async fn finish_query(
struct QueryPayload { struct QueryPayload {
script: String, script: String,
params: BTreeMap<String, serde_json::Value>, params: BTreeMap<String, serde_json::Value>,
immutable: Option<bool>,
} }
async fn text_query( async fn text_query(
@ -320,7 +322,19 @@ async fn text_query(
.into_iter() .into_iter()
.map(|(k, v)| (k, DataValue::from(v))) .map(|(k, v)| (k, DataValue::from(v)))
.collect(); .collect();
let result = spawn_blocking(move || st.db.run_script_fold_err(&payload.script, params)).await; let immutable = payload.immutable.unwrap_or(false);
let result = spawn_blocking(move || {
st.db.run_script_fold_err(
&payload.script,
params,
if immutable {
ScriptMutability::Immutable
} else {
ScriptMutability::Mutable
},
)
})
.await;
match result { match result {
Ok(res) => wrap_json(res), Ok(res) => wrap_json(res),
Err(err) => internal_error(err), Err(err) => internal_error(err),

@ -6,28 +6,26 @@
* You can obtain one at https://mozilla.org/MPL/2.0/. * You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
use crate::{new_cozo_mem, DataValue}; use crate::{DataValue, DbInstance};
#[test] #[test]
fn expression_eval() { fn expression_eval() {
let db = new_cozo_mem().unwrap(); let db = DbInstance::default();
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a] := a = if(2 + 3 > 1 * 99999, 190291021 + 14341234212 / 2121) ?[a] := a = if(2 + 3 > 1 * 99999, 190291021 + 14341234212 / 2121)
"#, "#,
Default::default(),
) )
.unwrap(); .unwrap();
assert_eq!(res.rows[0][0], DataValue::Null); assert_eq!(res.rows[0][0], DataValue::Null);
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a] := a = if(2 + 3 > 1, true, false) ?[a] := a = if(2 + 3 > 1, true, false)
"#, "#,
Default::default(),
) )
.unwrap(); .unwrap();
assert_eq!(res.rows[0][0].get_bool().unwrap(), true); assert_eq!(res.rows[0][0].get_bool().unwrap(), true);

@ -13,7 +13,7 @@ use serde_json::json;
use crate::data::functions::*; use crate::data::functions::*;
use crate::data::value::{DataValue, RegexWrapper}; use crate::data::value::{DataValue, RegexWrapper};
use crate::new_cozo_mem; use crate::DbInstance;
#[test] #[test]
fn test_add() { fn test_add() {
@ -1456,39 +1456,33 @@ fn test_to_bool() {
#[test] #[test]
fn test_coalesce() { fn test_coalesce() {
let db = new_cozo_mem().unwrap(); let db = DbInstance::default();
let res = db let res = db.run_default("?[a] := a = null ~ 1 ~ 2").unwrap().rows;
.run_script("?[a] := a = null ~ 1 ~ 2", Default::default())
.unwrap()
.rows;
assert_eq!(res[0][0], DataValue::from(1)); assert_eq!(res[0][0], DataValue::from(1));
let res = db let res = db
.run_script("?[a] := a = null ~ null ~ null", Default::default()) .run_default("?[a] := a = null ~ null ~ null")
.unwrap() .unwrap()
.rows; .rows;
assert_eq!(res[0][0], DataValue::Null); assert_eq!(res[0][0], DataValue::Null);
let res = db let res = db.run_default("?[a] := a = 2 ~ null ~ 1").unwrap().rows;
.run_script("?[a] := a = 2 ~ null ~ 1", Default::default())
.unwrap()
.rows;
assert_eq!(res[0][0], DataValue::from(2)); assert_eq!(res[0][0], DataValue::from(2));
} }
#[test] #[test]
fn test_range() { fn test_range() {
let db = new_cozo_mem().unwrap(); let db = DbInstance::default();
let res = db let res = db
.run_script("?[a] := a = int_range(1, 5)", Default::default()) .run_default("?[a] := a = int_range(1, 5)")
.unwrap() .unwrap()
.into_json(); .into_json();
assert_eq!(res["rows"][0][0], json!([1, 2, 3, 4])); assert_eq!(res["rows"][0][0], json!([1, 2, 3, 4]));
let res = db let res = db
.run_script("?[a] := a = int_range(5)", Default::default()) .run_default("?[a] := a = int_range(5)")
.unwrap() .unwrap()
.into_json(); .into_json();
assert_eq!(res["rows"][0][0], json!([0, 1, 2, 3, 4])); assert_eq!(res["rows"][0][0], json!([0, 1, 2, 3, 4]));
let res = db let res = db
.run_script("?[a] := a = int_range(15, 3, -2)", Default::default()) .run_default("?[a] := a = int_range(15, 3, -2)")
.unwrap() .unwrap()
.into_json(); .into_json();
assert_eq!(res["rows"][0][0], json!([15, 13, 11, 9, 7, 5])); assert_eq!(res["rows"][0][0], json!([15, 13, 11, 9, 7, 5]));

@ -19,108 +19,97 @@ fn test_validity() {
let _ = std::fs::remove_dir_all(path); let _ = std::fs::remove_dir_all(path);
let db_kind = env::var("COZO_TEST_DB_ENGINE").unwrap_or("mem".to_string()); let db_kind = env::var("COZO_TEST_DB_ENGINE").unwrap_or("mem".to_string());
println!("Using {} engine", db_kind); println!("Using {} engine", db_kind);
let db = DbInstance::new(&db_kind, path, Default::default()).unwrap(); let db = DbInstance::default();
db.run_script(":create vld {a, v: Validity => d}", Default::default()) db.run_default(":create vld {a, v: Validity => d}").unwrap();
.unwrap();
assert!(db assert!(db
.run_script( .run_default(
r#" r#"
?[a, v, d] <- [[1, [9223372036854775807, true], null]] ?[a, v, d] <- [[1, [9223372036854775807, true], null]]
:put vld {a, v => d} :put vld {a, v => d}
"#, "#,
Default::default(),
) )
.is_err()); .is_err());
assert!(db assert!(db
.run_script( .run_default(
r#" r#"
?[a, v, d] <- [[1, [-9223372036854775808, true], null]] ?[a, v, d] <- [[1, [-9223372036854775808, true], null]]
:put vld {a, v => d} :put vld {a, v => d}
"#, "#,
Default::default(),
) )
.is_err()); .is_err());
db.run_script( db.run_default(
r#" r#"
?[a, v, d] <- [[1, [0, true], 0]] ?[a, v, d] <- [[1, [0, true], 0]]
:put vld {a, v => d} :put vld {a, v => d}
"#, "#,
Default::default(),
) )
.unwrap(); .unwrap();
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d @ "NOW"} ?[a, v, d] := *vld{a, v, d @ "NOW"}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d} ?[a, v, d] := *vld{a, v, d}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
db.run_script( db.run_default(
r#" r#"
?[a, v, d] <- [[1, [1, false], 1]] ?[a, v, d] <- [[1, [1, false], 1]]
:put vld {a, v => d} :put vld {a, v => d}
"#, "#,
Default::default(),
) )
.unwrap(); .unwrap();
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d @ "NOW"} ?[a, v, d] := *vld{a, v, d @ "NOW"}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
assert_eq!(res.len(), 0); assert_eq!(res.len(), 0);
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d} ?[a, v, d] := *vld{a, v, d}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
db.run_script( db.run_default(
r#" r#"
?[a, v, d] <- [[1, "ASSERT", 2]] ?[a, v, d] <- [[1, "ASSERT", 2]]
:put vld {a, v => d} :put vld {a, v => d}
"#, "#,
Default::default(),
) )
.unwrap(); .unwrap();
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d @ "NOW"} ?[a, v, d] := *vld{a, v, d @ "NOW"}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
@ -128,72 +117,65 @@ fn test_validity() {
assert_eq!(res[0][2].get_int().unwrap(), 2); assert_eq!(res[0][2].get_int().unwrap(), 2);
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d} ?[a, v, d] := *vld{a, v, d}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
assert_eq!(res.len(), 3); assert_eq!(res.len(), 3);
db.run_script( db.run_default(
r#" r#"
?[a, v, d] <- [[1, "RETRACT", 3]] ?[a, v, d] <- [[1, "RETRACT", 3]]
:put vld {a, v => d} :put vld {a, v => d}
"#, "#,
Default::default(),
) )
.unwrap(); .unwrap();
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d @ "NOW"} ?[a, v, d] := *vld{a, v, d @ "NOW"}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
assert_eq!(res.len(), 0); assert_eq!(res.len(), 0);
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d} ?[a, v, d] := *vld{a, v, d}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
assert_eq!(res.len(), 4); assert_eq!(res.len(), 4);
db.run_script( db.run_default(
r#" r#"
?[a, v, d] <- [[1, [9223372036854775806, true], null]] ?[a, v, d] <- [[1, [9223372036854775806, true], null]]
:put vld {a, v => d} :put vld {a, v => d}
"#, "#,
Default::default(),
) )
.unwrap(); .unwrap();
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d @ "NOW"} ?[a, v, d] := *vld{a, v, d @ "NOW"}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
assert_eq!(res.len(), 0); assert_eq!(res.len(), 0);
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d @ "END"} ?[a, v, d] := *vld{a, v, d @ "END"}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
@ -201,11 +183,10 @@ fn test_validity() {
assert_eq!(res[0][2], DataValue::Null); assert_eq!(res[0][2], DataValue::Null);
let res = db let res = db
.run_script( .run_default(
r#" r#"
?[a, v, d] := *vld{a, v, d} ?[a, v, d] := *vld{a, v, d}
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;

@ -125,13 +125,13 @@ impl FixedRule for ShortestPathBFS {
mod tests { mod tests {
use crate::data::value::DataValue; use crate::data::value::DataValue;
use crate::new_cozo_mem; use crate::DbInstance;
#[test] #[test]
fn test_bfs_path() { fn test_bfs_path() {
let db = new_cozo_mem().unwrap(); let db = DbInstance::default();
let res = db let res = db
.run_script( .run_default(
r#" r#"
love[loving, loved] <- [['alice', 'eve'], love[loving, loved] <- [['alice', 'eve'],
['bob', 'alice'], ['bob', 'alice'],
@ -145,14 +145,13 @@ mod tests {
end[] <- [['bob']] end[] <- [['bob']]
?[fr, to, path] <~ ShortestPathBFS(love[], start[], end[]) ?[fr, to, path] <~ ShortestPathBFS(love[], start[], end[])
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;
println!("{:?}", res); println!("{:?}", res);
assert_eq!(res[0][2].get_slice().unwrap().len(), 3); assert_eq!(res[0][2].get_slice().unwrap().len(), 3);
let res = db let res = db
.run_script( .run_default(
r#" r#"
love[loving, loved] <- [['alice', 'eve'], love[loving, loved] <- [['alice', 'eve'],
['bob', 'alice'], ['bob', 'alice'],
@ -166,7 +165,6 @@ mod tests {
end[] <- [['george']] end[] <- [['george']]
?[fr, to, path] <~ ShortestPathBFS(love[], start[], end[]) ?[fr, to, path] <~ ShortestPathBFS(love[], start[], end[])
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;

@ -19,7 +19,7 @@
//! //!
//! let db = DbInstance::new("mem", "", Default::default()).unwrap(); //! let db = DbInstance::new("mem", "", Default::default()).unwrap();
//! let script = "?[a] := a in [1, 2, 3]"; //! let script = "?[a] := a in [1, 2, 3]";
//! let result = db.run_script(script, Default::default()).unwrap(); //! let result = db.run_script(script, Default::default(), ScriptMutability::Immutable).unwrap();
//! println!("{:?}", result); //! println!("{:?}", result);
//! ``` //! ```
//! We created an in-memory database above. There are other persistent options: //! We created an in-memory database above. There are other persistent options:
@ -68,23 +68,24 @@ pub use storage::{Storage, StoreTx};
pub use crate::data::expr::Expr; pub use crate::data::expr::Expr;
use crate::data::json::JsonValue; use crate::data::json::JsonValue;
pub use crate::data::symb::Symbol; pub use crate::data::symb::Symbol;
pub use crate::data::value::{Vector, JsonData}; pub use crate::data::value::{JsonData, Vector};
pub use crate::fixed_rule::SimpleFixedRule; pub use crate::fixed_rule::SimpleFixedRule;
pub use crate::parse::SourceSpan; pub use crate::parse::SourceSpan;
pub use crate::runtime::callback::CallbackOp; pub use crate::runtime::callback::CallbackOp;
pub use crate::runtime::db::Poison;
pub use crate::runtime::db::TransactionPayload;
pub use crate::runtime::db::evaluate_expressions; pub use crate::runtime::db::evaluate_expressions;
pub use crate::runtime::db::get_variables; pub use crate::runtime::db::get_variables;
pub use crate::runtime::db::Poison;
pub use crate::runtime::db::ScriptMutability;
pub use crate::runtime::db::TransactionPayload;
pub(crate) mod data; pub(crate) mod data;
pub(crate) mod fixed_rule; pub(crate) mod fixed_rule;
pub(crate) mod fts;
pub(crate) mod parse; pub(crate) mod parse;
pub(crate) mod query; pub(crate) mod query;
pub(crate) mod runtime; pub(crate) mod runtime;
pub(crate) mod storage; pub(crate) mod storage;
pub(crate) mod utils; pub(crate) mod utils;
pub(crate) mod fts;
/// A dispatcher for concrete storage implementations, wrapping [Db]. This is done so that /// A dispatcher for concrete storage implementations, wrapping [Db]. This is done so that
/// client code does not have to deal with generic code constantly. You may prefer to use /// client code does not have to deal with generic code constantly. You may prefer to use
@ -113,6 +114,12 @@ pub enum DbInstance {
TiKv(Db<TiKvStorage>), TiKv(Db<TiKvStorage>),
} }
impl Default for DbInstance {
fn default() -> Self {
Self::new("mem", "", Default::default()).unwrap()
}
}
impl DbInstance { impl DbInstance {
/// Create a DbInstance, which is a dispatcher for various concrete implementations. /// Create a DbInstance, which is a dispatcher for various concrete implementations.
/// The valid engines are: /// The valid engines are:
@ -168,19 +175,24 @@ impl DbInstance {
&self, &self,
payload: &str, payload: &str,
params: BTreeMap<String, DataValue>, params: BTreeMap<String, DataValue>,
mutability: ScriptMutability,
) -> Result<NamedRows> { ) -> Result<NamedRows> {
match self { match self {
DbInstance::Mem(db) => db.run_script(payload, params), DbInstance::Mem(db) => db.run_script(payload, params, mutability),
#[cfg(feature = "storage-sqlite")] #[cfg(feature = "storage-sqlite")]
DbInstance::Sqlite(db) => db.run_script(payload, params), DbInstance::Sqlite(db) => db.run_script(payload, params, mutability),
#[cfg(feature = "storage-rocksdb")] #[cfg(feature = "storage-rocksdb")]
DbInstance::RocksDb(db) => db.run_script(payload, params), DbInstance::RocksDb(db) => db.run_script(payload, params, mutability),
#[cfg(feature = "storage-sled")] #[cfg(feature = "storage-sled")]
DbInstance::Sled(db) => db.run_script(payload, params), DbInstance::Sled(db) => db.run_script(payload, params, mutability),
#[cfg(feature = "storage-tikv")] #[cfg(feature = "storage-tikv")]
DbInstance::TiKv(db) => db.run_script(payload, params), DbInstance::TiKv(db) => db.run_script(payload, params, mutability),
} }
} }
/// `run_script` with mutable script and no parameters
pub fn run_default(&self, payload: &str) -> Result<NamedRows> {
return self.run_script(payload, BTreeMap::new(), ScriptMutability::Mutable);
}
/// Run the CozoScript passed in. The `params` argument is a map of parameters. /// Run the CozoScript passed in. The `params` argument is a map of parameters.
/// Fold any error into the return JSON itself. /// Fold any error into the return JSON itself.
/// See [crate::Db::run_script]. /// See [crate::Db::run_script].
@ -188,11 +200,12 @@ impl DbInstance {
&self, &self,
payload: &str, payload: &str,
params: BTreeMap<String, DataValue>, params: BTreeMap<String, DataValue>,
mutability: ScriptMutability,
) -> JsonValue { ) -> JsonValue {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
let start = Instant::now(); let start = Instant::now();
match self.run_script(payload, params) { match self.run_script(payload, params, mutability) {
Ok(named_rows) => { Ok(named_rows) => {
let mut j_val = named_rows.into_json(); let mut j_val = named_rows.into_json();
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -209,7 +222,7 @@ impl DbInstance {
} }
/// Run the CozoScript passed in. The `params` argument is a map of parameters formatted as JSON. /// Run the CozoScript passed in. The `params` argument is a map of parameters formatted as JSON.
/// See [crate::Db::run_script]. /// See [crate::Db::run_script].
pub fn run_script_str(&self, payload: &str, params: &str) -> String { pub fn run_script_str(&self, payload: &str, params: &str, immutable: bool) -> String {
let params_json = if params.is_empty() { let params_json = if params.is_empty() {
BTreeMap::default() BTreeMap::default()
} else { } else {
@ -224,7 +237,16 @@ impl DbInstance {
} }
} }
}; };
self.run_script_fold_err(payload, params_json).to_string() self.run_script_fold_err(
payload,
params_json,
if immutable {
ScriptMutability::Immutable
} else {
ScriptMutability::Mutable
},
)
.to_string()
} }
/// Dispatcher method. See [crate::Db::export_relations]. /// Dispatcher method. See [crate::Db::export_relations].
pub fn export_relations<I, T>(&self, relations: I) -> Result<BTreeMap<String, NamedRows>> pub fn export_relations<I, T>(&self, relations: I) -> Result<BTreeMap<String, NamedRows>>

@ -641,7 +641,7 @@ mod tests {
#[test] #[test]
fn strange_case() { fn strange_case() {
let db = DbInstance::new("mem", "", "").unwrap(); let db = DbInstance::default();
let query = r#" let query = r#"
x[A] := A = 1 x[A] := A = 1
@ -653,10 +653,7 @@ mod tests {
:disable_magic_rewrite true :disable_magic_rewrite true
"#; "#;
let res = db let res = db.run_default(query).unwrap().into_json();
.run_script(query, Default::default())
.unwrap()
.into_json();
assert_eq!(res["rows"], json!([[0], [1]])); assert_eq!(res["rows"], json!([[0], [1]]));
} }
} }

@ -1036,7 +1036,7 @@ impl FtsSearchRA {
coll.write_str(" OR ").unwrap(); coll.write_str(" OR ").unwrap();
} }
coll.write_str(&s).unwrap(); coll.write_str(&s).unwrap();
}, }
d => bail!("Expected string for FTS search, got {:?}", d), d => bail!("Expected string for FTS search, got {:?}", d),
} }
} }
@ -2404,18 +2404,17 @@ impl<'a> Iterator for CachedMaterializedIterator<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::data::value::DataValue; use crate::data::value::DataValue;
use crate::new_cozo_mem; use crate::DbInstance;
#[test] #[test]
fn test_mat_join() { fn test_mat_join() {
let db = new_cozo_mem().unwrap(); let db = DbInstance::default();
let res = db let res = db
.run_script( .run_default(
r#" r#"
data[a, b] <- [[1, 2], [1, 3], [2, 3]] data[a, b] <- [[1, 2], [1, 3], [2, 3]]
?[x] := a = 3, data[x, a] ?[x] := a = 3, data[x, a]
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;

@ -310,13 +310,13 @@ impl NormalFormProgram {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::new_cozo_mem; use crate::DbInstance;
#[test] #[test]
fn test_dependencies() { fn test_dependencies() {
let db = new_cozo_mem().unwrap(); let db = DbInstance::default();
let _res = db let _res = db
.run_script( .run_default(
r#" r#"
x[a] <- [[1], [2]] x[a] <- [[1], [2]]
w[a] := a in [2] w[a] := a in [2]
@ -328,7 +328,6 @@ mod tests {
?[a] := z[a] ?[a] := z[a]
?[a] := w[a] ?[a] := w[a]
"#, "#,
Default::default(),
) )
.unwrap() .unwrap()
.rows; .rows;

@ -83,6 +83,15 @@ pub struct DbManifest {
pub storage_version: u64, pub storage_version: u64,
} }
/// Whether a script is mutable or immutable.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum ScriptMutability {
/// The script is mutable.
Mutable,
/// The script is immutable.
Immutable,
}
/// The database object of Cozo. /// The database object of Cozo.
#[derive(Clone)] #[derive(Clone)]
pub struct Db<S> { pub struct Db<S> {
@ -369,10 +378,26 @@ impl<'s, S: Storage<'s>> Db<S> {
&'s self, &'s self,
payload: &str, payload: &str,
params: BTreeMap<String, DataValue>, params: BTreeMap<String, DataValue>,
mutability: ScriptMutability,
) -> Result<NamedRows> {
let cur_vld = current_validity();
self.do_run_script(
payload,
&params,
cur_vld,
mutability == ScriptMutability::Immutable,
)
}
/// Run the CozoScript passed in. The `params` argument is a map of parameters.
pub fn run_script_read_only(
&'s self,
payload: &str,
params: BTreeMap<String, DataValue>,
) -> Result<NamedRows> { ) -> Result<NamedRows> {
let cur_vld = current_validity(); let cur_vld = current_validity();
self.do_run_script(payload, &params, cur_vld) self.do_run_script(payload, &params, cur_vld, true)
} }
/// Export relations to JSON data. /// Export relations to JSON data.
/// ///
/// `relations` contains names of the stored relations to export. /// `relations` contains names of the stored relations to export.
@ -847,6 +872,7 @@ impl<'s, S: Storage<'s>> Db<S> {
payload: &str, payload: &str,
param_pool: &BTreeMap<String, DataValue>, param_pool: &BTreeMap<String, DataValue>,
cur_vld: ValidityTs, cur_vld: ValidityTs,
read_only: bool,
) -> Result<NamedRows> { ) -> Result<NamedRows> {
match parse_script( match parse_script(
payload, payload,
@ -854,16 +880,24 @@ impl<'s, S: Storage<'s>> Db<S> {
&self.fixed_rules.read().unwrap(), &self.fixed_rules.read().unwrap(),
cur_vld, cur_vld,
)? { )? {
CozoScript::Single(p) => self.execute_single(cur_vld, p), CozoScript::Single(p) => self.execute_single(cur_vld, p, read_only),
CozoScript::Imperative(ps) => self.execute_imperative(cur_vld, &ps), CozoScript::Imperative(ps) => self.execute_imperative(cur_vld, &ps, read_only),
CozoScript::Sys(op) => self.run_sys_op(op), CozoScript::Sys(op) => self.run_sys_op(op, read_only),
} }
} }
fn execute_single(&'s self, cur_vld: ValidityTs, p: InputProgram) -> Result<NamedRows, Report> { fn execute_single(
&'s self,
cur_vld: ValidityTs,
p: InputProgram,
read_only: bool,
) -> Result<NamedRows, Report> {
let mut callback_collector = BTreeMap::new(); let mut callback_collector = BTreeMap::new();
let write_lock_names = p.needs_write_lock(); let write_lock_names = p.needs_write_lock();
let is_write = write_lock_names.is_some(); let is_write = write_lock_names.is_some();
if read_only && is_write {
bail!("write lock required for read-only query");
}
let write_lock = self.obtain_relation_locks(write_lock_names.iter()); let write_lock = self.obtain_relation_locks(write_lock_names.iter());
let _write_lock_guards = if is_write { let _write_lock_guards = if is_write {
Some(write_lock[0].read().unwrap()) Some(write_lock[0].read().unwrap())
@ -1130,7 +1164,7 @@ impl<'s, S: Storage<'s>> Db<S> {
Ok(NamedRows::new(headers, rows)) Ok(NamedRows::new(headers, rows))
} }
fn run_sys_op(&'s self, op: SysOp) -> Result<NamedRows> { fn run_sys_op(&'s self, op: SysOp, read_only: bool) -> Result<NamedRows> {
match op { match op {
SysOp::Explain(prog) => { SysOp::Explain(prog) => {
let mut tx = self.transact()?; let mut tx = self.transact()?;
@ -1142,6 +1176,9 @@ impl<'s, S: Storage<'s>> Db<S> {
self.explain_compiled(&compiled) self.explain_compiled(&compiled)
} }
SysOp::Compact => { SysOp::Compact => {
if read_only {
bail!("Cannot compact in read-only mode");
}
self.compact_relation()?; self.compact_relation()?;
Ok(NamedRows::new( Ok(NamedRows::new(
vec![STATUS_STR.to_string()], vec![STATUS_STR.to_string()],
@ -1160,6 +1197,9 @@ impl<'s, S: Storage<'s>> Db<S> {
)) ))
} }
SysOp::RemoveRelation(rel_names) => { SysOp::RemoveRelation(rel_names) => {
if read_only {
bail!("Cannot remove relations in read-only mode");
}
let rel_name_strs = rel_names.iter().map(|n| &n.name); let rel_name_strs = rel_names.iter().map(|n| &n.name);
let locks = self.obtain_relation_locks(rel_name_strs); let locks = self.obtain_relation_locks(rel_name_strs);
let _guards = locks.iter().map(|l| l.read().unwrap()).collect_vec(); let _guards = locks.iter().map(|l| l.read().unwrap()).collect_vec();
@ -1189,6 +1229,9 @@ impl<'s, S: Storage<'s>> Db<S> {
)) ))
} }
SysOp::CreateIndex(rel_name, idx_name, cols) => { SysOp::CreateIndex(rel_name, idx_name, cols) => {
if read_only {
bail!("Cannot create index in read-only mode");
}
let lock = self let lock = self
.obtain_relation_locks(iter::once(&rel_name.name)) .obtain_relation_locks(iter::once(&rel_name.name))
.pop() .pop()
@ -1203,6 +1246,9 @@ impl<'s, S: Storage<'s>> Db<S> {
)) ))
} }
SysOp::CreateVectorIndex(config) => { SysOp::CreateVectorIndex(config) => {
if read_only {
bail!("Cannot create vector index in read-only mode");
}
let lock = self let lock = self
.obtain_relation_locks(iter::once(&config.base_relation)) .obtain_relation_locks(iter::once(&config.base_relation))
.pop() .pop()
@ -1217,6 +1263,9 @@ impl<'s, S: Storage<'s>> Db<S> {
)) ))
} }
SysOp::CreateFtsIndex(config) => { SysOp::CreateFtsIndex(config) => {
if read_only {
bail!("Cannot create fts index in read-only mode");
}
let lock = self let lock = self
.obtain_relation_locks(iter::once(&config.base_relation)) .obtain_relation_locks(iter::once(&config.base_relation))
.pop() .pop()
@ -1231,6 +1280,9 @@ impl<'s, S: Storage<'s>> Db<S> {
)) ))
} }
SysOp::CreateMinHashLshIndex(config) => { SysOp::CreateMinHashLshIndex(config) => {
if read_only {
bail!("Cannot create minhash lsh index in read-only mode");
}
let lock = self let lock = self
.obtain_relation_locks(iter::once(&config.base_relation)) .obtain_relation_locks(iter::once(&config.base_relation))
.pop() .pop()
@ -1245,6 +1297,9 @@ impl<'s, S: Storage<'s>> Db<S> {
)) ))
} }
SysOp::RemoveIndex(rel_name, idx_name) => { SysOp::RemoveIndex(rel_name, idx_name) => {
if read_only {
bail!("Cannot remove index in read-only mode");
}
let lock = self let lock = self
.obtain_relation_locks(iter::once(&rel_name.name)) .obtain_relation_locks(iter::once(&rel_name.name))
.pop() .pop()
@ -1264,6 +1319,9 @@ impl<'s, S: Storage<'s>> Db<S> {
SysOp::ListColumns(rs) => self.list_columns(&rs), SysOp::ListColumns(rs) => self.list_columns(&rs),
SysOp::ListIndices(rs) => self.list_indices(&rs), SysOp::ListIndices(rs) => self.list_indices(&rs),
SysOp::RenameRelation(rename_pairs) => { SysOp::RenameRelation(rename_pairs) => {
if read_only {
bail!("Cannot rename relations in read-only mode");
}
let rel_names = rename_pairs.iter().flat_map(|(f, t)| [&f.name, &t.name]); let rel_names = rename_pairs.iter().flat_map(|(f, t)| [&f.name, &t.name]);
let locks = self.obtain_relation_locks(rel_names); let locks = self.obtain_relation_locks(rel_names);
let _guards = locks.iter().map(|l| l.read().unwrap()).collect_vec(); let _guards = locks.iter().map(|l| l.read().unwrap()).collect_vec();
@ -1318,6 +1376,9 @@ impl<'s, S: Storage<'s>> Db<S> {
)) ))
} }
SysOp::SetTriggers(name, puts, rms, replaces) => { SysOp::SetTriggers(name, puts, rms, replaces) => {
if read_only {
bail!("Cannot set triggers in read-only mode");
}
let mut tx = self.transact_write()?; let mut tx = self.transact_write()?;
tx.set_relation_triggers(name, puts, rms, replaces)?; tx.set_relation_triggers(name, puts, rms, replaces)?;
tx.commit_tx()?; tx.commit_tx()?;
@ -1327,6 +1388,9 @@ impl<'s, S: Storage<'s>> Db<S> {
)) ))
} }
SysOp::SetAccessLevel(names, level) => { SysOp::SetAccessLevel(names, level) => {
if read_only {
bail!("Cannot set access level in read-only mode");
}
let mut tx = self.transact_write()?; let mut tx = self.transact_write()?;
for name in names { for name in names {
tx.set_access_level(name, level)?; tx.set_access_level(name, level)?;

@ -247,9 +247,13 @@ impl<'s, S: Storage<'s>> Db<S> {
&'s self, &'s self,
cur_vld: ValidityTs, cur_vld: ValidityTs,
ps: &ImperativeProgram, ps: &ImperativeProgram,
read_only: bool,
) -> Result<NamedRows, Report> { ) -> Result<NamedRows, Report> {
let mut callback_collector = BTreeMap::new(); let mut callback_collector = BTreeMap::new();
let mut write_lock_names = BTreeSet::new(); let mut write_lock_names = BTreeSet::new();
if read_only && !write_lock_names.is_empty() {
bail!("Read-only imperative program attempted to acquire write locks");
}
for p in ps { for p in ps {
p.needs_write_locks(&mut write_lock_names); p.needs_write_locks(&mut write_lock_names);
} }
@ -338,7 +342,10 @@ impl SessionTx<'_> {
let k = k.replace('(', "_").replace(')', ""); let k = k.replace('(', "_").replace(')', "");
let k = Symbol::new(k.clone(), Default::default()); let k = Symbol::new(k.clone(), Default::default());
if key_bindings.contains(&k) { if key_bindings.contains(&k) {
bail!("Duplicate variable name {}, please use distinct variables in `as` construct.", k); bail!(
"Duplicate variable name {}, please use distinct variables in `as` construct.",
k
);
} }
key_bindings.push(k); key_bindings.push(k);
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -59,7 +59,10 @@ bool cozo_close_db(int32_t id);
* Returns a UTF-8-encoded C-string that **must** be freed with `cozo_free_str`. * Returns a UTF-8-encoded C-string that **must** be freed with `cozo_free_str`.
* The string contains the JSON return value of the query. * 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); char *cozo_run_query(int32_t db_id,
const char *script_raw,
const char *params_raw,
bool immutable_query);
/** /**
* Import data into relations * Import data into relations

@ -107,6 +107,7 @@ pub unsafe extern "C" fn cozo_run_query(
db_id: i32, db_id: i32,
script_raw: *const c_char, script_raw: *const c_char,
params_raw: *const c_char, params_raw: *const c_char,
immutable_query: bool,
) -> *mut c_char { ) -> *mut c_char {
let script = match CStr::from_ptr(script_raw).to_str() { let script = match CStr::from_ptr(script_raw).to_str() {
Ok(p) => p, Ok(p) => p,
@ -141,7 +142,7 @@ pub unsafe extern "C" fn cozo_run_query(
} }
}; };
let result = db.run_script_str(script, params_str); let result = db.run_script_str(script, params_str, immutable_query);
CString::new(result).unwrap().into_raw() CString::new(result).unwrap().into_raw()
} }

@ -88,7 +88,7 @@ pub extern "system" fn Java_org_cozodb_CozoJavaBridge_runQuery(
match get_db(id) { match get_db(id) {
None => env.new_string(DB_NOT_FOUND).unwrap().into_raw(), None => env.new_string(DB_NOT_FOUND).unwrap().into_raw(),
Some(db) => { Some(db) => {
let res = db.run_script_str(&script, &params_str); let res = db.run_script_str(&script, &params_str, false);
env.new_string(res).unwrap().into_raw() env.new_string(res).unwrap().into_raw()
} }
} }

@ -29,6 +29,7 @@ class CozoTx {
}) })
}) })
} }
abort() { abort() {
return native.abort_tx(this.tx_id) return native.abort_tx(this.tx_id)
} }
@ -51,7 +52,7 @@ class CozoDb {
return new CozoTx(native.multi_transact(this.db_id, !!write)) return new CozoTx(native.multi_transact(this.db_id, !!write))
} }
run(script, params) { run(script, params, immutable) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
params = params || {}; params = params || {};
native.query_db(this.db_id, script, params, (err, result) => { native.query_db(this.db_id, script, params, (err, result) => {
@ -60,7 +61,7 @@ class CozoDb {
} else { } else {
resolve(result) resolve(result)
} }
}) }, !!immutable)
}) })
} }

@ -406,11 +406,20 @@ fn query_db(mut cx: FunctionContext) -> JsResult<JsUndefined> {
js2params(&mut cx, params_js, &mut params)?; js2params(&mut cx, params_js, &mut params)?;
let callback = cx.argument::<JsFunction>(3)?.root(&mut cx); let callback = cx.argument::<JsFunction>(3)?.root(&mut cx);
let immutable = cx.argument::<JsBoolean>(4)?.value(&mut cx);
let channel = cx.channel(); let channel = cx.channel();
thread::spawn(move || { thread::spawn(move || {
let result = db.run_script(&query, params); let result = db.run_script(
&query,
params,
if immutable {
ScriptMutability::Immutable
} else {
ScriptMutability::Mutable
},
);
channel.send(move |mut cx| { channel.send(move |mut cx| {
let callback = callback.into_inner(&mut cx); let callback = callback.into_inner(&mut cx);
let this = cx.undefined(); let this = cx.undefined();

@ -213,10 +213,26 @@ impl CozoDbPy {
Err(err) => Err(PyException::new_err(format!("{err:?}"))), Err(err) => Err(PyException::new_err(format!("{err:?}"))),
} }
} }
pub fn run_script(&self, py: Python<'_>, query: &str, params: &PyDict) -> PyResult<PyObject> { pub fn run_script(
&self,
py: Python<'_>,
query: &str,
params: &PyDict,
immutable: bool,
) -> PyResult<PyObject> {
if let Some(db) = &self.db { if let Some(db) = &self.db {
let params = convert_params(params)?; let params = convert_params(params)?;
match py.allow_threads(|| db.run_script(query, params)) { match py.allow_threads(|| {
db.run_script(
query,
params,
if immutable {
ScriptMutability::Immutable
} else {
ScriptMutability::Mutable
},
)
}) {
Ok(rows) => Ok(named_rows_to_py(rows, py)), Ok(rows) => Ok(named_rows_to_py(rows, py)),
Err(err) => { Err(err) => {
let reports = format_error_as_json(err, Some(query)).to_string(); let reports = format_error_as_json(err, Some(query)).to_string();

@ -15,7 +15,7 @@ mod ffi {
fn new_cozo_db(engine: &str, path: &str, options: &str) -> Option<DbInstance>; fn new_cozo_db(engine: &str, path: &str, options: &str) -> Option<DbInstance>;
fn run_script_str(&self, payload: &str, params: &str) -> String; fn run_script_str(&self, payload: &str, params: &str, immutable: bool) -> String;
fn export_relations_str(&self, data: &str) -> String; fn export_relations_str(&self, data: &str) -> String;
fn import_relations_str(&self, data: &str) -> String; fn import_relations_str(&self, data: &str) -> String;
fn backup_db_str(&self, out_file: &str) -> String; fn backup_db_str(&self, out_file: &str) -> String;

@ -36,8 +36,8 @@ impl CozoDb {
let db = DbInstance::new("mem", "", "").unwrap(); let db = DbInstance::new("mem", "", "").unwrap();
Self { db } Self { db }
} }
pub fn run(&self, script: &str, params: &str) -> String { pub fn run(&self, script: &str, params: &str, immutable: bool) -> String {
self.db.run_script_str(script, params) self.db.run_script_str(script, params, immutable)
} }
pub fn export_relations(&self, data: &str) -> String { pub fn export_relations(&self, data: &str) -> String {
self.db.export_relations_str(data) self.db.export_relations_str(data)

Loading…
Cancel
Save