use bson::{Bson, Document};
use ciborium::Value as CborValue;
use indexmap::IndexMap;
use vantage_core::Result;
use vantage_dataset::traits::ReadableValueSet;
use vantage_table::table::Table;
use vantage_table::traits::column_like::ColumnLike;
use vantage_types::{EmptyEntity, Record};
use super::coerce::{self, BsonKind};
use super::{MongoTableShell, walk_bson_path};
use crate::id::{MongoId, id_variants};
use crate::mongodb::MongoDB;
use crate::types::{AnyMongoType, MongoType};
use crate::vista::cbor::{bson_to_cbor, cbor_to_bson};
pub(super) fn declared_kinds(
table: &Table<MongoDB, EmptyEntity>,
id_column: Option<&str>,
) -> IndexMap<String, BsonKind> {
table
.columns()
.iter()
.filter(|(name, _)| id_column != Some(name.as_str()))
.filter_map(|(name, col)| {
BsonKind::of_column_type(col.get_type()).map(|kind| (name.clone(), kind))
})
.collect()
}
impl MongoTableShell {
pub(super) fn flatten_for_write(
&self,
record: &Record<CborValue>,
stored: Option<&IndexMap<String, Bson>>,
) -> Result<Record<AnyMongoType>> {
let mut flat = self.flatten_paths(record, stored)?;
for (column, invariant) in self.table.invariants() {
let Some(outgoing) = flat.get(column).map(|v| v.to_bson()) else {
continue;
};
if id_variants(&outgoing) == Some(invariant.to_bson()) {
flat.insert(column.clone(), invariant.clone());
}
}
Ok(flat)
}
fn flatten_paths(
&self,
record: &Record<CborValue>,
stored: Option<&IndexMap<String, Bson>>,
) -> Result<Record<AnyMongoType>> {
let mut top = Document::new();
for (col_name, value) in record {
let fallback;
let path: &[String] = match self.column_paths.get(col_name) {
Some(p) => p.as_slice(),
None => {
fallback = vec![col_name.clone()];
&fallback
}
};
let old = stored.and_then(|row| walk_bson_path(row, path));
let bson_value = match (old, self.declared.get(col_name)) {
(Some(old), _) if bson_to_cbor(&old) == *value => old,
_ if path == ["_id"] => match value {
CborValue::Text(text) => self.parse_id(text).to_bson(),
other => cbor_to_bson(other),
},
(old, Some(kind)) => coerce::declared_over(col_name, *kind, old.as_ref(), value)?,
(Some(old), None) => coerce::preserve(&old, value),
(None, None) => cbor_to_bson(value),
};
if self.column_paths.is_empty() {
top.insert(col_name.clone(), bson_value);
} else {
insert_at_path(&mut top, path, bson_value);
}
}
Ok(top
.into_iter()
.map(|(k, v)| (k, AnyMongoType::untyped(v)))
.collect())
}
pub(super) async fn stored_row(&self, id: &MongoId) -> Result<Option<IndexMap<String, Bson>>> {
Ok(self
.table
.get_value(id.clone())
.await?
.map(|row| row.into_iter().map(|(k, v)| (k, v.into_value())).collect()))
}
}
fn insert_at_path(doc: &mut Document, path: &[String], value: Bson) {
if path.is_empty() {
return;
}
if path.len() == 1 {
doc.insert(path[0].clone(), value);
return;
}
let head = path[0].clone();
let entry = doc
.entry(head)
.or_insert_with(|| Bson::Document(Document::new()));
if !matches!(entry, Bson::Document(_)) {
*entry = Bson::Document(Document::new());
}
if let Bson::Document(sub) = entry {
insert_at_path(sub, &path[1..], value);
}
}
#[cfg(test)]
mod tests;