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 library raw_datastore_v3_impl; |
| 6 |
| 7 import 'dart:async'; |
| 8 import 'dart:convert' show UTF8; |
| 9 |
| 10 import 'package:fixnum/fixnum.dart'; |
| 11 import 'package:gcloud/common.dart'; |
| 12 import 'package:gcloud/datastore.dart' as raw; |
| 13 |
| 14 import '../appengine_context.dart'; |
| 15 |
| 16 import '../../api/errors.dart' as errors; |
| 17 import '../protobuf_api/rpc/rpc_service.dart'; |
| 18 import '../protobuf_api/internal/datastore_v3.pb.dart'; |
| 19 import '../protobuf_api/datastore_v3_service.dart'; |
| 20 |
| 21 // TODO(gcloud Issue #3): Unified exception handing. |
| 22 buildDatastoreException(RpcApplicationError error) { |
| 23 var errorCode = Error_ErrorCode.valueOf(error.code); |
| 24 switch (errorCode) { |
| 25 case Error_ErrorCode.BAD_REQUEST: |
| 26 return new raw.ApplicationError("Bad request: ${error.message}"); |
| 27 case Error_ErrorCode.CONCURRENT_TRANSACTION: |
| 28 return new raw.TransactionAbortedError(); |
| 29 case Error_ErrorCode.INTERNAL_ERROR: |
| 30 return new raw.InternalError(); |
| 31 case Error_ErrorCode.NEED_INDEX: |
| 32 return new raw.NeedIndexError(); |
| 33 case Error_ErrorCode.TIMEOUT: |
| 34 return new raw.TimeoutError(); |
| 35 case Error_ErrorCode.PERMISSION_DENIED: |
| 36 return new raw.PermissionDeniedError(); |
| 37 case Error_ErrorCode.BIGTABLE_ERROR: |
| 38 case Error_ErrorCode.COMMITTED_BUT_STILL_APPLYING: |
| 39 case Error_ErrorCode.CAPABILITY_DISABLED: |
| 40 case Error_ErrorCode.TRY_ALTERNATE_BACKEND: |
| 41 case Error_ErrorCode.SAFE_TIME_TOO_OLD: |
| 42 return new Exception(error); |
| 43 } |
| 44 } |
| 45 |
| 46 Future catchAndReThrowDatastoreException(Future future) { |
| 47 return future.catchError((error, stack) { |
| 48 throw buildDatastoreException(error); |
| 49 }, test: (error) => error is RpcApplicationError); |
| 50 } |
| 51 |
| 52 class TransactionImpl extends raw.Transaction { |
| 53 final Transaction _rpcTransaction; |
| 54 |
| 55 TransactionImpl(this._rpcTransaction); |
| 56 } |
| 57 |
| 58 class Codec { |
| 59 static final ORDER_DIRECTION_MAPPING = { |
| 60 raw.OrderDirection.Ascending : Query_Order_Direction.ASCENDING, |
| 61 raw.OrderDirection.Decending : Query_Order_Direction.DESCENDING, |
| 62 }; |
| 63 |
| 64 static final FILTER_RELATION_MAPPING = { |
| 65 raw.FilterRelation.LessThan : Query_Filter_Operator.LESS_THAN, |
| 66 raw.FilterRelation.LessThanOrEqual : |
| 67 Query_Filter_Operator.LESS_THAN_OR_EQUAL, |
| 68 raw.FilterRelation.Equal : Query_Filter_Operator.EQUAL, |
| 69 raw.FilterRelation.GreatherThan : Query_Filter_Operator.GREATER_THAN, |
| 70 raw.FilterRelation.GreatherThanOrEqual : |
| 71 Query_Filter_Operator.GREATER_THAN_OR_EQUAL, |
| 72 raw.FilterRelation.In : Query_Filter_Operator.IN, |
| 73 }; |
| 74 |
| 75 final String _application; |
| 76 |
| 77 Codec(this._application); |
| 78 |
| 79 raw.Entity decodeEntity(EntityProto pb) { |
| 80 var properties = {}; |
| 81 var unIndexedProperties = new Set<String>(); |
| 82 |
| 83 decodeProperties(List<Property> pbList, {bool indexed: true}) { |
| 84 for (var propertyPb in pbList) { |
| 85 var name = propertyPb.name; |
| 86 var value = decodeValue(propertyPb); |
| 87 |
| 88 if (!indexed) { |
| 89 unIndexedProperties.add(name); |
| 90 } |
| 91 |
| 92 // TODO: This is a hackisch way of detecting whether to construct a list |
| 93 // or not. We may be able to use the [propertyPb.multiple] flag, but |
| 94 // we could run into issues if we get the same name twice where the flag |
| 95 // is false. (Or the flag is sometimes set and sometimes not). |
| 96 if (!properties.containsKey(name)) { |
| 97 properties[name] = decodeValue(propertyPb); |
| 98 } else { |
| 99 var oldValue = properties[name]; |
| 100 if (oldValue is List) { |
| 101 properties[name].add(value); |
| 102 } else { |
| 103 properties[name] = [oldValue, value]; |
| 104 } |
| 105 } |
| 106 } |
| 107 } |
| 108 decodeProperties(pb.property, indexed: true); |
| 109 decodeProperties(pb.rawProperty, indexed: false); |
| 110 |
| 111 return new raw.Entity(decodeKey(pb.key), |
| 112 properties, |
| 113 unIndexedProperties: unIndexedProperties); |
| 114 } |
| 115 |
| 116 Object decodeValue(Property propertyPb) { |
| 117 var pb = propertyPb.value; |
| 118 if (pb.hasBytesValue()) { |
| 119 switch (propertyPb.meaning) { |
| 120 case Property_Meaning.BYTESTRING: |
| 121 case Property_Meaning.BLOB: |
| 122 return new raw.BlobValue(pb.bytesValue); |
| 123 case Property_Meaning.TEXT: |
| 124 default: |
| 125 return UTF8.decode(pb.bytesValue); |
| 126 } |
| 127 } else if (pb.hasInt64Value()) { |
| 128 var intValue = pb.int64Value.toInt(); |
| 129 switch (propertyPb.meaning) { |
| 130 case Property_Meaning.GD_WHEN: |
| 131 return new DateTime.fromMillisecondsSinceEpoch( |
| 132 intValue ~/ 1000, isUtc: true); |
| 133 default: |
| 134 return intValue; |
| 135 } |
| 136 } else if (pb.hasBooleanValue()) { |
| 137 return pb.booleanValue; |
| 138 } else if (pb.hasPointValue()) { |
| 139 throw new UnimplementedError("Point values are not supported yet."); |
| 140 } else if (pb.hasDoubleValue()) { |
| 141 return pb.doubleValue; |
| 142 } else if (pb.hasUserValue()) { |
| 143 throw new UnimplementedError("User values are not supported yet."); |
| 144 } else if (pb.hasReferenceValue()) { |
| 145 return decodeKeyValue(pb.referenceValue); |
| 146 } |
| 147 // "If no value field is set, the value is interpreted as a null" |
| 148 return null; |
| 149 } |
| 150 |
| 151 PropertyValue_ReferenceValue encodeKeyValue(raw.Key key) { |
| 152 var referencePb = new PropertyValue_ReferenceValue(); |
| 153 var partition = key.partition; |
| 154 if (partition != null && partition.namespace != null) { |
| 155 referencePb.nameSpace = partition.namespace; |
| 156 } |
| 157 referencePb.app = _application; |
| 158 referencePb.pathElement.addAll(key.elements.map((raw.KeyElement part) { |
| 159 var pathElementPb = new PropertyValue_ReferenceValue_PathElement(); |
| 160 pathElementPb.type = part.kind; |
| 161 if (part.id != null) { |
| 162 if (part.id is int) { |
| 163 pathElementPb.id = new Int64(part.id); |
| 164 } else { |
| 165 pathElementPb.name = part.id; |
| 166 } |
| 167 } |
| 168 return pathElementPb; |
| 169 })); |
| 170 return referencePb; |
| 171 } |
| 172 |
| 173 raw.Key decodeKeyValue(PropertyValue_ReferenceValue pb) { |
| 174 var keyElements = []; |
| 175 for (var part in pb.pathElement) { |
| 176 var id; |
| 177 if (part.hasName()) { |
| 178 id = part.name; |
| 179 } else if (part.hasId()) { |
| 180 id = part.id.toInt(); |
| 181 } else { |
| 182 throw new errors.ProtocolError( |
| 183 'Invalid ReferenceValue: no int/string id.'); |
| 184 } |
| 185 keyElements.add(new raw.KeyElement(part.type, id)); |
| 186 } |
| 187 var partition = new raw.Partition(pb.hasNameSpace() ? pb.nameSpace : null); |
| 188 return new raw.Key(keyElements, partition: partition); |
| 189 } |
| 190 |
| 191 raw.Key decodeKey(Reference pb) { |
| 192 var keyElements = []; |
| 193 for (var part in pb.path.element) { |
| 194 var id; |
| 195 if (part.hasName()) { |
| 196 id = part.name; |
| 197 } else if (part.hasId()) { |
| 198 id = part.id.toInt(); |
| 199 } else { |
| 200 throw new errors.ProtocolError( |
| 201 'Invalid ReferenceValue: no int/string id.'); |
| 202 } |
| 203 keyElements.add(new raw.KeyElement(part.type, id)); |
| 204 } |
| 205 var partition = new raw.Partition(pb.hasNameSpace() ? pb.nameSpace : null); |
| 206 return new raw.Key(keyElements, partition: partition); |
| 207 } |
| 208 |
| 209 EntityProto encodeEntity(raw.Entity entity) { |
| 210 var pb = new EntityProto(); |
| 211 pb.key = encodeKey(entity.key, enforceId: false); |
| 212 if (entity.key.elements.length > 1) { |
| 213 pb.entityGroup = |
| 214 _encodePath([entity.key.elements.first], enforceId: true); |
| 215 } else { |
| 216 pb.entityGroup = new Path(); |
| 217 } |
| 218 |
| 219 var unIndexedProperties = entity.unIndexedProperties; |
| 220 for (var property in entity.properties.keys) { |
| 221 bool indexProperty = (unIndexedProperties == null || |
| 222 !unIndexedProperties.contains(property)); |
| 223 var value = entity.properties[property]; |
| 224 if (value != null && value is List) { |
| 225 for (var entry in value) { |
| 226 var pbProperty = encodeProperty( |
| 227 property, entry, multiple: true, indexProperty: indexProperty); |
| 228 if (indexProperty) { |
| 229 pb.property.add(pbProperty); |
| 230 } else { |
| 231 pb.rawProperty.add(pbProperty); |
| 232 } |
| 233 } |
| 234 } else { |
| 235 var pbProperty = encodeProperty( |
| 236 property, value, indexProperty: indexProperty); |
| 237 if (indexProperty) { |
| 238 pb.property.add(pbProperty); |
| 239 } else { |
| 240 pb.rawProperty.add(pbProperty); |
| 241 } |
| 242 } |
| 243 } |
| 244 return pb; |
| 245 } |
| 246 |
| 247 Reference encodeKey(raw.Key key, {bool enforceId: true}) { |
| 248 var partition = key.partition; |
| 249 |
| 250 var pb = new Reference(); |
| 251 if (partition != null && partition.namespace != null) { |
| 252 pb.nameSpace = partition.namespace; |
| 253 } |
| 254 pb.app = _application; |
| 255 pb.path = _encodePath(key.elements, enforceId: enforceId); |
| 256 return pb; |
| 257 } |
| 258 |
| 259 Path _encodePath(List<raw.KeyElement> elements, {bool enforceId: true}) { |
| 260 var pathPb = new Path(); |
| 261 for (var part in elements) { |
| 262 var partPb = new Path_Element(); |
| 263 partPb.type = part.kind; |
| 264 if (part.id != null) { |
| 265 if (part.id is String) { |
| 266 partPb.name = part.id; |
| 267 } else if (part.id is int) { |
| 268 partPb.id = new Int64(part.id); |
| 269 } else { |
| 270 throw new raw.ApplicationError( |
| 271 'Only strings and integers are supported as IDs ' |
| 272 '(was: ${part.id.runtimeType}).'); |
| 273 } |
| 274 } else { |
| 275 if (enforceId) { |
| 276 throw new raw.ApplicationError( |
| 277 'Error while encoding entity key: id was null.'); |
| 278 } |
| 279 } |
| 280 pathPb.element.add(partPb); |
| 281 } |
| 282 return pathPb; |
| 283 } |
| 284 |
| 285 Property encodeProperty(String name, Object value, |
| 286 {bool multiple: false, bool indexProperty: false}) { |
| 287 var pb = new Property(); |
| 288 pb.name = name; |
| 289 pb.multiple = multiple; |
| 290 |
| 291 if (value == null) { |
| 292 // "If no value field is set, the value is interpreted as a null" |
| 293 pb.value = new PropertyValue(); |
| 294 } else if (value is String) { |
| 295 pb.value = new PropertyValue()..bytesValue = UTF8.encode(value); |
| 296 if (!indexProperty) { |
| 297 pb.meaning = Property_Meaning.TEXT; |
| 298 } |
| 299 } else if (value is int) { |
| 300 pb.value = new PropertyValue()..int64Value = new Int64(value); |
| 301 } else if (value is double) { |
| 302 pb.value = new PropertyValue()..doubleValue = value; |
| 303 } else if (value is bool) { |
| 304 pb.value = new PropertyValue()..booleanValue = value; |
| 305 } else if (value is raw.Key) { |
| 306 pb.value = new PropertyValue()..referenceValue = encodeKeyValue(value); |
| 307 } else if (value is raw.BlobValue) { |
| 308 pb.value = new PropertyValue()..bytesValue = value.bytes; |
| 309 if (indexProperty) { |
| 310 pb.meaning = Property_Meaning.BYTESTRING; |
| 311 } else { |
| 312 pb.meaning = Property_Meaning.BLOB; |
| 313 } |
| 314 } else if (value is DateTime) { |
| 315 var usSinceEpoch = new Int64(value.toUtc().millisecondsSinceEpoch * 1000); |
| 316 pb.value = new PropertyValue()..int64Value = usSinceEpoch; |
| 317 pb.meaning = Property_Meaning.GD_WHEN; |
| 318 } else { |
| 319 throw new raw.ApplicationError( |
| 320 'Cannot encode unsupported ${value.runtimeType} type.'); |
| 321 } |
| 322 return pb; |
| 323 } |
| 324 } |
| 325 |
| 326 class DatastoreV3RpcImpl implements raw.Datastore { |
| 327 final DataStoreV3ServiceClientRPCStub _clientRPCStub; |
| 328 final AppengineContext _appengineContext; |
| 329 final Codec _codec; |
| 330 |
| 331 DatastoreV3RpcImpl(RPCService rpcService, |
| 332 AppengineContext appengineContext, |
| 333 String ticket) |
| 334 : _clientRPCStub = new DataStoreV3ServiceClientRPCStub(rpcService, |
| 335 ticket), |
| 336 _appengineContext = appengineContext, |
| 337 _codec = new Codec(appengineContext.fullQualifiedApplicationId); |
| 338 |
| 339 Future<List<raw.Key>> allocateIds(List<raw.Key> keys) { |
| 340 // TODO: We may be able to group keys if they have the same parent+kind |
| 341 // and add a size > 1. |
| 342 var requests = []; |
| 343 for (var key in keys) { |
| 344 var request = new AllocateIdsRequest(); |
| 345 request.modelKey = _codec.encodeKey(key, enforceId: false); |
| 346 request.size = new Int64(1); |
| 347 requests.add(_clientRPCStub.AllocateIds(request)); |
| 348 } |
| 349 return catchAndReThrowDatastoreException( |
| 350 Future.wait(requests).then((List<AllocateIdsResponse> responses) { |
| 351 var result = []; |
| 352 for (int i = 0; i < keys.length; i++) { |
| 353 var key = keys[i]; |
| 354 var response = responses[i]; |
| 355 var id = response.start.toInt(); |
| 356 |
| 357 if ((response.end - response.start) != 0) { |
| 358 throw new errors.ProtocolError( |
| 359 "Server responded with invalid allocatedId range: " |
| 360 "start=${response.start}, end=${response.end}"); |
| 361 } |
| 362 |
| 363 var parts = key.elements.take(key.elements.length - 1).toList(); |
| 364 parts.add(new raw.KeyElement(key.elements.last.kind, id)); |
| 365 result.add(new raw.Key(parts, partition: key.partition)); |
| 366 } |
| 367 return result; |
| 368 })); |
| 369 } |
| 370 |
| 371 Future<raw.Transaction> beginTransaction({bool crossEntityGroup: false}) { |
| 372 var request = new BeginTransactionRequest(); |
| 373 request.allowMultipleEg = crossEntityGroup; |
| 374 request.app = _appengineContext.fullQualifiedApplicationId; |
| 375 |
| 376 return catchAndReThrowDatastoreException( |
| 377 _clientRPCStub.BeginTransaction(request).then((Transaction t) { |
| 378 return new TransactionImpl(t); |
| 379 })); |
| 380 } |
| 381 |
| 382 Future<raw.CommitResult> commit({List<raw.Entity> inserts, |
| 383 List<raw.Entity> autoIdInserts, |
| 384 List<raw.Key> deletes, |
| 385 TransactionImpl transaction}) { |
| 386 Future insertFuture, deleteFuture; |
| 387 |
| 388 Transaction rpcTransaction; |
| 389 if (transaction != null) rpcTransaction = transaction._rpcTransaction; |
| 390 |
| 391 // Inserts |
| 392 bool needInserts = inserts != null && inserts.length > 0; |
| 393 bool needAutoIdInserts = autoIdInserts != null && autoIdInserts.length > 0; |
| 394 int totalNumberOfInserts = 0; |
| 395 if (needInserts || needAutoIdInserts) { |
| 396 var request = new PutRequest(); |
| 397 if (rpcTransaction != null) request.transaction = rpcTransaction; |
| 398 |
| 399 if (needAutoIdInserts) { |
| 400 totalNumberOfInserts += autoIdInserts.length; |
| 401 for (var entity in autoIdInserts) { |
| 402 request.entity.add(_codec.encodeEntity(entity)); |
| 403 } |
| 404 } |
| 405 |
| 406 if (needInserts) { |
| 407 totalNumberOfInserts += inserts.length; |
| 408 for (var entity in inserts) { |
| 409 request.entity.add(_codec.encodeEntity(entity)); |
| 410 } |
| 411 } |
| 412 |
| 413 insertFuture = _clientRPCStub.Put(request).then((PutResponse response) { |
| 414 if (response.key.length != totalNumberOfInserts) { |
| 415 // TODO(gcloud Issue #3): Unified exception handing. |
| 416 throw new Exception( |
| 417 "Tried to insert $totalNumberOfInserts entities, but response " |
| 418 "seems to indicate we inserted ${response.key.length} entities."); |
| 419 } |
| 420 if (needAutoIdInserts) { |
| 421 // NOTE: Auto id inserts are at the beginning. |
| 422 return response.key.take(autoIdInserts.length).map(_codec.decodeKey) |
| 423 .toList(); |
| 424 } else { |
| 425 return []; |
| 426 } |
| 427 }); |
| 428 } else { |
| 429 insertFuture = new Future.value(); |
| 430 } |
| 431 |
| 432 // Deletes |
| 433 if (deletes != null && deletes.length > 0) { |
| 434 var request = new DeleteRequest(); |
| 435 if (rpcTransaction != null) request.transaction = rpcTransaction; |
| 436 |
| 437 for (var key in deletes) { |
| 438 request.key.add(_codec.encodeKey(key)); |
| 439 } |
| 440 deleteFuture = _clientRPCStub.Delete(request).then((_) => null); |
| 441 } else { |
| 442 deleteFuture = new Future.value(); |
| 443 } |
| 444 |
| 445 return catchAndReThrowDatastoreException( |
| 446 Future.wait([insertFuture, deleteFuture]).then((results) { |
| 447 var result = new raw.CommitResult(results[0]); |
| 448 if (rpcTransaction == null) return result; |
| 449 return _clientRPCStub.Commit(rpcTransaction).then((_) => result); |
| 450 })); |
| 451 } |
| 452 |
| 453 Future rollback(TransactionImpl transaction) { |
| 454 return catchAndReThrowDatastoreException( |
| 455 _clientRPCStub.Rollback(transaction._rpcTransaction) |
| 456 .then((_) => null)); |
| 457 } |
| 458 |
| 459 Future<List<raw.Entity>> lookup( |
| 460 List<raw.Key> keys, {TransactionImpl transaction}) { |
| 461 var request = new GetRequest(); |
| 462 // Make sure we don't get results out-of-order. |
| 463 request.allowDeferred = false; |
| 464 if (transaction != null) { |
| 465 request.transaction = transaction._rpcTransaction; |
| 466 request.strong = true; |
| 467 } |
| 468 for (var key in keys) { |
| 469 request.key.add(_codec.encodeKey(key)); |
| 470 } |
| 471 return catchAndReThrowDatastoreException( |
| 472 _clientRPCStub.Get(request).then((GetResponse response) { |
| 473 return response.entity.map((GetResponse_Entity pb) { |
| 474 if (pb.hasEntity()) return _codec.decodeEntity(pb.entity); |
| 475 return null; |
| 476 }).toList(); |
| 477 })); |
| 478 } |
| 479 |
| 480 Future<Page<raw.Entity>> query( |
| 481 raw.Query query, {raw.Partition partition, TransactionImpl transaction}) { |
| 482 if (query.kind == null && query.ancestorKey == null) { |
| 483 throw new raw.ApplicationError( |
| 484 "You must specify a kind or ancestorKey in a query"); |
| 485 } |
| 486 |
| 487 Transaction rpcTransaction = transaction != null |
| 488 ? transaction._rpcTransaction : null; |
| 489 |
| 490 var request = new Query(); |
| 491 if (partition != null && partition.namespace != null) { |
| 492 request.nameSpace = partition.namespace; |
| 493 } |
| 494 request.app = _appengineContext.fullQualifiedApplicationId; |
| 495 if (rpcTransaction != null) { |
| 496 request.transaction = rpcTransaction; |
| 497 request.strong = true; |
| 498 } |
| 499 if (query.kind != null) { |
| 500 request.kind = query.kind; |
| 501 } |
| 502 if (query.ancestorKey != null) { |
| 503 request.ancestor = _codec.encodeKey(query.ancestorKey); |
| 504 } |
| 505 |
| 506 if (query.offset != null) { |
| 507 request.offset = query.offset; |
| 508 } |
| 509 if (query.limit != null) { |
| 510 request.limit = query.limit; |
| 511 } |
| 512 |
| 513 if (query.filters != null) { |
| 514 for (var filter in query.filters) { |
| 515 var queryFilter = new Query_Filter(); |
| 516 queryFilter.op = Codec.FILTER_RELATION_MAPPING[filter.relation]; |
| 517 if (filter.relation == raw.FilterRelation.In) { |
| 518 if (filter.value == null || filter.value is! List) { |
| 519 throw new raw.ApplicationError('Filters with list entry checks ' |
| 520 'must have a list value for membership checking.'); |
| 521 } |
| 522 for (var listValue in filter.value) { |
| 523 queryFilter.property.add(_codec.encodeProperty( |
| 524 filter.name, listValue, indexProperty: true)); |
| 525 } |
| 526 } else { |
| 527 queryFilter.property.add(_codec.encodeProperty( |
| 528 filter.name, filter.value, indexProperty: true)); |
| 529 } |
| 530 request.filter.add(queryFilter); |
| 531 } |
| 532 } |
| 533 if (query.orders != null) { |
| 534 for (var order in query.orders) { |
| 535 var queryOrder = new Query_Order(); |
| 536 queryOrder.direction = Codec.ORDER_DIRECTION_MAPPING[order.direction]; |
| 537 queryOrder.property = order.propertyName; |
| 538 request.order.add(queryOrder); |
| 539 } |
| 540 } |
| 541 |
| 542 return catchAndReThrowDatastoreException( |
| 543 _clientRPCStub.RunQuery(request).then((QueryResult result) { |
| 544 return QueryPageImpl.fromQueryResult( |
| 545 _clientRPCStub, _codec, query.offset, 0, query.limit, result); |
| 546 })); |
| 547 } |
| 548 } |
| 549 |
| 550 // NOTE: We're never calling datastore_v3.DeleteCursor() here. |
| 551 // - we don't know when this is safe, due to the Page<> interface |
| 552 // - the devappserver2/apiServer does not implement `DeleteCursor()` |
| 553 class QueryPageImpl implements Page<raw.Entity> { |
| 554 final DataStoreV3ServiceClientRPCStub _clientRPCStub; |
| 555 final Codec _codec; |
| 556 final Cursor _cursor; |
| 557 |
| 558 final List<raw.Entity> _entities; |
| 559 final bool _isLast; |
| 560 |
| 561 // This is `Query.offset` and will be carried across page walking. |
| 562 final int _offset; |
| 563 |
| 564 // This is always non-`null` and contains the number of entities that were |
| 565 // skiped so far. |
| 566 final int _alreadySkipped; |
| 567 |
| 568 // If not `null` this will hold the remaining number of entities we are |
| 569 // allowed to receive according to `Query.limit`. |
| 570 final int _remainingNumberOfEntities; |
| 571 |
| 572 QueryPageImpl(this._clientRPCStub, |
| 573 this._codec, |
| 574 this._cursor, |
| 575 this._entities, |
| 576 this._isLast, |
| 577 this._offset, |
| 578 this._alreadySkipped, |
| 579 this._remainingNumberOfEntities); |
| 580 |
| 581 static QueryPageImpl fromQueryResult( |
| 582 DataStoreV3ServiceClientRPCStub clientRPCStub, |
| 583 Codec codec, |
| 584 int offset, |
| 585 int alreadySkipped, |
| 586 int remainingNumberOfEntities, |
| 587 QueryResult queryResult) { |
| 588 // If we have an offset: Check that in total we haven't skipped too many. |
| 589 if (offset != null && |
| 590 offset > 0 && |
| 591 queryResult.hasSkippedResults() && |
| 592 queryResult.skippedResults > (offset - alreadySkipped)) { |
| 593 throw new raw.DatastoreError( |
| 594 'Datastore was supposed to skip ${offset} entities, ' |
| 595 'but response indicated ' |
| 596 '${queryResult.skippedResults + alreadySkipped} entities were ' |
| 597 'skipped (which is more).'); |
| 598 } |
| 599 |
| 600 // If we have a limit: Check that in total we haven't gotten too many. |
| 601 if (remainingNumberOfEntities != null && |
| 602 remainingNumberOfEntities > 0 && |
| 603 queryResult.result.length > remainingNumberOfEntities) { |
| 604 throw new raw.DatastoreError( |
| 605 'Datastore returned more entitites (${queryResult.result.length}) ' |
| 606 'then the limit was ($remainingNumberOfEntities).'); |
| 607 } |
| 608 |
| 609 // If we have a limit: Calculate the remaining limit. |
| 610 int remainingEntities; |
| 611 if (remainingNumberOfEntities != null && remainingNumberOfEntities > 0) { |
| 612 remainingEntities = remainingNumberOfEntities - queryResult.result.length; |
| 613 } |
| 614 |
| 615 // Determine if this is the last query batch. |
| 616 bool isLast = !(queryResult.hasMoreResults() && queryResult.moreResults); |
| 617 |
| 618 // If we have an offset: Calculate the new number of skipped entities. |
| 619 int skipped = alreadySkipped; |
| 620 if (offset != null && offset > 0 && queryResult.hasSkippedResults()) { |
| 621 skipped += queryResult.skippedResults; |
| 622 } |
| 623 |
| 624 var entities = queryResult.result.map(codec.decodeEntity).toList(); |
| 625 return new QueryPageImpl( |
| 626 clientRPCStub, codec, queryResult.cursor, entities, isLast, |
| 627 offset, skipped, remainingEntities); |
| 628 } |
| 629 |
| 630 bool get isLast => _isLast; |
| 631 |
| 632 List<raw.Entity> get items => _entities; |
| 633 |
| 634 Future<Page<raw.Entity>> next({int pageSize}) { |
| 635 if (isLast) { |
| 636 return new Future.sync(() { |
| 637 throw new ArgumentError('Cannot call next() on last page.'); |
| 638 }); |
| 639 } |
| 640 |
| 641 var nextRequest = new NextRequest(); |
| 642 nextRequest.cursor = _cursor; |
| 643 |
| 644 if (pageSize != null && pageSize > 0) { |
| 645 nextRequest.count = pageSize; |
| 646 } |
| 647 |
| 648 if (_offset != null && (_offset - _alreadySkipped) > 0) { |
| 649 nextRequest.offset = _offset - _alreadySkipped; |
| 650 } else { |
| 651 nextRequest.offset = 0; |
| 652 } |
| 653 |
| 654 return catchAndReThrowDatastoreException( |
| 655 _clientRPCStub.Next(nextRequest).then((QueryResult result) { |
| 656 return QueryPageImpl.fromQueryResult( |
| 657 _clientRPCStub, _codec, _offset, _alreadySkipped, |
| 658 _remainingNumberOfEntities, result); |
| 659 })); |
| 660 } |
| 661 } |
OLD | NEW |