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 datastore_test; |
| 6 |
| 7 /// NOTE: In order to run these tests, the following datastore indices must |
| 8 /// exist: |
| 9 /// $ cat index.yaml |
| 10 /// indexes: |
| 11 /// - kind: TestQueryKind |
| 12 /// ancestor: no |
| 13 /// properties: |
| 14 /// - name: indexedProp |
| 15 /// direction: asc |
| 16 /// - name: blobPropertyIndexed |
| 17 /// direction: asc |
| 18 /// |
| 19 /// - kind: TestQueryKind |
| 20 /// ancestor: no |
| 21 /// properties: |
| 22 /// - name: listproperty |
| 23 /// - name: test_property |
| 24 /// direction: desc |
| 25 /// $ gcloud preview datastore create-indexes . |
| 26 /// 02:19 PM Host: appengine.google.com |
| 27 /// 02:19 PM Uploading index definitions. |
| 28 |
| 29 |
| 30 import 'dart:async'; |
| 31 |
| 32 import 'package:gcloud/datastore.dart'; |
| 33 import 'package:gcloud/src/datastore_impl.dart' as datastore_impl; |
| 34 import 'package:gcloud/common.dart'; |
| 35 import 'package:unittest/unittest.dart'; |
| 36 |
| 37 import '../error_matchers.dart'; |
| 38 import 'utils.dart'; |
| 39 |
| 40 import '../../common_e2e.dart'; |
| 41 |
| 42 Future sleep(Duration duration) { |
| 43 var completer = new Completer(); |
| 44 new Timer(duration, completer.complete); |
| 45 return completer.future; |
| 46 } |
| 47 |
| 48 Future<List<Entity>> consumePages(FirstPageProvider provider) { |
| 49 return new StreamFromPages(provider).stream.toList(); |
| 50 } |
| 51 |
| 52 runTests(Datastore datastore) { |
| 53 Future withTransaction(Function f, {bool xg: false}) { |
| 54 return datastore.beginTransaction(crossEntityGroup: xg).then(f); |
| 55 } |
| 56 |
| 57 Future<List<Key>> insert(List<Entity> entities, |
| 58 List<Entity> autoIdEntities, |
| 59 {bool transactional: true}) { |
| 60 if (transactional) { |
| 61 return withTransaction((Transaction transaction) { |
| 62 return datastore.commit(inserts: entities, |
| 63 autoIdInserts: autoIdEntities, |
| 64 transaction: transaction).then((result) { |
| 65 if (autoIdEntities != null && autoIdEntities.length > 0) { |
| 66 expect(result.autoIdInsertKeys.length, |
| 67 equals(autoIdEntities.length)); |
| 68 } |
| 69 return result.autoIdInsertKeys; |
| 70 }); |
| 71 }, xg: true); |
| 72 } else { |
| 73 return datastore.commit(inserts: entities, autoIdInserts: autoIdEntities) |
| 74 .then((result) { |
| 75 if (autoIdEntities != null && autoIdEntities.length > 0) { |
| 76 expect(result.autoIdInsertKeys.length, |
| 77 equals(autoIdEntities.length)); |
| 78 } |
| 79 return result.autoIdInsertKeys; |
| 80 }); |
| 81 } |
| 82 } |
| 83 |
| 84 Future delete(List<Key> keys, {bool transactional: true}) { |
| 85 if (transactional) { |
| 86 return withTransaction((Transaction t) { |
| 87 return datastore.commit(deletes: keys, transaction: t) |
| 88 .then((result) => null); |
| 89 }, xg: true); |
| 90 } else { |
| 91 return datastore.commit(deletes: keys).then((_) => _); |
| 92 } |
| 93 } |
| 94 |
| 95 Future<List<Entity>> lookup(List<Key> keys, {bool transactional: true}) { |
| 96 if (transactional) { |
| 97 return withTransaction((Transaction transaction) { |
| 98 return datastore.lookup(keys, transaction: transaction); |
| 99 }, xg: true); |
| 100 } else { |
| 101 return datastore.lookup(keys); |
| 102 } |
| 103 } |
| 104 |
| 105 bool isValidKey(Key key, {bool ignoreIds: false}) { |
| 106 if (key.elements.length == 0) return false; |
| 107 |
| 108 for (var element in key.elements) { |
| 109 if (element.kind == null || element.kind is! String) return false; |
| 110 if (!ignoreIds) { |
| 111 if (element.id == null || |
| 112 (element.id is! String && element.id is! int)) { |
| 113 return false; |
| 114 } |
| 115 } |
| 116 } |
| 117 return true; |
| 118 } |
| 119 |
| 120 bool compareKey(Key a, Key b, {bool ignoreIds: false}) { |
| 121 if (a.partition != b.partition) return false; |
| 122 if (a.elements.length != b.elements.length) return false; |
| 123 for (int i = 0; i < a.elements.length; i++) { |
| 124 if (a.elements[i].kind != b.elements[i].kind) return false; |
| 125 if (!ignoreIds && a.elements[i].id != b.elements[i].id) return false; |
| 126 } |
| 127 return true; |
| 128 } |
| 129 |
| 130 bool compareEntity(Entity a, Entity b, {bool ignoreIds: false}) { |
| 131 if (!compareKey(a.key, b.key, ignoreIds: ignoreIds)) return false; |
| 132 if (a.properties.length != b.properties.length) return false; |
| 133 for (var key in a.properties.keys) { |
| 134 if (!b.properties.containsKey(key)) return false; |
| 135 if (a.properties[key] != null && a.properties[key] is List) { |
| 136 var aList = a.properties[key]; |
| 137 var bList = b.properties[key]; |
| 138 if (aList.length != bList.length) return false; |
| 139 for (var i = 0; i < aList.length; i++) { |
| 140 if (aList[i] != bList[i]) return false; |
| 141 } |
| 142 } else if (a.properties[key] is BlobValue) { |
| 143 if (b.properties[key] is BlobValue) { |
| 144 var b1 = (a.properties[key] as BlobValue).bytes; |
| 145 var b2 = (b.properties[key] as BlobValue).bytes; |
| 146 if (b1.length != b2.length) return false; |
| 147 for (var i = 0; i < b1.length; i++) { |
| 148 if (b1[i] != b2[i]) return false; |
| 149 } |
| 150 return true; |
| 151 } |
| 152 return false; |
| 153 } else { |
| 154 if (a.properties[key] != b.properties[key]) { |
| 155 return false; |
| 156 } |
| 157 } |
| 158 } |
| 159 return true; |
| 160 } |
| 161 |
| 162 group('e2e_datastore', () { |
| 163 group('insert', () { |
| 164 Future<List<Key>> testInsert(List<Entity> entities, |
| 165 {bool transactional: false, bool xg: false, bool unnamed: true}) { |
| 166 Future<List<Key>> test(Transaction transaction) { |
| 167 return datastore.commit(autoIdInserts: entities, |
| 168 transaction: transaction) |
| 169 .then((CommitResult result) { |
| 170 expect(result.autoIdInsertKeys.length, equals(entities.length)); |
| 171 |
| 172 for (var i = 0; i < result.autoIdInsertKeys.length; i++) { |
| 173 var key = result.autoIdInsertKeys[i]; |
| 174 expect(isValidKey(key), isTrue); |
| 175 if (unnamed) { |
| 176 expect(compareKey(key, entities[i].key, ignoreIds: true), |
| 177 isTrue); |
| 178 } else { |
| 179 expect(compareKey(key, entities[i].key), isTrue); |
| 180 } |
| 181 } |
| 182 return result.autoIdInsertKeys; |
| 183 }); |
| 184 } |
| 185 |
| 186 if (transactional) { |
| 187 return withTransaction(test, xg: xg); |
| 188 } |
| 189 return test(null); |
| 190 } |
| 191 |
| 192 Future<List<Key>> testInsertNegative(List<Entity> entities, |
| 193 {bool transactional: false, bool xg: false}) { |
| 194 test(Transaction transaction) { |
| 195 expect(datastore.commit(inserts: entities, |
| 196 transaction: transaction), |
| 197 throwsA(isApplicationError)); |
| 198 } |
| 199 |
| 200 if (transactional) { |
| 201 return withTransaction(test, xg: xg); |
| 202 } |
| 203 return test(null); |
| 204 } |
| 205 |
| 206 var unnamedEntities1 = buildEntities(42, 43); |
| 207 var unnamedEntities5 = buildEntities(1, 6); |
| 208 var unnamedEntities20 = buildEntities(6, 26); |
| 209 var named20000 = buildEntities( |
| 210 1000, 21001, idFunction: (i) => 'named_${i}_of_10000'); |
| 211 |
| 212 test('insert', () { |
| 213 return testInsert(unnamedEntities5, transactional: false).then((keys) { |
| 214 return delete(keys).then((_) { |
| 215 return lookup(keys).then((List<Entity> entities) { |
| 216 entities.forEach((Entity e) => expect(e, isNull)); |
| 217 }); |
| 218 }); |
| 219 }); |
| 220 }); |
| 221 |
| 222 test('insert_transactional', () { |
| 223 return testInsert(unnamedEntities1, transactional: true).then((keys) { |
| 224 return delete(keys).then((_) { |
| 225 return lookup(keys).then((List<Entity> entities) { |
| 226 entities.forEach((Entity e) => expect(e, isNull)); |
| 227 }); |
| 228 }); |
| 229 }); |
| 230 }); |
| 231 |
| 232 test('insert_transactional_xg', () { |
| 233 return testInsert( |
| 234 unnamedEntities5, transactional: true, xg: true).then((keys) { |
| 235 return delete(keys).then((_) { |
| 236 return lookup(keys).then((List<Entity> entities) { |
| 237 entities.forEach((Entity e) => expect(e, isNull)); |
| 238 }); |
| 239 }); |
| 240 }); |
| 241 }); |
| 242 |
| 243 // Does not work with cloud datastore REST api, why? |
| 244 test('negative_insert_transactional', () { |
| 245 return testInsertNegative(unnamedEntities5, transactional: true); |
| 246 }); |
| 247 |
| 248 // Does not work with cloud datastore REST api, why? |
| 249 test('negative_insert_transactional_xg', () { |
| 250 return testInsertNegative( |
| 251 unnamedEntities20, transactional: true, xg: true); |
| 252 }); |
| 253 |
| 254 test('negative_insert_20000_entities', () { |
| 255 // Maybe it should not be a [DataStoreError] here? |
| 256 // FIXME/TODO: This was adapted |
| 257 expect(datastore.commit(inserts: named20000), |
| 258 throws); |
| 259 }); |
| 260 |
| 261 // TODO: test invalid inserts (like entities without key, ...) |
| 262 }); |
| 263 |
| 264 group('allocate_ids', () { |
| 265 test('allocate_ids_query', () { |
| 266 compareResult(List<Key> keys, List<Key> completedKeys) { |
| 267 expect(completedKeys.length, equals(keys.length)); |
| 268 for (int i = 0; i < keys.length; i++) { |
| 269 var insertedKey = keys[i]; |
| 270 var completedKey = completedKeys[i]; |
| 271 |
| 272 expect(completedKey.elements.length, |
| 273 equals(insertedKey.elements.length)); |
| 274 for (int j = 0; j < insertedKey.elements.length - 1; j++) { |
| 275 expect(completedKey.elements[j], equals(insertedKey.elements[j])); |
| 276 } |
| 277 for (int j = insertedKey.elements.length - 1; |
| 278 j < insertedKey.elements.length; |
| 279 j++) { |
| 280 expect(completedKey.elements[j].kind, |
| 281 equals(insertedKey.elements[j].kind)); |
| 282 expect(completedKey.elements[j].id, isNotNull); |
| 283 expect(completedKey.elements[j].id, isInt); |
| 284 } |
| 285 } |
| 286 } |
| 287 |
| 288 var keys = buildKeys(1, 4); |
| 289 return datastore.allocateIds(keys).then((List<Key> completedKeys) { |
| 290 compareResult(keys, completedKeys); |
| 291 // TODO: Make sure we can insert these keys |
| 292 // FIXME: Insert currently doesn't through if entities already exist! |
| 293 }); |
| 294 }); |
| 295 }); |
| 296 |
| 297 group('lookup', () { |
| 298 Future testLookup(List<Key> keysToLookup, |
| 299 List<Entity> entitiesToLookup, |
| 300 {bool transactional: false, |
| 301 bool xg: false, |
| 302 bool negative: false, |
| 303 bool named: false}) { |
| 304 expect(keysToLookup.length, equals(entitiesToLookup.length)); |
| 305 for (var i = 0; i < keysToLookup.length; i++) { |
| 306 expect(compareKey(keysToLookup[i], |
| 307 entitiesToLookup[i].key, |
| 308 ignoreIds: !named), isTrue); |
| 309 } |
| 310 |
| 311 Future test(Transaction transaction) { |
| 312 return datastore.lookup(keysToLookup) |
| 313 .then((List<Entity> entities) { |
| 314 expect(entities.length, equals(keysToLookup.length)); |
| 315 if (negative) { |
| 316 for (int i = 0; i < entities.length; i++) { |
| 317 expect(entities[i], isNull); |
| 318 } |
| 319 } else { |
| 320 for (var i = 0; i < entities.length; i++) { |
| 321 expect(compareKey(entities[i].key, keysToLookup[i]), isTrue); |
| 322 expect(compareEntity(entities[i], |
| 323 entitiesToLookup[i], |
| 324 ignoreIds: !named), isTrue); |
| 325 } |
| 326 } |
| 327 if (transaction != null) { |
| 328 return |
| 329 datastore.commit(transaction: transaction).then((_) => null); |
| 330 } |
| 331 }); |
| 332 } |
| 333 |
| 334 if (transactional) { |
| 335 return withTransaction(test, xg: xg); |
| 336 } |
| 337 return test(null); |
| 338 } |
| 339 |
| 340 var unnamedEntities1 = buildEntities(42, 43); |
| 341 var unnamedEntities5 = buildEntities(1, 6); |
| 342 var unnamedEntities20 = buildEntities(6, 26); |
| 343 var entitiesWithAllPropertyTypes = buildEntityWithAllProperties(1, 6); |
| 344 |
| 345 test('lookup', () { |
| 346 return insert([], unnamedEntities20, transactional: false).then((keys) { |
| 347 keys.forEach((key) => expect(isValidKey(key), isTrue)); |
| 348 return testLookup(keys, unnamedEntities20).then((_) { |
| 349 return delete(keys, transactional: false); |
| 350 }); |
| 351 }); |
| 352 }); |
| 353 |
| 354 test('lookup_with_all_properties', () { |
| 355 return insert(entitiesWithAllPropertyTypes, [], transactional: false) |
| 356 .then((_) { |
| 357 var keys = entitiesWithAllPropertyTypes.map((e) => e.key).toList(); |
| 358 return testLookup(keys, entitiesWithAllPropertyTypes).then((_) { |
| 359 return delete(keys, transactional: false); |
| 360 }); |
| 361 }); |
| 362 }); |
| 363 |
| 364 test('lookup_transactional', () { |
| 365 return insert([], unnamedEntities1).then((keys) { |
| 366 keys.forEach((key) => expect(isValidKey(key), isTrue)); |
| 367 return testLookup(keys, unnamedEntities1, transactional: true) |
| 368 .then((_) => delete(keys)); |
| 369 }); |
| 370 }); |
| 371 |
| 372 test('lookup_transactional_xg', () { |
| 373 return insert([], unnamedEntities5).then((keys) { |
| 374 keys.forEach((key) => expect(isValidKey(key), isTrue)); |
| 375 return testLookup( |
| 376 keys, unnamedEntities5, transactional: true, xg: true).then((_) { |
| 377 return delete(keys); |
| 378 }); |
| 379 }); |
| 380 }); |
| 381 |
| 382 // TODO: ancestor lookups, string id lookups |
| 383 }); |
| 384 |
| 385 group('delete', () { |
| 386 Future testDelete(List<Key> keys, |
| 387 {bool transactional: false, bool xg: false}) { |
| 388 Future test(Transaction transaction) { |
| 389 return datastore.commit(deletes: keys).then((_) { |
| 390 if (transaction != null) { |
| 391 return datastore.commit(transaction: transaction); |
| 392 } |
| 393 }); |
| 394 } |
| 395 |
| 396 if (transactional) { |
| 397 return withTransaction(test, xg: xg); |
| 398 } |
| 399 return test(null); |
| 400 } |
| 401 |
| 402 var unnamedEntities1 = buildEntities(42, 43); |
| 403 var unnamedEntities5 = buildEntities(1, 6); |
| 404 var unnamedEntities99 = buildEntities(6, 106); |
| 405 |
| 406 test('delete', () { |
| 407 return insert([], unnamedEntities99, transactional: false).then((keys) { |
| 408 keys.forEach((key) => expect(isValidKey(key), isTrue)); |
| 409 return lookup(keys, transactional: false).then((entities) { |
| 410 entities.forEach((e) => expect(e, isNotNull)); |
| 411 return testDelete(keys).then((_) { |
| 412 return lookup(keys, transactional: false).then((entities) { |
| 413 entities.forEach((e) => expect(e, isNull)); |
| 414 }); |
| 415 }); |
| 416 }); |
| 417 }); |
| 418 }); |
| 419 |
| 420 // This should not work with [unamedEntities20], but is working! |
| 421 // FIXME TODO FIXME : look into this. |
| 422 test('delete_transactional', () { |
| 423 return insert([], unnamedEntities99, transactional: false).then((keys) { |
| 424 keys.forEach((key) => expect(isValidKey(key), isTrue)); |
| 425 return lookup(keys, transactional: false).then((entities) { |
| 426 entities.forEach((e) => expect(e, isNotNull)); |
| 427 return testDelete(keys, transactional: true).then((_) { |
| 428 return lookup(keys, transactional: false).then((entities) { |
| 429 entities.forEach((e) => expect(e, isNull)); |
| 430 }); |
| 431 }); |
| 432 }); |
| 433 }); |
| 434 }); |
| 435 |
| 436 test('delete_transactional_xg', () { |
| 437 return insert([], unnamedEntities99, transactional: false).then((keys) { |
| 438 keys.forEach((key) => expect(isValidKey(key), isTrue)); |
| 439 return lookup(keys, transactional: false).then((entities) { |
| 440 expect(entities.length, equals(unnamedEntities99.length)); |
| 441 entities.forEach((e) => expect(e, isNotNull)); |
| 442 return testDelete(keys, transactional: true, xg: true).then((_) { |
| 443 return lookup(keys, transactional: false).then((entities) { |
| 444 expect(entities.length, equals(unnamedEntities99.length)); |
| 445 entities.forEach((e) => expect(e, isNull)); |
| 446 }); |
| 447 }); |
| 448 }); |
| 449 }); |
| 450 }); |
| 451 |
| 452 // TODO: ancestor deletes, string id deletes |
| 453 }); |
| 454 |
| 455 group('rollback', () { |
| 456 Future testRollback(List<Key> keys, {bool xg: false}) { |
| 457 return withTransaction((Transaction transaction) { |
| 458 return datastore.lookup(keys, transaction: transaction) |
| 459 .then((List<Entity> entitites) { |
| 460 return datastore.rollback(transaction); |
| 461 }); |
| 462 }, xg: xg); |
| 463 } |
| 464 |
| 465 var namedEntities1 = buildEntities(42, 43, idFunction: (i) => "i$i"); |
| 466 var namedEntities5 = buildEntities(1, 6, idFunction: (i) => "i$i"); |
| 467 |
| 468 var namedEntities1Keys = namedEntities1.map((e) => e.key).toList(); |
| 469 var namedEntities5Keys = namedEntities5.map((e) => e.key).toList(); |
| 470 |
| 471 test('rollback', () { |
| 472 return testRollback(namedEntities1Keys); |
| 473 }); |
| 474 |
| 475 test('rollback_xg', () { |
| 476 return testRollback(namedEntities5Keys, xg: true); |
| 477 }); |
| 478 }); |
| 479 |
| 480 group('empty_commit', () { |
| 481 Future testEmptyCommit( |
| 482 List<Key> keys, {bool transactional: false, bool xg: false}) { |
| 483 Future test(Transaction transaction) { |
| 484 return datastore.lookup(keys, transaction: transaction) |
| 485 .then((List<Entity> entitites) { |
| 486 return datastore.commit(transaction: transaction); |
| 487 }); |
| 488 } |
| 489 |
| 490 if (transactional) { |
| 491 return withTransaction(test, xg: xg); |
| 492 } else { |
| 493 return test(null); |
| 494 } |
| 495 } |
| 496 |
| 497 var namedEntities1 = buildEntities(42, 43, idFunction: (i) => "i$i"); |
| 498 var namedEntities5 = buildEntities(1, 6, idFunction: (i) => "i$i"); |
| 499 var namedEntities20 = buildEntities(6, 26, idFunction: (i) => "i$i"); |
| 500 |
| 501 var namedEntities1Keys = namedEntities1.map((e) => e.key).toList(); |
| 502 var namedEntities5Keys = namedEntities5.map((e) => e.key).toList(); |
| 503 var namedEntities20Keys = namedEntities20.map((e) => e.key).toList(); |
| 504 |
| 505 test('empty_commit', () { |
| 506 return testEmptyCommit(namedEntities20Keys); |
| 507 }); |
| 508 |
| 509 test('empty_commit_transactional', () { |
| 510 return testEmptyCommit(namedEntities1Keys); |
| 511 }); |
| 512 |
| 513 test('empty_commit_transactional_xg', () { |
| 514 return testEmptyCommit(namedEntities5Keys); |
| 515 }); |
| 516 |
| 517 test('negative_empty_commit_xg', () { |
| 518 expect(testEmptyCommit( |
| 519 namedEntities20Keys, transactional: true, xg: true), |
| 520 throwsA(isApplicationError)); |
| 521 }); |
| 522 }); |
| 523 |
| 524 group('conflicting_transaction', () { |
| 525 Future testConflictingTransaction( |
| 526 List<Entity> entities, {bool xg: false}) { |
| 527 Future test( |
| 528 List<Entity> entities, Transaction transaction, value) { |
| 529 |
| 530 // Change entities: |
| 531 var changedEntities = new List<Entity>(entities.length); |
| 532 for (int i = 0; i < entities.length; i++) { |
| 533 var entity = entities[i]; |
| 534 var newProperties = new Map.from(entity.properties); |
| 535 for (var prop in newProperties.keys) { |
| 536 newProperties[prop] = "${newProperties[prop]}conflict$value"; |
| 537 } |
| 538 changedEntities[i] = |
| 539 new Entity(entity.key, newProperties); |
| 540 } |
| 541 return datastore.commit(inserts: changedEntities, |
| 542 transaction: transaction); |
| 543 } |
| 544 |
| 545 // Insert first |
| 546 return insert(entities, [], transactional: true).then((_) { |
| 547 var keys = entities.map((e) => e.key).toList(); |
| 548 |
| 549 var NUM_TRANSACTIONS = 10; |
| 550 |
| 551 // Start transactions |
| 552 var transactions = []; |
| 553 for (var i = 0; i < NUM_TRANSACTIONS; i++) { |
| 554 transactions.add(datastore.beginTransaction(crossEntityGroup: xg)); |
| 555 } |
| 556 return Future.wait(transactions) |
| 557 .then((List<Transaction> transactions) { |
| 558 // Do a lookup for the entities in every transaction |
| 559 var lookups = []; |
| 560 for (var transaction in transactions) { |
| 561 lookups.add( |
| 562 datastore.lookup(keys, transaction: transaction)); |
| 563 } |
| 564 return Future.wait(lookups).then((List<List<Entity>> results) { |
| 565 // Do a conflicting commit in every transaction. |
| 566 var commits = []; |
| 567 for (var i = 0; i < transactions.length; i++) { |
| 568 var transaction = transactions[i]; |
| 569 commits.add(test(results[i], transaction, i)); |
| 570 } |
| 571 return Future.wait(commits); |
| 572 }); |
| 573 }); |
| 574 }); |
| 575 } |
| 576 |
| 577 var namedEntities1 = buildEntities(42, 43, idFunction: (i) => "i$i"); |
| 578 var namedEntities5 = buildEntities(1, 6, idFunction: (i) => "i$i"); |
| 579 |
| 580 test('conflicting_transaction', () { |
| 581 expect(testConflictingTransaction(namedEntities1), |
| 582 throwsA(isTransactionAbortedError)); |
| 583 }); |
| 584 |
| 585 test('conflicting_transaction_xg', () { |
| 586 expect(testConflictingTransaction(namedEntities5, xg: true), |
| 587 throwsA(isTransactionAbortedError)); |
| 588 }); |
| 589 }); |
| 590 |
| 591 group('query', () { |
| 592 Future testQuery(String kind, |
| 593 {List<Filter> filters, |
| 594 List<Order> orders, |
| 595 bool transactional: false, |
| 596 bool xg: false, |
| 597 int offset, |
| 598 int limit}) { |
| 599 Future<List<Entity>> test(Transaction transaction) { |
| 600 var query = new Query( |
| 601 kind: kind, filters: filters, orders: orders, |
| 602 offset: offset, limit: limit); |
| 603 return consumePages((_) => datastore.query(query)) |
| 604 .then((List<Entity> entities) { |
| 605 if (transaction != null) { |
| 606 return datastore.commit(transaction: transaction) |
| 607 .then((_) => entities); |
| 608 } |
| 609 return entities; |
| 610 }); |
| 611 } |
| 612 |
| 613 if (transactional) { |
| 614 return withTransaction(test, xg: xg); |
| 615 } |
| 616 return test(null); |
| 617 } |
| 618 |
| 619 Future testQueryAndCompare(String kind, |
| 620 List<Entity> expectedEntities, |
| 621 {List<Filter> filters, |
| 622 List<Order> orders, |
| 623 bool transactional: false, |
| 624 bool xg: false, |
| 625 bool correctOrder: true, |
| 626 int offset, |
| 627 int limit}) { |
| 628 return testQuery(kind, |
| 629 filters: filters, |
| 630 orders: orders, |
| 631 transactional: transactional, |
| 632 xg: xg, |
| 633 offset: offset, |
| 634 limit: limit).then((List<Entity> entities) { |
| 635 expect(entities.length, equals(expectedEntities.length)); |
| 636 |
| 637 if (correctOrder) { |
| 638 for (int i = 0; i < entities.length; i++) { |
| 639 expect(compareEntity(entities[i], expectedEntities[i]), isTrue); |
| 640 } |
| 641 } else { |
| 642 for (int i = 0; i < entities.length; i++) { |
| 643 bool found = false; |
| 644 for (int j = 0; j < expectedEntities.length; j++) { |
| 645 if (compareEntity(entities[i], expectedEntities[i])) { |
| 646 found = true; |
| 647 } |
| 648 } |
| 649 expect(found, isTrue); |
| 650 } |
| 651 } |
| 652 }); |
| 653 } |
| 654 Future testOffsetLimitQuery(String kind, |
| 655 List<Entity> expectedEntities, |
| 656 {List<Order> orders, |
| 657 bool transactional: false, |
| 658 bool xg: false}) { |
| 659 // We query for all subsets of expectedEntities |
| 660 // NOTE: This is O(0.5 * n^2) queries, but n is currently only 6. |
| 661 List<Function> queryTests = []; |
| 662 for (int start = 0; start < expectedEntities.length; start++) { |
| 663 for (int end = start; end < expectedEntities.length; end++) { |
| 664 int offset = start; |
| 665 int limit = end - start; |
| 666 var entities = expectedEntities.sublist(offset, offset + limit); |
| 667 queryTests.add(() { |
| 668 return testQueryAndCompare( |
| 669 kind, entities, transactional: transactional, |
| 670 xg: xg, orders: orders, |
| 671 offset: offset, limit: limit); |
| 672 }); |
| 673 } |
| 674 } |
| 675 // Query with limit higher than the number of results. |
| 676 queryTests.add(() { |
| 677 return testQueryAndCompare( |
| 678 kind, expectedEntities, transactional: transactional, |
| 679 xg: xg, orders: orders, |
| 680 offset: 0, limit: expectedEntities.length * 10); |
| 681 }); |
| 682 |
| 683 return Future.forEach(queryTests, (f) => f()); |
| 684 } |
| 685 |
| 686 const TEST_QUERY_KIND = 'TestQueryKind'; |
| 687 var stringNamedEntities = buildEntities( |
| 688 1, 6, idFunction: (i) => 'str$i', kind: TEST_QUERY_KIND); |
| 689 var stringNamedKeys = stringNamedEntities.map((e) => e.key).toList(); |
| 690 |
| 691 var QUERY_KEY = TEST_PROPERTY_KEY_PREFIX; |
| 692 var QUERY_UPPER_BOUND = "${TEST_PROPERTY_VALUE_PREFIX}4"; |
| 693 var QUERY_LOWER_BOUND = "${TEST_PROPERTY_VALUE_PREFIX}1"; |
| 694 var QUERY_LIST_ENTRY = '${TEST_LIST_VALUE}2'; |
| 695 var QUERY_INDEX_VALUE = '${TEST_INDEXED_PROPERTY_VALUE_PREFIX}1'; |
| 696 |
| 697 var reverseOrderFunction = (Entity a, Entity b) { |
| 698 // Reverse the order |
| 699 return -1 * (a.properties[QUERY_KEY] as String) |
| 700 .compareTo(b.properties[QUERY_KEY]); |
| 701 }; |
| 702 |
| 703 var filterFunction = (Entity entity) { |
| 704 var value = entity.properties[QUERY_KEY]; |
| 705 return value.compareTo(QUERY_UPPER_BOUND) == -1 && |
| 706 value.compareTo(QUERY_LOWER_BOUND) == 1; |
| 707 }; |
| 708 var listFilterFunction = (Entity entity) { |
| 709 var values = entity.properties[TEST_LIST_PROPERTY]; |
| 710 return values.contains(QUERY_LIST_ENTRY); |
| 711 }; |
| 712 var indexFilterMatches = (Entity entity) { |
| 713 return entity.properties[TEST_INDEXED_PROPERTY] == QUERY_INDEX_VALUE; |
| 714 }; |
| 715 |
| 716 var sorted = stringNamedEntities.toList()..sort(reverseOrderFunction); |
| 717 var filtered = stringNamedEntities.where(filterFunction).toList(); |
| 718 var sortedAndFiltered = sorted.where(filterFunction).toList(); |
| 719 var sortedAndListFiltered = sorted.where(listFilterFunction).toList(); |
| 720 var indexedEntity = sorted.where(indexFilterMatches).toList(); |
| 721 expect(indexedEntity.length, equals(1)); |
| 722 |
| 723 var filters = [ |
| 724 new Filter(FilterRelation.GreatherThan, QUERY_KEY, QUERY_LOWER_BOUND), |
| 725 new Filter(FilterRelation.LessThan, QUERY_KEY, QUERY_UPPER_BOUND), |
| 726 ]; |
| 727 var listFilters = [ |
| 728 new Filter(FilterRelation.In, TEST_LIST_PROPERTY, [QUERY_LIST_ENTRY]) |
| 729 ]; |
| 730 var indexedPropertyFilter = [ |
| 731 new Filter(FilterRelation.Equal, |
| 732 TEST_INDEXED_PROPERTY, |
| 733 QUERY_INDEX_VALUE), |
| 734 new Filter(FilterRelation.Equal, |
| 735 TEST_BLOB_INDEXED_PROPERTY, |
| 736 TEST_BLOB_INDEXED_VALUE) |
| 737 ]; |
| 738 var unIndexedPropertyFilter = [ |
| 739 new Filter(FilterRelation.Equal, |
| 740 TEST_UNINDEXED_PROPERTY, |
| 741 QUERY_INDEX_VALUE) |
| 742 ]; |
| 743 |
| 744 var orders = [new Order(OrderDirection.Decending, QUERY_KEY)]; |
| 745 |
| 746 test('query', () { |
| 747 return insert(stringNamedEntities, []).then((keys) { |
| 748 return waitUntilEntitiesReady(datastore, stringNamedKeys).then((_) { |
| 749 var tests = [ |
| 750 // EntityKind query |
| 751 () => testQueryAndCompare( |
| 752 TEST_QUERY_KIND, stringNamedEntities, transactional: false, |
| 753 correctOrder: false), |
| 754 () => testQueryAndCompare( |
| 755 TEST_QUERY_KIND, stringNamedEntities, transactional: true, |
| 756 correctOrder: false), |
| 757 () => testQueryAndCompare( |
| 758 TEST_QUERY_KIND, stringNamedEntities, transactional: true, |
| 759 correctOrder: false, xg: true), |
| 760 |
| 761 // EntityKind query with order |
| 762 () => testQueryAndCompare( |
| 763 TEST_QUERY_KIND, sorted, transactional: false, |
| 764 orders: orders), |
| 765 () => testQueryAndCompare( |
| 766 TEST_QUERY_KIND, sorted, transactional: true, |
| 767 orders: orders), |
| 768 () => testQueryAndCompare( |
| 769 TEST_QUERY_KIND, sorted, transactional: false, xg: true, |
| 770 orders: orders), |
| 771 |
| 772 // EntityKind query with filter |
| 773 () => testQueryAndCompare( |
| 774 TEST_QUERY_KIND, filtered, transactional: false, |
| 775 filters: filters), |
| 776 () => testQueryAndCompare( |
| 777 TEST_QUERY_KIND, filtered, transactional: true, |
| 778 filters: filters), |
| 779 () => testQueryAndCompare( |
| 780 TEST_QUERY_KIND, filtered, transactional: false, xg: true, |
| 781 filters: filters), |
| 782 |
| 783 // EntityKind query with filter + order |
| 784 () => testQueryAndCompare( |
| 785 TEST_QUERY_KIND, sortedAndFiltered, transactional: false, |
| 786 filters: filters, orders: orders), |
| 787 () => testQueryAndCompare( |
| 788 TEST_QUERY_KIND, sortedAndFiltered, transactional: true, |
| 789 filters: filters, orders: orders), |
| 790 () => testQueryAndCompare( |
| 791 TEST_QUERY_KIND, sortedAndFiltered, transactional: false, |
| 792 xg: true, filters: filters, orders: orders), |
| 793 |
| 794 // EntityKind query with IN filter + order |
| 795 () => testQueryAndCompare( |
| 796 TEST_QUERY_KIND, sortedAndListFiltered, transactional: false, |
| 797 filters: listFilters, orders: orders), |
| 798 () => testQueryAndCompare( |
| 799 TEST_QUERY_KIND, sortedAndListFiltered, transactional: true, |
| 800 filters: listFilters, orders: orders), |
| 801 () => testQueryAndCompare( |
| 802 TEST_QUERY_KIND, sortedAndListFiltered, transactional: false, |
| 803 xg: true, filters: listFilters, orders: orders), |
| 804 |
| 805 // Limit & Offset test |
| 806 () => testOffsetLimitQuery( |
| 807 TEST_QUERY_KIND, sorted, transactional: false, |
| 808 orders: orders), |
| 809 () => testOffsetLimitQuery( |
| 810 TEST_QUERY_KIND, sorted, transactional: true, orders: orders), |
| 811 () => testOffsetLimitQuery( |
| 812 TEST_QUERY_KIND, sorted, transactional: false, |
| 813 xg: true, orders: orders), |
| 814 |
| 815 // Query for indexed property |
| 816 () => testQueryAndCompare( |
| 817 TEST_QUERY_KIND, indexedEntity, transactional: false, |
| 818 filters: indexedPropertyFilter), |
| 819 () => testQueryAndCompare( |
| 820 TEST_QUERY_KIND, indexedEntity, transactional: true, |
| 821 filters: indexedPropertyFilter), |
| 822 () => testQueryAndCompare( |
| 823 TEST_QUERY_KIND, indexedEntity, transactional: false, |
| 824 xg: true, filters: indexedPropertyFilter), |
| 825 |
| 826 // Query for un-indexed property |
| 827 () => testQueryAndCompare( |
| 828 TEST_QUERY_KIND, [], transactional: false, |
| 829 filters: unIndexedPropertyFilter), |
| 830 () => testQueryAndCompare( |
| 831 TEST_QUERY_KIND, [], transactional: true, |
| 832 filters: unIndexedPropertyFilter), |
| 833 () => testQueryAndCompare( |
| 834 TEST_QUERY_KIND, [], transactional: false, |
| 835 xg: true, filters: unIndexedPropertyFilter), |
| 836 |
| 837 // Delete results |
| 838 () => delete(stringNamedKeys, transactional: true), |
| 839 |
| 840 // Wait until the entity deletes are reflected in the indices. |
| 841 () => waitUntilEntitiesGone(datastore, stringNamedKeys), |
| 842 |
| 843 // Make sure queries don't return results |
| 844 () => testQueryAndCompare( |
| 845 TEST_QUERY_KIND, [], transactional: false), |
| 846 () => testQueryAndCompare( |
| 847 TEST_QUERY_KIND, [], transactional: true), |
| 848 () => testQueryAndCompare( |
| 849 TEST_QUERY_KIND, [], transactional: true, xg: true), |
| 850 () => testQueryAndCompare( |
| 851 TEST_QUERY_KIND, [], transactional: false, |
| 852 filters: filters, orders: orders), |
| 853 ]; |
| 854 return Future.forEach(tests, (f) => f()); |
| 855 }); |
| 856 }); |
| 857 |
| 858 // TODO: query by multiple keys, multiple sort oders, ... |
| 859 }); |
| 860 |
| 861 test('ancestor_query', () { |
| 862 /* |
| 863 * This test creates an |
| 864 * RootKind:1 -- This defines the entity group (no entity with that key) |
| 865 * + SubKind:1 -- This a subpath (no entity with that key) |
| 866 * + SubSubKind:1 -- This is a real entity of kind SubSubKind |
| 867 * + SubSubKind2:1 -- This is a real entity of kind SubSubKind2 |
| 868 */ |
| 869 var rootKey = new Key.fromParent('RootKind', 1); |
| 870 var subKey = new Key.fromParent('SubKind', 1, parent: rootKey); |
| 871 var subSubKey = new Key.fromParent('SubSubKind', 1, parent: subKey); |
| 872 var subSubKey2 = new Key.fromParent('SubSubKind2', 1, parent: subKey); |
| 873 var properties = { 'foo' : 'bar' }; |
| 874 |
| 875 var entity = new Entity(subSubKey, properties); |
| 876 var entity2 = new Entity(subSubKey2, properties); |
| 877 |
| 878 var orders = [new Order(OrderDirection.Ascending, '__key__')]; |
| 879 |
| 880 return datastore.commit(inserts: [entity, entity2]).then((_) { |
| 881 var futures = [ |
| 882 // FIXME/TODO: Ancestor queries should be strongly consistent. |
| 883 // We should not need to wait for them. |
| 884 () { |
| 885 return waitUntilEntitiesReady(datastore, [subSubKey, subSubKey2]); |
| 886 }, |
| 887 // Test that lookup only returns inserted entities. |
| 888 () { |
| 889 return datastore.lookup([rootKey, subKey, subSubKey, subSubKey2]) |
| 890 .then((List<Entity> entities) { |
| 891 expect(entities.length, 4); |
| 892 expect(entities[0], isNull); |
| 893 expect(entities[1], isNull); |
| 894 expect(entities[2], isNotNull); |
| 895 expect(entities[3], isNotNull); |
| 896 expect(compareEntity(entity, entities[2]), isTrue); |
| 897 expect(compareEntity(entity2, entities[3]), isTrue); |
| 898 }); |
| 899 }, |
| 900 |
| 901 // Query by ancestor. |
| 902 // - by [rootKey] |
| 903 () { |
| 904 var ancestorQuery = |
| 905 new Query(ancestorKey: rootKey, orders: orders); |
| 906 return consumePages((_) => datastore.query(ancestorQuery)) |
| 907 .then((results) { |
| 908 expect(results.length, 2); |
| 909 expect(compareEntity(entity, results[0]), isTrue); |
| 910 expect(compareEntity(entity2, results[1]), isTrue); |
| 911 }); |
| 912 }, |
| 913 // - by [subKey] |
| 914 () { |
| 915 var ancestorQuery = |
| 916 new Query(ancestorKey: subKey, orders: orders); |
| 917 return consumePages((_) => datastore.query(ancestorQuery)) |
| 918 .then((results) { |
| 919 expect(results.length, 2); |
| 920 expect(compareEntity(entity, results[0]), isTrue); |
| 921 expect(compareEntity(entity2, results[1]), isTrue); |
| 922 }); |
| 923 }, |
| 924 // - by [subSubKey] |
| 925 () { |
| 926 var ancestorQuery = new Query(ancestorKey: subSubKey); |
| 927 return consumePages((_) => datastore.query(ancestorQuery)) |
| 928 .then((results) { |
| 929 expect(results.length, 1); |
| 930 expect(compareEntity(entity, results[0]), isTrue); |
| 931 }); |
| 932 }, |
| 933 // - by [subSubKey2] |
| 934 () { |
| 935 var ancestorQuery = new Query(ancestorKey: subSubKey2); |
| 936 return consumePages((_) => datastore.query(ancestorQuery)) |
| 937 .then((results) { |
| 938 expect(results.length, 1); |
| 939 expect(compareEntity(entity2, results[0]), isTrue); |
| 940 }); |
| 941 }, |
| 942 |
| 943 // Query by ancestor and kind. |
| 944 // - by [rootKey] + 'SubSubKind' |
| 945 () { |
| 946 var query = new Query(ancestorKey: rootKey, kind: 'SubSubKind'); |
| 947 return consumePages((_) => datastore.query(query)) |
| 948 .then((List<Entity> results) { |
| 949 expect(results.length, 1); |
| 950 expect(compareEntity(entity, results[0]), isTrue); |
| 951 }); |
| 952 }, |
| 953 // - by [rootKey] + 'SubSubKind2' |
| 954 () { |
| 955 var query = new Query(ancestorKey: rootKey, kind: 'SubSubKind2'); |
| 956 return consumePages((_) => datastore.query(query)) |
| 957 .then((List<Entity> results) { |
| 958 expect(results.length, 1); |
| 959 expect(compareEntity(entity2, results[0]), isTrue); |
| 960 }); |
| 961 }, |
| 962 // - by [subSubKey] + 'SubSubKind' |
| 963 () { |
| 964 var query = new Query(ancestorKey: subSubKey, kind: 'SubSubKind'); |
| 965 return consumePages((_) => datastore.query(query)) |
| 966 .then((List<Entity> results) { |
| 967 expect(results.length, 1); |
| 968 expect(compareEntity(entity, results[0]), isTrue); |
| 969 }); |
| 970 }, |
| 971 // - by [subSubKey2] + 'SubSubKind2' |
| 972 () { |
| 973 var query = |
| 974 new Query(ancestorKey: subSubKey2, kind: 'SubSubKind2'); |
| 975 return consumePages((_) => datastore.query(query)) |
| 976 .then((List<Entity> results) { |
| 977 expect(results.length, 1); |
| 978 expect(compareEntity(entity2, results[0]), isTrue); |
| 979 }); |
| 980 }, |
| 981 // - by [subSubKey] + 'SubSubKind2' |
| 982 () { |
| 983 var query = |
| 984 new Query(ancestorKey: subSubKey, kind: 'SubSubKind2'); |
| 985 return consumePages((_) => datastore.query(query)) |
| 986 .then((List<Entity> results) { |
| 987 expect(results.length, 0); |
| 988 }); |
| 989 }, |
| 990 // - by [subSubKey2] + 'SubSubKind' |
| 991 () { |
| 992 var query = |
| 993 new Query(ancestorKey: subSubKey2, kind: 'SubSubKind'); |
| 994 return consumePages((_) => datastore.query(query)) |
| 995 .then((List<Entity> results) { |
| 996 expect(results.length, 0); |
| 997 }); |
| 998 }, |
| 999 |
| 1000 // Cleanup |
| 1001 () { |
| 1002 return datastore.commit(deletes: [subSubKey, subSubKey2]); |
| 1003 } |
| 1004 ]; |
| 1005 return Future.forEach(futures, (f) => f()).then(expectAsync((_) {})); |
| 1006 }); |
| 1007 }); |
| 1008 }); |
| 1009 }); |
| 1010 } |
| 1011 |
| 1012 Future cleanupDB(Datastore db) { |
| 1013 Future<List<String>> getNamespaces() { |
| 1014 var q = new Query(kind: '__namespace__'); |
| 1015 return consumePages((_) => db.query(q)).then((List<Entity> entities) { |
| 1016 return entities.map((Entity e) { |
| 1017 var id = e.key.elements.last.id; |
| 1018 if (id == 1) return null; |
| 1019 return id; |
| 1020 }).toList(); |
| 1021 }); |
| 1022 } |
| 1023 |
| 1024 Future<List<String>> getKinds(String namespace) { |
| 1025 var partition = new Partition(namespace); |
| 1026 var q = new Query(kind: '__kind__'); |
| 1027 return consumePages((_) => db.query(q, partition: partition)) |
| 1028 .then((List<Entity> entities) { |
| 1029 return entities |
| 1030 .map((Entity e) => e.key.elements.last.id) |
| 1031 .where((String kind) => !kind.contains('__')) |
| 1032 .toList(); |
| 1033 }); |
| 1034 } |
| 1035 |
| 1036 // cleanup() will call itself again as long as the DB is not clean. |
| 1037 cleanup(String namespace, String kind) { |
| 1038 var partition = new Partition(namespace); |
| 1039 var q = new Query(kind: kind, limit: 500); |
| 1040 return consumePages((_) => db.query(q, partition: partition)) |
| 1041 .then((List<Entity> entities) { |
| 1042 if (entities.length == 0) return null; |
| 1043 |
| 1044 print('[cleanupDB]: Removing left-over ${entities.length} entities'); |
| 1045 var deletes = entities.map((e) => e.key).toList(); |
| 1046 return db.commit(deletes: deletes).then((_) => cleanup(namespace, kind)); |
| 1047 }); |
| 1048 } |
| 1049 |
| 1050 return getNamespaces().then((List<String> namespaces) { |
| 1051 return Future.forEach(namespaces, (String namespace) { |
| 1052 return getKinds(namespace).then((List<String> kinds) { |
| 1053 return Future.forEach(kinds, (String kind) { |
| 1054 return cleanup(namespace, kind); |
| 1055 }); |
| 1056 }); |
| 1057 }); |
| 1058 }); |
| 1059 } |
| 1060 |
| 1061 Future waitUntilEntitiesReady(Datastore db, List<Key> keys) { |
| 1062 return waitUntilEntitiesHelper(db, keys, true); |
| 1063 } |
| 1064 |
| 1065 Future waitUntilEntitiesGone(Datastore db, List<Key> keys) { |
| 1066 return waitUntilEntitiesHelper(db, keys, false); |
| 1067 } |
| 1068 |
| 1069 Future waitUntilEntitiesHelper(Datastore db, List<Key> keys, bool positive) { |
| 1070 var keysByKind = {}; |
| 1071 for (var key in keys) { |
| 1072 keysByKind.putIfAbsent(key.elements.last.kind, () => []).add(key); |
| 1073 } |
| 1074 |
| 1075 Future waitForKeys(String kind, List<Key> keys) { |
| 1076 var q = new Query(kind: kind); |
| 1077 return consumePages((_) => db.query(q)).then((entities) { |
| 1078 for (var key in keys) { |
| 1079 bool found = false; |
| 1080 for (var entity in entities) { |
| 1081 if (key == entity.key) found = true; |
| 1082 } |
| 1083 if (positive) { |
| 1084 if (!found) return waitForKeys(kind, keys); |
| 1085 } else { |
| 1086 if (found) return waitForKeys(kind, keys); |
| 1087 } |
| 1088 } |
| 1089 return null; |
| 1090 }); |
| 1091 } |
| 1092 |
| 1093 return Future.forEach(keysByKind.keys.toList(), (String kind) { |
| 1094 return waitForKeys(kind, keysByKind[kind]); |
| 1095 }); |
| 1096 } |
| 1097 |
| 1098 main() { |
| 1099 var scopes = datastore_impl.DatastoreImpl.SCOPES; |
| 1100 |
| 1101 withAuthClient(scopes, (String project, httpClient) { |
| 1102 var datastore = new datastore_impl.DatastoreImpl(httpClient, 's~$project'); |
| 1103 return cleanupDB(datastore).then((_) { |
| 1104 return runE2EUnittest(() => runTests(datastore)); |
| 1105 }); |
| 1106 }); |
| 1107 } |
OLD | NEW |