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