diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b371347 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +/pkg/policy/parser/*.go -diff +/pkg/policy/parser/generate.go diff +**/*.interp -diff +**/*.tokens -diff diff --git a/go.mod b/go.mod index f7f1829..25c51c6 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/nspcc-dev/neofs-sdk-go go 1.16 require ( - github.com/alecthomas/participle v0.7.1 + github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521073959-f0d4d129b7f1 github.com/nspcc-dev/neofs-api-go v1.27.0 github.com/stretchr/testify v1.6.1 go.uber.org/zap v1.10.0 diff --git a/go.sum b/go.sum index 799b7f7..ed17a36 100644 --- a/go.sum +++ b/go.sum @@ -10,15 +10,14 @@ github.com/Workiva/go-datastructures v1.0.50/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3 github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs= -github.com/alecthomas/participle v0.7.1/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= -github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521073959-f0d4d129b7f1 h1:zFRi26YWd7NIorBXe8UkevRl0dIvk/AnXHWaAiZG+Yk= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521073959-f0d4d129b7f1/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= diff --git a/pkg/policy/doc.go b/pkg/policy/doc.go index e2f94bb..cceebca 100644 --- a/pkg/policy/doc.go +++ b/pkg/policy/doc.go @@ -1,20 +1,19 @@ // Package policy provides facilities for creating policy from SQL-like language. -// eBNF grammar is provided in `grammar.ebnf` for illustration. +// ANTLRv4 grammar is provided in `parser/Query.g4` and `parser/QueryLexer.g4`. // // Current limitations: -// 1. Grouping filter expressions in parenthesis is not supported right now. -// Requiring this will make query too verbose, making it optional makes -// our grammar not LL(1). This can be supported in future. -// 2. Filters must be defined before they are used. -// This requirement may be relaxed in future. +// 1. Filters must be defined before they are used. +// This requirement may be relaxed in future. +// 2. Keywords are key-sensitive. This can be changed if necessary +// https://github.com/antlr/antlr4/blob/master/doc/case-insensitive-lexing.md . // // Example query: -// REP 1 in SPB -// REP 2 in Americas +// REP 1 IN SPB +// REP 2 IN Americas // CBF 4 -// SELECT 1 Node IN City FROM SPBSSD AS SPB -// SELECT 2 Node IN SAME City FROM Americas AS Americas +// SELECT 1 IN City FROM SPBSSD AS SPB +// SELECT 2 IN SAME City FROM Americas AS Americas // FILTER SSD EQ true AS IsSSD -// FILTER @IsSSD AND Country eq "RU" AND City eq "St.Petersburg" AS SPBSSD -// FILTER 'Continent' == 'North America' OR Continent == 'South America' AS Americas +// FILTER @IsSSD AND Country EQ "RU" AND City EQ "St.Petersburg" AS SPBSSD +// FILTER 'Continent' EQ 'North America' OR Continent EQ 'South America' AS Americas package policy diff --git a/pkg/policy/encode_test.go b/pkg/policy/encode_test.go index 10de5fe..d2bb716 100644 --- a/pkg/policy/encode_test.go +++ b/pkg/policy/encode_test.go @@ -1,7 +1,6 @@ package policy_test import ( - "fmt" "strings" "testing" @@ -30,7 +29,6 @@ FILTER City EQ SPB AND SSD EQ true OR City EQ SPB AND Rating GE 5 AS SPBSSD`, require.NoError(t, err) got := policy.Encode(q) - fmt.Println(strings.Join(got, "\n")) require.Equal(t, testCase, strings.Join(got, "\n")) } } diff --git a/pkg/policy/grammar.ebnf b/pkg/policy/grammar.ebnf deleted file mode 100644 index 2bcd3a2..0000000 --- a/pkg/policy/grammar.ebnf +++ /dev/null @@ -1,55 +0,0 @@ -Policy ::= - RepStmt, [RepStmt], - CbtStmt?, - [SelectStmt], - [FilterStmt], -; - -RepStmt ::= - 'REP', Number1, (* number of object replicas *) - ('AS', Ident)? (* optional selector name *) -; - -CbtStmt ::= 'CBF', Number1 (* container backup factor *) -; - -SelectStmt ::= - 'SELECT', Number1, (* number of nodes to select without container backup factor *) - ('IN', Clause?, Ident)?, (* bucket name *) - FROM, (Ident | '*'), (* filter reference or whole netmap *) - ('AS', Ident)? (* optional selector name *) -; - -Clause ::= - 'SAME' (* nodes from the same bucket *) - | 'DISTINCT' (* nodes from distinct buckets *) -; - -FilterStmt ::= - 'FILTER', AndChain, ['OR', AndChain], - 'AS', Ident (* obligatory filter name *) -; - -AndChain ::= - Expr, ['AND', Expr] -; - -Expr ::= - '@' Ident (* filter reference *) - | Key, Op, Value (* attribute filter *) -; - -Op ::= 'EQ' | 'NE' | 'GE' | 'GT' | 'LT' | 'LE' -; - -Key ::= Ident | String -; - -Value ::= Ident | Number | String -; - -Number1 ::= Digit1 [Digit]; -Number ::= Digit [Digit]; - -Digit1 ::= '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ; -Digit ::= '0' | Digit1; diff --git a/pkg/policy/grammar.go b/pkg/policy/grammar.go deleted file mode 100644 index 6fd228e..0000000 --- a/pkg/policy/grammar.go +++ /dev/null @@ -1,60 +0,0 @@ -//nolint:govet,golint // fails on struct tags here, but participle needs this syntax -package policy - -import ( - "github.com/alecthomas/participle" -) - -var parser *participle.Parser - -func init() { - p, err := participle.Build(&query{}) - if err != nil { - panic(err) - } - parser = p -} - -type query struct { - Replicas []*replicaStmt `@@+` - CBF uint32 `("CBF" @Int)?` - Selectors []*selectorStmt `@@*` - Filters []*filterStmt `@@*` -} - -type replicaStmt struct { - Count int `"REP" @Int` - Selector string `("IN" @Ident)?` -} - -type selectorStmt struct { - Count uint32 `"SELECT" @Int` - Bucket []string `("IN" @(("SAME" | "DISTINCT")? Ident))?` - Filter string `"FROM" @(Ident | "*")` - Name string `("AS" @Ident)?` -} - -type filterStmt struct { - Value *orChain `"FILTER" @@` - Name string `"AS" @Ident` -} - -type filterOrExpr struct { - Reference string `"@"@Ident` - Expr *simpleExpr `| @@` -} - -type orChain struct { - Clauses []*andChain `@@ ("OR" @@)*` -} - -type andChain struct { - Clauses []*filterOrExpr `@@ ("AND" @@)*` -} - -type simpleExpr struct { - Key string `@(Ident | String)` - // We don't use literals here to improve error messages. - Op string `@Ident` - Value string `@(Ident | String | Int)` -} diff --git a/pkg/policy/parser/Query.g4 b/pkg/policy/parser/Query.g4 new file mode 100644 index 0000000..fc74c6f --- /dev/null +++ b/pkg/policy/parser/Query.g4 @@ -0,0 +1,46 @@ +parser grammar Query; + +options { + tokenVocab = QueryLexer; +} + +policy: repStmt+ cbfStmt? selectStmt* filterStmt*; + +repStmt: + REP Count = NUMBER1 // number of object replicas + (IN Selector = ident)?; // optional selector name + +cbfStmt: CBF BackupFactor = NUMBER1; // container backup factor + +selectStmt: + SELECT Count = NUMBER1 // number of nodes to select without container backup factor *) + (IN clause? Bucket = ident)? // bucket name + FROM Filter = identWC // filter reference or whole netmap + (AS Name = ident)? // optional selector name + ; + +clause: CLAUSE_SAME | CLAUSE_DISTINCT; // nodes from distinct buckets + +filterExpr: + F1 = filterExpr Op = AND_OP F2 = filterExpr + | F1 = filterExpr Op = OR_OP F2 = filterExpr + | '(' Inner = filterExpr ')' + | expr + ; + +filterStmt: + FILTER Expr = filterExpr + AS Name = ident // obligatory filter name + ; + +expr: + AT Filter = ident // reference to named filter + | Key = filterKey SIMPLE_OP Value = filterValue // attribute comparison + ; + +filterKey : ident | STRING; +filterValue : ident | number | STRING; +number : ZERO | NUMBER1; +keyword : REP | IN | AS | SELECT | FROM | FILTER; +ident : keyword | IDENT; +identWC : ident | WILDCARD; diff --git a/pkg/policy/parser/Query.interp b/pkg/policy/parser/Query.interp new file mode 100644 index 0000000..9512ecb Binary files /dev/null and b/pkg/policy/parser/Query.interp differ diff --git a/pkg/policy/parser/Query.tokens b/pkg/policy/parser/Query.tokens new file mode 100644 index 0000000..7f5aee9 Binary files /dev/null and b/pkg/policy/parser/Query.tokens differ diff --git a/pkg/policy/parser/QueryLexer.g4 b/pkg/policy/parser/QueryLexer.g4 new file mode 100644 index 0000000..6c245b6 --- /dev/null +++ b/pkg/policy/parser/QueryLexer.g4 @@ -0,0 +1,41 @@ +lexer grammar QueryLexer; + +AND_OP : 'AND'; +OR_OP : 'OR'; +SIMPLE_OP : 'EQ' | 'NE' | 'GE' | 'GT' | 'LT' | 'LE'; + +REP : 'REP'; +IN : 'IN'; +AS : 'AS'; +CBF : 'CBF'; +SELECT : 'SELECT'; +FROM : 'FROM'; +FILTER : 'FILTER'; +WILDCARD : '*'; + +CLAUSE_SAME : 'SAME'; +CLAUSE_DISTINCT : 'DISTINCT'; + +L_PAREN : '('; +R_PAREN : ')'; +AT : '@'; + +IDENT : Nondigit (Digit | Nondigit)* ; +fragment Digit : [0-9] ; +fragment Nondigit : [a-zA-Z_] ; + +NUMBER1 : [1-9] Digit* ; +ZERO : '0' ; + +// Taken from antlr4 json grammar with minor corrections. +// https://github.com/antlr/grammars-v4/blob/master/json/JSON.g4 +STRING : '"' (ESC | SAFECODEPOINTDOUBLE)* '"' + | '\'' (ESC | SAFECODEPOINTSINGLE)* '\'' ; + +fragment ESC : '\\' (['"\\/bfnrt] | UNICODE) ; +fragment UNICODE : 'u' HEX HEX HEX HEX ; +fragment HEX : [0-9a-fA-F] ; +fragment SAFECODEPOINTSINGLE : ~ ['\\\u0000-\u001F] ; +fragment SAFECODEPOINTDOUBLE : ~ ["\\\u0000-\u001F] ; + +WS : [ \t\n\r] + -> skip ; diff --git a/pkg/policy/parser/QueryLexer.interp b/pkg/policy/parser/QueryLexer.interp new file mode 100644 index 0000000..d453ff6 Binary files /dev/null and b/pkg/policy/parser/QueryLexer.interp differ diff --git a/pkg/policy/parser/QueryLexer.tokens b/pkg/policy/parser/QueryLexer.tokens new file mode 100644 index 0000000..7f5aee9 Binary files /dev/null and b/pkg/policy/parser/QueryLexer.tokens differ diff --git a/pkg/policy/parser/generate.go b/pkg/policy/parser/generate.go new file mode 100644 index 0000000..850f362 --- /dev/null +++ b/pkg/policy/parser/generate.go @@ -0,0 +1,3 @@ +package parser + +//go:generate antlr4 -Dlanguage=Go -visitor QueryLexer.g4 Query.g4 diff --git a/pkg/policy/parser/query_base_listener.go b/pkg/policy/parser/query_base_listener.go new file mode 100644 index 0000000..d872ae1 Binary files /dev/null and b/pkg/policy/parser/query_base_listener.go differ diff --git a/pkg/policy/parser/query_base_visitor.go b/pkg/policy/parser/query_base_visitor.go new file mode 100644 index 0000000..69c59f7 Binary files /dev/null and b/pkg/policy/parser/query_base_visitor.go differ diff --git a/pkg/policy/parser/query_lexer.go b/pkg/policy/parser/query_lexer.go new file mode 100644 index 0000000..05d8f21 Binary files /dev/null and b/pkg/policy/parser/query_lexer.go differ diff --git a/pkg/policy/parser/query_listener.go b/pkg/policy/parser/query_listener.go new file mode 100644 index 0000000..3e77af5 Binary files /dev/null and b/pkg/policy/parser/query_listener.go differ diff --git a/pkg/policy/parser/query_parser.go b/pkg/policy/parser/query_parser.go new file mode 100644 index 0000000..c78c0f4 Binary files /dev/null and b/pkg/policy/parser/query_parser.go differ diff --git a/pkg/policy/parser/query_visitor.go b/pkg/policy/parser/query_visitor.go new file mode 100644 index 0000000..b6128a5 Binary files /dev/null and b/pkg/policy/parser/query_visitor.go differ diff --git a/pkg/policy/query.go b/pkg/policy/query.go index 1b6447e..9bf08ac 100644 --- a/pkg/policy/query.go +++ b/pkg/policy/query.go @@ -3,9 +3,12 @@ package policy import ( "errors" "fmt" + "strconv" "strings" + "github.com/antlr/antlr4/runtime/Go/antlr" "github.com/nspcc-dev/neofs-api-go/pkg/netmap" + "github.com/nspcc-dev/neofs-sdk-go/pkg/policy/parser" ) var ( @@ -19,83 +22,256 @@ var ( ErrUnknownFilter = errors.New("policy: filter not found") // ErrUnknownSelector is returned when a value of IN is unknown. ErrUnknownSelector = errors.New("policy: selector not found") + // ErrSyntaxError is returned for errors found by ANTLR parser. + ErrSyntaxError = errors.New("policy: syntax error") ) -func parse(s string) (*query, error) { - q := new(query) - err := parser.Parse(strings.NewReader(s), q) - if err != nil { - return nil, err - } - return q, nil +type policyVisitor struct { + errors []error + parser.BaseQueryVisitor + antlr.DefaultErrorListener } // Parse parses s into a placement policy. func Parse(s string) (*netmap.PlacementPolicy, error) { - q, err := parse(s) - if err != nil { + return parse(s) +} + +func newPolicyVisitor() *policyVisitor { + return &policyVisitor{} +} + +func parse(s string) (*netmap.PlacementPolicy, error) { + input := antlr.NewInputStream(s) + lexer := parser.NewQueryLexer(input) + stream := antlr.NewCommonTokenStream(lexer, 0) + + p := parser.NewQuery(stream) + p.BuildParseTrees = true + + v := newPolicyVisitor() + p.RemoveErrorListeners() + p.AddErrorListener(v) + pl := p.Policy().Accept(v) + + if len(v.errors) != 0 { + return nil, v.errors[0] + } + if err := validatePolicy(pl.(*netmap.PlacementPolicy)); err != nil { return nil, err } + return pl.(*netmap.PlacementPolicy), nil +} - seenFilters := map[string]bool{} - fs := make([]*netmap.Filter, 0, len(q.Filters)) - for _, qf := range q.Filters { - f, err := filterFromOrChain(qf.Value, seenFilters) - if err != nil { - return nil, err +func (p *policyVisitor) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) { + p.reportError(fmt.Errorf("%w: line %d:%d %s", ErrSyntaxError, line, column, msg)) +} + +func (p *policyVisitor) reportError(err error) interface{} { + p.errors = append(p.errors, err) + return nil +} + +// VisitPolicy implements parser.QueryVisitor interface. +func (p *policyVisitor) VisitPolicy(ctx *parser.PolicyContext) interface{} { + if len(p.errors) != 0 { + return nil + } + + pl := new(netmap.PlacementPolicy) + + repStmts := ctx.AllRepStmt() + rs := make([]*netmap.Replica, 0, len(repStmts)) + for _, r := range repStmts { + res, ok := r.Accept(p).(*netmap.Replica) + if !ok { + return nil } - f.SetName(qf.Name) - fs = append(fs, f) - seenFilters[qf.Name] = true + + rs = append(rs, res) + } + pl.SetReplicas(rs...) + + if cbfStmt := ctx.CbfStmt(); cbfStmt != nil { + cbf, ok := cbfStmt.(*parser.CbfStmtContext).Accept(p).(uint32) + if !ok { + return nil + } + pl.SetContainerBackupFactor(cbf) + } + + selStmts := ctx.AllSelectStmt() + ss := make([]*netmap.Selector, 0, len(selStmts)) + for _, s := range selStmts { + res, ok := s.Accept(p).(*netmap.Selector) + if !ok { + return nil + } + + ss = append(ss, res) + } + pl.SetSelectors(ss...) + + filtStmts := ctx.AllFilterStmt() + fs := make([]*netmap.Filter, 0, len(filtStmts)) + for _, f := range filtStmts { + fs = append(fs, f.Accept(p).(*netmap.Filter)) + } + pl.SetFilters(fs...) + + return pl +} + +func (p *policyVisitor) VisitCbfStmt(ctx *parser.CbfStmtContext) interface{} { + cbf, err := strconv.ParseUint(ctx.GetBackupFactor().GetText(), 10, 32) + if err != nil { + return p.reportError(ErrInvalidNumber) + } + + return uint32(cbf) +} + +// VisitRepStmt implements parser.QueryVisitor interface. +func (p *policyVisitor) VisitRepStmt(ctx *parser.RepStmtContext) interface{} { + num, err := strconv.ParseUint(ctx.GetCount().GetText(), 10, 32) + if err != nil { + return p.reportError(ErrInvalidNumber) + } + + rs := new(netmap.Replica) + rs.SetCount(uint32(num)) + + if sel := ctx.GetSelector(); sel != nil { + rs.SetSelector(sel.GetText()) + } + + return rs +} + +// VisitSelectStmt implements parser.QueryVisitor interface. +func (p *policyVisitor) VisitSelectStmt(ctx *parser.SelectStmtContext) interface{} { + res, err := strconv.ParseUint(ctx.GetCount().GetText(), 10, 32) + if err != nil { + return p.reportError(ErrInvalidNumber) + } + + s := new(netmap.Selector) + s.SetCount(uint32(res)) + + if clStmt := ctx.Clause(); clStmt != nil { + s.SetClause(clauseFromString(clStmt.GetText())) + } + + if bStmt := ctx.GetBucket(); bStmt != nil { + s.SetAttribute(ctx.GetBucket().GetText()) + } + + s.SetFilter(ctx.GetFilter().GetText()) // either ident or wildcard + + if ctx.AS() != nil { + s.SetName(ctx.GetName().GetText()) + } + return s +} + +// VisitFilterStmt implements parser.QueryVisitor interface. +func (p *policyVisitor) VisitFilterStmt(ctx *parser.FilterStmtContext) interface{} { + f := p.VisitFilterExpr(ctx.GetExpr().(*parser.FilterExprContext)).(*netmap.Filter) + f.SetName(ctx.GetName().GetText()) + return f +} + +func (p *policyVisitor) VisitFilterExpr(ctx *parser.FilterExprContext) interface{} { + if eCtx := ctx.Expr(); eCtx != nil { + return eCtx.Accept(p) + } + + if inner := ctx.GetInner(); inner != nil { + return inner.Accept(p) + } + + f := new(netmap.Filter) + op := operationFromString(ctx.GetOp().GetText()) + f.SetOperation(op) + + f1 := ctx.GetF1().Accept(p).(*netmap.Filter) + f2 := ctx.GetF2().Accept(p).(*netmap.Filter) + + // Consider f1=(.. AND ..) AND f2. This can be merged because our AND operation + // is of arbitrary arity. ANTLR generates left-associative parse-tree by default. + if f1.Operation() == op { + f.SetInnerFilters(append(f1.InnerFilters(), f2)...) + return f + } + + f.SetInnerFilters(f1, f2) + return f +} + +// VisitFilterKey implements parser.QueryVisitor interface. +func (p *policyVisitor) VisitFilterKey(ctx *parser.FilterKeyContext) interface{} { + if id := ctx.Ident(); id != nil { + return id.GetText() + } + + str := ctx.STRING().GetText() + return str[1 : len(str)-1] +} + +func (p *policyVisitor) VisitFilterValue(ctx *parser.FilterValueContext) interface{} { + if id := ctx.Ident(); id != nil { + return id.GetText() + } + + if num := ctx.Number(); num != nil { + return num.GetText() + } + + str := ctx.STRING().GetText() + return str[1 : len(str)-1] +} + +// VisitExpr implements parser.QueryVisitor interface. +func (p *policyVisitor) VisitExpr(ctx *parser.ExprContext) interface{} { + f := new(netmap.Filter) + if flt := ctx.GetFilter(); flt != nil { + f.SetName(flt.GetText()) + return f + } + + key := ctx.GetKey().Accept(p) + opStr := ctx.SIMPLE_OP().GetText() + value := ctx.GetValue().Accept(p) + + f.SetKey(key.(string)) + f.SetOperation(operationFromString(opStr)) + f.SetValue(value.(string)) + return f +} + +// validatePolicy checks high-level constraints such as filter link in SELECT +// being actually defined in FILTER section. +func validatePolicy(p *netmap.PlacementPolicy) error { + seenFilters := map[string]bool{} + for _, f := range p.Filters() { + seenFilters[f.Name()] = true } seenSelectors := map[string]bool{} - ss := make([]*netmap.Selector, 0, len(q.Selectors)) - for _, qs := range q.Selectors { - if qs.Filter != netmap.MainFilterName && !seenFilters[qs.Filter] { - return nil, fmt.Errorf("%w: '%s'", ErrUnknownFilter, qs.Filter) + for _, s := range p.Selectors() { + if flt := s.Filter(); flt != netmap.MainFilterName && !seenFilters[flt] { + return fmt.Errorf("%w: '%s'", ErrUnknownFilter, flt) } - s := netmap.NewSelector() - switch len(qs.Bucket) { - case 1: // only bucket - s.SetAttribute(qs.Bucket[0]) - case 2: // clause + bucket - s.SetClause(clauseFromString(qs.Bucket[0])) - s.SetAttribute(qs.Bucket[1]) - } - s.SetName(qs.Name) - seenSelectors[qs.Name] = true - s.SetFilter(qs.Filter) - if qs.Count == 0 { - return nil, fmt.Errorf("%w: SELECT", ErrInvalidNumber) - } - s.SetCount(qs.Count) - ss = append(ss, s) + seenSelectors[s.Name()] = true } - rs := make([]*netmap.Replica, 0, len(q.Replicas)) - for _, qr := range q.Replicas { - r := netmap.NewReplica() - if qr.Selector != "" { - if !seenSelectors[qr.Selector] { - return nil, fmt.Errorf("%w: '%s'", ErrUnknownSelector, qr.Selector) - } - r.SetSelector(qr.Selector) + for _, r := range p.Replicas() { + if sel := r.Selector(); sel != "" && !seenSelectors[sel] { + return fmt.Errorf("%w: '%s'", ErrUnknownSelector, sel) } - if qr.Count == 0 { - return nil, fmt.Errorf("%w: REP", ErrInvalidNumber) - } - r.SetCount(uint32(qr.Count)) - rs = append(rs, r) } - p := new(netmap.PlacementPolicy) - p.SetFilters(fs...) - p.SetSelectors(ss...) - p.SetReplicas(rs...) - p.SetContainerBackupFactor(q.CBF) - - return p, nil + return nil } func clauseFromString(s string) netmap.Clause { @@ -105,74 +281,31 @@ func clauseFromString(s string) netmap.Clause { case "DISTINCT": return netmap.ClauseDistinct default: - return 0 + // Such errors should be handled by ANTLR code thus this panic. + panic(fmt.Errorf("BUG: invalid clause: %s", s)) } } -func filterFromOrChain(expr *orChain, seen map[string]bool) (*netmap.Filter, error) { - var fs []*netmap.Filter - for _, ac := range expr.Clauses { - f, err := filterFromAndChain(ac, seen) - if err != nil { - return nil, err - } - fs = append(fs, f) - } - if len(fs) == 1 { - return fs[0], nil - } - - f := netmap.NewFilter() - f.SetOperation(netmap.OpOR) - f.SetInnerFilters(fs...) - return f, nil -} - -func filterFromAndChain(expr *andChain, seen map[string]bool) (*netmap.Filter, error) { - var fs []*netmap.Filter - for _, fe := range expr.Clauses { - var f *netmap.Filter - var err error - if fe.Expr != nil { - f, err = filterFromSimpleExpr(fe.Expr, seen) - } else { - f = netmap.NewFilter() - f.SetName(fe.Reference) - } - if err != nil { - return nil, err - } - fs = append(fs, f) - } - if len(fs) == 1 { - return fs[0], nil - } - - f := netmap.NewFilter() - f.SetOperation(netmap.OpAND) - f.SetInnerFilters(fs...) - return f, nil -} - -func filterFromSimpleExpr(se *simpleExpr, seen map[string]bool) (*netmap.Filter, error) { - f := netmap.NewFilter() - f.SetKey(se.Key) - switch se.Op { +func operationFromString(op string) netmap.Operation { + switch strings.ToUpper(op) { + case "AND": + return netmap.OpAND + case "OR": + return netmap.OpOR case "EQ": - f.SetOperation(netmap.OpEQ) + return netmap.OpEQ case "NE": - f.SetOperation(netmap.OpNE) + return netmap.OpNE case "GE": - f.SetOperation(netmap.OpGE) + return netmap.OpGE case "GT": - f.SetOperation(netmap.OpGT) + return netmap.OpGT case "LE": - f.SetOperation(netmap.OpLE) + return netmap.OpLE case "LT": - f.SetOperation(netmap.OpLT) + return netmap.OpLT default: - return nil, fmt.Errorf("%w: '%s'", ErrUnknownOp, se.Op) + // Such errors should be handled by ANTLR code thus this panic. + panic(fmt.Errorf("BUG: invalid operation: %s", op)) } - f.SetValue(se.Value) - return f, nil } diff --git a/pkg/policy/query_test.go b/pkg/policy/query_test.go index ebe2be2..9e27e67 100644 --- a/pkg/policy/query_test.go +++ b/pkg/policy/query_test.go @@ -2,6 +2,8 @@ package policy import ( "errors" + "fmt" + "math" "testing" "github.com/nspcc-dev/neofs-api-go/v2/netmap" @@ -80,6 +82,32 @@ func TestFromSelectNoAttribute(t *testing.T) { }) } +func TestString(t *testing.T) { + qTemplate := `REP 1 +SELECT 1 IN City FROM Filt +FILTER Property EQ %s AND Something NE 7 AS Filt` + + testCases := []string{ + `"double-quoted"`, + `"with ' single"`, + `'single-quoted'`, + `'with " double'`, + } + + for _, s := range testCases { + t.Run(s, func(t *testing.T) { + q := fmt.Sprintf(qTemplate, s) + r, err := Parse(q) + require.NoError(t, err) + + expected := newFilter("Filt", "", "", netmap.AND, + newFilter("", "Property", s[1:len(s)-1], netmap.EQ), + newFilter("", "Something", "7", netmap.NE)) + require.EqualValues(t, []*netmap.Filter{expected}, r.Filters()) + }) + } +} + func TestFromSelectClause(t *testing.T) { q := `REP 4 SELECT 3 IN Country FROM * @@ -188,6 +216,30 @@ FILTER City EQ "SPB" AND SSD EQ true OR City EQ "SPB" AND Rating GE 5 AS SPBSSD` require.EqualValues(t, expected, r) } +func TestBrackets(t *testing.T) { + q := `REP 7 IN SPB +SELECT 1 IN City FROM SPBSSD AS SPB +FILTER ( City EQ "SPB" OR SSD EQ true ) AND (City EQ "SPB" OR Rating GE 5) AS SPBSSD` + + expected := new(netmap.PlacementPolicy) + expected.SetReplicas([]*netmap.Replica{newReplica("SPB", 7)}) + expected.SetSelectors([]*netmap.Selector{ + newSelector(1, netmap.UnspecifiedClause, "City", "SPBSSD", "SPB"), + }) + expected.SetFilters([]*netmap.Filter{ + newFilter("SPBSSD", "", "", netmap.AND, + newFilter("", "", "", netmap.OR, + newFilter("", "City", "SPB", netmap.EQ), + newFilter("", "SSD", "true", netmap.EQ)), + newFilter("", "", "", netmap.OR, + newFilter("", "City", "SPB", netmap.EQ), + newFilter("", "Rating", "5", netmap.GE)))}) + + r, err := Parse(q) + require.NoError(t, err) + require.EqualValues(t, expected, r) +} + func TestValidation(t *testing.T) { t.Run("MissingSelector", func(t *testing.T) { q := `REP 3 IN RU` @@ -205,12 +257,12 @@ func TestValidation(t *testing.T) { SELECT 1 IN City FROM F FILTER Country KEK RU AS F` _, err := Parse(q) - require.True(t, errors.Is(err, ErrUnknownOp), "got: %v", err) + require.True(t, errors.Is(err, ErrSyntaxError), "got: %v", err) }) t.Run("TypoInREP", func(t *testing.T) { q := `REK 3` _, err := Parse(q) - require.Error(t, err) + require.True(t, errors.Is(err, ErrSyntaxError)) }) t.Run("InvalidFilterName", func(t *testing.T) { q := `REP 3 @@ -220,17 +272,28 @@ func TestValidation(t *testing.T) { _, err := Parse(q) require.Error(t, err) }) - t.Run("InvalidNumberInREP", func(t *testing.T) { - q := `REP 0` - _, err := Parse(q) - require.True(t, errors.Is(err, ErrInvalidNumber), "got: %v", err) - }) - t.Run("InvalidNumberInREP", func(t *testing.T) { - q := `REP 1 IN Good - SELECT 0 IN City FROM *` - _, err := Parse(q) - require.True(t, errors.Is(err, ErrInvalidNumber), "got: %v", err) - }) +} + +// Checks that an error is returned in cases when positive 32-bit integer is expected. +func TestInvalidNumbers(t *testing.T) { + tmpls := []string{ + "REP %d", + "REP 1 CBF %d", + "REP 1 SELECT %d FROM *", + } + for i := range tmpls { + zero := fmt.Sprintf(tmpls[i], 0) + t.Run(zero, func(t *testing.T) { + _, err := Parse(zero) + require.Error(t, err) + }) + + big := fmt.Sprintf(tmpls[i], int64(math.MaxUint32)+1) + t.Run(big, func(t *testing.T) { + _, err := Parse(big) + require.Error(t, err) + }) + } } func TestFilterStringSymbols(t *testing.T) {