diff --git a/agdb/src/db/db_value.rs b/agdb/src/db/db_value.rs index 31f505f55..9b6903562 100644 --- a/agdb/src/db/db_value.rs +++ b/agdb/src/db/db_value.rs @@ -94,6 +94,24 @@ impl DbValue { } } + /// Returns `bool` possibly converted from `i64`, `u64`, `f64` or `string`. + /// For numerical types any non-zero value will be `true`. For `string` only + /// "1" or "true" will be `true` and all other string values will be `false`. + /// Conversion from bytes or vectorized types is an error. + pub fn to_bool(&self) -> Result { + match self { + DbValue::Bytes(_) => Self::type_error("bytes", "bool"), + DbValue::I64(v) => Ok(*v != 0), + DbValue::U64(v) => Ok(*v != 0), + DbValue::F64(v) => Ok(*v != 0.0.into()), + DbValue::String(v) => Ok(v == "true" || v == "1"), + DbValue::VecI64(_) => Self::type_error("Vec", "bool"), + DbValue::VecU64(_) => Self::type_error("Vec", "bool"), + DbValue::VecF64(_) => Self::type_error("Vec", "bool"), + DbValue::VecString(_) => Self::type_error("Vec", "bool"), + } + } + /// Returns `DbF64` possibly converted from `i64` or `u64` /// or na error if the conversion failed or the value is of /// a different type. @@ -209,6 +227,24 @@ impl DbValue { } } + /// Returns `Vec` possibly converted from `bytes` or vectorized types. + /// For numerical types any non-zero value will be `true`. For `string` only + /// "1" or "true" will be `true` and all other string values will be `false`. + /// Conversion from i64, u64, f64 and string is an error. + pub fn vec_bool(&self) -> Result, DbError> { + match self { + DbValue::Bytes(v) => Ok(v.iter().map(|b| *b != 0).collect()), + DbValue::I64(_) => Self::type_error("i64", "Vec"), + DbValue::U64(_) => Self::type_error("u64", "Vec"), + DbValue::F64(_) => Self::type_error("f64", "Vec"), + DbValue::String(_) => Self::type_error("string", "Vec"), + DbValue::VecI64(v) => Ok(v.iter().map(|i| *i != 0).collect()), + DbValue::VecU64(v) => Ok(v.iter().map(|i| *i != 0).collect()), + DbValue::VecF64(v) => Ok(v.iter().map(|i| *i != 0.0.into()).collect()), + DbValue::VecString(v) => Ok(v.iter().map(|s| s == "true" || s == "1").collect()), + } + } + pub(crate) fn load_db_value( value_index: DbValueIndex, storage: &Storage, @@ -389,6 +425,12 @@ impl From<&str> for DbValue { } } +impl From for DbValue { + fn from(value: bool) -> Self { + DbValue::U64(if value { 1 } else { 0 }) + } +} + impl From> for DbValue { fn from(value: Vec) -> Self { DbValue::VecF64(value.iter().map(|i| (*i).into()).collect()) @@ -449,6 +491,12 @@ impl From> for DbValue { } } +impl From> for DbValue { + fn from(value: Vec) -> Self { + DbValue::Bytes(value.iter().map(|b| *b as u8).collect()) + } +} + impl TryFrom for Vec { type Error = DbError; @@ -583,6 +631,22 @@ impl TryFrom for Vec { } } +impl TryFrom for bool { + type Error = DbError; + + fn try_from(value: DbValue) -> Result { + value.to_bool() + } +} + +impl TryFrom for Vec { + type Error = DbError; + + fn try_from(value: DbValue) -> Result { + value.vec_bool() + } +} + impl Display for DbValue { fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { match self { @@ -1289,4 +1353,109 @@ mod tests { )) ); } + + #[test] + fn to_bool() { + assert!(DbValue::from(true).to_bool().unwrap()); + assert!(DbValue::from(1_u64).to_bool().unwrap()); + assert!(DbValue::from(10_i64).to_bool().unwrap()); + assert!(DbValue::from(1.1).to_bool().unwrap()); + assert!(DbValue::from("true").to_bool().unwrap()); + assert!(DbValue::from("1").to_bool().unwrap()); + + assert!(!DbValue::from(false).to_bool().unwrap()); + assert!(!DbValue::from(0_u64).to_bool().unwrap()); + assert!(!DbValue::from(0_i64).to_bool().unwrap()); + assert!(!DbValue::from(0.0).to_bool().unwrap()); + assert!(!DbValue::from("").to_bool().unwrap()); + assert!(!DbValue::from("2").to_bool().unwrap()); + + assert_eq!( + DbValue::from(vec![0_u8]).to_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'bytes' to 'bool'." + )) + ); + assert_eq!( + DbValue::from(vec![1_i64]).to_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'Vec' to 'bool'." + )) + ); + assert_eq!( + DbValue::from(vec![1_u64]).to_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'Vec' to 'bool'." + )) + ); + assert_eq!( + DbValue::from(vec![0.0_f64]).to_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'Vec' to 'bool'." + )) + ); + assert_eq!( + DbValue::from(vec!["a", ""]).to_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'Vec' to 'bool'." + )) + ); + assert_eq!( + DbValue::from(vec![true, false]).to_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'bytes' to 'bool'." + )) + ); + } + + #[test] + fn vec_bool() { + assert_eq!( + DbValue::from(vec![true, false]).vec_bool().unwrap(), + vec![true, false] + ); + assert_eq!( + DbValue::from(vec![1_i64, 0, -2]).vec_bool().unwrap(), + vec![true, false, true] + ); + assert_eq!( + DbValue::from(vec![1_u64, 0, 2]).vec_bool().unwrap(), + vec![true, false, true] + ); + assert_eq!( + DbValue::from(vec![1.1_f64, 0.0, 2.2]).vec_bool().unwrap(), + vec![true, false, true] + ); + assert_eq!( + DbValue::from(vec!["true", "1", "", "0", "false", "2"]) + .vec_bool() + .unwrap(), + vec![true, true, false, false, false, false] + ); + + assert_eq!( + DbValue::from(1_i64).vec_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'i64' to 'Vec'." + )) + ); + assert_eq!( + DbValue::from(1_u64).vec_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'u64' to 'Vec'." + )) + ); + assert_eq!( + DbValue::from(1.1_f64).vec_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'f64' to 'Vec'." + )) + ); + assert_eq!( + DbValue::from("true").vec_bool(), + Err(DbError::from( + "Type mismatch. Cannot convert 'string' to 'Vec'." + )) + ); + } } diff --git a/agdb/tests/db_user_value_test.rs b/agdb/tests/db_user_value_test.rs index da7eadbb4..611eefa78 100644 --- a/agdb/tests/db_user_value_test.rs +++ b/agdb/tests/db_user_value_test.rs @@ -27,6 +27,21 @@ struct User { status: Status, } +#[derive(UserValue)] +struct MyValue { + db_id: Option, + name: String, + age: u64, +} + +#[derive(UserValue, PartialEq, Debug)] +struct MyValueWithBool { + db_id: Option, + name: String, + is_active: bool, + truths: Vec, +} + impl From for DbValue { fn from(value: Status) -> Self { match value { @@ -450,13 +465,6 @@ fn try_from_db_element() { assert_eq!(user.password, "pswd"); } -#[derive(UserValue)] -struct MyValue { - db_id: Option, - name: String, - age: u64, -} - #[test] fn insert_element_alias_update_values() { let mut db = TestDb::new(); @@ -545,3 +553,48 @@ fn insert_element_missing_id() { "Id '1' not found", ); } + +#[test] +fn insert_element_bool() { + let mut db = TestDb::new(); + let mut my_value = MyValueWithBool { + db_id: None, + name: "my name".to_string(), + is_active: true, + truths: vec![true, false], + }; + db.exec_mut(QueryBuilder::insert().element(&my_value).query(), 3); + let my_value_from_db: MyValueWithBool = db + .exec_result(QueryBuilder::select().ids(1).query()) + .try_into() + .unwrap(); + my_value.db_id = Some(QueryId::Id(DbId(1))); + assert_eq!(my_value, my_value_from_db); +} + +#[test] +fn insert_element_to_bool_conversion() { + let mut db = TestDb::new(); + db.exec_mut( + QueryBuilder::insert() + .nodes() + .values(vec![vec![ + ("name", "my name").into(), + ("is_active", 50).into(), + ("truths", vec![1, 0]).into(), + ]]) + .query(), + 1, + ); + let my_value_from_db: MyValueWithBool = db + .exec_result(QueryBuilder::select().ids(1).query()) + .try_into() + .unwrap(); + let expected = MyValueWithBool { + db_id: Some(QueryId::Id(DbId(1))), + name: "my name".to_string(), + is_active: true, + truths: vec![true, false], + }; + assert_eq!(expected, my_value_from_db); +} diff --git a/docs/queries.md b/docs/queries.md index 9eff2b2a6..83010e812 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -167,7 +167,7 @@ pub trait DbUserValue: Sized { Typically you would derive this trait with `agdb::UserValue` procedural macro that uses the field names as keys (of type `String`) and loss-lessly converts the values when reading/writing from/to the database from supported types (e.g. field type `i32` will become `i64` in the database). -It is recommended but optional to have `db_id` field of type `Option` in your user defined types which will further allow you to directly update your values with a query shorthands. However it is optional and all other features will still work including conversion from `QueryResult` or passing your types to `values()` in the builders: +It is recommended but optional to have `db_id` field of type `Option>` (e.g. `Option` or `Option`) in your user defined types which will further allow you to directly update your values with a query shorthands. However it is optional and all other features will still work including conversion from `QueryResult` or passing your types to `values()` in the builders: ```Rust #[derive(UserValue)] @@ -191,6 +191,13 @@ Types not directly used in the database but for which the conversions are suppor - Vec <=> Vec - &str => String (only one way conversion to `String`) - Vec<&str> => Vec (only one way conversion to `Vec`) +- bool (\*) + +\* The boolean type is not a native type in the `agdb` but you can still use it in your types in any language. The `bool` type will be converted to `u64` (0 == false, 1 == true). The `Vec` type will be converted to `Vec` (bytes, 0 == false, 1 == true). The conversion back to `bool` is possible from wider range of values - the same rules apply for vectorized version which however cannot be converted to from single values: + +- u64 / i64: any non-zero value will be `true` +- f64: any value except `0.0` will be `true` +- string: only `"true"` or `"1"` will be `true` # QueryResult @@ -249,6 +256,7 @@ The enum variants can be conveniently accessed through methods named after each ```Rust fn bytes(&self) -> Result<&Vec, DbError>; +fn to_bool(&self) -> Result; fn to_f64(&self) -> Result; fn to_i64(&self) -> Result; fn to_u64(&self) -> Result; @@ -258,10 +266,10 @@ fn vec_f64(&self) -> Result<&Vec, DbError>; fn vec_i64(&self) -> Result<&Vec, DbError>; fn vec_u64(&self) -> Result<&Vec, DbError>; fn vec_string(&self) -> Result<&Vec, DbError>; - +fn vec_bool(&self) -> Result, DbError>; ``` -The numerical variants (`I64`, `U64`, `DbF64`) will attempt loss-less conversions where possible. To avoid copies all other variants return `&` where conversions are not possible even if they could be done in theory. The special case is `to_string()` provided by the `Display` trait. It converts any values into string (it also copies the `String` variant) and performs possibly lossy conversion from `Bytes` to UTF-8 string. +The numerical variants (`I64`, `U64`, `DbF64`) will attempt loss-less conversions where possible. To avoid copies all other variants return `&` where conversions are not possible even if they could be done in theory. The special case is `to_string()` provided by the `Display` trait. It converts any values into string (it also copies the `String` variant) and performs possibly lossy conversion from `Bytes` to UTF-8 string. For bool conversion details refer to [DbUserValue](#dbuservalue) section. # QueryError @@ -533,11 +541,10 @@ QueryBuilder::insert().values_uniform(vec![("k", "v").into(), (1, 10).into()]).i Inserts or updates key-value pairs (properties) of existing elements or insert new elements (nodes). You need to specify the `ids` [`QueryIds`](#queryids--queryid) and the list of `values`. The `values` can be either [`QueryValues::Single`](#queryvalues) that will insert the single set of properties to all elements identified by `ids` or [`QueryValues::Multi`](#queryvalues) that will insert to each `id` its own set of properties but their number must match the number of `ids`. If the user defined type contains `db_id` field of type `Option>` you can use the shorthand `insert().element() / .insert().elements()` that will infer the values and ids from your types. The `values()` will be inferred from user defined types if they implement `DbUserValue` trait (`#derive(agdb::UserValue)`). Both singular nad vectorized versions are supported. -If an id is non-0 or an existing alias that element will be updated in the database with provided values. - -If an id is `0` or an non-existent alias new element (node) will be inserted into the database with that alias. +- If an id is non-0 or an existing alias that element will be updated in the database with provided values. +- If an id is `0` or an non-existent alias new element (node) will be inserted into the database with that alias. -Note that this query is insert-or-update for both nodes and existing values. By inserting the same `key` its old value will be overwritten with the new one. +Note: that this query is insert-or-update for both nodes and existing values. By inserting the same `key` its old value will be overwritten with the new one. ## Remove