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