diff --git a/bql/grammar/grammar.go b/bql/grammar/grammar.go index 2f3e968a..5ecbd375 100644 --- a/bql/grammar/grammar.go +++ b/bql/grammar/grammar.go @@ -827,13 +827,52 @@ func BQL() *Grammar { { Elements: []Element{ NewTokenType(lexer.ItemSemicolon), - NewSymbol("CONSTRUCT_PREDICATE"), - NewSymbol("CONSTRUCT_OBJECT"), + NewSymbol("REIFICATION_PREDICATE"), + NewSymbol("REIFICATION_OBJECT"), NewSymbol("REIFICATION_CLAUSE"), }, }, {}, }, + "REIFICATION_PREDICATE": []*Clause{ + { + Elements: []Element{ + NewTokenType(lexer.ItemPredicate), + }, + }, + { + Elements: []Element{ + NewTokenType(lexer.ItemBinding), + }, + }, + }, + "REIFICATION_OBJECT": []*Clause{ + { + Elements: []Element{ + NewTokenType(lexer.ItemNode), + }, + }, + { + Elements: []Element{ + NewTokenType(lexer.ItemBlankNode), + }, + }, + { + Elements: []Element{ + NewTokenType(lexer.ItemPredicate), + }, + }, + { + Elements: []Element{ + NewTokenType(lexer.ItemLiteral), + }, + }, + { + Elements: []Element{ + NewTokenType(lexer.ItemBinding), + }, + }, + }, "MORE_CONSTRUCT_TRIPLES": []*Clause{ { Elements: []Element{ @@ -960,11 +999,17 @@ func SemanticBQL() *Grammar { // CONSTRUCT clause semantic hooks. setClauseHook(semanticBQL, []semantic.Symbol{"CONSTRUCT"}, nil, semantic.TypeBindingClauseHook(semantic.Construct)) setClauseHook(semanticBQL, []semantic.Symbol{"CONSTRUCT_FACTS"}, semantic.InitWorkingConstructClauseHook(), nil) + constructTriplesSymbols := []semantic.Symbol{"CONSTRUCT_TRIPLES", "MORE_CONSTRUCT_TRIPLES"} setClauseHook(semanticBQL, constructTriplesSymbols, semantic.NextWorkingConstructClauseHook(), semantic.NextWorkingConstructClauseHook()) + setElementHook(semanticBQL, []semantic.Symbol{"CONSTRUCT_TRIPLES"}, semantic.ConstructSubjectClauseHook(), nil) setElementHook(semanticBQL, []semantic.Symbol{"CONSTRUCT_PREDICATE"}, semantic.ConstructPredicateClauseHook(), nil) setElementHook(semanticBQL, []semantic.Symbol{"CONSTRUCT_OBJECT"}, semantic.ConstructObjectClauseHook(), nil) + setClauseHook(semanticBQL, []semantic.Symbol{"REIFICATION_CLAUSE"}, semantic.NextWorkingReificationClauseHook(), semantic.NextWorkingReificationClauseHook()) + setElementHook(semanticBQL, []semantic.Symbol{"REIFICATION_PREDICATE"}, semantic.ReificationPredicateClauseHook(), nil) + setElementHook(semanticBQL, []semantic.Symbol{"REIFICATION_OBJECT"}, semantic.ReificationObjectClauseHook(), nil) + return semanticBQL } diff --git a/bql/grammar/grammar_test.go b/bql/grammar/grammar_test.go index c3ea3da2..ede9e881 100644 --- a/bql/grammar/grammar_test.go +++ b/bql/grammar/grammar_test.go @@ -137,11 +137,11 @@ func TestAcceptByParse(t *testing.T) { } p, err := NewParser(BQL()) if err != nil { - t.Errorf("grammar.NewParser: should have produced a valid BQL parser, %v", err) + t.Errorf("grammar.NewParser: Should have produced a valid BQL parser, %v", err) } for _, input := range table { if err := p.Parse(NewLLk(input, 1), &semantic.Statement{}); err != nil { - t.Errorf("Parser.consume: failed to accept input %q with error %v", input, err) + t.Errorf("Parser.consume: Failed to accept input %q with error %v", input, err) } } } @@ -246,11 +246,11 @@ func TestRejectByParse(t *testing.T) { } p, err := NewParser(BQL()) if err != nil { - t.Errorf("grammar.NewParser: should have produced a valid BQL parser, %v", err) + t.Errorf("grammar.NewParser: Should have produced a valid BQL parser, %v", err) } for _, input := range table { if err := p.Parse(NewLLk(input, 1), &semantic.Statement{}); err == nil { - t.Errorf("Parser.consume: failed to reject input %q with parsing error", input) + t.Errorf("Parser.consume: Failed to reject input %q with parsing error", input) } } } @@ -288,18 +288,18 @@ func TestAcceptOpsByParseAndSemantic(t *testing.T) { } p, err := NewParser(SemanticBQL()) if err != nil { - t.Errorf("grammar.NewParser: should have produced a valid BQL parser, %v", err) + t.Errorf("grammar.NewParser: Should have produced a valid BQL parser, %v", err) } for _, entry := range table { st := &semantic.Statement{} 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) + 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) + t.Errorf("Parser.consume: Failed to collect right number of 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) + t.Errorf("Parser.consume: Failed to collect right number of triples for case %v; got %d, want %d", entry, got, want) } } } @@ -339,11 +339,11 @@ func TestAcceptQueryBySemanticParse(t *testing.T) { } p, err := NewParser(SemanticBQL()) if err != nil { - t.Errorf("grammar.NewParser: should have produced a valid BQL parser, %v", err) + t.Errorf("grammar.NewParser: Should have produced a valid BQL parser, %v", err) } for _, input := range table { if err := p.Parse(NewLLk(input, 1), &semantic.Statement{}); err != nil { - t.Errorf("Parser.consume: failed to accept input %q with error %v", input, err) + t.Errorf("Parser.consume: Failed to accept input %q with error %v", input, err) } } } @@ -370,12 +370,12 @@ func TestRejectByParseAndSemantic(t *testing.T) { } p, err := NewParser(SemanticBQL()) if err != nil { - t.Errorf("grammar.NewParser: should have produced a valid BQL parser, %v", err) + t.Errorf("grammar.NewParser: Should have produced a valid BQL parser, %v", err) } for _, entry := range table { st := &semantic.Statement{} if err := p.Parse(NewLLk(entry, 1), st); err == nil { - t.Errorf("Parser.consume: failed to reject invalid semantic entry %q", entry) + t.Errorf("Parser.consume: Failed to reject invalid semantic entry %q", entry) } } } @@ -400,15 +400,131 @@ func TestSemanticStatementGraphClausesLengthCorrectness(t *testing.T) { } p, err := NewParser(SemanticBQL()) if err != nil { - t.Errorf("grammar.NewParser: should have produced a valid BQL parser, %v", err) + t.Errorf("grammar.NewParser: Should have produced a valid BQL parser, %v", err) } for _, entry := range table { st := &semantic.Statement{} if err := p.Parse(NewLLk(entry.query, 1), st); err != nil { - t.Errorf("Parser.consume: failed to accept valid semantic entry %q", entry.query) + t.Errorf("Parser.consume: Failed to accept valid semantic entry %q", entry.query) } if got, want := len(st.GraphPatternClauses()), entry.want; got != want { t.Errorf("Invalid number of graph pattern clauses for query %q; got %d, want %d; %v", entry.query, got, want, st.GraphPatternClauses()) } } } + +func TestSemanticStatementConstructClausesLengthCorrectness(t *testing.T) { + table := []struct { + query string + want int + }{ + { + query: `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};`, + want: 1, + }, + { + query: `construct {?s "predicate_1"@[] ?o1; + "predicate_2"@[] ?o2. + ?s "predicate_3"@[] ?o3} into ?a from ?b where {?s "old_predicate_1"@[,] ?o1. + ?s "old_predicate_2"@[,] ?o2. + ?s "old_predicate_3"@[,] ?o3};`, + want: 2, + }, + } + p, err := NewParser(SemanticBQL()) + if err != nil { + t.Errorf("grammar.NewParser: Should have produced a valid BQL parser, %v", err) + } + for _, entry := range table { + st := &semantic.Statement{} + if err := p.Parse(NewLLk(entry.query, 1), st); err != nil { + t.Errorf("Parser.consume: Failed to accept valid semantic entry %q", entry.query) + } + if got, want := len(st.ConstructClauses()), entry.want; got != want { + t.Errorf("Invalid number of construct clauses for query %q; got %d, want %d; %v", entry.query, got, want, st.ConstructClauses()) + } + } +} + +func TestSemanticStatementReificationClausesLengthCorrectness(t *testing.T) { + table := []struct { + query string + want_one int + want_two int + }{ + { + query: `construct {?s "predicate_1"@[] ?o1; + "predicate_2"@[] ?o2. + ?s "predicate_3"@[] ?o3} into ?a from ?b where {?s "old_predicate_1"@[,] ?o1. + ?s "old_predicate_2"@[,] ?o2. + ?s "old_predicate_3"@[,] ?o3};`, + want_one: 1, + want_two: 0, + }, + { + query: `construct {?s "predicate_1"@[] ?o1; + "predicate_2"@[] ?o2; + "predicate_3"@[] ?o3. + ?s1 "predicate_1"@[] ?o1; + "predicate_2"@[] ?o2; + "predicate_3"@[] ?o3} into ?a from ?b where {?s "old_predicate_1"@[,] ?o1. + ?s "old_predicate_2"@[,] ?o2. + ?s1 "old_predicate_3"@[,] ?o3};`, + want_one: 2, + want_two: 2, + }, + { + query: `construct {?s "predicate_1"@[2015-07-19T13:12:04.669618843-07:00] ?o1; + "predicate_2"@[2015-07-19T13:12:04.669618843-07:00] ?o2. + ?s "predicate_3"@[2015-07-19T13:12:04.669618843-07:00] ?o3} into ?a from ?b where {?s "old_predicate_1"@[,] ?o1. + ?s "old_predicate_2"@[,] ?o2. + ?s "old_predicate_3"@[,] ?o3};`, + want_one: 1, + want_two: 0, + }, + { + query: `construct {?s "predicate_1"@[2015-07-19T13:12:04.669618843-07:00] ?o1; + "predicate_2"@[2015-07-19T13:12:04.669618843-07:00] ?o2; + "predicate_3"@[2015-07-19T13:12:04.669618843-07:00] ?o3. + ?s1 "predicate_1"@[2015-07-19T13:12:04.669618843-07:00] ?o1; + "predicate_2"@[2015-07-19T13:12:04.669618843-07:00] ?o2; + "predicate_3"@[2015-07-19T13:12:04.669618843-07:00] ?o3} into ?a from ?b where {?s "old_predicate_1"@[,] ?o1. + ?s "old_predicate_2"@[,] ?o2. + ?s1 "old_predicate_3"@[,] ?o3};`, + want_one: 2, + want_two: 2, + }, + { + query: `construct {?s "predicate_1"@[] ?o1; + "predicate_2"@[] ?o2; + "predicate_3"@[?t] ?o3. + ?s1 "predicate_1"@[] ?o1; + "predicate_2"@[] ?o2; + "predicate_3"@[?t] ?o3} into ?a from ?b where {?s "old_predicate_1"@[,] ?o1. + ?s "old_predicate_2"@[,] ?o2. + ?s1 "old_predicate_3"@[,] AT ?t ?o3};`, + want_one: 2, + want_two: 2, + }, + + } + p, err := NewParser(SemanticBQL()) + if err != nil { + t.Errorf("grammar.NewParser: Should have produced a valid BQL parser, %v", err) + } + for _, entry := range table { + st := &semantic.Statement{} + if err := p.Parse(NewLLk(entry.query, 1), st); err != nil { + t.Errorf("Parser.consume: Failed to accept valid semantic entry %q", entry.query) + } + if got, want := len(st.ConstructClauses()[0].ReificationClauses()), entry.want_one; got != want { + t.Errorf("Invalid number of reification clauses for query %q; got %d, want %d; %v", entry.query, got, want, st.ConstructClauses()[0].ReificationClauses()) + } + if got, want := len(st.ConstructClauses()[1].ReificationClauses()), entry.want_two; got != want { + t.Errorf("Invalid number of reification clauses for query %q; got %d, want %d; %v", entry.query, got, want, st.ConstructClauses()[0].ReificationClauses()) + } + } +} diff --git a/bql/semantic/hooks.go b/bql/semantic/hooks.go index f6892147..e124d54c 100644 --- a/bql/semantic/hooks.go +++ b/bql/semantic/hooks.go @@ -168,6 +168,24 @@ func ConstructObjectClauseHook() ElementHook { return constructObjectClause() } +// NextWorkingReificationClauseHook returns the singleton for adding the current reification clause +// and initializing a new reification clause within the working construct statement. +func NextWorkingReificationClauseHook() ClauseHook { + return NextWorkingReificationClause() +} + +// ReificationPredicateClauseHook returns the singleton for populating the predicate in the +// current reification clause within the working construct clause. +func ReificationPredicateClauseHook() ElementHook { + return reificationPredicateClause() +} + +// ReificationPredicateClauseHook returns the singleton for populating the object in the +// current reification clause within the working construct clause. +func ReificationObjectClauseHook() ElementHook { + return reificationObjectClause() +} + // TypeBindingClauseHook returns a ClauseHook that sets the binding type. func TypeBindingClauseHook(t StatementType) ClauseHook { var f ClauseHook @@ -1022,3 +1040,90 @@ func constructObjectClause() ElementHook { } return f } + +// NextWorkingReificationClause returns a clause hook to close the current reifcation +// clause and start a new reification clause within the working construct clause. +func NextWorkingReificationClause() ClauseHook { + var f ClauseHook + f = func(s *Statement, _ Symbol) (ClauseHook, error) { + s.WorkingConstructClause().AddWorkingReificationClause() + return f, nil + } + return f +} + +func reificationPredicateClause() ElementHook { + var f ElementHook + f = func(st *Statement, ce ConsumedElement) (ElementHook, error) { + if ce.IsSymbol() { + return f, nil + } + tkn := ce.Token() + c := st.WorkingConstructClause().WorkingReificationClause() + if c.P != nil { + return nil, fmt.Errorf("invalid predicate %v in construct clause, predicate already set to %v", tkn.Type, c.P) + } + if c.PID != "" { + return nil, fmt.Errorf("invalid predicate %v in construct clause, predicate already set to %v", tkn.Type, c.PID) + } + if c.PBinding != "" { + return nil, fmt.Errorf("invalid predicate %v in construct clause, predicate already set to %v", tkn.Type, c.PBinding) + } + switch tkn.Type { + case lexer.ItemPredicate: + p, pID, pAnchorBinding, pTemporal, err := processPredicate(ce) + if err != nil { + return nil, err + } + c.P, c.PID, c.PAnchorBinding, c.PTemporal = p, pID, pAnchorBinding, pTemporal + case lexer.ItemBinding: + c.PBinding = tkn.Text + } + return f, nil + } + return f +} + +func reificationObjectClause() ElementHook { + var f ElementHook + f = func(st *Statement, ce ConsumedElement) (ElementHook, error) { + if ce.IsSymbol() { + return f, nil + } + tkn := ce.Token() + c := st.WorkingConstructClause().WorkingReificationClause() + if c.O != nil { + return nil, fmt.Errorf("invalid object %v in construct clause, object already set to %v", tkn.Text, c.O) + } + if c.OID != "" { + return nil, fmt.Errorf("invalid object %v in construct clause, objct already set to %v", tkn.Type, c.OID) + } + if c.OBinding != "" { + return nil, fmt.Errorf("invalid object %v in construct clause, object already set to %v", tkn.Type, c.OBinding) + } + switch tkn.Type { + case lexer.ItemNode, lexer.ItemBlankNode, lexer.ItemLiteral: + obj, err := triple.ParseObject(tkn.Text, literal.DefaultBuilder()) + if err != nil { + return nil, err + } + c.O = obj + case lexer.ItemPredicate: + var ( + pred *predicate.Predicate + err error + ) + pred, c.OID, c.OAnchorBinding, c.OTemporal, err = processPredicate(ce) + if err != nil { + return nil, err + } + if pred != nil { + c.O = triple.NewPredicateObject(pred) + } + case lexer.ItemBinding: + c.OBinding = tkn.Text + } + return f, nil + } + return f +} diff --git a/bql/semantic/hooks_test.go b/bql/semantic/hooks_test.go index 2f72ce73..ff243df7 100644 --- a/bql/semantic/hooks_test.go +++ b/bql/semantic/hooks_test.go @@ -2295,3 +2295,304 @@ func TestConstructObjectClauseHook(t *testing.T) { }, }) } + +func TestNextWorkingReificationClauseHook(t *testing.T) { + f := NextWorkingReificationClause() + st := &Statement{} + st.ResetWorkingConstructClause() + wcc := st.WorkingConstructClause() + wcc.ResetWorkingReificationClause() + wrs := wcc.WorkingReificationClause() + wrs.PBinding = "?a" + f(st, Symbol("FOO")) + wrs = wcc.WorkingReificationClause() + wrs.PBinding = "?b" + f(st, Symbol("FOO")) + if got, want := len(wcc.ReificationClauses()), 2; got != want { + t.Errorf("semantic.NextReificationWorkingClause should have returned two clauses for statement %v; got %d, want %d", st, got, want) + } +} + +type testReificationClauseTable struct { + valid bool + id string + ces []ConsumedElement + want *ReificationClause +} + +func runTabulatedReificationClauseHookTest(t *testing.T, testName string, f ElementHook, table []testReificationClauseTable) { + st := &Statement{} + st.ResetWorkingConstructClause() + wcc := st.WorkingConstructClause() + wcc.ResetWorkingReificationClause() + failed := false + for _, entry := range table { + for _, ce := range entry.ces { + if _, err := f(st, ce); err != nil { + if entry.valid { + t.Errorf("%s case %q should have never failed with error: %v", testName, entry.id, err) + } else { + failed = true + } + } + } + if entry.valid { + if got, want := wcc.WorkingReificationClause(), entry.want; !reflect.DeepEqual(got, want) { + t.Errorf("%s case %q should have populated all required fields; got %+v, want %+v", testName, entry.id, got, want) + } + } else { + if !failed { + t.Errorf("%s failed to reject invalid case %q", testName, entry.id) + } + } + wcc.ResetWorkingReificationClause() + } +} + +func TestReificationPredicateClauseHook(t *testing.T) { + st := &Statement{} + f := reificationPredicateClause() + st.ResetWorkingConstructClause() + wcc := st.WorkingConstructClause() + wcc.ResetWorkingReificationClause() + ip, err := predicate.Parse(`"foo"@[]`) + if err != nil { + t.Fatalf("predicate.Parse failed with error %v", err) + } + tp, err := predicate.Parse(`"foo"@[2015-07-19T13:12:04.669618843-07:00]`) + if err != nil { + t.Fatalf("predicate.Parse failed with error %v", err) + } + runTabulatedReificationClauseHookTest(t, "semantic.reificationPredicateClause", f, []testReificationClauseTable{ + { + valid: true, + id: "valid immutable predicate", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_PREDICATE"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemPredicate, + Text: `"foo"@[]`, + }), + }, + want: &ReificationClause{ + P: ip, + PTemporal: false, + }, + }, + { + valid: true, + id: "valid temporal predicate", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_PREDICATE"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemPredicate, + Text: `"foo"@[2015-07-19T13:12:04.669618843-07:00]`, + }), + }, + want: &ReificationClause{ + P: tp, + PTemporal: true, + }, + }, + { + valid: true, + id: "valid temporal predicate with bound time anchor", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_PREDICATE"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemPredicate, + Text: `"foo"@[?bar]`, + }), + }, + want: &ReificationClause{ + PID: "foo", + PAnchorBinding: "?bar", + PTemporal: true, + }, + }, + { + valid: true, + id: "valid binding", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_PREDICATE"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?foo", + }), + }, + want: &ReificationClause{ + PBinding: "?foo", + }, + }, + { + valid: false, + id: "invalid temporal predicate and binding", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_PREDICATE"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemPredicate, + Text: `"foo"@[?bar]`, + }), + NewConsumedSymbol("FOO"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?foo", + }), + }, + want: &ReificationClause{}, + }, + }) +} + +func TestReificationObjectClauseHook(t *testing.T) { + st := &Statement{} + f := reificationObjectClause() + st.ResetWorkingConstructClause() + wcc := st.WorkingConstructClause() + wcc.ResetWorkingReificationClause() + n, err := node.Parse("/_") + if err != nil { + t.Fatalf("node.Parse failed with error %v", err) + } + no := triple.NewNodeObject(n) + bn, err := node.Parse("_:v1") + if err != nil { + t.Fatalf("node.Parse failed with error %v", err) + } + bno := triple.NewNodeObject(bn) + ip, err := predicate.Parse(`"foo"@[]`) + if err != nil { + t.Fatalf("predicate.Parse failed with error %v", err) + } + ipo := triple.NewPredicateObject(ip) + tp, err := predicate.Parse(`"foo"@[2015-07-19T13:12:04.669618843-07:00]`) + if err != nil { + t.Fatalf("predicate.Parse failed with error %v", err) + } + tpo := triple.NewPredicateObject(tp) + l, err := triple.ParseObject(`"1"^^type:int64`, literal.DefaultBuilder()) + if err != nil { + t.Fatalf("literal.Parse should never fail to parse %s with error %v", `"1"^^type:int64`, err) + } + runTabulatedReificationClauseHookTest(t, "semantic.reificationObjectClause", f, []testReificationClauseTable{ + { + valid: true, + id: "valid node object", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_OBJECT"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemNode, + Text: "/_", + }), + }, + want: &ReificationClause{ + O: no, + }, + }, + { + valid: true, + id: "valid blank node object", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_OBJECT"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBlankNode, + Text: "_:v1", + }), + }, + want: &ReificationClause{ + O: bno, + }, + }, + { + valid: true, + id: "valid literal object", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_OBJECT"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemLiteral, + Text: `"1"^^type:int64`, + }), + }, + want: &ReificationClause{ + O: l, + }, + }, + { + valid: true, + id: "valid immutable predicate object", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_OBJECT"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemPredicate, + Text: `"foo"@[]`, + }), + }, + want: &ReificationClause{ + O: ipo, + OTemporal: false, + }, + }, + { + valid: true, + id: "valid temporal predicate object", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_OBJECT"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemPredicate, + Text: `"foo"@[2015-07-19T13:12:04.669618843-07:00]`, + }), + }, + want: &ReificationClause{ + O: tpo, + OTemporal: true, + }, + }, + { + valid: true, + id: "valid temporal predicate object with bound time anchor", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_OBJECT"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemPredicate, + Text: `"foo"@[?bar]`, + }), + }, + want: &ReificationClause{ + OID: "foo", + OAnchorBinding: "?bar", + OTemporal: true, + }, + }, + { + valid: true, + id: "valid binding", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_OBJECT"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?foo", + }), + }, + want: &ReificationClause{ + OBinding: "?foo", + }, + }, + { + valid: false, + id: "invalid temporal predicate and binding objects", + ces: []ConsumedElement{ + NewConsumedSymbol("REIFICATION_OBJECT"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemPredicate, + Text: `"foo"@[?bar]`, + }), + NewConsumedSymbol("FOO"), + NewConsumedToken(&lexer.Token{ + Type: lexer.ItemBinding, + Text: "?foo", + }), + }, + want: &ReificationClause{}, + }, + }) +} diff --git a/bql/semantic/semantic.go b/bql/semantic/semantic.go index f8634677..4b6ba8f2 100644 --- a/bql/semantic/semantic.go +++ b/bql/semantic/semantic.go @@ -145,7 +145,8 @@ type ConstructClause struct { OAnchorBinding string OTemporal bool - ReificationClauses []*ReificationClause + reificationClauses []*ReificationClause + workingReificationClause *ReificationClause } // ReificationClause represents a clause used to reify a triple. @@ -383,6 +384,11 @@ func (c *ConstructClause) IsEmpty() bool { return reflect.DeepEqual(c, &ConstructClause{}) } +// IsEmpty will return true if there are no set values in the reification clause. +func (c *ReificationClause) IsEmpty() bool { + return reflect.DeepEqual(c, &ReificationClause{}) +} + // BindType sets the type of a statement. func (s *Statement) BindType(st StatementType) { s.sType = st @@ -631,16 +637,28 @@ func (s *Statement) InputBindings() []string { if c.PBinding != "" { res = append(res, c.PBinding) } + if c.PAnchorBinding != "" { + res = append(res, c.PAnchorBinding) + } if c.OBinding != "" { res = append(res, c.OBinding) } - for _, r := range c.ReificationClauses { + if c.OAnchorBinding != "" { + res = append(res, c.OAnchorBinding) + } + for _, r := range c.reificationClauses { if r.PBinding != "" { res = append(res, r.PBinding) } + if r.PAnchorBinding != "" { + res = append(res, r.PAnchorBinding) + } if r.OBinding != "" { res = append(res, r.OBinding) } + if r.OAnchorBinding != "" { + res = append(res, r.OAnchorBinding) + } } } return res @@ -710,7 +728,7 @@ func (s *Statement) ResetWorkingConstructClause() { s.workingConstructClause = &ConstructClause{} } -// WorkingConstructClause returns the current working clause. +// WorkingConstructClause returns the current working construct clause. func (s *Statement) WorkingConstructClause() *ConstructClause { return s.workingConstructClause } @@ -723,3 +741,30 @@ func (s *Statement) AddWorkingConstructClause() { } s.ResetWorkingConstructClause() } + +// ReificationClauses returns the list of reification clauses within the construct +// clause. +func (c *ConstructClause) ReificationClauses() []*ReificationClause { + return c.reificationClauses +} + +// ResetWorkingReificationClause resets the working reification clause in the +// construct clause. +func (c *ConstructClause) ResetWorkingReificationClause() { + c.workingReificationClause = &ReificationClause{} +} + +// WorkingReificationClause returns the working reification clause in the +// construct clause. +func (c *ConstructClause) WorkingReificationClause() *ReificationClause { + return c.workingReificationClause +} + +// AddWorkingReificationClause adds the working reification clause to the set +// of reification clauses belonging to the construct clause. +func (c *ConstructClause) AddWorkingReificationClause() { + if c.workingReificationClause != nil && !c.workingReificationClause.IsEmpty(){ + c.reificationClauses = append(c.reificationClauses, c.workingReificationClause) + } + c.ResetWorkingReificationClause() +} diff --git a/bql/semantic/semantic_test.go b/bql/semantic/semantic_test.go index 795a0d05..cae69686 100644 --- a/bql/semantic/semantic_test.go +++ b/bql/semantic/semantic_test.go @@ -190,6 +190,38 @@ func TestProjectionIsEmpty(t *testing.T) { } } +func TestConstructClauseManipulation(t *testing.T) { + st := &Statement{} + if st.WorkingConstructClause() != nil { + t.Fatalf("semantic.ConstructClause.WorkingConstructClause should not return a working construct clause without initialization in %v", st) + } + st.ResetWorkingConstructClause() + if st.WorkingConstructClause() == nil { + t.Fatalf("semantic.ConstructClause.WorkingConstructClause should return a working construct clause after initialization in %v", st) + } + st.AddWorkingConstructClause() + if got, want := len(st.ConstructClauses()), 0; got != want { + t.Fatalf("semantic.ConstructClause.ConstructClauses returns wrong number of clauses in %v; got %d, want %d", st, got, want) + } +} + +func TestReificationClauseManipulation(t *testing.T) { + st := &Statement{} + st.ResetWorkingConstructClause() + wcc := st.WorkingConstructClause() + if wcc.WorkingReificationClause() != nil { + t.Fatalf("semantic.ConstructClause.WorkingReificationClause should not return a working reification clause without initialization in %v", st) + } + wcc.ResetWorkingReificationClause() + if wcc.WorkingReificationClause() == nil { + t.Fatalf("semantic.ConstructClause.WorkingReificationClause should return a working reification clause after initialization in %v", st) + } + wcc.AddWorkingReificationClause() + if got, want := len(wcc.ReificationClauses()), 0; got != want { + t.Fatalf("semantic.ConstructClause.WorkingReificationClauses returns wrong number of clauses in %v; got %d, want %d", st, got, want) + } +} + func TestInputOutputBindings(t *testing.T) { s := &Statement{ projection: []*Projection{ @@ -211,7 +243,7 @@ func TestInputOutputBindings(t *testing.T) { SBinding: "?foo4", PBinding: "?foo5", OBinding: "?foo6", - ReificationClauses: []*ReificationClause{ + reificationClauses: []*ReificationClause{ { PBinding: "?foo7", OBinding: "?foo8", @@ -223,10 +255,21 @@ func TestInputOutputBindings(t *testing.T) { }, }, + { + PAnchorBinding: "?foo11", + OAnchorBinding: "?foo12", + reificationClauses: []*ReificationClause{ + { + PAnchorBinding: "?foo13", + OAnchorBinding: "?foo14", + }, + + }, + }, }, } want := []string{"?foo", "?bar", "?foo1", "?foo2", "?foo3", "?foo4", "?foo5", "?foo6", - "?foo7","?foo8", "?foo9", "?foo10"} + "?foo7","?foo8", "?foo9", "?foo10", "?foo11", "?foo12", "?foo13", "?foo14"} if got := s.InputBindings(); !reflect.DeepEqual(got, want) { t.Errorf("s.InputBindings return the wrong input binding; got %v, want %v", got, want) }