Skip to content

Commit b493f22

Browse files
authored
Add initial GROUP BY implementation (#301)
1 parent 993348b commit b493f22

File tree

9 files changed

+285
-24
lines changed

9 files changed

+285
-24
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- `serde` feature to `partiql-value` and `partiql-logical` with `Serialize` and `Deserialize` traits.
1717
- Adds `Display` for `LogicalPlan`
1818
- Expose `partiql_value::parse_ion` as a public API.
19+
- Implements `GROUP BY` operator in evaluator
20+
- Implements `HAVING` operator in evaluator
1921

2022
### Fixes
2123
- Fixes Tuple value duplicate equality and hashing

partiql-eval/src/eval/evaluable.rs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,77 @@ impl Evaluable for EvalJoin {
282282
}
283283
}
284284

285+
/// Represents an evaluation `GROUP BY` operator. For `GROUP BY` operational semantics, see section
286+
/// `11` of
287+
/// [PartiQL Specification — August 1, 2019](https://partiql.org/assets/PartiQL-Specification.pdf).
288+
#[derive(Debug)]
289+
pub struct EvalGroupBy {
290+
pub strategy: EvalGroupingStrategy,
291+
pub exprs: HashMap<String, Box<dyn EvalExpr>>,
292+
pub group_as_alias: Option<String>,
293+
pub input: Option<Value>,
294+
}
295+
296+
/// Represents the grouping qualifier: ALL or PARTIAL.
297+
#[derive(Debug)]
298+
pub enum EvalGroupingStrategy {
299+
GroupFull,
300+
GroupPartial,
301+
}
302+
303+
impl EvalGroupBy {
304+
#[inline]
305+
fn eval_group(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> Tuple {
306+
self.exprs
307+
.iter()
308+
.map(
309+
|(alias, expr)| match expr.evaluate(bindings, ctx).into_owned() {
310+
Missing => (alias.as_str(), Value::Null),
311+
val => (alias.as_str(), val),
312+
},
313+
)
314+
.collect::<Tuple>()
315+
}
316+
}
317+
318+
impl Evaluable for EvalGroupBy {
319+
fn evaluate(&mut self, ctx: &dyn EvalContext) -> Option<Value> {
320+
let group_as_alias = &self.group_as_alias;
321+
let input_value = self.input.take().expect("Error in retrieving input value");
322+
323+
match self.strategy {
324+
EvalGroupingStrategy::GroupPartial => todo!(),
325+
EvalGroupingStrategy::GroupFull => {
326+
let mut groups: HashMap<Tuple, Vec<Value>> = HashMap::new();
327+
for v in input_value.into_iter() {
328+
let v_as_tuple = v.coerce_to_tuple();
329+
groups
330+
.entry(self.eval_group(&v_as_tuple, ctx))
331+
.or_insert(vec![])
332+
.push(Value::Tuple(Box::new(v_as_tuple)));
333+
}
334+
335+
let bag = groups
336+
.into_iter()
337+
.map(|(k, v)| match group_as_alias {
338+
None => Value::from(k), // TODO: removing the values here will be insufficient for when aggregations are added since they may have nothing to aggregate over
339+
Some(alias) => {
340+
let mut tuple_with_group = k;
341+
tuple_with_group.insert(alias, Value::Bag(Box::new(Bag::from(v))));
342+
Value::from(tuple_with_group)
343+
}
344+
})
345+
.collect::<Bag>();
346+
Some(Value::from(bag))
347+
}
348+
}
349+
}
350+
351+
fn update_input(&mut self, input: Value, _branch_num: u8) {
352+
self.input = Some(input);
353+
}
354+
}
355+
285356
/// Represents an evaluation `Pivot` operator; the `Pivot` enables turning a collection into a
286357
/// tuple. For `Pivot` operational semantics, see section `6.2` of
287358
/// [PartiQL Specification — August 1, 2019](https://partiql.org/assets/PartiQL-Specification.pdf).
@@ -410,7 +481,7 @@ impl EvalFilter {
410481
Boolean(bool_val) => *bool_val,
411482
// Alike SQL, when the expression of the WHERE clause expression evaluates to
412483
// absent value or a value that is not a Boolean, PartiQL eliminates the corresponding
413-
// binding. PartiQL Specification August 1, August 1, 2019 Draft, Section 8. `WHERE clause`
484+
// binding. PartiQL Specification August 1, 2019 Draft, Section 8. `WHERE clause`
414485
_ => false,
415486
}
416487
}
@@ -432,6 +503,51 @@ impl Evaluable for EvalFilter {
432503
}
433504
}
434505

506+
/// Represents an evaluation `Having` operator; for an input bag of binding tuples the `Having`
507+
/// operator filters out the binding tuples that does not meet the condition expressed as `expr`,
508+
/// e.g. `a = 10` in `HAVING a = 10` expression.
509+
#[derive(Debug)]
510+
pub struct EvalHaving {
511+
pub expr: Box<dyn EvalExpr>,
512+
pub input: Option<Value>,
513+
}
514+
515+
impl EvalHaving {
516+
pub fn new(expr: Box<dyn EvalExpr>) -> Self {
517+
EvalHaving { expr, input: None }
518+
}
519+
520+
#[inline]
521+
fn eval_having(&self, bindings: &Tuple, ctx: &dyn EvalContext) -> bool {
522+
let result = self.expr.evaluate(bindings, ctx);
523+
match result.as_ref() {
524+
Boolean(bool_val) => *bool_val,
525+
// Alike SQL, when the expression of the HAVING clause expression evaluates to
526+
// absent value or a value that is not a Boolean, PartiQL eliminates the corresponding
527+
// binding. PartiQL Specification August 1, 2019 Draft, Section 11.1.
528+
// > HAVING behaves identical to a WHERE, once groups are already formulated earlier
529+
// See Section 8 on WHERE semantics
530+
_ => false,
531+
}
532+
}
533+
}
534+
535+
impl Evaluable for EvalHaving {
536+
fn evaluate(&mut self, ctx: &dyn EvalContext) -> Option<Value> {
537+
let input_value = self.input.take().expect("Error in retrieving input value");
538+
539+
let filtered = input_value
540+
.into_iter()
541+
.map(Value::coerce_to_tuple)
542+
.filter_map(|v| self.eval_having(&v, ctx).then_some(v));
543+
Some(Value::from(filtered.collect::<Bag>()))
544+
}
545+
546+
fn update_input(&mut self, input: Value, _branch_num: u8) {
547+
self.input = Some(input);
548+
}
549+
}
550+
435551
/// Represents an evaluation `LIMIT` and/or `OFFSET` operator.
436552
#[derive(Debug)]
437553
pub struct EvalLimitOffset {

partiql-eval/src/plan.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ use std::collections::HashMap;
55
use partiql_logical as logical;
66

77
use partiql_logical::{
8-
BinaryOp, BindingsOp, CallName, IsTypeExpr, JoinKind, LogicalPlan, OpId, PathComponent,
9-
Pattern, PatternMatchExpr, SearchedCase, Type, UnaryOp, ValueExpr,
8+
BinaryOp, BindingsOp, CallName, GroupingStrategy, IsTypeExpr, JoinKind, LogicalPlan, OpId,
9+
PathComponent, Pattern, PatternMatchExpr, SearchedCase, Type, UnaryOp, ValueExpr,
1010
};
1111

1212
use crate::eval;
13-
use crate::eval::evaluable::{EvalJoinKind, EvalSubQueryExpr, Evaluable};
13+
use crate::eval::evaluable::{EvalGroupingStrategy, EvalJoinKind, EvalSubQueryExpr, Evaluable};
1414
use crate::eval::expr::pattern_match::like_to_re_pattern;
1515
use crate::eval::expr::{
1616
EvalBagExpr, EvalBetweenExpr, EvalBinOp, EvalBinOpExpr, EvalDynamicLookup, EvalExpr, EvalFnAbs,
@@ -91,6 +91,10 @@ impl EvaluatorPlanner {
9191
expr: self.plan_values(expr),
9292
input: None,
9393
}),
94+
BindingsOp::Having(logical::Having { expr }) => Box::new(eval::evaluable::EvalHaving {
95+
expr: self.plan_values(expr),
96+
input: None,
97+
}),
9498
BindingsOp::Distinct => Box::new(eval::evaluable::EvalDistinct::new()),
9599
BindingsOp::Sink => Box::new(eval::evaluable::EvalSink { input: None }),
96100
BindingsOp::Pivot(logical::Pivot { key, value }) => Box::new(
@@ -129,6 +133,27 @@ impl EvaluatorPlanner {
129133
on,
130134
))
131135
}
136+
BindingsOp::GroupBy(logical::GroupBy {
137+
strategy,
138+
exprs,
139+
group_as_alias,
140+
}) => {
141+
let strategy = match strategy {
142+
GroupingStrategy::GroupFull => EvalGroupingStrategy::GroupFull,
143+
GroupingStrategy::GroupPartial => EvalGroupingStrategy::GroupPartial,
144+
};
145+
let exprs: HashMap<_, _> = exprs
146+
.iter()
147+
.map(|(k, v)| (k.clone(), self.plan_values(v)))
148+
.collect();
149+
let group_as_alias = group_as_alias.as_ref().map(|alias| alias.to_string());
150+
Box::new(eval::evaluable::EvalGroupBy {
151+
strategy,
152+
exprs,
153+
group_as_alias,
154+
input: None,
155+
})
156+
}
132157
BindingsOp::ExprQuery(logical::ExprQuery { expr }) => {
133158
let expr = self.plan_values(expr);
134159
Box::new(eval::evaluable::EvalExprQuery::new(expr))
@@ -143,7 +168,6 @@ impl EvaluatorPlanner {
143168
}
144169

145170
BindingsOp::SetOp => todo!("SetOp"),
146-
BindingsOp::GroupBy => todo!("GroupBy"),
147171
}
148172
}
149173

partiql-logical-planner/src/lower.rs

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ use partiql_ast::ast;
66
use partiql_ast::ast::{
77
Assignment, Bag, Between, BinOp, BinOpKind, Call, CallAgg, CallArg, CallArgNamed,
88
CaseSensitivity, CreateIndex, CreateTable, Ddl, DdlOp, Delete, Dml, DmlOp, DropIndex,
9-
DropTable, FromClause, FromLet, FromLetKind, GroupByExpr, Insert, InsertValue, Item, Join,
10-
JoinKind, JoinSpec, Like, List, Lit, NodeId, OnConflict, OrderByExpr, Path, PathStep,
11-
ProjectExpr, Projection, ProjectionKind, Query, QuerySet, Remove, SearchedCase, Select, Set,
12-
SetExpr, SetQuantifier, Sexp, SimpleCase, Struct, SymbolPrimitive, UniOp, UniOpKind, VarRef,
9+
DropTable, FromClause, FromLet, FromLetKind, GroupByExpr, GroupKey, GroupingStrategy, Insert,
10+
InsertValue, Item, Join, JoinKind, JoinSpec, Like, List, Lit, NodeId, OnConflict, OrderByExpr,
11+
Path, PathStep, ProjectExpr, Projection, ProjectionKind, Query, QuerySet, Remove, SearchedCase,
12+
Select, Set, SetExpr, SetQuantifier, Sexp, SimpleCase, Struct, SymbolPrimitive, UniOp,
13+
UniOpKind, VarRef,
1314
};
1415
use partiql_ast::visit::{Visit, Visitor};
1516
use partiql_logical as logical;
@@ -1087,17 +1088,71 @@ impl<'ast> Visitor<'ast> for AstToLogical {
10871088
}
10881089

10891090
fn exit_having_clause(&mut self, _having_clause: &'ast ast::HavingClause) {
1090-
let _env = self.exit_env();
1091-
todo!("having clause");
1091+
let mut env = self.exit_env();
1092+
assert_eq!(env.len(), 1);
1093+
1094+
let having = BindingsOp::Having(logical::Having {
1095+
expr: env.pop().unwrap(),
1096+
});
1097+
let id = self.plan.add_operator(having);
1098+
1099+
self.current_clauses_mut().having_clause.replace(id);
10921100
}
10931101

10941102
fn enter_group_by_expr(&mut self, _group_by_expr: &'ast GroupByExpr) {
1103+
self.enter_benv();
10951104
self.enter_env();
10961105
}
10971106

10981107
fn exit_group_by_expr(&mut self, _group_by_expr: &'ast GroupByExpr) {
1099-
let _env = self.exit_env();
1100-
todo!("group by clause");
1108+
let benv = self.exit_benv();
1109+
assert_eq!(benv.len(), 0); // TODO sub-query
1110+
let env = self.exit_env();
1111+
1112+
let group_as_alias = _group_by_expr
1113+
.group_as_alias
1114+
.as_ref()
1115+
.map(|SymbolPrimitive { value, case: _ }| value.clone());
1116+
1117+
let strategy = match _group_by_expr.strategy {
1118+
GroupingStrategy::GroupFull => logical::GroupingStrategy::GroupFull,
1119+
GroupingStrategy::GroupPartial => logical::GroupingStrategy::GroupPartial,
1120+
};
1121+
let mut exprs = HashMap::with_capacity(env.len() / 2);
1122+
let mut iter = env.into_iter();
1123+
while let Some(value) = iter.next() {
1124+
let alias = iter.next().unwrap();
1125+
let alias = match alias {
1126+
ValueExpr::Lit(lit) => match *lit {
1127+
Value::String(s) => (*s).clone(),
1128+
_ => panic!("unexpected literal"),
1129+
},
1130+
_ => panic!("unexpected alias type"),
1131+
};
1132+
exprs.insert(alias, value);
1133+
}
1134+
let group_by: BindingsOp = BindingsOp::GroupBy(logical::GroupBy {
1135+
strategy,
1136+
exprs,
1137+
group_as_alias,
1138+
});
1139+
1140+
let id = self.plan.add_operator(group_by);
1141+
self.current_clauses_mut().group_by_clause.replace(id);
1142+
}
1143+
1144+
fn exit_group_key(&mut self, _group_key: &'ast GroupKey) {
1145+
let as_key: &name_resolver::Symbol = self
1146+
.key_registry
1147+
.aliases
1148+
.get(self.current_node())
1149+
.expect("alias");
1150+
// TODO intern strings
1151+
let as_key = match as_key {
1152+
name_resolver::Symbol::Known(sym) => sym.value.clone(),
1153+
name_resolver::Symbol::Unknown(id) => format!("_{id}"),
1154+
};
1155+
self.push_value(as_key.into());
11011156
}
11021157

11031158
fn enter_order_by_expr(&mut self, _order_by_expr: &'ast OrderByExpr) {

partiql-logical-planner/src/name_resolver.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use fnv::FnvBuildHasher;
22
use indexmap::{IndexMap, IndexSet};
33
use partiql_ast::ast;
4+
use partiql_ast::ast::{GroupByExpr, GroupKey};
45
use partiql_ast::visit::{Visit, Visitor};
56
use std::sync::atomic::{AtomicU32, Ordering};
67

@@ -327,6 +328,36 @@ impl<'ast> Visitor<'ast> for NameResolver {
327328
.produce_required
328329
.insert(as_alias);
329330
}
331+
332+
fn exit_group_key(&mut self, group_key: &'ast GroupKey) {
333+
let id = *self.current_node();
334+
// get the "as" alias for each `GROUP BY` expr
335+
// 1. if explicitly given
336+
// 2. else try to infer if a simple variable reference or path
337+
// 3. else it is currently 'Unknown'
338+
let as_alias = if let Some(sym) = &group_key.as_alias {
339+
Symbol::Known(sym.clone())
340+
} else if let Some(sym) = infer_alias(&group_key.expr) {
341+
Symbol::Known(sym)
342+
} else {
343+
Symbol::Unknown(self.id_gen.next_id())
344+
};
345+
self.aliases.insert(id, as_alias.clone());
346+
self.keyref_stack
347+
.last_mut()
348+
.unwrap()
349+
.produce_required
350+
.insert(as_alias);
351+
}
352+
353+
fn exit_group_by_expr(&mut self, group_by_expr: &'ast GroupByExpr) {
354+
// add the `GROUP AS` alias
355+
if let Some(sym) = &group_by_expr.group_as_alias {
356+
let id = *self.current_node();
357+
let as_alias = Symbol::Known(sym.clone());
358+
self.aliases.insert(id, as_alias);
359+
}
360+
}
330361
}
331362

332363
/// Attempt to infer an alias for a simple variable reference expression.

0 commit comments

Comments
 (0)