use nodedb_types::DEFAULT_IDENTITY_COLUMN;
use nodedb_types::error::{NodeDbError, NodeDbResult};
use nodedb_types::value::Value;
const DESCRIBE_FIELD_COLUMN: &str = "field";
const DESCRIBE_KEY_COLUMN: &str = "primary_key";
pub(crate) fn is_identity_cell(name: &str, value: &Value, document_id: &str) -> bool {
name == DEFAULT_IDENTITY_COLUMN && matches!(value, Value::String(s) if s == document_id)
}
pub(crate) fn identity_column_from_describe(
collection: &str,
columns: &[String],
rows: &[Vec<Value>],
is_key: fn(&Value) -> Option<bool>,
) -> NodeDbResult<String> {
let index = |name: &str| {
columns.iter().position(|c| c == name).ok_or_else(|| {
describe_error(
collection,
format!("result has no '{name}' column; columns are {columns:?}"),
)
})
};
let field_idx = index(DESCRIBE_FIELD_COLUMN)?;
let key_idx = index(DESCRIBE_KEY_COLUMN)?;
let mut keys = Vec::new();
for row in rows {
let cell = row.get(key_idx).ok_or_else(|| {
describe_error(collection, format!("row {row:?} has no primary_key cell"))
})?;
let marked = is_key(cell).ok_or_else(|| {
describe_error(
collection,
format!("primary_key cell {cell:?} is not a bool"),
)
})?;
if !marked {
continue;
}
match row.get(field_idx) {
Some(Value::String(field)) => keys.push(field.clone()),
other => {
return Err(describe_error(
collection,
format!("key row names no field: {other:?}"),
));
}
}
}
match keys.as_slice() {
[key] => Ok(key.clone()),
[] => Err(describe_error(collection, "no field is the primary key")),
several => Err(describe_error(
collection,
format!("several fields are the primary key: {several:?}"),
)),
}
}
fn describe_error(collection: &str, detail: impl std::fmt::Display) -> NodeDbError {
NodeDbError::serialization(
"describe",
format!("identity column of '{collection}': {detail}"),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn text_bool(cell: &Value) -> Option<bool> {
match cell {
Value::String(s) if s == "t" => Some(true),
Value::String(s) if s == "f" => Some(false),
_ => None,
}
}
fn describe(rows: &[(&str, &str)]) -> (Vec<String>, Vec<Vec<Value>>) {
let columns = ["field", "type", "nullable", "primary_key"]
.iter()
.map(|c| c.to_string())
.collect();
let rows = rows
.iter()
.map(|(field, key)| {
vec![
Value::String((*field).into()),
Value::String("TEXT".into()),
Value::String("true".into()),
Value::String((*key).into()),
]
})
.collect();
(columns, rows)
}
#[test]
fn only_the_matching_id_cell_is_the_identity() {
assert!(is_identity_cell("id", &Value::String("d1".into()), "d1"));
assert!(!is_identity_cell(
"id",
&Value::String("other".into()),
"d1"
));
assert!(!is_identity_cell("id", &Value::Integer(1), "d1"));
assert!(!is_identity_cell("body", &Value::String("d1".into()), "d1"));
}
#[test]
fn the_describe_key_row_names_the_identity_column() {
let (columns, rows) = describe(&[("id", "f"), ("sku", "t"), ("name", "f")]);
let column =
identity_column_from_describe("c", &columns, &rows, text_bool).expect("one key row");
assert_eq!(column, "sku");
}
#[test]
fn a_describe_result_without_one_key_row_is_an_error() {
let (columns, rows) = describe(&[("id", "f"), ("name", "f")]);
let err =
identity_column_from_describe("c", &columns, &rows, text_bool).expect_err("no key row");
assert!(err.to_string().contains("'c'"), "{err}");
let (columns, rows) = describe(&[("a", "t"), ("b", "t")]);
assert!(identity_column_from_describe("c", &columns, &rows, text_bool).is_err());
let (columns, rows) = describe(&[("a", "maybe")]);
let err =
identity_column_from_describe("c", &columns, &rows, text_bool).expect_err("not a bool");
assert!(err.to_string().contains("not a bool"), "{err}");
let err = identity_column_from_describe("c", &columns[..3], &rows, text_bool)
.expect_err("no primary_key column");
assert!(err.to_string().contains("'primary_key'"), "{err}");
}
}