use std::collections::HashMap;
use serde_json::{json, Map, Value};
use crate::command::{
build_plan, check_readable_relations, ensure_context, has_pipeline, plan_query_ast_mut,
ERR_PERMISSION, T2Q_MAX_FEDERATION_ROWS,
};
use crate::computes::{merge_depends_into_ast, InjectInfo};
use crate::datasource::DataSourceConfig;
use crate::permission::{can_read_schema, merge_owner_condition, Context};
use crate::pipeline::{flatten_object_fields, parse_gql, Ast};
use crate::schema::{Profile, Registry, Schema};
use route::{detect_cross_source_sort, walk};
mod route;
pub const FEDERATION_VERSION: u64 = 2;
#[derive(Debug, Clone, PartialEq)]
pub(super) struct Loc {
pub(super) source: String,
pub(super) namespace: Option<String>,
}
pub(super) fn loc_of(schema: &Schema) -> Loc {
Loc {
source: schema.source().to_string(),
namespace: schema.namespace.clone(),
}
}
struct UnitSpec {
key: String,
source: String,
namespace: Option<String>,
model: String,
ast: Ast,
depth: usize,
}
struct EdgeSpec {
parent_model: String,
rel: String,
local: String,
foreign: String,
cardinality: String,
path: Vec<String>,
key: String,
}
pub fn plan_federated(
gql: &str,
params: &Map<String, Value>,
registry: &Registry,
ctx: Option<&Context>,
ds_config: &Value,
) -> Result<Value, String> {
let ds_cfg = DataSourceConfig::from_json(ds_config)?;
let max_rows_per_source = if registry.profile() == Profile::Text2Query {
T2Q_MAX_FEDERATION_ROWS
} else {
super::MAX_FEDERATION_ROWS
};
let mut params = params.clone();
let mut ast = parse_gql(gql)?;
ensure_context(registry, ctx)?;
let root_schema = registry.get(&ast.model)?.clone();
if ctx.is_some() && !can_read_schema(&root_schema, ctx) {
return Err(ERR_PERMISSION.to_string());
}
if ctx.is_some() {
if let Some(r) = ast.params.get("condition").cloned() {
let key = r.get(1..).unwrap_or("").to_string();
match merge_owner_condition(&root_schema, ctx, params.get(&key).cloned()) {
Some(v) => {
params.insert(key, v);
}
None => {
params.remove(&key);
}
}
}
}
let has_pipeline = has_pipeline(&ast, ¶ms);
let inject = if has_pipeline {
InjectInfo::default()
} else {
merge_depends_into_ast(&mut ast.relations, &root_schema)?
};
check_readable_relations(&ast.relations, &root_schema, registry, ctx)?;
let mut post_ast = ast.clone();
if !has_pipeline {
flatten_object_fields(&mut post_ast, &root_schema);
}
let root_loc = loc_of(&root_schema);
let mut units: Vec<UnitSpec> = Vec::new();
let mut edges: Vec<EdgeSpec> = Vec::new();
let mut degraded: Vec<Value> = Vec::new();
let mut fetch_ast = ast;
walk(
&fetch_ast.model,
&ds_cfg,
&mut fetch_ast.fields,
&mut fetch_ast.relations,
&[],
¶ms,
registry,
&mut units,
&mut edges,
&mut degraded,
)?;
detect_cross_source_sort(&fetch_ast, ¶ms, &edges, &mut degraded);
if has_pipeline && !edges.is_empty() {
return Err("联邦查询不支持用户 $pipeline(无法跨源下推)".to_string());
}
edges.sort_by_key(|e| e.path.len());
let root_plan = build_plan(fetch_ast, &post_ast, &inject, ¶ms, registry, ctx)?;
let mut sources = vec![json!({
"key": "0",
"source": root_loc.source,
"namespace": root_loc.namespace.map(|s| json!(s)).unwrap_or(Value::Null),
"model": root_schema.name,
"mode": root_plan.mode.as_str(),
"commands": root_plan.commands,
"sort": root_plan.sort.clone().unwrap_or(Value::Null),
})];
let mut unit_order: Vec<usize> = (0..units.len()).collect();
unit_order.sort_by_key(|&i| units[i].depth);
for &i in &unit_order {
let unit = &units[i];
let mut unit_params = params.clone();
let plan = plan_query_ast_mut(unit.ast.clone(), &mut unit_params, registry, ctx)?;
sources.push(json!({
"key": unit.key,
"source": unit.source,
"namespace": unit.namespace.as_ref().map(|s| json!(s)).unwrap_or(Value::Null),
"model": unit.model,
"mode": plan.mode.as_str(),
"commands": plan.commands,
"sort": plan.sort.clone().unwrap_or(Value::Null),
}));
}
let join_edges: Vec<Value> = edges
.iter()
.map(|e| {
json!({
"parent": e.parent_model,
"rel": e.rel,
"local": e.local,
"foreign": e.foreign,
"cardinality": e.cardinality,
"path": e.path,
"key": e.key,
})
})
.collect();
Ok(json!({
"v": FEDERATION_VERSION,
"kind": "federated",
"root": root_schema.name,
"sources": sources,
"join": { "type": "hash", "edges": join_edges },
"postprocess": root_plan.postprocess.clone().unwrap_or(Value::Null),
"degraded": degraded,
"maxRowsPerSource": max_rows_per_source,
}))
}
pub(crate) fn unit_index(sources: &[Value]) -> HashMap<String, usize> {
sources
.iter()
.enumerate()
.filter_map(|(i, s)| {
s.get("key")
.and_then(|k| k.as_str())
.map(|k| (k.to_string(), i))
})
.collect()
}