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