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::eq_param("region", "target_region"),
497//!                 Predicate::neq_param("status", "blocked_status"),
498//!                 Predicate::gt_param("score", "min_score"),
499//!                 Predicate::gte_param("score", "min_score_inclusive"),
500//!                 Predicate::lt_param("score", "max_score"),
501//!                 Predicate::lte_param("score", "max_score_inclusive"),
502//!             ]))
503//!             .as_("seed")
504//!             .store("seed_store")
505//!             .select("seed")
506//!             .inject("seed_store")
507//!             .within("seed_store")
508//!             .without("seed")
509//!             .dedup()
510//!             .order_by("score", Order::Desc)
511//!             .order_by_multiple(vec![("age", Order::Asc), ("score", Order::Desc)])
512//!             .limit(100)
513//!             .skip(5)
514//!             .range(0, 20),
515//!     )
516//!     .var_as("counted", g().n(NodeRef::var("filtered")).count())
517//!     .var_as("exists", g().n(NodeRef::var("filtered")).exists())
518//!     .var_as("ids", g().n(NodeRef::var("filtered")).id())
519//!     .var_as("labels", g().n(NodeRef::var("filtered")).label())
520//!     .var_as("values", g().n(NodeRef::var("filtered")).values(vec!["name", "email"]))
521//!     .var_as(
522//!         "value_map_some",
523//!         g().n(NodeRef::var("filtered"))
524//!             .value_map(Some(vec!["$id", "name", "email"])),
525//!     )
526//!     .var_as(
527//!         "value_map_all",
528//!         g().n(NodeRef::var("filtered")).value_map(None::<Vec<&str>>),
529//!     )
530//!     .var_as(
531//!         "projected",
532//!         g().n(NodeRef::var("filtered")).project(vec![
533//!             PropertyProjection::new("name"),
534//!             PropertyProjection::renamed("email", "contact"),
535//!         ]),
536//!     )
537//!     .returning(["filtered", "projected"]);
538//! ```
539//!
540//! ### Edge Traversal and Edge Terminals
541//!
542//! ```
543//! # use helix_dsl::prelude::*;
544//! read_batch()
545//!     .var_as(
546//!         "edge_ops",
547//!         g()
548//!             .e_where(SourcePredicate::gt("weight", 0.1f64))
549//!             .edge_has("weight", 1i64)
550//!             .edge_has_label("FOLLOWS")
551//!             .as_("edges_a")
552//!             .store("edges_b")
553//!             .dedup()
554//!             .order_by("weight", Order::Desc)
555//!             .limit(50)
556//!             .skip(2)
557//!             .range(0, 20),
558//!     )
559//!     .var_as("to_out_n", g().e(EdgeRef::var("edge_ops")).out_n())
560//!     .var_as("to_in_n", g().e(EdgeRef::var("edge_ops")).in_n())
561//!     .var_as("to_other_n", g().e(EdgeRef::var("edge_ops")).other_n())
562//!     .var_as("edge_count", g().e(EdgeRef::var("edge_ops")).count())
563//!     .var_as("edge_exists", g().e(EdgeRef::var("edge_ops")).exists())
564//!     .var_as("edge_ids", g().e(EdgeRef::var("edge_ops")).id())
565//!     .var_as("edge_labels", g().e(EdgeRef::var("edge_ops")).label())
566//!     .var_as("edge_props", g().e(EdgeRef::var("edge_ops")).edge_properties())
567//!     .returning(["edge_ops", "edge_props"]);
568//! ```
569//!
570//! ### Branching, Sub-Traversals, Repeat, Grouping, Paths, and Sack
571//!
572//! ```
573//! # use helix_dsl::prelude::*;
574//! read_batch()
575//!     .var_as(
576//!         "advanced",
577//!         g()
578//!             .n(1u64)
579//!             .out_e(Some("FOLLOWS"))
580//!             .in_n()
581//!             .in_e(Some("MENTIONS"))
582//!             .out_n()
583//!             .both_e(None::<&str>)
584//!             .other_n()
585//!             .repeat(
586//!                 RepeatConfig::new(sub().out(Some("FOLLOWS")))
587//!                     .times(2)
588//!                     .until(Predicate::has_key("stop"))
589//!                     .emit_all()
590//!                     .emit_before()
591//!                     .emit_after()
592//!                     .emit_if(Predicate::gt("score", 0i64))
593//!                     .max_depth(8),
594//!             )
595//!             .union(vec![
596//!                 sub().out(Some("LIKES")),
597//!                 SubTraversal::new()
598//!                     .out(Some("FOLLOWS"))
599//!                     .in_(Some("MENTIONS"))
600//!                     .both(None::<&str>)
601//!                     .out_e(Some("REL"))
602//!                     .in_e(Some("REL"))
603//!                     .both_e(None::<&str>)
604//!                     .out_n()
605//!                     .in_n()
606//!                     .other_n()
607//!                     .has("active", true)
608//!                     .has_label("User")
609//!                     .has_key("email")
610//!                     .where_(Predicate::eq("state", "ok"))
611//!                     .dedup()
612//!                     .within("allow")
613//!                     .without("deny")
614//!                     .edge_has("weight", 1i64)
615//!                     .edge_has_label("REL")
616//!                     .limit(10)
617//!                     .skip(1)
618//!                     .range(0, 5)
619//!                     .as_("s1")
620//!                     .store("s2")
621//!                     .select("s1")
622//!                     .order_by("score", Order::Desc)
623//!                     .order_by_multiple(vec![("age", Order::Asc)])
624//!                     .path()
625//!                     .simple_path(),
626//!             ])
627//!             .choose(
628//!                 Predicate::eq("vip", true),
629//!                 sub().out(Some("PREMIUM")),
630//!                 Some(sub().out(Some("STANDARD"))),
631//!             )
632//!             .coalesce(vec![sub().out(Some("POSTED")), sub().out(Some("COMMENTED"))])
633//!             .optional(sub().out(Some("MENTIONED")))
634//!             .fold()
635//!             .unfold()
636//!             .path()
637//!             .simple_path()
638//!             .with_sack(PropertyValue::I64(0))
639//!             .sack_set("weight")
640//!             .sack_add("weight")
641//!             .sack_get()
642//!             .dedup(),
643//!     )
644//!     .var_as("grouped", g().n_with_label("User").group("team"))
645//!     .var_as("grouped_count", g().n_with_label("User").group_count("team"))
646//!     .var_as(
647//!         "aggregate_count",
648//!         g().n_with_label("User")
649//!             .aggregate_by(AggregateFunction::Count, "score"),
650//!     )
651//!     .var_as(
652//!         "aggregate_sum",
653//!         g().n_with_label("User").aggregate_by(AggregateFunction::Sum, "score"),
654//!     )
655//!     .var_as(
656//!         "aggregate_min",
657//!         g().n_with_label("User").aggregate_by(AggregateFunction::Min, "score"),
658//!     )
659//!     .var_as(
660//!         "aggregate_max",
661//!         g().n_with_label("User").aggregate_by(AggregateFunction::Max, "score"),
662//!     )
663//!     .var_as(
664//!         "aggregate_mean",
665//!         g().n_with_label("User")
666//!             .aggregate_by(AggregateFunction::Mean, "score"),
667//!     )
668//!     .returning(["advanced", "grouped", "grouped_count", "aggregate_count"]);
669//! ```
670//!
671//! ### Read-Batch Conditions
672//!
673//! ```
674//! # use helix_dsl::prelude::*;
675//! read_batch()
676//!     .var_as("base", g().n_with_label("User"))
677//!     .var_as_if(
678//!         "if_not_empty",
679//!         BatchCondition::VarNotEmpty("base".to_string()),
680//!         g().n(NodeRef::var("base")).limit(10),
681//!     )
682//!     .var_as_if(
683//!         "if_empty",
684//!         BatchCondition::VarEmpty("base".to_string()),
685//!         g().n_with_label("FallbackUser"),
686//!     )
687//!     .var_as_if(
688//!         "if_min_size",
689//!         BatchCondition::VarMinSize("base".to_string(), 5),
690//!         g().n(NodeRef::var("base")).order_by("score", Order::Desc),
691//!     )
692//!     .var_as_if(
693//!         "if_prev_not_empty",
694//!         BatchCondition::PrevNotEmpty,
695//!         g().n(NodeRef::var("base")).count(),
696//!     )
697//!     .returning(["base", "if_not_empty", "if_empty", "if_min_size", "if_prev_not_empty"]);
698//! ```
699//!
700//! ### Write Sources, Mutations, and Vector Index Configuration
701//!
702//! ```
703//! # use helix_dsl::prelude::*;
704//! write_batch()
705//!     .var_as("created_user", g().add_n("User", vec![("name", "Alice")]))
706//!     .var_as(
707//!         "created_team",
708//!         g().n(NodeRef::var("created_user"))
709//!             .add_n("Team", vec![("name", "Graph")]),
710//!     )
711//!     .var_as(
712//!         "connected",
713//!         g().n(NodeRef::var("created_user")).add_e(
714//!             "MEMBER_OF",
715//!             NodeRef::var("created_team"),
716//!             vec![("since", "2026-01-01")],
717//!         ),
718//!     )
719//!     .var_as(
720//!         "updated",
721//!         g().n(NodeRef::var("created_user"))
722//!             .set_property("active", true)
723//!             .remove_property("old_field"),
724//!     )
725//!     .var_as(
726//!         "drop_some_edges",
727//!         g().n(NodeRef::var("created_user"))
728//!             .drop_edge(NodeRef::ids([2u64, 3]))
729//!             .drop_edge_by_id(EdgeRef::ids([40u64, 41])),
730//!     )
731//!     .var_as("drop_nodes", g().n(NodeRef::var("created_team")).drop())
732//!     .var_as("inject_from_empty", g().inject("created_user").has_label("User"))
733//!     .var_as("drop_edge_by_id_from_empty", g().drop_edge_by_id([90u64, 91]))
734//!     .var_as(
735//!         "create_vector_index_nodes",
736//!         g().create_vector_index_nodes(
737//!             "Doc",
738//!             "embedding",
739//!             Some("tenant_id"),
740//!         ),
741//!     )
742//!     .var_as(
743//!         "create_vector_index_edges",
744//!         g().create_vector_index_edges(
745//!             "SIMILAR",
746//!             "embedding",
747//!             None::<&str>,
748//!         ),
749//!     )
750//!     .var_as(
751//!         "create_vector_index_edges_alt",
752//!         g().create_vector_index_edges(
753//!             "RELATED",
754//!             "embedding",
755//!             None::<&str>,
756//!         ),
757//!     )
758//!     .var_as_if(
759//!         "write_if_not_empty",
760//!         BatchCondition::VarNotEmpty("created_user".to_string()),
761//!         g().n(NodeRef::var("created_user")).set_property("verified", true),
762//!     )
763//!     .returning([
764//!         "created_user",
765//!         "created_team",
766//!         "connected",
767//!         "updated",
768//!         "drop_some_edges",
769//!         "drop_nodes",
770//!         "inject_from_empty",
771//!         "drop_edge_by_id_from_empty",
772//!         "create_vector_index_nodes",
773//!         "create_vector_index_edges",
774//!         "create_vector_index_edges_alt",
775//!         "write_if_not_empty",
776//!     ]);
777//! ```
778//!
779//! ## Traversal Building Inside `var_as(...)`
780//!
781//! Common source steps:
782//! - `n(...)`, `n_where(...)`, `n_with_label(...)`
783//! - `e(...)`, `e_where(...)`, `e_with_label(...)`
784//! - `vector_search_nodes(...)`, `vector_search_edges(...)`
785//!   - current Helix runtime exposes vector hit metadata via virtual fields
786//!     (`$id`, `$distance`, `$from`, `$to`) in terminal projections
787//!
788//! Common navigation and filtering:
789//! - `out/in_/both`, `out_e/in_e/both_e`, `out_n/in_n/other_n`
790//! - `has`, `has_label`, `has_key`, `where_`, `within`, `without`, `dedup`
791//! - `limit`, `skip`, `range`, `order_by`, `order_by_multiple`
792//!
793//! Common terminal projections:
794//! - `count`, `exists`, `id`, `label`
795//! - `values`, `value_map`, `project`, `edge_properties`
796//!
797//! Write-only operations (usable in [`write_batch()`] traversals):
798//! - `add_n`, `add_e`, `set_property`, `remove_property`, `drop`, `drop_edge`, `drop_edge_by_id`
799//! - `create_vector_index_nodes`, `create_vector_index_edges`
800
801#![warn(missing_docs)]
802#![warn(clippy::all)]
803#![deny(unsafe_code)]
804
805use std::collections::{BTreeMap, HashMap};
806use std::marker::PhantomData;
807
808use serde::{Deserialize, Serialize};
809mod query_generator;
810
811pub use helix_dsl_macros::register;
812pub use query_generator::*;
813
814#[doc(hidden)]
815pub mod __private {
816    pub use inventory;
817}
818
819/// Type alias for node IDs
820pub type NodeId = u64;
821
822/// Type alias for edge IDs (separate namespace from node IDs)
823pub type EdgeId = u64;
824
825/// Arbitrary nested parameter value.
826pub type ParamValue = PropertyValue;
827
828/// Object-shaped parameter payload.
829pub type ParamObject = BTreeMap<String, PropertyValue>;
830
831// Typestate Markers
832
833/// Marker trait for all traversal states
834#[doc(hidden)]
835pub trait TraversalState: private::Sealed {}
836
837mod private {
838    /// Seal the TraversalState trait to prevent external implementation
839    pub trait Sealed {}
840    impl Sealed for super::Empty {}
841    impl Sealed for super::OnNodes {}
842    impl Sealed for super::OnEdges {}
843    impl Sealed for super::Terminal {}
844    impl Sealed for super::ReadOnly {}
845    impl Sealed for super::WriteEnabled {}
846}
847
848/// Initial state - no source step yet
849#[doc(hidden)]
850#[derive(Debug, Clone, Copy, PartialEq, Eq)]
851pub struct Empty;
852
853/// Traversal is currently operating on a node stream
854#[doc(hidden)]
855#[derive(Debug, Clone, Copy, PartialEq, Eq)]
856pub struct OnNodes;
857
858/// Traversal is currently operating on an edge stream
859#[doc(hidden)]
860#[derive(Debug, Clone, Copy, PartialEq, Eq)]
861pub struct OnEdges;
862
863/// Traversal has terminated - no more chaining allowed
864#[doc(hidden)]
865#[derive(Debug, Clone, Copy, PartialEq, Eq)]
866pub struct Terminal;
867
868impl TraversalState for Empty {}
869impl TraversalState for OnNodes {}
870impl TraversalState for OnEdges {}
871impl TraversalState for Terminal {}
872
873// MutationMode Markers
874
875/// Marker trait for mutation capability - tracks whether a traversal contains mutations
876#[doc(hidden)]
877pub trait MutationMode: private::Sealed {}
878
879/// Read-only traversal - no mutation steps
880#[doc(hidden)]
881#[derive(Debug, Clone, Copy, PartialEq, Eq)]
882pub struct ReadOnly;
883
884/// Write-enabled traversal - contains mutation steps
885#[doc(hidden)]
886#[derive(Debug, Clone, Copy, PartialEq, Eq)]
887pub struct WriteEnabled;
888
889impl MutationMode for ReadOnly {}
890impl MutationMode for WriteEnabled {}
891
892// Property Value Types
893
894/// A property value that can be stored on nodes or edges
895#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
896pub enum PropertyValue {
897    /// Null value
898    Null,
899    /// Boolean value
900    Bool(bool),
901    /// 64-bit signed integer
902    I64(i64),
903    /// 64-bit floating point
904    F64(f64),
905    /// 32-bit floating point
906    F32(f32),
907    /// UTF-8 string
908    String(String),
909    /// Raw bytes
910    Bytes(Vec<u8>),
911    /// Array of i64 values
912    I64Array(Vec<i64>),
913    /// Array of f64 values
914    F64Array(Vec<f64>),
915    /// Array of f32 values
916    F32Array(Vec<f32>),
917    /// Array of strings
918    StringArray(Vec<String>),
919    /// Heterogeneous array value for parameter payloads
920    Array(Vec<PropertyValue>),
921    /// Object/map value for parameter payloads
922    Object(BTreeMap<String, PropertyValue>),
923}
924
925impl PropertyValue {
926    /// Get value as string reference if it is a String
927    pub fn as_str(&self) -> Option<&str> {
928        match self {
929            PropertyValue::String(s) => Some(s),
930            _ => None,
931        }
932    }
933
934    /// Get value as i64 if it is an I64
935    pub fn as_i64(&self) -> Option<i64> {
936        match self {
937            PropertyValue::I64(n) => Some(*n),
938            _ => None,
939        }
940    }
941
942    /// Get value as f64 if it is an F64
943    pub fn as_f64(&self) -> Option<f64> {
944        match self {
945            PropertyValue::F64(n) => Some(*n),
946            PropertyValue::F32(n) => Some(*n as f64),
947            _ => None,
948        }
949    }
950
951    /// Get value as bool if it is a Bool
952    pub fn as_bool(&self) -> Option<bool> {
953        match self {
954            PropertyValue::Bool(b) => Some(*b),
955            _ => None,
956        }
957    }
958
959    /// Get value as array reference if it is an Array
960    pub fn as_array(&self) -> Option<&[PropertyValue]> {
961        match self {
962            PropertyValue::Array(values) => Some(values),
963            _ => None,
964        }
965    }
966
967    /// Get value as object reference if it is an Object
968    pub fn as_object(&self) -> Option<&BTreeMap<String, PropertyValue>> {
969        match self {
970            PropertyValue::Object(values) => Some(values),
971            _ => None,
972        }
973    }
974}
975
976impl From<&str> for PropertyValue {
977    fn from(s: &str) -> Self {
978        PropertyValue::String(s.to_string())
979    }
980}
981
982impl From<String> for PropertyValue {
983    fn from(s: String) -> Self {
984        PropertyValue::String(s)
985    }
986}
987
988impl From<i64> for PropertyValue {
989    fn from(n: i64) -> Self {
990        PropertyValue::I64(n)
991    }
992}
993
994impl From<i32> for PropertyValue {
995    fn from(n: i32) -> Self {
996        PropertyValue::I64(n as i64)
997    }
998}
999
1000impl From<f64> for PropertyValue {
1001    fn from(n: f64) -> Self {
1002        PropertyValue::F64(n)
1003    }
1004}
1005
1006impl From<f32> for PropertyValue {
1007    fn from(n: f32) -> Self {
1008        PropertyValue::F32(n)
1009    }
1010}
1011
1012impl From<bool> for PropertyValue {
1013    fn from(b: bool) -> Self {
1014        PropertyValue::Bool(b)
1015    }
1016}
1017
1018impl From<Vec<u8>> for PropertyValue {
1019    fn from(bytes: Vec<u8>) -> Self {
1020        PropertyValue::Bytes(bytes)
1021    }
1022}
1023
1024impl From<Vec<i64>> for PropertyValue {
1025    fn from(values: Vec<i64>) -> Self {
1026        PropertyValue::I64Array(values)
1027    }
1028}
1029
1030impl From<Vec<f64>> for PropertyValue {
1031    fn from(values: Vec<f64>) -> Self {
1032        PropertyValue::F64Array(values)
1033    }
1034}
1035
1036impl From<Vec<f32>> for PropertyValue {
1037    fn from(values: Vec<f32>) -> Self {
1038        PropertyValue::F32Array(values)
1039    }
1040}
1041
1042impl From<Vec<String>> for PropertyValue {
1043    fn from(values: Vec<String>) -> Self {
1044        PropertyValue::StringArray(values)
1045    }
1046}
1047
1048impl From<Vec<PropertyValue>> for PropertyValue {
1049    fn from(values: Vec<PropertyValue>) -> Self {
1050        PropertyValue::Array(values)
1051    }
1052}
1053
1054impl From<BTreeMap<String, PropertyValue>> for PropertyValue {
1055    fn from(values: BTreeMap<String, PropertyValue>) -> Self {
1056        PropertyValue::Object(values)
1057    }
1058}
1059
1060impl From<HashMap<String, PropertyValue>> for PropertyValue {
1061    fn from(values: HashMap<String, PropertyValue>) -> Self {
1062        PropertyValue::Object(values.into_iter().collect())
1063    }
1064}
1065
1066/// Mutation input value for add/set property operations.
1067#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1068pub enum PropertyInput {
1069    /// Store a literal property value.
1070    Value(PropertyValue),
1071    /// Resolve the value from an expression at execution time.
1072    Expr(Expr),
1073}
1074
1075impl PropertyInput {
1076    /// Create an input from a query parameter.
1077    pub fn param(name: impl Into<String>) -> Self {
1078        Self::Expr(Expr::param(name))
1079    }
1080}
1081
1082impl<T> From<T> for PropertyInput
1083where
1084    PropertyValue: From<T>,
1085{
1086    fn from(value: T) -> Self {
1087        Self::Value(value.into())
1088    }
1089}
1090
1091impl From<Expr> for PropertyInput {
1092    fn from(value: Expr) -> Self {
1093        Self::Expr(value)
1094    }
1095}
1096
1097// Reference Types
1098
1099/// A reference to nodes - can be concrete IDs or a variable name
1100///
1101/// This allows the AST to express operations without knowing actual IDs at build time.
1102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1103pub enum NodeRef {
1104    /// One or more concrete node IDs
1105    Ids(Vec<NodeId>),
1106    /// Reference nodes stored in a named variable
1107    Var(String),
1108}
1109
1110impl NodeRef {
1111    /// Create a reference to a single node ID
1112    pub fn id(id: NodeId) -> Self {
1113        NodeRef::Ids(vec![id])
1114    }
1115
1116    /// Create a reference to multiple node IDs
1117    pub fn ids(ids: impl IntoIterator<Item = NodeId>) -> Self {
1118        NodeRef::Ids(ids.into_iter().collect())
1119    }
1120
1121    /// Create a reference to a variable
1122    pub fn var(name: impl Into<String>) -> Self {
1123        NodeRef::Var(name.into())
1124    }
1125}
1126
1127impl From<NodeId> for NodeRef {
1128    fn from(id: NodeId) -> Self {
1129        NodeRef::Ids(vec![id])
1130    }
1131}
1132
1133impl From<Vec<NodeId>> for NodeRef {
1134    fn from(ids: Vec<NodeId>) -> Self {
1135        NodeRef::Ids(ids)
1136    }
1137}
1138
1139impl<const N: usize> From<[NodeId; N]> for NodeRef {
1140    fn from(ids: [NodeId; N]) -> Self {
1141        NodeRef::Ids(ids.to_vec())
1142    }
1143}
1144
1145impl From<&str> for NodeRef {
1146    fn from(var_name: &str) -> Self {
1147        NodeRef::Var(var_name.to_string())
1148    }
1149}
1150
1151/// A reference to edges - can be concrete IDs or a variable name
1152///
1153/// This allows the AST to express operations without knowing actual IDs at build time.
1154/// Edge IDs are separate from node IDs in the graph.
1155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1156pub enum EdgeRef {
1157    /// One or more concrete edge IDs
1158    Ids(Vec<EdgeId>),
1159    /// Reference edges stored in a named variable
1160    Var(String),
1161}
1162
1163impl EdgeRef {
1164    /// Create a reference to a single edge ID
1165    pub fn id(id: EdgeId) -> Self {
1166        EdgeRef::Ids(vec![id])
1167    }
1168
1169    /// Create a reference to multiple edge IDs
1170    pub fn ids(ids: impl IntoIterator<Item = EdgeId>) -> Self {
1171        EdgeRef::Ids(ids.into_iter().collect())
1172    }
1173
1174    /// Create a reference to a variable containing edges
1175    pub fn var(name: impl Into<String>) -> Self {
1176        EdgeRef::Var(name.into())
1177    }
1178}
1179
1180impl From<EdgeId> for EdgeRef {
1181    fn from(id: EdgeId) -> Self {
1182        EdgeRef::Ids(vec![id])
1183    }
1184}
1185
1186impl From<Vec<EdgeId>> for EdgeRef {
1187    fn from(ids: Vec<EdgeId>) -> Self {
1188        EdgeRef::Ids(ids)
1189    }
1190}
1191
1192impl<const N: usize> From<[EdgeId; N]> for EdgeRef {
1193    fn from(ids: [EdgeId; N]) -> Self {
1194        EdgeRef::Ids(ids.to_vec())
1195    }
1196}
1197
1198// Expression Types
1199
1200/// An expression for computed values, math operations, and property references
1201///
1202/// Expressions can be used in predicates for property-to-property comparisons,
1203/// computed values, and math operations.
1204///
1205/// Note: support for some expression variants is engine-dependent. In particular,
1206/// `Expr::Id` may be reserved or unsupported by some runtimes.
1207///
1208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1209pub enum Expr {
1210    /// Reference a property by name
1211    Property(String),
1212    /// The current element ID (engine-defined).
1213    Id,
1214    /// A constant value
1215    Constant(PropertyValue),
1216    /// Reference a query parameter by name
1217    Param(String),
1218    /// Addition: left + right
1219    Add(Box<Expr>, Box<Expr>),
1220    /// Subtraction: left - right
1221    Sub(Box<Expr>, Box<Expr>),
1222    /// Multiplication: left * right
1223    Mul(Box<Expr>, Box<Expr>),
1224    /// Division: left / right
1225    Div(Box<Expr>, Box<Expr>),
1226    /// Modulo: left % right
1227    Mod(Box<Expr>, Box<Expr>),
1228    /// Negation: -expr
1229    Neg(Box<Expr>),
1230}
1231
1232impl Expr {
1233    /// Create a property reference expression
1234    pub fn prop(name: impl Into<String>) -> Self {
1235        Expr::Property(name.into())
1236    }
1237
1238    /// Create a constant value expression
1239    pub fn val(value: impl Into<PropertyValue>) -> Self {
1240        Expr::Constant(value.into())
1241    }
1242
1243    /// Create an ID reference expression
1244    pub fn id() -> Self {
1245        Expr::Id
1246    }
1247
1248    /// Create a parameter reference expression
1249    pub fn param(name: impl Into<String>) -> Self {
1250        Expr::Param(name.into())
1251    }
1252
1253    /// Addition: self + other
1254    pub fn add(self, other: Expr) -> Self {
1255        Expr::Add(Box::new(self), Box::new(other))
1256    }
1257
1258    /// Subtraction: self - other
1259    pub fn sub(self, other: Expr) -> Self {
1260        Expr::Sub(Box::new(self), Box::new(other))
1261    }
1262
1263    /// Multiplication: self * other
1264    pub fn mul(self, other: Expr) -> Self {
1265        Expr::Mul(Box::new(self), Box::new(other))
1266    }
1267
1268    /// Division: self / other
1269    pub fn div(self, other: Expr) -> Self {
1270        Expr::Div(Box::new(self), Box::new(other))
1271    }
1272
1273    /// Modulo: self % other
1274    pub fn modulo(self, other: Expr) -> Self {
1275        Expr::Mod(Box::new(self), Box::new(other))
1276    }
1277
1278    /// Negation: -self
1279    pub fn neg(self) -> Self {
1280        Expr::Neg(Box::new(self))
1281    }
1282}
1283
1284/// A non-negative integer input used by stream-shaping steps like `limit`, `skip`, and `range`.
1285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1286pub enum StreamBound {
1287    /// A literal bound known at query-build time.
1288    Literal(usize),
1289    /// A computed or parameterized bound resolved by the runtime.
1290    Expr(Expr),
1291}
1292
1293impl StreamBound {
1294    /// Create a literal bound.
1295    pub fn literal(value: usize) -> Self {
1296        Self::Literal(value)
1297    }
1298
1299    /// Create an expression-backed bound.
1300    pub fn expr(expr: Expr) -> Self {
1301        Self::Expr(expr)
1302    }
1303}
1304
1305impl From<usize> for StreamBound {
1306    fn from(value: usize) -> Self {
1307        Self::Literal(value)
1308    }
1309}
1310
1311impl From<u32> for StreamBound {
1312    fn from(value: u32) -> Self {
1313        Self::Literal(value as usize)
1314    }
1315}
1316
1317impl From<u16> for StreamBound {
1318    fn from(value: u16) -> Self {
1319        Self::Literal(value as usize)
1320    }
1321}
1322
1323impl From<u8> for StreamBound {
1324    fn from(value: u8) -> Self {
1325        Self::Literal(value as usize)
1326    }
1327}
1328
1329impl From<i64> for StreamBound {
1330    fn from(value: i64) -> Self {
1331        if value >= 0 {
1332            Self::Literal(value as usize)
1333        } else {
1334            Self::Expr(Expr::val(value))
1335        }
1336    }
1337}
1338
1339impl From<i32> for StreamBound {
1340    fn from(value: i32) -> Self {
1341        if value >= 0 {
1342            Self::Literal(value as usize)
1343        } else {
1344            Self::Expr(Expr::val(value))
1345        }
1346    }
1347}
1348
1349impl From<Expr> for StreamBound {
1350    fn from(value: Expr) -> Self {
1351        Self::Expr(value)
1352    }
1353}
1354
1355/// Comparison operators for expression-based predicates
1356#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1357pub enum CompareOp {
1358    /// Equal
1359    Eq,
1360    /// Not equal
1361    Neq,
1362    /// Greater than
1363    Gt,
1364    /// Greater than or equal
1365    Gte,
1366    /// Less than
1367    Lt,
1368    /// Less than or equal
1369    Lte,
1370}
1371
1372// Predicate Types
1373
1374/// A predicate for filtering nodes by properties
1375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1376pub enum Predicate {
1377    /// Equals: property == value
1378    Eq(String, PropertyValue),
1379    /// Not equals: property != value
1380    Neq(String, PropertyValue),
1381    /// Greater than: property > value (for numeric/string)
1382    Gt(String, PropertyValue),
1383    /// Greater than or equal: property >= value
1384    Gte(String, PropertyValue),
1385    /// Less than: property < value
1386    Lt(String, PropertyValue),
1387    /// Less than or equal: property <= value
1388    Lte(String, PropertyValue),
1389    /// Between (inclusive): min <= property <= max
1390    Between(String, PropertyValue, PropertyValue),
1391    /// Property exists
1392    HasKey(String),
1393    /// String starts with prefix
1394    StartsWith(String, String),
1395    /// String ends with suffix
1396    EndsWith(String, String),
1397    /// String contains substring
1398    Contains(String, String),
1399    /// String contains a runtime expression result
1400    ContainsExpr(String, Expr),
1401    /// Logical AND of predicates
1402    And(Vec<Predicate>),
1403    /// Logical OR of predicates
1404    Or(Vec<Predicate>),
1405    /// Logical NOT of predicate
1406    Not(Box<Predicate>),
1407    /// Expression-based comparison (supports property-to-property, math, etc.)
1408    Compare {
1409        /// Left side of comparison
1410        left: Expr,
1411        /// Comparison operator
1412        op: CompareOp,
1413        /// Right side of comparison
1414        right: Expr,
1415    },
1416}
1417
1418/// A predicate that can be used in source steps (`n_where` / `e_where`).
1419///
1420/// This is a restricted subset of [`Predicate`] intended to be index- and
1421/// planner-friendly for "source" selection.
1422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1423pub enum SourcePredicate {
1424    /// Equals: property == value
1425    Eq(String, PropertyValue),
1426    /// Not equals: property != value
1427    Neq(String, PropertyValue),
1428    /// Greater than: property > value (for numeric/string)
1429    Gt(String, PropertyValue),
1430    /// Greater than or equal: property >= value
1431    Gte(String, PropertyValue),
1432    /// Less than: property < value
1433    Lt(String, PropertyValue),
1434    /// Less than or equal: property <= value
1435    Lte(String, PropertyValue),
1436    /// Between (inclusive): min <= property <= max
1437    Between(String, PropertyValue, PropertyValue),
1438    /// Property exists
1439    HasKey(String),
1440    /// String starts with prefix
1441    StartsWith(String, String),
1442    /// Logical AND of predicates
1443    And(Vec<SourcePredicate>),
1444    /// Logical OR of predicates
1445    Or(Vec<SourcePredicate>),
1446}
1447
1448impl SourcePredicate {
1449    /// Create an equality predicate
1450    pub fn eq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1451        SourcePredicate::Eq(property.into(), value.into())
1452    }
1453
1454    /// Create a not-equals predicate
1455    pub fn neq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1456        SourcePredicate::Neq(property.into(), value.into())
1457    }
1458
1459    /// Create a greater-than predicate
1460    pub fn gt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1461        SourcePredicate::Gt(property.into(), value.into())
1462    }
1463
1464    /// Create a greater-than-or-equal predicate
1465    pub fn gte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1466        SourcePredicate::Gte(property.into(), value.into())
1467    }
1468
1469    /// Create a less-than predicate
1470    pub fn lt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1471        SourcePredicate::Lt(property.into(), value.into())
1472    }
1473
1474    /// Create a less-than-or-equal predicate
1475    pub fn lte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1476        SourcePredicate::Lte(property.into(), value.into())
1477    }
1478
1479    /// Create a between predicate (inclusive)
1480    pub fn between(
1481        property: impl Into<String>,
1482        min: impl Into<PropertyValue>,
1483        max: impl Into<PropertyValue>,
1484    ) -> Self {
1485        SourcePredicate::Between(property.into(), min.into(), max.into())
1486    }
1487
1488    /// Create a has-key predicate
1489    pub fn has_key(property: impl Into<String>) -> Self {
1490        SourcePredicate::HasKey(property.into())
1491    }
1492
1493    /// Create a starts-with predicate
1494    pub fn starts_with(property: impl Into<String>, prefix: impl Into<String>) -> Self {
1495        SourcePredicate::StartsWith(property.into(), prefix.into())
1496    }
1497
1498    /// Combine predicates with AND
1499    pub fn and(predicates: Vec<SourcePredicate>) -> Self {
1500        SourcePredicate::And(predicates)
1501    }
1502
1503    /// Combine predicates with OR
1504    pub fn or(predicates: Vec<SourcePredicate>) -> Self {
1505        SourcePredicate::Or(predicates)
1506    }
1507}
1508
1509impl From<SourcePredicate> for Predicate {
1510    fn from(predicate: SourcePredicate) -> Self {
1511        match predicate {
1512            SourcePredicate::Eq(prop, val) => Predicate::Eq(prop, val),
1513            SourcePredicate::Neq(prop, val) => Predicate::Neq(prop, val),
1514            SourcePredicate::Gt(prop, val) => Predicate::Gt(prop, val),
1515            SourcePredicate::Gte(prop, val) => Predicate::Gte(prop, val),
1516            SourcePredicate::Lt(prop, val) => Predicate::Lt(prop, val),
1517            SourcePredicate::Lte(prop, val) => Predicate::Lte(prop, val),
1518            SourcePredicate::Between(prop, min, max) => Predicate::Between(prop, min, max),
1519            SourcePredicate::HasKey(prop) => Predicate::HasKey(prop),
1520            SourcePredicate::StartsWith(prop, prefix) => Predicate::StartsWith(prop, prefix),
1521            SourcePredicate::And(predicates) => {
1522                Predicate::And(predicates.into_iter().map(Predicate::from).collect())
1523            }
1524            SourcePredicate::Or(predicates) => {
1525                Predicate::Or(predicates.into_iter().map(Predicate::from).collect())
1526            }
1527        }
1528    }
1529}
1530
1531impl Predicate {
1532    /// Create an equality predicate
1533    pub fn eq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1534        Predicate::Eq(property.into(), value.into())
1535    }
1536
1537    /// Create a not-equals predicate
1538    pub fn neq(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1539        Predicate::Neq(property.into(), value.into())
1540    }
1541
1542    /// Create a greater-than predicate
1543    pub fn gt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1544        Predicate::Gt(property.into(), value.into())
1545    }
1546
1547    /// Create a greater-than-or-equal predicate
1548    pub fn gte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1549        Predicate::Gte(property.into(), value.into())
1550    }
1551
1552    /// Create a less-than predicate
1553    pub fn lt(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1554        Predicate::Lt(property.into(), value.into())
1555    }
1556
1557    /// Create a less-than-or-equal predicate
1558    pub fn lte(property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1559        Predicate::Lte(property.into(), value.into())
1560    }
1561
1562    /// Create a between predicate (inclusive)
1563    pub fn between(
1564        property: impl Into<String>,
1565        min: impl Into<PropertyValue>,
1566        max: impl Into<PropertyValue>,
1567    ) -> Self {
1568        Predicate::Between(property.into(), min.into(), max.into())
1569    }
1570
1571    /// Create a has-key predicate
1572    pub fn has_key(property: impl Into<String>) -> Self {
1573        Predicate::HasKey(property.into())
1574    }
1575
1576    /// Create a starts-with predicate
1577    pub fn starts_with(property: impl Into<String>, prefix: impl Into<String>) -> Self {
1578        Predicate::StartsWith(property.into(), prefix.into())
1579    }
1580
1581    /// Create an ends-with predicate
1582    pub fn ends_with(property: impl Into<String>, suffix: impl Into<String>) -> Self {
1583        Predicate::EndsWith(property.into(), suffix.into())
1584    }
1585
1586    /// Create a contains predicate
1587    pub fn contains(property: impl Into<String>, substring: impl Into<String>) -> Self {
1588        Predicate::Contains(property.into(), substring.into())
1589    }
1590
1591    /// Create a parameterized contains predicate: property contains param string
1592    pub fn contains_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1593        Predicate::ContainsExpr(property.into(), Expr::Param(param_name.into()))
1594    }
1595
1596    /// Combine predicates with AND
1597    pub fn and(predicates: Vec<Predicate>) -> Self {
1598        Predicate::And(predicates)
1599    }
1600
1601    /// Combine predicates with OR
1602    pub fn or(predicates: Vec<Predicate>) -> Self {
1603        Predicate::Or(predicates)
1604    }
1605
1606    /// Negate a predicate
1607    pub fn not(predicate: Predicate) -> Self {
1608        Predicate::Not(Box::new(predicate))
1609    }
1610
1611    /// Create an expression-based comparison predicate
1612    ///
1613    /// This supports property-to-property comparisons, math expressions, and more.
1614    ///
1615    pub fn compare(left: Expr, op: CompareOp, right: Expr) -> Self {
1616        Predicate::Compare { left, op, right }
1617    }
1618
1619    // Parameterized predicate constructors
1620
1621    /// Create a parameterized equality predicate: property == param
1622    ///
1623    /// The parameter value is provided at query execution time.
1624    ///
1625    pub fn eq_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1626        Predicate::Compare {
1627            left: Expr::Property(property.into()),
1628            op: CompareOp::Eq,
1629            right: Expr::Param(param_name.into()),
1630        }
1631    }
1632
1633    /// Create a parameterized not-equals predicate: property != param
1634    pub fn neq_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1635        Predicate::Compare {
1636            left: Expr::Property(property.into()),
1637            op: CompareOp::Neq,
1638            right: Expr::Param(param_name.into()),
1639        }
1640    }
1641
1642    /// Create a parameterized greater-than predicate: property > param
1643    pub fn gt_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1644        Predicate::Compare {
1645            left: Expr::Property(property.into()),
1646            op: CompareOp::Gt,
1647            right: Expr::Param(param_name.into()),
1648        }
1649    }
1650
1651    /// Create a parameterized greater-than-or-equal predicate: property >= param
1652    pub fn gte_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1653        Predicate::Compare {
1654            left: Expr::Property(property.into()),
1655            op: CompareOp::Gte,
1656            right: Expr::Param(param_name.into()),
1657        }
1658    }
1659
1660    /// Create a parameterized less-than predicate: property < param
1661    pub fn lt_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1662        Predicate::Compare {
1663            left: Expr::Property(property.into()),
1664            op: CompareOp::Lt,
1665            right: Expr::Param(param_name.into()),
1666        }
1667    }
1668
1669    /// Create a parameterized less-than-or-equal predicate: property <= param
1670    pub fn lte_param(property: impl Into<String>, param_name: impl Into<String>) -> Self {
1671        Predicate::Compare {
1672            left: Expr::Property(property.into()),
1673            op: CompareOp::Lte,
1674            right: Expr::Param(param_name.into()),
1675        }
1676    }
1677}
1678
1679// Supporting Types
1680
1681/// A property projection with optional renaming
1682#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1683pub struct PropertyProjection {
1684    /// Original property name in the data
1685    pub source: String,
1686    /// Name to use in the output (alias)
1687    pub alias: String,
1688}
1689
1690impl PropertyProjection {
1691    /// Create a projection without renaming
1692    pub fn new(name: impl Into<String>) -> Self {
1693        let n = name.into();
1694        Self {
1695            source: n.clone(),
1696            alias: n,
1697        }
1698    }
1699
1700    /// Create a projection with renaming
1701    pub fn renamed(source: impl Into<String>, alias: impl Into<String>) -> Self {
1702        Self {
1703            source: source.into(),
1704            alias: alias.into(),
1705        }
1706    }
1707}
1708
1709/// Sort order for ordering steps
1710#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1711pub enum Order {
1712    /// Ascending order (smallest first)
1713    Asc,
1714    /// Descending order (largest first)
1715    Desc,
1716}
1717
1718impl Default for Order {
1719    fn default() -> Self {
1720        Order::Asc
1721    }
1722}
1723
1724/// Emit behavior for repeat steps
1725#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1726pub enum EmitBehavior {
1727    /// Don't emit intermediate results.
1728    None,
1729    /// Emit the current node stream before each repeat iteration.
1730    Before,
1731    /// Emit the node stream produced by each repeat iteration.
1732    After,
1733    /// Emit both before and after each repeat iteration.
1734    All,
1735}
1736
1737impl Default for EmitBehavior {
1738    fn default() -> Self {
1739        EmitBehavior::None
1740    }
1741}
1742
1743/// Aggregation function for reduce operations
1744#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1745pub enum AggregateFunction {
1746    /// Count items
1747    Count,
1748    /// Sum numeric values
1749    Sum,
1750    /// Find minimum value
1751    Min,
1752    /// Find maximum value
1753    Max,
1754    /// Calculate mean/average
1755    Mean,
1756}
1757
1758// Sub-Traversal (for branching operations without typestate)
1759
1760/// A sub-traversal for use in branching operations (union, choose, coalesce, optional, repeat).
1761///
1762/// Sub-traversals don't track typestate because they start from an implicit context
1763/// provided by the parent traversal. This allows maximum flexibility in branching
1764/// while the parent traversal maintains compile-time safety.
1765#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1766pub struct SubTraversal {
1767    /// The steps in this sub-traversal
1768    pub steps: Vec<Step>,
1769}
1770
1771impl SubTraversal {
1772    /// Create a new empty sub-traversal
1773    pub fn new() -> Self {
1774        Self { steps: Vec::new() }
1775    }
1776
1777    // Navigation Steps (node -> node)
1778
1779    /// Traverse outgoing edges, optionally filtered by label
1780    pub fn out(mut self, label: Option<impl Into<String>>) -> Self {
1781        self.steps.push(Step::Out(label.map(|l| l.into())));
1782        self
1783    }
1784
1785    /// Traverse incoming edges, optionally filtered by label
1786    pub fn in_(mut self, label: Option<impl Into<String>>) -> Self {
1787        self.steps.push(Step::In(label.map(|l| l.into())));
1788        self
1789    }
1790
1791    /// Traverse edges in both directions, optionally filtered by label
1792    pub fn both(mut self, label: Option<impl Into<String>>) -> Self {
1793        self.steps.push(Step::Both(label.map(|l| l.into())));
1794        self
1795    }
1796
1797    // Edge Traversal Steps
1798
1799    /// Traverse to outgoing edges
1800    pub fn out_e(mut self, label: Option<impl Into<String>>) -> Self {
1801        self.steps.push(Step::OutE(label.map(|l| l.into())));
1802        self
1803    }
1804
1805    /// Traverse to incoming edges
1806    pub fn in_e(mut self, label: Option<impl Into<String>>) -> Self {
1807        self.steps.push(Step::InE(label.map(|l| l.into())));
1808        self
1809    }
1810
1811    /// Traverse to edges in both directions
1812    pub fn both_e(mut self, label: Option<impl Into<String>>) -> Self {
1813        self.steps.push(Step::BothE(label.map(|l| l.into())));
1814        self
1815    }
1816
1817    /// From edge, get the target node
1818    pub fn out_n(mut self) -> Self {
1819        self.steps.push(Step::OutN);
1820        self
1821    }
1822
1823    /// From edge, get the source node
1824    pub fn in_n(mut self) -> Self {
1825        self.steps.push(Step::InN);
1826        self
1827    }
1828
1829    /// From edge, get the "other" node (not the one we came from)
1830    pub fn other_n(mut self) -> Self {
1831        self.steps.push(Step::OtherN);
1832        self
1833    }
1834
1835    // Filter Steps
1836
1837    /// Filter by property value
1838    pub fn has(mut self, property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
1839        self.steps.push(Step::Has(property.into(), value.into()));
1840        self
1841    }
1842
1843    /// Filter by label (shorthand for has("$label", value))
1844    pub fn has_label(mut self, label: impl Into<String>) -> Self {
1845        self.steps.push(Step::HasLabel(label.into()));
1846        self
1847    }
1848
1849    /// Filter by property existence
1850    pub fn has_key(mut self, property: impl Into<String>) -> Self {
1851        self.steps.push(Step::HasKey(property.into()));
1852        self
1853    }
1854
1855    /// Filter by a complex predicate
1856    pub fn where_(mut self, predicate: Predicate) -> Self {
1857        self.steps.push(Step::Where(predicate));
1858        self
1859    }
1860
1861    /// Remove duplicates from the stream
1862    pub fn dedup(mut self) -> Self {
1863        self.steps.push(Step::Dedup);
1864        self
1865    }
1866
1867    /// Filter to nodes that exist in a variable
1868    pub fn within(mut self, var_name: impl Into<String>) -> Self {
1869        self.steps.push(Step::Within(var_name.into()));
1870        self
1871    }
1872
1873    /// Filter to nodes that do NOT exist in a variable
1874    pub fn without(mut self, var_name: impl Into<String>) -> Self {
1875        self.steps.push(Step::Without(var_name.into()));
1876        self
1877    }
1878
1879    // Edge Filter Steps
1880
1881    /// Filter edges by property value
1882    pub fn edge_has(
1883        mut self,
1884        property: impl Into<String>,
1885        value: impl Into<PropertyInput>,
1886    ) -> Self {
1887        self.steps
1888            .push(Step::EdgeHas(property.into(), value.into()));
1889        self
1890    }
1891
1892    /// Filter edges by label
1893    pub fn edge_has_label(mut self, label: impl Into<String>) -> Self {
1894        self.steps.push(Step::EdgeHasLabel(label.into()));
1895        self
1896    }
1897
1898    // Limit Steps
1899
1900    /// Take at most N items.
1901    pub fn limit(mut self, n: impl Into<StreamBound>) -> Self {
1902        self.steps.push(limit_step(n));
1903        self
1904    }
1905
1906    /// Skip the first N items.
1907    pub fn skip(mut self, n: impl Into<StreamBound>) -> Self {
1908        self.steps.push(skip_step(n));
1909        self
1910    }
1911
1912    /// Get items in a range [start, end).
1913    pub fn range(mut self, start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Self {
1914        self.steps.push(range_step(start, end));
1915        self
1916    }
1917
1918    // Variable Steps
1919
1920    /// Store current nodes with a name for later reference
1921    pub fn as_(mut self, name: impl Into<String>) -> Self {
1922        self.steps.push(Step::As(name.into()));
1923        self
1924    }
1925
1926    /// Store current nodes to a variable (same as `as_`)
1927    pub fn store(mut self, name: impl Into<String>) -> Self {
1928        self.steps.push(Step::Store(name.into()));
1929        self
1930    }
1931
1932    /// Replace current traversal with nodes from a variable
1933    pub fn select(mut self, name: impl Into<String>) -> Self {
1934        self.steps.push(Step::Select(name.into()));
1935        self
1936    }
1937
1938    // Ordering Steps
1939
1940    /// Order results by a property.
1941    ///
1942    /// Note: some interpreters represent intermediate streams as sets. In those
1943    /// engines, ordering may not be preserved in the returned node set.
1944    pub fn order_by(mut self, property: impl Into<String>, order: Order) -> Self {
1945        self.steps.push(Step::OrderBy(property.into(), order));
1946        self
1947    }
1948
1949    /// Order results by multiple properties with priorities.
1950    ///
1951    /// Note: some interpreters represent intermediate streams as sets. In those
1952    /// engines, ordering may not be preserved in the returned node set.
1953    pub fn order_by_multiple(mut self, orderings: Vec<(impl Into<String>, Order)>) -> Self {
1954        let orderings: Vec<(String, Order)> =
1955            orderings.into_iter().map(|(p, o)| (p.into(), o)).collect();
1956        self.steps.push(Step::OrderByMultiple(orderings));
1957        self
1958    }
1959
1960    // Path Steps
1961
1962    /// Include the full traversal path in results.
1963    ///
1964    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
1965    pub fn path(mut self) -> Self {
1966        self.steps.push(Step::Path);
1967        self
1968    }
1969
1970    /// Filter to only simple paths (no repeated nodes).
1971    ///
1972    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
1973    pub fn simple_path(mut self) -> Self {
1974        self.steps.push(Step::SimplePath);
1975        self
1976    }
1977}
1978
1979/// Create a new sub-traversal for use in branching operations
1980///
1981/// Use this instead of `g()` when building traversals for `union()`, `choose()`,
1982/// `coalesce()`, `optional()`, or `repeat()`.
1983///
1984pub fn sub() -> SubTraversal {
1985    SubTraversal::new()
1986}
1987
1988// Repeat Configuration
1989
1990/// Configuration for repeat steps
1991#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1992pub struct RepeatConfig {
1993    /// The sub-traversal to repeat
1994    pub traversal: SubTraversal,
1995    /// Maximum number of iterations (None = unlimited)
1996    pub times: Option<usize>,
1997    /// Condition to stop repeating (checked each iteration)
1998    pub until: Option<Predicate>,
1999    /// Whether to emit intermediate results
2000    pub emit: EmitBehavior,
2001    /// Optional predicate for conditional emit
2002    pub emit_predicate: Option<Predicate>,
2003    /// Maximum depth to prevent infinite loops (default: 100)
2004    pub max_depth: usize,
2005}
2006
2007impl RepeatConfig {
2008    /// Create a new repeat configuration
2009    pub fn new(traversal: SubTraversal) -> Self {
2010        Self {
2011            traversal,
2012            times: None,
2013            until: None,
2014            emit: EmitBehavior::None,
2015            emit_predicate: None,
2016            max_depth: 100,
2017        }
2018    }
2019
2020    /// Set the number of times to repeat
2021    pub fn times(mut self, n: usize) -> Self {
2022        self.times = Some(n);
2023        self
2024    }
2025
2026    /// Set the until condition
2027    pub fn until(mut self, predicate: Predicate) -> Self {
2028        self.until = Some(predicate);
2029        self
2030    }
2031
2032    /// Emit intermediate results before and after each iteration.
2033    pub fn emit_all(mut self) -> Self {
2034        self.emit = EmitBehavior::All;
2035        self
2036    }
2037
2038    /// Emit intermediate results before each iteration
2039    pub fn emit_before(mut self) -> Self {
2040        self.emit = EmitBehavior::Before;
2041        self
2042    }
2043
2044    /// Emit intermediate results after each iteration
2045    pub fn emit_after(mut self) -> Self {
2046        self.emit = EmitBehavior::After;
2047        self
2048    }
2049
2050    /// Emit intermediate results that match a predicate.
2051    ///
2052    /// This enables post-iteration emission (equivalent to [`EmitBehavior::After`])
2053    /// and applies `predicate` to decide which vertices to emit.
2054    pub fn emit_if(mut self, predicate: Predicate) -> Self {
2055        self.emit = EmitBehavior::After;
2056        self.emit_predicate = Some(predicate);
2057        self
2058    }
2059
2060    /// Set maximum depth to prevent infinite loops
2061    pub fn max_depth(mut self, depth: usize) -> Self {
2062        self.max_depth = depth;
2063        self
2064    }
2065}
2066
2067/// Dynamic index declaration used by runtime index-management steps.
2068#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2069pub enum IndexSpec {
2070    /// Equality index over node properties.
2071    NodeEquality {
2072        /// Node label to scope the index.
2073        label: String,
2074        /// Indexed property name.
2075        property: String,
2076    },
2077    /// Range index over node properties.
2078    NodeRange {
2079        /// Node label to scope the index.
2080        label: String,
2081        /// Indexed property name.
2082        property: String,
2083    },
2084    /// Equality index over edge properties.
2085    EdgeEquality {
2086        /// Edge label to scope the index.
2087        label: String,
2088        /// Indexed property name.
2089        property: String,
2090    },
2091    /// Range index over edge properties.
2092    EdgeRange {
2093        /// Edge label to scope the index.
2094        label: String,
2095        /// Indexed property name.
2096        property: String,
2097    },
2098    /// Vector index over node properties.
2099    NodeVector {
2100        /// Node label to scope the index.
2101        label: String,
2102        /// Property name containing vectors.
2103        property: String,
2104        /// Optional multitenant partition property.
2105        #[serde(default, skip_serializing_if = "Option::is_none")]
2106        tenant_property: Option<String>,
2107    },
2108    /// Vector index over edge properties.
2109    EdgeVector {
2110        /// Edge label to scope the index.
2111        label: String,
2112        /// Property name containing vectors.
2113        property: String,
2114        /// Optional multitenant partition property.
2115        #[serde(default, skip_serializing_if = "Option::is_none")]
2116        tenant_property: Option<String>,
2117    },
2118}
2119
2120impl IndexSpec {
2121    /// Build a node equality index declaration.
2122    pub fn node_equality(label: impl Into<String>, property: impl Into<String>) -> Self {
2123        Self::NodeEquality {
2124            label: label.into(),
2125            property: property.into(),
2126        }
2127    }
2128
2129    /// Build a node range index declaration.
2130    pub fn node_range(label: impl Into<String>, property: impl Into<String>) -> Self {
2131        Self::NodeRange {
2132            label: label.into(),
2133            property: property.into(),
2134        }
2135    }
2136
2137    /// Build an edge equality index declaration.
2138    pub fn edge_equality(label: impl Into<String>, property: impl Into<String>) -> Self {
2139        Self::EdgeEquality {
2140            label: label.into(),
2141            property: property.into(),
2142        }
2143    }
2144
2145    /// Build an edge range index declaration.
2146    pub fn edge_range(label: impl Into<String>, property: impl Into<String>) -> Self {
2147        Self::EdgeRange {
2148            label: label.into(),
2149            property: property.into(),
2150        }
2151    }
2152
2153    /// Build a node vector index declaration.
2154    pub fn node_vector(
2155        label: impl Into<String>,
2156        property: impl Into<String>,
2157        tenant_property: Option<impl Into<String>>,
2158    ) -> Self {
2159        Self::NodeVector {
2160            label: label.into(),
2161            property: property.into(),
2162            tenant_property: tenant_property.map(|value| value.into()),
2163        }
2164    }
2165
2166    /// Build an edge vector index declaration.
2167    pub fn edge_vector(
2168        label: impl Into<String>,
2169        property: impl Into<String>,
2170        tenant_property: Option<impl Into<String>>,
2171    ) -> Self {
2172        Self::EdgeVector {
2173            label: label.into(),
2174            property: property.into(),
2175            tenant_property: tenant_property.map(|value| value.into()),
2176        }
2177    }
2178}
2179
2180// Step Enum (AST Nodes)
2181
2182/// A single step in a traversal AST.
2183///
2184/// Most users should build traversals via [`g()`] and the [`Traversal`] builder.
2185/// This enum exists so the traversal can be inspected, serialized, transported,
2186/// and reconstructed with [`Traversal::from_steps`].
2187#[doc(hidden)]
2188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2189pub enum Step {
2190    // Source Steps - Start a traversal or switch context
2191    /// Start (or switch) on nodes.
2192    ///
2193    /// Typical usage is as the first traversal source via [`Traversal::n`].
2194    N(NodeRef),
2195
2196    /// Start from nodes matching a [`SourcePredicate`].
2197    NWhere(SourcePredicate),
2198
2199    /// Start (or switch) on edges.
2200    ///
2201    /// Typical usage is as the first traversal source via [`Traversal::e`].
2202    E(EdgeRef),
2203
2204    /// Start from edges matching a [`SourcePredicate`].
2205    EWhere(SourcePredicate),
2206
2207    /// Vector similarity search on nodes
2208    ///
2209    /// Start traversal from nodes with vectors similar to the query vector.
2210    /// Uses the HNSW index for the given (label, property) combination.
2211    ///
2212    /// Note: this step encodes the nearest-neighbor search inputs.
2213    /// Implementations may expose ranking and distance metadata at runtime.
2214    VectorSearchNodes {
2215        /// The node label to search
2216        label: String,
2217        /// The property name containing vectors
2218        property: String,
2219        /// Optional multitenant partition value.
2220        #[serde(default, skip_serializing_if = "Option::is_none")]
2221        tenant_value: Option<PropertyInput>,
2222        /// The query vector input.
2223        query_vector: PropertyInput,
2224        /// Number of nearest neighbors to return.
2225        k: StreamBound,
2226    },
2227
2228    /// Vector similarity search on edges
2229    ///
2230    /// Start traversal from edges with vectors similar to the query vector.
2231    /// Uses the HNSW index for the given (label, property) combination.
2232    ///
2233    /// Note: this step encodes the nearest-neighbor search inputs.
2234    /// Implementations may expose ranking and distance metadata at runtime.
2235    VectorSearchEdges {
2236        /// The edge label to search
2237        label: String,
2238        /// The property name containing vectors
2239        property: String,
2240        /// Optional multitenant partition value.
2241        #[serde(default, skip_serializing_if = "Option::is_none")]
2242        tenant_value: Option<PropertyInput>,
2243        /// The query vector input.
2244        query_vector: PropertyInput,
2245        /// Number of nearest neighbors to return.
2246        k: StreamBound,
2247    },
2248
2249    // Traversal Steps - Navigate the graph
2250    /// Traverse outgoing edges, optionally filtered by label.
2251    ///
2252    /// Builder forms:
2253    /// - `.out(Some("KNOWS"))`
2254    /// - `.out(None::<&str>)` (no label filter)
2255    Out(Option<String>),
2256
2257    /// Traverse incoming edges, optionally filtered by label.
2258    ///
2259    /// Builder forms:
2260    /// - `.in_(Some("KNOWS"))`
2261    /// - `.in_(None::<&str>)`
2262    In(Option<String>),
2263
2264    /// Traverse edges in both directions, optionally filtered by label.
2265    ///
2266    /// Builder forms:
2267    /// - `.both(Some("KNOWS"))`
2268    /// - `.both(None::<&str>)`
2269    Both(Option<String>),
2270
2271    // Edge Traversal Steps - Navigate to/from edges
2272    /// Traverse from nodes to outgoing edges.
2273    ///
2274    /// Builder forms:
2275    /// - `.out_e(Some("KNOWS"))`
2276    /// - `.out_e(None::<&str>)`
2277    OutE(Option<String>),
2278
2279    /// Traverse from nodes to incoming edges.
2280    ///
2281    /// Builder forms:
2282    /// - `.in_e(Some("KNOWS"))`
2283    /// - `.in_e(None::<&str>)`
2284    InE(Option<String>),
2285
2286    /// Traverse from nodes to edges in both directions.
2287    ///
2288    /// Builder forms:
2289    /// - `.both_e(Some("KNOWS"))`
2290    /// - `.both_e(None::<&str>)`
2291    BothE(Option<String>),
2292
2293    /// From an edge stream, switch back to nodes by selecting the edge target.
2294    ///
2295    /// Builder form: `.out_n()`
2296    OutN,
2297
2298    /// From an edge stream, switch back to nodes by selecting the edge source.
2299    ///
2300    /// Builder form: `.in_n()`
2301    InN,
2302
2303    /// From an edge stream, switch back to nodes by selecting the "other" endpoint.
2304    ///
2305    /// Builder form: `.other_n()`
2306    OtherN,
2307
2308    // Filter Steps - Reduce the stream
2309    /// Filter nodes by property equality: `.has("name", "Alice")`
2310    Has(String, PropertyValue),
2311
2312    /// Filter nodes by label: `.has_label("User")`.
2313    ///
2314    /// This is shorthand for filtering on the reserved `$label` property.
2315    HasLabel(String),
2316
2317    /// Filter nodes by property existence: `.has_key("email")`
2318    HasKey(String),
2319
2320    /// Filter nodes by a [`Predicate`]: `.where_(Predicate::gt("age", 18i64))`
2321    Where(Predicate),
2322
2323    /// Remove duplicates: `dedup()`
2324    Dedup,
2325
2326    /// Filter to nodes that exist in a variable: `within("x")`
2327    Within(String),
2328
2329    /// Filter to nodes that do NOT exist in a variable: `without("x")`
2330    Without(String),
2331
2332    // Edge Filter Steps - Filter edges
2333    /// Filter edges by property equality: `.edge_has("weight", 1i64)`
2334    EdgeHas(String, PropertyInput),
2335
2336    /// Filter edges by label: `.edge_has_label("KNOWS")`
2337    EdgeHasLabel(String),
2338
2339    // Limit Steps - Control stream size
2340    /// Take first N items: `limit(10)`
2341    Limit(usize),
2342
2343    /// Take first N items using a runtime-resolved expression.
2344    LimitBy(Expr),
2345
2346    /// Skip first N items: `skip(5)`
2347    Skip(usize),
2348
2349    /// Skip first N items using a runtime-resolved expression.
2350    SkipBy(Expr),
2351
2352    /// Get items in range [start, end): equivalent to skip(start).limit(end - start)
2353    Range(usize, usize),
2354
2355    /// Get items in range [start, end) using literal and/or runtime-resolved bounds.
2356    RangeBy(StreamBound, StreamBound),
2357
2358    // Variable Steps - Store and reference results
2359    /// Store the current stream in the traversal context under a name.
2360    ///
2361    /// Builder form: `.as_("x")`
2362    As(String),
2363
2364    /// Store the current stream in the traversal context under a name.
2365    ///
2366    /// Builder form: `.store("x")`
2367    Store(String),
2368
2369    /// Replace the current node stream with nodes referenced by a stored variable.
2370    ///
2371    /// Builder form: `.select("x")`
2372    Select(String),
2373
2374    // Terminal Steps - End the traversal
2375    /// Count results (returns single value)
2376    Count,
2377
2378    /// Check if any results exist (returns bool)
2379    Exists,
2380
2381    /// Get the ID of current nodes/edges (returns the ID as a value)
2382    Id,
2383
2384    /// Get the label of current nodes/edges (returns the $label property)
2385    Label,
2386
2387    // Property Projection Steps - Return property data
2388    /// Return specific node properties.
2389    ///
2390    /// Builder form: `.values(vec!["name", "age"])`
2391    Values(Vec<String>),
2392
2393    /// Return node properties as maps.
2394    ///
2395    /// Builder forms:
2396    /// - `.value_map(None::<Vec<&str>>)` (all properties)
2397    /// - `.value_map(Some(vec!["name", "age"]))`
2398    ValueMap(Option<Vec<String>>),
2399
2400    /// Project properties with optional renaming
2401    Project(Vec<PropertyProjection>),
2402
2403    /// Return edge properties for the current edge stream.
2404    ///
2405    /// Builder form: `.edge_properties()`
2406    EdgeProperties,
2407
2408    /// Create a runtime index, treating existing matching definitions as a no-op
2409    /// when `if_not_exists` is true.
2410    CreateIndex {
2411        /// Index specification to create.
2412        spec: IndexSpec,
2413        /// Whether duplicate creates should be ignored.
2414        if_not_exists: bool,
2415    },
2416
2417    /// Drop a runtime index.
2418    DropIndex {
2419        /// Index specification to drop.
2420        spec: IndexSpec,
2421    },
2422
2423    // Mutation Steps - Modify the graph (write transactions only)
2424    /// Create a vector index for nodes with the given label and property
2425    CreateVectorIndexNodes {
2426        /// Node label to scope the index
2427        label: String,
2428        /// Property name containing vectors
2429        property: String,
2430        /// Optional multitenant partition property.
2431        #[serde(default, skip_serializing_if = "Option::is_none")]
2432        tenant_property: Option<String>,
2433    },
2434
2435    /// Create a vector index for edges with the given label and property
2436    CreateVectorIndexEdges {
2437        /// Edge label to scope the index
2438        label: String,
2439        /// Property name containing vectors
2440        property: String,
2441        /// Optional multitenant partition property.
2442        #[serde(default, skip_serializing_if = "Option::is_none")]
2443        tenant_property: Option<String>,
2444    },
2445
2446    /// Add a node with a label and properties.
2447    ///
2448    /// Builder form: `.add_n("User", vec![("name", "Alice")])`
2449    /// The node ID is allocated automatically.
2450    /// The new node becomes the current traversal context.
2451    AddN {
2452        /// The node label (required)
2453        label: String,
2454        /// Optional properties
2455        properties: Vec<(String, PropertyInput)>,
2456    },
2457
2458    /// Add edges from the current nodes to `to`.
2459    ///
2460    /// Builder form: `.add_e("FOLLOWS", to, vec![("weight", 1i64)])`
2461    AddE {
2462        /// The edge label (required)
2463        label: String,
2464        /// Target nodes (by ID or variable)
2465        to: NodeRef,
2466        /// Optional edge properties
2467        properties: Vec<(String, PropertyInput)>,
2468    },
2469
2470    /// Set/update a property on the current nodes: `.set_property(name, value)`
2471    SetProperty(String, PropertyInput),
2472
2473    /// Remove a property from the current nodes: `.remove_property(name)`
2474    RemoveProperty(String),
2475
2476    /// Delete current nodes (and their edges): `drop()`
2477    Drop,
2478
2479    /// Delete edges from the current nodes to a target set: `.drop_edge(target)`
2480    ///
2481    /// **Note**: In multigraph scenarios, this removes ALL edges between the current
2482    /// nodes and the target nodes. Use `DropEdgeById` for precise edge removal.
2483    DropEdge(NodeRef),
2484
2485    /// Delete only edges with a specific label from the current nodes to a target set.
2486    DropEdgeLabeled {
2487        /// Target nodes to disconnect from.
2488        to: NodeRef,
2489        /// Edge label to remove.
2490        label: String,
2491    },
2492
2493    /// Delete specific edges by their IDs: `.drop_edge_by_id(edge_ref)`
2494    ///
2495    /// This is the multigraph-safe way to remove edges, as it removes specific
2496    /// edges rather than all edges between a pair of nodes.
2497    DropEdgeById(EdgeRef),
2498
2499    // Ordering Steps - Sort the stream
2500    /// Order the node stream by a property: `.order_by("age", Order::Desc)`
2501    OrderBy(String, Order),
2502
2503    /// Order by multiple properties with priorities
2504    OrderByMultiple(Vec<(String, Order)>),
2505
2506    // Loop/Repeat Steps - Iterative traversal
2507    /// Repeat a traversal body.
2508    ///
2509    /// Builder form: `.repeat(RepeatConfig::new(sub().out(None::<&str>)).times(3))`
2510    Repeat(RepeatConfig),
2511
2512    // Branching Steps - Conditional execution
2513    /// Execute multiple sub-traversals and merge their results: `.union(vec![...])`
2514    Union(Vec<SubTraversal>),
2515
2516    /// Conditional branching: `choose(predicate, then_traversal, else_traversal)`
2517    Choose {
2518        /// Condition to check
2519        condition: Predicate,
2520        /// Traversal if condition is true
2521        then_traversal: SubTraversal,
2522        /// Traversal if condition is false (optional)
2523        else_traversal: Option<SubTraversal>,
2524    },
2525
2526    /// Try sub-traversals in order until one produces results: `.coalesce(vec![...])`
2527    Coalesce(Vec<SubTraversal>),
2528
2529    /// Execute a sub-traversal if it produces results, otherwise pass through: `.optional(t)`
2530    Optional(SubTraversal),
2531
2532    // Aggregation Steps - Group and reduce
2533    /// Group by a property.
2534    Group(String),
2535
2536    /// Count occurrences grouped by a property.
2537    GroupCount(String),
2538
2539    /// Apply an aggregation function to a property.
2540    ///
2541    /// Builder form: `.aggregate_by(AggregateFunction::Sum, "price")`
2542    AggregateBy(AggregateFunction, String),
2543
2544    /// Barrier step.
2545    ///
2546    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2547    Fold,
2548
2549    /// Expand a collected list back into individual items.
2550    ///
2551    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2552    Unfold,
2553
2554    // Path Steps - Track traversal history
2555    /// Include the full traversal path in results.
2556    ///
2557    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2558    Path,
2559
2560    /// Filter to paths without repeated nodes (cycle detection).
2561    ///
2562    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2563    SimplePath,
2564
2565    // Sack Steps - Carry state through traversal
2566    /// Initialize a sack with a value.
2567    ///
2568    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2569    WithSack(PropertyValue),
2570
2571    /// Update the sack with a property value.
2572    ///
2573    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2574    SackSet(String),
2575
2576    /// Add to the sack (numeric only).
2577    ///
2578    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2579    SackAdd(String),
2580
2581    /// Get the current sack value.
2582    ///
2583    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
2584    SackGet,
2585
2586    // Inject Steps - Add values to the stream
2587    /// Inject nodes from a variable into the stream.
2588    ///
2589    /// As a source step, this starts from the variable's stored node set.
2590    /// When used mid-traversal, engines may interpret this as a union/merge.
2591    Inject(String),
2592}
2593
2594fn limit_step(bound: impl Into<StreamBound>) -> Step {
2595    match bound.into() {
2596        StreamBound::Literal(n) => Step::Limit(n),
2597        StreamBound::Expr(expr) => Step::LimitBy(expr),
2598    }
2599}
2600
2601fn skip_step(bound: impl Into<StreamBound>) -> Step {
2602    match bound.into() {
2603        StreamBound::Literal(n) => Step::Skip(n),
2604        StreamBound::Expr(expr) => Step::SkipBy(expr),
2605    }
2606}
2607
2608fn range_step(start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Step {
2609    let start = start.into();
2610    let end = end.into();
2611    match (&start, &end) {
2612        (StreamBound::Literal(start), StreamBound::Literal(end)) => Step::Range(*start, *end),
2613        _ => Step::RangeBy(start, end),
2614    }
2615}
2616
2617// Traversal with Typestate
2618
2619/// A complete traversal - a sequence of steps with compile-time state tracking.
2620///
2621/// The type parameter `S` tracks what kind of elements the traversal is currently
2622/// operating on, preventing invalid operation sequences at compile time.
2623///
2624/// # State Types
2625/// - `Empty` - No source step yet, only source operations allowed
2626/// - `OnNodes` - Currently on node stream, node operations allowed
2627/// - `OnEdges` - Currently on edge stream, edge operations allowed
2628/// - `Terminal` - Traversal complete, no more chaining allowed
2629///
2630/// The second type parameter `M` tracks mutation capability:
2631/// - `ReadOnly` - No mutation steps (can be used in read batches)
2632/// - `WriteEnabled` - Contains mutation steps (requires write batch)
2633#[derive(Debug, Clone, PartialEq)]
2634pub struct Traversal<S: TraversalState = OnNodes, M: MutationMode = ReadOnly> {
2635    /// The steps in this traversal.
2636    ///
2637    /// Mutating this vector directly bypasses typestate guarantees enforced by
2638    /// builder methods. Prefer builder APIs unless you intentionally need to
2639    /// manipulate raw AST steps.
2640    #[doc(hidden)]
2641    pub steps: Vec<Step>,
2642    /// Phantom data to track the typestate
2643    _state: PhantomData<S>,
2644    /// Phantom data to track mutation mode
2645    _mode: PhantomData<M>,
2646}
2647
2648impl<S: TraversalState, M: MutationMode> Default for Traversal<S, M> {
2649    fn default() -> Self {
2650        Self {
2651            steps: Vec::new(),
2652            _state: PhantomData,
2653            _mode: PhantomData,
2654        }
2655    }
2656}
2657
2658impl<S: TraversalState, M: MutationMode> Traversal<S, M> {
2659    /// Get the steps of this traversal
2660    #[doc(hidden)]
2661    pub fn into_steps(self) -> Vec<Step> {
2662        self.steps
2663    }
2664
2665    /// Check if this traversal has a terminal step
2666    #[doc(hidden)]
2667    pub fn has_terminal(&self) -> bool {
2668        self.steps.iter().any(|s| {
2669            matches!(
2670                s,
2671                Step::Count
2672                    | Step::Exists
2673                    | Step::Id
2674                    | Step::Label
2675                    | Step::Values(_)
2676                    | Step::ValueMap(_)
2677                    | Step::Project(_)
2678                    | Step::EdgeProperties
2679                    | Step::CreateIndex { .. }
2680                    | Step::DropIndex { .. }
2681                    | Step::CreateVectorIndexNodes { .. }
2682                    | Step::CreateVectorIndexEdges { .. }
2683            )
2684        })
2685    }
2686
2687    /// Create a traversal from steps
2688    ///
2689    /// This is useful for batch execution where queries are stored as Vec<Step>
2690    /// and need to be reconstructed into a Traversal.
2691    ///
2692    /// This constructor does not validate that `steps` matches `S`/`M`.
2693    /// Prefer builder entry points like [`g()`], [`read_batch()`], and [`write_batch()`]
2694    /// unless you intentionally need a raw reconstruction.
2695    #[doc(hidden)]
2696    pub fn from_steps(steps: Vec<Step>) -> Self {
2697        Self {
2698            steps,
2699            _state: PhantomData,
2700            _mode: PhantomData,
2701        }
2702    }
2703
2704    /// Add a step and transition to a new state (preserving mutation mode)
2705    fn push_step<T: TraversalState>(mut self, step: Step) -> Traversal<T, M> {
2706        self.steps.push(step);
2707        Traversal::from_steps(self.steps)
2708    }
2709
2710    /// Add a step and transition to WriteEnabled mode
2711    fn push_mutation_step<T: TraversalState>(mut self, step: Step) -> Traversal<T, WriteEnabled> {
2712        self.steps.push(step);
2713        Traversal::from_steps(self.steps)
2714    }
2715}
2716
2717// Empty State Implementation - Source Steps Only
2718
2719impl Traversal<Empty, ReadOnly> {
2720    /// Create a new empty traversal
2721    #[doc(hidden)]
2722    pub fn new() -> Self {
2723        Self {
2724            steps: Vec::new(),
2725            _state: PhantomData,
2726            _mode: PhantomData,
2727        }
2728    }
2729
2730    // Node Source Steps: Empty -> OnNodes
2731
2732    /// Start traversal from nodes by IDs or a variable.
2733    ///
2734    pub fn n(self, nodes: impl Into<NodeRef>) -> Traversal<OnNodes> {
2735        self.push_step(Step::N(nodes.into()))
2736    }
2737
2738    /// Start traversal from nodes matching a predicate
2739    ///
2740    ///
2741    pub fn n_where(self, predicate: SourcePredicate) -> Traversal<OnNodes> {
2742        self.push_step(Step::NWhere(predicate))
2743    }
2744
2745    /// Start traversal from nodes with a specific label
2746    ///
2747    /// This is a convenience method equivalent to `n_where(SourcePredicate::eq("$label", label))`.
2748    ///
2749    pub fn n_with_label(self, label: impl Into<String>) -> Traversal<OnNodes> {
2750        self.n_where(SourcePredicate::Eq(
2751            "$label".to_string(),
2752            PropertyValue::String(label.into()),
2753        ))
2754    }
2755
2756    /// Start traversal from nodes with a specific label and additional predicate
2757    ///
2758    /// This is a convenience method equivalent to
2759    /// `n_where(SourcePredicate::and(vec![SourcePredicate::eq("$label", label), predicate]))`.
2760    ///
2761    pub fn n_with_label_where(
2762        self,
2763        label: impl Into<String>,
2764        predicate: SourcePredicate,
2765    ) -> Traversal<OnNodes> {
2766        self.n_where(SourcePredicate::And(vec![
2767            SourcePredicate::Eq("$label".to_string(), PropertyValue::String(label.into())),
2768            predicate,
2769        ]))
2770    }
2771
2772    // Vector Search Source Steps
2773
2774    /// Start traversal from nodes with vectors similar to the query vector
2775    ///
2776    /// Uses the HNSW index for the given (label, property) combination to find
2777    /// the k nearest neighbors to the query vector.
2778    ///
2779    /// Runtime behavior in the current Helix interpreter:
2780    /// - returns top-k nearest hits (up to `k`) ordered by ascending distance
2781    /// - `value_map`, `values`, and `project` can read virtual fields `$id` and `$distance`
2782    /// - after traversing away from the hit stream (for example, `out`/`in_`),
2783    ///   distance metadata is no longer attached to downstream traversers
2784    ///
2785    /// # Arguments
2786    /// * `label` - The node label to search
2787    /// * `property` - The property name containing vectors
2788    /// * `query_vector` - The query vector
2789    /// * `k` - Number of nearest neighbors to return
2790    /// * `tenant_value` - Optional multitenant partition value
2791    ///
2792    pub fn vector_search_nodes(
2793        self,
2794        label: impl Into<String>,
2795        property: impl Into<String>,
2796        query_vector: Vec<f32>,
2797        k: usize,
2798        tenant_value: Option<PropertyValue>,
2799    ) -> Traversal<OnNodes> {
2800        self.vector_search_nodes_with(
2801            label,
2802            property,
2803            query_vector,
2804            k,
2805            tenant_value.map(PropertyInput::from),
2806        )
2807    }
2808
2809    /// Start traversal from nodes with vectors similar to the query vector.
2810    ///
2811    /// This variant accepts runtime-resolved inputs for the query vector, result
2812    /// count, and tenant partition value.
2813    pub fn vector_search_nodes_with(
2814        self,
2815        label: impl Into<String>,
2816        property: impl Into<String>,
2817        query_vector: impl Into<PropertyInput>,
2818        k: impl Into<StreamBound>,
2819        tenant_value: Option<PropertyInput>,
2820    ) -> Traversal<OnNodes> {
2821        self.push_step(Step::VectorSearchNodes {
2822            label: label.into(),
2823            property: property.into(),
2824            tenant_value,
2825            query_vector: query_vector.into(),
2826            k: k.into(),
2827        })
2828    }
2829
2830    // Edge Source Steps: Empty -> OnEdges
2831
2832    /// Start traversal from edges by IDs or a variable.
2833    ///
2834    pub fn e(self, edges: impl Into<EdgeRef>) -> Traversal<OnEdges> {
2835        self.push_step(Step::E(edges.into()))
2836    }
2837
2838    /// Start traversal from edges matching a predicate
2839    ///
2840    ///
2841    pub fn e_where(self, predicate: SourcePredicate) -> Traversal<OnEdges> {
2842        self.push_step(Step::EWhere(predicate))
2843    }
2844
2845    /// Start traversal from edges with a specific label
2846    ///
2847    /// This is a convenience method equivalent to `e_where(SourcePredicate::eq("$label", label))`.
2848    ///
2849    pub fn e_with_label(self, label: impl Into<String>) -> Traversal<OnEdges> {
2850        self.e_where(SourcePredicate::Eq(
2851            "$label".to_string(),
2852            PropertyValue::String(label.into()),
2853        ))
2854    }
2855
2856    /// Start traversal from edges with a specific label and additional predicate
2857    ///
2858    /// This is a convenience method equivalent to
2859    /// `e_where(SourcePredicate::and(vec![SourcePredicate::eq("$label", label), predicate]))`.
2860    ///
2861    pub fn e_with_label_where(
2862        self,
2863        label: impl Into<String>,
2864        predicate: SourcePredicate,
2865    ) -> Traversal<OnEdges> {
2866        self.e_where(SourcePredicate::And(vec![
2867            SourcePredicate::Eq("$label".to_string(), PropertyValue::String(label.into())),
2868            predicate,
2869        ]))
2870    }
2871
2872    /// Start traversal from edges with vectors similar to the query vector
2873    ///
2874    /// Uses the HNSW index for the given (label, property) combination to find
2875    /// the k nearest neighbors to the query vector.
2876    ///
2877    /// Runtime behavior in the current Helix interpreter:
2878    /// - returns top-k nearest hits (up to `k`) ordered by ascending distance
2879    /// - `edge_properties` includes virtual fields `$from`, `$to`, and `$distance`
2880    ///   (plus `$id` when available)
2881    ///
2882    /// # Arguments
2883    /// * `label` - The edge label to search
2884    /// * `property` - The property name containing vectors
2885    /// * `query_vector` - The query vector
2886    /// * `k` - Number of nearest neighbors to return
2887    /// * `tenant_value` - Optional multitenant partition value
2888    ///
2889    pub fn vector_search_edges(
2890        self,
2891        label: impl Into<String>,
2892        property: impl Into<String>,
2893        query_vector: Vec<f32>,
2894        k: usize,
2895        tenant_value: Option<PropertyValue>,
2896    ) -> Traversal<OnEdges> {
2897        self.vector_search_edges_with(
2898            label,
2899            property,
2900            query_vector,
2901            k,
2902            tenant_value.map(PropertyInput::from),
2903        )
2904    }
2905
2906    /// Start traversal from edges with vectors similar to the query vector.
2907    ///
2908    /// This variant accepts runtime-resolved inputs for the query vector, result
2909    /// count, and tenant partition value.
2910    pub fn vector_search_edges_with(
2911        self,
2912        label: impl Into<String>,
2913        property: impl Into<String>,
2914        query_vector: impl Into<PropertyInput>,
2915        k: impl Into<StreamBound>,
2916        tenant_value: Option<PropertyInput>,
2917    ) -> Traversal<OnEdges> {
2918        self.push_step(Step::VectorSearchEdges {
2919            label: label.into(),
2920            property: property.into(),
2921            tenant_value,
2922            query_vector: query_vector.into(),
2923            k: k.into(),
2924        })
2925    }
2926
2927    // Mutation Source Steps: Empty -> OnNodes
2928
2929    /// Create a runtime index if it does not already exist.
2930    pub fn create_index_if_not_exists(self, spec: IndexSpec) -> Traversal<Terminal, WriteEnabled> {
2931        self.push_mutation_step(Step::CreateIndex {
2932            spec,
2933            if_not_exists: true,
2934        })
2935    }
2936
2937    /// Drop a runtime index.
2938    pub fn drop_index(self, spec: IndexSpec) -> Traversal<Terminal, WriteEnabled> {
2939        self.push_mutation_step(Step::DropIndex { spec })
2940    }
2941
2942    /// Create a vector index on nodes.
2943    ///
2944    /// This is a write-only source step intended for index management. It does not
2945    /// produce a useful traversal stream, so the builder marks it as terminal.
2946    /// Runtime index parameters are selected by the database.
2947    ///
2948    pub fn create_vector_index_nodes(
2949        self,
2950        label: impl Into<String>,
2951        property: impl Into<String>,
2952        tenant_property: Option<impl Into<String>>,
2953    ) -> Traversal<Terminal, WriteEnabled> {
2954        self.create_index_if_not_exists(IndexSpec::node_vector(label, property, tenant_property))
2955    }
2956
2957    /// Create a vector index on edges.
2958    ///
2959    /// This is a write-only source step intended for index management. It does not
2960    /// produce a useful traversal stream, so the builder marks it as terminal.
2961    /// Runtime index parameters are selected by the database.
2962    ///
2963    pub fn create_vector_index_edges(
2964        self,
2965        label: impl Into<String>,
2966        property: impl Into<String>,
2967        tenant_property: Option<impl Into<String>>,
2968    ) -> Traversal<Terminal, WriteEnabled> {
2969        self.create_index_if_not_exists(IndexSpec::edge_vector(label, property, tenant_property))
2970    }
2971
2972    /// Add a new node with a label and optional properties.
2973    ///
2974    /// The node ID is automatically allocated.
2975    ///
2976    /// In the current Helix interpreter, this step creates exactly one node and
2977    /// starts the traversal from that node.
2978    ///
2979    pub fn add_n<K, V>(
2980        self,
2981        label: impl Into<String>,
2982        properties: Vec<(K, V)>,
2983    ) -> Traversal<OnNodes, WriteEnabled>
2984    where
2985        K: Into<String>,
2986        V: Into<PropertyInput>,
2987    {
2988        let props: Vec<(String, PropertyInput)> = properties
2989            .into_iter()
2990            .map(|(k, v)| (k.into(), v.into()))
2991            .collect();
2992        self.push_mutation_step(Step::AddN {
2993            label: label.into(),
2994            properties: props,
2995        })
2996    }
2997
2998    /// Start from nodes stored in a variable (Empty -> OnNodes).
2999    ///
3000    /// This is a convenience for starting from a node set previously saved via
3001    /// `store()` / `as_()` in the same traversal context.
3002    ///
3003    /// If you want to start from a variable that yields node IDs via other means
3004    /// (for example, an `id()` terminal result), prefer `n(NodeRef::var(name))`.
3005    ///
3006    pub fn inject(self, var_name: impl Into<String>) -> Traversal<OnNodes, ReadOnly> {
3007        self.push_step(Step::Inject(var_name.into()))
3008    }
3009
3010    /// Delete specific edges by their IDs without needing a source
3011    ///
3012    /// This is the multigraph-safe way to remove edges, as it removes specific
3013    /// edges rather than all edges between a pair of nodes.
3014    ///
3015    pub fn drop_edge_by_id(self, edges: impl Into<EdgeRef>) -> Traversal<OnNodes, WriteEnabled> {
3016        self.push_mutation_step(Step::DropEdgeById(edges.into()))
3017    }
3018}
3019
3020// OnNodes State Implementation
3021
3022impl<M: MutationMode> Traversal<OnNodes, M> {
3023    // Navigation Steps: OnNodes -> OnNodes
3024
3025    /// Traverse outgoing edges, optionally filtered by label
3026    pub fn out(self, label: Option<impl Into<String>>) -> Traversal<OnNodes, M> {
3027        self.push_step(Step::Out(label.map(|l| l.into())))
3028    }
3029
3030    /// Traverse incoming edges, optionally filtered by label
3031    pub fn in_(self, label: Option<impl Into<String>>) -> Traversal<OnNodes, M> {
3032        self.push_step(Step::In(label.map(|l| l.into())))
3033    }
3034
3035    /// Traverse edges in both directions, optionally filtered by label
3036    pub fn both(self, label: Option<impl Into<String>>) -> Traversal<OnNodes, M> {
3037        self.push_step(Step::Both(label.map(|l| l.into())))
3038    }
3039
3040    // Edge Traversal Steps: OnNodes -> OnEdges
3041
3042    /// Traverse to outgoing edges
3043    pub fn out_e(self, label: Option<impl Into<String>>) -> Traversal<OnEdges, M> {
3044        self.push_step(Step::OutE(label.map(|l| l.into())))
3045    }
3046
3047    /// Traverse to incoming edges
3048    pub fn in_e(self, label: Option<impl Into<String>>) -> Traversal<OnEdges, M> {
3049        self.push_step(Step::InE(label.map(|l| l.into())))
3050    }
3051
3052    /// Traverse to edges in both directions
3053    pub fn both_e(self, label: Option<impl Into<String>>) -> Traversal<OnEdges, M> {
3054        self.push_step(Step::BothE(label.map(|l| l.into())))
3055    }
3056
3057    // Node Filter Steps: OnNodes -> OnNodes
3058
3059    /// Filter by property value
3060    pub fn has(self, property: impl Into<String>, value: impl Into<PropertyValue>) -> Self {
3061        self.push_step(Step::Has(property.into(), value.into()))
3062    }
3063
3064    /// Filter by label (shorthand for has("$label", value))
3065    pub fn has_label(self, label: impl Into<String>) -> Self {
3066        self.push_step(Step::HasLabel(label.into()))
3067    }
3068
3069    /// Filter by property existence
3070    pub fn has_key(self, property: impl Into<String>) -> Self {
3071        self.push_step(Step::HasKey(property.into()))
3072    }
3073
3074    /// Filter by a complex predicate
3075    pub fn where_(self, predicate: Predicate) -> Self {
3076        self.push_step(Step::Where(predicate))
3077    }
3078
3079    /// Remove duplicates from the stream
3080    pub fn dedup(self) -> Self {
3081        self.push_step(Step::Dedup)
3082    }
3083
3084    /// Filter to nodes that exist in a variable
3085    pub fn within(self, var_name: impl Into<String>) -> Self {
3086        self.push_step(Step::Within(var_name.into()))
3087    }
3088
3089    /// Filter to nodes that do NOT exist in a variable
3090    pub fn without(self, var_name: impl Into<String>) -> Self {
3091        self.push_step(Step::Without(var_name.into()))
3092    }
3093
3094    // Limit Steps: OnNodes -> OnNodes
3095
3096    /// Take at most N items.
3097    pub fn limit(self, n: impl Into<StreamBound>) -> Self {
3098        self.push_step(limit_step(n))
3099    }
3100
3101    /// Skip the first N items.
3102    pub fn skip(self, n: impl Into<StreamBound>) -> Self {
3103        self.push_step(skip_step(n))
3104    }
3105
3106    /// Get items in a range [start, end)
3107    ///
3108    /// Equivalent to `.skip(start).limit(end - start)` but more concise.
3109    ///
3110    pub fn range(self, start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Self {
3111        self.push_step(range_step(start, end))
3112    }
3113
3114    // Variable Steps: OnNodes -> OnNodes
3115
3116    /// Store the current node stream in the traversal context under `name`.
3117    ///
3118    /// This is identical to `store()`; it exists for Gremlin-style naming.
3119    pub fn as_(self, name: impl Into<String>) -> Self {
3120        self.push_step(Step::As(name.into()))
3121    }
3122
3123    /// Store the current node stream in the traversal context under `name`.
3124    ///
3125    /// This does not change the current stream; it only creates/overwrites a
3126    /// named binding that later steps can reference.
3127    pub fn store(self, name: impl Into<String>) -> Self {
3128        self.push_step(Step::Store(name.into()))
3129    }
3130
3131    /// Replace the current node stream with nodes referenced by a variable.
3132    ///
3133    /// Use this when you want to *switch* streams. If you want to *merge* a stored
3134    /// node set into the current stream, use `inject()`.
3135    pub fn select(self, name: impl Into<String>) -> Self {
3136        self.push_step(Step::Select(name.into()))
3137    }
3138
3139    /// Union the current node stream with nodes stored in `var_name`.
3140    ///
3141    /// This keeps the current stream and adds any nodes stored in the named
3142    /// variable. Use `select()` to replace the stream instead.
3143    ///
3144    pub fn inject(self, var_name: impl Into<String>) -> Self {
3145        self.push_step(Step::Inject(var_name.into()))
3146    }
3147
3148    // Terminal Steps: OnNodes -> Terminal
3149
3150    /// Count the number of results
3151    pub fn count(self) -> Traversal<Terminal, M> {
3152        self.push_step(Step::Count)
3153    }
3154
3155    /// Check if any results exist
3156    pub fn exists(self) -> Traversal<Terminal, M> {
3157        self.push_step(Step::Exists)
3158    }
3159
3160    /// Get the ID of current nodes
3161    ///
3162    /// Returns the node ID as a value. Useful when you need
3163    /// to extract just the ID without other properties.
3164    ///
3165    pub fn id(self) -> Traversal<Terminal, M> {
3166        self.push_step(Step::Id)
3167    }
3168
3169    /// Get the label of current nodes
3170    ///
3171    /// Returns the $label property value.
3172    ///
3173    pub fn label(self) -> Traversal<Terminal, M> {
3174        self.push_step(Step::Label)
3175    }
3176
3177    /// Get specific property values from current nodes
3178    pub fn values(self, properties: Vec<impl Into<String>>) -> Traversal<Terminal, M> {
3179        self.push_step(Step::Values(
3180            properties.into_iter().map(|p| p.into()).collect(),
3181        ))
3182    }
3183
3184    /// Get properties as a map, optionally filtered to specific properties
3185    pub fn value_map(self, properties: Option<Vec<impl Into<String>>>) -> Traversal<Terminal, M> {
3186        self.push_step(Step::ValueMap(
3187            properties.map(|ps| ps.into_iter().map(|p| p.into()).collect()),
3188        ))
3189    }
3190
3191    /// Project properties with optional renaming
3192    pub fn project(self, projections: Vec<PropertyProjection>) -> Traversal<Terminal, M> {
3193        self.push_step(Step::Project(projections))
3194    }
3195
3196    // Ordering Steps: OnNodes -> OnNodes
3197
3198    /// Order results by a property.
3199    ///
3200    /// Note: some interpreters represent intermediate streams as sets. In those
3201    /// engines, ordering may not be preserved in the returned node set.
3202    ///
3203    pub fn order_by(self, property: impl Into<String>, order: Order) -> Self {
3204        self.push_step(Step::OrderBy(property.into(), order))
3205    }
3206
3207    /// Order results by multiple properties with priorities.
3208    ///
3209    /// Note: some interpreters represent intermediate streams as sets. In those
3210    /// engines, ordering may not be preserved in the returned node set.
3211    ///
3212    pub fn order_by_multiple(self, orderings: Vec<(impl Into<String>, Order)>) -> Self {
3213        let orderings: Vec<(String, Order)> =
3214            orderings.into_iter().map(|(p, o)| (p.into(), o)).collect();
3215        self.push_step(Step::OrderByMultiple(orderings))
3216    }
3217
3218    // Loop/Repeat Steps: OnNodes -> OnNodes
3219
3220    /// Repeat a traversal with configuration
3221    ///
3222    pub fn repeat(self, config: RepeatConfig) -> Self {
3223        self.push_step(Step::Repeat(config))
3224    }
3225
3226    // Branching Steps: OnNodes -> OnNodes
3227
3228    /// Execute multiple traversals and merge their results
3229    ///
3230    pub fn union(self, traversals: Vec<SubTraversal>) -> Self {
3231        self.push_step(Step::Union(traversals))
3232    }
3233
3234    /// Conditional execution based on a predicate
3235    ///
3236    pub fn choose(
3237        self,
3238        condition: Predicate,
3239        then_traversal: SubTraversal,
3240        else_traversal: Option<SubTraversal>,
3241    ) -> Self {
3242        self.push_step(Step::Choose {
3243            condition,
3244            then_traversal,
3245            else_traversal,
3246        })
3247    }
3248
3249    /// Try traversals in order until one produces results
3250    ///
3251    pub fn coalesce(self, traversals: Vec<SubTraversal>) -> Self {
3252        self.push_step(Step::Coalesce(traversals))
3253    }
3254
3255    /// Execute a traversal if it produces results, otherwise pass through original
3256    ///
3257    pub fn optional(self, traversal: SubTraversal) -> Self {
3258        self.push_step(Step::Optional(traversal))
3259    }
3260
3261    // Aggregation Steps: OnNodes -> OnNodes (or Terminal for some)
3262
3263    /// Group nodes by a property value.
3264    pub fn group(self, property: impl Into<String>) -> Traversal<Terminal, M> {
3265        self.push_step(Step::Group(property.into()))
3266    }
3267
3268    /// Count occurrences grouped by a property.
3269    pub fn group_count(self, property: impl Into<String>) -> Traversal<Terminal, M> {
3270        self.push_step(Step::GroupCount(property.into()))
3271    }
3272
3273    /// Apply an aggregation function to a property.
3274    pub fn aggregate_by(
3275        self,
3276        function: AggregateFunction,
3277        property: impl Into<String>,
3278    ) -> Traversal<Terminal, M> {
3279        self.push_step(Step::AggregateBy(function, property.into()))
3280    }
3281
3282    /// Barrier step.
3283    ///
3284    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3285    pub fn fold(self) -> Self {
3286        self.push_step(Step::Fold)
3287    }
3288
3289    /// Expand a collected list back into individual items.
3290    ///
3291    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3292    pub fn unfold(self) -> Self {
3293        self.push_step(Step::Unfold)
3294    }
3295
3296    // Path Steps: OnNodes -> OnNodes
3297
3298    /// Include the full traversal path in results.
3299    ///
3300    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3301    ///
3302    pub fn path(self) -> Self {
3303        self.push_step(Step::Path)
3304    }
3305
3306    /// Filter to only simple paths (no repeated nodes).
3307    ///
3308    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3309    pub fn simple_path(self) -> Self {
3310        self.push_step(Step::SimplePath)
3311    }
3312
3313    // Sack Steps: OnNodes -> OnNodes
3314
3315    /// Initialize a sack (traverser-local state) with a value.
3316    ///
3317    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3318    ///
3319    pub fn with_sack(self, initial: PropertyValue) -> Self {
3320        self.push_step(Step::WithSack(initial))
3321    }
3322
3323    /// Set the sack to a property value from the current node.
3324    ///
3325    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3326    pub fn sack_set(self, property: impl Into<String>) -> Self {
3327        self.push_step(Step::SackSet(property.into()))
3328    }
3329
3330    /// Add a property value to the sack (numeric types only).
3331    ///
3332    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3333    pub fn sack_add(self, property: impl Into<String>) -> Self {
3334        self.push_step(Step::SackAdd(property.into()))
3335    }
3336
3337    /// Get the current sack value.
3338    ///
3339    /// Note: this step is reserved; the current Helix interpreter treats it as a no-op.
3340    pub fn sack_get(self) -> Self {
3341        self.push_step(Step::SackGet)
3342    }
3343
3344    // Mutation Steps: OnNodes -> OnNodes (WriteEnabled)
3345
3346    /// Add a new node with a label and optional properties.
3347    ///
3348    /// The node ID is automatically allocated.
3349    ///
3350    /// In the current Helix interpreter, this step creates exactly one node and
3351    /// replaces the current node stream with that new node.
3352    pub fn add_n<K, V>(
3353        self,
3354        label: impl Into<String>,
3355        properties: Vec<(K, V)>,
3356    ) -> Traversal<OnNodes, WriteEnabled>
3357    where
3358        K: Into<String>,
3359        V: Into<PropertyInput>,
3360    {
3361        let props: Vec<(String, PropertyInput)> = properties
3362            .into_iter()
3363            .map(|(k, v)| (k.into(), v.into()))
3364            .collect();
3365        self.push_mutation_step(Step::AddN {
3366            label: label.into(),
3367            properties: props,
3368        })
3369    }
3370
3371    /// Add edges from the current nodes to target nodes.
3372    ///
3373    /// In the current Helix interpreter, this creates edges for every pair in the
3374    /// cartesian product `current_nodes x target_nodes` and leaves the current
3375    /// node stream unchanged.
3376    ///
3377    pub fn add_e<K, V>(
3378        self,
3379        label: impl Into<String>,
3380        to: impl Into<NodeRef>,
3381        properties: Vec<(K, V)>,
3382    ) -> Traversal<OnNodes, WriteEnabled>
3383    where
3384        K: Into<String>,
3385        V: Into<PropertyInput>,
3386    {
3387        let props: Vec<(String, PropertyInput)> = properties
3388            .into_iter()
3389            .map(|(k, v)| (k.into(), v.into()))
3390            .collect();
3391        self.push_mutation_step(Step::AddE {
3392            label: label.into(),
3393            to: to.into(),
3394            properties: props,
3395        })
3396    }
3397
3398    /// Set a property on current nodes
3399    pub fn set_property(
3400        self,
3401        name: impl Into<String>,
3402        value: impl Into<PropertyInput>,
3403    ) -> Traversal<OnNodes, WriteEnabled> {
3404        self.push_mutation_step(Step::SetProperty(name.into(), value.into()))
3405    }
3406
3407    /// Remove a property from current nodes
3408    pub fn remove_property(self, name: impl Into<String>) -> Traversal<OnNodes, WriteEnabled> {
3409        self.push_mutation_step(Step::RemoveProperty(name.into()))
3410    }
3411
3412    /// Delete current nodes and their edges
3413    pub fn drop(self) -> Traversal<OnNodes, WriteEnabled> {
3414        self.push_mutation_step(Step::Drop)
3415    }
3416
3417    /// Delete edges from current nodes to target nodes
3418    ///
3419    /// **Note**: In multigraph scenarios, this removes ALL edges between the current
3420    /// nodes and the target nodes. Use `drop_edge_by_id` for precise edge removal.
3421    pub fn drop_edge(self, to: impl Into<NodeRef>) -> Traversal<OnNodes, WriteEnabled> {
3422        self.push_mutation_step(Step::DropEdge(to.into()))
3423    }
3424
3425    /// Delete only edges with a specific label from current nodes to target nodes.
3426    pub fn drop_edge_labeled(
3427        self,
3428        to: impl Into<NodeRef>,
3429        label: impl Into<String>,
3430    ) -> Traversal<OnNodes, WriteEnabled> {
3431        self.push_mutation_step(Step::DropEdgeLabeled {
3432            to: to.into(),
3433            label: label.into(),
3434        })
3435    }
3436
3437    /// Delete specific edges by their IDs
3438    ///
3439    /// This is the multigraph-safe way to remove edges, as it removes specific
3440    /// edges rather than all edges between a pair of nodes.
3441    ///
3442    pub fn drop_edge_by_id(self, edges: impl Into<EdgeRef>) -> Traversal<OnNodes, WriteEnabled> {
3443        self.push_mutation_step(Step::DropEdgeById(edges.into()))
3444    }
3445}
3446
3447// OnEdges State Implementation
3448
3449impl<M: MutationMode> Traversal<OnEdges, M> {
3450    // Node Extraction Steps: OnEdges -> OnNodes
3451
3452    /// From edge, get the target node
3453    pub fn out_n(self) -> Traversal<OnNodes, M> {
3454        self.push_step(Step::OutN)
3455    }
3456
3457    /// From edge, get the source node
3458    pub fn in_n(self) -> Traversal<OnNodes, M> {
3459        self.push_step(Step::InN)
3460    }
3461
3462    /// From edge, get the "other" node (not the one we came from)
3463    pub fn other_n(self) -> Traversal<OnNodes, M> {
3464        self.push_step(Step::OtherN)
3465    }
3466
3467    // Edge Filter Steps: OnEdges -> OnEdges
3468
3469    /// Filter edges by property value
3470    pub fn edge_has(self, property: impl Into<String>, value: impl Into<PropertyInput>) -> Self {
3471        self.push_step(Step::EdgeHas(property.into(), value.into()))
3472    }
3473
3474    /// Filter edges by label
3475    pub fn edge_has_label(self, label: impl Into<String>) -> Self {
3476        self.push_step(Step::EdgeHasLabel(label.into()))
3477    }
3478
3479    /// Remove duplicates from the stream
3480    pub fn dedup(self) -> Self {
3481        self.push_step(Step::Dedup)
3482    }
3483
3484    // Limit Steps: OnEdges -> OnEdges
3485
3486    /// Take at most N items.
3487    pub fn limit(self, n: impl Into<StreamBound>) -> Self {
3488        self.push_step(limit_step(n))
3489    }
3490
3491    /// Skip the first N items.
3492    pub fn skip(self, n: impl Into<StreamBound>) -> Self {
3493        self.push_step(skip_step(n))
3494    }
3495
3496    /// Get items in a range [start, end)
3497    pub fn range(self, start: impl Into<StreamBound>, end: impl Into<StreamBound>) -> Self {
3498        self.push_step(range_step(start, end))
3499    }
3500
3501    // Variable Steps: OnEdges -> OnEdges
3502
3503    /// Store current edges with a name for later reference
3504    pub fn as_(self, name: impl Into<String>) -> Self {
3505        self.push_step(Step::As(name.into()))
3506    }
3507
3508    /// Store current edges to a variable (same as `as_`)
3509    pub fn store(self, name: impl Into<String>) -> Self {
3510        self.push_step(Step::Store(name.into()))
3511    }
3512
3513    // Terminal Steps: OnEdges -> Terminal
3514
3515    /// Count the number of edges
3516    pub fn count(self) -> Traversal<Terminal, M> {
3517        self.push_step(Step::Count)
3518    }
3519
3520    /// Check if any edges exist
3521    pub fn exists(self) -> Traversal<Terminal, M> {
3522        self.push_step(Step::Exists)
3523    }
3524
3525    /// Get the ID of current edges
3526    pub fn id(self) -> Traversal<Terminal, M> {
3527        self.push_step(Step::Id)
3528    }
3529
3530    /// Get the label of current edges
3531    pub fn label(self) -> Traversal<Terminal, M> {
3532        self.push_step(Step::Label)
3533    }
3534
3535    /// Get edge properties
3536    pub fn edge_properties(self) -> Traversal<Terminal, M> {
3537        self.push_step(Step::EdgeProperties)
3538    }
3539
3540    // Ordering Steps: OnEdges -> OnEdges
3541
3542    /// Order results by a property.
3543    ///
3544    /// Note: some interpreters represent intermediate streams as sets. In those
3545    /// engines, ordering may not be preserved in the returned edge set.
3546    pub fn order_by(self, property: impl Into<String>, order: Order) -> Self {
3547        self.push_step(Step::OrderBy(property.into(), order))
3548    }
3549}
3550
3551// Terminal State - No additional methods (traversal is complete)
3552
3553// Terminal has no additional methods - the traversal is complete
3554
3555// Entry Point
3556
3557/// Create a new traversal - the entry point for building queries
3558///
3559pub fn g() -> Traversal<Empty> {
3560    Traversal::new()
3561}
3562
3563// Batch Query Types
3564
3565/// Condition for conditional query execution within a batch
3566///
3567#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3568pub enum BatchCondition {
3569    /// Execute only if the named variable is not empty
3570    VarNotEmpty(String),
3571    /// Execute only if the named variable is empty
3572    VarEmpty(String),
3573    /// Execute only if the named variable has at least N items
3574    VarMinSize(String, usize),
3575    /// Execute only if the previous query result was not empty
3576    PrevNotEmpty,
3577}
3578
3579/// A single query within a batch
3580#[doc(hidden)]
3581#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3582pub struct NamedQuery {
3583    /// Variable name to store result (required for var_as)
3584    pub name: Option<String>,
3585    /// The traversal steps to execute for this query.
3586    pub steps: Vec<Step>,
3587    /// Skip if condition fails
3588    pub condition: Option<BatchCondition>,
3589}
3590
3591/// A batch entry executed in sequence.
3592#[doc(hidden)]
3593#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3594pub enum BatchEntry {
3595    /// Execute a single traversal query.
3596    Query(NamedQuery),
3597    /// Execute the enclosed entries once per object in the named array param.
3598    ForEach {
3599        /// The top-level parameter containing an array of objects.
3600        param: String,
3601        /// Entries to execute for each object.
3602        body: Vec<BatchEntry>,
3603    },
3604}
3605
3606/// A batch of read-only queries for sequential execution in one transaction
3607///
3608/// This allows multiple related read queries to be executed atomically,
3609/// with results stored in named variables that can be referenced
3610/// by subsequent queries and returned as a structured result.
3611///
3612/// **Important**: ReadBatch only accepts read-only traversals (no mutations).
3613/// Attempting to add a traversal containing mutation steps will fail at compile time.
3614///
3615#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
3616pub struct ReadBatch {
3617    /// Queries to execute in order.
3618    ///
3619    /// Most users should build this with [`ReadBatch::var_as`] / [`ReadBatch::var_as_if`]
3620    /// to preserve type-level read-only guarantees.
3621    #[doc(hidden)]
3622    pub queries: Vec<BatchEntry>,
3623    /// Variables to include in final result (empty = all named variables)
3624    #[doc(hidden)]
3625    pub returns: Vec<String>,
3626}
3627
3628impl ReadBatch {
3629    /// Create a new empty read batch
3630    #[doc(hidden)]
3631    pub fn new() -> Self {
3632        Self {
3633            queries: Vec::new(),
3634            returns: Vec::new(),
3635        }
3636    }
3637
3638    /// Add a read-only query that stores result in a named variable
3639    ///
3640    /// The traversal is executed and its result is stored in a variable
3641    /// that can be referenced by subsequent queries using `NodeRef::var()`.
3642    ///
3643    /// **Note**: Only accepts read-only traversals. Mutation traversals will fail at compile time.
3644    ///
3645    pub fn var_as<S: TraversalState>(
3646        mut self,
3647        name: &str,
3648        traversal: Traversal<S, ReadOnly>,
3649    ) -> Self {
3650        self.queries.push(BatchEntry::Query(NamedQuery {
3651            name: Some(name.to_string()),
3652            steps: traversal.into_steps(),
3653            condition: None,
3654        }));
3655        self
3656    }
3657
3658    /// Add a conditional read-only query that only executes if the condition is met
3659    ///
3660    pub fn var_as_if<S: TraversalState>(
3661        mut self,
3662        name: &str,
3663        condition: BatchCondition,
3664        traversal: Traversal<S, ReadOnly>,
3665    ) -> Self {
3666        self.queries.push(BatchEntry::Query(NamedQuery {
3667            name: Some(name.to_string()),
3668            steps: traversal.into_steps(),
3669            condition: Some(condition),
3670        }));
3671        self
3672    }
3673
3674    /// Execute the provided body once per object in the named array parameter.
3675    pub fn for_each_param(mut self, param: &str, body: ReadBatch) -> Self {
3676        self.queries.push(BatchEntry::ForEach {
3677            param: param.to_string(),
3678            body: body.queries,
3679        });
3680        self
3681    }
3682
3683    /// Specify which variables to return (call at end)
3684    ///
3685    /// If not called, all named variables are returned.
3686    ///
3687    pub fn returning<I, S>(mut self, vars: I) -> Self
3688    where
3689        I: IntoIterator<Item = S>,
3690        S: Into<String>,
3691    {
3692        self.returns = vars.into_iter().map(|s| s.into()).collect();
3693        self
3694    }
3695}
3696
3697/// A batch of write queries for sequential execution in one transaction
3698///
3699/// This allows multiple related queries (including mutations) to be executed atomically,
3700/// with results stored in named variables that can be referenced
3701/// by subsequent queries and returned as a structured result.
3702///
3703/// **Note**: WriteBatch accepts both read-only and mutation traversals.
3704///
3705#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
3706pub struct WriteBatch {
3707    /// Queries to execute in order.
3708    ///
3709    /// Most users should build this with [`WriteBatch::var_as`] / [`WriteBatch::var_as_if`]
3710    /// for clearer intent and safer construction.
3711    #[doc(hidden)]
3712    pub queries: Vec<BatchEntry>,
3713    /// Variables to include in final result (empty = all named variables)
3714    #[doc(hidden)]
3715    pub returns: Vec<String>,
3716}
3717
3718impl WriteBatch {
3719    /// Create a new empty write batch
3720    #[doc(hidden)]
3721    pub fn new() -> Self {
3722        Self {
3723            queries: Vec::new(),
3724            returns: Vec::new(),
3725        }
3726    }
3727
3728    /// Add a query that stores result in a named variable
3729    ///
3730    /// The traversal is executed and its result is stored in a variable
3731    /// that can be referenced by subsequent queries using `NodeRef::var()`.
3732    ///
3733    /// Accepts both read-only and mutation traversals.
3734    ///
3735    pub fn var_as<S: TraversalState, M: MutationMode>(
3736        mut self,
3737        name: &str,
3738        traversal: Traversal<S, M>,
3739    ) -> Self {
3740        self.queries.push(BatchEntry::Query(NamedQuery {
3741            name: Some(name.to_string()),
3742            steps: traversal.into_steps(),
3743            condition: None,
3744        }));
3745        self
3746    }
3747
3748    /// Add a conditional query that only executes if the condition is met
3749    ///
3750    pub fn var_as_if<S: TraversalState, M: MutationMode>(
3751        mut self,
3752        name: &str,
3753        condition: BatchCondition,
3754        traversal: Traversal<S, M>,
3755    ) -> Self {
3756        self.queries.push(BatchEntry::Query(NamedQuery {
3757            name: Some(name.to_string()),
3758            steps: traversal.into_steps(),
3759            condition: Some(condition),
3760        }));
3761        self
3762    }
3763
3764    /// Execute the provided body once per object in the named array parameter.
3765    pub fn for_each_param(mut self, param: &str, body: WriteBatch) -> Self {
3766        self.queries.push(BatchEntry::ForEach {
3767            param: param.to_string(),
3768            body: body.queries,
3769        });
3770        self
3771    }
3772
3773    /// Specify which variables to return (call at end)
3774    ///
3775    /// If not called, all named variables are returned.
3776    ///
3777    pub fn returning<I, S>(mut self, vars: I) -> Self
3778    where
3779        I: IntoIterator<Item = S>,
3780        S: Into<String>,
3781    {
3782        self.returns = vars.into_iter().map(|s| s.into()).collect();
3783        self
3784    }
3785}
3786
3787/// A batch query payload for wire transport or storage
3788#[doc(hidden)]
3789#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3790pub enum BatchQuery {
3791    /// Read-only batch
3792    Read(ReadBatch),
3793    /// Write-capable batch
3794    Write(WriteBatch),
3795}
3796
3797/// Create a new read batch - the entry point for read-only multi-query transactions
3798///
3799pub fn read_batch() -> ReadBatch {
3800    ReadBatch::new()
3801}
3802
3803/// Create a new write batch - the entry point for write multi-query transactions
3804///
3805pub fn write_batch() -> WriteBatch {
3806    WriteBatch::new()
3807}
3808
3809/// Common query-builder imports.
3810///
3811/// This module re-exports the APIs most users reach for when building read and
3812/// write batches.
3813///
3814/// Typical usage in application code: `use helix_dsl::prelude::*;`
3815#[allow(missing_docs)]
3816pub mod prelude {
3817    pub use crate::{
3818        g, read_batch, register, sub, write_batch, AggregateFunction, BatchCondition, BatchEntry,
3819        CompareOp, EdgeId, EdgeRef, EmitBehavior, Expr, IndexSpec, NodeId, NodeRef, Order,
3820        ParamObject, ParamValue, Predicate, PropertyInput, PropertyProjection, PropertyValue,
3821        ReadBatch, RepeatConfig, SourcePredicate, StreamBound, SubTraversal, Traversal, WriteBatch,
3822    };
3823}
3824
3825/// Helper type alias for property maps
3826#[doc(hidden)]
3827pub type PropertyMap = HashMap<String, PropertyValue>;
3828
3829#[cfg(test)]
3830mod tests {
3831    use std::collections::BTreeMap;
3832
3833    use crate::query_generator::{
3834        deserialize_query_bundle, serialize_query_bundle, GenerateError, QueryBundle,
3835        QueryParamType, QueryParameter, QUERY_BUNDLE_VERSION,
3836    };
3837
3838    use super::*;
3839
3840    fn query_entry(entry: &BatchEntry) -> &NamedQuery {
3841        match entry {
3842            BatchEntry::Query(query) => query,
3843            other => panic!("expected query entry, got {other:?}"),
3844        }
3845    }
3846
3847    #[test]
3848    fn query_bundle_roundtrip_with_bincode_fixint() {
3849        let mut bundle = QueryBundle::default();
3850        bundle.read_routes.insert(
3851            "read_users".to_string(),
3852            read_batch().var_as("count", g().n([1u64]).count()),
3853        );
3854        bundle.read_parameters.insert(
3855            "read_users".to_string(),
3856            vec![QueryParameter {
3857                name: "filters".to_string(),
3858                ty: QueryParamType::Object,
3859            }],
3860        );
3861        bundle.write_routes.insert(
3862            "create_user".to_string(),
3863            write_batch().var_as("created", g().add_n("User", vec![("name", "Alice")])),
3864        );
3865        bundle.write_parameters.insert(
3866            "create_user".to_string(),
3867            vec![QueryParameter {
3868                name: "data".to_string(),
3869                ty: QueryParamType::Array(Box::new(QueryParamType::Object)),
3870            }],
3871        );
3872
3873        let bytes = serialize_query_bundle(&bundle).expect("serialize query bundle");
3874        let decoded = deserialize_query_bundle(&bytes).expect("deserialize query bundle");
3875
3876        assert_eq!(decoded.version, QUERY_BUNDLE_VERSION);
3877        assert_eq!(decoded.read_routes.len(), 1);
3878        assert_eq!(decoded.write_routes.len(), 1);
3879        assert!(decoded.read_routes.contains_key("read_users"));
3880        assert!(decoded.write_routes.contains_key("create_user"));
3881        assert_eq!(
3882            decoded.read_parameters.get("read_users"),
3883            Some(&vec![QueryParameter {
3884                name: "filters".to_string(),
3885                ty: QueryParamType::Object,
3886            }])
3887        );
3888        assert_eq!(
3889            decoded.write_parameters.get("create_user"),
3890            Some(&vec![QueryParameter {
3891                name: "data".to_string(),
3892                ty: QueryParamType::Array(Box::new(QueryParamType::Object)),
3893            }])
3894        );
3895    }
3896
3897    #[test]
3898    fn query_bundle_rejects_unsupported_version() {
3899        let mut bundle = QueryBundle::default();
3900        bundle.version = QUERY_BUNDLE_VERSION + 1;
3901
3902        let bytes = serialize_query_bundle(&bundle).expect("serialize query bundle");
3903        let err = deserialize_query_bundle(&bytes).expect_err("version should fail");
3904
3905        assert!(matches!(
3906            err,
3907            GenerateError::UnsupportedVersion {
3908                found: _,
3909                expected: QUERY_BUNDLE_VERSION,
3910            }
3911        ));
3912    }
3913
3914    #[test]
3915    fn test_traversal_builder() {
3916        let t = g()
3917            .n([1u64, 2, 3])
3918            .out(Some("FOLLOWS"))
3919            .has("active", "true")
3920            .limit(10);
3921
3922        assert_eq!(t.steps.len(), 4);
3923        assert!(matches!(&t.steps[0], Step::N(NodeRef::Ids(ids)) if ids == &vec![1, 2, 3]));
3924        assert!(matches!(&t.steps[1], Step::Out(Some(label)) if label == "FOLLOWS"));
3925    }
3926
3927    #[test]
3928    fn test_variable_steps() {
3929        let t = g()
3930            .n([1u64])
3931            .out(None::<String>)
3932            .as_("neighbors")
3933            .out(None::<String>)
3934            .within("neighbors");
3935
3936        assert_eq!(t.steps.len(), 5);
3937        assert!(matches!(&t.steps[2], Step::As(name) if name == "neighbors"));
3938        assert!(matches!(&t.steps[4], Step::Within(name) if name == "neighbors"));
3939    }
3940
3941    #[test]
3942    fn test_terminal_detection() {
3943        let t1 = g().n([1u64]).out(None::<String>);
3944        assert!(!t1.has_terminal());
3945
3946        let t2 = g().n([1u64]).count();
3947        assert!(t2.has_terminal());
3948
3949        let t3 = g().n([1u64]).exists();
3950        assert!(t3.has_terminal());
3951    }
3952
3953    #[test]
3954    fn test_node_ref_from_impls() {
3955        let r1: NodeRef = 42u64.into();
3956        assert!(matches!(r1, NodeRef::Ids(ids) if ids == vec![42]));
3957
3958        let r2: NodeRef = vec![1u64, 2, 3].into();
3959        assert!(matches!(r2, NodeRef::Ids(ids) if ids == vec![1, 2, 3]));
3960
3961        let r3: NodeRef = "my_var".into();
3962        assert!(matches!(r3, NodeRef::Var(name) if name == "my_var"));
3963    }
3964
3965    #[test]
3966    fn test_add_n_and_add_e() {
3967        let t = g()
3968            .add_n("User", vec![("name", "Alice")])
3969            .as_("alice")
3970            .add_n("User", vec![("name", "Bob")])
3971            .add_e("KNOWS", NodeRef::var("alice"), vec![("since", "2024")]);
3972
3973        assert_eq!(t.steps.len(), 4);
3974        assert!(
3975            matches!(&t.steps[0], Step::AddN { label, properties } if label == "User" && properties.len() == 1)
3976        );
3977        assert!(
3978            matches!(&t.steps[3], Step::AddE { label, to: NodeRef::Var(name), .. } if label == "KNOWS" && name == "alice")
3979        );
3980    }
3981
3982    #[test]
3983    fn test_predicate_builder() {
3984        let p1 = Predicate::eq("name", "Alice");
3985        assert!(
3986            matches!(p1, Predicate::Eq(prop, PropertyValue::String(val)) if prop == "name" && val == "Alice")
3987        );
3988
3989        let p2 = Predicate::and(vec![
3990            Predicate::eq("status", "active"),
3991            Predicate::gt("age", "18"),
3992        ]);
3993        assert!(matches!(p2, Predicate::And(preds) if preds.len() == 2));
3994    }
3995
3996    #[test]
3997    fn test_edge_traversal() {
3998        // This should compile: nodes -> edges -> nodes
3999        let t = g()
4000            .n([1u64])
4001            .out_e(Some("FOLLOWS"))
4002            .edge_has("weight", 1i64)
4003            .out_n()
4004            .has_label("User");
4005
4006        assert_eq!(t.steps.len(), 5);
4007    }
4008
4009    #[test]
4010    fn test_sub_traversal() {
4011        let t = g()
4012            .n([1u64])
4013            .union(vec![sub().out(Some("FOLLOWS")), sub().out(Some("LIKES"))]);
4014
4015        assert_eq!(t.steps.len(), 2);
4016        if let Step::Union(subs) = &t.steps[1] {
4017            assert_eq!(subs.len(), 2);
4018        } else {
4019            panic!("Expected Union step");
4020        }
4021    }
4022
4023    #[test]
4024    fn test_repeat_with_sub_traversal() {
4025        let t = g()
4026            .n([1u64])
4027            .repeat(RepeatConfig::new(sub().out(None::<&str>)).times(3));
4028
4029        assert_eq!(t.steps.len(), 2);
4030    }
4031
4032    #[test]
4033    fn test_read_batch_construction() {
4034        let b = read_batch()
4035            .var_as(
4036                "user",
4037                g().n_where(SourcePredicate::eq("username", "alice")),
4038            )
4039            .var_as("friends", g().n(NodeRef::var("user")).out(Some("FOLLOWS")))
4040            .returning(["user", "friends"]);
4041
4042        assert_eq!(b.queries.len(), 2);
4043        assert_eq!(b.returns, vec!["user", "friends"]);
4044
4045        let first = query_entry(&b.queries[0]);
4046        let second = query_entry(&b.queries[1]);
4047
4048        // First query: user
4049        assert_eq!(first.name, Some("user".to_string()));
4050        assert!(first.condition.is_none());
4051        assert_eq!(first.steps.len(), 1); // NWhere
4052
4053        // Second query: friends
4054        assert_eq!(second.name, Some("friends".to_string()));
4055        assert_eq!(second.steps.len(), 2); // N + Out
4056    }
4057
4058    #[test]
4059    fn test_read_batch_conditional() {
4060        let b = read_batch()
4061            .var_as("user", g().n_where(SourcePredicate::eq("id", 1i64)))
4062            .var_as_if(
4063                "posts",
4064                BatchCondition::VarNotEmpty("user".to_string()),
4065                g().n(NodeRef::var("user")).out(Some("POSTED")),
4066            );
4067
4068        assert_eq!(b.queries.len(), 2);
4069
4070        // Second query has condition
4071        assert!(matches!(
4072            &query_entry(&b.queries[1]).condition,
4073            Some(BatchCondition::VarNotEmpty(name)) if name == "user"
4074        ));
4075    }
4076
4077    #[test]
4078    fn test_read_batch_with_terminal() {
4079        let b = read_batch()
4080            .var_as("user", g().n([1u64]).value_map(None::<Vec<&str>>))
4081            .var_as("friend_count", g().n([1u64]).out(Some("FOLLOWS")).count())
4082            .returning(["user", "friend_count"]);
4083
4084        assert_eq!(b.queries.len(), 2);
4085
4086        // First query ends with ValueMap
4087        assert!(matches!(
4088            query_entry(&b.queries[0]).steps.last(),
4089            Some(Step::ValueMap(_))
4090        ));
4091
4092        // Second query ends with Count
4093        assert!(matches!(
4094            query_entry(&b.queries[1]).steps.last(),
4095            Some(Step::Count)
4096        ));
4097    }
4098
4099    #[test]
4100    fn test_write_batch_construction() {
4101        let b = write_batch()
4102            .var_as("user", g().add_n("User", vec![("name", "Alice")]))
4103            .var_as("post", g().add_n("Post", vec![("title", "Hello")]))
4104            .returning(["user", "post"]);
4105
4106        assert_eq!(b.queries.len(), 2);
4107        assert_eq!(b.returns, vec!["user", "post"]);
4108    }
4109
4110    #[test]
4111    fn test_property_input_from_expr() {
4112        let traversal = g().add_n(
4113            "User",
4114            vec![
4115                ("name", PropertyInput::param("name")),
4116                ("age", PropertyInput::param("age")),
4117            ],
4118        );
4119
4120        assert!(matches!(
4121            &traversal.steps[0],
4122            Step::AddN { properties, .. }
4123                if matches!(&properties[0].1, PropertyInput::Expr(Expr::Param(name)) if name == "name")
4124                && matches!(&properties[1].1, PropertyInput::Expr(Expr::Param(name)) if name == "age")
4125        ));
4126    }
4127
4128    #[test]
4129    fn test_edge_has_accepts_param_input() {
4130        let traversal = g()
4131            .e([1u64])
4132            .edge_has("targetExternalId", PropertyInput::param("targetExternalId"));
4133
4134        assert!(matches!(
4135            &traversal.steps[1],
4136            Step::EdgeHas(property, PropertyInput::Expr(Expr::Param(name)))
4137                if property == "targetExternalId" && name == "targetExternalId"
4138        ));
4139    }
4140
4141    #[test]
4142    fn test_write_batch_for_each_param() {
4143        let body = write_batch()
4144            .var_as(
4145                "existing",
4146                g().n_where(SourcePredicate::eq("$label", "User")),
4147            )
4148            .var_as(
4149                "created",
4150                g().add_n("User", vec![("name", PropertyInput::param("name"))]),
4151            );
4152
4153        let batch = write_batch().for_each_param("data", body);
4154
4155        assert_eq!(batch.queries.len(), 1);
4156        assert!(matches!(
4157            &batch.queries[0],
4158            BatchEntry::ForEach { param, body }
4159                if param == "data" && matches!(&body[1], BatchEntry::Query(NamedQuery { name: Some(name), .. }) if name == "created")
4160        ));
4161    }
4162
4163    #[test]
4164    fn test_property_value_nested_payload_variants() {
4165        let mut row = BTreeMap::new();
4166        row.insert("externalId".to_string(), PropertyValue::from("u-1"));
4167        row.insert("active".to_string(), PropertyValue::from(true));
4168
4169        let payload = PropertyValue::from(vec![PropertyValue::from(row.clone())]);
4170
4171        assert!(matches!(payload.as_array(), Some(values) if values.len() == 1));
4172        assert_eq!(
4173            payload
4174                .as_array()
4175                .and_then(|values| values[0].as_object())
4176                .and_then(|map| map.get("externalId"))
4177                .and_then(PropertyValue::as_str),
4178            Some("u-1")
4179        );
4180    }
4181
4182    #[test]
4183    fn test_vector_search_steps() {
4184        let embedding = vec![0.1f32; 4];
4185        let t = g().vector_search_nodes("Doc", "embedding", embedding.clone(), 5, None);
4186        assert!(matches!(
4187            &t.steps[0],
4188            Step::VectorSearchNodes {
4189                label,
4190                property,
4191                query_vector: PropertyInput::Value(PropertyValue::F32Array(values)),
4192                k: StreamBound::Literal(k),
4193                tenant_value,
4194            }
4195                if label == "Doc"
4196                    && property == "embedding"
4197                    && values == &embedding
4198                    && *k == 5
4199                    && tenant_value.is_none()
4200        ));
4201
4202        let t2 = g().vector_search_edges("SIMILAR", "embedding", embedding.clone(), 3, None);
4203        assert!(matches!(
4204            &t2.steps[0],
4205            Step::VectorSearchEdges {
4206                label,
4207                property,
4208                query_vector: PropertyInput::Value(PropertyValue::F32Array(values)),
4209                k: StreamBound::Literal(k),
4210                tenant_value,
4211            }
4212                if label == "SIMILAR"
4213                    && property == "embedding"
4214                    && values == &embedding
4215                    && *k == 3
4216                    && tenant_value.is_none()
4217        ));
4218
4219        let t3 = g().vector_search_nodes(
4220            "Doc",
4221            "embedding",
4222            vec![0.1f32; 4],
4223            5,
4224            Some(PropertyValue::from("tenant-a")),
4225        );
4226        assert!(matches!(
4227            &t3.steps[0],
4228            Step::VectorSearchNodes {
4229                label,
4230                property,
4231                k: StreamBound::Literal(k),
4232                tenant_value: Some(PropertyInput::Value(PropertyValue::String(value))),
4233                ..
4234            } if label == "Doc" && property == "embedding" && *k == 5 && value == "tenant-a"
4235        ));
4236    }
4237
4238    #[test]
4239    fn test_parameterized_vector_search_steps() {
4240        let t = g().vector_search_nodes_with(
4241            "Doc",
4242            "embedding",
4243            PropertyInput::param("queryVector"),
4244            Expr::param("limit"),
4245            Some(PropertyInput::param("firmId")),
4246        );
4247
4248        assert!(matches!(
4249            &t.steps[0],
4250            Step::VectorSearchNodes {
4251                label,
4252                property,
4253                query_vector: PropertyInput::Expr(Expr::Param(query_vector)),
4254                k: StreamBound::Expr(Expr::Param(limit)),
4255                tenant_value: Some(PropertyInput::Expr(Expr::Param(firm_id))),
4256            }
4257                if label == "Doc"
4258                    && property == "embedding"
4259                    && query_vector == "queryVector"
4260                    && limit == "limit"
4261                    && firm_id == "firmId"
4262        ));
4263    }
4264
4265    #[test]
4266    fn test_parameterized_stream_bounds() {
4267        let range = g()
4268            .n_with_label("User")
4269            .range(Expr::param("start"), Expr::param("end"));
4270        assert!(matches!(
4271            range.steps.as_slice(),
4272            [
4273                Step::NWhere(_),
4274                Step::RangeBy(StreamBound::Expr(Expr::Param(start)), StreamBound::Expr(Expr::Param(end))),
4275            ] if start == "start" && end == "end"
4276        ));
4277
4278        let ordered = g()
4279            .n_with_label("User")
4280            .order_by("age", Order::Desc)
4281            .limit(Expr::param("limit"))
4282            .skip(Expr::param("offset"));
4283        assert!(matches!(
4284            ordered.steps.as_slice(),
4285            [
4286                Step::NWhere(_),
4287                Step::OrderBy(property, Order::Desc),
4288                Step::LimitBy(Expr::Param(limit)),
4289                Step::SkipBy(Expr::Param(offset)),
4290            ] if property == "age" && limit == "limit" && offset == "offset"
4291        ));
4292    }
4293
4294    #[test]
4295    fn test_contains_param_predicate() {
4296        assert!(matches!(
4297            Predicate::contains_param("location", "city"),
4298            Predicate::ContainsExpr(property, Expr::Param(param))
4299                if property == "location" && param == "city"
4300        ));
4301    }
4302
4303    #[test]
4304    fn test_create_vector_index_steps() {
4305        let t = g().create_vector_index_nodes("Doc", "embedding", None::<&str>);
4306        assert!(t.has_terminal());
4307        assert!(matches!(
4308            &t.steps[0],
4309            Step::CreateIndex {
4310                spec: IndexSpec::NodeVector {
4311                    label,
4312                    property,
4313                    tenant_property,
4314                },
4315                if_not_exists,
4316            } if label == "Doc"
4317                && property == "embedding"
4318                && tenant_property.is_none()
4319                && *if_not_exists
4320        ));
4321
4322        let t2 = g().create_vector_index_edges("REL", "embedding", None::<&str>);
4323        assert!(matches!(
4324            &t2.steps[0],
4325            Step::CreateIndex {
4326                spec: IndexSpec::EdgeVector {
4327                    label,
4328                    property,
4329                    tenant_property,
4330                },
4331                if_not_exists,
4332            } if label == "REL"
4333                && property == "embedding"
4334                && tenant_property.is_none()
4335                && *if_not_exists
4336        ));
4337
4338        let t3 = g().create_vector_index_nodes("Doc", "embedding", Some("tenant_id"));
4339        assert!(matches!(
4340            &t3.steps[0],
4341            Step::CreateIndex {
4342                spec: IndexSpec::NodeVector {
4343                    tenant_property: Some(tenant_property),
4344                    ..
4345                },
4346                if_not_exists,
4347            } if tenant_property == "tenant_id" && *if_not_exists
4348        ));
4349    }
4350
4351    #[test]
4352    fn test_generic_index_steps() {
4353        let create = g().create_index_if_not_exists(IndexSpec::node_equality("User", "status"));
4354        assert!(create.has_terminal());
4355        assert!(matches!(
4356            &create.steps[0],
4357            Step::CreateIndex {
4358                spec: IndexSpec::NodeEquality { label, property },
4359                if_not_exists,
4360            } if label == "User" && property == "status" && *if_not_exists
4361        ));
4362
4363        let drop = g().drop_index(IndexSpec::edge_range("FOLLOWS", "weight"));
4364        assert!(drop.has_terminal());
4365        assert!(matches!(
4366            &drop.steps[0],
4367            Step::DropIndex {
4368                spec: IndexSpec::EdgeRange { label, property },
4369            } if label == "FOLLOWS" && property == "weight"
4370        ));
4371    }
4372}