Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SLQ having() #339

Merged
merged 2 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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