Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

path.Order() #796

Merged
merged 11 commits into from
Oct 8, 2019
2 changes: 1 addition & 1 deletion data/testdata.nq
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<alice> <follows> <bob> .
<bob> <follows> <fred> .
<bob> <status> "cool_person" .
<dani> <follows> <bob> .
<charlie> <follows> <bob> .
<charlie> <follows> <dani> .
<dani> <follows> <bob> .
<dani> <follows> <greg> .
<dani> <status> "cool_person" .
<emily> <follows> <fred> .
Expand Down
3 changes: 3 additions & 0 deletions docs/gizmoapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
203 changes: 203 additions & 0 deletions graph/iterator/sort.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions graph/path/morphism_apply_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions graph/path/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
67 changes: 64 additions & 3 deletions graph/path/pathtest/pathtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ type test struct {
expect []quad.Value
expectAlt [][]quad.Value
tag string
unsorted bool
}

// Define morphisms without a QuadStore
Expand Down Expand Up @@ -433,6 +434,60 @@ func testSet(qs graph.QuadStore) []test {
path: StartPath(qs, quad.IRI("<not-existing>")),
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
},
}
}

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion graph/shape/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
27 changes: 27 additions & 0 deletions graph/shape/shape.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading