use std::collections::HashMap;
use serde_json::{json, Map, Value};
use crate::datasource::DataSourceConfig;
use crate::pipeline::{is_nullish, param, Ast, RelAst};
use crate::schema::{Location, Registry};
use super::{select, EdgeSpec, UnitSpec};
fn ensure_field(fields: &mut Vec<String>, field: &str) {
if field.is_empty() || field == "_id" {
return;
}
if !fields.iter().any(|f| f == field) {
fields.push(field.to_string());
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn walk(
parent_model: &str,
ds_cfg: &DataSourceConfig,
pinned: &HashMap<String, Location>,
fields: &mut Vec<String>,
relations: &mut Vec<(String, RelAst)>,
path: &[String],
params: &Map<String, Value>,
registry: &Registry,
units: &mut Vec<UnitSpec>,
edges: &mut Vec<EdgeSpec>,
degraded: &mut Vec<Value>,
) -> Result<(), String> {
let parent_schema = registry.get(parent_model)?.clone();
let parent_loc = pinned
.get(parent_model)
.cloned()
.unwrap_or_else(|| parent_schema.location());
let mut i = 0usize;
while i < relations.len() {
let name = relations[i].0.clone();
let Some(rel_def) = parent_schema.relations.get(&name).cloned() else {
i += 1;
continue;
};
if rel_def.model.is_empty() {
i += 1;
continue;
}
let child_schema = registry.get(&rel_def.model)?.clone();
let child_loc = pinned
.get(&child_schema.name)
.cloned()
.unwrap_or_else(|| child_schema.location());
let mut child_path = path.to_vec();
child_path.push(name.clone());
{
let (_, rel_ast) = &mut relations[i];
walk(
&rel_def.model,
ds_cfg,
pinned,
&mut rel_ast.fields,
&mut rel_ast.relations,
&child_path,
params,
registry,
units,
edges,
degraded,
)?;
}
if select::compatible(&parent_loc, &child_loc, ds_cfg) {
i += 1;
continue;
}
if rel_def.rel_type != "one" && rel_def.rel_type != "many" {
return Err(format!(
"跨源关系 {}.{} 的 cardinality 非法(须为 one/many,实际 {:?})",
parent_model, name, rel_def.rel_type
));
}
let (_, rel_ast) = relations.remove(i);
ensure_field(fields, &rel_def.local_field);
let mut child_ast = Ast {
model: rel_def.model.clone(),
params: rel_ast.params.clone(),
fields: rel_ast.fields.clone(),
relations: rel_ast.relations.clone(),
};
ensure_field(&mut child_ast.fields, &rel_def.foreign_field);
for key in ["sort", "skip", "limit"] {
if let Some(r) = rel_ast.params.get(key) {
if !is_nullish(param(params, Some(r))) {
degraded.push(json!({
"code": "crossSourceChildPaging",
"layer": "federation",
"message": format!(
"跨源关系 {}.{} 的 ${} 无法按父下推",
parent_model, name, key
),
"hint": "把该关系的 $sort/$skip/$limit 上移到根查询,或由 Host 在内存 join 后自行分页",
}));
}
}
}
let key = (units.len() + 1).to_string();
let depth = path.len();
units.push(UnitSpec {
key: key.clone(),
source: child_loc.source.clone(),
database: child_loc.database.clone(),
schema: child_loc.schema.clone(),
model: rel_def.model.clone(),
ast: child_ast,
depth,
});
edges.push(EdgeSpec {
parent_model: parent_model.to_string(),
rel: name,
local: rel_def.local_field.clone(),
foreign: rel_def.foreign_field.clone(),
cardinality: rel_def.rel_type.clone(),
path: path.to_vec(),
key,
});
}
Ok(())
}
pub(super) fn detect_cross_source_sort(
fetch_ast: &Ast,
params: &Map<String, Value>,
edges: &[EdgeSpec],
degraded: &mut Vec<Value>,
) {
let cross_root_rels: Vec<&str> = edges
.iter()
.filter(|e| e.path.is_empty())
.map(|e| e.rel.as_str())
.collect();
if cross_root_rels.is_empty() {
return;
}
let Some(r) = fetch_ast.params.get("sort") else {
return;
};
let Some(Value::Object(sort)) = param(params, Some(r)) else {
return;
};
for k in sort.keys() {
let prefix = k.split('.').next().unwrap_or("");
if cross_root_rels.contains(&prefix) {
degraded.push(json!({
"code": "crossSourceSort",
"layer": "federation",
"message": format!("跨源排序无法下推: {}", k),
"hint": "排序改用本源字段,或由 Host 在内存 join 后自行排序",
}));
}
}
}