Skip to content

Commit

Permalink
Implement SLQ having() (#339)
Browse files Browse the repository at this point in the history
* Implemented SLQ having()
  • Loading branch information
neilotoole authored Nov 22, 2023
1 parent b8cee88 commit f0d83cd
Show file tree
Hide file tree
Showing 26 changed files with 1,268 additions and 751 deletions.
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Breaking changes are annotated with ☢️, and alpha/beta features with 🐥.

## Upcoming

### Added

- [#338]: While `sq` has had [`group_by`](https://sq.io/docs/query#group_by) for some time,
somehow the [`having`](https://sq.io/docs/query#having) mechanism was never implemented. That's fixed.

```shell
$ sq '.payment | .customer_id, sum(.amount):spend |
group_by(.customer_id) | having(sum(.amount) > 200)'
customer_id spend
526 221.55
148 216.54
```


## [v0.45.0] - 2023-11-21

### Changed
Expand Down Expand Up @@ -343,7 +359,7 @@ to SLQ (`sq`'s query language).
3
```
You may want to use `--no-header` (`-H`) when using `sq` as a calculator.
```shell
$ sq -H 1+2
3
Expand Down Expand Up @@ -917,6 +933,7 @@ make working with lots of sources much easier.
[#279]: https://github.com/neilotoole/sq/issues/279
[#308]: https://github.com/neilotoole/sq/pull/308
[#335]: https://github.com/neilotoole/sq/issues/335
[#338]: https://github.com/neilotoole/sq/issues/338
[v0.15.2]: https://github.com/neilotoole/sq/releases/tag/v0.15.2
Expand Down
17 changes: 16 additions & 1 deletion grammar/SLQ.g4
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ element
| selectorElement
| join
| groupBy
| having
| orderBy
| rowRange
| uniqueFunc
Expand Down Expand Up @@ -150,13 +151,27 @@ The 'group_by' construct implments the SQL "GROUP BY" clause.
Syonyms:
- 'group_by' for jq interoperability.
https://stedolan.github.io/jq/manual/v1.6/#group_by(path_expression)
- 'group': for legacy sq compabibility. Should this be deprecated and removed?
*/

GROUP_BY: 'group_by';
groupByTerm: selector | func;
groupBy: GROUP_BY '(' groupByTerm (',' groupByTerm)* ')';


/*
having
------
The 'having' construct implements the SQL "HAVING" clause.
It is a top-level segment clause, and must be preceded by a 'group_by' clause.
.payment | .customer_id, sum(.amount) |
group_by(.customer_id) | having(sum(.amount) > 100)
*/

HAVING: 'having';
having: HAVING '(' expr ')';

/*
order_by
------
Expand Down
5 changes: 5 additions & 0 deletions libsq/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ type AST struct {
text string
}

// ast implements ast.Node.
func (a *AST) ast() *AST {
return a
}

// Parent implements ast.Node.
func (a *AST) Parent() Node {
return nil
Expand Down
71 changes: 69 additions & 2 deletions libsq/ast/groupby.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ var groupByAllowedChildren = []reflect.Type{
typeFuncNode,
}

var _ Node = (*GroupByNode)(nil)

// GroupByNode models GROUP BY. The children of GroupBy node can be
// of type selector or FuncNode.
type GroupByNode struct {
Expand Down Expand Up @@ -47,6 +49,9 @@ func (n *GroupByNode) String() string {

// VisitGroupBy implements slq.SLQVisitor.
func (v *parseTreeVisitor) VisitGroupBy(ctx *slq.GroupByContext) any {
if existing := FindNodes[*GroupByNode](v.cur.ast()); len(existing) > 0 {
return errorf("only one group_by() clause allowed")
}
node := &GroupByNode{}
node.ctx = ctx
node.text = ctx.GetText()
Expand All @@ -55,12 +60,74 @@ func (v *parseTreeVisitor) VisitGroupBy(ctx *slq.GroupByContext) any {
}

return v.using(node, func() any {
// This will result in VisitOrderByTerm being called on the children.
return v.VisitChildren(ctx)
})
}

// VisitGroupByTerm implements slq.SLQVisitor.
func (v *parseTreeVisitor) VisitGroupByTerm(ctx *slq.GroupByTermContext) interface{} {
func (v *parseTreeVisitor) VisitGroupByTerm(ctx *slq.GroupByTermContext) any {
return v.VisitChildren(ctx)
}

var _ Node = (*HavingNode)(nil)

// HavingNode models the HAVING clause. It must always be preceded
// by a GROUP BY clause.
type HavingNode struct {
baseNode
}

// VisitHaving implements slq.SLQVisitor.
func (v *parseTreeVisitor) VisitHaving(ctx *slq.HavingContext) any {
if existing := FindNodes[*HavingNode](v.cur.ast()); len(existing) > 0 {
return errorf("only one having() clause allowed")
}

// Check that the preceding node is a GroupByNode.
if _, err := NodePrevSegmentChild[*GroupByNode](v.cur); err != nil {
return err
}

node := &HavingNode{}
node.ctx = ctx
node.text = ctx.GetText()
if err := v.cur.AddChild(node); err != nil {
return err
}

return v.using(node, func() any {
return v.VisitChildren(ctx)
})
}

// AddChild implements Node.
func (n *HavingNode) AddChild(child Node) error {
if len(n.children) > 0 {
return errorf("having() clause can only have one child")
}
if err := nodesAreOnlyOfType([]Node{child}, typeExprNode); err != nil {
return err
}

n.addChild(child)
return child.SetParent(n)
}

// SetChildren implements ast.Node.
func (n *HavingNode) SetChildren(children []Node) error {
if len(children) > 1 {
return errorf("having() clause can only have one child")
}
if err := nodesAreOnlyOfType(children, typeExprNode); err != nil {
return err
}

n.doSetChildren(children)
return nil
}

// String returns a log/debug-friendly representation.
func (n *HavingNode) String() string {
text := nodeString(n)
return text
}
24 changes: 24 additions & 0 deletions libsq/ast/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,30 @@ func (in *Inspector) FindGroupByNode() (*GroupByNode, error) {
return nil, nil //nolint:nilnil
}

// FindHavingNode returns the HavingNode, or nil if not found.
func (in *Inspector) FindHavingNode() (*HavingNode, error) {
segs := in.ast.Segments()

for i := range segs {
nodes := nodesWithType(segs[i].Children(), typeHavingNode)
switch len(nodes) {
case 0:
// No GroupByNode in this segment, continue searching.
continue
case 1:
// Found it
node, _ := nodes[0].(*HavingNode)
return node, nil
default:
// Shouldn't be possible
return nil, errorf("segment {%s} has %d HavingNode children, but max is 1",
segs[i], len(nodes))
}
}

return nil, nil //nolint:nilnil
}

// FindTableSegments returns the segments that have at least one child
// that is a ast.TblSelectorNode.
func (in *Inspector) FindTableSegments() []*SegmentNode {
Expand Down
5 changes: 4 additions & 1 deletion libsq/ast/internal/slq/SLQ.interp

Large diffs are not rendered by default.

80 changes: 41 additions & 39 deletions libsq/ast/internal/slq/SLQ.tokens
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,32 @@ PROPRIETARY_FUNC_NAME=25
JOIN_TYPE=26
WHERE=27
GROUP_BY=28
ORDER_BY=29
ALIAS_RESERVED=30
ARG=31
NULL=32
ID=33
WS=34
LPAR=35
RPAR=36
LBRA=37
RBRA=38
COMMA=39
PIPE=40
COLON=41
NN=42
NUMBER=43
LT_EQ=44
LT=45
GT_EQ=46
GT=47
NEQ=48
EQ=49
NAME=50
HANDLE=51
STRING=52
LINECOMMENT=53
HAVING=29
ORDER_BY=30
ALIAS_RESERVED=31
ARG=32
NULL=33
ID=34
WS=35
LPAR=36
RPAR=37
LBRA=38
RBRA=39
COMMA=40
PIPE=41
COLON=42
NN=43
NUMBER=44
LT_EQ=45
LT=46
GT_EQ=47
GT=48
NEQ=49
EQ=50
NAME=51
HANDLE=52
STRING=53
LINECOMMENT=54
';'=1
'*'=2
'sum'=3
Expand All @@ -76,17 +77,18 @@ LINECOMMENT=53
'~'=23
'!'=24
'group_by'=28
'null'=32
'('=35
')'=36
'['=37
']'=38
','=39
'|'=40
':'=41
'<='=44
'<'=45
'>='=46
'>'=47
'!='=48
'=='=49
'having'=29
'null'=33
'('=36
')'=37
'['=38
']'=39
','=40
'|'=41
':'=42
'<='=45
'<'=46
'>='=47
'>'=48
'!='=49
'=='=50
5 changes: 4 additions & 1 deletion libsq/ast/internal/slq/SLQLexer.interp

Large diffs are not rendered by default.

80 changes: 41 additions & 39 deletions libsq/ast/internal/slq/SLQLexer.tokens
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,32 @@ PROPRIETARY_FUNC_NAME=25
JOIN_TYPE=26
WHERE=27
GROUP_BY=28
ORDER_BY=29
ALIAS_RESERVED=30
ARG=31
NULL=32
ID=33
WS=34
LPAR=35
RPAR=36
LBRA=37
RBRA=38
COMMA=39
PIPE=40
COLON=41
NN=42
NUMBER=43
LT_EQ=44
LT=45
GT_EQ=46
GT=47
NEQ=48
EQ=49
NAME=50
HANDLE=51
STRING=52
LINECOMMENT=53
HAVING=29
ORDER_BY=30
ALIAS_RESERVED=31
ARG=32
NULL=33
ID=34
WS=35
LPAR=36
RPAR=37
LBRA=38
RBRA=39
COMMA=40
PIPE=41
COLON=42
NN=43
NUMBER=44
LT_EQ=45
LT=46
GT_EQ=47
GT=48
NEQ=49
EQ=50
NAME=51
HANDLE=52
STRING=53
LINECOMMENT=54
';'=1
'*'=2
'sum'=3
Expand All @@ -76,17 +77,18 @@ LINECOMMENT=53
'~'=23
'!'=24
'group_by'=28
'null'=32
'('=35
')'=36
'['=37
']'=38
','=39
'|'=40
':'=41
'<='=44
'<'=45
'>='=46
'>'=47
'!='=48
'=='=49
'having'=29
'null'=33
'('=36
')'=37
'['=38
']'=39
','=40
'|'=41
':'=42
'<='=45
'<'=46
'>='=47
'>'=48
'!='=49
'=='=50
Loading

0 comments on commit f0d83cd

Please sign in to comment.