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}