oqx/context.rs
1//! The tier-2 seam: a [`DataContext`] binds OQX's query semantics to a concrete
2//! data model. The engine never reaches into host data directly — it asks the
3//! context to resolve named roots, read properties/relations, coerce a relation
4//! result into rows, compute identity (for `follow` dedup and `distinct` over
5//! unprojected rows), and optionally supply custom scalar functions/methods.
6//! The same query semantics then run over plain values, a lazy store-backed
7//! graph, or a remote API without changing the engine.
8//!
9//! (Performant execution over a real store is the tier-3 seam — see
10//! [`crate::planner`] — which pushes work into the store instead of driving it
11//! row by row here.)
12
13use crate::Result;
14use crate::regex_dialect::RegexDialect;
15use crate::semantics::{builtin_function, builtin_method_with, coerce_collection};
16use crate::value::{Object, Value};
17
18pub trait DataContext {
19 /// Resolve a named root (the `from <name>` source / directive receiver).
20 /// Unknown names are `Value::Undefined`.
21 fn root(&self, name: &str) -> Value;
22
23 /// Read a property/relation off a row: a bare identifier (`field`), a
24 /// `.field` segment, or a `^field` outer reference all come through here,
25 /// each against exactly the row of the scope it names. An absent property
26 /// is `Value::Undefined`; the engine never looks elsewhere for it.
27 fn get(&self, row: &Value, key: &str) -> Value;
28
29 /// Coerce a relation/source value into rows.
30 fn to_rows(&self, value: &Value) -> Vec<Value>;
31
32 /// Identity of a row for `follow` cycle detection / dedup and for
33 /// `distinct` over unprojected rows.
34 fn identity(&self, row: &Value) -> Value;
35
36 /// Optional custom free function. `None` = not handled: the engine falls
37 /// back to the builtin table, then errors if that has no such function.
38 fn call_function(&self, name: &str, args: &[Value]) -> Option<Result<Value>> {
39 let _ = (name, args);
40 None
41 }
42
43 /// Optional custom method (`recv.name(args)`). `None` = not handled, as
44 /// for [`DataContext::call_function`].
45 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<Result<Value>> {
46 let _ = (name, recv, args);
47 None
48 }
49
50 /// The regex dialect `matches()` compiles against. [`RegexDialect::Oqx`]
51 /// (the default) is the portable baseline the spec tests;
52 /// [`RegexDialect::Native`] hands the pattern to the `regex` crate as is —
53 /// implementation-defined, not portable. The engine dispatches `matches`
54 /// through [`DataContext::call_method`], so this is read by the context's
55 /// own `matches` ([`DefaultContext`] does, via
56 /// [`crate::semantics::builtin_method_with`]); plain
57 /// [`crate::semantics::builtin_method`] is always the baseline.
58 fn regex_dialect(&self) -> RegexDialect {
59 RegexDialect::Oqx
60 }
61}
62
63/// The default context: plain [`Value`]s. Named roots come from an [`Object`]
64/// map; properties are object keys (and array indices spelled as integers);
65/// identity is the `id` property when present, else the row itself
66/// (structural identity — the spec's rule; the reference uses reference
67/// identity for id-less objects, which has no portable meaning).
68#[derive(Clone, Debug, Default)]
69pub struct DefaultContext {
70 roots: Object,
71 regex_dialect: RegexDialect,
72}
73
74impl DefaultContext {
75 pub fn new(roots: Object) -> Self {
76 Self {
77 roots,
78 regex_dialect: RegexDialect::Oqx,
79 }
80 }
81
82 /// Opt `matches()` into a regex dialect (see [`DataContext::regex_dialect`]).
83 pub fn with_regex_dialect(mut self, dialect: RegexDialect) -> Self {
84 self.regex_dialect = dialect;
85 self
86 }
87
88 pub fn roots(&self) -> &Object {
89 &self.roots
90 }
91}
92
93impl DataContext for DefaultContext {
94 fn root(&self, name: &str) -> Value {
95 self.roots.get(name).cloned().unwrap_or(Value::Undefined)
96 }
97
98 fn get(&self, row: &Value, key: &str) -> Value {
99 match row {
100 Value::Object(o) => o.get(key).cloned().unwrap_or(Value::Undefined),
101 Value::Array(a) => match key.parse::<usize>() {
102 Ok(i) if key == i.to_string() => a.get(i).cloned().unwrap_or(Value::Undefined),
103 _ => Value::Undefined,
104 },
105 _ => Value::Undefined,
106 }
107 }
108
109 fn to_rows(&self, value: &Value) -> Vec<Value> {
110 coerce_collection(value).into_owned()
111 }
112
113 fn identity(&self, row: &Value) -> Value {
114 if let Value::Object(o) = row {
115 if let Some(id) = o.get("id") {
116 return id.clone();
117 }
118 }
119 row.clone()
120 }
121
122 fn call_function(&self, name: &str, args: &[Value]) -> Option<Result<Value>> {
123 builtin_function(name, args)
124 }
125
126 fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<Result<Value>> {
127 builtin_method_with(self.regex_dialect, name, recv, args)
128 }
129
130 fn regex_dialect(&self) -> RegexDialect {
131 self.regex_dialect
132 }
133}