query.go 4.7 KB
Newer Older
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
package query

/*
Query represents storage for any key-value pair.

tl;dr:

  queries are supported across datastores.
  Cheap on top of relational dbs, and expensive otherwise.
  Pick the right tool for the job!

In addition to the key-value store get and set semantics, datastore
provides an interface to retrieve multiple records at a time through
the use of queries. The datastore Query model gleans a common set of
operations performed when querying. To avoid pasting here years of
database research, let’s summarize the operations datastore supports.

Query Operations:

  * namespace - scope the query, usually by object type
  * filters - select a subset of values by applying constraints
  * orders - sort the results by applying sort conditions
  * limit - impose a numeric limit on the number of results
  * offset - skip a number of results (for efficient pagination)

datastore combines these operations into a simple Query class that allows
applications to define their constraints in a simple, generic, way without
introducing datastore specific calls, languages, etc.

Of course, different datastores provide relational query support across a
wide spectrum, from full support in traditional databases to none at all in
most key-value stores. Datastore aims to provide a common, simple interface
for the sake of application evolution over time and keeping large code bases
free of tool-specific code. It would be ridiculous to claim to support high-
performance queries on architectures that obviously do not. Instead, datastore
provides the interface, ideally translating queries to their native form
(e.g. into SQL for MySQL).

However, on the wrong datastore, queries can potentially incur the high cost
of performing the aforemantioned query operations on the data set directly in
Go. It is the client’s responsibility to select the right tool for the job:
pick a data storage solution that fits the application’s needs now, and wrap
it with a datastore implementation. As the needs change, swap out datastore
implementations to support your new use cases. Some applications, particularly
in early development stages, can afford to incurr the cost of queries on non-
relational databases (e.g. using a FSDatastore and not worry about a database
at all). When it comes time to switch the tool for performance, updating the
application code can be as simple as swapping the datastore in one place, not
all over the application code base. This gain in engineering time, both at
initial development and during later iterations, can significantly offset the
cost of the layer of abstraction.

*/
type Query struct {

	// Prefix namespaces the query to results whose keys have the Prefix
	Prefix string

	// Filters filter out results from the query
	// They apply sequentially.
	Filters []Filter

	// Orders reorder the query results according to given Orders.
	// They apply sequentially.
	Orders []Order

	// Limit imposes a maximum on the number of results
	Limit int

	// Offset instructs datastores to skip a given number of results.
	Offset int
}

// Entry is a query result entry.
type Entry struct {
	Key   string // cant be ds.Key because circular imports ...!!!
	Value interface{}
}

// Results is a set of Query results
type Results struct {
	Query Query // the query these Results correspond to

	done chan struct{}
	res  chan Entry
	all  []Entry
}

// ResultsWithEntriesChan returns a Results object from a
// channel of ResultEntries. It's merely an encapsulation
// that provides for AllEntries() functionality.
func ResultsWithEntriesChan(q Query, res <-chan Entry) *Results {
	r := &Results{
		Query: q,
		done:  make(chan struct{}),
		res:   make(chan Entry),
		all:   []Entry{},
	}

	// go consume all the results and add them to the results.
	go func() {
		for e := range res {
			r.all = append(r.all, e)
			r.res <- e
		}
		close(r.res)
		close(r.done)
	}()
	return r
}

// ResultsWithEntries returns a Results object from a
// channel of ResultEntries. It's merely an encapsulation
// that provides for AllEntries() functionality.
func ResultsWithEntries(q Query, res []Entry) *Results {
	r := &Results{
		Query: q,
		done:  make(chan struct{}),
		res:   make(chan Entry),
		all:   res,
	}

	// go add all the results
	go func() {
		for _, e := range res {
			r.res <- e
		}
		close(r.res)
		close(r.done)
	}()
	return r
}

// Entries() returns results through a channel.
// Results may arrive at any time.
// The channel may or may not be buffered.
// The channel may or may not rate limit the query processing.
func (r *Results) Entries() <-chan Entry {
	return r.res
}

// AllEntries returns all the entries in Results.
// It blocks until all the results have come in.
func (r *Results) AllEntries() []Entry {
	for e := range r.res {
		_ = e
	}
	<-r.done
	return r.all
}