Skip to main content

cyrs_plan/
ser.rs

1//! Serde implementations for the plan IR (spec 0001 §12).
2//!
3//! All plan types derive `Serialize` + `Deserialize` behind the `serde`
4//! feature gate. Enum variants use an internally-tagged representation
5//! (`#[serde(tag = "op", rename_all = "snake_case")]`) so that every JSON
6//! operator object carries an `"op"` discriminant field.
7//!
8//! Determinism: `Expr::Map` uses `Vec<(SmolStr, Expr)>` (ordered by
9//! insertion, which the lowering pass controls). `PlanStatement::var_map`
10//! uses `IndexMap` (insertion-ordered). No `HashMap` crosses the public API.
11//! See spec §17.14.
12
13use serde::{Deserialize, Serialize};
14use smol_str::SmolStr;
15
16use crate::{
17    AggExpr, BinOp, Direction, Expr, LabelSet, ListPredKind, NodeSpec, OpId, OrderKey, Projection,
18    ReadOp, RelLength, RelSpec, SortDir, UnaryOp, UnionKind, VarId, WriteOp,
19};
20
21// ── VarId / OpId ─────────────────────────────────────────────────────────────
22
23impl Serialize for VarId {
24    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
25        s.serialize_u32(self.0)
26    }
27}
28
29impl<'de> Deserialize<'de> for VarId {
30    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
31        u32::deserialize(d).map(VarId)
32    }
33}
34
35impl Serialize for OpId {
36    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
37        s.serialize_u32(self.0)
38    }
39}
40
41impl<'de> Deserialize<'de> for OpId {
42    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
43        u32::deserialize(d).map(OpId)
44    }
45}
46
47// ── LabelSet ──────────────────────────────────────────────────────────────────
48
49impl Serialize for LabelSet {
50    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
51        self.0.serialize(s)
52    }
53}
54
55impl<'de> Deserialize<'de> for LabelSet {
56    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
57        Vec::<SmolStr>::deserialize(d).map(LabelSet)
58    }
59}
60
61// ── Direction ─────────────────────────────────────────────────────────────────
62
63impl Serialize for Direction {
64    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
65        let tag = match self {
66            Direction::Outgoing => "outgoing",
67            Direction::Incoming => "incoming",
68            Direction::Undirected => "undirected",
69        };
70        s.serialize_str(tag)
71    }
72}
73
74impl<'de> Deserialize<'de> for Direction {
75    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
76        let s = String::deserialize(d)?;
77        match s.as_str() {
78            "outgoing" => Ok(Direction::Outgoing),
79            "incoming" => Ok(Direction::Incoming),
80            "undirected" => Ok(Direction::Undirected),
81            other => Err(serde::de::Error::unknown_variant(
82                other,
83                &["outgoing", "incoming", "undirected"],
84            )),
85        }
86    }
87}
88
89// ── RelLength ─────────────────────────────────────────────────────────────────
90
91#[derive(Serialize, Deserialize)]
92#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
93enum RelLengthSer {
94    Single,
95    Variable { min: Option<u64>, max: Option<u64> },
96}
97
98impl Serialize for RelLength {
99    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
100        let proxy = match self {
101            RelLength::Single => RelLengthSer::Single,
102            RelLength::Variable { min, max } => RelLengthSer::Variable {
103                min: *min,
104                max: *max,
105            },
106        };
107        proxy.serialize(s)
108    }
109}
110
111impl<'de> Deserialize<'de> for RelLength {
112    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
113        match RelLengthSer::deserialize(d)? {
114            RelLengthSer::Single => Ok(RelLength::Single),
115            RelLengthSer::Variable { min, max } => Ok(RelLength::Variable { min, max }),
116        }
117    }
118}
119
120// ── UnionKind ─────────────────────────────────────────────────────────────────
121
122impl Serialize for UnionKind {
123    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
124        let tag = match self {
125            UnionKind::All => "all",
126            UnionKind::Distinct => "distinct",
127        };
128        s.serialize_str(tag)
129    }
130}
131
132impl<'de> Deserialize<'de> for UnionKind {
133    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
134        let s = String::deserialize(d)?;
135        match s.as_str() {
136            "all" => Ok(UnionKind::All),
137            "distinct" => Ok(UnionKind::Distinct),
138            other => Err(serde::de::Error::unknown_variant(
139                other,
140                &["all", "distinct"],
141            )),
142        }
143    }
144}
145
146// ── SortDir ───────────────────────────────────────────────────────────────────
147
148impl Serialize for SortDir {
149    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
150        let tag = match self {
151            SortDir::Asc => "asc",
152            SortDir::Desc => "desc",
153        };
154        s.serialize_str(tag)
155    }
156}
157
158impl<'de> Deserialize<'de> for SortDir {
159    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
160        let s = String::deserialize(d)?;
161        match s.as_str() {
162            "asc" => Ok(SortDir::Asc),
163            "desc" => Ok(SortDir::Desc),
164            other => Err(serde::de::Error::unknown_variant(other, &["asc", "desc"])),
165        }
166    }
167}
168
169// ── BinOp ─────────────────────────────────────────────────────────────────────
170
171impl Serialize for BinOp {
172    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
173        let tag = match self {
174            BinOp::Add => "add",
175            BinOp::Sub => "sub",
176            BinOp::Mul => "mul",
177            BinOp::Div => "div",
178            BinOp::Mod => "mod",
179            BinOp::Pow => "pow",
180            BinOp::Eq => "eq",
181            BinOp::Neq => "neq",
182            BinOp::Lt => "lt",
183            BinOp::Le => "le",
184            BinOp::Gt => "gt",
185            BinOp::Ge => "ge",
186            BinOp::And => "and",
187            BinOp::Or => "or",
188            BinOp::Xor => "xor",
189            BinOp::In => "in",
190            BinOp::StartsWith => "starts_with",
191            BinOp::EndsWith => "ends_with",
192            BinOp::Contains => "contains",
193            BinOp::RegexMatch => "regex_match",
194            BinOp::Concat => "concat",
195        };
196        s.serialize_str(tag)
197    }
198}
199
200impl<'de> Deserialize<'de> for BinOp {
201    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
202        let s = String::deserialize(d)?;
203        match s.as_str() {
204            "add" => Ok(BinOp::Add),
205            "sub" => Ok(BinOp::Sub),
206            "mul" => Ok(BinOp::Mul),
207            "div" => Ok(BinOp::Div),
208            "mod" => Ok(BinOp::Mod),
209            "pow" => Ok(BinOp::Pow),
210            "eq" => Ok(BinOp::Eq),
211            "neq" => Ok(BinOp::Neq),
212            "lt" => Ok(BinOp::Lt),
213            "le" => Ok(BinOp::Le),
214            "gt" => Ok(BinOp::Gt),
215            "ge" => Ok(BinOp::Ge),
216            "and" => Ok(BinOp::And),
217            "or" => Ok(BinOp::Or),
218            "xor" => Ok(BinOp::Xor),
219            "in" => Ok(BinOp::In),
220            "starts_with" => Ok(BinOp::StartsWith),
221            "ends_with" => Ok(BinOp::EndsWith),
222            "contains" => Ok(BinOp::Contains),
223            "regex_match" => Ok(BinOp::RegexMatch),
224            "concat" => Ok(BinOp::Concat),
225            other => Err(serde::de::Error::unknown_variant(
226                other,
227                &[
228                    "add",
229                    "sub",
230                    "mul",
231                    "div",
232                    "mod",
233                    "pow",
234                    "eq",
235                    "neq",
236                    "lt",
237                    "le",
238                    "gt",
239                    "ge",
240                    "and",
241                    "or",
242                    "xor",
243                    "in",
244                    "starts_with",
245                    "ends_with",
246                    "contains",
247                    "regex_match",
248                    "concat",
249                ],
250            )),
251        }
252    }
253}
254
255// ── UnaryOp ───────────────────────────────────────────────────────────────────
256
257impl Serialize for UnaryOp {
258    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
259        let tag = match self {
260            UnaryOp::Neg => "neg",
261            UnaryOp::Not => "not",
262        };
263        s.serialize_str(tag)
264    }
265}
266
267impl<'de> Deserialize<'de> for UnaryOp {
268    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
269        let s = String::deserialize(d)?;
270        match s.as_str() {
271            "neg" => Ok(UnaryOp::Neg),
272            "not" => Ok(UnaryOp::Not),
273            other => Err(serde::de::Error::unknown_variant(other, &["neg", "not"])),
274        }
275    }
276}
277
278// ── ListPredKind ──────────────────────────────────────────────────────────────
279
280impl Serialize for ListPredKind {
281    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
282        let tag = match self {
283            ListPredKind::Any => "any",
284            ListPredKind::All => "all",
285            ListPredKind::None => "none",
286            ListPredKind::Single => "single",
287        };
288        s.serialize_str(tag)
289    }
290}
291
292impl<'de> Deserialize<'de> for ListPredKind {
293    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
294        let s = String::deserialize(d)?;
295        match s.as_str() {
296            "any" => Ok(ListPredKind::Any),
297            "all" => Ok(ListPredKind::All),
298            "none" => Ok(ListPredKind::None),
299            "single" => Ok(ListPredKind::Single),
300            other => Err(serde::de::Error::unknown_variant(
301                other,
302                &["any", "all", "none", "single"],
303            )),
304        }
305    }
306}
307
308// ── NodeSpec / RelSpec ────────────────────────────────────────────────────────
309
310#[derive(Serialize, Deserialize)]
311#[serde(deny_unknown_fields)]
312struct NodeSpecSer {
313    labels: LabelSet,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    properties: Option<Expr>,
316}
317
318impl Serialize for NodeSpec {
319    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
320        NodeSpecSer {
321            labels: self.labels.clone(),
322            properties: self.properties.clone(),
323        }
324        .serialize(s)
325    }
326}
327
328impl<'de> Deserialize<'de> for NodeSpec {
329    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
330        let proxy = NodeSpecSer::deserialize(d)?;
331        Ok(NodeSpec {
332            labels: proxy.labels,
333            properties: proxy.properties,
334        })
335    }
336}
337
338#[derive(Serialize, Deserialize)]
339#[serde(deny_unknown_fields)]
340struct RelSpecSer {
341    types: Vec<SmolStr>,
342    direction: Direction,
343    length: RelLength,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    properties: Option<Expr>,
346}
347
348impl Serialize for RelSpec {
349    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
350        RelSpecSer {
351            types: self.types.clone(),
352            direction: self.direction,
353            length: self.length.clone(),
354            properties: self.properties.clone(),
355        }
356        .serialize(s)
357    }
358}
359
360impl<'de> Deserialize<'de> for RelSpec {
361    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
362        let proxy = RelSpecSer::deserialize(d)?;
363        Ok(RelSpec {
364            types: proxy.types,
365            direction: proxy.direction,
366            length: proxy.length,
367            properties: proxy.properties,
368        })
369    }
370}
371
372// ── Projection / OrderKey / AggExpr ──────────────────────────────────────────
373
374#[derive(Serialize, Deserialize)]
375#[serde(deny_unknown_fields)]
376struct ProjectionSer {
377    expr: Expr,
378    alias: SmolStr,
379}
380
381impl Serialize for Projection {
382    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
383        ProjectionSer {
384            expr: self.expr.clone(),
385            alias: self.alias.clone(),
386        }
387        .serialize(s)
388    }
389}
390
391impl<'de> Deserialize<'de> for Projection {
392    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
393        let proxy = ProjectionSer::deserialize(d)?;
394        Ok(Projection {
395            expr: proxy.expr,
396            alias: proxy.alias,
397        })
398    }
399}
400
401#[derive(Serialize, Deserialize)]
402#[serde(deny_unknown_fields)]
403struct OrderKeySer {
404    expr: Expr,
405    dir: SortDir,
406}
407
408impl Serialize for OrderKey {
409    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
410        OrderKeySer {
411            expr: self.expr.clone(),
412            dir: self.dir,
413        }
414        .serialize(s)
415    }
416}
417
418impl<'de> Deserialize<'de> for OrderKey {
419    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
420        let proxy = OrderKeySer::deserialize(d)?;
421        Ok(OrderKey {
422            expr: proxy.expr,
423            dir: proxy.dir,
424        })
425    }
426}
427
428#[derive(Serialize, Deserialize)]
429#[serde(deny_unknown_fields)]
430struct AggExprSer {
431    func: SmolStr,
432    args: Vec<Expr>,
433    distinct: bool,
434}
435
436impl Serialize for AggExpr {
437    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
438        AggExprSer {
439            func: self.func.clone(),
440            args: self.args.clone(),
441            distinct: self.distinct,
442        }
443        .serialize(s)
444    }
445}
446
447impl<'de> Deserialize<'de> for AggExpr {
448    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
449        let proxy = AggExprSer::deserialize(d)?;
450        Ok(AggExpr {
451            func: proxy.func,
452            args: proxy.args,
453            distinct: proxy.distinct,
454        })
455    }
456}
457
458// ── Expr ──────────────────────────────────────────────────────────────────────
459//
460// Internally tagged via `"kind"` discriminant (not `"op"` — that would
461// conflict with the `op` field in BinOp/UnaryOp variants). Each variant
462// carries only the fields the spec lists.
463
464#[derive(Serialize, Deserialize)]
465#[serde(tag = "kind", rename_all = "snake_case")]
466enum ExprSer {
467    Null,
468    Bool {
469        value: bool,
470    },
471    Int {
472        value: i64,
473    },
474    Float {
475        value: f64,
476    },
477    #[serde(rename = "str")]
478    String {
479        value: SmolStr,
480    },
481    Var {
482        id: VarId,
483    },
484    Prop {
485        target: Box<Expr>,
486        prop: SmolStr,
487    },
488    Index {
489        target: Box<Expr>,
490        index: Box<Expr>,
491    },
492    Slice {
493        target: Box<Expr>,
494        #[serde(skip_serializing_if = "Option::is_none")]
495        start: Option<Box<Expr>>,
496        #[serde(skip_serializing_if = "Option::is_none")]
497        end: Option<Box<Expr>>,
498    },
499    List {
500        items: Vec<Expr>,
501    },
502    Map {
503        // Vec preserves insertion order (deterministic). Spec §17.14.
504        entries: Vec<(SmolStr, Expr)>,
505    },
506    Call {
507        func: SmolStr,
508        args: Vec<Expr>,
509    },
510    BinOp {
511        bin_op: BinOp,
512        lhs: Box<Expr>,
513        rhs: Box<Expr>,
514    },
515    UnaryOp {
516        unary_op: UnaryOp,
517        operand: Box<Expr>,
518    },
519    Case {
520        #[serde(skip_serializing_if = "Option::is_none")]
521        scrutinee: Option<Box<Expr>>,
522        arms: Vec<(Expr, Expr)>,
523        #[serde(skip_serializing_if = "Option::is_none")]
524        otherwise: Option<Box<Expr>>,
525    },
526    IsNull {
527        operand: Box<Expr>,
528        negated: bool,
529    },
530    InList {
531        operand: Box<Expr>,
532        list: Box<Expr>,
533    },
534    ListPredicate {
535        #[serde(rename = "pred_kind")]
536        kind: ListPredKind,
537        var: VarId,
538        iterable: Box<Expr>,
539        #[serde(skip_serializing_if = "Option::is_none")]
540        predicate: Option<Box<Expr>>,
541    },
542    Param {
543        name: SmolStr,
544    },
545    /// Pattern-predicate existential check — `EXISTS(<pattern>)` in
546    /// expression position (cy-lve).
547    Exists {
548        pattern: Box<ReadOp>,
549    },
550}
551
552impl Serialize for Expr {
553    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
554        let proxy: ExprSer = match self {
555            Expr::Null => ExprSer::Null,
556            Expr::Bool(v) => ExprSer::Bool { value: *v },
557            Expr::Int(v) => ExprSer::Int { value: *v },
558            Expr::Float(v) => ExprSer::Float { value: *v },
559            Expr::String(v) => ExprSer::String { value: v.clone() },
560            Expr::Var(id) => ExprSer::Var { id: *id },
561            Expr::Prop { target, prop } => ExprSer::Prop {
562                target: Box::new(*target.clone()),
563                prop: prop.clone(),
564            },
565            Expr::Index { target, index } => ExprSer::Index {
566                target: Box::new(*target.clone()),
567                index: Box::new(*index.clone()),
568            },
569            Expr::Slice { target, start, end } => ExprSer::Slice {
570                target: Box::new(*target.clone()),
571                start: start.as_ref().map(|e| Box::new(*e.clone())),
572                end: end.as_ref().map(|e| Box::new(*e.clone())),
573            },
574            Expr::List(items) => ExprSer::List {
575                items: items.clone(),
576            },
577            Expr::Map(entries) => ExprSer::Map {
578                entries: entries.clone(),
579            },
580            Expr::Call { func, args } => ExprSer::Call {
581                func: func.clone(),
582                args: args.clone(),
583            },
584            Expr::BinOp { op, lhs, rhs } => ExprSer::BinOp {
585                bin_op: *op,
586                lhs: Box::new(*lhs.clone()),
587                rhs: Box::new(*rhs.clone()),
588            },
589            Expr::UnaryOp { op, operand } => ExprSer::UnaryOp {
590                unary_op: *op,
591                operand: Box::new(*operand.clone()),
592            },
593            Expr::Case {
594                scrutinee,
595                arms,
596                otherwise,
597            } => ExprSer::Case {
598                scrutinee: scrutinee.as_ref().map(|e| Box::new(*e.clone())),
599                arms: arms.clone(),
600                otherwise: otherwise.as_ref().map(|e| Box::new(*e.clone())),
601            },
602            Expr::IsNull { operand, negated } => ExprSer::IsNull {
603                operand: Box::new(*operand.clone()),
604                negated: *negated,
605            },
606            Expr::InList { operand, list } => ExprSer::InList {
607                operand: Box::new(*operand.clone()),
608                list: Box::new(*list.clone()),
609            },
610            Expr::ListPredicate {
611                kind,
612                var,
613                iterable,
614                predicate,
615            } => ExprSer::ListPredicate {
616                kind: *kind,
617                var: *var,
618                iterable: Box::new(*iterable.clone()),
619                predicate: predicate.as_ref().map(|p| Box::new(*p.clone())),
620            },
621            Expr::Param { name } => ExprSer::Param { name: name.clone() },
622            Expr::Exists { pattern } => ExprSer::Exists {
623                pattern: pattern.clone(),
624            },
625        };
626        proxy.serialize(s)
627    }
628}
629
630impl<'de> Deserialize<'de> for Expr {
631    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
632        let proxy = ExprSer::deserialize(d)?;
633        Ok(match proxy {
634            ExprSer::Null => Expr::Null,
635            ExprSer::Bool { value } => Expr::Bool(value),
636            ExprSer::Int { value } => Expr::Int(value),
637            ExprSer::Float { value } => Expr::Float(value),
638            ExprSer::String { value } => Expr::String(value),
639            ExprSer::Var { id } => Expr::Var(id),
640            ExprSer::Prop { target, prop } => Expr::Prop {
641                target: Box::new(*target),
642                prop,
643            },
644            ExprSer::Index { target, index } => Expr::Index {
645                target: Box::new(*target),
646                index: Box::new(*index),
647            },
648            ExprSer::Slice { target, start, end } => Expr::Slice {
649                target: Box::new(*target),
650                start: start.map(|e| Box::new(*e)),
651                end: end.map(|e| Box::new(*e)),
652            },
653            ExprSer::List { items } => Expr::List(items),
654            ExprSer::Map { entries } => Expr::Map(entries),
655            ExprSer::Call { func, args } => Expr::Call { func, args },
656            ExprSer::BinOp { bin_op, lhs, rhs } => Expr::BinOp {
657                op: bin_op,
658                lhs: Box::new(*lhs),
659                rhs: Box::new(*rhs),
660            },
661            ExprSer::UnaryOp { unary_op, operand } => Expr::UnaryOp {
662                op: unary_op,
663                operand: Box::new(*operand),
664            },
665            ExprSer::Case {
666                scrutinee,
667                arms,
668                otherwise,
669            } => Expr::Case {
670                scrutinee: scrutinee.map(|e| Box::new(*e)),
671                arms,
672                otherwise: otherwise.map(|e| Box::new(*e)),
673            },
674            ExprSer::IsNull { operand, negated } => Expr::IsNull {
675                operand: Box::new(*operand),
676                negated,
677            },
678            ExprSer::InList { operand, list } => Expr::InList {
679                operand: Box::new(*operand),
680                list: Box::new(*list),
681            },
682            ExprSer::ListPredicate {
683                kind,
684                var,
685                iterable,
686                predicate,
687            } => Expr::ListPredicate {
688                kind,
689                var,
690                iterable: Box::new(*iterable),
691                predicate: predicate.map(|p| Box::new(*p)),
692            },
693            ExprSer::Param { name } => Expr::Param { name },
694            ExprSer::Exists { pattern } => Expr::Exists { pattern },
695        })
696    }
697}
698
699// ── ReadOp ────────────────────────────────────────────────────────────────────
700
701#[derive(Serialize, Deserialize)]
702#[serde(tag = "op", rename_all = "snake_case")]
703enum ReadOpSer {
704    Source {
705        #[serde(skip_serializing_if = "Option::is_none")]
706        label: Option<LabelSet>,
707        bind: VarId,
708    },
709    Expand {
710        input: OpId,
711        from: VarId,
712        rel: RelSpec,
713        to: NodeSpec,
714        bind_rel: VarId,
715        bind_to: VarId,
716    },
717    Filter {
718        input: OpId,
719        predicate: Expr,
720    },
721    Project {
722        input: OpId,
723        items: Vec<Projection>,
724    },
725    Aggregate {
726        input: OpId,
727        keys: Vec<Expr>,
728        aggs: Vec<AggExpr>,
729    },
730    OrderBy {
731        input: OpId,
732        keys: Vec<OrderKey>,
733    },
734    Skip {
735        input: OpId,
736        count: Expr,
737    },
738    Limit {
739        input: OpId,
740        count: Expr,
741    },
742    Distinct {
743        input: OpId,
744    },
745    Unwind {
746        input: OpId,
747        list: Expr,
748        bind: VarId,
749    },
750    Union {
751        left: OpId,
752        right: OpId,
753        kind: UnionKind,
754    },
755    With {
756        input: OpId,
757        items: Vec<Projection>,
758        #[serde(skip_serializing_if = "Option::is_none")]
759        filter: Option<Expr>,
760    },
761    OptionalJoin {
762        input: OpId,
763        pattern: Box<ReadOp>,
764    },
765}
766
767impl Serialize for ReadOp {
768    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
769        let proxy: ReadOpSer = match self {
770            ReadOp::Source { label, bind } => ReadOpSer::Source {
771                label: label.clone(),
772                bind: *bind,
773            },
774            ReadOp::Expand {
775                input,
776                from,
777                rel,
778                to,
779                bind_rel,
780                bind_to,
781            } => ReadOpSer::Expand {
782                input: *input,
783                from: *from,
784                rel: rel.clone(),
785                to: to.clone(),
786                bind_rel: *bind_rel,
787                bind_to: *bind_to,
788            },
789            ReadOp::Filter { input, predicate } => ReadOpSer::Filter {
790                input: *input,
791                predicate: predicate.clone(),
792            },
793            ReadOp::Project { input, items } => ReadOpSer::Project {
794                input: *input,
795                items: items.clone(),
796            },
797            ReadOp::Aggregate { input, keys, aggs } => ReadOpSer::Aggregate {
798                input: *input,
799                keys: keys.clone(),
800                aggs: aggs.clone(),
801            },
802            ReadOp::OrderBy { input, keys } => ReadOpSer::OrderBy {
803                input: *input,
804                keys: keys.clone(),
805            },
806            ReadOp::Skip { input, count } => ReadOpSer::Skip {
807                input: *input,
808                count: count.clone(),
809            },
810            ReadOp::Limit { input, count } => ReadOpSer::Limit {
811                input: *input,
812                count: count.clone(),
813            },
814            ReadOp::Distinct { input } => ReadOpSer::Distinct { input: *input },
815            ReadOp::Unwind { input, list, bind } => ReadOpSer::Unwind {
816                input: *input,
817                list: list.clone(),
818                bind: *bind,
819            },
820            ReadOp::Union { left, right, kind } => ReadOpSer::Union {
821                left: *left,
822                right: *right,
823                kind: *kind,
824            },
825            ReadOp::With {
826                input,
827                items,
828                filter,
829            } => ReadOpSer::With {
830                input: *input,
831                items: items.clone(),
832                filter: filter.clone(),
833            },
834            ReadOp::OptionalJoin { input, pattern } => ReadOpSer::OptionalJoin {
835                input: *input,
836                pattern: pattern.clone(),
837            },
838        };
839        proxy.serialize(s)
840    }
841}
842
843impl<'de> Deserialize<'de> for ReadOp {
844    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
845        let proxy = ReadOpSer::deserialize(d)?;
846        Ok(match proxy {
847            ReadOpSer::Source { label, bind } => ReadOp::Source { label, bind },
848            ReadOpSer::Expand {
849                input,
850                from,
851                rel,
852                to,
853                bind_rel,
854                bind_to,
855            } => ReadOp::Expand {
856                input,
857                from,
858                rel,
859                to,
860                bind_rel,
861                bind_to,
862            },
863            ReadOpSer::Filter { input, predicate } => ReadOp::Filter { input, predicate },
864            ReadOpSer::Project { input, items } => ReadOp::Project { input, items },
865            ReadOpSer::Aggregate { input, keys, aggs } => ReadOp::Aggregate { input, keys, aggs },
866            ReadOpSer::OrderBy { input, keys } => ReadOp::OrderBy { input, keys },
867            ReadOpSer::Skip { input, count } => ReadOp::Skip { input, count },
868            ReadOpSer::Limit { input, count } => ReadOp::Limit { input, count },
869            ReadOpSer::Distinct { input } => ReadOp::Distinct { input },
870            ReadOpSer::Unwind { input, list, bind } => ReadOp::Unwind { input, list, bind },
871            ReadOpSer::Union { left, right, kind } => ReadOp::Union { left, right, kind },
872            ReadOpSer::With {
873                input,
874                items,
875                filter,
876            } => ReadOp::With {
877                input,
878                items,
879                filter,
880            },
881            ReadOpSer::OptionalJoin { input, pattern } => ReadOp::OptionalJoin { input, pattern },
882        })
883    }
884}
885
886// ── WriteOp ───────────────────────────────────────────────────────────────────
887
888#[derive(Serialize, Deserialize)]
889#[serde(tag = "op", rename_all = "snake_case")]
890enum WriteOpSer {
891    CreateNode {
892        labels: Vec<SmolStr>,
893        props: Expr,
894        #[serde(skip_serializing_if = "Option::is_none")]
895        bind: Option<VarId>,
896    },
897    CreateRel {
898        from: VarId,
899        to: VarId,
900        rel_type: SmolStr,
901        props: Expr,
902        #[serde(skip_serializing_if = "Option::is_none")]
903        bind: Option<VarId>,
904    },
905    MergeNode {
906        labels: Vec<SmolStr>,
907        props: Expr,
908        on_create: Vec<WriteOp>,
909        on_match: Vec<WriteOp>,
910        #[serde(skip_serializing_if = "Option::is_none")]
911        bind: Option<VarId>,
912    },
913    MergeRel {
914        from: VarId,
915        to: VarId,
916        rel_type: SmolStr,
917        props: Expr,
918        on_create: Vec<WriteOp>,
919        on_match: Vec<WriteOp>,
920        #[serde(skip_serializing_if = "Option::is_none")]
921        bind: Option<VarId>,
922    },
923    SetProperty {
924        target: VarId,
925        prop: SmolStr,
926        value: Expr,
927    },
928    SetLabels {
929        target: VarId,
930        labels: Vec<SmolStr>,
931    },
932    RemoveProperty {
933        target: VarId,
934        prop: SmolStr,
935    },
936    RemoveLabels {
937        target: VarId,
938        labels: Vec<SmolStr>,
939    },
940    Delete {
941        targets: Vec<Expr>,
942        detach: bool,
943    },
944}
945
946impl Serialize for WriteOp {
947    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
948        let proxy: WriteOpSer = match self {
949            WriteOp::CreateNode {
950                labels,
951                props,
952                bind,
953            } => WriteOpSer::CreateNode {
954                labels: labels.clone(),
955                props: props.clone(),
956                bind: *bind,
957            },
958            WriteOp::CreateRel {
959                from,
960                to,
961                rel_type,
962                props,
963                bind,
964            } => WriteOpSer::CreateRel {
965                from: *from,
966                to: *to,
967                rel_type: rel_type.clone(),
968                props: props.clone(),
969                bind: *bind,
970            },
971            WriteOp::MergeNode {
972                labels,
973                props,
974                on_create,
975                on_match,
976                bind,
977            } => WriteOpSer::MergeNode {
978                labels: labels.clone(),
979                props: props.clone(),
980                on_create: on_create.clone(),
981                on_match: on_match.clone(),
982                bind: *bind,
983            },
984            WriteOp::MergeRel {
985                from,
986                to,
987                rel_type,
988                props,
989                on_create,
990                on_match,
991                bind,
992            } => WriteOpSer::MergeRel {
993                from: *from,
994                to: *to,
995                rel_type: rel_type.clone(),
996                props: props.clone(),
997                on_create: on_create.clone(),
998                on_match: on_match.clone(),
999                bind: *bind,
1000            },
1001            WriteOp::SetProperty {
1002                target,
1003                prop,
1004                value,
1005            } => WriteOpSer::SetProperty {
1006                target: *target,
1007                prop: prop.clone(),
1008                value: value.clone(),
1009            },
1010            WriteOp::SetLabels { target, labels } => WriteOpSer::SetLabels {
1011                target: *target,
1012                labels: labels.clone(),
1013            },
1014            WriteOp::RemoveProperty { target, prop } => WriteOpSer::RemoveProperty {
1015                target: *target,
1016                prop: prop.clone(),
1017            },
1018            WriteOp::RemoveLabels { target, labels } => WriteOpSer::RemoveLabels {
1019                target: *target,
1020                labels: labels.clone(),
1021            },
1022            WriteOp::Delete { targets, detach } => WriteOpSer::Delete {
1023                targets: targets.clone(),
1024                detach: *detach,
1025            },
1026        };
1027        proxy.serialize(s)
1028    }
1029}
1030
1031impl<'de> Deserialize<'de> for WriteOp {
1032    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1033        let proxy = WriteOpSer::deserialize(d)?;
1034        Ok(match proxy {
1035            WriteOpSer::CreateNode {
1036                labels,
1037                props,
1038                bind,
1039            } => WriteOp::CreateNode {
1040                labels,
1041                props,
1042                bind,
1043            },
1044            WriteOpSer::CreateRel {
1045                from,
1046                to,
1047                rel_type,
1048                props,
1049                bind,
1050            } => WriteOp::CreateRel {
1051                from,
1052                to,
1053                rel_type,
1054                props,
1055                bind,
1056            },
1057            WriteOpSer::MergeNode {
1058                labels,
1059                props,
1060                on_create,
1061                on_match,
1062                bind,
1063            } => WriteOp::MergeNode {
1064                labels,
1065                props,
1066                on_create,
1067                on_match,
1068                bind,
1069            },
1070            WriteOpSer::MergeRel {
1071                from,
1072                to,
1073                rel_type,
1074                props,
1075                on_create,
1076                on_match,
1077                bind,
1078            } => WriteOp::MergeRel {
1079                from,
1080                to,
1081                rel_type,
1082                props,
1083                on_create,
1084                on_match,
1085                bind,
1086            },
1087            WriteOpSer::SetProperty {
1088                target,
1089                prop,
1090                value,
1091            } => WriteOp::SetProperty {
1092                target,
1093                prop,
1094                value,
1095            },
1096            WriteOpSer::SetLabels { target, labels } => WriteOp::SetLabels { target, labels },
1097            WriteOpSer::RemoveProperty { target, prop } => WriteOp::RemoveProperty { target, prop },
1098            WriteOpSer::RemoveLabels { target, labels } => WriteOp::RemoveLabels { target, labels },
1099            WriteOpSer::Delete { targets, detach } => WriteOp::Delete { targets, detach },
1100        })
1101    }
1102}
1103
1104// ── PlanStatement ─────────────────────────────────────────────────────────────
1105//
1106// `var_map` is IndexMap<VarId, HirVarId>. Serialised as an array of
1107// `[plan_var_u32, hir_var_u32]` pairs to keep ordering stable.
1108
1109use crate::lower::PlanStatement;
1110use cyrs_hir::VarId as HirVarId;
1111use indexmap::IndexMap;
1112
1113#[derive(Serialize, Deserialize)]
1114#[serde(deny_unknown_fields)]
1115struct PlanStatementSer {
1116    ops: Vec<ReadOp>,
1117    write_ops: Vec<WriteOp>,
1118    /// Ordered pairs of `(plan_var_id, hir_var_id)`. Insertion order is
1119    /// preserved by `IndexMap` (spec §17.14 determinism).
1120    var_map: Vec<(u32, u32)>,
1121}
1122
1123impl Serialize for PlanStatement {
1124    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1125        let var_map: Vec<(u32, u32)> = self
1126            .var_map
1127            .iter()
1128            .map(|(plan_v, hir_v)| (plan_v.0, hir_v.0))
1129            .collect();
1130        PlanStatementSer {
1131            ops: self.ops.clone(),
1132            write_ops: self.write_ops.clone(),
1133            var_map,
1134        }
1135        .serialize(s)
1136    }
1137}
1138
1139impl<'de> Deserialize<'de> for PlanStatement {
1140    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1141        let proxy = PlanStatementSer::deserialize(d)?;
1142        let mut var_map: IndexMap<VarId, HirVarId> = IndexMap::with_capacity(proxy.var_map.len());
1143        for (plan_v, hir_v) in proxy.var_map {
1144            var_map.insert(VarId(plan_v), HirVarId(hir_v));
1145        }
1146        Ok(PlanStatement {
1147            ops: proxy.ops,
1148            write_ops: proxy.write_ops,
1149            var_map,
1150        })
1151    }
1152}