Skip to main content

helix_dsl/
lib.rs

1//! # helix-enterprise-ql Query Guide
2//!
3//! `helix-enterprise-ql` (crate name: `helix_dsl`) is a query builder centered on two entry points:
4//! - [`read_batch()`] for read-only transactions
5//! - [`write_batch()`] for write-capable transactions
6//!
7//! Everything in this crate is designed to be composed inside those batch chains.
8//! You write one or more named traversals with `.var_as(...)` / `.var_as_if(...)`, then
9//! choose the final payload with `.returning(...)`.
10//!
11//! For shorter query code, import the curated builder API:
12//! ```
13//! use helix_dsl::prelude::*;
14//! ```
15//!
16//! ## Core Shape
17//!
18//! Read chain:
19//! `read_batch() -> var_as / var_as_if -> returning`
20//!
21//! Write chain:
22//! `write_batch() -> var_as / var_as_if -> returning`
23//!
24//! Each `var_as` call accepts a traversal expression, usually starting with `g()`.
25//! Traversals can read, traverse, filter, aggregate, or mutate depending on whether
26//! they are used in a read or write batch.
27//!
28//! ## Read Batches
29//!
30//! ```
31//! # use helix_dsl::prelude::*;
32//! read_batch()
33//!     .var_as(
34//!         "user",
35//!         g().n_where(SourcePredicate::eq("username", "alice")),
36//!     )
37//!     .var_as(
38//!         "friends",
39//!         g()
40//!             .n(NodeRef::var("user"))
41//!             .out(Some("FOLLOWS"))
42//!             .dedup()
43//!             .limit(100),
44//!     )
45//!     .returning(["user", "friends"]);
46//! ```
47//!
48//! ```
49//! # use helix_dsl::prelude::*;
50//! read_batch()
51//!     .var_as(
52//!         "active_users",
53//!         g()
54//!             .n_with_label_where("User", SourcePredicate::eq("status", "active"))
55//!             .where_(Predicate::gt("score", 100i64))
56//!             .order_by("score", Order::Desc)
57//!             .limit(25)
58//!             .value_map(Some(vec!["$id", "name", "score"])),
59//!     )
60//!     .returning(["active_users"]);
61//! ```
62//!
63//! ## Conditional Queries
64//!
65//! Use [`BatchCondition`] with `var_as_if` to run later queries only when earlier
66//! variables satisfy runtime conditions.
67//!
68//! ```
69//! # use helix_dsl::prelude::*;
70//! read_batch()
71//!     .var_as(
72//!         "user",
73//!         g().n_where(SourcePredicate::eq("username", "alice")),
74//!     )
75//!     .var_as_if(
76//!         "posts",
77//!         BatchCondition::VarNotEmpty("user".to_string()),
78//!         g().n(NodeRef::var("user")).out(Some("POSTED")),
79//!     )
80//!     .returning(["user", "posts"]);
81//! ```
82//!
83//! ## Write Batches
84//!
85//! ```
86//! # use helix_dsl::prelude::*;
87//! write_batch()
88//!     .var_as(
89//!         "alice",
90//!         g().add_n("User", vec![("name", "Alice"), ("tier", "pro")]),
91//!     )
92//!     .var_as("bob", g().add_n("User", vec![("name", "Bob")]))
93//!     .var_as(
94//!         "linked",
95//!         g()
96//!             .n(NodeRef::var("alice"))
97//!             .add_e(
98//!                 "FOLLOWS",
99//!                 NodeRef::var("bob"),
100//!                 vec![("since", "2026-01-01")],
101//!             )
102//!             .count(),
103//!     )
104//!     .returning(["alice", "bob", "linked"]);
105//! ```
106//!
107//! ```
108//! # use helix_dsl::prelude::*;
109//! write_batch()
110//!     .var_as(
111//!         "inactive_users",
112//!         g().n_with_label_where(
113//!             "User",
114//!             SourcePredicate::eq("status", "inactive"),
115//!         ),
116//!     )
117//!     .var_as_if(
118//!         "deactivated_count",
119//!         BatchCondition::VarNotEmpty("inactive_users".to_string()),
120//!         g()
121//!             .n(NodeRef::var("inactive_users"))
122//!             .set_property("deactivated", true)
123//!             .count(),
124//!     )
125//!     .returning(["deactivated_count"]);
126//! ```
127//!
128//! ## Vector Search Operations (End-to-End)
129//!
130//! The current Helix interpreter executes vector search as top-k nearest-neighbor
131//! lookup with these runtime semantics:
132//! - returns up to `k` hits (top-k behavior)
133//! - hit order is ascending by `$distance` (smaller is closer)
134//! - hit metadata can be read through virtual fields in projections:
135//!   - node hits: `$id`, `$distance`
136//!   - edge hits: `$id`, `$from`, `$to`, `$distance`
137//!
138//! ### Result field contract
139//!
140//! | Field | Type | Node hits | Edge hits | Meaning |
141//! |---|---|---:|---:|---|
142//! | `$id` | integer | yes | yes* | Node ID (for node hits) or edge ID (for edge hits) |
143//! | `$distance` | floating-point | yes | yes | Vector distance from query (`lower` = closer) |
144//! | `$from` | integer | no | yes | Edge source node ID |
145//! | `$to` | integer | no | yes | Edge target node ID |
146//!
147//! `*` For edge hits, `$id` is present when an edge ID is available in storage.
148//!
149//! Contract scope in the current Helix interpreter:
150//! - available on direct vector-hit streams and projection terminals
151//! - available in `value_map`, `values`, `project`, and (for edges) `edge_properties`
152//! - once a traversal step leaves the hit stream (`out`, `in_`, `both`, etc.),
153//!   downstream traversers no longer carry distance metadata
154//!
155//! ### 1) Create indexes and insert vectors
156//!
157//! ```
158//! # use helix_dsl::prelude::*;
159//! write_batch()
160//!     .var_as(
161//!         "create_doc_index",
162//!         g().create_vector_index_nodes(
163//!             "Doc",
164//!             "embedding",
165//!             None::<&str>,
166//!         ),
167//!     )
168//!     .var_as(
169//!         "create_similar_index",
170//!         g().create_vector_index_edges(
171//!             "SIMILAR",
172//!             "embedding",
173//!             None::<&str>,
174//!         ),
175//!     )
176//!     .var_as(
177//!         "doc_a",
178//!         g().add_n(
179//!             "Doc",
180//!             vec![
181//!                 ("title", PropertyValue::from("A")),
182//!                 ("embedding", PropertyValue::from(vec![1.0f32, 0.0, 0.0])),
183//!             ],
184//!         ),
185//!     )
186//!     .var_as(
187//!         "doc_b",
188//!         g().add_n(
189//!             "Doc",
190//!             vec![
191//!                 ("title", PropertyValue::from("B")),
192//!                 ("embedding", PropertyValue::from(vec![0.9f32, 0.1, 0.0])),
193//!             ],
194//!         ),
195//!     )
196//!     .returning(["create_doc_index", "doc_a", "doc_b"]);
197//! ```
198//!
199//! ### 2) Node vector search: get ranked hits and fetch node properties
200//!
201//! ```
202//! # use helix_dsl::prelude::*;
203//! read_batch()
204//!     .var_as(
205//!         "doc_hits",
206//!         g().vector_search_nodes("Doc", "embedding", vec![1.0f32, 0.0, 0.0], 5, None)
207//!             .value_map(Some(vec!["$id", "$distance", "title"])),
208//!     )
209//!     .returning(["doc_hits"]);
210//! ```
211//!
212//! ```text
213//! doc_hits rows (example shape):
214//! [
215//!   { "$id": 42, "$distance": 0.0031, "title": "A" },
216//!   { "$id": 77, "$distance": 0.0198, "title": "B" }
217//! ]
218//! ```
219//!
220//! ### 3) Use `project(...)` on vector hits (including distance)
221//!
222//! ```
223//! # use helix_dsl::prelude::*;
224//! read_batch()
225//!     .var_as(
226//!         "ranked_docs",
227//!         g().vector_search_nodes("Doc", "embedding", vec![1.0f32, 0.0, 0.0], 10, None)
228//!             .project(vec![
229//!                 PropertyProjection::renamed("$id", "doc_id"),
230//!                 PropertyProjection::renamed("$distance", "score"),
231//!                 PropertyProjection::new("title"),
232//!             ]),
233//!     )
234//!     .returning(["ranked_docs"]);
235//! ```
236//!
237//! ### 4) Traverse from hit IDs to related entities
238//!
239//! Store hit rows (with `$id` + `$distance`) and then use `NodeRef::var(...)` to
240//! continue graph traversal from those hit IDs.
241//!
242//! ```
243//! # use helix_dsl::prelude::*;
244//! read_batch()
245//!     .var_as(
246//!         "doc_hit_rows",
247//!         g().vector_search_nodes("Doc", "embedding", vec![1.0f32, 0.0, 0.0], 5, None)
248//!             .value_map(Some(vec!["$id", "$distance", "title"])),
249//!     )
250//!     .var_as(
251//!         "authors",
252//!         g().n(NodeRef::var("doc_hit_rows"))
253//!             .out(Some("AUTHORED_BY"))
254//!             .value_map(Some(vec!["$id", "name"])),
255//!     )
256//!     .returning(["doc_hit_rows", "authors"]);
257//! ```
258//!
259//! ### 5) Edge vector search and endpoint/property extraction
260//!
261//! ```
262//! # use helix_dsl::prelude::*;
263//! read_batch()
264//!     .var_as(
265//!         "edge_hits",
266//!         g().vector_search_edges("SIMILAR", "embedding", vec![1.0f32, 0.0, 0.0], 10, None)
267//!             .edge_properties(),
268//!     )
269//!     .var_as(
270//!         "targets",
271//!         g().e(EdgeRef::var("edge_hits"))
272//!             .out_n()
273//!             .value_map(Some(vec!["$id", "title"])),
274//!     )
275//!     .returning(["edge_hits", "targets"]);
276//! ```
277//!
278//! `edge_hits` rows include `$from`, `$to`, and `$distance` (and `$id` when available),
279//! so you can inspect ranking metadata and still traverse from those edges.
280//!
281//! ### 6) Optional multitenancy
282//!
283//! ```
284//! # use helix_dsl::prelude::*;
285//! write_batch()
286//!     .var_as(
287//!         "create_mt_index",
288//!         g().create_vector_index_nodes(
289//!             "Doc",
290//!             "embedding",
291//!             Some("tenant_id"),
292//!         ),
293//!     )
294//!     .var_as(
295//!         "insert_acme",
296//!         g().add_n(
297//!             "Doc",
298//!             vec![
299//!                 ("tenant_id", PropertyValue::from("acme")),
300//!                 ("title", PropertyValue::from("Acme doc")),
301//!                 ("embedding", PropertyValue::from(vec![1.0f32, 0.0, 0.0])),
302//!             ],
303//!         ),
304//!     )
305//!     .returning(["create_mt_index", "insert_acme"]);
306//! ```
307//!
308//! ```
309//! # use helix_dsl::prelude::*;
310//! read_batch()
311//!     .var_as(
312//!         "acme_hits",
313//!         g().vector_search_nodes(
314//!             "Doc",
315//!             "embedding",
316//!             vec![1.0f32, 0.0, 0.0],
317//!             5,
318//!             Some(PropertyValue::from("acme")),
319//!         )
320//!         .value_map(Some(vec!["$id", "$distance", "title"])),
321//!     )
322//!     .returning(["acme_hits"]);
323//! ```
324//!
325//! Multitenant behavior in the current Helix interpreter:
326//! - multitenant index + missing `tenant_value` on search => query error
327//! - multitenant index + unknown tenant => empty result set
328//! - write with vector present but missing tenant property => write error
329//!
330//! ## Edge-First Reads
331//!
332//! ```
333//! # use helix_dsl::prelude::*;
334//! read_batch()
335//!     .var_as(
336//!         "heavy_edges",
337//!         g()
338//!             .e_where(SourcePredicate::gt("weight", 0.8f64))
339//!             .edge_has_label("FOLLOWS")
340//!             .order_by("weight", Order::Desc)
341//!             .limit(50),
342//!     )
343//!     .var_as(
344//!         "targets",
345//!         g()
346//!             .e(EdgeRef::var("heavy_edges"))
347//!             .out_n()
348//!             .dedup(),
349//!     )
350//!     .returning(["heavy_edges", "targets"]);
351//! ```
352//!
353//! ## Branching and Repetition
354//!
355//! ```
356//! # use helix_dsl::prelude::*;
357//! read_batch()
358//!     .var_as(
359//!         "recommendations",
360//!         g()
361//!             .n(1u64)
362//!             .store("seed")
363//!             .repeat(RepeatConfig::new(sub().out(Some("FOLLOWS"))).times(2))
364//!             .without("seed")
365//!             .union(vec![sub().out(Some("LIKES"))])
366//!             .dedup()
367//!             .limit(30),
368//!     )
369//!     .returning(["recommendations"]);
370//! ```
371//!
372//! ## Complete Function Coverage
373//!
374//! The examples below are a catalog-style reference showing every public query-builder
375//! function in a `read_batch()` / `write_batch()` flow.
376//!
377//! ### Sources, NodeRef, EdgeRef, and Vector Search
378//!
379//! ```
380//! # use helix_dsl::prelude::*;
381//! read_batch()
382//!     .var_as("n_id", g().n(NodeRef::id(1)))
383//!     .var_as("n_ids", g().n(NodeRef::ids([1u64, 2, 3])))
384//!     .var_as("n_var", g().n(NodeRef::var("n_ids")))
385//!     .var_as(
386//!         "n_where_all",
387//!         g().n_where(SourcePredicate::and(vec![
388//!             SourcePredicate::eq("kind", "user"),
389//!             SourcePredicate::neq("status", "deleted"),
390//!             SourcePredicate::gt("score", 10i64),
391//!             SourcePredicate::gte("score", 10i64),
392//!             SourcePredicate::lt("score", 100i64),
393//!             SourcePredicate::lte("score", 100i64),
394//!             SourcePredicate::between("age", 18i64, 65i64),
395//!             SourcePredicate::has_key("email"),
396//!             SourcePredicate::starts_with("name", "a"),
397//!             SourcePredicate::or(vec![
398//!                 SourcePredicate::eq("tier", "pro"),
399//!                 SourcePredicate::eq("tier", "team"),
400//!             ]),
401//!         ])),
402//!     )
403//!     .var_as("n_label", g().n_with_label("User"))
404//!     .var_as(
405//!         "n_label_where",
406//!         g().n_with_label_where("User", SourcePredicate::eq("active", true)),
407//!     )
408//!     .var_as("e_id", g().e(EdgeRef::id(10)))
409//!     .var_as("e_ids", g().e(EdgeRef::ids([10u64, 11, 12])))
410//!     .var_as("e_var", g().e(EdgeRef::var("e_ids")))
411//!     .var_as("e_where", g().e_where(SourcePredicate::gte("weight", 0.5f64)))
412//!     .var_as("e_label", g().e_with_label("FOLLOWS"))
413//!     .var_as(
414//!         "e_label_where",
415//!         g().e_with_label_where("FOLLOWS", SourcePredicate::lt("weight", 2.0f64)),
416//!     )
417//!     .var_as(
418//!         "vector_nodes",
419//!         g().vector_search_nodes("Doc", "embedding", vec![0.1f32; 4], 5, None),
420//!     )
421//!     .var_as(
422//!         "vector_edges",
423//!         g().vector_search_edges("SIMILAR", "embedding", vec![0.2f32; 4], 4, None),
424//!     )
425//!     .var_as(
426//!         "vector_nodes_tenant",
427//!         g().vector_search_nodes(
428//!             "Doc",
429//!             "embedding",
430//!             vec![0.1f32; 4],
431//!             5,
432//!             Some(PropertyValue::from("acme")),
433//!         ),
434//!     )
435//!     .var_as(
436//!         "vector_edges_tenant",
437//!         g().vector_search_edges(
438//!             "SIMILAR",
439//!             "embedding",
440//!             vec![0.2f32; 4],
441//!             4,
442//!             Some(PropertyValue::from("acme")),
443//!         ),
444//!     )
445//!     .returning(["n_id", "e_id", "vector_nodes"]);
446//! ```
447//!
448//! ### Node Traversal, Filters, Predicates, Expressions, and Projections
449//!
450//! ```
451//! # use helix_dsl::prelude::*;
452//! read_batch()
453//!     .var_as(
454//!         "filtered",
455//!         g()
456//!             .n(1u64)
457//!             .out(Some("FOLLOWS"))
458//!             .in_(Some("MENTIONS"))
459//!             .both(None::<&str>)
460//!             .has(
461//!                 "name",
462//!                 PropertyValue::from("alice").as_str().unwrap_or("alice"),
463//!             )
464//!             .has("visits", PropertyValue::from(42i64).as_i64().unwrap_or(0i64))
465//!             .has("ratio", PropertyValue::from(1.5f64).as_f64().unwrap_or(0.0f64))
466//!             .has("active", PropertyValue::from(true).as_bool().unwrap_or(false))
467//!             .has_label("User")
468//!             .has_key("email")
469//!             .where_(Predicate::and(vec![
470//!                 Predicate::eq("status", "active"),
471//!                 Predicate::neq("tier", "banned"),
472//!                 Predicate::gt("score", 10i64),
473//!                 Predicate::gte("score", 10i64),
474//!                 Predicate::lt("score", 100i64),
475//!                 Predicate::lte("score", 100i64),
476//!                 Predicate::between("age", 18i64, 65i64),
477//!                 Predicate::has_key("email"),
478//!                 Predicate::starts_with("name", "a"),
479//!                 Predicate::ends_with("email", ".com"),
480//!                 Predicate::contains("bio", "graph"),
481//!                 Predicate::not(Predicate::or(vec![
482//!                     Predicate::eq("role", "bot"),
483//!                     Predicate::eq("role", "system"),
484//!                 ])),
485//!                 Predicate::compare(
486//!                     Expr::prop("price")
487//!                         .mul(Expr::prop("qty"))
488//!                         .add(Expr::val(10i64))
489//!                         .sub(Expr::param("discount"))
490//!                         .div(Expr::val(2i64))
491//!                         .modulo(Expr::val(3i64))
492//!                         .add(Expr::id().neg()),
493//!                     CompareOp::Gt,
494//!                     Expr::val(100i64),
495//!                 ),
496//!                 Predicate::is_in(
497//!                     "status",
498//!                     vec!["active".to_string(), "pending".to_string()],
499//!                 ),
500//!                 Predicate::eq_param("region", "target_region"),
501//!                 Predicate::is_in_param("country", "allowed_countries"),
502//!                 Predicate::neq_param("status", "blocked_status"),
503//!                 Predicate::gt_param("score", "min_score"),
504//!                 Predicate::gte_param("score", "min_score_inclusive"),
505//!                 Predicate::lt_param("score", "max_score"),
506//!                 Predicate::lte_param("score", "max_score_inclusive"),
507//!             ]))
508//!             .as_("seed")
509//!             .store("seed_store")
510//!             .select("seed")
511//!             .inject("seed_store")
512//!             .within("seed_store")
513//!             .without("seed")
514//!             .dedup()
515//!             .order_by("score", Order::Desc)
516//!             .order_by_multiple(vec![("age", Order::Asc), ("score", Order::Desc)])
517//!             .limit(100)
518//!             .skip(5)
519//!             .range(0, 20),
520//!     )
521//!     .var_as("counted", g().n(NodeRef::var("filtered")).count())
522//!     .var_as("exists", g().n(NodeRef::var("filtered")).exists())
523//!     .var_as("ids", g().n(NodeRef::var("filtered")).id())
524//!     .var_as("labels", g().n(NodeRef::var("filtered")).label())
525//!     .var_as("values", g().n(NodeRef::var("filtered")).values(vec!["name", "email"]))
526//!     .var_as(
527//!         "value_map_some",
528//!         g().n(NodeRef::var("filtered"))
529//!             .value_map(Some(vec!["$id", "name", "email"])),
530//!     )
531//!     .var_as(
532//!         "value_map_all",
533//!         g().n(NodeRef::var("filtered")).value_map(None::<Vec<&str>>),
534//!     )
535//!     .var_as(
536//!         "projected",
537//!         g().n(NodeRef::var("filtered")).project(vec![
538//!             PropertyProjection::new("name"),
539//!             PropertyProjection::renamed("email", "contact"),
540//!         ]),
541//!     )
542//!     .returning(["filtered", "projected"]);
543//! ```
544//!
545//! ### Edge Traversal and Edge Terminals
546//!
547//! ```
548//! # use helix_dsl::prelude::*;
549//! read_batch()
550//!     .var_as(
551//!         "edge_ops",
552//!         g()
553//!             .e_where(SourcePredicate::gt("weight", 0.1f64))
554//!             .edge_has("weight", 1i64)
555//!             .edge_has_label("FOLLOWS")
556//!             .as_("edges_a")
557//!             .store("edges_b")
558//!             .dedup()
559//!             .order_by("weight", Order::Desc)
560//!             .limit(50)
561//!             .skip(2)
562//!             .range(0, 20),
563//!     )
564//!     .var_as("to_out_n", g().e(EdgeRef::var("edge_ops")).out_n())
565//!     .var_as("to_in_n", g().e(EdgeRef::var("edge_ops")).in_n())
566//!     .var_as("to_other_n", g().e(EdgeRef::var("edge_ops")).other_n())
567//!     .var_as("edge_count", g().e(EdgeRef::var("edge_ops")).count())
568//!     .var_as("edge_exists", g().e(EdgeRef::var("edge_ops")).exists())
569//!     .var_as("edge_ids", g().e(EdgeRef::var("edge_ops")).id())
570//!     .var_as("edge_labels", g().e(EdgeRef::var("edge_ops")).label())
571//!     .var_as("edge_props", g().e(EdgeRef::var("edge_ops")).edge_properties())
572//!     .returning(["edge_ops", "edge_props"]);
573//! ```
574//!
575//! ### Branching, Sub-Traversals, Repeat, Grouping, Paths, and Sack
576//!
577//! ```
578//! # use helix_dsl::prelude::*;
579//! read_batch()
580//!     .var_as(
581//!         "advanced",
582//!         g()
583//!             .n(1u64)
584//!             .out_e(Some("FOLLOWS"))
585//!             .in_n()
586//!             .in_e(Some("MENTIONS"))
587//!             .out_n()
588//!             .both_e(None::<&str>)
589//!             .other_n()
590//!             .repeat(
591//!                 RepeatConfig::new(sub().out(Some("FOLLOWS")))
592//!                     .times(2)
593//!                     .until(Predicate::has_key("stop"))
594//!                     .emit_all()
595//!                     .emit_before()
596//!                     .emit_after()
597//!                     .emit_if(Predicate::gt("score", 0i64))
598//!                     .max_depth(8),
599//!             )
600//!             .union(vec![
601//!                 sub().out(Some("LIKES")),
602//!                 SubTraversal::new()
603//!                     .out(Some("FOLLOWS"))
604//!                     .in_(Some("MENTIONS"))
605//!                     .both(None::<&str>)
606//!                     .out_e(Some("REL"))
607//!                     .in_e(Some("REL"))
608//!                     .both_e(None::<&str>)
609//!                     .out_n()
610//!                     .in_n()
611//!                     .other_n()
612//!                     .has("active", true)
613//!                     .has_label("User")
614//!                     .has_key("email")
615//!                     .where_(Predicate::eq("state", "ok"))
616//!                     .dedup()
617//!                     .within("allow")
618//!                     .without("deny")
619//!                     .edge_has("weight", 1i64)
620//!                     .edge_has_label("REL")
621//!                     .limit(10)
622//!                     .skip(1)
623//!                     .range(0, 5)
624//!                     .as_("s1")
625//!                     .store("s2")
626//!                     .select("s1")
627//!                     .order_by("score", Order::Desc)
628//!                     .order_by_multiple(vec![("age", Order::Asc)])
629//!                     .path()
630//!                     .simple_path(),
631//!             ])
632//!             .choose(
633//!                 Predicate::eq("vip", true),
634//!                 sub().out(Some("PREMIUM")),
635//!                 Some(sub().out(Some("STANDARD"))),
636//!             )
637//!             .coalesce(vec![sub().out(Some("POSTED")), sub().out(Some("COMMENTED"))])
638//!             .optional(sub().out(Some("MENTIONED")))
639//!             .fold()
640//!             .unfold()
641//!             .path()
642//!             .simple_path()
643//!             .with_sack(PropertyValue::I64(0))
644//!             .sack_set("weight")
645//!             .sack_add("weight")
646//!             .sack_get()
647//!             .dedup(),
648//!     )
649//!     .var_as("grouped", g().n_with_label("User").group("team"))
650//!     .var_as("grouped_count", g().n_with_label("User").group_count("team"))
651//!     .var_as(
652//!         "aggregate_count",
653//!         g().n_with_label("User")
654//!             .aggregate_by(AggregateFunction::Count, "score"),
655//!     )
656//!     .var_as(
657//!         "aggregate_sum",
658//!         g().n_with_label("User").aggregate_by(AggregateFunction::Sum, "score"),
659//!     )
660//!     .var_as(
661//!         "aggregate_min",
662//!         g().n_with_label("User").aggregate_by(AggregateFunction::Min, "score"),
663//!     )
664//!     .var_as(
665//!         "aggregate_max",
666//!         g().n_with_label("User").aggregate_by(AggregateFunction::Max, "score"),
667//!     )
668//!     .var_as(
669//!         "aggregate_mean",
670//!         g().n_with_label("User")
671//!             .aggregate_by(AggregateFunction::Mean, "score"),
672//!     )
673//!     .returning(["advanced", "grouped", "grouped_count", "aggregate_count"]);
674//! ```
675//!
676//! ### Read-Batch Conditions
677//!
678//! ```
679//! # use helix_dsl::prelude::*;
680//! read_batch()
681//!     .var_as("base", g().n_with_label("User"))
682//!     .var_as_if(
683//!         "if_not_empty",
684//!         BatchCondition::VarNotEmpty("base".to_string()),
685//!         g().n(NodeRef::var("base")).limit(10),
686//!     )
687//!     .var_as_if(
688//!         "if_empty",
689//!         BatchCondition::VarEmpty("base".to_string()),
690//!         g().n_with_label("FallbackUser"),
691//!     )
692//!     .var_as_if(
693//!         "if_min_size",
694//!         BatchCondition::VarMinSize("base".to_string(), 5),
695//!         g().n(NodeRef::var("base")).order_by("score", Order::Desc),
696//!     )
697//!     .var_as_if(
698//!         "if_prev_not_empty",
699//!         BatchCondition::PrevNotEmpty,
700//!         g().n(NodeRef::var("base")).count(),
701//!     )
702//!     .returning(["base", "if_not_empty", "if_empty", "if_min_size", "if_prev_not_empty"]);
703//! ```
704//!
705//! ### Write Sources, Mutations, and Vector Index Configuration
706//!
707//! ```
708//! # use helix_dsl::prelude::*;
709//! write_batch()
710//!     .var_as("created_user", g().add_n("User", vec![("name", "Alice")]))
711//!     .var_as(
712//!         "created_team",
713//!         g().n(NodeRef::var("created_user"))
714//!             .add_n("Team", vec![("name", "Graph")]),
715//!     )
716//!     .var_as(
717//!         "connected",
718//!         g().n(NodeRef::var("created_user")).add_e(
719//!             "MEMBER_OF",
720//!             NodeRef::var("created_team"),
721//!             vec![("since", "2026-01-01")],
722//!         ),
723//!     )
724//!     .var_as(
725//!         "updated",
726//!         g().n(NodeRef::var("created_user"))
727//!             .set_property("active", true)
728//!             .remove_property("old_field"),
729//!     )
730//!     .var_as(
731//!         "drop_some_edges",
732//!         g().n(NodeRef::var("created_user"))
733//!             .drop_edge(NodeRef::ids([2u64, 3]))
734//!             .drop_edge_by_id(EdgeRef::ids([40u64, 41])),
735//!     )
736//!     .var_as("drop_nodes", g().n(NodeRef::var("created_team")).drop())
737//!     .var_as("inject_from_empty", g().inject("created_user").has_label("User"))
738//!     .var_as("drop_edge_by_id_from_empty", g().drop_edge_by_id([90u64, 91]))
739//!     .var_as(
740//!         "create_vector_index_nodes",
741//!         g().create_vector_index_nodes(
742//!             "Doc",
743//!             "embedding",
744//!             Some("tenant_id"),
745//!         ),
746//!     )
747//!     .var_as(
748//!         "create_vector_index_edges",
749//!         g().create_vector_index_edges(
750//!             "SIMILAR",
751//!             "embedding",
752//!             None::<&str>,
753//!         ),
754//!     )
755//!     .var_as(
756//!         "create_vector_index_edges_alt",
757//!         g().create_vector_index_edges(
758//!             "RELATED",
759//!             "embedding",
760//!             None::<&str>,
761//!         ),
762//!     )
763//!     .var_as_if(
764//!         "write_if_not_empty",
765//!         BatchCondition::VarNotEmpty("created_user".to_string()),
766//!         g().n(NodeRef::var("created_user")).set_property("verified", true),
767//!     )
768//!     .returning([
769//!         "created_user",
770//!         "created_team",
771//!         "connected",
772//!         "updated",
773//!         "drop_some_edges",
774//!         "drop_nodes",
775//!         "inject_from_empty",
776//!         "drop_edge_by_id_from_empty",
777//!         "create_vector_index_nodes",
778//!         "create_vector_index_edges",
779//!         "create_vector_index_edges_alt",
780//!         "write_if_not_empty",
781//!     ]);
782//! ```
783//!
784//! ## Traversal Building Inside `var_as(...)`
785//!
786//! Common source steps:
787//! - `n(...)`, `n_where(...)`, `n_with_label(...)`
788//! - `e(...)`, `e_where(...)`, `e_with_label(...)`
789//! - `vector_search_nodes(...)`, `vector_search_edges(...)`
790//!   - current Helix runtime exposes vector hit metadata via virtual fields
791//!     (`$id`, `$distance`, `$from`, `$to`) in terminal projections
792//!
793//! Common navigation and filtering:
794//! - `out/in_/both`, `out_e/in_e/both_e`, `out_n/in_n/other_n`
795//! - `has`, `has_label`, `has_key`, `where_`, `within`, `without`, `dedup`
796//! - `limit`, `skip`, `range`, `order_by`, `order_by_multiple`
797//!
798//! Common terminal projections:
799//! - `count`, `exists`, `id`, `label`
800//! - `values`, `value_map`, `project`, `edge_properties`
801//!
802//! Write-only operations (usable in [`write_batch()`] traversals):
803//! - `add_n`, `add_e`, `set_property`, `remove_property`, `drop`, `drop_edge`, `drop_edge_by_id`
804//! - `create_vector_index_nodes`, `create_vector_index_edges`
805
806#![warn(missing_docs)]
807#![warn(clippy::all)]
808#![deny(unsafe_code)]
809
810use std::collections::{BTreeMap, HashMap};
811use std::marker::PhantomData;
812
813use chrono::{SecondsFormat, Utc};
814use serde::{Deserialize, Serialize};
815mod query_generator;
816
817pub use helix_dsl_macros::register;
818pub use query_generator::*;
819
820#[doc(hidden)]
821pub mod __private {
822    use std::collections::BTreeMap;
823
824    pub use inventory;
825
826    pub fn dynamic_query_value_from_property_value(
827        value: crate::PropertyValue,
828        path: impl Into<String>,
829    ) -> Result<crate::DynamicQueryValue, crate::DynamicQueryError> {
830        fn convert(
831            value: crate::PropertyValue,
832            path: String,
833        ) -> Result<crate::DynamicQueryValue, crate::DynamicQueryError> {
834            Ok(match value {
835                crate::PropertyValue::Null => crate::DynamicQueryValue::Null,
836                crate::PropertyValue::Bool(value) => crate::DynamicQueryValue::Bool(value),
837                crate::PropertyValue::I64(value) => crate::DynamicQueryValue::I64(value),
838                crate::PropertyValue::DateTime(value) => crate::DynamicQueryValue::String(
839                    crate::DateTime::from_millis(value)
840                        .to_rfc3339()
841                        .ok_or_else(|| crate::DynamicQueryError::invalid_datetime(path, value))?,
842                ),
843                crate::PropertyValue::F64(value) => crate::DynamicQueryValue::F64(value),
844                crate::PropertyValue::F32(value) => crate::DynamicQueryValue::F32(value),
845                crate::PropertyValue::String(value) => crate::DynamicQueryValue::String(value),
846                crate::PropertyValue::Bytes(_) => {
847                    return Err(crate::DynamicQueryError::unsupported_bytes(path));
848                }
849                crate::PropertyValue::I64Array(values) => crate::DynamicQueryValue::Array(
850                    values
851                        .into_iter()
852                        .map(crate::DynamicQueryValue::I64)
853                        .collect(),
854                ),
855                crate::PropertyValue::F64Array(values) => crate::DynamicQueryValue::Array(
856                    values
857                        .into_iter()
858                        .map(crate::DynamicQueryValue::F64)
859                        .collect(),
860                ),
861                crate::PropertyValue::F32Array(values) => crate::DynamicQueryValue::Array(
862                    values
863                        .into_iter()
864                        .map(crate::DynamicQueryValue::F32)
865                        .collect(),
866                ),
867                crate::PropertyValue::StringArray(values) => crate::DynamicQueryValue::Array(
868                    values
869                        .into_iter()
870                        .map(crate::DynamicQueryValue::String)
871                        .collect(),
872                ),
873                crate::PropertyValue::Array(values) => crate::DynamicQueryValue::Array(
874                    values
875                        .into_iter()
876                        .enumerate()
877                        .map(|(index, value)| convert(value, format!("{}[{}]", path, index)))
878                        .collect::<Result<Vec<_>, _>>()?,
879                ),
880                crate::PropertyValue::Object(values) => crate::DynamicQueryValue::Object(
881                    values
882                        .into_iter()
883                        .map(|(key, value)| {
884                            let entry_path = format!("{}.{}", path, key);
885                            Ok((key, convert(value, entry_path)?))
886                        })
887                        .collect::<Result<BTreeMap<_, _>, crate::DynamicQueryError>>()?,
888                ),
889            })
890        }
891
892        convert(value, path.into())
893    }
894}
895
896/// Type alias for node IDs
897pub type NodeId = u64;
898
899/// Type alias for edge IDs (separate namespace from node IDs)
900pub type EdgeId = u64;
901
902/// Arbitrary nested parameter value.
903pub type ParamValue = PropertyValue;
904
905/// Object-shaped parameter payload.
906pub type ParamObject = BTreeMap<String, PropertyValue>;
907
908// Typestate Markers
909
910/// Marker trait for all traversal states
911#[doc(hidden)]
912pub trait TraversalState: private::Sealed {}
913
914mod private {
915    /// Seal the TraversalState trait to prevent external implementation
916    pub trait Sealed {}
917    impl Sealed for super::Empty {}
918    impl Sealed for super::OnNodes {}
919    impl Sealed for super::OnEdges {}
920    impl Sealed for super::Terminal {}
921    impl Sealed for super::ReadOnly {}
922    impl Sealed for super::WriteEnabled {}
923}
924
925/// Initial state - no source step yet
926#[doc(hidden)]
927#[derive(Debug, Clone, Copy, PartialEq, Eq)]
928pub struct Empty;
929
930/// Traversal is currently operating on a node stream
931#[doc(hidden)]
932#[derive(Debug, Clone, Copy, PartialEq, Eq)]
933pub struct OnNodes;
934
935/// Traversal is currently operating on an edge stream
936#[doc(hidden)]
937#[derive(Debug, Clone, Copy, PartialEq, Eq)]
938pub struct OnEdges;
939
940/// Traversal has terminated - no more chaining allowed
941#[doc(hidden)]
942#[derive(Debug, Clone, Copy, PartialEq, Eq)]
943pub struct Terminal;
944
945impl TraversalState for Empty {}
946impl TraversalState for OnNodes {}
947impl TraversalState for OnEdges {}
948impl TraversalState for Terminal {}
949
950// MutationMode Markers
951
952/// Marker trait for mutation capability - tracks whether a traversal contains mutations
953#[doc(hidden)]
954pub trait MutationMode: private::Sealed {}
955
956/// Read-only traversal - no mutation steps
957#[doc(hidden)]
958#[derive(Debug, Clone, Copy, PartialEq, Eq)]
959pub struct ReadOnly;
960
961/// Write-enabled traversal - contains mutation steps
962#[doc(hidden)]
963#[derive(Debug, Clone, Copy, PartialEq, Eq)]
964pub struct WriteEnabled;
965
966impl MutationMode for ReadOnly {}
967impl MutationMode for WriteEnabled {}
968
969// Property Value Types
970
971/// A property value that can be stored on nodes or edges
972#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
973pub enum PropertyValue {
974    /// Null value
975    Null,
976    /// Boolean value
977    Bool(bool),
978    /// 64-bit signed integer
979    I64(i64),
980    /// UTC datetime stored as epoch milliseconds
981    DateTime(i64),
982    /// 64-bit floating point
983    F64(f64),
984    /// 32-bit floating point
985    F32(f32),
986    /// UTF-8 string
987    String(String),
988    /// Raw bytes
989    Bytes(Vec<u8>),
990    /// Array of i64 values
991    I64Array(Vec<i64>),
992    /// Array of f64 values
993    F64Array(Vec<f64>),
994    /// Array of f32 values
995    F32Array(Vec<f32>),
996    /// Array of strings
997    StringArray(Vec<String>),
998    /// Heterogeneous array value for parameter payloads
999    Array(Vec<PropertyValue>),
1000    /// Object/map value for parameter payloads
1001    Object(BTreeMap<String, PropertyValue>),
1002}
1003
1004impl PropertyValue {
1005    /// Get value as string reference if it is a String
1006    pub fn as_str(&self) -> Option<&str> {
1007        match self {
1008            PropertyValue::String(s) => Some(s),
1009            _ => None,
1010        }
1011    }
1012
1013    /// Get value as i64 if it is an I64
1014    pub fn as_i64(&self) -> Option<i64> {
1015        match self {
1016            PropertyValue::I64(n) => Some(*n),
1017            _ => None,
1018        }
1019    }
1020
1021    /// Create a typed datetime value from UTC epoch milliseconds
1022    pub fn datetime_millis(millis: i64) -> Self {
1023        Self::DateTime(millis)
1024    }
1025
1026    /// Get the datetime as UTC epoch milliseconds if it is a DateTime
1027    pub fn as_datetime_millis(&self) -> Option<i64> {
1028        match self {
1029            PropertyValue::DateTime(millis) => Some(*millis),
1030            _ => None,
1031        }
1032    }
1033
1034    /// Get value as f64 if it is an F64
1035    pub fn as_f64(&self) -> Option<f64> {
1036        match self {
1037            PropertyValue::F64(n) => Some(*n),
1038            PropertyValue::F32(n) => Some(*n as f64),
1039            _ => None,
1040        }
1041    }
1042
1043    /// Get value as bool if it is a Bool
1044    pub fn as_bool(&self) -> Option<bool> {
1045        match self {
1046            PropertyValue::Bool(b) => Some(*b),
1047            _ => None,
1048        }
1049    }
1050
1051    /// Get value as array reference if it is an Array
1052    pub fn as_array(&self) -> Option<&[PropertyValue]> {
1053        match self {
1054            PropertyValue::Array(values) => Some(values),
1055            _ => None,
1056        }
1057    }
1058
1059    /// Get value as object reference if it is an Object
1060    pub fn as_object(&self) -> Option<&BTreeMap<String, PropertyValue>> {
1061        match self {
1062            PropertyValue::Object(values) => Some(values),
1063            _ => None,
1064        }
1065    }
1066}
1067
1068impl From<&str> for PropertyValue {
1069    fn from(s: &str) -> Self {
1070        PropertyValue::String(s.to_string())
1071    }
1072}
1073
1074impl From<String> for PropertyValue {
1075    fn from(s: String) -> Self {
1076        PropertyValue::String(s)
1077    }
1078}
1079
1080impl From<i64> for PropertyValue {
1081    fn from(n: i64) -> Self {
1082        PropertyValue::I64(n)
1083    }
1084}
1085
1086/// UTC datetime represented internally as epoch milliseconds.
1087#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1088pub struct DateTime(i64);
1089
1090impl DateTime {
1091    /// Create a datetime from UTC epoch milliseconds.
1092    pub fn from_millis(millis: i64) -> Self {
1093        Self(millis)
1094    }
1095
1096    /// Parse an RFC3339 datetime string and normalize it to UTC.
1097    pub fn parse_rfc3339(input: &str) -> Result<Self, chrono::ParseError> {
1098        Ok(Self(
1099            chrono::DateTime::parse_from_rfc3339(input)?
1100                .with_timezone(&Utc)
1101                .timestamp_millis(),
1102        ))
1103    }
1104
1105    /// Return the UTC epoch milliseconds.
1106    pub fn millis(self) -> i64 {
1107        self.0
1108    }
1109
1110    /// Format this datetime as a canonical RFC3339 UTC string.
1111    pub fn to_rfc3339(self) -> Option<String> {
1112        chrono::DateTime::<Utc>::from_timestamp_millis(self.0)
1113            .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true))
1114    }
1115}
1116
1117impl From<DateTime> for PropertyValue {
1118    fn from(value: DateTime) -> Self {
1119        PropertyValue::DateTime(value.millis())
1120    }
1121}
1122
1123impl From<i32> for PropertyValue {
1124    fn from(n: i32) -> Self {
1125        PropertyValue::I64(n as i64)
1126    }
1127}
1128
1129impl From<f64> for PropertyValue {
1130    fn from(n: f64) -> Self {
1131        PropertyValue::F64(n)
1132    }
1133}
1134
1135impl From<f32> for PropertyValue {
1136    fn from(n: f32) -> Self {
1137        PropertyValue::F32(n)
1138    }
1139}
1140
1141impl From<bool> for PropertyValue {
1142    fn from(b: bool) -> Self {
1143        PropertyValue::Bool(b)
1144    }
1145}
1146
1147impl From<Vec<u8>> for PropertyValue {
1148    fn from(bytes: Vec<u8>) -> Self {
1149        PropertyValue::Bytes(bytes)
1150    }
1151}
1152
1153impl From<Vec<i64>> for PropertyValue {
1154    fn from(values: Vec<i64>) -> Self {
1155        PropertyValue::I64Array(values)
1156    }
1157}
1158
1159impl From<Vec<f64>> for PropertyValue {
1160    fn from(values: Vec<f64>) -> Self {
1161        PropertyValue::F64Array(values)
1162    }
1163}
1164
1165impl From<Vec<f32>> for PropertyValue {
1166    fn from(values: Vec<f32>) -> Self {
1167        PropertyValue::F32Array(values)
1168    }
1169}
1170
1171impl From<Vec<String>> for PropertyValue {
1172    fn from(values: Vec<String>) -> Self {
1173        PropertyValue::StringArray(values)
1174    }
1175}
1176
1177impl From<Vec<PropertyValue>> for PropertyValue {
1178    fn from(values: Vec<PropertyValue>) -> Self {
1179        PropertyValue::Array(values)
1180    }
1181}
1182
1183impl From<BTreeMap<String, PropertyValue>> for PropertyValue {
1184    fn from(values: BTreeMap<String, PropertyValue>) -> Self {
1185        PropertyValue::Object(values)
1186    }
1187}
1188
1189impl From<HashMap<String, PropertyValue>> for PropertyValue {
1190    fn from(values: HashMap<String, PropertyValue>) -> Self {
1191        PropertyValue::Object(values.into_iter().collect())
1192    }
1193}
1194
1195/// Mutation input value for add/set property operations.
1196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1197pub enum PropertyInput {
1198    /// Store a literal property value.
1199    Value(PropertyValue),
1200    /// Resolve the value from an expression at execution time.
1201    Expr(Expr),
1202}
1203
1204impl PropertyInput {
1205    /// Create an input from a query parameter.
1206    pub fn param(name: impl Into<String>) -> Self {
1207        Self::Expr(Expr::param(name))
1208    }
1209}
1210
1211impl<T> From<T> for PropertyInput
1212where
1213    PropertyValue: From<T>,
1214{
1215    fn from(value: T) -> Self {
1216        Self::Value(value.into())
1217    }
1218}
1219
1220impl From<Expr> for PropertyInput {
1221    fn from(value: Expr) -> Self {
1222        Self::Expr(value)
1223    }
1224}
1225
1226// Reference Types
1227
1228/// A reference to nodes - can be concrete IDs or a variable name
1229///
1230/// This allows the AST to express operations without knowing actual IDs at build time.
1231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1232pub enum NodeRef {
1233    /// All nodes in storage
1234    All,
1235    /// One or more concrete node IDs
1236    Ids(Vec<NodeId>),
1237    /// Reference nodes stored in a named variable
1238    Var(String),
1239    /// Reference node IDs from a runtime parameter
1240    Param(String),
1241}
1242
1243impl NodeRef {
1244    /// Create a reference to all nodes
1245    pub fn all() -> Self {
1246        NodeRef::All
1247    }
1248
1249    /// Create a reference to a single node ID
1250    pub fn id(id: NodeId) -> Self {
1251        NodeRef::Ids(vec![id])
1252    }
1253
1254    /// Create a reference to multiple node IDs
1255    pub fn ids(ids: impl IntoIterator<Item = NodeId>) -> Self {
1256        NodeRef::Ids(ids.into_iter().collect())
1257    }
1258
1259    /// Create a reference to a variable
1260    pub fn var(name: impl Into<String>) -> Self {
1261        NodeRef::Var(name.into())
1262    }
1263
1264    /// Create a reference to node IDs stored in a runtime parameter
1265    pub fn param(name: impl Into<String>) -> Self {
1266        NodeRef::Param(name.into())
1267    }
1268}
1269
1270impl From<NodeId> for NodeRef {
1271    fn from(id: NodeId) -> Self {
1272        NodeRef::Ids(vec![id])
1273    }
1274}
1275
1276impl From<Vec<NodeId>> for NodeRef {
1277    fn from(ids: Vec<NodeId>) -> Self {
1278        NodeRef::Ids(ids)
1279    }
1280}
1281
1282impl<const N: usize> From<[NodeId; N]> for NodeRef {
1283    fn from(ids: [NodeId; N]) -> Self {
1284        NodeRef::Ids(ids.to_vec())
1285    }
1286}
1287
1288impl From<&str> for NodeRef {
1289    fn from(var_name: &str) -> Self {
1290        NodeRef::Var(var_name.to_string())
1291    }
1292}
1293
1294/// A reference to edges - can be concrete IDs or a variable name
1295///
1296/// This allows the AST to express operations without knowing actual IDs at build time.
1297/// Edge IDs are separate from node IDs in the graph.
1298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1299pub enum EdgeRef {
1300    /// One or more concrete edge IDs
1301    Ids(Vec<EdgeId>),
1302    /// Reference edges stored in a named variable
1303    Var(String),
1304    /// Reference edge IDs from a runtime parameter
1305    Param(String),
1306}
1307
1308impl EdgeRef {
1309    /// Create a reference to a single edge ID
1310    pub fn id(id: EdgeId) -> Self {
1311        EdgeRef::Ids(vec![id])
1312    }
1313
1314    /// Create a reference to multiple edge IDs
1315    pub fn ids(ids: impl IntoIterator<Item = EdgeId>) -> Self {
1316        EdgeRef::Ids(ids.into_iter().collect())
1317    }
1318
1319    /// Create a reference to a variable containing edges
1320    pub fn var(name: impl Into<String>) -> Self {
1321        EdgeRef::Var(name.into())
1322    }
1323
1324    /// Create a reference to edge IDs stored in a runtime parameter
1325    pub fn param(name: impl Into<String>) -> Self {
1326        EdgeRef::Param(name.into())
1327    }
1328}
1329
1330impl From<EdgeId> for EdgeRef {
1331    fn from(id: EdgeId) -> Self {
1332        EdgeRef::Ids(vec![id])
1333    }
1334}
1335
1336impl From<Vec<EdgeId>> for EdgeRef {
1337    fn from(ids: Vec<EdgeId>) -> Self {
1338        EdgeRef::Ids(ids)
1339    }
1340}
1341
1342impl<const N: usize> From<[EdgeId; N]> for EdgeRef {
1343    fn from(ids: [EdgeId; N]) -> Self {
1344        EdgeRef::Ids(ids.to_vec())
1345    }
1346}
1347
1348// Expression Types
1349
1350/// An expression for computed values, math operations, and property references
1351///
1352/// Expressions can be used in predicates for property-to-property comparisons,
1353/// computed values, and math operations.
1354///
1355/// Note: support for some expression variants is engine-dependent. In particular,
1356/// `Expr::Id` may be reserved or unsupported by some runtimes.
1357///
1358#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1359pub enum Expr {
1360    /// Reference a property by name
1361    Property(String),
1362    /// The current element ID (engine-defined).
1363    Id,
1364    /// Server-side current timestamp in UTC epoch milliseconds.
1365    Timestamp,
1366    /// Server-side current datetime as a typed `DateTime` value.
1367    DateTimeNow,
1368    /// A constant value
1369    Constant(PropertyValue),
1370    /// Reference a query parameter by name
1371    Param(String),
1372    /// Addition: left + right
1373    Add(Box<Expr>, Box<Expr>),
1374    /// Subtraction: left - right
1375    Sub(Box<Expr>, Box<Expr>),
1376    /// Multiplication: left * right
1377    Mul(Box<Expr>, Box<Expr>),
1378    /// Division: left / right
1379    Div(Box<Expr>, Box<Expr>),
1380    /// Modulo: left % right
1381    Mod(Box<Expr>, Box<Expr>),
1382    /// Negation: -expr
1383    Neg(Box<Expr>),
1384    /// Conditional expression that evaluates the first matching branch.
1385    Case {
1386        /// Ordered predicate/expression branches.
1387        when_then: Vec<(Predicate, Expr)>,
1388        /// Fallback expression. When omitted, the result is explicit `Null`.
1389        else_expr: Option<Box<Expr>>,
1390    },
1391}
1392
1393impl Expr {
1394    /// Create a property reference expression
1395    pub fn prop(name: impl Into<String>) -> Self {
1396        Expr::Property(name.into())
1397    }
1398
1399    /// Create a constant value expression
1400    pub fn val(value: impl Into<PropertyValue>) -> Self {
1401        Expr::Constant(value.into())
1402    }
1403
1404    /// Create an ID reference expression
1405    pub fn id() -> Self {
1406        Expr::Id
1407    }
1408
1409    /// Create a server-side timestamp expression (UTC epoch milliseconds).
1410    pub fn timestamp() -> Self {
1411        Expr::Timestamp
1412    }
1413
1414    /// Create a server-side datetime expression.
1415    pub fn datetime() -> Self {
1416        Expr::DateTimeNow
1417    }
1418
1419    /// Create a parameter reference expression
1420    pub fn param(name: impl Into<String>) -> Self {
1421        Expr::Param(name.into())
1422    }
1423
1424    /// Addition: self + other
1425    pub fn add(self, other: Expr) -> Self {
1426        Expr::Add(Box::new(self), Box::new(other))
1427    }
1428
1429    /// Subtraction: self - other
1430    pub fn sub(self, other: Expr) -> Self {
1431        Expr::Sub(Box::new(self), Box::new(other))
1432    }
1433
1434    /// Multiplication: self * other
1435    pub fn mul(self, other: Expr) -> Self {
1436        Expr::Mul(Box::new(self), Box::new(other))
1437    }
1438
1439    /// Division: self / other
1440    pub fn div(self, other: Expr) -> Self {
1441        Expr::Div(Box::new(self), Box::new(other))
1442    }
1443
1444    /// Modulo: self % other
1445    pub fn modulo(self, other: Expr) -> Self {
1446        Expr::Mod(Box::new(self), Box::new(other))
1447    }
1448
1449    /// Negation: -self
1450    pub fn neg(self) -> Self {
1451        Expr::Neg(Box::new(self))
1452    }
1453
1454    /// Create a conditional expression.
1455    pub fn case(when_then: Vec<(Predicate, Expr)>, else_expr: Option<Expr>) -> Self {
1456        Expr::Case {
1457            when_then,
1458            else_expr: else_expr.map(Box::new),
1459        }
1460    }
1461}
1462
1463/// A non-negative integer input used by stream-shaping steps like `limit`, `skip`, and `range`.
1464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1465pub enum StreamBound {
1466    /// A literal bound known at query-build time.
1467    Literal(usize),
1468    /// A computed or parameterized bound resolved by the runtime.
1469    Expr(Expr),
1470}
1471
1472impl StreamBound {
1473    /// Create a literal bound.
1474    pub fn literal(value: usize) -> Self {
1475        Self::Literal(value)
1476    }
1477
1478    /// Create an expression-backed bound.
1479    pub fn expr(expr: Expr) -> Self {
1480        Self::Expr(expr)
1481    }
1482}
1483
1484impl From<usize> for StreamBound {
1485    fn from(value: usize) -> Self {
1486        Self::Literal(value)
1487    }
1488}
1489
1490impl From<u32> for StreamBound {
1491    fn from(value: u32) -> Self {
1492        Self::Literal(value as usize)
1493    }
1494}
1495
1496impl From<u16> for StreamBound {
1497    fn from(value: u16) -> Self {
1498        Self::Literal(value as usize)
1499    }
1500}
1501
1502impl From<u8> for StreamBound {
1503    fn from(value: u8) -> Self {
1504        Self::Literal(value as usize)
1505    }
1506}
1507
1508impl From<i64> for StreamBound {
1509    fn from(value: i64) -> Self {
1510        if value >= 0 {
1511            Self::Literal(value as usize)
1512        } else {
1513            Self::Expr(Expr::val(value))
1514        }
1515    }
1516}
1517
1518impl From<i32> for StreamBound {
1519    fn from(value: i32) -> Self {
1520        if value >= 0 {
1521            Self::Literal(value as usize)
1522        } else {
1523            Self::Expr(Expr::val(value))
1524        }
1525    }
1526}
1527
1528impl From<Expr> for StreamBound {
1529    fn from(value: Expr) -> Self {
1530        Self::Expr(value)
1531    }
1532}
1533
1534/// Comparison operators for expression-based predicates
1535#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1536pub enum CompareOp {
1537    /// Equal
1538    Eq,
1539    /// Not equal
1540    Neq,
1541    /// Greater than
1542    Gt,
1543    /// Greater than or equal
1544    Gte,
1545    /// Less than
1546    Lt,
1547    /// Less than or equal
1548    Lte,
1549}
1550
1551// Predicate Types
1552
1553/// A predicate for filtering nodes by properties
1554#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1555pub enum Predicate {
1556    /// Equals: property == value
1557    Eq(String, PropertyValue),
1558    /// Not equals: property != value
1559    Neq(String, PropertyValue),
1560    /// Greater than: property > value (for numeric/string)
1561    Gt(String, PropertyValue),
1562    /// Greater than or equal: property >= value
1563    Gte(String, PropertyValue),
1564    /// Less than: property < value
1565    Lt(String, PropertyValue),
1566    /// Less than or equal: property <= value
1567    Lte(String, PropertyValue),
1568    /// Between (inclusive): min <= property <= max
1569    Between(String, PropertyValue, PropertyValue),
1570    /// Property exists
1571    HasKey(String),
1572    /// Property is missing or explicitly null.
1573    IsNull(String),
1574    /// Property exists and is not null.
1575    IsNotNull(String),
1576    /// String starts with prefix
1577    StartsWith(String, String),
1578    /// String ends with suffix
1579    EndsWith(String, String),
1580    /// String contains substring
1581    Contains(String, String),
1582    /// String contains a runtime expression result
1583    ContainsExpr(String, Expr),
1584    /// Property value is equal to one of the provided values
1585    IsIn(String, PropertyValue),
1586    /// Property value is equal to one of the values produced by a runtime expression
1587    IsInExpr(String, Expr),
1588    /// Logical AND of predicates
1589    And(Vec<Predicate>),
1590    /// Logical OR of predicates
1591    Or(Vec<Predicate>),
1592    /// Logical NOT of predicate
1593    Not(Box<Predicate>),
1594    /// Expression-based comparison (supports property-to-property, math, etc.)
1595    Compare {
1596        /// Left side of comparison
1597        left: Expr,
1598        /// Comparison operator
1599        op: CompareOp,
1600        /// Right side of comparison
1601        right: Expr,
1602    },
1603}
1604
1605/// A predicate that can be used in source steps (`n_where` / `e_where`).
1606///
1607/// This is a restricted subset of [`Predicate`] intended to be index- and
1608/// planner-friendly for "source" selection.
1609#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1610pub enum SourcePredicate {
1611    /// Equals: property == value
1612    Eq(String, PropertyValue),
1613    /// Not equals: property != value
1614    Neq(String, PropertyValue),
1615    /// Greater than: property > value (for numeric/string)
1616    Gt(String, PropertyValue),
1617    /// Greater than or equal: property >= value
1618    Gte(String, PropertyValue),
1619    /// Less than: property < value
1620    Lt(String, PropertyValue),
1621    /// Less than or equal: property <= value
1622    Lte(String, PropertyValue),
1623    /// Between (inclusive): min <= property <= max
1624    Between(String, PropertyValue, PropertyValue),
1625    /// Property exists
1626    HasKey(String),
1627    /// String starts with prefix
1628    StartsWith(String, String),
1629    /// Logical AND of predicates
1630    And(Vec<SourcePredicate>),
1631    /// Logical OR of predicates
1632    Or(Vec<SourcePredicate>),
1633}
1634
1635impl SourcePredicate {
1636    /// Create an equality predicate
1637    pub fn eq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1638        SourcePredicate::Eq(property.into(), value.into())
1639    }
1640
1641    /// Create a not-equals predicate
1642    pub fn neq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1643        SourcePredicate::Neq(property.into(), value.into())
1644    }
1645
1646    /// Create a greater-than predicate
1647    pub fn gt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1648        SourcePredicate::Gt(property.into(), value.into())
1649    }
1650
1651    /// Create a greater-than-or-equal predicate
1652    pub fn gte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1653        SourcePredicate::Gte(property.into(), value.into())
1654    }
1655
1656    /// Create a less-than predicate
1657    pub fn lt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1658        SourcePredicate::Lt(property.into(), value.into())
1659    }
1660
1661    /// Create a less-than-or-equal predicate
1662    pub fn lte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1663        SourcePredicate::Lte(property.into(), value.into())
1664    }
1665
1666    /// Create a between predicate (inclusive)
1667    pub fn between(
1668        property: impl Into<String>,
1669        min: impl Into<PropertyValue>,
1670        max: impl Into<PropertyValue>,
1671    ) -> Self {
1672        SourcePredicate::Between(property.into(), min.into(), max.into())
1673    }
1674
1675    /// Create a has-key predicate
1676    pub fn has_key(property: impl Into<String>) -> Self {
1677        SourcePredicate::HasKey(property.into())
1678    }
1679
1680    /// Create a starts-with predicate
1681    pub fn starts_with(property: impl Into<String>, prefix: impl Into<String>) -> Self {
1682        SourcePredicate::StartsWith(property.into(), prefix.into())
1683    }
1684
1685    /// Combine predicates with AND
1686    pub fn and(predicates: Vec<SourcePredicate>) -> Self {
1687        SourcePredicate::And(predicates)
1688    }
1689
1690    /// Combine predicates with OR
1691    pub fn or(predicates: Vec<SourcePredicate>) -> Self {
1692        SourcePredicate::Or(predicates)
1693    }
1694}
1695
1696impl From<SourcePredicate> for Predicate {
1697    fn from(predicate: SourcePredicate) -> Self {
1698        match predicate {
1699            SourcePredicate::Eq(prop, val) => Predicate::Eq(prop, val),
1700            SourcePredicate::Neq(prop, val) => Predicate::Neq(prop, val),
1701            SourcePredicate::Gt(prop, val) => Predicate::Gt(prop, val),
1702            SourcePredicate::Gte(prop, val) => Predicate::Gte(prop, val),
1703            SourcePredicate::Lt(prop, val) => Predicate::Lt(prop, val),
1704            SourcePredicate::Lte(prop, val) => Predicate::Lte(prop, val),
1705            SourcePredicate::Between(prop, min, max) => Predicate::Between(prop, min, max),
1706            SourcePredicate::HasKey(prop) => Predicate::HasKey(prop),
1707            SourcePredicate::StartsWith(prop, prefix) => Predicate::StartsWith(prop, prefix),
1708            SourcePredicate::And(predicates) => {
1709                Predicate::And(predicates.into_iter().map(Predicate::from).collect())
1710            }
1711            SourcePredicate::Or(predicates) => {
1712                Predicate::Or(predicates.into_iter().map(Predicate::from).collect())
1713            }
1714        }
1715    }
1716}
1717
1718impl Predicate {
1719    /// Create an equality predicate
1720    pub fn eq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1721        Predicate::Eq(property.into(), value.into())
1722    }
1723
1724    /// Create a not-equals predicate
1725    pub fn neq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1726        Predicate::Neq(property.into(), value.into())
1727    }
1728
1729    /// Create a greater-than predicate
1730    pub fn gt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1731        Predicate::Gt(property.into(), value.into())
1732    }
1733
1734    /// Create a greater-than-or-equal predicate
1735    pub fn gte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1736        Predicate::Gte(property.into(), value.into())
1737    }
1738
1739    /// Create a less-than predicate
1740    pub fn lt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1741        Predicate::Lt(property.into(), value.into())
1742    }
1743
1744    /// Create a less-than-or-equal predicate
1745    pub fn lte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1746        Predicate::Lte(property.into(), value.into())
1747    }
1748
1749    /// Create a between predicate (inclusive)
1750    pub fn between(
1751        property: impl Into<String>,
1752        min: impl Into<PropertyValue>,
1753        max: impl Into<PropertyValue>,
1754    ) -> Self {
1755        Predicate::Between(property.into(), min.into(), max.into())
1756    }
1757
1758    /// Create a has-key predicate
1759    pub fn has_key(property: impl Into<String>) -> Self {
1760        Predicate::HasKey(property.into())
1761    }
1762
1763    /// Create an `IS NULL` predicate.
1764    pub fn is_null(property: impl Into<String>) -> Self {
1765        Predicate::IsNull(property.into())
1766    }
1767
1768    /// Create an `IS NOT NULL` predicate.
1769    pub fn is_not_null(property: impl Into<String>) -> Self {
1770        Predicate::IsNotNull(property.into())
1771    }
1772
1773    /// Create a starts-with predicate
1774    pub fn starts_with(property: impl Into<String>, prefix: impl Into<String>) -> Self {
1775        Predicate::StartsWith(property.into(), prefix.into())
1776    }
1777
1778    /// Create an ends-with predicate
1779    pub fn ends_with(property: impl Into<String>, suffix: impl Into<String>) -> Self {
1780        Predicate::EndsWith(property.into(), suffix.into())
1781    }
1782
1783    /// Create a contains predicate
1784    pub fn contains(property: impl Into<String>, substring: impl Into<String>) -> Self {
1785        Predicate::Contains(property.into(), substring.into())
1786    }
1787
1788    /// Create a parameterized contains predicate: property contains param string
1789    pub fn contains_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1790        Predicate::ContainsExpr(property.into(), Expr::Param(param_name.into()))
1791    }
1792
1793    /// Create an `IN` predicate with a literal array value.
1794    pub fn is_in(property: impl Into<String>, values: impl Into<PropertyValue>) -> Self {
1795        Predicate::IsIn(property.into(), values.into())
1796    }
1797
1798    /// Create an `IN` predicate whose values are resolved from an expression.
1799    pub fn is_in_expr(property: impl Into<String>, values: Expr) -> Self {
1800        Predicate::IsInExpr(property.into(), values)
1801    }
1802
1803    /// Create a parameterized `IN` predicate: property IN param_array.
1804    pub fn is_in_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1805        Predicate::IsInExpr(property.into(), Expr::Param(param_name.into()))
1806    }
1807
1808    /// Combine predicates with AND
1809    pub fn and(predicates: Vec<Predicate>) -> Self {
1810        Predicate::And(predicates)
1811    }
1812
1813    /// Combine predicates with OR
1814    pub fn or(predicates: Vec<Predicate>) -> Self {
1815        Predicate::Or(predicates)
1816    }
1817
1818    /// Negate a predicate
1819    pub fn not(predicate: Predicate) -> Self {
1820        Predicate::Not(Box::new(predicate))
1821    }
1822
1823    /// Create an expression-based comparison predicate
1824    ///
1825    /// This supports property-to-property comparisons, math expressions, and more.
1826    ///
1827    pub fn compare(left: Expr, op: CompareOp, right: Expr) -> Self {
1828        Predicate::Compare { left, op, right }
1829    }
1830
1831    // Parameterized predicate constructors
1832
1833    /// Create a parameterized equality predicate: property == param
1834    ///
1835    /// The parameter value is provided at query execution time.
1836    ///
1837    pub fn eq_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1838        Predicate::Compare {
1839            left: Expr::Property(property.into()),
1840            op: CompareOp::Eq,
1841            right: Expr::Param(param_name.into()),
1842        }
1843    }
1844
1845    /// Create a parameterized not-equals predicate: property != param
1846    pub fn neq_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1847        Predicate::Compare {
1848            left: Expr::Property(property.into()),
1849            op: CompareOp::Neq,
1850            right: Expr::Param(param_name.into()),
1851        }
1852    }
1853
1854    /// Create a parameterized greater-than predicate: property > param
1855    pub fn gt_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1856        Predicate::Compare {
1857            left: Expr::Property(property.into()),
1858            op: CompareOp::Gt,
1859            right: Expr::Param(param_name.into()),
1860        }
1861    }
1862
1863    /// Create a parameterized greater-than-or-equal predicate: property >= param
1864    pub fn gte_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1865        Predicate::Compare {
1866            left: Expr::Property(property.into()),
1867            op: CompareOp::Gte,
1868            right: Expr::Param(param_name.into()),
1869        }
1870    }
1871
1872    /// Create a parameterized less-than predicate: property < param
1873    pub fn lt_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1874        Predicate::Compare {
1875            left: Expr::Property(property.into()),
1876            op: CompareOp::Lt,
1877            right: Expr::Param(param_name.into()),
1878        }
1879    }
1880
1881    /// Create a parameterized less-than-or-equal predicate: property <= param
1882    pub fn lte_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1883        Predicate::Compare {
1884            left: Expr::Property(property.into()),
1885            op: CompareOp::Lte,
1886            right: Expr::Param(param_name.into()),
1887        }
1888    }
1889}
1890
1891// Supporting Types
1892
1893/// A property projection with optional renaming
1894#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1895pub struct PropertyProjection {
1896    /// Original property name in the data
1897    pub source: String,
1898    /// Name to use in the output (alias)
1899    pub alias: String,
1900}
1901
1902impl PropertyProjection {
1903    /// Create a projection without renaming
1904    pub fn new(name: impl Into<String>) -> Self {
1905        let n = name.into();
1906        Self {
1907            source: n.clone(),
1908            alias: n,
1909        }
1910    }
1911
1912    /// Create a projection with renaming
1913    pub fn renamed(source: impl Into<String>, alias: impl Into<String>) -> Self {
1914        Self {
1915            source: source.into(),
1916            alias: alias.into(),
1917        }
1918    }
1919}
1920
1921/// An expression-backed projection.
1922#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1923pub struct ExprProjection {
1924    /// Name to use in the output.
1925    pub alias: String,
1926    /// Expression to evaluate.
1927    pub expr: Expr,
1928}
1929
1930impl ExprProjection {
1931    /// Create a projection from an expression.
1932    pub fn new(alias: impl Into<String>, expr: Expr) -> Self {
1933        Self {
1934            alias: alias.into(),
1935            expr,
1936        }
1937    }
1938}
1939
1940/// A terminal projection entry.
1941#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1942#[serde(untagged)]
1943pub enum Projection {
1944    /// Project a property with optional renaming.
1945    Property(PropertyProjection),
1946    /// Project a computed expression.
1947    Expr(ExprProjection),
1948}
1949
1950impl Projection {
1951    /// Project a property with optional renaming.
1952    pub fn property(source: impl Into<String>, alias: impl Into<String>) -> Self {
1953        Self::Property(PropertyProjection::renamed(source, alias))
1954    }
1955
1956    /// Project a computed expression.
1957    pub fn expr(alias: impl Into<String>, expr: Expr) -> Self {
1958        Self::Expr(ExprProjection::new(alias, expr))
1959    }
1960}
1961
1962impl From<PropertyProjection> for Projection {
1963    fn from(value: PropertyProjection) -> Self {
1964        Self::Property(value)
1965    }
1966}
1967
1968impl From<ExprProjection> for Projection {
1969    fn from(value: ExprProjection) -> Self {
1970        Self::Expr(value)
1971    }
1972}
1973
1974/// Sort order for ordering steps
1975#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1976pub enum Order {
1977    /// Ascending order (smallest first)
1978    Asc,
1979    /// Descending order (largest first)
1980    Desc,
1981}
1982
1983impl Default for Order {
1984    fn default() -> Self {
1985        Order::Asc
1986    }
1987}
1988
1989/// Emit behavior for repeat steps
1990#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1991pub enum EmitBehavior {
1992    /// Don't emit intermediate results.
1993    None,
1994    /// Emit the current node stream before each repeat iteration.
1995    Before,
1996    /// Emit the node stream produced by each repeat iteration.
1997    After,
1998    /// Emit both before and after each repeat iteration.
1999    All,
2000}
2001
2002impl Default for EmitBehavior {
2003    fn default() -> Self {
2004        EmitBehavior::None
2005    }
2006}
2007
2008/// Aggregation function for reduce operations
2009#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2010pub enum AggregateFunction {
2011    /// Count items
2012    Count,
2013    /// Sum numeric values
2014    Sum,
2015    /// Find minimum value
2016    Min,
2017    /// Find maximum value
2018    Max,
2019    /// Calculate mean/average
2020    Mean,
2021}
2022
2023// Sub-Traversal (for branching operations without typestate)
2024
2025/// A sub-traversal for use in branching operations (union, choose, coalesce, optional, repeat).
2026///
2027/// Sub-traversals don't track typestate because they start from an implicit context
2028/// provided by the parent traversal. This allows maximum flexibility in branching
2029/// while the parent traversal maintains compile-time safety.
2030#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
2031pub struct SubTraversal {
2032    /// The steps in this sub-traversal
2033    pub steps: Vec<Step>,
2034}
2035
2036impl SubTraversal {
2037    /// Create a new empty sub-traversal
2038    pub fn new() -> Self {
2039        Self { steps: Vec::new() }
2040    }
2041
2042    // Navigation Steps (node -> node)
2043
2044    /// Traverse outgoing edges, optionally filtered by label
2045    pub fn out(mut self, label: Option<impl Into<String>>) -> Self {
2046        self.steps.push(Step::Out(label.map(|l| l.into())));
2047        self
2048    }
2049
2050    /// Traverse incoming edges, optionally filtered by label
2051    pub fn in_(mut self, label: Option<impl Into<String>>) -> Self {
2052        self.steps.push(Step::In(label.map(|l| l.into())));
2053        self
2054    }
2055
2056    /// Traverse edges in both directions, optionally filtered by label
2057    pub fn both(mut self, label: Option<impl Into<String>>) -> Self {
2058        self.steps.push(Step::Both(label.map(|l| l.into())));
2059        self
2060    }
2061
2062    // Edge Traversal Steps
2063
2064    /// Traverse to outgoing edges
2065    pub fn out_e(mut self, label: Option<impl Into<String>>) -> Self {
2066        self.steps.push(Step::OutE(label.map(|l| l.into())));
2067        self
2068    }
2069
2070    /// Traverse to incoming edges
2071    pub fn in_e(mut self, label: Option<impl Into<String>>) -> Self {
2072        self.steps.push(Step::InE(label.map(|l| l.into())));
2073        self
2074    }
2075
2076    /// Traverse to edges in both directions
2077    pub fn both_e(mut self, label: Option<impl Into<String>>) -> Self {
2078        self.steps.push(Step::BothE(label.map(|l| l.into())));
2079        self
2080    }
2081
2082    /// From edge, get the target node
2083    pub fn out_n(mut self) -> Self {
2084        self.steps.push(Step::OutN);
2085        self
2086    }
2087
2088    /// From edge, get the source node
2089    pub fn in_n(mut self) -> Self {
2090        self.steps.push(Step::InN);
2091        self
2092    }
2093
2094    /// From edge, get the "other" node (not the one we came from)
2095    pub fn other_n(mut self) -> Self {
2096        self.steps.push(Step::OtherN);
2097        self
2098    }
2099
2100    // Filter Steps
2101
2102    /// Filter by property value
2103    pub fn has(mut self, property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
2104        self.steps.push(Step::Has(property.into(), value.into()));
2105        self
2106    }
2107
2108    /// Filter by label (shorthand for has("$label", value))
2109    pub fn has_label(mut self, label: impl Into<String>) -> Self {
2110        self.steps.push(Step::HasLabel(label.into()));
2111        self
2112    }
2113
2114    /// Filter by property existence
2115    pub fn has_key(mut self, property: impl Into<String>) -> Self {
2116        self.steps.push(Step::HasKey(property.into()));
2117        self
2118    }
2119
2120    /// Filter by a complex predicate
2121    pub fn where_(mut self, predicate: Predicate) -> Self {
2122        self.steps.push(Step::Where(predicate));
2123        self
2124    }
2125
2126    /// Remove duplicates from the stream
2127    pub fn dedup(mut self) -> Self {
2128        self.steps.push(Step::Dedup);
2129        self
2130    }
2131
2132    /// Filter to nodes that exist in a variable
2133    pub fn within(mut self, var_name: impl Into<String>) -> Self {
2134        self.steps.push(Step::Within(var_name.into()));
2135        self
2136    }
2137
2138    /// Filter to nodes that do NOT exist in a variable
2139    pub fn without(mut self, var_name: impl Into<String>) -> Self {
2140        self.steps.push(Step::Without(var_name.into()));
2141        self
2142    }
2143
2144    // Edge Filter Steps
2145
2146    /// Filter edges by property value
2147    pub fn edge_has(
2148        mut self,
2149        property: impl Into<String>,
2150        value: impl Into<PropertyInput>,
2151    ) -> Self {
2152        self.steps
2153            .push(Step::EdgeHas(property.into(), value.into()));
2154        self
2155    }
2156
2157    /// Filter edges by label
2158    pub fn edge_has_label(mut self, label: impl Into<String>) -> Self {
2159        self.steps.push(Step::EdgeHasLabel(label.into()));
2160        self
2161    }
2162
2163    // Limit Steps
2164
2165    /// Take at most N items.
2166    pub fn limit(mut self, n: impl Into<StreamBound>) -> Self {
2167        self.steps.push(limit_step(n));
2168        self
2169    }
2170
2171    /// Skip the first N items.
2172    pub fn skip(mut self, n: impl Into<StreamBound>) -> Self {
2173        self.steps.push(skip_step(n));
2174        self
2175    }
2176
2177    /// Get items in a range [start, end).
2178    pub fn range(mut self, start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Self {
2179        self.steps.push(range_step(start, end));
2180        self
2181    }
2182
2183    // Variable Steps
2184
2185    /// Store current nodes with a name for later reference
2186    pub fn as_(mut self, name: impl Into<String>) -> Self {
2187        self.steps.push(Step::As(name.into()));
2188        self
2189    }
2190
2191    /// Store current nodes to a variable (same as `as_`)
2192    pub fn store(mut self, name: impl Into<String>) -> Self {
2193        self.steps.push(Step::Store(name.into()));
2194        self
2195    }
2196
2197    /// Replace current traversal with nodes from a variable
2198    pub fn select(mut self, name: impl Into<String>) -> Self {
2199        self.steps.push(Step::Select(name.into()));
2200        self
2201    }
2202
2203    // Ordering Steps
2204
2205    /// Order results by a property.
2206    ///
2207    /// Note: some interpreters represent intermediate streams as sets. In those
2208    /// engines, ordering may not be preserved in the returned node set.
2209    pub fn order_by(mut self, property: impl Into<String>, order: Order) -> Self {
2210        self.steps.push(Step::OrderBy(property.into(), order));
2211        self
2212    }
2213
2214    /// Order results by multiple properties with priorities.
2215    ///
2216    /// Note: some interpreters represent intermediate streams as sets. In those
2217    /// engines, ordering may not be preserved in the returned node set.
2218    pub fn order_by_multiple(mut self, orderings: Vec<(impl Into<String>, Order)>) -> Self {
2219        let orderings: Vec<(String, Order)> =
2220            orderings.into_iter().map(|(p, o)| (p.into(), o)).collect();
2221        self.steps.push(Step::OrderByMultiple(orderings));
2222        self
2223    }
2224
2225    // Path Steps
2226
2227    /// Include the full traversal path in results.
2228    ///
2229    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2230    pub fn path(mut self) -> Self {
2231        self.steps.push(Step::Path);
2232        self
2233    }
2234
2235    /// Filter to only simple paths (no repeated nodes).
2236    ///
2237    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2238    pub fn simple_path(mut self) -> Self {
2239        self.steps.push(Step::SimplePath);
2240        self
2241    }
2242}
2243
2244/// Create a new sub-traversal for use in branching operations
2245///
2246/// Use this instead of `g()` when building traversals for `union()`, `choose()`,
2247/// `coalesce()`, `optional()`, or `repeat()`.
2248///
2249pub fn sub() -> SubTraversal {
2250    SubTraversal::new()
2251}
2252
2253// Repeat Configuration
2254
2255/// Configuration for repeat steps
2256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2257pub struct RepeatConfig {
2258    /// The sub-traversal to repeat
2259    pub traversal: SubTraversal,
2260    /// Maximum number of iterations (None = unlimited)
2261    pub times: Option<usize>,
2262    /// Condition to stop repeating (checked each iteration)
2263    pub until: Option<Predicate>,
2264    /// Whether to emit intermediate results
2265    pub emit: EmitBehavior,
2266    /// Optional predicate for conditional emit
2267    pub emit_predicate: Option<Predicate>,
2268    /// Maximum depth to prevent infinite loops (default: 100)
2269    pub max_depth: usize,
2270}
2271
2272impl RepeatConfig {
2273    /// Create a new repeat configuration
2274    pub fn new(traversal: SubTraversal) -> Self {
2275        Self {
2276            traversal,
2277            times: None,
2278            until: None,
2279            emit: EmitBehavior::None,
2280            emit_predicate: None,
2281            max_depth: 100,
2282        }
2283    }
2284
2285    /// Set the number of times to repeat
2286    pub fn times(mut self, n: usize) -> Self {
2287        self.times = Some(n);
2288        self
2289    }
2290
2291    /// Set the until condition
2292    pub fn until(mut self, predicate: Predicate) -> Self {
2293        self.until = Some(predicate);
2294        self
2295    }
2296
2297    /// Emit intermediate results before and after each iteration.
2298    pub fn emit_all(mut self) -> Self {
2299        self.emit = EmitBehavior::All;
2300        self
2301    }
2302
2303    /// Emit intermediate results before each iteration
2304    pub fn emit_before(mut self) -> Self {
2305        self.emit = EmitBehavior::Before;
2306        self
2307    }
2308
2309    /// Emit intermediate results after each iteration
2310    pub fn emit_after(mut self) -> Self {
2311        self.emit = EmitBehavior::After;
2312        self
2313    }
2314
2315    /// Emit intermediate results that match a predicate.
2316    ///
2317    /// This enables post-iteration emission (equivalent to [`EmitBehavior::After`])
2318    /// and applies `predicate` to decide which vertices to emit.
2319    pub fn emit_if(mut self, predicate: Predicate) -> Self {
2320        self.emit = EmitBehavior::After;
2321        self.emit_predicate = Some(predicate);
2322        self
2323    }
2324
2325    /// Set maximum depth to prevent infinite loops
2326    pub fn max_depth(mut self, depth: usize) -> Self {
2327        self.max_depth = depth;
2328        self
2329    }
2330}
2331
2332/// Dynamic index declaration used by runtime index-management steps.
2333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2334pub enum IndexSpec {
2335    /// Equality index over node properties.
2336    NodeEquality {
2337        /// Node label to scope the index.
2338        label: String,
2339        /// Indexed property name.
2340        property: String,
2341        /// Whether the index enforces uniqueness for supported non-null values.
2342        #[serde(default)]
2343        unique: bool,
2344    },
2345    /// Range index over node properties.
2346    NodeRange {
2347        /// Node label to scope the index.
2348        label: String,
2349        /// Indexed property name.
2350        property: String,
2351    },
2352    /// Equality index over edge properties.
2353    EdgeEquality {
2354        /// Edge label to scope the index.
2355        label: String,
2356        /// Indexed property name.
2357        property: String,
2358    },
2359    /// Range index over edge properties.
2360    EdgeRange {
2361        /// Edge label to scope the index.
2362        label: String,
2363        /// Indexed property name.
2364        property: String,
2365    },
2366    /// Vector index over node properties.
2367    NodeVector {
2368        /// Node label to scope the index.
2369        label: String,
2370        /// Property name containing vectors.
2371        property: String,
2372        /// Optional multitenant partition property.
2373        #[serde(default, skip_serializing_if = "Option::is_none")]
2374        tenant_property: Option<String>,
2375    },
2376    /// Text index over node properties.
2377    NodeText {
2378        /// Node label to scope the index.
2379        label: String,
2380        /// Property name containing text.
2381        property: String,
2382        /// Optional multitenant partition property.
2383        #[serde(default, skip_serializing_if = "Option::is_none")]
2384        tenant_property: Option<String>,
2385    },
2386    /// Vector index over edge properties.
2387    EdgeVector {
2388        /// Edge label to scope the index.
2389        label: String,
2390        /// Property name containing vectors.
2391        property: String,
2392        /// Optional multitenant partition property.
2393        #[serde(default, skip_serializing_if = "Option::is_none")]
2394        tenant_property: Option<String>,
2395    },
2396    /// Text index over edge properties.
2397    EdgeText {
2398        /// Edge label to scope the index.
2399        label: String,
2400        /// Property name containing text.
2401        property: String,
2402        /// Optional multitenant partition property.
2403        #[serde(default, skip_serializing_if = "Option::is_none")]
2404        tenant_property: Option<String>,
2405    },
2406}
2407
2408impl IndexSpec {
2409    /// Build a node equality index declaration.
2410    pub fn node_equality(label: impl Into<String>, property: impl Into<String>) -> Self {
2411        Self::NodeEquality {
2412            label: label.into(),
2413            property: property.into(),
2414            unique: false,
2415        }
2416    }
2417
2418    /// Build a unique node equality index declaration.
2419    pub fn node_unique_equality(label: impl Into<String>, property: impl Into<String>) -> Self {
2420        Self::NodeEquality {
2421            label: label.into(),
2422            property: property.into(),
2423            unique: true,
2424        }
2425    }
2426
2427    /// Build a node range index declaration.
2428    pub fn node_range(label: impl Into<String>, property: impl Into<String>) -> Self {
2429        Self::NodeRange {
2430            label: label.into(),
2431            property: property.into(),
2432        }
2433    }
2434
2435    /// Build an edge equality index declaration.
2436    pub fn edge_equality(label: impl Into<String>, property: impl Into<String>) -> Self {
2437        Self::EdgeEquality {
2438            label: label.into(),
2439            property: property.into(),
2440        }
2441    }
2442
2443    /// Build an edge range index declaration.
2444    pub fn edge_range(label: impl Into<String>, property: impl Into<String>) -> Self {
2445        Self::EdgeRange {
2446            label: label.into(),
2447            property: property.into(),
2448        }
2449    }
2450
2451    /// Build a node vector index declaration.
2452    pub fn node_vector(
2453        label: impl Into<String>,
2454        property: impl Into<String>,
2455        tenant_property: Option<impl Into<String>>,
2456    ) -> Self {
2457        Self::NodeVector {
2458            label: label.into(),
2459            property: property.into(),
2460            tenant_property: tenant_property.map(|value| value.into()),
2461        }
2462    }
2463
2464    /// Build a node text index declaration.
2465    pub fn node_text(
2466        label: impl Into<String>,
2467        property: impl Into<String>,
2468        tenant_property: Option<impl Into<String>>,
2469    ) -> Self {
2470        Self::NodeText {
2471            label: label.into(),
2472            property: property.into(),
2473            tenant_property: tenant_property.map(|value| value.into()),
2474        }
2475    }
2476
2477    /// Build an edge vector index declaration.
2478    pub fn edge_vector(
2479        label: impl Into<String>,
2480        property: impl Into<String>,
2481        tenant_property: Option<impl Into<String>>,
2482    ) -> Self {
2483        Self::EdgeVector {
2484            label: label.into(),
2485            property: property.into(),
2486            tenant_property: tenant_property.map(|value| value.into()),
2487        }
2488    }
2489
2490    /// Build an edge text index declaration.
2491    pub fn edge_text(
2492        label: impl Into<String>,
2493        property: impl Into<String>,
2494        tenant_property: Option<impl Into<String>>,
2495    ) -> Self {
2496        Self::EdgeText {
2497            label: label.into(),
2498            property: property.into(),
2499            tenant_property: tenant_property.map(|value| value.into()),
2500        }
2501    }
2502}
2503
2504// Step Enum (AST Nodes)
2505
2506/// A single step in a traversal AST.
2507///
2508/// Most users should build traversals via [`g()`] and the [`Traversal`] builder.
2509/// This enum exists so the traversal can be inspected, serialized, transported,
2510/// and reconstructed with [`Traversal::from_steps`].
2511#[doc(hidden)]
2512#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2513pub enum Step {
2514    // Source Steps - Start a traversal or switch context
2515    /// Start (or switch) on nodes.
2516    ///
2517    /// Typical usage is as the first traversal source via [`Traversal::n`].
2518    N(NodeRef),
2519
2520    /// Start from nodes matching a [`SourcePredicate`].
2521    NWhere(SourcePredicate),
2522
2523    /// Start (or switch) on edges.
2524    ///
2525    /// Typical usage is as the first traversal source via [`Traversal::e`].
2526    E(EdgeRef),
2527
2528    /// Start from edges matching a [`SourcePredicate`].
2529    EWhere(SourcePredicate),
2530
2531    /// Vector similarity search on nodes
2532    ///
2533    /// Start traversal from nodes with vectors similar to the query vector.
2534    /// Uses the HNSW index for the given (label, property) combination.
2535    ///
2536    /// Note: this step encodes the nearest-neighbor search inputs.
2537    /// Implementations may expose ranking and distance metadata at runtime.
2538    VectorSearchNodes {
2539        /// The node label to search
2540        label: String,
2541        /// The property name containing vectors
2542        property: String,
2543        /// Optional multitenant partition value.
2544        #[serde(default, skip_serializing_if = "Option::is_none")]
2545        tenant_value: Option<PropertyInput>,
2546        /// The query vector input.
2547        query_vector: PropertyInput,
2548        /// Number of nearest neighbors to return.
2549        k: StreamBound,
2550    },
2551
2552    /// BM25 text search on nodes.
2553    TextSearchNodes {
2554        /// The node label to search.
2555        label: String,
2556        /// The property name containing indexed text.
2557        property: String,
2558        /// Optional multitenant partition value.
2559        #[serde(default, skip_serializing_if = "Option::is_none")]
2560        tenant_value: Option<PropertyInput>,
2561        /// The query text input.
2562        query_text: PropertyInput,
2563        /// Number of ranked results to return.
2564        k: StreamBound,
2565    },
2566
2567    /// Vector similarity search on edges
2568    ///
2569    /// Start traversal from edges with vectors similar to the query vector.
2570    /// Uses the HNSW index for the given (label, property) combination.
2571    ///
2572    /// Note: this step encodes the nearest-neighbor search inputs.
2573    /// Implementations may expose ranking and distance metadata at runtime.
2574    VectorSearchEdges {
2575        /// The edge label to search
2576        label: String,
2577        /// The property name containing vectors
2578        property: String,
2579        /// Optional multitenant partition value.
2580        #[serde(default, skip_serializing_if = "Option::is_none")]
2581        tenant_value: Option<PropertyInput>,
2582        /// The query vector input.
2583        query_vector: PropertyInput,
2584        /// Number of nearest neighbors to return.
2585        k: StreamBound,
2586    },
2587
2588    /// BM25 text search on edges.
2589    TextSearchEdges {
2590        /// The edge label to search.
2591        label: String,
2592        /// The property name containing indexed text.
2593        property: String,
2594        /// Optional multitenant partition value.
2595        #[serde(default, skip_serializing_if = "Option::is_none")]
2596        tenant_value: Option<PropertyInput>,
2597        /// The query text input.
2598        query_text: PropertyInput,
2599        /// Number of ranked results to return.
2600        k: StreamBound,
2601    },
2602
2603    // Traversal Steps - Navigate the graph
2604    /// Traverse outgoing edges, optionally filtered by label.
2605    ///
2606    /// Builder forms:
2607    /// - `.out(Some("KNOWS"))`
2608    /// - `.out(None::<&str>)` (no label filter)
2609    Out(Option<String>),
2610
2611    /// Traverse incoming edges, optionally filtered by label.
2612    ///
2613    /// Builder forms:
2614    /// - `.in_(Some("KNOWS"))`
2615    /// - `.in_(None::<&str>)`
2616    In(Option<String>),
2617
2618    /// Traverse edges in both directions, optionally filtered by label.
2619    ///
2620    /// Builder forms:
2621    /// - `.both(Some("KNOWS"))`
2622    /// - `.both(None::<&str>)`
2623    Both(Option<String>),
2624
2625    // Edge Traversal Steps - Navigate to/from edges
2626    /// Traverse from nodes to outgoing edges.
2627    ///
2628    /// Builder forms:
2629    /// - `.out_e(Some("KNOWS"))`
2630    /// - `.out_e(None::<&str>)`
2631    OutE(Option<String>),
2632
2633    /// Traverse from nodes to incoming edges.
2634    ///
2635    /// Builder forms:
2636    /// - `.in_e(Some("KNOWS"))`
2637    /// - `.in_e(None::<&str>)`
2638    InE(Option<String>),
2639
2640    /// Traverse from nodes to edges in both directions.
2641    ///
2642    /// Builder forms:
2643    /// - `.both_e(Some("KNOWS"))`
2644    /// - `.both_e(None::<&str>)`
2645    BothE(Option<String>),
2646
2647    /// From an edge stream, switch back to nodes by selecting the edge target.
2648    ///
2649    /// Builder form: `.out_n()`
2650    OutN,
2651
2652    /// From an edge stream, switch back to nodes by selecting the edge source.
2653    ///
2654    /// Builder form: `.in_n()`
2655    InN,
2656
2657    /// From an edge stream, switch back to nodes by selecting the "other" endpoint.
2658    ///
2659    /// Builder form: `.other_n()`
2660    OtherN,
2661
2662    // Filter Steps - Reduce the stream
2663    /// Filter nodes by property equality: `.has("name", "Alice")`
2664    Has(String, PropertyValue),
2665
2666    /// Filter nodes by label: `.has_label("User")`.
2667    ///
2668    /// This is shorthand for filtering on the reserved `$label` property.
2669    HasLabel(String),
2670
2671    /// Filter nodes by property existence: `.has_key("email")`
2672    HasKey(String),
2673
2674    /// Filter nodes by a [`Predicate`]: `.where_(Predicate::gt("age", 18i64))`
2675    /// or `.where_(Predicate::is_in("status", vec!["active".to_string()]))`
2676    Where(Predicate),
2677
2678    /// Remove duplicates: `dedup()`
2679    Dedup,
2680
2681    /// Filter to nodes that exist in a variable: `within("x")`
2682    Within(String),
2683
2684    /// Filter to nodes that do NOT exist in a variable: `without("x")`
2685    Without(String),
2686
2687    // Edge Filter Steps - Filter edges
2688    /// Filter edges by property equality: `.edge_has("weight", 1i64)`
2689    EdgeHas(String, PropertyInput),
2690
2691    /// Filter edges by label: `.edge_has_label("KNOWS")`
2692    EdgeHasLabel(String),
2693
2694    // Limit Steps - Control stream size
2695    /// Take first N items: `limit(10)`
2696    Limit(usize),
2697
2698    /// Take first N items using a runtime-resolved expression.
2699    LimitBy(Expr),
2700
2701    /// Skip first N items: `skip(5)`
2702    Skip(usize),
2703
2704    /// Skip first N items using a runtime-resolved expression.
2705    SkipBy(Expr),
2706
2707    /// Get items in range [start, end): equivalent to skip(start).limit(end - start)
2708    Range(usize, usize),
2709
2710    /// Get items in range [start, end) using literal and/or runtime-resolved bounds.
2711    RangeBy(StreamBound, StreamBound),
2712
2713    // Variable Steps - Store and reference results
2714    /// Store the current stream in the traversal context under a name.
2715    ///
2716    /// Builder form: `.as_("x")`
2717    As(String),
2718
2719    /// Store the current stream in the traversal context under a name.
2720    ///
2721    /// Builder form: `.store("x")`
2722    Store(String),
2723
2724    /// Replace the current node stream with nodes referenced by a stored variable.
2725    ///
2726    /// Builder form: `.select("x")`
2727    Select(String),
2728
2729    // Terminal Steps - End the traversal
2730    /// Count results (returns single value)
2731    Count,
2732
2733    /// Check if any results exist (returns bool)
2734    Exists,
2735
2736    /// Get the ID of current nodes/edges (returns the ID as a value)
2737    Id,
2738
2739    /// Get the label of current nodes/edges (returns the $label property)
2740    Label,
2741
2742    // Property Projection Steps - Return property data
2743    /// Return specific node properties.
2744    ///
2745    /// Builder form: `.values(vec!["name", "age"])`
2746    Values(Vec<String>),
2747
2748    /// Return node properties as maps.
2749    ///
2750    /// Builder forms:
2751    /// - `.value_map(None::<Vec<&str>>)` (all properties)
2752    /// - `.value_map(Some(vec!["name", "age"]))`
2753    ValueMap(Option<Vec<String>>),
2754
2755    /// Project properties and expressions with optional renaming.
2756    Project(Vec<Projection>),
2757
2758    /// Return edge properties for the current edge stream.
2759    ///
2760    /// Builder form: `.edge_properties()`
2761    EdgeProperties,
2762
2763    /// Create a runtime index, treating existing matching definitions as a no-op
2764    /// when `if_not_exists` is true.
2765    CreateIndex {
2766        /// Index specification to create.
2767        spec: IndexSpec,
2768        /// Whether duplicate creates should be ignored.
2769        if_not_exists: bool,
2770    },
2771
2772    /// Drop a runtime index.
2773    DropIndex {
2774        /// Index specification to drop.
2775        spec: IndexSpec,
2776    },
2777
2778    // Mutation Steps - Modify the graph (write transactions only)
2779    /// Create a vector index for nodes with the given label and property
2780    CreateVectorIndexNodes {
2781        /// Node label to scope the index
2782        label: String,
2783        /// Property name containing vectors
2784        property: String,
2785        /// Optional multitenant partition property.
2786        #[serde(default, skip_serializing_if = "Option::is_none")]
2787        tenant_property: Option<String>,
2788    },
2789
2790    /// Create a vector index for edges with the given label and property
2791    CreateVectorIndexEdges {
2792        /// Edge label to scope the index
2793        label: String,
2794        /// Property name containing vectors
2795        property: String,
2796        /// Optional multitenant partition property.
2797        #[serde(default, skip_serializing_if = "Option::is_none")]
2798        tenant_property: Option<String>,
2799    },
2800
2801    /// Create a text index for nodes with the given label and property.
2802    CreateTextIndexNodes {
2803        /// Node label to scope the index.
2804        label: String,
2805        /// Property name containing text.
2806        property: String,
2807        /// Optional multitenant partition property.
2808        #[serde(default, skip_serializing_if = "Option::is_none")]
2809        tenant_property: Option<String>,
2810    },
2811
2812    /// Create a text index for edges with the given label and property.
2813    CreateTextIndexEdges {
2814        /// Edge label to scope the index.
2815        label: String,
2816        /// Property name containing text.
2817        property: String,
2818        /// Optional multitenant partition property.
2819        #[serde(default, skip_serializing_if = "Option::is_none")]
2820        tenant_property: Option<String>,
2821    },
2822
2823    /// Add a node with a label and properties.
2824    ///
2825    /// Builder form: `.add_n("User", vec![("name", "Alice")])`
2826    /// The node ID is allocated automatically.
2827    /// The new node becomes the current traversal context.
2828    AddN {
2829        /// The node label (required)
2830        label: String,
2831        /// Optional properties
2832        properties: Vec<(String, PropertyInput)>,
2833    },
2834
2835    /// Add edges from the current nodes to `to`.
2836    ///
2837    /// Builder form: `.add_e("FOLLOWS", to, vec![("weight", 1i64)])`
2838    AddE {
2839        /// The edge label (required)
2840        label: String,
2841        /// Target nodes (by ID or variable)
2842        to: NodeRef,
2843        /// Optional edge properties
2844        properties: Vec<(String, PropertyInput)>,
2845    },
2846
2847    /// Set/update a property on the current nodes: `.set_property(name, value)`
2848    SetProperty(String, PropertyInput),
2849
2850    /// Remove a property from the current nodes: `.remove_property(name)`
2851    RemoveProperty(String),
2852
2853    /// Delete current nodes (and their edges): `drop()`
2854    Drop,
2855
2856    /// Delete edges from the current nodes to a target set: `.drop_edge(target)`
2857    ///
2858    /// **Note**: In multigraph scenarios, this removes ALL edges between the current
2859    /// nodes and the target nodes. Use `DropEdgeById` for precise edge removal.
2860    DropEdge(NodeRef),
2861
2862    /// Delete only edges with a specific label from the current nodes to a target set.
2863    DropEdgeLabeled {
2864        /// Target nodes to disconnect from.
2865        to: NodeRef,
2866        /// Edge label to remove.
2867        label: String,
2868    },
2869
2870    /// Delete specific edges by their IDs: `.drop_edge_by_id(edge_ref)`
2871    ///
2872    /// This is the multigraph-safe way to remove edges, as it removes specific
2873    /// edges rather than all edges between a pair of nodes.
2874    DropEdgeById(EdgeRef),
2875
2876    // Ordering Steps - Sort the stream
2877    /// Order the node stream by a property: `.order_by("age", Order::Desc)`
2878    OrderBy(String, Order),
2879
2880    /// Order by multiple properties with priorities
2881    OrderByMultiple(Vec<(String, Order)>),
2882
2883    // Loop/Repeat Steps - Iterative traversal
2884    /// Repeat a traversal body.
2885    ///
2886    /// Builder form: `.repeat(RepeatConfig::new(sub().out(None::<&str>)).times(3))`
2887    Repeat(RepeatConfig),
2888
2889    // Branching Steps - Conditional execution
2890    /// Execute multiple sub-traversals and merge their results: `.union(vec![...])`
2891    Union(Vec<SubTraversal>),
2892
2893    /// Conditional branching: `choose(predicate, then_traversal, else_traversal)`
2894    Choose {
2895        /// Condition to check
2896        condition: Predicate,
2897        /// Traversal if condition is true
2898        then_traversal: SubTraversal,
2899        /// Traversal if condition is false (optional)
2900        else_traversal: Option<SubTraversal>,
2901    },
2902
2903    /// Try sub-traversals in order until one produces results: `.coalesce(vec![...])`
2904    Coalesce(Vec<SubTraversal>),
2905
2906    /// Execute a sub-traversal if it produces results, otherwise pass through: `.optional(t)`
2907    Optional(SubTraversal),
2908
2909    // Aggregation Steps - Group and reduce
2910    /// Group by a property.
2911    Group(String),
2912
2913    /// Count occurrences grouped by a property.
2914    GroupCount(String),
2915
2916    /// Apply an aggregation function to a property.
2917    ///
2918    /// Builder form: `.aggregate_by(AggregateFunction::Sum, "price")`
2919    AggregateBy(AggregateFunction, String),
2920
2921    /// Barrier step.
2922    ///
2923    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2924    Fold,
2925
2926    /// Expand a collected list back into individual items.
2927    ///
2928    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2929    Unfold,
2930
2931    // Path Steps - Track traversal history
2932    /// Include the full traversal path in results.
2933    ///
2934    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2935    Path,
2936
2937    /// Filter to paths without repeated nodes (cycle detection).
2938    ///
2939    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2940    SimplePath,
2941
2942    // Sack Steps - Carry state through traversal
2943    /// Initialize a sack with a value.
2944    ///
2945    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2946    WithSack(PropertyValue),
2947
2948    /// Update the sack with a property value.
2949    ///
2950    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2951    SackSet(String),
2952
2953    /// Add to the sack (numeric only).
2954    ///
2955    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2956    SackAdd(String),
2957
2958    /// Get the current sack value.
2959    ///
2960    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2961    SackGet,
2962
2963    // Inject Steps - Add values to the stream
2964    /// Inject nodes from a variable into the stream.
2965    ///
2966    /// As a source step, this starts from the variable's stored node set.
2967    /// When used mid-traversal, engines may interpret this as a union/merge.
2968    Inject(String),
2969}
2970
2971fn limit_step(bound: impl Into<StreamBound>) -> Step {
2972    match bound.into() {
2973        StreamBound::Literal(n) => Step::Limit(n),
2974        StreamBound::Expr(expr) => Step::LimitBy(expr),
2975    }
2976}
2977
2978fn skip_step(bound: impl Into<StreamBound>) -> Step {
2979    match bound.into() {
2980        StreamBound::Literal(n) => Step::Skip(n),
2981        StreamBound::Expr(expr) => Step::SkipBy(expr),
2982    }
2983}
2984
2985fn range_step(start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Step {
2986    let start = start.into();
2987    let end = end.into();
2988    match (&start, &end) {
2989        (StreamBound::Literal(start), StreamBound::Literal(end)) => Step::Range(*start, *end),
2990        _ => Step::RangeBy(start, end),
2991    }
2992}
2993
2994// Traversal with Typestate
2995
2996/// A complete traversal - a sequence of steps with compile-time state tracking.
2997///
2998/// The type parameter `S` tracks what kind of elements the traversal is currently
2999/// operating on, preventing invalid operation sequences at compile time.
3000///
3001/// # State Types
3002/// - `Empty` - No source step yet, only source operations allowed
3003/// - `OnNodes` - Currently on node stream, node operations allowed
3004/// - `OnEdges` - Currently on edge stream, edge operations allowed
3005/// - `Terminal` - Traversal complete, no more chaining allowed
3006///
3007/// The second type parameter `M` tracks mutation capability:
3008/// - `ReadOnly` - No mutation steps (can be used in read batches)
3009/// - `WriteEnabled` - Contains mutation steps (requires write batch)
3010#[derive(Debug, Clone, PartialEq)]
3011pub struct Traversal<S: TraversalState = OnNodes, M: MutationMode = ReadOnly> {
3012    /// The steps in this traversal.
3013    ///
3014    /// Mutating this vector directly bypasses typestate guarantees enforced by
3015    /// builder methods. Prefer builder APIs unless you intentionally need to
3016    /// manipulate raw AST steps.
3017    #[doc(hidden)]
3018    pub steps: Vec<Step>,
3019    /// Phantom data to track the typestate
3020    _state: PhantomData<S>,
3021    /// Phantom data to track mutation mode
3022    _mode: PhantomData<M>,
3023}
3024
3025impl<S: TraversalState, M: MutationMode> Default for Traversal<S, M> {
3026    fn default() -> Self {
3027        Self {
3028            steps: Vec::new(),
3029            _state: PhantomData,
3030            _mode: PhantomData,
3031        }
3032    }
3033}
3034
3035impl<S: TraversalState, M: MutationMode> Traversal<S, M> {
3036    /// Get the steps of this traversal
3037    #[doc(hidden)]
3038    pub fn into_steps(self) -> Vec<Step> {
3039        self.steps
3040    }
3041
3042    /// Check if this traversal has a terminal step
3043    #[doc(hidden)]
3044    pub fn has_terminal(&self) -> bool {
3045        self.steps.iter().any(|s| {
3046            matches!(
3047                s,
3048                Step::Count
3049                    | Step::Exists
3050                    | Step::Id
3051                    | Step::Label
3052                    | Step::Values(_)
3053                    | Step::ValueMap(_)
3054                    | Step::Project(_)
3055                    | Step::EdgeProperties
3056                    | Step::CreateIndex { .. }
3057                    | Step::DropIndex { .. }
3058                    | Step::CreateVectorIndexNodes { .. }
3059                    | Step::CreateVectorIndexEdges { .. }
3060                    | Step::CreateTextIndexNodes { .. }
3061                    | Step::CreateTextIndexEdges { .. }
3062            )
3063        })
3064    }
3065
3066    /// Create a traversal from steps
3067    ///
3068    /// This is useful for batch execution where queries are stored as Vec<Step>
3069    /// and need to be reconstructed into a Traversal.
3070    ///
3071    /// This constructor does not validate that `steps` matches `S`/`M`.
3072    /// Prefer builder entry points like [`g()`], [`read_batch()`], and [`write_batch()`]
3073    /// unless you intentionally need a raw reconstruction.
3074    #[doc(hidden)]
3075    pub fn from_steps(steps: Vec<Step>) -> Self {
3076        Self {
3077            steps,
3078            _state: PhantomData,
3079            _mode: PhantomData,
3080        }
3081    }
3082
3083    /// Add a step and transition to a new state (preserving mutation mode)
3084    fn push_step<T: TraversalState>(mut self, step: Step) -> Traversal<T, M> {
3085        self.steps.push(step);
3086        Traversal::from_steps(self.steps)
3087    }
3088
3089    /// Add a step and transition to WriteEnabled mode
3090    fn push_mutation_step<T: TraversalState>(mut self, step: Step) -> Traversal<T, WriteEnabled> {
3091        self.steps.push(step);
3092        Traversal::from_steps(self.steps)
3093    }
3094}
3095
3096// Empty State Implementation - Source Steps Only
3097
3098impl Traversal<Empty, ReadOnly> {
3099    /// Create a new empty traversal
3100    #[doc(hidden)]
3101    pub fn new() -> Self {
3102        Self {
3103            steps: Vec::new(),
3104            _state: PhantomData,
3105            _mode: PhantomData,
3106        }
3107    }
3108
3109    // Node Source Steps: Empty -> OnNodes
3110
3111    /// Start traversal from nodes by IDs or a variable.
3112    ///
3113    pub fn n(self, nodes: impl Into<NodeRef>) -> Traversal<OnNodes> {
3114        self.push_step(Step::N(nodes.into()))
3115    }
3116
3117    /// Start traversal from nodes matching a predicate
3118    ///
3119    ///
3120    pub fn n_where(self, predicate: SourcePredicate) -> Traversal<OnNodes> {
3121        self.push_step(Step::NWhere(predicate))
3122    }
3123
3124    /// Start traversal from nodes with a specific label
3125    ///
3126    /// This is a convenience method equivalent to `n_where(SourcePredicate::eq("$label", label))`.
3127    ///
3128    pub fn n_with_label(self, label: impl Into<String>) -> Traversal<OnNodes> {
3129        self.n_where(SourcePredicate::Eq(
3130            "$label".to_string(),
3131            PropertyValue::String(label.into()),
3132        ))
3133    }
3134
3135    /// Start traversal from nodes with a specific label and additional predicate
3136    ///
3137    /// This is a convenience method equivalent to
3138    /// `n_where(SourcePredicate::and(vec![SourcePredicate::eq("$label", label), predicate]))`.
3139    ///
3140    pub fn n_with_label_where(
3141        self,
3142        label: impl Into<String>,
3143        predicate: SourcePredicate,
3144    ) -> Traversal<OnNodes> {
3145        self.n_where(SourcePredicate::And(vec![
3146            SourcePredicate::Eq("$label".to_string(), PropertyValue::String(label.into())),
3147            predicate,
3148        ]))
3149    }
3150
3151    // Vector Search Source Steps
3152
3153    /// Start traversal from nodes with vectors similar to the query vector
3154    ///
3155    /// Uses the HNSW index for the given (label, property) combination to find
3156    /// the k nearest neighbors to the query vector.
3157    ///
3158    /// Runtime behavior in the current Helix interpreter:
3159    /// - returns top-k nearest hits (up to `k`) ordered by ascending distance
3160    /// - `value_map`, `values`, and `project` can read virtual fields `$id` and `$distance`
3161    /// - after traversing away from the hit stream (for example, `out`/`in_`),
3162    ///   distance metadata is no longer attached to downstream traversers
3163    ///
3164    /// # Arguments
3165    /// * `label` - The node label to search
3166    /// * `property` - The property name containing vectors
3167    /// * `query_vector` - The query vector
3168    /// * `k` - Number of nearest neighbors to return
3169    /// * `tenant_value` - Optional multitenant partition value
3170    ///
3171    pub fn vector_search_nodes(
3172        self,
3173        label: impl Into<String>,
3174        property: impl Into<String>,
3175        query_vector: Vec<f32>,
3176        k: usize,
3177        tenant_value: Option<PropertyValue>,
3178    ) -> Traversal<OnNodes> {
3179        self.vector_search_nodes_with(
3180            label,
3181            property,
3182            query_vector,
3183            k,
3184            tenant_value.map(PropertyInput::from),
3185        )
3186    }
3187
3188    /// Start traversal from nodes with vectors similar to the query vector.
3189    ///
3190    /// This variant accepts runtime-resolved inputs for the query vector, result
3191    /// count, and tenant partition value.
3192    pub fn vector_search_nodes_with(
3193        self,
3194        label: impl Into<String>,
3195        property: impl Into<String>,
3196        query_vector: impl Into<PropertyInput>,
3197        k: impl Into<StreamBound>,
3198        tenant_value: Option<PropertyInput>,
3199    ) -> Traversal<OnNodes> {
3200        self.push_step(Step::VectorSearchNodes {
3201            label: label.into(),
3202            property: property.into(),
3203            tenant_value,
3204            query_vector: query_vector.into(),
3205            k: k.into(),
3206        })
3207    }
3208
3209    /// Start traversal from nodes matching a text query.
3210    pub fn text_search_nodes(
3211        self,
3212        label: impl Into<String>,
3213        property: impl Into<String>,
3214        query_text: impl Into<String>,
3215        k: usize,
3216        tenant_value: Option<PropertyValue>,
3217    ) -> Traversal<OnNodes> {
3218        self.text_search_nodes_with(
3219            label,
3220            property,
3221            PropertyInput::from(query_text.into()),
3222            k,
3223            tenant_value.map(PropertyInput::from),
3224        )
3225    }
3226
3227    /// Start traversal from nodes matching a text query with runtime-resolved inputs.
3228    pub fn text_search_nodes_with(
3229        self,
3230        label: impl Into<String>,
3231        property: impl Into<String>,
3232        query_text: impl Into<PropertyInput>,
3233        k: impl Into<StreamBound>,
3234        tenant_value: Option<PropertyInput>,
3235    ) -> Traversal<OnNodes> {
3236        self.push_step(Step::TextSearchNodes {
3237            label: label.into(),
3238            property: property.into(),
3239            tenant_value,
3240            query_text: query_text.into(),
3241            k: k.into(),
3242        })
3243    }
3244
3245    // Edge Source Steps: Empty -> OnEdges
3246
3247    /// Start traversal from edges by IDs or a variable.
3248    ///
3249    pub fn e(self, edges: impl Into<EdgeRef>) -> Traversal<OnEdges> {
3250        self.push_step(Step::E(edges.into()))
3251    }
3252
3253    /// Start traversal from edges matching a predicate
3254    ///
3255    ///
3256    pub fn e_where(self, predicate: SourcePredicate) -> Traversal<OnEdges> {
3257        self.push_step(Step::EWhere(predicate))
3258    }
3259
3260    /// Start traversal from edges with a specific label
3261    ///
3262    /// This is a convenience method equivalent to `e_where(SourcePredicate::eq("$label", label))`.
3263    ///
3264    pub fn e_with_label(self, label: impl Into<String>) -> Traversal<OnEdges> {
3265        self.e_where(SourcePredicate::Eq(
3266            "$label".to_string(),
3267            PropertyValue::String(label.into()),
3268        ))
3269    }
3270
3271    /// Start traversal from edges with a specific label and additional predicate
3272    ///
3273    /// This is a convenience method equivalent to
3274    /// `e_where(SourcePredicate::and(vec![SourcePredicate::eq("$label", label), predicate]))`.
3275    ///
3276    pub fn e_with_label_where(
3277        self,
3278        label: impl Into<String>,
3279        predicate: SourcePredicate,
3280    ) -> Traversal<OnEdges> {
3281        self.e_where(SourcePredicate::And(vec![
3282            SourcePredicate::Eq("$label".to_string(), PropertyValue::String(label.into())),
3283            predicate,
3284        ]))
3285    }
3286
3287    /// Start traversal from edges with vectors similar to the query vector
3288    ///
3289    /// Uses the HNSW index for the given (label, property) combination to find
3290    /// the k nearest neighbors to the query vector.
3291    ///
3292    /// Runtime behavior in the current Helix interpreter:
3293    /// - returns top-k nearest hits (up to `k`) ordered by ascending distance
3294    /// - `edge_properties` includes virtual fields `$from`, `$to`, and `$distance`
3295    ///   (plus `$id` when available)
3296    ///
3297    /// # Arguments
3298    /// * `label` - The edge label to search
3299    /// * `property` - The property name containing vectors
3300    /// * `query_vector` - The query vector
3301    /// * `k` - Number of nearest neighbors to return
3302    /// * `tenant_value` - Optional multitenant partition value
3303    ///
3304    pub fn vector_search_edges(
3305        self,
3306        label: impl Into<String>,
3307        property: impl Into<String>,
3308        query_vector: Vec<f32>,
3309        k: usize,
3310        tenant_value: Option<PropertyValue>,
3311    ) -> Traversal<OnEdges> {
3312        self.vector_search_edges_with(
3313            label,
3314            property,
3315            query_vector,
3316            k,
3317            tenant_value.map(PropertyInput::from),
3318        )
3319    }
3320
3321    /// Start traversal from edges with vectors similar to the query vector.
3322    ///
3323    /// This variant accepts runtime-resolved inputs for the query vector, result
3324    /// count, and tenant partition value.
3325    pub fn vector_search_edges_with(
3326        self,
3327        label: impl Into<String>,
3328        property: impl Into<String>,
3329        query_vector: impl Into<PropertyInput>,
3330        k: impl Into<StreamBound>,
3331        tenant_value: Option<PropertyInput>,
3332    ) -> Traversal<OnEdges> {
3333        self.push_step(Step::VectorSearchEdges {
3334            label: label.into(),
3335            property: property.into(),
3336            tenant_value,
3337            query_vector: query_vector.into(),
3338            k: k.into(),
3339        })
3340    }
3341
3342    /// Start traversal from edges matching a text query.
3343    pub fn text_search_edges(
3344        self,
3345        label: impl Into<String>,
3346        property: impl Into<String>,
3347        query_text: impl Into<String>,
3348        k: usize,
3349        tenant_value: Option<PropertyValue>,
3350    ) -> Traversal<OnEdges> {
3351        self.text_search_edges_with(
3352            label,
3353            property,
3354            PropertyInput::from(query_text.into()),
3355            k,
3356            tenant_value.map(PropertyInput::from),
3357        )
3358    }
3359
3360    /// Start traversal from edges matching a text query with runtime-resolved inputs.
3361    pub fn text_search_edges_with(
3362        self,
3363        label: impl Into<String>,
3364        property: impl Into<String>,
3365        query_text: impl Into<PropertyInput>,
3366        k: impl Into<StreamBound>,
3367        tenant_value: Option<PropertyInput>,
3368    ) -> Traversal<OnEdges> {
3369        self.push_step(Step::TextSearchEdges {
3370            label: label.into(),
3371            property: property.into(),
3372            tenant_value,
3373            query_text: query_text.into(),
3374            k: k.into(),
3375        })
3376    }
3377
3378    // Mutation Source Steps: Empty -> OnNodes
3379
3380    /// Create a runtime index if it does not already exist.
3381    pub fn create_index_if_not_exists(self, spec: IndexSpec) -> Traversal<Terminal, WriteEnabled> {
3382        self.push_mutation_step(Step::CreateIndex {
3383            spec,
3384            if_not_exists: true,
3385        })
3386    }
3387
3388    /// Drop a runtime index.
3389    pub fn drop_index(self, spec: IndexSpec) -> Traversal<Terminal, WriteEnabled> {
3390        self.push_mutation_step(Step::DropIndex { spec })
3391    }
3392
3393    /// Create a vector index on nodes.
3394    ///
3395    /// This is a write-only source step intended for index management. It does not
3396    /// produce a useful traversal stream, so the builder marks it as terminal.
3397    /// Runtime index parameters are selected by the database.
3398    ///
3399    pub fn create_vector_index_nodes(
3400        self,
3401        label: impl Into<String>,
3402        property: impl Into<String>,
3403        tenant_property: Option<impl Into<String>>,
3404    ) -> Traversal<Terminal, WriteEnabled> {
3405        self.create_index_if_not_exists(IndexSpec::node_vector(label, property, tenant_property))
3406    }
3407
3408    /// Create a vector index on edges.
3409    ///
3410    /// This is a write-only source step intended for index management. It does not
3411    /// produce a useful traversal stream, so the builder marks it as terminal.
3412    /// Runtime index parameters are selected by the database.
3413    ///
3414    pub fn create_vector_index_edges(
3415        self,
3416        label: impl Into<String>,
3417        property: impl Into<String>,
3418        tenant_property: Option<impl Into<String>>,
3419    ) -> Traversal<Terminal, WriteEnabled> {
3420        self.create_index_if_not_exists(IndexSpec::edge_vector(label, property, tenant_property))
3421    }
3422
3423    /// Create a text index on nodes.
3424    pub fn create_text_index_nodes(
3425        self,
3426        label: impl Into<String>,
3427        property: impl Into<String>,
3428        tenant_property: Option<impl Into<String>>,
3429    ) -> Traversal<Terminal, WriteEnabled> {
3430        self.create_index_if_not_exists(IndexSpec::node_text(label, property, tenant_property))
3431    }
3432
3433    /// Create a text index on edges.
3434    pub fn create_text_index_edges(
3435        self,
3436        label: impl Into<String>,
3437        property: impl Into<String>,
3438        tenant_property: Option<impl Into<String>>,
3439    ) -> Traversal<Terminal, WriteEnabled> {
3440        self.create_index_if_not_exists(IndexSpec::edge_text(label, property, tenant_property))
3441    }
3442
3443    /// Add a new node with a label and optional properties.
3444    ///
3445    /// The node ID is automatically allocated.
3446    ///
3447    /// In the current Helix interpreter, this step creates exactly one node and
3448    /// starts the traversal from that node.
3449    ///
3450    pub fn add_n<K, V>(
3451        self,
3452        label: impl Into<String>,
3453        properties: Vec<(K, V)>,
3454    ) -> Traversal<OnNodes, WriteEnabled>
3455    where
3456        K: Into<String>,
3457        V: Into<PropertyInput>,
3458    {
3459        let props: Vec<(String, PropertyInput)> = properties
3460            .into_iter()
3461            .map(|(k, v)| (k.into(), v.into()))
3462            .collect();
3463        self.push_mutation_step(Step::AddN {
3464            label: label.into(),
3465            properties: props,
3466        })
3467    }
3468
3469    /// Start from nodes stored in a variable (Empty -> OnNodes).
3470    ///
3471    /// This is a convenience for starting from a node set previously saved via
3472    /// `store()` / `as_()` in the same traversal context.
3473    ///
3474    /// If you want to start from a variable that yields node IDs via other means
3475    /// (for example, an `id()` terminal result), prefer `n(NodeRef::var(name))`.
3476    ///
3477    pub fn inject(self, var_name: impl Into<String>) -> Traversal<OnNodes, ReadOnly> {
3478        self.push_step(Step::Inject(var_name.into()))
3479    }
3480
3481    /// Delete specific edges by their IDs without needing a source
3482    ///
3483    /// This is the multigraph-safe way to remove edges, as it removes specific
3484    /// edges rather than all edges between a pair of nodes.
3485    ///
3486    pub fn drop_edge_by_id(self, edges: impl Into<EdgeRef>) -> Traversal<OnNodes, WriteEnabled> {
3487        self.push_mutation_step(Step::DropEdgeById(edges.into()))
3488    }
3489}
3490
3491// OnNodes State Implementation
3492
3493impl<M: MutationMode> Traversal<OnNodes, M> {
3494    // Navigation Steps: OnNodes -> OnNodes
3495
3496    /// Traverse outgoing edges, optionally filtered by label
3497    pub fn out(self, label: Option<impl Into<String>>) -> Traversal<OnNodes, M> {
3498        self.push_step(Step::Out(label.map(|l| l.into())))
3499    }
3500
3501    /// Traverse incoming edges, optionally filtered by label
3502    pub fn in_(self, label: Option<impl Into<String>>) -> Traversal<OnNodes, M> {
3503        self.push_step(Step::In(label.map(|l| l.into())))
3504    }
3505
3506    /// Traverse edges in both directions, optionally filtered by label
3507    pub fn both(self, label: Option<impl Into<String>>) -> Traversal<OnNodes, M> {
3508        self.push_step(Step::Both(label.map(|l| l.into())))
3509    }
3510
3511    // Edge Traversal Steps: OnNodes -> OnEdges
3512
3513    /// Traverse to outgoing edges
3514    pub fn out_e(self, label: Option<impl Into<String>>) -> Traversal<OnEdges, M> {
3515        self.push_step(Step::OutE(label.map(|l| l.into())))
3516    }
3517
3518    /// Traverse to incoming edges
3519    pub fn in_e(self, label: Option<impl Into<String>>) -> Traversal<OnEdges, M> {
3520        self.push_step(Step::InE(label.map(|l| l.into())))
3521    }
3522
3523    /// Traverse to edges in both directions
3524    pub fn both_e(self, label: Option<impl Into<String>>) -> Traversal<OnEdges, M> {
3525        self.push_step(Step::BothE(label.map(|l| l.into())))
3526    }
3527
3528    // Node Filter Steps: OnNodes -> OnNodes
3529
3530    /// Filter by property value
3531    pub fn has(self, property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
3532        self.push_step(Step::Has(property.into(), value.into()))
3533    }
3534
3535    /// Filter by label (shorthand for has("$label", value))
3536    pub fn has_label(self, label: impl Into<String>) -> Self {
3537        self.push_step(Step::HasLabel(label.into()))
3538    }
3539
3540    /// Filter by property existence
3541    pub fn has_key(self, property: impl Into<String>) -> Self {
3542        self.push_step(Step::HasKey(property.into()))
3543    }
3544
3545    /// Filter by a complex predicate
3546    pub fn where_(self, predicate: Predicate) -> Self {
3547        self.push_step(Step::Where(predicate))
3548    }
3549
3550    /// Remove duplicates from the stream
3551    pub fn dedup(self) -> Self {
3552        self.push_step(Step::Dedup)
3553    }
3554
3555    /// Filter to nodes that exist in a variable
3556    pub fn within(self, var_name: impl Into<String>) -> Self {
3557        self.push_step(Step::Within(var_name.into()))
3558    }
3559
3560    /// Filter to nodes that do NOT exist in a variable
3561    pub fn without(self, var_name: impl Into<String>) -> Self {
3562        self.push_step(Step::Without(var_name.into()))
3563    }
3564
3565    // Limit Steps: OnNodes -> OnNodes
3566
3567    /// Take at most N items.
3568    pub fn limit(self, n: impl Into<StreamBound>) -> Self {
3569        self.push_step(limit_step(n))
3570    }
3571
3572    /// Skip the first N items.
3573    pub fn skip(self, n: impl Into<StreamBound>) -> Self {
3574        self.push_step(skip_step(n))
3575    }
3576
3577    /// Get items in a range [start, end)
3578    ///
3579    /// Equivalent to `.skip(start).limit(end - start)` but more concise.
3580    ///
3581    pub fn range(self, start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Self {
3582        self.push_step(range_step(start, end))
3583    }
3584
3585    // Variable Steps: OnNodes -> OnNodes
3586
3587    /// Store the current node stream in the traversal context under `name`.
3588    ///
3589    /// This is identical to `store()`; it exists for Gremlin-style naming.
3590    pub fn as_(self, name: impl Into<String>) -> Self {
3591        self.push_step(Step::As(name.into()))
3592    }
3593
3594    /// Store the current node stream in the traversal context under `name`.
3595    ///
3596    /// This does not change the current stream; it only creates/overwrites a
3597    /// named binding that later steps can reference.
3598    pub fn store(self, name: impl Into<String>) -> Self {
3599        self.push_step(Step::Store(name.into()))
3600    }
3601
3602    /// Replace the current node stream with nodes referenced by a variable.
3603    ///
3604    /// Use this when you want to *switch* streams. If you want to *merge* a stored
3605    /// node set into the current stream, use `inject()`.
3606    pub fn select(self, name: impl Into<String>) -> Self {
3607        self.push_step(Step::Select(name.into()))
3608    }
3609
3610    /// Union the current node stream with nodes stored in `var_name`.
3611    ///
3612    /// This keeps the current stream and adds any nodes stored in the named
3613    /// variable. Use `select()` to replace the stream instead.
3614    ///
3615    pub fn inject(self, var_name: impl Into<String>) -> Self {
3616        self.push_step(Step::Inject(var_name.into()))
3617    }
3618
3619    // Terminal Steps: OnNodes -> Terminal
3620
3621    /// Count the number of results
3622    pub fn count(self) -> Traversal<Terminal, M> {
3623        self.push_step(Step::Count)
3624    }
3625
3626    /// Check if any results exist
3627    pub fn exists(self) -> Traversal<Terminal, M> {
3628        self.push_step(Step::Exists)
3629    }
3630
3631    /// Get the ID of current nodes
3632    ///
3633    /// Returns the node ID as a value. Useful when you need
3634    /// to extract just the ID without other properties.
3635    ///
3636    pub fn id(self) -> Traversal<Terminal, M> {
3637        self.push_step(Step::Id)
3638    }
3639
3640    /// Get the label of current nodes
3641    ///
3642    /// Returns the $label property value.
3643    ///
3644    pub fn label(self) -> Traversal<Terminal, M> {
3645        self.push_step(Step::Label)
3646    }
3647
3648    /// Get specific property values from current nodes
3649    pub fn values(self, properties: Vec<impl Into<String>>) -> Traversal<Terminal, M> {
3650        self.push_step(Step::Values(
3651            properties.into_iter().map(|p| p.into()).collect(),
3652        ))
3653    }
3654
3655    /// Get properties as a map, optionally filtered to specific properties
3656    pub fn value_map(self, properties: Option<Vec<impl Into<String>>>) -> Traversal<Terminal, M> {
3657        self.push_step(Step::ValueMap(
3658            properties.map(|ps| ps.into_iter().map(|p| p.into()).collect()),
3659        ))
3660    }
3661
3662    /// Project properties and expressions with optional renaming.
3663    pub fn project<P>(self, projections: Vec<P>) -> Traversal<Terminal, M>
3664    where
3665        P: Into<Projection>,
3666    {
3667        self.push_step(Step::Project(
3668            projections.into_iter().map(Into::into).collect(),
3669        ))
3670    }
3671
3672    // Ordering Steps: OnNodes -> OnNodes
3673
3674    /// Order results by a property.
3675    ///
3676    /// Note: some interpreters represent intermediate streams as sets. In those
3677    /// engines, ordering may not be preserved in the returned node set.
3678    ///
3679    pub fn order_by(self, property: impl Into<String>, order: Order) -> Self {
3680        self.push_step(Step::OrderBy(property.into(), order))
3681    }
3682
3683    /// Order results by multiple properties with priorities.
3684    ///
3685    /// Note: some interpreters represent intermediate streams as sets. In those
3686    /// engines, ordering may not be preserved in the returned node set.
3687    ///
3688    pub fn order_by_multiple(self, orderings: Vec<(impl Into<String>, Order)>) -> Self {
3689        let orderings: Vec<(String, Order)> =
3690            orderings.into_iter().map(|(p, o)| (p.into(), o)).collect();
3691        self.push_step(Step::OrderByMultiple(orderings))
3692    }
3693
3694    // Loop/Repeat Steps: OnNodes -> OnNodes
3695
3696    /// Repeat a traversal with configuration
3697    ///
3698    pub fn repeat(self, config: RepeatConfig) -> Self {
3699        self.push_step(Step::Repeat(config))
3700    }
3701
3702    // Branching Steps: OnNodes -> OnNodes
3703
3704    /// Execute multiple traversals and merge their results
3705    ///
3706    pub fn union(self, traversals: Vec<SubTraversal>) -> Self {
3707        self.push_step(Step::Union(traversals))
3708    }
3709
3710    /// Conditional execution based on a predicate
3711    ///
3712    pub fn choose(
3713        self,
3714        condition: Predicate,
3715        then_traversal: SubTraversal,
3716        else_traversal: Option<SubTraversal>,
3717    ) -> Self {
3718        self.push_step(Step::Choose {
3719            condition,
3720            then_traversal,
3721            else_traversal,
3722        })
3723    }
3724
3725    /// Try traversals in order until one produces results
3726    ///
3727    pub fn coalesce(self, traversals: Vec<SubTraversal>) -> Self {
3728        self.push_step(Step::Coalesce(traversals))
3729    }
3730
3731    /// Execute a traversal per input item and fall back to the original item when that
3732    /// input produces no results.
3733    ///
3734    /// Note: when the optional branch changes the runtime stream family (for example,
3735    /// nodes to edges), unmatched inputs drop out of that branch result instead of
3736    /// producing nullable row bindings.
3737    ///
3738    pub fn optional(self, traversal: SubTraversal) -> Self {
3739        self.push_step(Step::Optional(traversal))
3740    }
3741
3742    // Aggregation Steps: OnNodes -> OnNodes (or Terminal for some)
3743
3744    /// Group nodes by a property value.
3745    pub fn group(self, property: impl Into<String>) -> Traversal<Terminal, M> {
3746        self.push_step(Step::Group(property.into()))
3747    }
3748
3749    /// Count occurrences grouped by a property.
3750    pub fn group_count(self, property: impl Into<String>) -> Traversal<Terminal, M> {
3751        self.push_step(Step::GroupCount(property.into()))
3752    }
3753
3754    /// Apply an aggregation function to a property.
3755    pub fn aggregate_by(
3756        self,
3757        function: AggregateFunction,
3758        property: impl Into<String>,
3759    ) -> Traversal<Terminal, M> {
3760        self.push_step(Step::AggregateBy(function, property.into()))
3761    }
3762
3763    /// Barrier step.
3764    ///
3765    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3766    pub fn fold(self) -> Self {
3767        self.push_step(Step::Fold)
3768    }
3769
3770    /// Expand a collected list back into individual items.
3771    ///
3772    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3773    pub fn unfold(self) -> Self {
3774        self.push_step(Step::Unfold)
3775    }
3776
3777    // Path Steps: OnNodes -> OnNodes
3778
3779    /// Include the full traversal path in results.
3780    ///
3781    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3782    ///
3783    pub fn path(self) -> Self {
3784        self.push_step(Step::Path)
3785    }
3786
3787    /// Filter to only simple paths (no repeated nodes).
3788    ///
3789    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3790    pub fn simple_path(self) -> Self {
3791        self.push_step(Step::SimplePath)
3792    }
3793
3794    // Sack Steps: OnNodes -> OnNodes
3795
3796    /// Initialize a sack (traverser-local state) with a value.
3797    ///
3798    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3799    ///
3800    pub fn with_sack(self, initial: PropertyValue) -> Self {
3801        self.push_step(Step::WithSack(initial))
3802    }
3803
3804    /// Set the sack to a property value from the current node.
3805    ///
3806    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3807    pub fn sack_set(self, property: impl Into<String>) -> Self {
3808        self.push_step(Step::SackSet(property.into()))
3809    }
3810
3811    /// Add a property value to the sack (numeric types only).
3812    ///
3813    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3814    pub fn sack_add(self, property: impl Into<String>) -> Self {
3815        self.push_step(Step::SackAdd(property.into()))
3816    }
3817
3818    /// Get the current sack value.
3819    ///
3820    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3821    pub fn sack_get(self) -> Self {
3822        self.push_step(Step::SackGet)
3823    }
3824
3825    // Mutation Steps: OnNodes -> OnNodes (WriteEnabled)
3826
3827    /// Add a new node with a label and optional properties.
3828    ///
3829    /// The node ID is automatically allocated.
3830    ///
3831    /// In the current Helix interpreter, this step creates exactly one node and
3832    /// replaces the current node stream with that new node.
3833    pub fn add_n<K, V>(
3834        self,
3835        label: impl Into<String>,
3836        properties: Vec<(K, V)>,
3837    ) -> Traversal<OnNodes, WriteEnabled>
3838    where
3839        K: Into<String>,
3840        V: Into<PropertyInput>,
3841    {
3842        let props: Vec<(String, PropertyInput)> = properties
3843            .into_iter()
3844            .map(|(k, v)| (k.into(), v.into()))
3845            .collect();
3846        self.push_mutation_step(Step::AddN {
3847            label: label.into(),
3848            properties: props,
3849        })
3850    }
3851
3852    /// Add edges from the current nodes to target nodes.
3853    ///
3854    /// In the current Helix interpreter, this creates edges for every pair in the
3855    /// cartesian product `current_nodes x target_nodes` and leaves the current
3856    /// node stream unchanged.
3857    ///
3858    pub fn add_e<K, V>(
3859        self,
3860        label: impl Into<String>,
3861        to: impl Into<NodeRef>,
3862        properties: Vec<(K, V)>,
3863    ) -> Traversal<OnNodes, WriteEnabled>
3864    where
3865        K: Into<String>,
3866        V: Into<PropertyInput>,
3867    {
3868        let props: Vec<(String, PropertyInput)> = properties
3869            .into_iter()
3870            .map(|(k, v)| (k.into(), v.into()))
3871            .collect();
3872        self.push_mutation_step(Step::AddE {
3873            label: label.into(),
3874            to: to.into(),
3875            properties: props,
3876        })
3877    }
3878
3879    /// Set a property on current nodes
3880    pub fn set_property(
3881        self,
3882        name: impl Into<String>,
3883        value: impl Into<PropertyInput>,
3884    ) -> Traversal<OnNodes, WriteEnabled> {
3885        self.push_mutation_step(Step::SetProperty(name.into(), value.into()))
3886    }
3887
3888    /// Remove a property from current nodes
3889    pub fn remove_property(self, name: impl Into<String>) -> Traversal<OnNodes, WriteEnabled> {
3890        self.push_mutation_step(Step::RemoveProperty(name.into()))
3891    }
3892
3893    /// Delete current nodes and their edges
3894    pub fn drop(self) -> Traversal<OnNodes, WriteEnabled> {
3895        self.push_mutation_step(Step::Drop)
3896    }
3897
3898    /// Delete edges from current nodes to target nodes
3899    ///
3900    /// **Note**: In multigraph scenarios, this removes ALL edges between the current
3901    /// nodes and the target nodes. Use `drop_edge_by_id` for precise edge removal.
3902    pub fn drop_edge(self, to: impl Into<NodeRef>) -> Traversal<OnNodes, WriteEnabled> {
3903        self.push_mutation_step(Step::DropEdge(to.into()))
3904    }
3905
3906    /// Delete only edges with a specific label from current nodes to target nodes.
3907    pub fn drop_edge_labeled(
3908        self,
3909        to: impl Into<NodeRef>,
3910        label: impl Into<String>,
3911    ) -> Traversal<OnNodes, WriteEnabled> {
3912        self.push_mutation_step(Step::DropEdgeLabeled {
3913            to: to.into(),
3914            label: label.into(),
3915        })
3916    }
3917
3918    /// Delete specific edges by their IDs
3919    ///
3920    /// This is the multigraph-safe way to remove edges, as it removes specific
3921    /// edges rather than all edges between a pair of nodes.
3922    ///
3923    pub fn drop_edge_by_id(self, edges: impl Into<EdgeRef>) -> Traversal<OnNodes, WriteEnabled> {
3924        self.push_mutation_step(Step::DropEdgeById(edges.into()))
3925    }
3926}
3927
3928// OnEdges State Implementation
3929
3930impl<M: MutationMode> Traversal<OnEdges, M> {
3931    // Node Extraction Steps: OnEdges -> OnNodes
3932
3933    /// From edge, get the target node
3934    pub fn out_n(self) -> Traversal<OnNodes, M> {
3935        self.push_step(Step::OutN)
3936    }
3937
3938    /// From edge, get the source node
3939    pub fn in_n(self) -> Traversal<OnNodes, M> {
3940        self.push_step(Step::InN)
3941    }
3942
3943    /// From edge, get the "other" node (not the one we came from)
3944    pub fn other_n(self) -> Traversal<OnNodes, M> {
3945        self.push_step(Step::OtherN)
3946    }
3947
3948    // Edge Filter Steps: OnEdges -> OnEdges
3949
3950    /// Filter edges by property value
3951    pub fn edge_has(self, property: impl Into<String>, value: impl Into<PropertyInput>) -> Self {
3952        self.push_step(Step::EdgeHas(property.into(), value.into()))
3953    }
3954
3955    /// Filter edges by label
3956    pub fn edge_has_label(self, label: impl Into<String>) -> Self {
3957        self.push_step(Step::EdgeHasLabel(label.into()))
3958    }
3959
3960    /// Remove duplicates from the stream
3961    pub fn dedup(self) -> Self {
3962        self.push_step(Step::Dedup)
3963    }
3964
3965    // Limit Steps: OnEdges -> OnEdges
3966
3967    /// Take at most N items.
3968    pub fn limit(self, n: impl Into<StreamBound>) -> Self {
3969        self.push_step(limit_step(n))
3970    }
3971
3972    /// Skip the first N items.
3973    pub fn skip(self, n: impl Into<StreamBound>) -> Self {
3974        self.push_step(skip_step(n))
3975    }
3976
3977    /// Get items in a range [start, end)
3978    pub fn range(self, start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Self {
3979        self.push_step(range_step(start, end))
3980    }
3981
3982    // Variable Steps: OnEdges -> OnEdges
3983
3984    /// Store current edges with a name for later reference
3985    pub fn as_(self, name: impl Into<String>) -> Self {
3986        self.push_step(Step::As(name.into()))
3987    }
3988
3989    /// Store current edges to a variable (same as `as_`)
3990    pub fn store(self, name: impl Into<String>) -> Self {
3991        self.push_step(Step::Store(name.into()))
3992    }
3993
3994    // Terminal Steps: OnEdges -> Terminal
3995
3996    /// Count the number of edges
3997    pub fn count(self) -> Traversal<Terminal, M> {
3998        self.push_step(Step::Count)
3999    }
4000
4001    /// Check if any edges exist
4002    pub fn exists(self) -> Traversal<Terminal, M> {
4003        self.push_step(Step::Exists)
4004    }
4005
4006    /// Get the ID of current edges
4007    pub fn id(self) -> Traversal<Terminal, M> {
4008        self.push_step(Step::Id)
4009    }
4010
4011    /// Get the label of current edges
4012    pub fn label(self) -> Traversal<Terminal, M> {
4013        self.push_step(Step::Label)
4014    }
4015
4016    /// Get edge properties
4017    pub fn edge_properties(self) -> Traversal<Terminal, M> {
4018        self.push_step(Step::EdgeProperties)
4019    }
4020
4021    // Ordering Steps: OnEdges -> OnEdges
4022
4023    /// Order results by a property.
4024    ///
4025    /// Note: some interpreters represent intermediate streams as sets. In those
4026    /// engines, ordering may not be preserved in the returned edge set.
4027    pub fn order_by(self, property: impl Into<String>, order: Order) -> Self {
4028        self.push_step(Step::OrderBy(property.into(), order))
4029    }
4030}
4031
4032// Terminal State - No additional methods (traversal is complete)
4033
4034// Terminal has no additional methods - the traversal is complete
4035
4036// Entry Point
4037
4038/// Create a new traversal - the entry point for building queries
4039///
4040pub fn g() -> Traversal<Empty> {
4041    Traversal::new()
4042}
4043
4044// Batch Query Types
4045
4046/// Condition for conditional query execution within a batch
4047///
4048#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4049pub enum BatchCondition {
4050    /// Execute only if the named variable is not empty
4051    VarNotEmpty(String),
4052    /// Execute only if the named variable is empty
4053    VarEmpty(String),
4054    /// Execute only if the named variable has at least N items
4055    VarMinSize(String, usize),
4056    /// Execute only if the previous query result was not empty
4057    PrevNotEmpty,
4058}
4059
4060/// A single query within a batch
4061#[doc(hidden)]
4062#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4063pub struct NamedQuery {
4064    /// Variable name to store result (required for var_as)
4065    pub name: Option<String>,
4066    /// The traversal steps to execute for this query.
4067    pub steps: Vec<Step>,
4068    /// Skip if condition fails
4069    pub condition: Option<BatchCondition>,
4070}
4071
4072/// A batch entry executed in sequence.
4073#[doc(hidden)]
4074#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4075pub enum BatchEntry {
4076    /// Execute a single traversal query.
4077    Query(NamedQuery),
4078    /// Execute the enclosed entries once per object in the named array param.
4079    ForEach {
4080        /// The top-level parameter containing an array of objects.
4081        param: String,
4082        /// Entries to execute for each object.
4083        body: Vec<BatchEntry>,
4084    },
4085}
4086
4087/// A batch of read-only queries for sequential execution in one transaction
4088///
4089/// This allows multiple related read queries to be executed atomically,
4090/// with results stored in named variables that can be referenced
4091/// by subsequent queries and returned as a structured result.
4092///
4093/// **Important**: ReadBatch only accepts read-only traversals (no mutations).
4094/// Attempting to add a traversal containing mutation steps will fail at compile time.
4095///
4096#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
4097pub struct ReadBatch {
4098    /// Queries to execute in order.
4099    ///
4100    /// Most users should build this with [`ReadBatch::var_as`] / [`ReadBatch::var_as_if`]
4101    /// to preserve type-level read-only guarantees.
4102    #[doc(hidden)]
4103    pub queries: Vec<BatchEntry>,
4104    /// Variables to include in final result (empty = all named variables)
4105    #[doc(hidden)]
4106    pub returns: Vec<String>,
4107}
4108
4109impl ReadBatch {
4110    /// Create a new empty read batch
4111    #[doc(hidden)]
4112    pub fn new() -> Self {
4113        Self {
4114            queries: Vec::new(),
4115            returns: Vec::new(),
4116        }
4117    }
4118
4119    /// Add a read-only query that stores result in a named variable
4120    ///
4121    /// The traversal is executed and its result is stored in a variable
4122    /// that can be referenced by subsequent queries using `NodeRef::var()`.
4123    ///
4124    /// **Note**: Only accepts read-only traversals. Mutation traversals will fail at compile time.
4125    ///
4126    pub fn var_as<S: TraversalState>(
4127        mut self,
4128        name: &str,
4129        traversal: Traversal<S, ReadOnly>,
4130    ) -> Self {
4131        self.queries.push(BatchEntry::Query(NamedQuery {
4132            name: Some(name.to_string()),
4133            steps: traversal.into_steps(),
4134            condition: None,
4135        }));
4136        self
4137    }
4138
4139    /// Add a conditional read-only query that only executes if the condition is met
4140    ///
4141    pub fn var_as_if<S: TraversalState>(
4142        mut self,
4143        name: &str,
4144        condition: BatchCondition,
4145        traversal: Traversal<S, ReadOnly>,
4146    ) -> Self {
4147        self.queries.push(BatchEntry::Query(NamedQuery {
4148            name: Some(name.to_string()),
4149            steps: traversal.into_steps(),
4150            condition: Some(condition),
4151        }));
4152        self
4153    }
4154
4155    /// Execute the provided body once per object in the named array parameter.
4156    pub fn for_each_param(mut self, param: &str, body: ReadBatch) -> Self {
4157        self.queries.push(BatchEntry::ForEach {
4158            param: param.to_string(),
4159            body: body.queries,
4160        });
4161        self
4162    }
4163
4164    /// Specify which variables to return (call at end)
4165    ///
4166    /// If not called, all named variables are returned.
4167    ///
4168    pub fn returning<I, S>(mut self, vars: I) -> Self
4169    where
4170        I: IntoIterator<Item = S>,
4171        S: Into<String>,
4172    {
4173        self.returns = vars.into_iter().map(|s| s.into()).collect();
4174        self
4175    }
4176}
4177
4178/// A batch of write queries for sequential execution in one transaction
4179///
4180/// This allows multiple related queries (including mutations) to be executed atomically,
4181/// with results stored in named variables that can be referenced
4182/// by subsequent queries and returned as a structured result.
4183///
4184/// **Note**: WriteBatch accepts both read-only and mutation traversals.
4185///
4186#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
4187pub struct WriteBatch {
4188    /// Queries to execute in order.
4189    ///
4190    /// Most users should build this with [`WriteBatch::var_as`] / [`WriteBatch::var_as_if`]
4191    /// for clearer intent and safer construction.
4192    #[doc(hidden)]
4193    pub queries: Vec<BatchEntry>,
4194    /// Variables to include in final result (empty = all named variables)
4195    #[doc(hidden)]
4196    pub returns: Vec<String>,
4197}
4198
4199impl WriteBatch {
4200    /// Create a new empty write batch
4201    #[doc(hidden)]
4202    pub fn new() -> Self {
4203        Self {
4204            queries: Vec::new(),
4205            returns: Vec::new(),
4206        }
4207    }
4208
4209    /// Add a query that stores result in a named variable
4210    ///
4211    /// The traversal is executed and its result is stored in a variable
4212    /// that can be referenced by subsequent queries using `NodeRef::var()`.
4213    ///
4214    /// Accepts both read-only and mutation traversals.
4215    ///
4216    pub fn var_as<S: TraversalState, M: MutationMode>(
4217        mut self,
4218        name: &str,
4219        traversal: Traversal<S, M>,
4220    ) -> Self {
4221        self.queries.push(BatchEntry::Query(NamedQuery {
4222            name: Some(name.to_string()),
4223            steps: traversal.into_steps(),
4224            condition: None,
4225        }));
4226        self
4227    }
4228
4229    /// Add a conditional query that only executes if the condition is met
4230    ///
4231    pub fn var_as_if<S: TraversalState, M: MutationMode>(
4232        mut self,
4233        name: &str,
4234        condition: BatchCondition,
4235        traversal: Traversal<S, M>,
4236    ) -> Self {
4237        self.queries.push(BatchEntry::Query(NamedQuery {
4238            name: Some(name.to_string()),
4239            steps: traversal.into_steps(),
4240            condition: Some(condition),
4241        }));
4242        self
4243    }
4244
4245    /// Execute the provided body once per object in the named array parameter.
4246    pub fn for_each_param(mut self, param: &str, body: WriteBatch) -> Self {
4247        self.queries.push(BatchEntry::ForEach {
4248            param: param.to_string(),
4249            body: body.queries,
4250        });
4251        self
4252    }
4253
4254    /// Specify which variables to return (call at end)
4255    ///
4256    /// If not called, all named variables are returned.
4257    ///
4258    pub fn returning<I, S>(mut self, vars: I) -> Self
4259    where
4260        I: IntoIterator<Item = S>,
4261        S: Into<String>,
4262    {
4263        self.returns = vars.into_iter().map(|s| s.into()).collect();
4264        self
4265    }
4266}
4267
4268/// A batch query payload for wire transport or storage
4269#[doc(hidden)]
4270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4271#[serde(untagged)]
4272pub enum BatchQuery {
4273    /// Read-only batch
4274    Read(ReadBatch),
4275    /// Write-capable batch
4276    Write(WriteBatch),
4277}
4278
4279/// Errors returned while building or serializing a dynamic query request payload.
4280#[derive(Debug)]
4281pub enum DynamicQueryError {
4282    /// Failed to serialize the request to JSON.
4283    Serialize(sonic_rs::Error),
4284    /// Failed to decode serialized JSON bytes as UTF-8.
4285    Utf8(std::string::FromUtf8Error),
4286    /// Dynamic query JSON cannot faithfully represent a bytes parameter.
4287    UnsupportedBytesParameter(String),
4288    /// A datetime parameter could not be rendered as RFC3339.
4289    InvalidDateTimeParameter {
4290        /// Parameter path within the request payload.
4291        path: String,
4292        /// Raw UTC epoch milliseconds that failed to render.
4293        millis: i64,
4294    },
4295}
4296
4297impl DynamicQueryError {
4298    /// Build an error for a bytes parameter path that cannot be represented safely.
4299    pub fn unsupported_bytes(path: impl Into<String>) -> Self {
4300        Self::UnsupportedBytesParameter(path.into())
4301    }
4302
4303    /// Build an error for a datetime value that cannot be rendered safely.
4304    pub fn invalid_datetime(path: impl Into<String>, millis: i64) -> Self {
4305        Self::InvalidDateTimeParameter {
4306            path: path.into(),
4307            millis,
4308        }
4309    }
4310}
4311
4312impl std::fmt::Display for DynamicQueryError {
4313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4314        match self {
4315            Self::Serialize(err) => write!(f, "json serialization error: {err}"),
4316            Self::Utf8(err) => write!(f, "utf8 conversion error: {err}"),
4317            Self::UnsupportedBytesParameter(path) => write!(
4318                f,
4319                "parameter '{path}' uses bytes, which the dynamic query JSON route cannot represent"
4320            ),
4321            Self::InvalidDateTimeParameter { path, millis } => write!(
4322                f,
4323                "parameter '{path}' uses datetime millis '{millis}', which cannot be rendered as RFC3339"
4324            ),
4325        }
4326    }
4327}
4328
4329impl std::error::Error for DynamicQueryError {
4330    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
4331        match self {
4332            Self::Serialize(err) => Some(err),
4333            Self::Utf8(err) => Some(err),
4334            Self::UnsupportedBytesParameter(_) => None,
4335            Self::InvalidDateTimeParameter { .. } => None,
4336        }
4337    }
4338}
4339
4340impl From<sonic_rs::Error> for DynamicQueryError {
4341    fn from(value: sonic_rs::Error) -> Self {
4342        Self::Serialize(value)
4343    }
4344}
4345
4346impl From<std::string::FromUtf8Error> for DynamicQueryError {
4347    fn from(value: std::string::FromUtf8Error) -> Self {
4348        Self::Utf8(value)
4349    }
4350}
4351
4352/// Request type accepted by the gateway dynamic query route.
4353#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4354#[serde(rename_all = "lowercase")]
4355pub enum DynamicQueryRequestType {
4356    /// Read-only dynamic query request.
4357    Read,
4358    /// Write-capable dynamic query request.
4359    Write,
4360}
4361
4362/// JSON-compatible parameter value for a dynamic query request payload.
4363#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4364#[serde(untagged)]
4365pub enum DynamicQueryValue {
4366    /// Null JSON value.
4367    Null,
4368    /// Boolean JSON value.
4369    Bool(bool),
4370    /// 64-bit signed integer JSON value.
4371    I64(i64),
4372    /// 64-bit floating-point JSON value.
4373    F64(f64),
4374    /// 32-bit floating-point JSON value.
4375    F32(f32),
4376    /// UTF-8 string JSON value.
4377    String(String),
4378    /// Array JSON value.
4379    Array(Vec<DynamicQueryValue>),
4380    /// Object JSON value.
4381    Object(BTreeMap<String, DynamicQueryValue>),
4382}
4383
4384/// Full JSON payload accepted by the gateway dynamic query route.
4385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4386pub struct DynamicQueryRequest {
4387    /// Whether the inline query should execute as a read or write.
4388    #[serde(rename = "request_type")]
4389    pub request_type: DynamicQueryRequestType,
4390    /// Inline query AST payload.
4391    pub query: BatchQuery,
4392    /// Runtime parameters forwarded to the query engine.
4393    #[serde(default, skip_serializing_if = "Option::is_none")]
4394    pub parameters: Option<BTreeMap<String, DynamicQueryValue>>,
4395    /// Optional parameter schema used by runtimes to coerce typed inputs.
4396    #[serde(default, skip_serializing_if = "Option::is_none")]
4397    pub parameter_types: Option<BTreeMap<String, QueryParamType>>,
4398}
4399
4400impl DynamicQueryRequest {
4401    fn new(request_type: DynamicQueryRequestType, query: BatchQuery) -> Self {
4402        Self {
4403            request_type,
4404            query,
4405            parameters: None,
4406            parameter_types: None,
4407        }
4408    }
4409
4410    /// Create a dynamic request payload for a read batch.
4411    pub fn read(query: ReadBatch) -> Self {
4412        Self::new(DynamicQueryRequestType::Read, BatchQuery::Read(query))
4413    }
4414
4415    /// Create a dynamic request payload for a write batch.
4416    pub fn write(query: WriteBatch) -> Self {
4417        Self::new(DynamicQueryRequestType::Write, BatchQuery::Write(query))
4418    }
4419
4420    /// Insert a named parameter value into the request payload.
4421    pub fn insert_parameter_value(&mut self, name: impl Into<String>, value: DynamicQueryValue) {
4422        self.parameters
4423            .get_or_insert_with(BTreeMap::new)
4424            .insert(name.into(), value);
4425    }
4426
4427    /// Insert a named parameter type into the request payload.
4428    pub fn insert_parameter_type(&mut self, name: impl Into<String>, ty: QueryParamType) {
4429        self.parameter_types
4430            .get_or_insert_with(BTreeMap::new)
4431            .insert(name.into(), ty);
4432    }
4433
4434    /// Insert a named parameter value and return the updated request.
4435    pub fn with_parameter_value(
4436        mut self,
4437        name: impl Into<String>,
4438        value: DynamicQueryValue,
4439    ) -> Self {
4440        self.insert_parameter_value(name, value);
4441        self
4442    }
4443
4444    /// Insert a named parameter type and return the updated request.
4445    pub fn with_parameter_type(mut self, name: impl Into<String>, ty: QueryParamType) -> Self {
4446        self.insert_parameter_type(name, ty);
4447        self
4448    }
4449
4450    /// Serialize the request payload to JSON bytes.
4451    pub fn to_json_bytes(&self) -> Result<Vec<u8>, DynamicQueryError> {
4452        Ok(sonic_rs::to_vec(self)?)
4453    }
4454
4455    /// Serialize the request payload to a JSON string.
4456    pub fn to_json_string(&self) -> Result<String, DynamicQueryError> {
4457        Ok(String::from_utf8(self.to_json_bytes()?)?)
4458    }
4459}
4460
4461/// Create a new read batch - the entry point for read-only multi-query transactions
4462///
4463pub fn read_batch() -> ReadBatch {
4464    ReadBatch::new()
4465}
4466
4467/// Create a new write batch - the entry point for write multi-query transactions
4468///
4469pub fn write_batch() -> WriteBatch {
4470    WriteBatch::new()
4471}
4472
4473/// Common query-builder imports.
4474///
4475/// This module re-exports the APIs most users reach for when building read and
4476/// write batches.
4477///
4478/// Typical usage in application code: `use helix_dsl::prelude::*;`
4479#[allow(missing_docs)]
4480pub mod prelude {
4481    pub use crate::{
4482        g, read_batch, register, sub, write_batch, AggregateFunction, BatchCondition, BatchEntry,
4483        CompareOp, DateTime, DynamicQueryError, DynamicQueryRequest, DynamicQueryRequestType,
4484        DynamicQueryValue, EdgeId, EdgeRef, EmitBehavior, Expr, ExprProjection, IndexSpec, NodeId,
4485        NodeRef, Order, ParamObject, ParamValue, Predicate, Projection, PropertyInput,
4486        PropertyProjection, PropertyValue, ReadBatch, RepeatConfig, SourcePredicate, StreamBound,
4487        SubTraversal, Traversal, WriteBatch,
4488    };
4489}
4490
4491/// Helper type alias for property maps
4492#[doc(hidden)]
4493pub type PropertyMap = HashMap<String, PropertyValue>;
4494
4495#[cfg(test)]
4496mod tests {
4497    use std::collections::BTreeMap;
4498
4499    use crate::query_generator::{
4500        deserialize_query_bundle, serialize_query_bundle, GenerateError, QueryBundle,
4501        QueryParamType, QueryParameter, QUERY_BUNDLE_VERSION,
4502    };
4503
4504    use super::*;
4505
4506    fn query_entry(entry: &BatchEntry) -> &NamedQuery {
4507        match entry {
4508            BatchEntry::Query(query) => query,
4509            other => panic!("expected query entry, got {other:?}"),
4510        }
4511    }
4512
4513    #[test]
4514    fn query_bundle_roundtrip_with_bincode_fixint() {
4515        let mut bundle = QueryBundle::default();
4516        bundle.read_routes.insert(
4517            "read_users".to_string(),
4518            read_batch().var_as(
4519                "count",
4520                g().n_with_label("User")
4521                    .where_(Predicate::is_in(
4522                        "status",
4523                        vec!["active".to_string(), "pending".to_string()],
4524                    ))
4525                    .count(),
4526            ),
4527        );
4528        bundle.read_parameters.insert(
4529            "read_users".to_string(),
4530            vec![QueryParameter {
4531                name: "filters".to_string(),
4532                ty: QueryParamType::Object,
4533            }],
4534        );
4535        bundle.write_routes.insert(
4536            "create_user".to_string(),
4537            write_batch().var_as("created", g().add_n("User", vec![("name", "Alice")])),
4538        );
4539        bundle.write_parameters.insert(
4540            "create_user".to_string(),
4541            vec![QueryParameter {
4542                name: "data".to_string(),
4543                ty: QueryParamType::Array(Box::new(QueryParamType::Object)),
4544            }],
4545        );
4546
4547        let bytes = serialize_query_bundle(&bundle).expect("serialize query bundle");
4548        let decoded = deserialize_query_bundle(&bytes).expect("deserialize query bundle");
4549
4550        assert_eq!(decoded.version, QUERY_BUNDLE_VERSION);
4551        assert_eq!(decoded.read_routes.len(), 1);
4552        assert_eq!(decoded.write_routes.len(), 1);
4553        assert!(decoded.read_routes.contains_key("read_users"));
4554        assert!(decoded.write_routes.contains_key("create_user"));
4555        assert_eq!(
4556            decoded.read_parameters.get("read_users"),
4557            Some(&vec![QueryParameter {
4558                name: "filters".to_string(),
4559                ty: QueryParamType::Object,
4560            }])
4561        );
4562        assert_eq!(
4563            decoded.write_parameters.get("create_user"),
4564            Some(&vec![QueryParameter {
4565                name: "data".to_string(),
4566                ty: QueryParamType::Array(Box::new(QueryParamType::Object)),
4567            }])
4568        );
4569    }
4570
4571    #[test]
4572    fn query_bundle_rejects_unsupported_version() {
4573        let mut bundle = QueryBundle::default();
4574        bundle.version = QUERY_BUNDLE_VERSION + 1;
4575
4576        let bytes = serialize_query_bundle(&bundle).expect("serialize query bundle");
4577        let err = deserialize_query_bundle(&bytes).expect_err("version should fail");
4578
4579        assert!(matches!(
4580            err,
4581            GenerateError::UnsupportedVersion {
4582                found: _,
4583                expected: QUERY_BUNDLE_VERSION,
4584            }
4585        ));
4586    }
4587
4588    #[test]
4589    fn test_traversal_builder() {
4590        let t = g()
4591            .n([1u64, 2, 3])
4592            .out(Some("FOLLOWS"))
4593            .has("active", "true")
4594            .limit(10);
4595
4596        assert_eq!(t.steps.len(), 4);
4597        assert!(matches!(&t.steps[0], Step::N(NodeRef::Ids(ids)) if ids == &vec![1, 2, 3]));
4598        assert!(matches!(&t.steps[1], Step::Out(Some(label)) if label == "FOLLOWS"));
4599    }
4600
4601    #[test]
4602    fn test_variable_steps() {
4603        let t = g()
4604            .n([1u64])
4605            .out(None::<String>)
4606            .as_("neighbors")
4607            .out(None::<String>)
4608            .within("neighbors");
4609
4610        assert_eq!(t.steps.len(), 5);
4611        assert!(matches!(&t.steps[2], Step::As(name) if name == "neighbors"));
4612        assert!(matches!(&t.steps[4], Step::Within(name) if name == "neighbors"));
4613    }
4614
4615    #[test]
4616    fn test_terminal_detection() {
4617        let t1 = g().n([1u64]).out(None::<String>);
4618        assert!(!t1.has_terminal());
4619
4620        let t2 = g().n([1u64]).count();
4621        assert!(t2.has_terminal());
4622
4623        let t3 = g().n([1u64]).exists();
4624        assert!(t3.has_terminal());
4625    }
4626
4627    #[test]
4628    fn test_node_ref_from_impls() {
4629        let all = NodeRef::all();
4630        assert!(matches!(all, NodeRef::All));
4631
4632        let r1: NodeRef = 42u64.into();
4633        assert!(matches!(r1, NodeRef::Ids(ids) if ids == vec![42]));
4634
4635        let r2: NodeRef = vec![1u64, 2, 3].into();
4636        assert!(matches!(r2, NodeRef::Ids(ids) if ids == vec![1, 2, 3]));
4637
4638        let r3: NodeRef = "my_var".into();
4639        assert!(matches!(r3, NodeRef::Var(name) if name == "my_var"));
4640
4641        let r4 = NodeRef::param("node_id");
4642        assert!(matches!(r4, NodeRef::Param(name) if name == "node_id"));
4643    }
4644
4645    #[test]
4646    fn test_edge_ref_param_constructor() {
4647        let r = EdgeRef::param("edge_id");
4648        assert!(matches!(r, EdgeRef::Param(name) if name == "edge_id"));
4649    }
4650
4651    #[test]
4652    fn test_add_n_and_add_e() {
4653        let t = g()
4654            .add_n("User", vec![("name", "Alice")])
4655            .as_("alice")
4656            .add_n("User", vec![("name", "Bob")])
4657            .add_e("KNOWS", NodeRef::var("alice"), vec![("since", "2024")]);
4658
4659        assert_eq!(t.steps.len(), 4);
4660        assert!(
4661            matches!(&t.steps[0], Step::AddN { label, properties } if label == "User" && properties.len() == 1)
4662        );
4663        assert!(
4664            matches!(&t.steps[3], Step::AddE { label, to: NodeRef::Var(name), .. } if label == "KNOWS" && name == "alice")
4665        );
4666    }
4667
4668    #[test]
4669    fn test_predicate_builder() {
4670        let p1 = Predicate::eq("name", "Alice");
4671        assert!(
4672            matches!(p1, Predicate::Eq(prop, PropertyValue::String(val)) if prop == "name" && val == "Alice")
4673        );
4674
4675        let p_in = Predicate::is_in("status", vec!["active".to_string(), "pending".to_string()]);
4676        assert!(matches!(
4677            p_in,
4678            Predicate::IsIn(prop, PropertyValue::StringArray(values))
4679                if prop == "status" && values == vec!["active".to_string(), "pending".to_string()]
4680        ));
4681
4682        let p2 = Predicate::and(vec![
4683            Predicate::eq("status", "active"),
4684            Predicate::gt("age", "18"),
4685        ]);
4686        assert!(matches!(p2, Predicate::And(preds) if preds.len() == 2));
4687    }
4688
4689    #[test]
4690    fn test_edge_traversal() {
4691        // This should compile: nodes -> edges -> nodes
4692        let t = g()
4693            .n([1u64])
4694            .out_e(Some("FOLLOWS"))
4695            .edge_has("weight", 1i64)
4696            .out_n()
4697            .has_label("User");
4698
4699        assert_eq!(t.steps.len(), 5);
4700    }
4701
4702    #[test]
4703    fn test_sub_traversal() {
4704        let t = g()
4705            .n([1u64])
4706            .union(vec![sub().out(Some("FOLLOWS")), sub().out(Some("LIKES"))]);
4707
4708        assert_eq!(t.steps.len(), 2);
4709        if let Step::Union(subs) = &t.steps[1] {
4710            assert_eq!(subs.len(), 2);
4711        } else {
4712            panic!("Expected Union step");
4713        }
4714    }
4715
4716    #[test]
4717    fn test_repeat_with_sub_traversal() {
4718        let t = g()
4719            .n([1u64])
4720            .repeat(RepeatConfig::new(sub().out(None::<&str>)).times(3));
4721
4722        assert_eq!(t.steps.len(), 2);
4723    }
4724
4725    #[test]
4726    fn test_read_batch_construction() {
4727        let b = read_batch()
4728            .var_as(
4729                "user",
4730                g().n_where(SourcePredicate::eq("username", "alice")),
4731            )
4732            .var_as("friends", g().n(NodeRef::var("user")).out(Some("FOLLOWS")))
4733            .returning(["user", "friends"]);
4734
4735        assert_eq!(b.queries.len(), 2);
4736        assert_eq!(b.returns, vec!["user", "friends"]);
4737
4738        let first = query_entry(&b.queries[0]);
4739        let second = query_entry(&b.queries[1]);
4740
4741        // First query: user
4742        assert_eq!(first.name, Some("user".to_string()));
4743        assert!(first.condition.is_none());
4744        assert_eq!(first.steps.len(), 1); // NWhere
4745
4746        // Second query: friends
4747        assert_eq!(second.name, Some("friends".to_string()));
4748        assert_eq!(second.steps.len(), 2); // N + Out
4749    }
4750
4751    #[test]
4752    fn test_read_batch_conditional() {
4753        let b = read_batch()
4754            .var_as("user", g().n_where(SourcePredicate::eq("id", 1i64)))
4755            .var_as_if(
4756                "posts",
4757                BatchCondition::VarNotEmpty("user".to_string()),
4758                g().n(NodeRef::var("user")).out(Some("POSTED")),
4759            );
4760
4761        assert_eq!(b.queries.len(), 2);
4762
4763        // Second query has condition
4764        assert!(matches!(
4765            &query_entry(&b.queries[1]).condition,
4766            Some(BatchCondition::VarNotEmpty(name)) if name == "user"
4767        ));
4768    }
4769
4770    #[test]
4771    fn test_read_batch_with_terminal() {
4772        let b = read_batch()
4773            .var_as("user", g().n([1u64]).value_map(None::<Vec<&str>>))
4774            .var_as("friend_count", g().n([1u64]).out(Some("FOLLOWS")).count())
4775            .returning(["user", "friend_count"]);
4776
4777        assert_eq!(b.queries.len(), 2);
4778
4779        // First query ends with ValueMap
4780        assert!(matches!(
4781            query_entry(&b.queries[0]).steps.last(),
4782            Some(Step::ValueMap(_))
4783        ));
4784
4785        // Second query ends with Count
4786        assert!(matches!(
4787            query_entry(&b.queries[1]).steps.last(),
4788            Some(Step::Count)
4789        ));
4790    }
4791
4792    #[test]
4793    fn test_write_batch_construction() {
4794        let b = write_batch()
4795            .var_as("user", g().add_n("User", vec![("name", "Alice")]))
4796            .var_as("post", g().add_n("Post", vec![("title", "Hello")]))
4797            .returning(["user", "post"]);
4798
4799        assert_eq!(b.queries.len(), 2);
4800        assert_eq!(b.returns, vec!["user", "post"]);
4801    }
4802
4803    #[test]
4804    fn test_property_input_from_expr() {
4805        let traversal = g().add_n(
4806            "User",
4807            vec![
4808                ("name", PropertyInput::param("name")),
4809                ("age", PropertyInput::param("age")),
4810            ],
4811        );
4812
4813        assert!(matches!(
4814            &traversal.steps[0],
4815            Step::AddN { properties, .. }
4816                if matches!(&properties[0].1, PropertyInput::Expr(Expr::Param(name)) if name == "name")
4817                && matches!(&properties[1].1, PropertyInput::Expr(Expr::Param(name)) if name == "age")
4818        ));
4819    }
4820
4821    #[test]
4822    fn test_edge_has_accepts_param_input() {
4823        let traversal = g()
4824            .e([1u64])
4825            .edge_has("targetExternalId", PropertyInput::param("targetExternalId"));
4826
4827        assert!(matches!(
4828            &traversal.steps[1],
4829            Step::EdgeHas(property, PropertyInput::Expr(Expr::Param(name)))
4830                if property == "targetExternalId" && name == "targetExternalId"
4831        ));
4832    }
4833
4834    #[test]
4835    fn test_write_batch_for_each_param() {
4836        let body = write_batch()
4837            .var_as(
4838                "existing",
4839                g().n_where(SourcePredicate::eq("$label", "User")),
4840            )
4841            .var_as(
4842                "created",
4843                g().add_n("User", vec![("name", PropertyInput::param("name"))]),
4844            );
4845
4846        let batch = write_batch().for_each_param("data", body);
4847
4848        assert_eq!(batch.queries.len(), 1);
4849        assert!(matches!(
4850            &batch.queries[0],
4851            BatchEntry::ForEach { param, body }
4852                if param == "data" && matches!(&body[1], BatchEntry::Query(NamedQuery { name: Some(name), .. }) if name == "created")
4853        ));
4854    }
4855
4856    #[test]
4857    fn test_property_value_nested_payload_variants() {
4858        let mut row = BTreeMap::new();
4859        row.insert("externalId".to_string(), PropertyValue::from("u-1"));
4860        row.insert("active".to_string(), PropertyValue::from(true));
4861
4862        let payload = PropertyValue::from(vec![PropertyValue::from(row.clone())]);
4863
4864        assert!(matches!(payload.as_array(), Some(values) if values.len() == 1));
4865        assert_eq!(
4866            payload
4867                .as_array()
4868                .and_then(|values| values[0].as_object())
4869                .and_then(|map| map.get("externalId"))
4870                .and_then(PropertyValue::as_str),
4871            Some("u-1")
4872        );
4873    }
4874
4875    #[test]
4876    fn test_vector_search_steps() {
4877        let embedding = vec![0.1f32; 4];
4878        let t = g().vector_search_nodes("Doc", "embedding", embedding.clone(), 5, None);
4879        assert!(matches!(
4880            &t.steps[0],
4881            Step::VectorSearchNodes {
4882                label,
4883                property,
4884                query_vector: PropertyInput::Value(PropertyValue::F32Array(values)),
4885                k: StreamBound::Literal(k),
4886                tenant_value,
4887            }
4888                if label == "Doc"
4889                    && property == "embedding"
4890                    && values == &embedding
4891                    && *k == 5
4892                    && tenant_value.is_none()
4893        ));
4894
4895        let t2 = g().vector_search_edges("SIMILAR", "embedding", embedding.clone(), 3, None);
4896        assert!(matches!(
4897            &t2.steps[0],
4898            Step::VectorSearchEdges {
4899                label,
4900                property,
4901                query_vector: PropertyInput::Value(PropertyValue::F32Array(values)),
4902                k: StreamBound::Literal(k),
4903                tenant_value,
4904            }
4905                if label == "SIMILAR"
4906                    && property == "embedding"
4907                    && values == &embedding
4908                    && *k == 3
4909                    && tenant_value.is_none()
4910        ));
4911
4912        let t3 = g().vector_search_nodes(
4913            "Doc",
4914            "embedding",
4915            vec![0.1f32; 4],
4916            5,
4917            Some(PropertyValue::from("tenant-a")),
4918        );
4919        assert!(matches!(
4920            &t3.steps[0],
4921            Step::VectorSearchNodes {
4922                label,
4923                property,
4924                k: StreamBound::Literal(k),
4925                tenant_value: Some(PropertyInput::Value(PropertyValue::String(value))),
4926                ..
4927            } if label == "Doc" && property == "embedding" && *k == 5 && value == "tenant-a"
4928        ));
4929    }
4930
4931    #[test]
4932    fn test_parameterized_vector_search_steps() {
4933        let t = g().vector_search_nodes_with(
4934            "Doc",
4935            "embedding",
4936            PropertyInput::param("queryVector"),
4937            Expr::param("limit"),
4938            Some(PropertyInput::param("firmId")),
4939        );
4940
4941        assert!(matches!(
4942            &t.steps[0],
4943            Step::VectorSearchNodes {
4944                label,
4945                property,
4946                query_vector: PropertyInput::Expr(Expr::Param(query_vector)),
4947                k: StreamBound::Expr(Expr::Param(limit)),
4948                tenant_value: Some(PropertyInput::Expr(Expr::Param(firm_id))),
4949            }
4950                if label == "Doc"
4951                    && property == "embedding"
4952                    && query_vector == "queryVector"
4953                    && limit == "limit"
4954                && firm_id == "firmId"
4955        ));
4956    }
4957
4958    #[test]
4959    fn test_text_search_steps() {
4960        let t = g().text_search_nodes("Doc", "body", "alice search", 5, None);
4961        assert!(matches!(
4962            &t.steps[0],
4963            Step::TextSearchNodes {
4964                label,
4965                property,
4966                query_text: PropertyInput::Value(PropertyValue::String(query)),
4967                k: StreamBound::Literal(k),
4968                tenant_value,
4969            }
4970                if label == "Doc"
4971                    && property == "body"
4972                    && query == "alice search"
4973                    && *k == 5
4974                    && tenant_value.is_none()
4975        ));
4976
4977        let t2 = g().text_search_edges(
4978            "REL",
4979            "body",
4980            "alice edge",
4981            3,
4982            Some(PropertyValue::from("tenant-a")),
4983        );
4984        assert!(matches!(
4985            &t2.steps[0],
4986            Step::TextSearchEdges {
4987                label,
4988                property,
4989                query_text: PropertyInput::Value(PropertyValue::String(query)),
4990                k: StreamBound::Literal(k),
4991                tenant_value: Some(PropertyInput::Value(PropertyValue::String(tenant))),
4992            }
4993                if label == "REL"
4994                    && property == "body"
4995                    && query == "alice edge"
4996                    && *k == 3
4997                    && tenant == "tenant-a"
4998        ));
4999    }
5000
5001    #[test]
5002    fn test_parameterized_text_search_steps() {
5003        let t = g().text_search_nodes_with(
5004            "Doc",
5005            "body",
5006            PropertyInput::param("queryText"),
5007            Expr::param("limit"),
5008            Some(PropertyInput::param("tenantId")),
5009        );
5010
5011        assert!(matches!(
5012            &t.steps[0],
5013            Step::TextSearchNodes {
5014                label,
5015                property,
5016                query_text: PropertyInput::Expr(Expr::Param(query_text)),
5017                k: StreamBound::Expr(Expr::Param(limit)),
5018                tenant_value: Some(PropertyInput::Expr(Expr::Param(tenant_id))),
5019            }
5020                if label == "Doc"
5021                    && property == "body"
5022                    && query_text == "queryText"
5023                    && limit == "limit"
5024                    && tenant_id == "tenantId"
5025        ));
5026    }
5027
5028    #[test]
5029    fn test_parameterized_stream_bounds() {
5030        let range = g()
5031            .n_with_label("User")
5032            .range(Expr::param("start"), Expr::param("end"));
5033        assert!(matches!(
5034            range.steps.as_slice(),
5035            [
5036                Step::NWhere(_),
5037                Step::RangeBy(StreamBound::Expr(Expr::Param(start)), StreamBound::Expr(Expr::Param(end))),
5038            ] if start == "start" && end == "end"
5039        ));
5040
5041        let ordered = g()
5042            .n_with_label("User")
5043            .order_by("age", Order::Desc)
5044            .limit(Expr::param("limit"))
5045            .skip(Expr::param("offset"));
5046        assert!(matches!(
5047            ordered.steps.as_slice(),
5048            [
5049                Step::NWhere(_),
5050                Step::OrderBy(property, Order::Desc),
5051                Step::LimitBy(Expr::Param(limit)),
5052                Step::SkipBy(Expr::Param(offset)),
5053            ] if property == "age" && limit == "limit" && offset == "offset"
5054        ));
5055    }
5056
5057    #[test]
5058    fn test_contains_param_predicate() {
5059        assert!(matches!(
5060            Predicate::contains_param("location", "city"),
5061            Predicate::ContainsExpr(property, Expr::Param(param))
5062                if property == "location" && param == "city"
5063        ));
5064    }
5065
5066    #[test]
5067    fn test_is_in_param_predicate() {
5068        assert!(matches!(
5069            Predicate::is_in_param("location", "cities"),
5070            Predicate::IsInExpr(property, Expr::Param(param))
5071                if property == "location" && param == "cities"
5072        ));
5073    }
5074
5075    #[test]
5076    fn test_create_vector_index_steps() {
5077        let t = g().create_vector_index_nodes("Doc", "embedding", None::<&str>);
5078        assert!(t.has_terminal());
5079        assert!(matches!(
5080            &t.steps[0],
5081            Step::CreateIndex {
5082                spec: IndexSpec::NodeVector {
5083                    label,
5084                    property,
5085                    tenant_property,
5086                },
5087                if_not_exists,
5088            } if label == "Doc"
5089                && property == "embedding"
5090                && tenant_property.is_none()
5091                && *if_not_exists
5092        ));
5093
5094        let t2 = g().create_vector_index_edges("REL", "embedding", None::<&str>);
5095        assert!(matches!(
5096            &t2.steps[0],
5097            Step::CreateIndex {
5098                spec: IndexSpec::EdgeVector {
5099                    label,
5100                    property,
5101                    tenant_property,
5102                },
5103                if_not_exists,
5104            } if label == "REL"
5105                && property == "embedding"
5106                && tenant_property.is_none()
5107                && *if_not_exists
5108        ));
5109
5110        let t3 = g().create_vector_index_nodes("Doc", "embedding", Some("tenant_id"));
5111        assert!(matches!(
5112            &t3.steps[0],
5113            Step::CreateIndex {
5114                spec: IndexSpec::NodeVector {
5115                    tenant_property: Some(tenant_property),
5116                    ..
5117                },
5118                if_not_exists,
5119            } if tenant_property == "tenant_id" && *if_not_exists
5120        ));
5121    }
5122
5123    #[test]
5124    fn test_create_text_index_steps() {
5125        let t = g().create_text_index_nodes("Doc", "body", None::<&str>);
5126        assert!(matches!(
5127            &t.steps[0],
5128            Step::CreateIndex {
5129                spec: IndexSpec::NodeText {
5130                    label,
5131                    property,
5132                    tenant_property,
5133                },
5134                if_not_exists,
5135            } if label == "Doc"
5136                && property == "body"
5137                && tenant_property.is_none()
5138                && *if_not_exists
5139        ));
5140
5141        let t2 = g().create_text_index_edges("REL", "body", Some("tenant_id"));
5142        assert!(matches!(
5143            &t2.steps[0],
5144            Step::CreateIndex {
5145                spec: IndexSpec::EdgeText {
5146                    label,
5147                    property,
5148                    tenant_property: Some(tenant_property),
5149                },
5150                if_not_exists,
5151            } if label == "REL"
5152                && property == "body"
5153                && tenant_property == "tenant_id"
5154                && *if_not_exists
5155        ));
5156    }
5157
5158    #[test]
5159    fn test_generic_index_steps() {
5160        let create = g().create_index_if_not_exists(IndexSpec::node_equality("User", "status"));
5161        assert!(create.has_terminal());
5162        assert!(matches!(
5163            &create.steps[0],
5164            Step::CreateIndex {
5165                spec: IndexSpec::NodeEquality {
5166                    label,
5167                    property,
5168                    unique,
5169                },
5170                if_not_exists,
5171            } if label == "User" && property == "status" && !unique && *if_not_exists
5172        ));
5173
5174        let drop = g().drop_index(IndexSpec::edge_range("FOLLOWS", "weight"));
5175        assert!(drop.has_terminal());
5176        assert!(matches!(
5177            &drop.steps[0],
5178            Step::DropIndex {
5179                spec: IndexSpec::EdgeRange { label, property },
5180            } if label == "FOLLOWS" && property == "weight"
5181        ));
5182    }
5183
5184    #[test]
5185    fn test_unique_node_equality_constructor() {
5186        assert_eq!(
5187            IndexSpec::node_unique_equality("User", "email"),
5188            IndexSpec::NodeEquality {
5189                label: "User".to_string(),
5190                property: "email".to_string(),
5191                unique: true,
5192            }
5193        );
5194    }
5195
5196    #[test]
5197    fn test_node_equality_deserializes_unique_default_false() {
5198        let decoded: IndexSpec =
5199            sonic_rs::from_str(r#"{"NodeEquality":{"label":"User","property":"status"}}"#)
5200                .expect("deserialize old node equality payload");
5201
5202        assert_eq!(
5203            decoded,
5204            IndexSpec::NodeEquality {
5205                label: "User".to_string(),
5206                property: "status".to_string(),
5207                unique: false,
5208            }
5209        );
5210    }
5211
5212    #[test]
5213    fn test_node_unique_equality_serializes_unique_flag() {
5214        let encoded = sonic_rs::to_string(&IndexSpec::node_unique_equality("User", "status"))
5215            .expect("serialize unique node equality");
5216
5217        assert!(encoded.contains(r#""NodeEquality""#));
5218        assert!(encoded.contains(r#""unique":true"#));
5219    }
5220}