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