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