1use crate::value::Value;
19
20#[derive(Debug, Clone)]
21pub struct ExecutionPlanSpec {
22 pub groups: Vec<Vec<usize>>,
23 pub concurrency: i64,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum PolicyKind {
28 Fail,
29 Retry,
30 Continue,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum RelationKind {
35 Single,
36 Connection,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum ElementPolicyKind {
44 Error,
45 Skip,
46}
47
48impl ElementPolicyKind {
49 pub fn parse(s: &str) -> Option<ElementPolicyKind> {
51 match s {
52 "error" => Some(ElementPolicyKind::Error),
53 "skip" => Some(ElementPolicyKind::Skip),
54 _ => None,
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
60pub struct OpSpec {
61 pub id: String,
62 pub parent: Option<usize>,
63 pub bind_field: Option<String>,
64 pub relation_kind: Option<RelationKind>,
65 pub policy: Option<String>,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ErrorKind {
75 TypeMismatch,
77 MissingField,
79 Overflow,
81}
82
83impl ErrorKind {
84 pub fn as_str(self) -> &'static str {
85 match self {
86 ErrorKind::TypeMismatch => "typeMismatch",
87 ErrorKind::MissingField => "missingField",
88 ErrorKind::Overflow => "overflow",
89 }
90 }
91
92 pub fn parse(s: &str) -> Option<ErrorKind> {
94 match s {
95 "typeMismatch" => Some(ErrorKind::TypeMismatch),
96 "missingField" => Some(ErrorKind::MissingField),
97 "overflow" => Some(ErrorKind::Overflow),
98 _ => None,
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Default)]
113pub struct ErrorDetail {
114 pub kind: Option<ErrorKind>,
115 pub model: Option<String>,
117 pub field: Option<String>,
119 pub expected_type: Option<String>,
121 pub actual_wire_type: Option<String>,
123 pub raw_value: Option<String>,
125 pub context: std::collections::BTreeMap<String, String>,
127}
128
129#[derive(Debug, Clone)]
134pub enum ExecOutcome {
135 Ok(Value),
136 Error(String, Option<Box<ErrorDetail>>),
137}
138
139impl ExecOutcome {
140 pub fn error(message: impl Into<String>) -> ExecOutcome {
142 ExecOutcome::Error(message.into(), None)
143 }
144}
145
146#[derive(Debug, Clone)]
147enum OpState {
148 Ok(Value),
149 Skipped,
150 }
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum PlanFailureCode {
155 OpFailed,
156 UnknownPolicy,
157 InvalidPlan,
158}
159
160impl PlanFailureCode {
161 pub fn as_str(self) -> &'static str {
162 match self {
163 PlanFailureCode::OpFailed => "OP_FAILED",
164 PlanFailureCode::UnknownPolicy => "UNKNOWN_POLICY",
165 PlanFailureCode::InvalidPlan => "INVALID_PLAN",
166 }
167 }
168}
169
170#[derive(Debug, Clone)]
171pub struct PlanFailure {
172 pub code: PlanFailureCode,
173 pub message: String,
174 pub detail: Option<Box<ErrorDetail>>,
178}
179
180impl std::fmt::Display for PlanFailure {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 write!(f, "{}: {}", self.code.as_str(), self.message)
183 }
184}
185impl std::error::Error for PlanFailure {}
186
187fn fail<T>(code: PlanFailureCode, message: impl Into<String>) -> Result<T, PlanFailure> {
188 fail_detail(code, message, None)
189}
190
191fn fail_detail<T>(
192 code: PlanFailureCode,
193 message: impl Into<String>,
194 detail: Option<Box<ErrorDetail>>,
195) -> Result<T, PlanFailure> {
196 Err(PlanFailure {
197 code,
198 detail,
199 message: message.into(),
200 })
201}
202
203#[derive(Debug)]
204pub struct RunResult {
205 states: Vec<Option<OpState>>,
206 pub executed: Vec<String>,
207 pub skipped: Vec<String>,
208 ops_ref: Vec<(String, Option<RelationKind>)>,
209}
210
211impl RunResult {
212 pub fn final_tree(&self) -> Vec<(String, Value)> {
214 let mut tree = Vec::new();
215 for (i, st) in self.states.iter().enumerate() {
216 match st {
217 Some(OpState::Ok(v)) => tree.push((self.ops_ref[i].0.clone(), v.clone())),
218 Some(OpState::Skipped) => tree.push((
219 self.ops_ref[i].0.clone(),
220 unproduced_value(self.ops_ref[i].1),
221 )),
222 None => {}
223 }
224 }
225 tree
226 }
227}
228
229pub(crate) fn unproduced_value(kind: Option<RelationKind>) -> Value {
232 if kind == Some(RelationKind::Connection) {
233 Value::Obj(vec![
234 ("items".into(), Value::Arr(vec![])),
235 ("cursor".into(), Value::Null),
236 ])
237 } else {
238 Value::Null
239 }
240}
241
242fn parse_policy(raw: &Option<String>) -> Result<PolicyKind, PlanFailure> {
243 match raw.as_deref() {
244 None | Some("fail") => Ok(PolicyKind::Fail),
245 Some("retry") => Ok(PolicyKind::Retry),
246 Some("continue") => Ok(PolicyKind::Continue),
247 Some(other) => fail(
248 PlanFailureCode::UnknownPolicy,
249 format!("unknown policy kind: {other}"),
250 ),
251 }
252}
253
254pub fn run_plan<F>(
256 plan: Option<&ExecutionPlanSpec>,
257 ops: &[OpSpec],
258 mut exec: F,
259) -> Result<RunResult, PlanFailure>
260where
261 F: FnMut(&OpSpec, Option<&Value>) -> ExecOutcome,
262{
263 let n = ops.len();
264 let mut states: Vec<Option<OpState>> = vec![None; n];
265 let mut executed_idx: Vec<usize> = Vec::new();
266 let mut skipped_idx: Vec<usize> = Vec::new();
267
268 let stages: Vec<Vec<usize>> = match plan {
269 Some(p) => p.groups.clone(),
270 None => (0..n).map(|i| vec![i]).collect(),
271 };
272
273 validate_coverage(&stages, n)?;
274
275 for stage in &stages {
276 let mut ordered = stage.clone();
277 ordered.sort_unstable(); for &i in &ordered {
279 let st = run_op(
280 i,
281 &ops[i],
282 &states,
283 &mut exec,
284 &mut executed_idx,
285 &mut skipped_idx,
286 )?;
287 states[i] = Some(st);
288 }
289 }
290
291 executed_idx.sort_unstable();
292 skipped_idx.sort_unstable();
293
294 Ok(RunResult {
295 executed: executed_idx.iter().map(|&i| ops[i].id.clone()).collect(),
296 skipped: skipped_idx.iter().map(|&i| ops[i].id.clone()).collect(),
297 ops_ref: ops
298 .iter()
299 .map(|o| (o.id.clone(), o.relation_kind))
300 .collect(),
301 states,
302 })
303}
304
305pub fn run_plan_parallel<F>(
325 plan: Option<&ExecutionPlanSpec>,
326 ops: &[OpSpec],
327 exec: F,
328) -> Result<RunResult, PlanFailure>
329where
330 F: Fn(&OpSpec, Option<&Value>) -> ExecOutcome + Sync,
331{
332 let n = ops.len();
333 let mut states: Vec<Option<OpState>> = vec![None; n];
334 let mut executed_idx: Vec<usize> = Vec::new();
335 let mut skipped_idx: Vec<usize> = Vec::new();
336
337 let stages: Vec<Vec<usize>> = match plan {
338 Some(p) => p.groups.clone(),
339 None => (0..n).map(|i| vec![i]).collect(),
340 };
341 let concurrency = plan.map(|p| p.concurrency).unwrap_or(1);
342
343 validate_coverage(&stages, n)?;
344
345 for stage in &stages {
346 let mut ordered = stage.clone();
347 ordered.sort_unstable();
348
349 let has_intra_deps = ordered
350 .iter()
351 .any(|&i| matches!(ops[i].parent, Some(p) if ordered.contains(&p)));
352
353 if concurrency <= 1 || ordered.len() <= 1 || has_intra_deps {
354 for &i in &ordered {
356 let st = run_op(
357 i,
358 &ops[i],
359 &states,
360 &mut |op: &OpSpec, b: Option<&Value>| exec(op, b),
361 &mut executed_idx,
362 &mut skipped_idx,
363 )?;
364 states[i] = Some(st);
365 }
366 continue;
367 }
368
369 let pres: Vec<Preflight> = ordered
371 .iter()
372 .map(|&i| preflight_op(&ops[i], &states))
373 .collect();
374
375 let jobs: Vec<usize> = (0..ordered.len())
378 .filter(|&k| matches!(pres[k], Preflight::Exec { .. }))
379 .collect();
380 let workers = (concurrency.max(1) as usize).min(jobs.len());
381 let cursor = std::sync::atomic::AtomicUsize::new(0);
382 let slots: Vec<std::sync::Mutex<Option<ExecOutcome>>> = (0..ordered.len())
383 .map(|_| std::sync::Mutex::new(None))
384 .collect();
385
386 std::thread::scope(|scope| {
387 for _ in 0..workers {
388 scope.spawn(|| loop {
389 let j = cursor.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
390 if j >= jobs.len() {
391 break;
392 }
393 let k = jobs[j];
394 let i = ordered[k];
395 let bound = match &pres[k] {
396 Preflight::Exec { bound, .. } => bound.as_ref(),
397 _ => unreachable!(),
398 };
399 let outcome = exec(&ops[i], bound);
400 *slots[k].lock().unwrap() = Some(outcome);
401 });
402 }
403 }); for (k, &i) in ordered.iter().enumerate() {
407 match &pres[k] {
408 Preflight::InvalidPolicy(msg) => {
409 return fail(PlanFailureCode::UnknownPolicy, msg.clone())
410 }
411 Preflight::Skip => {
412 skipped_idx.push(i);
413 states[i] = Some(OpState::Skipped);
414 }
415 Preflight::Exec { policy, .. } => {
416 let outcome = slots[k]
417 .lock()
418 .unwrap()
419 .take()
420 .expect("dispatched op must have an outcome");
421 let st = interpret_outcome(
422 i,
423 &ops[i],
424 *policy,
425 outcome,
426 &mut executed_idx,
427 &mut skipped_idx,
428 )?;
429 states[i] = Some(st);
430 }
431 }
432 }
433 }
434
435 executed_idx.sort_unstable();
436 skipped_idx.sort_unstable();
437
438 Ok(RunResult {
439 executed: executed_idx.iter().map(|&i| ops[i].id.clone()).collect(),
440 skipped: skipped_idx.iter().map(|&i| ops[i].id.clone()).collect(),
441 ops_ref: ops
442 .iter()
443 .map(|o| (o.id.clone(), o.relation_kind))
444 .collect(),
445 states,
446 })
447}
448
449enum Preflight {
454 InvalidPolicy(String),
455 Skip,
456 Exec {
457 policy: PolicyKind,
458 bound: Option<Value>,
459 },
460}
461
462fn preflight_op(op: &OpSpec, states: &[Option<OpState>]) -> Preflight {
463 let policy = match parse_policy(&op.policy) {
464 Ok(p) => p,
465 Err(e) => return Preflight::InvalidPolicy(e.message), };
467
468 let mut bound_value: Option<Value> = None;
469 if let Some(pidx) = op.parent {
470 match states.get(pidx).and_then(|s| s.as_ref()) {
471 Some(OpState::Ok(pv)) => {
472 if let Some(field) = &op.bind_field {
473 let bound = pv.obj_get(field);
474 match bound {
475 None | Some(Value::Null) => return Preflight::Skip,
476 Some(b) => bound_value = Some(b.clone()),
477 }
478 }
479 }
480 _ => {
481 return Preflight::Skip;
483 }
484 }
485 }
486 Preflight::Exec {
487 policy,
488 bound: bound_value,
489 }
490}
491
492fn interpret_outcome(
495 i: usize,
496 op: &OpSpec,
497 policy: PolicyKind,
498 outcome: ExecOutcome,
499 executed_idx: &mut Vec<usize>,
500 skipped_idx: &mut Vec<usize>,
501) -> Result<OpState, PlanFailure> {
502 executed_idx.push(i);
503
504 match outcome {
505 ExecOutcome::Ok(v) => Ok(OpState::Ok(v)),
506 ExecOutcome::Error(e, detail) => match policy {
509 PolicyKind::Fail => fail_detail(
510 PlanFailureCode::OpFailed,
511 format!("operation '{}' failed under 'fail' policy: {e}", op.id),
512 detail,
513 ),
514 PolicyKind::Retry => fail_detail(
515 PlanFailureCode::OpFailed,
516 format!(
517 "operation '{}' failed under 'retry' policy (exhausted): {e}",
518 op.id
519 ),
520 detail,
521 ),
522 PolicyKind::Continue => {
523 skipped_idx.push(i);
525 executed_idx.pop(); Ok(OpState::Skipped)
527 }
528 },
529 }
530}
531
532fn run_op<F>(
533 i: usize,
534 op: &OpSpec,
535 states: &[Option<OpState>],
536 exec: &mut F,
537 executed_idx: &mut Vec<usize>,
538 skipped_idx: &mut Vec<usize>,
539) -> Result<OpState, PlanFailure>
540where
541 F: FnMut(&OpSpec, Option<&Value>) -> ExecOutcome,
542{
543 let (policy, bound_value) = match preflight_op(op, states) {
544 Preflight::InvalidPolicy(msg) => return fail(PlanFailureCode::UnknownPolicy, msg),
545 Preflight::Skip => {
546 skipped_idx.push(i);
547 return Ok(OpState::Skipped);
548 }
549 Preflight::Exec { policy, bound } => (policy, bound),
550 };
551
552 let outcome = exec(op, bound_value.as_ref());
553 interpret_outcome(i, op, policy, outcome, executed_idx, skipped_idx)
554}
555
556fn validate_coverage(stages: &[Vec<usize>], n: usize) -> Result<(), PlanFailure> {
557 let mut seen = std::collections::HashSet::new();
558 for stage in stages {
559 for &i in stage {
560 if i >= n {
561 return fail(
562 PlanFailureCode::InvalidPlan,
563 format!("stage index {i} out of range [0,{n})"),
564 );
565 }
566 if !seen.insert(i) {
567 return fail(
568 PlanFailureCode::InvalidPlan,
569 format!("operation {i} appears in more than one stage"),
570 );
571 }
572 }
573 }
574 if seen.len() != n {
575 return fail(
576 PlanFailureCode::InvalidPlan,
577 format!(
578 "plan does not cover all {n} operations (covered {})",
579 seen.len()
580 ),
581 );
582 }
583 Ok(())
584}