use crate::Result;
use crate::ast::Query;
use crate::context::{DataContext, DefaultContext};
use crate::engine::{Engine, InMemoryEngine, OqxResult};
use crate::plan::ROWS_ROOT;
use crate::value::{Object, Value};
pub struct Plan {
pub rows: Vec<Value>,
pub residual: Query,
pub context: Option<Box<dyn DataContext>>,
}
impl Plan {
pub fn new(rows: Vec<Value>, residual: Query) -> Self {
Self {
rows,
residual,
context: None,
}
}
pub fn with_context(mut self, context: impl DataContext + 'static) -> Self {
self.context = Some(Box::new(context));
self
}
}
impl std::fmt::Debug for Plan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Plan")
.field("rows", &self.rows)
.field("residual", &self.residual)
.field("context", &self.context.as_ref().map(|_| "<DataContext>"))
.finish()
}
}
pub trait QueryPlanner {
fn plan(&self, query: &Query, params: &[Value]) -> Option<Plan>;
}
pub struct PlannedEngine<P: QueryPlanner> {
planner: P,
fallback: Box<dyn DataContext>,
}
impl<P: QueryPlanner> PlannedEngine<P> {
pub fn new(planner: P) -> Self {
Self::with_fallback(planner, DefaultContext::default())
}
pub fn with_fallback(planner: P, fallback: impl DataContext + 'static) -> Self {
Self {
planner,
fallback: Box::new(fallback),
}
}
pub fn planner(&self) -> &P {
&self.planner
}
}
impl<P: QueryPlanner> Engine for PlannedEngine<P> {
fn run(&self, query: &Query, bindings: &[Value]) -> Result<OqxResult> {
let Some(plan) = self.planner.plan(query, bindings) else {
let ctx = Overlay {
rows: None,
inner: self.fallback.as_ref(),
};
return InMemoryEngine::new(ctx).run(query, bindings);
};
let Plan {
rows,
residual,
context,
} = plan;
match context {
None => {
let mut roots = Object::with_capacity(1);
roots.insert(ROWS_ROOT, Value::Array(rows));
InMemoryEngine::new(DefaultContext::new(roots)).run(&residual, bindings)
}
Some(ctx) => {
let ctx = Overlay {
rows: Some(Value::Array(rows)),
inner: ctx.as_ref(),
};
InMemoryEngine::new(ctx).run(&residual, bindings)
}
}
}
}
struct Overlay<'a> {
rows: Option<Value>,
inner: &'a dyn DataContext,
}
impl DataContext for Overlay<'_> {
fn root(&self, name: &str) -> Value {
match &self.rows {
Some(rows) if name == ROWS_ROOT => rows.clone(),
_ => self.inner.root(name),
}
}
fn get(&self, row: &Value, key: &str) -> Result<Value> {
self.inner.get(row, key)
}
fn to_rows(&self, value: &Value) -> Vec<Value> {
self.inner.to_rows(value)
}
fn identity(&self, row: &Value) -> Value {
self.inner.identity(row)
}
fn call_function(&self, name: &str, args: &[Value]) -> Option<Result<Value>> {
self.inner.call_function(name, args)
}
fn call_method(&self, name: &str, recv: &Value, args: &[Value]) -> Option<Result<Value>> {
self.inner.call_method(name, recv, args)
}
fn regex_dialect(&self) -> crate::regex_dialect::RegexDialect {
self.inner.regex_dialect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_string;
use crate::plan::residual_query;
use std::cell::Cell;
fn num(n: f64) -> Value {
Value::Number(n)
}
fn obj(pairs: &[(&str, Value)]) -> Value {
Value::Object(
pairs
.iter()
.map(|(k, v)| ((*k).to_owned(), v.clone()))
.collect(),
)
}
struct Scripted {
calls: Cell<usize>,
answer: fn(&Query) -> Option<Plan>,
}
impl QueryPlanner for Scripted {
fn plan(&self, query: &Query, _params: &[Value]) -> Option<Plan> {
self.calls.set(self.calls.get() + 1);
(self.answer)(query)
}
}
struct Things;
impl DataContext for Things {
fn root(&self, name: &str) -> Value {
if name == "things" {
Value::Array(vec![obj(&[("n", num(1.0))]), obj(&[("n", num(2.0))])])
} else {
Value::Undefined
}
}
fn get(&self, row: &Value, key: &str) -> Result<Value> {
if key == "double" {
return Ok(match row.as_object().and_then(|o| o.get("n")) {
Some(Value::Number(n)) => num(n * 2.0),
_ => Value::Undefined,
});
}
Ok(DefaultContext::read(row, key))
}
fn to_rows(&self, value: &Value) -> Vec<Value> {
DefaultContext::default().to_rows(value)
}
fn identity(&self, row: &Value) -> Value {
DefaultContext::default().identity(row)
}
}
#[test]
fn overlay_serves_the_rows_root_and_forwards_everything_else() {
let rows = Value::Array(vec![obj(&[("n", num(7.0))])]);
let with_rows = Overlay {
rows: Some(rows.clone()),
inner: &Things,
};
assert_eq!(with_rows.root(ROWS_ROOT), rows);
assert_eq!(with_rows.root("things"), Things.root("things"));
assert_eq!(
with_rows.get(&obj(&[("n", num(3.0))]), "double").unwrap(),
num(6.0)
);
assert_eq!(
with_rows.to_rows(&Value::Array(vec![num(1.0)])),
vec![num(1.0)]
);
assert_eq!(
with_rows.identity(&obj(&[("id", num(9.0)), ("n", num(1.0))])),
num(9.0)
);
assert!(with_rows.call_function("size", &[]).is_none());
assert!(with_rows.call_method("lower", &Value::Null, &[]).is_none());
let without = Overlay {
rows: None,
inner: &Things,
};
assert_eq!(without.root(ROWS_ROOT), Value::Undefined);
}
#[test]
fn plan_debug_does_not_require_a_debug_context() {
let q = parse_string("n from things").unwrap();
let plan = Plan::new(vec![num(1.0)], residual_query(&q, None)).with_context(Things);
let text = format!("{plan:?}");
assert!(text.contains("<DataContext>"), "{text}");
assert!(plan.context.is_some());
assert!(Plan::new(Vec::new(), q).context.is_none());
}
#[test]
fn declined_plan_runs_the_query_over_the_fallback_context() {
let planner = Scripted {
calls: Cell::new(0),
answer: |_| None,
};
let engine = PlannedEngine::with_fallback(planner, Things);
let q = parse_string("d: double from things where n == 2").unwrap();
let res = engine.run(&q, &[]).unwrap();
assert_eq!(res, OqxResult::Collect(vec![obj(&[("d", num(4.0))])]));
assert_eq!(engine.planner().calls.get(), 1);
}
#[test]
fn a_plan_without_context_runs_the_residual_over_plain_rows() {
let planner = Scripted {
calls: Cell::new(0),
answer: |q| {
Some(Plan::new(
vec![obj(&[("n", num(5.0))]), obj(&[("n", num(6.0))])],
residual_query(q, None),
))
},
};
let engine = PlannedEngine::new(planner);
let q = parse_string("n from nowhere where n > 100 order by n desc").unwrap();
let res = engine.run(&q, &[]).unwrap();
assert_eq!(
res,
OqxResult::Collect(vec![obj(&[("n", num(6.0))]), obj(&[("n", num(5.0))])])
);
}
#[test]
fn a_plan_with_context_navigates_the_rows_through_it() {
let planner = Scripted {
calls: Cell::new(0),
answer: |q| {
Some(
Plan::new(vec![obj(&[("n", num(5.0))])], residual_query(q, None))
.with_context(Things),
)
},
};
let engine = PlannedEngine::new(planner);
let q = parse_string("d: double from things").unwrap();
let res = engine.run(&q, &[]).unwrap();
assert_eq!(res, OqxResult::Collect(vec![obj(&[("d", num(10.0))])]));
}
}