diff --git a/cmd/cayley/cayley.go b/cmd/cayley/cayley.go index 62d62bb76..a13d4dbab 100644 --- a/cmd/cayley/cayley.go +++ b/cmd/cayley/cayley.go @@ -143,6 +143,7 @@ func init() { command.NewConvertCmd(), command.NewDedupCommand(), command.NewHealthCmd(), + command.NewSchemaCommand(), ) rootCmd.PersistentFlags().StringP("config", "c", "", "path to an explicit configuration file") diff --git a/cmd/cayley/command/schema.go b/cmd/cayley/command/schema.go new file mode 100644 index 000000000..bd3bc1f64 --- /dev/null +++ b/cmd/cayley/command/schema.go @@ -0,0 +1,42 @@ +package command + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + + "github.com/cayleygraph/cayley/internal/linkedql/schema" +) + +func NewSchemaCommand() *cobra.Command { + root := &cobra.Command{ + Use: "schema", + Short: "Commands related to RDF schema", + } + root.AddCommand( + NewLinkedQLSchemaCommand(), + ) + return root +} + +func NewLinkedQLSchemaCommand() *cobra.Command { + return &cobra.Command{ + Use: "linkedql", + Short: "Generate LinkedQL Schema to stdout", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf("too many arguments provided, expected 0") + } + data := schema.Generate() + buf := bytes.NewBuffer(nil) + err := json.Indent(buf, data, "", "\t") + if err != nil { + return err + } + fmt.Println(buf) + return nil + }, + } +} diff --git a/cmd/generate_linkedql_client/generate_linkedql_client.go b/cmd/generate_linkedql_client/generate_linkedql_client.go new file mode 100644 index 000000000..59079bc18 --- /dev/null +++ b/cmd/generate_linkedql_client/generate_linkedql_client.go @@ -0,0 +1,318 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io" + "os" + + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/graph/memstore" + "github.com/cayleygraph/cayley/owl" + "github.com/cayleygraph/quad" + "github.com/cayleygraph/quad/jsonld" + "github.com/cayleygraph/quad/voc/rdfs" +) + +const schemaFile = "linkedql.json" +const outputFilePath = "query/linkedql/client/client.go" + +var stepIRI = quad.IRI("http://cayley.io/linkedql#Step") +var pathStepIRI = quad.IRI("http://cayley.io/linkedql#PathStep") +var iteratorStepIRI = quad.IRI("http://cayley.io/linkedql#IteratorStep") + +func main() { + ctx := context.TODO() + qs, err := loadSchema() + + if err != nil { + panic(err) + } + + stepClass, err := owl.GetClass(ctx, qs, stepIRI) + + if err != nil { + panic(err) + } + + stepSubClasses := stepClass.SubClasses() + var decls []ast.Decl + + for _, stepSubClass := range stepSubClasses { + if stepSubClass.Identifier == pathStepIRI || stepSubClass.Identifier == iteratorStepIRI { + continue + } + stepSubClassDecls, err := stepSubClassToDecls(stepSubClass) + if err != nil { + panic(err) + } + decls = append(decls, stepSubClassDecls...) + } + + // Create a FileSet for node. Since the node does not come + // from a real source file, fset will be empty. + fset := token.NewFileSet() + file, err := getFile(fset) + + if err != nil { + panic(err) + } + + file.Decls = append(file.Decls, decls...) + + err = writeFile(fset, file, outputFilePath) + + if err != nil { + panic(err) + } +} + +// loadSchema loads the schema file into an in-memory store +func loadSchema() (graph.QuadStore, error) { + jsonFile, err := os.Open(schemaFile) + if err != nil { + return nil, err + } + var o interface{} + qs := memstore.New() + json.NewDecoder(jsonFile).Decode(&o) + reader := jsonld.NewReaderFromMap(o) + for true { + quad, err := reader.ReadQuad() + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + qs.AddQuad(quad) + } + return qs, nil +} + +var xsdString = quad.IRI("http://www.w3.org/2001/XMLSchema#string") +var rdfsResource = quad.IRI(rdfs.Resource).Full() +var stringIdent = ast.NewIdent("string") + +var pathTypeIdent = ast.NewIdent("Path") +var pathIdent = ast.NewIdent("p") + +func stepSubClassToDecls(stepSubClass *owl.Class) ([]ast.Decl, error) { + var decls []ast.Decl + hasFrom := false + iri, ok := stepSubClass.Identifier.(quad.IRI) + if !ok { + return nil, fmt.Errorf("Unexpected class identifier %v of type %T", stepSubClass.Identifier, stepSubClass.Identifier) + } + properties := stepSubClass.Properties() + + var paramsList []*ast.Field + for _, property := range properties { + _type, err := propertyToValueType(stepSubClass, property) + if err != nil { + return nil, err + } + ident := iriToIdent(property.Identifier) + if ident.Name == "from" { + hasFrom = true + continue + } + paramsList = append(paramsList, &ast.Field{ + Names: []*ast.Ident{ident}, + Type: _type, + }) + } + elts := []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.BasicLit{ + Kind: token.STRING, + Value: "\"@type\"", + }, + Value: &ast.BasicLit{ + Kind: token.STRING, + Value: "\"" + string(iri) + "\"", + }, + }, + } + if hasFrom { + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.BasicLit{ + Kind: token.STRING, + Value: "\"from\"", + }, + Value: pathIdent, + }) + } + + for _, property := range properties { + ident := iriToIdent(property.Identifier) + if ident.Name == "from" { + continue + } + var value ast.Expr + value = iriToIdent(property.Identifier) + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.BasicLit{ + Kind: token.STRING, + Value: "\"" + string(property.Identifier) + "\"", + }, + Value: value, + }) + } + + var recv *ast.FieldList + + if hasFrom { + recv = &ast.FieldList{ + List: []*ast.Field{ + &ast.Field{ + Names: []*ast.Ident{pathIdent}, + Type: pathTypeIdent, + }, + }, + } + } + + comment, err := stepSubClass.Comment() + + var doc *ast.CommentGroup + + if err == nil { + doc = &ast.CommentGroup{ + List: []*ast.Comment{ + { + Text: "// " + iriToStringIdent(iri) + " " + comment, + }, + }, + } + } + + decls = append(decls, &ast.FuncDecl{ + Name: iriToIdent(iri), + Doc: doc, + Type: &ast.FuncType{ + Params: &ast.FieldList{List: paramsList}, + Results: &ast.FieldList{ + List: []*ast.Field{ + &ast.Field{ + Names: nil, + Type: pathTypeIdent, + }, + }, + }, + }, + Recv: recv, + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.ReturnStmt{ + Results: []ast.Expr{ + &ast.CompositeLit{ + Type: pathTypeIdent, + Elts: elts, + }, + }, + }, + }, + }, + }) + return decls, nil +} + +var quadValueType = &ast.SelectorExpr{ + Sel: ast.NewIdent("Value"), + X: ast.NewIdent("quad"), +} + +func propertyToValueType(class *owl.Class, property *owl.Property) (ast.Expr, error) { + _range, err := property.Range() + if err != nil { + return nil, err + } + isSlice := true + isPTR := false + cardinality, err := class.CardinalityOf(property) + if cardinality == int64(1) { + isSlice = false + isPTR = false + } + maxCardinality, err := class.MaxCardinalityOf(property) + if maxCardinality == int64(1) { + isSlice = false + isPTR = true + } + var t ast.Expr + if _range == xsdString { + t = stringIdent + } else if _range == pathStepIRI { + t = pathTypeIdent + } else if _range == rdfsResource { + t = quadValueType + } else { + return nil, fmt.Errorf("Unexpected range %v", _range) + } + if isPTR { + t = &ast.StarExpr{ + X: t, + } + } + if isSlice { + t = &ast.ArrayType{ + Elt: t, + } + } + return t, nil +} + +func getFile(fset *token.FileSet) (*ast.File, error) { + src := ` +package client + +import ( + "github.com/cayleygraph/quad" +) + +type Path map[string]interface{} + ` + file, err := parser.ParseFile(fset, "", src, 0) + + if err != nil { + return nil, err + } + + return file, nil +} + +// writeFile writes given file of given fset to given path +func writeFile(fset *token.FileSet, file *ast.File, path string) error { + f, err := os.Create(path) + + if err != nil { + return err + } + + w := bufio.NewWriter(f) + + err = format.Node(w, fset, file) + + if err != nil { + return err + } + + w.Flush() + f.Close() + + return nil +} + +func iriToStringIdent(iri quad.IRI) string { + return string(iri)[26:] +} + +func iriToIdent(iri quad.IRI) *ast.Ident { + return ast.NewIdent(iriToStringIdent(iri)) +} diff --git a/gogen.go b/gogen.go index c48953715..76bb7bb8b 100644 --- a/gogen.go +++ b/gogen.go @@ -1,3 +1,3 @@ package cayley -//go:generate go run ./cmd/docgen/docgen.go -i ./docs/GizmoAPI.md.in -o ./docs/GizmoAPI.md +//go:generate go run ./cmd/generate_linkedql_client/generate_linkedql_client.go diff --git a/internal/linkedql/schema/schema.go b/internal/linkedql/schema/schema.go new file mode 100644 index 000000000..c12e9c0a6 --- /dev/null +++ b/internal/linkedql/schema/schema.go @@ -0,0 +1,297 @@ +package schema + +import ( + "encoding/json" + "reflect" + + "github.com/cayleygraph/cayley/query/linkedql" + "github.com/cayleygraph/quad" + "github.com/cayleygraph/quad/voc/rdfs" +) + +var ( + pathStep = reflect.TypeOf((*linkedql.PathStep)(nil)).Elem() + iteratorStep = reflect.TypeOf((*linkedql.IteratorStep)(nil)).Elem() + value = reflect.TypeOf((*quad.Value)(nil)).Elem() + operator = reflect.TypeOf((*linkedql.Operator)(nil)).Elem() +) + +func typeToRange(t reflect.Type) string { + if t.Kind() == reflect.Slice { + return typeToRange(t.Elem()) + } + // TODO: add XSD types to voc package + if t.Kind() == reflect.String { + return "xsd:string" + } + if t.Kind() == reflect.Bool { + return "xsd:boolean" + } + if kind := t.Kind(); kind == reflect.Int64 || kind == reflect.Int { + return "xsd:int" + } + if t.Implements(pathStep) { + return linkedql.Prefix + "PathStep" + } + if t.Implements(operator) { + return linkedql.Prefix + "Operator" + } + if t.Implements(value) { + return rdfs.Resource + } + panic("Unexpected type " + t.String()) +} + +// identified is used for referencing a type +type identified struct { + ID string `json:"@id"` +} + +// newIdentified creates new identified struct +func newIdentified(id string) identified { + return identified{ID: id} +} + +// cardinalityRestriction is used to indicate a how many values can a property get +type cardinalityRestriction struct { + ID string `json:"@id"` + Type string `json:"@type"` + Cardinality int `json:"owl:cardinality"` + Property identified `json:"owl:onProperty"` +} + +func newBlankNodeID() string { + return quad.RandomBlankNode().String() +} + +// newSingleCardinalityRestriction creates a cardinality of 1 restriction for given property +func newSingleCardinalityRestriction(prop string) cardinalityRestriction { + return cardinalityRestriction{ + ID: newBlankNodeID(), + Type: "owl:Restriction", + Cardinality: 1, + Property: identified{ID: prop}, + } +} + +// getOWLPropertyType for given kind of value type returns property OWL type +func getOWLPropertyType(kind reflect.Kind) string { + if kind == reflect.String || kind == reflect.Bool || kind == reflect.Int64 || kind == reflect.Int { + return "owl:DatatypeProperty" + } + return "owl:ObjectProperty" +} + +// property is used to declare a property +type property struct { + ID string `json:"@id"` + Type string `json:"@type"` + Domain interface{} `json:"rdfs:domain"` + Range interface{} `json:"rdfs:range"` +} + +// class is used to declare a class +type class struct { + ID string `json:"@id"` + Type string `json:"@type"` + Comment string `json:"rdfs:comment"` + SuperClasses []interface{} `json:"rdfs:subClassOf"` +} + +// newClass creates a new class struct +func newClass(id string, superClasses []interface{}, comment string) class { + return class{ + ID: id, + Type: rdfs.Class, + SuperClasses: superClasses, + Comment: comment, + } +} + +// getStepTypeClasses for given step type returns the matching class identifiers +func getStepTypeClasses(t reflect.Type) []string { + var typeClasses []string + if t.Implements(pathStep) { + typeClasses = append(typeClasses, linkedql.Prefix+"PathStep") + } + if t.Implements(iteratorStep) { + typeClasses = append(typeClasses, linkedql.Prefix+"IteratorStep") + } + return typeClasses +} + +type list struct { + Members []interface{} `json:"@list"` +} + +func newList(members []interface{}) list { + return list{ + Members: members, + } +} + +type unionOf struct { + ID string `json:"@id"` + Type string `json:"@type"` + List list `json:"owl:unionOf"` +} + +func newUnionOf(classes []string) unionOf { + var members []interface{} + for _, class := range classes { + members = append(members, newIdentified(class)) + } + return unionOf{ + ID: newBlankNodeID(), + Type: "owl:Class", + List: newList(members), + } +} + +func newGenerator() *generator { + return &generator{ + propToTypes: make(map[string]map[string]struct{}), + propToDomains: make(map[string]map[string]struct{}), + propToRanges: make(map[string]map[string]struct{}), + } +} + +type generator struct { + out []interface{} + propToTypes map[string]map[string]struct{} + propToDomains map[string]map[string]struct{} + propToRanges map[string]map[string]struct{} +} + +// returns super types +func (g *generator) addTypeFields(name string, t reflect.Type, indirect bool) []interface{} { + var super []interface{} + for j := 0; j < t.NumField(); j++ { + f := t.Field(j) + if f.Anonymous { + if f.Type.Kind() != reflect.Struct || !indirect { + continue + } + super = append(super, g.addTypeFields(name, f.Type, false)...) + continue + } + prop := linkedql.Prefix + f.Tag.Get("json") + if f.Type.Kind() != reflect.Slice { + super = append(super, newSingleCardinalityRestriction(prop)) + } + typ := getOWLPropertyType(f.Type.Kind()) + + if g.propToTypes[prop] == nil { + g.propToTypes[prop] = make(map[string]struct{}) + } + g.propToTypes[prop][typ] = struct{}{} + + if g.propToDomains[prop] == nil { + g.propToDomains[prop] = make(map[string]struct{}) + } + g.propToDomains[prop][name] = struct{}{} + + if g.propToRanges[prop] == nil { + g.propToRanges[prop] = make(map[string]struct{}) + } + g.propToRanges[prop][typeToRange(f.Type)] = struct{}{} + } + return super +} + +func (g *generator) AddType(name string, t reflect.Type) { + step, ok := reflect.New(t).Interface().(linkedql.Step) + if !ok { + return + } + var super []interface{} + stepTypeClasses := getStepTypeClasses(reflect.PtrTo(t)) + for _, typeClass := range stepTypeClasses { + super = append(super, newIdentified(typeClass)) + } + super = append(super, g.addTypeFields(name, t, true)...) + g.out = append(g.out, newClass(name, super, step.Description())) +} + +func (g *generator) Generate() []byte { + for prop, types := range g.propToTypes { + if len(types) != 1 { + panic("Properties must be either object properties or datatype properties. " + prop + " has both.") + } + var typ string + for t := range types { + typ = t + break + } + var domains []string + for d := range g.propToDomains[prop] { + domains = append(domains, d) + } + var ranges []string + for r := range g.propToRanges[prop] { + ranges = append(ranges, r) + } + var dom interface{} + if len(domains) == 1 { + dom = domains[0] + } else { + dom = newUnionOf(domains) + } + var rng interface{} + if len(ranges) == 1 { + rng = newIdentified(ranges[0]) + } else { + rng = newUnionOf(ranges) + } + g.out = append(g.out, property{ + ID: prop, + Type: typ, + Domain: dom, + Range: rng, + }) + } + graph := []interface{}{ + map[string]string{ + "@id": "linkedql:Step", + "@type": "owl:Class", + }, + map[string]interface{}{ + "@id": "linkedql:PathStep", + "@type": "owl:Class", + "rdfs:subClassOf": map[string]string{"@id": "linkedql:Step"}, + }, + map[string]interface{}{ + "@id": "linkedql:IteratorStep", + "@type": "owl:Class", + "rdfs:subClassOf": map[string]string{"@id": "linkedql:Step"}, + }, + } + graph = append(graph, g.out...) + data, err := json.Marshal(map[string]interface{}{ + "@context": map[string]interface{}{ + "rdf": map[string]string{"@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#"}, + "rdfs": map[string]string{"@id": "http://www.w3.org/2000/01/rdf-schema#"}, + "owl": map[string]string{"@id": "http://www.w3.org/2002/07/owl#"}, + "xsd": map[string]string{"@id": "http://www.w3.org/2001/XMLSchema#"}, + "linkedql": map[string]string{"@id": "http://cayley.io/linkedql#"}, + }, + "@graph": graph, + }) + if err != nil { + panic(err) + } + return data +} + +// Generate a schema in JSON-LD format that contains all registered LinkedQL types and properties. +func Generate() []byte { + g := newGenerator() + for _, name := range linkedql.RegisteredTypes() { + t, ok := linkedql.TypeByName(name) + if !ok { + panic("type is registered, but the lookup fails") + } + g.AddType(name, t) + } + return g.Generate() +} diff --git a/internal/linkedql/schema/schema_test.go b/internal/linkedql/schema/schema_test.go new file mode 100644 index 000000000..ada1f6110 --- /dev/null +++ b/internal/linkedql/schema/schema_test.go @@ -0,0 +1,15 @@ +package schema + +import ( + "encoding/json" + "testing" +) + +func TestMarshalSchema(t *testing.T) { + out := Generate() + var o interface{} + err := json.Unmarshal(out, &o) + if err != nil { + t.Fatal(err) + } +} diff --git a/linkedql.json b/linkedql.json new file mode 100644 index 000000000..74a4cd838 --- /dev/null +++ b/linkedql.json @@ -0,0 +1,1171 @@ +{ + "@context": { + "linkedql": { + "@id": "http://cayley.io/linkedql#" + }, + "owl": { + "@id": "http://www.w3.org/2002/07/owl#" + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#" + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#" + } + }, + "@graph": [ + { + "@id": "linkedql:Step", + "@type": "owl:Class" + }, + { + "@id": "linkedql:PathStep", + "@type": "owl:Class", + "rdfs:subClassOf": { + "@id": "linkedql:Step" + } + }, + { + "@id": "linkedql:IteratorStep", + "@type": "owl:Class", + "rdfs:subClassOf": { + "@id": "linkedql:Step" + } + }, + { + "@id": "linkedql:Is", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to all the values resolved by the from step which are included in provided values.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3613043289672097141", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:FollowReverse", + "@type": "rdfs:Class", + "rdfs:comment": "is the same as follow but follows the chain in the reverse direction. Flips View and ViewReverse where appropriate, the net result being a virtual predicate followed in the reverse direction. Starts at the end of the morphism and follows it backwards (with appropriate flipped directions) to the g.M() location.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n9083091194127967913", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8063055307955585418", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:followed" + } + } + ] + }, + { + "@id": "linkedql:Labels", + "@type": "rdfs:Class", + "rdfs:comment": "gets the list of inbound and outbound quad labels", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8459855700265692209", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Unique", + "@type": "rdfs:Class", + "rdfs:comment": "removes duplicate values from the path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n2710973548941612352", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Vertex", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to all the existing objects and primitive values in the graph. If provided with values resolves to a sublist of all the existing values in the graph.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + } + ] + }, + { + "@id": "linkedql:Placeholder", + "@type": "rdfs:Class", + "rdfs:comment": "is like Vertex but resolves to the values in the context it is placed in. It should only be used where a PathStep is expected and can't be resolved on its own.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + } + ] + }, + { + "@id": "linkedql:Count", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to the number of the resolved values of the from step", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3236938820904012665", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Skip", + "@type": "rdfs:Class", + "rdfs:comment": "skips a number of nodes for current path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8331721404640161545", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n5928433611785803765", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:offset" + } + } + ] + }, + { + "@id": "linkedql:Back", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to the values of the previous the step or the values assigned to name in a former step.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n7318884210772532587", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n5159571487667235037", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:name" + } + } + ] + }, + { + "@id": "linkedql:Properties", + "@type": "rdfs:Class", + "rdfs:comment": "adds tags for all properties of the current entity", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n9018236344034770769", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:PropertyNames", + "@type": "rdfs:Class", + "rdfs:comment": "gets the list of predicates that are pointing out from a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n4445626993086337411", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:ReversePropertyNamesAs", + "@type": "rdfs:Class", + "rdfs:comment": "tags the list of predicates that are pointing in to a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8112324779916985026", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n5338493051676230997", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:tag" + } + } + ] + }, + { + "@id": "linkedql:PropertyNamesAs", + "@type": "rdfs:Class", + "rdfs:comment": "tags the list of predicates that are pointing out from a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n5768812609992315503", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8287472848602919900", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:tag" + } + } + ] + }, + { + "@id": "linkedql:ReverseProperties", + "@type": "rdfs:Class", + "rdfs:comment": "gets all the properties the current entity / value is referenced at", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n443301574217807823", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Select", + "@type": "rdfs:Class", + "rdfs:comment": "Select returns flat records of tags matched in the query", + "rdfs:subClassOf": [ + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8116377088366467444", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Value", + "@type": "rdfs:Class", + "rdfs:comment": "Value returns a single value matched in the query", + "rdfs:subClassOf": [ + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n1211474119861900589", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:HasReverse", + "@type": "rdfs:Class", + "rdfs:comment": "is the same as Has, but sets constraint in reverse direction.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n793190407192836422", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n7208921910592501003", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:property" + } + } + ] + }, + { + "@id": "linkedql:In", + "@type": "rdfs:Class", + "rdfs:comment": "aliases for ViewReverse", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8239793418546475073", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n4982870279148793564", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Order", + "@type": "rdfs:Class", + "rdfs:comment": "sorts the results in ascending order according to the current entity / value", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n4658869256845579334", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:ReversePropertyNames", + "@type": "rdfs:Class", + "rdfs:comment": "gets the list of predicates that are pointing in to a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n2234785206717184905", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:ViewBoth", + "@type": "rdfs:Class", + "rdfs:comment": "is like View but resolves to both the object values and references to the values of the given properties in via. It is the equivalent for the Union of View and ViewReverse of the same property.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n1375394481257745149", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8456254640763031822", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Difference", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to all the values resolved by the from step different then the values resolved by the provided steps. Caution: it might be slow to execute.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3325168672420575460", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:SelectFirst", + "@type": "rdfs:Class", + "rdfs:comment": "Like Select but only returns the first result", + "rdfs:subClassOf": [ + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3390240884242909143", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:As", + "@type": "rdfs:Class", + "rdfs:comment": "assigns the resolved values of the from step to a given name. The name can be used with the Select and Documents steps to retrieve the values or to return to the values in further steps with the Back step. It resolves to the values of the from step.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n2526633157694802657", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n3429357401325124988", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:name" + } + } + ] + }, + { + "@id": "linkedql:Filter", + "@type": "rdfs:Class", + "rdfs:comment": "applies constraints to a set of nodes. Can be used to filter values by range or match strings.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n4690243504327224094", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n739931276592480213", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:filter" + } + } + ] + }, + { + "@id": "linkedql:Has", + "@type": "rdfs:Class", + "rdfs:comment": "filters all paths which are, at this point, on the subject for the given predicate and object, but do not follow the path, merely filter the possible paths. Usually useful for starting with all nodes, or limiting to a subset depending on some predicate/value pair.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n4325294036727421714", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n632449057788695932", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:property" + } + } + ] + }, + { + "@id": "linkedql:ViewReverse", + "@type": "rdfs:Class", + "rdfs:comment": "is the inverse of View. Starting with the nodes in `path` on the object, follow the quads with predicates defined by `predicatePath` to their subjects.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n1202338489403740068", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8462290765396416742", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Limit", + "@type": "rdfs:Class", + "rdfs:comment": "limits a number of nodes for current path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8154336094443820555", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n1095931710072978339", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:limit" + } + } + ] + }, + { + "@id": "linkedql:Union", + "@type": "rdfs:Class", + "rdfs:comment": "returns the combined paths of the two queries. Notice that it's per-path, not per-node. Once again, if multiple paths reach the same destination, they might have had different ways of getting there (and different tags).", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n7940960461887459839", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:View", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to the values of the given property or properties in via of the current objects. If via is a path it's resolved values will be used as properties.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n6177289074236883404", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n4127567549237631394", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Out", + "@type": "rdfs:Class", + "rdfs:comment": "aliases for View", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n301236345131105742", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8700954704201023894", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Intersect", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to all the same values resolved by the from step and the provided steps.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n6461582695449891350", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Follow", + "@type": "rdfs:Class", + "rdfs:comment": "is the way to use a path prepared with Morphism. Applies the path chain on the morphism object to the current path. Starts as if at the g.M() and follows through the morphism path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3939936992220835105", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8565384274678116400", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:followed" + } + } + ] + }, + { + "@id": "linkedql:values", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n3487211217454711567", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Is" + }, + { + "@id": "linkedql:Vertex" + }, + { + "@id": "linkedql:HasReverse" + }, + { + "@id": "linkedql:Has" + } + ] + } + }, + "rdfs:range": { + "@id": "rdfs:Resource" + } + }, + { + "@id": "linkedql:offset", + "@type": "owl:DatatypeProperty", + "rdfs:domain": "linkedql:Skip", + "rdfs:range": { + "@id": "xsd:int" + } + }, + { + "@id": "linkedql:tag", + "@type": "owl:DatatypeProperty", + "rdfs:domain": { + "@id": "_:n6718853691878301872", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:ReversePropertyNamesAs" + }, + { + "@id": "linkedql:PropertyNamesAs" + } + ] + } + }, + "rdfs:range": { + "@id": "xsd:string" + } + }, + { + "@id": "linkedql:tags", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n2724656406774357364", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Select" + }, + { + "@id": "linkedql:SelectFirst" + } + ] + } + }, + "rdfs:range": { + "@id": "xsd:string" + } + }, + { + "@id": "linkedql:steps", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n7733331238921588911", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Difference" + }, + { + "@id": "linkedql:Union" + }, + { + "@id": "linkedql:Intersect" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + }, + { + "@id": "linkedql:filter", + "@type": "owl:ObjectProperty", + "rdfs:domain": "linkedql:Filter", + "rdfs:range": { + "@id": "linkedql:Operator" + } + }, + { + "@id": "linkedql:limit", + "@type": "owl:DatatypeProperty", + "rdfs:domain": "linkedql:Limit", + "rdfs:range": { + "@id": "xsd:int" + } + }, + { + "@id": "linkedql:from", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n6836387890704961384", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:ReversePropertyNamesAs" + }, + { + "@id": "linkedql:Value" + }, + { + "@id": "linkedql:HasReverse" + }, + { + "@id": "linkedql:Has" + }, + { + "@id": "linkedql:View" + }, + { + "@id": "linkedql:Unique" + }, + { + "@id": "linkedql:Count" + }, + { + "@id": "linkedql:Properties" + }, + { + "@id": "linkedql:ReverseProperties" + }, + { + "@id": "linkedql:ReversePropertyNames" + }, + { + "@id": "linkedql:Filter" + }, + { + "@id": "linkedql:ViewReverse" + }, + { + "@id": "linkedql:In" + }, + { + "@id": "linkedql:Difference" + }, + { + "@id": "linkedql:Out" + }, + { + "@id": "linkedql:FollowReverse" + }, + { + "@id": "linkedql:Labels" + }, + { + "@id": "linkedql:PropertyNames" + }, + { + "@id": "linkedql:Order" + }, + { + "@id": "linkedql:As" + }, + { + "@id": "linkedql:Union" + }, + { + "@id": "linkedql:Back" + }, + { + "@id": "linkedql:PropertyNamesAs" + }, + { + "@id": "linkedql:Intersect" + }, + { + "@id": "linkedql:Is" + }, + { + "@id": "linkedql:Skip" + }, + { + "@id": "linkedql:Select" + }, + { + "@id": "linkedql:ViewBoth" + }, + { + "@id": "linkedql:SelectFirst" + }, + { + "@id": "linkedql:Limit" + }, + { + "@id": "linkedql:Follow" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + }, + { + "@id": "linkedql:followed", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n1351742506042781062", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:FollowReverse" + }, + { + "@id": "linkedql:Follow" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + }, + { + "@id": "linkedql:name", + "@type": "owl:DatatypeProperty", + "rdfs:domain": { + "@id": "_:n7858581684545641474", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Back" + }, + { + "@id": "linkedql:As" + } + ] + } + }, + "rdfs:range": { + "@id": "xsd:string" + } + }, + { + "@id": "linkedql:names", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n9117289071709940667", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Properties" + }, + { + "@id": "linkedql:ReverseProperties" + } + ] + } + }, + "rdfs:range": { + "@id": "xsd:string" + } + }, + { + "@id": "linkedql:property", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n722017989461747795", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:HasReverse" + }, + { + "@id": "linkedql:Has" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + }, + { + "@id": "linkedql:properties", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n5924667912709065573", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:In" + }, + { + "@id": "linkedql:ViewBoth" + }, + { + "@id": "linkedql:ViewReverse" + }, + { + "@id": "linkedql:View" + }, + { + "@id": "linkedql:Out" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + } + ] +} diff --git a/owl/owl.go b/owl/owl.go new file mode 100644 index 000000000..d9eaf6e0f --- /dev/null +++ b/owl/owl.go @@ -0,0 +1,234 @@ +package owl + +import ( + "context" + "fmt" + + "github.com/cayleygraph/cayley/clog" + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/graph/iterator" + "github.com/cayleygraph/cayley/query/path" + "github.com/cayleygraph/quad" + "github.com/cayleygraph/quad/voc/rdf" + "github.com/cayleygraph/quad/voc/rdfs" +) + +type Class struct { + ctx context.Context + qs graph.QuadStore + ref graph.Ref + Identifier quad.Value +} + +func (c *Class) path() *path.Path { + return path.StartPath(c.qs, c.Identifier) +} + +// listContainingPath returns a path of lists containing given value +func listContainignPath(qs graph.QuadStore, value quad.Value) *path.Path { + firstPath := path.StartPath(qs, value).In(quad.IRI(rdf.First).Full()) + return firstPath.Or( + firstPath.FollowRecursive(path.StartMorphism().In(quad.IRI(rdf.Rest).Full()), 0, nil), + ) +} + +func classFromRef(ctx context.Context, qs graph.QuadStore, ref graph.Ref) *Class { + val := qs.NameOf(ref) + return &Class{ + ctx: ctx, + qs: qs, + ref: ref, + Identifier: val, + } +} + +var domain = quad.IRI(rdfs.Domain).Full() + +// Properties return all the properties a class instance may have +func (c *Class) Properties() []*Property { + // TODO(@iddan): check for super classes properties + p := c.path().Or(listContainignPath(c.qs, c.Identifier).In(quad.IRI(UnionOf))). + In(domain) + it := p.BuildIterator(c.ctx).Iterate() + var properties []*Property + for it.Next(c.ctx) { + property, err := propertyFromRef(c.ctx, c.qs, it.Result()) + if err != nil { + clog.Warningf(err.Error()) + continue + } + properties = append(properties, property) + } + return properties +} + +func (c *Class) ParentClasses() []*Class { + it := parentClassesPath(c).BuildIterator(c.ctx).Iterate() + var classes []*Class + for it.Next(c.ctx) { + class := classFromRef(c.ctx, c.qs, it.Result()) + classes = append(classes, class) + } + return classes +} + +var rdfsComment = quad.IRI("rdfs:comment").Full() + +// Comment returns classs's comment +func (c *Class) Comment() (string, error) { + it := c.path().Out(rdfsComment).BuildIterator(c.ctx).Iterate() + for it.Next(c.ctx) { + ref := it.Result() + value := c.qs.NameOf(ref) + stringValue, ok := value.(quad.String) + if ok { + return string(stringValue), nil + } + typedStringValue, ok := value.(quad.TypedString) + if ok { + return string(typedStringValue.Value), nil + } + + } + return "", fmt.Errorf("No comment exist for %v", c.Identifier) +} + +func parentClassesPath(c *Class) *path.Path { + return c.path().Out(quad.IRI(rdfs.SubClassOf).Full()) +} + +func restrictionsPath(c *Class) *path.Path { + return parentClassesPath(c). + Has(quad.IRI(rdf.Type).Full(), quad.IRI(Restriction)) +} + +func allPropertyRestrictionsPath(c *Class, property *Property) *path.Path { + return restrictionsPath(c). + Has(quad.IRI(OnProperty), property.Identifier) +} + +func propertyRestrictionPath(c *Class, property *Property, restrictionProperty quad.IRI) *path.Path { + return allPropertyRestrictionsPath(c, property). + Out(restrictionProperty) +} + +func intFromScanner(ctx context.Context, it iterator.Scanner, qs graph.QuadStore) (int64, error) { + for it.Next(ctx) { + ref := it.Result() + value := qs.NameOf(ref) + intValue, ok := value.(quad.Int) + var native interface{} + if ok { + native = intValue.Native() + } + typedString, ok := value.(quad.TypedString) + if ok { + native = typedString.Native() + } + if native == nil { + return -1, fmt.Errorf("Unexpected value %v of type %T", value, value) + } + i, ok := native.(int64) + if !ok { + return -1, fmt.Errorf("Unexpected value %v of type %T", native, native) + } + return i, nil + } + return -1, fmt.Errorf("Iterator has not emitted any value") +} + +// CardinalityOf returns the defined exact cardinality for the property for the class +// If exact cardinality is not defined for the class returns an error +func (c *Class) CardinalityOf(property *Property) (int64, error) { + p := propertyRestrictionPath(c, property, quad.IRI(Cardinality)) + it := p.BuildIterator(c.ctx).Iterate() + cardinality, err := intFromScanner(c.ctx, it, c.qs) + if err != nil { + return -1, fmt.Errorf("No cardinality is defined for property %v for class %v", property.Identifier, c.Identifier) + } + return cardinality, nil +} + +// MaxCardinalityOf returns the defined max cardinality for the property for the class +// If max cardinality is not defined for the class returns an error +func (c *Class) MaxCardinalityOf(property *Property) (int64, error) { + p := propertyRestrictionPath(c, property, quad.IRI(MaxCardinality)) + it := p.BuildIterator(c.ctx).Iterate() + cardinality, err := intFromScanner(c.ctx, it, c.qs) + if err != nil { + return -1, fmt.Errorf("No maxCardinality is defined for property %v for class %v", property.Identifier, c.Identifier) + } + return cardinality, nil +} + +var subClassOf = quad.IRI(rdfs.SubClassOf).Full() + +// SubClasses returns all the classes defined as sub classes of the class +func (c *Class) SubClasses() []*Class { + p := c.path().FollowRecursive(path.StartMorphism().In(subClassOf), 0, nil) + it := p.BuildIterator(c.ctx).Iterate() + var subClasses []*Class + for it.Next(c.ctx) { + class := classFromRef(c.ctx, c.qs, it.Result()) + subClasses = append(subClasses, class) + } + return subClasses +} + +// GetClass returns for given identifier a class object representing a class defined in given store. +// If the identifier is not of a class in the store returns an error. +func GetClass(ctx context.Context, qs graph.QuadStore, identifier quad.IRI) (*Class, error) { + ref := qs.ValueOf(identifier) + if ref == nil { + return nil, fmt.Errorf("Identifier %v does not exist in the store", identifier) + } + // TODO(iddan): validate given identifier is an OWL class + return &Class{Identifier: identifier, ref: ref, qs: qs, ctx: ctx}, nil +} + +type Property struct { + ctx context.Context + qs graph.QuadStore + ref graph.Ref + Identifier quad.IRI +} + +func GetProperty(ctx context.Context, qs graph.QuadStore, identifier quad.IRI) (*Property, error) { + ref := qs.ValueOf(identifier) + if ref == nil { + return nil, fmt.Errorf("Identifier %v does not exist in the store", identifier) + } + // TODO(iddan): validate given identifier is an OWL property + return &Property{ + ctx: ctx, + qs: qs, + ref: ref, + Identifier: identifier, + }, nil +} + +func propertyFromRef(ctx context.Context, qs graph.QuadStore, ref graph.Ref) (*Property, error) { + val := qs.NameOf(ref) + iri, ok := val.(quad.IRI) + if !ok { + return nil, fmt.Errorf("Predicate of unexpected type %T. Predicates should be IRIs", val) + } + return &Property{ + ctx: ctx, + qs: qs, + ref: ref, + Identifier: iri, + }, nil +} + +// Range returns the expected target type of a property +func (p *Property) Range() (quad.Value, error) { + rangePath := path.StartPath(p.qs, p.Identifier).Out(quad.IRI(rdfs.Range).Full()) + it := rangePath.BuildIterator(p.ctx).Iterate() + for it.Next(p.ctx) { + ref := it.Result() + value := p.qs.NameOf(ref) + return value, nil + } + return nil, fmt.Errorf("No range was defined for property %v", p) +} diff --git a/owl/owl_test.go b/owl/owl_test.go new file mode 100644 index 000000000..815b145ea --- /dev/null +++ b/owl/owl_test.go @@ -0,0 +1,299 @@ +package owl + +import ( + "context" + "testing" + + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/graph/memstore" + "github.com/cayleygraph/cayley/query/path" + "github.com/cayleygraph/quad" + "github.com/cayleygraph/quad/voc/rdf" + "github.com/cayleygraph/quad/voc/rdfs" + "github.com/stretchr/testify/require" +) + +var fooID = quad.IRI("ex:Foo").Full() +var barID = quad.IRI("ex:Bar").Full() +var garID = quad.IRI("ex:Gar").Full() +var bazID = quad.IRI("ex:baz").Full() +var fooBarGarUnion = quad.RandomBlankNode() +var fooBazCardinalityRestriction = quad.RandomBlankNode() +var barBazMaxCardinalityRestriction = quad.RandomBlankNode() +var exampleGraph = quad.IRI("ex:graph") +var fooClassQuads = []quad.Quad{ + quad.Quad{ + Subject: fooID, + Predicate: quad.IRI(rdf.Type).Full(), + Object: quad.IRI(rdfs.Class).Full(), + Label: exampleGraph, + }, +} +var bazPropertyQuads = []quad.Quad{ + quad.Quad{ + Subject: barID, + Predicate: quad.IRI(rdfs.SubClassOf).Full(), + Object: fooID, + Label: exampleGraph, + }, + + quad.Quad{ + Subject: bazID, + Predicate: quad.IRI(rdfs.Domain).Full(), + Object: fooID, + Label: exampleGraph, + }, + quad.Quad{ + Subject: bazID, + Predicate: quad.IRI(rdfs.Range).Full(), + Object: barID, + Label: exampleGraph, + }, +} +var fooBazCardinalityRestrictionQuads = []quad.Quad{ + quad.Quad{ + Subject: fooBazCardinalityRestriction, + Predicate: quad.IRI(rdf.Type).Full(), + Object: quad.IRI(Restriction), + Label: exampleGraph, + }, + quad.Quad{ + Subject: fooBazCardinalityRestriction, + Predicate: quad.IRI(OnProperty), + Object: bazID, + Label: exampleGraph, + }, + quad.Quad{ + Subject: fooBazCardinalityRestriction, + Predicate: quad.IRI(Cardinality), + Object: quad.Int(1), + Label: exampleGraph, + }, + quad.Quad{ + Subject: fooID, + Predicate: quad.IRI(rdfs.SubClassOf).Full(), + Object: fooBazCardinalityRestriction, + Label: exampleGraph, + }, +} +var barBazCardinalityRestrictionQuad = []quad.Quad{ + quad.Quad{ + Subject: barBazMaxCardinalityRestriction, + Predicate: quad.IRI(rdf.Type).Full(), + Object: quad.IRI(Restriction), + Label: exampleGraph, + }, + quad.Quad{ + Subject: barBazMaxCardinalityRestriction, + Predicate: quad.IRI(OnProperty), + Object: bazID, + Label: exampleGraph, + }, + quad.Quad{ + Subject: barBazMaxCardinalityRestriction, + Predicate: quad.IRI(MaxCardinality), + Object: quad.Int(1), + Label: exampleGraph, + }, + quad.Quad{ + Subject: barID, + Predicate: quad.IRI(rdfs.SubClassOf).Full(), + Object: barBazMaxCardinalityRestriction, + Label: exampleGraph, + }, +} + +func listQuads(items []quad.Value, label quad.Value) (quad.Value, []quad.Quad) { + var quads []quad.Quad + list := quad.RandomBlankNode() + cursor := list + for i, item := range items { + first := quad.Quad{ + Subject: cursor, + Predicate: quad.IRI(rdf.First).Full(), + Object: item, + Label: label, + } + var rest quad.Quad + if i < len(items)-1 { + rest = quad.Quad{ + Subject: cursor, + Predicate: quad.IRI(rdf.Rest).Full(), + Object: quad.IRI(rdf.Nil).Full(), + Label: label, + } + } else { + nextCursor := quad.RandomBlankNode() + rest = quad.Quad{ + Subject: cursor, + Predicate: quad.IRI(rdf.Rest).Full(), + Object: nextCursor, + Label: label, + } + cursor = nextCursor + } + quads = append(quads, first, rest) + } + return list, quads +} + +func getUnionQuads() []quad.Quad { + var unionQuads []quad.Quad + membersList, membersQuads := listQuads( + []quad.Value{fooID, barID, garID}, + exampleGraph, + ) + unionQuads = append(unionQuads, membersQuads...) + unionQuads = append(unionQuads, quad.Quad{ + Subject: fooBarGarUnion, + Predicate: quad.IRI(UnionOf), + Object: membersList, + Label: exampleGraph, + }) + return unionQuads +} + +func getTestSet() []quad.Quad { + var testSet []quad.Quad + testSet = append(testSet, fooBazCardinalityRestrictionQuads...) + testSet = append(testSet, barBazCardinalityRestrictionQuad...) + testSet = append(testSet, bazPropertyQuads...) + testSet = append(testSet, getUnionQuads()...) + return testSet +} + +func TestListContainingPath(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + p := listContainignPath(qs, fooID).In(quad.IRI(UnionOf)) + values := collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBarGarUnion, + }, values) + p = listContainignPath(qs, barID).In(quad.IRI(UnionOf)) + values = collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBarGarUnion, + }, values) + p = listContainignPath(qs, garID).In(quad.IRI(UnionOf)) + values = collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBarGarUnion, + }, values) + p = listContainignPath(qs, bazID).In(quad.IRI(UnionOf)) + values = collectPath(ctx, qs, p) + require.Equal(t, []quad.Value(nil), values) +} + +func TestGetClass(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + class, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + require.Equal(t, class.Identifier, fooID) +} + +func TestSubClasses(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + barClass, err := GetClass(ctx, qs, barID) + require.NoError(t, err) + subClasses := fooClass.SubClasses() + require.Len(t, subClasses, 1) + require.Contains(t, subClasses, barClass) +} + +func TestProperties(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + properties := fooClass.Properties() + require.Len(t, properties, 1) + require.Contains(t, properties, bazProperty) +} + +func TestParentClasses(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + properties := fooClass.Properties() + require.Len(t, properties, 1) + require.Contains(t, properties, bazProperty) +} + +func TestCardinalityOf(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + cardinality, err := fooClass.CardinalityOf(bazProperty) + require.NoError(t, err) + require.Equal(t, cardinality, int64(1)) +} + +func TestMaxCardinalityOf(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, barID) + require.NoError(t, err) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + cardinality, err := fooClass.MaxCardinalityOf(bazProperty) + require.NoError(t, err) + require.Equal(t, cardinality, int64(1)) +} + +func TestRange(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + _range, err := bazProperty.Range() + require.NoError(t, err) + require.Equal(t, _range, barID) +} + +func collectPath(ctx context.Context, qs graph.QuadStore, p *path.Path) []quad.Value { + var values []quad.Value + it := p.BuildIterator(ctx).Iterate() + for it.Next(ctx) { + ref := it.Result() + value := qs.NameOf(ref) + values = append(values, value) + } + return values +} + +func TestParentClassesPath(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + p := parentClassesPath(fooClass) + values := collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBazCardinalityRestriction, + }, values) +} + +func TestRestrictionsPath(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + p := restrictionsPath(fooClass) + values := collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBazCardinalityRestriction, + }, values) +} diff --git a/owl/voc.go b/owl/voc.go new file mode 100644 index 000000000..7af2655ff --- /dev/null +++ b/owl/voc.go @@ -0,0 +1,21 @@ +// Package owl contains constants of the Web Ontology Language (OWL) +package owl + +import "github.com/cayleygraph/quad/voc" + +func init() { + voc.RegisterPrefix(Prefix, NS) +} + +const ( + NS = `http://www.w3.org/2002/07/owl#` + Prefix = `owl:` +) + +const ( + UnionOf = NS + "unionOf" + Restriction = NS + "Restriction" + OnProperty = NS + "onProperty" + Cardinality = NS + "cardinality" + MaxCardinality = NS + "maxCardinality" +) diff --git a/query/linkedql/client/client.go b/query/linkedql/client/client.go new file mode 100644 index 000000000..c491eef72 --- /dev/null +++ b/query/linkedql/client/client.go @@ -0,0 +1,172 @@ +package client + +import ( + "github.com/cayleygraph/quad" +) + +type Path map[string]interface{} + +// As assigns the resolved values of the from step to a given name. The name can be used with the Select and Documents steps to retrieve the values or to return to the values in further steps with the Back step. It resolves to the values of the from step. +func (p Path) As(name string) Path { + return Path{"@type": "http://cayley.io/linkedql#As", "from": p, "http://cayley.io/linkedql#name": name} +} + +// Back resolves to the values of the previous the step or the values assigned to name in a former step. +func (p Path) Back(name string) Path { + return Path{"@type": "http://cayley.io/linkedql#Back", "from": p, "http://cayley.io/linkedql#name": name} +} + +// Count resolves to the number of the resolved values of the from step +func (p Path) Count() Path { + return Path{"@type": "http://cayley.io/linkedql#Count", "from": p} +} + +// Difference resolves to all the values resolved by the from step different then the values resolved by the provided steps. Caution: it might be slow to execute. +func (p Path) Difference(steps []Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Difference", "from": p, "http://cayley.io/linkedql#steps": steps} +} + +// Filter applies constraints to a set of nodes. Can be used to filter values by range or match strings. +func (p Path) Filter() Path { + return Path{"@type": "http://cayley.io/linkedql#Filter", "from": p} +} + +// Follow is the way to use a path prepared with Morphism. Applies the path chain on the morphism object to the current path. Starts as if at the g.M() and follows through the morphism path. +func (p Path) Follow(followed Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Follow", "from": p, "http://cayley.io/linkedql#followed": followed} +} + +// FollowReverse is the same as follow but follows the chain in the reverse direction. Flips View and ViewReverse where appropriate, the net result being a virtual predicate followed in the reverse direction. Starts at the end of the morphism and follows it backwards (with appropriate flipped directions) to the g.M() location. +func (p Path) FollowReverse(followed Path) Path { + return Path{"@type": "http://cayley.io/linkedql#FollowReverse", "from": p, "http://cayley.io/linkedql#followed": followed} +} + +// Has filters all paths which are, at this point, on the subject for the given predicate and object, but do not follow the path, merely filter the possible paths. Usually useful for starting with all nodes, or limiting to a subset depending on some predicate/value pair. +func (p Path) Has(property Path, values []quad.Value) Path { + return Path{"@type": "http://cayley.io/linkedql#Has", "from": p, "http://cayley.io/linkedql#property": property, "http://cayley.io/linkedql#values": values} +} + +// HasReverse is the same as Has, but sets constraint in reverse direction. +func (p Path) HasReverse(property Path, values []quad.Value) Path { + return Path{"@type": "http://cayley.io/linkedql#HasReverse", "from": p, "http://cayley.io/linkedql#property": property, "http://cayley.io/linkedql#values": values} +} + +// In aliases for ViewReverse +func (p Path) In(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#In", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// Intersect resolves to all the same values resolved by the from step and the provided steps. +func (p Path) Intersect(steps []Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Intersect", "from": p, "http://cayley.io/linkedql#steps": steps} +} + +// Is resolves to all the values resolved by the from step which are included in provided values. +func (p Path) Is(values []quad.Value) Path { + return Path{"@type": "http://cayley.io/linkedql#Is", "from": p, "http://cayley.io/linkedql#values": values} +} + +// Labels gets the list of inbound and outbound quad labels +func (p Path) Labels() Path { + return Path{"@type": "http://cayley.io/linkedql#Labels", "from": p} +} + +// Limit limits a number of nodes for current path. +func (p Path) Limit() Path { + return Path{"@type": "http://cayley.io/linkedql#Limit", "from": p} +} + +// Order sorts the results in ascending order according to the current entity / value +func (p Path) Order() Path { + return Path{"@type": "http://cayley.io/linkedql#Order", "from": p} +} + +// Out aliases for View +func (p Path) Out(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Out", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// Properties adds tags for all properties of the current entity +func (p Path) Properties(names []string) Path { + return Path{"@type": "http://cayley.io/linkedql#Properties", "from": p, "http://cayley.io/linkedql#names": names} +} + +// PropertyNames gets the list of predicates that are pointing out from a node. +func (p Path) PropertyNames() Path { + return Path{"@type": "http://cayley.io/linkedql#PropertyNames", "from": p} +} + +// PropertyNamesAs tags the list of predicates that are pointing out from a node. +func (p Path) PropertyNamesAs(tag string) Path { + return Path{"@type": "http://cayley.io/linkedql#PropertyNamesAs", "from": p, "http://cayley.io/linkedql#tag": tag} +} + +// ReverseProperties gets all the properties the current entity / value is referenced at +func (p Path) ReverseProperties(names []string) Path { + return Path{"@type": "http://cayley.io/linkedql#ReverseProperties", "from": p, "http://cayley.io/linkedql#names": names} +} + +// ReversePropertyNames gets the list of predicates that are pointing in to a node. +func (p Path) ReversePropertyNames() Path { + return Path{"@type": "http://cayley.io/linkedql#ReversePropertyNames", "from": p} +} + +// ReversePropertyNamesAs tags the list of predicates that are pointing in to a node. +func (p Path) ReversePropertyNamesAs(tag string) Path { + return Path{"@type": "http://cayley.io/linkedql#ReversePropertyNamesAs", "from": p, "http://cayley.io/linkedql#tag": tag} +} + +// Select Select returns flat records of tags matched in the query +func (p Path) Select(tags []string) Path { + return Path{"@type": "http://cayley.io/linkedql#Select", "from": p, "http://cayley.io/linkedql#tags": tags} +} + +// SelectFirst Like Select but only returns the first result +func (p Path) SelectFirst(tags []string) Path { + return Path{"@type": "http://cayley.io/linkedql#SelectFirst", "from": p, "http://cayley.io/linkedql#tags": tags} +} + +// Skip skips a number of nodes for current path. +func (p Path) Skip() Path { + return Path{"@type": "http://cayley.io/linkedql#Skip", "from": p} +} + +// Union returns the combined paths of the two queries. Notice that it's per-path, not per-node. Once again, if multiple paths reach the same destination, they might have had different ways of getting there (and different tags). +func (p Path) Union(steps []Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Union", "from": p, "http://cayley.io/linkedql#steps": steps} +} + +// Unique removes duplicate values from the path. +func (p Path) Unique() Path { + return Path{"@type": "http://cayley.io/linkedql#Unique", "from": p} +} + +// Value Value returns a single value matched in the query +func (p Path) Value() Path { + return Path{"@type": "http://cayley.io/linkedql#Value", "from": p} +} + +// Vertex resolves to all the existing objects and primitive values in the graph. If provided with values resolves to a sublist of all the existing values in the graph. +func Vertex(values []quad.Value) Path { + return Path{"@type": "http://cayley.io/linkedql#Vertex", "http://cayley.io/linkedql#values": values} +} + +// View resolves to the values of the given property or properties in via of the current objects. If via is a path it's resolved values will be used as properties. +func (p Path) View(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#View", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// ViewBoth is like View but resolves to both the object values and references to the values of the given properties in via. It is the equivalent for the Union of View and ViewReverse of the same property. +func (p Path) ViewBoth(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#ViewBoth", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// ViewReverse is the inverse of View. Starting with the nodes in `path` on the object, follow the quads with predicates defined by `predicatePath` to their subjects. +func (p Path) ViewReverse(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#ViewReverse", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// Placeholder is like Vertex but resolves to the values in the context it is placed in. It should only be used where a PathStep is expected and can't be resolved on its own. +func Placeholder() Path { + return Path{"@type": "http://cayley.io/linkedql#Placeholder"} +} diff --git a/query/linkedql/final_steps.go b/query/linkedql/final_steps.go new file mode 100644 index 000000000..69caea264 --- /dev/null +++ b/query/linkedql/final_steps.go @@ -0,0 +1,123 @@ +package linkedql + +import ( + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/query" + "github.com/cayleygraph/quad" +) + +func init() { + Register(&Select{}) + Register(&SelectFirst{}) + Register(&Value{}) +} + +var _ IteratorStep = (*Select)(nil) + +// Select corresponds to .select(). +type Select struct { + Tags []string `json:"tags"` + From PathStep `json:"from"` +} + +// Type implements Step. +func (s *Select) Type() quad.IRI { + return Prefix + "Select" +} + +// Description implements Step. +func (s *Select) Description() string { + return "Select returns flat records of tags matched in the query" +} + +// BuildIterator implements IteratorStep +func (s *Select) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + valueIt, err := NewValueIteratorFromPathStep(s.From, qs) + if err != nil { + return nil, err + } + return &TagsIterator{valueIt: valueIt, selected: s.Tags}, nil +} + +var _ IteratorStep = (*SelectFirst)(nil) + +// SelectFirst corresponds to .selectFirst(). +type SelectFirst struct { + Tags []string `json:"tags"` + From PathStep `json:"from"` +} + +// Type implements Step. +func (s *SelectFirst) Type() quad.IRI { + return Prefix + "SelectFirst" +} + +// Description implements Step. +func (s *SelectFirst) Description() string { + return "Like Select but only returns the first result" +} + +func singleValueIteratorFromPathStep(step PathStep, qs graph.QuadStore) (*ValueIterator, error) { + p, err := step.BuildPath(qs) + if err != nil { + return nil, err + } + return NewValueIterator(p.Limit(1), qs), nil +} + +// BuildIterator implements IteratorStep +func (s *SelectFirst) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + it, err := singleValueIteratorFromPathStep(s.From, qs) + if err != nil { + return nil, err + } + return &TagsIterator{it, s.Tags}, nil +} + +var _ IteratorStep = (*Value)(nil) + +// Value corresponds to .value(). +type Value struct { + From PathStep `json:"from"` +} + +// Type implements Step. +func (s *Value) Type() quad.IRI { + return Prefix + "Value" +} + +// Description implements Step. +func (s *Value) Description() string { + return "Value returns a single value matched in the query" +} + +// BuildIterator implements IteratorStep +func (s *Value) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return singleValueIteratorFromPathStep(s.From, qs) +} + +var _ IteratorStep = (*Documents)(nil) + +// Documents corresponds to .documents(). +type Documents struct { + From DocumentStep `json:"from"` +} + +// Type implements Step. +func (s *Documents) Type() quad.IRI { + return Prefix + "Documents" +} + +// Description implements Step. +func (s *Documents) Description() string { + return "Documents return documents of the tags matched in the query associated with their entity" +} + +// BuildIterator implements IteratorStep +func (s *Documents) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + it, err := s.From.BuildDocumentIterator(qs) + if err != nil { + return nil, err + } + return it, nil +} diff --git a/query/linkedql/iter_docs.go b/query/linkedql/iter_docs.go new file mode 100644 index 000000000..22b8b35db --- /dev/null +++ b/query/linkedql/iter_docs.go @@ -0,0 +1,98 @@ +package linkedql + +import ( + "context" + + "github.com/cayleygraph/cayley/graph/iterator" + "github.com/cayleygraph/cayley/graph/refs" + "github.com/cayleygraph/cayley/query" + "github.com/cayleygraph/cayley/query/path" + "github.com/cayleygraph/quad" +) + +var _ query.Iterator = (*DocumentIterator)(nil) + +type document = map[string]interface{} + +// DocumentIterator is an iterator of documents from the graph +type DocumentIterator struct { + qs refs.Namer + path *path.Path + ids []quad.Value + scanner iterator.Scanner + properties map[quad.Value]map[string][]quad.Value + current int +} + +// NewDocumentIterator returns a new DocumentIterator for a QuadStore and Path. +func NewDocumentIterator(qs refs.Namer, p *path.Path) *DocumentIterator { + return &DocumentIterator{qs: qs, path: p, current: -1} +} + +// Next implements query.Iterator. +func (it *DocumentIterator) Next(ctx context.Context) bool { + if it.properties == nil { + it.properties = make(map[quad.Value]map[string][]quad.Value) + it.scanner = it.path.BuildIterator(ctx).Iterate() + for it.scanner.Next(ctx) { + id := it.qs.NameOf(it.scanner.Result()) + it.ids = append(it.ids, id) + + tags := make(map[string]refs.Ref) + it.scanner.TagResults(tags) + + for k, ref := range tags { + value := it.qs.NameOf(ref) + m, ok := it.properties[id] + if !ok { + m = make(map[string][]quad.Value) + it.properties[id] = m + } + m[k] = append(m[k], value) + } + } + } + if it.current < len(it.ids)-1 { + it.current++ + return true + } + return false +} + +// Result implements query.Iterator. +func (it *DocumentIterator) Result() interface{} { + if it.current >= len(it.ids) { + return nil + } + id := it.ids[it.current] + var sid string + switch val := id.(type) { + case quad.IRI: + sid = string(val) + case quad.BNode: + sid = val.String() + } + d := document{ + "@id": sid, + } + for k, v := range it.properties[id] { + d[k] = v + } + return d +} + +// Err implements query.Iterator. +func (it *DocumentIterator) Err() error { + if it.scanner == nil { + return nil + } + return it.scanner.Err() +} + +// Close implements query.Iterator. +func (it *DocumentIterator) Close() error { + if it.scanner == nil { + return nil + } + return it.scanner.Close() +} diff --git a/query/linkedql/iter_tags.go b/query/linkedql/iter_tags.go new file mode 100644 index 000000000..ecad68315 --- /dev/null +++ b/query/linkedql/iter_tags.go @@ -0,0 +1,51 @@ +package linkedql + +import ( + "context" + + "github.com/cayleygraph/cayley/graph/refs" + "github.com/cayleygraph/cayley/query" + "github.com/cayleygraph/quad" +) + +var _ query.Iterator = (*TagsIterator)(nil) + +// TagsIterator is a result iterator for records consisting of selected tags +// or all the tags in the query. +type TagsIterator struct { + valueIt *ValueIterator + selected []string +} + +// Next implements query.Iterator. +func (it *TagsIterator) Next(ctx context.Context) bool { + return it.valueIt.Next(ctx) +} + +// Result implements query.Iterator. +func (it *TagsIterator) Result() interface{} { + refTags := make(map[string]refs.Ref) + it.valueIt.scanner.TagResults(refTags) + + tags := make(map[string]quad.Value) + if it.selected != nil { + for _, tag := range it.selected { + tags[tag] = it.valueIt.namer.NameOf(refTags[tag]) + } + } else { + for tag, ref := range refTags { + tags[tag] = it.valueIt.namer.NameOf(ref) + } + } + return tags +} + +// Err implements query.Iterator. +func (it *TagsIterator) Err() error { + return it.valueIt.Err() +} + +// Close implements query.Iterator. +func (it *TagsIterator) Close() error { + return it.valueIt.Close() +} diff --git a/query/linkedql/iter_values.go b/query/linkedql/iter_values.go new file mode 100644 index 000000000..45322c590 --- /dev/null +++ b/query/linkedql/iter_values.go @@ -0,0 +1,67 @@ +package linkedql + +import ( + "context" + + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/graph/iterator" + "github.com/cayleygraph/cayley/graph/refs" + "github.com/cayleygraph/cayley/query" + "github.com/cayleygraph/cayley/query/path" +) + +var _ query.Iterator = (*ValueIterator)(nil) + +// ValueIterator is an iterator of values from the graph. +type ValueIterator struct { + namer refs.Namer + path *path.Path + scanner iterator.Scanner +} + +// NewValueIterator returns a new ValueIterator for a path and namer. +func NewValueIterator(p *path.Path, namer refs.Namer) *ValueIterator { + return &ValueIterator{namer: namer, path: p} +} + +// NewValueIteratorFromPathStep attempts to build a path from PathStep and return a new ValueIterator of it. +// If BuildPath fails returns error. +func NewValueIteratorFromPathStep(step PathStep, qs graph.QuadStore) (*ValueIterator, error) { + p, err := step.BuildPath(qs) + if err != nil { + return nil, err + } + return NewValueIterator(p, qs), nil +} + +// Next implements query.Iterator. +func (it *ValueIterator) Next(ctx context.Context) bool { + if it.scanner == nil { + it.scanner = it.path.BuildIterator(ctx).Iterate() + } + return it.scanner.Next(ctx) +} + +// Result implements query.Iterator. +func (it *ValueIterator) Result() interface{} { + if it.scanner == nil { + return nil + } + return it.namer.NameOf(it.scanner.Result()) +} + +// Err implements query.Iterator. +func (it *ValueIterator) Err() error { + if it.scanner == nil { + return nil + } + return it.scanner.Err() +} + +// Close implements query.Iterator. +func (it *ValueIterator) Close() error { + if it.scanner == nil { + return nil + } + return it.scanner.Close() +} diff --git a/query/linkedql/linkedql.go b/query/linkedql/linkedql.go new file mode 100644 index 000000000..f121214e2 --- /dev/null +++ b/query/linkedql/linkedql.go @@ -0,0 +1,18 @@ +package linkedql + +import ( + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/query" +) + +// Name is the name exposed to the query interface. +const Name = "linkedql" + +func init() { + query.RegisterLanguage(query.Language{ + Name: Name, + Session: func(qs graph.QuadStore) query.Session { + return NewSession(qs) + }, + }) +} diff --git a/query/linkedql/namespace.go b/query/linkedql/namespace.go new file mode 100644 index 000000000..b8804adfc --- /dev/null +++ b/query/linkedql/namespace.go @@ -0,0 +1,12 @@ +package linkedql + +import "github.com/cayleygraph/quad/voc" + +const ( + Namespace = "http://cayley.io/linkedql#" + Prefix = "linkedql:" +) + +func init() { + voc.Register(voc.Namespace{Full: Namespace, Prefix: Prefix}) +} diff --git a/query/linkedql/operators.go b/query/linkedql/operators.go new file mode 100644 index 000000000..2e7dacf6f --- /dev/null +++ b/query/linkedql/operators.go @@ -0,0 +1,144 @@ +package linkedql + +import ( + "regexp" + + "github.com/cayleygraph/cayley/graph/iterator" + "github.com/cayleygraph/cayley/query/path" + "github.com/cayleygraph/cayley/query/shape" + "github.com/cayleygraph/quad" +) + +// Operator represents an operator used in a query inside a step (e.g. greater than). +type Operator interface { + RegistryItem + Apply(p *path.Path) (*path.Path, error) +} + +// LessThan corresponds to lt(). +type LessThan struct { + Value quad.Value `json:"value"` +} + +// Type implements Operator. +func (s *LessThan) Type() quad.IRI { + return Prefix + "LessThan" +} + +// Description implements Operator. +func (s *LessThan) Description() string { + return "Less than filters out values that are not less than given value" +} + +// Apply implements Operator. +func (s *LessThan) Apply(p *path.Path) (*path.Path, error) { + return p.Filter(iterator.CompareLT, s.Value), nil +} + +// LessThanEquals corresponds to lte(). +type LessThanEquals struct { + Value quad.Value `json:"value"` +} + +// Type implements Operator. +func (s *LessThanEquals) Type() quad.IRI { + return Prefix + "LessThanEquals" +} + +// Description implements Operator. +func (s *LessThanEquals) Description() string { + return "Less than equals filters out values that are not less than or equal given value" +} + +// Apply implements Operator. +func (s *LessThanEquals) Apply(p *path.Path) (*path.Path, error) { + return p.Filter(iterator.CompareLTE, s.Value), nil +} + +// GreaterThan corresponds to gt(). +type GreaterThan struct { + Value quad.Value `json:"value"` +} + +// Type implements Operator. +func (s *GreaterThan) Type() quad.IRI { + return Prefix + "GreaterThan" +} + +// Description implements Operator. +func (s *GreaterThan) Description() string { + return "Greater than equals filters out values that are not greater than given value" +} + +// Apply implements Operator. +func (s *GreaterThan) Apply(p *path.Path) (*path.Path, error) { + return p.Filter(iterator.CompareGT, s.Value), nil +} + +// GreaterThanEquals corresponds to gte(). +type GreaterThanEquals struct { + Value quad.Value `json:"value"` +} + +// Type implements Operator. +func (s *GreaterThanEquals) Type() quad.IRI { + return Prefix + "GreaterThanEquals" +} + +// Description implements Operator. +func (s *GreaterThanEquals) Description() string { + return "Greater than equals filters out values that are not greater than or equal given value" +} + +// Apply implements Operator. +func (s *GreaterThanEquals) Apply(p *path.Path) (*path.Path, error) { + return p.Filter(iterator.CompareGTE, s.Value), nil +} + +// RegExp corresponds to regex(). +type RegExp struct { + Pattern string `json:"pattern"` + IncludeIRIs bool `json:"includeIRIs,omitempty"` +} + +// Type implements Operator. +func (s *RegExp) Type() quad.IRI { + return Prefix + "RegExp" +} + +// Description implements Operator. +func (s *RegExp) Description() string { + return "RegExp filters out values that do not match given pattern. If includeIRIs is set to true it matches IRIs in addition to literals." +} + +// Apply implements Operator. +func (s *RegExp) Apply(p *path.Path) (*path.Path, error) { + pattern, err := regexp.Compile(s.Pattern) + if err != nil { + return nil, err + } + if s.IncludeIRIs { + return p.RegexWithRefs(pattern), nil + } + return p.RegexWithRefs(pattern), nil +} + +// Like corresponds to like(). +type Like struct { + Pattern string `json:"pattern"` +} + +// Type implements Operator. +func (s *Like) Type() quad.IRI { + return Prefix + "Like" +} + +// Description implements Operator. +func (s *Like) Description() string { + return "Like filters out values that do not match given pattern." +} + +// Apply implements Operator. +func (s *Like) Apply(p *path.Path) (*path.Path, error) { + return p.Filters(shape.Wildcard{Pattern: s.Pattern}), nil +} diff --git a/query/linkedql/registry.go b/query/linkedql/registry.go new file mode 100644 index 000000000..a67934d60 --- /dev/null +++ b/query/linkedql/registry.go @@ -0,0 +1,270 @@ +package linkedql + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/cayleygraph/quad" +) + +var ( + typeByName = make(map[string]reflect.Type) + nameByType = make(map[reflect.Type]string) +) + +// TypeByName returns a type by its registration name. See Register. +func TypeByName(name string) (reflect.Type, bool) { + t, ok := typeByName[name] + return t, ok +} + +// Registered types returns type names of all registered types. +func RegisteredTypes() []string { + out := make([]string, 0, len(typeByName)) + for k := range typeByName { + out = append(out, k) + } + return out +} + +// RegistryItem in the registry. +type RegistryItem interface { + Type() quad.IRI + Description() string +} + +// Register adds an Item type to the registry. +func Register(typ RegistryItem) { + tp := reflect.TypeOf(typ) + if tp.Kind() == reflect.Ptr { + tp = tp.Elem() + } + if tp.Kind() != reflect.Struct { + panic("only structs are allowed") + } + name := string(typ.Type()) + if _, ok := typeByName[name]; ok { + panic("this name was already registered") + } + typeByName[name] = tp + nameByType[tp] = name +} + +var ( + quadValue = reflect.TypeOf((*quad.Value)(nil)).Elem() + quadSliceValue = reflect.TypeOf([]quad.Value{}) + quadIRI = reflect.TypeOf(quad.IRI("")) + quadSliceIRI = reflect.TypeOf([]quad.IRI{}) +) + +// Unmarshal attempts to unmarshal an Item or returns error. +func Unmarshal(data []byte) (RegistryItem, error) { + var m map[string]json.RawMessage + if err := json.Unmarshal(data, &m); err != nil { + return nil, err + } + var typ string + if err := json.Unmarshal(m["@type"], &typ); err != nil { + return nil, err + } + delete(m, "@type") + tp, ok := TypeByName(typ) + if !ok { + return nil, fmt.Errorf("unsupported item: %q", typ) + } + item := reflect.New(tp).Elem() + for i := 0; i < tp.NumField(); i++ { + f := tp.Field(i) + name := f.Name + tag := strings.SplitN(f.Tag.Get("json"), ",", 2)[0] + if tag == "-" { + continue + } else if tag != "" { + name = tag + } + v, ok := m[name] + if !ok { + continue + } + fv := item.Field(i) + switch f.Type { + case quadValue: + var a interface{} + err := json.Unmarshal(v, &a) + if err != nil { + return nil, err + } + value, err := parseValue(v) + if err != nil { + return nil, err + } + fv.Set(reflect.ValueOf(value)) + continue + case quadSliceValue: + var a []interface{} + err := json.Unmarshal(v, &a) + if err != nil { + return nil, err + } + var values []quad.Value + for _, item := range a { + value, err := parseValue(item) + if err != nil { + return nil, err + } + values = append(values, value) + } + fv.Set(reflect.ValueOf(values)) + continue + case quadIRI: + var a interface{} + err := json.Unmarshal(v, &a) + if err != nil { + return nil, err + } + s, ok := a.(string) + if !ok { + return nil, fmt.Errorf("Expected a string but received %v instead", a) + } + val, err := parseIRI(s) + if err != nil { + return nil, err + } + fv.Set(reflect.ValueOf(val)) + continue + case quadSliceIRI: + var a []interface{} + err := json.Unmarshal(v, &a) + if err != nil { + return nil, err + } + var values []quad.IRI + for _, item := range a { + s, ok := item.(string) + if !ok { + return nil, fmt.Errorf("Expected a string but received %v instead", item) + } + val, err := parseIRI(s) + if err != nil { + return nil, err + } + values = append(values, val) + } + fv.Set(reflect.ValueOf(values)) + continue + } + switch f.Type.Kind() { + case reflect.Interface: + s, err := Unmarshal(v) + if err != nil { + return nil, err + } + fv.Set(reflect.ValueOf(s)) + case reflect.Slice: + el := f.Type.Elem() + if el.Kind() != reflect.Interface { + err := json.Unmarshal(v, fv.Addr().Interface()) + if err != nil { + return nil, err + } + } else { + var arr []json.RawMessage + if err := json.Unmarshal(v, &arr); err != nil { + return nil, err + } + if arr != nil { + va := reflect.MakeSlice(f.Type, len(arr), len(arr)) + for i, v := range arr { + s, err := Unmarshal(v) + if err != nil { + return nil, err + } + va.Index(i).Set(reflect.ValueOf(s)) + } + fv.Set(va) + } + } + default: + err := json.Unmarshal(v, fv.Addr().Interface()) + if err != nil { + return nil, err + } + } + } + return item.Addr().Interface().(RegistryItem), nil +} + +func parseBNode(s string) (quad.BNode, error) { + if !strings.HasPrefix(s, "_:") { + return "", fmt.Errorf("blank node ID must start with \"_:\"") + } + return quad.BNode(s[2:]), nil +} + +func parseIRI(s string) (quad.IRI, error) { + return quad.IRI(s), nil +} + +func parseIdentifier(s string) (quad.Value, error) { + bnode, err := parseBNode(s) + if err == nil { + return bnode, nil + } + iri, err := parseIRI(s) + if err == nil { + return iri, nil + } + return nil, fmt.Errorf("can not parse JSON-LD identifier: %#v", s) +} + +func parseIdentifierString(a interface{}) (string, error) { + m, ok := a.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("unexpected type: %T", a) + } + id, ok := m["@id"].(string) + if !ok { + return "", fmt.Errorf("expected a @id key") + } + return id, nil +} + +func parseLiteral(a interface{}) (quad.Value, error) { + switch a := a.(type) { + case string: + return quad.String(a), nil + case int64: + return quad.Int(a), nil + case float64: + return quad.Float(a), nil + case bool: + return quad.Bool(a), nil + case map[string]interface{}: + if val, ok := a["@value"].(string); ok { + if lang, ok := a["@language"].(string); ok { + return quad.LangString{Value: quad.String(val), Lang: lang}, nil + } + if typ, ok := a["@type"].(string); ok { + return quad.TypedString{Value: quad.String(val), Type: quad.IRI(typ)}, nil + } + } + } + return nil, fmt.Errorf("can not parse %#v as a literal", a) +} + +func parseValue(a interface{}) (quad.Value, error) { + identifierString, err := parseIdentifierString(a) + if err == nil { + identifier, err := parseIdentifier(identifierString) + if err == nil { + return identifier, nil + } + } + lit, err := parseLiteral(a) + if err == nil { + return lit, nil + } + return nil, fmt.Errorf("can not parse JSON-LD value: %#v", a) +} diff --git a/query/linkedql/registry_test.go b/query/linkedql/registry_test.go new file mode 100644 index 000000000..5035e14fa --- /dev/null +++ b/query/linkedql/registry_test.go @@ -0,0 +1,124 @@ +package linkedql + +import ( + "testing" + + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/query" + "github.com/cayleygraph/quad" + "github.com/cayleygraph/cayley/query/path" + + "github.com/stretchr/testify/require" +) + +func init() { + Register(&TestStep{}) +} + +var unmarshalCases = []struct { + name string + data string + exp Step +}{ + { + name: "simple", + data: `{ + "@type": "cayley:TestStep", + "limit": 10 +}`, + exp: &TestStep{Limit: 10}, + }, + { + name: "simple", + data: `{ + "@type": "cayley:TestStep", + "tags": ["a", "b"] +}`, + exp: &TestStep{Tags: []string{"a", "b"}}, + }, + { + name: "nested", + data: `{ + "@type": "cayley:TestStep", + "limit": 10, + "main": { + "@type": "cayley:TestStep", + "limit": 15, + "main": { + "@type": "cayley:TestStep", + "limit": 20 + } + } +}`, + exp: &TestStep{ + Limit: 10, + Main: &TestStep{ + Limit: 15, + Main: &TestStep{ + Limit: 20, + }, + }, + }, + }, + { + name: "nested slice", + data: `{ + "@type": "cayley:TestStep", + "limit": 10, + "sub": [ + { + "@type": "cayley:TestStep", + "limit": 15 + }, + { + "@type": "cayley:TestStep", + "limit": 20 + } + ] +}`, + exp: &TestStep{ + Limit: 10, + Sub: []PathStep{ + &TestStep{ + Limit: 15, + }, + &TestStep{ + Limit: 20, + }, + }, + }, + }, +} + +type TestStep struct { + Limit int `json:"limit"` + Tags []string `json:"tags"` + Main PathStep `json:"main"` + Sub []PathStep `json:"sub"` +} + +func (s *TestStep) Type() quad.IRI { + return "cayley:TestStep" +} + +func (s *TestStep) Description() string { + return "A TestStep for checking the registry" +} + +func (s *TestStep) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + panic("Can't build iterator for TestStep") +} + +func (s *TestStep) BuildPath(qs graph.QuadStore) (*path.Path, error) { + panic("Can't build path for TestStep") +} + +func TestUnmarshalStep(t *testing.T) { + for _, c := range unmarshalCases { + t.Run(c.name, func(t *testing.T) { + s, err := Unmarshal([]byte(c.data)) + require.NoError(t, err) + require.Equal(t, c.exp, s) + }) + } +} diff --git a/query/linkedql/session.go b/query/linkedql/session.go new file mode 100644 index 000000000..b6e36e0d5 --- /dev/null +++ b/query/linkedql/session.go @@ -0,0 +1,36 @@ +package linkedql + +import ( + "context" + "errors" + + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/query" +) + +var _ query.Session = &Session{} + +// Session represents a LinkedQL query processing. +type Session struct { + qs graph.QuadStore +} + +// NewSession creates a new Session. +func NewSession(qs graph.QuadStore) *Session { + return &Session{ + qs: qs, + } +} + +// Execute for a given context, query and options return an iterator of results. +func (s *Session) Execute(ctx context.Context, query string, opt query.Options) (query.Iterator, error) { + item, err := Unmarshal([]byte(query)) + if err != nil { + return nil, err + } + step, ok := item.(IteratorStep) + if !ok { + return nil, errors.New("Must execute a valid step") + } + return step.BuildIterator(s.qs) +} diff --git a/query/linkedql/steps.go b/query/linkedql/steps.go new file mode 100644 index 000000000..a8e85e63e --- /dev/null +++ b/query/linkedql/steps.go @@ -0,0 +1,1311 @@ +package linkedql + +import ( + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/query" + "github.com/cayleygraph/cayley/query/path" + "github.com/cayleygraph/quad" +) + +func init() { + Register(&Vertex{}) + Register(&Placeholder{}) + Register(&View{}) + Register(&Out{}) + Register(&As{}) + Register(&Intersect{}) + Register(&Is{}) + Register(&Back{}) + Register(&ViewBoth{}) + Register(&Count{}) + Register(&Difference{}) + Register(&Filter{}) + Register(&Follow{}) + Register(&FollowReverse{}) + Register(&Has{}) + Register(&HasReverse{}) + Register(&ViewReverse{}) + Register(&In{}) + Register(&ReversePropertyNames{}) + Register(&Labels{}) + Register(&Limit{}) + Register(&PropertyNames{}) + Register(&Properties{}) + Register(&ReversePropertyNamesAs{}) + Register(&PropertyNamesAs{}) + Register(&ReverseProperties{}) + Register(&Skip{}) + Register(&Union{}) + Register(&Unique{}) + Register(&Order{}) +} + +// Step is the tree representation of a call in a Path context. +// +// Example: +// g.V(g.IRI("alice")) +// is represented as +// &Vertex{ Values: []quad.Value{quad.IRI("alice")} } +// +// g.V().out(g.IRI("likes")) +// is represented as +// &Out{ Properties: []quad.Value{quad.IRI("likes")}, From: &Vertex{} } +type Step interface { + RegistryItem +} + +// IteratorStep is a step that can build an Iterator. +type IteratorStep interface { + Step + BuildIterator(qs graph.QuadStore) (query.Iterator, error) +} + +// PathStep is a Step that can build a Path. +type PathStep interface { + Step + BuildPath(qs graph.QuadStore) (*path.Path, error) +} + +// DocumentStep is a Step that can build a DocumentIterator. +type DocumentStep interface { + Step + BuildDocumentIterator(qs graph.QuadStore) (*DocumentIterator, error) +} + +// PropertyPath is an interface to be used where a path of properties is expected. +type PropertyPath = PathStep + +// PropertyIRIs is a slice of property IRIs. +type PropertyIRIs []quad.IRI + +// BuildPath implements PropertyPath. +func (p PropertyIRIs) BuildPath(qs graph.QuadStore) (*path.Path, error) { + var values []quad.Value + for _, iri := range p { + values = append(values, iri) + } + vertex := &Vertex{Values: values} + return vertex.BuildPath(qs) +} + +// PropertyIRIStrings is a slice of property IRI strings. +type PropertyIRIStrings []string + +// BuildPath implements PropertyPath. +func (p PropertyIRIStrings) BuildPath(qs graph.QuadStore) (*path.Path, error) { + var iris PropertyIRIs + for _, iri := range p { + iris = append(iris, quad.IRI(iri)) + } + return iris.BuildPath(qs) +} + +// EntityIdentifier is an interface to be used where a single entity identifier is expected. +type EntityIdentifier interface { + BuildIdentifier() (quad.Value, error) +} + +// EntityIRI is an entity IRI. +type EntityIRI quad.IRI + +// BuildIdentifier implements EntityIdentifier +func (i EntityIRI) BuildIdentifier() (quad.Value, error) { + return quad.IRI(i), nil +} + +// EntityBNode is an entity BNode. +type EntityBNode quad.BNode + +// BuildIdentifier implements EntityIdentifier +func (i EntityBNode) BuildIdentifier() (quad.Value, error) { + return quad.BNode(i), nil +} + +// EntityIdentifierString is an entity IRI or BNode strings. +type EntityIdentifierString string + +// BuildIdentifier implements EntityIdentifier +func (i EntityIdentifierString) BuildIdentifier() (quad.Value, error) { + return parseIdentifier(string(i)) +} + +var _ IteratorStep = (*Vertex)(nil) +var _ PathStep = (*Vertex)(nil) + +// Entity corresponds to g.Entity(). +type Entity struct { + Identifier EntityIdentifier `json:"identifier"` +} + +// Type implements Step. +func (s *Entity) Type() quad.IRI { + return Prefix + "Entity" +} + +// Description implements Step. +func (s *Entity) Description() string { + return "resolves to the object matching given identifier in the graph." +} + +// BuildIterator implements IteratorStep. +func (s *Entity) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Entity) BuildPath(qs graph.QuadStore) (*path.Path, error) { + identifier, err := s.Identifier.BuildIdentifier() + if err != nil { + return nil, err + } + return path.StartPath(qs, identifier), nil +} + +var _ IteratorStep = (*Vertex)(nil) +var _ PathStep = (*Vertex)(nil) + +// Entities corresponds to g.Entities(). +type Entities struct { + Identifiers []EntityIdentifier `json:"identifiers"` +} + +// Type implements Step. +func (s *Entities) Type() quad.IRI { + return Prefix + "Entities" +} + +// Description implements Step. +func (s *Entities) Description() string { + return "resolves to all the existing objects in the graph. If provided with identifiers resolves to a sublist of all the existing identifiers in the graph." +} + +// BuildIterator implements IteratorStep. +func (s *Entities) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Entities) BuildPath(qs graph.QuadStore) (*path.Path, error) { + var values []quad.Value + for _, identifier := range s.Identifiers { + value, err := identifier.BuildIdentifier() + if err != nil { + return nil, err + } + values = append(values, value) + } + // TODO(iddan): Construct a path that only match entities + return path.StartPath(qs, values...), nil +} + +var _ IteratorStep = (*Vertex)(nil) +var _ PathStep = (*Vertex)(nil) + +// Vertex corresponds to g.Vertex() and g.V(). +type Vertex struct { + Values []quad.Value `json:"values"` +} + +// Type implements Step. +func (s *Vertex) Type() quad.IRI { + return Prefix + "Vertex" +} + +// Description implements Step. +func (s *Vertex) Description() string { + return "resolves to all the existing objects and primitive values in the graph. If provided with values resolves to a sublist of all the existing values in the graph." +} + +// BuildIterator implements IteratorStep. +func (s *Vertex) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Vertex) BuildPath(qs graph.QuadStore) (*path.Path, error) { + return path.StartPath(qs, s.Values...), nil +} + +var _ PathStep = (*Placeholder)(nil) + +// Placeholder corresponds to .Placeholder(). +type Placeholder struct{} + +// Type implements Step. +func (s *Placeholder) Type() quad.IRI { + return "linkedql:Placeholder" +} + +// Description implements Step. +func (s *Placeholder) Description() string { + return "is like Vertex but resolves to the values in the context it is placed in. It should only be used where a PathStep is expected and can't be resolved on its own." +} + +// BuildPath implements PathStep. +func (s *Placeholder) BuildPath(qs graph.QuadStore) (*path.Path, error) { + return path.StartMorphism(), nil +} + +var _ IteratorStep = (*View)(nil) +var _ PathStep = (*View)(nil) + +// View corresponds to .view(). +type View struct { + From PathStep `json:"from"` + Properties PropertyPath `json:"properties"` +} + +// Type implements Step. +func (s *View) Type() quad.IRI { + return Prefix + "View" +} + +// Description implements Step. +func (s *View) Description() string { + return "resolves to the values of the given property or properties in via of the current objects. If via is a path it's resolved values will be used as properties." +} + +// BuildIterator implements IteratorStep. +func (s *View) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *View) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + viaPath, err := s.Properties.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Out(viaPath), nil +} + +var _ IteratorStep = (*Out)(nil) +var _ PathStep = (*Out)(nil) + +// Out is an alias for View. +type Out struct { + View +} + +// Type implements Step. +func (s *Out) Type() quad.IRI { + return Prefix + "Out" +} + +// Description implements Step. +func (s *Out) Description() string { + return "aliases for View" +} + +var _ IteratorStep = (*As)(nil) +var _ PathStep = (*As)(nil) + +// As corresponds to .tag(). +type As struct { + From PathStep `json:"from"` + Name string `json:"name"` +} + +// Type implements Step. +func (s *As) Type() quad.IRI { + return Prefix + "As" +} + +// Description implements Step. +func (s *As) Description() string { + return "assigns the resolved values of the from step to a given name. The name can be used with the Select and Documents steps to retrieve the values or to return to the values in further steps with the Back step. It resolves to the values of the from step." +} + +// BuildIterator implements IteratorStep. +func (s *As) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *As) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Tag(s.Name), nil +} + +var _ IteratorStep = (*Intersect)(nil) +var _ PathStep = (*Intersect)(nil) + +// Intersect represents .intersect() and .and(). +type Intersect struct { + From PathStep `json:"from"` + Steps []PathStep `json:"steps"` +} + +// Type implements Step. +func (s *Intersect) Type() quad.IRI { + return Prefix + "Intersect" +} + +// Description implements Step. +func (s *Intersect) Description() string { + return "resolves to all the same values resolved by the from step and the provided steps." +} + +// BuildIterator implements IteratorStep. +func (s *Intersect) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Intersect) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + p := fromPath + for _, step := range s.Steps { + stepPath, err := step.BuildPath(qs) + if err != nil { + return nil, err + } + p = p.And(stepPath) + } + return p, nil +} + +var _ IteratorStep = (*Is)(nil) +var _ PathStep = (*Is)(nil) + +// Is corresponds to .back(). +type Is struct { + From PathStep `json:"from"` + Values []quad.Value `json:"values"` +} + +// Type implements Step. +func (s *Is) Type() quad.IRI { + return Prefix + "Is" +} + +// Description implements Step. +func (s *Is) Description() string { + return "resolves to all the values resolved by the from step which are included in provided values." +} + +// BuildIterator implements IteratorStep. +func (s *Is) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Is) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Is(s.Values...), nil +} + +var _ IteratorStep = (*Back)(nil) +var _ PathStep = (*Back)(nil) + +// Back corresponds to .back(). +type Back struct { + From PathStep `json:"from"` + Name string `json:"name"` +} + +// Type implements Step. +func (s *Back) Type() quad.IRI { + return Prefix + "Back" +} + +// Description implements Step. +func (s *Back) Description() string { + return "resolves to the values of the previous the step or the values assigned to name in a former step." +} + +// BuildIterator implements IteratorStep. +func (s *Back) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Back) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Back(s.Name), nil +} + +var _ IteratorStep = (*ViewBoth)(nil) +var _ PathStep = (*ViewBoth)(nil) + +// ViewBoth corresponds to .viewBoth(). +type ViewBoth struct { + From PathStep `json:"from"` + Properties PropertyPath `json:"properties"` +} + +// Type implements Step. +func (s *ViewBoth) Type() quad.IRI { + return Prefix + "ViewBoth" +} + +// Description implements Step. +func (s *ViewBoth) Description() string { + return "is like View but resolves to both the object values and references to the values of the given properties in via. It is the equivalent for the Union of View and ViewReverse of the same property." +} + +// BuildIterator implements IteratorStep. +func (s *ViewBoth) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *ViewBoth) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + viaPath, err := s.Properties.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Both(viaPath), nil +} + +var _ IteratorStep = (*Both)(nil) +var _ PathStep = (*Both)(nil) + +// Both corresponds to .both(). +type Both struct { + ViewBoth +} + +// Type implements Step. +func (s *Both) Type() quad.IRI { + return Prefix + "Both" +} + +// Description implements Step. +func (s *Both) Description() string { + return "aliases for ViewBoth" +} + +var _ IteratorStep = (*Count)(nil) +var _ PathStep = (*Count)(nil) + +// Count corresponds to .count(). +type Count struct { + From PathStep `json:"from"` +} + +// Type implements Step. +func (s *Count) Type() quad.IRI { + return Prefix + "Count" +} + +// Description implements Step. +func (s *Count) Description() string { + return "resolves to the number of the resolved values of the from step" +} + +// BuildIterator implements IteratorStep. +func (s *Count) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Count) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Count(), nil +} + +var _ IteratorStep = (*Difference)(nil) +var _ PathStep = (*Difference)(nil) + +// Difference corresponds to .difference(). +type Difference struct { + From PathStep `json:"from"` + Steps []PathStep `json:"steps"` +} + +// Type implements Step. +func (s *Difference) Type() quad.IRI { + return Prefix + "Difference" +} + +// Description implements Step. +func (s *Difference) Description() string { + return "resolves to all the values resolved by the from step different then the values resolved by the provided steps. Caution: it might be slow to execute." +} + +// BuildIterator implements IteratorStep. +func (s *Difference) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Difference) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + path := fromPath + for _, step := range s.Steps { + p, err := step.BuildPath(qs) + if err != nil { + return nil, err + } + path = path.Except(p) + } + return path, nil +} + +var _ IteratorStep = (*Filter)(nil) +var _ PathStep = (*Filter)(nil) + +// Filter corresponds to filter(). +type Filter struct { + From PathStep `json:"from"` + Filter Operator `json:"filter"` +} + +// Type implements Step. +func (s *Filter) Type() quad.IRI { + return Prefix + "Filter" +} + +// Description implements Step. +func (s *Filter) Description() string { + return "applies constraints to a set of nodes. Can be used to filter values by range or match strings." +} + +// BuildIterator implements IteratorStep. +func (s *Filter) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Filter) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromIt, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return s.Filter.Apply(fromIt) +} + +var _ IteratorStep = (*Follow)(nil) +var _ PathStep = (*Follow)(nil) + +// Follow corresponds to .follow(). +type Follow struct { + From PathStep `json:"from"` + Followed PathStep `json:"followed"` +} + +// Type implements Step. +func (s *Follow) Type() quad.IRI { + return Prefix + "Follow" +} + +// Description implements Step. +func (s *Follow) Description() string { + return "is the way to use a path prepared with Morphism. Applies the path chain on the morphism object to the current path. Starts as if at the g.M() and follows through the morphism path." +} + +// BuildIterator implements IteratorStep. +func (s *Follow) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Follow) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + p, err := s.Followed.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Follow(p), nil +} + +var _ IteratorStep = (*FollowReverse)(nil) +var _ PathStep = (*FollowReverse)(nil) + +// FollowReverse corresponds to .followR(). +type FollowReverse struct { + From PathStep `json:"from"` + Followed PathStep `json:"followed"` +} + +// Type implements Step. +func (s *FollowReverse) Type() quad.IRI { + return Prefix + "FollowReverse" +} + +// Description implements Step. +func (s *FollowReverse) Description() string { + return "is the same as follow but follows the chain in the reverse direction. Flips View and ViewReverse where appropriate, the net result being a virtual predicate followed in the reverse direction. Starts at the end of the morphism and follows it backwards (with appropriate flipped directions) to the g.M() location." +} + +// BuildIterator implements IteratorStep. +func (s *FollowReverse) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *FollowReverse) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + p, err := s.Followed.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.FollowReverse(p), nil +} + +var _ IteratorStep = (*Has)(nil) +var _ PathStep = (*Has)(nil) + +// Has corresponds to .has(). +type Has struct { + From PathStep `json:"from"` + Property PropertyPath `json:"property"` + Values []quad.Value `json:"values"` +} + +// Type implements Step. +func (s *Has) Type() quad.IRI { + return Prefix + "Has" +} + +// Description implements Step. +func (s *Has) Description() string { + return "filters all paths which are, at this point, on the subject for the given predicate and object, but do not follow the path, merely filter the possible paths. Usually useful for starting with all nodes, or limiting to a subset depending on some predicate/value pair." +} + +// BuildIterator implements IteratorStep. +func (s *Has) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Has) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + viaPath, err := s.Property.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Has(viaPath, s.Values...), nil +} + +var _ IteratorStep = (*HasReverse)(nil) +var _ PathStep = (*HasReverse)(nil) + +// HasReverse corresponds to .hasR(). +type HasReverse struct { + From PathStep `json:"from"` + Property PropertyPath `json:"property"` + Values []quad.Value `json:"values"` +} + +// Type implements Step. +func (s *HasReverse) Type() quad.IRI { + return Prefix + "HasReverse" +} + +// Description implements Step. +func (s *HasReverse) Description() string { + return "is the same as Has, but sets constraint in reverse direction." +} + +// BuildIterator implements IteratorStep. +func (s *HasReverse) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *HasReverse) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + viaPath, err := s.Property.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.HasReverse(viaPath, s.Values...), nil +} + +var _ IteratorStep = (*ViewReverse)(nil) +var _ PathStep = (*ViewReverse)(nil) + +// ViewReverse corresponds to .viewReverse(). +type ViewReverse struct { + From PathStep `json:"from"` + Properties PropertyPath `json:"properties"` +} + +// Type implements Step. +func (s *ViewReverse) Type() quad.IRI { + return Prefix + "ViewReverse" +} + +// Description implements Step. +func (s *ViewReverse) Description() string { + return "is the inverse of View. Starting with the nodes in `path` on the object, follow the quads with predicates defined by `predicatePath` to their subjects." +} + +// BuildIterator implements IteratorStep. +func (s *ViewReverse) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *ViewReverse) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + viaPath, err := s.Properties.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.In(viaPath), nil +} + +var _ IteratorStep = (*In)(nil) +var _ PathStep = (*In)(nil) + +// In is an alias for ViewReverse. +type In struct { + ViewReverse +} + +// Type implements Step. +func (s *In) Type() quad.IRI { + return Prefix + "In" +} + +// Description implements Step. +func (s *In) Description() string { + return "aliases for ViewReverse" +} + +var _ IteratorStep = (*ReversePropertyNames)(nil) +var _ PathStep = (*ReversePropertyNames)(nil) + +// ReversePropertyNames corresponds to .reversePropertyNames(). +type ReversePropertyNames struct { + From PathStep `json:"from"` +} + +// Type implements Step. +func (s *ReversePropertyNames) Type() quad.IRI { + return Prefix + "ReversePropertyNames" +} + +// Description implements Step. +func (s *ReversePropertyNames) Description() string { + return "gets the list of predicates that are pointing in to a node." +} + +// BuildIterator implements IteratorStep. +func (s *ReversePropertyNames) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *ReversePropertyNames) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.InPredicates(), nil +} + +var _ IteratorStep = (*Labels)(nil) +var _ PathStep = (*Labels)(nil) + +// Labels corresponds to .labels(). +type Labels struct { + From PathStep `json:"from"` +} + +// Type implements Step. +func (s *Labels) Type() quad.IRI { + return Prefix + "Labels" +} + +// Description implements Step. +func (s *Labels) Description() string { + return "gets the list of inbound and outbound quad labels" +} + +// BuildIterator implements IteratorStep. +func (s *Labels) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Labels) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Labels(), nil +} + +var _ IteratorStep = (*Limit)(nil) +var _ PathStep = (*Limit)(nil) + +// Limit corresponds to .limit(). +type Limit struct { + From PathStep `json:"from"` + Limit int64 `json:"limit"` +} + +// Type implements Step. +func (s *Limit) Type() quad.IRI { + return Prefix + "Limit" +} + +// Description implements Step. +func (s *Limit) Description() string { + return "limits a number of nodes for current path." +} + +// BuildIterator implements IteratorStep. +func (s *Limit) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Limit) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Limit(s.Limit), nil +} + +var _ IteratorStep = (*PropertyNames)(nil) +var _ PathStep = (*PropertyNames)(nil) + +// PropertyNames corresponds to .propertyNames(). +type PropertyNames struct { + From PathStep `json:"from"` +} + +// Type implements Step. +func (s *PropertyNames) Type() quad.IRI { + return Prefix + "PropertyNames" +} + +// Description implements Step. +func (s *PropertyNames) Description() string { + return "gets the list of predicates that are pointing out from a node." +} + +// BuildIterator implements IteratorStep. +func (s *PropertyNames) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *PropertyNames) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.OutPredicates(), nil +} + +var _ IteratorStep = (*Properties)(nil) +var _ PathStep = (*Properties)(nil) + +// Properties corresponds to .properties(). +type Properties struct { + From PathStep `json:"from"` + // TODO(iddan): Use PropertyPath + Names []quad.IRI `json:"names"` +} + +// Type implements Step. +func (s *Properties) Type() quad.IRI { + return Prefix + "Properties" +} + +// Description implements Step. +func (s *Properties) Description() string { + return "adds tags for all properties of the current entity" +} + +// BuildIterator implements IteratorStep. +// TODO(iddan): Default tag to Via. +func (s *Properties) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildDocumentIterator implements DocumentsStep. +func (s *Properties) BuildDocumentIterator(qs graph.QuadStore) (*DocumentIterator, error) { + p, err := s.BuildPath(qs) + if err != nil { + return nil, err + } + it, err := NewValueIterator(p, qs), nil + if err != nil { + return nil, err + } + return NewDocumentIterator(qs, it.path), nil +} + +// BuildPath implements PathStep. +func (s *Properties) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + p := fromPath + if s.Names != nil { + for _, name := range s.Names { + tag := string(name) + p = p.Save(name, tag) + } + } else { + panic("Not implemented: should tag all properties") + } + return p, nil +} + +var _ IteratorStep = (*ReversePropertyNamesAs)(nil) +var _ PathStep = (*ReversePropertyNamesAs)(nil) + +// ReversePropertyNamesAs corresponds to .reversePropertyNamesAs(). +type ReversePropertyNamesAs struct { + From PathStep `json:"from"` + Tag string `json:"tag"` +} + +// Type implements Step. +func (s *ReversePropertyNamesAs) Type() quad.IRI { + return Prefix + "ReversePropertyNamesAs" +} + +// Description implements Step. +func (s *ReversePropertyNamesAs) Description() string { + return "tags the list of predicates that are pointing in to a node." +} + +// BuildIterator implements IteratorStep. +func (s *ReversePropertyNamesAs) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *ReversePropertyNamesAs) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.SavePredicates(true, s.Tag), nil +} + +var _ IteratorStep = (*PropertyNamesAs)(nil) +var _ PathStep = (*PropertyNamesAs)(nil) + +// PropertyNamesAs corresponds to .propertyNamesAs(). +type PropertyNamesAs struct { + From PathStep `json:"from"` + Tag string `json:"tag"` +} + +// Type implements Step. +func (s *PropertyNamesAs) Type() quad.IRI { + return Prefix + "PropertyNamesAs" +} + +// Description implements Step. +func (s *PropertyNamesAs) Description() string { + return "tags the list of predicates that are pointing out from a node." +} + +// BuildIterator implements IteratorStep. +func (s *PropertyNamesAs) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *PropertyNamesAs) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.SavePredicates(false, s.Tag), nil +} + +var _ IteratorStep = (*ReverseProperties)(nil) +var _ PathStep = (*ReverseProperties)(nil) + +// ReverseProperties corresponds to .reverseProperties(). +type ReverseProperties struct { + From PathStep `json:"from"` + // TODO(iddan): Use property path + Names []quad.IRI `json:"names"` +} + +// Type implements Step. +func (s *ReverseProperties) Type() quad.IRI { + return Prefix + "ReverseProperties" +} + +// Description implements Step. +func (s *ReverseProperties) Description() string { + return "gets all the properties the current entity / value is referenced at" +} + +// BuildIterator implements IteratorStep. +func (s *ReverseProperties) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *ReverseProperties) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + p := fromPath + for _, name := range s.Names { + p = fromPath.SaveReverse(name, string(name)) + } + return p, nil +} + +var _ IteratorStep = (*Skip)(nil) +var _ PathStep = (*Skip)(nil) + +// Skip corresponds to .skip(). +type Skip struct { + From PathStep `json:"from"` + Offset int64 `json:"offset"` +} + +// Type implements Step. +func (s *Skip) Type() quad.IRI { + return Prefix + "Skip" +} + +// Description implements Step. +func (s *Skip) Description() string { + return "skips a number of nodes for current path." +} + +// BuildIterator implements IteratorStep. +func (s *Skip) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Skip) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Skip(s.Offset), nil +} + +var _ IteratorStep = (*Union)(nil) +var _ PathStep = (*Union)(nil) + +// Union corresponds to .union() and .or(). +type Union struct { + From PathStep `json:"from"` + Steps []PathStep `json:"steps"` +} + +// Type implements Step. +func (s *Union) Type() quad.IRI { + return Prefix + "Union" +} + +// Description implements Step. +func (s *Union) Description() string { + return "returns the combined paths of the two queries. Notice that it's per-path, not per-node. Once again, if multiple paths reach the same destination, they might have had different ways of getting there (and different tags)." +} + +// BuildIterator implements IteratorStep. +func (s *Union) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Union) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + p := fromPath + for _, step := range s.Steps { + valuePath, err := step.BuildPath(qs) + if err != nil { + return nil, err + } + p = p.Or(valuePath) + } + return p, nil +} + +var _ IteratorStep = (*Unique)(nil) +var _ PathStep = (*Unique)(nil) + +// Unique corresponds to .unique(). +type Unique struct { + From PathStep `json:"from"` +} + +// Type implements Step. +func (s *Unique) Type() quad.IRI { + return Prefix + "Unique" +} + +// Description implements Step. +func (s *Unique) Description() string { + return "removes duplicate values from the path." +} + +// BuildIterator implements IteratorStep. +func (s *Unique) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Unique) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Unique(), nil +} + +var _ IteratorStep = (*Order)(nil) +var _ PathStep = (*Order)(nil) + +// Order corresponds to .order(). +type Order struct { + From PathStep `json:"from"` +} + +// Type implements Step. +func (s *Order) Type() quad.IRI { + return Prefix + "Order" +} + +// Description implements Step. +func (s *Order) Description() string { + return "sorts the results in ascending order according to the current entity / value" +} + +// BuildIterator implements IteratorStep. +func (s *Order) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Order) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Order(), nil +} + +var _ IteratorStep = (*Optional)(nil) +var _ PathStep = (*Optional)(nil) + +// Optional corresponds to .optional(). +type Optional struct { + From PathStep `json:"from"` + Step PathStep `json:"step"` +} + +// Type implements Step. +func (s *Optional) Type() quad.IRI { + return Prefix + "Optional" +} + +// Description implements Step. +func (s *Optional) Description() string { + return "attempts to follow the given path from the current entity / value, if fails the entity / value will still be kept in the results" +} + +// BuildIterator implements IteratorStep. +func (s *Optional) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Optional) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + p, err := s.Step.BuildPath(qs) + if err != nil { + return nil, err + } + return fromPath.Optional(p), nil +} + +// Where corresponds to .where(). +type Where struct { + From PathStep `json:"from"` + Steps []PathStep `json:"steps"` +} + +// Type implements Step. +func (s *Where) Type() quad.IRI { + return Prefix + "Where" +} + +// Description implements Step. +func (s *Where) Description() string { + return "applies each provided step in steps in isolation on from" +} + +// BuildIterator implements IteratorStep. +func (s *Where) BuildIterator(qs graph.QuadStore) (query.Iterator, error) { + return NewValueIteratorFromPathStep(s, qs) +} + +// BuildPath implements PathStep. +func (s *Where) BuildPath(qs graph.QuadStore) (*path.Path, error) { + fromPath, err := s.From.BuildPath(qs) + if err != nil { + return nil, err + } + p := fromPath + for _, step := range s.Steps { + stepPath, err := step.BuildPath(qs) + if err != nil { + return nil, err + } + p = p.And(stepPath.Reverse()) + } + return p, nil +} diff --git a/query/linkedql/steps_test.go b/query/linkedql/steps_test.go new file mode 100644 index 000000000..df298a30d --- /dev/null +++ b/query/linkedql/steps_test.go @@ -0,0 +1,614 @@ +package linkedql + +import ( + "context" + "testing" + + "github.com/cayleygraph/cayley/graph/memstore" + "github.com/cayleygraph/quad" + "github.com/stretchr/testify/require" +) + +var singleQuadData = []quad.Quad{ + quad.MakeIRI("alice", "likes", "bob", ""), +} + +var testCases = []struct { + name string + data []quad.Quad + query IteratorStep + results []interface{} +}{ + { + name: "All Entities", + data: singleQuadData, + query: &Entities{Identifiers: []EntityIdentifier{EntityIdentifierString("alice")}}, + results: []interface{}{ + quad.IRI("alice"), + }, + }, + { + name: "All Vertices", + data: singleQuadData, + query: &Vertex{Values: nil}, + results: []interface{}{ + quad.IRI("alice"), + quad.IRI("likes"), + quad.IRI("bob"), + }, + }, + { + name: "Select", + data: singleQuadData, + query: &Select{ + From: &As{ + From: &View{ + From: &As{ + From: &Vertex{}, + Name: "liker", + }, + Properties: &Vertex{Values: []quad.Value{quad.IRI("likes")}}, + }, + Name: "liked", + }, + }, + results: []interface{}{ + map[string]quad.Value{ + "liker": quad.IRI("alice"), + "liked": quad.IRI("bob"), + }, + }, + }, + { + name: "Select with tags", + data: singleQuadData, + query: &Select{ + Tags: []string{"liker"}, + From: &As{ + From: &View{ + From: &As{ + From: &Vertex{}, + Name: "liker", + }, + Properties: &Vertex{Values: []quad.Value{quad.IRI("likes")}}, + }, + Name: "liked", + }, + }, + results: []interface{}{ + map[string]quad.Value{ + "liker": quad.IRI("alice"), + }, + }, + }, + { + name: "Back", + data: singleQuadData, + query: &Back{ + From: &View{ + From: &Vertex{ + Values: []quad.Value{quad.IRI("alice")}, + }, + Properties: &Vertex{ + Values: []quad.Value{ + quad.IRI("likes"), + }, + }, + }, + }, + results: []interface{}{ + quad.IRI("alice"), + }, + }, + { + name: "ViewBoth", + data: []quad.Quad{ + quad.MakeIRI("alice", "likes", "bob", ""), + quad.MakeIRI("bob", "likes", "dan", ""), + }, + query: &ViewBoth{ + From: &Vertex{ + Values: []quad.Value{quad.IRI("bob")}, + }, + Properties: &Vertex{Values: []quad.Value{quad.IRI("likes")}}, + }, + results: []interface{}{ + quad.IRI("alice"), + quad.IRI("dan"), + }, + }, + { + name: "Count", + data: singleQuadData, + query: &Count{ + From: &Vertex{Values: []quad.Value{}}, + }, + results: []interface{}{ + quad.Int(4), + }, + }, + { + name: "Difference", + data: singleQuadData, + query: &Difference{ + From: &Vertex{ + Values: []quad.Value{quad.IRI("alice"), quad.IRI("likes")}, + }, + Steps: []PathStep{ + &Vertex{ + Values: []quad.Value{quad.IRI("likes")}, + }, + }, + }, + results: []interface{}{ + quad.IRI("alice"), + }, + }, + { + name: "Filter RegExp", + data: []quad.Quad{ + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.String("Alice"), Label: nil}, + }, + query: &Filter{ + From: &Vertex{Values: []quad.Value{}}, + Filter: &RegExp{Pattern: "A"}, + }, + results: []interface{}{ + quad.String("Alice"), + }, + }, + { + name: "Filter Like", + data: []quad.Quad{ + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.String("Alice"), Label: nil}, + }, + query: &Filter{ + From: &Vertex{Values: []quad.Value{}}, + Filter: &Like{Pattern: "a%"}, + }, + results: []interface{}{ + quad.IRI("alice"), + }, + }, + { + name: "Filter LessThan", + data: []quad.Quad{ + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(0), Label: nil}, + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(1), Label: nil}, + }, + query: &Filter{ + From: &Vertex{Values: []quad.Value{}}, + Filter: &LessThan{Value: quad.Int(1)}, + }, + results: []interface{}{ + quad.Int(0), + }, + }, + { + name: "Filter GreaterThan", + data: []quad.Quad{ + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(0), Label: nil}, + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(1), Label: nil}, + }, + query: &Filter{ + From: &Vertex{Values: []quad.Value{}}, + Filter: &GreaterThan{Value: quad.Int(0)}, + }, + results: []interface{}{ + quad.Int(1), + }, + }, + { + name: "Filter LessThanEquals", + data: []quad.Quad{ + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(-1), Label: nil}, + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(0), Label: nil}, + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(1), Label: nil}, + }, + query: &Filter{ + From: &Vertex{Values: []quad.Value{}}, + Filter: &LessThanEquals{Value: quad.Int(0)}, + }, + results: []interface{}{ + quad.Int(-1), + quad.Int(0), + }, + }, + { + name: "Filter GreaterThanEquals", + data: []quad.Quad{ + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(0), Label: nil}, + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(1), Label: nil}, + {Subject: quad.IRI("alice"), Predicate: quad.IRI("name"), Object: quad.Int(2), Label: nil}, + }, + query: &Filter{ + From: &Vertex{Values: []quad.Value{}}, + Filter: &GreaterThanEquals{Value: quad.Int(1)}, + }, + results: []interface{}{ + quad.Int(1), + quad.Int(2), + }, + }, + { + name: "Has", + data: singleQuadData, + query: &Has{ + From: &Vertex{ + Values: []quad.Value{}, + }, + Property: &Vertex{ + Values: []quad.Value{quad.IRI("likes")}, + }, + Values: []quad.Value{quad.IRI("bob")}, + }, + results: []interface{}{ + quad.IRI("alice"), + }, + }, + { + name: "HasReverse", + data: singleQuadData, + query: &HasReverse{ + From: &Vertex{ + Values: []quad.Value{}, + }, + Property: &Vertex{ + Values: []quad.Value{quad.IRI("likes")}, + }, + Values: []quad.Value{quad.IRI("alice")}, + }, + results: []interface{}{ + quad.IRI("bob"), + }, + }, + { + name: "ViewReverse", + data: singleQuadData, + query: &ViewReverse{ + From: &Vertex{Values: []quad.Value{}}, + Properties: &Vertex{Values: []quad.Value{quad.IRI("likes")}}, + }, + results: []interface{}{ + quad.IRI("alice"), + }, + }, + { + name: "PropertyNames", + data: singleQuadData, + query: &PropertyNames{ + From: &Vertex{Values: []quad.Value{}}, + }, + results: []interface{}{ + quad.IRI("likes"), + }, + }, + { + name: "Intersect", + data: []quad.Quad{ + quad.MakeIRI("bob", "likes", "alice", ""), + quad.MakeIRI("dani", "likes", "alice", ""), + }, + query: &Intersect{ + From: &View{ + From: &Vertex{Values: []quad.Value{quad.IRI("bob")}}, + Properties: &Vertex{ + Values: []quad.Value{quad.IRI("likes")}, + }, + }, + Steps: []PathStep{ + &View{ + From: &Vertex{Values: []quad.Value{quad.IRI("bob")}}, + Properties: &Vertex{Values: []quad.Value{quad.IRI("likes")}}, + }, + }, + }, + results: []interface{}{ + quad.IRI("alice"), + }, + }, + { + name: "Is", + data: singleQuadData, + query: &Is{ + Values: []quad.Value{quad.IRI("bob")}, + From: &View{ + From: &Vertex{Values: []quad.Value{quad.IRI("alice")}}, + Properties: &Vertex{ + Values: []quad.Value{quad.IRI("likes")}, + }, + }, + }, + results: []interface{}{ + quad.IRI("bob"), + }, + }, + { + name: "Limit", + data: singleQuadData, + query: &Limit{ + Limit: 2, + From: &Vertex{ + Values: []quad.Value{}, + }, + }, + results: []interface{}{ + quad.IRI("alice"), + quad.IRI("likes"), + }, + }, + { + name: "View", + data: singleQuadData, + query: &View{ + From: &Vertex{Values: []quad.Value{}}, + Properties: &Vertex{Values: []quad.Value{quad.IRI("likes")}}, + }, + results: []interface{}{ + quad.IRI("bob"), + }, + }, + { + name: "PropertyNames", + data: singleQuadData, + query: &PropertyNames{ + From: &Vertex{Values: []quad.Value{}}, + }, + results: []interface{}{ + quad.IRI("likes"), + }, + }, + { + name: "Properties", + data: singleQuadData, + query: &Select{ + From: &Properties{ + From: &Vertex{Values: []quad.Value{}}, + Names: []quad.IRI{quad.IRI("likes")}, + }, + }, + results: []interface{}{ + map[string]quad.Value{ + "likes": quad.IRI("bob"), + }, + }, + }, + { + name: "ReversePropertyNamesAs", + data: singleQuadData, + query: &Select{ + From: &ReversePropertyNamesAs{ + From: &Vertex{Values: []quad.Value{}}, + Tag: "predicate", + }, + }, + results: []interface{}{ + map[string]quad.Value{ + "predicate": quad.IRI("likes"), + }, + }, + }, + { + name: "PropertyNamesAs", + data: singleQuadData, + query: &Select{ + From: &PropertyNamesAs{ + From: &Vertex{Values: []quad.Value{}}, + Tag: "predicate", + }, + }, + results: []interface{}{ + map[string]quad.Value{ + "predicate": quad.IRI("likes"), + }, + }, + }, + { + name: "ReverseProperties", + data: singleQuadData, + query: &Select{ + From: &ReverseProperties{ + From: &Vertex{Values: []quad.Value{}}, + Names: []quad.IRI{quad.IRI("likes")}, + }, + }, + results: []interface{}{ + map[string]quad.Value{ + "likes": quad.IRI("alice"), + }, + }, + }, + { + name: "Skip", + data: singleQuadData, + query: &Skip{ + Offset: 2, + From: &Vertex{ + Values: []quad.Value{}, + }, + }, + results: []interface{}{ + quad.IRI("bob"), + }, + }, + { + name: "Union", + data: singleQuadData, + query: &Union{ + From: &Vertex{ + Values: []quad.Value{quad.IRI("alice")}, + }, + Steps: []PathStep{ + &Vertex{ + Values: []quad.Value{quad.IRI("bob")}, + }, + }, + }, + results: []interface{}{ + quad.IRI("alice"), + quad.IRI("bob"), + }, + }, + { + name: "SelectFirst", + data: singleQuadData, + query: &SelectFirst{ + From: &As{ + From: &View{ + From: &As{ + Name: "liker", + From: &Vertex{}, + }, + Properties: &Vertex{Values: []quad.Value{quad.IRI("likes")}}, + }, + Name: "liked", + }, + }, + results: []interface{}{ + map[string]quad.Value{ + "liker": quad.IRI("alice"), + "liked": quad.IRI("bob"), + }, + }, + }, + { + name: "Unique", + data: singleQuadData, + query: &Unique{ + From: &Vertex{ + Values: []quad.Value{quad.IRI("alice"), quad.IRI("alice"), quad.IRI("bob")}, + }, + }, + results: []interface{}{ + quad.IRI("alice"), + quad.IRI("bob"), + }, + }, + { + name: "Order", + data: singleQuadData, + query: &Order{ + From: &Vertex{}, + }, + results: []interface{}{ + quad.IRI("alice"), + quad.IRI("bob"), + quad.IRI("likes"), + }, + }, + { + name: "Optional", + data: []quad.Quad{ + quad.MakeIRI("alice", "likes", "bob", ""), + quad.MakeIRI("alice", "name", "Alice", ""), + quad.MakeIRI("bob", "name", "Bob", ""), + }, + query: &Select{ + From: &Optional{ + From: &Properties{ + From: &Vertex{Values: []quad.Value{}}, + Names: []quad.IRI{quad.IRI("name")}, + }, + Step: &Properties{ + From: &Placeholder{}, + Names: []quad.IRI{quad.IRI("likes")}, + }, + }, + }, + results: []interface{}{ + map[string]quad.Value{ + "likes": quad.IRI("bob"), + "name": quad.IRI("Alice"), + }, + map[string]quad.Value{ + "name": quad.IRI("Bob"), + }, + }, + }, + { + name: "Where", + data: []quad.Quad{ + quad.MakeIRI("alice", "likes", "bob", ""), + quad.MakeIRI("alice", "name", "Alice", ""), + quad.MakeIRI("bob", "name", "Bob", ""), + }, + query: &Select{ + From: &As{ + From: &Where{ + From: &Vertex{}, + Steps: []PathStep{ + &As{ + From: &View{ + From: &View{ + From: &Placeholder{}, + Properties: &Vertex{Values: []quad.Value{quad.IRI("likes")}}, + }, + Properties: &Vertex{Values: []quad.Value{quad.IRI("name")}}, + }, + Name: "likesName", + }, + &As{ + From: &View{ + From: &Placeholder{}, + Properties: &Vertex{Values: []quad.Value{quad.IRI("name")}}, + }, + Name: "name", + }, + }, + }, + Name: "person", + }, + }, + results: []interface{}{ + map[string]quad.Value{ + "person": quad.IRI("alice"), + "name": quad.IRI("Alice"), + "likesName": quad.IRI("Bob"), + }, + }, + }, + { + name: "Documents", + data: []quad.Quad{ + quad.MakeIRI("alice", "likes", "bob", ""), + quad.MakeIRI("alice", "name", "Alice", ""), + quad.MakeIRI("bob", "name", "Bob", ""), + quad.MakeIRI("bob", "likes", "alice", ""), + }, + query: &Documents{ + From: &Properties{ + From: &Vertex{Values: []quad.Value{}}, + Names: []quad.IRI{quad.IRI("name"), quad.IRI("likes")}, + }, + }, + results: []interface{}{ + map[string]interface{}{ + "@id": "alice", + "name": []quad.Value{quad.IRI("Alice")}, + "likes": []quad.Value{quad.IRI("bob")}, + }, + map[string]interface{}{ + "@id": "bob", + "name": []quad.Value{quad.IRI("Bob")}, + "likes": []quad.Value{quad.IRI("alice")}, + }, + }, + }, +} + +func TestLinkedQL(t *testing.T) { + for _, c := range testCases { + t.Run(c.name, func(t *testing.T) { + store := memstore.New(c.data...) + ctx := context.TODO() + iterator, err := c.query.BuildIterator(store) + require.NoError(t, err) + var results []interface{} + for iterator.Next(ctx) { + results = append(results, iterator.Result()) + } + require.NoError(t, iterator.Err()) + require.Equal(t, c.results, results) + }) + } +}