Revision a09b6f2ee3d05036112172f3f2816638e8c8953f authored by Olaf Conradi on 04 March 2023, 23:51:57 UTC, committed by Olaf Conradi on 04 March 2023, 23:51:57 UTC
1 parent 94f1b49
Raw File
linksto.go
// Copyright 2014 The Cayley Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package graph

// Defines one of the base iterators, the LinksTo iterator. A LinksTo takes a
// subiterator of nodes, and contains an iteration of links which "link to"
// those nodes in a given direction.
//
// Next()ing a LinksTo is straightforward -- iterate through all links to //
// things in the subiterator, and then advance the subiterator, and do it again.
// LinksTo is therefore sensitive to growing with a fanout. (A small-sized
// subiterator could cause LinksTo to be large).
//
// Contains()ing a LinksTo means, given a link, take the direction we care about
// and check if it's in our subiterator. Checking is therefore fairly cheap, and
// similar to checking the subiterator alone.
//
// Can be seen as the dual of the HasA iterator.

import (
	"context"
	"fmt"

	"github.com/cayleygraph/cayley/graph/iterator"
	"github.com/cayleygraph/cayley/graph/refs"
	"github.com/cayleygraph/quad"
)

// A LinksTo has a reference back to the graph.QuadStore (to create the iterators
// for each node) the subiterator, and the direction the iterator comes from.
// `next_it` is the tempoarary iterator held per result in `primary_it`.
type LinksTo struct {
	qs      QuadIndexer
	primary iterator.Shape
	dir     quad.Direction
	size    refs.Size
}

// NewLinksTo construct a new LinksTo iterator around a direction and a subiterator of
// nodes.
func NewLinksTo(qs QuadIndexer, it iterator.Shape, d quad.Direction) *LinksTo {
	return &LinksTo{
		qs:      qs,
		primary: it,
		dir:     d,
	}
}

// Direction returns the direction under consideration.
func (it *LinksTo) Direction() quad.Direction { return it.dir }

func (it *LinksTo) Iterate() iterator.Scanner {
	return newLinksToNext(it.qs, it.primary.Iterate(), it.dir)
}

func (it *LinksTo) Lookup() iterator.Index {
	return newLinksToContains(it.qs, it.primary.Lookup(), it.dir)
}

func (it *LinksTo) String() string {
	return fmt.Sprintf("LinksTo(%v)", it.dir)
}

// SubIterators returns a list containing only our subiterator.
func (it *LinksTo) SubIterators() []iterator.Shape {
	return []iterator.Shape{it.primary}
}

// Optimize the LinksTo, by replacing it if it can be.
func (it *LinksTo) Optimize(ctx context.Context) (iterator.Shape, bool) {
	newPrimary, changed := it.primary.Optimize(ctx)
	if changed {
		it.primary = newPrimary
		if iterator.IsNull(it.primary) {
			return it.primary, true
		}
	}
	return it, false
}

// Stats returns a guess as to how big or costly it is to next the iterator.
func (it *LinksTo) Stats(ctx context.Context) (iterator.Costs, error) {
	subitStats, err := it.primary.Stats(ctx)
	// TODO(barakmich): These should really come from the quadstore itself
	checkConstant := int64(1)
	nextConstant := int64(2)
	return iterator.Costs{
		NextCost:     nextConstant + subitStats.NextCost,
		ContainsCost: checkConstant + subitStats.ContainsCost,
		Size:         it.getSize(ctx),
	}, err
}

func (it *LinksTo) getSize(ctx context.Context) refs.Size {
	if it.size.Value != 0 {
		return it.size
	}
	if fixed, ok := it.primary.(*iterator.Fixed); ok {
		// get real sizes from sub iterators
		var (
			sz    int64
			exact = true
		)
		for _, v := range fixed.Values() {
			sit := it.qs.QuadIterator(it.dir, v)
			st, _ := sit.Stats(ctx)
			sz += st.Size.Value
			exact = exact && st.Size.Exact
		}
		it.size.Value, it.size.Exact = sz, exact
		return it.size
	}
	stats, _ := it.qs.Stats(ctx, false)
	maxSize := stats.Quads.Value/2 + 1
	// TODO(barakmich): It should really come from the quadstore itself
	const fanoutFactor = 20
	st, _ := it.primary.Stats(ctx)
	value := st.Size.Value * fanoutFactor
	if value > maxSize {
		value = maxSize
	}
	it.size.Value, it.size.Exact = value, false
	return it.size
}

// A LinksTo has a reference back to the graph.QuadStore (to create the iterators
// for each node) the subiterator, and the direction the iterator comes from.
// `next_it` is the tempoarary iterator held per result in `primary_it`.
type linksToNext struct {
	qs      QuadIndexer
	primary iterator.Scanner
	dir     quad.Direction
	nextIt  iterator.Scanner
	result  refs.Ref
	err     error
}

// Construct a new LinksTo iterator around a direction and a subiterator of
// nodes.
func newLinksToNext(qs QuadIndexer, it iterator.Scanner, d quad.Direction) iterator.Scanner {
	return &linksToNext{
		qs:      qs,
		primary: it,
		dir:     d,
		nextIt:  iterator.NewNull().Iterate(),
	}
}

// Return the direction under consideration.
func (it *linksToNext) Direction() quad.Direction { return it.dir }

// Tag these results, and our subiterator's results.
func (it *linksToNext) TagResults(dst map[string]refs.Ref) {
	it.primary.TagResults(dst)
}

func (it *linksToNext) String() string {
	return fmt.Sprintf("LinksToNext(%v)", it.dir)
}

// Next()ing a LinksTo operates as described above.
func (it *linksToNext) Next(ctx context.Context) bool {
	for {
		if it.nextIt.Next(ctx) {
			it.result = it.nextIt.Result()
			return true
		}

		// If there's an error in the 'next' iterator, we save it and we're done.
		it.err = it.nextIt.Err()
		if it.err != nil {
			return false
		}

		// Subiterator is empty, get another one
		if !it.primary.Next(ctx) {
			// Possibly save error
			it.err = it.primary.Err()

			// We're out of nodes in our subiterator, so we're done as well.
			return false
		}
		it.nextIt.Close()
		it.nextIt = it.qs.QuadIterator(it.dir, it.primary.Result()).Iterate()

		// Continue -- return the first in the next set.
	}
}

func (it *linksToNext) Err() error {
	return it.err
}

func (it *linksToNext) Result() refs.Ref {
	return it.result
}

// Close closes the iterator.  It closes all subiterators it can, but
// returns the first error it encounters.
func (it *linksToNext) Close() error {
	err := it.nextIt.Close()

	_err := it.primary.Close()
	if _err != nil && err == nil {
		err = _err
	}

	return err
}

// We won't ever have a new result, but our subiterators might.
func (it *linksToNext) NextPath(ctx context.Context) bool {
	ok := it.primary.NextPath(ctx)
	if !ok {
		it.err = it.primary.Err()
	}
	return ok
}

// A LinksTo has a reference back to the graph.QuadStore (to create the iterators
// for each node) the subiterator, and the direction the iterator comes from.
// `next_it` is the tempoarary iterator held per result in `primary_it`.
type linksToContains struct {
	qs      QuadIndexer
	primary iterator.Index
	dir     quad.Direction
	result  refs.Ref
	err     error
}

// Construct a new LinksTo iterator around a direction and a subiterator of
// nodes.
func newLinksToContains(qs QuadIndexer, it iterator.Index, d quad.Direction) iterator.Index {
	return &linksToContains{
		qs:      qs,
		primary: it,
		dir:     d,
	}
}

// Return the direction under consideration.
func (it *linksToContains) Direction() quad.Direction { return it.dir }

// Tag these results, and our subiterator's results.
func (it *linksToContains) TagResults(dst map[string]refs.Ref) {
	it.primary.TagResults(dst)
}

func (it *linksToContains) String() string {
	return fmt.Sprintf("LinksToContains(%v)", it.dir)
}

// If it checks in the right direction for the subiterator, it is a valid link
// for the LinksTo.
func (it *linksToContains) Contains(ctx context.Context, val refs.Ref) bool {
	node, err := it.qs.QuadDirection(val, it.dir)
	if err != nil {
		it.err = err
		return false
	}
	if it.primary.Contains(ctx, node) {
		it.result = val
		return true
	}
	return false
}

func (it *linksToContains) Err() error {
	if it.err != nil {
		return it.err
	}
	return it.primary.Err()
}

func (it *linksToContains) Result() refs.Ref {
	return it.result
}

// Close closes the iterator.  It closes all subiterators it can, but
// returns the first error it encounters.
func (it *linksToContains) Close() error {
	return it.primary.Close()
}

// We won't ever have a new result, but our subiterators might.
func (it *linksToContains) NextPath(ctx context.Context) bool {
	return it.primary.NextPath(ctx)
}
back to top