Skip to main content

rulemorph/
v2_eval.rs

1//! v2 Evaluation Context and Functions for rulemorph v2.0
2//!
3//! This module provides the evaluation context and functions for v2 expressions,
4//! including pipe value tracking, let bindings, and item/acc scopes.
5
6use serde_json::Value as JsonValue;
7use std::collections::{HashMap, HashSet};
8
9use crate::error::{TransformError, TransformErrorKind};
10use crate::model::{Expr, ExprOp, ExprRef};
11use crate::path::{get_path, parse_path};
12use crate::transform::{
13    EvalItem as V1EvalItem, EvalLocals as V1EvalLocals, EvalValue as V1EvalValue,
14    eval_op as eval_v1_op,
15};
16use crate::v2_model::{
17    V2Comparison, V2ComparisonOp, V2Condition, V2Expr, V2IfStep, V2LetStep, V2MapStep, V2OpStep,
18    V2Pipe, V2Ref, V2Start, V2Step,
19};
20
21// =============================================================================
22// EvalValue - Same as v1 transform
23// =============================================================================
24
25/// Evaluation result - either a value or missing
26#[derive(Debug, Clone, PartialEq)]
27pub enum EvalValue {
28    Missing,
29    Value(JsonValue),
30}
31
32impl EvalValue {
33    pub fn is_missing(&self) -> bool {
34        matches!(self, EvalValue::Missing)
35    }
36
37    pub fn into_value(self) -> Option<JsonValue> {
38        match self {
39            EvalValue::Value(v) => Some(v),
40            EvalValue::Missing => None,
41        }
42    }
43
44    pub fn as_value(&self) -> Option<&JsonValue> {
45        match self {
46            EvalValue::Value(v) => Some(v),
47            EvalValue::Missing => None,
48        }
49    }
50}
51
52// =============================================================================
53// V2EvalContext - Evaluation context for v2 expressions
54// =============================================================================
55
56/// Item in a map/filter operation
57#[derive(Clone)]
58pub struct EvalItem<'a> {
59    pub value: &'a JsonValue,
60    pub index: usize,
61}
62
63/// v2 evaluation context - tracks pipe value, let bindings, and iteration scopes
64#[derive(Clone)]
65pub struct V2EvalContext<'a> {
66    /// Current pipe value ($)
67    pipe_value: Option<EvalValue>,
68    /// Let-bound variables (local scope)
69    let_bindings: HashMap<String, EvalValue>,
70    /// Item scope for map/filter operations (@item)
71    item: Option<EvalItem<'a>>,
72    /// Accumulator scope for reduce/fold operations (@acc)
73    acc: Option<&'a JsonValue>,
74}
75
76impl<'a> V2EvalContext<'a> {
77    /// Create a new empty context
78    pub fn new() -> Self {
79        Self {
80            pipe_value: None,
81            let_bindings: HashMap::new(),
82            item: None,
83            acc: None,
84        }
85    }
86
87    /// Create a new context with a pipe value
88    pub fn with_pipe_value(mut self, value: EvalValue) -> Self {
89        self.pipe_value = Some(value);
90        self
91    }
92
93    /// Create a new context with a let binding added
94    pub fn with_let_binding(mut self, name: String, value: EvalValue) -> Self {
95        self.let_bindings.insert(name, value);
96        self
97    }
98
99    /// Create a new context with multiple let bindings added
100    pub fn with_let_bindings(mut self, bindings: Vec<(String, EvalValue)>) -> Self {
101        for (name, value) in bindings {
102            self.let_bindings.insert(name, value);
103        }
104        self
105    }
106
107    /// Create a new context with item scope (for map/filter operations)
108    pub fn with_item(mut self, item: EvalItem<'a>) -> Self {
109        self.item = Some(item);
110        self
111    }
112
113    /// Create a new context with accumulator scope (for reduce/fold operations)
114    pub fn with_acc(mut self, acc: &'a JsonValue) -> Self {
115        self.acc = Some(acc);
116        self
117    }
118
119    /// Get the current pipe value
120    pub fn get_pipe_value(&self) -> Option<&EvalValue> {
121        self.pipe_value.as_ref()
122    }
123
124    /// Resolve a local variable name
125    pub fn resolve_local(&self, name: &str) -> Option<&EvalValue> {
126        self.let_bindings.get(name)
127    }
128
129    /// Get the current item (if in map/filter scope)
130    pub fn get_item(&self) -> Option<&EvalItem<'a>> {
131        self.item.as_ref()
132    }
133
134    /// Get the current accumulator (if in reduce/fold scope)
135    pub fn get_acc(&self) -> Option<&JsonValue> {
136        self.acc
137    }
138
139    /// Check if item scope is available
140    pub fn has_item_scope(&self) -> bool {
141        self.item.is_some()
142    }
143
144    /// Check if accumulator scope is available
145    pub fn has_acc_scope(&self) -> bool {
146        self.acc.is_some()
147    }
148}
149
150impl<'a> Default for V2EvalContext<'a> {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156// =============================================================================
157// V2EvalContext Tests (T12)
158// =============================================================================
159
160#[cfg(test)]
161mod v2_eval_context_tests {
162    use super::*;
163    use serde_json::json;
164
165    #[test]
166    fn test_context_new() {
167        let ctx = V2EvalContext::new();
168        assert!(ctx.get_pipe_value().is_none());
169        assert!(ctx.resolve_local("x").is_none());
170        assert!(ctx.get_item().is_none());
171        assert!(ctx.get_acc().is_none());
172    }
173
174    #[test]
175    fn test_context_with_pipe_value() {
176        let ctx = V2EvalContext::new().with_pipe_value(EvalValue::Value(json!(42)));
177        assert!(ctx.get_pipe_value().is_some());
178        assert_eq!(ctx.get_pipe_value(), Some(&EvalValue::Value(json!(42))));
179    }
180
181    #[test]
182    fn test_context_with_let_binding() {
183        let ctx =
184            V2EvalContext::new().with_let_binding("x".to_string(), EvalValue::Value(json!(100)));
185        assert!(ctx.resolve_local("x").is_some());
186        assert_eq!(ctx.resolve_local("x"), Some(&EvalValue::Value(json!(100))));
187        assert!(ctx.resolve_local("y").is_none());
188    }
189
190    #[test]
191    fn test_context_with_multiple_let_bindings() {
192        let ctx = V2EvalContext::new().with_let_bindings(vec![
193            ("a".to_string(), EvalValue::Value(json!(1))),
194            ("b".to_string(), EvalValue::Value(json!(2))),
195        ]);
196        assert!(ctx.resolve_local("a").is_some());
197        assert!(ctx.resolve_local("b").is_some());
198        assert!(ctx.resolve_local("c").is_none());
199    }
200
201    #[test]
202    fn test_context_scope_chain() {
203        let ctx =
204            V2EvalContext::new().with_let_binding("x".to_string(), EvalValue::Value(json!(1)));
205        let inner_ctx = ctx
206            .clone()
207            .with_let_binding("y".to_string(), EvalValue::Value(json!(2)));
208
209        // Inner context has both x and y
210        assert!(inner_ctx.resolve_local("x").is_some());
211        assert!(inner_ctx.resolve_local("y").is_some());
212
213        // Outer context only has x
214        assert!(ctx.resolve_local("x").is_some());
215        assert!(ctx.resolve_local("y").is_none());
216    }
217
218    #[test]
219    fn test_context_with_item() {
220        let item_value = json!({"name": "test"});
221        let ctx = V2EvalContext::new().with_item(EvalItem {
222            value: &item_value,
223            index: 0,
224        });
225        assert!(ctx.has_item_scope());
226        assert!(ctx.get_item().is_some());
227        let item = ctx.get_item().unwrap();
228        assert_eq!(item.value, &json!({"name": "test"}));
229        assert_eq!(item.index, 0);
230    }
231
232    #[test]
233    fn test_context_with_acc() {
234        let acc_value = json!(0);
235        let ctx = V2EvalContext::new().with_acc(&acc_value);
236        assert!(ctx.has_acc_scope());
237        assert!(ctx.get_acc().is_some());
238        assert_eq!(ctx.get_acc(), Some(&json!(0)));
239    }
240
241    #[test]
242    fn test_eval_value_is_missing() {
243        assert!(EvalValue::Missing.is_missing());
244        assert!(!EvalValue::Value(json!(null)).is_missing());
245    }
246
247    #[test]
248    fn test_eval_value_into_value() {
249        assert_eq!(EvalValue::Missing.into_value(), None);
250        assert_eq!(
251            EvalValue::Value(json!("hello")).into_value(),
252            Some(json!("hello"))
253        );
254    }
255
256    #[test]
257    fn test_eval_value_as_value() {
258        let missing = EvalValue::Missing;
259        let val = EvalValue::Value(json!(42));
260        assert!(missing.as_value().is_none());
261        assert_eq!(val.as_value(), Some(&json!(42)));
262    }
263
264    #[test]
265    fn test_context_preserves_pipe_value_after_let() {
266        let ctx = V2EvalContext::new()
267            .with_pipe_value(EvalValue::Value(json!(100)))
268            .with_let_binding("x".to_string(), EvalValue::Value(json!(50)));
269
270        // Pipe value should still be accessible
271        assert_eq!(ctx.get_pipe_value(), Some(&EvalValue::Value(json!(100))));
272        // Let binding should also be accessible
273        assert_eq!(ctx.resolve_local("x"), Some(&EvalValue::Value(json!(50))));
274    }
275}
276
277// =============================================================================
278// v2 Reference Evaluation (T13)
279// =============================================================================
280
281/// Helper to get value at path string
282fn get_path_str<'a>(
283    value: &'a JsonValue,
284    path_str: &str,
285    error_path: &str,
286) -> Result<EvalValue, TransformError> {
287    let tokens = parse_path(path_str).map_err(|_| {
288        TransformError::new(
289            TransformErrorKind::ExprError,
290            format!("invalid path: {}", path_str),
291        )
292        .with_path(error_path)
293    })?;
294    match get_path(value, &tokens) {
295        Some(v) => Ok(EvalValue::Value(v.clone())),
296        None => Ok(EvalValue::Missing),
297    }
298}
299
300/// Evaluate a v2 reference to get its value
301pub fn eval_v2_ref<'a>(
302    v2_ref: &V2Ref,
303    record: &'a JsonValue,
304    context: Option<&'a JsonValue>,
305    out: &'a JsonValue,
306    path: &str,
307    ctx: &V2EvalContext<'a>,
308) -> Result<EvalValue, TransformError> {
309    match v2_ref {
310        V2Ref::Input(ref_path) => {
311            if ref_path.is_empty() {
312                Ok(EvalValue::Value(record.clone()))
313            } else {
314                get_path_str(record, ref_path, path)
315            }
316        }
317        V2Ref::Context(ref_path) => {
318            let ctx_value = match context {
319                Some(value) => value,
320                None => return Ok(EvalValue::Missing),
321            };
322            if ref_path.is_empty() {
323                Ok(EvalValue::Value(ctx_value.clone()))
324            } else {
325                get_path_str(ctx_value, ref_path, path)
326            }
327        }
328        V2Ref::Out(ref_path) => {
329            if ref_path.is_empty() {
330                Ok(EvalValue::Value(out.clone()))
331            } else {
332                get_path_str(out, ref_path, path)
333            }
334        }
335        V2Ref::Item(ref_path) => {
336            let item = ctx.get_item().ok_or_else(|| {
337                TransformError::new(
338                    TransformErrorKind::ExprError,
339                    "@item is only available in map/filter operations",
340                )
341                .with_path(path)
342            })?;
343            if ref_path.is_empty() {
344                Ok(EvalValue::Value(item.value.clone()))
345            } else if ref_path == "index" {
346                Ok(EvalValue::Value(JsonValue::Number(item.index.into())))
347            } else if let Some(rest) = ref_path.strip_prefix("value.") {
348                get_path_str(item.value, rest, path)
349            } else if ref_path == "value" {
350                Ok(EvalValue::Value(item.value.clone()))
351            } else {
352                // Direct path on item value
353                get_path_str(item.value, ref_path, path)
354            }
355        }
356        V2Ref::Acc(ref_path) => {
357            let acc = ctx.get_acc().ok_or_else(|| {
358                TransformError::new(
359                    TransformErrorKind::ExprError,
360                    "@acc is only available in reduce/fold operations",
361                )
362                .with_path(path)
363            })?;
364            if ref_path.is_empty() {
365                Ok(EvalValue::Value(acc.clone()))
366            } else if let Some(rest) = ref_path.strip_prefix("value.") {
367                get_path_str(acc, rest, path)
368            } else if ref_path == "value" {
369                Ok(EvalValue::Value(acc.clone()))
370            } else {
371                // Direct path on acc value
372                get_path_str(acc, ref_path, path)
373            }
374        }
375        V2Ref::Local(var_name) => {
376            let value = ctx.resolve_local(var_name).ok_or_else(|| {
377                TransformError::new(
378                    TransformErrorKind::ExprError,
379                    format!("undefined variable: @{}", var_name),
380                )
381                .with_path(path)
382            })?;
383            Ok(value.clone())
384        }
385    }
386}
387
388// =============================================================================
389// v2 Reference Evaluation Tests (T13)
390// =============================================================================
391
392#[cfg(test)]
393mod v2_ref_eval_tests {
394    use super::*;
395    use serde_json::json;
396
397    #[test]
398    fn test_eval_input_ref() {
399        let record = json!({"name": "Alice", "age": 30});
400        let ctx = V2EvalContext::new();
401        let result = eval_v2_ref(
402            &V2Ref::Input("name".to_string()),
403            &record,
404            None,
405            &json!({}),
406            "test",
407            &ctx,
408        );
409        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Alice")));
410    }
411
412    #[test]
413    fn test_eval_input_ref_nested() {
414        let record = json!({"user": {"profile": {"name": "Bob"}}});
415        let ctx = V2EvalContext::new();
416        let result = eval_v2_ref(
417            &V2Ref::Input("user.profile.name".to_string()),
418            &record,
419            None,
420            &json!({}),
421            "test",
422            &ctx,
423        );
424        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Bob")));
425    }
426
427    #[test]
428    fn test_eval_input_ref_missing() {
429        let record = json!({"name": "Alice"});
430        let ctx = V2EvalContext::new();
431        let result = eval_v2_ref(
432            &V2Ref::Input("nonexistent".to_string()),
433            &record,
434            None,
435            &json!({}),
436            "test",
437            &ctx,
438        );
439        assert!(matches!(result, Ok(EvalValue::Missing)));
440    }
441
442    #[test]
443    fn test_eval_context_ref() {
444        let record = json!({});
445        let context = json!({"rate": 1.5, "config": {"enabled": true}});
446        let ctx = V2EvalContext::new();
447        let result = eval_v2_ref(
448            &V2Ref::Context("rate".to_string()),
449            &record,
450            Some(&context),
451            &json!({}),
452            "test",
453            &ctx,
454        );
455        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(1.5)));
456    }
457
458    #[test]
459    fn test_eval_context_ref_nested() {
460        let record = json!({});
461        let context = json!({"config": {"enabled": true}});
462        let ctx = V2EvalContext::new();
463        let result = eval_v2_ref(
464            &V2Ref::Context("config.enabled".to_string()),
465            &record,
466            Some(&context),
467            &json!({}),
468            "test",
469            &ctx,
470        );
471        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(true)));
472    }
473
474    #[test]
475    fn test_eval_context_ref_no_context_missing() {
476        let record = json!({});
477        let ctx = V2EvalContext::new();
478        let result = eval_v2_ref(
479            &V2Ref::Context("rate".to_string()),
480            &record,
481            None,
482            &json!({}),
483            "test",
484            &ctx,
485        );
486        assert!(matches!(result, Ok(EvalValue::Missing)));
487    }
488
489    #[test]
490    fn test_eval_out_ref() {
491        let record = json!({});
492        let out = json!({"computed": 42});
493        let ctx = V2EvalContext::new();
494        let result = eval_v2_ref(
495            &V2Ref::Out("computed".to_string()),
496            &record,
497            None,
498            &out,
499            "test",
500            &ctx,
501        );
502        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(42)));
503    }
504
505    #[test]
506    fn test_eval_local_ref() {
507        let ctx = V2EvalContext::new()
508            .with_let_binding("price".to_string(), EvalValue::Value(json!(100)));
509        let result = eval_v2_ref(
510            &V2Ref::Local("price".to_string()),
511            &json!({}),
512            None,
513            &json!({}),
514            "test",
515            &ctx,
516        );
517        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100)));
518    }
519
520    #[test]
521    fn test_eval_local_ref_undefined_error() {
522        let ctx = V2EvalContext::new();
523        let result = eval_v2_ref(
524            &V2Ref::Local("undefined".to_string()),
525            &json!({}),
526            None,
527            &json!({}),
528            "test",
529            &ctx,
530        );
531        assert!(result.is_err());
532    }
533
534    #[test]
535    fn test_eval_item_ref() {
536        let item_value = json!({"name": "item1", "value": 10});
537        let ctx = V2EvalContext::new().with_item(EvalItem {
538            value: &item_value,
539            index: 2,
540        });
541        let result = eval_v2_ref(
542            &V2Ref::Item("name".to_string()),
543            &json!({}),
544            None,
545            &json!({}),
546            "test",
547            &ctx,
548        );
549        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("item1")));
550    }
551
552    #[test]
553    fn test_eval_item_ref_index() {
554        let item_value = json!({"name": "item1"});
555        let ctx = V2EvalContext::new().with_item(EvalItem {
556            value: &item_value,
557            index: 5,
558        });
559        let result = eval_v2_ref(
560            &V2Ref::Item("index".to_string()),
561            &json!({}),
562            None,
563            &json!({}),
564            "test",
565            &ctx,
566        );
567        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(5)));
568    }
569
570    #[test]
571    fn test_eval_item_ref_no_scope_error() {
572        let ctx = V2EvalContext::new();
573        let result = eval_v2_ref(
574            &V2Ref::Item("value".to_string()),
575            &json!({}),
576            None,
577            &json!({}),
578            "test",
579            &ctx,
580        );
581        assert!(result.is_err());
582    }
583
584    #[test]
585    fn test_eval_acc_ref() {
586        let acc_value = json!(100);
587        let ctx = V2EvalContext::new().with_acc(&acc_value);
588        let result = eval_v2_ref(
589            &V2Ref::Acc(String::new()),
590            &json!({}),
591            None,
592            &json!({}),
593            "test",
594            &ctx,
595        );
596        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100)));
597    }
598
599    #[test]
600    fn test_eval_acc_ref_no_scope_error() {
601        let ctx = V2EvalContext::new();
602        let result = eval_v2_ref(
603            &V2Ref::Acc("value".to_string()),
604            &json!({}),
605            None,
606            &json!({}),
607            "test",
608            &ctx,
609        );
610        assert!(result.is_err());
611    }
612
613    #[test]
614    fn test_eval_input_ref_empty_path() {
615        let record = json!({"name": "Alice"});
616        let ctx = V2EvalContext::new();
617        let result = eval_v2_ref(
618            &V2Ref::Input(String::new()),
619            &record,
620            None,
621            &json!({}),
622            "test",
623            &ctx,
624        );
625        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!({"name": "Alice"})));
626    }
627}
628
629// =============================================================================
630// v2 Start Value Evaluation (T14)
631// =============================================================================
632
633/// Evaluate a v2 start value
634pub fn eval_v2_start<'a>(
635    start: &V2Start,
636    record: &'a JsonValue,
637    context: Option<&'a JsonValue>,
638    out: &'a JsonValue,
639    path: &str,
640    ctx: &V2EvalContext<'a>,
641) -> Result<EvalValue, TransformError> {
642    match start {
643        V2Start::Ref(v2_ref) => eval_v2_ref(v2_ref, record, context, out, path, ctx),
644        V2Start::PipeValue => {
645            // If pipe value is not available, return Missing instead of error
646            // This allows ops like lookup_first that don't use pipe input to work
647            Ok(ctx.get_pipe_value().cloned().unwrap_or(EvalValue::Missing))
648        }
649        V2Start::Literal(value) => Ok(EvalValue::Value(value.clone())),
650        V2Start::V1Expr(_expr) => {
651            // V1 expressions would be evaluated using the existing v1 eval logic
652            // For now, return an error as this is a fallback case
653            Err(TransformError::new(
654                TransformErrorKind::ExprError,
655                "v1 expression fallback not yet implemented",
656            )
657            .with_path(path))
658        }
659    }
660}
661
662// =============================================================================
663// v2 Start Value Evaluation Tests (T14)
664// =============================================================================
665
666#[cfg(test)]
667mod v2_start_eval_tests {
668    use super::*;
669    use serde_json::json;
670
671    #[test]
672    fn test_eval_start_literal_string() {
673        let ctx = V2EvalContext::new();
674        let result = eval_v2_start(
675            &V2Start::Literal(json!("hello")),
676            &json!({}),
677            None,
678            &json!({}),
679            "test",
680            &ctx,
681        );
682        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("hello")));
683    }
684
685    #[test]
686    fn test_eval_start_literal_number() {
687        let ctx = V2EvalContext::new();
688        let result = eval_v2_start(
689            &V2Start::Literal(json!(42)),
690            &json!({}),
691            None,
692            &json!({}),
693            "test",
694            &ctx,
695        );
696        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(42)));
697    }
698
699    #[test]
700    fn test_eval_start_literal_bool() {
701        let ctx = V2EvalContext::new();
702        let result = eval_v2_start(
703            &V2Start::Literal(json!(true)),
704            &json!({}),
705            None,
706            &json!({}),
707            "test",
708            &ctx,
709        );
710        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(true)));
711    }
712
713    #[test]
714    fn test_eval_start_literal_null() {
715        let ctx = V2EvalContext::new();
716        let result = eval_v2_start(
717            &V2Start::Literal(json!(null)),
718            &json!({}),
719            None,
720            &json!({}),
721            "test",
722            &ctx,
723        );
724        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(null)));
725    }
726
727    #[test]
728    fn test_eval_start_literal_array() {
729        let ctx = V2EvalContext::new();
730        let result = eval_v2_start(
731            &V2Start::Literal(json!([1, 2, 3])),
732            &json!({}),
733            None,
734            &json!({}),
735            "test",
736            &ctx,
737        );
738        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([1, 2, 3])));
739    }
740
741    #[test]
742    fn test_eval_start_literal_object() {
743        let ctx = V2EvalContext::new();
744        let result = eval_v2_start(
745            &V2Start::Literal(json!({"key": "value"})),
746            &json!({}),
747            None,
748            &json!({}),
749            "test",
750            &ctx,
751        );
752        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!({"key": "value"})));
753    }
754
755    #[test]
756    fn test_eval_start_ref() {
757        let ctx = V2EvalContext::new();
758        let result = eval_v2_start(
759            &V2Start::Ref(V2Ref::Input("name".to_string())),
760            &json!({"name": "Bob"}),
761            None,
762            &json!({}),
763            "test",
764            &ctx,
765        );
766        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Bob")));
767    }
768
769    #[test]
770    fn test_eval_start_pipe_value() {
771        let ctx = V2EvalContext::new().with_pipe_value(EvalValue::Value(json!(42)));
772        let result = eval_v2_start(
773            &V2Start::PipeValue,
774            &json!({}),
775            None,
776            &json!({}),
777            "test",
778            &ctx,
779        );
780        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(42)));
781    }
782
783    #[test]
784    fn test_eval_start_pipe_value_not_available() {
785        // When pipe value is not set, it returns Missing (not error)
786        // This allows ops like lookup_first that don't use pipe input to work
787        let ctx = V2EvalContext::new();
788        let result = eval_v2_start(
789            &V2Start::PipeValue,
790            &json!({}),
791            None,
792            &json!({}),
793            "test",
794            &ctx,
795        );
796        assert!(result.is_ok());
797        assert_eq!(result.unwrap(), EvalValue::Missing);
798    }
799
800    #[test]
801    fn test_eval_start_pipe_value_missing() {
802        let ctx = V2EvalContext::new().with_pipe_value(EvalValue::Missing);
803        let result = eval_v2_start(
804            &V2Start::PipeValue,
805            &json!({}),
806            None,
807            &json!({}),
808            "test",
809            &ctx,
810        );
811        assert!(matches!(result, Ok(EvalValue::Missing)));
812    }
813}
814
815// =============================================================================
816// v2 Op Step Evaluation (T15)
817// =============================================================================
818
819/// Evaluate a v2 pipe expression
820pub fn eval_v2_pipe<'a>(
821    pipe: &V2Pipe,
822    record: &'a JsonValue,
823    context: Option<&'a JsonValue>,
824    out: &'a JsonValue,
825    path: &str,
826    ctx: &V2EvalContext<'a>,
827) -> Result<EvalValue, TransformError> {
828    // Evaluate start value
829    let mut current = eval_v2_start(&pipe.start, record, context, out, path, ctx)?;
830    let mut current_ctx = ctx.clone();
831
832    // Apply each step
833    for (i, step) in pipe.steps.iter().enumerate() {
834        let step_path = format!("{}[{}]", path, i + 1);
835        // Update context with current pipe value for each step
836        current_ctx = current_ctx.clone().with_pipe_value(current.clone());
837
838        match step {
839            V2Step::Op(op_step) => {
840                current = eval_v2_op_step(
841                    op_step,
842                    current,
843                    record,
844                    context,
845                    out,
846                    &step_path,
847                    &current_ctx,
848                )?;
849            }
850            V2Step::Let(let_step) => {
851                // Let step doesn't change pipe value, just adds bindings to context
852                current_ctx = eval_v2_let_step(
853                    let_step,
854                    current.clone(),
855                    record,
856                    context,
857                    out,
858                    &step_path,
859                    &current_ctx,
860                )?;
861            }
862            V2Step::If(if_step) => {
863                current = eval_v2_if_step(
864                    if_step,
865                    current,
866                    record,
867                    context,
868                    out,
869                    &step_path,
870                    &current_ctx,
871                )?;
872            }
873            V2Step::Map(map_step) => {
874                current = eval_v2_map_step(
875                    map_step,
876                    current,
877                    record,
878                    context,
879                    out,
880                    &step_path,
881                    &current_ctx,
882                )?;
883            }
884            V2Step::Ref(v2_ref) => {
885                // Reference step evaluates the reference and returns its value
886                current = eval_v2_ref(v2_ref, record, context, out, &step_path, &current_ctx)?;
887            }
888        }
889    }
890
891    Ok(current)
892}
893
894/// Evaluate a v2 let step - binds variables to context without changing pipe value
895pub fn eval_v2_let_step<'a>(
896    let_step: &V2LetStep,
897    pipe_value: EvalValue,
898    record: &'a JsonValue,
899    context: Option<&'a JsonValue>,
900    out: &'a JsonValue,
901    path: &str,
902    ctx: &V2EvalContext<'a>,
903) -> Result<V2EvalContext<'a>, TransformError> {
904    let mut new_ctx = ctx.clone().with_pipe_value(pipe_value);
905
906    for (name, expr) in &let_step.bindings {
907        let binding_path = format!("{}.{}", path, name);
908        let value = eval_v2_expr(expr, record, context, out, &binding_path, &new_ctx)?;
909        new_ctx = new_ctx.with_let_binding(name.clone(), value);
910    }
911
912    Ok(new_ctx)
913}
914
915/// Evaluate a v2 if step - conditional branching
916pub fn eval_v2_if_step<'a>(
917    if_step: &V2IfStep,
918    pipe_value: EvalValue,
919    record: &'a JsonValue,
920    context: Option<&'a JsonValue>,
921    out: &'a JsonValue,
922    path: &str,
923    ctx: &V2EvalContext<'a>,
924) -> Result<EvalValue, TransformError> {
925    // Create context with current pipe value for condition evaluation
926    let cond_ctx = ctx.clone().with_pipe_value(pipe_value.clone());
927
928    // Evaluate condition
929    let cond_path = format!("{}.cond", path);
930    let cond_result =
931        eval_v2_condition(&if_step.cond, record, context, out, &cond_path, &cond_ctx)?;
932
933    if cond_result {
934        // Execute then branch
935        let then_path = format!("{}.then", path);
936        eval_v2_pipe(
937            &if_step.then_branch,
938            record,
939            context,
940            out,
941            &then_path,
942            &cond_ctx,
943        )
944    } else if let Some(ref else_branch) = if_step.else_branch {
945        // Execute else branch
946        let else_path = format!("{}.else", path);
947        eval_v2_pipe(else_branch, record, context, out, &else_path, &cond_ctx)
948    } else {
949        // No else branch, return pipe value unchanged
950        Ok(pipe_value)
951    }
952}
953
954/// Evaluate a v2 map step - iterates over arrays
955pub fn eval_v2_map_step<'a>(
956    map_step: &V2MapStep,
957    pipe_value: EvalValue,
958    record: &'a JsonValue,
959    context: Option<&'a JsonValue>,
960    out: &'a JsonValue,
961    path: &str,
962    ctx: &V2EvalContext<'a>,
963) -> Result<EvalValue, TransformError> {
964    // Get the array to iterate over
965    let arr = match &pipe_value {
966        EvalValue::Missing => {
967            return Ok(EvalValue::Missing);
968        }
969        EvalValue::Value(JsonValue::Array(arr)) => arr,
970        EvalValue::Value(other) => {
971            return Err(TransformError::new(
972                TransformErrorKind::ExprError,
973                format!("map step requires array, got {:?}", other),
974            )
975            .with_path(path));
976        }
977    };
978
979    // Map over each element
980    let mut results = Vec::with_capacity(arr.len());
981    for (index, item_value) in arr.iter().enumerate() {
982        let item_path = format!("{}[{}]", path, index);
983
984        // Create context with item scope
985        let item_ctx = ctx
986            .clone()
987            .with_pipe_value(EvalValue::Value(item_value.clone()))
988            .with_item(EvalItem {
989                value: item_value,
990                index,
991            });
992
993        // Apply all steps to this item
994        let mut current = EvalValue::Value(item_value.clone());
995        let mut step_ctx = item_ctx.clone(); // Declare outside loop to preserve let bindings
996
997        for (step_idx, step) in map_step.steps.iter().enumerate() {
998            let step_path = format!("{}.step[{}]", item_path, step_idx);
999            step_ctx = step_ctx.clone().with_pipe_value(current.clone());
1000
1001            match step {
1002                V2Step::Op(op_step) => {
1003                    current = eval_v2_op_step(
1004                        op_step, current, record, context, out, &step_path, &step_ctx,
1005                    )?;
1006                }
1007                V2Step::Let(let_step) => {
1008                    // Let in map context - evaluate and update context to preserve bindings
1009                    step_ctx = eval_v2_let_step(
1010                        let_step,
1011                        current.clone(),
1012                        record,
1013                        context,
1014                        out,
1015                        &step_path,
1016                        &step_ctx,
1017                    )?;
1018                    // Let doesn't change pipe value
1019                    current = step_ctx.get_pipe_value().cloned().unwrap_or(current);
1020                }
1021                V2Step::If(if_step) => {
1022                    current = eval_v2_if_step(
1023                        if_step, current, record, context, out, &step_path, &step_ctx,
1024                    )?;
1025                }
1026                V2Step::Map(nested_map) => {
1027                    current = eval_v2_map_step(
1028                        nested_map, current, record, context, out, &step_path, &step_ctx,
1029                    )?;
1030                }
1031                V2Step::Ref(v2_ref) => {
1032                    // Reference step evaluates the reference and returns its value
1033                    current = eval_v2_ref(v2_ref, record, context, out, &step_path, &step_ctx)?;
1034                }
1035            };
1036        }
1037
1038        // Only add non-missing values to results
1039        if let EvalValue::Value(v) = current {
1040            results.push(v);
1041        }
1042    }
1043
1044    Ok(EvalValue::Value(JsonValue::Array(results)))
1045}
1046
1047/// Evaluate a v2 condition - returns bool
1048pub fn eval_v2_condition<'a>(
1049    condition: &V2Condition,
1050    record: &'a JsonValue,
1051    context: Option<&'a JsonValue>,
1052    out: &'a JsonValue,
1053    path: &str,
1054    ctx: &V2EvalContext<'a>,
1055) -> Result<bool, TransformError> {
1056    match condition {
1057        V2Condition::All(conditions) => {
1058            for (i, cond) in conditions.iter().enumerate() {
1059                let cond_path = format!("{}[{}]", path, i);
1060                if !eval_v2_condition(cond, record, context, out, &cond_path, ctx)? {
1061                    return Ok(false);
1062                }
1063            }
1064            Ok(true)
1065        }
1066        V2Condition::Any(conditions) => {
1067            for (i, cond) in conditions.iter().enumerate() {
1068                let cond_path = format!("{}[{}]", path, i);
1069                if eval_v2_condition(cond, record, context, out, &cond_path, ctx)? {
1070                    return Ok(true);
1071                }
1072            }
1073            Ok(false)
1074        }
1075        V2Condition::Comparison(comparison) => {
1076            eval_v2_comparison(comparison, record, context, out, path, ctx)
1077        }
1078        V2Condition::Expr(expr) => {
1079            let expr_path = format!("{}.expr", path);
1080            let value = eval_v2_expr(expr, record, context, out, &expr_path, ctx)?;
1081            match value {
1082                EvalValue::Value(JsonValue::Bool(flag)) => Ok(flag),
1083                EvalValue::Missing => Ok(false),
1084                EvalValue::Value(_) => Err(TransformError::new(
1085                    TransformErrorKind::ExprError,
1086                    "when/record_when must evaluate to boolean",
1087                )
1088                .with_path(&expr_path)),
1089            }
1090        }
1091    }
1092}
1093
1094/// Evaluate a v2 comparison
1095fn eval_v2_comparison<'a>(
1096    comparison: &V2Comparison,
1097    record: &'a JsonValue,
1098    context: Option<&'a JsonValue>,
1099    out: &'a JsonValue,
1100    path: &str,
1101    ctx: &V2EvalContext<'a>,
1102) -> Result<bool, TransformError> {
1103    if comparison.args.len() != 2 {
1104        return Err(TransformError::new(
1105            TransformErrorKind::ExprError,
1106            format!(
1107                "comparison requires exactly 2 arguments, got {}",
1108                comparison.args.len()
1109            ),
1110        )
1111        .with_path(path));
1112    }
1113
1114    let left_path = format!("{}.args[0]", path);
1115    let right_path = format!("{}.args[1]", path);
1116
1117    let left = eval_v2_expr(&comparison.args[0], record, context, out, &left_path, ctx)?;
1118    let right = eval_v2_expr(&comparison.args[1], record, context, out, &right_path, ctx)?;
1119
1120    match comparison.op {
1121        V2ComparisonOp::Eq => Ok(compare_values_eq(&left, &right)),
1122        V2ComparisonOp::Ne => Ok(!compare_values_eq(&left, &right)),
1123        V2ComparisonOp::Gt => {
1124            compare_values_ord(&left, &right, path).map(|ord| ord == std::cmp::Ordering::Greater)
1125        }
1126        V2ComparisonOp::Gte => {
1127            compare_values_ord(&left, &right, path).map(|ord| ord != std::cmp::Ordering::Less)
1128        }
1129        V2ComparisonOp::Lt => {
1130            compare_values_ord(&left, &right, path).map(|ord| ord == std::cmp::Ordering::Less)
1131        }
1132        V2ComparisonOp::Lte => {
1133            compare_values_ord(&left, &right, path).map(|ord| ord != std::cmp::Ordering::Greater)
1134        }
1135        V2ComparisonOp::Match => compare_values_match(&left, &right, path),
1136    }
1137}
1138
1139/// Compare two values for equality
1140fn compare_values_eq(left: &EvalValue, right: &EvalValue) -> bool {
1141    match (left, right) {
1142        (EvalValue::Value(l), EvalValue::Value(r)) => l == r,
1143        (EvalValue::Missing, EvalValue::Missing) => true,
1144        (EvalValue::Missing, EvalValue::Value(r)) => r.is_null(),
1145        (EvalValue::Value(l), EvalValue::Missing) => l.is_null(),
1146    }
1147}
1148
1149/// Compare two values for ordering
1150fn compare_values_ord(
1151    left: &EvalValue,
1152    right: &EvalValue,
1153    path: &str,
1154) -> Result<std::cmp::Ordering, TransformError> {
1155    match (left, right) {
1156        (EvalValue::Value(l), EvalValue::Value(r)) => {
1157            // Try numeric comparison first
1158            if let (Some(l_num), Some(r_num)) = (value_as_f64(l), value_as_f64(r)) {
1159                return Ok(l_num
1160                    .partial_cmp(&r_num)
1161                    .unwrap_or(std::cmp::Ordering::Equal));
1162            }
1163            // Try string comparison
1164            if let (Some(l_str), Some(r_str)) = (value_as_str(l), value_as_str(r)) {
1165                return Ok(l_str.cmp(r_str));
1166            }
1167            Err(TransformError::new(
1168                TransformErrorKind::ExprError,
1169                "cannot compare values of different types",
1170            )
1171            .with_path(path))
1172        }
1173        _ => Err(TransformError::new(
1174            TransformErrorKind::ExprError,
1175            "cannot compare missing values",
1176        )
1177        .with_path(path)),
1178    }
1179}
1180
1181/// Compare with regex match
1182fn compare_values_match(
1183    left: &EvalValue,
1184    right: &EvalValue,
1185    path: &str,
1186) -> Result<bool, TransformError> {
1187    let text = match left {
1188        EvalValue::Value(JsonValue::String(s)) => s.as_str(),
1189        _ => {
1190            return Err(TransformError::new(
1191                TransformErrorKind::ExprError,
1192                "match operator requires string on left side",
1193            )
1194            .with_path(path));
1195        }
1196    };
1197
1198    let pattern = match right {
1199        EvalValue::Value(JsonValue::String(s)) => s.as_str(),
1200        _ => {
1201            return Err(TransformError::new(
1202                TransformErrorKind::ExprError,
1203                "match operator requires regex pattern string on right side",
1204            )
1205            .with_path(path));
1206        }
1207    };
1208
1209    let re = regex::Regex::new(pattern).map_err(|e| {
1210        TransformError::new(
1211            TransformErrorKind::ExprError,
1212            format!("invalid regex pattern: {}", e),
1213        )
1214        .with_path(path)
1215    })?;
1216
1217    Ok(re.is_match(text))
1218}
1219
1220/// Helper to get f64 from JsonValue
1221fn value_as_f64(v: &JsonValue) -> Option<f64> {
1222    match v {
1223        JsonValue::Number(n) => n.as_f64(),
1224        JsonValue::String(s) => s.parse::<f64>().ok(),
1225        _ => None,
1226    }
1227}
1228
1229/// Helper to get str from JsonValue
1230fn value_as_str(v: &JsonValue) -> Option<&str> {
1231    match v {
1232        JsonValue::String(s) => Some(s.as_str()),
1233        _ => None,
1234    }
1235}
1236
1237/// Evaluate a v2 expression
1238pub fn eval_v2_expr<'a>(
1239    expr: &V2Expr,
1240    record: &'a JsonValue,
1241    context: Option<&'a JsonValue>,
1242    out: &'a JsonValue,
1243    path: &str,
1244    ctx: &V2EvalContext<'a>,
1245) -> Result<EvalValue, TransformError> {
1246    match expr {
1247        V2Expr::Pipe(pipe) => eval_v2_pipe(pipe, record, context, out, path, ctx),
1248        V2Expr::V1Fallback(_) => Err(TransformError::new(
1249            TransformErrorKind::ExprError,
1250            "v1 fallback not yet implemented",
1251        )
1252        .with_path(path)),
1253    }
1254}
1255
1256/// Helper to convert EvalValue to string
1257fn eval_value_as_string(value: &EvalValue, path: &str) -> Result<String, TransformError> {
1258    match value {
1259        EvalValue::Missing => Err(TransformError::new(
1260            TransformErrorKind::ExprError,
1261            "expected string, got missing value",
1262        )
1263        .with_path(path)),
1264        EvalValue::Value(v) => match v {
1265            JsonValue::String(s) => Ok(s.clone()),
1266            JsonValue::Number(n) => Ok(n.to_string()),
1267            JsonValue::Bool(b) => Ok(b.to_string()),
1268            _ => Err(TransformError::new(
1269                TransformErrorKind::ExprError,
1270                format!("expected string, got {:?}", v),
1271            )
1272            .with_path(path)),
1273        },
1274    }
1275}
1276
1277/// Helper to convert EvalValue to number
1278fn eval_value_as_number(value: &EvalValue, path: &str) -> Result<f64, TransformError> {
1279    match value {
1280        EvalValue::Missing => Err(TransformError::new(
1281            TransformErrorKind::ExprError,
1282            "expected number, got missing value",
1283        )
1284        .with_path(path)),
1285        EvalValue::Value(v) => match v {
1286            JsonValue::Number(n) => n.as_f64().ok_or_else(|| {
1287                TransformError::new(TransformErrorKind::ExprError, "number conversion failed")
1288                    .with_path(path)
1289            }),
1290            JsonValue::String(s) => s.parse::<f64>().map_err(|_| {
1291                TransformError::new(
1292                    TransformErrorKind::ExprError,
1293                    "failed to parse string as number",
1294                )
1295                .with_path(path)
1296            }),
1297            _ => Err(TransformError::new(
1298                TransformErrorKind::ExprError,
1299                format!("expected number, got {:?}", v),
1300            )
1301            .with_path(path)),
1302        },
1303    }
1304}
1305
1306fn value_as_bool(value: &JsonValue, path: &str) -> Result<bool, TransformError> {
1307    match value {
1308        JsonValue::Bool(flag) => Ok(*flag),
1309        _ => Err(
1310            TransformError::new(TransformErrorKind::ExprError, "value must be a boolean")
1311                .with_path(path),
1312        ),
1313    }
1314}
1315
1316fn value_as_string(value: &JsonValue, path: &str) -> Result<String, TransformError> {
1317    match value {
1318        JsonValue::String(value) => Ok(value.clone()),
1319        _ => Err(
1320            TransformError::new(TransformErrorKind::ExprError, "value must be a string")
1321                .with_path(path),
1322        ),
1323    }
1324}
1325
1326fn value_to_number(value: &JsonValue, path: &str, message: &str) -> Result<f64, TransformError> {
1327    match value {
1328        JsonValue::Number(n) => n.as_f64().filter(|f| f.is_finite()).ok_or_else(|| {
1329            TransformError::new(TransformErrorKind::ExprError, message).with_path(path)
1330        }),
1331        JsonValue::String(s) => s
1332            .parse::<f64>()
1333            .ok()
1334            .filter(|f| f.is_finite())
1335            .ok_or_else(|| {
1336                TransformError::new(TransformErrorKind::ExprError, message).with_path(path)
1337            }),
1338        _ => Err(TransformError::new(TransformErrorKind::ExprError, message).with_path(path)),
1339    }
1340}
1341
1342fn compare_eq_v1(
1343    left: &JsonValue,
1344    right: &JsonValue,
1345    left_path: &str,
1346    right_path: &str,
1347) -> Result<bool, TransformError> {
1348    if left.is_null() || right.is_null() {
1349        return Ok(left.is_null() && right.is_null());
1350    }
1351
1352    let left_value = value_to_string(left, left_path)?;
1353    let right_value = value_to_string(right, right_path)?;
1354    Ok(left_value == right_value)
1355}
1356
1357fn compare_numbers_v1<F>(
1358    left: &JsonValue,
1359    right: &JsonValue,
1360    left_path: &str,
1361    right_path: &str,
1362    compare: F,
1363) -> Result<bool, TransformError>
1364where
1365    F: FnOnce(f64, f64) -> bool,
1366{
1367    let left_value = value_to_number(left, left_path, "comparison operand must be a number")?;
1368    let right_value = value_to_number(right, right_path, "comparison operand must be a number")?;
1369    Ok(compare(left_value, right_value))
1370}
1371
1372fn match_regex_v1(
1373    left: &JsonValue,
1374    right: &JsonValue,
1375    left_path: &str,
1376    right_path: &str,
1377) -> Result<bool, TransformError> {
1378    let value = value_as_string(left, left_path)?;
1379    let pattern = value_as_string(right, right_path)?;
1380    let regex = regex::Regex::new(&pattern).map_err(|e| {
1381        TransformError::new(
1382            TransformErrorKind::ExprError,
1383            format!("invalid regex pattern: {}", e),
1384        )
1385        .with_path(right_path)
1386    })?;
1387    Ok(regex.is_match(&value))
1388}
1389
1390fn eval_v2_expr_or_null<'a>(
1391    expr: &V2Expr,
1392    record: &'a JsonValue,
1393    context: Option<&'a JsonValue>,
1394    out: &'a JsonValue,
1395    path: &str,
1396    ctx: &V2EvalContext<'a>,
1397) -> Result<JsonValue, TransformError> {
1398    match eval_v2_expr(expr, record, context, out, path, ctx)? {
1399        EvalValue::Missing => Ok(JsonValue::Null),
1400        EvalValue::Value(value) => Ok(value),
1401    }
1402}
1403
1404fn eval_v2_predicate_expr<'a>(
1405    expr: &V2Expr,
1406    record: &'a JsonValue,
1407    context: Option<&'a JsonValue>,
1408    out: &'a JsonValue,
1409    path: &str,
1410    ctx: &V2EvalContext<'a>,
1411) -> Result<bool, TransformError> {
1412    match eval_v2_expr(expr, record, context, out, path, ctx)? {
1413        EvalValue::Missing => Ok(false),
1414        EvalValue::Value(value) => {
1415            if value.is_null() {
1416                return Ok(false);
1417            }
1418            value_as_bool(&value, path)
1419        }
1420    }
1421}
1422
1423fn eval_v2_key_expr_string<'a>(
1424    expr: &V2Expr,
1425    record: &'a JsonValue,
1426    context: Option<&'a JsonValue>,
1427    out: &'a JsonValue,
1428    path: &str,
1429    ctx: &V2EvalContext<'a>,
1430) -> Result<String, TransformError> {
1431    let value = match eval_v2_expr(expr, record, context, out, path, ctx)? {
1432        EvalValue::Missing => {
1433            return Err(TransformError::new(
1434                TransformErrorKind::ExprError,
1435                "expr arg must not be missing",
1436            )
1437            .with_path(path));
1438        }
1439        EvalValue::Value(value) => value,
1440    };
1441    if value.is_null() {
1442        return Err(TransformError::new(
1443            TransformErrorKind::ExprError,
1444            "expr arg must not be null",
1445        )
1446        .with_path(path));
1447    }
1448    value_to_string(&value, path)
1449}
1450
1451#[derive(Clone, Copy, PartialEq, Eq)]
1452enum SortKeyKind {
1453    Number,
1454    String,
1455    Bool,
1456}
1457
1458#[derive(Clone)]
1459enum SortKey {
1460    Number(f64),
1461    String(String),
1462    Bool(bool),
1463}
1464
1465impl SortKey {
1466    fn kind(&self) -> SortKeyKind {
1467        match self {
1468            SortKey::Number(_) => SortKeyKind::Number,
1469            SortKey::String(_) => SortKeyKind::String,
1470            SortKey::Bool(_) => SortKeyKind::Bool,
1471        }
1472    }
1473}
1474
1475fn compare_sort_keys(left: &SortKey, right: &SortKey) -> std::cmp::Ordering {
1476    match (left, right) {
1477        (SortKey::Number(l), SortKey::Number(r)) => {
1478            l.partial_cmp(r).unwrap_or(std::cmp::Ordering::Equal)
1479        }
1480        (SortKey::String(l), SortKey::String(r)) => l.cmp(r),
1481        (SortKey::Bool(l), SortKey::Bool(r)) => l.cmp(r),
1482        _ => std::cmp::Ordering::Equal,
1483    }
1484}
1485
1486fn eval_v2_sort_key<'a>(
1487    expr: &V2Expr,
1488    record: &'a JsonValue,
1489    context: Option<&'a JsonValue>,
1490    out: &'a JsonValue,
1491    path: &str,
1492    ctx: &V2EvalContext<'a>,
1493) -> Result<SortKey, TransformError> {
1494    let value = match eval_v2_expr(expr, record, context, out, path, ctx)? {
1495        EvalValue::Missing => {
1496            return Err(TransformError::new(
1497                TransformErrorKind::ExprError,
1498                "expr arg must not be missing",
1499            )
1500            .with_path(path));
1501        }
1502        EvalValue::Value(value) => value,
1503    };
1504    if value.is_null() {
1505        return Err(TransformError::new(
1506            TransformErrorKind::ExprError,
1507            "expr arg must not be null",
1508        )
1509        .with_path(path));
1510    }
1511
1512    match value {
1513        JsonValue::Number(number) => {
1514            let value = number
1515                .as_f64()
1516                .filter(|value| value.is_finite())
1517                .ok_or_else(|| {
1518                    TransformError::new(
1519                        TransformErrorKind::ExprError,
1520                        "sort_by key must be a finite number",
1521                    )
1522                    .with_path(path)
1523                })?;
1524            Ok(SortKey::Number(value))
1525        }
1526        JsonValue::String(value) => Ok(SortKey::String(value)),
1527        JsonValue::Bool(value) => Ok(SortKey::Bool(value)),
1528        _ => Err(TransformError::new(
1529            TransformErrorKind::ExprError,
1530            "sort_by key must be string/number/bool",
1531        )
1532        .with_path(path)),
1533    }
1534}
1535
1536fn eval_v2_array_from_eval_value(
1537    value: EvalValue,
1538    path: &str,
1539) -> Result<Vec<JsonValue>, TransformError> {
1540    match value {
1541        EvalValue::Missing => Ok(Vec::new()),
1542        EvalValue::Value(value) => {
1543            if value.is_null() {
1544                Ok(Vec::new())
1545            } else if let JsonValue::Array(items) = value {
1546                Ok(items)
1547            } else {
1548                Err(
1549                    TransformError::new(TransformErrorKind::ExprError, "expr arg must be an array")
1550                        .with_path(path),
1551                )
1552            }
1553        }
1554    }
1555}
1556fn v2_eval_to_v1_eval(value: &EvalValue) -> V1EvalValue {
1557    match value {
1558        EvalValue::Missing => V1EvalValue::Missing,
1559        EvalValue::Value(v) => V1EvalValue::Value(v.clone()),
1560    }
1561}
1562
1563fn v1_eval_to_v2_eval(value: V1EvalValue) -> EvalValue {
1564    match value {
1565        V1EvalValue::Missing => EvalValue::Missing,
1566        V1EvalValue::Value(v) => EvalValue::Value(v),
1567    }
1568}
1569
1570fn map_v2_op_name(op: &str) -> &str {
1571    match op {
1572        "add" => "+",
1573        "subtract" => "-",
1574        "multiply" => "*",
1575        "divide" => "/",
1576        _ => op,
1577    }
1578}
1579
1580fn eval_v2_op_with_v1_fallback<'a>(
1581    op_step: &V2OpStep,
1582    pipe_value: EvalValue,
1583    record: &'a JsonValue,
1584    context: Option<&'a JsonValue>,
1585    out: &'a JsonValue,
1586    path: &str,
1587    ctx: &V2EvalContext<'a>,
1588) -> Result<EvalValue, TransformError> {
1589    let mut v1_locals_map: HashMap<String, V1EvalValue> = ctx
1590        .let_bindings
1591        .iter()
1592        .map(|(k, v)| (k.clone(), v2_eval_to_v1_eval(v)))
1593        .collect();
1594    let mut arg_refs = Vec::with_capacity(op_step.args.len());
1595    for (index, arg) in op_step.args.iter().enumerate() {
1596        let arg_path = format!("{}.args[{}]", path, index);
1597        let value = eval_v2_expr(arg, record, context, out, &arg_path, ctx)?;
1598        let mut key = format!("__v2_arg{}", index);
1599        if v1_locals_map.contains_key(&key) {
1600            let mut suffix = 1usize;
1601            while v1_locals_map.contains_key(&format!("{}{}", key, suffix)) {
1602                suffix += 1;
1603            }
1604            key = format!("{}{}", key, suffix);
1605        }
1606        v1_locals_map.insert(key.clone(), v2_eval_to_v1_eval(&value));
1607        arg_refs.push(Expr::Ref(ExprRef {
1608            ref_path: format!("local.{}", key),
1609        }));
1610    }
1611
1612    let expr_op = ExprOp {
1613        op: map_v2_op_name(&op_step.op).to_string(),
1614        args: arg_refs,
1615    };
1616
1617    let v1_pipe = v2_eval_to_v1_eval(&pipe_value);
1618    let v1_item = ctx.get_item().map(|item| V1EvalItem {
1619        value: item.value,
1620        index: item.index,
1621    });
1622    let v1_locals = V1EvalLocals {
1623        item: v1_item,
1624        acc: ctx.get_acc(),
1625        pipe: Some(&v1_pipe),
1626        locals: Some(&v1_locals_map),
1627    };
1628
1629    let result = eval_v1_op(
1630        &expr_op,
1631        record,
1632        context,
1633        out,
1634        path,
1635        Some(&v1_pipe),
1636        Some(&v1_locals),
1637    )?;
1638
1639    Ok(v1_eval_to_v2_eval(result))
1640}
1641
1642fn number_to_string(number: &serde_json::Number) -> String {
1643    if let Some(i) = number.as_i64() {
1644        return i.to_string();
1645    }
1646    if let Some(u) = number.as_u64() {
1647        return u.to_string();
1648    }
1649    if let Some(f) = number.as_f64() {
1650        let mut s = format!("{}", f);
1651        if s.contains('.') {
1652            while s.ends_with('0') {
1653                s.pop();
1654            }
1655            if s.ends_with('.') {
1656                s.pop();
1657            }
1658        }
1659        return s;
1660    }
1661    number.to_string()
1662}
1663
1664fn value_to_string(value: &JsonValue, path: &str) -> Result<String, TransformError> {
1665    match value {
1666        JsonValue::String(s) => Ok(s.clone()),
1667        JsonValue::Number(n) => Ok(number_to_string(n)),
1668        JsonValue::Bool(b) => Ok(b.to_string()),
1669        _ => Err(TransformError::new(
1670            TransformErrorKind::ExprError,
1671            "value must be string/number/bool",
1672        )
1673        .with_path(path)),
1674    }
1675}
1676
1677fn cast_to_int(value: &JsonValue, path: &str) -> Result<JsonValue, TransformError> {
1678    match value {
1679        JsonValue::Number(n) => {
1680            if let Some(i) = n.as_i64() {
1681                Ok(JsonValue::Number(i.into()))
1682            } else if let Some(f) = n.as_f64() {
1683                if (f.fract()).abs() < f64::EPSILON {
1684                    Ok(JsonValue::Number((f as i64).into()))
1685                } else {
1686                    Err(type_cast_error("int", path))
1687                }
1688            } else {
1689                Err(type_cast_error("int", path))
1690            }
1691        }
1692        JsonValue::String(s) => s
1693            .parse::<i64>()
1694            .map(|i| JsonValue::Number(i.into()))
1695            .map_err(|_| type_cast_error("int", path)),
1696        _ => Err(type_cast_error("int", path)),
1697    }
1698}
1699
1700fn cast_to_float(value: &JsonValue, path: &str) -> Result<JsonValue, TransformError> {
1701    match value {
1702        JsonValue::Number(n) => n
1703            .as_f64()
1704            .ok_or_else(|| type_cast_error("float", path))
1705            .and_then(|f| {
1706                serde_json::Number::from_f64(f)
1707                    .map(JsonValue::Number)
1708                    .ok_or_else(|| type_cast_error("float", path))
1709            }),
1710        JsonValue::String(s) => s
1711            .parse::<f64>()
1712            .map_err(|_| type_cast_error("float", path))
1713            .and_then(|f| {
1714                serde_json::Number::from_f64(f)
1715                    .map(JsonValue::Number)
1716                    .ok_or_else(|| type_cast_error("float", path))
1717            }),
1718        _ => Err(type_cast_error("float", path)),
1719    }
1720}
1721
1722fn cast_to_bool(value: &JsonValue, path: &str) -> Result<JsonValue, TransformError> {
1723    match value {
1724        JsonValue::Bool(b) => Ok(JsonValue::Bool(*b)),
1725        JsonValue::String(s) => match s.to_lowercase().as_str() {
1726            "true" => Ok(JsonValue::Bool(true)),
1727            "false" => Ok(JsonValue::Bool(false)),
1728            _ => Err(type_cast_error("bool", path)),
1729        },
1730        _ => Err(type_cast_error("bool", path)),
1731    }
1732}
1733
1734fn type_cast_error(type_name: &str, path: &str) -> TransformError {
1735    TransformError::new(
1736        TransformErrorKind::ExprError,
1737        format!("failed to cast to {}", type_name),
1738    )
1739    .with_path(path)
1740}
1741
1742fn eval_type_cast(op: &str, value: &EvalValue, path: &str) -> Result<EvalValue, TransformError> {
1743    match value {
1744        EvalValue::Missing => Ok(EvalValue::Missing),
1745        EvalValue::Value(v) => {
1746            let casted = match op {
1747                "string" => JsonValue::String(value_to_string(v, path)?),
1748                "int" => cast_to_int(v, path)?,
1749                "float" => cast_to_float(v, path)?,
1750                "bool" => cast_to_bool(v, path)?,
1751                _ => {
1752                    return Err(TransformError::new(
1753                        TransformErrorKind::ExprError,
1754                        "unknown cast op",
1755                    )
1756                    .with_path(path));
1757                }
1758            };
1759            Ok(EvalValue::Value(casted))
1760        }
1761    }
1762}
1763
1764/// Evaluate a v2 op step with a pipe value as implicit first argument
1765pub fn eval_v2_op_step<'a>(
1766    op_step: &V2OpStep,
1767    pipe_value: EvalValue,
1768    record: &'a JsonValue,
1769    context: Option<&'a JsonValue>,
1770    out: &'a JsonValue,
1771    path: &str,
1772    ctx: &V2EvalContext<'a>,
1773) -> Result<EvalValue, TransformError> {
1774    // Create a new context with the current pipe value
1775    let step_ctx = ctx.clone().with_pipe_value(pipe_value.clone());
1776
1777    // Handle "@..." as a reference (from shorthand string in step position)
1778    if op_step.op.starts_with('@') {
1779        use crate::v2_parser::parse_v2_ref;
1780        if let Some(v2_ref) = parse_v2_ref(&op_step.op) {
1781            return eval_v2_ref(&v2_ref, record, context, out, path, &step_ctx);
1782        }
1783        return Err(TransformError::new(
1784            TransformErrorKind::ExprError,
1785            format!("invalid reference: {}", op_step.op),
1786        )
1787        .with_path(path));
1788    }
1789
1790    match op_step.op.as_str() {
1791        // String operations
1792        "trim" => {
1793            if matches!(pipe_value, EvalValue::Missing) {
1794                return Ok(EvalValue::Missing);
1795            }
1796            let s = eval_value_as_string(&pipe_value, path)?;
1797            Ok(EvalValue::Value(JsonValue::String(s.trim().to_string())))
1798        }
1799        "lowercase" => {
1800            if matches!(pipe_value, EvalValue::Missing) {
1801                return Ok(EvalValue::Missing);
1802            }
1803            let s = eval_value_as_string(&pipe_value, path)?;
1804            Ok(EvalValue::Value(JsonValue::String(s.to_lowercase())))
1805        }
1806        "uppercase" => {
1807            if matches!(pipe_value, EvalValue::Missing) {
1808                return Ok(EvalValue::Missing);
1809            }
1810            let s = eval_value_as_string(&pipe_value, path)?;
1811            Ok(EvalValue::Value(JsonValue::String(s.to_uppercase())))
1812        }
1813        "to_string" => match &pipe_value {
1814            EvalValue::Missing => Ok(EvalValue::Missing),
1815            EvalValue::Value(v) => {
1816                let s = match v {
1817                    JsonValue::String(s) => s.clone(),
1818                    JsonValue::Number(n) => n.to_string(),
1819                    JsonValue::Bool(b) => b.to_string(),
1820                    JsonValue::Null => "null".to_string(),
1821                    JsonValue::Array(_) | JsonValue::Object(_) => v.to_string(),
1822                };
1823                Ok(EvalValue::Value(JsonValue::String(s)))
1824            }
1825        },
1826        "concat" => {
1827            // Pipe value is first, then args
1828            let mut parts = Vec::new();
1829            if matches!(pipe_value, EvalValue::Missing) {
1830                return Ok(EvalValue::Missing);
1831            }
1832            parts.push(eval_value_as_string(&pipe_value, path)?);
1833            for (i, arg) in op_step.args.iter().enumerate() {
1834                let arg_path = format!("{}.args[{}]", path, i);
1835                let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1836                if matches!(arg_value, EvalValue::Missing) {
1837                    return Ok(EvalValue::Missing);
1838                }
1839                parts.push(eval_value_as_string(&arg_value, &arg_path)?);
1840            }
1841            Ok(EvalValue::Value(JsonValue::String(parts.join(""))))
1842        }
1843        "string" | "int" | "float" | "bool" => {
1844            eval_type_cast(op_step.op.as_str(), &pipe_value, path)
1845        }
1846
1847        // Numeric operations
1848        "add" | "+" => {
1849            if matches!(pipe_value, EvalValue::Missing) {
1850                return Ok(EvalValue::Missing);
1851            }
1852            let mut result = eval_value_as_number(&pipe_value, path)?;
1853            for (i, arg) in op_step.args.iter().enumerate() {
1854                let arg_path = format!("{}.args[{}]", path, i);
1855                let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1856                if matches!(arg_value, EvalValue::Missing) {
1857                    return Ok(EvalValue::Missing);
1858                }
1859                result += eval_value_as_number(&arg_value, &arg_path)?;
1860            }
1861            Ok(EvalValue::Value(serde_json::json!(result)))
1862        }
1863        "subtract" | "-" => {
1864            if op_step.args.is_empty() {
1865                return Err(TransformError::new(
1866                    TransformErrorKind::ExprError,
1867                    "subtract requires at least one argument",
1868                )
1869                .with_path(path));
1870            }
1871            if matches!(pipe_value, EvalValue::Missing) {
1872                return Ok(EvalValue::Missing);
1873            }
1874            let mut result = eval_value_as_number(&pipe_value, path)?;
1875            for (i, arg) in op_step.args.iter().enumerate() {
1876                let arg_path = format!("{}.args[{}]", path, i);
1877                let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1878                if matches!(arg_value, EvalValue::Missing) {
1879                    return Ok(EvalValue::Missing);
1880                }
1881                result -= eval_value_as_number(&arg_value, &arg_path)?;
1882            }
1883            Ok(EvalValue::Value(serde_json::json!(result)))
1884        }
1885        "multiply" | "*" => {
1886            if matches!(pipe_value, EvalValue::Missing) {
1887                return Ok(EvalValue::Missing);
1888            }
1889            let mut result = eval_value_as_number(&pipe_value, path)?;
1890            for (i, arg) in op_step.args.iter().enumerate() {
1891                let arg_path = format!("{}.args[{}]", path, i);
1892                let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1893                if matches!(arg_value, EvalValue::Missing) {
1894                    return Ok(EvalValue::Missing);
1895                }
1896                result *= eval_value_as_number(&arg_value, &arg_path)?;
1897            }
1898            Ok(EvalValue::Value(serde_json::json!(result)))
1899        }
1900        "divide" | "/" => {
1901            if op_step.args.is_empty() {
1902                return Err(TransformError::new(
1903                    TransformErrorKind::ExprError,
1904                    "divide requires at least one argument",
1905                )
1906                .with_path(path));
1907            }
1908            if matches!(pipe_value, EvalValue::Missing) {
1909                return Ok(EvalValue::Missing);
1910            }
1911            let mut result = eval_value_as_number(&pipe_value, path)?;
1912            for (i, arg) in op_step.args.iter().enumerate() {
1913                let arg_path = format!("{}.args[{}]", path, i);
1914                let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1915                if matches!(arg_value, EvalValue::Missing) {
1916                    return Ok(EvalValue::Missing);
1917                }
1918                let divisor = eval_value_as_number(&arg_value, &arg_path)?;
1919                if divisor == 0.0 {
1920                    return Err(TransformError::new(
1921                        TransformErrorKind::ExprError,
1922                        "division by zero",
1923                    )
1924                    .with_path(&arg_path));
1925                }
1926                result /= divisor;
1927            }
1928            Ok(EvalValue::Value(serde_json::json!(result)))
1929        }
1930        "map" => {
1931            if op_step.args.len() != 1 {
1932                return Err(TransformError::new(
1933                    TransformErrorKind::ExprError,
1934                    "map requires exactly one argument",
1935                )
1936                .with_path(path));
1937            }
1938            let array = match pipe_value {
1939                EvalValue::Missing => {
1940                    return Ok(EvalValue::Missing);
1941                }
1942                EvalValue::Value(JsonValue::Array(items)) => items,
1943                EvalValue::Value(other) => {
1944                    return Err(TransformError::new(
1945                        TransformErrorKind::ExprError,
1946                        format!("expr arg must be an array, got {:?}", other),
1947                    )
1948                    .with_path(path));
1949                }
1950            };
1951            let arg_path = format!("{}.args[0]", path);
1952            let mut results = Vec::new();
1953            for (index, item) in array.iter().enumerate() {
1954                let item_ctx = step_ctx
1955                    .clone()
1956                    .with_pipe_value(EvalValue::Value(item.clone()))
1957                    .with_item(EvalItem { value: item, index });
1958                let value =
1959                    eval_v2_expr(&op_step.args[0], record, context, out, &arg_path, &item_ctx)?;
1960                if let EvalValue::Value(value) = value {
1961                    results.push(value);
1962                }
1963            }
1964            Ok(EvalValue::Value(JsonValue::Array(results)))
1965        }
1966        "filter" => {
1967            if op_step.args.len() != 1 {
1968                return Err(TransformError::new(
1969                    TransformErrorKind::ExprError,
1970                    "filter requires exactly one argument",
1971                )
1972                .with_path(path));
1973            }
1974            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
1975            let arg_path = format!("{}.args[0]", path);
1976            let mut results = Vec::new();
1977            for (index, item) in array.iter().enumerate() {
1978                let item_ctx = step_ctx
1979                    .clone()
1980                    .with_pipe_value(EvalValue::Value(item.clone()))
1981                    .with_item(EvalItem { value: item, index });
1982                if eval_v2_predicate_expr(
1983                    &op_step.args[0],
1984                    record,
1985                    context,
1986                    out,
1987                    &arg_path,
1988                    &item_ctx,
1989                )? {
1990                    results.push(item.clone());
1991                }
1992            }
1993            Ok(EvalValue::Value(JsonValue::Array(results)))
1994        }
1995        "flat_map" => {
1996            if op_step.args.len() != 1 {
1997                return Err(TransformError::new(
1998                    TransformErrorKind::ExprError,
1999                    "flat_map requires exactly one argument",
2000                )
2001                .with_path(path));
2002            }
2003            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2004            let arg_path = format!("{}.args[0]", path);
2005            let mut results = Vec::new();
2006            for (index, item) in array.iter().enumerate() {
2007                let item_ctx = step_ctx
2008                    .clone()
2009                    .with_pipe_value(EvalValue::Value(item.clone()))
2010                    .with_item(EvalItem { value: item, index });
2011                let value = eval_v2_expr_or_null(
2012                    &op_step.args[0],
2013                    record,
2014                    context,
2015                    out,
2016                    &arg_path,
2017                    &item_ctx,
2018                )?;
2019                match value {
2020                    JsonValue::Array(items) => results.extend(items),
2021                    value => results.push(value),
2022                }
2023            }
2024            Ok(EvalValue::Value(JsonValue::Array(results)))
2025        }
2026        "group_by" => {
2027            if op_step.args.len() != 1 {
2028                return Err(TransformError::new(
2029                    TransformErrorKind::ExprError,
2030                    "group_by requires exactly one argument",
2031                )
2032                .with_path(path));
2033            }
2034            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2035            let arg_path = format!("{}.args[0]", path);
2036            let mut results = serde_json::Map::new();
2037            for (index, item) in array.iter().enumerate() {
2038                let item_ctx = step_ctx
2039                    .clone()
2040                    .with_pipe_value(EvalValue::Value(item.clone()))
2041                    .with_item(EvalItem { value: item, index });
2042                let key = eval_v2_key_expr_string(
2043                    &op_step.args[0],
2044                    record,
2045                    context,
2046                    out,
2047                    &arg_path,
2048                    &item_ctx,
2049                )?;
2050                let entry = results
2051                    .entry(key)
2052                    .or_insert_with(|| JsonValue::Array(Vec::new()));
2053                if let JsonValue::Array(items) = entry {
2054                    items.push(item.clone());
2055                }
2056            }
2057            Ok(EvalValue::Value(JsonValue::Object(results)))
2058        }
2059        "key_by" => {
2060            if op_step.args.len() != 1 {
2061                return Err(TransformError::new(
2062                    TransformErrorKind::ExprError,
2063                    "key_by requires exactly one argument",
2064                )
2065                .with_path(path));
2066            }
2067            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2068            let arg_path = format!("{}.args[0]", path);
2069            let mut results = serde_json::Map::new();
2070            for (index, item) in array.iter().enumerate() {
2071                let item_ctx = step_ctx
2072                    .clone()
2073                    .with_pipe_value(EvalValue::Value(item.clone()))
2074                    .with_item(EvalItem { value: item, index });
2075                let key = eval_v2_key_expr_string(
2076                    &op_step.args[0],
2077                    record,
2078                    context,
2079                    out,
2080                    &arg_path,
2081                    &item_ctx,
2082                )?;
2083                results.insert(key, item.clone());
2084            }
2085            Ok(EvalValue::Value(JsonValue::Object(results)))
2086        }
2087        "partition" => {
2088            if op_step.args.len() != 1 {
2089                return Err(TransformError::new(
2090                    TransformErrorKind::ExprError,
2091                    "partition requires exactly one argument",
2092                )
2093                .with_path(path));
2094            }
2095            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2096            let arg_path = format!("{}.args[0]", path);
2097            let mut matched = Vec::new();
2098            let mut unmatched = Vec::new();
2099            for (index, item) in array.iter().enumerate() {
2100                let item_ctx = step_ctx
2101                    .clone()
2102                    .with_pipe_value(EvalValue::Value(item.clone()))
2103                    .with_item(EvalItem { value: item, index });
2104                if eval_v2_predicate_expr(
2105                    &op_step.args[0],
2106                    record,
2107                    context,
2108                    out,
2109                    &arg_path,
2110                    &item_ctx,
2111                )? {
2112                    matched.push(item.clone());
2113                } else {
2114                    unmatched.push(item.clone());
2115                }
2116            }
2117            Ok(EvalValue::Value(JsonValue::Array(vec![
2118                JsonValue::Array(matched),
2119                JsonValue::Array(unmatched),
2120            ])))
2121        }
2122        "distinct_by" => {
2123            if op_step.args.len() != 1 {
2124                return Err(TransformError::new(
2125                    TransformErrorKind::ExprError,
2126                    "distinct_by requires exactly one argument",
2127                )
2128                .with_path(path));
2129            }
2130            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2131            let arg_path = format!("{}.args[0]", path);
2132            let mut results = Vec::new();
2133            let mut seen = HashSet::new();
2134            for (index, item) in array.iter().enumerate() {
2135                let item_ctx = step_ctx
2136                    .clone()
2137                    .with_pipe_value(EvalValue::Value(item.clone()))
2138                    .with_item(EvalItem { value: item, index });
2139                let key = eval_v2_key_expr_string(
2140                    &op_step.args[0],
2141                    record,
2142                    context,
2143                    out,
2144                    &arg_path,
2145                    &item_ctx,
2146                )?;
2147                if seen.insert(key) {
2148                    results.push(item.clone());
2149                }
2150            }
2151            Ok(EvalValue::Value(JsonValue::Array(results)))
2152        }
2153        "sort_by" => {
2154            if !(1..=2).contains(&op_step.args.len()) {
2155                return Err(TransformError::new(
2156                    TransformErrorKind::ExprError,
2157                    "sort_by requires one or two arguments",
2158                )
2159                .with_path(path));
2160            }
2161            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2162            if array.is_empty() {
2163                return Ok(EvalValue::Value(JsonValue::Array(Vec::new())));
2164            }
2165            let expr_path = format!("{}.args[0]", path);
2166            let order = if op_step.args.len() == 2 {
2167                let order_path = format!("{}.args[1]", path);
2168                let order_value = eval_v2_expr(
2169                    &op_step.args[1],
2170                    record,
2171                    context,
2172                    out,
2173                    &order_path,
2174                    &step_ctx,
2175                )?;
2176                let order = match order_value {
2177                    EvalValue::Missing => return Ok(EvalValue::Missing),
2178                    EvalValue::Value(value) => value_to_string(&value, &order_path)?,
2179                };
2180                if order != "asc" && order != "desc" {
2181                    return Err(TransformError::new(
2182                        TransformErrorKind::ExprError,
2183                        "order must be asc or desc",
2184                    )
2185                    .with_path(order_path));
2186                }
2187                order
2188            } else {
2189                "asc".to_string()
2190            };
2191
2192            struct SortItem {
2193                key: SortKey,
2194                index: usize,
2195                value: JsonValue,
2196            }
2197
2198            let mut items = Vec::with_capacity(array.len());
2199            let mut key_kind: Option<SortKeyKind> = None;
2200            for (index, item) in array.iter().enumerate() {
2201                let item_ctx = step_ctx
2202                    .clone()
2203                    .with_pipe_value(EvalValue::Value(item.clone()))
2204                    .with_item(EvalItem { value: item, index });
2205                let key = eval_v2_sort_key(
2206                    &op_step.args[0],
2207                    record,
2208                    context,
2209                    out,
2210                    &expr_path,
2211                    &item_ctx,
2212                )?;
2213                let kind = key.kind();
2214                if let Some(existing) = key_kind {
2215                    if existing != kind {
2216                        return Err(TransformError::new(
2217                            TransformErrorKind::ExprError,
2218                            "sort_by keys must be all the same type",
2219                        )
2220                        .with_path(expr_path));
2221                    }
2222                } else {
2223                    key_kind = Some(kind);
2224                }
2225                items.push(SortItem {
2226                    key,
2227                    index,
2228                    value: item.clone(),
2229                });
2230            }
2231
2232            items.sort_by(|left, right| {
2233                let mut ordering = compare_sort_keys(&left.key, &right.key);
2234                if order == "desc" {
2235                    ordering = ordering.reverse();
2236                }
2237                if ordering == std::cmp::Ordering::Equal {
2238                    left.index.cmp(&right.index)
2239                } else {
2240                    ordering
2241                }
2242            });
2243
2244            let results = items.into_iter().map(|item| item.value).collect::<Vec<_>>();
2245            Ok(EvalValue::Value(JsonValue::Array(results)))
2246        }
2247        "find" => {
2248            if op_step.args.len() != 1 {
2249                return Err(TransformError::new(
2250                    TransformErrorKind::ExprError,
2251                    "find requires exactly one argument",
2252                )
2253                .with_path(path));
2254            }
2255            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2256            let arg_path = format!("{}.args[0]", path);
2257            for (index, item) in array.iter().enumerate() {
2258                let item_ctx = step_ctx
2259                    .clone()
2260                    .with_pipe_value(EvalValue::Value(item.clone()))
2261                    .with_item(EvalItem { value: item, index });
2262                if eval_v2_predicate_expr(
2263                    &op_step.args[0],
2264                    record,
2265                    context,
2266                    out,
2267                    &arg_path,
2268                    &item_ctx,
2269                )? {
2270                    return Ok(EvalValue::Value(item.clone()));
2271                }
2272            }
2273            Ok(EvalValue::Value(JsonValue::Null))
2274        }
2275        "find_index" => {
2276            if op_step.args.len() != 1 {
2277                return Err(TransformError::new(
2278                    TransformErrorKind::ExprError,
2279                    "find_index requires exactly one argument",
2280                )
2281                .with_path(path));
2282            }
2283            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2284            let arg_path = format!("{}.args[0]", path);
2285            for (index, item) in array.iter().enumerate() {
2286                let item_ctx = step_ctx
2287                    .clone()
2288                    .with_pipe_value(EvalValue::Value(item.clone()))
2289                    .with_item(EvalItem { value: item, index });
2290                if eval_v2_predicate_expr(
2291                    &op_step.args[0],
2292                    record,
2293                    context,
2294                    out,
2295                    &arg_path,
2296                    &item_ctx,
2297                )? {
2298                    return Ok(EvalValue::Value(JsonValue::Number((index as i64).into())));
2299                }
2300            }
2301            Ok(EvalValue::Value(JsonValue::Number((-1).into())))
2302        }
2303        "reduce" => {
2304            if op_step.args.len() != 1 {
2305                return Err(TransformError::new(
2306                    TransformErrorKind::ExprError,
2307                    "reduce requires exactly one argument",
2308                )
2309                .with_path(path));
2310            }
2311            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2312            if array.is_empty() {
2313                return Ok(EvalValue::Value(JsonValue::Null));
2314            }
2315            let expr_path = format!("{}.args[0]", path);
2316            let mut acc = array[0].clone();
2317            for (index, item) in array.iter().enumerate().skip(1) {
2318                let item_ctx = step_ctx
2319                    .clone()
2320                    .with_pipe_value(EvalValue::Value(item.clone()))
2321                    .with_item(EvalItem { value: item, index })
2322                    .with_acc(&acc);
2323                let value = eval_v2_expr_or_null(
2324                    &op_step.args[0],
2325                    record,
2326                    context,
2327                    out,
2328                    &expr_path,
2329                    &item_ctx,
2330                )?;
2331                acc = value;
2332            }
2333            Ok(EvalValue::Value(acc))
2334        }
2335        "fold" => {
2336            if op_step.args.len() != 2 {
2337                return Err(TransformError::new(
2338                    TransformErrorKind::ExprError,
2339                    "fold requires exactly two arguments",
2340                )
2341                .with_path(path));
2342            }
2343            let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2344            let init_path = format!("{}.args[0]", path);
2345            let initial = match eval_v2_expr(
2346                &op_step.args[0],
2347                record,
2348                context,
2349                out,
2350                &init_path,
2351                &step_ctx,
2352            )? {
2353                EvalValue::Missing => return Ok(EvalValue::Missing),
2354                EvalValue::Value(value) => value,
2355            };
2356            let expr_path = format!("{}.args[1]", path);
2357            let mut acc = initial;
2358            for (index, item) in array.iter().enumerate() {
2359                let item_ctx = step_ctx
2360                    .clone()
2361                    .with_pipe_value(EvalValue::Value(item.clone()))
2362                    .with_item(EvalItem { value: item, index })
2363                    .with_acc(&acc);
2364                let value = eval_v2_expr_or_null(
2365                    &op_step.args[1],
2366                    record,
2367                    context,
2368                    out,
2369                    &expr_path,
2370                    &item_ctx,
2371                )?;
2372                acc = value;
2373            }
2374            Ok(EvalValue::Value(acc))
2375        }
2376        "zip_with" => {
2377            if op_step.args.len() < 2 {
2378                return Err(TransformError::new(
2379                    TransformErrorKind::ExprError,
2380                    "zip_with requires at least two arguments",
2381                )
2382                .with_path(path));
2383            }
2384            let mut arrays = Vec::new();
2385            arrays.push(eval_v2_array_from_eval_value(pipe_value.clone(), path)?);
2386            for (index, arg) in op_step.args.iter().enumerate().take(op_step.args.len() - 1) {
2387                let arg_path = format!("{}.args[{}]", path, index);
2388                let value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
2389                arrays.push(eval_v2_array_from_eval_value(value, &arg_path)?);
2390            }
2391
2392            let min_len = arrays.iter().map(|items| items.len()).min().unwrap_or(0);
2393            let expr_index = op_step.args.len() - 1;
2394            let expr_path = format!("{}.args[{}]", path, expr_index);
2395            let expr = &op_step.args[expr_index];
2396            let mut results = Vec::with_capacity(min_len);
2397            for row_index in 0..min_len {
2398                let mut row = Vec::with_capacity(arrays.len());
2399                for array in &arrays {
2400                    row.push(array[row_index].clone());
2401                }
2402                let row_value = JsonValue::Array(row);
2403                let item_ctx = step_ctx
2404                    .clone()
2405                    .with_pipe_value(EvalValue::Value(row_value.clone()))
2406                    .with_item(EvalItem {
2407                        value: &row_value,
2408                        index: row_index,
2409                    });
2410                let value =
2411                    eval_v2_expr_or_null(expr, record, context, out, &expr_path, &item_ctx)?;
2412                results.push(value);
2413            }
2414            Ok(EvalValue::Value(JsonValue::Array(results)))
2415        }
2416        "first" => match &pipe_value {
2417            EvalValue::Missing => Ok(EvalValue::Missing),
2418            EvalValue::Value(JsonValue::Array(arr)) => {
2419                if let Some(value) = arr.first() {
2420                    Ok(EvalValue::Value(value.clone()))
2421                } else {
2422                    Ok(EvalValue::Missing)
2423                }
2424            }
2425            EvalValue::Value(other) => Err(TransformError::new(
2426                TransformErrorKind::ExprError,
2427                format!("first requires array, got {:?}", other),
2428            )
2429            .with_path(path)),
2430        },
2431        "last" => match &pipe_value {
2432            EvalValue::Missing => Ok(EvalValue::Missing),
2433            EvalValue::Value(JsonValue::Array(arr)) => {
2434                if let Some(value) = arr.last() {
2435                    Ok(EvalValue::Value(value.clone()))
2436                } else {
2437                    Ok(EvalValue::Missing)
2438                }
2439            }
2440            EvalValue::Value(other) => Err(TransformError::new(
2441                TransformErrorKind::ExprError,
2442                format!("last requires array, got {:?}", other),
2443            )
2444            .with_path(path)),
2445        },
2446
2447        // Coalesce
2448        "coalesce" => {
2449            // If pipe value is present and not null, use it
2450            if let EvalValue::Value(v) = &pipe_value {
2451                if !v.is_null() {
2452                    return Ok(pipe_value);
2453                }
2454            }
2455            // Otherwise, try args in order
2456            for (i, arg) in op_step.args.iter().enumerate() {
2457                let arg_path = format!("{}.args[{}]", path, i);
2458                let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
2459                if let EvalValue::Value(v) = &arg_value {
2460                    if !v.is_null() {
2461                        return Ok(arg_value);
2462                    }
2463                }
2464            }
2465            Ok(EvalValue::Missing)
2466        }
2467        "and" | "or" => {
2468            let is_and = op_step.op == "and";
2469            let total_len = op_step.args.len() + 1;
2470            if total_len < 2 {
2471                return Err(TransformError::new(
2472                    TransformErrorKind::ExprError,
2473                    "expr.args must contain at least two items",
2474                )
2475                .with_path(format!("{}.args", path)));
2476            }
2477
2478            let mut saw_missing = false;
2479            match &pipe_value {
2480                EvalValue::Missing => saw_missing = true,
2481                EvalValue::Value(value) => {
2482                    let flag = value_as_bool(value, path)?;
2483                    if is_and {
2484                        if !flag {
2485                            return Ok(EvalValue::Value(JsonValue::Bool(false)));
2486                        }
2487                    } else if flag {
2488                        return Ok(EvalValue::Value(JsonValue::Bool(true)));
2489                    }
2490                }
2491            }
2492
2493            for (index, arg) in op_step.args.iter().enumerate() {
2494                let arg_path = format!("{}.args[{}]", path, index);
2495                let value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
2496                match value {
2497                    EvalValue::Missing => {
2498                        saw_missing = true;
2499                        continue;
2500                    }
2501                    EvalValue::Value(value) => {
2502                        let flag = value_as_bool(&value, &arg_path)?;
2503                        if is_and {
2504                            if !flag {
2505                                return Ok(EvalValue::Value(JsonValue::Bool(false)));
2506                            }
2507                        } else if flag {
2508                            return Ok(EvalValue::Value(JsonValue::Bool(true)));
2509                        }
2510                    }
2511                }
2512            }
2513
2514            if saw_missing {
2515                Ok(EvalValue::Missing)
2516            } else {
2517                Ok(EvalValue::Value(JsonValue::Bool(is_and)))
2518            }
2519        }
2520        "not" => {
2521            if !op_step.args.is_empty() {
2522                return Err(TransformError::new(
2523                    TransformErrorKind::ExprError,
2524                    "expr.args must contain exactly one item",
2525                )
2526                .with_path(format!("{}.args", path)));
2527            }
2528            match pipe_value {
2529                EvalValue::Missing => Ok(EvalValue::Missing),
2530                EvalValue::Value(value) => {
2531                    let flag = value_as_bool(&value, path)?;
2532                    Ok(EvalValue::Value(JsonValue::Bool(!flag)))
2533                }
2534            }
2535        }
2536        "==" | "!=" | "<" | "<=" | ">" | ">=" | "~=" | "eq" | "ne" | "lt" | "lte" | "gt"
2537        | "gte" | "match" => {
2538            if op_step.args.len() != 1 {
2539                return Err(TransformError::new(
2540                    TransformErrorKind::ExprError,
2541                    "expr.args must contain exactly one item",
2542                )
2543                .with_path(format!("{}.args", path)));
2544            }
2545            let left = match pipe_value {
2546                EvalValue::Missing => JsonValue::Null,
2547                EvalValue::Value(value) => value,
2548            };
2549            let right_path = format!("{}.args[0]", path);
2550            let right = eval_v2_expr_or_null(
2551                &op_step.args[0],
2552                record,
2553                context,
2554                out,
2555                &right_path,
2556                &step_ctx,
2557            )?;
2558            let left_path = path.to_string();
2559            let op = match op_step.op.as_str() {
2560                "eq" => "==",
2561                "ne" => "!=",
2562                "lt" => "<",
2563                "lte" => "<=",
2564                "gt" => ">",
2565                "gte" => ">=",
2566                "match" => "~=",
2567                other => other,
2568            };
2569            let result = match op {
2570                "==" => compare_eq_v1(&left, &right, &left_path, &right_path)?,
2571                "!=" => !compare_eq_v1(&left, &right, &left_path, &right_path)?,
2572                "<" => compare_numbers_v1(&left, &right, &left_path, &right_path, |l, r| l < r)?,
2573                "<=" => compare_numbers_v1(&left, &right, &left_path, &right_path, |l, r| l <= r)?,
2574                ">" => compare_numbers_v1(&left, &right, &left_path, &right_path, |l, r| l > r)?,
2575                ">=" => compare_numbers_v1(&left, &right, &left_path, &right_path, |l, r| l >= r)?,
2576                "~=" => match_regex_v1(&left, &right, &left_path, &right_path)?,
2577                _ => false,
2578            };
2579            Ok(EvalValue::Value(JsonValue::Bool(result)))
2580        }
2581        "pick" | "omit" => {
2582            if op_step.args.is_empty() {
2583                return Err(TransformError::new(
2584                    TransformErrorKind::ExprError,
2585                    format!("{} requires at least one argument", op_step.op),
2586                )
2587                .with_path(format!("{}.args", path)));
2588            }
2589
2590            let mut path_values = Vec::new();
2591            for (index, arg) in op_step.args.iter().enumerate() {
2592                let arg_path = format!("{}.args[{}]", path, index);
2593                let value = match eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)? {
2594                    EvalValue::Missing => return Ok(EvalValue::Missing),
2595                    EvalValue::Value(value) => value,
2596                };
2597                if value.is_null() {
2598                    return Err(TransformError::new(
2599                        TransformErrorKind::ExprError,
2600                        "expr arg must not be null",
2601                    )
2602                    .with_path(arg_path));
2603                }
2604                match value {
2605                    JsonValue::String(path_value) => {
2606                        path_values.push(JsonValue::String(path_value));
2607                    }
2608                    JsonValue::Array(items) => {
2609                        for (item_index, item) in items.iter().enumerate() {
2610                            let item_path = format!("{}.args[{}][{}]", path, index, item_index);
2611                            let path_value = item.as_str().ok_or_else(|| {
2612                                TransformError::new(
2613                                    TransformErrorKind::ExprError,
2614                                    "paths must be a string or array of strings",
2615                                )
2616                                .with_path(item_path)
2617                            })?;
2618                            path_values.push(JsonValue::String(path_value.to_string()));
2619                        }
2620                    }
2621                    _ => {
2622                        return Err(TransformError::new(
2623                            TransformErrorKind::ExprError,
2624                            "paths must be a string or array of strings",
2625                        )
2626                        .with_path(arg_path));
2627                    }
2628                }
2629            }
2630
2631            let normalized_op = V2OpStep {
2632                op: op_step.op.clone(),
2633                args: vec![V2Expr::Pipe(V2Pipe {
2634                    start: V2Start::Literal(JsonValue::Array(path_values)),
2635                    steps: vec![],
2636                })],
2637            };
2638            eval_v2_op_with_v1_fallback(
2639                &normalized_op,
2640                pipe_value,
2641                record,
2642                context,
2643                out,
2644                path,
2645                &step_ctx,
2646            )
2647        }
2648
2649        // Lookup operations - v2 keyword format: lookup_first: {from: ..., match: [...], get: ...}
2650        // For v2, lookup args are parsed from V2OpStep with special handling
2651        // Explicit from:
2652        // args[0] = from (array to search in)
2653        // args[1] = match key (field name in array items to match)
2654        // args[2] = match value (value to match against)
2655        // args[3] = get (optional - field to extract from matched item)
2656        // Implicit from (pipe value):
2657        // args[0] = match key
2658        // args[1] = match value
2659        // args[2] = get (optional)
2660        "lookup_first" => {
2661            if op_step.args.len() < 2 {
2662                return Err(TransformError::new(
2663                    TransformErrorKind::ExprError,
2664                    "lookup_first requires at least 2 arguments: match_key, match_value",
2665                )
2666                .with_path(path));
2667            }
2668
2669            let args = &op_step.args;
2670            let from_path = format!("{}.from", path);
2671            let match_key_path = format!("{}.match_key", path);
2672            let get_path = format!("{}.get", path);
2673
2674            let (from_value, match_key_value, match_value, get_field) = match args.len() {
2675                0 | 1 => unreachable!("guarded above"),
2676                2 => {
2677                    let match_key_value = eval_v2_expr(
2678                        &args[0],
2679                        record,
2680                        context,
2681                        out,
2682                        &format!("{}.args[0]", path),
2683                        &step_ctx,
2684                    )?;
2685                    let match_value = eval_v2_expr(
2686                        &args[1],
2687                        record,
2688                        context,
2689                        out,
2690                        &format!("{}.args[1]", path),
2691                        &step_ctx,
2692                    )?;
2693                    (pipe_value.clone(), match_key_value, match_value, None)
2694                }
2695                3 => {
2696                    if matches!(pipe_value, EvalValue::Missing) {
2697                        let first_value = eval_v2_expr(
2698                            &args[0],
2699                            record,
2700                            context,
2701                            out,
2702                            &format!("{}.args[0]", path),
2703                            &step_ctx,
2704                        )?;
2705                        let use_explicit_from =
2706                            matches!(first_value, EvalValue::Value(JsonValue::Array(_)));
2707                        if !use_explicit_from {
2708                            return Ok(EvalValue::Missing);
2709                        }
2710                        let match_key_value = eval_v2_expr(
2711                            &args[1],
2712                            record,
2713                            context,
2714                            out,
2715                            &format!("{}.args[1]", path),
2716                            &step_ctx,
2717                        )?;
2718                        let match_value = eval_v2_expr(
2719                            &args[2],
2720                            record,
2721                            context,
2722                            out,
2723                            &format!("{}.args[2]", path),
2724                            &step_ctx,
2725                        )?;
2726                        (first_value, match_key_value, match_value, None)
2727                    } else {
2728                        let first_value = eval_v2_expr(
2729                            &args[0],
2730                            record,
2731                            context,
2732                            out,
2733                            &format!("{}.args[0]", path),
2734                            &step_ctx,
2735                        )?;
2736                        let use_explicit_from = matches!(
2737                            first_value,
2738                            EvalValue::Value(JsonValue::Array(_)) | EvalValue::Missing
2739                        );
2740                        if use_explicit_from {
2741                            let match_key_value = eval_v2_expr(
2742                                &args[1],
2743                                record,
2744                                context,
2745                                out,
2746                                &format!("{}.args[1]", path),
2747                                &step_ctx,
2748                            )?;
2749                            let match_value = eval_v2_expr(
2750                                &args[2],
2751                                record,
2752                                context,
2753                                out,
2754                                &format!("{}.args[2]", path),
2755                                &step_ctx,
2756                            )?;
2757                            (first_value, match_key_value, match_value, None)
2758                        } else {
2759                            let match_value = eval_v2_expr(
2760                                &args[1],
2761                                record,
2762                                context,
2763                                out,
2764                                &format!("{}.args[1]", path),
2765                                &step_ctx,
2766                            )?;
2767                            let get_value = eval_v2_expr(
2768                                &args[2],
2769                                record,
2770                                context,
2771                                out,
2772                                &format!("{}.args[2]", path),
2773                                &step_ctx,
2774                            )?;
2775                            let get_field = Some(eval_value_as_string(&get_value, &get_path)?);
2776                            (pipe_value.clone(), first_value, match_value, get_field)
2777                        }
2778                    }
2779                }
2780                _ => {
2781                    let from_value = eval_v2_expr(
2782                        &args[0],
2783                        record,
2784                        context,
2785                        out,
2786                        &format!("{}.args[0]", path),
2787                        &step_ctx,
2788                    )?;
2789                    let match_key_value = eval_v2_expr(
2790                        &args[1],
2791                        record,
2792                        context,
2793                        out,
2794                        &format!("{}.args[1]", path),
2795                        &step_ctx,
2796                    )?;
2797                    let match_value = eval_v2_expr(
2798                        &args[2],
2799                        record,
2800                        context,
2801                        out,
2802                        &format!("{}.args[2]", path),
2803                        &step_ctx,
2804                    )?;
2805                    let get_value = eval_v2_expr(
2806                        &args[3],
2807                        record,
2808                        context,
2809                        out,
2810                        &format!("{}.args[3]", path),
2811                        &step_ctx,
2812                    )?;
2813                    let get_field = Some(eval_value_as_string(&get_value, &get_path)?);
2814                    (from_value, match_key_value, match_value, get_field)
2815                }
2816            };
2817
2818            // Evaluate 'from' - the array to search in
2819            let arr = match &from_value {
2820                EvalValue::Value(JsonValue::Array(arr)) => arr,
2821                EvalValue::Missing => return Ok(EvalValue::Missing),
2822                _ => {
2823                    return Err(TransformError::new(
2824                        TransformErrorKind::ExprError,
2825                        "lookup_first 'from' must be an array",
2826                    )
2827                    .with_path(&from_path));
2828                }
2829            };
2830
2831            // Get match key as string
2832            let match_key = eval_value_as_string(&match_key_value, &match_key_path)?;
2833            if matches!(match_value, EvalValue::Missing) {
2834                return Ok(EvalValue::Missing);
2835            }
2836
2837            // Search for first matching item
2838            for item in arr {
2839                if let JsonValue::Object(obj) = item {
2840                    if let Some(field_val) = obj.get(&match_key) {
2841                        let item_val = EvalValue::Value(field_val.clone());
2842                        if compare_values_eq(&item_val, &match_value) {
2843                            // Found a match
2844                            if let Some(ref get_key) = get_field {
2845                                // Return specific field from matched item
2846                                return match obj.get(get_key) {
2847                                    Some(v) => Ok(EvalValue::Value(v.clone())),
2848                                    None => Ok(EvalValue::Missing),
2849                                };
2850                            } else {
2851                                // Return entire matched item
2852                                return Ok(EvalValue::Value(item.clone()));
2853                            }
2854                        }
2855                    }
2856                }
2857            }
2858
2859            Ok(EvalValue::Missing)
2860        }
2861
2862        "lookup" => {
2863            if op_step.args.len() < 2 {
2864                return Err(TransformError::new(
2865                    TransformErrorKind::ExprError,
2866                    "lookup requires at least 2 arguments: match_key, match_value",
2867                )
2868                .with_path(path));
2869            }
2870
2871            let args = &op_step.args;
2872            let from_path = format!("{}.from", path);
2873            let match_key_path = format!("{}.match_key", path);
2874            let get_path = format!("{}.get", path);
2875
2876            let (from_value, match_key_value, match_value, get_field) = match args.len() {
2877                0 | 1 => unreachable!("guarded above"),
2878                2 => {
2879                    let match_key_value = eval_v2_expr(
2880                        &args[0],
2881                        record,
2882                        context,
2883                        out,
2884                        &format!("{}.args[0]", path),
2885                        &step_ctx,
2886                    )?;
2887                    let match_value = eval_v2_expr(
2888                        &args[1],
2889                        record,
2890                        context,
2891                        out,
2892                        &format!("{}.args[1]", path),
2893                        &step_ctx,
2894                    )?;
2895                    (pipe_value.clone(), match_key_value, match_value, None)
2896                }
2897                3 => {
2898                    if matches!(pipe_value, EvalValue::Missing) {
2899                        let first_value = eval_v2_expr(
2900                            &args[0],
2901                            record,
2902                            context,
2903                            out,
2904                            &format!("{}.args[0]", path),
2905                            &step_ctx,
2906                        )?;
2907                        let use_explicit_from =
2908                            matches!(first_value, EvalValue::Value(JsonValue::Array(_)));
2909                        if !use_explicit_from {
2910                            return Ok(EvalValue::Missing);
2911                        }
2912                        let match_key_value = eval_v2_expr(
2913                            &args[1],
2914                            record,
2915                            context,
2916                            out,
2917                            &format!("{}.args[1]", path),
2918                            &step_ctx,
2919                        )?;
2920                        let match_value = eval_v2_expr(
2921                            &args[2],
2922                            record,
2923                            context,
2924                            out,
2925                            &format!("{}.args[2]", path),
2926                            &step_ctx,
2927                        )?;
2928                        (first_value, match_key_value, match_value, None)
2929                    } else {
2930                        let first_value = eval_v2_expr(
2931                            &args[0],
2932                            record,
2933                            context,
2934                            out,
2935                            &format!("{}.args[0]", path),
2936                            &step_ctx,
2937                        )?;
2938                        let use_explicit_from = matches!(
2939                            first_value,
2940                            EvalValue::Value(JsonValue::Array(_)) | EvalValue::Missing
2941                        );
2942                        if use_explicit_from {
2943                            let match_key_value = eval_v2_expr(
2944                                &args[1],
2945                                record,
2946                                context,
2947                                out,
2948                                &format!("{}.args[1]", path),
2949                                &step_ctx,
2950                            )?;
2951                            let match_value = eval_v2_expr(
2952                                &args[2],
2953                                record,
2954                                context,
2955                                out,
2956                                &format!("{}.args[2]", path),
2957                                &step_ctx,
2958                            )?;
2959                            (first_value, match_key_value, match_value, None)
2960                        } else {
2961                            let match_value = eval_v2_expr(
2962                                &args[1],
2963                                record,
2964                                context,
2965                                out,
2966                                &format!("{}.args[1]", path),
2967                                &step_ctx,
2968                            )?;
2969                            let get_value = eval_v2_expr(
2970                                &args[2],
2971                                record,
2972                                context,
2973                                out,
2974                                &format!("{}.args[2]", path),
2975                                &step_ctx,
2976                            )?;
2977                            let get_field = Some(eval_value_as_string(&get_value, &get_path)?);
2978                            (pipe_value.clone(), first_value, match_value, get_field)
2979                        }
2980                    }
2981                }
2982                _ => {
2983                    let from_value = eval_v2_expr(
2984                        &args[0],
2985                        record,
2986                        context,
2987                        out,
2988                        &format!("{}.args[0]", path),
2989                        &step_ctx,
2990                    )?;
2991                    let match_key_value = eval_v2_expr(
2992                        &args[1],
2993                        record,
2994                        context,
2995                        out,
2996                        &format!("{}.args[1]", path),
2997                        &step_ctx,
2998                    )?;
2999                    let match_value = eval_v2_expr(
3000                        &args[2],
3001                        record,
3002                        context,
3003                        out,
3004                        &format!("{}.args[2]", path),
3005                        &step_ctx,
3006                    )?;
3007                    let get_value = eval_v2_expr(
3008                        &args[3],
3009                        record,
3010                        context,
3011                        out,
3012                        &format!("{}.args[3]", path),
3013                        &step_ctx,
3014                    )?;
3015                    let get_field = Some(eval_value_as_string(&get_value, &get_path)?);
3016                    (from_value, match_key_value, match_value, get_field)
3017                }
3018            };
3019
3020            // Evaluate 'from' - the array to search in
3021            let arr = match &from_value {
3022                EvalValue::Value(JsonValue::Array(arr)) => arr,
3023                EvalValue::Missing => return Ok(EvalValue::Missing),
3024                _ => {
3025                    return Err(TransformError::new(
3026                        TransformErrorKind::ExprError,
3027                        "lookup 'from' must be an array",
3028                    )
3029                    .with_path(&from_path));
3030                }
3031            };
3032
3033            // Get match key as string
3034            let match_key = eval_value_as_string(&match_key_value, &match_key_path)?;
3035            if matches!(match_value, EvalValue::Missing) {
3036                return Ok(EvalValue::Missing);
3037            }
3038
3039            // Search for ALL matching items
3040            let mut results = Vec::new();
3041            for item in arr {
3042                if let JsonValue::Object(obj) = item {
3043                    if let Some(field_val) = obj.get(&match_key) {
3044                        let item_val = EvalValue::Value(field_val.clone());
3045                        if compare_values_eq(&item_val, &match_value) {
3046                            // Found a match
3047                            if let Some(ref get_key) = get_field {
3048                                // Add specific field from matched item
3049                                if let Some(v) = obj.get(get_key) {
3050                                    results.push(v.clone());
3051                                }
3052                            } else {
3053                                // Add entire matched item
3054                                results.push(item.clone());
3055                            }
3056                        }
3057                    }
3058                }
3059            }
3060
3061            Ok(EvalValue::Value(JsonValue::Array(results)))
3062        }
3063
3064        // Default case - fall back to v1 op evaluation
3065        _ => {
3066            eval_v2_op_with_v1_fallback(op_step, pipe_value, record, context, out, path, &step_ctx)
3067        }
3068    }
3069}
3070
3071// =============================================================================
3072// v2 Op Step Evaluation Tests (T15)
3073// =============================================================================
3074
3075#[cfg(test)]
3076mod v2_op_step_eval_tests {
3077    use super::*;
3078    use serde_json::{Value as JsonValue, json};
3079
3080    fn lit(value: JsonValue) -> V2Expr {
3081        V2Expr::Pipe(V2Pipe {
3082            start: V2Start::Literal(value),
3083            steps: vec![],
3084        })
3085    }
3086
3087    #[test]
3088    fn test_eval_op_trim() {
3089        let op = V2OpStep {
3090            op: "trim".to_string(),
3091            args: vec![],
3092        };
3093        let ctx = V2EvalContext::new();
3094        let result = eval_v2_op_step(
3095            &op,
3096            EvalValue::Value(json!("  hello  ")),
3097            &json!({}),
3098            None,
3099            &json!({}),
3100            "test",
3101            &ctx,
3102        );
3103        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("hello")));
3104    }
3105
3106    #[test]
3107    fn test_eval_op_lowercase() {
3108        let op = V2OpStep {
3109            op: "lowercase".to_string(),
3110            args: vec![],
3111        };
3112        let ctx = V2EvalContext::new();
3113        let result = eval_v2_op_step(
3114            &op,
3115            EvalValue::Value(json!("HELLO")),
3116            &json!({}),
3117            None,
3118            &json!({}),
3119            "test",
3120            &ctx,
3121        );
3122        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("hello")));
3123    }
3124
3125    #[test]
3126    fn test_eval_op_uppercase() {
3127        let op = V2OpStep {
3128            op: "uppercase".to_string(),
3129            args: vec![],
3130        };
3131        let ctx = V2EvalContext::new();
3132        let result = eval_v2_op_step(
3133            &op,
3134            EvalValue::Value(json!("hello")),
3135            &json!({}),
3136            None,
3137            &json!({}),
3138            "test",
3139            &ctx,
3140        );
3141        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("HELLO")));
3142    }
3143
3144    #[test]
3145    fn test_eval_op_to_string() {
3146        let op = V2OpStep {
3147            op: "to_string".to_string(),
3148            args: vec![],
3149        };
3150        let ctx = V2EvalContext::new();
3151
3152        // Number to string
3153        let result = eval_v2_op_step(
3154            &op,
3155            EvalValue::Value(json!(42)),
3156            &json!({}),
3157            None,
3158            &json!({}),
3159            "test",
3160            &ctx,
3161        );
3162        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("42")));
3163
3164        // Bool to string
3165        let result = eval_v2_op_step(
3166            &op,
3167            EvalValue::Value(json!(true)),
3168            &json!({}),
3169            None,
3170            &json!({}),
3171            "test",
3172            &ctx,
3173        );
3174        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("true")));
3175    }
3176
3177    #[test]
3178    fn test_eval_op_replace() {
3179        let op = V2OpStep {
3180            op: "replace".to_string(),
3181            args: vec![lit(json!("world")), lit(json!("there"))],
3182        };
3183        let ctx = V2EvalContext::new();
3184        let result = eval_v2_op_step(
3185            &op,
3186            EvalValue::Value(json!("hello world")),
3187            &json!({}),
3188            None,
3189            &json!({}),
3190            "test",
3191            &ctx,
3192        );
3193        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("hello there")));
3194    }
3195
3196    #[test]
3197    fn test_eval_op_split_and_pad() {
3198        let split = V2OpStep {
3199            op: "split".to_string(),
3200            args: vec![lit(json!(","))],
3201        };
3202        let pad_start = V2OpStep {
3203            op: "pad_start".to_string(),
3204            args: vec![lit(json!(3)), lit(json!("0"))],
3205        };
3206        let pad_end = V2OpStep {
3207            op: "pad_end".to_string(),
3208            args: vec![lit(json!(3)), lit(json!("0"))],
3209        };
3210        let ctx = V2EvalContext::new();
3211
3212        let split_result = eval_v2_op_step(
3213            &split,
3214            EvalValue::Value(json!("a,b,c")),
3215            &json!({}),
3216            None,
3217            &json!({}),
3218            "test",
3219            &ctx,
3220        );
3221        assert!(matches!(
3222            split_result,
3223            Ok(EvalValue::Value(v)) if v == json!(["a", "b", "c"])
3224        ));
3225
3226        let pad_start_result = eval_v2_op_step(
3227            &pad_start,
3228            EvalValue::Value(json!("7")),
3229            &json!({}),
3230            None,
3231            &json!({}),
3232            "test",
3233            &ctx,
3234        );
3235        assert!(matches!(pad_start_result, Ok(EvalValue::Value(v)) if v == json!("007")));
3236
3237        let pad_end_result = eval_v2_op_step(
3238            &pad_end,
3239            EvalValue::Value(json!("7")),
3240            &json!({}),
3241            None,
3242            &json!({}),
3243            "test",
3244            &ctx,
3245        );
3246        assert!(matches!(pad_end_result, Ok(EvalValue::Value(v)) if v == json!("700")));
3247    }
3248
3249    #[test]
3250    fn test_eval_op_round_and_to_base() {
3251        let round = V2OpStep {
3252            op: "round".to_string(),
3253            args: vec![lit(json!(2))],
3254        };
3255        let to_base = V2OpStep {
3256            op: "to_base".to_string(),
3257            args: vec![lit(json!(2))],
3258        };
3259        let ctx = V2EvalContext::new();
3260
3261        let rounded = eval_v2_op_step(
3262            &round,
3263            EvalValue::Value(json!(1.2345)),
3264            &json!({}),
3265            None,
3266            &json!({}),
3267            "test",
3268            &ctx,
3269        )
3270        .unwrap();
3271        if let EvalValue::Value(v) = rounded {
3272            let value = v.as_f64().unwrap();
3273            assert!((value - 1.23).abs() < 1e-9);
3274        } else {
3275            panic!("expected rounded value");
3276        }
3277
3278        let base = eval_v2_op_step(
3279            &to_base,
3280            EvalValue::Value(json!(10)),
3281            &json!({}),
3282            None,
3283            &json!({}),
3284            "test",
3285            &ctx,
3286        );
3287        assert!(matches!(base, Ok(EvalValue::Value(v)) if v == json!("1010")));
3288    }
3289
3290    #[test]
3291    fn test_eval_op_json_merge() {
3292        let op = V2OpStep {
3293            op: "merge".to_string(),
3294            args: vec![lit(json!({"b": 2}))],
3295        };
3296        let ctx = V2EvalContext::new();
3297        let result = eval_v2_op_step(
3298            &op,
3299            EvalValue::Value(json!({"a": 1})),
3300            &json!({}),
3301            None,
3302            &json!({}),
3303            "test",
3304            &ctx,
3305        );
3306        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!({"a": 1, "b": 2})));
3307    }
3308
3309    #[test]
3310    fn test_eval_op_array_map_and_reduce() {
3311        let map_expr = V2Expr::Pipe(V2Pipe {
3312            start: V2Start::Ref(V2Ref::Item(String::new())),
3313            steps: vec![V2Step::Op(V2OpStep {
3314                op: "add".to_string(),
3315                args: vec![lit(json!(1))],
3316            })],
3317        });
3318        let map = V2OpStep {
3319            op: "map".to_string(),
3320            args: vec![map_expr],
3321        };
3322        let reduce_expr = V2Expr::Pipe(V2Pipe {
3323            start: V2Start::Ref(V2Ref::Acc(String::new())),
3324            steps: vec![V2Step::Op(V2OpStep {
3325                op: "add".to_string(),
3326                args: vec![V2Expr::Pipe(V2Pipe {
3327                    start: V2Start::Ref(V2Ref::Item(String::new())),
3328                    steps: vec![],
3329                })],
3330            })],
3331        });
3332        let reduce = V2OpStep {
3333            op: "reduce".to_string(),
3334            args: vec![reduce_expr],
3335        };
3336        let ctx = V2EvalContext::new();
3337
3338        let map_result = eval_v2_op_step(
3339            &map,
3340            EvalValue::Value(json!([1, 2, 3])),
3341            &json!({}),
3342            None,
3343            &json!({}),
3344            "test",
3345            &ctx,
3346        );
3347        assert!(matches!(map_result, Ok(EvalValue::Value(v)) if v == json!([2.0, 3.0, 4.0])));
3348
3349        let reduce_result = eval_v2_op_step(
3350            &reduce,
3351            EvalValue::Value(json!([1, 2, 3])),
3352            &json!({}),
3353            None,
3354            &json!({}),
3355            "test",
3356            &ctx,
3357        );
3358        assert!(matches!(reduce_result, Ok(EvalValue::Value(v)) if v == json!(6.0)));
3359    }
3360
3361    #[test]
3362    fn test_eval_op_first_last() {
3363        let first = V2OpStep {
3364            op: "first".to_string(),
3365            args: vec![],
3366        };
3367        let last = V2OpStep {
3368            op: "last".to_string(),
3369            args: vec![],
3370        };
3371        let ctx = V2EvalContext::new();
3372
3373        let first_result = eval_v2_op_step(
3374            &first,
3375            EvalValue::Value(json!([1, 2])),
3376            &json!({}),
3377            None,
3378            &json!({}),
3379            "test",
3380            &ctx,
3381        );
3382        assert!(matches!(first_result, Ok(EvalValue::Value(v)) if v == json!(1)));
3383
3384        let last_result = eval_v2_op_step(
3385            &last,
3386            EvalValue::Value(json!([1, 2])),
3387            &json!({}),
3388            None,
3389            &json!({}),
3390            "test",
3391            &ctx,
3392        );
3393        assert!(matches!(last_result, Ok(EvalValue::Value(v)) if v == json!(2)));
3394    }
3395
3396    #[test]
3397    fn test_eval_op_type_casts() {
3398        let op_int = V2OpStep {
3399            op: "int".to_string(),
3400            args: vec![],
3401        };
3402        let op_float = V2OpStep {
3403            op: "float".to_string(),
3404            args: vec![],
3405        };
3406        let op_bool = V2OpStep {
3407            op: "bool".to_string(),
3408            args: vec![],
3409        };
3410        let op_string = V2OpStep {
3411            op: "string".to_string(),
3412            args: vec![],
3413        };
3414        let ctx = V2EvalContext::new();
3415
3416        let int_result = eval_v2_op_step(
3417            &op_int,
3418            EvalValue::Value(json!("42")),
3419            &json!({}),
3420            None,
3421            &json!({}),
3422            "test",
3423            &ctx,
3424        );
3425        assert!(matches!(int_result, Ok(EvalValue::Value(v)) if v == json!(42)));
3426
3427        let float_result = eval_v2_op_step(
3428            &op_float,
3429            EvalValue::Value(json!("3.14")),
3430            &json!({}),
3431            None,
3432            &json!({}),
3433            "test",
3434            &ctx,
3435        );
3436        if let Ok(EvalValue::Value(v)) = float_result {
3437            let value = v.as_f64().unwrap();
3438            assert!((value - 3.14).abs() < 1e-9);
3439        } else {
3440            panic!("expected float cast");
3441        }
3442
3443        let bool_result = eval_v2_op_step(
3444            &op_bool,
3445            EvalValue::Value(json!("true")),
3446            &json!({}),
3447            None,
3448            &json!({}),
3449            "test",
3450            &ctx,
3451        );
3452        assert!(matches!(bool_result, Ok(EvalValue::Value(v)) if v == json!(true)));
3453
3454        let string_result = eval_v2_op_step(
3455            &op_string,
3456            EvalValue::Value(json!(12)),
3457            &json!({}),
3458            None,
3459            &json!({}),
3460            "test",
3461            &ctx,
3462        );
3463        assert!(matches!(string_result, Ok(EvalValue::Value(v)) if v == json!("12")));
3464    }
3465
3466    #[test]
3467    fn test_eval_op_and_or_short_circuit() {
3468        let or_op = V2OpStep {
3469            op: "or".to_string(),
3470            args: vec![V2Expr::Pipe(V2Pipe {
3471                start: V2Start::Literal(json!(1)),
3472                steps: vec![V2Step::Op(V2OpStep {
3473                    op: "divide".to_string(),
3474                    args: vec![V2Expr::Pipe(V2Pipe {
3475                        start: V2Start::Literal(json!(0)),
3476                        steps: vec![],
3477                    })],
3478                })],
3479            })],
3480        };
3481        let and_op = V2OpStep {
3482            op: "and".to_string(),
3483            args: vec![V2Expr::Pipe(V2Pipe {
3484                start: V2Start::Literal(json!(1)),
3485                steps: vec![V2Step::Op(V2OpStep {
3486                    op: "divide".to_string(),
3487                    args: vec![V2Expr::Pipe(V2Pipe {
3488                        start: V2Start::Literal(json!(0)),
3489                        steps: vec![],
3490                    })],
3491                })],
3492            })],
3493        };
3494        let ctx = V2EvalContext::new();
3495
3496        let or_result = eval_v2_op_step(
3497            &or_op,
3498            EvalValue::Value(json!(true)),
3499            &json!({}),
3500            None,
3501            &json!({}),
3502            "test",
3503            &ctx,
3504        );
3505        assert!(matches!(or_result, Ok(EvalValue::Value(v)) if v == json!(true)));
3506
3507        let and_result = eval_v2_op_step(
3508            &and_op,
3509            EvalValue::Value(json!(false)),
3510            &json!({}),
3511            None,
3512            &json!({}),
3513            "test",
3514            &ctx,
3515        );
3516        assert!(matches!(and_result, Ok(EvalValue::Value(v)) if v == json!(false)));
3517    }
3518
3519    #[test]
3520    fn test_eval_op_add() {
3521        let op = V2OpStep {
3522            op: "add".to_string(),
3523            args: vec![V2Expr::Pipe(V2Pipe {
3524                start: V2Start::Literal(json!(10)),
3525                steps: vec![],
3526            })],
3527        };
3528        let ctx = V2EvalContext::new();
3529        let result = eval_v2_op_step(
3530            &op,
3531            EvalValue::Value(json!(5)),
3532            &json!({}),
3533            None,
3534            &json!({}),
3535            "test",
3536            &ctx,
3537        );
3538        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(15.0)));
3539    }
3540
3541    #[test]
3542    fn test_eval_op_subtract() {
3543        let op = V2OpStep {
3544            op: "subtract".to_string(),
3545            args: vec![V2Expr::Pipe(V2Pipe {
3546                start: V2Start::Literal(json!(3)),
3547                steps: vec![],
3548            })],
3549        };
3550        let ctx = V2EvalContext::new();
3551        let result = eval_v2_op_step(
3552            &op,
3553            EvalValue::Value(json!(10)),
3554            &json!({}),
3555            None,
3556            &json!({}),
3557            "test",
3558            &ctx,
3559        );
3560        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(7.0)));
3561    }
3562
3563    #[test]
3564    fn test_eval_op_comparison_aliases() {
3565        let ctx = V2EvalContext::new();
3566        let cases = [
3567            ("eq", json!(1), json!("1"), true),
3568            ("ne", json!(1), json!(2), true),
3569            ("lt", json!(5), json!(10), true),
3570            ("lte", json!(10), json!(10), true),
3571            ("gt", json!(10), json!(5), true),
3572            ("gte", json!(10), json!(10), true),
3573            ("match", json!("apple"), json!("^a.*"), true),
3574        ];
3575
3576        for (op, left, right, expected) in cases {
3577            let op_step = V2OpStep {
3578                op: op.to_string(),
3579                args: vec![lit(right)],
3580            };
3581            let result = eval_v2_op_step(
3582                &op_step,
3583                EvalValue::Value(left),
3584                &json!({}),
3585                None,
3586                &json!({}),
3587                "test",
3588                &ctx,
3589            );
3590            assert!(
3591                matches!(result, Ok(EvalValue::Value(v)) if v == json!(expected)),
3592                "op {}",
3593                op
3594            );
3595        }
3596    }
3597
3598    #[test]
3599    fn test_eval_op_pick_multiple_paths() {
3600        let op = V2OpStep {
3601            op: "pick".to_string(),
3602            args: vec![lit(json!("name")), lit(json!("price"))],
3603        };
3604        let ctx = V2EvalContext::new();
3605        let result = eval_v2_op_step(
3606            &op,
3607            EvalValue::Value(json!({"name": "apple", "price": 100, "category": "fruit"})),
3608            &json!({}),
3609            None,
3610            &json!({}),
3611            "test",
3612            &ctx,
3613        );
3614        assert!(matches!(
3615            result,
3616            Ok(EvalValue::Value(v)) if v == json!({"name": "apple", "price": 100})
3617        ));
3618    }
3619
3620    #[test]
3621    fn test_eval_op_omit_multiple_paths() {
3622        let op = V2OpStep {
3623            op: "omit".to_string(),
3624            args: vec![lit(json!("category")), lit(json!("price"))],
3625        };
3626        let ctx = V2EvalContext::new();
3627        let result = eval_v2_op_step(
3628            &op,
3629            EvalValue::Value(json!({"name": "apple", "price": 100, "category": "fruit"})),
3630            &json!({}),
3631            None,
3632            &json!({}),
3633            "test",
3634            &ctx,
3635        );
3636        assert!(matches!(
3637            result,
3638            Ok(EvalValue::Value(v)) if v == json!({"name": "apple"})
3639        ));
3640    }
3641
3642    #[test]
3643    fn test_eval_op_pick_paths_array_arg() {
3644        let op = V2OpStep {
3645            op: "pick".to_string(),
3646            args: vec![lit(json!(["name", "price"]))],
3647        };
3648        let ctx = V2EvalContext::new();
3649        let result = eval_v2_op_step(
3650            &op,
3651            EvalValue::Value(json!({"name": "apple", "price": 100, "category": "fruit"})),
3652            &json!({}),
3653            None,
3654            &json!({}),
3655            "test",
3656            &ctx,
3657        );
3658        assert!(matches!(
3659            result,
3660            Ok(EvalValue::Value(v)) if v == json!({"name": "apple", "price": 100})
3661        ));
3662    }
3663
3664    #[test]
3665    fn test_eval_op_multiply() {
3666        let op = V2OpStep {
3667            op: "multiply".to_string(),
3668            args: vec![V2Expr::Pipe(V2Pipe {
3669                start: V2Start::Literal(json!(0.9)),
3670                steps: vec![],
3671            })],
3672        };
3673        let ctx = V2EvalContext::new();
3674        let result = eval_v2_op_step(
3675            &op,
3676            EvalValue::Value(json!(100)),
3677            &json!({}),
3678            None,
3679            &json!({}),
3680            "test",
3681            &ctx,
3682        );
3683        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(90.0)));
3684    }
3685
3686    #[test]
3687    fn test_eval_op_divide() {
3688        let op = V2OpStep {
3689            op: "divide".to_string(),
3690            args: vec![V2Expr::Pipe(V2Pipe {
3691                start: V2Start::Literal(json!(2)),
3692                steps: vec![],
3693            })],
3694        };
3695        let ctx = V2EvalContext::new();
3696        let result = eval_v2_op_step(
3697            &op,
3698            EvalValue::Value(json!(10)),
3699            &json!({}),
3700            None,
3701            &json!({}),
3702            "test",
3703            &ctx,
3704        );
3705        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(5.0)));
3706    }
3707
3708    #[test]
3709    fn test_eval_op_divide_by_zero() {
3710        let op = V2OpStep {
3711            op: "divide".to_string(),
3712            args: vec![V2Expr::Pipe(V2Pipe {
3713                start: V2Start::Literal(json!(0)),
3714                steps: vec![],
3715            })],
3716        };
3717        let ctx = V2EvalContext::new();
3718        let result = eval_v2_op_step(
3719            &op,
3720            EvalValue::Value(json!(10)),
3721            &json!({}),
3722            None,
3723            &json!({}),
3724            "test",
3725            &ctx,
3726        );
3727        assert!(result.is_err());
3728    }
3729
3730    #[test]
3731    fn test_eval_op_coalesce() {
3732        let op = V2OpStep {
3733            op: "coalesce".to_string(),
3734            args: vec![V2Expr::Pipe(V2Pipe {
3735                start: V2Start::Literal(json!("default")),
3736                steps: vec![],
3737            })],
3738        };
3739        let ctx = V2EvalContext::new();
3740
3741        // When pipe value is present, use it
3742        let result = eval_v2_op_step(
3743            &op,
3744            EvalValue::Value(json!("value")),
3745            &json!({}),
3746            None,
3747            &json!({}),
3748            "test",
3749            &ctx,
3750        );
3751        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("value")));
3752
3753        // When pipe value is null, use first non-null arg
3754        let result = eval_v2_op_step(
3755            &op,
3756            EvalValue::Value(json!(null)),
3757            &json!({}),
3758            None,
3759            &json!({}),
3760            "test",
3761            &ctx,
3762        );
3763        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("default")));
3764
3765        // When pipe value is missing, use first non-null arg
3766        let result = eval_v2_op_step(
3767            &op,
3768            EvalValue::Missing,
3769            &json!({}),
3770            None,
3771            &json!({}),
3772            "test",
3773            &ctx,
3774        );
3775        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("default")));
3776    }
3777
3778    #[test]
3779    fn test_eval_op_unknown() {
3780        let op = V2OpStep {
3781            op: "unknown_op".to_string(),
3782            args: vec![],
3783        };
3784        let ctx = V2EvalContext::new();
3785        let result = eval_v2_op_step(
3786            &op,
3787            EvalValue::Value(json!("test")),
3788            &json!({}),
3789            None,
3790            &json!({}),
3791            "test",
3792            &ctx,
3793        );
3794        assert!(result.is_err());
3795    }
3796}
3797
3798// =============================================================================
3799// v2 Let Step Evaluation Tests (T16)
3800// =============================================================================
3801
3802#[cfg(test)]
3803mod v2_let_step_eval_tests {
3804    use super::*;
3805    use serde_json::json;
3806
3807    #[test]
3808    fn test_eval_let_single_binding() {
3809        let let_step = V2LetStep {
3810            bindings: vec![(
3811                "x".to_string(),
3812                V2Expr::Pipe(V2Pipe {
3813                    start: V2Start::Literal(json!(42)),
3814                    steps: vec![],
3815                }),
3816            )],
3817        };
3818        let record = json!({});
3819        let out = json!({});
3820        let ctx = V2EvalContext::new();
3821        let result = eval_v2_let_step(
3822            &let_step,
3823            EvalValue::Value(json!("pipe_value")),
3824            &record,
3825            None,
3826            &out,
3827            "test",
3828            &ctx,
3829        );
3830        assert!(result.is_ok());
3831        let new_ctx = result.unwrap();
3832        assert_eq!(
3833            new_ctx.resolve_local("x"),
3834            Some(&EvalValue::Value(json!(42)))
3835        );
3836    }
3837
3838    #[test]
3839    fn test_eval_let_multiple_bindings() {
3840        let let_step = V2LetStep {
3841            bindings: vec![
3842                (
3843                    "a".to_string(),
3844                    V2Expr::Pipe(V2Pipe {
3845                        start: V2Start::Literal(json!(1)),
3846                        steps: vec![],
3847                    }),
3848                ),
3849                (
3850                    "b".to_string(),
3851                    V2Expr::Pipe(V2Pipe {
3852                        start: V2Start::Literal(json!(2)),
3853                        steps: vec![],
3854                    }),
3855                ),
3856            ],
3857        };
3858        let record = json!({});
3859        let out = json!({});
3860        let ctx = V2EvalContext::new();
3861        let result = eval_v2_let_step(
3862            &let_step,
3863            EvalValue::Value(json!("pipe")),
3864            &record,
3865            None,
3866            &out,
3867            "test",
3868            &ctx,
3869        );
3870        assert!(result.is_ok());
3871        let new_ctx = result.unwrap();
3872        assert_eq!(
3873            new_ctx.resolve_local("a"),
3874            Some(&EvalValue::Value(json!(1)))
3875        );
3876        assert_eq!(
3877            new_ctx.resolve_local("b"),
3878            Some(&EvalValue::Value(json!(2)))
3879        );
3880    }
3881
3882    #[test]
3883    fn test_eval_let_binding_uses_pipe_value() {
3884        // let: { x: $ } should bind x to current pipe value
3885        let let_step = V2LetStep {
3886            bindings: vec![(
3887                "x".to_string(),
3888                V2Expr::Pipe(V2Pipe {
3889                    start: V2Start::PipeValue,
3890                    steps: vec![],
3891                }),
3892            )],
3893        };
3894        let record = json!({});
3895        let out = json!({});
3896        let ctx = V2EvalContext::new();
3897        let result = eval_v2_let_step(
3898            &let_step,
3899            EvalValue::Value(json!(100)),
3900            &record,
3901            None,
3902            &out,
3903            "test",
3904            &ctx,
3905        );
3906        assert!(result.is_ok());
3907        let new_ctx = result.unwrap();
3908        assert_eq!(
3909            new_ctx.resolve_local("x"),
3910            Some(&EvalValue::Value(json!(100)))
3911        );
3912    }
3913
3914    #[test]
3915    fn test_eval_let_binding_from_input() {
3916        let let_step = V2LetStep {
3917            bindings: vec![(
3918                "name".to_string(),
3919                V2Expr::Pipe(V2Pipe {
3920                    start: V2Start::Ref(V2Ref::Input("user.name".to_string())),
3921                    steps: vec![],
3922                }),
3923            )],
3924        };
3925        let record = json!({"user": {"name": "Alice"}});
3926        let out = json!({});
3927        let ctx = V2EvalContext::new();
3928        let result = eval_v2_let_step(
3929            &let_step,
3930            EvalValue::Value(json!("ignored")),
3931            &record,
3932            None,
3933            &out,
3934            "test",
3935            &ctx,
3936        );
3937        assert!(result.is_ok());
3938        let new_ctx = result.unwrap();
3939        assert_eq!(
3940            new_ctx.resolve_local("name"),
3941            Some(&EvalValue::Value(json!("Alice")))
3942        );
3943    }
3944
3945    #[test]
3946    fn test_eval_let_binding_chain() {
3947        // let: { x: 10, y: @x } - y should be able to reference x
3948        let let_step = V2LetStep {
3949            bindings: vec![
3950                (
3951                    "x".to_string(),
3952                    V2Expr::Pipe(V2Pipe {
3953                        start: V2Start::Literal(json!(10)),
3954                        steps: vec![],
3955                    }),
3956                ),
3957                (
3958                    "y".to_string(),
3959                    V2Expr::Pipe(V2Pipe {
3960                        start: V2Start::Ref(V2Ref::Local("x".to_string())),
3961                        steps: vec![],
3962                    }),
3963                ),
3964            ],
3965        };
3966        let record = json!({});
3967        let out = json!({});
3968        let ctx = V2EvalContext::new();
3969        let result = eval_v2_let_step(
3970            &let_step,
3971            EvalValue::Value(json!("pipe")),
3972            &record,
3973            None,
3974            &out,
3975            "test",
3976            &ctx,
3977        );
3978        assert!(result.is_ok());
3979        let new_ctx = result.unwrap();
3980        assert_eq!(
3981            new_ctx.resolve_local("x"),
3982            Some(&EvalValue::Value(json!(10)))
3983        );
3984        assert_eq!(
3985            new_ctx.resolve_local("y"),
3986            Some(&EvalValue::Value(json!(10)))
3987        );
3988    }
3989
3990    #[test]
3991    fn test_eval_pipe_with_let() {
3992        // [100, { let: { x: $ } }, @x] -> 100
3993        let pipe = V2Pipe {
3994            start: V2Start::Literal(json!(100)),
3995            steps: vec![V2Step::Let(V2LetStep {
3996                bindings: vec![(
3997                    "x".to_string(),
3998                    V2Expr::Pipe(V2Pipe {
3999                        start: V2Start::PipeValue,
4000                        steps: vec![],
4001                    }),
4002                )],
4003            })],
4004        };
4005        let record = json!({});
4006        let out = json!({});
4007        let ctx = V2EvalContext::new();
4008        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
4009        // Let step doesn't change pipe value
4010        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100)));
4011    }
4012
4013    #[test]
4014    fn test_eval_pipe_let_then_op() {
4015        // [100, { let: { factor: 2 } }, { op: "multiply", args: [@factor] }] -> 200
4016        let pipe = V2Pipe {
4017            start: V2Start::Literal(json!(100)),
4018            steps: vec![
4019                V2Step::Let(V2LetStep {
4020                    bindings: vec![(
4021                        "factor".to_string(),
4022                        V2Expr::Pipe(V2Pipe {
4023                            start: V2Start::Literal(json!(2)),
4024                            steps: vec![],
4025                        }),
4026                    )],
4027                }),
4028                V2Step::Op(V2OpStep {
4029                    op: "multiply".to_string(),
4030                    args: vec![V2Expr::Pipe(V2Pipe {
4031                        start: V2Start::Ref(V2Ref::Local("factor".to_string())),
4032                        steps: vec![],
4033                    })],
4034                }),
4035            ],
4036        };
4037        let record = json!({});
4038        let out = json!({});
4039        let ctx = V2EvalContext::new();
4040        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
4041        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(200.0)));
4042    }
4043}
4044
4045// =============================================================================
4046// v2 If Step Evaluation Tests (T17)
4047// =============================================================================
4048
4049#[cfg(test)]
4050mod v2_if_step_eval_tests {
4051    use super::*;
4052    use serde_json::json;
4053
4054    // ------ Condition evaluation tests ------
4055
4056    #[test]
4057    fn test_eval_condition_eq_true() {
4058        let cond = V2Condition::Comparison(V2Comparison {
4059            op: V2ComparisonOp::Eq,
4060            args: vec![
4061                V2Expr::Pipe(V2Pipe {
4062                    start: V2Start::Literal(json!(10)),
4063                    steps: vec![],
4064                }),
4065                V2Expr::Pipe(V2Pipe {
4066                    start: V2Start::Literal(json!(10)),
4067                    steps: vec![],
4068                }),
4069            ],
4070        });
4071        let record = json!({});
4072        let out = json!({});
4073        let ctx = V2EvalContext::new();
4074        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4075        assert!(matches!(result, Ok(true)));
4076    }
4077
4078    #[test]
4079    fn test_eval_condition_eq_false() {
4080        let cond = V2Condition::Comparison(V2Comparison {
4081            op: V2ComparisonOp::Eq,
4082            args: vec![
4083                V2Expr::Pipe(V2Pipe {
4084                    start: V2Start::Literal(json!(10)),
4085                    steps: vec![],
4086                }),
4087                V2Expr::Pipe(V2Pipe {
4088                    start: V2Start::Literal(json!(20)),
4089                    steps: vec![],
4090                }),
4091            ],
4092        });
4093        let record = json!({});
4094        let out = json!({});
4095        let ctx = V2EvalContext::new();
4096        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4097        assert!(matches!(result, Ok(false)));
4098    }
4099
4100    #[test]
4101    fn test_eval_condition_eq_numeric_string_is_false() {
4102        let cond = V2Condition::Comparison(V2Comparison {
4103            op: V2ComparisonOp::Eq,
4104            args: vec![
4105                V2Expr::Pipe(V2Pipe {
4106                    start: V2Start::Literal(json!("1")),
4107                    steps: vec![],
4108                }),
4109                V2Expr::Pipe(V2Pipe {
4110                    start: V2Start::Literal(json!(1)),
4111                    steps: vec![],
4112                }),
4113            ],
4114        });
4115        let record = json!({});
4116        let out = json!({});
4117        let ctx = V2EvalContext::new();
4118        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4119        assert!(matches!(result, Ok(false)));
4120    }
4121
4122    #[test]
4123    fn test_eval_condition_eq_missing_as_null() {
4124        let cond = V2Condition::Comparison(V2Comparison {
4125            op: V2ComparisonOp::Eq,
4126            args: vec![
4127                V2Expr::Pipe(V2Pipe {
4128                    start: V2Start::Ref(V2Ref::Input("optional".to_string())),
4129                    steps: vec![],
4130                }),
4131                V2Expr::Pipe(V2Pipe {
4132                    start: V2Start::Literal(json!(null)),
4133                    steps: vec![],
4134                }),
4135            ],
4136        });
4137        let record = json!({});
4138        let out = json!({});
4139        let ctx = V2EvalContext::new();
4140        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4141        assert!(matches!(result, Ok(true)));
4142    }
4143
4144    #[test]
4145    fn test_eval_condition_ne() {
4146        let cond = V2Condition::Comparison(V2Comparison {
4147            op: V2ComparisonOp::Ne,
4148            args: vec![
4149                V2Expr::Pipe(V2Pipe {
4150                    start: V2Start::Literal(json!("a")),
4151                    steps: vec![],
4152                }),
4153                V2Expr::Pipe(V2Pipe {
4154                    start: V2Start::Literal(json!("b")),
4155                    steps: vec![],
4156                }),
4157            ],
4158        });
4159        let record = json!({});
4160        let out = json!({});
4161        let ctx = V2EvalContext::new();
4162        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4163        assert!(matches!(result, Ok(true)));
4164    }
4165
4166    #[test]
4167    fn test_eval_condition_gt() {
4168        let cond = V2Condition::Comparison(V2Comparison {
4169            op: V2ComparisonOp::Gt,
4170            args: vec![
4171                V2Expr::Pipe(V2Pipe {
4172                    start: V2Start::Literal(json!(20)),
4173                    steps: vec![],
4174                }),
4175                V2Expr::Pipe(V2Pipe {
4176                    start: V2Start::Literal(json!(10)),
4177                    steps: vec![],
4178                }),
4179            ],
4180        });
4181        let record = json!({});
4182        let out = json!({});
4183        let ctx = V2EvalContext::new();
4184        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4185        assert!(matches!(result, Ok(true)));
4186    }
4187
4188    #[test]
4189    fn test_eval_condition_gt_non_numeric_string_compares_lexicographically() {
4190        let cond = V2Condition::Comparison(V2Comparison {
4191            op: V2ComparisonOp::Gt,
4192            args: vec![
4193                V2Expr::Pipe(V2Pipe {
4194                    start: V2Start::Literal(json!("B")),
4195                    steps: vec![],
4196                }),
4197                V2Expr::Pipe(V2Pipe {
4198                    start: V2Start::Literal(json!("A")),
4199                    steps: vec![],
4200                }),
4201            ],
4202        });
4203        let record = json!({});
4204        let out = json!({});
4205        let ctx = V2EvalContext::new();
4206        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4207        assert!(matches!(result, Ok(true)));
4208    }
4209
4210    #[test]
4211    fn test_eval_condition_lt() {
4212        let cond = V2Condition::Comparison(V2Comparison {
4213            op: V2ComparisonOp::Lt,
4214            args: vec![
4215                V2Expr::Pipe(V2Pipe {
4216                    start: V2Start::Literal(json!(5)),
4217                    steps: vec![],
4218                }),
4219                V2Expr::Pipe(V2Pipe {
4220                    start: V2Start::Literal(json!(10)),
4221                    steps: vec![],
4222                }),
4223            ],
4224        });
4225        let record = json!({});
4226        let out = json!({});
4227        let ctx = V2EvalContext::new();
4228        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4229        assert!(matches!(result, Ok(true)));
4230    }
4231
4232    #[test]
4233    fn test_eval_condition_gte_equal() {
4234        let cond = V2Condition::Comparison(V2Comparison {
4235            op: V2ComparisonOp::Gte,
4236            args: vec![
4237                V2Expr::Pipe(V2Pipe {
4238                    start: V2Start::Literal(json!(10)),
4239                    steps: vec![],
4240                }),
4241                V2Expr::Pipe(V2Pipe {
4242                    start: V2Start::Literal(json!(10)),
4243                    steps: vec![],
4244                }),
4245            ],
4246        });
4247        let record = json!({});
4248        let out = json!({});
4249        let ctx = V2EvalContext::new();
4250        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4251        assert!(matches!(result, Ok(true)));
4252    }
4253
4254    #[test]
4255    fn test_eval_condition_lte_less() {
4256        let cond = V2Condition::Comparison(V2Comparison {
4257            op: V2ComparisonOp::Lte,
4258            args: vec![
4259                V2Expr::Pipe(V2Pipe {
4260                    start: V2Start::Literal(json!(5)),
4261                    steps: vec![],
4262                }),
4263                V2Expr::Pipe(V2Pipe {
4264                    start: V2Start::Literal(json!(10)),
4265                    steps: vec![],
4266                }),
4267            ],
4268        });
4269        let record = json!({});
4270        let out = json!({});
4271        let ctx = V2EvalContext::new();
4272        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4273        assert!(matches!(result, Ok(true)));
4274    }
4275
4276    #[test]
4277    fn test_eval_condition_match() {
4278        let cond = V2Condition::Comparison(V2Comparison {
4279            op: V2ComparisonOp::Match,
4280            args: vec![
4281                V2Expr::Pipe(V2Pipe {
4282                    start: V2Start::Literal(json!("hello123")),
4283                    steps: vec![],
4284                }),
4285                V2Expr::Pipe(V2Pipe {
4286                    start: V2Start::Literal(json!("^hello\\d+")),
4287                    steps: vec![],
4288                }),
4289            ],
4290        });
4291        let record = json!({});
4292        let out = json!({});
4293        let ctx = V2EvalContext::new();
4294        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4295        assert!(matches!(result, Ok(true)));
4296    }
4297
4298    #[test]
4299    fn test_eval_condition_all_true() {
4300        let cond = V2Condition::All(vec![
4301            V2Condition::Comparison(V2Comparison {
4302                op: V2ComparisonOp::Gt,
4303                args: vec![
4304                    V2Expr::Pipe(V2Pipe {
4305                        start: V2Start::Literal(json!(10)),
4306                        steps: vec![],
4307                    }),
4308                    V2Expr::Pipe(V2Pipe {
4309                        start: V2Start::Literal(json!(5)),
4310                        steps: vec![],
4311                    }),
4312                ],
4313            }),
4314            V2Condition::Comparison(V2Comparison {
4315                op: V2ComparisonOp::Lt,
4316                args: vec![
4317                    V2Expr::Pipe(V2Pipe {
4318                        start: V2Start::Literal(json!(10)),
4319                        steps: vec![],
4320                    }),
4321                    V2Expr::Pipe(V2Pipe {
4322                        start: V2Start::Literal(json!(20)),
4323                        steps: vec![],
4324                    }),
4325                ],
4326            }),
4327        ]);
4328        let record = json!({});
4329        let out = json!({});
4330        let ctx = V2EvalContext::new();
4331        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4332        assert!(matches!(result, Ok(true)));
4333    }
4334
4335    #[test]
4336    fn test_eval_condition_all_false() {
4337        let cond = V2Condition::All(vec![
4338            V2Condition::Comparison(V2Comparison {
4339                op: V2ComparisonOp::Gt,
4340                args: vec![
4341                    V2Expr::Pipe(V2Pipe {
4342                        start: V2Start::Literal(json!(10)),
4343                        steps: vec![],
4344                    }),
4345                    V2Expr::Pipe(V2Pipe {
4346                        start: V2Start::Literal(json!(5)),
4347                        steps: vec![],
4348                    }),
4349                ],
4350            }),
4351            V2Condition::Comparison(V2Comparison {
4352                op: V2ComparisonOp::Lt, // 10 < 5 is false
4353                args: vec![
4354                    V2Expr::Pipe(V2Pipe {
4355                        start: V2Start::Literal(json!(10)),
4356                        steps: vec![],
4357                    }),
4358                    V2Expr::Pipe(V2Pipe {
4359                        start: V2Start::Literal(json!(5)),
4360                        steps: vec![],
4361                    }),
4362                ],
4363            }),
4364        ]);
4365        let record = json!({});
4366        let out = json!({});
4367        let ctx = V2EvalContext::new();
4368        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4369        assert!(matches!(result, Ok(false)));
4370    }
4371
4372    #[test]
4373    fn test_eval_condition_any_true() {
4374        let cond = V2Condition::Any(vec![
4375            V2Condition::Comparison(V2Comparison {
4376                op: V2ComparisonOp::Eq,
4377                args: vec![
4378                    V2Expr::Pipe(V2Pipe {
4379                        start: V2Start::Literal(json!("admin")),
4380                        steps: vec![],
4381                    }),
4382                    V2Expr::Pipe(V2Pipe {
4383                        start: V2Start::Literal(json!("user")),
4384                        steps: vec![],
4385                    }),
4386                ],
4387            }),
4388            V2Condition::Comparison(V2Comparison {
4389                op: V2ComparisonOp::Gt,
4390                args: vec![
4391                    V2Expr::Pipe(V2Pipe {
4392                        start: V2Start::Literal(json!(100)),
4393                        steps: vec![],
4394                    }),
4395                    V2Expr::Pipe(V2Pipe {
4396                        start: V2Start::Literal(json!(50)),
4397                        steps: vec![],
4398                    }),
4399                ],
4400            }),
4401        ]);
4402        let record = json!({});
4403        let out = json!({});
4404        let ctx = V2EvalContext::new();
4405        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4406        assert!(matches!(result, Ok(true)));
4407    }
4408
4409    #[test]
4410    fn test_eval_condition_any_false() {
4411        let cond = V2Condition::Any(vec![
4412            V2Condition::Comparison(V2Comparison {
4413                op: V2ComparisonOp::Eq,
4414                args: vec![
4415                    V2Expr::Pipe(V2Pipe {
4416                        start: V2Start::Literal(json!(1)),
4417                        steps: vec![],
4418                    }),
4419                    V2Expr::Pipe(V2Pipe {
4420                        start: V2Start::Literal(json!(2)),
4421                        steps: vec![],
4422                    }),
4423                ],
4424            }),
4425            V2Condition::Comparison(V2Comparison {
4426                op: V2ComparisonOp::Eq,
4427                args: vec![
4428                    V2Expr::Pipe(V2Pipe {
4429                        start: V2Start::Literal(json!(3)),
4430                        steps: vec![],
4431                    }),
4432                    V2Expr::Pipe(V2Pipe {
4433                        start: V2Start::Literal(json!(4)),
4434                        steps: vec![],
4435                    }),
4436                ],
4437            }),
4438        ]);
4439        let record = json!({});
4440        let out = json!({});
4441        let ctx = V2EvalContext::new();
4442        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4443        assert!(matches!(result, Ok(false)));
4444    }
4445
4446    #[test]
4447    fn test_eval_condition_expr_truthy() {
4448        let cond = V2Condition::Expr(V2Expr::Pipe(V2Pipe {
4449            start: V2Start::Literal(json!(true)),
4450            steps: vec![],
4451        }));
4452        let record = json!({});
4453        let out = json!({});
4454        let ctx = V2EvalContext::new();
4455        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4456        assert!(matches!(result, Ok(true)));
4457    }
4458
4459    #[test]
4460    fn test_eval_condition_expr_falsy() {
4461        let cond = V2Condition::Expr(V2Expr::Pipe(V2Pipe {
4462            start: V2Start::Literal(json!(false)),
4463            steps: vec![],
4464        }));
4465        let record = json!({});
4466        let out = json!({});
4467        let ctx = V2EvalContext::new();
4468        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4469        assert!(matches!(result, Ok(false)));
4470    }
4471
4472    #[test]
4473    fn test_eval_condition_expr_non_bool_errors() {
4474        let cond = V2Condition::Expr(V2Expr::Pipe(V2Pipe {
4475            start: V2Start::Literal(json!("active")),
4476            steps: vec![],
4477        }));
4478        let record = json!({});
4479        let out = json!({});
4480        let ctx = V2EvalContext::new();
4481        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4482        assert!(matches!(result, Err(err)
4483            if err.kind == TransformErrorKind::ExprError
4484                && err.message == "when/record_when must evaluate to boolean"
4485                && err.path.as_deref() == Some("test.expr")
4486        ));
4487    }
4488
4489    #[test]
4490    fn test_eval_condition_expr_missing_is_false() {
4491        let cond = V2Condition::Expr(V2Expr::Pipe(V2Pipe {
4492            start: V2Start::Ref(V2Ref::Input("active".to_string())),
4493            steps: vec![],
4494        }));
4495        let record = json!({});
4496        let out = json!({});
4497        let ctx = V2EvalContext::new();
4498        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4499        assert!(matches!(result, Ok(false)));
4500    }
4501
4502    #[test]
4503    fn test_eval_condition_with_pipe_value() {
4504        // Condition: { gt: ["$", 100] }
4505        let cond = V2Condition::Comparison(V2Comparison {
4506            op: V2ComparisonOp::Gt,
4507            args: vec![
4508                V2Expr::Pipe(V2Pipe {
4509                    start: V2Start::PipeValue,
4510                    steps: vec![],
4511                }),
4512                V2Expr::Pipe(V2Pipe {
4513                    start: V2Start::Literal(json!(100)),
4514                    steps: vec![],
4515                }),
4516            ],
4517        });
4518        let record = json!({});
4519        let out = json!({});
4520        let ctx = V2EvalContext::new().with_pipe_value(EvalValue::Value(json!(150)));
4521        let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4522        assert!(matches!(result, Ok(true)));
4523    }
4524
4525    // ------ If step evaluation tests ------
4526
4527    #[test]
4528    fn test_eval_if_step_then_branch() {
4529        // if: { cond: { gt: ["$", 10] }, then: [{ multiply: 2 }] }
4530        let if_step = V2IfStep {
4531            cond: V2Condition::Comparison(V2Comparison {
4532                op: V2ComparisonOp::Gt,
4533                args: vec![
4534                    V2Expr::Pipe(V2Pipe {
4535                        start: V2Start::PipeValue,
4536                        steps: vec![],
4537                    }),
4538                    V2Expr::Pipe(V2Pipe {
4539                        start: V2Start::Literal(json!(10)),
4540                        steps: vec![],
4541                    }),
4542                ],
4543            }),
4544            then_branch: V2Pipe {
4545                start: V2Start::PipeValue,
4546                steps: vec![V2Step::Op(V2OpStep {
4547                    op: "multiply".to_string(),
4548                    args: vec![V2Expr::Pipe(V2Pipe {
4549                        start: V2Start::Literal(json!(2)),
4550                        steps: vec![],
4551                    })],
4552                })],
4553            },
4554            else_branch: None,
4555        };
4556        let record = json!({});
4557        let out = json!({});
4558        let ctx = V2EvalContext::new();
4559        let result = eval_v2_if_step(
4560            &if_step,
4561            EvalValue::Value(json!(20)),
4562            &record,
4563            None,
4564            &out,
4565            "test",
4566            &ctx,
4567        );
4568        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(40.0)));
4569    }
4570
4571    #[test]
4572    fn test_eval_if_step_else_branch() {
4573        // if: { cond: { gt: ["$", 10] }, then: [{ multiply: 2 }], else: [{ multiply: 0.5 }] }
4574        let if_step = V2IfStep {
4575            cond: V2Condition::Comparison(V2Comparison {
4576                op: V2ComparisonOp::Gt,
4577                args: vec![
4578                    V2Expr::Pipe(V2Pipe {
4579                        start: V2Start::PipeValue,
4580                        steps: vec![],
4581                    }),
4582                    V2Expr::Pipe(V2Pipe {
4583                        start: V2Start::Literal(json!(10)),
4584                        steps: vec![],
4585                    }),
4586                ],
4587            }),
4588            then_branch: V2Pipe {
4589                start: V2Start::PipeValue,
4590                steps: vec![V2Step::Op(V2OpStep {
4591                    op: "multiply".to_string(),
4592                    args: vec![V2Expr::Pipe(V2Pipe {
4593                        start: V2Start::Literal(json!(2)),
4594                        steps: vec![],
4595                    })],
4596                })],
4597            },
4598            else_branch: Some(V2Pipe {
4599                start: V2Start::PipeValue,
4600                steps: vec![V2Step::Op(V2OpStep {
4601                    op: "multiply".to_string(),
4602                    args: vec![V2Expr::Pipe(V2Pipe {
4603                        start: V2Start::Literal(json!(0.5)),
4604                        steps: vec![],
4605                    })],
4606                })],
4607            }),
4608        };
4609        let record = json!({});
4610        let out = json!({});
4611        let ctx = V2EvalContext::new();
4612        // pipe value 5 is less than 10, so else branch is taken
4613        let result = eval_v2_if_step(
4614            &if_step,
4615            EvalValue::Value(json!(5)),
4616            &record,
4617            None,
4618            &out,
4619            "test",
4620            &ctx,
4621        );
4622        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(2.5)));
4623    }
4624
4625    #[test]
4626    fn test_eval_if_step_no_else_returns_pipe_value() {
4627        // if: { cond: { gt: ["$", 10] }, then: [{ multiply: 2 }] }
4628        let if_step = V2IfStep {
4629            cond: V2Condition::Comparison(V2Comparison {
4630                op: V2ComparisonOp::Gt,
4631                args: vec![
4632                    V2Expr::Pipe(V2Pipe {
4633                        start: V2Start::PipeValue,
4634                        steps: vec![],
4635                    }),
4636                    V2Expr::Pipe(V2Pipe {
4637                        start: V2Start::Literal(json!(10)),
4638                        steps: vec![],
4639                    }),
4640                ],
4641            }),
4642            then_branch: V2Pipe {
4643                start: V2Start::PipeValue,
4644                steps: vec![V2Step::Op(V2OpStep {
4645                    op: "multiply".to_string(),
4646                    args: vec![V2Expr::Pipe(V2Pipe {
4647                        start: V2Start::Literal(json!(2)),
4648                        steps: vec![],
4649                    })],
4650                })],
4651            },
4652            else_branch: None,
4653        };
4654        let record = json!({});
4655        let out = json!({});
4656        let ctx = V2EvalContext::new();
4657        // pipe value 5 is less than 10, no else branch, returns original pipe value
4658        let result = eval_v2_if_step(
4659            &if_step,
4660            EvalValue::Value(json!(5)),
4661            &record,
4662            None,
4663            &out,
4664            "test",
4665            &ctx,
4666        );
4667        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(5)));
4668    }
4669
4670    #[test]
4671    fn test_eval_pipe_with_if_step() {
4672        // [10000, { if: { cond: { gt: ["$", 5000] }, then: [{ multiply: 0.9 }] } }]
4673        let pipe = V2Pipe {
4674            start: V2Start::Literal(json!(10000)),
4675            steps: vec![V2Step::If(V2IfStep {
4676                cond: V2Condition::Comparison(V2Comparison {
4677                    op: V2ComparisonOp::Gt,
4678                    args: vec![
4679                        V2Expr::Pipe(V2Pipe {
4680                            start: V2Start::PipeValue,
4681                            steps: vec![],
4682                        }),
4683                        V2Expr::Pipe(V2Pipe {
4684                            start: V2Start::Literal(json!(5000)),
4685                            steps: vec![],
4686                        }),
4687                    ],
4688                }),
4689                then_branch: V2Pipe {
4690                    start: V2Start::PipeValue,
4691                    steps: vec![V2Step::Op(V2OpStep {
4692                        op: "multiply".to_string(),
4693                        args: vec![V2Expr::Pipe(V2Pipe {
4694                            start: V2Start::Literal(json!(0.9)),
4695                            steps: vec![],
4696                        })],
4697                    })],
4698                },
4699                else_branch: None,
4700            })],
4701        };
4702        let record = json!({});
4703        let out = json!({});
4704        let ctx = V2EvalContext::new();
4705        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
4706        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(9000.0)));
4707    }
4708
4709    #[test]
4710    fn test_eval_if_with_input_condition() {
4711        // if: { cond: { eq: ["@input.role", "admin"] }, then: [100], else: [50] }
4712        let if_step = V2IfStep {
4713            cond: V2Condition::Comparison(V2Comparison {
4714                op: V2ComparisonOp::Eq,
4715                args: vec![
4716                    V2Expr::Pipe(V2Pipe {
4717                        start: V2Start::Ref(V2Ref::Input("role".to_string())),
4718                        steps: vec![],
4719                    }),
4720                    V2Expr::Pipe(V2Pipe {
4721                        start: V2Start::Literal(json!("admin")),
4722                        steps: vec![],
4723                    }),
4724                ],
4725            }),
4726            then_branch: V2Pipe {
4727                start: V2Start::Literal(json!(100)),
4728                steps: vec![],
4729            },
4730            else_branch: Some(V2Pipe {
4731                start: V2Start::Literal(json!(50)),
4732                steps: vec![],
4733            }),
4734        };
4735        let record = json!({"role": "admin"});
4736        let out = json!({});
4737        let ctx = V2EvalContext::new();
4738        let result = eval_v2_if_step(
4739            &if_step,
4740            EvalValue::Value(json!(0)),
4741            &record,
4742            None,
4743            &out,
4744            "test",
4745            &ctx,
4746        );
4747        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100)));
4748
4749        // When not admin
4750        let record2 = json!({"role": "user"});
4751        let result2 = eval_v2_if_step(
4752            &if_step,
4753            EvalValue::Value(json!(0)),
4754            &record2,
4755            None,
4756            &out,
4757            "test",
4758            &ctx,
4759        );
4760        assert!(matches!(result2, Ok(EvalValue::Value(v)) if v == json!(50)));
4761    }
4762
4763    #[test]
4764    fn test_eval_nested_if() {
4765        // Nested if: if x > 100 then (if x > 500 then "gold" else "silver") else "bronze"
4766        let if_step = V2IfStep {
4767            cond: V2Condition::Comparison(V2Comparison {
4768                op: V2ComparisonOp::Gt,
4769                args: vec![
4770                    V2Expr::Pipe(V2Pipe {
4771                        start: V2Start::PipeValue,
4772                        steps: vec![],
4773                    }),
4774                    V2Expr::Pipe(V2Pipe {
4775                        start: V2Start::Literal(json!(100)),
4776                        steps: vec![],
4777                    }),
4778                ],
4779            }),
4780            then_branch: V2Pipe {
4781                start: V2Start::PipeValue,
4782                steps: vec![V2Step::If(V2IfStep {
4783                    cond: V2Condition::Comparison(V2Comparison {
4784                        op: V2ComparisonOp::Gt,
4785                        args: vec![
4786                            V2Expr::Pipe(V2Pipe {
4787                                start: V2Start::PipeValue,
4788                                steps: vec![],
4789                            }),
4790                            V2Expr::Pipe(V2Pipe {
4791                                start: V2Start::Literal(json!(500)),
4792                                steps: vec![],
4793                            }),
4794                        ],
4795                    }),
4796                    then_branch: V2Pipe {
4797                        start: V2Start::Literal(json!("gold")),
4798                        steps: vec![],
4799                    },
4800                    else_branch: Some(V2Pipe {
4801                        start: V2Start::Literal(json!("silver")),
4802                        steps: vec![],
4803                    }),
4804                })],
4805            },
4806            else_branch: Some(V2Pipe {
4807                start: V2Start::Literal(json!("bronze")),
4808                steps: vec![],
4809            }),
4810        };
4811        let record = json!({});
4812        let out = json!({});
4813        let ctx = V2EvalContext::new();
4814
4815        // 50 -> bronze
4816        let result = eval_v2_if_step(
4817            &if_step,
4818            EvalValue::Value(json!(50)),
4819            &record,
4820            None,
4821            &out,
4822            "test",
4823            &ctx,
4824        );
4825        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("bronze")));
4826
4827        // 200 -> silver
4828        let result = eval_v2_if_step(
4829            &if_step,
4830            EvalValue::Value(json!(200)),
4831            &record,
4832            None,
4833            &out,
4834            "test",
4835            &ctx,
4836        );
4837        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("silver")));
4838
4839        // 600 -> gold
4840        let result = eval_v2_if_step(
4841            &if_step,
4842            EvalValue::Value(json!(600)),
4843            &record,
4844            None,
4845            &out,
4846            "test",
4847            &ctx,
4848        );
4849        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("gold")));
4850    }
4851}
4852
4853// =============================================================================
4854// v2 Map Step Evaluation Tests (T18)
4855// =============================================================================
4856
4857#[cfg(test)]
4858mod v2_map_step_eval_tests {
4859    use super::*;
4860    use serde_json::json;
4861
4862    #[test]
4863    fn test_eval_map_step_simple() {
4864        // map: [uppercase] on ["a", "b", "c"] -> ["A", "B", "C"]
4865        let map_step = V2MapStep {
4866            steps: vec![V2Step::Op(V2OpStep {
4867                op: "uppercase".to_string(),
4868                args: vec![],
4869            })],
4870        };
4871        let record = json!({});
4872        let out = json!({});
4873        let ctx = V2EvalContext::new();
4874        let result = eval_v2_map_step(
4875            &map_step,
4876            EvalValue::Value(json!(["a", "b", "c"])),
4877            &record,
4878            None,
4879            &out,
4880            "test",
4881            &ctx,
4882        );
4883        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["A", "B", "C"])));
4884    }
4885
4886    #[test]
4887    fn test_eval_map_step_with_multiply() {
4888        // map: [multiply: 2] on [1, 2, 3] -> [2, 4, 6]
4889        let map_step = V2MapStep {
4890            steps: vec![V2Step::Op(V2OpStep {
4891                op: "multiply".to_string(),
4892                args: vec![V2Expr::Pipe(V2Pipe {
4893                    start: V2Start::Literal(json!(2)),
4894                    steps: vec![],
4895                })],
4896            })],
4897        };
4898        let record = json!({});
4899        let out = json!({});
4900        let ctx = V2EvalContext::new();
4901        let result = eval_v2_map_step(
4902            &map_step,
4903            EvalValue::Value(json!([1, 2, 3])),
4904            &record,
4905            None,
4906            &out,
4907            "test",
4908            &ctx,
4909        );
4910        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([2.0, 4.0, 6.0])));
4911    }
4912
4913    #[test]
4914    fn test_eval_map_step_empty_array() {
4915        let map_step = V2MapStep {
4916            steps: vec![V2Step::Op(V2OpStep {
4917                op: "uppercase".to_string(),
4918                args: vec![],
4919            })],
4920        };
4921        let record = json!({});
4922        let out = json!({});
4923        let ctx = V2EvalContext::new();
4924        let result = eval_v2_map_step(
4925            &map_step,
4926            EvalValue::Value(json!([])),
4927            &record,
4928            None,
4929            &out,
4930            "test",
4931            &ctx,
4932        );
4933        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([])));
4934    }
4935
4936    #[test]
4937    fn test_eval_map_step_missing_returns_missing() {
4938        let map_step = V2MapStep {
4939            steps: vec![V2Step::Op(V2OpStep {
4940                op: "uppercase".to_string(),
4941                args: vec![],
4942            })],
4943        };
4944        let record = json!({});
4945        let out = json!({});
4946        let ctx = V2EvalContext::new();
4947        let result = eval_v2_map_step(
4948            &map_step,
4949            EvalValue::Missing,
4950            &record,
4951            None,
4952            &out,
4953            "test",
4954            &ctx,
4955        );
4956        assert!(matches!(result, Ok(EvalValue::Missing)));
4957    }
4958
4959    #[test]
4960    fn test_eval_map_step_non_array_error() {
4961        let map_step = V2MapStep {
4962            steps: vec![V2Step::Op(V2OpStep {
4963                op: "uppercase".to_string(),
4964                args: vec![],
4965            })],
4966        };
4967        let record = json!({});
4968        let out = json!({});
4969        let ctx = V2EvalContext::new();
4970        let result = eval_v2_map_step(
4971            &map_step,
4972            EvalValue::Value(json!("not an array")),
4973            &record,
4974            None,
4975            &out,
4976            "test",
4977            &ctx,
4978        );
4979        assert!(result.is_err());
4980    }
4981
4982    #[test]
4983    fn test_eval_map_step_with_item_ref() {
4984        // Access @item.name from each object
4985        let map_step = V2MapStep {
4986            steps: vec![V2Step::Op(V2OpStep {
4987                op: "concat".to_string(),
4988                args: vec![V2Expr::Pipe(V2Pipe {
4989                    start: V2Start::Literal(json!("!")),
4990                    steps: vec![],
4991                })],
4992            })],
4993        };
4994        let record = json!({});
4995        let out = json!({});
4996        let ctx = V2EvalContext::new();
4997        let result = eval_v2_map_step(
4998            &map_step,
4999            EvalValue::Value(json!(["hello", "world"])),
5000            &record,
5001            None,
5002            &out,
5003            "test",
5004            &ctx,
5005        );
5006        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["hello!", "world!"])));
5007    }
5008
5009    #[test]
5010    fn test_eval_map_step_with_item_index() {
5011        // Create pipe that returns @item.index
5012        // This requires testing through the full context
5013        let pipe = V2Pipe {
5014            start: V2Start::Ref(V2Ref::Input("items".to_string())),
5015            steps: vec![V2Step::Map(V2MapStep {
5016                steps: vec![], // Just return the item as-is
5017            })],
5018        };
5019        let record = json!({"items": [10, 20, 30]});
5020        let out = json!({});
5021        let ctx = V2EvalContext::new();
5022        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5023        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([10, 20, 30])));
5024    }
5025
5026    #[test]
5027    fn test_eval_map_step_multiple_ops() {
5028        // map: [trim, uppercase] on ["  a  ", "  b  "] -> ["A", "B"]
5029        let map_step = V2MapStep {
5030            steps: vec![
5031                V2Step::Op(V2OpStep {
5032                    op: "trim".to_string(),
5033                    args: vec![],
5034                }),
5035                V2Step::Op(V2OpStep {
5036                    op: "uppercase".to_string(),
5037                    args: vec![],
5038                }),
5039            ],
5040        };
5041        let record = json!({});
5042        let out = json!({});
5043        let ctx = V2EvalContext::new();
5044        let result = eval_v2_map_step(
5045            &map_step,
5046            EvalValue::Value(json!(["  a  ", "  b  "])),
5047            &record,
5048            None,
5049            &out,
5050            "test",
5051            &ctx,
5052        );
5053        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["A", "B"])));
5054    }
5055
5056    #[test]
5057    fn test_eval_pipe_with_map_step() {
5058        // Full pipe: [@input.names, { map: [uppercase] }]
5059        let pipe = V2Pipe {
5060            start: V2Start::Ref(V2Ref::Input("names".to_string())),
5061            steps: vec![V2Step::Map(V2MapStep {
5062                steps: vec![V2Step::Op(V2OpStep {
5063                    op: "uppercase".to_string(),
5064                    args: vec![],
5065                })],
5066            })],
5067        };
5068        let record = json!({"names": ["alice", "bob"]});
5069        let out = json!({});
5070        let ctx = V2EvalContext::new();
5071        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5072        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["ALICE", "BOB"])));
5073    }
5074
5075    #[test]
5076    fn test_eval_map_with_if_step() {
5077        // map with conditional: double if > 5, else keep
5078        let map_step = V2MapStep {
5079            steps: vec![V2Step::If(V2IfStep {
5080                cond: V2Condition::Comparison(V2Comparison {
5081                    op: V2ComparisonOp::Gt,
5082                    args: vec![
5083                        V2Expr::Pipe(V2Pipe {
5084                            start: V2Start::PipeValue,
5085                            steps: vec![],
5086                        }),
5087                        V2Expr::Pipe(V2Pipe {
5088                            start: V2Start::Literal(json!(5)),
5089                            steps: vec![],
5090                        }),
5091                    ],
5092                }),
5093                then_branch: V2Pipe {
5094                    start: V2Start::PipeValue,
5095                    steps: vec![V2Step::Op(V2OpStep {
5096                        op: "multiply".to_string(),
5097                        args: vec![V2Expr::Pipe(V2Pipe {
5098                            start: V2Start::Literal(json!(2)),
5099                            steps: vec![],
5100                        })],
5101                    })],
5102                },
5103                else_branch: None,
5104            })],
5105        };
5106        let record = json!({});
5107        let out = json!({});
5108        let ctx = V2EvalContext::new();
5109        // [3, 7, 2, 10] -> [3, 14, 2, 20] (only 7 and 10 are > 5)
5110        let result = eval_v2_map_step(
5111            &map_step,
5112            EvalValue::Value(json!([3, 7, 2, 10])),
5113            &record,
5114            None,
5115            &out,
5116            "test",
5117            &ctx,
5118        );
5119        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([3, 14.0, 2, 20.0])));
5120    }
5121
5122    #[test]
5123    fn test_eval_nested_map() {
5124        // Nested map: [[1, 2], [3, 4]] -> map of (map multiply 2) -> [[2, 4], [6, 8]]
5125        let map_step = V2MapStep {
5126            steps: vec![V2Step::Map(V2MapStep {
5127                steps: vec![V2Step::Op(V2OpStep {
5128                    op: "multiply".to_string(),
5129                    args: vec![V2Expr::Pipe(V2Pipe {
5130                        start: V2Start::Literal(json!(2)),
5131                        steps: vec![],
5132                    })],
5133                })],
5134            })],
5135        };
5136        let record = json!({});
5137        let out = json!({});
5138        let ctx = V2EvalContext::new();
5139        let result = eval_v2_map_step(
5140            &map_step,
5141            EvalValue::Value(json!([[1, 2], [3, 4]])),
5142            &record,
5143            None,
5144            &out,
5145            "test",
5146            &ctx,
5147        );
5148        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([[2.0, 4.0], [6.0, 8.0]])));
5149    }
5150
5151    #[test]
5152    fn test_eval_map_objects() {
5153        // Map over array of objects and extract a field
5154        // Since we're using pipe value directly, this tests object handling
5155        let pipe = V2Pipe {
5156            start: V2Start::Ref(V2Ref::Input("users".to_string())),
5157            steps: vec![V2Step::Map(V2MapStep {
5158                steps: vec![], // No-op, just return items
5159            })],
5160        };
5161        let record = json!({"users": [{"name": "Alice"}, {"name": "Bob"}]});
5162        let out = json!({});
5163        let ctx = V2EvalContext::new();
5164        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5165        assert!(
5166            matches!(result, Ok(EvalValue::Value(v)) if v == json!([{"name": "Alice"}, {"name": "Bob"}]))
5167        );
5168    }
5169}
5170
5171// =============================================================================
5172// v2 Pipe Full Evaluation Tests (T19)
5173// =============================================================================
5174
5175#[cfg(test)]
5176mod v2_pipe_eval_tests {
5177    use super::*;
5178    use serde_json::json;
5179
5180    #[test]
5181    fn test_eval_pipe_simple_ref() {
5182        let pipe = V2Pipe {
5183            start: V2Start::Ref(V2Ref::Input("name".to_string())),
5184            steps: vec![],
5185        };
5186        let record = json!({"name": "Alice"});
5187        let out = json!({});
5188        let ctx = V2EvalContext::new();
5189        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5190        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Alice")));
5191    }
5192
5193    #[test]
5194    fn test_eval_pipe_literal_start() {
5195        let pipe = V2Pipe {
5196            start: V2Start::Literal(json!(42)),
5197            steps: vec![],
5198        };
5199        let record = json!({});
5200        let out = json!({});
5201        let ctx = V2EvalContext::new();
5202        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5203        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(42)));
5204    }
5205
5206    #[test]
5207    fn test_eval_pipe_chain_ops() {
5208        // ["  hello  ", trim, uppercase]
5209        let pipe = V2Pipe {
5210            start: V2Start::Literal(json!("  hello  ")),
5211            steps: vec![
5212                V2Step::Op(V2OpStep {
5213                    op: "trim".to_string(),
5214                    args: vec![],
5215                }),
5216                V2Step::Op(V2OpStep {
5217                    op: "uppercase".to_string(),
5218                    args: vec![],
5219                }),
5220            ],
5221        };
5222        let record = json!({});
5223        let out = json!({});
5224        let ctx = V2EvalContext::new();
5225        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5226        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("HELLO")));
5227    }
5228
5229    #[test]
5230    fn test_eval_pipe_with_context() {
5231        // [@context.multiplier, multiply: @input.value]
5232        let pipe = V2Pipe {
5233            start: V2Start::Ref(V2Ref::Context("multiplier".to_string())),
5234            steps: vec![V2Step::Op(V2OpStep {
5235                op: "multiply".to_string(),
5236                args: vec![V2Expr::Pipe(V2Pipe {
5237                    start: V2Start::Ref(V2Ref::Input("value".to_string())),
5238                    steps: vec![],
5239                })],
5240            })],
5241        };
5242        let record = json!({"value": 10});
5243        let context = json!({"multiplier": 5});
5244        let out = json!({});
5245        let ctx = V2EvalContext::new();
5246        let result = eval_v2_pipe(&pipe, &record, Some(&context), &out, "test", &ctx);
5247        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(50.0)));
5248    }
5249
5250    #[test]
5251    fn test_eval_pipe_with_out_ref() {
5252        // [@out.previous, add: 1]
5253        let pipe = V2Pipe {
5254            start: V2Start::Ref(V2Ref::Out("previous".to_string())),
5255            steps: vec![V2Step::Op(V2OpStep {
5256                op: "add".to_string(),
5257                args: vec![V2Expr::Pipe(V2Pipe {
5258                    start: V2Start::Literal(json!(1)),
5259                    steps: vec![],
5260                })],
5261            })],
5262        };
5263        let record = json!({});
5264        let out = json!({"previous": 99});
5265        let ctx = V2EvalContext::new();
5266        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5267        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100.0)));
5268    }
5269
5270    #[test]
5271    fn test_eval_pipe_complex_chain() {
5272        // [@input.price, let: {original: $}, multiply: 0.9, let: {discounted: $},
5273        //  if: {cond: {gt: [$, 1000]}, then: [subtract: 100]}]
5274        let pipe = V2Pipe {
5275            start: V2Start::Ref(V2Ref::Input("price".to_string())),
5276            steps: vec![
5277                V2Step::Let(V2LetStep {
5278                    bindings: vec![(
5279                        "original".to_string(),
5280                        V2Expr::Pipe(V2Pipe {
5281                            start: V2Start::PipeValue,
5282                            steps: vec![],
5283                        }),
5284                    )],
5285                }),
5286                V2Step::Op(V2OpStep {
5287                    op: "multiply".to_string(),
5288                    args: vec![V2Expr::Pipe(V2Pipe {
5289                        start: V2Start::Literal(json!(0.9)),
5290                        steps: vec![],
5291                    })],
5292                }),
5293                V2Step::If(V2IfStep {
5294                    cond: V2Condition::Comparison(V2Comparison {
5295                        op: V2ComparisonOp::Gt,
5296                        args: vec![
5297                            V2Expr::Pipe(V2Pipe {
5298                                start: V2Start::PipeValue,
5299                                steps: vec![],
5300                            }),
5301                            V2Expr::Pipe(V2Pipe {
5302                                start: V2Start::Literal(json!(1000)),
5303                                steps: vec![],
5304                            }),
5305                        ],
5306                    }),
5307                    then_branch: V2Pipe {
5308                        start: V2Start::PipeValue,
5309                        steps: vec![V2Step::Op(V2OpStep {
5310                            op: "subtract".to_string(),
5311                            args: vec![V2Expr::Pipe(V2Pipe {
5312                                start: V2Start::Literal(json!(100)),
5313                                steps: vec![],
5314                            })],
5315                        })],
5316                    },
5317                    else_branch: None,
5318                }),
5319            ],
5320        };
5321        let record = json!({"price": 2000});
5322        let out = json!({});
5323        let ctx = V2EvalContext::new();
5324        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5325        // 2000 * 0.9 = 1800 > 1000, so 1800 - 100 = 1700
5326        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(1700.0)));
5327    }
5328
5329    #[test]
5330    fn test_eval_pipe_all_step_types() {
5331        // Test combining let, op, if, map in one pipe
5332        let pipe = V2Pipe {
5333            start: V2Start::Ref(V2Ref::Input("items".to_string())),
5334            steps: vec![
5335                // map: multiply each by 2
5336                V2Step::Map(V2MapStep {
5337                    steps: vec![V2Step::Op(V2OpStep {
5338                        op: "multiply".to_string(),
5339                        args: vec![V2Expr::Pipe(V2Pipe {
5340                            start: V2Start::Literal(json!(2)),
5341                            steps: vec![],
5342                        })],
5343                    })],
5344                }),
5345            ],
5346        };
5347        let record = json!({"items": [1, 2, 3]});
5348        let out = json!({});
5349        let ctx = V2EvalContext::new();
5350        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5351        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([2.0, 4.0, 6.0])));
5352    }
5353
5354    #[test]
5355    fn test_eval_pipe_coalesce_chain() {
5356        // [@input.primary, coalesce: @input.secondary, coalesce: "default"]
5357        let pipe = V2Pipe {
5358            start: V2Start::Ref(V2Ref::Input("primary".to_string())),
5359            steps: vec![
5360                V2Step::Op(V2OpStep {
5361                    op: "coalesce".to_string(),
5362                    args: vec![V2Expr::Pipe(V2Pipe {
5363                        start: V2Start::Ref(V2Ref::Input("secondary".to_string())),
5364                        steps: vec![],
5365                    })],
5366                }),
5367                V2Step::Op(V2OpStep {
5368                    op: "coalesce".to_string(),
5369                    args: vec![V2Expr::Pipe(V2Pipe {
5370                        start: V2Start::Literal(json!("default")),
5371                        steps: vec![],
5372                    })],
5373                }),
5374            ],
5375        };
5376
5377        // Test with primary present
5378        let record = json!({"primary": "first"});
5379        let out = json!({});
5380        let ctx = V2EvalContext::new();
5381        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5382        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("first")));
5383
5384        // Test with primary null, secondary present
5385        let record = json!({"primary": null, "secondary": "second"});
5386        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5387        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("second")));
5388
5389        // Test with both null, use default
5390        let record = json!({"primary": null, "secondary": null});
5391        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5392        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("default")));
5393    }
5394
5395    #[test]
5396    fn test_eval_expr_with_v2_pipe() {
5397        let expr = V2Expr::Pipe(V2Pipe {
5398            start: V2Start::Literal(json!("hello")),
5399            steps: vec![V2Step::Op(V2OpStep {
5400                op: "uppercase".to_string(),
5401                args: vec![],
5402            })],
5403        });
5404        let record = json!({});
5405        let out = json!({});
5406        let ctx = V2EvalContext::new();
5407        let result = eval_v2_expr(&expr, &record, None, &out, "test", &ctx);
5408        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("HELLO")));
5409    }
5410
5411    #[test]
5412    fn test_eval_pipe_deep_nesting() {
5413        // Deeply nested: input -> map -> if -> op
5414        let pipe = V2Pipe {
5415            start: V2Start::Ref(V2Ref::Input("scores".to_string())),
5416            steps: vec![V2Step::Map(V2MapStep {
5417                steps: vec![V2Step::If(V2IfStep {
5418                    cond: V2Condition::Comparison(V2Comparison {
5419                        op: V2ComparisonOp::Gte,
5420                        args: vec![
5421                            V2Expr::Pipe(V2Pipe {
5422                                start: V2Start::PipeValue,
5423                                steps: vec![],
5424                            }),
5425                            V2Expr::Pipe(V2Pipe {
5426                                start: V2Start::Literal(json!(60)),
5427                                steps: vec![],
5428                            }),
5429                        ],
5430                    }),
5431                    then_branch: V2Pipe {
5432                        start: V2Start::Literal(json!("pass")),
5433                        steps: vec![],
5434                    },
5435                    else_branch: Some(V2Pipe {
5436                        start: V2Start::Literal(json!("fail")),
5437                        steps: vec![],
5438                    }),
5439                })],
5440            })],
5441        };
5442        let record = json!({"scores": [80, 55, 90, 45]});
5443        let out = json!({});
5444        let ctx = V2EvalContext::new();
5445        let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5446        assert!(
5447            matches!(result, Ok(EvalValue::Value(v)) if v == json!(["pass", "fail", "pass", "fail"]))
5448        );
5449    }
5450}
5451
5452// =============================================================================
5453// v2 Lookup Evaluation Tests (T20)
5454// =============================================================================
5455
5456#[cfg(test)]
5457mod v2_lookup_eval_tests {
5458    use super::*;
5459    use serde_json::json;
5460
5461    fn make_departments() -> JsonValue {
5462        json!([
5463            {"id": 1, "name": "Engineering", "budget": 100000},
5464            {"id": 2, "name": "Sales", "budget": 50000},
5465            {"id": 3, "name": "HR", "budget": 30000}
5466        ])
5467    }
5468
5469    #[test]
5470    fn test_lookup_first_basic() {
5471        // lookup_first: {from: @context.departments, match: [id, 2], get: name}
5472        let op = V2OpStep {
5473            op: "lookup_first".to_string(),
5474            args: vec![
5475                V2Expr::Pipe(V2Pipe {
5476                    start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5477                    steps: vec![],
5478                }),
5479                V2Expr::Pipe(V2Pipe {
5480                    start: V2Start::Literal(json!("id")),
5481                    steps: vec![],
5482                }),
5483                V2Expr::Pipe(V2Pipe {
5484                    start: V2Start::Literal(json!(2)),
5485                    steps: vec![],
5486                }),
5487                V2Expr::Pipe(V2Pipe {
5488                    start: V2Start::Literal(json!("name")),
5489                    steps: vec![],
5490                }),
5491            ],
5492        };
5493        let record = json!({});
5494        let context = json!({"departments": make_departments()});
5495        let out = json!({});
5496        let ctx = V2EvalContext::new();
5497        let result = eval_v2_op_step(
5498            &op,
5499            EvalValue::Value(json!(null)),
5500            &record,
5501            Some(&context),
5502            &out,
5503            "test",
5504            &ctx,
5505        );
5506        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Sales")));
5507    }
5508
5509    #[test]
5510    fn test_lookup_first_uses_pipe_value_from() {
5511        let op = V2OpStep {
5512            op: "lookup_first".to_string(),
5513            args: vec![
5514                V2Expr::Pipe(V2Pipe {
5515                    start: V2Start::Literal(json!("id")),
5516                    steps: vec![],
5517                }),
5518                V2Expr::Pipe(V2Pipe {
5519                    start: V2Start::Literal(json!(2)),
5520                    steps: vec![],
5521                }),
5522                V2Expr::Pipe(V2Pipe {
5523                    start: V2Start::Literal(json!("budget")),
5524                    steps: vec![],
5525                }),
5526            ],
5527        };
5528        let record = json!({});
5529        let out = json!({});
5530        let ctx = V2EvalContext::new();
5531        let result = eval_v2_op_step(
5532            &op,
5533            EvalValue::Value(make_departments()),
5534            &record,
5535            None,
5536            &out,
5537            "test",
5538            &ctx,
5539        );
5540        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(50000)));
5541    }
5542
5543    #[test]
5544    fn test_lookup_first_no_match() {
5545        let op = V2OpStep {
5546            op: "lookup_first".to_string(),
5547            args: vec![
5548                V2Expr::Pipe(V2Pipe {
5549                    start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5550                    steps: vec![],
5551                }),
5552                V2Expr::Pipe(V2Pipe {
5553                    start: V2Start::Literal(json!("id")),
5554                    steps: vec![],
5555                }),
5556                V2Expr::Pipe(V2Pipe {
5557                    start: V2Start::Literal(json!(999)), // Non-existent ID
5558                    steps: vec![],
5559                }),
5560                V2Expr::Pipe(V2Pipe {
5561                    start: V2Start::Literal(json!("name")),
5562                    steps: vec![],
5563                }),
5564            ],
5565        };
5566        let record = json!({});
5567        let context = json!({"departments": make_departments()});
5568        let out = json!({});
5569        let ctx = V2EvalContext::new();
5570        let result = eval_v2_op_step(
5571            &op,
5572            EvalValue::Value(json!(null)),
5573            &record,
5574            Some(&context),
5575            &out,
5576            "test",
5577            &ctx,
5578        );
5579        assert!(matches!(result, Ok(EvalValue::Missing)));
5580    }
5581
5582    #[test]
5583    fn test_lookup_first_return_whole_object() {
5584        // Without 'get', return the whole matched object
5585        let op = V2OpStep {
5586            op: "lookup_first".to_string(),
5587            args: vec![
5588                V2Expr::Pipe(V2Pipe {
5589                    start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5590                    steps: vec![],
5591                }),
5592                V2Expr::Pipe(V2Pipe {
5593                    start: V2Start::Literal(json!("id")),
5594                    steps: vec![],
5595                }),
5596                V2Expr::Pipe(V2Pipe {
5597                    start: V2Start::Literal(json!(1)),
5598                    steps: vec![],
5599                }),
5600            ],
5601        };
5602        let record = json!({});
5603        let context = json!({"departments": make_departments()});
5604        let out = json!({});
5605        let ctx = V2EvalContext::new();
5606        let result = eval_v2_op_step(
5607            &op,
5608            EvalValue::Value(json!(null)),
5609            &record,
5610            Some(&context),
5611            &out,
5612            "test",
5613            &ctx,
5614        );
5615        assert!(
5616            matches!(result, Ok(EvalValue::Value(v)) if v == json!({"id": 1, "name": "Engineering", "budget": 100000}))
5617        );
5618    }
5619
5620    #[test]
5621    fn test_lookup_first_with_input_match_value() {
5622        // Match using value from input
5623        let op = V2OpStep {
5624            op: "lookup_first".to_string(),
5625            args: vec![
5626                V2Expr::Pipe(V2Pipe {
5627                    start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5628                    steps: vec![],
5629                }),
5630                V2Expr::Pipe(V2Pipe {
5631                    start: V2Start::Literal(json!("id")),
5632                    steps: vec![],
5633                }),
5634                V2Expr::Pipe(V2Pipe {
5635                    start: V2Start::Ref(V2Ref::Input("dept_id".to_string())),
5636                    steps: vec![],
5637                }),
5638                V2Expr::Pipe(V2Pipe {
5639                    start: V2Start::Literal(json!("name")),
5640                    steps: vec![],
5641                }),
5642            ],
5643        };
5644        let record = json!({"dept_id": 3});
5645        let context = json!({"departments": make_departments()});
5646        let out = json!({});
5647        let ctx = V2EvalContext::new();
5648        let result = eval_v2_op_step(
5649            &op,
5650            EvalValue::Value(json!(null)),
5651            &record,
5652            Some(&context),
5653            &out,
5654            "test",
5655            &ctx,
5656        );
5657        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("HR")));
5658    }
5659
5660    #[test]
5661    fn test_lookup_first_missing_match_value_does_not_match_null() {
5662        let users = json!([
5663            {"id": null, "name": "MissingUser"},
5664            {"id": 1, "name": "Alice"}
5665        ]);
5666        let op = V2OpStep {
5667            op: "lookup_first".to_string(),
5668            args: vec![
5669                V2Expr::Pipe(V2Pipe {
5670                    start: V2Start::Ref(V2Ref::Context("users".to_string())),
5671                    steps: vec![],
5672                }),
5673                V2Expr::Pipe(V2Pipe {
5674                    start: V2Start::Literal(json!("id")),
5675                    steps: vec![],
5676                }),
5677                V2Expr::Pipe(V2Pipe {
5678                    start: V2Start::Ref(V2Ref::Input("user_id".to_string())),
5679                    steps: vec![],
5680                }),
5681                V2Expr::Pipe(V2Pipe {
5682                    start: V2Start::Literal(json!("name")),
5683                    steps: vec![],
5684                }),
5685            ],
5686        };
5687        let record = json!({});
5688        let context = json!({"users": users});
5689        let out = json!({});
5690        let ctx = V2EvalContext::new();
5691        let result = eval_v2_op_step(
5692            &op,
5693            EvalValue::Value(json!(null)),
5694            &record,
5695            Some(&context),
5696            &out,
5697            "test",
5698            &ctx,
5699        );
5700        assert!(matches!(result, Ok(EvalValue::Missing)));
5701    }
5702
5703    #[test]
5704    fn test_lookup_all_matches() {
5705        // lookup (not lookup_first) returns all matches
5706        let employees = json!([
5707            {"name": "Alice", "dept": "Engineering"},
5708            {"name": "Bob", "dept": "Sales"},
5709            {"name": "Charlie", "dept": "Engineering"},
5710            {"name": "Diana", "dept": "HR"}
5711        ]);
5712        let op = V2OpStep {
5713            op: "lookup".to_string(),
5714            args: vec![
5715                V2Expr::Pipe(V2Pipe {
5716                    start: V2Start::Ref(V2Ref::Context("employees".to_string())),
5717                    steps: vec![],
5718                }),
5719                V2Expr::Pipe(V2Pipe {
5720                    start: V2Start::Literal(json!("dept")),
5721                    steps: vec![],
5722                }),
5723                V2Expr::Pipe(V2Pipe {
5724                    start: V2Start::Literal(json!("Engineering")),
5725                    steps: vec![],
5726                }),
5727                V2Expr::Pipe(V2Pipe {
5728                    start: V2Start::Literal(json!("name")),
5729                    steps: vec![],
5730                }),
5731            ],
5732        };
5733        let record = json!({});
5734        let context = json!({"employees": employees});
5735        let out = json!({});
5736        let ctx = V2EvalContext::new();
5737        let result = eval_v2_op_step(
5738            &op,
5739            EvalValue::Value(json!(null)),
5740            &record,
5741            Some(&context),
5742            &out,
5743            "test",
5744            &ctx,
5745        );
5746        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["Alice", "Charlie"])));
5747    }
5748
5749    #[test]
5750    fn test_lookup_no_matches() {
5751        let op = V2OpStep {
5752            op: "lookup".to_string(),
5753            args: vec![
5754                V2Expr::Pipe(V2Pipe {
5755                    start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5756                    steps: vec![],
5757                }),
5758                V2Expr::Pipe(V2Pipe {
5759                    start: V2Start::Literal(json!("id")),
5760                    steps: vec![],
5761                }),
5762                V2Expr::Pipe(V2Pipe {
5763                    start: V2Start::Literal(json!(999)),
5764                    steps: vec![],
5765                }),
5766            ],
5767        };
5768        let record = json!({});
5769        let context = json!({"departments": make_departments()});
5770        let out = json!({});
5771        let ctx = V2EvalContext::new();
5772        let result = eval_v2_op_step(
5773            &op,
5774            EvalValue::Value(json!(null)),
5775            &record,
5776            Some(&context),
5777            &out,
5778            "test",
5779            &ctx,
5780        );
5781        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([])));
5782    }
5783
5784    #[test]
5785    fn test_lookup_missing_match_value_does_not_match_null() {
5786        let users = json!([
5787            {"id": null, "name": "MissingUser"},
5788            {"id": 1, "name": "Alice"}
5789        ]);
5790        let op = V2OpStep {
5791            op: "lookup".to_string(),
5792            args: vec![
5793                V2Expr::Pipe(V2Pipe {
5794                    start: V2Start::Ref(V2Ref::Context("users".to_string())),
5795                    steps: vec![],
5796                }),
5797                V2Expr::Pipe(V2Pipe {
5798                    start: V2Start::Literal(json!("id")),
5799                    steps: vec![],
5800                }),
5801                V2Expr::Pipe(V2Pipe {
5802                    start: V2Start::Ref(V2Ref::Input("user_id".to_string())),
5803                    steps: vec![],
5804                }),
5805                V2Expr::Pipe(V2Pipe {
5806                    start: V2Start::Literal(json!("name")),
5807                    steps: vec![],
5808                }),
5809            ],
5810        };
5811        let record = json!({});
5812        let context = json!({"users": users});
5813        let out = json!({});
5814        let ctx = V2EvalContext::new();
5815        let result = eval_v2_op_step(
5816            &op,
5817            EvalValue::Value(json!(null)),
5818            &record,
5819            Some(&context),
5820            &out,
5821            "test",
5822            &ctx,
5823        );
5824        assert!(matches!(result, Ok(EvalValue::Missing)));
5825    }
5826
5827    #[test]
5828    fn test_lookup_first_missing_from() {
5829        let op = V2OpStep {
5830            op: "lookup_first".to_string(),
5831            args: vec![
5832                V2Expr::Pipe(V2Pipe {
5833                    start: V2Start::Ref(V2Ref::Context("nonexistent".to_string())),
5834                    steps: vec![],
5835                }),
5836                V2Expr::Pipe(V2Pipe {
5837                    start: V2Start::Literal(json!("id")),
5838                    steps: vec![],
5839                }),
5840                V2Expr::Pipe(V2Pipe {
5841                    start: V2Start::Literal(json!(1)),
5842                    steps: vec![],
5843                }),
5844            ],
5845        };
5846        let record = json!({});
5847        let context = json!({});
5848        let out = json!({});
5849        let ctx = V2EvalContext::new();
5850        let result = eval_v2_op_step(
5851            &op,
5852            EvalValue::Value(json!(null)),
5853            &record,
5854            Some(&context),
5855            &out,
5856            "test",
5857            &ctx,
5858        );
5859        // Missing 'from' returns Missing
5860        assert!(matches!(result, Ok(EvalValue::Missing)));
5861    }
5862
5863    #[test]
5864    fn test_lookup_first_insufficient_args() {
5865        let op = V2OpStep {
5866            op: "lookup_first".to_string(),
5867            args: vec![V2Expr::Pipe(V2Pipe {
5868                start: V2Start::Literal(json!([])),
5869                steps: vec![],
5870            })],
5871        };
5872        let record = json!({});
5873        let out = json!({});
5874        let ctx = V2EvalContext::new();
5875        let result = eval_v2_op_step(
5876            &op,
5877            EvalValue::Value(json!(null)),
5878            &record,
5879            None,
5880            &out,
5881            "test",
5882            &ctx,
5883        );
5884        assert!(result.is_err());
5885    }
5886
5887    #[test]
5888    fn test_lookup_in_pipe() {
5889        // Full pipe: lookup then transform result
5890        // Simpler test: just lookup and verify
5891        let pipe = V2Pipe {
5892            start: V2Start::Literal(json!(null)),
5893            steps: vec![V2Step::Op(V2OpStep {
5894                op: "lookup_first".to_string(),
5895                args: vec![
5896                    V2Expr::Pipe(V2Pipe {
5897                        start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5898                        steps: vec![],
5899                    }),
5900                    V2Expr::Pipe(V2Pipe {
5901                        start: V2Start::Literal(json!("id")),
5902                        steps: vec![],
5903                    }),
5904                    V2Expr::Pipe(V2Pipe {
5905                        start: V2Start::Ref(V2Ref::Input("dept_id".to_string())),
5906                        steps: vec![],
5907                    }),
5908                    V2Expr::Pipe(V2Pipe {
5909                        start: V2Start::Literal(json!("budget")),
5910                        steps: vec![],
5911                    }),
5912                ],
5913            })],
5914        };
5915        let record = json!({"dept_id": 2}); // Sales dept
5916        let context = json!({"departments": make_departments()});
5917        let out = json!({});
5918        let ctx = V2EvalContext::new();
5919        let result = eval_v2_pipe(&pipe, &record, Some(&context), &out, "test", &ctx);
5920        // Sales budget is 50000
5921        assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(50000)));
5922    }
5923
5924    #[test]
5925    fn test_lookup_then_multiply() {
5926        // Two-step pipe: lookup, then multiply
5927        let pipe = V2Pipe {
5928            start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5929            steps: vec![],
5930        };
5931        let record = json!({"dept_id": 2});
5932        let context = json!({"departments": make_departments()});
5933        let out = json!({});
5934        let ctx = V2EvalContext::new();
5935
5936        // First verify context is accessible
5937        let result = eval_v2_pipe(&pipe, &record, Some(&context), &out, "test", &ctx);
5938        assert!(result.is_ok());
5939
5940        // Now test just the lookup op step directly
5941        let op = V2OpStep {
5942            op: "lookup_first".to_string(),
5943            args: vec![
5944                V2Expr::Pipe(V2Pipe {
5945                    start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5946                    steps: vec![],
5947                }),
5948                V2Expr::Pipe(V2Pipe {
5949                    start: V2Start::Literal(json!("id")),
5950                    steps: vec![],
5951                }),
5952                V2Expr::Pipe(V2Pipe {
5953                    start: V2Start::Literal(json!(2)),
5954                    steps: vec![],
5955                }),
5956                V2Expr::Pipe(V2Pipe {
5957                    start: V2Start::Literal(json!("budget")),
5958                    steps: vec![],
5959                }),
5960            ],
5961        };
5962        let result = eval_v2_op_step(
5963            &op,
5964            EvalValue::Value(json!(null)),
5965            &record,
5966            Some(&context),
5967            &out,
5968            "test",
5969            &ctx,
5970        );
5971        assert!(matches!(result, Ok(EvalValue::Value(ref v)) if *v == json!(50000)));
5972
5973        // Now multiply it
5974        let multiply_op = V2OpStep {
5975            op: "multiply".to_string(),
5976            args: vec![V2Expr::Pipe(V2Pipe {
5977                start: V2Start::Literal(json!(1.1)),
5978                steps: vec![],
5979            })],
5980        };
5981        let budget = result.unwrap();
5982        let result2 = eval_v2_op_step(&multiply_op, budget, &record, None, &out, "test", &ctx);
5983        // multiply returns f64, check approximately 55000
5984        match result2 {
5985            Ok(EvalValue::Value(v)) => {
5986                let num = v.as_f64().expect("should be number");
5987                assert!(
5988                    (num - 55000.0).abs() < 0.001,
5989                    "expected 55000.0, got {}",
5990                    num
5991                );
5992            }
5993            other => panic!("expected Ok(EvalValue::Value), got {:?}", other),
5994        }
5995    }
5996}