Merge pull request #3 from fyrchik/policy-parser

Use ANTLR for policy parsing
This commit is contained in:
Roman Khimov 2021-06-15 13:21:34 +03:00 committed by GitHub
commit 56ef32f21e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 434 additions and 263 deletions

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
/pkg/policy/parser/*.go -diff
/pkg/policy/parser/generate.go diff
**/*.interp -diff
**/*.tokens -diff

2
go.mod
View file

@ -3,7 +3,7 @@ module github.com/nspcc-dev/neofs-sdk-go
go 1.16 go 1.16
require ( 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/nspcc-dev/neofs-api-go v1.27.0
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
go.uber.org/zap v1.10.0 go.uber.org/zap v1.10.0

5
go.sum
View file

@ -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/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/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/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-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/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-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/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/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/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/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 v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=

View file

@ -1,20 +1,19 @@
// Package policy provides facilities for creating policy from SQL-like language. // 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: // Current limitations:
// 1. Grouping filter expressions in parenthesis is not supported right now. // 1. Filters must be defined before they are used.
// 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. // 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: // Example query:
// REP 1 in SPB // REP 1 IN SPB
// REP 2 in Americas // REP 2 IN Americas
// CBF 4 // CBF 4
// SELECT 1 Node IN City FROM SPBSSD AS SPB // SELECT 1 IN City FROM SPBSSD AS SPB
// SELECT 2 Node IN SAME City FROM Americas AS Americas // SELECT 2 IN SAME City FROM Americas AS Americas
// FILTER SSD EQ true AS IsSSD // FILTER SSD EQ true AS IsSSD
// FILTER @IsSSD AND Country eq "RU" AND City eq "St.Petersburg" AS SPBSSD // FILTER @IsSSD AND Country EQ "RU" AND City EQ "St.Petersburg" AS SPBSSD
// FILTER 'Continent' == 'North America' OR Continent == 'South America' AS Americas // FILTER 'Continent' EQ 'North America' OR Continent EQ 'South America' AS Americas
package policy package policy

View file

@ -1,7 +1,6 @@
package policy_test package policy_test
import ( import (
"fmt"
"strings" "strings"
"testing" "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) require.NoError(t, err)
got := policy.Encode(q) got := policy.Encode(q)
fmt.Println(strings.Join(got, "\n"))
require.Equal(t, testCase, strings.Join(got, "\n")) require.Equal(t, testCase, strings.Join(got, "\n"))
} }
} }

View file

@ -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;

View file

@ -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)`
}

View file

@ -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;

Binary file not shown.

Binary file not shown.

View file

@ -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 ;

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,3 @@
package parser
//go:generate antlr4 -Dlanguage=Go -visitor QueryLexer.g4 Query.g4

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -3,9 +3,12 @@ package policy
import ( import (
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/antlr/antlr4/runtime/Go/antlr"
"github.com/nspcc-dev/neofs-api-go/pkg/netmap" "github.com/nspcc-dev/neofs-api-go/pkg/netmap"
"github.com/nspcc-dev/neofs-sdk-go/pkg/policy/parser"
) )
var ( var (
@ -19,83 +22,256 @@ var (
ErrUnknownFilter = errors.New("policy: filter not found") ErrUnknownFilter = errors.New("policy: filter not found")
// ErrUnknownSelector is returned when a value of IN is unknown. // ErrUnknownSelector is returned when a value of IN is unknown.
ErrUnknownSelector = errors.New("policy: selector not found") 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) { type policyVisitor struct {
q := new(query) errors []error
err := parser.Parse(strings.NewReader(s), q) parser.BaseQueryVisitor
if err != nil { antlr.DefaultErrorListener
return nil, err
}
return q, nil
} }
// Parse parses s into a placement policy. // Parse parses s into a placement policy.
func Parse(s string) (*netmap.PlacementPolicy, error) { func Parse(s string) (*netmap.PlacementPolicy, error) {
q, err := parse(s) return parse(s)
if err != nil {
return nil, err
} }
seenFilters := map[string]bool{} func newPolicyVisitor() *policyVisitor {
fs := make([]*netmap.Filter, 0, len(q.Filters)) return &policyVisitor{}
for _, qf := range q.Filters { }
f, err := filterFromOrChain(qf.Value, seenFilters)
if err != nil { 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 nil, err
} }
f.SetName(qf.Name) return pl.(*netmap.PlacementPolicy), nil
fs = append(fs, f) }
seenFilters[qf.Name] = true
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
}
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{} seenSelectors := map[string]bool{}
ss := make([]*netmap.Selector, 0, len(q.Selectors)) for _, s := range p.Selectors() {
for _, qs := range q.Selectors { if flt := s.Filter(); flt != netmap.MainFilterName && !seenFilters[flt] {
if qs.Filter != netmap.MainFilterName && !seenFilters[qs.Filter] { return fmt.Errorf("%w: '%s'", ErrUnknownFilter, flt)
return nil, fmt.Errorf("%w: '%s'", ErrUnknownFilter, qs.Filter)
} }
s := netmap.NewSelector() seenSelectors[s.Name()] = true
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)
} }
rs := make([]*netmap.Replica, 0, len(q.Replicas)) for _, r := range p.Replicas() {
for _, qr := range q.Replicas { if sel := r.Selector(); sel != "" && !seenSelectors[sel] {
r := netmap.NewReplica() return fmt.Errorf("%w: '%s'", ErrUnknownSelector, sel)
if qr.Selector != "" {
if !seenSelectors[qr.Selector] {
return nil, fmt.Errorf("%w: '%s'", ErrUnknownSelector, qr.Selector)
} }
r.SetSelector(qr.Selector)
}
if qr.Count == 0 {
return nil, fmt.Errorf("%w: REP", ErrInvalidNumber)
}
r.SetCount(uint32(qr.Count))
rs = append(rs, r)
} }
p := new(netmap.PlacementPolicy) return nil
p.SetFilters(fs...)
p.SetSelectors(ss...)
p.SetReplicas(rs...)
p.SetContainerBackupFactor(q.CBF)
return p, nil
} }
func clauseFromString(s string) netmap.Clause { func clauseFromString(s string) netmap.Clause {
@ -105,74 +281,31 @@ func clauseFromString(s string) netmap.Clause {
case "DISTINCT": case "DISTINCT":
return netmap.ClauseDistinct return netmap.ClauseDistinct
default: 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) { func operationFromString(op string) netmap.Operation {
var fs []*netmap.Filter switch strings.ToUpper(op) {
for _, ac := range expr.Clauses { case "AND":
f, err := filterFromAndChain(ac, seen) return netmap.OpAND
if err != nil { case "OR":
return nil, err return netmap.OpOR
}
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 {
case "EQ": case "EQ":
f.SetOperation(netmap.OpEQ) return netmap.OpEQ
case "NE": case "NE":
f.SetOperation(netmap.OpNE) return netmap.OpNE
case "GE": case "GE":
f.SetOperation(netmap.OpGE) return netmap.OpGE
case "GT": case "GT":
f.SetOperation(netmap.OpGT) return netmap.OpGT
case "LE": case "LE":
f.SetOperation(netmap.OpLE) return netmap.OpLE
case "LT": case "LT":
f.SetOperation(netmap.OpLT) return netmap.OpLT
default: 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
} }

View file

@ -2,6 +2,8 @@ package policy
import ( import (
"errors" "errors"
"fmt"
"math"
"testing" "testing"
"github.com/nspcc-dev/neofs-api-go/v2/netmap" "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) { func TestFromSelectClause(t *testing.T) {
q := `REP 4 q := `REP 4
SELECT 3 IN Country FROM * 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) 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) { func TestValidation(t *testing.T) {
t.Run("MissingSelector", func(t *testing.T) { t.Run("MissingSelector", func(t *testing.T) {
q := `REP 3 IN RU` q := `REP 3 IN RU`
@ -205,12 +257,12 @@ func TestValidation(t *testing.T) {
SELECT 1 IN City FROM F SELECT 1 IN City FROM F
FILTER Country KEK RU AS F` FILTER Country KEK RU AS F`
_, err := Parse(q) _, 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) { t.Run("TypoInREP", func(t *testing.T) {
q := `REK 3` q := `REK 3`
_, err := Parse(q) _, err := Parse(q)
require.Error(t, err) require.True(t, errors.Is(err, ErrSyntaxError))
}) })
t.Run("InvalidFilterName", func(t *testing.T) { t.Run("InvalidFilterName", func(t *testing.T) {
q := `REP 3 q := `REP 3
@ -220,18 +272,29 @@ func TestValidation(t *testing.T) {
_, err := Parse(q) _, err := Parse(q)
require.Error(t, err) require.Error(t, err)
}) })
t.Run("InvalidNumberInREP", func(t *testing.T) { }
q := `REP 0`
_, err := Parse(q) // Checks that an error is returned in cases when positive 32-bit integer is expected.
require.True(t, errors.Is(err, ErrInvalidNumber), "got: %v", err) 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)
}) })
t.Run("InvalidNumberInREP", func(t *testing.T) {
q := `REP 1 IN Good big := fmt.Sprintf(tmpls[i], int64(math.MaxUint32)+1)
SELECT 0 IN City FROM *` t.Run(big, func(t *testing.T) {
_, err := Parse(q) _, err := Parse(big)
require.True(t, errors.Is(err, ErrInvalidNumber), "got: %v", err) require.Error(t, err)
}) })
} }
}
func TestFilterStringSymbols(t *testing.T) { func TestFilterStringSymbols(t *testing.T) {
q := `REP 1 IN S q := `REP 1 IN S