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
229fn unproduced_value(kind: Option<RelationKind>) -> Value {
230 if kind == Some(RelationKind::Connection) {
231 Value::Obj(vec![
232 ("items".into(), Value::Arr(vec![])),
233 ("cursor".into(), Value::Null),
234 ])
235 } else {
236 Value::Null
237 }
238}
239
240fn parse_policy(raw: &Option<String>) -> Result<PolicyKind, PlanFailure> {
241 match raw.as_deref() {
242 None | Some("fail") => Ok(PolicyKind::Fail),
243 Some("retry") => Ok(PolicyKind::Retry),
244 Some("continue") => Ok(PolicyKind::Continue),
245 Some(other) => fail(
246 PlanFailureCode::UnknownPolicy,
247 format!("unknown policy kind: {other}"),
248 ),
249 }
250}
251
252pub fn run_plan<F>(
254 plan: Option<&ExecutionPlanSpec>,
255 ops: &[OpSpec],
256 mut exec: F,
257) -> Result<RunResult, PlanFailure>
258where
259 F: FnMut(&OpSpec, Option<&Value>) -> ExecOutcome,
260{
261 let n = ops.len();
262 let mut states: Vec<Option<OpState>> = vec![None; n];
263 let mut executed_idx: Vec<usize> = Vec::new();
264 let mut skipped_idx: Vec<usize> = Vec::new();
265
266 let stages: Vec<Vec<usize>> = match plan {
267 Some(p) => p.groups.clone(),
268 None => (0..n).map(|i| vec![i]).collect(),
269 };
270
271 validate_coverage(&stages, n)?;
272
273 for stage in &stages {
274 let mut ordered = stage.clone();
275 ordered.sort_unstable(); for &i in &ordered {
277 let st = run_op(
278 i,
279 &ops[i],
280 &states,
281 &mut exec,
282 &mut executed_idx,
283 &mut skipped_idx,
284 )?;
285 states[i] = Some(st);
286 }
287 }
288
289 executed_idx.sort_unstable();
290 skipped_idx.sort_unstable();
291
292 Ok(RunResult {
293 executed: executed_idx.iter().map(|&i| ops[i].id.clone()).collect(),
294 skipped: skipped_idx.iter().map(|&i| ops[i].id.clone()).collect(),
295 ops_ref: ops
296 .iter()
297 .map(|o| (o.id.clone(), o.relation_kind))
298 .collect(),
299 states,
300 })
301}
302
303pub fn run_plan_parallel<F>(
323 plan: Option<&ExecutionPlanSpec>,
324 ops: &[OpSpec],
325 exec: F,
326) -> Result<RunResult, PlanFailure>
327where
328 F: Fn(&OpSpec, Option<&Value>) -> ExecOutcome + Sync,
329{
330 let n = ops.len();
331 let mut states: Vec<Option<OpState>> = vec![None; n];
332 let mut executed_idx: Vec<usize> = Vec::new();
333 let mut skipped_idx: Vec<usize> = Vec::new();
334
335 let stages: Vec<Vec<usize>> = match plan {
336 Some(p) => p.groups.clone(),
337 None => (0..n).map(|i| vec![i]).collect(),
338 };
339 let concurrency = plan.map(|p| p.concurrency).unwrap_or(1);
340
341 validate_coverage(&stages, n)?;
342
343 for stage in &stages {
344 let mut ordered = stage.clone();
345 ordered.sort_unstable();
346
347 let has_intra_deps = ordered
348 .iter()
349 .any(|&i| matches!(ops[i].parent, Some(p) if ordered.contains(&p)));
350
351 if concurrency <= 1 || ordered.len() <= 1 || has_intra_deps {
352 for &i in &ordered {
354 let st = run_op(
355 i,
356 &ops[i],
357 &states,
358 &mut |op: &OpSpec, b: Option<&Value>| exec(op, b),
359 &mut executed_idx,
360 &mut skipped_idx,
361 )?;
362 states[i] = Some(st);
363 }
364 continue;
365 }
366
367 let pres: Vec<Preflight> = ordered
369 .iter()
370 .map(|&i| preflight_op(&ops[i], &states))
371 .collect();
372
373 let jobs: Vec<usize> = (0..ordered.len())
376 .filter(|&k| matches!(pres[k], Preflight::Exec { .. }))
377 .collect();
378 let workers = (concurrency.max(1) as usize).min(jobs.len());
379 let cursor = std::sync::atomic::AtomicUsize::new(0);
380 let slots: Vec<std::sync::Mutex<Option<ExecOutcome>>> = (0..ordered.len())
381 .map(|_| std::sync::Mutex::new(None))
382 .collect();
383
384 std::thread::scope(|scope| {
385 for _ in 0..workers {
386 scope.spawn(|| loop {
387 let j = cursor.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
388 if j >= jobs.len() {
389 break;
390 }
391 let k = jobs[j];
392 let i = ordered[k];
393 let bound = match &pres[k] {
394 Preflight::Exec { bound, .. } => bound.as_ref(),
395 _ => unreachable!(),
396 };
397 let outcome = exec(&ops[i], bound);
398 *slots[k].lock().unwrap() = Some(outcome);
399 });
400 }
401 }); for (k, &i) in ordered.iter().enumerate() {
405 match &pres[k] {
406 Preflight::InvalidPolicy(msg) => {
407 return fail(PlanFailureCode::UnknownPolicy, msg.clone())
408 }
409 Preflight::Skip => {
410 skipped_idx.push(i);
411 states[i] = Some(OpState::Skipped);
412 }
413 Preflight::Exec { policy, .. } => {
414 let outcome = slots[k]
415 .lock()
416 .unwrap()
417 .take()
418 .expect("dispatched op must have an outcome");
419 let st = interpret_outcome(
420 i,
421 &ops[i],
422 *policy,
423 outcome,
424 &mut executed_idx,
425 &mut skipped_idx,
426 )?;
427 states[i] = Some(st);
428 }
429 }
430 }
431 }
432
433 executed_idx.sort_unstable();
434 skipped_idx.sort_unstable();
435
436 Ok(RunResult {
437 executed: executed_idx.iter().map(|&i| ops[i].id.clone()).collect(),
438 skipped: skipped_idx.iter().map(|&i| ops[i].id.clone()).collect(),
439 ops_ref: ops
440 .iter()
441 .map(|o| (o.id.clone(), o.relation_kind))
442 .collect(),
443 states,
444 })
445}
446
447enum Preflight {
452 InvalidPolicy(String),
453 Skip,
454 Exec {
455 policy: PolicyKind,
456 bound: Option<Value>,
457 },
458}
459
460fn preflight_op(op: &OpSpec, states: &[Option<OpState>]) -> Preflight {
461 let policy = match parse_policy(&op.policy) {
462 Ok(p) => p,
463 Err(e) => return Preflight::InvalidPolicy(e.message), };
465
466 let mut bound_value: Option<Value> = None;
467 if let Some(pidx) = op.parent {
468 match states.get(pidx).and_then(|s| s.as_ref()) {
469 Some(OpState::Ok(pv)) => {
470 if let Some(field) = &op.bind_field {
471 let bound = pv.obj_get(field);
472 match bound {
473 None | Some(Value::Null) => return Preflight::Skip,
474 Some(b) => bound_value = Some(b.clone()),
475 }
476 }
477 }
478 _ => {
479 return Preflight::Skip;
481 }
482 }
483 }
484 Preflight::Exec {
485 policy,
486 bound: bound_value,
487 }
488}
489
490fn interpret_outcome(
493 i: usize,
494 op: &OpSpec,
495 policy: PolicyKind,
496 outcome: ExecOutcome,
497 executed_idx: &mut Vec<usize>,
498 skipped_idx: &mut Vec<usize>,
499) -> Result<OpState, PlanFailure> {
500 executed_idx.push(i);
501
502 match outcome {
503 ExecOutcome::Ok(v) => Ok(OpState::Ok(v)),
504 ExecOutcome::Error(e, detail) => match policy {
507 PolicyKind::Fail => fail_detail(
508 PlanFailureCode::OpFailed,
509 format!("operation '{}' failed under 'fail' policy: {e}", op.id),
510 detail,
511 ),
512 PolicyKind::Retry => fail_detail(
513 PlanFailureCode::OpFailed,
514 format!(
515 "operation '{}' failed under 'retry' policy (exhausted): {e}",
516 op.id
517 ),
518 detail,
519 ),
520 PolicyKind::Continue => {
521 skipped_idx.push(i);
523 executed_idx.pop(); Ok(OpState::Skipped)
525 }
526 },
527 }
528}
529
530fn run_op<F>(
531 i: usize,
532 op: &OpSpec,
533 states: &[Option<OpState>],
534 exec: &mut F,
535 executed_idx: &mut Vec<usize>,
536 skipped_idx: &mut Vec<usize>,
537) -> Result<OpState, PlanFailure>
538where
539 F: FnMut(&OpSpec, Option<&Value>) -> ExecOutcome,
540{
541 let (policy, bound_value) = match preflight_op(op, states) {
542 Preflight::InvalidPolicy(msg) => return fail(PlanFailureCode::UnknownPolicy, msg),
543 Preflight::Skip => {
544 skipped_idx.push(i);
545 return Ok(OpState::Skipped);
546 }
547 Preflight::Exec { policy, bound } => (policy, bound),
548 };
549
550 let outcome = exec(op, bound_value.as_ref());
551 interpret_outcome(i, op, policy, outcome, executed_idx, skipped_idx)
552}
553
554fn validate_coverage(stages: &[Vec<usize>], n: usize) -> Result<(), PlanFailure> {
555 let mut seen = std::collections::HashSet::new();
556 for stage in stages {
557 for &i in stage {
558 if i >= n {
559 return fail(
560 PlanFailureCode::InvalidPlan,
561 format!("stage index {i} out of range [0,{n})"),
562 );
563 }
564 if !seen.insert(i) {
565 return fail(
566 PlanFailureCode::InvalidPlan,
567 format!("operation {i} appears in more than one stage"),
568 );
569 }
570 }
571 }
572 if seen.len() != n {
573 return fail(
574 PlanFailureCode::InvalidPlan,
575 format!(
576 "plan does not cover all {n} operations (covered {})",
577 seen.len()
578 ),
579 );
580 }
581 Ok(())
582}