OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 part of gcloud.db; |
| 6 |
| 7 /** |
| 8 * A function definition for transactional functions. |
| 9 * |
| 10 * The function will be given a [Transaction] object which can be used to make |
| 11 * lookups/queries and queue modifications (inserts/updates/deletes). |
| 12 */ |
| 13 typedef Future TransactionHandler(Transaction transaction); |
| 14 |
| 15 /** |
| 16 * A datastore transaction. |
| 17 * |
| 18 * It can be used for making lookups/queries and queue modifcations |
| 19 * (inserts/updates/deletes). Finally the transaction can be either committed |
| 20 * or rolled back. |
| 21 */ |
| 22 class Transaction { |
| 23 static const int _TRANSACTION_STARTED = 0; |
| 24 static const int _TRANSACTION_ROLLED_BACK = 1; |
| 25 static const int _TRANSACTION_COMMITTED = 2; |
| 26 |
| 27 final DatastoreDB db; |
| 28 final datastore.Transaction _datastoreTransaction; |
| 29 |
| 30 final List<Model> _inserts = []; |
| 31 final List<Key> _deletes = []; |
| 32 |
| 33 int _transactionState = _TRANSACTION_STARTED; |
| 34 |
| 35 Transaction(this.db, this._datastoreTransaction); |
| 36 |
| 37 /** |
| 38 * Looks up [keys] within this transaction. |
| 39 */ |
| 40 Future<List<Model>> lookup(List<Key> keys) { |
| 41 return _lookupHelper(db, keys, datastoreTransaction: _datastoreTransaction); |
| 42 } |
| 43 |
| 44 /** |
| 45 * Enqueues [inserts] and [deletes] which should be commited at commit time. |
| 46 */ |
| 47 void queueMutations({List<Model> inserts, List<Key> deletes}) { |
| 48 _checkSealed(); |
| 49 if (inserts != null) { |
| 50 _inserts.addAll(inserts); |
| 51 } |
| 52 if (deletes != null) { |
| 53 _deletes.addAll(deletes); |
| 54 } |
| 55 } |
| 56 |
| 57 /** |
| 58 * Query for [kind] models with [ancestorKey]. |
| 59 * |
| 60 * Note that [ancestorKey] is required, since a transaction is not allowed to |
| 61 * touch/look at an arbitrary number of rows. |
| 62 */ |
| 63 Query query(Type kind, Key ancestorKey, {Partition partition}) { |
| 64 _checkSealed(); |
| 65 return new Query(db, |
| 66 kind, |
| 67 partition: partition, |
| 68 ancestorKey: ancestorKey, |
| 69 datastoreTransaction: _datastoreTransaction); |
| 70 } |
| 71 |
| 72 /** |
| 73 * Rolls this transaction back. |
| 74 */ |
| 75 Future rollback() { |
| 76 _checkSealed(changeState: _TRANSACTION_ROLLED_BACK); |
| 77 return db.datastore.rollback(_datastoreTransaction); |
| 78 } |
| 79 |
| 80 /** |
| 81 * Commits this transaction including all of the queued mutations. |
| 82 */ |
| 83 Future commit() { |
| 84 _checkSealed(changeState: _TRANSACTION_COMMITTED); |
| 85 return _commitHelper(db, |
| 86 inserts: _inserts, |
| 87 deletes: _deletes, |
| 88 datastoreTransaction: _datastoreTransaction); |
| 89 } |
| 90 |
| 91 _checkSealed({int changeState}) { |
| 92 if (_transactionState == _TRANSACTION_COMMITTED) { |
| 93 throw new StateError( |
| 94 'The transaction has already been committed.'); |
| 95 } else if (_transactionState == _TRANSACTION_ROLLED_BACK) { |
| 96 throw new StateError( |
| 97 'The transaction has already been rolled back.'); |
| 98 } |
| 99 if (changeState != null) { |
| 100 _transactionState = changeState; |
| 101 } |
| 102 } |
| 103 } |
| 104 |
| 105 class Query { |
| 106 final _relationMapping = const <String, datastore.FilterRelation> { |
| 107 '<': datastore.FilterRelation.LessThan, |
| 108 '<=': datastore.FilterRelation.LessThanOrEqual, |
| 109 '>': datastore.FilterRelation.GreatherThan, |
| 110 '>=': datastore.FilterRelation.GreatherThanOrEqual, |
| 111 '=': datastore.FilterRelation.Equal, |
| 112 'IN': datastore.FilterRelation.In, |
| 113 }; |
| 114 |
| 115 final DatastoreDB _db; |
| 116 final datastore.Transaction _transaction; |
| 117 final String _kind; |
| 118 |
| 119 final Partition _partition; |
| 120 final Key _ancestorKey; |
| 121 |
| 122 final List<datastore.Filter> _filters = []; |
| 123 final List<datastore.Order> _orders = []; |
| 124 int _offset; |
| 125 int _limit; |
| 126 |
| 127 Query(DatastoreDB dbImpl, Type kind, |
| 128 {Partition partition, Key ancestorKey, |
| 129 datastore.Transaction datastoreTransaction}) |
| 130 : _db = dbImpl, |
| 131 _kind = dbImpl.modelDB.kindName(kind), |
| 132 _partition = partition, |
| 133 _ancestorKey = ancestorKey, _transaction = datastoreTransaction; |
| 134 |
| 135 /** |
| 136 * Adds a filter to this [Query]. |
| 137 * |
| 138 * [filterString] has form "name OP" where 'name' is a fieldName of the |
| 139 * model and OP is an operator. The following operators are supported: |
| 140 * |
| 141 * * '<' (less than) |
| 142 * * '<=' (less than or equal) |
| 143 * * '>' (greater than) |
| 144 * * '>=' (greater than or equal) |
| 145 * * '=' (equal) |
| 146 * * 'IN' (in - `comparisonObject` must be a list) |
| 147 * |
| 148 * [comparisonObject] is the object for comparison. |
| 149 */ |
| 150 void filter(String filterString, Object comparisonObject) { |
| 151 var parts = filterString.split(' '); |
| 152 if (parts.length != 2 || !_relationMapping.containsKey(parts[1])) { |
| 153 throw new ArgumentError( |
| 154 "Invalid filter string '$filterString'."); |
| 155 } |
| 156 |
| 157 // TODO: do value transformation on [comparisionObject] |
| 158 |
| 159 var propertyName = _convertToDatastoreName(parts[0]); |
| 160 _filters.add(new datastore.Filter( |
| 161 _relationMapping[parts[1]], propertyName, comparisonObject)); |
| 162 } |
| 163 |
| 164 /** |
| 165 * Adds an order to this [Query]. |
| 166 * |
| 167 * [orderString] has the form "-name" where 'name' is a fieldName of the model |
| 168 * and the optional '-' says whether the order is decending or ascending. |
| 169 */ |
| 170 void order(String orderString) { |
| 171 // TODO: validate [orderString] (e.g. is name valid) |
| 172 if (orderString.startsWith('-')) { |
| 173 _orders.add(new datastore.Order( |
| 174 datastore.OrderDirection.Decending, |
| 175 _convertToDatastoreName(orderString.substring(1)))); |
| 176 } else { |
| 177 _orders.add(new datastore.Order( |
| 178 datastore.OrderDirection.Ascending, |
| 179 _convertToDatastoreName(orderString))); |
| 180 } |
| 181 } |
| 182 |
| 183 /** |
| 184 * Sets the [offset] of this [Query]. |
| 185 * |
| 186 * When running this query, [offset] results will be skipped. |
| 187 */ |
| 188 void offset(int offset) { |
| 189 _offset = offset; |
| 190 } |
| 191 |
| 192 /** |
| 193 * Sets the [limit] of this [Query]. |
| 194 * |
| 195 * When running this query, a maximum of [limit] results will be returned. |
| 196 */ |
| 197 void limit(int limit) { |
| 198 _limit = limit; |
| 199 } |
| 200 |
| 201 /** |
| 202 * Execute this [Query] on the datastore. |
| 203 * |
| 204 * Outside of transactions this method might return stale data or may not |
| 205 * return the newest updates performed on the datastore since updates |
| 206 * will be reflected in the indices in an eventual consistent way. |
| 207 */ |
| 208 Stream<Model> run() { |
| 209 var ancestorKey; |
| 210 if (_ancestorKey != null) { |
| 211 ancestorKey = _db.modelDB.toDatastoreKey(_ancestorKey); |
| 212 } |
| 213 var query = new datastore.Query( |
| 214 ancestorKey: ancestorKey, kind: _kind, |
| 215 filters: _filters, orders: _orders, |
| 216 offset: _offset, limit: _limit); |
| 217 |
| 218 var partition; |
| 219 if (_partition != null) { |
| 220 partition = new datastore.Partition(_partition.namespace); |
| 221 } |
| 222 |
| 223 return new StreamFromPages((int pageSize) { |
| 224 return _db.datastore.query( |
| 225 query, transaction: _transaction, partition: partition); |
| 226 }).stream.map(_db.modelDB.fromDatastoreEntity); |
| 227 } |
| 228 |
| 229 // TODO: |
| 230 // - add runPaged() returning Page<Model> |
| 231 // - add run*() method once we have EntityResult{Entity,Cursor} in low-level |
| 232 // API. |
| 233 |
| 234 String _convertToDatastoreName(String name) { |
| 235 var propertyName = |
| 236 _db.modelDB.fieldNameToPropertyName(_kind, name); |
| 237 if (propertyName == null) { |
| 238 throw new ArgumentError( |
| 239 "Field $name is not available for kind $_kind"); |
| 240 } |
| 241 return propertyName; |
| 242 } |
| 243 } |
| 244 |
| 245 class DatastoreDB { |
| 246 final datastore.Datastore datastore; |
| 247 final ModelDB _modelDB; |
| 248 Partition _defaultPartition; |
| 249 |
| 250 DatastoreDB(this.datastore, {ModelDB modelDB}) |
| 251 : _modelDB = modelDB != null ? modelDB : new ModelDBImpl() { |
| 252 _defaultPartition = new Partition(null); |
| 253 } |
| 254 |
| 255 /** |
| 256 * The [ModelDB] used to serialize/deserialize objects. |
| 257 */ |
| 258 ModelDB get modelDB => _modelDB; |
| 259 |
| 260 /** |
| 261 * Gets the empty key using the default [Partition]. |
| 262 * |
| 263 * Model keys with parent set to [emptyKey] will create their own entity |
| 264 * groups. |
| 265 */ |
| 266 Key get emptyKey => defaultPartition.emptyKey; |
| 267 |
| 268 /** |
| 269 * Gets the default [Partition]. |
| 270 */ |
| 271 Partition get defaultPartition => _defaultPartition; |
| 272 |
| 273 /** |
| 274 * Creates a new [Partition] with namespace [namespace]. |
| 275 */ |
| 276 Partition newPartition(String namespace) { |
| 277 return new Partition(namespace); |
| 278 } |
| 279 |
| 280 /** |
| 281 * Begins a new a new transaction. |
| 282 * |
| 283 * A transaction can touch only a limited number of entity groups. This limit |
| 284 * is currently 5. |
| 285 */ |
| 286 // TODO: Add retries and/or auto commit/rollback. |
| 287 Future withTransaction(TransactionHandler transactionHandler) { |
| 288 return datastore.beginTransaction(crossEntityGroup: true) |
| 289 .then((datastoreTransaction) { |
| 290 var transaction = new Transaction(this, datastoreTransaction); |
| 291 return transactionHandler(transaction); |
| 292 }); |
| 293 } |
| 294 |
| 295 /** |
| 296 * Build a query for [kind] models. |
| 297 */ |
| 298 Query query(Type kind, {Partition partition, Key ancestorKey}) { |
| 299 return new Query(this, |
| 300 kind, |
| 301 partition: partition, |
| 302 ancestorKey: ancestorKey); |
| 303 } |
| 304 |
| 305 /** |
| 306 * Looks up [keys] in the datastore and returns a list of [Model] objects. |
| 307 * |
| 308 * For transactions, please use [beginTransaction] and call the [lookup] |
| 309 * method on it's returned [Transaction] object. |
| 310 */ |
| 311 Future<List<Model>> lookup(List<Key> keys) { |
| 312 return _lookupHelper(this, keys); |
| 313 } |
| 314 |
| 315 /** |
| 316 * Add [inserts] to the datastore and remove [deletes] from it. |
| 317 * |
| 318 * The order of inserts and deletes is not specified. When the commit is done |
| 319 * direct lookups will see the effect but non-ancestor queries will see the |
| 320 * change in an eventual consistent way. |
| 321 * |
| 322 * For transactions, please use `beginTransaction` and it's returned |
| 323 * [Transaction] object. |
| 324 */ |
| 325 Future commit({List<Model> inserts, List<Key> deletes}) { |
| 326 return _commitHelper(this, inserts: inserts, deletes: deletes); |
| 327 } |
| 328 } |
| 329 |
| 330 Future _commitHelper(DatastoreDB db, |
| 331 {List<Model> inserts, |
| 332 List<Key> deletes, |
| 333 datastore.Transaction datastoreTransaction}) { |
| 334 var entityInserts, entityAutoIdInserts, entityDeletes; |
| 335 var autoIdModelInserts; |
| 336 if (inserts != null) { |
| 337 entityInserts = []; |
| 338 entityAutoIdInserts = []; |
| 339 autoIdModelInserts = []; |
| 340 |
| 341 for (var model in inserts) { |
| 342 // If parent was not explicity set, we assume this model will map to |
| 343 // it's own entity group. |
| 344 if (model.parentKey == null) { |
| 345 model.parentKey = db.defaultPartition.emptyKey; |
| 346 } |
| 347 if (model.id == null) { |
| 348 autoIdModelInserts.add(model); |
| 349 entityAutoIdInserts.add(db.modelDB.toDatastoreEntity(model)); |
| 350 } else { |
| 351 entityInserts.add(db.modelDB.toDatastoreEntity(model)); |
| 352 } |
| 353 } |
| 354 } |
| 355 if (deletes != null) { |
| 356 entityDeletes = deletes.map(db.modelDB.toDatastoreKey).toList(); |
| 357 } |
| 358 |
| 359 return db.datastore.commit(inserts: entityInserts, |
| 360 autoIdInserts: entityAutoIdInserts, |
| 361 deletes: entityDeletes, |
| 362 transaction: datastoreTransaction) |
| 363 .then((datastore.CommitResult result) { |
| 364 if (entityAutoIdInserts != null && entityAutoIdInserts.length > 0) { |
| 365 for (var i = 0; i < result.autoIdInsertKeys.length; i++) { |
| 366 var key = db.modelDB.fromDatastoreKey(result.autoIdInsertKeys[i]); |
| 367 autoIdModelInserts[i].parentKey = key.parent; |
| 368 autoIdModelInserts[i].id = key.id; |
| 369 } |
| 370 } |
| 371 }); |
| 372 } |
| 373 |
| 374 Future<List<Model>> _lookupHelper( |
| 375 DatastoreDB db, List<Key> keys, |
| 376 {datastore.Transaction datastoreTransaction}) { |
| 377 var entityKeys = keys.map(db.modelDB.toDatastoreKey).toList(); |
| 378 return db.datastore.lookup(entityKeys, transaction: datastoreTransaction) |
| 379 .then((List<datastore.Entity> entities) { |
| 380 return entities.map(db.modelDB.fromDatastoreEntity).toList(); |
| 381 }); |
| 382 } |
OLD | NEW |