| Index: pkg/gcloud/lib/src/datastore_impl.dart
|
| diff --git a/pkg/gcloud/lib/src/datastore_impl.dart b/pkg/gcloud/lib/src/datastore_impl.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..c79920f4b47797c5531c6cf7fdfc64c90bd9f056
|
| --- /dev/null
|
| +++ b/pkg/gcloud/lib/src/datastore_impl.dart
|
| @@ -0,0 +1,699 @@
|
| +// 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.
|
| +
|
| +library gcloud.datastore_impl;
|
| +
|
| +import 'dart:async';
|
| +
|
| +import 'package:http/http.dart' as http;
|
| +
|
| +import '../datastore.dart' as datastore;
|
| +import '../common.dart' show Page;
|
| +import 'package:googleapis_beta/datastore/v1beta2.dart' as api;
|
| +
|
| +class TransactionImpl implements datastore.Transaction {
|
| + final String data;
|
| + TransactionImpl(this.data);
|
| +}
|
| +
|
| +class DatastoreImpl implements datastore.Datastore {
|
| + static const List<String> SCOPES = const <String>[
|
| + api.DatastoreApi.DatastoreScope,
|
| + api.DatastoreApi.UserinfoEmailScope,
|
| + ];
|
| +
|
| + final api.DatastoreApi _api;
|
| + final String _project;
|
| +
|
| + DatastoreImpl(http.Client client, this._project)
|
| + : _api = new api.DatastoreApi(client);
|
| +
|
| + api.Key _convertDatastore2ApiKey(datastore.Key key) {
|
| + var apiKey = new api.Key();
|
| +
|
| + apiKey.partitionId = new api.PartitionId()
|
| + ..datasetId = _project
|
| + ..namespace = key.partition.namespace;
|
| +
|
| + apiKey.path = key.elements.map((datastore.KeyElement element) {
|
| + var part = new api.KeyPathElement();
|
| + part.kind = element.kind;
|
| + if (element.id is int) {
|
| + part.id = '${element.id}';
|
| + } else if (element.id is String) {
|
| + part.name = element.id;
|
| + }
|
| + return part;
|
| + }).toList();
|
| +
|
| + return apiKey;
|
| + }
|
| +
|
| + static datastore.Key _convertApi2DatastoreKey(api.Key key) {
|
| + var elements = key.path.map((api.KeyPathElement element) {
|
| + if (element.id != null) {
|
| + return new datastore.KeyElement(element.kind, int.parse(element.id));
|
| + } else if (element.name != null) {
|
| + return new datastore.KeyElement(element.kind, element.name);
|
| + } else {
|
| + throw new datastore.DatastoreError(
|
| + 'Invalid server response: Expected allocated name/id.');
|
| + }
|
| + }).toList();
|
| +
|
| + var partition;
|
| + if (key.partitionId != null) {
|
| + partition = new datastore.Partition(key.partitionId.namespace);
|
| + // TODO: assert projectId.
|
| + }
|
| + return new datastore.Key(elements, partition: partition);
|
| + }
|
| +
|
| + bool _compareApiKey(api.Key a, api.Key b) {
|
| + if (a.path.length != b.path.length) return false;
|
| +
|
| + // FIXME(Issue #2): Is this comparison working correctly?
|
| + if (a.partitionId != null) {
|
| + if (b.partitionId == null) return false;
|
| + if (a.partitionId.datasetId != b.partitionId.datasetId) return false;
|
| + if (a.partitionId.namespace != b.partitionId.namespace) return false;
|
| + } else {
|
| + if (b.partitionId != null) return false;
|
| + }
|
| +
|
| + for (int i = 0; i < a.path.length; i++) {
|
| + if (a.path[i].id != b.path[i].id ||
|
| + a.path[i].name != b.path[i].name ||
|
| + a.path[i].kind != b.path[i].kind) return false;
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + static _convertApi2DatastorePropertyValue(api.Value value) {
|
| + if (value.booleanValue != null)
|
| + return value.booleanValue;
|
| + else if (value.integerValue != null)
|
| + return int.parse(value.integerValue);
|
| + else if (value.doubleValue != null)
|
| + return value.doubleValue;
|
| + else if (value.stringValue != null)
|
| + return value.stringValue;
|
| + else if (value.dateTimeValue != null)
|
| + return value.dateTimeValue;
|
| + else if (value.blobValue != null)
|
| + return new datastore.BlobValue(value.blobValueAsBytes);
|
| + else if (value.keyValue != null)
|
| + return _convertApi2DatastoreKey(value.keyValue);
|
| + else if (value.listValue != null)
|
| + // FIXME(Issue #3): Consistently handle exceptions.
|
| + throw new Exception('Cannot have lists inside lists.');
|
| + else if (value.blobKeyValue != null)
|
| + throw new UnsupportedError('Blob keys are not supported.');
|
| + else if (value.entityValue != null)
|
| + throw new UnsupportedError('Entity values are not supported.');
|
| + return null;
|
| + }
|
| +
|
| + api.Value _convertDatastore2ApiPropertyValue(
|
| + value, bool indexed, {bool lists: true}) {
|
| + var apiValue = new api.Value()
|
| + ..indexed = indexed;
|
| + if (value == null) {
|
| + return apiValue;
|
| + } else if (value is bool) {
|
| + return apiValue
|
| + ..booleanValue = value;
|
| + } else if (value is int) {
|
| + return apiValue
|
| + ..integerValue = '$value';
|
| + } else if (value is double) {
|
| + return apiValue
|
| + ..doubleValue = value;
|
| + } else if (value is String) {
|
| + return apiValue
|
| + ..stringValue = value;
|
| + } else if (value is DateTime) {
|
| + return apiValue
|
| + ..dateTimeValue = value;
|
| + } else if (value is datastore.BlobValue) {
|
| + return apiValue
|
| + ..blobValueAsBytes = value.bytes;
|
| + } else if (value is datastore.Key) {
|
| + return apiValue
|
| + ..keyValue = _convertDatastore2ApiKey(value);
|
| + } else if (value is List) {
|
| + if (!lists) {
|
| + // FIXME(Issue #3): Consistently handle exceptions.
|
| + throw new Exception('List values are not allowed.');
|
| + }
|
| +
|
| + convertItem(i)
|
| + => _convertDatastore2ApiPropertyValue(i, indexed, lists: false);
|
| +
|
| + return new api.Value()
|
| + ..listValue = value.map(convertItem).toList();
|
| + } else {
|
| + throw new UnsupportedError(
|
| + 'Types ${value.runtimeType} cannot be used for serializing.');
|
| + }
|
| + }
|
| +
|
| + static _convertApi2DatastoreProperty(api.Property property) {
|
| + if (property.booleanValue != null)
|
| + return property.booleanValue;
|
| + else if (property.integerValue != null)
|
| + return int.parse(property.integerValue);
|
| + else if (property.doubleValue != null)
|
| + return property.doubleValue;
|
| + else if (property.stringValue != null)
|
| + return property.stringValue;
|
| + else if (property.dateTimeValue != null)
|
| + return property.dateTimeValue;
|
| + else if (property.blobValue != null)
|
| + return new datastore.BlobValue(property.blobValueAsBytes);
|
| + else if (property.keyValue != null)
|
| + return _convertApi2DatastoreKey(property.keyValue);
|
| + else if (property.listValue != null)
|
| + return
|
| + property.listValue.map(_convertApi2DatastorePropertyValue).toList();
|
| + else if (property.blobKeyValue != null)
|
| + throw new UnsupportedError('Blob keys are not supported.');
|
| + else if (property.entityValue != null)
|
| + throw new UnsupportedError('Entity values are not supported.');
|
| + return null;
|
| + }
|
| +
|
| + api.Property _convertDatastore2ApiProperty(
|
| + value, bool indexed, {bool lists: true}) {
|
| + var apiProperty = new api.Property()
|
| + ..indexed = indexed;
|
| + if (value == null) {
|
| + return null;
|
| + } else if (value is bool) {
|
| + return apiProperty
|
| + ..booleanValue = value;
|
| + } else if (value is int) {
|
| + return apiProperty
|
| + ..integerValue = '$value';
|
| + } else if (value is double) {
|
| + return apiProperty
|
| + ..doubleValue = value;
|
| + } else if (value is String) {
|
| + return apiProperty
|
| + ..stringValue = value;
|
| + } else if (value is DateTime) {
|
| + return apiProperty
|
| + ..dateTimeValue = value;
|
| + } else if (value is datastore.BlobValue) {
|
| + return apiProperty
|
| + ..blobValueAsBytes = value.bytes;
|
| + } else if (value is datastore.Key) {
|
| + return apiProperty
|
| + ..keyValue = _convertDatastore2ApiKey(value);
|
| + } else if (value is List) {
|
| + if (!lists) {
|
| + // FIXME(Issue #3): Consistently handle exceptions.
|
| + throw new Exception('List values are not allowed.');
|
| + }
|
| + convertItem(i)
|
| + => _convertDatastore2ApiPropertyValue(i, indexed, lists: false);
|
| + return new api.Property()..listValue = value.map(convertItem).toList();
|
| + } else {
|
| + throw new UnsupportedError(
|
| + 'Types ${value.runtimeType} cannot be used for serializing.');
|
| + }
|
| + }
|
| +
|
| + static datastore.Entity _convertApi2DatastoreEntity(api.Entity entity) {
|
| + var unindexedProperties = new Set();
|
| + var properties = {};
|
| +
|
| + if (entity.properties != null) {
|
| + entity.properties.forEach((String name, api.Property property) {
|
| + properties[name] = _convertApi2DatastoreProperty(property);
|
| + if (property.indexed == false) {
|
| + // TODO(Issue #$4): Should we support mixed indexed/non-indexed list
|
| + // values?
|
| + if (property.listValue != null) {
|
| + if (property.listValue.length > 0) {
|
| + var firstIndexed = property.listValue.first.indexed;
|
| + for (int i = 1; i < property.listValue.length; i++) {
|
| + if (property.listValue[i].indexed != firstIndexed) {
|
| + throw new Exception('Some list entries are indexed and some '
|
| + 'are not. This is currently not supported.');
|
| + }
|
| + }
|
| + if (firstIndexed == false) {
|
| + unindexedProperties.add(name);
|
| + }
|
| + }
|
| + } else {
|
| + unindexedProperties.add(name);
|
| + }
|
| + }
|
| + });
|
| + }
|
| + return new datastore.Entity(_convertApi2DatastoreKey(entity.key),
|
| + properties,
|
| + unIndexedProperties: unindexedProperties);
|
| + }
|
| +
|
| + api.Entity _convertDatastore2ApiEntity(datastore.Entity entity) {
|
| + var apiEntity = new api.Entity();
|
| +
|
| + apiEntity.key = _convertDatastore2ApiKey(entity.key);
|
| + apiEntity.properties = {};
|
| + if (entity.properties != null) {
|
| + for (var key in entity.properties.keys) {
|
| + var value = entity.properties[key];
|
| + bool indexed = false;
|
| + if (entity.unIndexedProperties != null) {
|
| + indexed = !entity.unIndexedProperties.contains(key);
|
| + }
|
| + var property = _convertDatastore2ApiPropertyValue(value, indexed);
|
| + apiEntity.properties[key] = property;
|
| + }
|
| + }
|
| + return apiEntity;
|
| + }
|
| +
|
| + static Map<datastore.FilterRelation, String> relationMapping = const {
|
| + datastore.FilterRelation.LessThan: 'LESS_THAN',
|
| + datastore.FilterRelation.LessThanOrEqual: 'LESS_THAN_OR_EQUAL',
|
| + datastore.FilterRelation.Equal: 'EQUAL',
|
| + datastore.FilterRelation.GreatherThan: 'GREATER_THAN',
|
| + datastore.FilterRelation.GreatherThanOrEqual: 'GREATER_THAN_OR_EQUAL',
|
| + // TODO(Issue #5): IN operator not supported currently.
|
| + };
|
| +
|
| + api.Filter _convertDatastore2ApiFilter(datastore.Filter filter) {
|
| + var pf = new api.PropertyFilter();
|
| + var operator = relationMapping[filter.relation];
|
| + // FIXME(Issue #5): Is this OK?
|
| + if (filter.relation == datastore.FilterRelation.In) {
|
| + operator = 'EQUAL';
|
| + }
|
| +
|
| + if (operator == null) {
|
| + throw new ArgumentError('Unknown filter relation: ${filter.relation}.');
|
| + }
|
| + pf.operator = operator;
|
| + pf.property = new api.PropertyReference()..name = filter.name;
|
| +
|
| + // FIXME(Issue #5): Is this OK?
|
| + var value = filter.value;
|
| + if (filter.relation == datastore.FilterRelation.In) {
|
| + if (value is List && value.length == 1) {
|
| + value = value.first;
|
| + } else {
|
| + throw new ArgumentError('List values not supported');
|
| + }
|
| + }
|
| +
|
| + pf.value = _convertDatastore2ApiPropertyValue(value, true, lists: false);
|
| + return new api.Filter()..propertyFilter = pf;
|
| + }
|
| +
|
| + api.Filter _convertDatastoreAncestorKey2ApiFilter(datastore.Key key) {
|
| + var pf = new api.PropertyFilter();
|
| + pf.operator = 'HAS_ANCESTOR';
|
| + pf.property = new api.PropertyReference()..name = '__key__';
|
| + pf.value = new api.Value()..keyValue = _convertDatastore2ApiKey(key);
|
| + return new api.Filter()..propertyFilter = pf;
|
| + }
|
| +
|
| + api.Filter _convertDatastore2ApiFilters(List<datastore.Filter> filters,
|
| + datastore.Key ancestorKey) {
|
| + if ((filters == null || filters.length == 0) && ancestorKey == null) {
|
| + return null;
|
| + }
|
| +
|
| + var compFilter = new api.CompositeFilter();
|
| + if (filters != null) {
|
| + compFilter.filters = filters.map(_convertDatastore2ApiFilter).toList();
|
| + }
|
| + if (ancestorKey != null) {
|
| + var filter = _convertDatastoreAncestorKey2ApiFilter(ancestorKey);
|
| + if (compFilter.filters == null) {
|
| + compFilter.filters = [filter];
|
| + } else {
|
| + compFilter.filters.add(filter);
|
| + }
|
| + }
|
| + compFilter.operator = 'AND';
|
| + return new api.Filter()..compositeFilter = compFilter;
|
| + }
|
| +
|
| + api.PropertyOrder _convertDatastore2ApiOrder(datastore.Order order) {
|
| + var property = new api.PropertyReference()..name = order.propertyName;
|
| + var direction = order.direction == datastore.OrderDirection.Ascending
|
| + ? 'ASCENDING' : 'DESCENDING';
|
| + return new api.PropertyOrder()
|
| + ..direction = direction
|
| + ..property = property;
|
| + }
|
| +
|
| + List<api.PropertyOrder> _convertDatastore2ApiOrders(
|
| + List<datastore.Order> orders) {
|
| + if (orders == null) return null;
|
| +
|
| + return orders.map(_convertDatastore2ApiOrder).toList();
|
| + }
|
| +
|
| + static Future _handleError(error, stack) {
|
| + if (error is api.DetailedApiRequestError) {
|
| + if (error.status == 400) {
|
| + return new Future.error(
|
| + new datastore.ApplicationError(error.message), stack);
|
| + } else if (error.status == 409) {
|
| + // NOTE: This is reported as:
|
| + // "too much contention on these datastore entities"
|
| + // TODO:
|
| + return new Future.error(new datastore.TransactionAbortedError(), stack);
|
| + } else if (error.status == 412) {
|
| + return new Future.error(new datastore.NeedIndexError(), stack);
|
| + }
|
| + }
|
| + return new Future.error(error, stack);
|
| + }
|
| +
|
| + Future<List<datastore.Key>> allocateIds(List<datastore.Key> keys) {
|
| + var request = new api.AllocateIdsRequest();
|
| + request..keys = keys.map(_convertDatastore2ApiKey).toList();
|
| + return _api.datasets.allocateIds(request, _project).then((response) {
|
| + return response.keys.map(_convertApi2DatastoreKey).toList();
|
| + }, onError: _handleError);
|
| + }
|
| +
|
| + Future<datastore.Transaction> beginTransaction(
|
| + {bool crossEntityGroup: false}) {
|
| + var request = new api.BeginTransactionRequest();
|
| + // TODO: Should this be made configurable?
|
| + request.isolationLevel = 'SERIALIZABLE';
|
| + return _api.datasets.beginTransaction(request, _project).then((result) {
|
| + return new TransactionImpl(result.transaction);
|
| + }, onError: _handleError);
|
| + }
|
| +
|
| + Future<datastore.CommitResult> commit({List<datastore.Entity> inserts,
|
| + List<datastore.Entity> autoIdInserts,
|
| + List<datastore.Key> deletes,
|
| + datastore.Transaction transaction}) {
|
| + var request = new api.CommitRequest();
|
| +
|
| + if (transaction != null) {
|
| + request.mode = 'TRANSACTIONAL';
|
| + request.transaction = (transaction as TransactionImpl).data;
|
| + } else {
|
| + request.mode = 'NON_TRANSACTIONAL';
|
| + }
|
| +
|
| + request.mutation = new api.Mutation();
|
| + if (inserts != null) {
|
| + request.mutation.upsert = new List(inserts.length);
|
| + for (int i = 0; i < inserts.length; i++) {
|
| + request.mutation.upsert[i] = _convertDatastore2ApiEntity(inserts[i]);
|
| + }
|
| + }
|
| + if (autoIdInserts != null) {
|
| + request.mutation.insertAutoId = new List(autoIdInserts.length);
|
| + for (int i = 0; i < autoIdInserts.length; i++) {
|
| + request.mutation.insertAutoId[i] =
|
| + _convertDatastore2ApiEntity(autoIdInserts[i]);
|
| + }
|
| + }
|
| + if (deletes != null) {
|
| + request.mutation.delete = new List(deletes.length);
|
| + for (int i = 0; i < deletes.length; i++) {
|
| + request.mutation.delete[i] = _convertDatastore2ApiKey(deletes[i]);
|
| + }
|
| + }
|
| + return _api.datasets.commit(request, _project).then((result) {
|
| + var keys;
|
| + if (autoIdInserts != null && autoIdInserts.length > 0) {
|
| + keys = result
|
| + .mutationResult
|
| + .insertAutoIdKeys
|
| + .map(_convertApi2DatastoreKey).toList();
|
| + }
|
| + return new datastore.CommitResult(keys);
|
| + }, onError: _handleError);
|
| + }
|
| +
|
| + Future<List<datastore.Entity>> lookup(List<datastore.Key> keys,
|
| + {datastore.Transaction transaction}) {
|
| + var apiKeys = keys.map(_convertDatastore2ApiKey).toList();
|
| + var request = new api.LookupRequest();
|
| + request.keys = apiKeys;
|
| + if (transaction != null) {
|
| + // TODO: Make readOptions more configurable.
|
| + request.readOptions = new api.ReadOptions();
|
| + request.readOptions.transaction = (transaction as TransactionImpl).data;
|
| + }
|
| + return _api.datasets.lookup(request, _project).then((response) {
|
| + if (response.deferred != null && response.deferred.length > 0) {
|
| + throw new datastore.DatastoreError(
|
| + 'Could not successfully look up all keys due to resource '
|
| + 'constraints.');
|
| + }
|
| +
|
| + // NOTE: This is worst-case O(n^2)!
|
| + // Maybe we can optimize this somehow. But the API says:
|
| + // message LookupResponse {
|
| + // // The order of results in these fields is undefined and has no relation to
|
| + // // the order of the keys in the input.
|
| + //
|
| + // // Entities found as ResultType.FULL entities.
|
| + // repeated EntityResult found = 1;
|
| + //
|
| + // // Entities not found as ResultType.KEY_ONLY entities.
|
| + // repeated EntityResult missing = 2;
|
| + //
|
| + // // A list of keys that were not looked up due to resource constraints.
|
| + // repeated Key deferred = 3;
|
| + // }
|
| + var entities = new List(apiKeys.length);
|
| + for (int i = 0; i < apiKeys.length; i++) {
|
| + var apiKey = apiKeys[i];
|
| +
|
| + bool found = false;
|
| +
|
| + if (response.found != null) {
|
| + for (var result in response.found) {
|
| + if (_compareApiKey(apiKey, result.entity.key)) {
|
| + entities[i] = _convertApi2DatastoreEntity(result.entity);
|
| + found = true;
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +
|
| + if (found) continue;
|
| +
|
| + if (response.missing != null) {
|
| + for (var result in response.missing) {
|
| + if (_compareApiKey(apiKey, result.entity.key)) {
|
| + entities[i] = null;
|
| + found = true;
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +
|
| + if (!found) {
|
| + throw new datastore.DatastoreError('Invalid server response: '
|
| + 'Tried to lookup ${apiKey.toJson()} but entity was neither in '
|
| + 'missing nor in found.');
|
| + }
|
| + }
|
| + return entities;
|
| + }, onError: _handleError);
|
| + }
|
| +
|
| + Future<Page<datastore.Entity>> query(
|
| + datastore.Query query, {datastore.Partition partition,
|
| + datastore.Transaction transaction}) {
|
| + // NOTE: We explicitly do not set 'limit' here, since this is handled by
|
| + // QueryPageImpl.runQuery.
|
| + var apiQuery = new api.Query()
|
| + ..filter = _convertDatastore2ApiFilters(query.filters,
|
| + query.ancestorKey)
|
| + ..order = _convertDatastore2ApiOrders(query.orders)
|
| + ..offset = query.offset;
|
| +
|
| + if (query.kind != null) {
|
| + apiQuery.kinds = [new api.KindExpression()..name = query.kind];
|
| + }
|
| +
|
| + var request = new api.RunQueryRequest();
|
| + request.query = apiQuery;
|
| + if (transaction != null) {
|
| + // TODO: Make readOptions more configurable.
|
| + request.readOptions = new api.ReadOptions();
|
| + request.readOptions.transaction = (transaction as TransactionImpl).data;
|
| + }
|
| + if (partition != null) {
|
| + request.partitionId = new api.PartitionId()
|
| + ..namespace = partition.namespace;
|
| + }
|
| +
|
| + return QueryPageImpl.runQuery(_api, _project, request, query.limit)
|
| + .catchError(_handleError);
|
| + }
|
| +
|
| + Future rollback(datastore.Transaction transaction) {
|
| + // TODO: Handle [transaction]
|
| + var request = new api.RollbackRequest()
|
| + ..transaction = (transaction as TransactionImpl).data;
|
| + return _api.datasets.rollback(request, _project).catchError(_handleError);
|
| + }
|
| +}
|
| +
|
| +class QueryPageImpl implements Page<datastore.Entity> {
|
| + static const int MAX_ENTITIES_PER_RESPONSE = 2000;
|
| +
|
| + final api.DatastoreApi _api;
|
| + final String _project;
|
| + final api.RunQueryRequest _nextRequest;
|
| + final List<datastore.Entity> _entities;
|
| + final bool _isLast;
|
| +
|
| + // This might be `null` in which case we request as many as we can get.
|
| + final int _remainingNumberOfEntities;
|
| +
|
| + QueryPageImpl(this._api, this._project,
|
| + this._nextRequest, this._entities,
|
| + this._isLast, this._remainingNumberOfEntities);
|
| +
|
| + static Future<QueryPageImpl> runQuery(api.DatastoreApi api,
|
| + String project,
|
| + api.RunQueryRequest request,
|
| + int limit,
|
| + {int batchSize}) {
|
| + int batchLimit = batchSize;
|
| + if (batchLimit == null) {
|
| + batchLimit = MAX_ENTITIES_PER_RESPONSE;
|
| + }
|
| + if (limit != null && limit < batchLimit) {
|
| + batchLimit = limit;
|
| + }
|
| +
|
| + request.query.limit = batchLimit;
|
| +
|
| + return api.datasets.runQuery(request, project).then((response) {
|
| + var returnedEntities = const [];
|
| +
|
| + var batch = response.batch;
|
| + if (batch.entityResults != null) {
|
| + returnedEntities = batch.entityResults
|
| + .map((result) => result.entity)
|
| + .map(DatastoreImpl._convertApi2DatastoreEntity)
|
| + .toList();
|
| + }
|
| +
|
| + // This check is only necessary for the first request/response pair
|
| + // (if offset was supplied).
|
| + if (request.query.offset != null &&
|
| + request.query.offset > 0 &&
|
| + request.query.offset != response.batch.skippedResults) {
|
| + throw new datastore.DatastoreError(
|
| + 'Server did not skip over the specified ${request.query.offset} '
|
| + 'entities.');
|
| + }
|
| +
|
| + if (limit != null && returnedEntities.length > limit) {
|
| + throw new datastore.DatastoreError(
|
| + 'Server returned more entities then the limit for the request'
|
| + '(${request.query.limit}) was.');
|
| + }
|
| +
|
| +
|
| + // FIXME: TODO: Big hack!
|
| + // It looks like Apiary/Atlas is currently broken.
|
| + /*
|
| + if (limit != null &&
|
| + returnedEntities.length < batchLimit &&
|
| + response.batch.moreResults == 'MORE_RESULTS_AFTER_LIMIT') {
|
| + throw new datastore.DatastoreError(
|
| + 'Server returned response with less entities then the limit was, '
|
| + 'but signals there are more results after the limit.');
|
| + }
|
| + */
|
| +
|
| + // In case a limit was specified, we need to subtraction the number of
|
| + // entities we already got.
|
| + // (the checks above guarantee that this subraction is >= 0).
|
| + int remainingEntities;
|
| + if (limit != null) {
|
| + remainingEntities = limit - returnedEntities.length;
|
| + }
|
| +
|
| + // If the server signals there are more entities and we either have no
|
| + // limit or our limit has not been reached, we set `moreBatches` to
|
| + // `true`.
|
| + bool moreBatches =
|
| + (remainingEntities == null || remainingEntities > 0) &&
|
| + response.batch.moreResults == 'MORE_RESULTS_AFTER_LIMIT';
|
| +
|
| + bool gotAll = limit != null && remainingEntities == 0;
|
| + bool noMore = response.batch.moreResults == 'NO_MORE_RESULTS';
|
| + bool isLast = gotAll || noMore;
|
| +
|
| + // As a sanity check, we assert that `moreBatches XOR isLast`.
|
| + assert (isLast != moreBatches);
|
| +
|
| + // FIXME: TODO: Big hack!
|
| + // It looks like Apiary/Atlas is currently broken.
|
| + if (moreBatches && returnedEntities.length == 0) {
|
| + print('Warning: Api to Google Cloud Datastore returned bogus response. '
|
| + 'Trying a workaround.');
|
| + isLast = true;
|
| + moreBatches = false;
|
| + }
|
| +
|
| + if (!isLast && response.batch.endCursor == null) {
|
| + throw new datastore.DatastoreError(
|
| + 'Server did not supply an end cursor, even though the query '
|
| + 'is not done.');
|
| + }
|
| +
|
| + if (isLast) {
|
| + return new QueryPageImpl(
|
| + api, project, request, returnedEntities, true, null);
|
| + } else {
|
| + // NOTE: We reuse the old RunQueryRequest object here .
|
| +
|
| + // The offset will be 0 from now on, since the first request will have
|
| + // skipped over the first `offset` results.
|
| + request.query.offset = 0;
|
| +
|
| + // Furthermore we set the startCursor to the endCursor of the previous
|
| + // result batch, so we can continue where we left off.
|
| + request.query.startCursor = batch.endCursor;
|
| +
|
| + return new QueryPageImpl(
|
| + api, project, request, returnedEntities, false, remainingEntities);
|
| + }
|
| + });
|
| + }
|
| +
|
| + bool get isLast => _isLast;
|
| +
|
| + List<datastore.Entity> get items => _entities;
|
| +
|
| + Future<Page<datastore.Entity>> next({int pageSize}) {
|
| + // NOTE: We do not respect [pageSize] here, the only mechanism we can
|
| + // really use is `query.limit`, but this is user-specified when making
|
| + // the query.
|
| + if (isLast) {
|
| + return new Future.sync(() {
|
| + throw new ArgumentError('Cannot call next() on last page.');
|
| + });
|
| + }
|
| +
|
| + return QueryPageImpl.runQuery(
|
| + _api, _project, _nextRequest, _remainingNumberOfEntities)
|
| + .catchError(DatastoreImpl._handleError);
|
| + }
|
| +}
|
|
|