Index: pkg/gcloud/lib/src/db/db.dart |
diff --git a/pkg/gcloud/lib/src/db/db.dart b/pkg/gcloud/lib/src/db/db.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..07c5efb7b5ac3b7bafedfcd0da4508e144c370c3 |
--- /dev/null |
+++ b/pkg/gcloud/lib/src/db/db.dart |
@@ -0,0 +1,382 @@ |
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+part of gcloud.db; |
+ |
+/** |
+ * A function definition for transactional functions. |
+ * |
+ * The function will be given a [Transaction] object which can be used to make |
+ * lookups/queries and queue modifications (inserts/updates/deletes). |
+ */ |
+typedef Future TransactionHandler(Transaction transaction); |
+ |
+/** |
+ * A datastore transaction. |
+ * |
+ * It can be used for making lookups/queries and queue modifcations |
+ * (inserts/updates/deletes). Finally the transaction can be either committed |
+ * or rolled back. |
+ */ |
+class Transaction { |
+ static const int _TRANSACTION_STARTED = 0; |
+ static const int _TRANSACTION_ROLLED_BACK = 1; |
+ static const int _TRANSACTION_COMMITTED = 2; |
+ |
+ final DatastoreDB db; |
+ final datastore.Transaction _datastoreTransaction; |
+ |
+ final List<Model> _inserts = []; |
+ final List<Key> _deletes = []; |
+ |
+ int _transactionState = _TRANSACTION_STARTED; |
+ |
+ Transaction(this.db, this._datastoreTransaction); |
+ |
+ /** |
+ * Looks up [keys] within this transaction. |
+ */ |
+ Future<List<Model>> lookup(List<Key> keys) { |
+ return _lookupHelper(db, keys, datastoreTransaction: _datastoreTransaction); |
+ } |
+ |
+ /** |
+ * Enqueues [inserts] and [deletes] which should be commited at commit time. |
+ */ |
+ void queueMutations({List<Model> inserts, List<Key> deletes}) { |
+ _checkSealed(); |
+ if (inserts != null) { |
+ _inserts.addAll(inserts); |
+ } |
+ if (deletes != null) { |
+ _deletes.addAll(deletes); |
+ } |
+ } |
+ |
+ /** |
+ * Query for [kind] models with [ancestorKey]. |
+ * |
+ * Note that [ancestorKey] is required, since a transaction is not allowed to |
+ * touch/look at an arbitrary number of rows. |
+ */ |
+ Query query(Type kind, Key ancestorKey, {Partition partition}) { |
+ _checkSealed(); |
+ return new Query(db, |
+ kind, |
+ partition: partition, |
+ ancestorKey: ancestorKey, |
+ datastoreTransaction: _datastoreTransaction); |
+ } |
+ |
+ /** |
+ * Rolls this transaction back. |
+ */ |
+ Future rollback() { |
+ _checkSealed(changeState: _TRANSACTION_ROLLED_BACK); |
+ return db.datastore.rollback(_datastoreTransaction); |
+ } |
+ |
+ /** |
+ * Commits this transaction including all of the queued mutations. |
+ */ |
+ Future commit() { |
+ _checkSealed(changeState: _TRANSACTION_COMMITTED); |
+ return _commitHelper(db, |
+ inserts: _inserts, |
+ deletes: _deletes, |
+ datastoreTransaction: _datastoreTransaction); |
+ } |
+ |
+ _checkSealed({int changeState}) { |
+ if (_transactionState == _TRANSACTION_COMMITTED) { |
+ throw new StateError( |
+ 'The transaction has already been committed.'); |
+ } else if (_transactionState == _TRANSACTION_ROLLED_BACK) { |
+ throw new StateError( |
+ 'The transaction has already been rolled back.'); |
+ } |
+ if (changeState != null) { |
+ _transactionState = changeState; |
+ } |
+ } |
+} |
+ |
+class Query { |
+ final _relationMapping = const <String, datastore.FilterRelation> { |
+ '<': datastore.FilterRelation.LessThan, |
+ '<=': datastore.FilterRelation.LessThanOrEqual, |
+ '>': datastore.FilterRelation.GreatherThan, |
+ '>=': datastore.FilterRelation.GreatherThanOrEqual, |
+ '=': datastore.FilterRelation.Equal, |
+ 'IN': datastore.FilterRelation.In, |
+ }; |
+ |
+ final DatastoreDB _db; |
+ final datastore.Transaction _transaction; |
+ final String _kind; |
+ |
+ final Partition _partition; |
+ final Key _ancestorKey; |
+ |
+ final List<datastore.Filter> _filters = []; |
+ final List<datastore.Order> _orders = []; |
+ int _offset; |
+ int _limit; |
+ |
+ Query(DatastoreDB dbImpl, Type kind, |
+ {Partition partition, Key ancestorKey, |
+ datastore.Transaction datastoreTransaction}) |
+ : _db = dbImpl, |
+ _kind = dbImpl.modelDB.kindName(kind), |
+ _partition = partition, |
+ _ancestorKey = ancestorKey, _transaction = datastoreTransaction; |
+ |
+ /** |
+ * Adds a filter to this [Query]. |
+ * |
+ * [filterString] has form "name OP" where 'name' is a fieldName of the |
+ * model and OP is an operator. The following operators are supported: |
+ * |
+ * * '<' (less than) |
+ * * '<=' (less than or equal) |
+ * * '>' (greater than) |
+ * * '>=' (greater than or equal) |
+ * * '=' (equal) |
+ * * 'IN' (in - `comparisonObject` must be a list) |
+ * |
+ * [comparisonObject] is the object for comparison. |
+ */ |
+ void filter(String filterString, Object comparisonObject) { |
+ var parts = filterString.split(' '); |
+ if (parts.length != 2 || !_relationMapping.containsKey(parts[1])) { |
+ throw new ArgumentError( |
+ "Invalid filter string '$filterString'."); |
+ } |
+ |
+ // TODO: do value transformation on [comparisionObject] |
+ |
+ var propertyName = _convertToDatastoreName(parts[0]); |
+ _filters.add(new datastore.Filter( |
+ _relationMapping[parts[1]], propertyName, comparisonObject)); |
+ } |
+ |
+ /** |
+ * Adds an order to this [Query]. |
+ * |
+ * [orderString] has the form "-name" where 'name' is a fieldName of the model |
+ * and the optional '-' says whether the order is decending or ascending. |
+ */ |
+ void order(String orderString) { |
+ // TODO: validate [orderString] (e.g. is name valid) |
+ if (orderString.startsWith('-')) { |
+ _orders.add(new datastore.Order( |
+ datastore.OrderDirection.Decending, |
+ _convertToDatastoreName(orderString.substring(1)))); |
+ } else { |
+ _orders.add(new datastore.Order( |
+ datastore.OrderDirection.Ascending, |
+ _convertToDatastoreName(orderString))); |
+ } |
+ } |
+ |
+ /** |
+ * Sets the [offset] of this [Query]. |
+ * |
+ * When running this query, [offset] results will be skipped. |
+ */ |
+ void offset(int offset) { |
+ _offset = offset; |
+ } |
+ |
+ /** |
+ * Sets the [limit] of this [Query]. |
+ * |
+ * When running this query, a maximum of [limit] results will be returned. |
+ */ |
+ void limit(int limit) { |
+ _limit = limit; |
+ } |
+ |
+ /** |
+ * Execute this [Query] on the datastore. |
+ * |
+ * Outside of transactions this method might return stale data or may not |
+ * return the newest updates performed on the datastore since updates |
+ * will be reflected in the indices in an eventual consistent way. |
+ */ |
+ Stream<Model> run() { |
+ var ancestorKey; |
+ if (_ancestorKey != null) { |
+ ancestorKey = _db.modelDB.toDatastoreKey(_ancestorKey); |
+ } |
+ var query = new datastore.Query( |
+ ancestorKey: ancestorKey, kind: _kind, |
+ filters: _filters, orders: _orders, |
+ offset: _offset, limit: _limit); |
+ |
+ var partition; |
+ if (_partition != null) { |
+ partition = new datastore.Partition(_partition.namespace); |
+ } |
+ |
+ return new StreamFromPages((int pageSize) { |
+ return _db.datastore.query( |
+ query, transaction: _transaction, partition: partition); |
+ }).stream.map(_db.modelDB.fromDatastoreEntity); |
+ } |
+ |
+ // TODO: |
+ // - add runPaged() returning Page<Model> |
+ // - add run*() method once we have EntityResult{Entity,Cursor} in low-level |
+ // API. |
+ |
+ String _convertToDatastoreName(String name) { |
+ var propertyName = |
+ _db.modelDB.fieldNameToPropertyName(_kind, name); |
+ if (propertyName == null) { |
+ throw new ArgumentError( |
+ "Field $name is not available for kind $_kind"); |
+ } |
+ return propertyName; |
+ } |
+} |
+ |
+class DatastoreDB { |
+ final datastore.Datastore datastore; |
+ final ModelDB _modelDB; |
+ Partition _defaultPartition; |
+ |
+ DatastoreDB(this.datastore, {ModelDB modelDB}) |
+ : _modelDB = modelDB != null ? modelDB : new ModelDBImpl() { |
+ _defaultPartition = new Partition(null); |
+ } |
+ |
+ /** |
+ * The [ModelDB] used to serialize/deserialize objects. |
+ */ |
+ ModelDB get modelDB => _modelDB; |
+ |
+ /** |
+ * Gets the empty key using the default [Partition]. |
+ * |
+ * Model keys with parent set to [emptyKey] will create their own entity |
+ * groups. |
+ */ |
+ Key get emptyKey => defaultPartition.emptyKey; |
+ |
+ /** |
+ * Gets the default [Partition]. |
+ */ |
+ Partition get defaultPartition => _defaultPartition; |
+ |
+ /** |
+ * Creates a new [Partition] with namespace [namespace]. |
+ */ |
+ Partition newPartition(String namespace) { |
+ return new Partition(namespace); |
+ } |
+ |
+ /** |
+ * Begins a new a new transaction. |
+ * |
+ * A transaction can touch only a limited number of entity groups. This limit |
+ * is currently 5. |
+ */ |
+ // TODO: Add retries and/or auto commit/rollback. |
+ Future withTransaction(TransactionHandler transactionHandler) { |
+ return datastore.beginTransaction(crossEntityGroup: true) |
+ .then((datastoreTransaction) { |
+ var transaction = new Transaction(this, datastoreTransaction); |
+ return transactionHandler(transaction); |
+ }); |
+ } |
+ |
+ /** |
+ * Build a query for [kind] models. |
+ */ |
+ Query query(Type kind, {Partition partition, Key ancestorKey}) { |
+ return new Query(this, |
+ kind, |
+ partition: partition, |
+ ancestorKey: ancestorKey); |
+ } |
+ |
+ /** |
+ * Looks up [keys] in the datastore and returns a list of [Model] objects. |
+ * |
+ * For transactions, please use [beginTransaction] and call the [lookup] |
+ * method on it's returned [Transaction] object. |
+ */ |
+ Future<List<Model>> lookup(List<Key> keys) { |
+ return _lookupHelper(this, keys); |
+ } |
+ |
+ /** |
+ * Add [inserts] to the datastore and remove [deletes] from it. |
+ * |
+ * The order of inserts and deletes is not specified. When the commit is done |
+ * direct lookups will see the effect but non-ancestor queries will see the |
+ * change in an eventual consistent way. |
+ * |
+ * For transactions, please use `beginTransaction` and it's returned |
+ * [Transaction] object. |
+ */ |
+ Future commit({List<Model> inserts, List<Key> deletes}) { |
+ return _commitHelper(this, inserts: inserts, deletes: deletes); |
+ } |
+} |
+ |
+Future _commitHelper(DatastoreDB db, |
+ {List<Model> inserts, |
+ List<Key> deletes, |
+ datastore.Transaction datastoreTransaction}) { |
+ var entityInserts, entityAutoIdInserts, entityDeletes; |
+ var autoIdModelInserts; |
+ if (inserts != null) { |
+ entityInserts = []; |
+ entityAutoIdInserts = []; |
+ autoIdModelInserts = []; |
+ |
+ for (var model in inserts) { |
+ // If parent was not explicity set, we assume this model will map to |
+ // it's own entity group. |
+ if (model.parentKey == null) { |
+ model.parentKey = db.defaultPartition.emptyKey; |
+ } |
+ if (model.id == null) { |
+ autoIdModelInserts.add(model); |
+ entityAutoIdInserts.add(db.modelDB.toDatastoreEntity(model)); |
+ } else { |
+ entityInserts.add(db.modelDB.toDatastoreEntity(model)); |
+ } |
+ } |
+ } |
+ if (deletes != null) { |
+ entityDeletes = deletes.map(db.modelDB.toDatastoreKey).toList(); |
+ } |
+ |
+ return db.datastore.commit(inserts: entityInserts, |
+ autoIdInserts: entityAutoIdInserts, |
+ deletes: entityDeletes, |
+ transaction: datastoreTransaction) |
+ .then((datastore.CommitResult result) { |
+ if (entityAutoIdInserts != null && entityAutoIdInserts.length > 0) { |
+ for (var i = 0; i < result.autoIdInsertKeys.length; i++) { |
+ var key = db.modelDB.fromDatastoreKey(result.autoIdInsertKeys[i]); |
+ autoIdModelInserts[i].parentKey = key.parent; |
+ autoIdModelInserts[i].id = key.id; |
+ } |
+ } |
+ }); |
+} |
+ |
+Future<List<Model>> _lookupHelper( |
+ DatastoreDB db, List<Key> keys, |
+ {datastore.Transaction datastoreTransaction}) { |
+ var entityKeys = keys.map(db.modelDB.toDatastoreKey).toList(); |
+ return db.datastore.lookup(entityKeys, transaction: datastoreTransaction) |
+ .then((List<datastore.Entity> entities) { |
+ return entities.map(db.modelDB.fromDatastoreEntity).toList(); |
+ }); |
+} |