diff --git a/data/testdata.nq b/data/testdata.nq index 7c1dce31d..d8fc62ebc 100644 --- a/data/testdata.nq +++ b/data/testdata.nq @@ -1,9 +1,9 @@ . . "cool_person" . + . . . - . . "cool_person" . . diff --git a/docs/gizmoapi.md b/docs/gizmoapi.md index 811785d78..b0a7fa407 100644 --- a/docs/gizmoapi.md +++ b/docs/gizmoapi.md @@ -677,3 +677,6 @@ cFollows.union(dFollows).all(); Unique removes duplicate values from the path. +### `path.order()` + +Order returns values from the path in ascending order. diff --git a/graph/iterator/sort.go b/graph/iterator/sort.go new file mode 100644 index 000000000..e643c33d3 --- /dev/null +++ b/graph/iterator/sort.go @@ -0,0 +1,203 @@ +package iterator + +import ( + "context" + "sort" + + "github.com/cayleygraph/cayley/graph" +) + +var _ graph.IteratorFuture = &Sort{} + +// Sort iterator orders values from it's subiterator. +type Sort struct { + it *sortIt + graph.Iterator +} + +// NewSort creates a new Sort iterator. +// TODO(dennwc): This iterator must not be used inside And: it may be moved to a Contains branch and won't do anything. +// We should make And/Intersect account for this. +func NewSort(namer graph.Namer, it graph.Iterator) *Sort { + return &Sort{ + it: newSort(namer, graph.AsShape(it)), + } +} + +// AsShape returns Sort's underlying iterator shape +func (it *Sort) AsShape() graph.IteratorShape { + return it.it +} + +type sortIt struct { + namer graph.Namer + subIt graph.IteratorShape +} + +var _ graph.IteratorShapeCompat = (*sortIt)(nil) + +func newSort(namer graph.Namer, subIt graph.IteratorShape) *sortIt { + return &sortIt{namer, subIt} +} + +func (it *sortIt) Iterate() graph.Scanner { + return newSortNext(it.namer, it.subIt.Iterate()) +} + +func (it *sortIt) AsLegacy() graph.Iterator { + it2 := &Sort{it: it} + it2.Iterator = graph.NewLegacy(it, it2) + return it2 +} + +func (it *sortIt) Lookup() graph.Index { + // TODO(dennwc): Lookup doesn't need any sorting. Using it this way is a bug in the optimizer. + // But instead of failing here, let still allow the query to execute. It won't be sorted, + // but it will work at least. Later consider changing returning an error here. + return it.subIt.Lookup() +} + +func (it *sortIt) Optimize(ctx context.Context) (graph.IteratorShape, bool) { + newIt, optimized := it.subIt.Optimize(ctx) + if optimized { + it.subIt = newIt + } + return it, false +} + +func (it *sortIt) Stats(ctx context.Context) (graph.IteratorCosts, error) { + subStats, err := it.subIt.Stats(ctx) + return graph.IteratorCosts{ + // TODO(dennwc): better cost calculation; we probably need an InitCost defined in graph.IteratorCosts + NextCost: subStats.NextCost * 2, + ContainsCost: subStats.ContainsCost, + Size: graph.Size{ + Size: subStats.Size.Size, + Exact: true, + }, + }, err +} + +func (it *sortIt) String() string { + return "Sort" +} + +// SubIterators returns a slice of the sub iterators. +func (it *sortIt) SubIterators() []graph.IteratorShape { + return []graph.IteratorShape{it.subIt} +} + +type sortValue struct { + result + str string + paths []result +} +type sortByString []sortValue + +func (v sortByString) Len() int { return len(v) } +func (v sortByString) Less(i, j int) bool { + return v[i].str < v[j].str +} +func (v sortByString) Swap(i, j int) { v[i], v[j] = v[j], v[i] } + +type sortNext struct { + namer graph.Namer + subIt graph.Scanner + ordered sortByString + result result + err error + index int + pathIndex int +} + +func newSortNext(namer graph.Namer, subIt graph.Scanner) *sortNext { + return &sortNext{ + namer: namer, + subIt: subIt, + pathIndex: -1, + } +} + +func (it *sortNext) TagResults(dst map[string]graph.Value) { + for tag, value := range it.result.tags { + dst[tag] = value + } +} + +func (it *sortNext) Err() error { + return it.err +} + +func (it *sortNext) Result() graph.Value { + return it.result.id +} + +func (it *sortNext) Next(ctx context.Context) bool { + if it.err != nil { + return false + } + if it.ordered == nil { + v, err := getSortedValues(ctx, it.namer, it.subIt) + it.ordered = v + it.err = err + if it.err != nil { + return false + } + } + if it.index >= len(it.ordered) { + return false + } + it.pathIndex = -1 + it.result = it.ordered[it.index].result + it.index++ + return true +} + +func (it *sortNext) NextPath(ctx context.Context) bool { + if it.index >= len(it.ordered) { + return false + } + r := it.ordered[it.index] + if it.pathIndex+1 >= len(r.paths) { + return false + } + it.pathIndex++ + it.result = r.paths[it.pathIndex] + return true +} + +func (it *sortNext) Close() error { + it.ordered = nil + return it.subIt.Close() +} + +func (it *sortNext) String() string { + return "SortNext" +} + +func getSortedValues(ctx context.Context, namer graph.Namer, it graph.Scanner) (sortByString, error) { + var v sortByString + for it.Next(ctx) { + id := it.Result() + // TODO(dennwc): batch and use graph.ValuesOf + name := namer.NameOf(id) + str := name.String() + tags := make(map[string]graph.Ref) + it.TagResults(tags) + val := sortValue{ + result: result{id, tags}, + str: str, + } + for it.NextPath(ctx) { + tags = make(map[string]graph.Ref) + it.TagResults(tags) + val.paths = append(val.paths, result{id, tags}) + } + v = append(v, val) + } + if err := it.Err(); err != nil { + return v, err + } + sort.Sort(v) + return v, nil +} diff --git a/graph/path/morphism_apply_functions.go b/graph/path/morphism_apply_functions.go index dcafc3cde..47460dc52 100644 --- a/graph/path/morphism_apply_functions.go +++ b/graph/path/morphism_apply_functions.go @@ -412,6 +412,15 @@ func skipMorphism(v int64) morphism { } } +func orderMorphism() morphism { + return morphism{ + Reversal: func(ctx *pathContext) (morphism, *pathContext) { return orderMorphism(), ctx }, + Apply: func(in shape.Shape, ctx *pathContext) (shape.Shape, *pathContext) { + return shape.Sort{From: in}, ctx + }, + } +} + // limitMorphism will limit a number of values-- if number is negative or zero, this function // acts as a passthrough for the previous iterator. func limitMorphism(v int64) morphism { diff --git a/graph/path/path.go b/graph/path/path.go index 0957e6474..e60958c66 100644 --- a/graph/path/path.go +++ b/graph/path/path.go @@ -536,6 +536,11 @@ func (p *Path) Skip(v int64) *Path { return p } +func (p *Path) Order() *Path { + p.stack = append(p.stack, orderMorphism()) + return p +} + // Limit will limit a number of values in result set. func (p *Path) Limit(v int64) *Path { p.stack = append(p.stack, limitMorphism(v)) diff --git a/graph/path/pathtest/pathtest.go b/graph/path/pathtest/pathtest.go index 14ac270b0..a15e7cfc8 100644 --- a/graph/path/pathtest/pathtest.go +++ b/graph/path/pathtest/pathtest.go @@ -92,6 +92,7 @@ type test struct { expect []quad.Value expectAlt [][]quad.Value tag string + unsorted bool } // Define morphisms without a QuadStore @@ -433,6 +434,60 @@ func testSet(qs graph.QuadStore) []test { path: StartPath(qs, quad.IRI("")), expect: nil, }, + { + message: "use order", + path: StartPath(qs).Order(), + expect: []quad.Value{ + vAlice, + vAre, + vBob, + vCharlie, + vDani, + vEmily, + vFollows, + vFred, + vGreg, + vPredicate, + vSmartGraph, + vStatus, + vCool, + vSmart, + }, + }, + { + message: "use order tags", + path: StartPath(qs).Tag("target").Order(), + tag: "target", + expect: []quad.Value{ + vAlice, + vAre, + vBob, + vCharlie, + vDani, + vEmily, + vFollows, + vFred, + vGreg, + vPredicate, + vSmartGraph, + vStatus, + vCool, + vSmart, + }, + }, + { + message: "order with a next path", + path: StartPath(qs, vDani, vBob).Save(vFollows, "target").Order(), + tag: "target", + expect: []quad.Value{vBob, vFred, vGreg}, + }, + { + message: "order with a next path", + path: StartPath(qs).Order().Has(vFollows, vBob), + expect: []quad.Value{vAlice, vCharlie, vDani}, + unsorted: true, + skip: true, // TODO(dennwc): optimize Order in And properly + }, } } @@ -470,20 +525,26 @@ func RunTestMorphisms(t *testing.T, fnc testutil.DatabaseFunc) { t.Error(err) return } - sort.Sort(quad.ByValueString(got)) + if !test.unsorted { + sort.Sort(quad.ByValueString(got)) + } var eq bool exp := test.expect if test.expectAlt != nil { for _, alt := range test.expectAlt { exp = alt - sort.Sort(quad.ByValueString(exp)) + if !test.unsorted { + sort.Sort(quad.ByValueString(exp)) + } eq = reflect.DeepEqual(got, exp) if eq { break } } } else { - sort.Sort(quad.ByValueString(test.expect)) + if !test.unsorted { + sort.Sort(quad.ByValueString(test.expect)) + } eq = reflect.DeepEqual(got, test.expect) } if !eq { diff --git a/graph/shape/path.go b/graph/shape/path.go index 57112677e..9688001b2 100644 --- a/graph/shape/path.go +++ b/graph/shape/path.go @@ -240,4 +240,4 @@ func Compare(nodes Shape, op iterator.Operator, v quad.Value) Shape { func Iterate(ctx context.Context, qs graph.QuadStore, s Shape) *graph.IterateChain { it := BuildIterator(qs, s) return graph.Iterate(ctx, it).On(qs) -} +} \ No newline at end of file diff --git a/graph/shape/shape.go b/graph/shape/shape.go index 055710ab1..d756af53d 100644 --- a/graph/shape/shape.go +++ b/graph/shape/shape.go @@ -1433,3 +1433,30 @@ func FilterQuads(subject, predicate, object, label []quad.Value) Shape { } return q } + +type Sort struct { + From Shape +} + +func (s Sort) BuildIterator(qs graph.QuadStore) graph.Iterator { + if IsNull(s.From) { + return iterator.NewNull() + } + it := s.From.BuildIterator(qs) + return iterator.NewSort(qs, it) +} +func (s Sort) Optimize(r Optimizer) (Shape, bool) { + if IsNull(s.From) { + return nil, true + } + var opt bool + s.From, opt = s.From.Optimize(r) + if IsNull(s.From) { + return nil, true + } + if r != nil { + ns, nopt := r.OptimizeShape(s) + return ns, opt || nopt + } + return s, opt +} diff --git a/query/gizmo/gizmo_test.go b/query/gizmo/gizmo_test.go index da192ff63..44060a90a 100644 --- a/query/gizmo/gizmo_test.go +++ b/query/gizmo/gizmo_test.go @@ -540,7 +540,7 @@ var testQueries = []struct { arr = g.V("").in("").toArray(2) for (i in arr) g.emit(arr[i]); `, - expect: []string{"", ""}, + expect: []string{"", ""}, }, { message: "show ForEach", @@ -554,7 +554,7 @@ var testQueries = []struct { query: ` g.V("").in("").forEach(2, function(o){g.emit(o.id)}); `, - expect: []string{"", ""}, + expect: []string{"", ""}, }, { message: "clone paths", @@ -639,6 +639,51 @@ var testQueries = []struct { file: multiGraphTestFile, expect: []string{""}, }, + { + message: "use order", + query: ` + g.V().order().all() + `, + expect: []string{ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "cool_person", + "smart_person", + }, + }, + { + message: "use order tags", + query: ` + g.V().Tag("target").order().all() + `, + tag: "target", + expect: []string{ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "cool_person", + "smart_person", + }, + }, } func runQueryGetTag(rec func(), g []quad.Quad, qu string, tag string, limit int) ([]string, error) { diff --git a/query/gizmo/traversals.go b/query/gizmo/traversals.go index e56e75b5a..4035ddf10 100644 --- a/query/gizmo/traversals.go +++ b/query/gizmo/traversals.go @@ -684,6 +684,11 @@ func (p *pathObject) Skip(offset int) *pathObject { return p.new(np) } +func (p *pathObject) Order() *pathObject { + np := p.clonePath().Order() + return p.new(np) +} + // Backwards compatibility func (p *pathObject) CapitalizedIs(call goja.FunctionCall) goja.Value { return p.Is(call) diff --git a/query/graphql/graphql_test.go b/query/graphql/graphql_test.go index 4547f4f4c..ec3f8a9de 100644 --- a/query/graphql/graphql_test.go +++ b/query/graphql/graphql_test.go @@ -123,8 +123,8 @@ var casesExecute = []struct { "follows": nil, "followed": []M{ {ValueKey: quad.IRI("alice")}, - {ValueKey: quad.IRI("charlie")}, {ValueKey: quad.IRI("dani")}, + {ValueKey: quad.IRI("charlie")}, }, }, { @@ -283,8 +283,8 @@ var casesExecute = []struct { {"id": quad.IRI("fred")}, {"id": quad.IRI("status")}, {"id": quad.String("cool_person")}, - {"id": quad.IRI("charlie")}, {"id": quad.IRI("dani"), "status": quad.String("cool_person")}, + {"id": quad.IRI("charlie")}, {"id": quad.IRI("greg"), "status": []quad.Value{ quad.String("cool_person"), quad.String("smart_person"), diff --git a/query/mql/mql_test.go b/query/mql/mql_test.go index 5e4165397..205238b65 100644 --- a/query/mql/mql_test.go +++ b/query/mql/mql_test.go @@ -69,8 +69,8 @@ var testQueries = []struct { {"id": ""}, {"id": ""}, {"id": "cool_person"}, - {"id": ""}, {"id": ""}, + {"id": ""}, {"id": ""}, {"id": ""}, {"id": ""}, @@ -124,8 +124,8 @@ var testQueries = []struct { expect: ` [ {"id": "", "": {"id": "", "": "cool_person"}}, - {"id": "", "": {"id": "", "": "cool_person"}}, {"id": "", "": {"id": "", "": "cool_person"}}, + {"id": "", "": {"id": "", "": "cool_person"}}, {"id": "", "": {"id": "", "": "cool_person"}} ] `,