Skip to main content

helix_db/
dsl.rs

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