diff --git a/.gitignore b/.gitignore index 07102c19..8abb0fce 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,28 @@ pom.xml.asc /fixtures/*.db-shm /fixtures/*.db-wal /query-parser/out/ +## Build generated +/sdks/swift/Mentat/build/ +DerivedData +build.xcarchive + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +/sdks/swift/Mentat/*.xcodeproj/project.xcworkspace/xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa \ No newline at end of file diff --git a/android_build_all.sh b/android_build_all.sh new file mode 100755 index 00000000..61430482 --- /dev/null +++ b/android_build_all.sh @@ -0,0 +1,4 @@ +# This will eventually become a complete build script, not just for Android +cargo build -p mentat_ffi --target i686-linux-android --release +cargo build -p mentat_ffi --target armv7-linux-androideabi --release +cargo build -p mentat_ffi --target aarch64-linux-android --release diff --git a/build/version.rs b/build/version.rs index 66b38976..f67ff18c 100644 --- a/build/version.rs +++ b/build/version.rs @@ -19,7 +19,7 @@ use rustc_version::{ /// MIN_VERSION should be changed when there's a new minimum version of rustc required /// to build the project. -static MIN_VERSION: &'static str = "1.24.0"; +static MIN_VERSION: &'static str = "1.25.0"; fn main() { let ver = version().unwrap(); diff --git a/core/src/types.rs b/core/src/types.rs index ad426a12..ca3b86d4 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -755,6 +755,27 @@ impl Binding { _ => None, } } + + pub fn into_c_string(self) -> Option<*mut c_char> { + match self { + Binding::Scalar(v) => v.into_c_string(), + _ => None, + } + } + + pub fn into_kw_c_string(self) -> Option<*mut c_char> { + match self { + Binding::Scalar(v) => v.into_kw_c_string(), + _ => None, + } + } + + pub fn into_uuid_c_string(self) -> Option<*mut c_char> { + match self { + Binding::Scalar(v) => v.into_uuid_c_string(), + _ => None, + } + } } #[test] diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index cd872585..7c1e7c10 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -1,10 +1,14 @@ [package] name = "mentat_ffi" -version = "0.1.0" +version = "0.0.1" authors = ["Emily Toop "] +[lib] +name = "mentat_ffi" +crate-type = ["lib", "staticlib", "cdylib"] + [dependencies] libc = "0.2" [dependencies.mentat] -path = ".." +path = "../" diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index b657e871..fc7d734b 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -8,6 +8,65 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. +//! This module exposes an Foreign Function Interface (FFI) that allows Mentat to be +//! called from other languages. +//! +//! Functions that are available to other languages in this module are defined as +//! extern "C" functions which allow them to be layed out correctly for the +//! platform's C ABI. They all have a `#[no_mangle]` decorator to ensure +//! Rust's name mangling is turned off, so that it is easier to link to. +//! +//! Mentat's FFI contains unsafe code. As it is an interface between foreign code +//! and native Rust code, Rust cannot guarantee that the types and data that have been passed +//! to it from another language are present and in the format it is expecting. +//! This interface is designed to ensure that nothing unsafe passes through this module +//! and enters Mentat proper +//! +//! Structs defined with `#[repr(C)]` are guaranteed to have a layout that is compatible +//! with the platform's representation in C. +//! +//! This API passes pointers in two ways, depending on the lifetime of the value and +//! what value owns it. +//! Pointers to values that are guaranteed to live beyond the lifetime of the function, +//! are passed over the FFI as a raw pointer. +//! +//! `value as *const Binding` +//! +//! Pointers to values that cannot be guaranteed to live beyond the lifetime of the function +//! are first `Box`ed so that they live on the heap, and the raw pointer passed this way. +//! +//! `Box::into_raw(Box::new(value))` +//! +//! The memory for a value that is moved onto the heap before being passed over the FFI +//! is no longer managed by Rust, but Rust still owns the value. Therefore the pointer +//! must be returned to Rust in order to be released. To this effect a number of `destructor` +//! functions are provided for each Rust value type that is passed, as is a catch all destructor +//! to release memory for `#[repr(C)]` values. +//! The destructors reclaim the memory via [Box](std::boxed::Box) and then drop the reference, causing the +//! memory to be released. +//! +//! A macro has been provided to make defining destructors easier. +//! +//! `define_destructor!(query_builder_destroy, QueryBuilder);` +//! +//! Passing a pointer to memory that has already been released will cause Mentat to crash, +//! so callers have to be careful to ensure they manage their pointers properly. +//! Failure to call a destructor for a value on the heap will cause a memory leak. +//! +//! Generally, the functions exposed in this module have a direct mapping to existing Mentat APIs, +//! in order to keep application logic to a minumum and provide the greatest flexibility +//! for callers using the interface. However, in some cases a single convenience function +//! has been provided in order to make the interface easier to use and reduce the number +//! of calls that have to be made over the FFI to perform a task. An example of this is +//! `store_register_observer`, which takes a single native callback function that is then +//! wrapped inside a Rust closure and added to a [TxObserver](mentat::TxObserver) struct. This is then used to +//! register the observer with the store. +//! +//! [Result](std::result::Result) and [Option](std::option::Option) Rust types have `repr(C)` structs that mirror them. This is to provide a more +//! native access pattern to callers and to enable easier passing of optional types and error +//! propogation. These types have implemented [From](std::convert::From) such that conversion from the Rust type +//! to the C type is as painless as possible. + extern crate libc; extern crate mentat; @@ -17,6 +76,7 @@ use std::collections::{ use std::os::raw::{ c_char, c_int, + c_longlong, c_void, }; use std::slice; @@ -25,9 +85,8 @@ use std::sync::{ }; use std::vec; -use libc::time_t; - pub use mentat::{ + Binding, Entid, FindSpec, HasSchema, @@ -38,15 +97,24 @@ pub use mentat::{ QueryInputs, QueryOutput, QueryResults, + RelResult, Store, Syncable, TypedValue, TxObserver, + TxReport, Uuid, ValueType, Variable, }; +pub use mentat::entity_builder::{ + BuildTerms, + EntityBuilder, + InProgressBuilder, + IntoThing, +}; + pub mod android; pub mod utils; @@ -56,24 +124,53 @@ pub use utils::strings::{ string_to_c_char, }; -pub type TypedValueIterator = vec::IntoIter; -pub type TypedValueListIterator = vec::IntoIter>; +pub use utils::log; +pub type BindingIterator = vec::IntoIter; +pub type BindingListIterator = std::slice::Chunks<'static, mentat::Binding>; + +/// A C representation of the change provided by the transaction observers +/// from a single transact. +/// Holds a transaction identifier, the changes as a set of affected attributes +/// and the length of the list of changes. +/// +/// #Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. #[repr(C)] #[derive(Debug, Clone)] -pub struct ExternTxReport { +pub struct TransactionChange { pub txid: Entid, - pub changes: Box<[Entid]>, pub changes_len: usize, + pub changes: Box<[Entid]>, } + /// A C representation of the list of changes provided by the transaction observers. + /// Provides the list of changes as the length of the list. +/// +/// #Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. #[repr(C)] #[derive(Debug)] -pub struct ExternTxReportList { - pub reports: Box<[ExternTxReport]>, +pub struct TxChangeList { + pub reports: Box<[TransactionChange]>, pub len: usize, } +/// A C representation Rust's [Option](std::option::Option). +/// A value of `Some` results in `value` containing a raw pointer as a `c_void`. +/// A value of `None` results in `value` containing a null pointer. +/// +/// #Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. #[repr(C)] #[derive(Debug)] pub struct ExternOption { @@ -88,6 +185,16 @@ impl From> for ExternOption { } } +/// A C representation Rust's [Result](std::result::Result). +/// A value of `Ok` results in `ok` containing a raw pointer as a `c_void` +/// and `err` containing a null pointer. +/// A value of `Err` results in `value` containing a null pointer and `err` containing an error message. +/// +/// #Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. #[repr(C)] #[derive(Debug)] pub struct ExternResult { @@ -106,7 +213,7 @@ impl From> for ExternResult where E: std::error::Error { }, Err(e) => { ExternResult { - err: string_to_c_char(e.description()), + err: string_to_c_char(e.to_string()), ok: std::ptr::null(), } } @@ -114,11 +221,17 @@ impl From> for ExternResult where E: std::error::Error { } } -// A store cannot be opened twice to the same location. -// Once created, the reference to the store is held by the caller and not Rust, -// therefore the caller is responsible for calling `destroy` to release the memory -// used by the Store in order to avoid a memory leak. +/// A store cannot be opened twice to the same location. +/// Once created, the reference to the store is held by the caller and not Rust, +/// therefore the caller is responsible for calling `destroy` to release the memory +/// used by the [Store](mentat::Store) in order to avoid a memory leak. // TODO: Start returning `ExternResult`s rather than crashing on error. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `store_destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] pub extern "C" fn store_open(uri: *const c_char) -> *mut Store { let uri = c_char_to_string(uri); @@ -136,9 +249,68 @@ pub extern "C" fn store_open(uri: *const c_char) -> *mut Store { // TODO: begin_transaction +/// Performs a single transaction against the store. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. +/// +// TODO: Document the errors that can result from transact +#[no_mangle] +pub unsafe extern "C" fn store_transact(store: *mut Store, transaction: *const c_char) -> *mut ExternResult { + let store = &mut*store; + let transaction = c_char_to_string(transaction); + let result = store.begin_transaction().and_then(|mut in_progress| { + in_progress.transact(transaction).and_then(|tx_report| { + in_progress.commit() + .map(|_| tx_report) + }) + }); + Box::into_raw(Box::new(result.into())) +} + +/// Fetches the `tx_id` for the given [TxReport](mentat::TxReport). +#[no_mangle] +pub unsafe extern "C" fn tx_report_get_entid(tx_report: *mut TxReport) -> c_longlong { + let tx_report = &*tx_report; + tx_report.tx_id as c_longlong +} + +/// Fetches the `tx_instant` for the given [TxReport](mentat::TxReport). +#[no_mangle] +pub unsafe extern "C" fn tx_report_get_tx_instant(tx_report: *mut TxReport) -> c_longlong { + let tx_report = &*tx_report; + tx_report.tx_instant.timestamp() as c_longlong +} + +/// Fetches the [Entid](mentat::Entid) assigned to the `tempid` during the transaction represented +/// by the given [TxReport](mentat::TxReport). +#[no_mangle] +pub unsafe extern "C" fn tx_report_entity_for_temp_id(tx_report: *mut TxReport, tempid: *const c_char) -> *mut c_longlong { + let tx_report = &*tx_report; + let key = c_char_to_string(tempid); + if let Some(entid) = tx_report.tempids.get(key) { + Box::into_raw(Box::new(entid.clone() as c_longlong)) + } else { + std::ptr::null_mut() + } +} + // TODO: cache // TODO: q_once + +/// Creates a [QueryBuilder](mentat::QueryBuilder) from the given store to execute the provided query. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `query_builder_destroy` is provided for releasing the memory for this +/// pointer type. +/// +/// TODO: Update QueryBuilder so it only takes a [Store](mentat::Store) pointer on execution #[no_mangle] pub unsafe extern "C" fn store_query<'a>(store: *mut Store, query: *const c_char) -> *mut QueryBuilder<'a> { let query = c_char_to_string(query); @@ -147,28 +319,28 @@ pub unsafe extern "C" fn store_query<'a>(store: *mut Store, query: *const c_char Box::into_raw(Box::new(query_builder)) } +/// Binds a [TypedValue::Long](mentat::TypedValue::Long) to a [Variable](mentat::Variable) with the given name. #[no_mangle] -pub unsafe extern "C" fn query_builder_bind_int(query_builder: *mut QueryBuilder, var: *const c_char, value: c_int) { - let var = c_char_to_string(var); - let query_builder = &mut*query_builder; - let value = value as i32; - query_builder.bind_value(&var, value); -} - -#[no_mangle] -pub unsafe extern "C" fn query_builder_bind_long(query_builder: *mut QueryBuilder, var: *const c_char, value: i64) { +pub unsafe extern "C" fn query_builder_bind_long(query_builder: *mut QueryBuilder, var: *const c_char, value: c_longlong) { let var = c_char_to_string(var); let query_builder = &mut*query_builder; query_builder.bind_long(&var, value); } +/// Binds a [TypedValue::Ref](mentat::TypedValue::Ref) to a [Variable](mentat::Variable) with the given name. #[no_mangle] -pub unsafe extern "C" fn query_builder_bind_ref(query_builder: *mut QueryBuilder, var: *const c_char, value: i64) { +pub unsafe extern "C" fn query_builder_bind_ref(query_builder: *mut QueryBuilder, var: *const c_char, value: c_longlong) { let var = c_char_to_string(var); let query_builder = &mut*query_builder; query_builder.bind_ref(&var, value); } +/// Binds a [TypedValue::Ref](mentat::TypedValue::Ref) to a [Variable](mentat::Variable) with the given name. Takes a keyword as a c string in the format +/// `:namespace/name` and converts it into an [NamespacedKeyworf](mentat::NamespacedKeyword). +/// +/// # Panics +/// +/// If the provided keyword does not map to a valid keyword in the schema. #[no_mangle] pub unsafe extern "C" fn query_builder_bind_ref_kw(query_builder: *mut QueryBuilder, var: *const c_char, value: *const c_char) { let var = c_char_to_string(var); @@ -179,6 +351,8 @@ pub unsafe extern "C" fn query_builder_bind_ref_kw(query_builder: *mut QueryBuil } } +/// Binds a [TypedValue::Ref](mentat::TypedValue::Ref) to a [Variable](mentat::Variable) with the given name. Takes a keyword as a c string in the format +/// `:namespace/name` and converts it into an [NamespacedKeyworf](mentat::NamespacedKeyword). #[no_mangle] pub unsafe extern "C" fn query_builder_bind_kw(query_builder: *mut QueryBuilder, var: *const c_char, value: *const c_char) { let var = c_char_to_string(var); @@ -187,7 +361,7 @@ pub unsafe extern "C" fn query_builder_bind_kw(query_builder: *mut QueryBuilder, query_builder.bind_value(&var, kw); } -// boolean +/// Binds a [TypedValue::Boolean](mentat::TypedValue::Boolean) to a [Variable](mentat::Variable) with the given name. #[no_mangle] pub unsafe extern "C" fn query_builder_bind_boolean(query_builder: *mut QueryBuilder, var: *const c_char, value: bool) { let var = c_char_to_string(var); @@ -195,7 +369,7 @@ pub unsafe extern "C" fn query_builder_bind_boolean(query_builder: *mut QueryBui query_builder.bind_value(&var, value); } -// double +/// Binds a [TypedValue::Double](mentat::TypedValue::Double) to a [Variable](mentat::Variable) with the given name. #[no_mangle] pub unsafe extern "C" fn query_builder_bind_double(query_builder: *mut QueryBuilder, var: *const c_char, value: f64) { let var = c_char_to_string(var); @@ -203,15 +377,16 @@ pub unsafe extern "C" fn query_builder_bind_double(query_builder: *mut QueryBuil query_builder.bind_value(&var, value); } -// instant +/// Binds a [TypedValue::Instant](mentat::TypedValue::Instant) to a [Variable](mentat::Variable) with the given name. +/// Takes a timestamp in microseconds. #[no_mangle] -pub unsafe extern "C" fn query_builder_bind_timestamp(query_builder: *mut QueryBuilder, var: *const c_char, value: time_t) { +pub unsafe extern "C" fn query_builder_bind_timestamp(query_builder: *mut QueryBuilder, var: *const c_char, value: c_longlong) { let var = c_char_to_string(var); let query_builder = &mut*query_builder; - query_builder.bind_instant(&var, value as i64); + query_builder.bind_instant(&var, value); } -// string +/// Binds a [TypedValue::String](mentat::TypedValue::String) to a [Variable](mentat::Variable) with the given name. #[no_mangle] pub unsafe extern "C" fn query_builder_bind_string(query_builder: *mut QueryBuilder, var: *const c_char, value: *const c_char) { let var = c_char_to_string(var); @@ -220,22 +395,51 @@ pub unsafe extern "C" fn query_builder_bind_string(query_builder: *mut QueryBuil query_builder.bind_value(&var, value); } -// uuid +/// Binds a [TypedValue::Uuid](mentat::TypedValue::Uuid) to a [Variable](mentat::Variable) with the given name. +/// Takes a `UUID` as a byte slice of length 16. This maps directly to the `uuid_t` C type. #[no_mangle] -pub unsafe extern "C" fn query_builder_bind_uuid(query_builder: *mut QueryBuilder, var: *const c_char, value: *const c_char) { +pub unsafe extern "C" fn query_builder_bind_uuid(query_builder: *mut QueryBuilder, var: *const c_char, value: *mut [u8; 16]) { let var = c_char_to_string(var); - let value = Uuid::parse_str(&c_char_to_string(value)).expect("valid uuid"); + let value = &*value; + let value = Uuid::from_bytes(value).expect("valid uuid"); let query_builder = &mut*query_builder; query_builder.bind_value(&var, value); } +/// Executes a query and returns the results as a [Scalar](mentat::QueryResults::Scalar). +/// +/// # Panics +/// +/// If the find set of the query executed is not structured `[:find ?foo . :where ...]`. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] pub unsafe extern "C" fn query_builder_execute_scalar(query_builder: *mut QueryBuilder) -> *mut ExternResult { let query_builder = &mut*query_builder; let results = query_builder.execute_scalar(); - Box::into_raw(Box::new(results.into())) + let extern_result = match results { + Ok(Some(v)) => ExternResult { err: std::ptr::null(), ok: Box::into_raw(Box::new(v)) as *const _ as *const c_void, }, + Ok(None) => ExternResult { err: std::ptr::null(), ok: std::ptr::null(), }, + Err(e) => ExternResult { err: string_to_c_char(e.to_string()), ok: std::ptr::null(), } + }; + Box::into_raw(Box::new(extern_result)) } +/// Executes a query and returns the results as a [Coll](mentat::QueryResults::Coll). +/// +/// # Panics +/// +/// If the find set of the query executed is not structured `[:find [?foo ...] :where ...]`. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] pub unsafe extern "C" fn query_builder_execute_coll(query_builder: *mut QueryBuilder) -> *mut ExternResult { let query_builder = &mut*query_builder; @@ -243,13 +447,40 @@ pub unsafe extern "C" fn query_builder_execute_coll(query_builder: *mut QueryBui Box::into_raw(Box::new(results.into())) } +/// Executes a query and returns the results as a [Tuple](mentat::QueryResults::Tuple). +/// +/// # Panics +/// +/// If the find set of the query executed is not structured `[:find [?foo ?bar] :where ...]`. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] pub unsafe extern "C" fn query_builder_execute_tuple(query_builder: *mut QueryBuilder) -> *mut ExternResult { let query_builder = &mut*query_builder; let results = query_builder.execute_tuple(); - Box::into_raw(Box::new(results.into())) + let extern_result = match results { + Ok(Some(v)) => ExternResult { err: std::ptr::null(), ok: Box::into_raw(Box::new(v)) as *const _ as *const c_void, }, + Ok(None) => ExternResult { err: std::ptr::null(), ok: std::ptr::null(), }, + Err(e) => ExternResult { err: string_to_c_char(e.to_string()), ok: std::ptr::null(), } + }; + Box::into_raw(Box::new(extern_result)) } +/// Executes a query and returns the results as a [Rel](mentat::QueryResults::Rel). +/// +/// # Panics +/// +/// If the find set of the query executed is not structured `[:find ?foo ?bar :where ...]`. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] pub unsafe extern "C" fn query_builder_execute(query_builder: *mut QueryBuilder) -> *mut ExternResult { let query_builder = &mut*query_builder; @@ -257,273 +488,382 @@ pub unsafe extern "C" fn query_builder_execute(query_builder: *mut QueryBuilder) Box::into_raw(Box::new(results.into())) } -// as_long -#[no_mangle] -pub unsafe extern "C" fn typed_value_as_long(typed_value: *mut TypedValue) -> i64 { - let typed_value = Box::from_raw(typed_value); - typed_value.into_long().expect("Typed value cannot be coerced into a Long") +fn unwrap_conversion(value: Option, expected_type: ValueType) -> T { + match value { + Some(v) => v, + None => panic!("Typed value cannot be coerced into a {}", expected_type) + } } -// as_entid +/// Consumes a [Binding](mentat::Binding) and returns the value as a C `long`. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Long](mentat::ValueType::Long). #[no_mangle] -pub unsafe extern "C" fn typed_value_as_entid(typed_value: *mut TypedValue) -> Entid { +pub unsafe extern "C" fn typed_value_into_long(typed_value: *mut Binding) -> c_longlong { let typed_value = Box::from_raw(typed_value); - typed_value.into_entid().expect("Typed value cannot be coerced into an Entid") + unwrap_conversion(typed_value.into_long(), ValueType::Long) } -// kw +/// Consumes a [Binding](mentat::Binding) and returns the value as an [Entid](mentat::Entid). +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Long](mentat::ValueType::Ref). #[no_mangle] -pub unsafe extern "C" fn typed_value_as_kw(typed_value: *mut TypedValue) -> *const c_char { +pub unsafe extern "C" fn typed_value_into_entid(typed_value: *mut Binding) -> Entid { let typed_value = Box::from_raw(typed_value); - typed_value.into_kw_c_string().expect("Typed value cannot be coerced into a Namespaced Keyword") + unwrap_conversion(typed_value.into_entid(), ValueType::Ref) } -//as_boolean +/// Consumes a [Binding](mentat::Binding) and returns the value as an keyword C `String`. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Long](mentat::ValueType::Ref). #[no_mangle] -pub unsafe extern "C" fn typed_value_as_boolean(typed_value: *mut TypedValue) -> bool { +pub unsafe extern "C" fn typed_value_into_kw(typed_value: *mut Binding) -> *const c_char { let typed_value = Box::from_raw(typed_value); - typed_value.into_boolean().expect("Typed value cannot be coerced into a Boolean") + unwrap_conversion(typed_value.into_kw_c_string(), ValueType::Keyword) as *const c_char } -//as_double +/// Consumes a [Binding](mentat::Binding) and returns the value as a boolean represented as an `i32`. +/// If the value of the boolean is `true` the value returned is 1. +/// If the value of the boolean is `false` the value returned is 0. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Long](mentat::ValueType::Boolean). #[no_mangle] -pub unsafe extern "C" fn typed_value_as_double(typed_value: *mut TypedValue) -> f64 { +pub unsafe extern "C" fn typed_value_into_boolean(typed_value: *mut Binding) -> i32 { let typed_value = Box::from_raw(typed_value); - typed_value.into_double().expect("Typed value cannot be coerced into a Double") + if unwrap_conversion(typed_value.into_boolean(), ValueType::Boolean) { 1 } else { 0 } } -//as_timestamp +/// Consumes a [Binding](mentat::Binding) and returns the value as a `f64`. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Long](mentat::ValueType::Double). #[no_mangle] -pub unsafe extern "C" fn typed_value_as_timestamp(typed_value: *mut TypedValue) -> i64 { +pub unsafe extern "C" fn typed_value_into_double(typed_value: *mut Binding) -> f64 { let typed_value = Box::from_raw(typed_value); - let val = typed_value.into_timestamp().expect("Typed value cannot be coerced into a Timestamp"); - val + unwrap_conversion(typed_value.into_double(), ValueType::Double) } -//as_string +/// Consumes a [Binding](mentat::Binding) and returns the value as a microsecond timestamp. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Long](mentat::ValueType::Instant). #[no_mangle] -pub unsafe extern "C" fn typed_value_as_string(typed_value: *mut TypedValue) -> *const c_char { +pub unsafe extern "C" fn typed_value_into_timestamp(typed_value: *mut Binding) -> c_longlong { let typed_value = Box::from_raw(typed_value); - typed_value.into_c_string().expect("Typed value cannot be coerced into a String") + unwrap_conversion(typed_value.into_timestamp(), ValueType::Instant) } -//as_uuid +/// Consumes a [Binding](mentat::Binding) and returns the value as a C `String`. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Long](mentat::ValueType::String). #[no_mangle] -pub unsafe extern "C" fn typed_value_as_uuid(typed_value: *mut TypedValue) -> *const c_char { +pub unsafe extern "C" fn typed_value_into_string(typed_value: *mut Binding) -> *const c_char { let typed_value = Box::from_raw(typed_value); - typed_value.into_uuid_c_string().expect("Typed value cannot be coerced into a String") + unwrap_conversion(typed_value.into_c_string(), ValueType::String) as *const c_char } +/// Consumes a [Binding](mentat::Binding) and returns the value as a UUID byte slice of length 16. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Long](mentat::ValueType::Uuid). #[no_mangle] -pub unsafe extern "C" fn row_at_index(rows: *mut Vec>, index: c_int) -> *mut Vec { +pub unsafe extern "C" fn typed_value_into_uuid(typed_value: *mut Binding) -> *mut [u8; 16] { + let typed_value = Box::from_raw(typed_value); + let value = unwrap_conversion(typed_value.into_uuid(), ValueType::Uuid); + Box::into_raw(Box::new(*value.as_bytes())) +} + +/// Returns the [ValueType](mentat::ValueType) of this [Binding](mentat::Binding). +#[no_mangle] +pub unsafe extern "C" fn typed_value_value_type(typed_value: *mut Binding) -> ValueType { + let typed_value = &*typed_value; + typed_value.value_type().unwrap_or_else(|| panic!("Binding is not Scalar and has no ValueType")) +} + +/// Returns the value at the provided `index` as a `Vec`. +/// If there is no value present at the `index`, a null pointer is returned. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `typed_value_result_set_destroy` is provided for releasing the memory for this +/// pointer type. +#[no_mangle] +pub unsafe extern "C" fn row_at_index(rows: *mut RelResult, index: c_int) -> *mut Vec { let result = &*rows; - result.get(index as usize).map_or_else(std::ptr::null_mut, |v| Box::into_raw(Box::new(v.clone()))) + result.row(index as usize).map_or_else(std::ptr::null_mut, |v| Box::into_raw(Box::new(v.to_vec()))) } +/// Consumes the `Vec>` and returns an iterator over the values. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `typed_value_result_set_iter_destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] -pub unsafe extern "C" fn rows_iter(rows: *mut Vec>) -> *mut TypedValueListIterator { - let result = Box::from_raw(rows); - Box::into_raw(Box::new(result.into_iter())) +pub unsafe extern "C" fn typed_value_result_set_into_iter(rows: *mut RelResult) -> *mut BindingListIterator { + let result = &*rows; + let rows = result.rows(); + Box::into_raw(Box::new(rows)) } +/// Returns the next value in the `iter` as a `Vec`. +/// If there is no value next value, a null pointer is returned. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `typed_value_list_destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] -pub unsafe extern "C" fn rows_iter_next(iter: *mut TypedValueListIterator) -> *mut Vec { +pub unsafe extern "C" fn typed_value_result_set_iter_next(iter: *mut BindingListIterator) -> *mut Vec { let iter = &mut *iter; - iter.next().map_or(std::ptr::null_mut(), |v| Box::into_raw(Box::new(v))) + iter.next().map_or(std::ptr::null_mut(), |v| Box::into_raw(Box::new(v.to_vec()))) } +/// Consumes the `Vec` and returns an iterator over the values. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `typed_value_list_iter_destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] -pub unsafe extern "C" fn values_iter(values: *mut Vec) -> *mut TypedValueIterator { +pub unsafe extern "C" fn typed_value_list_into_iter(values: *mut Vec) -> *mut BindingIterator { let result = Box::from_raw(values); Box::into_raw(Box::new(result.into_iter())) } +/// Returns the next value in the `iter` as a [Binding](mentat::Binding). +/// If there is no value next value, a null pointer is returned. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `typed_value_destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] -pub unsafe extern "C" fn values_iter_next(iter: *mut TypedValueIterator) -> *const TypedValue { +pub unsafe extern "C" fn typed_value_list_iter_next(iter: *mut BindingIterator) -> *mut Binding { let iter = &mut *iter; - iter.next().map_or_else(std::ptr::null, |v| &v as *const TypedValue) + iter.next().map_or(std::ptr::null_mut(), |v| Box::into_raw(Box::new(v))) } -//as_long +/// Returns the value at the provided `index` as a [Binding](mentat::Binding). +/// If there is no value present at the `index`, a null pointer is returned. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `typed_value_destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] -pub unsafe extern "C" fn values_iter_next_as_long(iter: *mut TypedValueIterator) -> *const i64 { - let iter = &mut *iter; - iter.next().map_or_else(std::ptr::null, |v| &v.into_long().expect("Typed value cannot be coerced into a Long") as *const i64) -} -// as ref -#[no_mangle] -pub unsafe extern "C" fn values_iter_next_as_entid(iter: *mut TypedValueIterator) -> *const Entid { - let iter = &mut *iter; - iter.next().map_or_else(std::ptr::null, |v| &v.into_entid().expect("Typed value cannot be coerced into am Entid") as *const Entid) -} - -// as kw -#[no_mangle] -pub unsafe extern "C" fn values_iter_next_as_kw(iter: *mut TypedValueIterator) -> *const c_char { - let iter = &mut *iter; - iter.next().map_or_else(std::ptr::null, |v| v.into_kw_c_string().expect("Typed value cannot be coerced into a Namespaced Keyword")) -} - -//as_boolean -#[no_mangle] -pub unsafe extern "C" fn values_iter_next_as_boolean(iter: *mut TypedValueIterator) -> *const bool { - let iter = &mut *iter; - iter.next().map_or_else(std::ptr::null, |v| &v.into_boolean().expect("Typed value cannot be coerced into a Boolean") as *const bool) -} - -//as_double -#[no_mangle] -pub unsafe extern "C" fn values_iter_next_as_double(iter: *mut TypedValueIterator) -> *const f64 { - let iter = &mut *iter; - iter.next().map_or_else(std::ptr::null, |v| &v.into_double().expect("Typed value cannot be coerced into a Double") as *const f64) -} - -//as_timestamp -#[no_mangle] -pub unsafe extern "C" fn values_iter_next_as_timestamp(iter: *mut TypedValueIterator) -> *const i64 { - let iter = &mut *iter; - iter.next().map_or_else(std::ptr::null, |v| v.into_timestamp().expect("Typed value cannot be coerced into a Timestamp") as *const i64) -} - -//as_string -#[no_mangle] -pub unsafe extern "C" fn values_iter_next_as_string(iter: *mut TypedValueIterator) -> *const c_char { - let iter = &mut *iter; - iter.next().map_or_else(std::ptr::null, |v| v.into_c_string().expect("Typed value cannot be coerced into a String")) -} - -//as_uuid -#[no_mangle] -pub unsafe extern "C" fn values_iter_next_as_uuid(iter: *mut TypedValueIterator) -> *const c_char { - let iter = &mut *iter; - iter.next().map_or_else(std::ptr::null, |v| v.into_uuid_c_string().expect("Typed value cannot be coerced into a Uuid")) -} - -#[no_mangle] -pub unsafe extern "C" fn value_at_index(values: *mut Vec, index: c_int) -> *const TypedValue { +pub unsafe extern "C" fn value_at_index(values: *mut Vec, index: c_int) -> *const Binding { let result = &*values; - result.get(index as usize).expect("No value at index") as *const TypedValue + result.get(index as usize).expect("No value at index") as *const Binding } -//as_long +/// Returns the value of the [Binding](mentat::Binding) at `index` as a `long`. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not `ValueType::Long`. +/// If there is no value at `index`. #[no_mangle] -pub unsafe extern "C" fn value_at_index_as_long(values: *mut Vec, index: c_int) -> i64 { +pub unsafe extern "C" fn value_at_index_into_long(values: *mut Vec, index: c_int) -> c_longlong { let result = &*values; let value = result.get(index as usize).expect("No value at index"); - value.clone().into_long().expect("Typed value cannot be coerced into a Long") + unwrap_conversion(value.clone().into_long(), ValueType::Long) } -// as ref + +/// Returns the value of the [Binding](mentat::Binding) at `index` as an [Entid](mentat::Entid). +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not `ValueType::Ref`. +/// If there is no value at `index`. #[no_mangle] -pub unsafe extern "C" fn value_at_index_as_entid(values: *mut Vec, index: c_int) -> Entid { +pub unsafe extern "C" fn value_at_index_into_entid(values: *mut Vec, index: c_int) -> Entid { let result = &*values; let value = result.get(index as usize).expect("No value at index"); - value.clone().into_entid().expect("Typed value cannot be coerced into an Entid") + unwrap_conversion(value.clone().into_entid(), ValueType::Ref) } -// as kw +/// Returns the value of the [Binding](mentat::Binding) at `index` as a keyword C `String`. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Ref](mentat::ValueType::Ref). +/// If there is no value at `index`. #[no_mangle] -pub unsafe extern "C" fn value_at_index_as_kw(values: *mut Vec, index: c_int) -> *const c_char { +pub unsafe extern "C" fn value_at_index_into_kw(values: *mut Vec, index: c_int) -> *const c_char { let result = &*values; let value = result.get(index as usize).expect("No value at index"); - value.clone().into_kw_c_string().expect("Typed value cannot be coerced into a Namespaced Keyword") + unwrap_conversion(value.clone().into_kw_c_string(), ValueType::Keyword) as *const c_char } -//as_boolean +/// Returns the value of the [Binding](mentat::Binding) at `index` as a boolean represented by a `i32`. +/// If the value of the `boolean` is `true` then the value returned is 1. +/// If the value of the `boolean` is `false` then the value returned is 0. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Long](mentat::ValueType::Long). +/// If there is no value at `index`. #[no_mangle] -pub unsafe extern "C" fn value_at_index_as_boolean(values: *mut Vec, index: c_int) -> bool { +pub unsafe extern "C" fn value_at_index_into_boolean(values: *mut Vec, index: c_int) -> i32 { let result = &*values; let value = result.get(index as usize).expect("No value at index"); - value.clone().into_boolean().expect("Typed value cannot be coerced into a Boolean") + if unwrap_conversion(value.clone().into_boolean(), ValueType::Boolean) { 1 } else { 0 } } -//as_double +/// Returns the value of the [Binding](mentat::Binding) at `index` as an `f64`. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Double](mentat::ValueType::Double). +/// If there is no value at `index`. #[no_mangle] -pub unsafe extern "C" fn value_at_index_as_double(values: *mut Vec, index: c_int) -> f64 { +pub unsafe extern "C" fn value_at_index_into_double(values: *mut Vec, index: c_int) -> f64 { let result = &*values; let value = result.get(index as usize).expect("No value at index"); - value.clone().into_double().expect("Typed value cannot be coerced into a Double") + unwrap_conversion(value.clone().into_double(), ValueType::Double) } -//as_timestamp +/// Returns the value of the [Binding](mentat::Binding) at `index` as a microsecond timestamp. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Instant](mentat::ValueType::Instant). +/// If there is no value at `index`. #[no_mangle] -pub unsafe extern "C" fn value_at_index_as_timestamp(values: *mut Vec, index: c_int) -> i64 { +pub unsafe extern "C" fn value_at_index_into_timestamp(values: *mut Vec, index: c_int) -> c_longlong { let result = &*values; let value = result.get(index as usize).expect("No value at index"); - value.clone().into_timestamp().expect("Typed value cannot be coerced into a timestamp") + unwrap_conversion(value.clone().into_timestamp(), ValueType::Instant) } -//as_string +/// Returns the value of the [Binding](mentat::Binding) at `index` as a C `String`. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::String](mentat::ValueType::String). +/// If there is no value at `index`. #[no_mangle] -pub unsafe extern "C" fn value_at_index_as_string(values: *mut Vec, index: c_int) -> *mut c_char { +pub unsafe extern "C" fn value_at_index_into_string(values: *mut Vec, index: c_int) -> *const c_char { let result = &*values; let value = result.get(index as usize).expect("No value at index"); - value.clone().into_c_string().expect("Typed value cannot be coerced into a String") + unwrap_conversion(value.clone().into_c_string(), ValueType::String) as *const c_char } -//as_uuid +/// Returns the value of the [Binding](mentat::Binding) at `index` as a UUID byte slice of length 16. +/// +/// # Panics +/// +/// If the [ValueType](mentat::ValueType) of the [Binding](mentat::Binding) is not [ValueType::Uuid](mentat::ValueType::Uuid). +/// If there is no value at `index`. #[no_mangle] -pub unsafe extern "C" fn value_at_index_as_uuid(values: *mut Vec, index: c_int) -> *mut c_char { +pub unsafe extern "C" fn value_at_index_into_uuid(values: *mut Vec, index: c_int) -> *mut [u8; 16] { let result = &*values; let value = result.get(index as usize).expect("No value at index"); - value.clone().into_uuid_c_string().expect("Typed value cannot be coerced into a Uuid") + let uuid = unwrap_conversion(value.clone().into_uuid(), ValueType::Uuid); + Box::into_raw(Box::new(*uuid.as_bytes())) } -// TODO: q_prepare - -// TODO: q_explain - -// TODO: lookup_values_for_attribute - +/// Returns an [ExternResult](ExternResult) containing the [Binding](mentat::Binding) associated with the `attribute` as `:namespace/name` +/// for the given `entid`. +/// If there is a value for that `attribute` on the entity with id `entid` then the value is returned in `ok`. +/// If there no value for that `attribute` on the entity with id `entid` but the attribute is value, +/// then a null pointer is returned in `ok`. +/// If there is no [Attribute](mentat::Attribute) in the [Schema](mentat::Schema) for the given `attribute` then an error is returned in `err`. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `destroy` is provided for releasing the memory for this +/// pointer type. +/// +/// TODO: list the types of error that can be caused by this function #[no_mangle] -pub unsafe extern "C" fn store_value_for_attribute(store: *mut Store, entid: i64, attribute: *const c_char) -> *mut ExternResult { +pub unsafe extern "C" fn store_value_for_attribute(store: *mut Store, entid: c_longlong, attribute: *const c_char) -> *mut ExternResult { let store = &*store; let kw = kw_from_string(c_char_to_string(attribute)); let value = match store.lookup_value_for_attribute(entid, &kw) { - Ok(Some(v)) => ExternResult { ok: Box::into_raw(Box::new(v)) as *const _ as *const c_void, err: std::ptr::null() }, + Ok(Some(v)) => { + let value: Binding = v.into(); + ExternResult { ok: Box::into_raw(Box::new(value)) as *const _ as *const c_void, err: std::ptr::null() } + }, Ok(None) => ExternResult { ok: std::ptr::null(), err: std::ptr::null() }, - Err(e) => ExternResult { ok: std::ptr::null(), err: string_to_c_char(e.description()) }, + Err(e) => ExternResult { ok: std::ptr::null(), err: string_to_c_char(e.to_string()) }, }; Box::into_raw(Box::new(value)) } +/// Registers a [TxObserver](mentat::TxObserver) with the `key` to observe changes to `attributes` +/// on this `store`. +/// Calls `callback` is a relevant transaction occurs. +/// +/// # Panics +/// +/// If there is no [Attribute](mentat::Attribute) in the [Schema](mentat::Schema) for a given `attribute`. +/// #[no_mangle] pub unsafe extern "C" fn store_register_observer(store: *mut Store, key: *const c_char, attributes: *const Entid, attributes_len: usize, - callback: extern fn(key: *const c_char, reports: &ExternTxReportList)) { + callback: extern fn(key: *const c_char, reports: &TxChangeList)) { let store = &mut*store; let mut attribute_set = BTreeSet::new(); let slice = slice::from_raw_parts(attributes, attributes_len); attribute_set.extend(slice.iter()); let key = c_char_to_string(key); let tx_observer = Arc::new(TxObserver::new(attribute_set, move |obs_key, batch| { - let extern_reports: Vec = batch.into_iter().map(|(tx_id, changes)| { + let extern_reports: Vec = batch.into_iter().map(|(tx_id, changes)| { let changes: Vec = changes.into_iter().map(|i|*i).collect(); let len = changes.len(); - ExternTxReport { + TransactionChange { txid: *tx_id, changes: changes.into_boxed_slice(), changes_len: len, } }).collect(); let len = extern_reports.len(); - let reports = ExternTxReportList { + let reports = TxChangeList { reports: extern_reports.into_boxed_slice(), len: len, }; callback(string_to_c_char(obs_key), &reports); })); - store.register_observer(key, tx_observer); + store.register_observer(key.to_string(), tx_observer); } +/// Unregisters a [TxObserver](mentat::TxObserver) with the `key` to observe changes on this `store`. #[no_mangle] pub unsafe extern "C" fn store_unregister_observer(store: *mut Store, key: *const c_char) { let store = &mut*store; - let key = c_char_to_string(key); + let key = c_char_to_string(key).to_string(); store.unregister_observer(&key); } +/// Returns the [Entid](mentat::Entid) associated with the `attr` as `:namespace/name`. +/// +/// # Panics +/// +/// If there is no [Attribute](mentat::Attribute) in the [Schema](mentat::Schema) for `attr`. #[no_mangle] pub unsafe extern "C" fn store_entid_for_attribute(store: *mut Store, attr: *const c_char) -> Entid { let store = &mut*store; @@ -534,127 +874,71 @@ pub unsafe extern "C" fn store_entid_for_attribute(store: *mut Store, attr: *con current_schema.get_entid(&kw).expect("Unable to find entid for invalid attribute").into() } +/// Returns the value at the provided `index` as a [TransactionChange](TransactionChange) . +/// +/// # Panics +/// +/// If there is no value present at the `index`. +/// +/// # Safety +/// +/// Callers are responsible for managing the memory for the return value. +/// A destructor `typed_value_destroy` is provided for releasing the memory for this +/// pointer type. #[no_mangle] -pub unsafe extern "C" fn tx_report_list_entry_at(tx_report_list: *mut ExternTxReportList, index: c_int) -> *const ExternTxReport { +pub unsafe extern "C" fn tx_change_list_entry_at(tx_report_list: *mut TxChangeList, index: c_int) -> *const TransactionChange { let tx_report_list = &*tx_report_list; let index = index as usize; let report = Box::new(tx_report_list.reports[index].clone()); Box::into_raw(report) } +/// Returns the value at the provided `index` as a [Entid](mentat::Entid) . +/// +/// # Panics +/// +/// If there is no value present at the `index`. #[no_mangle] -pub unsafe extern "C" fn changelist_entry_at(tx_report: *mut ExternTxReport, index: c_int) -> Entid { +pub unsafe extern "C" fn changelist_entry_at(tx_report: *mut TransactionChange, index: c_int) -> Entid { let tx_report = &*tx_report; let index = index as usize; tx_report.changes[index].clone() } -#[no_mangle] -pub unsafe extern "C" fn store_sync(store: *mut Store, user_uuid: *const c_char, server_uri: *const c_char) -> *mut ExternResult { - let store = &mut*store; - let user_uuid = c_char_to_string(user_uuid); - let server_uri = c_char_to_string(server_uri); - let res = store.sync(&server_uri, &user_uuid); - Box::into_raw(Box::new(res.into())) -} - -fn assert_datom(store: &mut Store, entid: E, attribute: String, value: V) -> *mut ExternResult -where E: Into, - V: Into { - let kw = kw_from_string(attribute); - let res = store.assert_datom(entid.into(), kw, value.into()); - Box::into_raw(Box::new(res.into())) -} - -#[no_mangle] -pub unsafe extern "C" fn store_set_long_for_attribute_on_entid(store: *mut Store, entid: Entid, attribute: *const c_char, value: i64) -> *mut ExternResult { - let store = &mut*store; - let kw = kw_from_string(c_char_to_string(attribute)); - let res = store.assert_datom(KnownEntid(entid), kw, TypedValue::Long(value)); - Box::into_raw(Box::new(res.into())) -} - -#[no_mangle] -pub unsafe extern "C" fn store_set_entid_for_attribute_on_entid(store: *mut Store, entid: Entid, attribute: *const c_char, value: Entid) -> *mut ExternResult { - let store = &mut*store; - let kw = kw_from_string(c_char_to_string(attribute)); - let res = store.assert_datom(KnownEntid(entid), kw, TypedValue::Ref(value)); - Box::into_raw(Box::new(res.into())) -} - -#[no_mangle] -pub unsafe extern "C" fn store_set_kw_ref_for_attribute_on_entid(store: *mut Store, entid: Entid, attribute: *const c_char, value: *const c_char) -> *mut ExternResult { - let store = &mut*store; - let kw = kw_from_string(c_char_to_string(attribute)); - let value = kw_from_string(c_char_to_string(value)); - let is_valid = store.conn().current_schema().get_entid(&value); - if is_valid.is_none() { - return Box::into_raw(Box::new(ExternResult { ok: std::ptr::null_mut(), err: string_to_c_char(format!("Unknown attribute {:?}", value)) })); - } - let kw_entid = is_valid.unwrap(); - let res = store.assert_datom(KnownEntid(entid), kw, TypedValue::Ref(kw_entid.into())); - Box::into_raw(Box::new(res.into())) -} - -#[no_mangle] -pub unsafe extern "C" fn store_set_boolean_for_attribute_on_entid(store: *mut Store, entid: Entid, attribute: *const c_char, value: bool) -> *mut ExternResult { - let store = &mut*store; - assert_datom(store, KnownEntid(entid), c_char_to_string(attribute), value) -} - -#[no_mangle] -pub unsafe extern "C" fn store_set_double_for_attribute_on_entid(store: *mut Store, entid: Entid, attribute: *const c_char, value: f64) -> *mut ExternResult { - let store = &mut*store; - assert_datom(store, KnownEntid(entid), c_char_to_string(attribute), value) -} - -#[no_mangle] -pub unsafe extern "C" fn store_set_timestamp_for_attribute_on_entid(store: *mut Store, entid: Entid, attribute: *const c_char, value: time_t) -> *mut ExternResult { - let store = &mut*store; - let kw = kw_from_string(c_char_to_string(attribute)); - let res = store.assert_datom(KnownEntid(entid), kw, TypedValue::instant(value as i64)); - Box::into_raw(Box::new(res.into())) -} - -#[no_mangle] -pub unsafe extern "C" fn store_set_string_for_attribute_on_entid(store: *mut Store, entid: Entid, attribute: *const c_char, value: *const c_char) -> *mut ExternResult { - let store = &mut*store; - assert_datom(store, KnownEntid(entid), c_char_to_string(attribute), c_char_to_string(value)) -} - -#[no_mangle] -pub unsafe extern "C" fn store_set_uuid_for_attribute_on_entid(store: *mut Store, entid: Entid, attribute: *const c_char, value: *const c_char) -> *mut ExternResult { - let store = &mut*store; - let uuid = Uuid::parse_str(&c_char_to_string(value)).expect("valid uuid"); - assert_datom(store, KnownEntid(entid), c_char_to_string(attribute), uuid) -} - +/// destroy function for releasing the memory for `repr(C)` structs. #[no_mangle] pub unsafe extern "C" fn destroy(obj: *mut c_void) { - if !obj.is_null() { - let obj_to_release = Box::from_raw(obj); - println!("object to release {:?}", obj_to_release); - } + let _ = Box::from_raw(obj); } +/// Creates a function with a given `$name` that releases the memroy for a type `$t`. macro_rules! define_destructor ( ($name:ident, $t:ty) => ( #[no_mangle] pub unsafe extern "C" fn $name(obj: *mut $t) { - if !obj.is_null() { let _ = Box::from_raw(obj); } + let _ = Box::from_raw(obj); } ) ); + +/// Destructor for releasing the memory of [QueryBuilder](mentat::QueryBuilder) . define_destructor!(query_builder_destroy, QueryBuilder); +/// Destructor for releasing the memory of [Store](mentat::Store) . define_destructor!(store_destroy, Store); -define_destructor!(typed_value_destroy, TypedValue); +/// Destructor for releasing the memory of [TxReport](mentat::TxReport) . +define_destructor!(tx_report_destroy, TxReport); -define_destructor!(typed_value_list_destroy, Vec); +/// Destructor for releasing the memory of [Binding](mentat::Binding). +define_destructor!(typed_value_destroy, Binding); -define_destructor!(typed_value_list_iter_destroy, TypedValueIterator); +define_destructor!(typed_value_list_destroy, Vec); -define_destructor!(typed_value_result_set_destroy, Vec>); +/// Destructor for releasing the memory of [BindingIterator](BindingIterator) . +define_destructor!(typed_value_list_iter_destroy, BindingIterator); -define_destructor!(typed_value_result_set_iter_destroy, TypedValueListIterator); +define_destructor!(typed_value_result_set_destroy, RelResult); + +/// Destructor for releasing the memory of [BindingListIterator](BindingListIterator) . +define_destructor!(typed_value_result_set_iter_destroy, BindingListIterator); diff --git a/ffi/src/utils.rs b/ffi/src/utils.rs index 76aeee47..3dffd4b4 100644 --- a/ffi/src/utils.rs +++ b/ffi/src/utils.rs @@ -19,19 +19,18 @@ pub mod strings { Keyword, }; - pub fn c_char_to_string(cchar: *const c_char) -> String { + pub fn c_char_to_string(cchar: *const c_char) -> &'static str { let c_str = unsafe { CStr::from_ptr(cchar) }; - let r_str = c_str.to_str().unwrap_or(""); - r_str.to_string() + c_str.to_str().unwrap_or("") } pub fn string_to_c_char(r_string: T) -> *mut c_char where T: Into { CString::new(r_string.into()).unwrap().into_raw() } + pub fn kw_from_string(keyword_string: &'static str) -> Keyword { // TODO: validate. The input might not be a keyword! - pub fn kw_from_string(mut keyword_string: String) -> Keyword { - let attr_name = keyword_string.split_off(1); + let attr_name = keyword_string.trim_left_matches(":"); let parts: Vec<&str> = attr_name.split("/").collect(); Keyword::namespaced(parts[0], parts[1]) } diff --git a/sdks/android/Mentat/.gitignore b/sdks/android/Mentat/.gitignore new file mode 100644 index 00000000..39fb081a --- /dev/null +++ b/sdks/android/Mentat/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/sdks/android/Mentat/.project b/sdks/android/Mentat/.project new file mode 100644 index 00000000..6f872c74 --- /dev/null +++ b/sdks/android/Mentat/.project @@ -0,0 +1,17 @@ + + + Mentat + Project Mentat created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/sdks/android/Mentat/build.gradle b/sdks/android/Mentat/build.gradle new file mode 100644 index 00000000..4aac5c62 --- /dev/null +++ b/sdks/android/Mentat/build.gradle @@ -0,0 +1,27 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.1.60' + repositories { + jcenter() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + google() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/sdks/android/Mentat/gradle.properties b/sdks/android/Mentat/gradle.properties new file mode 100644 index 00000000..aac7c9b4 --- /dev/null +++ b/sdks/android/Mentat/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/sdks/android/Mentat/gradle/wrapper/gradle-wrapper.properties b/sdks/android/Mentat/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..d82ad3fe --- /dev/null +++ b/sdks/android/Mentat/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Mar 29 09:07:24 BST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/sdks/android/Mentat/gradlew b/sdks/android/Mentat/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/sdks/android/Mentat/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/sdks/android/Mentat/gradlew.bat b/sdks/android/Mentat/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/sdks/android/Mentat/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sdks/android/Mentat/library/.classpath b/sdks/android/Mentat/library/.classpath new file mode 100644 index 00000000..eb19361b --- /dev/null +++ b/sdks/android/Mentat/library/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/sdks/android/Mentat/library/.gitignore b/sdks/android/Mentat/library/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/sdks/android/Mentat/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sdks/android/Mentat/library/.project b/sdks/android/Mentat/library/.project new file mode 100644 index 00000000..ac485d7c --- /dev/null +++ b/sdks/android/Mentat/library/.project @@ -0,0 +1,23 @@ + + + app + Project app created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/sdks/android/Mentat/library/build.gradle b/sdks/android/Mentat/library/build.gradle new file mode 100644 index 00000000..e23b31fc --- /dev/null +++ b/sdks/android/Mentat/library/build.gradle @@ -0,0 +1,42 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 26 + defaultConfig { + minSdkVersion 26 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + androidTest.assets.srcDirs += '../../../../fixtures' + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + androidTestImplementation 'com.android.support:support-annotations:24.0.0' + androidTestImplementation 'com.android.support.test:runner:0.5' + androidTestImplementation 'com.android.support.test:rules:0.5' + implementation 'com.android.support:appcompat-v7:26.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.0.2' + implementation 'com.android.support:design:26.1.0' + testImplementation 'junit:junit:4.12' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + implementation 'net.java.dev.jna:jna:4.5.1' +} +repositories { + mavenCentral() +} diff --git a/sdks/android/Mentat/library/proguard-rules.pro b/sdks/android/Mentat/library/proguard-rules.pro new file mode 100644 index 00000000..2e8dcd72 --- /dev/null +++ b/sdks/android/Mentat/library/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ~/.android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/sdks/android/Mentat/library/src/androidTest/java/com/mozilla/mentat/Expectation.java b/sdks/android/Mentat/library/src/androidTest/java/com/mozilla/mentat/Expectation.java new file mode 100644 index 00000000..de8d5d73 --- /dev/null +++ b/sdks/android/Mentat/library/src/androidTest/java/com/mozilla/mentat/Expectation.java @@ -0,0 +1,27 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import java.util.EventListener; + +interface ExpectationEventListener extends EventListener { + public void fulfill(); +} + +public class Expectation implements ExpectationEventListener { + public boolean isFulfilled = false; + public void fulfill() { + this.isFulfilled = true; + synchronized (this) { + notifyAll( ); + } + } +} diff --git a/sdks/android/Mentat/library/src/androidTest/java/com/mozilla/mentat/FFIIntegrationTest.java b/sdks/android/Mentat/library/src/androidTest/java/com/mozilla/mentat/FFIIntegrationTest.java new file mode 100644 index 00000000..dfe8b515 --- /dev/null +++ b/sdks/android/Mentat/library/src/androidTest/java/com/mozilla/mentat/FFIIntegrationTest.java @@ -0,0 +1,725 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import android.content.Context; +import android.content.res.AssetManager; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.UUID; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + */ +@RunWith(AndroidJUnit4.class) +public class FFIIntegrationTest { + + Mentat mentat = null; + + @Test + public void openInMemoryStoreSucceeds() throws Exception { + Mentat mentat = new Mentat(); + assertNotNull(mentat); + } + + @Test + public void openStoreInLocationSucceeds() throws Exception { + Context context = InstrumentationRegistry.getTargetContext(); + String path = context.getDatabasePath("test.db").getAbsolutePath(); + Mentat mentat = new Mentat(path); + assertNotNull(mentat); + } + + public String readFile(String fileName) { + Context testContext = InstrumentationRegistry.getInstrumentation().getContext(); + AssetManager assetManager = testContext.getAssets(); + try { + InputStream inputStream = assetManager.open(fileName); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder out = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + out.append(line + "\n"); + } + return out.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + public TxReport transactCitiesSchema(Mentat mentat) { + String citiesSchema = this.readFile("cities.schema"); + return mentat.transact(citiesSchema); + } + + public TxReport transactSeattleData(Mentat mentat) { + String seattleData = this.readFile("all_seattle.edn"); + return mentat.transact(seattleData); + } + + public Mentat openAndInitializeCitiesStore() { + if (this.mentat == null) { + this.mentat = new Mentat(); + this.transactCitiesSchema(mentat); + this.transactSeattleData(mentat); + } + + return this.mentat; + } + + public TxReport populateWithTypesSchema(Mentat mentat) { + String schema = "[\n" + + " [:db/add \"b\" :db/ident :foo/boolean]\n" + + " [:db/add \"b\" :db/valueType :db.type/boolean]\n" + + " [:db/add \"b\" :db/cardinality :db.cardinality/one]\n" + + " [:db/add \"l\" :db/ident :foo/long]\n" + + " [:db/add \"l\" :db/valueType :db.type/long]\n" + + " [:db/add \"l\" :db/cardinality :db.cardinality/one]\n" + + " [:db/add \"r\" :db/ident :foo/ref]\n" + + " [:db/add \"r\" :db/valueType :db.type/ref]\n" + + " [:db/add \"r\" :db/cardinality :db.cardinality/one]\n" + + " [:db/add \"i\" :db/ident :foo/instant]\n" + + " [:db/add \"i\" :db/valueType :db.type/instant]\n" + + " [:db/add \"i\" :db/cardinality :db.cardinality/one]\n" + + " [:db/add \"d\" :db/ident :foo/double]\n" + + " [:db/add \"d\" :db/valueType :db.type/double]\n" + + " [:db/add \"d\" :db/cardinality :db.cardinality/one]\n" + + " [:db/add \"s\" :db/ident :foo/string]\n" + + " [:db/add \"s\" :db/valueType :db.type/string]\n" + + " [:db/add \"s\" :db/cardinality :db.cardinality/one]\n" + + " [:db/add \"k\" :db/ident :foo/keyword]\n" + + " [:db/add \"k\" :db/valueType :db.type/keyword]\n" + + " [:db/add \"k\" :db/cardinality :db.cardinality/one]\n" + + " [:db/add \"u\" :db/ident :foo/uuid]\n" + + " [:db/add \"u\" :db/valueType :db.type/uuid]\n" + + " [:db/add \"u\" :db/cardinality :db.cardinality/one]\n" + + " ]"; + TxReport report = mentat.transact(schema); + Long stringEntid = report.getEntidForTempId("s"); + + String data = "[\n" + + " [:db/add \"a\" :foo/boolean true]\n" + + " [:db/add \"a\" :foo/long 25]\n" + + " [:db/add \"a\" :foo/instant #inst \"2017-01-01T11:00:00.000Z\"]\n" + + " [:db/add \"a\" :foo/double 11.23]\n" + + " [:db/add \"a\" :foo/string \"The higher we soar the smaller we appear to those who cannot fly.\"]\n" + + " [:db/add \"a\" :foo/keyword :foo/string]\n" + + " [:db/add \"a\" :foo/uuid #uuid \"550e8400-e29b-41d4-a716-446655440000\"]\n" + + " [:db/add \"b\" :foo/boolean false]\n" + + " [:db/add \"b\" :foo/ref "+ stringEntid +"]\n" + + " [:db/add \"b\" :foo/long 50]\n" + + " [:db/add \"b\" :foo/instant #inst \"2018-01-01T11:00:00.000Z\"]\n" + + " [:db/add \"b\" :foo/double 22.46]\n" + + " [:db/add \"b\" :foo/string \"Silence is worse; all truths that are kept silent become poisonous.\"]\n" + + " [:db/add \"b\" :foo/uuid #uuid \"4cb3f828-752d-497a-90c9-b1fd516d5644\"]\n" + + " ]"; + return mentat.transact(data); + } + + @Test + public void transactingVocabularySucceeds() { + Mentat mentat = new Mentat(); + TxReport schemaReport = this.transactCitiesSchema(mentat); + assertNotNull(schemaReport); + assertTrue(schemaReport.getTxId() > 0); + } + + @Test + public void transactingEntitiesSucceeds() { + Mentat mentat = new Mentat(); + this.transactCitiesSchema(mentat); + TxReport dataReport = this.transactSeattleData(mentat); + assertNotNull(dataReport); + assertTrue(dataReport.getTxId() > 0); + Long entid = dataReport.getEntidForTempId("a17592186045605"); + assertEquals(65733, entid.longValue()); + } + + @Test + public void runScalarSucceeds() throws InterruptedException { + Mentat mentat = openAndInitializeCitiesStore(); + String query = "[:find ?n . :in ?name :where [(fulltext $ :community/name ?name) [[?e ?n]]]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindString("?name", "Wallingford").runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals("KOMO Communities - Wallingford", value.asString()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void runCollSucceeds() throws InterruptedException { + Mentat mentat = openAndInitializeCitiesStore(); + String query = "[:find [?when ...] :where [_ :db/txInstant ?when] :order (asc ?when)]"; + final Expectation expectation = new Expectation(); + mentat.query(query).runColl(new CollResultHandler() { + @Override + public void handleList(CollResult list) { + assertNotNull(list); + for (int i = 0; i < 3; ++i) { + assertNotNull(list.asDate(i)); + } + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void runCollResultIteratorSucceeds() throws InterruptedException { + Mentat mentat = openAndInitializeCitiesStore(); + String query = "[:find [?when ...] :where [_ :db/txInstant ?when] :order (asc ?when)]"; + final Expectation expectation = new Expectation(); + mentat.query(query).runColl(new CollResultHandler() { + @Override + public void handleList(CollResult list) { + assertNotNull(list); + + for(TypedValue value: list) { + assertNotNull(value.asDate()); + } + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void runTupleSucceeds() throws InterruptedException { + Mentat mentat = openAndInitializeCitiesStore(); + String query = "[:find [?name ?cat]\n" + + " :where\n" + + " [?c :community/name ?name]\n" + + " [?c :community/type :community.type/website]\n" + + " [(fulltext $ :community/category \"food\") [[?c ?cat]]]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).runTuple(new TupleResultHandler() { + @Override + public void handleRow(TupleResult row) { + assertNotNull(row); + String name = row.asString(0); + String category = row.asString(1); + assert(name == "Community Harvest of Southwest Seattle"); + assert(category == "sustainable food"); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void runRelIteratorSucceeds() throws InterruptedException { + Mentat mentat = openAndInitializeCitiesStore(); + String query = "[:find ?name ?cat\n" + + " :where\n" + + " [?c :community/name ?name]\n" + + " [?c :community/type :community.type/website]\n" + + " [(fulltext $ :community/category \"food\") [[?c ?cat]]]]"; + + final LinkedHashMap expectedResults = new LinkedHashMap(); + expectedResults.put("InBallard", "food"); + expectedResults.put("Seattle Chinatown Guide", "food"); + expectedResults.put("Community Harvest of Southwest Seattle", "sustainable food"); + expectedResults.put("University District Food Bank", "food bank"); + final Expectation expectation = new Expectation(); + mentat.query(query).run(new RelResultHandler() { + @Override + public void handleRows(RelResult rows) { + assertNotNull(rows); + int index = 0; + for (TupleResult row: rows) { + String name = row.asString(0); + assertNotNull(name); + String category = row.asString(1); + assertNotNull(category); + String expectedCategory = expectedResults.get(name).toString(); + assertNotNull(expectedCategory); + assertEquals(expectedCategory, category); + ++index; + } + assertEquals(expectedResults.size(), index); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void runRelSucceeds() throws InterruptedException { + Mentat mentat = openAndInitializeCitiesStore(); + String query = "[:find ?name ?cat\n" + + " :where\n" + + " [?c :community/name ?name]\n" + + " [?c :community/type :community.type/website]\n" + + " [(fulltext $ :community/category \"food\") [[?c ?cat]]]]"; + + final LinkedHashMap expectedResults = new LinkedHashMap(); + expectedResults.put("InBallard", "food"); + expectedResults.put("Seattle Chinatown Guide", "food"); + expectedResults.put("Community Harvest of Southwest Seattle", "sustainable food"); + expectedResults.put("University District Food Bank", "food bank"); + final Expectation expectation = new Expectation(); + mentat.query(query).run(new RelResultHandler() { + @Override + public void handleRows(RelResult rows) { + assertNotNull(rows); + for (int i = 0; i < expectedResults.size(); ++i) { + TupleResult row = rows.rowAtIndex(i); + assertNotNull(row); + String name = row.asString(0); + assertNotNull(name); + String category = row.asString(1); + assertNotNull(category); + String expectedCategory = expectedResults.get(name).toString(); + assertNotNull(expectedCategory); + assertEquals(expectedCategory, category); + } + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void bindingLongValueSucceeds() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?e . :in ?long :where [?e :foo/long ?long]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindLong("?long", 25).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(aEntid, value.asEntid()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void bindingRefValueSucceeds() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + long stringEntid = mentat.entIdForAttribute(":foo/string"); + final Long bEntid = report.getEntidForTempId("b"); + String query = "[:find ?e . :in ?ref :where [?e :foo/ref ?ref]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindEntidReference("?ref", stringEntid).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(bEntid, value.asEntid()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void bindingRefKwValueSucceeds() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + String refKeyword = ":foo/string"; + final Long bEntid = report.getEntidForTempId("b"); + String query = "[:find ?e . :in ?ref :where [?e :foo/ref ?ref]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindKeywordReference("?ref", refKeyword).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(bEntid, value.asEntid()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void bindingKwValueSucceeds() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?e . :in ?kw :where [?e :foo/keyword ?kw]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindKeyword("?kw", ":foo/string").runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(aEntid, value.asEntid()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void bindingDateValueSucceeds() throws InterruptedException, ParseException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + + Date date = new Date(1523896758000L); + String query = "[:find [?e ?d] :in ?now :where [?e :foo/instant ?d] [(< ?d ?now)]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindDate("?now", date).runTuple(new TupleResultHandler() { + @Override + public void handleRow(TupleResult row) { + assertNotNull(row); + TypedValue value = row.get(0); + assertNotNull(value); + assertEquals(aEntid, value.asEntid()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void bindingStringValueSucceeds() throws InterruptedException { + Mentat mentat = this.openAndInitializeCitiesStore(); + String query = "[:find ?n . :in ?name :where [(fulltext $ :community/name ?name) [[?e ?n]]]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindString("?name", "Wallingford").runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals("KOMO Communities - Wallingford", value.asString()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void bindingUuidValueSucceeds() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?e . :in ?uuid :where [?e :foo/uuid ?uuid]]"; + UUID uuid = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"); + final Expectation expectation = new Expectation(); + mentat.query(query).bindUUID("?uuid", uuid).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(aEntid, value.asEntid()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void bindingBooleanValueSucceeds() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?e . :in ?bool :where [?e :foo/boolean ?bool]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindBoolean("?bool", true).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(aEntid, value.asEntid()); + expectation.fulfill(); + } + }); + + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void bindingDoubleValueSucceeds() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?e . :in ?double :where [?e :foo/double ?double]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindDouble("?double", 11.23).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(aEntid, value.asEntid()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void typedValueConvertsToLong() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?v . :in ?e :where [?e :foo/long ?v]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(25, value.asLong().longValue()); + assertEquals(25, value.asLong().longValue()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void typedValueConvertsToRef() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?e . :where [?e :foo/long 25]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(aEntid, value.asEntid()); + assertEquals(aEntid, value.asEntid()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void typedValueConvertsToKeyword() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?v . :in ?e :where [?e :foo/keyword ?v]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(":foo/string", value.asKeyword()); + assertEquals(":foo/string", value.asKeyword()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void typedValueConvertsToBoolean() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?v . :in ?e :where [?e :foo/boolean ?v]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(true, value.asBoolean()); + assertEquals(true, value.asBoolean()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void typedValueConvertsToDouble() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?v . :in ?e :where [?e :foo/double ?v]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(new Double(11.23), value.asDouble()); + assertEquals(new Double(11.23), value.asDouble()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void typedValueConvertsToDate() throws InterruptedException, ParseException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?v . :in ?e :where [?e :foo/instant ?v]]"; + final Expectation expectation = new Expectation(); + DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.ENGLISH); + format.parse("2017-01-01T11:00:00+00:00"); + final Calendar expectedDate = format.getCalendar(); + mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(expectedDate.getTime(), value.asDate()); + assertEquals(expectedDate.getTime(), value.asDate()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void typedValueConvertsToString() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?v . :in ?e :where [?e :foo/string ?v]]"; + final Expectation expectation = new Expectation(); + mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals("The higher we soar the smaller we appear to those who cannot fly.", value.asString()); + assertEquals("The higher we soar the smaller we appear to those who cannot fly.", value.asString()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void typedValueConvertsToUUID() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + String query = "[:find ?v . :in ?e :where [?e :foo/uuid ?v]]"; + final UUID expectedUUID = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"); + final Expectation expectation = new Expectation(); + mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() { + @Override + public void handleValue(TypedValue value) { + assertNotNull(value); + assertEquals(expectedUUID, value.asUUID()); + assertEquals(expectedUUID, value.asUUID()); + expectation.fulfill(); + } + }); + synchronized (expectation) { + expectation.wait(1000); + } + assertTrue(expectation.isFulfilled); + } + + @Test + public void valueForAttributeOfEntitySucceeds() throws InterruptedException { + Mentat mentat = new Mentat(); + TxReport report = this.populateWithTypesSchema(mentat); + final Long aEntid = report.getEntidForTempId("a"); + TypedValue value = mentat.valueForAttributeOfEntity(":foo/long", aEntid); + assertNotNull(value); + assertEquals(25, value.asLong().longValue()); + } + + @Test + public void entidForAttributeSucceeds() { + Mentat mentat = new Mentat(); + this.populateWithTypesSchema(mentat); + long entid = mentat.entIdForAttribute(":foo/long"); + assertEquals(65540, entid); + } +} diff --git a/sdks/android/Mentat/library/src/main/AndroidManifest.xml b/sdks/android/Mentat/library/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a6c591ea --- /dev/null +++ b/sdks/android/Mentat/library/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/AttributeList.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/AttributeList.java new file mode 100644 index 00000000..76e7e593 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/AttributeList.java @@ -0,0 +1,48 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Structure; +import com.sun.jna.ptr.IntByReference; + +import java.io.Closeable; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a C struct of a list of Strings containing attributes in the format + * `:namespace/name`. + */ +public class AttributeList extends Structure implements Closeable { + public static class ByReference extends AttributeList implements Structure.ByReference { + } + + public static class ByValue extends AttributeList implements Structure.ByValue { + } + + public IntByReference attributes; + public int numberOfItems; + // Used by the Swift counterpart, JNA does this for us automagically. + // But we still need it here so that the number of fields and their order is correct + public int len; + + @Override + protected List getFieldOrder() { + return Arrays.asList("attributes", "numberOfItems", "len"); + } + + @Override + public void close() { + if (this.getPointer() != null) { + JNA.INSTANCE.destroy(this.getPointer()); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/ColResultIterator.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/ColResultIterator.java new file mode 100644 index 00000000..d5ef1f59 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/ColResultIterator.java @@ -0,0 +1,54 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; + +import java.util.Iterator; + +/** + * Iterator for a {@link CollResult} + */ +public class ColResultIterator extends RustObject implements Iterator { + + Pointer nextPointer; + + ColResultIterator(Pointer iterator) { + this.rawPointer = iterator; + } + + private Pointer getNextPointer() { + return JNA.INSTANCE.typed_value_list_iter_next(this.rawPointer); + } + + @Override + public boolean hasNext() { + this.nextPointer = getNextPointer(); + return this.nextPointer != null; + } + + @Override + public TypedValue next() { + Pointer next = this.nextPointer == null ? getNextPointer() : this.nextPointer; + if (next == null) { + return null; + } + + return new TypedValue(next); + } + + @Override + public void close() { + if (this.rawPointer != null) { + JNA.INSTANCE.typed_value_list_iter_destroy(this.rawPointer); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/CollResult.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/CollResult.java new file mode 100644 index 00000000..384eb6f3 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/CollResult.java @@ -0,0 +1,60 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; + +import java.util.Date; +import java.util.UUID; + +/** + * Wraps a `Coll` result from a Mentat query. + * A `Coll` result is a list of rows of single values of type {@link TypedValue}. + * Values for individual rows can be fetched as {@link TypedValue} or converted into a requested type. + *

+ * Row values can be fetched as one of the following types: + *

    + *
  • {@link TypedValue}
  • + *
  • long
  • + *
  • Entid (as long)
  • + *
  • Keyword (as String)
  • + *
  • boolean
  • + *
  • double
  • + *
  • {@link Date}
  • + *
  • {@link String}
  • + *
  • {@link UUID}
  • + *
+ *

+ * To iterate over the result set use standard iteration flows. + */ +public class CollResult extends TupleResult implements Iterable { + + public CollResult(Pointer pointer) { + super(pointer); + } + + @Override + public void close() { + if (this.rawPointer != null) { + JNA.INSTANCE.destroy(this.rawPointer); + } + } + + @Override + public ColResultIterator iterator() { + Pointer iterPointer = JNA.INSTANCE.typed_value_list_into_iter(this.rawPointer); + this.rawPointer = null; + if (iterPointer == null) { + return null; + } + return new ColResultIterator(iterPointer); + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/CollResultHandler.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/CollResultHandler.java new file mode 100644 index 00000000..9f6842e9 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/CollResultHandler.java @@ -0,0 +1,18 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +/** + * Interface defining the structure of a callback from a query returning a {@link CollResult}. + */ +public interface CollResultHandler { + void handleList(CollResult list); +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/JNA.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/JNA.java new file mode 100644 index 00000000..202c23ab --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/JNA.java @@ -0,0 +1,101 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.NativeLibrary; +import com.sun.jna.Pointer; + +/** + * JNA interface for FFI to Mentat's Rust library + * Each function definition here link directly to a function in Mentat's FFI crate. + * Signatures must match for the linking to work correctly. + */ +public interface JNA extends Library { + String JNA_LIBRARY_NAME = "mentat_ffi"; + NativeLibrary JNA_NATIVE_LIB = NativeLibrary.getInstance(JNA_LIBRARY_NAME); + + JNA INSTANCE = (JNA) Native.loadLibrary(JNA_LIBRARY_NAME, JNA.class); + + Pointer store_open(String dbPath); + + void destroy(Pointer obj); + void query_builder_destroy(Pointer obj); + void store_destroy(Pointer obj); + void typed_value_destroy(Pointer obj); + void typed_value_list_destroy(Pointer obj); + void typed_value_list_iter_destroy(Pointer obj); + void typed_value_result_set_destroy(Pointer obj); + void typed_value_result_set_iter_destroy(Pointer obj); + void tx_report_destroy(Pointer obj); + + // transact + RustResult store_transact(Pointer store, String transaction); + Pointer tx_report_entity_for_temp_id(Pointer report, String tempid); + long tx_report_get_entid(Pointer report); + long tx_report_get_tx_instant(Pointer report); + + // sync + RustResult store_sync(Pointer store, String userUuid, String serverUri); + + // observers + void store_register_observer(Pointer store, String key, Pointer attributes, int len, TxObserverCallback callback); + void store_unregister_observer(Pointer store, String key); + long store_entid_for_attribute(Pointer store, String attr); + + // Query Building + Pointer store_query(Pointer store, String query); + RustResult store_value_for_attribute(Pointer store, long entid, String attribute); + void query_builder_bind_long(Pointer query, String var, long value); + void query_builder_bind_ref(Pointer query, String var, long value); + void query_builder_bind_ref_kw(Pointer query, String var, String value); + void query_builder_bind_kw(Pointer query, String var, String value); + void query_builder_bind_boolean(Pointer query, String var, int value); + void query_builder_bind_double(Pointer query, String var, double value); + void query_builder_bind_timestamp(Pointer query, String var, long value); + void query_builder_bind_string(Pointer query, String var, String value); + void query_builder_bind_uuid(Pointer query, String var, Pointer value); + + // Query Execution + RustResult query_builder_execute(Pointer query); + RustResult query_builder_execute_scalar(Pointer query); + RustResult query_builder_execute_coll(Pointer query); + RustResult query_builder_execute_tuple(Pointer query); + + // Query Result Processing + long typed_value_into_long(Pointer value); + long typed_value_into_entid(Pointer value); + String typed_value_into_kw(Pointer value); + String typed_value_into_string(Pointer value); + Pointer typed_value_into_uuid(Pointer value); + int typed_value_into_boolean(Pointer value); + double typed_value_into_double(Pointer value); + long typed_value_into_timestamp(Pointer value); + Pointer typed_value_value_type(Pointer value); + + Pointer row_at_index(Pointer rows, int index); + Pointer typed_value_result_set_into_iter(Pointer rows); + Pointer typed_value_result_set_iter_next(Pointer iter); + + Pointer typed_value_list_into_iter(Pointer rows); + Pointer typed_value_list_iter_next(Pointer iter); + + Pointer value_at_index(Pointer rows, int index); + long value_at_index_into_long(Pointer rows, int index); + long value_at_index_into_entid(Pointer rows, int index); + String value_at_index_into_kw(Pointer rows, int index); + String value_at_index_into_string(Pointer rows, int index); + Pointer value_at_index_into_uuid(Pointer rows, int index); + long value_at_index_into_boolean(Pointer rows, int index); + double value_at_index_into_double(Pointer rows, int index); + long value_at_index_into_timestamp(Pointer rows, int index); +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/Mentat.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/Mentat.java new file mode 100644 index 00000000..087647af --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/Mentat.java @@ -0,0 +1,147 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import android.util.Log; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; + +/** + * The primary class for accessing Mentat's API.
+ * This class provides all of the basic API that can be found in Mentat's Store struct.
+ * The raw pointer it holds is a pointer to a Store. + */ +public class Mentat extends RustObject { + + static { + System.loadLibrary("mentat_ffi"); + } + + /** + * Open a connection to a Store in a given location.
+ * If the store does not already exist, one will be created. + * @param dbPath The URI as a String of the store to open. + */ + public Mentat(String dbPath) { + this.rawPointer = JNA.INSTANCE.store_open(dbPath); + } + + /** + * Open a connection to an in-memory Store. + */ + public Mentat() { + this.rawPointer = JNA.INSTANCE.store_open(""); + } + + /** + * Create a new Mentat with the provided pointer to a Mentat Store + * @param rawPointer A pointer to a Mentat Store. + */ + public Mentat(Pointer rawPointer) { this.rawPointer = rawPointer; } + + /** + * Simple transact of an EDN string. + * TODO: Throw an exception if the transact fails + * @param transaction The string, as EDN, to be transacted. + * @return The {@link TxReport} of the completed transaction + */ + public TxReport transact(String transaction) { + RustResult result = JNA.INSTANCE.store_transact(this.rawPointer, transaction); + if (result.isFailure()) { + Log.e("Mentat", result.err); + return null; + } + + if (result.isSuccess()) { + return new TxReport(result.ok); + } else { + return null; + } + } + + /** + * Get the the `Entid` of the attribute + * @param attribute The string represeting the attribute whose `Entid` we are after. The string is represented as `:namespace/name`. + * @return The `Entid` associated with the attribute. + */ + public long entIdForAttribute(String attribute) { + return JNA.INSTANCE.store_entid_for_attribute(this.rawPointer, attribute); + } + + /** + * Start a query. + * @param query The string represeting the the query to be executed. + * @return The {@link Query} representing the query that can be executed. + */ + public Query query(String query) { + return new Query(JNA.INSTANCE.store_query(this.rawPointer, query)); + } + + /** + * Retrieve a single value of an attribute for an Entity + * TODO: Throw an exception if there is no the result contains an error. + * @param attribute The string the attribute whose value is to be returned. The string is represented as `:namespace/name`. + * @param entid The `Entid` of the entity we want the value from. + * @return The {@link TypedValue} containing the value of the attribute for the entity. + */ + public TypedValue valueForAttributeOfEntity(String attribute, long entid) { + RustResult result = JNA.INSTANCE.store_value_for_attribute(this.rawPointer, entid, attribute); + + if (result.isSuccess()) { + return new TypedValue(result.ok); + } + + if (result.isFailure()) { + Log.e("Mentat", result.err); + } + + return null; + } + + /** + * Register an callback and a set of attributes to observer for transaction observation. + * The callback function is called when a transaction occurs in the `Store` that this `Mentat` + * is connected to that affects the attributes that an observer has registered for. + * @param key `String` representing an identifier for the observer. + * @param attributes An array of Strings representing the attributes that the observer wishes + * to be notified about if they are referenced in a transaction. + * @param callback the function to call when an observer notice is fired. + */ + public void registerObserver(String key, String[] attributes, TxObserverCallback callback) { + // turn string array into int array + long[] attrEntids = new long[attributes.length]; + for(int i = 0; i < attributes.length; i++) { + attrEntids[i] = JNA.INSTANCE.store_entid_for_attribute(this.rawPointer, attributes[i]); + } + final Pointer entidsNativeArray = new Memory(8 * attrEntids.length); + entidsNativeArray.write(0, attrEntids, 0, attrEntids.length); + JNA.INSTANCE.store_register_observer(rawPointer, key, entidsNativeArray, attrEntids.length, callback); + } + + /** + * Unregister the observer that was registered with the provided key such that it will no longer be called + * if a transaction occurs that affects the attributes that the observer was registered to observe. + *

+ * The observer will need to re-register if it wants to start observing again. + * @param key String representing an identifier for the observer. + */ + public void unregisterObserver(String key) { + JNA.INSTANCE.store_unregister_observer(rawPointer, key); + } + + @Override + public void close() { + if (this.rawPointer != null) { + JNA.INSTANCE.store_destroy(this.rawPointer); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/Query.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/Query.java new file mode 100644 index 00000000..8a4cb0f2 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/Query.java @@ -0,0 +1,330 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import android.util.Log; + +import com.sun.jna.Memory; +import com.sun.jna.Pointer; + +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.UUID; + +/** + * This class allows you to construct a query, bind values to variables and run those queries against a mentat DB. + *

+ * This class cannot be created directly, but must be created through `Mentat.query(String:)`. + *

+ * The types of values you can bind are: + *

    + *
  • {@link TypedValue}
  • + *
  • long
  • + *
  • Entid (as long)
  • + *
  • Keyword (as String)
  • + *
  • boolean
  • + *
  • double
  • + *
  • {@link Date}
  • + *
  • {@link String}
  • + *
  • {@link UUID}
  • + *
+ *

+ *

+ * Each bound variable must have a corresponding value in the query string used to create this query. + *

+ *

{@code
+ * String query = "[:find ?name ?cat\n" +
+ *          "        :in ?type\n" +
+ *          "        :where\n" +
+ *          "        [?c :community/name ?name]\n" +
+ *          "        [?c :community/type ?type]\n" +
+ *          "        [?c :community/category ?cat]]";
+ * mentat.query(query).bindKeywordReference("?type", ":community.type/website").run(new RelResultHandler() {
+ *      @Override
+ *      public void handleRows(RelResult rows) {
+ *          ...
+ *      }
+ * });
+ *}
+ *

+ * Queries can be run and the results returned in a number of different formats. Individual result values are returned as `TypedValues` and + * the format differences relate to the number and structure of those values. The result format is related to the format provided in the query string. + *

+ * - `Rel` - This is the default `run` function and returns a list of rows of values. Queries that wish to have `Rel` results should format their query strings: + * + *

{@code
+ * String query = "[: find ?a ?b ?c\n" +
+ *          "        : where ... ]";
+ * mentat.query(query).run(new RelResultHandler() {
+ *      @Override
+ *      public void handleRows(RelResult rows) {
+ *          ...
+ *      }
+ * });
+ *}
+ *

+ * - `Scalar` - This returns a single value as a result. This can be optional, as the value may not be present. Queries that wish to have `Scalar` results should format their query strings: + * + *

{@code
+ * String query = "[: find ?a .\n" +
+ *          "        : where ... ]";
+ * mentat.query(query).runScalar(new ScalarResultHandler() {
+ *      @Override
+ *      public void handleValue(TypedValue value) {
+ *          ...
+ *      }
+ * });
+ *}
+ *

+ * - `Coll` - This returns a list of single values as a result. Queries that wish to have `Coll` results should format their query strings: + *

{@code
+ * String query = "[: find [?a ...]\n" +
+ *          "        : where ... ]";
+ * mentat.query(query).runColl(new ScalarResultHandler() {
+ *      @Override
+ *      public void handleList(CollResult list) {
+ *          ...
+ *      }
+ * });
+ *}
+ *

+ * - `Tuple` - This returns a single row of values. Queries that wish to have `Tuple` results should format their query strings: + *

{@code
+ * String query = "[: find [?a ?b ?c]\n" +
+ *          "        : where ... ]";
+ * mentat.query(query).runTuple(new TupleResultHandler() {
+ *      @Override
+ *      public void handleRow(TupleResult row) {
+ *          ...
+ *      }
+ * });
+ *}
+ */ +public class Query extends RustObject { + + public Query(Pointer pointer) { + this.rawPointer = pointer; + } + + /** + * Binds a long value to the provided variable name. + * TODO: Throw an exception if the query raw pointer has been consumed. + * @param varName The name of the variable in the format `?name`. + * @param value The value to be bound + * @return This {@link Query} such that further function can be called. + */ + Query bindLong(String varName, long value) { + this.validate(); + JNA.INSTANCE.query_builder_bind_long(this.rawPointer, varName, value); + return this; + } + + /** + * Binds a Entid value to the provided variable name. + * TODO: Throw an exception if the query raw pointer has been consumed. + * @param varName The name of the variable in the format `?name`. + * @param value The value to be bound + * @return This {@link Query} such that further function can be called. + */ + Query bindEntidReference(String varName, long value) { + this.validate(); + JNA.INSTANCE.query_builder_bind_ref(this.rawPointer, varName, value); + return this; + } + + /** + * Binds a String keyword value to the provided variable name. + * TODO: Throw an exception if the query raw pointer has been consumed. + * @param varName The name of the variable in the format `?name`. + * @param value The value to be bound + * @return This {@link Query} such that further function can be called. + */ + Query bindKeywordReference(String varName, String value) { + this.validate(); + JNA.INSTANCE.query_builder_bind_ref_kw(this.rawPointer, varName, value); + return this; + } + + /** + * Binds a keyword value to the provided variable name. + * TODO: Throw an exception if the query raw pointer has been consumed. + * @param varName The name of the variable in the format `?name`. + * @param value The value to be bound + * @return This {@link Query} such that further function can be called. + */ + Query bindKeyword(String varName, String value) { + this.validate(); + JNA.INSTANCE.query_builder_bind_kw(this.rawPointer, varName, value); + return this; + } + + /** + * Binds a boolean value to the provided variable name. + * TODO: Throw an exception if the query raw pointer has been consumed. + * @param varName The name of the variable in the format `?name`. + * @param value The value to be bound + * @return This {@link Query} such that further function can be called. + */ + Query bindBoolean(String varName, boolean value) { + this.validate(); + JNA.INSTANCE.query_builder_bind_boolean(this.rawPointer, varName, value ? 1 : 0); + return this; + } + + /** + * Binds a double value to the provided variable name. + * TODO: Throw an exception if the query raw pointer has been consumed. + * @param varName The name of the variable in the format `?name`. + * @param value The value to be bound + * @return This {@link Query} such that further function can be called. + */ + Query bindDouble(String varName, double value) { + this.validate(); + JNA.INSTANCE.query_builder_bind_double(this.rawPointer, varName, value); + return this; + } + + /** + * Binds a {@link Date} value to the provided variable name. + * TODO: Throw an exception if the query raw pointer has been consumed. + * @param varName The name of the variable in the format `?name`. + * @param value The value to be bound + * @return This {@link Query} such that further function can be called. + */ + Query bindDate(String varName, Date value) { + this.validate(); + long timestamp = value.getTime() * 1000; + JNA.INSTANCE.query_builder_bind_timestamp(this.rawPointer, varName, timestamp); + return this; + } + + /** + * Binds a {@link String} value to the provided variable name. + * TODO: Throw an exception if the query raw pointer has been consumed. + * @param varName The name of the variable in the format `?name`. + * @param value The value to be bound + * @return This {@link Query} such that further function can be called. + */ + Query bindString(String varName, String value) { + this.validate(); + JNA.INSTANCE.query_builder_bind_string(this.rawPointer, varName, value); + return this; + } + + /** + * Binds a {@link UUID} value to the provided variable name. + * TODO: Throw an exception if the query raw pointer has been consumed. + * @param varName The name of the variable in the format `?name`. + * @param value The value to be bound + * @return This {@link Query} such that further function can be called. + */ + Query bindUUID(String varName, UUID value) { + this.validate(); + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(value.getMostSignificantBits()); + bb.putLong(value.getLeastSignificantBits()); + byte[] bytes = bb.array(); + final Pointer bytesNativeArray = new Memory(bytes.length); + bytesNativeArray.write(0, bytes, 0, bytes.length); + JNA.INSTANCE.query_builder_bind_uuid(this.rawPointer, varName, bytesNativeArray); + return this; + } + + /** + * Execute the query with the values bound associated with this {@link Query} and call the provided + * callback function with the results as a list of rows of {@link TypedValue}s. + * TODO: Throw an exception if the query raw pointer has been consumed or the query fails to execute + * @param handler the handler to call with the results of this query + */ + void run(final RelResultHandler handler) { + this.validate(); + RustResult result = JNA.INSTANCE.query_builder_execute(rawPointer); + rawPointer = null; + + if (result.isFailure()) { + Log.e("Query", result.err); + return; + } + handler.handleRows(new RelResult(result.ok)); + } + + /** + * Execute the query with the values bound associated with this {@link Query} and call the provided + * callback function with the results with the result as a single {@link TypedValue}. + * TODO: Throw an exception if the query raw pointer has been consumed or the query fails to execute + * @param handler the handler to call with the results of this query + */ + void runScalar(final ScalarResultHandler handler) { + this.validate(); + RustResult result = JNA.INSTANCE.query_builder_execute_scalar(rawPointer); + rawPointer = null; + + if (result.isFailure()) { + Log.e("Query", result.err); + return; + } + + if (result.isSuccess()) { + handler.handleValue(new TypedValue(result.ok)); + } else { + handler.handleValue(null); + } + } + + /** + * Execute the query with the values bound associated with this {@link Query} and call the provided + * callback function with the results with the result as a list of single {@link TypedValue}s. + * TODO: Throw an exception if the query raw pointer has been consumed or the query fails to execute + * @param handler the handler to call with the results of this query + */ + void runColl(final CollResultHandler handler) { + this.validate(); + RustResult result = JNA.INSTANCE.query_builder_execute_coll(rawPointer); + rawPointer = null; + + if (result.isFailure()) { + Log.e("Query", result.err); + return; + } + handler.handleList(new CollResult(result.ok)); + } + + /** + * Execute the query with the values bound associated with this {@link Query} and call the provided + * callback function with the results with the result as a list of single {@link TypedValue}s. + * TODO: Throw an exception if the query raw pointer has been consumed or the query fails to execute + * @param handler the handler to call with the results of this query + */ + void runTuple(final TupleResultHandler handler) { + this.validate(); + RustResult result = JNA.INSTANCE.query_builder_execute_tuple(rawPointer); + rawPointer = null; + + if (result.isFailure()) { + Log.e("Query", result.err); + return; + } + + if (result.isSuccess()) { + handler.handleRow(new TupleResult(result.ok)); + } else { + handler.handleRow(null); + } + } + + @Override + public void close() { + if (this.rawPointer == null) { + return; + } + JNA.INSTANCE.query_builder_destroy(this.rawPointer); + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RelResult.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RelResult.java new file mode 100644 index 00000000..0baab4a4 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RelResult.java @@ -0,0 +1,86 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; + +/** + * Wraps a `Rel` result from a Mentat query. + * A `Rel` result is a list of rows of `TypedValues`. + * Individual rows can be fetched or the set can be iterated. + *

+ * To fetch individual rows from a `RelResult` use `row(Int32)`. + *

+ *
{@code
+ * mentat.query(query).run(new RelResultHandler() {
+ *      @Override
+ *      public void handleRows(RelResult rows) {
+ *          TupleResult row1 = rows.rowAtIndex(0);
+ *          TupleResult row2 = rows.rowAtIndex(1);
+ *          ...
+ *      }
+ * });
+ *}
+ *

+ * To iterate over the result set use standard iteration flows. + *
{@code
+ * mentat.query(query).run(new RelResultHandler() {
+ *      @Override
+ *      public void handleRows(RelResult rows) {
+ *          for (TupleResult row: rows) {
+ *                ...
+ *          }
+ *      }
+ * });
+ *}
+ *

+ * Note that iteration is consuming and can only be done once. + */ +public class RelResult extends RustObject implements Iterable { + + public RelResult(Pointer pointer) { + this.rawPointer = pointer; + } + + /** + * Fetch the row at the requested index. + * TODO: Throw an exception if the result set has already been iterated. + * @param index the index of the row to be fetched + * @return The row at the requested index as a `TupleResult`, if present, or nil if there is no row at that index. + */ + public TupleResult rowAtIndex(int index) { + this.validate(); + Pointer pointer = JNA.INSTANCE.row_at_index(this.rawPointer, index); + if (pointer == null) { + return null; + } + + return new TupleResult(pointer); + } + + @Override + public RelResultIterator iterator() { + this.validate(); + Pointer iterPointer = JNA.INSTANCE.typed_value_result_set_into_iter(this.rawPointer); + this.rawPointer = null; + if (iterPointer == null) { + return null; + } + return new RelResultIterator(iterPointer); + } + + @Override + public void close() { + if (this.rawPointer != null) { + JNA.INSTANCE.typed_value_result_set_destroy(this.rawPointer); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RelResultHandler.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RelResultHandler.java new file mode 100644 index 00000000..f9a4e39c --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RelResultHandler.java @@ -0,0 +1,18 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +/** + * Interface defining the structure of a callback from a query returning a {@link RelResult}. + */ +public interface RelResultHandler { + void handleRows(RelResult rows); +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RelResultIterator.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RelResultIterator.java new file mode 100644 index 00000000..28577a3f --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RelResultIterator.java @@ -0,0 +1,54 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; + +import java.util.Iterator; +/** + * Iterator for a {@link RelResult} + */ +public class RelResultIterator extends RustObject implements Iterator { + + Pointer nextPointer; + + RelResultIterator(Pointer iterator) { + this.rawPointer = iterator; + } + + private Pointer getNextPointer() { + return JNA.INSTANCE.typed_value_result_set_iter_next(this.rawPointer); + } + + @Override + public boolean hasNext() { + this.nextPointer = getNextPointer(); + return this.nextPointer != null; + } + + @Override + public TupleResult next() { + Pointer next = this.nextPointer == null ? getNextPointer() : this.nextPointer; + if (next == null) { + return null; + } + + return new TupleResult(next); + } + + + @Override + public void close() { + if (this.rawPointer != null) { + JNA.INSTANCE.typed_value_result_set_iter_destroy(this.rawPointer); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RustObject.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RustObject.java new file mode 100644 index 00000000..35f7b8fb --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RustObject.java @@ -0,0 +1,34 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; + +import java.io.Closeable; + +/** + * Base class that wraps an non-optional {@link Pointer} representing a pointer to a Rust object. + * This class implements {@link Closeable} but does not provide an implementation, forcing all + * subclasses to implement it. This ensures that all classes that inherit from RustObject + * will have their {@link Pointer} destroyed when the Java wrapper is destroyed. + */ +abstract class RustObject implements Closeable { + Pointer rawPointer; + + /** + * Throws a {@link NullPointerException} if the underlying {@link Pointer} is null. + */ + void validate() { + if (this.rawPointer == null) { + throw new NullPointerException(this.getClass() + " consumed"); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RustResult.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RustResult.java new file mode 100644 index 00000000..dc19ddc4 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/RustResult.java @@ -0,0 +1,62 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a C struct containing a {@link Pointer}s and String that map to a Rust Result. + * A RustResult will contain either an ok value, OR an err value, or neither - never both. + */ +public class RustResult extends Structure implements Closeable { + public static class ByReference extends RustResult implements Structure.ByReference { + } + + public static class ByValue extends RustResult implements Structure.ByValue { + } + + public Pointer ok; + public String err; + + /** + * Is there an value attached to this result + * @return true if a value is present, false otherwise + */ + public boolean isSuccess() { + return this.ok != null; + } + + /** + * Is there an error attached to this result? + * @return true is an error is present, false otherwise + */ + public boolean isFailure() { + return this.err != null; + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("ok", "err"); + } + + @Override + public void close() throws IOException { + if (this.getPointer() != null) { + JNA.INSTANCE.destroy(this.getPointer()); + } + } +} \ No newline at end of file diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/ScalarResultHandler.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/ScalarResultHandler.java new file mode 100644 index 00000000..ad4b2dd3 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/ScalarResultHandler.java @@ -0,0 +1,18 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +/** + * Interface defining the structure of a callback from a query returning a single {@link TypedValue}. + */ +public interface ScalarResultHandler { + void handleValue(TypedValue value); +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TupleResult.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TupleResult.java new file mode 100644 index 00000000..a9701682 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TupleResult.java @@ -0,0 +1,168 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; + +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.UUID; + +/** + * Wraps a `Tuple` result from a Mentat query. + * A `Tuple` result is a single row {@link TypedValue}s. + * Values for individual fields can be fetched as {@link TypedValue} or converted into a requested type. + *

+ * Field values can be fetched as one of the following types: + *

    + *
  • {@link TypedValue}
  • + *
  • long
  • + *
  • Entid (as long)
  • + *
  • Keyword (as String)
  • + *
  • boolean
  • + *
  • double
  • + *
  • {@link Date}
  • + *
  • {@link String}
  • + *
  • {@link UUID}
  • + *
+ *

+ * To iterate over the result set use standard iteration flows. + */ +public class TupleResult extends RustObject { + + public TupleResult(Pointer pointer) { + this.rawPointer = pointer; + } + + /** + * Return the {@link TypedValue} at the specified index. + * If the index is greater than the number of values then this function will crash. + * @param index The index of the value to fetch. + * @return The {@link TypedValue} at that index. + */ + public TypedValue get(Integer index) { + this.validate(); + Pointer pointer = JNA.INSTANCE.value_at_index(this.rawPointer, index); + if (pointer == null) { + return null; + } + return new TypedValue(pointer); + } + + /** + * Return the {@link Long} at the specified index. + * If the index is greater than the number of values then this function will crash. + * If the value type if the {@link TypedValue} at this index is not `Long` then this function will crash. + * @param index The index of the value to fetch. + * @return The {@link Long} at that index. + */ + public Long asLong(Integer index) { + this.validate(); + return JNA.INSTANCE.value_at_index_into_long(this.rawPointer, index); + } + + /** + * Return the Entid at the specified index. + * If the index is greater than the number of values then this function will crash. + * If the value type if the {@link TypedValue} at this index is not `Ref` then this function will crash. + * @param index The index of the value to fetch. + * @return The Entid at that index. + */ + public Long asEntid(Integer index) { + this.validate(); + return JNA.INSTANCE.value_at_index_into_entid(this.rawPointer, index); + } + + /** + * Return the keyword {@link String} at the specified index. + * If the index is greater than the number of values then this function will crash. + * If the value type if the {@link TypedValue} at this index is not `Keyword` then this function will crash. + * @param index The index of the value to fetch. + * @return The keyword at that index. + */ + public String asKeyword(Integer index) { + this.validate(); + return JNA.INSTANCE.value_at_index_into_kw(this.rawPointer, index); + } + + /** + * Return the {@link Boolean} at the specified index. + * If the index is greater than the number of values then this function will crash. + * If the value type if the {@link TypedValue} at this index is not `Boolean` then this function will crash. + * @param index The index of the value to fetch. + * @return The {@link Boolean} at that index. + */ + public Boolean asBool(Integer index) { + this.validate(); + return JNA.INSTANCE.value_at_index_into_boolean(this.rawPointer, index) == 0 ? false : true; + } + + /** + * Return the {@link Double} at the specified index. + * If the index is greater than the number of values then this function will crash. + * If the value type if the {@link TypedValue} at this index is not `Double` then this function will crash. + * @param index The index of the value to fetch. + * @return The {@link Double} at that index. + */ + public Double asDouble(Integer index) { + this.validate(); + return JNA.INSTANCE.value_at_index_into_double(this.rawPointer, index); + } + + /** + * Return the {@link Date} at the specified index. + * If the index is greater than the number of values then this function will crash. + * If the value type if the {@link TypedValue} at this index is not `Instant` then this function will crash. + * @param index The index of the value to fetch. + * @return The {@link Date} at that index. + */ + public Date asDate(Integer index) { + this.validate(); + return new Date(JNA.INSTANCE.value_at_index_into_timestamp(this.rawPointer, index)); + } + + /** + * Return the {@link String} at the specified index. + * If the index is greater than the number of values then this function will crash. + * If the value type if the {@link TypedValue} at this index is not `String` then this function will crash. + * @param index The index of the value to fetch. + * @return The {@link String} at that index. + */ + public String asString(Integer index) { + this.validate(); + return JNA.INSTANCE.value_at_index_into_string(this.rawPointer, index); + } + + /** + * Return the {@link UUID} at the specified index. + * If the index is greater than the number of values then this function will crash. + * If the value type if the {@link TypedValue} at this index is not `Uuid` then this function will crash. + * @param index The index of the value to fetch. + * @return The {@link UUID} at that index. + */ + public UUID asUUID(Integer index) { + this.validate(); + Pointer uuidPtr = JNA.INSTANCE.value_at_index_into_uuid(this.rawPointer, index); + byte[] bytes = uuidPtr.getByteArray(0, 16); + ByteBuffer bb = ByteBuffer.wrap(bytes); + long high = bb.getLong(); + long low = bb.getLong(); + + return new UUID(high, low); + } + + @Override + public void close() { + if (this.rawPointer != null) { + JNA.INSTANCE.typed_value_list_destroy(this.rawPointer); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TupleResultHandler.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TupleResultHandler.java new file mode 100644 index 00000000..4eb7f766 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TupleResultHandler.java @@ -0,0 +1,18 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +/** + * Interface defining the structure of a callback from a query returning a {@link TupleResult}. + */ +public interface TupleResultHandler { + void handleRow(TupleResult row); +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxChange.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxChange.java new file mode 100644 index 00000000..d5b6a830 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxChange.java @@ -0,0 +1,64 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +import java.io.Closeable; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a C struct representing changes that occured during a transaction. + * These changes contain the transaction identifier, a {@link Pointer} to a list of affected attribute + * Entids and the number of items that the list contains. + */ +public class TxChange extends Structure implements Closeable { + public static class ByReference extends TxChange implements Structure.ByReference { + } + + public static class ByValue extends TxChange implements Structure.ByValue { + } + + public int txid; + public Pointer changes; + public int numberOfItems; + // Used by the Swift counterpart, JNA does this for us automagically. + // But we still need it here so that the number of fields and their order is correct + public int changes_len; + + /** + * Get the affected attributes for this transaction + * @return The changes as a list of Entids of affected attributes + */ + public List getChanges() { + final long[] array = (long[]) changes.getLongArray(0, numberOfItems); + Long[] longArray = new Long[numberOfItems]; + int idx = 0; + for (long change: array) { + longArray[idx++] = change; + } + return Arrays.asList(longArray); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("txid", "changes", "changes_len", "numberOfItems"); + } + + @Override + public void close() { + if (this.getPointer() != null) { + JNA.INSTANCE.destroy(this.getPointer()); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxChangeList.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxChangeList.java new file mode 100644 index 00000000..f27d163a --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxChangeList.java @@ -0,0 +1,56 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Structure; + +import java.io.Closeable; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a C struct containing a list of {@link TxChange}s that occured. + */ +public class TxChangeList extends Structure implements Closeable { + public static class ByReference extends TxChangeList implements Structure.ByReference { + } + + public static class ByValue extends TxChangeList implements Structure.ByValue { + } + + public TxChange.ByReference reports; + public int numberOfItems; + // Used by the Swift counterpart, JNA does this for us automagically. + // // But we still need it here so that the number of fields and their order is correct + public int len; + + /** + * Get the changes that occured + * @return a list of {@link TxChange}s for the notification + */ + public List getReports() { + final TxChange[] array = (TxChange[]) reports.toArray(numberOfItems); + return Arrays.asList(array); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList("reports", "numberOfItems", "len"); + } + + @Override + public void close() { + final TxChange[] nativeReports = (TxChange[]) reports.toArray(numberOfItems); + for (TxChange nativeReport : nativeReports) { + nativeReport.close(); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxObserverCallback.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxObserverCallback.java new file mode 100644 index 00000000..9b474954 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxObserverCallback.java @@ -0,0 +1,20 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Callback; + +/** + * Protocol to be implemented by any object that wishes to register for transaction observation + */ +public interface TxObserverCallback extends Callback { + void transactionObserverCalled(String key, TxChangeList.ByReference reports); +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxReport.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxReport.java new file mode 100644 index 00000000..abeaf530 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TxReport.java @@ -0,0 +1,89 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; + +import java.util.Date; + +/** + * This class wraps a raw pointer than points to a Rust `TxReport` object. + *

+ * The `TxReport` contains information about a successful Mentat transaction. + *

+ * This information includes: + *
    + *
  • `txId` - the identifier for the transaction.
  • + *
  • `txInstant` - the time that the transaction occured.
  • + *
  • a map of temporary identifiers provided in the transaction and the `Entid`s that they were mapped to.
  • + *
+ *

+ * Access an `Entid` for a temporary identifier that was provided in the transaction can be done through `entid(String:)`. + *

+ *
{@code
+ * TxReport report = mentat.transact("[[:db/add "a" :foo/boolean true]]");
+ * long aEntid = report.getEntidForTempId("a");
+ *}
+ */ +public class TxReport extends RustObject { + + private Long txId; + private Date txInstant; + + + public TxReport(Pointer pointer) { + this.rawPointer = pointer; + } + + /** + * Get the identifier for the transaction. + * @return The identifier for the transaction. + */ + public Long getTxId() { + if (this.txId == null) { + this.txId = JNA.INSTANCE.tx_report_get_entid(this.rawPointer); + } + + return this.txId; + } + + /** + * Get the time that the transaction occured. + * @return The time that the transaction occured. + */ + public Date getTxInstant() { + if (this.txInstant == null) { + this.txInstant = new Date(JNA.INSTANCE.tx_report_get_tx_instant(this.rawPointer)); + } + return this.txInstant; + } + + /** + * Access an `Entid` for a temporary identifier that was provided in the transaction. + * @param tempId A {@link String} representing the temporary identifier to fetch the `Entid` for. + * @return The `Entid` for the temporary identifier, if present, otherwise `null`. + */ + public Long getEntidForTempId(String tempId) { + Pointer longPointer = JNA.INSTANCE.tx_report_entity_for_temp_id(this.rawPointer, tempId); + if (longPointer == null) { + return null; + } + + return longPointer.getLong(0); + } + + @Override + public void close() { + if (this.rawPointer != null) { + JNA.INSTANCE.tx_report_destroy(this.rawPointer); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TypedValue.java b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TypedValue.java new file mode 100644 index 00000000..943e9753 --- /dev/null +++ b/sdks/android/Mentat/library/src/main/java/com/mozilla/mentat/TypedValue.java @@ -0,0 +1,157 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * Copyright 2018 Mozilla + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +package com.mozilla.mentat; + +import com.sun.jna.Pointer; + +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.UUID; + +/** + * A wrapper around Mentat's `TypedValue` Rust object. This class wraps a raw pointer to a Rust `TypedValue` + * struct and provides accessors to the values according to expected result type. + *

+ * As the FFI functions for fetching values are consuming, this class keeps a copy of the result internally after + * fetching so that the value can be referenced several times. + *

+ * Also, due to the consuming nature of the FFI layer, this class also manages it's raw pointer, nilling it after calling the + * FFI conversion function so that the underlying base class can manage cleanup. + */ +public class TypedValue extends RustObject { + + private Object value; + + private boolean isConsumed() { + return this.rawPointer == null; + } + + public TypedValue(Pointer pointer) { + this.rawPointer = pointer; + } + + /** + * This value as a {@link Long}. This function will panic if the `ValueType` of this + * {@link TypedValue} is not a `Long` + * @return the value of this {@link TypedValue} as a {@link Long} + */ + public Long asLong() { + if (!this.isConsumed()) { + this.value = JNA.INSTANCE.typed_value_into_long(this.rawPointer); + this.rawPointer = null; + } + return (Long)value; + } + + /** + * This value as a Entid. This function will panic if the `ValueType` of this + * {@link TypedValue} is not a `Ref` + * @return the value of this {@link TypedValue} as a Entid + */ + public Long asEntid() { + if (!this.isConsumed()) { + this.value = JNA.INSTANCE.typed_value_into_entid(this.rawPointer); + this.rawPointer = null; + } + return (Long)value; + } + + /** + * This value as a keyword {@link String}. This function will panic if the `ValueType` of this + * {@link TypedValue} is not a `Keyword` + * @return the value of this {@link TypedValue} as a Keyword + */ + public String asKeyword() { + if (!this.isConsumed()) { + this.value = JNA.INSTANCE.typed_value_into_kw(this.rawPointer); + this.rawPointer = null; + } + return (String)value; + } + + /** + * This value as a {@link Boolean}. This function will panic if the `ValueType` of this + * {@link TypedValue} is not a `Boolean` + * @return the value of this {@link TypedValue} as a {@link Boolean} + */ + public Boolean asBoolean() { + if (!this.isConsumed()) { + long value = JNA.INSTANCE.typed_value_into_boolean(this.rawPointer); + this.value = value == 0 ? false : true; + this.rawPointer = null; + } + return (Boolean) this.value; + } + + /** + * This value as a {@link Double}. This function will panic if the `ValueType` of this + * {@link TypedValue} is not a `Double` + * @return the value of this {@link TypedValue} as a {@link Double} + */ + public Double asDouble() { + if (!this.isConsumed()) { + this.value = JNA.INSTANCE.typed_value_into_double(this.rawPointer); + this.rawPointer = null; + } + return (Double)value; + } + + /** + * This value as a {@link Date}. This function will panic if the `ValueType` of this + * {@link TypedValue} is not a `Instant` + * @return the value of this {@link TypedValue} as a {@link Date} + */ + public Date asDate() { + if (!this.isConsumed()) { + this.value = new Date(JNA.INSTANCE.typed_value_into_timestamp(this.rawPointer) * 1_000); + this.rawPointer = null; + } + return (Date)this.value; + } + + /** + * This value as a {@link String}. This function will panic if the `ValueType` of this + * {@link TypedValue} is not a `String` + * @return the value of this {@link TypedValue} as a {@link String} + */ + public String asString() { + if (!this.isConsumed()) { + this.value = JNA.INSTANCE.typed_value_into_string(this.rawPointer); + this.rawPointer = null; + } + return (String)value; + } + + /** + * This value as a {@link UUID}. This function will panic if the `ValueType` of this + * {@link TypedValue} is not a `Uuid` + * @return the value of this {@link TypedValue} as a {@link UUID} + */ + public UUID asUUID() { + if (!this.isConsumed()) { + Pointer uuidPtr = JNA.INSTANCE.typed_value_into_uuid(this.rawPointer); + byte[] bytes = uuidPtr.getByteArray(0, 16); + ByteBuffer bb = ByteBuffer.wrap(bytes); + long high = bb.getLong(); + long low = bb.getLong(); + this.value = new UUID(high, low); + this.rawPointer = null; + } + return (UUID)this.value; + } + + @Override + public void close() { + if (this.rawPointer != null) { + JNA.INSTANCE.typed_value_destroy(this.rawPointer); + } + } +} diff --git a/sdks/android/Mentat/library/src/main/jniLibs/arm64/libjnidispatch.so b/sdks/android/Mentat/library/src/main/jniLibs/arm64/libjnidispatch.so new file mode 100644 index 00000000..d2b1a8f2 Binary files /dev/null and b/sdks/android/Mentat/library/src/main/jniLibs/arm64/libjnidispatch.so differ diff --git a/sdks/android/Mentat/library/src/main/jniLibs/arm64/libmentat_ffi.so b/sdks/android/Mentat/library/src/main/jniLibs/arm64/libmentat_ffi.so new file mode 100644 index 00000000..6bed1721 Binary files /dev/null and b/sdks/android/Mentat/library/src/main/jniLibs/arm64/libmentat_ffi.so differ diff --git a/sdks/android/Mentat/library/src/main/jniLibs/armeabi/libjnidispatch.so b/sdks/android/Mentat/library/src/main/jniLibs/armeabi/libjnidispatch.so new file mode 100644 index 00000000..bc167135 Binary files /dev/null and b/sdks/android/Mentat/library/src/main/jniLibs/armeabi/libjnidispatch.so differ diff --git a/sdks/android/Mentat/library/src/main/jniLibs/armeabi/libmentat_ffi.so b/sdks/android/Mentat/library/src/main/jniLibs/armeabi/libmentat_ffi.so new file mode 100644 index 00000000..ac20e94b Binary files /dev/null and b/sdks/android/Mentat/library/src/main/jniLibs/armeabi/libmentat_ffi.so differ diff --git a/sdks/android/Mentat/library/src/main/jniLibs/x86/libjnidispatch.so b/sdks/android/Mentat/library/src/main/jniLibs/x86/libjnidispatch.so new file mode 100644 index 00000000..a87d0865 Binary files /dev/null and b/sdks/android/Mentat/library/src/main/jniLibs/x86/libjnidispatch.so differ diff --git a/sdks/android/Mentat/library/src/main/jniLibs/x86/libmentat_ffi.so b/sdks/android/Mentat/library/src/main/jniLibs/x86/libmentat_ffi.so new file mode 100755 index 00000000..1160a11a Binary files /dev/null and b/sdks/android/Mentat/library/src/main/jniLibs/x86/libmentat_ffi.so differ diff --git a/sdks/android/Mentat/library/src/main/res/values/strings.xml b/sdks/android/Mentat/library/src/main/res/values/strings.xml new file mode 100644 index 00000000..30b035ae --- /dev/null +++ b/sdks/android/Mentat/library/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Mentat + diff --git a/sdks/android/Mentat/settings.gradle b/sdks/android/Mentat/settings.gradle new file mode 100644 index 00000000..d8f14a13 --- /dev/null +++ b/sdks/android/Mentat/settings.gradle @@ -0,0 +1 @@ +include ':library' diff --git a/sdks/swift/Mentat/Mentat.xcodeproj/project.pbxproj b/sdks/swift/Mentat/Mentat.xcodeproj/project.pbxproj new file mode 100644 index 00000000..37886d23 --- /dev/null +++ b/sdks/swift/Mentat/Mentat.xcodeproj/project.pbxproj @@ -0,0 +1,615 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 7B74483D208DF667006CFFB0 /* Result+Unwrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B74483C208DF667006CFFB0 /* Result+Unwrap.swift */; }; + 7BAE75A22089020E00895D37 /* libmentat_ffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BEB7D23207BE2AF000369AD /* libmentat_ffi.a */; }; + 7BAE75A42089022B00895D37 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BAE75A32089022B00895D37 /* libsqlite3.tbd */; }; + 7BDB96942077C299009D0651 /* Mentat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BDB968A2077C299009D0651 /* Mentat.framework */; }; + 7BDB96992077C299009D0651 /* MentatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96982077C299009D0651 /* MentatTests.swift */; }; + 7BDB969B2077C299009D0651 /* Mentat.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BDB968D2077C299009D0651 /* Mentat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7BDB96AF2077C38E009D0651 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96A62077C38D009D0651 /* Query.swift */; }; + 7BDB96B02077C38E009D0651 /* Mentat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96A72077C38D009D0651 /* Mentat.swift */; }; + 7BDB96B12077C38E009D0651 /* store.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BDB96A82077C38E009D0651 /* store.h */; }; + 7BDB96B22077C38E009D0651 /* RelResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96A92077C38E009D0651 /* RelResult.swift */; }; + 7BDB96B32077C38E009D0651 /* RustObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96AA2077C38E009D0651 /* RustObject.swift */; }; + 7BDB96B42077C38E009D0651 /* OptionalRustObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96AB2077C38E009D0651 /* OptionalRustObject.swift */; }; + 7BDB96B52077C38E009D0651 /* TupleResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96AC2077C38E009D0651 /* TupleResult.swift */; }; + 7BDB96B72077C38E009D0651 /* TypedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96AE2077C38E009D0651 /* TypedValue.swift */; }; + 7BDB96C22077CD98009D0651 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BDB96C12077CD98009D0651 /* libresolv.tbd */; }; + 7BDB96C62077D347009D0651 /* Date+Int64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96C52077D346009D0651 /* Date+Int64.swift */; }; + 7BDB96C9207B735A009D0651 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 7BDB96C8207B735A009D0651 /* fixtures */; }; + 7BDB96CC207B7684009D0651 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96CB207B7684009D0651 /* Errors.swift */; }; + 7BEB7D2C207D03DA000369AD /* TxReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BEB7D2B207D03DA000369AD /* TxReport.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7BDB96952077C299009D0651 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7BDB96812077C299009D0651 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7BDB96892077C299009D0651; + remoteInfo = Mentat; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 7B74483C208DF667006CFFB0 /* Result+Unwrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Result+Unwrap.swift"; path = "Mentat/Extensions/Result+Unwrap.swift"; sourceTree = SOURCE_ROOT; }; + 7B911E1A2085081D000998CB /* libtoodle.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtoodle.a; path = "../../../../sync-storage-prototype/rust/target/universal/release/libtoodle.a"; sourceTree = ""; }; + 7BAE75A32089022B00895D37 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; + 7BDB968A2077C299009D0651 /* Mentat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mentat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BDB968D2077C299009D0651 /* Mentat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Mentat.h; sourceTree = ""; }; + 7BDB968E2077C299009D0651 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7BDB96932077C299009D0651 /* MentatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MentatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7BDB96982077C299009D0651 /* MentatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentatTests.swift; sourceTree = ""; }; + 7BDB969A2077C299009D0651 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7BDB96A62077C38D009D0651 /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = ""; }; + 7BDB96A72077C38D009D0651 /* Mentat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mentat.swift; sourceTree = ""; }; + 7BDB96A82077C38E009D0651 /* store.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = store.h; sourceTree = ""; }; + 7BDB96A92077C38E009D0651 /* RelResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelResult.swift; sourceTree = ""; }; + 7BDB96AA2077C38E009D0651 /* RustObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RustObject.swift; sourceTree = ""; }; + 7BDB96AB2077C38E009D0651 /* OptionalRustObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalRustObject.swift; sourceTree = ""; }; + 7BDB96AC2077C38E009D0651 /* TupleResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TupleResult.swift; sourceTree = ""; }; + 7BDB96AE2077C38E009D0651 /* TypedValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypedValue.swift; sourceTree = ""; }; + 7BDB96BF2077CD7A009D0651 /* libmentat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmentat.a; path = ../../../target/universal/release/libmentat.a; sourceTree = ""; }; + 7BDB96C12077CD98009D0651 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + 7BDB96C32077D090009D0651 /* module.map */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.map; sourceTree = ""; }; + 7BDB96C52077D346009D0651 /* Date+Int64.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Int64.swift"; sourceTree = ""; }; + 7BDB96C8207B735A009D0651 /* fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fixtures; path = ../../../../fixtures; sourceTree = ""; }; + 7BDB96CB207B7684009D0651 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; + 7BEB7D21207BDDEF000369AD /* libtoodle.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtoodle.a; path = ../../../target/universal/release/libtoodle.a; sourceTree = ""; }; + 7BEB7D23207BE2AF000369AD /* libmentat_ffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmentat_ffi.a; path = ../../../target/universal/release/libmentat_ffi.a; sourceTree = ""; }; + 7BEB7D2B207D03DA000369AD /* TxReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxReport.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7BDB96862077C299009D0651 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BAE75A42089022B00895D37 /* libsqlite3.tbd in Frameworks */, + 7BDB96C22077CD98009D0651 /* libresolv.tbd in Frameworks */, + 7BAE75A22089020E00895D37 /* libmentat_ffi.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BDB96902077C299009D0651 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDB96942077C299009D0651 /* Mentat.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7BDB96802077C299009D0651 = { + isa = PBXGroup; + children = ( + 7BDB968C2077C299009D0651 /* Mentat */, + 7BDB96972077C299009D0651 /* MentatTests */, + 7BDB968B2077C299009D0651 /* Products */, + 7BDB96BE2077CD7A009D0651 /* Frameworks */, + ); + sourceTree = ""; + }; + 7BDB968B2077C299009D0651 /* Products */ = { + isa = PBXGroup; + children = ( + 7BDB968A2077C299009D0651 /* Mentat.framework */, + 7BDB96932077C299009D0651 /* MentatTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7BDB968C2077C299009D0651 /* Mentat */ = { + isa = PBXGroup; + children = ( + 7BDB96CA207B7672009D0651 /* Errors */, + 7BDB96C42077D346009D0651 /* Extensions */, + 7BDB96BA2077C42B009D0651 /* Core */, + 7BDB96A42077C301009D0651 /* Query */, + 7BDB96B92077C403009D0651 /* Rust */, + 7BDB96A82077C38E009D0651 /* store.h */, + 7BDB96A72077C38D009D0651 /* Mentat.swift */, + 7BEB7D26207BE5BB000369AD /* Transact */, + 7BDB968D2077C299009D0651 /* Mentat.h */, + 7BDB968E2077C299009D0651 /* Info.plist */, + 7BDB96C32077D090009D0651 /* module.map */, + ); + path = Mentat; + sourceTree = ""; + }; + 7BDB96972077C299009D0651 /* MentatTests */ = { + isa = PBXGroup; + children = ( + 7BDB96C8207B735A009D0651 /* fixtures */, + 7BDB96982077C299009D0651 /* MentatTests.swift */, + 7BDB969A2077C299009D0651 /* Info.plist */, + ); + path = MentatTests; + sourceTree = ""; + }; + 7BDB96A42077C301009D0651 /* Query */ = { + isa = PBXGroup; + children = ( + 7BDB96A62077C38D009D0651 /* Query.swift */, + 7BDB96A92077C38E009D0651 /* RelResult.swift */, + 7BDB96AC2077C38E009D0651 /* TupleResult.swift */, + ); + path = Query; + sourceTree = ""; + }; + 7BDB96B92077C403009D0651 /* Rust */ = { + isa = PBXGroup; + children = ( + 7BDB96AB2077C38E009D0651 /* OptionalRustObject.swift */, + 7BDB96AA2077C38E009D0651 /* RustObject.swift */, + ); + path = Rust; + sourceTree = ""; + }; + 7BDB96BA2077C42B009D0651 /* Core */ = { + isa = PBXGroup; + children = ( + 7BDB96AE2077C38E009D0651 /* TypedValue.swift */, + ); + path = Core; + sourceTree = ""; + }; + 7BDB96BE2077CD7A009D0651 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7BAE75A32089022B00895D37 /* libsqlite3.tbd */, + 7B911E1A2085081D000998CB /* libtoodle.a */, + 7BEB7D23207BE2AF000369AD /* libmentat_ffi.a */, + 7BEB7D21207BDDEF000369AD /* libtoodle.a */, + 7BDB96C12077CD98009D0651 /* libresolv.tbd */, + 7BDB96BF2077CD7A009D0651 /* libmentat.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7BDB96C42077D346009D0651 /* Extensions */ = { + isa = PBXGroup; + children = ( + 7B74483C208DF667006CFFB0 /* Result+Unwrap.swift */, + 7BDB96C52077D346009D0651 /* Date+Int64.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 7BDB96CA207B7672009D0651 /* Errors */ = { + isa = PBXGroup; + children = ( + 7BDB96CB207B7684009D0651 /* Errors.swift */, + ); + path = Errors; + sourceTree = ""; + }; + 7BEB7D26207BE5BB000369AD /* Transact */ = { + isa = PBXGroup; + children = ( + 7BEB7D2B207D03DA000369AD /* TxReport.swift */, + ); + path = Transact; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 7BDB96872077C299009D0651 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDB96B12077C38E009D0651 /* store.h in Headers */, + 7BDB969B2077C299009D0651 /* Mentat.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 7BDB96892077C299009D0651 /* Mentat */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7BDB969E2077C299009D0651 /* Build configuration list for PBXNativeTarget "Mentat" */; + buildPhases = ( + 7BDB96852077C299009D0651 /* Sources */, + 7BDB96862077C299009D0651 /* Frameworks */, + 7BDB96872077C299009D0651 /* Headers */, + 7BDB96882077C299009D0651 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Mentat; + productName = Mentat; + productReference = 7BDB968A2077C299009D0651 /* Mentat.framework */; + productType = "com.apple.product-type.framework"; + }; + 7BDB96922077C299009D0651 /* MentatTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7BDB96A12077C299009D0651 /* Build configuration list for PBXNativeTarget "MentatTests" */; + buildPhases = ( + 7BDB968F2077C299009D0651 /* Sources */, + 7BDB96902077C299009D0651 /* Frameworks */, + 7BDB96912077C299009D0651 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7BDB96962077C299009D0651 /* PBXTargetDependency */, + ); + name = MentatTests; + productName = MentatTests; + productReference = 7BDB96932077C299009D0651 /* MentatTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7BDB96812077C299009D0651 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0930; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = Mozilla; + TargetAttributes = { + 7BDB96892077C299009D0651 = { + CreatedOnToolsVersion = 9.3; + LastSwiftMigration = 0930; + }; + 7BDB96922077C299009D0651 = { + CreatedOnToolsVersion = 9.3; + }; + }; + }; + buildConfigurationList = 7BDB96842077C299009D0651 /* Build configuration list for PBXProject "Mentat" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 7BDB96802077C299009D0651; + productRefGroup = 7BDB968B2077C299009D0651 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7BDB96892077C299009D0651 /* Mentat */, + 7BDB96922077C299009D0651 /* MentatTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7BDB96882077C299009D0651 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BDB96912077C299009D0651 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDB96C9207B735A009D0651 /* fixtures in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7BDB96852077C299009D0651 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDB96B32077C38E009D0651 /* RustObject.swift in Sources */, + 7BDB96C62077D347009D0651 /* Date+Int64.swift in Sources */, + 7BEB7D2C207D03DA000369AD /* TxReport.swift in Sources */, + 7BDB96B42077C38E009D0651 /* OptionalRustObject.swift in Sources */, + 7BDB96B22077C38E009D0651 /* RelResult.swift in Sources */, + 7BDB96AF2077C38E009D0651 /* Query.swift in Sources */, + 7BDB96CC207B7684009D0651 /* Errors.swift in Sources */, + 7BDB96B02077C38E009D0651 /* Mentat.swift in Sources */, + 7BDB96B72077C38E009D0651 /* TypedValue.swift in Sources */, + 7BDB96B52077C38E009D0651 /* TupleResult.swift in Sources */, + 7B74483D208DF667006CFFB0 /* Result+Unwrap.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BDB968F2077C299009D0651 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BDB96992077C299009D0651 /* MentatTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7BDB96962077C299009D0651 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7BDB96892077C299009D0651 /* Mentat */; + targetProxy = 7BDB96952077C299009D0651 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 7BDB969C2077C299009D0651 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "-fembed-bitcode"; + OTHER_LDFLAGS = ""; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Mentat"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 7BDB969D2077C299009D0651 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.3; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "-fembed-bitcode"; + OTHER_LDFLAGS = ""; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Mentat"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 7BDB969F2077C299009D0651 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 8BHJ767F4Y; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Mentat/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../../target/universal/release"; + OTHER_CFLAGS = "-fembed-bitcode"; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.Mentat; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Mentat"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7BDB96A02077C299009D0651 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 8BHJ767F4Y; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTABILITY = NO; + INFOPLIST_FILE = Mentat/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../../target/universal/release"; + OTHER_CFLAGS = "-fembed-bitcode"; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.Mentat; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Mentat"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7BDB96A22077C299009D0651 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 8BHJ767F4Y; + INFOPLIST_FILE = MentatTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.MentatTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7BDB96A32077C299009D0651 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 8BHJ767F4Y; + INFOPLIST_FILE = MentatTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.MentatTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7BDB96842077C299009D0651 /* Build configuration list for PBXProject "Mentat" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7BDB969C2077C299009D0651 /* Debug */, + 7BDB969D2077C299009D0651 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7BDB969E2077C299009D0651 /* Build configuration list for PBXNativeTarget "Mentat" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7BDB969F2077C299009D0651 /* Debug */, + 7BDB96A02077C299009D0651 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7BDB96A12077C299009D0651 /* Build configuration list for PBXNativeTarget "MentatTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7BDB96A22077C299009D0651 /* Debug */, + 7BDB96A32077C299009D0651 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7BDB96812077C299009D0651 /* Project object */; +} diff --git a/sdks/swift/Mentat/Mentat.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/sdks/swift/Mentat/Mentat.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..8f0c7f34 --- /dev/null +++ b/sdks/swift/Mentat/Mentat.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/sdks/swift/Mentat/Mentat.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/sdks/swift/Mentat/Mentat.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/sdks/swift/Mentat/Mentat.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/sdks/swift/Mentat/Mentat.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/sdks/swift/Mentat/Mentat.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/sdks/swift/Mentat/Mentat.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/sdks/swift/Mentat/Mentat.xcodeproj/xcshareddata/xcschemes/Mentat Debug.xcscheme b/sdks/swift/Mentat/Mentat.xcodeproj/xcshareddata/xcschemes/Mentat Debug.xcscheme new file mode 100644 index 00000000..b08914c4 --- /dev/null +++ b/sdks/swift/Mentat/Mentat.xcodeproj/xcshareddata/xcschemes/Mentat Debug.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sdks/swift/Mentat/Mentat.xcodeproj/xcshareddata/xcschemes/Mentat.xcscheme b/sdks/swift/Mentat/Mentat.xcodeproj/xcshareddata/xcschemes/Mentat.xcscheme new file mode 100644 index 00000000..c03b16b0 --- /dev/null +++ b/sdks/swift/Mentat/Mentat.xcodeproj/xcshareddata/xcschemes/Mentat.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sdks/swift/Mentat/Mentat/Core/TypedValue.swift b/sdks/swift/Mentat/Mentat/Core/TypedValue.swift new file mode 100644 index 00000000..3db504a2 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Core/TypedValue.swift @@ -0,0 +1,181 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +/** + A wrapper around Mentat's `TypedValue` Rust object. This class wraps a raw pointer to a Rust `TypedValue` + struct and provides accessors to the values according to expected result type. + + As the FFI functions for fetching values are consuming, this class keeps a copy of the result internally after + fetching so that the value can be referenced several times. + + Also, due to the consuming nature of the FFI layer, this class also manages it's raw pointer, nilling it after calling the + FFI conversion function so that the underlying base class can manage cleanup. + */ +class TypedValue: OptionalRustObject { + + private var value: Any? + + /** + The `ValueType` for this `TypedValue`. + - Returns: The `ValueType` for this `TypedValue`. + */ + var valueType: ValueType { + return typed_value_value_type(self.raw!) + } + + private func isConsumed() -> Bool { + return self.raw == nil + } + + /** + This value as a `Int64`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Long` + + - Returns: the value of this `TypedValue` as a `Int64` + */ + func asLong() -> Int64 { + defer { + self.raw = nil + } + if !self.isConsumed() { + self.value = typed_value_into_long(self.raw!) + } + return self.value as! Int64 + } + + /** + This value as an `Entid`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Ref` + + - Returns: the value of this `TypedValue` as an `Entid` + */ + func asEntid() -> Entid { + defer { + self.raw = nil + } + + if !self.isConsumed() { + self.value = typed_value_into_entid(self.raw!) + } + return self.value as! Entid + } + + /** + This value as a keyword `String`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Keyword` + + - Returns: the value of this `TypedValue` as a keyword `String` + */ + func asKeyword() -> String { + defer { + self.raw = nil + } + + if !self.isConsumed() { + self.value = String(cString: typed_value_into_kw(self.raw!)) + } + return self.value as! String + } + + /** + This value as a `Bool`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Boolean` + + - Returns: the value of this `TypedValue` as a `Bool` + */ + func asBool() -> Bool { + defer { + self.raw = nil + } + + if !self.isConsumed() { + let v = typed_value_into_boolean(self.raw!) + self.value = v > 0 + } + return self.value as! Bool + } + + /** + This value as a `Double`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Double` + + - Returns: the value of this `TypedValue` as a `Double` + */ + func asDouble() -> Double { + defer { + self.raw = nil + } + + if !self.isConsumed() { + self.value = typed_value_into_double(self.raw!) + } + return self.value as! Double + } + + /** + This value as a `Date`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Instant` + + - Returns: the value of this `TypedValue` as a `Date` + */ + func asDate() -> Date { + defer { + self.raw = nil + } + + if !self.isConsumed() { + let timestamp = typed_value_into_timestamp(self.raw!) + self.value = Date(timeIntervalSince1970: TimeInterval(timestamp)) + } + return self.value as! Date + } + + /** + This value as a `String`. This function will panic if the `ValueType` of this `TypedValue` + is not a `String` + + - Returns: the value of this `TypedValue` as a `String` + */ + func asString() -> String { + defer { + self.raw = nil + } + + if !self.isConsumed() { + self.value = String(cString: typed_value_into_string(self.raw!)) + } + return self.value as! String + } + + /** + This value as a `UUID`. This function will panic if the `ValueType` of this `TypedValue` + is not a `Uuid` + + - Returns: the value of this `TypedValue` as a `UUID?`. If the `UUID` is not valid then this function returns nil. + */ + func asUUID() -> UUID? { + defer { + self.raw = nil + } + + if !self.isConsumed() { + let bytes = typed_value_into_uuid(self.raw!).pointee + self.value = UUID(uuid: bytes) + } + return self.value as! UUID? + } + + override func cleanup(pointer: OpaquePointer) { + typed_value_destroy(pointer) + } +} diff --git a/sdks/swift/Mentat/Mentat/Errors/Errors.swift b/sdks/swift/Mentat/Mentat/Errors/Errors.swift new file mode 100644 index 00000000..9091219b --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Errors/Errors.swift @@ -0,0 +1,30 @@ +// +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation + +public enum QueryError: Error { + case invalidKeyword(message: String) + case executionFailed(message: String) +} + +public struct MentatError: Error { + let message: String +} + +public enum PointerError: Error { + case pointerConsumed +} + +public enum ResultError: Error { + case error(message: String) + case empty +} diff --git a/sdks/swift/Mentat/Mentat/Extensions/Date+Int64.swift b/sdks/swift/Mentat/Mentat/Extensions/Date+Int64.swift new file mode 100644 index 00000000..068c666b --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Extensions/Date+Int64.swift @@ -0,0 +1,22 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation + +extension Date { + /** + This `Date` as microseconds. + + - Returns: The `timeIntervalSince1970` in microseconds + */ + func toMicroseconds() -> Int64 { + return Int64(self.timeIntervalSince1970 * 1_000_000) + } +} diff --git a/sdks/swift/Mentat/Mentat/Extensions/Result+Unwrap.swift b/sdks/swift/Mentat/Mentat/Extensions/Result+Unwrap.swift new file mode 100644 index 00000000..3d94903a --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Extensions/Result+Unwrap.swift @@ -0,0 +1,50 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +extension Result { + /** + Force unwraps a result. + Expects there to be a value attached and throws an error is there is not. + + - Throws: `ResultError.error` if the result contains an error + - Throws: `ResultError.empty` if the result contains no error but also no result. + + - Returns: The pointer to the successful result value. + */ + @discardableResult public func unwrap() throws -> UnsafeMutableRawPointer { + guard let success = self.ok else { + if let error = self.err { + throw ResultError.error(message: String(cString: error)) + } + throw ResultError.empty + } + return success + } + + /** + Unwraps an optional result, yielding either a successful value or a nil. + + - Throws: `ResultError.error` if the result contains an error + + - Returns: The pointer to the successful result value, or nil if no value is present. + */ + @discardableResult public func tryUnwrap() throws -> UnsafeMutableRawPointer? { + guard let success = self.ok else { + if let error = self.err { + throw ResultError.error(message: String(cString: error)) + } + return nil + } + return success + } +} diff --git a/sdks/swift/Mentat/Mentat/Info.plist b/sdks/swift/Mentat/Mentat/Info.plist new file mode 100644 index 00000000..1007fd9d --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/sdks/swift/Mentat/Mentat/Mentat.h b/sdks/swift/Mentat/Mentat/Mentat.h new file mode 100644 index 00000000..e08f33ca --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Mentat.h @@ -0,0 +1,18 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +#import + +//! Project version number for Mentat. +FOUNDATION_EXPORT double MentatVersionNumber; + +//! Project version string for Mentat. +FOUNDATION_EXPORT const unsigned char MentatVersionString[]; + diff --git a/sdks/swift/Mentat/Mentat/Mentat.swift b/sdks/swift/Mentat/Mentat/Mentat.swift new file mode 100644 index 00000000..5dd6fe66 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Mentat.swift @@ -0,0 +1,170 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation + +import MentatStore + +typealias Entid = Int64 + +/** + Protocol to be implemented by any object that wishes to register for transaction observation + */ +protocol Observing { + func transactionDidOccur(key: String, reports: [TxChange]) +} + +/** + Protocol to be implemented by any object that provides an interface to Mentat's transaction observers. + */ +protocol Observable { + func register(key: String, observer: Observing, attributes: [String]) + func unregister(key: String) +} + +/** + The primary class for accessing Mentat's API. + This class provides all of the basic API that can be found in Mentat's Store struct. + The raw pointer it holds is a pointer to a Store. +*/ +class Mentat: RustObject { + fileprivate static var observers = [String: Observing]() + + /** + Create a new Mentat with the provided pointer to a Mentat Store + - Parameter raw: A pointer to a Mentat Store. + */ + required override init(raw: OpaquePointer) { + super.init(raw: raw) + } + + /** + Open a connection to a Store in a given location. + If the store does not already exist, one will be created. + + - Parameter storeURI: The URI as a String of the store to open. + If no store URI is provided, an in-memory store will be opened. + */ + convenience init(storeURI: String = "") { + self.init(raw: store_open(storeURI)) + } + + /** + Simple transact of an EDN string. + - Parameter transaction: The string, as EDN, to be transacted + + - Throws: `MentatError` if the an error occured during the transaction, or the TxReport is nil. + + - Returns: The `TxReport` of the completed transaction + */ + func transact(transaction: String) throws -> TxReport { + let result = store_transact(self.raw, transaction).pointee + return TxReport(raw: try result.unwrap()) + } + + /** + Get the the `Entid` of the attribute. + - Parameter attribute: The string represeting the attribute whose `Entid` we are after. + The string is represented as `:namespace/name`. + + - Returns: The `Entid` associated with the attribute. + */ + func entidForAttribute(attribute: String) -> Entid { + return Entid(store_entid_for_attribute(self.raw, attribute)) + } + + /** + Start a query. + - Parameter query: The string represeting the the query to be executed. + + - Returns: The `Query` representing the query that can be executed. + */ + func query(query: String) -> Query { + return Query(raw: store_query(self.raw, query)) + } + + /** + Retrieve a single value of an attribute for an Entity + - Parameter attribute: The string the attribute whose value is to be returned. + The string is represented as `:namespace/name`. + - Parameter entid: The `Entid` of the entity we want the value from. + + - Returns: The `TypedValue` containing the value of the attribute for the entity. + */ + func value(forAttribute attribute: String, ofEntity entid: Entid) throws -> TypedValue? { + let result = store_value_for_attribute(self.raw, entid, attribute).pointee + return TypedValue(raw: try result.unwrap()) + } + + // Destroys the pointer by passing it back into Rust to be cleaned up + override func cleanup(pointer: OpaquePointer) { + store_destroy(pointer) + } +} + +/** + Set up `Mentat` to provide an interface to Mentat's transaction observation + */ +extension Mentat: Observable { + /** + Register an `Observing` and a set of attributes to observer for transaction observation. + The `transactionDidOccur(String: [TxChange]:)` function is called when a transaction + occurs in the `Store` that this `Mentat` is connected to that affects the attributes that an + `Observing` has registered for. + + - Parameter key: `String` representing an identifier for the `Observing`. + - Parameter observer: The `Observing` to be notified when a transaction occurs. + - Parameter attributes: An `Array` of `Strings` representing the attributes that the `Observing` + wishes to be notified about if they are referenced in a transaction. + */ + func register(key: String, observer: Observing, attributes: [String]) { + let attrEntIds = attributes.map({ (kw) -> Entid in + let entid = Entid(self.entidForAttribute(attribute: kw)); + return entid + }) + + let ptr = UnsafeMutablePointer.allocate(capacity: attrEntIds.count) + let entidPointer = UnsafeMutableBufferPointer(start: ptr, count: attrEntIds.count) + var _ = entidPointer.initialize(from: attrEntIds) + + guard let firstElement = entidPointer.baseAddress else { + return + } + Mentat.observers[key] = observer + store_register_observer(self.raw, key, firstElement, Entid(attributes.count), transactionObserverCallback) + + } + + /** + Unregister the `Observing` that was registered with the provided key such that it will no longer be called + if a transaction occurs that affects the attributes that `Observing` was registered to observe. + + The `Observing` will need to re-register if it wants to start observing again. + + - Parameter key: `String` representing an identifier for the `Observing`. + */ + func unregister(key: String) { + Mentat.observers.removeValue(forKey: key) + store_unregister_observer(self.raw, key) + } +} + + +/** + This function needs to be static as callbacks passed into Rust from Swift cannot contain state. Therefore the observers are static, as is + the function that we pass into Rust to receive the callback. + */ +private func transactionObserverCallback(key: UnsafePointer, reports: UnsafePointer) { + let key = String(cString: key) + guard let observer = Mentat.observers[key] else { return } + DispatchQueue.global(qos: .background).async { + observer.transactionDidOccur(key: key, reports: [TxChange]()) + } +} diff --git a/sdks/swift/Mentat/Mentat/Query/Query.swift b/sdks/swift/Mentat/Mentat/Query/Query.swift new file mode 100644 index 00000000..7521b022 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Query/Query.swift @@ -0,0 +1,337 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + + +/** + This class allows you to construct a query, bind values to variables and run those queries against a mentat DB. + + This class cannot be created directly, but must be created through `Mentat.query(String:)`. + + The types of values you can bind are + - `Int64` + - `Entid` + - `Keyword` + - `Bool` + - `Double` + - `Date` + - `String` + - `UUID`. + + Each bound variable must have a corresponding value in the query string used to create this query. + + ``` + let query = """ + [:find ?name ?cat + :in ?type + :where + [?c :community/name ?name] + [?c :community/type ?type] + [?c :community/category ?cat]] + """ + mentat.query(query: query) + .bind(varName: "?type", toKeyword: ":community.type/website") + .run { result in + ... + } + ``` + + Queries can be run and the results returned in a number of different formats. Individual result values are returned as `TypedValues` and + the format differences relate to the number and structure of those values. The result format is related to the format provided in the query string. + + - `Rel` - This is the default `run` function and returns a list of rows of values. Queries that wish to have `Rel` results should format their query strings: + ``` + let query = """ + [: find ?a ?b ?c + : where ... ] + """ + mentat.query(query: query) + .run { result in + ... + } + ``` + - `Scalar` - This returns a single value as a result. This can be optional, as the value may not be present. Queries that wish to have `Scalar` results should format their query strings: + ``` + let query = """ + [: find ?a . + : where ... ] + """ + mentat.query(query: query) + .runScalar { result in + ... + } + ``` + - `Coll` - This returns a list of single values as a result. Queries that wish to have `Coll` results should format their query strings: + ``` + let query = """ + [: find [?a ...] + : where ... ] + """ + mentat.query(query: query) + .runColl { result in + ... + } + ``` + - `Tuple` - This returns a single row of values. Queries that wish to have `Tuple` results should format their query strings: + ``` + let query = """ + [: find [?a ?b ?c] + : where ... ] + """ + mentat.query(query: query) + .runTuple { result in + ... + } + ``` + */ +class Query: OptionalRustObject { + + /** + Binds a `Int64` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toLong value: Int64) throws -> Query { + query_builder_bind_long(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `Entid` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toReference value: Entid) throws -> Query { + query_builder_bind_ref(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `String` value representing a keyword for an attribute to the provided variable name. + Keywords take the format `:namespace/name`. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toReference value: String) throws -> Query { + query_builder_bind_ref_kw(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a keyword `String` value to the provided variable name. + Keywords take the format `:namespace/name`. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toKeyword value: String) throws -> Query { + query_builder_bind_kw(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `Bool` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toBoolean value: Bool) throws -> Query { + query_builder_bind_boolean(try! self.validPointer(), varName, value ? 1 : 0) + return self + } + + /** + Binds a `Double` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toDouble value: Double) throws -> Query { + query_builder_bind_double(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `Date` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toDate value: Date) throws -> Query { + query_builder_bind_timestamp(try! self.validPointer(), varName, value.toMicroseconds()) + return self + } + + /** + Binds a `String` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toString value: String) throws -> Query { + query_builder_bind_string(try! self.validPointer(), varName, value) + return self + } + + /** + Binds a `UUID` value to the provided variable name. + + - Parameter varName: The name of the variable in the format `?name`. + - Parameter value: The value to be bound + + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed. + + - Returns: This `Query` such that further function can be called. + */ + func bind(varName: String, toUuid value: UUID) throws -> Query { + var rawUuid = value.uuid + withUnsafePointer(to: &rawUuid) { uuidPtr in + query_builder_bind_uuid(try! self.validPointer(), varName, uuidPtr) + } + return self + } + + /** + Execute the query with the values bound associated with this `Query` and call the provided callback function with the results as a list of rows of `TypedValues`. + + - Parameter callback: the function to call with the results of this query + + - Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, or that + variable we incorrectly bound, or that the query provided was not `Rel`. + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed. + */ + func run(callback: @escaping (RelResult?) -> Void) throws { + let result = query_builder_execute(try! self.validPointer()) + self.raw = nil + + if let err = result.pointee.err { + let message = String(cString: err) + throw QueryError.executionFailed(message: message) + } + guard let results = result.pointee.ok else { + callback(nil) + return + } + callback(RelResult(raw: results)) + } + + /** + Execute the query with the values bound associated with this `Query` and call the provided callback function with the result as a single `TypedValue`. + + - Parameter callback: the function to call with the results of this query + + - Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, that + variable we incorrectly bound, or that the query provided was not `Scalar`. + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed. + */ + func runScalar(callback: @escaping (TypedValue?) -> Void) throws { + let result = query_builder_execute_scalar(try! self.validPointer()) + self.raw = nil + + if let err = result.pointee.err { + let message = String(cString: err) + throw QueryError.executionFailed(message: message) + } + guard let results = result.pointee.ok else { + callback(nil) + return + } + callback(TypedValue(raw: OpaquePointer(results))) + } + + /** + Execute the query with the values bound associated with this `Query` and call the provided callback function with the result as a list of single `TypedValues`. + + - Parameter callback: the function to call with the results of this query + + - Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, that + variable we incorrectly bound, or that the query provided was not `Coll`. + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed. + */ + func runColl(callback: @escaping (ColResult?) -> Void) throws { + let result = query_builder_execute_coll(try! self.validPointer()) + self.raw = nil + + if let err = result.pointee.err { + let message = String(cString: err) + throw QueryError.executionFailed(message: message) + } + guard let results = result.pointee.ok else { + callback(nil) + return + } + callback(ColResult(raw: results)) + } + + /** + Execute the query with the values bound associated with this `Query` and call the provided callback function with the result as a list of single `TypedValues`. + + - Parameter callback: the function to call with the results of this query + + - Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, that + variable we incorrectly bound, or that the query provided was not `Tuple`. + - Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed. + */ + func runTuple(callback: @escaping (TupleResult?) -> Void) throws { + let result = query_builder_execute_tuple(try! self.validPointer()) + self.raw = nil + + if let err = result.pointee.err { + let message = String(cString: err) + throw QueryError.executionFailed(message: message) + } + guard let results = result.pointee.ok else { + callback(nil) + return + } + callback(TupleResult(raw: OpaquePointer(results))) + } + + override func cleanup(pointer: OpaquePointer) { + query_builder_destroy(pointer) + } +} diff --git a/sdks/swift/Mentat/Mentat/Query/RelResult.swift b/sdks/swift/Mentat/Mentat/Query/RelResult.swift new file mode 100644 index 00000000..7124c035 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Query/RelResult.swift @@ -0,0 +1,106 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +/** + Wraps a `Rel` result from a Mentat query. + A `Rel` result is a list of rows of `TypedValues`. + Individual rows can be fetched or the set can be iterated. + + To fetch individual rows from a `RelResult` use `row(Int32)`. + + ``` + query.run { rows in + let row1 = rows.row(0) + let row2 = rows.row(1) + } + ``` + + To iterate over the result set use standard iteration flows. + ``` + query.run { rows in + rows.forEach { row in + ... + } + } + ``` + + Note that iteration is consuming and can only be done once. + */ +class RelResult: OptionalRustObject { + + /** + Fetch the row at the requested index. + + - Parameter index: the index of the row to be fetched + + - Throws: `PointerError.pointerConsumed` if the result set has already been iterated. + + - Returns: The row at the requested index as a `TupleResult`, if present, or nil if there is no row at that index. + */ + func row(index: Int32) throws -> TupleResult? { + guard let row = row_at_index(try self.validPointer(), index) else { + return nil + } + return TupleResult(raw: row) + } + + override func cleanup(pointer: OpaquePointer) { + destroy(UnsafeMutableRawPointer(pointer)) + } +} + +/** + Iterator for `RelResult`. + + To iterate over the result set use standard iteration flows. + ``` + query.run { result in + rows.forEach { row in + ... + } + } + ``` + + Note that iteration is consuming and can only be done once. + */ +class RelResultIterator: OptionalRustObject, IteratorProtocol { + typealias Element = TupleResult + + init(iter: OpaquePointer?) { + super.init(raw: iter) + } + + func next() -> Element? { + guard let iter = self.raw, + let rowPtr = typed_value_result_set_iter_next(iter) else { + return nil + } + return TupleResult(raw: rowPtr) + } + + override func cleanup(pointer: OpaquePointer) { + typed_value_result_set_iter_destroy(pointer) + } +} + +extension RelResult: Sequence { + func makeIterator() -> RelResultIterator { + do { + let rowIter = typed_value_result_set_into_iter(try self.validPointer()) + self.raw = nil + return RelResultIterator(iter: rowIter) + } catch { + return RelResultIterator(iter: nil) + } + } +} diff --git a/sdks/swift/Mentat/Mentat/Query/TupleResult.swift b/sdks/swift/Mentat/Mentat/Query/TupleResult.swift new file mode 100644 index 00000000..d413581b --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Query/TupleResult.swift @@ -0,0 +1,217 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +/** + Wraps a `Tuple` result from a Mentat query. + A `Tuple` result is a list of `TypedValues`. + Individual values can be fetched as `TypedValues` or converted into a requested type. + + Values can be fetched as one of the following types: + - `TypedValue` + - `Int64` + - `Entid` + - `Keyword` + - `Bool` + - `Double` + - `Date` + - `String` + - `UUID`. + */ +class TupleResult: OptionalRustObject { + + /** + Return the `TypedValue` at the specified index. + If the index is greater than the number of values then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `TypedValue` at that index. + */ + func get(index: Int) -> TypedValue { + return TypedValue(raw: value_at_index(self.raw!, Int32(index))) + } + + /** + Return the `Int64` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Long` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Int64` at that index. + */ + func asLong(index: Int) -> Int64 { + return value_at_index_into_long(self.raw!, Int32(index)) + } + + /** + Return the `Entid` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Ref` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Entid` at that index. + */ + func asEntid(index: Int) -> Entid { + return value_at_index_into_entid(self.raw!, Int32(index)) + } + + /** + Return the keyword `String` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Keyword` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The keyword `String` at that index. + */ + func asKeyword(index: Int) -> String { + return String(cString: value_at_index_into_kw(self.raw!, Int32(index))) + } + + /** + Return the `Bool` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Boolean` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Bool` at that index. + */ + func asBool(index: Int) -> Bool { + return value_at_index_into_boolean(self.raw!, Int32(index)) == 0 ? false : true + } + + /** + Return the `Double` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Double` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Double` at that index. + */ + func asDouble(index: Int) -> Double { + return value_at_index_into_double(self.raw!, Int32(index)) + } + + /** + Return the `Date` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Instant` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `Date` at that index. + */ + func asDate(index: Int) -> Date { + return Date(timeIntervalSince1970: TimeInterval(value_at_index_into_timestamp(self.raw!, Int32(index)))) + } + + /** + Return the `String` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `String` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `String` at that index. + */ + func asString(index: Int) -> String { + return String(cString: value_at_index_into_string(self.raw!, Int32(index))) + } + + /** + Return the `UUID` at the specified index. + If the index is greater than the number of values then this function will crash. + If the value type if the `TypedValue` at this index is not `Uuid` then this function will crash. + + - Parameter index: The index of the value to fetch. + + - Returns: The `UUID` at that index. + */ + func asUUID(index: Int) -> UUID? { + return UUID(uuid: value_at_index_into_uuid(self.raw!, Int32(index)).pointee) + } + + override func cleanup(pointer: OpaquePointer) { + typed_value_list_destroy(pointer) + } +} + +/** + Wraps a `Coll` result from a Mentat query. + A `Coll` result is a list of rows of single values of type `TypedValue`. + Values for individual rows can be fetched as `TypedValue` or converted into a requested type. + + Row values can be fetched as one of the following types: + - `TypedValue` + - `Int64` + - `Entid` + - `Keyword` + - `Bool` + - `Double` + - `Date` + - `String` + - `UUID`. + */ +class ColResult: TupleResult { +} + +/** + Iterator for `ColResult`. + + To iterate over the result set use standard iteration flows. + ``` + query.runColl { rows in + rows.forEach { value in + ... + } + } + ``` + + Note that iteration is consuming and can only be done once. + */ +class ColResultIterator: OptionalRustObject, IteratorProtocol { + typealias Element = TypedValue + + init(iter: OpaquePointer?) { + super.init(raw: iter) + } + + func next() -> Element? { + guard let iter = self.raw, + let rowPtr = typed_value_list_iter_next(iter) else { + return nil + } + return TypedValue(raw: rowPtr) + } + + override func cleanup(pointer: OpaquePointer) { + typed_value_list_iter_destroy(pointer) + } +} + +extension ColResult: Sequence { + func makeIterator() -> ColResultIterator { + defer { + self.raw = nil + } + guard let raw = self.raw else { + return ColResultIterator(iter: nil) + } + let rowIter = typed_value_list_into_iter(raw) + return ColResultIterator(iter: rowIter) + } +} diff --git a/sdks/swift/Mentat/Mentat/Rust/OptionalRustObject.swift b/sdks/swift/Mentat/Mentat/Rust/OptionalRustObject.swift new file mode 100644 index 00000000..7f2fe74d --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Rust/OptionalRustObject.swift @@ -0,0 +1,68 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +/** + Base class that wraps an optional `OpaquePointer` representing a pointer to a Rust object. + This class should be used to wrap Rust pointer that point to consuming structs, that is, calling a function + for that Rust pointer, will cause Rust to destroy the pointer, leaving the Swift pointer dangling. + These classes are responsible for ensuring that their raw `OpaquePointer` are `nil`led after calling a consuming + FFI function. + This class provides cleanup functions on deinit, ensuring that all classes + that inherit from it will have their `OpaquePointer` destroyed when the Swift wrapper is destroyed. + If a class does not override `cleanup` then a `fatalError` is thrown. + The optional pointer is managed here such that is the pointer is nil, then the cleanup function is not called + ensuring that we do not double free the pointer on exit. + */ +class OptionalRustObject: Destroyable { + var raw: OpaquePointer? + lazy var uniqueId: ObjectIdentifier = { + ObjectIdentifier(self) + }() + + init(raw: UnsafeMutableRawPointer) { + self.raw = OpaquePointer(raw) + } + + init(raw: OpaquePointer?) { + self.raw = raw + } + + func intoRaw() -> OpaquePointer? { + return self.raw + } + + deinit { + guard let raw = self.raw else { return } + self.cleanup(pointer: raw) + } + + /** + Provides a non-optional `OpaquePointer` if one exists for this class. + + - Throws: `Pointer.pointerConsumed` if the raw pointer wrapped by this class is nil + + - Returns: the raw `OpaquePointer` wrapped by this class. + */ + func validPointer() throws -> OpaquePointer { + guard let r = self.raw else { + throw PointerError.pointerConsumed + } + + return r + } + + func cleanup(pointer: OpaquePointer) { + fatalError("\(cleanup) is not implemented.") + } +} + diff --git a/sdks/swift/Mentat/Mentat/Rust/RustObject.swift b/sdks/swift/Mentat/Mentat/Rust/RustObject.swift new file mode 100644 index 00000000..7648fadb --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Rust/RustObject.swift @@ -0,0 +1,49 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation +import MentatStore + +protocol Destroyable { + func cleanup(pointer: OpaquePointer) +} + +/** + Base class that wraps an non-optional `OpaquePointer` representing a pointer to a Rust object. + This class provides cleanup functions on deinit, ensuring that all classes + that inherit from it will have their `OpaquePointer` destroyed when the Swift wrapper is destroyed. + If a class does not override `cleanup` then a `fatalError` is thrown. + */ +public class RustObject: Destroyable { + var raw: OpaquePointer + + init(raw: OpaquePointer) { + self.raw = raw + } + + init(raw: UnsafeMutableRawPointer) { + self.raw = OpaquePointer(raw) + } + + init?(raw: OpaquePointer?) { + guard let r = raw else { + return nil + } + self.raw = r + } + + deinit { + self.cleanup(pointer: self.raw) + } + + func cleanup(pointer: OpaquePointer) { + fatalError("\(cleanup) is not implemented.") + } +} diff --git a/sdks/swift/Mentat/Mentat/Transact/TxReport.swift b/sdks/swift/Mentat/Mentat/Transact/TxReport.swift new file mode 100644 index 00000000..8658158d --- /dev/null +++ b/sdks/swift/Mentat/Mentat/Transact/TxReport.swift @@ -0,0 +1,61 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import Foundation + +import MentatStore + +/** + This class wraps a raw pointer than points to a Rust `TxReport` object. + + The `TxReport` contains information about a successful Mentat transaction. + + This information includes: + - `txId` - the identifier for the transaction. + - `txInstant` - the time that the transaction occured. + - a map of temporary identifiers provided in the transaction and the `Entid`s that they were mapped to, + + Access an `Entid` for a temporary identifier that was provided in the transaction can be done through `entid(String:)`. + + ``` + let report = mentat.transact("[[:db/add "a" :foo/boolean true]]") + let aEntid = report.entid(forTempId: "a") + ``` + */ +class TxReport: RustObject { + + // The identifier for the transaction. + public var txId: Entid { + return tx_report_get_entid(self.raw) + } + + // The time that the transaction occured. + public var txInstant: Date { + return Date(timeIntervalSince1970: TimeInterval(tx_report_get_tx_instant(self.raw))) + } + + /** + Access an `Entid` for a temporary identifier that was provided in the transaction. + + - Parameter tempId: A `String` representing the temporary identifier to fetch the `Entid` for. + + - Returns: The `Entid` for the temporary identifier, if present, otherwise `nil`. + */ + public func entid(forTempId tempId: String) -> Entid? { + guard let entidPtr = tx_report_entity_for_temp_id(self.raw, tempId) else { + return nil + } + return entidPtr.pointee + } + + override func cleanup(pointer: OpaquePointer) { + tx_report_destroy(pointer) + } +} diff --git a/sdks/swift/Mentat/Mentat/module.map b/sdks/swift/Mentat/Mentat/module.map new file mode 100644 index 00000000..5cb44399 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/module.map @@ -0,0 +1,4 @@ +module MentatStore [system][extern_c] { + header "store.h" + export * +} diff --git a/sdks/swift/Mentat/Mentat/store.h b/sdks/swift/Mentat/Mentat/store.h new file mode 100644 index 00000000..7f86e009 --- /dev/null +++ b/sdks/swift/Mentat/Mentat/store.h @@ -0,0 +1,171 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +#ifndef store_h +#define store_h +#include +#include + +/* + * This file contains headers for all of the structs and functions that map directly to the functions + * defined in mentat/ffi/src/lib.rs. + * + * The C in this file is specifically formatted to be used with Objective C and Swift and contains + * macros and flags that will not be recognised by other C based languages. + */ + +/* + A mapping of the TxChange repr(C) Rust object. + The memory for this is managed by Swift. + */ +struct TxChange { + int64_t txid; + int64_t*_Nonnull* _Nonnull changes; + uint64_t len; +}; + +/* + A mapping of the TxChangeList repr(C) Rust object. + The memory for this is managed by Swift. + */ +struct TxChangeList { + struct TxChange*_Nonnull* _Nonnull reports; + uint64_t len; +}; +typedef struct TxChangeList TxChangeList; + +/* + A mapping of the ExternResult repr(C) Rust object. + The memory for this is managed by Swift. + */ +struct Result { + void* _Nullable ok; + char* _Nullable err; +}; +typedef struct Result Result; + +/* + A mapping of the ExternOption repr(C) Rust object. + The memory for this is managed by Swift. + */ +struct Option { + void* _Nullable value; +}; +typedef struct Option Option; + +/* + A Mapping for the ValueType Rust object. + */ +typedef NS_ENUM(NSInteger, ValueType) { + ValueTypeRef = 1, + ValueTypeBoolean, + ValueTypeInstant, + ValueTypeLong, + ValueTypeDouble, + ValueTypeString, + ValueTypeKeyword, + ValueTypeUuid +}; + +// Opaque Structs mapping to Rust types that are passed over the FFI boundary +struct EntityBuilder; +struct InProgress; +struct InProgressBuilder; +struct Query; +struct QueryResultRow; +struct QueryResultRows; +struct QueryRowsIterator; +struct QueryRowIterator; +struct Store; +struct TxReport; +struct TypedValue; + +// Store +struct Store*_Nonnull store_open(const char*_Nonnull uri); + +// Destructors. +void destroy(void* _Nullable obj); +void query_builder_destroy(struct Query* _Nullable obj); +void store_destroy(struct Store* _Nonnull obj); +void tx_report_destroy(struct TxReport* _Nonnull obj); +void typed_value_destroy(struct TypedValue* _Nullable obj); +void typed_value_list_destroy(struct QueryResultRow* _Nullable obj); +void typed_value_list_iter_destroy(struct QueryRowIterator* _Nullable obj); +void typed_value_result_set_destroy(struct QueryResultRows* _Nullable obj); +void typed_value_result_set_iter_destroy(struct QueryRowsIterator* _Nullable obj); + +// transact +struct Result*_Nonnull store_transact(struct Store*_Nonnull store, const char* _Nonnull transaction); +const int64_t* _Nullable tx_report_entity_for_temp_id(const struct TxReport* _Nonnull report, const char* _Nonnull tempid); +int64_t tx_report_get_entid(const struct TxReport* _Nonnull report); +int64_t tx_report_get_tx_instant(const struct TxReport* _Nonnull report); + + +// Sync +struct Result*_Nonnull store_sync(struct Store*_Nonnull store, const char* _Nonnull user_uuid, const char* _Nonnull server_uri); + +// Observers +void store_register_observer(struct Store*_Nonnull store, const char* _Nonnull key, const int64_t* _Nonnull attributes, const int64_t len, void (*_Nonnull callback_fn)(const char* _Nonnull key, const struct TxChangeList* _Nonnull reports)); +void store_unregister_observer(struct Store*_Nonnull store, const char* _Nonnull key); +int64_t store_entid_for_attribute(struct Store*_Nonnull store, const char*_Nonnull attr); +int64_t changelist_entry_at(const struct TxChange* _Nonnull report, size_t index); + +// Query +struct Query*_Nonnull store_query(struct Store*_Nonnull store, const char* _Nonnull query); +struct Result*_Nonnull store_value_for_attribute(struct Store*_Nonnull store, const int64_t entid, const char* _Nonnull attribute); + +// Query Variable Binding +void query_builder_bind_long(struct Query*_Nonnull query, const char* _Nonnull var, const int64_t value); +void query_builder_bind_ref(struct Query*_Nonnull query, const char* _Nonnull var, const int64_t value); +void query_builder_bind_ref_kw(struct Query*_Nonnull query, const char* _Nonnull var, const char* _Nonnull value); +void query_builder_bind_kw(struct Query*_Nonnull query, const char* _Nonnull var, const char* _Nonnull value); +void query_builder_bind_boolean(struct Query*_Nonnull query, const char* _Nonnull var, const int32_t value); +void query_builder_bind_double(struct Query*_Nonnull query, const char* _Nonnull var, const double value); +void query_builder_bind_timestamp(struct Query*_Nonnull query, const char* _Nonnull var, const int64_t value); +void query_builder_bind_string(struct Query*_Nonnull query, const char* _Nonnull var, const char* _Nonnull value); +void query_builder_bind_uuid(struct Query*_Nonnull query, const char* _Nonnull var, const uuid_t* _Nonnull value); + +// Query execution +struct Result*_Nonnull query_builder_execute(struct Query*_Nonnull query); +struct Result*_Nonnull query_builder_execute_scalar(struct Query*_Nonnull query); +struct Result*_Nonnull query_builder_execute_coll(struct Query*_Nonnull query); +struct Result*_Nonnull query_builder_execute_tuple(struct Query*_Nonnull query); + +// Query Result Processing +int64_t typed_value_into_long(struct TypedValue*_Nonnull value); +int64_t typed_value_into_entid(struct TypedValue*_Nonnull value); +const char* _Nonnull typed_value_into_kw(struct TypedValue*_Nonnull value); +int32_t typed_value_into_boolean(struct TypedValue*_Nonnull value); +double typed_value_into_double(struct TypedValue*_Nonnull value); +int64_t typed_value_into_timestamp(struct TypedValue*_Nonnull value); +const char* _Nonnull typed_value_into_string(struct TypedValue*_Nonnull value); +const uuid_t* _Nonnull typed_value_into_uuid(struct TypedValue*_Nonnull value); +enum ValueType typed_value_value_type(struct TypedValue*_Nonnull value); + +struct QueryResultRow* _Nullable row_at_index(struct QueryResultRows* _Nonnull rows, const int32_t index); +struct QueryRowsIterator* _Nonnull typed_value_result_set_into_iter(struct QueryResultRows* _Nonnull rows); +struct QueryResultRow* _Nullable typed_value_result_set_iter_next(struct QueryRowsIterator* _Nonnull iter); +struct QueryRowIterator* _Nonnull typed_value_list_into_iter(struct QueryResultRow* _Nonnull row); +struct TypedValue* _Nullable typed_value_list_iter_next(struct QueryRowIterator* _Nonnull iter); + +struct TypedValue* _Nonnull value_at_index(struct QueryResultRow* _Nonnull row, const int32_t index); +int64_t value_at_index_into_long(struct QueryResultRow* _Nonnull row, const int32_t index); +int64_t value_at_index_into_entid(struct QueryResultRow* _Nonnull row, const int32_t index); +const char* _Nonnull value_at_index_into_kw(struct QueryResultRow* _Nonnull row, const int32_t index); +int32_t value_at_index_into_boolean(struct QueryResultRow* _Nonnull row, const int32_t index); +double value_at_index_into_double(struct QueryResultRow* _Nonnull row, const int32_t index); +int64_t value_at_index_into_timestamp(struct QueryResultRow* _Nonnull row, const int32_t index); +const char* _Nonnull value_at_index_into_string(struct QueryResultRow* _Nonnull row, const int32_t index); +const uuid_t* _Nonnull value_at_index_into_uuid(struct QueryResultRow* _Nonnull row, const int32_t index); + +// Transaction change lists +const struct TxChange* _Nullable tx_change_list_entry_at(const struct TxChangeList* _Nonnull list, size_t index); + +#endif /* store_h */ diff --git a/sdks/swift/Mentat/MentatTests/Info.plist b/sdks/swift/Mentat/MentatTests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/sdks/swift/Mentat/MentatTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/sdks/swift/Mentat/MentatTests/MentatTests.swift b/sdks/swift/Mentat/MentatTests/MentatTests.swift new file mode 100644 index 00000000..3587e559 --- /dev/null +++ b/sdks/swift/Mentat/MentatTests/MentatTests.swift @@ -0,0 +1,765 @@ +/* Copyright 2018 Mozilla + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ + +import XCTest + +@testable import Mentat + +class MentatTests: XCTestCase { + + var citiesSchema: String? + var seattleData: String? + var store: Mentat? + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + // test that a store can be opened in memory + func testOpenInMemoryStore() { + XCTAssertNotNil(Mentat().raw) + } + + // test that a store can be opened in a specific location + func testOpenStoreInLocation() { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsURL = paths[0] + let storeURI = documentsURL.appendingPathComponent("test.db", isDirectory: false).absoluteString + XCTAssertNotNil(Mentat(storeURI: storeURI).raw) + } + + func readFile(forResource resource: String, withExtension ext: String, subdirectory: String ) throws -> String { + let bundle = Bundle(for: type(of: self)) + let schemaUrl = bundle.url(forResource: resource, withExtension: ext, subdirectory: subdirectory)! + let contents = try String(contentsOf: schemaUrl) + return contents + } + + func readCitiesSchema() throws -> String { + guard let schema = self.citiesSchema else { + self.citiesSchema = try self.readFile(forResource: "cities", withExtension: "schema", subdirectory: "fixtures") + return self.citiesSchema! + } + + return schema + } + + func readSeattleData() throws -> String { + guard let data = self.seattleData else { + self.seattleData = try self.readFile(forResource: "all_seattle", withExtension: "edn", subdirectory: "fixtures") + return self.seattleData! + } + + return data + } + + func transactCitiesSchema(mentat: Mentat) throws -> TxReport { + let vocab = try readCitiesSchema() + let report = try mentat.transact(transaction: vocab) + return report + } + + func transactSeattleData(mentat: Mentat) throws -> TxReport { + let data = try readSeattleData() + let report = try mentat.transact(transaction: data) + return report + } + + func openAndInitializeCitiesStore() -> Mentat { + guard let mentat = self.store else { + let mentat = Mentat() + let _ = try! self.transactCitiesSchema(mentat: mentat) + let _ = try! self.transactSeattleData(mentat: mentat) + self.store = mentat + return mentat + } + + return mentat + } + + func populateWithTypesSchema(mentat: Mentat) -> (TxReport?, TxReport?) { + do { + let schema = """ + [ + [:db/add "b" :db/ident :foo/boolean] + [:db/add "b" :db/valueType :db.type/boolean] + [:db/add "b" :db/cardinality :db.cardinality/one] + [:db/add "l" :db/ident :foo/long] + [:db/add "l" :db/valueType :db.type/long] + [:db/add "l" :db/cardinality :db.cardinality/one] + [:db/add "r" :db/ident :foo/ref] + [:db/add "r" :db/valueType :db.type/ref] + [:db/add "r" :db/cardinality :db.cardinality/one] + [:db/add "i" :db/ident :foo/instant] + [:db/add "i" :db/valueType :db.type/instant] + [:db/add "i" :db/cardinality :db.cardinality/one] + [:db/add "d" :db/ident :foo/double] + [:db/add "d" :db/valueType :db.type/double] + [:db/add "d" :db/cardinality :db.cardinality/one] + [:db/add "s" :db/ident :foo/string] + [:db/add "s" :db/valueType :db.type/string] + [:db/add "s" :db/cardinality :db.cardinality/one] + [:db/add "k" :db/ident :foo/keyword] + [:db/add "k" :db/valueType :db.type/keyword] + [:db/add "k" :db/cardinality :db.cardinality/one] + [:db/add "u" :db/ident :foo/uuid] + [:db/add "u" :db/valueType :db.type/uuid] + [:db/add "u" :db/cardinality :db.cardinality/one] + ] + """ + let report = try mentat.transact(transaction: schema) + let stringEntid = report.entid(forTempId: "s")! + + let data = """ + [ + [:db/add "a" :foo/boolean true] + [:db/add "a" :foo/long 25] + [:db/add "a" :foo/instant #inst "2017-01-01T11:00:00.000Z"] + [:db/add "a" :foo/double 11.23] + [:db/add "a" :foo/string "The higher we soar the smaller we appear to those who cannot fly."] + [:db/add "a" :foo/keyword :foo/string] + [:db/add "a" :foo/uuid #uuid "550e8400-e29b-41d4-a716-446655440000"] + [:db/add "b" :foo/boolean false] + [:db/add "b" :foo/ref \(stringEntid)] + [:db/add "b" :foo/keyword :foo/string] + [:db/add "b" :foo/long 50] + [:db/add "b" :foo/instant #inst "2018-01-01T11:00:00.000Z"] + [:db/add "b" :foo/double 22.46] + [:db/add "b" :foo/string "Silence is worse; all truths that are kept silent become poisonous."] + [:db/add "b" :foo/uuid #uuid "4cb3f828-752d-497a-90c9-b1fd516d5644"] + ] + """ + let dataReport = try mentat.transact(transaction: data) + return (report, dataReport) + } catch { + assertionFailure(error.localizedDescription) + } + return (nil, nil) + } + + func test1TransactVocabulary() { + do { + let mentat = Mentat() + let vocab = try readCitiesSchema() + let report = try mentat.transact(transaction: vocab) + XCTAssertNotNil(report) + assert(report.txId > 0) + } catch { + assertionFailure(error.localizedDescription) + } + } + + func test2TransactEntities() { + do { + let mentat = Mentat() + let vocab = try readCitiesSchema() + let _ = try mentat.transact(transaction: vocab) + let data = try readSeattleData() + let report = try mentat.transact(transaction: data) + XCTAssertNotNil(report) + assert(report.txId > 0) + let entid = report.entid(forTempId: "a17592186045438") + assert(entid == 65566) + } catch { + assertionFailure(error.localizedDescription) + } + } + + func testQueryScalar() { + let mentat = openAndInitializeCitiesStore() + let query = "[:find ?n . :in ?name :where [(fulltext $ :community/name ?name) [[?e ?n]]]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query).bind(varName: "?name", toString: "Wallingford").runScalar(callback: { scalarResult in + guard let result = scalarResult?.asString() else { + return assertionFailure("No String value received") + } + assert(result == "KOMO Communities - Wallingford") + expect.fulfill() + })) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testQueryColl() { + let mentat = openAndInitializeCitiesStore() + let query = "[:find [?when ...] :where [_ :db/txInstant ?when] :order (asc ?when)]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query).runColl(callback: { collResult in + guard let rows = collResult else { + return assertionFailure("No results received") + } + // we are expecting 3 results + for i in 0..<3 { + let _ = rows.asDate(index: i) + assert(true) + } + expect.fulfill() + })) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testQueryCollResultIterator() { + let mentat = openAndInitializeCitiesStore() + let query = "[:find [?when ...] :where [_ :db/txInstant ?when] :order (asc ?when)]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query).runColl(callback: { collResult in + guard let rows = collResult else { + return assertionFailure("No results received") + } + + rows.forEach({ (value) in + assert(value.valueType.rawValue == 2) + }) + expect.fulfill() + })) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testQueryTuple() { + let mentat = openAndInitializeCitiesStore() + let query = """ + [:find [?name ?cat] + :where + [?c :community/name ?name] + [?c :community/type :community.type/website] + [(fulltext $ :community/category "food") [[?c ?cat]]]] + """ + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query).runTuple(callback: { tupleResult in + guard let tuple = tupleResult else { + return assertionFailure("expecting a result") + } + let name = tuple.asString(index: 0) + let category = tuple.asString(index: 1) + assert(name == "Community Harvest of Southwest Seattle") + assert(category == "sustainable food") + expect.fulfill() + })) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testQueryRel() { + let mentat = openAndInitializeCitiesStore() + let query = """ + [:find ?name ?cat + :where + [?c :community/name ?name] + [?c :community/type :community.type/website] + [(fulltext $ :community/category "food") [[?c ?cat]]]] + """ + let expect = expectation(description: "Query is executed") + let expectedResults = [("InBallard", "food"), + ("Seattle Chinatown Guide", "food"), + ("Community Harvest of Southwest Seattle", "sustainable food"), + ("University District Food Bank", "food bank")] + XCTAssertNoThrow(try mentat.query(query: query).run(callback: { relResult in + guard let rows = relResult else { + return assertionFailure("No results received") + } + + for (i, row) in rows.enumerated() { + let (name, category) = expectedResults[i] + assert( row.asString(index: 0) == name) + assert(row.asString(index: 1) == category) + } + expect.fulfill() + })) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testQueryRelResultIterator() { + let mentat = openAndInitializeCitiesStore() + let query = """ + [:find ?name ?cat + :where + [?c :community/name ?name] + [?c :community/type :community.type/website] + [(fulltext $ :community/category "food") [[?c ?cat]]]] + """ + let expect = expectation(description: "Query is executed") + let expectedResults = [("InBallard", "food"), + ("Seattle Chinatown Guide", "food"), + ("Community Harvest of Southwest Seattle", "sustainable food"), + ("University District Food Bank", "food bank")] + XCTAssertNoThrow(try mentat.query(query: query).run(callback: { relResult in + guard let rows = relResult else { + return assertionFailure("No results received") + } + + var i = 0 + rows.forEach({ (row) in + let (name, category) = expectedResults[i] + i += 1 + assert(row.asString(index: 0) == name) + assert(row.asString(index: 1) == category) + }) + assert(i == 4) + expect.fulfill() + })) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testBindLong() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a") + let query = "[:find ?e . :in ?long :where [?e :foo/long ?long]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?long", toLong: 25) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asEntid() == aEntid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testBindRef() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let stringEntid = mentat.entidForAttribute(attribute: ":foo/string") + let bEntid = report!.entid(forTempId: "b") + let query = "[:find ?e . :in ?ref :where [?e :foo/ref ?ref]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?ref", toReference: stringEntid) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asEntid() == bEntid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testBindKwRef() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let bEntid = report!.entid(forTempId: "b") + let query = "[:find ?e . :in ?ref :where [?e :foo/ref ?ref]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?ref", toReference: ":foo/string") + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asEntid() == bEntid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testBindKw() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a") + let query = "[:find ?e . :in ?kw :where [?e :foo/keyword ?kw]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?kw", toKeyword: ":foo/string") + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asEntid() == aEntid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testBindDate() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a") + let query = "[:find [?e ?d] :in ?now :where [?e :foo/instant ?d] [(< ?d ?now)]]" + let expect = expectation(description: "Query is executed") + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + let boundDate = formatter.date(from: "2018-04-16T16:39:18+00:00")! + + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?now", toDate: boundDate) + .runTuple { row in + XCTAssertNotNil(row) + assert(row?.asEntid(index: 0) == aEntid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + + func testBindString() { + let mentat = openAndInitializeCitiesStore() + let query = "[:find ?n . :in ?name :where [(fulltext $ :community/name ?name) [[?e ?n]]]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?name", toString: "Wallingford") + .runScalar(callback: { scalarResult in + guard let result = scalarResult?.asString() else { + return assertionFailure("No String value received") + } + assert(result == "KOMO Communities - Wallingford") + expect.fulfill() + })) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testBindUuid() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a") + let query = "[:find ?e . :in ?uuid :where [?e :foo/uuid ?uuid]]" + let uuid = UUID(uuidString: "550e8400-e29b-41d4-a716-446655440000")! + let expect = expectation(description: "Query is rund") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?uuid", toUuid: uuid) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asEntid() == aEntid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testBindBoolean() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a") + let query = "[:find ?e . :in ?bool :where [?e :foo/boolean ?bool]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?bool", toBoolean: true) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asEntid() == aEntid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testBindDouble() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a") + let query = "[:find ?e . :in ?double :where [?e :foo/double ?double]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?double", toDouble: 11.23) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asEntid() == aEntid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testTypedValueAsLong() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a")! + let query = "[:find ?v . :in ?e :where [?e :foo/long ?v]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?e", toReference: aEntid) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asLong() == 25) + assert(value?.asLong() == 25) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testTypedValueAsRef() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a")! + let query = "[:find ?e . :where [?e :foo/long 25]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asEntid() == aEntid) + assert(value?.asEntid() == aEntid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testTypedValueAsKw() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a")! + let query = "[:find ?v . :in ?e :where [?e :foo/keyword ?v]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?e", toReference: aEntid) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asKeyword() == ":foo/string") + assert(value?.asKeyword() == ":foo/string") + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testTypedValueAsBoolean() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a")! + let query = "[:find ?v . :in ?e :where [?e :foo/boolean ?v]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?e", toReference: aEntid) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asBool() == true) + assert(value?.asBool() == true) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testTypedValueAsDouble() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a")! + let query = "[:find ?v . :in ?e :where [?e :foo/double ?v]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?e", toReference: aEntid) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asDouble() == 11.23) + assert(value?.asDouble() == 11.23) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testTypedValueAsDate() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a")! + let query = "[:find ?v . :in ?e :where [?e :foo/instant ?v]]" + let expect = expectation(description: "Query is executed") + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + let expectedDate = formatter.date(from: "2017-01-01T11:00:00+00:00") + + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?e", toReference: aEntid) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asDate() == expectedDate) + assert(value?.asDate() == expectedDate) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testTypedValueAsString() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a")! + let query = "[:find ?v . :in ?e :where [?e :foo/string ?v]]" + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?e", toReference: aEntid) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asString() == "The higher we soar the smaller we appear to those who cannot fly.") + assert(value?.asString() == "The higher we soar the smaller we appear to those who cannot fly.") + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testTypedValueAsUuid() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a")! + let query = "[:find ?v . :in ?e :where [?e :foo/uuid ?v]]" + let expectedUuid = UUID(uuidString: "550e8400-e29b-41d4-a716-446655440000")! + let expect = expectation(description: "Query is executed") + XCTAssertNoThrow(try mentat.query(query: query) + .bind(varName: "?e", toReference: aEntid) + .runScalar { value in + XCTAssertNotNil(value) + assert(value?.asUUID() == expectedUuid) + assert(value?.asUUID() == expectedUuid) + expect.fulfill() + }) + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testValueForAttributeOfEntity() { + let mentat = Mentat() + let (_, report) = self.populateWithTypesSchema(mentat: mentat) + let aEntid = report!.entid(forTempId: "a")! + var value: TypedValue? = nil; + XCTAssertNoThrow(value = try mentat.value(forAttribute: ":foo/long", ofEntity: aEntid)) + XCTAssertNotNil(value) + assert(value?.asLong() == 25) + } + + func testEntidForAttribute() { + let mentat = Mentat() + let _ = self.populateWithTypesSchema(mentat: mentat) + let entid = mentat.entidForAttribute(attribute: ":foo/long") + assert(entid == 65540) + } + + func testMultipleQueries() { + let mentat = Mentat() + let _ = self.populateWithTypesSchema(mentat: mentat) + let q1 = mentat.query(query: "[:find ?x :where [?x _ _]]") + + let q1Expect = expectation(description: "Query 1 is executed") + XCTAssertNoThrow(try q1.run { results in + XCTAssertNotNil(results) + q1Expect.fulfill() + }) + + let q2 = mentat.query(query: "[:find ?x :where [_ _ ?x]]") + let q2Expect = expectation(description: "Query 2 is executed") + XCTAssertNoThrow(try q2.run { results in + XCTAssertNotNil(results) + q2Expect.fulfill() + }) + + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testNestedQueries() { + let mentat = Mentat() + let _ = self.populateWithTypesSchema(mentat: mentat) + let q1 = mentat.query(query: "[:find ?x :where [?x _ _]]") + let q2 = mentat.query(query: "[:find ?x :where [_ _ ?x]]") + + let expect = expectation(description: "Query 1 is executed") + XCTAssertNoThrow(try q1.run { results in + XCTAssertNotNil(results) + try? q2.run { results in + XCTAssertNotNil(results) + expect.fulfill() + } + }) + + waitForExpectations(timeout: 1) { error in + if let error = error { + assertionFailure("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + + // TODO: Add tests for transaction observation +} diff --git a/src/conn.rs b/src/conn.rs index 813afe74..585c9943 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -94,7 +94,6 @@ use mentat_tolstoy::Syncer; use uuid::Uuid; use entity_builder::{ - BuildTerms, InProgressBuilder, TermBuilder, }; @@ -647,10 +646,6 @@ impl Store { pub fn unregister_observer(&mut self, key: &String) { self.conn.unregister_observer(key); } - - pub fn assert_datom(&mut self, entid: T, attribute: Keyword, value: TypedValue) -> Result<()> where T: Into { - self.conn.assert_datom(&mut self.sqlite, entid, attribute, value) - } } impl Queryable for Store { @@ -973,18 +968,6 @@ impl Conn { pub fn unregister_observer(&mut self, key: &String) { self.tx_observer_service.lock().unwrap().deregister(key); } - - // TODO: expose the entity builder over FFI and remove the need for this function entirely - // It's really only here in order to keep the FFI layer as thin as possible. - // Once the entity builder is exposed, we can perform all of these functions over FFI from the client. - pub fn assert_datom(&mut self, sqlite: &mut rusqlite::Connection, entid: T, attribute: Keyword, value: TypedValue) -> Result<()> where T: Into { - let in_progress = self.begin_transaction(sqlite)?; - let mut builder = in_progress.builder().describe(entid.into()); - builder.add_kw(&attribute, value)?; - builder.commit() - .map_err(|e| e.into()) - .and(Ok(())) - } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 029ad1e9..d73b9529 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,7 @@ pub use mentat_query::{ pub use mentat_db::{ CORE_SCHEMA_VERSION, DB_SCHEMA_CORE, + AttributeSet, TxObserver, TxReport, new_connection, diff --git a/src/query_builder.rs b/src/query_builder.rs index 2d998eb7..5abeca60 100644 --- a/src/query_builder.rs +++ b/src/query_builder.rs @@ -14,10 +14,12 @@ use std::collections::{ }; use mentat_core::{ + DateTime, Entid, Keyword, Binding, TypedValue, + Utc, ValueType, }; @@ -37,15 +39,15 @@ use errors::{ }; pub struct QueryBuilder<'a> { - sql: String, + query: String, values: BTreeMap, types: BTreeMap, store: &'a mut Store, } impl<'a> QueryBuilder<'a> { - pub fn new(store: &'a mut Store, sql: T) -> QueryBuilder where T: Into { - QueryBuilder { sql: sql.into(), values: BTreeMap::new(), types: BTreeMap::new(), store } + pub fn new(store: &'a mut Store, query: T) -> QueryBuilder where T: Into { + QueryBuilder { query: query.into(), values: BTreeMap::new(), types: BTreeMap::new(), store } } pub fn bind_value(&mut self, var: &str, value: T) -> &mut Self where T: Into { @@ -71,6 +73,12 @@ impl<'a> QueryBuilder<'a> { pub fn bind_instant(&mut self, var: &str, value: i64) -> &mut Self { self.values.insert(Variable::from_valid_name(var), TypedValue::instant(value)); + + self + } + + pub fn bind_date_time(&mut self, var: &str, value: DateTime) -> &mut Self { + self.values.insert(Variable::from_valid_name(var), TypedValue::Instant(value)); self } @@ -84,7 +92,7 @@ impl<'a> QueryBuilder<'a> { let types = ::std::mem::replace(&mut self.types, Default::default()); let query_inputs = QueryInputs::new(types, values)?; let read = self.store.begin_read()?; - read.q_once(&self.sql, query_inputs) + read.q_once(&self.query, query_inputs) } pub fn execute_scalar(&mut self) -> Result> { @@ -116,6 +124,11 @@ mod test { Store, }; + use mentat_core::{ + DateTime, + Utc, + }; + #[test] fn test_scalar_query() { let mut store = Store::open("").expect("store connection");