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 }