Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[db] Support bool conversion in db values #1088 #1115

Merged
merged 2 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions agdb/src/db/db_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool, DbError> {
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<i64>", "bool"),
DbValue::VecU64(_) => Self::type_error("Vec<u64>", "bool"),
DbValue::VecF64(_) => Self::type_error("Vec<f64>", "bool"),
DbValue::VecString(_) => Self::type_error("Vec<string>", "bool"),
}
}

/// Returns `DbF64` possibly converted from `i64` or `u64`
/// or na error if the conversion failed or the value is of
/// a different type.
Expand Down Expand Up @@ -209,6 +227,24 @@ impl DbValue {
}
}

/// Returns `Vec<bool>` 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<Vec<bool>, DbError> {
match self {
DbValue::Bytes(v) => Ok(v.iter().map(|b| *b != 0).collect()),
DbValue::I64(_) => Self::type_error("i64", "Vec<bool>"),
DbValue::U64(_) => Self::type_error("u64", "Vec<bool>"),
DbValue::F64(_) => Self::type_error("f64", "Vec<bool>"),
DbValue::String(_) => Self::type_error("string", "Vec<bool>"),
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<D: StorageData>(
value_index: DbValueIndex,
storage: &Storage<D>,
Expand Down Expand Up @@ -389,6 +425,12 @@ impl From<&str> for DbValue {
}
}

impl From<bool> for DbValue {
fn from(value: bool) -> Self {
DbValue::U64(if value { 1 } else { 0 })
}
}

impl From<Vec<f32>> for DbValue {
fn from(value: Vec<f32>) -> Self {
DbValue::VecF64(value.iter().map(|i| (*i).into()).collect())
Expand Down Expand Up @@ -449,6 +491,12 @@ impl From<Vec<u8>> for DbValue {
}
}

impl From<Vec<bool>> for DbValue {
fn from(value: Vec<bool>) -> Self {
DbValue::Bytes(value.iter().map(|b| *b as u8).collect())
}
}

impl TryFrom<DbValue> for Vec<u8> {
type Error = DbError;

Expand Down Expand Up @@ -583,6 +631,22 @@ impl TryFrom<DbValue> for Vec<String> {
}
}

impl TryFrom<DbValue> for bool {
type Error = DbError;

fn try_from(value: DbValue) -> Result<Self, Self::Error> {
value.to_bool()
}
}

impl TryFrom<DbValue> for Vec<bool> {
type Error = DbError;

fn try_from(value: DbValue) -> Result<Self, Self::Error> {
value.vec_bool()
}
}

impl Display for DbValue {
fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
match self {
Expand Down Expand Up @@ -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<i64>' to 'bool'."
))
);
assert_eq!(
DbValue::from(vec![1_u64]).to_bool(),
Err(DbError::from(
"Type mismatch. Cannot convert 'Vec<u64>' to 'bool'."
))
);
assert_eq!(
DbValue::from(vec![0.0_f64]).to_bool(),
Err(DbError::from(
"Type mismatch. Cannot convert 'Vec<f64>' to 'bool'."
))
);
assert_eq!(
DbValue::from(vec!["a", ""]).to_bool(),
Err(DbError::from(
"Type mismatch. Cannot convert 'Vec<string>' 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<bool>'."
))
);
assert_eq!(
DbValue::from(1_u64).vec_bool(),
Err(DbError::from(
"Type mismatch. Cannot convert 'u64' to 'Vec<bool>'."
))
);
assert_eq!(
DbValue::from(1.1_f64).vec_bool(),
Err(DbError::from(
"Type mismatch. Cannot convert 'f64' to 'Vec<bool>'."
))
);
assert_eq!(
DbValue::from("true").vec_bool(),
Err(DbError::from(
"Type mismatch. Cannot convert 'string' to 'Vec<bool>'."
))
);
}
}
67 changes: 60 additions & 7 deletions agdb/tests/db_user_value_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ struct User {
status: Status,
}

#[derive(UserValue)]
struct MyValue {
db_id: Option<QueryId>,
name: String,
age: u64,
}

#[derive(UserValue, PartialEq, Debug)]
struct MyValueWithBool {
db_id: Option<QueryId>,
name: String,
is_active: bool,
truths: Vec<bool>,
}

impl From<Status> for DbValue {
fn from(value: Status) -> Self {
match value {
Expand Down Expand Up @@ -450,13 +465,6 @@ fn try_from_db_element() {
assert_eq!(user.password, "pswd");
}

#[derive(UserValue)]
struct MyValue {
db_id: Option<QueryId>,
name: String,
age: u64,
}

#[test]
fn insert_element_alias_update_values() {
let mut db = TestDb::new();
Expand Down Expand Up @@ -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);
}
21 changes: 14 additions & 7 deletions docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<DbId>` 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<T: Into<DbId>>` (e.g. `Option<QueryId>` or `Option<DbId>`) 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)]
Expand All @@ -191,6 +191,13 @@ Types not directly used in the database but for which the conversions are suppor
- Vec<f32> <=> Vec<f64>
- &str => String (only one way conversion to `String`)
- Vec<&str> => Vec<String> (only one way conversion to `Vec<String>`)
- 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<bool>` type will be converted to `Vec<u8>` (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

Expand Down Expand Up @@ -249,6 +256,7 @@ The enum variants can be conveniently accessed through methods named after each

```Rust
fn bytes(&self) -> Result<&Vec<u8>, DbError>;
fn to_bool(&self) -> Result<bool, DbError>;
fn to_f64(&self) -> Result<DbF64, DbError>;
fn to_i64(&self) -> Result<i64, DbError>;
fn to_u64(&self) -> Result<u64, DbError>;
Expand All @@ -258,10 +266,10 @@ fn vec_f64(&self) -> Result<&Vec<DbF64>, DbError>;
fn vec_i64(&self) -> Result<&Vec<i64>, DbError>;
fn vec_u64(&self) -> Result<&Vec<u64>, DbError>;
fn vec_string(&self) -> Result<&Vec<String>, DbError>;

fn vec_bool(&self) -> Result<Vec<bool>, 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

Expand Down Expand Up @@ -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<T: Into<QueryId>>` 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

Expand Down