301 lines
7.9 KiB
Go
301 lines
7.9 KiB
Go
package gocb
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
gocbcore "github.com/couchbase/gocbcore/v9"
|
|
)
|
|
|
|
type jsonAnalyticsMetrics struct {
|
|
ElapsedTime string `json:"elapsedTime"`
|
|
ExecutionTime string `json:"executionTime"`
|
|
ResultCount uint64 `json:"resultCount"`
|
|
ResultSize uint64 `json:"resultSize"`
|
|
MutationCount uint64 `json:"mutationCount,omitempty"`
|
|
SortCount uint64 `json:"sortCount,omitempty"`
|
|
ErrorCount uint64 `json:"errorCount,omitempty"`
|
|
WarningCount uint64 `json:"warningCount,omitempty"`
|
|
ProcessedObjects uint64 `json:"processedObjects,omitempty"`
|
|
}
|
|
|
|
type jsonAnalyticsWarning struct {
|
|
Code uint32 `json:"code"`
|
|
Message string `json:"msg"`
|
|
}
|
|
|
|
type jsonAnalyticsResponse struct {
|
|
RequestID string `json:"requestID"`
|
|
ClientContextID string `json:"clientContextID"`
|
|
Status string `json:"status"`
|
|
Warnings []jsonAnalyticsWarning `json:"warnings"`
|
|
Metrics jsonAnalyticsMetrics `json:"metrics"`
|
|
Signature interface{} `json:"signature"`
|
|
}
|
|
|
|
// AnalyticsMetrics encapsulates various metrics gathered during a queries execution.
|
|
type AnalyticsMetrics struct {
|
|
ElapsedTime time.Duration
|
|
ExecutionTime time.Duration
|
|
ResultCount uint64
|
|
ResultSize uint64
|
|
MutationCount uint64
|
|
SortCount uint64
|
|
ErrorCount uint64
|
|
WarningCount uint64
|
|
ProcessedObjects uint64
|
|
}
|
|
|
|
func (metrics *AnalyticsMetrics) fromData(data jsonAnalyticsMetrics) error {
|
|
elapsedTime, err := time.ParseDuration(data.ElapsedTime)
|
|
if err != nil {
|
|
logDebugf("Failed to parse query metrics elapsed time: %s", err)
|
|
}
|
|
|
|
executionTime, err := time.ParseDuration(data.ExecutionTime)
|
|
if err != nil {
|
|
logDebugf("Failed to parse query metrics execution time: %s", err)
|
|
}
|
|
|
|
metrics.ElapsedTime = elapsedTime
|
|
metrics.ExecutionTime = executionTime
|
|
metrics.ResultCount = data.ResultCount
|
|
metrics.ResultSize = data.ResultSize
|
|
metrics.MutationCount = data.MutationCount
|
|
metrics.SortCount = data.SortCount
|
|
metrics.ErrorCount = data.ErrorCount
|
|
metrics.WarningCount = data.WarningCount
|
|
metrics.ProcessedObjects = data.ProcessedObjects
|
|
|
|
return nil
|
|
}
|
|
|
|
// AnalyticsWarning encapsulates any warnings returned by a query.
|
|
type AnalyticsWarning struct {
|
|
Code uint32
|
|
Message string
|
|
}
|
|
|
|
func (warning *AnalyticsWarning) fromData(data jsonAnalyticsWarning) error {
|
|
warning.Code = data.Code
|
|
warning.Message = data.Message
|
|
|
|
return nil
|
|
}
|
|
|
|
// AnalyticsMetaData provides access to the meta-data properties of a query result.
|
|
type AnalyticsMetaData struct {
|
|
RequestID string
|
|
ClientContextID string
|
|
Metrics AnalyticsMetrics
|
|
Signature interface{}
|
|
Warnings []AnalyticsWarning
|
|
}
|
|
|
|
func (meta *AnalyticsMetaData) fromData(data jsonAnalyticsResponse) error {
|
|
metrics := AnalyticsMetrics{}
|
|
if err := metrics.fromData(data.Metrics); err != nil {
|
|
return err
|
|
}
|
|
|
|
warnings := make([]AnalyticsWarning, len(data.Warnings))
|
|
for wIdx, jsonWarning := range data.Warnings {
|
|
err := warnings[wIdx].fromData(jsonWarning)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
meta.RequestID = data.RequestID
|
|
meta.ClientContextID = data.ClientContextID
|
|
meta.Metrics = metrics
|
|
meta.Signature = data.Signature
|
|
meta.Warnings = warnings
|
|
|
|
return nil
|
|
}
|
|
|
|
// AnalyticsResult allows access to the results of a query.
|
|
type AnalyticsResult struct {
|
|
reader analyticsRowReader
|
|
|
|
rowBytes []byte
|
|
}
|
|
|
|
func newAnalyticsResult(reader analyticsRowReader) *AnalyticsResult {
|
|
return &AnalyticsResult{
|
|
reader: reader,
|
|
}
|
|
}
|
|
|
|
type analyticsRowReader interface {
|
|
NextRow() []byte
|
|
Err() error
|
|
MetaData() ([]byte, error)
|
|
Close() error
|
|
}
|
|
|
|
// Next assigns the next result from the results into the value pointer, returning whether the read was successful.
|
|
func (r *AnalyticsResult) Next() bool {
|
|
rowBytes := r.reader.NextRow()
|
|
if rowBytes == nil {
|
|
return false
|
|
}
|
|
|
|
r.rowBytes = rowBytes
|
|
return true
|
|
}
|
|
|
|
// Row returns the value of the current row
|
|
func (r *AnalyticsResult) Row(valuePtr interface{}) error {
|
|
if r.rowBytes == nil {
|
|
return ErrNoResult
|
|
}
|
|
|
|
if bytesPtr, ok := valuePtr.(*json.RawMessage); ok {
|
|
*bytesPtr = r.rowBytes
|
|
return nil
|
|
}
|
|
|
|
return json.Unmarshal(r.rowBytes, valuePtr)
|
|
}
|
|
|
|
// Err returns any errors that have occurred on the stream
|
|
func (r *AnalyticsResult) Err() error {
|
|
return r.reader.Err()
|
|
}
|
|
|
|
// Close marks the results as closed, returning any errors that occurred during reading the results.
|
|
func (r *AnalyticsResult) Close() error {
|
|
return r.reader.Close()
|
|
}
|
|
|
|
// One assigns the first value from the results into the value pointer.
|
|
// It will close the results but not before iterating through all remaining
|
|
// results, as such this should only be used for very small resultsets - ideally
|
|
// of, at most, length 1.
|
|
func (r *AnalyticsResult) One(valuePtr interface{}) error {
|
|
// Read the bytes from the first row
|
|
valueBytes := r.reader.NextRow()
|
|
if valueBytes == nil {
|
|
return ErrNoResult
|
|
}
|
|
|
|
// Skip through the remaining rows
|
|
for r.reader.NextRow() != nil {
|
|
// do nothing with the row
|
|
}
|
|
|
|
return json.Unmarshal(valueBytes, valuePtr)
|
|
}
|
|
|
|
// MetaData returns any meta-data that was available from this query. Note that
|
|
// the meta-data will only be available once the object has been closed (either
|
|
// implicitly or explicitly).
|
|
func (r *AnalyticsResult) MetaData() (*AnalyticsMetaData, error) {
|
|
metaDataBytes, err := r.reader.MetaData()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var jsonResp jsonAnalyticsResponse
|
|
err = json.Unmarshal(metaDataBytes, &jsonResp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var metaData AnalyticsMetaData
|
|
err = metaData.fromData(jsonResp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &metaData, nil
|
|
}
|
|
|
|
// AnalyticsQuery executes the analytics query statement on the server.
|
|
func (c *Cluster) AnalyticsQuery(statement string, opts *AnalyticsOptions) (*AnalyticsResult, error) {
|
|
if opts == nil {
|
|
opts = &AnalyticsOptions{}
|
|
}
|
|
|
|
span := c.tracer.StartSpan("Query", opts.parentSpan).
|
|
SetTag("couchbase.service", "analytics")
|
|
defer span.Finish()
|
|
|
|
timeout := opts.Timeout
|
|
if opts.Timeout == 0 {
|
|
timeout = c.timeoutsConfig.AnalyticsTimeout
|
|
}
|
|
deadline := time.Now().Add(timeout)
|
|
|
|
retryStrategy := c.retryStrategyWrapper
|
|
if opts.RetryStrategy != nil {
|
|
retryStrategy = newRetryStrategyWrapper(opts.RetryStrategy)
|
|
}
|
|
|
|
queryOpts, err := opts.toMap()
|
|
if err != nil {
|
|
return nil, AnalyticsError{
|
|
InnerError: wrapError(err, "failed to generate query options"),
|
|
Statement: statement,
|
|
ClientContextID: opts.ClientContextID,
|
|
}
|
|
}
|
|
|
|
var priorityInt int32
|
|
if opts.Priority {
|
|
priorityInt = -1
|
|
}
|
|
|
|
queryOpts["statement"] = statement
|
|
|
|
return c.execAnalyticsQuery(span, queryOpts, priorityInt, deadline, retryStrategy)
|
|
}
|
|
|
|
func maybeGetAnalyticsOption(options map[string]interface{}, name string) string {
|
|
if value, ok := options[name].(string); ok {
|
|
return value
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *Cluster) execAnalyticsQuery(
|
|
span requestSpan,
|
|
options map[string]interface{},
|
|
priority int32,
|
|
deadline time.Time,
|
|
retryStrategy *retryStrategyWrapper,
|
|
) (*AnalyticsResult, error) {
|
|
provider, err := c.getAnalyticsProvider()
|
|
if err != nil {
|
|
return nil, AnalyticsError{
|
|
InnerError: wrapError(err, "failed to get query provider"),
|
|
Statement: maybeGetAnalyticsOption(options, "statement"),
|
|
ClientContextID: maybeGetAnalyticsOption(options, "client_context_id"),
|
|
}
|
|
}
|
|
|
|
reqBytes, err := json.Marshal(options)
|
|
if err != nil {
|
|
return nil, AnalyticsError{
|
|
InnerError: wrapError(err, "failed to marshall query body"),
|
|
Statement: maybeGetAnalyticsOption(options, "statement"),
|
|
ClientContextID: maybeGetAnalyticsOption(options, "client_context_id"),
|
|
}
|
|
}
|
|
|
|
res, err := provider.AnalyticsQuery(gocbcore.AnalyticsQueryOptions{
|
|
Payload: reqBytes,
|
|
Priority: int(priority),
|
|
RetryStrategy: retryStrategy,
|
|
Deadline: deadline,
|
|
TraceContext: span.Context(),
|
|
})
|
|
if err != nil {
|
|
return nil, maybeEnhanceAnalyticsError(err)
|
|
}
|
|
|
|
return newAnalyticsResult(res), nil
|
|
}
|