github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/range.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package dosa
    22  
    23  import (
    24  	"bytes"
    25  	"fmt"
    26  	"reflect"
    27  	"sort"
    28  
    29  	"github.com/golang/mock/gomock"
    30  	"github.com/pkg/errors"
    31  )
    32  
    33  // RangeOp is used to specify constraints to Range calls
    34  type RangeOp struct {
    35  	pager
    36  	conditioner
    37  }
    38  
    39  // NewRangeOp returns a new RangeOp instance
    40  func NewRangeOp(object DomainObject) *RangeOp {
    41  	rop := &RangeOp{
    42  		conditioner: conditioner{
    43  			object:     object,
    44  			conditions: map[string][]*Condition{},
    45  		},
    46  	}
    47  	return rop
    48  }
    49  
    50  // Limit sets the number of rows returned per call. Default is 100
    51  func (r *RangeOp) Limit(n int) *RangeOp {
    52  	r.limit = n
    53  	return r
    54  }
    55  
    56  // Offset sets the pagination token. If not set, an empty token would be used.
    57  func (r *RangeOp) Offset(token string) *RangeOp {
    58  	r.token = token
    59  	return r
    60  }
    61  
    62  // Fields list the non-key fields users want to fetch.
    63  // PrimaryKey fields are always fetched.
    64  func (r *RangeOp) Fields(fields []string) *RangeOp {
    65  	r.fieldsToRead = fields
    66  	return r
    67  }
    68  
    69  // String satisfies the Stringer interface
    70  func (r *RangeOp) String() string {
    71  	result := &bytes.Buffer{}
    72  	if r.conditions == nil || len(r.conditions) == 0 {
    73  		result.WriteString("<empty>")
    74  	} else {
    75  		// sort the fields by name for deterministic results
    76  		keys := make([]string, 0, len(r.conditions))
    77  		for key := range r.conditions {
    78  			keys = append(keys, key)
    79  		}
    80  		sort.Strings(keys)
    81  		for _, field := range keys {
    82  			conds := r.conditions[field]
    83  			if result.Len() > 0 {
    84  				result.WriteString(", ")
    85  			}
    86  			result.WriteString(field)
    87  			result.WriteString(" ")
    88  			for i, cond := range conds {
    89  				if i > 0 {
    90  					fmt.Fprintf(result, ", %s ", field)
    91  				}
    92  				fmt.Fprintf(result, "%s %v", cond.Op.String(), cond.Value)
    93  			}
    94  		}
    95  	}
    96  	addLimitTokenString(result, r.limit, r.token)
    97  	return result.String()
    98  }
    99  
   100  // Eq is used to express an equality constraint for a range query
   101  func (r *RangeOp) Eq(fieldName string, value interface{}) *RangeOp {
   102  	r.appendOp(Eq, fieldName, value)
   103  	return r
   104  }
   105  
   106  // Gt is used to express an "greater than" constraint for a range query
   107  func (r *RangeOp) Gt(fieldName string, value interface{}) *RangeOp {
   108  	r.appendOp(Gt, fieldName, value)
   109  	return r
   110  }
   111  
   112  // GtOrEq is used to express an "greater than or equal" constraint for a
   113  // range query
   114  func (r *RangeOp) GtOrEq(fieldName string, value interface{}) *RangeOp {
   115  	r.appendOp(GtOrEq, fieldName, value)
   116  	return r
   117  }
   118  
   119  // Lt is used to express a "less than" constraint for a range query
   120  func (r *RangeOp) Lt(fieldName string, value interface{}) *RangeOp {
   121  	r.appendOp(Lt, fieldName, value)
   122  	return r
   123  }
   124  
   125  // LtOrEq is used to express a "less than or equal" constraint for a
   126  // range query
   127  func (r *RangeOp) LtOrEq(fieldName string, value interface{}) *RangeOp {
   128  	r.appendOp(LtOrEq, fieldName, value)
   129  	return r
   130  }
   131  
   132  type rangeOpMatcher struct {
   133  	conds map[string]map[Condition]bool
   134  	p     pager
   135  	typ   reflect.Type
   136  }
   137  
   138  // EqRangeOp creates a gomock Matcher that will match any RangeOp with the same conditions, limit, token, and fields
   139  // as those specified in the op argument.
   140  func EqRangeOp(op *RangeOp) gomock.Matcher {
   141  	conds := make(map[string]map[Condition]bool)
   142  	for col, colConds := range op.conditions {
   143  		conds[col] = make(map[Condition]bool, len(colConds))
   144  		for _, cond := range colConds {
   145  			conds[col][*cond] = true
   146  		}
   147  	}
   148  
   149  	return rangeOpMatcher{
   150  		conds: conds,
   151  		p:     op.pager,
   152  		typ:   reflect.TypeOf(op.object).Elem(),
   153  	}
   154  }
   155  
   156  // Matches satisfies the gomock.Matcher interface
   157  func (m rangeOpMatcher) Matches(x interface{}) bool {
   158  	op, ok := x.(*RangeOp)
   159  	if !ok {
   160  		return false
   161  	}
   162  
   163  	for col, conds := range op.conditions {
   164  		for _, condition := range conds {
   165  			if !m.conds[col][*condition] {
   166  				return false
   167  			}
   168  		}
   169  	}
   170  
   171  	return m.p.equals(op.pager) && reflect.TypeOf(op.object).Elem() == m.typ
   172  }
   173  
   174  // String satisfies the gomock.Matcher and Stringer interface
   175  func (m rangeOpMatcher) String() string {
   176  	return fmt.Sprintf(
   177  		" is equal to RangeOp with conditions %v, token %s, limit %d, fields %v, and entity type %v",
   178  		m.conds,
   179  		m.p.token,
   180  		m.p.limit,
   181  		m.p.fieldsToRead,
   182  		m.typ)
   183  }
   184  
   185  // IndexFromConditions returns the name of the index or the base table to use, along with the key info
   186  // for that index. If no suitable index could be found, an error is returned
   187  func (ei *EntityInfo) IndexFromConditions(conditions map[string][]*Condition) (name string, key *PrimaryKey, err error) {
   188  	identityFunc := func(s string) string { return s }
   189  	// see if we match the primary key for this table
   190  	var baseTableError error
   191  	if baseTableError = EnsureValidRangeConditions(ei.Def, ei.Def.Key, conditions, identityFunc); baseTableError == nil {
   192  		return ei.Def.Name, ei.Def.Key, nil
   193  	}
   194  	if len(ei.Def.Indexes) == 0 {
   195  		return "", nil, baseTableError
   196  	}
   197  	// see if we match an index on this table
   198  	var indexDef *IndexDefinition
   199  	for name, indexDef = range ei.Def.Indexes {
   200  		key = indexDef.Key
   201  		// we check the range conditions before adding the uniqueness columns
   202  		if err := EnsureValidRangeConditions(ei.Def, key, conditions, identityFunc); err == nil {
   203  			return name, ei.Def.UniqueKey(key), nil
   204  		}
   205  	}
   206  	// none of the indexes work, so fail
   207  	return "", nil, errors.Wrapf(baseTableError, "No index matches specified conditions")
   208  }