diff --git a/crm/repository/ql/ast_nodes.go b/crm/repository/ql/ast_nodes.go index 62e8b4eb9..6988e0ca5 100644 --- a/crm/repository/ql/ast_nodes.go +++ b/crm/repository/ql/ast_nodes.go @@ -40,6 +40,11 @@ type ( Keyword string } + Interval struct { + Value string + Unit string + } + Column struct { Expr ASTNodes Alias string @@ -58,6 +63,8 @@ func (n Operator) String() string { return n.Kind } func (n Keyword) String() string { return n.Keyword } +func (n Interval) String() string { return fmt.Sprintf("INTERVAL %s %s", n.Value, n.Unit) } + func (n Function) String() string { return fmt.Sprintf("%s(%s)", n.Name, n.Arguments) } func (n Ident) String() string { return n.Value } diff --git a/crm/repository/ql/ast_parser.go b/crm/repository/ql/ast_parser.go index 065926d25..f1233f2b9 100644 --- a/crm/repository/ql/ast_parser.go +++ b/crm/repository/ql/ast_parser.go @@ -176,7 +176,11 @@ checkToken: list = append(list, Operator{Kind: t.literal}) goto next case KEYWORD: - list = append(list, Keyword{Keyword: t.literal}) + if keyword, err := p.parseKeyword(t); err != nil { + return nil, err + } else { + list = append(list, keyword) + } goto next case NUMBER: list = append(list, Number{Value: t.literal}) @@ -239,6 +243,13 @@ next: case OPERATOR: list = append(list, Operator{Kind: t.literal}) goto next + case KEYWORD: + if keyword, err := p.parseKeyword(t); err != nil { + return nil, err + } else { + list = append(list, keyword) + } + goto next case IDENT: if p.peekToken(1).Is(OPERATOR) { // Looks like we have an expression ahead of us @@ -280,3 +291,33 @@ next: return nil, fmt.Errorf("unexpected token while parsing set (%v)", t) } } + +func (p *Parser) parseKeyword(t Token) (list ASTNode, err error) { + switch strings.ToUpper(t.literal) { + case "INTERVAL": + i := Interval{Value: p.nextToken().literal} + u := p.nextToken() + + if u.code != IDENT { + return nil, fmt.Errorf("expecting identifier, got %v", t) + } else { + switch strings.ToUpper(u.literal) { + case "MICROSECOND", "SECOND", "MINUTE", "HOUR", + "DAY", "WEEK", "MONTH", "QUARTER", "YEAR", + "SECOND_MICROSECOND", "MINUTE_MICROSECOND", "MINUTE_SECOND", "HOUR_MICROSECOND", "HOUR_SECOND", + "HOUR_MINUTE", "DAY_MICROSECOND", "DAY_SECOND", "DAY_MINUTE", "DAY_HOUR", "YEAR_MONTH": + // All good + break + default: + return nil, fmt.Errorf("expecting interval unit, got %v", u.literal) + } + } + + i.Unit = u.literal + + return i, nil + + default: + return Keyword{Keyword: t.literal}, nil + } +} diff --git a/crm/repository/ql/ast_parser_test.go b/crm/repository/ql/ast_parser_test.go index 9cfb644e3..8f3d0eac7 100644 --- a/crm/repository/ql/ast_parser_test.go +++ b/crm/repository/ql/ast_parser_test.go @@ -129,6 +129,15 @@ func TestAstParser_Parser(t *testing.T) { Number{Value: "1"}, }, }, + { + parser: NewParser().ParseExpression, + in: `NOW() > DATE_SUB(col, INTERVAL 31 DAY)`, + tree: ASTNodes{ + Function{Name: "NOW"}, + Operator{Kind: ">"}, + Function{Name: "DATE_SUB", Arguments: ASTSet{Ident{Value: "col"}, Interval{Value: "31", Unit: "DAY"}}}, + }, + }, { parser: NewParser().ParseExpression, in: `foo LIKE 'bar%'`, diff --git a/crm/repository/ql/squirrel.go b/crm/repository/ql/squirrel.go index 446cca346..0ad8924ad 100644 --- a/crm/repository/ql/squirrel.go +++ b/crm/repository/ql/squirrel.go @@ -75,6 +75,10 @@ func (n Keyword) ToSql() (string, []interface{}, error) { return n.Keyword, nil, nil } +func (n Interval) ToSql() (string, []interface{}, error) { + return fmt.Sprintf("INTERVAL ? %s", n.Unit), []interface{}{n.Value}, nil +} + func (n Operator) ToSql() (string, []interface{}, error) { var op = n.Kind diff --git a/crm/repository/ql/token_consumers.go b/crm/repository/ql/token_consumers.go index 6a890574e..c48acc940 100644 --- a/crm/repository/ql/token_consumers.go +++ b/crm/repository/ql/token_consumers.go @@ -87,7 +87,7 @@ func (TokenConsumerIdent) Consume(s RuneReader) Token { switch lit { case "LIKE", "NOT", "AND", "OR", "XOR": return Token{code: OPERATOR, literal: lit} - case "DESC", "ASC": + case "DESC", "ASC", "INTERVAL": return Token{code: KEYWORD, literal: lit} } diff --git a/crm/repository/record_report_builder.go b/crm/repository/record_report_builder.go index 10bb532f9..51e91ae84 100644 --- a/crm/repository/record_report_builder.go +++ b/crm/repository/record_report_builder.go @@ -52,7 +52,17 @@ func stdGroupByFuncHandler(f ql.Function) (ql.Function, error) { default: return f, fmt.Errorf("unsupported group-by function %q", f.Name) } +} +// Identifiers should be names of the fields (physical table columns OR json fields, defined in module) +func stdFilterFuncHandler(f ql.Function) (ql.Function, error) { + switch strings.ToUpper(f.Name) { + case "CONCAT", "QUARTER", "YEAR", "DATE", "NOW", "DATE_ADD", "DATE_SUB", "DATE_FORMAT": + return f, nil + + default: + return f, fmt.Errorf("unsupported group-by function %q", f.Name) + } } func NewRecordReportBuilder(moduleID uint64) *recordReportBuilder { @@ -83,7 +93,7 @@ func (b *recordReportBuilder) SetFilter(filters string) (err error) { p := ql.NewParser() p.OnIdent = ql.MakeIdentWrapHandler(jsonWrap, "created_at", "updated_at", "id", "user_id") - p.OnFunction = stdGroupByFuncHandler + p.OnFunction = stdFilterFuncHandler b.filter, err = p.ParseExpression(filters) return