From c20594435ea4acea154eb587eeb6429ccbc137b2 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 2 Sep 2020 17:42:02 -0300 Subject: [PATCH 01/15] Add tests for FILTER with latest anchor (in "grammar_test.go") --- bql/grammar/grammar_test.go | 94 +++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/bql/grammar/grammar_test.go b/bql/grammar/grammar_test.go index 53f761c6..614bfc67 100644 --- a/bql/grammar/grammar_test.go +++ b/bql/grammar/grammar_test.go @@ -194,6 +194,20 @@ func TestAcceptByParse(t *testing.T) { ?n "_object"@[] ?o};`, // Show the graphs. `show graphs;`, + // Test FILTER clause inside WHERE. + `select ?a + from ?b + where { + ?s ?p ?o . + FILTER latest(?p) + };`, + `select ?a + from ?b + where { + ?s ?p ?o . + FILTER latest(?p) . + FILTER latest(?o) + };`, // Test optional trailing dot after the last clause inside WHERE. `select ?a from ?b @@ -229,6 +243,19 @@ func TestAcceptByParse(t *testing.T) { ?s ?p ?o . /u ?p ?o };`, + `select ?a + from ?b + where { + ?s ?p ?o . + FILTER latest(?p) . + };`, + `select ?a + from ?b + where { + ?s ?p ?o . + FILTER latest(?p) . + FILTER latest(?o) . + };`, } p, err := NewParser(BQL()) if err != nil { @@ -386,6 +413,46 @@ func TestRejectByParse(t *testing.T) { where {?n "_subject"@[] ?s. ?n "_predicate"@[] ?p. ?n "_object"@[] ?o};`, + // Test invalid FILTER clause inside WHERE. + `select ?a + from ?b + where { + ?s ?p ?o . + FILTER latest ?p + };`, + `select ?a + from ?b + where { + ?s ?p ?o . + FILTER latest (?p) + };`, + `select ?a + from ?b + where { + FILTER latest(?p) . + ?s ?p ?o + };`, + `select ?a + from ?b + where { + ?s ?p ?o . + FILTER latest(?p) . + /u ?p ?o + };`, + `select ?a + from ?b + where { + ?s ?p ?o . + ?s ?p ?o . + FILTER latest(?p) . + /u ?p ?o + };`, + `select ?a + from ?b + where { + ?s ?p ?o . + FILTER late^st(?p) + };`, // Test invalid trailing dot use inside WHERE. `select ?a from ?b @@ -411,6 +478,19 @@ func TestRejectByParse(t *testing.T) { ?s ?p ?o /u ?p ?o };`, + `select ?a + from ?b + where { + ?s ?p ?o + FILTER latest(?p) + };`, + `select ?a + from ?b + where { + ?s ?p ?o . + FILTER latest(?p) + FILTER latest(?o) + };`, } p, err := NewParser(BQL()) if err != nil { @@ -565,6 +645,13 @@ func TestAcceptQueryBySemanticParse(t *testing.T) { `select ?s from ?g where{/_ as ?s ?p "id"@[?foo, ?bar] as ?o} order by ?s;`, `select ?s as ?a, ?o as ?b, ?o as ?c from ?g where{?s ?p ?o} order by ?a ASC, ?b DESC;`, `select ?s as ?a, ?o as ?b, ?o as ?c from ?g where{?s ?p ?o} order by ?a ASC, ?b DESC, ?a ASC, ?b DESC, ?c;`, + // Test valid FILTER clause for grammar with hooks. + `select ?p + from ?b + where { + ?s ?p ?o . + FILTER latest(?p) + };`, } p, err := NewParser(SemanticBQL()) if err != nil { @@ -596,6 +683,13 @@ func TestRejectByParseAndSemantic(t *testing.T) { `select ?s as ?a, ?o as ?b, ?o as ?c from ?g where{?s ?p ?o} order by ?a ASC, ?a DESC;`, // Wrong limit literal. `select ?s as ?a, ?o as ?b, ?o as ?c from ?g where{?s ?p ?o} LIMIT "true"^^type:bool;`, + // Reject not supported FILTER function. + `select ?p, ?o + from ?test + where { + /u ?p ?o . + FILTER notSupportedFilterFunction(?p) + };`, } p, err := NewParser(SemanticBQL()) if err != nil { From 929e1128ee634857acec27fd8b408e4cebd4b07e Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Thu, 3 Sep 2020 12:16:23 -0300 Subject: [PATCH 02/15] Add tests for FILTER with latest anchor (in "lexer_test.go") --- bql/lexer/lexer_test.go | 55 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/bql/lexer/lexer_test.go b/bql/lexer/lexer_test.go index 55a5c0e1..d30fd594 100644 --- a/bql/lexer/lexer_test.go +++ b/bql/lexer/lexer_test.go @@ -76,6 +76,8 @@ func TestTokenTypeString(t *testing.T) { {ItemShow, "SHOW"}, {ItemGraphs, "GRAPHS"}, {ItemOptional, "OPTIONAL"}, + {ItemFilter, "FILTER"}, + {ItemFilterFunction, "FILTER_FUNCTION"}, {TokenType(-1), "UNKNOWN"}, } @@ -388,6 +390,29 @@ func TestIndividualTokens(t *testing.T) { {Type: ItemEOF}, }, }, + { + `FILTER latest(?p)`, + []Token{ + {Type: ItemFilter, Text: "FILTER"}, + {Type: ItemFilterFunction, Text: "latest"}, + {Type: ItemLPar, Text: "("}, + {Type: ItemBinding, Text: "?p"}, + {Type: ItemRPar, Text: ")"}, + {Type: ItemEOF}, + }, + }, + { + `FILTER latest(?p) .`, + []Token{ + {Type: ItemFilter, Text: "FILTER"}, + {Type: ItemFilterFunction, Text: "latest"}, + {Type: ItemLPar, Text: "("}, + {Type: ItemBinding, Text: "?p"}, + {Type: ItemRPar, Text: ")"}, + {Type: ItemDot, Text: "."}, + {Type: ItemEOF}, + }, + }, } for _, test := range table { @@ -578,6 +603,36 @@ func TestValidTokenQuery(t *testing.T) { ItemHaving, ItemBinding, ItemEQ, ItemTime, ItemSemicolon, ItemEOF, }, }, + { + `select ?s ?p ?o + from ?foo + where { + ?s ?p ?o . + FILTER latest(?p) + };`, + []TokenType{ + ItemQuery, ItemBinding, ItemBinding, ItemBinding, ItemFrom, ItemBinding, + ItemWhere, ItemLBracket, ItemBinding, ItemBinding, ItemBinding, ItemDot, + ItemFilter, ItemFilterFunction, ItemLPar, ItemBinding, ItemRPar, + ItemRBracket, ItemSemicolon, ItemEOF, + }, + }, + { + `select ?s ?p ?o + from ?foo + where { + ?s ?p ?o . + FILTER latest(?p) . + FILTER latest(?o) + };`, + []TokenType{ + ItemQuery, ItemBinding, ItemBinding, ItemBinding, ItemFrom, ItemBinding, + ItemWhere, ItemLBracket, ItemBinding, ItemBinding, ItemBinding, ItemDot, + ItemFilter, ItemFilterFunction, ItemLPar, ItemBinding, ItemRPar, ItemDot, + ItemFilter, ItemFilterFunction, ItemLPar, ItemBinding, ItemRPar, + ItemRBracket, ItemSemicolon, ItemEOF, + }, + }, } for _, test := range table { From 8c01c52b6ffa2ab719422ece778f7d6b1d639087 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 2 Sep 2020 17:53:12 -0300 Subject: [PATCH 03/15] Add token type "ItemFilter" to the lexer --- bql/lexer/lexer.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bql/lexer/lexer.go b/bql/lexer/lexer.go index 0c45a69b..f6566143 100644 --- a/bql/lexer/lexer.go +++ b/bql/lexer/lexer.go @@ -139,6 +139,8 @@ const ( ItemGraphs // ItemOptional identifies optional graph pattern clauses. ItemOptional + // ItemFilter represents the filter keyword in BQL. + ItemFilter ) func (tt TokenType) String() string { @@ -253,6 +255,8 @@ func (tt TokenType) String() string { return "GRAPHS" case ItemOptional: return "OPTIONAL" + case ItemFilter: + return "FILTER" default: return "UNKNOWN" } From 14387eeef83098e5d47e7f5a17da9595afbe63e2 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 2 Sep 2020 18:07:24 -0300 Subject: [PATCH 04/15] Add token type "ItemFilterFunction" to the lexer --- bql/lexer/lexer.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bql/lexer/lexer.go b/bql/lexer/lexer.go index f6566143..4638bbc9 100644 --- a/bql/lexer/lexer.go +++ b/bql/lexer/lexer.go @@ -141,6 +141,8 @@ const ( ItemOptional // ItemFilter represents the filter keyword in BQL. ItemFilter + // ItemFilterFunction represents a filter function in BQL. + ItemFilterFunction ) func (tt TokenType) String() string { @@ -257,6 +259,8 @@ func (tt TokenType) String() string { return "OPTIONAL" case ItemFilter: return "FILTER" + case ItemFilterFunction: + return "FILTER_FUNCTION" default: return "UNKNOWN" } From 09c25a8ccc3d060e0492200d97b3d953c0215ec7 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 2 Sep 2020 18:27:11 -0300 Subject: [PATCH 05/15] Add support for FILTER clauses in the grammar --- bql/grammar/grammar.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/bql/grammar/grammar.go b/bql/grammar/grammar.go index ec85d8d7..dd8e667b 100644 --- a/bql/grammar/grammar.go +++ b/bql/grammar/grammar.go @@ -327,6 +327,7 @@ func moreClauses() []*Clause { Elements: []Element{ NewTokenType(lexer.ItemDot), NewSymbol("CLAUSES"), + NewSymbol("FILTER_CLAUSES"), }, }, {}, @@ -365,6 +366,33 @@ func clauses() []*Clause { } } +func moreFilterClauses() []*Clause { + return []*Clause{ + { + Elements: []Element{ + NewTokenType(lexer.ItemDot), + NewSymbol("FILTER_CLAUSES"), + }, + }, + {}, + } +} +func filterClauses() []*Clause { + return []*Clause{ + { + Elements: []Element{ + NewTokenType(lexer.ItemFilter), + NewTokenType(lexer.ItemFilterFunction), + NewTokenType(lexer.ItemLPar), + NewTokenType(lexer.ItemBinding), + NewTokenType(lexer.ItemRPar), + NewSymbol("MORE_FILTER_CLAUSES"), + }, + }, + {}, + } +} + func optionalClauses() []*Clause { return []*Clause{ { @@ -1266,6 +1294,8 @@ func BQL() *Grammar { "MORE_CLAUSES": moreClauses(), "CLAUSES": clauses(), "OPTIONAL_CLAUSE": optionalClauses(), + "FILTER_CLAUSES": filterClauses(), + "MORE_FILTER_CLAUSES": moreFilterClauses(), "SUBJECT_EXTRACT": subjectExtractClauses(), "SUBJECT_TYPE": subjectTypeClauses(), "SUBJECT_ID": subjectIDClauses(), From 0d5f4265a43c51b9fbae25618db9a42993ad802f Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 2 Sep 2020 19:27:43 -0300 Subject: [PATCH 06/15] Add support for FILTER clauses in the lexer --- bql/lexer/lexer.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bql/lexer/lexer.go b/bql/lexer/lexer.go index 4638bbc9..1cbd5570 100644 --- a/bql/lexer/lexer.go +++ b/bql/lexer/lexer.go @@ -302,6 +302,7 @@ const ( from = "from" where = "where" optional = "optional" + filter = "filter" as = "as" before = "before" after = "after" @@ -410,6 +411,9 @@ func lexToken(l *lexer) stateFn { return lexPredicateOrLiteral } if unicode.IsLetter(r) { + if l.lastTokenType == ItemFilter { + return lexFilterFunction + } return lexKeyword } } @@ -625,6 +629,10 @@ func lexKeyword(l *lexer) stateFn { consumeKeyword(l, ItemOptional) return lexSpace } + if strings.EqualFold(input, filter) { + consumeKeyword(l, ItemFilter) + return lexSpace + } if strings.EqualFold(input, typeKeyword) { consumeKeyword(l, ItemType) return lexSpace @@ -656,6 +664,25 @@ func lexKeyword(l *lexer) stateFn { return nil } +// lexFilterFunction lexes a filter function out of the input (used in FILTER clauses). +func lexFilterFunction(l *lexer) stateFn { + l.next() + var nr rune + for { + nr = l.next() + if nr == leftPar { + l.backup() + break + } + if !unicode.IsLetter(nr) { + l.emitError(`invalid rune in filter function: "` + string(nr) + `"; filter functions should be formed only by letters`) + return nil + } + } + l.emit(ItemFilterFunction) + return lexSpace +} + func lexNode(l *lexer) stateFn { ltID := false for done := false; !done; { From e6dfa1674cfa10e46d0ba02bf2625fc258429a47 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Tue, 8 Sep 2020 18:30:56 -0300 Subject: [PATCH 07/15] Add "filters" to the "Statement" type --- bql/semantic/semantic.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bql/semantic/semantic.go b/bql/semantic/semantic.go index 4f6175aa..7520ffef 100644 --- a/bql/semantic/semantic.go +++ b/bql/semantic/semantic.go @@ -103,6 +103,7 @@ type Statement struct { limitSet bool limit int64 lookupOptions storage.LookupOptions + filters []*FilterClause } // GraphClause represents a clause of a graph pattern in a where clause. @@ -143,6 +144,16 @@ type GraphClause struct { OTemporal bool } +// FilterClause represents a FILTER clause inside WHERE. +// Operation below refers to the filter function being applied (eg: "latest"), Binding refers to the binding it +// will be applied to and Value, when specified, contains the second argument of the filter function (not applicable for all +// Operations - some like "latest" do not use it while others like "greaterThan" do, see Issue 129). +type FilterClause struct { + Operation string + Binding string + Value string +} + // ConstructClause represents a singular clause within a construct statement. type ConstructClause struct { S *node.Node @@ -584,6 +595,11 @@ func (s *Statement) GraphPatternClauses() []*GraphClause { return s.pattern } +// FilterClauses returns the list of FILTER clauses. +func (s *Statement) FilterClauses() []*FilterClause { + return s.filters +} + // ResetWorkingGraphClause resets the current working graph clause. func (s *Statement) ResetWorkingGraphClause() { s.workingClause = &GraphClause{} From f9b86b460161f03b348cebec6b510b3a5329f397 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 9 Sep 2020 11:25:02 -0300 Subject: [PATCH 08/15] Add "workingFilter" to the "Statement" type --- bql/semantic/semantic.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bql/semantic/semantic.go b/bql/semantic/semantic.go index 7520ffef..2e9ee4f1 100644 --- a/bql/semantic/semantic.go +++ b/bql/semantic/semantic.go @@ -104,6 +104,7 @@ type Statement struct { limit int64 lookupOptions storage.LookupOptions filters []*FilterClause + workingFilter *FilterClause } // GraphClause represents a clause of a graph pattern in a where clause. @@ -610,6 +611,11 @@ func (s *Statement) WorkingClause() *GraphClause { return s.workingClause } +// WorkingFilter returns the current working filter. +func (s *Statement) WorkingFilter() *FilterClause { + return s.workingFilter +} + // AddWorkingGraphClause adds the current working graph clause to the set of // clauses that form the graph pattern. func (s *Statement) AddWorkingGraphClause() { From 5509267f041d54a79559aa676421e1ec2f98fbc9 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 9 Sep 2020 17:52:02 -0300 Subject: [PATCH 09/15] Create FILTER clause hook --- bql/grammar/grammar.go | 6 ++++ bql/semantic/hooks.go | 60 ++++++++++++++++++++++++++++++++++++++++ bql/semantic/semantic.go | 16 +++++++++++ 3 files changed, 82 insertions(+) diff --git a/bql/grammar/grammar.go b/bql/grammar/grammar.go index dd8e667b..fa71d5d4 100644 --- a/bql/grammar/grammar.go +++ b/bql/grammar/grammar.go @@ -1430,6 +1430,12 @@ func SemanticBQL() *Grammar { } setElementHook(semanticBQL, objSymbols, semantic.WhereObjectClauseHook(), nil) + // Filter clause hook. + filterSymbols := []semantic.Symbol{ + "FILTER_CLAUSES", + } + setElementHook(semanticBQL, filterSymbols, semantic.WhereFilterClauseHook(), nil) + // Collect binding variables variables. varSymbols := []semantic.Symbol{ "VARS", "VARS_AS", "MORE_VARS", "COUNT_DISTINCT", diff --git a/bql/semantic/hooks.go b/bql/semantic/hooks.go index 3ff5a2e5..76b5b7dc 100644 --- a/bql/semantic/hooks.go +++ b/bql/semantic/hooks.go @@ -92,6 +92,12 @@ func WhereObjectClauseHook() ElementHook { return whereObjectClause() } +// WhereFilterClauseHook returns the singleton for the working filter clause hook that +// populates the filters list. +func WhereFilterClauseHook() ElementHook { + return whereFilterClause() +} + // VarAccumulatorHook returns the singleton for accumulating variable // projections. func VarAccumulatorHook() ElementHook { @@ -336,6 +342,7 @@ func whereInitWorkingClause() ClauseHook { var hook ClauseHook hook = func(s *Statement, _ Symbol) (ClauseHook, error) { s.ResetWorkingGraphClause() + s.ResetWorkingFilterClause() return hook, nil } return hook @@ -664,6 +671,59 @@ func whereObjectClause() ElementHook { return hook } +// whereFilterClause returns an element hook that updates the working filter clause and, +// if the filter clause is complete, populates the filters list of the statement. +func whereFilterClause() ElementHook { + var hook ElementHook + supportedFilterFunctions := map[string]bool{ + "latest": true, + } + + hook = func(st *Statement, ce ConsumedElement) (ElementHook, error) { + if ce.IsSymbol() { + return hook, nil + } + + tkn := ce.Token() + currFilter := st.WorkingFilter() + switch tkn.Type { + case lexer.ItemFilterFunction: + if currFilter == nil { + return nil, fmt.Errorf("could not add filter function %q to nil filter clause", tkn.Text) + } + if currFilter.Operation != "" { + return nil, fmt.Errorf("invalid filter function %q on filter clause since already set to %q", tkn.Text, currFilter.Operation) + } + if !supportedFilterFunctions[tkn.Text] { + return nil, fmt.Errorf("filter function %q on filter clause is not supported", tkn.Text) + } + currFilter.Operation = tkn.Text + return hook, nil + case lexer.ItemBinding: + if currFilter == nil { + return nil, fmt.Errorf("could not add binding %q to nil filter clause", tkn.Text) + } + if currFilter.Operation == "" { + return nil, fmt.Errorf("could not add binding %q to a filter clause that does not have a filter function previously set", tkn.Text) + } + if currFilter.Binding != "" { + return nil, fmt.Errorf("invalid binding %q on filter clause since already set to %q", tkn.Text, currFilter.Binding) + } + currFilter.Binding = tkn.Text + return hook, nil + case lexer.ItemRPar: + if currFilter == nil || currFilter.Operation == "" || currFilter.Binding == "" { + return nil, fmt.Errorf("could not add invalid working filter %q to the statement filters list", currFilter) + } + st.AddWorkingFilterClause() + } + + return hook, nil + } + + return hook +} + // varAccumulator returns an element hook that updates the object // modifiers on the working graph clause. func varAccumulator() ElementHook { diff --git a/bql/semantic/semantic.go b/bql/semantic/semantic.go index 2e9ee4f1..3748b7bf 100644 --- a/bql/semantic/semantic.go +++ b/bql/semantic/semantic.go @@ -411,6 +411,11 @@ func (c *GraphClause) IsEmpty() bool { return reflect.DeepEqual(c, &GraphClause{}) } +// IsEmpty will return true if there are no set values in the filter clause. +func (f *FilterClause) IsEmpty() bool { + return reflect.DeepEqual(f, &FilterClause{}) +} + // String returns a readable representation of a construct clause. func (c *ConstructClause) String() string { b := bytes.NewBufferString("{ ") @@ -606,6 +611,11 @@ func (s *Statement) ResetWorkingGraphClause() { s.workingClause = &GraphClause{} } +// ResetWorkingFilterClause resets the current working filter clause. +func (s *Statement) ResetWorkingFilterClause() { + s.workingFilter = &FilterClause{} +} + // WorkingClause returns the current working clause. func (s *Statement) WorkingClause() *GraphClause { return s.workingClause @@ -625,6 +635,12 @@ func (s *Statement) AddWorkingGraphClause() { s.ResetWorkingGraphClause() } +// AddWorkingFilterClause adds the current working filter clause to the filters list. +func (s *Statement) AddWorkingFilterClause() { + s.filters = append(s.filters, s.workingFilter) + s.ResetWorkingFilterClause() +} + // Projection returns the available projections in the statement. func (s *Statement) Projection() []*Projection { return s.projection From f8ca53b70aa4ffda21aa0b9d46dba50226c6f2e9 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 9 Sep 2020 18:55:00 -0300 Subject: [PATCH 10/15] Add String method to FilterClause --- bql/semantic/semantic.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bql/semantic/semantic.go b/bql/semantic/semantic.go index 3748b7bf..fe556d0a 100644 --- a/bql/semantic/semantic.go +++ b/bql/semantic/semantic.go @@ -416,6 +416,11 @@ func (f *FilterClause) IsEmpty() bool { return reflect.DeepEqual(f, &FilterClause{}) } +// String returns a string representation of the filter clause. +func (f *FilterClause) String() string { + return fmt.Sprintf("%+v", *f) +} + // String returns a readable representation of a construct clause. func (c *ConstructClause) String() string { b := bytes.NewBufferString("{ ") From 240efce98bc7f715c29dfbd8fd0336baca3244fc Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Mon, 14 Sep 2020 19:51:36 -0300 Subject: [PATCH 11/15] Add tests for FILTER clauses in the semantic.Statement level (in "semantic_test.go") --- bql/semantic/semantic_test.go | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/bql/semantic/semantic_test.go b/bql/semantic/semantic_test.go index 6528276b..fbd2aff7 100644 --- a/bql/semantic/semantic_test.go +++ b/bql/semantic/semantic_test.go @@ -259,6 +259,32 @@ func TestGraphClauseManipulation(t *testing.T) { } } +func TestFilterClauseManipulation(t *testing.T) { + st := &Statement{} + + t.Run("test workingFilter initial states", func(t *testing.T) { + if wf := st.WorkingFilter(); wf != nil { + t.Fatalf(`semantic.Statement.WorkingFilter() = %q for statement "%v" without initialization; want nil`, wf, st) + } + st.ResetWorkingFilterClause() + if wf := st.WorkingFilter(); wf == nil || !wf.IsEmpty() { + t.Fatalf(`semantic.Statement.WorkingFilter() = %q for statement "%v" after call to ResetWorkingFilterClause; want empty FilterClause`, wf, st) + } + }) + + t.Run("test call to add workingFilter", func(t *testing.T) { + wf := st.WorkingFilter() + *wf = FilterClause{Operation: "latest", Binding: "?p"} + st.AddWorkingFilterClause() + if got, want := len(st.FilterClauses()), 1; got != want { + t.Fatalf(`len(semantic.Statement.FilterClauses()) = %d for statement "%v"; want %d`, got, st, want) + } + if wf = st.WorkingFilter(); wf == nil || !wf.IsEmpty() { + t.Fatalf(`semantic.Statement.WorkingFilter() = %q for statement "%v"; want empty FilterClause`, wf, st) + } + }) +} + func TestBindingListing(t *testing.T) { stm := Statement{} stm.ResetWorkingGraphClause() @@ -324,6 +350,27 @@ func TestIsEmptyClause(t *testing.T) { } +func TestIsEmptyFilterClause(t *testing.T) { + testTable := []struct { + in *FilterClause + want bool + }{ + { + in: &FilterClause{}, + want: true, + }, + { + in: &FilterClause{Operation: "latest", Binding: "?p"}, + want: false, + }, + } + for _, entry := range testTable { + if got := entry.in.IsEmpty(); got != entry.want { + t.Errorf("FilterClause.IsEmpty(%q) = %v; want %v", entry.in, got, entry.want) + } + } +} + func TestSortedGraphPatternClauses(t *testing.T) { s := &Statement{ pattern: []*GraphClause{ From 2bb35508c45be742d1bfef495e36717703dc0dd4 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Mon, 14 Sep 2020 21:19:02 -0300 Subject: [PATCH 12/15] Add tests for WhereFilterClauseHook (in "hooks_test.go") --- bql/semantic/convert.go | 5 + bql/semantic/hooks_test.go | 216 ++++++++++++++++++++++++++++++++++++- 2 files changed, 219 insertions(+), 2 deletions(-) diff --git a/bql/semantic/convert.go b/bql/semantic/convert.go index af62fc54..70e17beb 100644 --- a/bql/semantic/convert.go +++ b/bql/semantic/convert.go @@ -69,6 +69,11 @@ func (c ConsumedElement) Token() *lexer.Token { return c.token } +// Token returns the boxed token. +func (c ConsumedElement) String() string { + return fmt.Sprintf("{isSymbol=%v, symbol=%s, token=%s}", c.isSymbol, c.symbol, c.token) +} + // ToNode converts the node found by the lexer and converts it into a BadWolf // node. func ToNode(ce ConsumedElement) (*node.Node, error) { diff --git a/bql/semantic/hooks_test.go b/bql/semantic/hooks_test.go index 4494449d..6bb25905 100644 --- a/bql/semantic/hooks_test.go +++ b/bql/semantic/hooks_test.go @@ -179,8 +179,220 @@ func TestWhereInitClauseHook(t *testing.T) { f := whereInitWorkingClause() st := &Statement{} f(st, Symbol("FOO")) - if st.WorkingClause() == nil { - t.Errorf("semantic.WhereInitWorkingClause should have returned a valid working clause for statement %v", st) + if wc := st.WorkingClause(); wc == nil || !wc.IsEmpty() { + t.Errorf(`semantic.Statement.WorkingClause() = %q for statement "%v" after call to semantic.WhereInitWorkingClause; want empty GraphClause`, wc, st) + } + if wf := st.WorkingFilter(); wf == nil || !wf.IsEmpty() { + t.Errorf(`semantic.Statement.WorkingFilter() = %q for statement "%v" after call to semantic.WhereInitWorkingClause; want empty FilterClause`, wf, st) + } +} + +func TestWhereFilterClauseHook(t *testing.T) { + st := &Statement{} + f := whereFilterClause() + st.ResetWorkingFilterClause() + + testTable := []struct { + id string + ces []ConsumedElement + want *FilterClause + }{ + { + id: "FILTER latest(?p)", + ces: []ConsumedElement{ + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilter, + Text: "FILTER", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilterFunction, + Text: "latest", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemLPar, + Text: "(", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?p", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemRPar, + Text: ")", + }), + NewConsumedSymbol("FOO"), + }, + want: &FilterClause{ + Operation: "latest", + Binding: "?p", + }, + }, + { + id: "FILTER latest(?o)", + ces: []ConsumedElement{ + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilter, + Text: "FILTER", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilterFunction, + Text: "latest", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemLPar, + Text: "(", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?o", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemRPar, + Text: ")", + }), + NewConsumedSymbol("FOO"), + }, + want: &FilterClause{ + Operation: "latest", + Binding: "?o", + }, + }, + } + for i, entry := range testTable { + for _, ce := range entry.ces { + if _, err := f(st, ce); err != nil { + t.Errorf("%q: semantic.WhereFilterClauseHook(%s) = _, %v; want _, nil", entry.id, ce, err) + } + } + if got, want := st.FilterClauses()[i], entry.want; !reflect.DeepEqual(got, want) { + t.Errorf("%q: semantic.Statement.FilterClauses()[%d] = %s; want %s", entry.id, i, got, want) + } + } + + if got, want := len(st.FilterClauses()), len(testTable); got != want { + t.Errorf("len(semantic.Statement.FilterClauses()) = %d after consuming %d valid FILTER clauses; want %d", got, want, want) + } +} + +func TestWhereFilterClauseHookError(t *testing.T) { + st := &Statement{} + f := whereFilterClause() + st.ResetWorkingFilterClause() + + testTable := []struct { + id string + ces []ConsumedElement + ceIndexError int + }{ + { + id: "FILTER notSupportedFilterFunction(?p)", + ces: []ConsumedElement{ + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilter, + Text: "FILTER", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilterFunction, + Text: "notSupportedFilterFunction", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemLPar, + Text: "(", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?p", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemRPar, + Text: ")", + }), + }, + ceIndexError: 1, + }, + { + id: "FILTER latest latest(?p)", + ces: []ConsumedElement{ + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilter, + Text: "FILTER", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilterFunction, + Text: "latest", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilterFunction, + Text: "latest", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemLPar, + Text: "(", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?p", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemRPar, + Text: ")", + }), + }, + ceIndexError: 2, + }, + { + id: "FILTER latest(?p, ?o)", + ces: []ConsumedElement{ + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilter, + Text: "FILTER", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemFilterFunction, + Text: "latest", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemLPar, + Text: "(", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?p", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemComma, + Text: ",", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?o", + }), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemRPar, + Text: ")", + }), + }, + ceIndexError: 5, + }, + } + for _, entry := range testTable { + var err error + for i, ce := range entry.ces { + if _, err = f(st, ce); err != nil { + if i != entry.ceIndexError { + t.Errorf("%q: semantic.WhereFilterClauseHook(%v) = _, %v; want _, nil since the expected error should be when consuming %v", entry.id, ce, err, entry.ces[entry.ceIndexError]) + } + break + } + } + if err == nil { + t.Errorf("%q: semantic.WhereFilterClauseHook(%v) = _, nil; want _, error", entry.id, entry.ces[entry.ceIndexError]) + } + st.ResetWorkingFilterClause() + } + + if got, want := len(st.FilterClauses()), 0; got != want { + t.Errorf("len(semantic.Statement.FilterClauses()) = %d; want %d", got, want) } } From 06ee90224d46b913b04af7b8199387c0d1ebe4c4 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 7 Oct 2020 11:39:25 -0300 Subject: [PATCH 13/15] Fix the comment for the "String" method of "ConsumedElement" --- bql/semantic/convert.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bql/semantic/convert.go b/bql/semantic/convert.go index 70e17beb..8060d986 100644 --- a/bql/semantic/convert.go +++ b/bql/semantic/convert.go @@ -69,7 +69,7 @@ func (c ConsumedElement) Token() *lexer.Token { return c.token } -// Token returns the boxed token. +// String returns a string representation of the ConsumedElement. func (c ConsumedElement) String() string { return fmt.Sprintf("{isSymbol=%v, symbol=%s, token=%s}", c.isSymbol, c.symbol, c.token) } From 89bcc1a501fc82cd24311ea9dbd9ce008b685245 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 7 Oct 2020 12:46:17 -0300 Subject: [PATCH 14/15] Use "t.Run" in "hooks_test.go" for "TestWhereFilterClauseHook" and "TestWhereFilterClauseHookError" --- bql/semantic/hooks_test.go | 56 ++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/bql/semantic/hooks_test.go b/bql/semantic/hooks_test.go index 6bb25905..b9dd18fe 100644 --- a/bql/semantic/hooks_test.go +++ b/bql/semantic/hooks_test.go @@ -259,19 +259,23 @@ func TestWhereFilterClauseHook(t *testing.T) { }, } for i, entry := range testTable { - for _, ce := range entry.ces { - if _, err := f(st, ce); err != nil { - t.Errorf("%q: semantic.WhereFilterClauseHook(%s) = _, %v; want _, nil", entry.id, ce, err) + t.Run(entry.id, func(t *testing.T) { + for _, ce := range entry.ces { + if _, err := f(st, ce); err != nil { + t.Errorf("%q: semantic.WhereFilterClauseHook(%s) = _, %v; want _, nil", entry.id, ce, err) + } } - } - if got, want := st.FilterClauses()[i], entry.want; !reflect.DeepEqual(got, want) { - t.Errorf("%q: semantic.Statement.FilterClauses()[%d] = %s; want %s", entry.id, i, got, want) - } + if got, want := st.FilterClauses()[i], entry.want; !reflect.DeepEqual(got, want) { + t.Errorf("%q: semantic.Statement.FilterClauses()[%d] = %s; want %s", entry.id, i, got, want) + } + }) } - if got, want := len(st.FilterClauses()), len(testTable); got != want { - t.Errorf("len(semantic.Statement.FilterClauses()) = %d after consuming %d valid FILTER clauses; want %d", got, want, want) - } + t.Run(fmt.Sprintf("final length filters list expects %d", len(testTable)), func(t *testing.T) { + if got, want := len(st.FilterClauses()), len(testTable); got != want { + t.Errorf("len(semantic.Statement.FilterClauses()) = %d after consuming %d valid FILTER clauses; want %d", got, want, want) + } + }) } func TestWhereFilterClauseHookError(t *testing.T) { @@ -376,24 +380,28 @@ func TestWhereFilterClauseHookError(t *testing.T) { }, } for _, entry := range testTable { - var err error - for i, ce := range entry.ces { - if _, err = f(st, ce); err != nil { - if i != entry.ceIndexError { - t.Errorf("%q: semantic.WhereFilterClauseHook(%v) = _, %v; want _, nil since the expected error should be when consuming %v", entry.id, ce, err, entry.ces[entry.ceIndexError]) + t.Run(entry.id, func(t *testing.T) { + var err error + for i, ce := range entry.ces { + if _, err = f(st, ce); err != nil { + if i != entry.ceIndexError { + t.Errorf("%q: semantic.WhereFilterClauseHook(%v) = _, %v; want _, nil since the expected error should be when consuming %v", entry.id, ce, err, entry.ces[entry.ceIndexError]) + } + break } - break } - } - if err == nil { - t.Errorf("%q: semantic.WhereFilterClauseHook(%v) = _, nil; want _, error", entry.id, entry.ces[entry.ceIndexError]) - } - st.ResetWorkingFilterClause() + if err == nil { + t.Errorf("%q: semantic.WhereFilterClauseHook(%v) = _, nil; want _, error", entry.id, entry.ces[entry.ceIndexError]) + } + st.ResetWorkingFilterClause() + }) } - if got, want := len(st.FilterClauses()), 0; got != want { - t.Errorf("len(semantic.Statement.FilterClauses()) = %d; want %d", got, want) - } + t.Run("final length filters list expects 0", func(t *testing.T) { + if got, want := len(st.FilterClauses()), 0; got != want { + t.Errorf("len(semantic.Statement.FilterClauses()) = %d; want %d", got, want) + } + }) } func TestWhereWorkingClauseHook(t *testing.T) { From 13e02b037e61c8384b99ac83225a38fac8c45503 Mon Sep 17 00:00:00 2001 From: Roger Leite Lucena Date: Wed, 7 Oct 2020 14:21:09 -0300 Subject: [PATCH 15/15] Improve subtests names for "t.Run" in "semantic_test.go" --- bql/semantic/semantic_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bql/semantic/semantic_test.go b/bql/semantic/semantic_test.go index fbd2aff7..5bcd16c4 100644 --- a/bql/semantic/semantic_test.go +++ b/bql/semantic/semantic_test.go @@ -262,7 +262,7 @@ func TestGraphClauseManipulation(t *testing.T) { func TestFilterClauseManipulation(t *testing.T) { st := &Statement{} - t.Run("test workingFilter initial states", func(t *testing.T) { + t.Run("workingFilter initial states ok", func(t *testing.T) { if wf := st.WorkingFilter(); wf != nil { t.Fatalf(`semantic.Statement.WorkingFilter() = %q for statement "%v" without initialization; want nil`, wf, st) } @@ -272,7 +272,7 @@ func TestFilterClauseManipulation(t *testing.T) { } }) - t.Run("test call to add workingFilter", func(t *testing.T) { + t.Run("add workingFilter success", func(t *testing.T) { wf := st.WorkingFilter() *wf = FilterClause{Operation: "latest", Binding: "?p"} st.AddWorkingFilterClause()