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 std::rc::Rc;
15
16use crate::optimize::hash_index::RowIndex;
17use crate::regex_dialect::RegexDialect;
18use crate::semantics::{builtin_function, builtin_method_with, coerce_collection};
19use crate::value::{Object, Value};
20
21pub trait DataContext {
22    /// Resolve a named root (the `from <name>` source / directive receiver).
23    /// Unknown names are `Value::Undefined`.
24    fn root(&self, name: &str) -> Value;
25
26    /// Read a property/relation off a row: a bare identifier (`field`), a
27    /// `.field` segment, or a `^field` outer reference all come through here,
28    /// each against exactly the row of the scope it names. An absent property
29    /// is `Ok(Value::Undefined)`; the engine never looks elsewhere for it.
30    ///
31    /// The error channel is the host's: an `Err` (an eval-stage
32    /// [`crate::OqxError`], see [`crate::OqxError::eval`]) aborts the query and
33    /// is returned from `run` exactly like a thrown error from the reference's
34    /// `get` — a context can reject a reserved name or surface a failed store
35    /// read at the point it happens instead of stashing it for after the run.
36    /// A context that never fails wraps its value in `Ok`;
37    /// [`DefaultContext::read`] is the plain-value read to delegate to.
38    fn get(&self, row: &Value, key: &str) -> Result<Value>;
39
40    /// Coerce a relation/source value into rows.
41    fn to_rows(&self, value: &Value) -> Vec<Value>;
42
43    /// Optional: a value this context handed out as a stand-in for a
44    /// collection it has not read yet (a lazy table handle), resolved to what
45    /// it stands for. The engine calls it on every value it is about to observe
46    /// AS A VALUE — an operand of `==`/`in`/arithmetic, a function or method
47    /// argument, a projected item, an `order by` or `distinct` key, a `where`
48    /// scalar, a lift — and never on a value it reads in ROW POSITION (the
49    /// query source, a block receiver, a body-level `from`, a `follow`
50    /// destination), which goes to [`DataContext::to_rows`] and
51    /// [`DataContext::index_for`] as the context handed it out, so a
52    /// store-backed context can answer a probe on the handle without reading the
53    /// table and still never lets the stand-in be seen by the language. The
54    /// default is the identity (a context whose values are what they are).
55    fn materialize(&self, value: Value) -> Value {
56        value
57    }
58
59    /// Identity of a row for `follow` cycle detection / dedup and for
60    /// `distinct` over unprojected rows.
61    fn identity(&self, row: &Value) -> Value;
62
63    /// Optional custom free function. `None` = not handled: the engine falls
64    /// back to the builtin table, then errors if that has no such function.
65    fn call_function(&self, name: &str, args: &[Value]) -> Option<Result<Value>> {
66        let _ = (name, args);
67        None
68    }
69
70    /// Optional custom method (`recv.name(args)`). `None` = not handled, as
71    /// for [`DataContext::call_function`].
72    fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<Result<Value>> {
73        let _ = (name, recv, args);
74        None
75    }
76
77    /// The regex dialect `matches()` compiles against. [`RegexDialect::Oqx`]
78    /// (the default) is the portable baseline the spec tests;
79    /// [`RegexDialect::Native`] hands the pattern to the `regex` crate as is —
80    /// implementation-defined, not portable. The engine dispatches `matches`
81    /// through [`DataContext::call_method`], so this is read by the context's
82    /// own `matches` ([`DefaultContext`] does, via
83    /// [`crate::semantics::builtin_method_with`]); plain
84    /// [`crate::semantics::builtin_method`] is always the baseline.
85    fn regex_dialect(&self) -> RegexDialect {
86        RegexDialect::Oqx
87    }
88
89    /// Optional: a pre-built equality index over `collection` (a value this
90    /// context served as a root or relation) on the property path `path`
91    /// (`["customer_id"]`, `["meta", "id"]`; an empty path keys by the row
92    /// itself). The engine asks before building its own [`crate::optimize::HashIndex`]
93    /// for a correlated equality in a nested block (`where id == ^customer_id`);
94    /// `None` (the default) lets it build one. `lookup(value)` must return the
95    /// ascending positions, into `to_rows(collection)` in order, of the rows
96    /// whose value at `path` equals `value` under OQX equality (SEMANTICS §5).
97    /// An index may also implement [`RowIndex::lookup_rows`]: the engine then
98    /// probes it for a statically stable receiver BEFORE reading the
99    /// collection, which is never materialized when the index answers.
100    /// [`crate::adapters::indexed::IndexedContext`] implements the positional
101    /// form. The `Rc` lets a context create indexes on demand.
102    fn index_for(&self, collection: &Value, path: &[String]) -> Option<Rc<dyn RowIndex + '_>> {
103        let _ = (collection, path);
104        None
105    }
106}
107
108/// The default context: plain [`Value`]s. Named roots come from an [`Object`]
109/// map; properties are object keys (and array indices spelled as integers);
110/// identity is the `id` property when present, else the row itself
111/// (structural identity — the spec's rule; the reference uses reference
112/// identity for id-less objects, which has no portable meaning).
113#[derive(Clone, Debug, Default)]
114pub struct DefaultContext {
115    roots: Object,
116    regex_dialect: RegexDialect,
117}
118
119impl DefaultContext {
120    pub fn new(roots: Object) -> Self {
121        Self {
122            roots,
123            regex_dialect: RegexDialect::Oqx,
124        }
125    }
126
127    /// Opt `matches()` into a regex dialect (see [`DataContext::regex_dialect`]).
128    pub fn with_regex_dialect(mut self, dialect: RegexDialect) -> Self {
129        self.regex_dialect = dialect;
130        self
131    }
132
133    pub fn roots(&self) -> &Object {
134        &self.roots
135    }
136
137    /// The plain-value property read [`DataContext::get`] performs for
138    /// [`DefaultContext`]: an object's own key, an array's element for an
139    /// integer-spelled key (`"0"`, `"1"`, …), otherwise `Value::Undefined`
140    /// (primitives have no properties). It cannot fail, so a custom context
141    /// that only adds computed keys can fall back to it:
142    ///
143    /// ```
144    /// use oqx::{DataContext, DefaultContext, Result, Value};
145    ///
146    /// struct Computed;
147    /// impl DataContext for Computed {
148    ///     fn root(&self, _name: &str) -> Value { Value::Undefined }
149    ///     fn get(&self, row: &Value, key: &str) -> Result<Value> {
150    ///         if key == "shout" {
151    ///             return Ok(match DefaultContext::read(row, "name") {
152    ///                 Value::Str(s) => Value::Str(s.to_uppercase()),
153    ///                 _ => Value::Undefined,
154    ///             });
155    ///         }
156    ///         Ok(DefaultContext::read(row, key))
157    ///     }
158    ///     fn to_rows(&self, v: &Value) -> Vec<Value> { DefaultContext::default().to_rows(v) }
159    ///     fn identity(&self, row: &Value) -> Value { DefaultContext::default().identity(row) }
160    /// }
161    /// ```
162    pub fn read(row: &Value, key: &str) -> Value {
163        match row {
164            Value::Object(o) => o.get(key).cloned().unwrap_or(Value::Undefined),
165            Value::Array(a) => match key.parse::<usize>() {
166                Ok(i) if key == i.to_string() => a.get(i).cloned().unwrap_or(Value::Undefined),
167                _ => Value::Undefined,
168            },
169            _ => Value::Undefined,
170        }
171    }
172}
173
174impl DataContext for DefaultContext {
175    fn root(&self, name: &str) -> Value {
176        self.roots.get(name).cloned().unwrap_or(Value::Undefined)
177    }
178
179    fn get(&self, row: &Value, key: &str) -> Result<Value> {
180        Ok(Self::read(row, key))
181    }
182
183    fn to_rows(&self, value: &Value) -> Vec<Value> {
184        coerce_collection(value).into_owned()
185    }
186
187    fn identity(&self, row: &Value) -> Value {
188        if let Value::Object(o) = row {
189            if let Some(id) = o.get("id") {
190                return id.clone();
191            }
192        }
193        row.clone()
194    }
195
196    fn call_function(&self, name: &str, args: &[Value]) -> Option<Result<Value>> {
197        builtin_function(name, args)
198    }
199
200    fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<Result<Value>> {
201        builtin_method_with(self.regex_dialect, name, recv, args)
202    }
203
204    fn regex_dialect(&self) -> RegexDialect {
205        self.regex_dialect
206    }
207}