use nodedb_types::collection::CollectionType;
use nodedb_types::collection_config::{PartitionStrategy, PrimaryEngine};
use nodedb_types::id::DatabaseId;
use nodedb_types::sync::wire::CollectionDescriptor;
use crate::nodedb::collection::CollectionMeta;
pub(crate) fn descriptor_from_meta(
meta: &CollectionMeta,
tenant_id: u64,
) -> Option<CollectionDescriptor> {
if let Some(json) = &meta.descriptor_json {
match sonic_rs::from_str::<CollectionDescriptor>(json) {
Ok(descriptor) => return Some(descriptor),
Err(e) => {
tracing::warn!(
collection = %meta.name,
error = %e,
"descriptor_json present but failed to deserialize; falling back to synthesis"
);
}
}
}
let collection_type = synthesize_collection_type(meta)?;
let primary = if collection_type.is_kv() {
PrimaryEngine::KeyValue
} else {
PrimaryEngine::Document
};
let partition_strategy = PartitionStrategy::default_for_collection_type(&collection_type);
Some(CollectionDescriptor {
tenant_id,
database_id: DatabaseId::DEFAULT,
name: meta.name.clone(),
collection_type,
bitemporal: meta.bitemporal,
crdt: meta.crdt,
fields: meta.fields.clone(),
primary,
vector_primary: None,
partition_strategy,
declared_primary_key: None,
descriptor_version: 1,
})
}
fn synthesize_collection_type(meta: &CollectionMeta) -> Option<CollectionType> {
match meta.collection_type.as_str() {
"document" | "document_schemaless" => Some(CollectionType::document()),
"kv" => {
let Some(config_json) = &meta.config_json else {
tracing::warn!(
collection = %meta.name,
"kv collection missing config_json; cannot announce without a schema"
);
return None;
};
match sonic_rs::from_str::<nodedb_types::KvConfig>(config_json) {
Ok(cfg) => Some(CollectionType::KeyValue(cfg)),
Err(e) => {
tracing::warn!(
collection = %meta.name,
error = %e,
"kv collection config_json failed to deserialize; cannot announce"
);
None
}
}
}
"columnar" | "timeseries" | "spatial" => match meta.collection_type.parse() {
Ok(ct) => Some(ct),
Err(e) => {
tracing::warn!(
collection = %meta.name,
error = %e,
"failed to parse columnar-family collection type; cannot announce"
);
None
}
},
"document_strict" => {
tracing::warn!(
collection = %meta.name,
"strict collection without descriptor_json cannot be announced \
(FromStr yields an empty placeholder schema)"
);
None
}
other => {
tracing::warn!(
collection = %meta.name,
collection_type = other,
"unrecognized collection type; cannot announce"
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base_meta(collection_type: &str) -> CollectionMeta {
CollectionMeta {
name: "widgets".to_string(),
collection_type: collection_type.to_string(),
created_at_ms: 0,
fields: Vec::new(),
config_json: None,
descriptor_json: None,
bitemporal: false,
crdt: false,
}
}
#[test]
fn descriptor_json_roundtrips_verbatim() {
let descriptor = CollectionDescriptor {
tenant_id: 7,
database_id: DatabaseId::new(42),
name: "widgets".into(),
collection_type: CollectionType::document(),
bitemporal: true,
crdt: false,
fields: vec![("email".into(), "string".into())],
primary: PrimaryEngine::Document,
vector_primary: None,
partition_strategy: PartitionStrategy::CollectionHomed,
declared_primary_key: Some("id".into()),
descriptor_version: 3,
};
let mut meta = base_meta("document");
meta.descriptor_json = Some(sonic_rs::to_string(&descriptor).unwrap());
let out = descriptor_from_meta(&meta, 1).expect("descriptor should roundtrip");
assert_eq!(out, descriptor);
let other = descriptor_from_meta(&meta, 99).expect("descriptor should roundtrip");
assert_eq!(other.tenant_id, 7);
}
#[test]
fn synthesized_descriptor_carries_passed_tenant() {
let meta = base_meta("document");
let out = descriptor_from_meta(&meta, 77).expect("document should synthesize");
assert_eq!(out.tenant_id, 77);
}
#[test]
fn document_synthesizes_schemaless() {
let meta = base_meta("document");
let out = descriptor_from_meta(&meta, 1).expect("document should synthesize");
assert!(out.collection_type.is_schemaless());
assert_eq!(out.primary, PrimaryEngine::Document);
}
#[test]
fn kv_rebuilds_from_config_json() {
let schema = nodedb_types::columnar::StrictSchema::new(vec![
nodedb_types::columnar::ColumnDef::required(
"key",
nodedb_types::columnar::ColumnType::String,
)
.with_primary_key(),
])
.unwrap();
let config = nodedb_types::KvConfig {
schema,
ttl: None,
capacity_hint: 0,
inline_threshold: nodedb_types::kv::KV_DEFAULT_INLINE_THRESHOLD,
};
let mut meta = base_meta("kv");
meta.config_json = Some(sonic_rs::to_string(&config).unwrap());
let out = descriptor_from_meta(&meta, 1).expect("kv should rebuild");
assert!(out.collection_type.is_kv());
assert_eq!(out.primary, PrimaryEngine::KeyValue);
}
#[test]
fn kv_without_config_json_is_none() {
let meta = base_meta("kv");
assert!(descriptor_from_meta(&meta, 1).is_none());
}
#[test]
fn strict_without_descriptor_json_is_none() {
let meta = base_meta("document_strict");
assert!(descriptor_from_meta(&meta, 1).is_none());
}
#[test]
fn unrecognized_type_is_none() {
let meta = base_meta("bogus");
assert!(descriptor_from_meta(&meta, 1).is_none());
}
}