diff --git a/bql/grammar/grammar.go b/bql/grammar/grammar.go index 7529ebca..34d53ed1 100644 --- a/bql/grammar/grammar.go +++ b/bql/grammar/grammar.go @@ -34,7 +34,7 @@ func BQL() *Grammar { NewTokenType(lexer.ItemQuery), NewSymbol("VARS"), NewTokenType(lexer.ItemFrom), - NewSymbol("GRAPHS"), + NewSymbol("INPUT_GRAPHS"), NewSymbol("WHERE"), NewSymbol("GROUP_BY"), NewSymbol("ORDER_BY"), @@ -49,7 +49,7 @@ func BQL() *Grammar { NewTokenType(lexer.ItemInsert), NewTokenType(lexer.ItemData), NewTokenType(lexer.ItemInto), - NewSymbol("GRAPHS"), + NewSymbol("OUTPUT_GRAPHS"), NewTokenType(lexer.ItemLBracket), NewTokenType(lexer.ItemNode), NewTokenType(lexer.ItemPredicate), @@ -64,7 +64,7 @@ func BQL() *Grammar { NewTokenType(lexer.ItemDelete), NewTokenType(lexer.ItemData), NewTokenType(lexer.ItemFrom), - NewSymbol("GRAPHS"), + NewSymbol("INPUT_GRAPHS"), NewTokenType(lexer.ItemLBracket), NewTokenType(lexer.ItemNode), NewTokenType(lexer.ItemPredicate), @@ -93,9 +93,9 @@ func BQL() *Grammar { NewTokenType(lexer.ItemConstruct), NewSymbol("CONSTRUCT_FACTS"), NewTokenType(lexer.ItemInto), - NewSymbol("GRAPHS"), + NewSymbol("OUTPUT_GRAPHS"), NewTokenType(lexer.ItemFrom), - NewSymbol("GRAPHS"), + NewSymbol("INPUT_GRAPHS"), NewSymbol("WHERE"), NewSymbol("HAVING"), NewTokenType(lexer.ItemSemicolon), @@ -194,6 +194,42 @@ func BQL() *Grammar { }, {}, }, + "INPUT_GRAPHS": []*Clause{ + { + Elements: []Element{ + NewTokenType(lexer.ItemBinding), + NewSymbol("MORE_INPUT_GRAPHS"), + }, + }, + }, + "MORE_INPUT_GRAPHS": []*Clause{ + { + Elements: []Element{ + NewTokenType(lexer.ItemComma), + NewTokenType(lexer.ItemBinding), + NewSymbol("MORE_INPUT_GRAPHS"), + }, + }, + {}, + }, + "OUTPUT_GRAPHS": []*Clause{ + { + Elements: []Element{ + NewTokenType(lexer.ItemBinding), + NewSymbol("MORE_OUTPUT_GRAPHS"), + }, + }, + }, + "MORE_OUTPUT_GRAPHS": []*Clause{ + { + Elements: []Element{ + NewTokenType(lexer.ItemComma), + NewTokenType(lexer.ItemBinding), + NewSymbol("MORE_OUTPUT_GRAPHS"), + }, + }, + {}, + }, "WHERE": []*Clause{ { Elements: []Element{ @@ -919,6 +955,14 @@ func SemanticBQL() *Grammar { graphSymbols := []semantic.Symbol{"GRAPHS", "MORE_GRAPHS"} setElementHook(semanticBQL, graphSymbols, semantic.GraphAccumulatorHook(), nil) + // Add graph binding collection to INPUT_GRAPHS and MORE_INPUT_GRAPHS clauses. + inputGraphSymbols := []semantic.Symbol{"INPUT_GRAPHS", "MORE_INPUT_GRAPHS"} + setElementHook(semanticBQL, inputGraphSymbols, semantic.InputGraphAccumulatorHook(), nil) + + // Add graph binding collection to OUTPUT_GRAPHS and MORE_OUTPUT_GRAPHS clauses. + outputGraphSymbols := []semantic.Symbol{"OUTPUT_GRAPHS", "MORE_OUTPUT_GRAPHS"} + setElementHook(semanticBQL, outputGraphSymbols, semantic.OutputGraphAccumulatorHook(), nil) + // Insert and Delete semantic hooks addition. insertSymbols := []semantic.Symbol{ "INSERT_OBJECT", "INSERT_DATA", "DELETE_OBJECT", "DELETE_DATA", diff --git a/bql/grammar/grammar_test.go b/bql/grammar/grammar_test.go index ede9e881..b1a8d81a 100644 --- a/bql/grammar/grammar_test.go +++ b/bql/grammar/grammar_test.go @@ -15,6 +15,7 @@ package grammar import ( + "reflect" "testing" "github.com/google/badwolf/bql/semantic" @@ -255,36 +256,54 @@ func TestRejectByParse(t *testing.T) { } } -func TestAcceptOpsByParseAndSemantic(t *testing.T) { +func TestAcceptGraphOpsByParseAndSemantic(t *testing.T) { + var empty []string table := []struct { - query string - graphs int - triples int + query string + graphs []string + inputGraphs []string + outputGraphs []string + triples int }{ - // Insert data. - {`insert data into ?a {/_ "bar"@[1975-01-01T00:01:01.999999999Z] /_};`, 1, 1}, - {`insert data into ?a {/_ "bar"@[] "bar"@[1975-01-01T00:01:01.999999999Z]};`, 1, 1}, - {`insert data into ?a {/_ "bar"@[] "yeah"^^type:text};`, 1, 1}, - // Insert into multiple graphs. - {`insert data into ?a,?b,?c {/_ "bar"@[] /_};`, 3, 1}, + // Create graphs. All graphs are regular graphs. + {`create graph ?foo1, ?bar1;`, []string{"?foo1", "?bar1"}, empty, empty, 0}, + // Drop graphs. All graphs are regular graphs. + {`drop graph ?foo2, ?bar2;`, []string{"?foo2", "?bar2"}, empty, empty, 0}, + + // Insert data. All graphs are output graphs. + {`insert data into ?a {/_ "bar"@[1975-01-01T00:01:01.999999999Z] /_};`, empty, empty, []string{"?a"}, 1}, + {`insert data into ?a {/_ "bar"@[] "bar"@[1975-01-01T00:01:01.999999999Z]};`, empty, empty, []string{"?a"}, 1}, + {`insert data into ?a {/_ "bar"@[] "yeah"^^type:text};`, empty, empty, []string{"?a"}, 1}, + // Insert into multiple output graphs. + {`insert data into ?a,?b,?c {/_ "bar"@[] /_};`, empty, empty, []string{"?a", "?b", "?c"}, 1}, // Insert multiple data. {`insert data into ?a {/_ "bar"@[] /_ . /_ "bar"@[] "bar"@[1975-01-01T00:01:01.999999999Z] . - /_ "bar"@[] "yeah"^^type:text};`, 1, 3}, - // Delete data. - {`delete data from ?a {/_ "bar"@[] /_};`, 1, 1}, - {`delete data from ?a {/_ "bar"@[] "bar"@[1975-01-01T00:01:01.999999999Z]};`, 1, 1}, - {`delete data from ?a {/_ "bar"@[] "yeah"^^type:text};`, 1, 1}, - // Delete from multiple graphs. - {`delete data from ?a,?b,?c {/_ "bar"@[1975-01-01T00:01:01.999999999Z] /_};`, 3, 1}, + /_ "bar"@[] "yeah"^^type:text};`, empty, empty, []string{"?a"}, 3}, + + // Delete data. All graphs are input graphs. + {`delete data from ?a {/_ "bar"@[] /_};`, empty, []string{"?a"}, empty, 1}, + {`delete data from ?a {/_ "bar"@[] "bar"@[1975-01-01T00:01:01.999999999Z]};`, empty, []string{"?a"}, empty, 1}, + {`delete data from ?a {/_ "bar"@[] "yeah"^^type:text};`, empty, []string{"?a"}, empty, 1}, + // Delete from multiple input graphs. + {`delete data from ?a,?b,?c {/_ "bar"@[1975-01-01T00:01:01.999999999Z] /_};`, empty, []string{"?a", "?b", "?c"}, empty, 1}, // Delete multiple data. {`delete data from ?a {/_ "bar"@[] /_ . /_ "bar"@[] "bar"@[1975-01-01T00:01:01.999999999Z] . - /_ "bar"@[] "yeah"^^type:text};`, 1, 3}, - // Create graphs. - {`create graph ?foo;`, 1, 0}, - // Drop graphs. - {`drop graph ?foo, ?bar;`, 2, 0}, + /_ "bar"@[] "yeah"^^type:text};`, empty, []string{"?a"}, empty, 3}, + + // Construct data. Graphs can be input or output graphs. + {`construct {?s "predicate_1"@[] ?o1; + "predicate_2"@[] ?o2} into ?a from ?b where {?s "old_predicate_1"@[,] ?o1. + ?s "old_predicate_2"@[,] ?o2. + ?s "old_predicate_3"@[,] ?o3};`, + empty, []string{"?b"}, []string{"?a"}, 0}, + // construct data into multiple output graphs from multple input graphs. + {`construct {?s "predicate_1"@[] ?o1; + "predicate_2"@[] ?o2} into ?a, ?b from ?c, ?d where {?s "old_predicate_1"@[,] ?o1. + ?s "old_predicate_2"@[,] ?o2. + ?s "old_predicate_3"@[,] ?o3};`, + empty, []string{"?c", "?d"}, []string{"?a", "?b"}, 0}, } p, err := NewParser(SemanticBQL()) if err != nil { @@ -295,8 +314,14 @@ func TestAcceptOpsByParseAndSemantic(t *testing.T) { if err := p.Parse(NewLLk(entry.query, 1), st); err != nil { t.Errorf("Parser.consume: Failed to accept entry %q with error %v", entry, err) } - if got, want := len(st.GraphNames()), entry.graphs; got != want { - t.Errorf("Parser.consume: Failed to collect right number of graphs for case %v; got %d, want %d", entry, got, want) + if got, want := st.GraphNames(), entry.graphs; !reflect.DeepEqual(got, want) { + t.Errorf("Parser.consume: Failed to collect the right graphs for case %v; got %d, want %d", entry, got, want) + } + if got, want := st.InputGraphNames(), entry.inputGraphs; !reflect.DeepEqual(got, want) { + t.Errorf("Parser.consume: Failed to collect the right input graphs for case %v; got %d, want %d", entry, got, want) + } + if got, want := st.OutputGraphNames(), entry.outputGraphs; !reflect.DeepEqual(got, want) { + t.Errorf("Parser.consume: Failed to collect the right output graphs for case %v; got %d, want %d", entry, got, want) } if got, want := len(st.Data()), entry.triples; got != want { t.Errorf("Parser.consume: Failed to collect right number of triples for case %v; got %d, want %d", entry, got, want) diff --git a/bql/planner/planner.go b/bql/planner/planner.go index d1157bb5..c4bc93ae 100644 --- a/bql/planner/planner.go +++ b/bql/planner/planner.go @@ -152,7 +152,7 @@ func update(ctx context.Context, stm *semantic.Statement, store storage.Store, f errs = append(errs, err.Error()) } - for _, graphBinding := range stm.GraphNames() { + for _, graphBinding := range stm.OutputGraphNames() { wg.Add(1) go func(graph string) { defer wg.Done() @@ -191,7 +191,7 @@ func (p *insertPlan) Execute(ctx context.Context) (*table.Table, error) { // String returns a readable description of the execution plan. func (p *insertPlan) String() string { b := bytes.NewBufferString("INSERT plan:\n\n") - for _, g := range p.stm.Graphs() { + for _, g := range p.stm.OutputGraphs() { b.WriteString(fmt.Sprintf("store(%q).Graph(%q).AddTriples(_, data)\n", p.store.Name(nil), g)) } b.WriteString("where data:\n") @@ -228,7 +228,7 @@ func (p *deletePlan) Execute(ctx context.Context) (*table.Table, error) { // String returns a readable description of the execution plan. func (p *deletePlan) String() string { b := bytes.NewBufferString("DELETE plan:\n\n") - for _, g := range p.stm.Graphs() { + for _, g := range p.stm.InputGraphs() { b.WriteString(fmt.Sprintf("store(%q).Graph(%q).RemoveTriples(_, data)\n", p.store.Name(nil), g)) } b.WriteString("where data:\n") @@ -270,7 +270,7 @@ func newQueryPlan(ctx context.Context, store storage.Store, stm *semantic.Statem stm: stm, store: store, bndgs: bs, - grfsNames: stm.GraphNames(), + grfsNames: stm.InputGraphNames(), cls: stm.SortedGraphPatternClauses(), tbl: t, chanSize: chanSize, @@ -524,7 +524,7 @@ func (p *queryPlan) filterOnExistence(ctx context.Context, cls *semantic.GraphCl return fmt.Errorf("failed to fully specify clause %v for row %+v", cls, r) } exist := false - for _, g := range p.stm.Graphs() { + for _, g := range p.stm.InputGraphs() { t, err := triple.New(sbj, prd, obj) if err != nil { return err @@ -708,12 +708,12 @@ func (p *queryPlan) limit() { func (p *queryPlan) Execute(ctx context.Context) (*table.Table, error) { // Fetch and catch graph instances. trace(p.tracer, func() []string { - return []string{fmt.Sprintf("Caching graph instances for graphs %v", p.stm.GraphNames())} + return []string{fmt.Sprintf("Caching graph instances for graphs %v", p.stm.InputGraphNames())} }) if err := p.stm.Init(ctx, p.store); err != nil { return nil, err } - p.grfs = p.stm.Graphs() + p.grfs = p.stm.InputGraphs() // Retrieve the data. lo := p.stm.GlobalLookupOptions() trace(p.tracer, func() []string { diff --git a/bql/planner/planner_test.go b/bql/planner/planner_test.go index e31e498e..3c825754 100644 --- a/bql/planner/planner_test.go +++ b/bql/planner/planner_test.go @@ -501,14 +501,14 @@ func TestPlannerQuery(t *testing.T) { } tbl, err := plnr.Execute(ctx) if err != nil { - t.Errorf("planner.Excecute failed for query %q with error %v", entry.q, err) + t.Errorf("planner.Execute failed for query %q with error %v", entry.q, err) continue } if got, want := len(tbl.Bindings()), entry.nbs; got != want { t.Errorf("tbl.Bindings returned the wrong number of bindings for %q; got %d, want %d", entry.q, got, want) } if got, want := len(tbl.Rows()), entry.nrws; got != want { - t.Errorf("planner.Excecute failed to return the expected number of rows for query %q; got %d want %d\nGot:\n%v\n", entry.q, got, want, tbl) + t.Errorf("planner.Execute failed to return the expected number of rows for query %q; got %d want %d\nGot:\n%v\n", entry.q, got, want, tbl) } } } @@ -552,13 +552,13 @@ func TestTreeTraversalToRoot(t *testing.T) { } tbl, err := plnr.Execute(ctx) if err != nil { - t.Errorf("planner.Excecute failed for query %q with error %v", traversalQuery, err) + t.Errorf("planner.Execute failed for query %q with error %v", traversalQuery, err) } if got, want := len(tbl.Bindings()), 1; got != want { t.Errorf("tbl.Bindings returned the wrong number of bindings for %q; got %d, want %d", traversalQuery, got, want) } if got, want := len(tbl.Rows()), 1; got != want { - t.Errorf("planner.Excecute failed to return the expected number of rows for query %q; got %d want %d\nGot:\n%v\n", traversalQuery, got, want, tbl) + t.Errorf("planner.Execute failed to return the expected number of rows for query %q; got %d want %d\nGot:\n%v\n", traversalQuery, got, want, tbl) } } @@ -599,13 +599,13 @@ func TestChaining(t *testing.T) { } tbl, err := plnr.Execute(ctx) if err != nil { - t.Errorf("planner.Excecute failed for query %q with error %v", traversalQuery, err) + t.Errorf("planner.Execute failed for query %q with error %v", traversalQuery, err) } if got, want := len(tbl.Bindings()), 1; got != want { t.Errorf("tbl.Bindings returned the wrong number of bindings for %q; got %d, want %d", traversalQuery, got, want) } if got, want := len(tbl.Rows()), 1; got != want { - t.Errorf("planner.Excecute failed to return the expected number of rows for query %q; got %d want %d\nGot:\n%v\n", traversalQuery, got, want, tbl) + t.Errorf("planner.Execute failed to return the expected number of rows for query %q; got %d want %d\nGot:\n%v\n", traversalQuery, got, want, tbl) } } @@ -657,13 +657,13 @@ func TestReificationResolutionIssue70(t *testing.T) { } tbl, err := plnr.Execute(ctx) if err != nil { - t.Fatalf("planner.Excecute failed for query %q with error %v", query, err) + t.Fatalf("planner.Execute failed for query %q with error %v", query, err) } if got, want := len(tbl.Bindings()), 2; got != want { t.Errorf("tbl.Bindings returned the wrong number of bindings for %q; got %d, want %d", query, got, want) } if got, want := len(tbl.Rows()), 1; got != want { - t.Errorf("planner.Excecute failed to return the expected number of rows for query %q; got %d want %d\nGot:\n%v\n", query, got, want, tbl) + t.Errorf("planner.Execute failed to return the expected number of rows for query %q; got %d want %d\nGot:\n%v\n", query, got, want, tbl) } } @@ -688,7 +688,7 @@ func benchmarkQuery(query string, b *testing.B) { } _, err = plnr.Execute(ctx) if err != nil { - b.Errorf("planner.Excecute failed for query %q with error %v", query, err) + b.Errorf("planner.Execute failed for query %q with error %v", query, err) } } } diff --git a/bql/semantic/hooks.go b/bql/semantic/hooks.go index e124d54c..1b107c86 100644 --- a/bql/semantic/hooks.go +++ b/bql/semantic/hooks.go @@ -54,6 +54,16 @@ func GraphAccumulatorHook() ElementHook { return graphAccumulator() } +// InputGraphAccumulatorHook returns the singleton for input graph accumulation. +func InputGraphAccumulatorHook() ElementHook { + return inputGraphAccumulator() +} + +// OutputGraphAccumulatorHook returns the singleton for output graph accumulation. +func OutputGraphAccumulatorHook() ElementHook { + return outputGraphAccumulator() +} + // WhereInitWorkingClauseHook returns the singleton for graph accumulation. func WhereInitWorkingClauseHook() ClauseHook { return whereInitWorkingClause() @@ -271,7 +281,51 @@ func graphAccumulator() ElementHook { st.AddGraph(strings.TrimSpace(tkn.Text)) return hook, nil default: - return nil, fmt.Errorf("hook.GrapAccumulator requires a binding to refer to a graph, got %v instead", tkn) + return nil, fmt.Errorf("hook.GraphAccumulator requires a binding to refer to a graph, got %v instead", tkn) + } + } + return hook +} + +// inputGraphAccumulator returns an element hook that keeps track of the graphs +// listed in a statement. +func inputGraphAccumulator() ElementHook { + var hook ElementHook + hook = func(st *Statement, ce ConsumedElement) (ElementHook, error) { + if ce.IsSymbol() { + return hook, nil + } + tkn := ce.Token() + switch tkn.Type { + case lexer.ItemComma: + return hook, nil + case lexer.ItemBinding: + st.AddInputGraph(strings.TrimSpace(tkn.Text)) + return hook, nil + default: + return nil, fmt.Errorf("hook.InputGraphAccumulator requires a binding to refer to a graph, got %v instead", tkn) + } + } + return hook +} + +// outputGraphAccumulator returns an element hook that keeps track of the graphs +// listed in a statement. +func outputGraphAccumulator() ElementHook { + var hook ElementHook + hook = func(st *Statement, ce ConsumedElement) (ElementHook, error) { + if ce.IsSymbol() { + return hook, nil + } + tkn := ce.Token() + switch tkn.Type { + case lexer.ItemComma: + return hook, nil + case lexer.ItemBinding: + st.AddOutputGraph(strings.TrimSpace(tkn.Text)) + return hook, nil + default: + return nil, fmt.Errorf("hook.OutputGraphAccumulator requires a binding to refer to a graph, got %v instead", tkn) } } return hook diff --git a/bql/semantic/hooks_test.go b/bql/semantic/hooks_test.go index ff243df7..883017de 100644 --- a/bql/semantic/hooks_test.go +++ b/bql/semantic/hooks_test.go @@ -91,7 +91,7 @@ func TestDataAccumulatorHook(t *testing.T) { } } -func TestSemanticAcceptInsertDelete(t *testing.T) { +func TestGraphAccumulatorElementHooks(t *testing.T) { st := &Statement{} ces := []ConsumedElement{ NewConsumedSymbol("FOO"), @@ -111,6 +111,7 @@ func TestSemanticAcceptInsertDelete(t *testing.T) { } var ( hook ElementHook + data []string err error ) hook = graphAccumulator() @@ -120,7 +121,7 @@ func TestSemanticAcceptInsertDelete(t *testing.T) { t.Errorf("semantic.GraphAccumulator hook should have never failed for %v with error %v", ce, err) } } - data := st.GraphNames() + data = st.GraphNames() if len(data) != 2 { t.Errorf("semantic.GraphAccumulator hook should have produced 2 graph bindings; instead produced %v", st.Graphs()) } @@ -129,6 +130,40 @@ func TestSemanticAcceptInsertDelete(t *testing.T) { t.Errorf("semantic.GraphAccumulator hook failed to provied either ?foo or ?bar; got %v instead", g) } } + + hook = inputGraphAccumulator() + for _, ce := range ces { + hook, err = hook(st, ce) + if err != nil { + t.Errorf("semantic.InputGraphAccumulator hook should have never failed for %v with error %v", ce, err) + } + } + data = st.InputGraphNames() + if len(data) != 2 { + t.Errorf("semantic.InputGraphAccumulator hook should have produced 2 graph bindings; instead produced %v", st.Graphs()) + } + for _, g := range data { + if g != "?foo" && g != "?bar" { + t.Errorf("semantic.InputGraphAccumulator hook failed to provied either ?foo or ?bar; got %v instead", g) + } + } + + hook = outputGraphAccumulator() + for _, ce := range ces { + hook, err = hook(st, ce) + if err != nil { + t.Errorf("semantic.OutputGraphAccumulator hook should have never failed for %v with error %v", ce, err) + } + } + data = st.OutputGraphNames() + if len(data) != 2 { + t.Errorf("semantic.OutputGraphAccumulator hook should have produced 2 graph bindings; instead produced %v", st.Graphs()) + } + for _, g := range data { + if g != "?foo" && g != "?bar" { + t.Errorf("semantic.OutputGraphAccumulator hook failed to provied either ?foo or ?bar; got %v instead", g) + } + } } func TestTypeBindingClauseHook(t *testing.T) { diff --git a/bql/semantic/semantic.go b/bql/semantic/semantic.go index 4b6ba8f2..8a35f63d 100644 --- a/bql/semantic/semantic.go +++ b/bql/semantic/semantic.go @@ -76,6 +76,10 @@ type Statement struct { sType StatementType graphNames []string graphs []storage.Graph + inputGraphNames []string + inputGraphs []storage.Graph + outputGraphNames []string + outputGraphs []storage.Graph data []*triple.Triple pattern []*GraphClause workingClause *GraphClause @@ -409,7 +413,37 @@ func (s *Statement) Graphs() []storage.Graph { return s.graphs } -// Init initialize the graphs givne the graph names. +// InputGraphNames returns the list of input graphs listed on the statement. +func (s *Statement) InputGraphNames() []string { + return s.inputGraphNames +} + +// AddInputGraph adds an input graph to a given statement. +func (s *Statement) AddInputGraph(g string) { + s.inputGraphNames = append(s.inputGraphNames, g) +} + +// InputGraphs returns the list of input graphs listed on the statement. +func (s *Statement) InputGraphs() []storage.Graph { + return s.inputGraphs +} + +// OutputGraphNames returns the list of output graphs listed on the statement. +func (s *Statement) OutputGraphNames() []string { + return s.outputGraphNames +} + +// AddOutputGraph adds an output graph to a given statement. +func (s *Statement) AddOutputGraph(g string) { + s.outputGraphNames = append(s.outputGraphNames, g) +} + +// OutputGraphs returns the list of output graphs listed on the statement. +func (s *Statement) OutputGraphs() []storage.Graph { + return s.outputGraphs +} + +// Init initializes all graphs given the graph names. func (s *Statement) Init(ctx context.Context, st storage.Store) error { for _, gn := range s.graphNames { g, err := st.Graph(ctx, gn) @@ -418,6 +452,20 @@ func (s *Statement) Init(ctx context.Context, st storage.Store) error { } s.graphs = append(s.graphs, g) } + for _, ign := range s.inputGraphNames { + ig, err := st.Graph(ctx, ign) + if err != nil { + return err + } + s.inputGraphs = append(s.inputGraphs, ig) + } + for _, ogn := range s.outputGraphNames { + og, err := st.Graph(ctx, ogn) + if err != nil { + return err + } + s.outputGraphs = append(s.outputGraphs, og) + } return nil } diff --git a/bql/semantic/semantic_test.go b/bql/semantic/semantic_test.go index cae69686..8acdc831 100644 --- a/bql/semantic/semantic_test.go +++ b/bql/semantic/semantic_test.go @@ -41,6 +41,24 @@ func TestStatementAddGraph(t *testing.T) { } } +func TestStatementAddInputGraph(t *testing.T) { + st := &Statement{} + st.BindType(Query) + st.AddInputGraph("?foo") + if got, want := st.InputGraphNames(), []string{"?foo"}; !reflect.DeepEqual(got, want) { + t.Errorf("semantic.AddInputGraph returned the wrong graphs available; got %v, want %v", got, want) + } +} + +func TestStatementAddOutputGraph(t *testing.T) { + st := &Statement{} + st.BindType(Query) + st.AddOutputGraph("?foo") + if got, want := st.OutputGraphNames(), []string{"?foo"}; !reflect.DeepEqual(got, want) { + t.Errorf("semantic.AddOutputGraph returned the wrong graphs available; got %v, want %v", got, want) + } +} + func TestStatementAddData(t *testing.T) { tr, err := triple.Parse(`/_ "foo"@[] /_`, literal.DefaultBuilder()) if err != nil {