Skip to main content

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}