use bson::{Bson, Document, doc};
use super::MongoSelect;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct JoinHop {
pub from: String,
pub local_field: String,
pub foreign_field: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct JoinColumn {
pub name: String,
pub hops: Vec<JoinHop>,
pub field: String,
}
const ALIAS_PREFIX: &str = "__vc";
pub(crate) fn join_alias(i: usize) -> String {
format!("{ALIAS_PREFIX}{i}")
}
pub(crate) fn alias_index(field: &str) -> Option<usize> {
let digits = field.strip_prefix(ALIAS_PREFIX)?;
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
digits.parse().ok()
}
fn temp_field(k: usize) -> String {
format!("__vj{k}")
}
fn names_alias(path: &str, count: usize) -> bool {
let head = path.split('.').next().unwrap_or(path);
alias_index(head).is_some_and(|i| i < count)
}
pub(crate) fn filter_uses_joins(filter: &Document, count: usize) -> bool {
count > 0
&& filter
.iter()
.any(|(k, v)| names_alias(k, count) || mentions_joins(v, count))
}
fn mentions_joins(value: &Bson, count: usize) -> bool {
match value {
Bson::Document(d) => filter_uses_joins(d, count),
Bson::Array(items) => items.iter().any(|v| mentions_joins(v, count)),
Bson::String(s) => s.strip_prefix('$').is_some_and(|p| names_alias(p, count)),
_ => false,
}
}
impl MongoSelect {
pub(crate) fn join_stages(&self) -> Vec<Document> {
if self.joins.is_empty() {
return Vec::new();
}
let mut stages = Vec::new();
let mut chains: Vec<&[JoinHop]> = Vec::new();
let mut values = Document::new();
for (i, column) in self.joins.iter().enumerate() {
let mut parent: Option<usize> = None;
for depth in 1..=column.hops.len() {
let chain = &column.hops[..depth];
let k = match chains.iter().position(|c| *c == chain) {
Some(k) => k,
None => {
let hop = &chain[depth - 1];
let local = match parent {
Some(p) => format!("{}.{}", temp_field(p), hop.local_field),
None => hop.local_field.clone(),
};
let temp = temp_field(chains.len());
stages.push(doc! { "$lookup": {
"from": &hop.from,
"localField": local,
"foreignField": &hop.foreign_field,
"as": &temp,
} });
stages.push(doc! { "$set": { &temp: { "$first": format!("${temp}") } } });
chains.push(chain);
chains.len() - 1
}
};
parent = Some(k);
}
if let Some(k) = parent {
values.insert(
join_alias(i),
format!("${}.{}", temp_field(k), column.field),
);
}
}
stages.push(doc! { "$addFields": values });
let temps: Vec<String> = (0..chains.len()).map(temp_field).collect();
stages.push(doc! { "$unset": temps });
stages
}
pub(crate) fn sort_uses_joins(&self) -> bool {
self.sort
.iter()
.any(|(field, _)| names_alias(field, self.joins.len()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hop(from: &str, local: &str) -> JoinHop {
JoinHop {
from: from.into(),
local_field: local.into(),
foreign_field: "_id".into(),
}
}
fn column(name: &str, hops: Vec<JoinHop>, field: &str) -> JoinColumn {
JoinColumn {
name: name.into(),
hops,
field: field.into(),
}
}
#[test]
fn one_hop_join_renders_lookup_alias_and_cleanup() {
let mut s = MongoSelect::new();
s.joins = vec![column(
"client.name",
vec![hop("clients", "client_id")],
"name",
)];
assert_eq!(
s.join_stages(),
vec![
doc! { "$lookup": { "from": "clients", "localField": "client_id", "foreignField": "_id", "as": "__vj0" } },
doc! { "$set": { "__vj0": { "$first": "$__vj0" } } },
doc! { "$addFields": { "__vc0": "$__vj0.name" } },
doc! { "$unset": ["__vj0"] },
]
);
}
#[test]
fn two_hop_join_chains_through_the_first_lookup() {
let mut s = MongoSelect::new();
s.joins = vec![column(
"client.country.name",
vec![hop("clients", "client_id"), hop("countries", "country_id")],
"name",
)];
assert_eq!(
s.join_stages(),
vec![
doc! { "$lookup": { "from": "clients", "localField": "client_id", "foreignField": "_id", "as": "__vj0" } },
doc! { "$set": { "__vj0": { "$first": "$__vj0" } } },
doc! { "$lookup": { "from": "countries", "localField": "__vj0.country_id", "foreignField": "_id", "as": "__vj1" } },
doc! { "$set": { "__vj1": { "$first": "$__vj1" } } },
doc! { "$addFields": { "__vc0": "$__vj1.name" } },
doc! { "$unset": ["__vj0", "__vj1"] },
]
);
}
#[test]
fn columns_sharing_a_hop_share_its_lookup() {
let mut s = MongoSelect::new();
let client = hop("clients", "client_id");
s.joins = vec![
column("client.name", vec![client.clone()], "name"),
column("client.city", vec![client], "address.city"),
];
let stages = s.join_stages();
assert_eq!(stages.len(), 4, "{stages:?}");
assert_eq!(
stages[2],
doc! { "$addFields": { "__vc0": "$__vj0.name", "__vc1": "$__vj0.address.city" } }
);
}
#[test]
fn no_joins_no_stages() {
assert!(MongoSelect::new().join_stages().is_empty());
}
#[test]
fn alias_index_parses_only_aliases() {
assert_eq!(join_alias(3), "__vc3");
assert_eq!(alias_index("__vc3"), Some(3));
assert_eq!(alias_index("__vc"), None);
assert_eq!(alias_index("__vc1x"), None);
assert_eq!(alias_index("name"), None);
}
#[test]
fn filter_mentions_join_aliases_anywhere() {
let filter = doc! { "$and": [ { "n": 1 }, { "__vc1": { "$regex": "a" } } ] };
assert!(filter_uses_joins(&filter, 2));
assert!(!filter_uses_joins(&filter, 1));
assert!(!filter_uses_joins(&doc! { "__vc0": 1 }, 0));
let expr = doc! { "$expr": { "$eq": ["$__vc0", "x"] } };
assert!(filter_uses_joins(&expr, 1));
assert!(!filter_uses_joins(&doc! { "client.name": "x" }, 1));
}
}