You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

345 lines
12 KiB

//
// DISCLAIMER
//
// Copyright 2017 ArangoDB GmbH, Cologne, Germany
//
// 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.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//
package driver
import (
"context"
"encoding/json"
"path"
"reflect"
"sync"
"sync/atomic"
"time"
)
// newCursor creates a new Cursor implementation.
func newCursor(data cursorData, endpoint string, db *database, allowDirtyReads bool) (Cursor, error) {
if db == nil {
return nil, WithStack(InvalidArgumentError{Message: "db is nil"})
}
return &cursor{
cursorData: data,
endpoint: endpoint,
db: db,
conn: db.conn,
allowDirtyReads: allowDirtyReads,
}, nil
}
type cursor struct {
cursorData
endpoint string
resultIndex int
db *database
conn Connection
closed int32
closeMutex sync.Mutex
allowDirtyReads bool
lastReadWasDirty bool
}
// CursorStats TODO: all these int64 should be changed into uint64
type cursorStats struct {
// The total number of data-modification operations successfully executed.
WritesExecutedInt int64 `json:"writesExecuted,omitempty"`
// The total number of data-modification operations that were unsuccessful
WritesIgnoredInt int64 `json:"writesIgnored,omitempty"`
// The total number of documents iterated over when scanning a collection without an index.
ScannedFullInt int64 `json:"scannedFull,omitempty"`
// The total number of documents iterated over when scanning a collection using an index.
ScannedIndexInt int64 `json:"scannedIndex,omitempty"`
// The total number of documents that were removed after executing a filter condition in a FilterNode
FilteredInt int64 `json:"filtered,omitempty"`
// The total number of documents that matched the search condition if the query's final LIMIT statement were not present.
FullCountInt int64 `json:"fullCount,omitempty"`
// Query execution time (wall-clock time). value will be set from the outside
ExecutionTimeInt float64 `json:"executionTime,omitempty"`
Nodes []cursorPlanNodes `json:"nodes,omitempty"`
HttpRequests int64 `json:"httpRequests,omitempty"`
PeakMemoryUsage int64 `json:"peakMemoryUsage,omitempty"`
// CursorsCreated the total number of cursor objects created during query execution. Cursor objects are created for index lookups.
CursorsCreated uint64 `json:"cursorsCreated,omitempty"`
// CursorsRearmed the total number of times an existing cursor object was repurposed.
// Repurposing an existing cursor object is normally more efficient compared to destroying an existing cursor object
// and creating a new one from scratch.
CursorsRearmed uint64 `json:"cursorsRearmed,omitempty"`
// CacheHits the total number of index entries read from in-memory caches for indexes of type edge or persistent.
// This value will only be non-zero when reading from indexes that have an in-memory cache enabled,
// and when the query allows using the in-memory cache (i.e. using equality lookups on all index attributes).
CacheHits uint64 `json:"cacheHits,omitempty"`
// CacheMisses the total number of cache read attempts for index entries that could not be served from in-memory caches for indexes of type edge or persistent.
// This value will only be non-zero when reading from indexes that have an in-memory cache enabled,
// the query allows using the in-memory cache (i.e. using equality lookups on all index attributes) and the looked up values are not present in the cache.
CacheMisses uint64 `json:"cacheMisses,omitempty"`
}
type cursorPlan struct {
Nodes []cursorPlanNodes `json:"nodes,omitempty"`
Rules []string `json:"rules,omitempty"`
Collections []cursorPlanCollection `json:"collections,omitempty"`
Variables []cursorPlanVariable `json:"variables,omitempty"`
EstimatedCost float64 `json:"estimatedCost,omitempty"`
EstimatedNrItems int `json:"estimatedNrItems,omitempty"`
IsModificationQuery bool `json:"isModificationQuery,omitempty"`
}
type cursorExtra struct {
Stats cursorStats `json:"stats,omitempty"`
Profile cursorProfile `json:"profile,omitempty"`
Plan *cursorPlan `json:"plan,omitempty"`
Warnings []warn `json:"warnings,omitempty"`
}
type warn struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (c cursorExtra) GetStatistics() QueryStatistics {
return c.Stats
}
func (c cursorExtra) GetProfileRaw() ([]byte, bool, error) {
if c.Profile == nil {
return nil, false, nil
}
d, err := json.Marshal(c.Profile)
if err != nil {
return nil, true, err
}
return d, true, nil
}
func (c cursorExtra) GetPlanRaw() ([]byte, bool, error) {
if c.Plan == nil {
return nil, false, nil
}
d, err := json.Marshal(c.Plan)
if err != nil {
return nil, true, err
}
return d, true, nil
}
type cursorPlanVariable struct {
ID int `json:"id"`
Name string `json:"name"`
IsDataFromCollection bool `json:"isDataFromCollection"`
IsFullDocumentFromCollection bool `json:"isFullDocumentFromCollection"`
}
type cursorPlanCollection struct {
Name string `json:"name"`
Type string `json:"type"`
}
type cursorPlanNodes map[string]interface{}
type cursorProfile map[string]interface{}
type cursorData struct {
Key string `json:"_key,omitempty"`
Count int64 `json:"count,omitempty"` // the total number of result documents available (only available if the query was executed with the count attribute set)
ID string `json:"id"` // id of temporary cursor created on the server (optional, see above)
Result []*RawObject `json:"result,omitempty"` // an array of result documents (might be empty if query has no results)
HasMore bool `json:"hasMore,omitempty"` // A boolean indicator whether there are more results available for the cursor on the server
Extra cursorExtra `json:"extra"`
Cached bool `json:"cached,omitempty"`
ArangoError
}
// relPath creates the relative path to this cursor (`_db/<db-name>/_api/cursor`)
func (c *cursor) relPath() string {
return path.Join(c.db.relPath(), "_api", "cursor")
}
// Name returns the name of the collection.
func (c *cursor) HasMore() bool {
return c.resultIndex < len(c.Result) || c.cursorData.HasMore
}
// Count returns the total number of result documents available.
// A valid return value is only available when the cursor has been created with a context that was
// prepare with `WithQueryCount`.
func (c *cursor) Count() int64 {
return c.cursorData.Count
}
// Close deletes the cursor and frees the resources associated with it.
func (c *cursor) Close() error {
if c == nil {
// Avoid panics in the case that someone defer's a close before checking that the cursor is not nil.
return nil
}
if c := atomic.LoadInt32(&c.closed); c != 0 {
return nil
}
c.closeMutex.Lock()
defer c.closeMutex.Unlock()
if c.closed == 0 {
if c.cursorData.ID != "" {
// Force use of initial endpoint
ctx := WithEndpoint(nil, c.endpoint)
req, err := c.conn.NewRequest("DELETE", path.Join(c.relPath(), c.cursorData.ID))
if err != nil {
return WithStack(err)
}
resp, err := c.conn.Do(ctx, req)
if err != nil {
return WithStack(err)
}
if err := resp.CheckStatus(202); err != nil {
return WithStack(err)
}
}
atomic.StoreInt32(&c.closed, 1)
}
return nil
}
// ReadDocument reads the next document from the cursor.
// The document data is stored into result, the document meta data is returned.
// If the cursor has no more documents, a NoMoreDocuments error is returned.
func (c *cursor) ReadDocument(ctx context.Context, result interface{}) (DocumentMeta, error) {
// Force use of initial endpoint
ctx = WithEndpoint(ctx, c.endpoint)
if c.resultIndex >= len(c.Result) && c.cursorData.HasMore {
// This is required since we are interested if this was a dirty read
// but we do not want to trash the users bool reference.
var wasDirtyRead bool
fetchctx := ctx
if c.allowDirtyReads {
fetchctx = WithAllowDirtyReads(ctx, &wasDirtyRead)
}
// Fetch next batch
req, err := c.conn.NewRequest("PUT", path.Join(c.relPath(), c.cursorData.ID))
if err != nil {
return DocumentMeta{}, WithStack(err)
}
cs := applyContextSettings(fetchctx, req)
resp, err := c.conn.Do(fetchctx, req)
if err != nil {
return DocumentMeta{}, WithStack(err)
}
if err := resp.CheckStatus(200); err != nil {
return DocumentMeta{}, WithStack(err)
}
loadContextResponseValues(cs, resp)
var data cursorData
if err := resp.ParseBody("", &data); err != nil {
return DocumentMeta{}, WithStack(err)
}
c.cursorData = data
c.resultIndex = 0
c.lastReadWasDirty = wasDirtyRead
}
// ReadDocument should act as if it would actually do a read
// hence update the bool reference
if c.allowDirtyReads {
setDirtyReadFlagIfRequired(ctx, c.lastReadWasDirty)
}
index := c.resultIndex
if index >= len(c.Result) {
// Out of data
return DocumentMeta{}, WithStack(NoMoreDocumentsError{})
}
c.resultIndex++
var meta DocumentMeta
resultPtr := c.Result[index]
if resultPtr == nil {
// Got NULL result
rv := reflect.ValueOf(result)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return DocumentMeta{}, WithStack(&json.InvalidUnmarshalError{Type: reflect.TypeOf(result)})
}
e := rv.Elem()
e.Set(reflect.Zero(e.Type()))
} else {
if err := c.conn.Unmarshal(*resultPtr, &meta); err != nil {
// If a cursor returns something other than a document, this will fail.
// Just ignore it.
}
if err := c.conn.Unmarshal(*resultPtr, result); err != nil {
return DocumentMeta{}, WithStack(err)
}
}
return meta, nil
}
// Return execution statistics for this cursor. This might not
// be valid if the cursor has been created with a context that was
// prepared with `WithStream`
func (c *cursor) Statistics() QueryStatistics {
return c.cursorData.Extra.Stats
}
func (c *cursor) Extra() QueryExtra {
return c.cursorData.Extra
}
// the total number of data-modification operations successfully executed.
func (cs cursorStats) WritesExecuted() int64 {
return cs.WritesExecutedInt
}
// The total number of data-modification operations that were unsuccessful
func (cs cursorStats) WritesIgnored() int64 {
return cs.WritesIgnoredInt
}
// The total number of documents iterated over when scanning a collection without an index.
func (cs cursorStats) ScannedFull() int64 {
return cs.ScannedFullInt
}
// The total number of documents iterated over when scanning a collection using an index.
func (cs cursorStats) ScannedIndex() int64 {
return cs.ScannedIndexInt
}
// the total number of documents that were removed after executing a filter condition in a FilterNode
func (cs cursorStats) Filtered() int64 {
return cs.FilteredInt
}
// Returns the numer of results before the last LIMIT in the query was applied.
// A valid return value is only available when the has been created with a context that was
// prepared with `WithFullCount`. Additionally this will also not return a valid value if
// the context was prepared with `WithStream`.
func (cs cursorStats) FullCount() int64 {
return cs.FullCountInt
}
// query execution time (wall-clock time). value will be set from the outside
func (cs cursorStats) ExecutionTime() time.Duration {
return time.Duration(cs.ExecutionTimeInt * float64(time.Second))
}