Skip to content

Commit b6f73b7

Browse files
authored
Fix subquery global binding lookup and error propagation (#454)
1 parent f9ea25c commit b6f73b7

File tree

7 files changed

+204
-7
lines changed

7 files changed

+204
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
### Added
1212

1313
### Fixed
14+
- partiql-eval: Fixed propagation of errors in subqueries to outer query
15+
- partiql-eval: Fixed handling of nested binding environments in subqueries
1416

1517
## [0.7.0] - 2024-03-12
1618
### Changed

partiql-catalog/src/context.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ use partiql_value::{BindingsName, DateTime, Tuple, Value};
22
use std::any::Any;
33
use std::fmt::Debug;
44

5-
pub trait Bindings<T>: Debug {
5+
pub trait Bindings<T>: Debug
6+
where
7+
T: Debug,
8+
{
69
fn get(&self, name: &BindingsName<'_>) -> Option<&T>;
710
}
811

partiql-eval/src/env.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,33 @@ pub mod basic {
9696
}
9797
}
9898
}
99+
100+
#[derive(Debug)]
101+
pub struct NestedBindings<'a, T>
102+
where
103+
T: Debug,
104+
{
105+
bindings: MapBindings<T>,
106+
parent: &'a dyn Bindings<T>,
107+
}
108+
109+
impl<'a, T> NestedBindings<'a, T>
110+
where
111+
T: Debug,
112+
{
113+
pub fn new(bindings: MapBindings<T>, parent: &'a dyn Bindings<T>) -> Self {
114+
Self { bindings, parent }
115+
}
116+
}
117+
118+
impl<'a, T> Bindings<T> for NestedBindings<'a, T>
119+
where
120+
T: Debug,
121+
{
122+
fn get(&self, name: &BindingsName<'_>) -> Option<&T> {
123+
self.bindings.get(name).or_else(|| self.parent.get(name))
124+
}
125+
}
99126
}
100127

101128
#[cfg(test)]

partiql-eval/src/eval/evaluable.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,10 +1321,15 @@ impl EvalExpr for EvalSubQueryExpr {
13211321
let nested_ctx: NestedContext<'_, '_> = NestedContext::new(bindings, ctx);
13221322

13231323
let mut plan = self.plan.borrow_mut();
1324-
if let Ok(evaluated) = plan.execute_mut(&nested_ctx) {
1325-
evaluated.result
1326-
} else {
1327-
Missing
1324+
1325+
match plan.execute_mut(&nested_ctx) {
1326+
Ok(evaluated) => evaluated.result,
1327+
Err(err) => {
1328+
for e in err.errors {
1329+
ctx.add_error(e);
1330+
}
1331+
Missing
1332+
}
13281333
}
13291334
};
13301335
Cow::Owned(value)

partiql-eval/src/eval/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use petgraph::{Directed, Outgoing};
1313

1414
use partiql_value::Value;
1515

16-
use crate::env::basic::MapBindings;
16+
use crate::env::basic::{MapBindings, NestedBindings};
1717

1818
use petgraph::graph::NodeIndex;
1919

@@ -181,6 +181,7 @@ impl<'a> BasicContext<'a> {
181181
}
182182
}
183183
}
184+
184185
impl<'a> SessionContext<'a> for BasicContext<'a> {
185186
fn bindings(&self) -> &dyn Bindings<Value> {
186187
&self.bindings
@@ -195,6 +196,7 @@ impl<'a> SessionContext<'a> for BasicContext<'a> {
195196
self.user.get(&key).copied()
196197
}
197198
}
199+
198200
impl<'a> EvalContext<'a> for BasicContext<'a> {
199201
fn as_session(&'a self) -> &'a dyn SessionContext<'a> {
200202
self
@@ -215,12 +217,13 @@ impl<'a> EvalContext<'a> for BasicContext<'a> {
215217

216218
#[derive(Debug)]
217219
pub struct NestedContext<'a, 'c> {
218-
pub bindings: MapBindings<Value>,
220+
pub bindings: NestedBindings<'a, Value>,
219221
pub parent: &'a dyn EvalContext<'c>,
220222
}
221223

222224
impl<'a, 'c> NestedContext<'a, 'c> {
223225
pub fn new(bindings: MapBindings<Value>, parent: &'a dyn EvalContext<'c>) -> Self {
226+
let bindings = NestedBindings::new(bindings, parent.bindings());
224227
NestedContext { bindings, parent }
225228
}
226229
}

partiql/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#![deny(rust_2018_idioms)]
22
#![deny(clippy::all)]
33

4+
mod subquery_tests;
5+
46
#[cfg(test)]
57
mod tests {
68
use partiql_ast_passes::error::AstTransformationError;

partiql/src/subquery_tests.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#![deny(rust_2018_idioms)]
2+
#![deny(clippy::all)]
3+
#![warn(clippy::pedantic)]
4+
5+
#[cfg(test)]
6+
mod tests {
7+
use partiql_catalog::context::SystemContext;
8+
use partiql_catalog::{Catalog, PartiqlCatalog};
9+
use partiql_eval::env::basic::MapBindings;
10+
use partiql_eval::error::EvalErr;
11+
use partiql_eval::eval::BasicContext;
12+
use partiql_eval::plan::EvaluationMode;
13+
use partiql_logical::{LogicalPlan, ProjectValue, VarRefType};
14+
use partiql_value::{bag, tuple, Bag, BindingsName, DateTime, Value};
15+
use std::any::Any;
16+
17+
#[track_caller]
18+
#[inline]
19+
pub(crate) fn evaluate(
20+
catalog: &dyn Catalog,
21+
logical: partiql_logical::LogicalPlan<partiql_logical::BindingsOp>,
22+
bindings: MapBindings<Value>,
23+
ctx_vals: &[(String, &(dyn Any))],
24+
) -> Result<Value, EvalErr> {
25+
let mut planner =
26+
partiql_eval::plan::EvaluatorPlanner::new(EvaluationMode::Strict, catalog);
27+
28+
let mut plan = planner.compile(&logical).expect("Expect no plan error");
29+
30+
let sys = SystemContext {
31+
now: DateTime::from_system_now_utc(),
32+
};
33+
let mut ctx = BasicContext::new(bindings, sys);
34+
for (k, v) in ctx_vals {
35+
ctx.user.insert(k.as_str().into(), *v);
36+
}
37+
plan.execute_mut(&ctx).map(|out| out.result)
38+
}
39+
40+
#[test]
41+
fn locals_in_subqueries() {
42+
// `SELECT VALUE _1 from (SELECT VALUE foo from <<{'a': 'b'}>> AS foo) AS _1;`
43+
let mut sub_query = partiql_logical::LogicalPlan::new();
44+
let scan_op_id =
45+
sub_query.add_operator(partiql_logical::BindingsOp::Scan(partiql_logical::Scan {
46+
expr: partiql_logical::ValueExpr::Lit(Box::new(Value::Bag(Box::new(Bag::from(
47+
vec![tuple![("a", "b")].into()],
48+
))))),
49+
as_key: "foo".into(),
50+
at_key: None,
51+
}));
52+
let project_value_op_id = sub_query.add_operator(
53+
partiql_logical::BindingsOp::ProjectValue(partiql_logical::ProjectValue {
54+
expr: partiql_logical::ValueExpr::VarRef(
55+
BindingsName::CaseSensitive("foo".into()),
56+
VarRefType::Local,
57+
),
58+
}),
59+
);
60+
sub_query.add_flow(scan_op_id, project_value_op_id);
61+
62+
let sink_op_id = sub_query.add_operator(partiql_logical::BindingsOp::Sink);
63+
sub_query.add_flow(project_value_op_id, sink_op_id);
64+
65+
let mut plan = LogicalPlan::new();
66+
let scan_op_id =
67+
plan.add_operator(partiql_logical::BindingsOp::Scan(partiql_logical::Scan {
68+
expr: partiql_logical::ValueExpr::SubQueryExpr(partiql_logical::SubQueryExpr {
69+
plan: sub_query,
70+
}),
71+
as_key: "_1".into(),
72+
at_key: None,
73+
}));
74+
75+
let project_value_op_id =
76+
plan.add_operator(partiql_logical::BindingsOp::ProjectValue(ProjectValue {
77+
expr: partiql_logical::ValueExpr::VarRef(
78+
BindingsName::CaseSensitive("_1".into()),
79+
VarRefType::Local,
80+
),
81+
}));
82+
plan.add_flow(scan_op_id, project_value_op_id);
83+
84+
let sink_op_id = plan.add_operator(partiql_logical::BindingsOp::Sink);
85+
plan.add_flow(project_value_op_id, sink_op_id);
86+
87+
let catalog = PartiqlCatalog::default();
88+
let bindings = MapBindings::default();
89+
let res = evaluate(&catalog, plan, bindings, &[]).expect("should eval correctly");
90+
dbg!(&res);
91+
assert!(res != Value::Missing);
92+
assert_eq!(res, Value::from(bag![tuple![("a", "b")]]))
93+
}
94+
95+
#[test]
96+
fn globals_in_subqueries() {
97+
// `foo` is defined in global environment as `<<{'a': 'b'}>>`
98+
// `SELECT VALUE _1 FROM (SELECT VALUE foo FROM foo AS foo) AS _1;`
99+
let mut sub_query = partiql_logical::LogicalPlan::new();
100+
let scan_op_id =
101+
sub_query.add_operator(partiql_logical::BindingsOp::Scan(partiql_logical::Scan {
102+
expr: partiql_logical::ValueExpr::VarRef(
103+
BindingsName::CaseSensitive("foo".into()),
104+
VarRefType::Global,
105+
),
106+
as_key: "foo".into(),
107+
at_key: None,
108+
}));
109+
let project_value_op_id = sub_query.add_operator(
110+
partiql_logical::BindingsOp::ProjectValue(partiql_logical::ProjectValue {
111+
expr: partiql_logical::ValueExpr::VarRef(
112+
BindingsName::CaseSensitive("foo".into()),
113+
VarRefType::Local,
114+
),
115+
}),
116+
);
117+
sub_query.add_flow(scan_op_id, project_value_op_id);
118+
119+
let sink_op_id = sub_query.add_operator(partiql_logical::BindingsOp::Sink);
120+
sub_query.add_flow(project_value_op_id, sink_op_id);
121+
122+
let mut plan = LogicalPlan::new();
123+
let scan_op_id =
124+
plan.add_operator(partiql_logical::BindingsOp::Scan(partiql_logical::Scan {
125+
expr: partiql_logical::ValueExpr::SubQueryExpr(partiql_logical::SubQueryExpr {
126+
plan: sub_query,
127+
}),
128+
as_key: "_1".into(),
129+
at_key: None,
130+
}));
131+
132+
let project_value_op_id =
133+
plan.add_operator(partiql_logical::BindingsOp::ProjectValue(ProjectValue {
134+
expr: partiql_logical::ValueExpr::VarRef(
135+
BindingsName::CaseSensitive("_1".into()),
136+
VarRefType::Local,
137+
),
138+
}));
139+
plan.add_flow(scan_op_id, project_value_op_id);
140+
141+
let sink_op_id = plan.add_operator(partiql_logical::BindingsOp::Sink);
142+
plan.add_flow(project_value_op_id, sink_op_id);
143+
144+
let catalog = PartiqlCatalog::default();
145+
let mut bindings = MapBindings::default();
146+
bindings.insert(
147+
"foo",
148+
Value::Bag(Box::new(Bag::from(vec![tuple![("a", "b")].into()]))),
149+
);
150+
let res = evaluate(&catalog, plan, bindings, &[]).expect("should eval correctly");
151+
dbg!(&res);
152+
assert!(res != Value::Missing);
153+
assert_eq!(res, Value::from(bag![tuple![("a", "b")]]))
154+
}
155+
}

0 commit comments

Comments
 (0)