Skip to main content

dataflow_rs/expr/
operators.rs

1//! The operators an engine evaluates, as datalogic-rs describes its own
2//! (`Engine::operators`), plus the engine's `secret` and the host's.
3
4use std::borrow::Cow;
5
6/// The family an operator belongs to.
7///
8/// [`Core`](Self::Core) and [`Secret`](Self::Secret) are always on; the
9/// extension families are selected at runtime on
10/// [`ExprEngineBuilder`](super::ExprEngineBuilder) ([`family`], [`families`],
11/// [`all_operators`]). [`Datetime`](Self::Datetime) and
12/// [`Tensor`](Self::Tensor) also need their cargo features (`datetime`,
13/// `tensor`) compiled in.
14///
15/// This crate's own, not `datalogic_rs::Family`: it adds the engine's
16/// [`Secret`](Self::Secret) and a host's [`Custom`](Self::Custom). The names
17/// differ, so a host imports both unaliased.
18///
19/// [`family`]: super::ExprEngineBuilder::family
20/// [`families`]: super::ExprEngineBuilder::families
21/// [`all_operators`]: super::ExprEngineBuilder::all_operators
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum OperatorFamily {
25    /// Always present.
26    Core,
27    /// Date and time operators (`now`, `datetime`, `timestamp`, `parse_date`,
28    /// `format_date`, `date_diff`). Needs the `datetime` feature.
29    Datetime,
30    /// String operators (`length`, `starts_with`, `ends_with`, `upper`,
31    /// `lower`, `trim`, `split`).
32    ExtString,
33    /// Array operators (`sort`, `slice`, `group_by`, `distinct`).
34    ExtArray,
35    /// Math operators (`abs`, `ceil`, `floor`).
36    ExtMath,
37    /// Control-flow operators (`exists`, `??`, `switch`, `type`).
38    ExtControl,
39    /// Object operators (`keys`, `values`, `entries`).
40    ExtObject,
41    /// JSONLogic's `try` and `throw` operators.
42    ErrorHandling,
43    /// Tensor operators (`tensor`, `shape`, `full`, `cast`, `concat`, …).
44    /// Needs the `tensor` feature.
45    Tensor,
46    /// The engine's own `secret` operator.
47    Secret,
48    /// An operator the host registered.
49    Custom,
50}
51
52impl OperatorFamily {
53    /// The family of datalogic's family `upstream`. `None` for one this
54    /// crate has no variant for (`flagd`), which the engine never turns on.
55    fn of(upstream: datalogic_rs::Family) -> Option<Self> {
56        use datalogic_rs::Family as F;
57        Some(match upstream {
58            F::Core => Self::Core,
59            F::DateTime => Self::Datetime,
60            F::ExtString => Self::ExtString,
61            F::ExtArray => Self::ExtArray,
62            F::ExtMath => Self::ExtMath,
63            F::ExtControl => Self::ExtControl,
64            F::ExtObject => Self::ExtObject,
65            F::ErrorHandling => Self::ErrorHandling,
66            F::Tensor => Self::Tensor,
67            _ => return None,
68        })
69    }
70
71    /// The datalogic family `self` selects on `EngineBuilder::with_families`,
72    /// or `None` for [`Secret`](Self::Secret) and [`Custom`](Self::Custom).
73    pub(crate) fn to_upstream(self) -> Option<datalogic_rs::Family> {
74        use datalogic_rs::Family as F;
75        Some(match self {
76            Self::Core => F::Core,
77            Self::Datetime => F::DateTime,
78            Self::ExtString => F::ExtString,
79            Self::ExtArray => F::ExtArray,
80            Self::ExtMath => F::ExtMath,
81            Self::ExtControl => F::ExtControl,
82            Self::ExtObject => F::ExtObject,
83            Self::ErrorHandling => F::ErrorHandling,
84            Self::Tensor => F::Tensor,
85            Self::Secret | Self::Custom => return None,
86        })
87    }
88
89    /// The family datalogic names `name` in an operator's description.
90    fn named(name: &str) -> Option<Self> {
91        datalogic_rs::Family::ALL
92            .iter()
93            .find(|f| f.name() == name)
94            .and_then(|f| Self::of(*f))
95    }
96}
97
98/// The operator families [`ExprEngineBuilder::all_operators`](super::ExprEngineBuilder::all_operators)
99/// turns on: the six always-compiled extension families, plus
100/// [`OperatorFamily::Datetime`] when the `datetime` feature is compiled in.
101/// [`OperatorFamily::Tensor`] stays out: its operator names (`shape`, `full`,
102/// `cast`, `concat`) are ordinary JSON keys.
103pub(crate) const ALL_OPERATORS: &[OperatorFamily] = &[
104    #[cfg(feature = "datetime")]
105    OperatorFamily::Datetime,
106    OperatorFamily::ExtString,
107    OperatorFamily::ExtArray,
108    OperatorFamily::ExtMath,
109    OperatorFamily::ExtControl,
110    OperatorFamily::ExtObject,
111    OperatorFamily::ErrorHandling,
112];
113
114/// One operator an engine can evaluate.
115///
116/// This crate's own, not `datalogic_rs::OperatorInfo`, which it is built
117/// from: its [`family`](Self::family) is this crate's [`OperatorFamily`].
118///
119/// ```
120/// use dataflow_rs::datalogic_rs::{Family, OperatorInfo};
121/// use dataflow_rs::expr::{Operator, OperatorFamily};
122///
123/// let _: Option<Operator> = None;
124/// let _: Option<OperatorFamily> = None;
125/// let _: Option<OperatorInfo> = None;
126/// let _: Option<Family> = None;
127/// ```
128#[derive(Clone, Debug, PartialEq, Eq)]
129#[non_exhaustive]
130pub struct Operator {
131    /// The name an expression spells it with.
132    pub name: Cow<'static, str>,
133    /// The family it belongs to.
134    pub family: OperatorFamily,
135    /// It evaluates an argument once per element, with `var` reading the element.
136    pub scoping: bool,
137    /// The same arguments always give the same result.
138    pub deterministic: bool,
139}
140
141impl Operator {
142    /// A host operator: not scoping, not deterministic, until told otherwise.
143    pub fn custom(name: impl Into<Cow<'static, str>>) -> Self {
144        Self {
145            name: name.into(),
146            family: OperatorFamily::Custom,
147            scoping: false,
148            deterministic: false,
149        }
150    }
151
152    /// Sets whether the operator evaluates an argument once per element.
153    #[must_use]
154    pub fn scoping(mut self, yes: bool) -> Self {
155        self.scoping = yes;
156        self
157    }
158
159    /// Sets whether the same arguments always give the same result.
160    #[must_use]
161    pub fn deterministic(mut self, yes: bool) -> Self {
162        self.deterministic = yes;
163        self
164    }
165
166    pub(crate) fn secret() -> Self {
167        Self {
168            name: Cow::Borrowed(super::secrets::SECRET_OPERATOR),
169            family: OperatorFamily::Secret,
170            scoping: false,
171            deterministic: true,
172        }
173    }
174
175    /// One row per name datalogic's operator answers to, its canonical name
176    /// first, then each alias (`val`, then `var`): an expression may spell
177    /// it either way. Scoping is its `scoped_arg`; only the clock makes it
178    /// non-deterministic.
179    pub(crate) fn from_logic(op: datalogic_rs::OperatorInfo) -> impl Iterator<Item = Self> {
180        let family = OperatorFamily::named(op.family);
181        debug_assert!(
182            family.is_some(),
183            "`{}` is in family `{}`, which no feature turns on",
184            op.name,
185            op.family
186        );
187        family.into_iter().flat_map(move |family| {
188            std::iter::once(op.name)
189                .chain(op.aliases.iter().copied())
190                .map(move |name| Self {
191                    name: Cow::Borrowed(name),
192                    family,
193                    scoping: op.scoped_arg.is_some(),
194                    deterministic: op.effect != "clock",
195                })
196        })
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::expr::ExprEngine;
204    use serde_json::{Value, json};
205
206    fn probe(engine: &ExprEngine, logic: Value) -> Value {
207        engine.eval_json(&logic)
208    }
209
210    fn rows() -> Value {
211        json!([{"a": 1}, {"a": 2}])
212    }
213
214    /// Every operator `all_operators` lists, as `ExprEngine::operators` does.
215    fn listed() -> Vec<Operator> {
216        ExprEngine::builder()
217            .all_operators()
218            .build()
219            .unwrap()
220            .operators()
221            .to_vec()
222    }
223
224    #[test]
225    fn the_expanded_names_are_datalogics_in_its_order_with_no_duplicate() {
226        let logic = datalogic_rs::Engine::builder()
227            .with_families(ALL_OPERATORS.iter().filter_map(|f| f.to_upstream()))
228            .build();
229        let ours: Vec<String> = listed()
230            .into_iter()
231            .filter(|o| o.family != OperatorFamily::Secret)
232            .map(|o| o.name.into_owned())
233            .collect();
234        let mut theirs: Vec<&str> = logic.builtin_operator_names().collect();
235        let mut sorted = ours.clone();
236        sorted.sort_unstable();
237        sorted.dedup();
238        assert_eq!(sorted.len(), ours.len(), "a name twice");
239        theirs.sort_unstable();
240        assert_eq!(
241            sorted, theirs,
242            "every name datalogic answers to, aliases included"
243        );
244        // An alias follows its operator.
245        let at = |name: &str| ours.iter().position(|n| n == name).unwrap();
246        assert_eq!(at("var"), at("val") + 1);
247        assert_eq!(at("?:"), at("if") + 1);
248    }
249
250    #[test]
251    fn the_scoping_and_the_clock_are_datalogics_facts() {
252        let ops = listed();
253        let scoping: Vec<&str> = ops.iter().filter(|o| o.scoping).map(|o| &*o.name).collect();
254        let non: Vec<&str> = ops
255            .iter()
256            .filter(|o| !o.deterministic)
257            .map(|o| &*o.name)
258            .collect();
259        let mut want_scoping = vec![
260            "filter", "map", "reduce", "all", "some", "none", "sort", "group_by", "distinct", "try",
261        ];
262        let mut want_non = Vec::new();
263        if cfg!(feature = "datetime") {
264            want_non.push("now");
265        }
266        let mut scoping = scoping;
267        scoping.sort_unstable();
268        want_scoping.sort_unstable();
269        assert_eq!(scoping, want_scoping);
270        assert_eq!(non, want_non);
271    }
272
273    #[test]
274    fn every_family_datalogic_has_but_flagd_is_one_of_ours() {
275        // A family datalogic adds fails here until it is mapped or named
276        // unwanted; `flagd` is feature-flag specific and never turned on.
277        for family in datalogic_rs::Family::ALL {
278            let mapped = OperatorFamily::of(*family);
279            assert_eq!(
280                mapped.is_none(),
281                family.name() == "Flagd",
282                "{}",
283                family.name()
284            );
285            if let Some(ours) = mapped {
286                assert_eq!(ours.to_upstream(), Some(*family));
287            }
288        }
289    }
290
291    #[test]
292    fn the_effects_are_the_ones_this_crate_reads() {
293        // `deterministic` reads `effect`: a new effect fails here at the
294        // bump that brings it, rather than silently counting as pure.
295        let logic = datalogic_rs::Engine::builder()
296            .with_families(ALL_OPERATORS.iter().filter_map(|f| f.to_upstream()))
297            .build();
298        let mut effects: Vec<&str> = logic.operators().map(|o| o.effect).collect();
299        effects.sort_unstable();
300        effects.dedup();
301        for effect in effects {
302            assert!(
303                ["pure", "clock", "throws", "catches"].contains(&effect),
304                "an effect this crate does not read: {effect}"
305            );
306        }
307    }
308
309    #[test]
310    fn core_scoping_operators_read_the_element() {
311        let e = ExprEngine::default();
312        let a = rows();
313        assert_eq!(probe(&e, json!({"map": [a, {"var": "a"}]})), json!([1, 2]));
314        assert_eq!(
315            probe(&e, json!({"filter": [a, {">": [{"var": "a"}, 1]}]})),
316            json!([{"a": 2}])
317        );
318        assert_eq!(
319            probe(&e, json!({"all": [a, {">": [{"var": "a"}, 0]}]})),
320            json!(true)
321        );
322        assert_eq!(
323            probe(&e, json!({"some": [a, {">": [{"var": "a"}, 1]}]})),
324            json!(true)
325        );
326        assert_eq!(
327            probe(&e, json!({"none": [a, {">": [{"var": "a"}, 5]}]})),
328            json!(true)
329        );
330        assert_eq!(
331            probe(
332                &e,
333                json!({"reduce": [a, {"+": [{"var": "current.a"}, {"var": "accumulator"}]}, 0]})
334            ),
335            json!(3)
336        );
337    }
338
339    #[test]
340    fn ext_array_scoping_operators_read_the_element() {
341        let e = ExprEngine::builder()
342            .family(OperatorFamily::ExtArray)
343            .build()
344            .unwrap();
345        let a = rows();
346        assert_eq!(
347            probe(&e, json!({"sort": [a, false, {"var": "a"}]})),
348            json!([{"a": 2}, {"a": 1}])
349        );
350        assert_eq!(
351            probe(&e, json!({"group_by": [a, {"var": "a"}]})),
352            json!([
353                {"key": 1, "items": [{"a": 1}]},
354                {"key": 2, "items": [{"a": 2}]}
355            ])
356        );
357    }
358}