github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/cassandra/datastore_crud.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 cassandra
    22  
    23  import (
    24  	"context"
    25  	"sort"
    26  
    27  	"github.com/gocql/gocql"
    28  	"github.com/pkg/errors"
    29  	"github.com/uber-go/dosa"
    30  )
    31  
    32  func sortFieldValue(obj map[string]dosa.FieldValue) ([]string, []interface{}, error) {
    33  	columns := make([]string, len(obj))
    34  
    35  	pos := 0
    36  	for k := range obj {
    37  		columns[pos] = k
    38  		pos++
    39  	}
    40  
    41  	sort.Strings(columns)
    42  
    43  	values := make([]interface{}, len(obj))
    44  	for pos, c := range columns {
    45  		values[pos] = obj[c]
    46  		var err error
    47  		// specially handling for uuid is needed
    48  		if u, ok := obj[c].(dosa.UUID); ok {
    49  			values[pos], err = gocql.ParseUUID(string(u))
    50  			if err != nil {
    51  				return nil, nil, errors.Wrapf(err, "invalid uuid %s", u)
    52  			}
    53  		}
    54  	}
    55  
    56  	return columns, values, nil
    57  }
    58  
    59  // CreateIfNotExists creates an object if not exists
    60  func (c *Connector) CreateIfNotExists(ctx context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error {
    61  	keyspace := c.KsMapper.Keyspace(ei.Ref.Scope, ei.Ref.NamePrefix)
    62  	table := ei.Def.Name
    63  
    64  	sortedColumns, sortedValues, err := sortFieldValue(values)
    65  	if err != nil {
    66  		return err
    67  	}
    68  
    69  	stmt, err := InsertStmt(
    70  		Keyspace(keyspace),
    71  		Table(table),
    72  		Columns(sortedColumns),
    73  		Values(sortedValues),
    74  		IfNotExist(true),
    75  	)
    76  	if err != nil {
    77  		return errors.Wrap(err, "failed to create cql statement")
    78  	}
    79  
    80  	applied, err := c.Session.Query(stmt, sortedValues...).WithContext(ctx).MapScanCAS(map[string]interface{}{})
    81  	if err != nil {
    82  		return errors.Wrapf(err, "failed to execute CreateIfNotExists query in cassandra: %s", stmt)
    83  	}
    84  
    85  	if !applied {
    86  		return &dosa.ErrAlreadyExists{}
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  // Read reads an object based on primary key
    93  func (c *Connector) Read(ctx context.Context, ei *dosa.EntityInfo, keys map[string]dosa.FieldValue, fieldsToRead []string) (map[string]dosa.FieldValue, error) {
    94  	keyspace := c.KsMapper.Keyspace(ei.Ref.Scope, ei.Ref.NamePrefix)
    95  	table := ei.Def.Name
    96  
    97  	fields := fieldsToRead
    98  	if len(fields) == 0 {
    99  		fields = extractNonKeyColumns(ei.Def)
   100  	}
   101  
   102  	sort.Strings(fields)
   103  
   104  	conds := make([]*ColumnCondition, len(keys))
   105  	pos := 0
   106  	for name, value := range keys {
   107  		conds[pos] = &ColumnCondition{
   108  			Name: name,
   109  			Condition: &dosa.Condition{
   110  				Op:    dosa.Eq,
   111  				Value: value,
   112  			},
   113  		}
   114  		pos++
   115  	}
   116  	sort.Sort(sortedColumnCondition(conds))
   117  
   118  	_, sortedValues, err := sortFieldValue(keys)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	stmt, err := SelectStmt(
   124  		Keyspace(keyspace),
   125  		Table(table),
   126  		Columns(fields),
   127  		Conditions(conds),
   128  		Limit(1),
   129  	)
   130  	if err != nil {
   131  		return nil, errors.Wrap(err, "failed to create cql statement")
   132  	}
   133  
   134  	result := make(map[string]interface{})
   135  	// TODO workon timeout trace features
   136  	if err := c.Session.Query(stmt, sortedValues...).WithContext(ctx).MapScan(result); err != nil {
   137  		if err == gocql.ErrNotFound {
   138  			return nil, &dosa.ErrNotFound{}
   139  		}
   140  		return nil, errors.Wrapf(err, "failed to execute read query in Cassandra: %s", stmt)
   141  	}
   142  
   143  	return convertToDOSATypes(ei, result), nil
   144  }
   145  
   146  // Upsert means update an existing object or create a new object
   147  func (c *Connector) Upsert(ctx context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error {
   148  	keyspace := c.KsMapper.Keyspace(ei.Ref.Scope, ei.Ref.NamePrefix)
   149  	table := ei.Def.Name
   150  
   151  	sortedColumns, sortedValues, err := sortFieldValue(values)
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	stmt, err := InsertStmt(
   157  		Keyspace(keyspace),
   158  		Table(table),
   159  		Columns(sortedColumns),
   160  		Values(sortedValues),
   161  		IfNotExist(false),
   162  	)
   163  	if err != nil {
   164  		return errors.Wrap(err, "failed to create cql statement")
   165  	}
   166  
   167  	if err := c.Session.Query(stmt, sortedValues...).WithContext(ctx).Exec(); err != nil {
   168  		return errors.Wrapf(err, "failed to execute upsert query in cassandra: %s", stmt)
   169  	}
   170  
   171  	return nil
   172  }
   173  
   174  // Remove object based on primary key
   175  func (c *Connector) Remove(ctx context.Context, ei *dosa.EntityInfo, keys map[string]dosa.FieldValue) error {
   176  	conds := make([]*ColumnCondition, len(keys))
   177  	pos := 0
   178  	for name, value := range keys {
   179  		conds[pos] = &ColumnCondition{
   180  			Name: name,
   181  			Condition: &dosa.Condition{
   182  				Op:    dosa.Eq,
   183  				Value: value,
   184  			},
   185  		}
   186  		pos++
   187  	}
   188  	sort.Sort(sortedColumnCondition(conds))
   189  
   190  	_, sortedValues, err := sortFieldValue(keys)
   191  	if err != nil {
   192  		return err
   193  	}
   194  
   195  	return c.remove(ctx, ei, conds, sortedValues)
   196  }
   197  
   198  // RemoveRange removes a range of objects based on column conditions.
   199  func (c *Connector) RemoveRange(ctx context.Context, ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition) error {
   200  	conds, values, err := prepareConditions(columnConditions)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	return c.remove(ctx, ei, conds, values)
   205  }
   206  
   207  func (c *Connector) remove(ctx context.Context, ei *dosa.EntityInfo, conds []*ColumnCondition, values []interface{}) error {
   208  	keyspace := c.KsMapper.Keyspace(ei.Ref.Scope, ei.Ref.NamePrefix)
   209  	table := ei.Def.Name
   210  
   211  	stmt, err := DeleteStmt(
   212  		Keyspace(keyspace),
   213  		Table(table),
   214  		Conditions(conds),
   215  	)
   216  
   217  	if err != nil {
   218  		return errors.Wrap(err, "failed to create cql statement")
   219  	}
   220  
   221  	if err := c.Session.Query(stmt, values...).WithContext(ctx).Exec(); err != nil {
   222  		return errors.Wrapf(err, "failed to execute remove query in Cassandra: %s", stmt)
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  func convertToDOSATypes(ei *dosa.EntityInfo, row map[string]interface{}) map[string]dosa.FieldValue {
   229  	res := make(map[string]dosa.FieldValue)
   230  	ct := extractColumnTypes(ei)
   231  	for k, v := range row {
   232  		dosaType := ct[k]
   233  		raw := v
   234  		// special handling
   235  		switch dosaType {
   236  		case dosa.TUUID:
   237  			uuid := raw.(gocql.UUID).String()
   238  			raw = dosa.UUID(uuid)
   239  		// for whatever reason, gocql returns int for int32 field
   240  		// TODO: decide whether to store timestamp as int64 for better resolution; see
   241  		// https://code.uberinternal.com/T733022
   242  		case dosa.Int32:
   243  			raw = int32(raw.(int))
   244  		}
   245  		res[k] = raw
   246  	}
   247  	return res
   248  }
   249  
   250  func extractColumnTypes(ei *dosa.EntityInfo) map[string]dosa.Type {
   251  	m := make(map[string]dosa.Type)
   252  	for _, c := range ei.Def.Columns {
   253  		m[c.Name] = c.Type
   254  	}
   255  	return m
   256  }
   257  
   258  func extractNonKeyColumns(ed *dosa.EntityDefinition) []string {
   259  	columns := ed.ColumnTypes()
   260  	keySet := ed.KeySet()
   261  	res := []string{}
   262  	for name := range columns {
   263  		if _, ok := keySet[name]; !ok {
   264  			res = append(res, name)
   265  		}
   266  	}
   267  	return res
   268  }