1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.appsearch.cts.app; 18 19 import static android.app.appsearch.AppSearchResult.RESULT_NOT_FOUND; 20 import static android.app.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess; 21 import static android.app.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments; 22 import static android.app.appsearch.testutil.AppSearchTestUtils.doGet; 23 24 import static com.google.common.truth.Truth.assertThat; 25 26 import static org.junit.Assert.assertThrows; 27 28 import android.annotation.NonNull; 29 import android.app.appsearch.AppSearchBatchResult; 30 import android.app.appsearch.AppSearchResult; 31 import android.app.appsearch.AppSearchSchema; 32 import android.app.appsearch.AppSearchSessionShim; 33 import android.app.appsearch.GenericDocument; 34 import android.app.appsearch.Migrator; 35 import android.app.appsearch.PutDocumentsRequest; 36 import android.app.appsearch.SearchResultsShim; 37 import android.app.appsearch.SearchSpec; 38 import android.app.appsearch.SetSchemaRequest; 39 import android.app.appsearch.SetSchemaResponse; 40 41 import com.google.common.util.concurrent.ListenableFuture; 42 43 import org.junit.After; 44 import org.junit.Before; 45 import org.junit.Test; 46 47 import java.util.ArrayList; 48 import java.util.List; 49 import java.util.concurrent.ExecutionException; 50 51 /* 52 * For schema migration, we have 4 factors 53 * A. is ForceOverride set to true? 54 * B. is the schema change backwards compatible? 55 * C. is shouldTrigger return true? 56 * D. is there a migration triggered for each incompatible type and no deleted types? 57 * If B is true then D could never be false, so that will give us 12 combinations. 58 * 59 * Trigger Delete First Second 60 * A B C D Migration Types SetSchema SetSchema 61 * TRUE TRUE TRUE TRUE Yes succeeds succeeds(noop) 62 * TRUE TRUE FALSE TRUE succeeds succeeds(noop) 63 * TRUE FALSE TRUE TRUE Yes fail succeeds 64 * TRUE FALSE TRUE FALSE Yes Yes fail succeeds 65 * TRUE FALSE FALSE TRUE Yes fail succeeds 66 * TRUE FALSE FALSE FALSE Yes fail succeeds 67 * FALSE TRUE TRUE TRUE Yes succeeds succeeds(noop) 68 * FALSE TRUE FALSE TRUE succeeds succeeds(noop) 69 * FALSE FALSE TRUE TRUE Yes fail succeeds 70 * FALSE FALSE TRUE FALSE Yes fail throw error 71 * FALSE FALSE FALSE TRUE Impossible case, migrators are inactivity 72 * FALSE FALSE FALSE FALSE fail throw error 73 */ 74 public abstract class AppSearchSchemaMigrationCtsTestBase { 75 76 private static final String DB_NAME = ""; 77 private static final long DOCUMENT_CREATION_TIME = 12345L; 78 private static final Migrator ACTIVE_NOOP_MIGRATOR = 79 new Migrator() { 80 @Override 81 public boolean shouldMigrate(int currentVersion, int finalVersion) { 82 return true; 83 } 84 85 @NonNull 86 @Override 87 public GenericDocument onUpgrade( 88 int currentVersion, int finalVersion, @NonNull GenericDocument document) { 89 return document; 90 } 91 92 @NonNull 93 @Override 94 public GenericDocument onDowngrade( 95 int currentVersion, int finalVersion, @NonNull GenericDocument document) { 96 return document; 97 } 98 }; 99 private static final Migrator INACTIVE_MIGRATOR = 100 new Migrator() { 101 @Override 102 public boolean shouldMigrate(int currentVersion, int finalVersion) { 103 return false; 104 } 105 106 @NonNull 107 @Override 108 public GenericDocument onUpgrade( 109 int currentVersion, int finalVersion, @NonNull GenericDocument document) { 110 return document; 111 } 112 113 @NonNull 114 @Override 115 public GenericDocument onDowngrade( 116 int currentVersion, int finalVersion, @NonNull GenericDocument document) { 117 return document; 118 } 119 }; 120 121 private AppSearchSessionShim mDb; 122 createSearchSessionAsync( @onNull String dbName)123 protected abstract ListenableFuture<AppSearchSessionShim> createSearchSessionAsync( 124 @NonNull String dbName); 125 126 @Before setUp()127 public void setUp() throws Exception { 128 mDb = createSearchSessionAsync(DB_NAME).get(); 129 130 // Cleanup whatever documents may still exist in these databases. This is needed in 131 // addition to tearDown in case a test exited without completing properly. 132 AppSearchSchema schema = 133 new AppSearchSchema.Builder("testSchema") 134 .addProperty( 135 new AppSearchSchema.StringPropertyConfig.Builder("subject") 136 .setCardinality( 137 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 138 .setIndexingType( 139 AppSearchSchema.StringPropertyConfig 140 .INDEXING_TYPE_PREFIXES) 141 .setTokenizerType( 142 AppSearchSchema.StringPropertyConfig 143 .TOKENIZER_TYPE_PLAIN) 144 .build()) 145 .build(); 146 mDb.setSchemaAsync( 147 new SetSchemaRequest.Builder() 148 .addSchemas(schema) 149 .setForceOverride(true) 150 .build()) 151 .get(); 152 GenericDocument doc = 153 new GenericDocument.Builder<>("namespace", "id0", "testSchema") 154 .setPropertyString("subject", "testPut example1") 155 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 156 .build(); 157 AppSearchBatchResult<String, Void> result = 158 checkIsBatchResultSuccess( 159 mDb.putAsync( 160 new PutDocumentsRequest.Builder() 161 .addGenericDocuments(doc) 162 .build())); 163 assertThat(result.getSuccesses()).containsExactly("id0", null); 164 assertThat(result.getFailures()).isEmpty(); 165 } 166 167 @After tearDown()168 public void tearDown() throws Exception { 169 // Cleanup whatever documents may still exist in these databases. 170 mDb.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get(); 171 } 172 173 @Test test_ForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType()174 public void test_ForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType() 175 throws Exception { 176 // create a backwards compatible schema and update the version 177 AppSearchSchema backwardsCompatibleTriggerSchema = 178 new AppSearchSchema.Builder("testSchema") 179 .addProperty( 180 new AppSearchSchema.StringPropertyConfig.Builder("subject") 181 .setCardinality( 182 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 183 .setIndexingType( 184 AppSearchSchema.StringPropertyConfig 185 .INDEXING_TYPE_PREFIXES) 186 .setTokenizerType( 187 AppSearchSchema.StringPropertyConfig 188 .TOKENIZER_TYPE_PLAIN) 189 .build()) 190 .build(); 191 192 mDb.setSchemaAsync( 193 new SetSchemaRequest.Builder() 194 .addSchemas(backwardsCompatibleTriggerSchema) 195 .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR) 196 .setForceOverride(true) 197 .setVersion(2) // upgrade version 198 .build()) 199 .get(); 200 } 201 202 @Test testForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType()203 public void testForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType() 204 throws Exception { 205 // create a backwards compatible schema but don't update the version 206 AppSearchSchema backwardsCompatibleNoTriggerSchema = 207 new AppSearchSchema.Builder("testSchema") 208 .addProperty( 209 new AppSearchSchema.StringPropertyConfig.Builder("subject") 210 .setCardinality( 211 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 212 .setIndexingType( 213 AppSearchSchema.StringPropertyConfig 214 .INDEXING_TYPE_PREFIXES) 215 .setTokenizerType( 216 AppSearchSchema.StringPropertyConfig 217 .TOKENIZER_TYPE_PLAIN) 218 .build()) 219 .build(); 220 221 mDb.setSchemaAsync( 222 new SetSchemaRequest.Builder() 223 .addSchemas(backwardsCompatibleNoTriggerSchema) 224 .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR) 225 .setForceOverride(true) 226 .build()) 227 .get(); 228 } 229 230 @Test testForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType()231 public void testForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType() 232 throws Exception { 233 // create a backwards incompatible schema and update the version 234 AppSearchSchema backwardsIncompatibleTriggerSchema = 235 new AppSearchSchema.Builder("testSchema").build(); 236 237 mDb.setSchemaAsync( 238 new SetSchemaRequest.Builder() 239 .addSchemas(backwardsIncompatibleTriggerSchema) 240 .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR) 241 .setForceOverride(true) 242 .setVersion(2) // upgrade version 243 .build()) 244 .get(); 245 } 246 247 @Test testForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType()248 public void testForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType() 249 throws Exception { 250 // create a backwards incompatible schema and update the version 251 AppSearchSchema backwardsIncompatibleTriggerSchema = 252 new AppSearchSchema.Builder("testSchema").build(); 253 254 mDb.setSchemaAsync( 255 new SetSchemaRequest.Builder() 256 .addSchemas(backwardsIncompatibleTriggerSchema) 257 .setMigrator("testSchema", INACTIVE_MIGRATOR) // ND 258 .setForceOverride(true) 259 .setVersion(2) // upgrade version 260 .build()) 261 .get(); 262 } 263 264 @Test testForceOverride_BackwardsIncompatible_NoTrigger_MigrateIncompatibleType()265 public void testForceOverride_BackwardsIncompatible_NoTrigger_MigrateIncompatibleType() 266 throws Exception { 267 // create a backwards incompatible schema but don't update the version 268 AppSearchSchema backwardsIncompatibleNoTriggerSchema = 269 new AppSearchSchema.Builder("testSchema").build(); 270 271 mDb.setSchemaAsync( 272 new SetSchemaRequest.Builder() 273 .addSchemas(backwardsIncompatibleNoTriggerSchema) 274 .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR) 275 .setForceOverride(true) 276 .build()) 277 .get(); 278 } 279 280 @Test testForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType()281 public void testForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType() 282 throws Exception { 283 // create a backwards incompatible schema but don't update the version 284 AppSearchSchema backwardsIncompatibleNoMigrateIncompatibleTypeSchema = 285 new AppSearchSchema.Builder("testSchema").build(); 286 287 mDb.setSchemaAsync( 288 new SetSchemaRequest.Builder() 289 .addSchemas(backwardsIncompatibleNoMigrateIncompatibleTypeSchema) 290 .setMigrator("testSchema", INACTIVE_MIGRATOR) // ND 291 .setForceOverride(true) 292 .build()) 293 .get(); 294 } 295 296 @Test testNoForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType()297 public void testNoForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType() 298 throws Exception { 299 // create a backwards compatible schema and update the version 300 AppSearchSchema backwardsCompatibleTriggerSchema = 301 new AppSearchSchema.Builder("testSchema") 302 .addProperty( 303 new AppSearchSchema.StringPropertyConfig.Builder("subject") 304 .setCardinality( 305 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 306 .setIndexingType( 307 AppSearchSchema.StringPropertyConfig 308 .INDEXING_TYPE_PREFIXES) 309 .setTokenizerType( 310 AppSearchSchema.StringPropertyConfig 311 .TOKENIZER_TYPE_PLAIN) 312 .build()) 313 .build(); 314 315 mDb.setSchemaAsync( 316 new SetSchemaRequest.Builder() 317 .addSchemas(backwardsCompatibleTriggerSchema) 318 .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR) 319 .setVersion(2) // upgrade version 320 .build()) 321 .get(); 322 } 323 324 @Test testNoForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType()325 public void testNoForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType() 326 throws Exception { 327 // create a backwards compatible schema but don't update the version 328 AppSearchSchema backwardsCompatibleNoTriggerSchema = 329 new AppSearchSchema.Builder("testSchema") 330 .addProperty( 331 new AppSearchSchema.StringPropertyConfig.Builder("subject") 332 .setCardinality( 333 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 334 .setIndexingType( 335 AppSearchSchema.StringPropertyConfig 336 .INDEXING_TYPE_PREFIXES) 337 .setTokenizerType( 338 AppSearchSchema.StringPropertyConfig 339 .TOKENIZER_TYPE_PLAIN) 340 .build()) 341 .build(); 342 343 mDb.setSchemaAsync( 344 new SetSchemaRequest.Builder() 345 .addSchemas(backwardsCompatibleNoTriggerSchema) 346 .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR) 347 .setForceOverride(true) 348 .build()) 349 .get(); 350 } 351 352 @Test testNoForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType()353 public void testNoForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType() 354 throws Exception { 355 // create a backwards incompatible schema and update the version 356 AppSearchSchema backwardsIncompatibleTriggerSchema = 357 new AppSearchSchema.Builder("testSchema").build(); 358 359 mDb.setSchemaAsync( 360 new SetSchemaRequest.Builder() 361 .addSchemas(backwardsIncompatibleTriggerSchema) 362 .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR) 363 .setVersion(2) // upgrade version 364 .build()) 365 .get(); 366 } 367 368 @Test testNoForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType()369 public void testNoForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType() 370 throws Exception { 371 // create a backwards incompatible schema and update the version 372 AppSearchSchema backwardsCompatibleTriggerSchema = 373 new AppSearchSchema.Builder("testSchema").build(); 374 375 ExecutionException exception = 376 assertThrows( 377 ExecutionException.class, 378 () -> 379 mDb.setSchemaAsync( 380 new SetSchemaRequest.Builder() 381 .addSchemas( 382 backwardsCompatibleTriggerSchema) 383 .setMigrator( 384 "testSchema", 385 INACTIVE_MIGRATOR) // ND 386 .setVersion(2) // upgrade version 387 .build()) 388 .get()); 389 assertThat(exception).hasMessageThat().contains("Schema is incompatible."); 390 } 391 392 @Test testNoForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType()393 public void testNoForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType() 394 throws Exception { 395 // create a backwards incompatible schema but don't update the version 396 AppSearchSchema backwardsIncompatibleNoTriggerNoMigrateIncompatibleTypeSchema = 397 new AppSearchSchema.Builder("testSchema").build(); 398 399 ExecutionException exception = 400 assertThrows( 401 ExecutionException.class, 402 () -> 403 mDb.setSchemaAsync( 404 new SetSchemaRequest.Builder() 405 .addSchemas( 406 backwardsIncompatibleNoTriggerNoMigrateIncompatibleTypeSchema) 407 .setMigrator( 408 "testSchema", 409 INACTIVE_MIGRATOR) // ND 410 .build()) 411 .get()); 412 assertThat(exception).hasMessageThat().contains("Schema is incompatible."); 413 } 414 415 @Test testSchemaMigration()416 public void testSchemaMigration() throws Exception { 417 AppSearchSchema schema = 418 new AppSearchSchema.Builder("testSchema") 419 .addProperty( 420 new AppSearchSchema.StringPropertyConfig.Builder("subject") 421 .setCardinality( 422 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 423 .setIndexingType( 424 AppSearchSchema.StringPropertyConfig 425 .INDEXING_TYPE_PREFIXES) 426 .setTokenizerType( 427 AppSearchSchema.StringPropertyConfig 428 .TOKENIZER_TYPE_PLAIN) 429 .build()) 430 .addProperty( 431 new AppSearchSchema.StringPropertyConfig.Builder("To") 432 .setCardinality( 433 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 434 .setIndexingType( 435 AppSearchSchema.StringPropertyConfig 436 .INDEXING_TYPE_PREFIXES) 437 .setTokenizerType( 438 AppSearchSchema.StringPropertyConfig 439 .TOKENIZER_TYPE_PLAIN) 440 .build()) 441 .build(); 442 mDb.setSchemaAsync( 443 new SetSchemaRequest.Builder() 444 .addSchemas(schema) 445 .setForceOverride(true) 446 .build()) 447 .get(); 448 449 GenericDocument doc1 = 450 new GenericDocument.Builder<>("namespace", "id1", "testSchema") 451 .setPropertyString("subject", "testPut example1") 452 .setPropertyString("To", "testTo example1") 453 .build(); 454 GenericDocument doc2 = 455 new GenericDocument.Builder<>("namespace", "id2", "testSchema") 456 .setPropertyString("subject", "testPut example2") 457 .setPropertyString("To", "testTo example2") 458 .build(); 459 460 AppSearchBatchResult<String, Void> result = 461 checkIsBatchResultSuccess( 462 mDb.putAsync( 463 new PutDocumentsRequest.Builder() 464 .addGenericDocuments(doc1, doc2) 465 .build())); 466 assertThat(result.getSuccesses()).containsExactly("id1", null, "id2", null); 467 assertThat(result.getFailures()).isEmpty(); 468 469 // create new schema type and upgrade the version number 470 AppSearchSchema newSchema = 471 new AppSearchSchema.Builder("testSchema") 472 .addProperty( 473 new AppSearchSchema.StringPropertyConfig.Builder("subject") 474 .setCardinality( 475 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 476 .setIndexingType( 477 AppSearchSchema.StringPropertyConfig 478 .INDEXING_TYPE_PREFIXES) 479 .setTokenizerType( 480 AppSearchSchema.StringPropertyConfig 481 .TOKENIZER_TYPE_PLAIN) 482 .build()) 483 .build(); 484 485 // set the new schema to AppSearch, the first document will be migrated successfully but the 486 // second one will be failed. 487 488 Migrator migrator = 489 new Migrator() { 490 @Override 491 public boolean shouldMigrate(int currentVersion, int finalVersion) { 492 return currentVersion != finalVersion; 493 } 494 495 @NonNull 496 @Override 497 public GenericDocument onUpgrade( 498 int currentVersion, 499 int finalVersion, 500 @NonNull GenericDocument document) { 501 if (document.getId().equals("id2")) { 502 return new GenericDocument.Builder<>( 503 document.getNamespace(), 504 document.getId(), 505 document.getSchemaType()) 506 .setPropertyString("subject", "testPut example2") 507 .setPropertyString( 508 "to", "Expect to fail, property not in the schema") 509 .build(); 510 } 511 return new GenericDocument.Builder<>( 512 document.getNamespace(), 513 document.getId(), 514 document.getSchemaType()) 515 .setPropertyString("subject", "testPut example1 migrated") 516 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 517 .build(); 518 } 519 520 @NonNull 521 @Override 522 public GenericDocument onDowngrade( 523 int currentVersion, 524 int finalVersion, 525 @NonNull GenericDocument document) { 526 throw new IllegalStateException( 527 "Downgrade should not be triggered for this test"); 528 } 529 }; 530 531 SetSchemaResponse setSchemaResponse = 532 mDb.setSchemaAsync( 533 new SetSchemaRequest.Builder() 534 .addSchemas(newSchema) 535 .setMigrator("testSchema", migrator) 536 .setVersion(2) // upgrade version 537 .build()) 538 .get(); 539 540 // Check the schema has been saved 541 assertThat(mDb.getSchemaAsync().get().getSchemas()).containsExactly(newSchema); 542 543 assertThat(setSchemaResponse.getDeletedTypes()).isEmpty(); 544 assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("testSchema"); 545 assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("testSchema"); 546 547 // Check migrate the first document is success 548 GenericDocument expected = 549 new GenericDocument.Builder<>("namespace", "id1", "testSchema") 550 .setPropertyString("subject", "testPut example1 migrated") 551 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 552 .build(); 553 assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected); 554 555 // Check migrate the second document is fail. 556 assertThat(setSchemaResponse.getMigrationFailures()).hasSize(1); 557 SetSchemaResponse.MigrationFailure migrationFailure = 558 setSchemaResponse.getMigrationFailures().get(0); 559 assertThat(migrationFailure.getNamespace()).isEqualTo("namespace"); 560 assertThat(migrationFailure.getSchemaType()).isEqualTo("testSchema"); 561 assertThat(migrationFailure.getDocumentId()).isEqualTo("id2"); 562 563 AppSearchResult<Void> actualResult = migrationFailure.getAppSearchResult(); 564 assertThat(actualResult.isSuccess()).isFalse(); 565 assertThat(actualResult.getResultCode()).isEqualTo(RESULT_NOT_FOUND); 566 assertThat(actualResult.getErrorMessage()) 567 .contains("Property config 'to' not found for key"); 568 } 569 570 @Test testSchemaMigration_downgrade()571 public void testSchemaMigration_downgrade() throws Exception { 572 AppSearchSchema schema = 573 new AppSearchSchema.Builder("testSchema") 574 .addProperty( 575 new AppSearchSchema.StringPropertyConfig.Builder("subject") 576 .setCardinality( 577 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 578 .setIndexingType( 579 AppSearchSchema.StringPropertyConfig 580 .INDEXING_TYPE_PREFIXES) 581 .setTokenizerType( 582 AppSearchSchema.StringPropertyConfig 583 .TOKENIZER_TYPE_PLAIN) 584 .build()) 585 .addProperty( 586 new AppSearchSchema.StringPropertyConfig.Builder("To") 587 .setCardinality( 588 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 589 .setIndexingType( 590 AppSearchSchema.StringPropertyConfig 591 .INDEXING_TYPE_PREFIXES) 592 .setTokenizerType( 593 AppSearchSchema.StringPropertyConfig 594 .TOKENIZER_TYPE_PLAIN) 595 .build()) 596 .build(); 597 mDb.setSchemaAsync( 598 new SetSchemaRequest.Builder() 599 .addSchemas(schema) 600 .setForceOverride(true) 601 .setVersion(3) 602 .build()) 603 .get(); 604 605 GenericDocument doc1 = 606 new GenericDocument.Builder<>("namespace", "id1", "testSchema") 607 .setPropertyString("subject", "testPut example1") 608 .setPropertyString("To", "testTo example1") 609 .build(); 610 611 AppSearchBatchResult<String, Void> result = 612 checkIsBatchResultSuccess( 613 mDb.putAsync( 614 new PutDocumentsRequest.Builder() 615 .addGenericDocuments(doc1) 616 .build())); 617 assertThat(result.getSuccesses()).containsExactly("id1", null); 618 assertThat(result.getFailures()).isEmpty(); 619 620 // create new schema type and upgrade the version number 621 AppSearchSchema newSchema = 622 new AppSearchSchema.Builder("testSchema") 623 .addProperty( 624 new AppSearchSchema.StringPropertyConfig.Builder("subject") 625 .setCardinality( 626 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 627 .setIndexingType( 628 AppSearchSchema.StringPropertyConfig 629 .INDEXING_TYPE_PREFIXES) 630 .setTokenizerType( 631 AppSearchSchema.StringPropertyConfig 632 .TOKENIZER_TYPE_PLAIN) 633 .build()) 634 .build(); 635 636 // set the new schema to AppSearch 637 Migrator migrator = 638 new Migrator() { 639 @Override 640 public boolean shouldMigrate(int currentVersion, int finalVersion) { 641 return currentVersion != finalVersion; 642 } 643 644 @NonNull 645 @Override 646 public GenericDocument onUpgrade( 647 int currentVersion, 648 int finalVersion, 649 @NonNull GenericDocument document) { 650 throw new IllegalStateException( 651 "Upgrade should not be triggered for this test"); 652 } 653 654 @NonNull 655 @Override 656 public GenericDocument onDowngrade( 657 int currentVersion, 658 int finalVersion, 659 @NonNull GenericDocument document) { 660 return new GenericDocument.Builder<>( 661 document.getNamespace(), 662 document.getId(), 663 document.getSchemaType()) 664 .setPropertyString("subject", "testPut example1 migrated") 665 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 666 .build(); 667 } 668 }; 669 670 SetSchemaResponse setSchemaResponse = 671 mDb.setSchemaAsync( 672 new SetSchemaRequest.Builder() 673 .addSchemas(newSchema) 674 .setMigrator("testSchema", migrator) 675 .setVersion(1) // downgrade version 676 .build()) 677 .get(); 678 679 // Check the schema has been saved 680 assertThat(mDb.getSchemaAsync().get().getSchemas()).containsExactly(newSchema); 681 682 assertThat(setSchemaResponse.getDeletedTypes()).isEmpty(); 683 assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("testSchema"); 684 assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("testSchema"); 685 686 // Check migrate is success 687 GenericDocument expected = 688 new GenericDocument.Builder<>("namespace", "id1", "testSchema") 689 .setPropertyString("subject", "testPut example1 migrated") 690 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 691 .build(); 692 assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected); 693 } 694 695 @Test testSchemaMigration_sameVersion()696 public void testSchemaMigration_sameVersion() throws Exception { 697 AppSearchSchema schema = 698 new AppSearchSchema.Builder("testSchema") 699 .addProperty( 700 new AppSearchSchema.StringPropertyConfig.Builder("subject") 701 .setCardinality( 702 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 703 .setIndexingType( 704 AppSearchSchema.StringPropertyConfig 705 .INDEXING_TYPE_PREFIXES) 706 .setTokenizerType( 707 AppSearchSchema.StringPropertyConfig 708 .TOKENIZER_TYPE_PLAIN) 709 .build()) 710 .addProperty( 711 new AppSearchSchema.StringPropertyConfig.Builder("To") 712 .setCardinality( 713 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 714 .setIndexingType( 715 AppSearchSchema.StringPropertyConfig 716 .INDEXING_TYPE_PREFIXES) 717 .setTokenizerType( 718 AppSearchSchema.StringPropertyConfig 719 .TOKENIZER_TYPE_PLAIN) 720 .build()) 721 .build(); 722 mDb.setSchemaAsync( 723 new SetSchemaRequest.Builder() 724 .addSchemas(schema) 725 .setForceOverride(true) 726 .setVersion(3) 727 .build()) 728 .get(); 729 730 GenericDocument doc1 = 731 new GenericDocument.Builder<>("namespace", "id1", "testSchema") 732 .setPropertyString("subject", "testPut example1") 733 .setPropertyString("To", "testTo example1") 734 .build(); 735 736 AppSearchBatchResult<String, Void> result = 737 checkIsBatchResultSuccess( 738 mDb.putAsync( 739 new PutDocumentsRequest.Builder() 740 .addGenericDocuments(doc1) 741 .build())); 742 assertThat(result.getSuccesses()).containsExactly("id1", null); 743 assertThat(result.getFailures()).isEmpty(); 744 745 // create new schema type with the same version number 746 AppSearchSchema newSchema = 747 new AppSearchSchema.Builder("testSchema") 748 .addProperty( 749 new AppSearchSchema.StringPropertyConfig.Builder("subject") 750 .setCardinality( 751 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 752 .setIndexingType( 753 AppSearchSchema.StringPropertyConfig 754 .INDEXING_TYPE_PREFIXES) 755 .setTokenizerType( 756 AppSearchSchema.StringPropertyConfig 757 .TOKENIZER_TYPE_PLAIN) 758 .build()) 759 .build(); 760 761 // set the new schema to AppSearch 762 Migrator migrator = 763 new Migrator() { 764 765 @Override 766 public boolean shouldMigrate(int currentVersion, int finalVersion) { 767 return currentVersion != finalVersion; 768 } 769 770 @NonNull 771 @Override 772 public GenericDocument onUpgrade( 773 int currentVersion, 774 int finalVersion, 775 @NonNull GenericDocument document) { 776 throw new IllegalStateException( 777 "Upgrade should not be triggered for this test"); 778 } 779 780 @NonNull 781 @Override 782 public GenericDocument onDowngrade( 783 int currentVersion, 784 int finalVersion, 785 @NonNull GenericDocument document) { 786 throw new IllegalStateException( 787 "Downgrade should not be triggered for this test"); 788 } 789 }; 790 791 // SetSchema with forceOverride=false 792 ExecutionException exception = 793 assertThrows( 794 ExecutionException.class, 795 () -> 796 mDb.setSchemaAsync( 797 new SetSchemaRequest.Builder() 798 .addSchemas(newSchema) 799 .setMigrator("testSchema", migrator) 800 .setVersion(3) // same version 801 .build()) 802 .get()); 803 assertThat(exception).hasMessageThat().contains("Schema is incompatible."); 804 805 // SetSchema with forceOverride=true 806 SetSchemaResponse setSchemaResponse = 807 mDb.setSchemaAsync( 808 new SetSchemaRequest.Builder() 809 .addSchemas(newSchema) 810 .setMigrator("testSchema", migrator) 811 .setVersion(3) // same version 812 .setForceOverride(true) 813 .build()) 814 .get(); 815 816 assertThat(mDb.getSchemaAsync().get().getSchemas()).containsExactly(newSchema); 817 818 assertThat(setSchemaResponse.getDeletedTypes()).isEmpty(); 819 assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("testSchema"); 820 assertThat(setSchemaResponse.getMigratedTypes()).isEmpty(); 821 } 822 823 @Test testSchemaMigration_noMigration()824 public void testSchemaMigration_noMigration() throws Exception { 825 AppSearchSchema schema = 826 new AppSearchSchema.Builder("testSchema") 827 .addProperty( 828 new AppSearchSchema.StringPropertyConfig.Builder("subject") 829 .setCardinality( 830 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 831 .setIndexingType( 832 AppSearchSchema.StringPropertyConfig 833 .INDEXING_TYPE_PREFIXES) 834 .setTokenizerType( 835 AppSearchSchema.StringPropertyConfig 836 .TOKENIZER_TYPE_PLAIN) 837 .build()) 838 .addProperty( 839 new AppSearchSchema.StringPropertyConfig.Builder("To") 840 .setCardinality( 841 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 842 .setIndexingType( 843 AppSearchSchema.StringPropertyConfig 844 .INDEXING_TYPE_PREFIXES) 845 .setTokenizerType( 846 AppSearchSchema.StringPropertyConfig 847 .TOKENIZER_TYPE_PLAIN) 848 .build()) 849 .build(); 850 mDb.setSchemaAsync( 851 new SetSchemaRequest.Builder() 852 .addSchemas(schema) 853 .setForceOverride(true) 854 .setVersion(2) 855 .build()) 856 .get(); 857 858 GenericDocument doc1 = 859 new GenericDocument.Builder<>("namespace", "id1", "testSchema") 860 .setPropertyString("subject", "testPut example1") 861 .setPropertyString("To", "testTo example1") 862 .build(); 863 864 AppSearchBatchResult<String, Void> result = 865 checkIsBatchResultSuccess( 866 mDb.putAsync( 867 new PutDocumentsRequest.Builder() 868 .addGenericDocuments(doc1) 869 .build())); 870 assertThat(result.getSuccesses()).containsExactly("id1", null); 871 assertThat(result.getFailures()).isEmpty(); 872 873 // create new schema type and upgrade the version number 874 AppSearchSchema newSchema = 875 new AppSearchSchema.Builder("testSchema") 876 .addProperty( 877 new AppSearchSchema.StringPropertyConfig.Builder("subject") 878 .setCardinality( 879 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) 880 .setIndexingType( 881 AppSearchSchema.StringPropertyConfig 882 .INDEXING_TYPE_PREFIXES) 883 .setTokenizerType( 884 AppSearchSchema.StringPropertyConfig 885 .TOKENIZER_TYPE_PLAIN) 886 .build()) 887 .build(); 888 889 // Set start version to be 3 means we won't trigger migration for 2. 890 Migrator migrator = 891 new Migrator() { 892 893 @Override 894 public boolean shouldMigrate(int currentVersion, int finalVersion) { 895 return currentVersion > 2 && currentVersion != finalVersion; 896 } 897 898 @NonNull 899 @Override 900 public GenericDocument onUpgrade( 901 int currentVersion, 902 int finalVersion, 903 @NonNull GenericDocument document) { 904 throw new IllegalStateException( 905 "Upgrade should not be triggered for this test"); 906 } 907 908 @NonNull 909 @Override 910 public GenericDocument onDowngrade( 911 int currentVersion, 912 int finalVersion, 913 @NonNull GenericDocument document) { 914 throw new IllegalStateException( 915 "Downgrade should not be triggered for this test"); 916 } 917 }; 918 919 // SetSchema with forceOverride=false 920 ExecutionException exception = 921 assertThrows( 922 ExecutionException.class, 923 () -> 924 mDb.setSchemaAsync( 925 new SetSchemaRequest.Builder() 926 .addSchemas(newSchema) 927 .setMigrator("testSchema", migrator) 928 .setVersion(4) // upgrade version 929 .build()) 930 .get()); 931 assertThat(exception).hasMessageThat().contains("Schema is incompatible."); 932 } 933 934 @Test testSchemaMigration_sourceToNowhere()935 public void testSchemaMigration_sourceToNowhere() throws Exception { 936 // set the source schema to AppSearch 937 AppSearchSchema schema = new AppSearchSchema.Builder("sourceSchema").build(); 938 mDb.setSchemaAsync( 939 new SetSchemaRequest.Builder() 940 .addSchemas(schema) 941 .setForceOverride(true) 942 .build()) 943 .get(); 944 945 // save a doc to the source type 946 GenericDocument doc = 947 new GenericDocument.Builder<>("namespace", "id1", "sourceSchema") 948 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 949 .build(); 950 AppSearchBatchResult<String, Void> result = 951 checkIsBatchResultSuccess( 952 mDb.putAsync( 953 new PutDocumentsRequest.Builder() 954 .addGenericDocuments(doc) 955 .build())); 956 assertThat(result.getSuccesses()).containsExactly("id1", null); 957 assertThat(result.getFailures()).isEmpty(); 958 959 Migrator migratorSourceToNowhere = 960 new Migrator() { 961 @Override 962 public boolean shouldMigrate(int currentVersion, int finalVersion) { 963 return true; 964 } 965 966 @NonNull 967 @Override 968 public GenericDocument onUpgrade( 969 int currentVersion, 970 int finalVersion, 971 @NonNull GenericDocument document) { 972 return new GenericDocument.Builder<>( 973 "zombieNamespace", "zombieId", "nonExistSchema") 974 .build(); 975 } 976 977 @NonNull 978 @Override 979 public GenericDocument onDowngrade( 980 int currentVersion, 981 int finalVersion, 982 @NonNull GenericDocument document) { 983 return document; 984 } 985 }; 986 987 // SetSchema with forceOverride=false 988 // Source type exist, destination type doesn't exist. 989 ExecutionException exception = 990 assertThrows( 991 ExecutionException.class, 992 () -> 993 mDb.setSchemaAsync( 994 new SetSchemaRequest.Builder() 995 .addSchemas( 996 new AppSearchSchema.Builder( 997 "emptySchema") 998 .build()) 999 .setMigrator( 1000 "sourceSchema", 1001 migratorSourceToNowhere) 1002 .setVersion(2) 1003 .build()) // upgrade version 1004 .get()); 1005 assertThat(exception) 1006 .hasMessageThat() 1007 .contains( 1008 "Receive a migrated document with schema type: nonExistSchema. " 1009 + "But the schema types doesn't exist in the request"); 1010 1011 // SetSchema with forceOverride=true 1012 // Source type exist, destination type doesn't exist. 1013 exception = 1014 assertThrows( 1015 ExecutionException.class, 1016 () -> 1017 mDb.setSchemaAsync( 1018 new SetSchemaRequest.Builder() 1019 .addSchemas( 1020 new AppSearchSchema.Builder( 1021 "emptySchema") 1022 .build()) 1023 .setMigrator( 1024 "sourceSchema", 1025 migratorSourceToNowhere) 1026 .setForceOverride(true) 1027 .setVersion(2) 1028 .build()) // upgrade version 1029 .get()); 1030 assertThat(exception) 1031 .hasMessageThat() 1032 .contains( 1033 "Receive a migrated document with schema type: nonExistSchema. " 1034 + "But the schema types doesn't exist in the request"); 1035 } 1036 1037 @Test testSchemaMigration_nowhereToDestination()1038 public void testSchemaMigration_nowhereToDestination() throws Exception { 1039 // set the destination schema to AppSearch 1040 AppSearchSchema destinationSchema = 1041 new AppSearchSchema.Builder("destinationSchema").build(); 1042 mDb.setSchemaAsync( 1043 new SetSchemaRequest.Builder() 1044 .addSchemas(destinationSchema) 1045 .setForceOverride(true) 1046 .build()) 1047 .get(); 1048 1049 Migrator migratorNowhereToDestination = 1050 new Migrator() { 1051 @Override 1052 public boolean shouldMigrate(int currentVersion, int finalVersion) { 1053 return true; 1054 } 1055 1056 @NonNull 1057 @Override 1058 public GenericDocument onUpgrade( 1059 int currentVersion, 1060 int finalVersion, 1061 @NonNull GenericDocument document) { 1062 return document; 1063 } 1064 1065 @NonNull 1066 @Override 1067 public GenericDocument onDowngrade( 1068 int currentVersion, 1069 int finalVersion, 1070 @NonNull GenericDocument document) { 1071 return document; 1072 } 1073 }; 1074 1075 // Source type doesn't exist, destination type exist. Since source type doesn't exist, 1076 // no matter force override or not, the migrator won't be invoked 1077 // SetSchema with forceOverride=false 1078 SetSchemaResponse setSchemaResponse = 1079 mDb.setSchemaAsync( 1080 new SetSchemaRequest.Builder() 1081 .addSchemas(destinationSchema) 1082 .addSchemas( 1083 new AppSearchSchema.Builder("emptySchema").build()) 1084 .setMigrator("nonExistSchema", migratorNowhereToDestination) 1085 .setVersion(2) // upgrade version 1086 .build()) 1087 .get(); 1088 assertThat(setSchemaResponse.getMigratedTypes()).isEmpty(); 1089 1090 // SetSchema with forceOverride=true 1091 setSchemaResponse = 1092 mDb.setSchemaAsync( 1093 new SetSchemaRequest.Builder() 1094 .addSchemas(destinationSchema) 1095 .addSchemas( 1096 new AppSearchSchema.Builder("emptySchema").build()) 1097 .setMigrator("nonExistSchema", migratorNowhereToDestination) 1098 .setVersion(2) // upgrade version 1099 .setForceOverride(true) 1100 .build()) 1101 .get(); 1102 assertThat(setSchemaResponse.getMigratedTypes()).isEmpty(); 1103 } 1104 1105 @Test testSchemaMigration_nowhereToNowhere()1106 public void testSchemaMigration_nowhereToNowhere() throws Exception { 1107 // set empty schema 1108 mDb.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get(); 1109 Migrator migratorNowhereToNowhere = 1110 new Migrator() { 1111 @Override 1112 public boolean shouldMigrate(int currentVersion, int finalVersion) { 1113 return true; 1114 } 1115 1116 @NonNull 1117 @Override 1118 public GenericDocument onUpgrade( 1119 int currentVersion, 1120 int finalVersion, 1121 @NonNull GenericDocument document) { 1122 return document; 1123 } 1124 1125 @NonNull 1126 @Override 1127 public GenericDocument onDowngrade( 1128 int currentVersion, 1129 int finalVersion, 1130 @NonNull GenericDocument document) { 1131 return document; 1132 } 1133 }; 1134 1135 // Source type doesn't exist, destination type exist. Since source type doesn't exist, 1136 // no matter force override or not, the migrator won't be invoked 1137 // SetSchema with forceOverride=false 1138 SetSchemaResponse setSchemaResponse = 1139 mDb.setSchemaAsync( 1140 new SetSchemaRequest.Builder() 1141 .addSchemas( 1142 new AppSearchSchema.Builder("emptySchema").build()) 1143 .setMigrator("nonExistSchema", migratorNowhereToNowhere) 1144 .setVersion(2) // upgrade version 1145 .build()) 1146 .get(); 1147 assertThat(setSchemaResponse.getMigratedTypes()).isEmpty(); 1148 1149 // SetSchema with forceOverride=true 1150 setSchemaResponse = 1151 mDb.setSchemaAsync( 1152 new SetSchemaRequest.Builder() 1153 .addSchemas( 1154 new AppSearchSchema.Builder("emptySchema").build()) 1155 .setMigrator("nonExistSchema", migratorNowhereToNowhere) 1156 .setVersion(2) // upgrade version 1157 .setForceOverride(true) 1158 .build()) 1159 .get(); 1160 assertThat(setSchemaResponse.getMigratedTypes()).isEmpty(); 1161 } 1162 1163 @Test testSchemaMigration_toAnotherType()1164 public void testSchemaMigration_toAnotherType() throws Exception { 1165 // set the source schema to AppSearch 1166 AppSearchSchema sourceSchema = new AppSearchSchema.Builder("sourceSchema").build(); 1167 mDb.setSchemaAsync( 1168 new SetSchemaRequest.Builder() 1169 .addSchemas(sourceSchema) 1170 .setForceOverride(true) 1171 .build()) 1172 .get(); 1173 1174 // save a doc to the source type 1175 GenericDocument doc = 1176 new GenericDocument.Builder<>("namespace", "id1", "sourceSchema").build(); 1177 AppSearchBatchResult<String, Void> result = 1178 checkIsBatchResultSuccess( 1179 mDb.putAsync( 1180 new PutDocumentsRequest.Builder() 1181 .addGenericDocuments(doc) 1182 .build())); 1183 assertThat(result.getSuccesses()).containsExactly("id1", null); 1184 assertThat(result.getFailures()).isEmpty(); 1185 1186 // create the destination type and migrator 1187 AppSearchSchema destinationSchema = 1188 new AppSearchSchema.Builder("destinationSchema").build(); 1189 Migrator migrator = 1190 new Migrator() { 1191 @Override 1192 public boolean shouldMigrate(int currentVersion, int finalVersion) { 1193 return true; 1194 } 1195 1196 @NonNull 1197 @Override 1198 public GenericDocument onUpgrade( 1199 int currentVersion, 1200 int finalVersion, 1201 @NonNull GenericDocument document) { 1202 return new GenericDocument.Builder<>( 1203 "namespace", document.getId(), "destinationSchema") 1204 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1205 .build(); 1206 } 1207 1208 @NonNull 1209 @Override 1210 public GenericDocument onDowngrade( 1211 int currentVersion, 1212 int finalVersion, 1213 @NonNull GenericDocument document) { 1214 return document; 1215 } 1216 }; 1217 1218 // SetSchema with forceOverride=false and increase overall version 1219 SetSchemaResponse setSchemaResponse = 1220 mDb.setSchemaAsync( 1221 new SetSchemaRequest.Builder() 1222 .addSchemas(destinationSchema) 1223 .setMigrator("sourceSchema", migrator) 1224 .setForceOverride(false) 1225 .setVersion(2) // upgrade version 1226 .build()) 1227 .get(); 1228 assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("sourceSchema"); 1229 assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty(); 1230 assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("sourceSchema"); 1231 1232 // Check successfully migrate the doc to the destination type 1233 GenericDocument expected = 1234 new GenericDocument.Builder<>("namespace", "id1", "destinationSchema") 1235 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1236 .build(); 1237 assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected); 1238 } 1239 1240 @Test testSchemaMigration_toMultipleDestinationType()1241 public void testSchemaMigration_toMultipleDestinationType() throws Exception { 1242 // set the source schema to AppSearch 1243 AppSearchSchema sourceSchema = 1244 new AppSearchSchema.Builder("Person") 1245 .addProperty( 1246 new AppSearchSchema.LongPropertyConfig.Builder("Age") 1247 .setCardinality( 1248 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1249 .build()) 1250 .build(); 1251 mDb.setSchemaAsync( 1252 new SetSchemaRequest.Builder() 1253 .addSchemas(sourceSchema) 1254 .setForceOverride(true) 1255 .build()) 1256 .get(); 1257 1258 // save a child and an adult to the Person type 1259 GenericDocument childDoc = 1260 new GenericDocument.Builder<>("namespace", "Person1", "Person") 1261 .setPropertyLong("Age", 6) 1262 .build(); 1263 GenericDocument adultDoc = 1264 new GenericDocument.Builder<>("namespace", "Person2", "Person") 1265 .setPropertyLong("Age", 36) 1266 .build(); 1267 AppSearchBatchResult<String, Void> result = 1268 checkIsBatchResultSuccess( 1269 mDb.putAsync( 1270 new PutDocumentsRequest.Builder() 1271 .addGenericDocuments(childDoc, adultDoc) 1272 .build())); 1273 assertThat(result.getSuccesses()).containsExactly("Person1", null, "Person2", null); 1274 assertThat(result.getFailures()).isEmpty(); 1275 1276 // create the migrator 1277 Migrator migrator = 1278 new Migrator() { 1279 @Override 1280 public boolean shouldMigrate(int currentVersion, int finalVersion) { 1281 return true; 1282 } 1283 1284 @NonNull 1285 @Override 1286 public GenericDocument onUpgrade( 1287 int currentVersion, 1288 int finalVersion, 1289 @NonNull GenericDocument document) { 1290 if (document.getPropertyLong("Age") < 21) { 1291 return new GenericDocument.Builder<>("namespace", "child-id", "Child") 1292 .setPropertyLong("Age", document.getPropertyLong("Age")) 1293 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1294 .build(); 1295 } else { 1296 return new GenericDocument.Builder<>("namespace", "adult-id", "Adult") 1297 .setPropertyLong("Age", document.getPropertyLong("Age")) 1298 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1299 .build(); 1300 } 1301 } 1302 1303 @NonNull 1304 @Override 1305 public GenericDocument onDowngrade( 1306 int currentVersion, 1307 int finalVersion, 1308 @NonNull GenericDocument document) { 1309 return document; 1310 } 1311 }; 1312 1313 // create adult and child schema 1314 AppSearchSchema adultSchema = 1315 new AppSearchSchema.Builder("Adult") 1316 .addProperty( 1317 new AppSearchSchema.LongPropertyConfig.Builder("Age") 1318 .setCardinality( 1319 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1320 .build()) 1321 .build(); 1322 AppSearchSchema childSchema = 1323 new AppSearchSchema.Builder("Child") 1324 .addProperty( 1325 new AppSearchSchema.LongPropertyConfig.Builder("Age") 1326 .setCardinality( 1327 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1328 .build()) 1329 .build(); 1330 1331 // SetSchema with forceOverride=false and increase overall version 1332 SetSchemaResponse setSchemaResponse = 1333 mDb.setSchemaAsync( 1334 new SetSchemaRequest.Builder() 1335 .addSchemas(adultSchema, childSchema) 1336 .setMigrator("Person", migrator) 1337 .setForceOverride(false) 1338 .setVersion(2) // upgrade version 1339 .build()) 1340 .get(); 1341 assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("Person"); 1342 assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty(); 1343 assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("Person"); 1344 1345 // Check successfully migrate the child doc 1346 GenericDocument expectedInChild = 1347 new GenericDocument.Builder<>("namespace", "child-id", "Child") 1348 .setPropertyLong("Age", 6) 1349 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1350 .build(); 1351 assertThat(doGet(mDb, "namespace", "child-id")).containsExactly(expectedInChild); 1352 1353 // Check successfully migrate the adult doc 1354 GenericDocument expectedInAdult = 1355 new GenericDocument.Builder<>("namespace", "adult-id", "Adult") 1356 .setPropertyLong("Age", 36) 1357 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1358 .build(); 1359 assertThat(doGet(mDb, "namespace", "adult-id")).containsExactly(expectedInAdult); 1360 } 1361 1362 @Test testSchemaMigration_loadTest()1363 public void testSchemaMigration_loadTest() throws Exception { 1364 // set the two source type A & B to AppSearch 1365 AppSearchSchema sourceSchemaA = 1366 new AppSearchSchema.Builder("schemaA") 1367 .addProperty( 1368 new AppSearchSchema.LongPropertyConfig.Builder("num") 1369 .setCardinality( 1370 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1371 .build()) 1372 .build(); 1373 AppSearchSchema sourceSchemaB = 1374 new AppSearchSchema.Builder("schemaB") 1375 .addProperty( 1376 new AppSearchSchema.LongPropertyConfig.Builder("num") 1377 .setCardinality( 1378 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1379 .build()) 1380 .build(); 1381 mDb.setSchemaAsync( 1382 new SetSchemaRequest.Builder() 1383 .addSchemas(sourceSchemaA, sourceSchemaB) 1384 .setForceOverride(true) 1385 .build()) 1386 .get(); 1387 1388 // save 100 docs to each type 1389 PutDocumentsRequest.Builder putRequestBuilder = new PutDocumentsRequest.Builder(); 1390 for (int i = 0; i < 100; i++) { 1391 GenericDocument docInA = 1392 new GenericDocument.Builder<>("namespace", "idA-" + i, "schemaA") 1393 .setPropertyLong("num", i) 1394 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1395 .build(); 1396 GenericDocument docInB = 1397 new GenericDocument.Builder<>("namespace", "idB-" + i, "schemaB") 1398 .setPropertyLong("num", i) 1399 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1400 .build(); 1401 putRequestBuilder.addGenericDocuments(docInA, docInB); 1402 } 1403 AppSearchBatchResult<String, Void> result = 1404 checkIsBatchResultSuccess(mDb.putAsync(putRequestBuilder.build())); 1405 assertThat(result.getFailures()).isEmpty(); 1406 1407 // create three destination types B, C & D 1408 AppSearchSchema destinationSchemaB = 1409 new AppSearchSchema.Builder("schemaB") 1410 .addProperty( 1411 new AppSearchSchema.LongPropertyConfig.Builder("numNewProperty") 1412 .setCardinality( 1413 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1414 .build()) 1415 .build(); 1416 AppSearchSchema destinationSchemaC = 1417 new AppSearchSchema.Builder("schemaC") 1418 .addProperty( 1419 new AppSearchSchema.LongPropertyConfig.Builder("num") 1420 .setCardinality( 1421 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1422 .build()) 1423 .build(); 1424 AppSearchSchema destinationSchemaD = 1425 new AppSearchSchema.Builder("schemaD") 1426 .addProperty( 1427 new AppSearchSchema.LongPropertyConfig.Builder("num") 1428 .setCardinality( 1429 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1430 .build()) 1431 .build(); 1432 1433 // Create an active migrator for type A which will migrate first 50 docs to C and second 1434 // 50 docs to D 1435 Migrator migratorA = 1436 new Migrator() { 1437 @Override 1438 public boolean shouldMigrate(int currentVersion, int finalVersion) { 1439 return true; 1440 } 1441 1442 @NonNull 1443 @Override 1444 public GenericDocument onUpgrade( 1445 int currentVersion, 1446 int finalVersion, 1447 @NonNull GenericDocument document) { 1448 if (document.getPropertyLong("num") < 50) { 1449 return new GenericDocument.Builder<>( 1450 "namespace", document.getId() + "-destC", "schemaC") 1451 .setPropertyLong("num", document.getPropertyLong("num")) 1452 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1453 .build(); 1454 } else { 1455 return new GenericDocument.Builder<>( 1456 "namespace", document.getId() + "-destD", "schemaD") 1457 .setPropertyLong("num", document.getPropertyLong("num")) 1458 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1459 .build(); 1460 } 1461 } 1462 1463 @NonNull 1464 @Override 1465 public GenericDocument onDowngrade( 1466 int currentVersion, 1467 int finalVersion, 1468 @NonNull GenericDocument document) { 1469 return document; 1470 } 1471 }; 1472 1473 // Create an active migrator for type B which will migrate first 50 docs to B and second 1474 // 50 docs to D 1475 Migrator migratorB = 1476 new Migrator() { 1477 @Override 1478 public boolean shouldMigrate(int currentVersion, int finalVersion) { 1479 return true; 1480 } 1481 1482 @NonNull 1483 @Override 1484 public GenericDocument onUpgrade( 1485 int currentVersion, 1486 int finalVersion, 1487 @NonNull GenericDocument document) { 1488 if (document.getPropertyLong("num") < 50) { 1489 return new GenericDocument.Builder<>( 1490 "namespace", document.getId() + "-destB", "schemaB") 1491 .setPropertyLong( 1492 "numNewProperty", document.getPropertyLong("num")) 1493 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1494 .build(); 1495 } else { 1496 return new GenericDocument.Builder<>( 1497 "namespace", document.getId() + "-destD", "schemaD") 1498 .setPropertyLong("num", document.getPropertyLong("num")) 1499 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1500 .build(); 1501 } 1502 } 1503 1504 @NonNull 1505 @Override 1506 public GenericDocument onDowngrade( 1507 int currentVersion, 1508 int finalVersion, 1509 @NonNull GenericDocument document) { 1510 return document; 1511 } 1512 }; 1513 1514 // SetSchema with forceOverride=false and increase overall version 1515 SetSchemaResponse setSchemaResponse = 1516 mDb.setSchemaAsync( 1517 new SetSchemaRequest.Builder() 1518 .addSchemas( 1519 destinationSchemaB, 1520 destinationSchemaC, 1521 destinationSchemaD) 1522 .setMigrator("schemaA", migratorA) 1523 .setMigrator("schemaB", migratorB) 1524 .setForceOverride(false) 1525 .setVersion(2) // upgrade version 1526 .build()) 1527 .get(); 1528 assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("schemaA"); 1529 assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("schemaB"); 1530 assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("schemaA", "schemaB"); 1531 1532 // generate expected documents 1533 List<GenericDocument> expectedDocs = new ArrayList<>(); 1534 for (int i = 0; i < 50; i++) { 1535 GenericDocument docAToC = 1536 new GenericDocument.Builder<>("namespace", "idA-" + i + "-destC", "schemaC") 1537 .setPropertyLong("num", i) 1538 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1539 .build(); 1540 GenericDocument docBToB = 1541 new GenericDocument.Builder<>("namespace", "idB-" + i + "-destB", "schemaB") 1542 .setPropertyLong("numNewProperty", i) 1543 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1544 .build(); 1545 expectedDocs.add(docAToC); 1546 expectedDocs.add(docBToB); 1547 } 1548 1549 for (int i = 50; i < 100; i++) { 1550 GenericDocument docAToD = 1551 new GenericDocument.Builder<>("namespace", "idA-" + i + "-destD", "schemaD") 1552 .setPropertyLong("num", i) 1553 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1554 .build(); 1555 GenericDocument docBToD = 1556 new GenericDocument.Builder<>("namespace", "idB-" + i + "-destD", "schemaD") 1557 .setPropertyLong("num", i) 1558 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1559 .build(); 1560 expectedDocs.add(docAToD); 1561 expectedDocs.add(docBToD); 1562 } 1563 // query all documents and compare 1564 SearchResultsShim searchResults = 1565 mDb.search( 1566 "", 1567 new SearchSpec.Builder() 1568 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) 1569 .build()); 1570 List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults); 1571 assertThat(documents).containsExactlyElementsIn(expectedDocs); 1572 } 1573 1574 // *************************** Multi-step migration tests ****************************** 1575 // Version structure and how version bumps: 1576 // Version 1: Start - typeA docs contains "subject" property. 1577 // Version 2: typeA docs get new "body" property, contains "subject" and "body" now. 1578 // Version 3: typeA docs is migrated to typeB, typeA docs got removed, typeB doc contains 1579 // "subject" and "body" property. 1580 // Version 4: typeB docs remove "subject" property, contains only "body" now. 1581 1582 // Create a multi-step migrator for A, which could migrate version 1-3 to 4. 1583 private static final Migrator MULTI_STEP_MIGRATOR_A = 1584 new Migrator() { 1585 @Override 1586 public boolean shouldMigrate(int currentVersion, int finalVersion) { 1587 return currentVersion < 3; 1588 } 1589 1590 @NonNull 1591 @Override 1592 public GenericDocument onUpgrade( 1593 int currentVersion, int finalVersion, @NonNull GenericDocument document) { 1594 GenericDocument.Builder<?> docBuilder = 1595 new GenericDocument.Builder<>("namespace", "id", "TypeB") 1596 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME); 1597 if (currentVersion == 2) { 1598 docBuilder.setPropertyString("body", document.getPropertyString("body")); 1599 } else { 1600 docBuilder.setPropertyString( 1601 "body", "new content for the newly added 'body' property"); 1602 } 1603 return docBuilder.build(); 1604 } 1605 1606 @NonNull 1607 @Override 1608 public GenericDocument onDowngrade( 1609 int currentVersion, int finalVersion, @NonNull GenericDocument document) { 1610 return document; 1611 } 1612 }; 1613 1614 // create a multi-step migrator for B, which could migrate version 1-3 to 4. 1615 private static final Migrator MULTI_STEP_MIGRATOR_B = 1616 new Migrator() { 1617 @Override 1618 public boolean shouldMigrate(int currentVersion, int finalVersion) { 1619 return currentVersion == 3; 1620 } 1621 1622 @NonNull 1623 @Override 1624 public GenericDocument onUpgrade( 1625 int currentVersion, int finalVersion, @NonNull GenericDocument document) { 1626 return new GenericDocument.Builder<>("namespace", "id", "TypeB") 1627 .setPropertyString("body", document.getPropertyString("body")) 1628 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1629 .build(); 1630 } 1631 1632 @NonNull 1633 @Override 1634 public GenericDocument onDowngrade( 1635 int currentVersion, int finalVersion, @NonNull GenericDocument document) { 1636 return document; 1637 } 1638 }; 1639 1640 // create a setSchemaRequest, which could migrate version 1-3 to 4. 1641 private static final SetSchemaRequest MULTI_STEP_REQUEST = 1642 new SetSchemaRequest.Builder() 1643 .addSchemas( 1644 new AppSearchSchema.Builder("TypeB") 1645 .addProperty( 1646 new AppSearchSchema.StringPropertyConfig.Builder("body") 1647 .setCardinality( 1648 AppSearchSchema.PropertyConfig 1649 .CARDINALITY_REQUIRED) 1650 .setIndexingType( 1651 AppSearchSchema.StringPropertyConfig 1652 .INDEXING_TYPE_PREFIXES) 1653 .setTokenizerType( 1654 AppSearchSchema.StringPropertyConfig 1655 .TOKENIZER_TYPE_PLAIN) 1656 .build()) 1657 .build()) 1658 .setMigrator("TypeA", MULTI_STEP_MIGRATOR_A) 1659 .setMigrator("TypeB", MULTI_STEP_MIGRATOR_B) 1660 .setVersion(4) 1661 .build(); 1662 1663 @Test testSchemaMigration_multiStep1To4()1664 public void testSchemaMigration_multiStep1To4() throws Exception { 1665 // set version 1 to the database, only contain TypeA 1666 AppSearchSchema typeA = 1667 new AppSearchSchema.Builder("TypeA") 1668 .addProperty( 1669 new AppSearchSchema.StringPropertyConfig.Builder("subject") 1670 .setCardinality( 1671 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1672 .setIndexingType( 1673 AppSearchSchema.StringPropertyConfig 1674 .INDEXING_TYPE_PREFIXES) 1675 .setTokenizerType( 1676 AppSearchSchema.StringPropertyConfig 1677 .TOKENIZER_TYPE_PLAIN) 1678 .build()) 1679 .build(); 1680 mDb.setSchemaAsync( 1681 new SetSchemaRequest.Builder() 1682 .addSchemas(typeA) 1683 .setForceOverride(true) 1684 .setVersion(1) 1685 .build()) 1686 .get(); 1687 1688 // save a doc to version 1. 1689 GenericDocument doc = 1690 new GenericDocument.Builder<>("namespace", "id", "TypeA") 1691 .setPropertyString("subject", "subject") 1692 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1693 .build(); 1694 AppSearchBatchResult<String, Void> result = 1695 checkIsBatchResultSuccess( 1696 mDb.putAsync( 1697 new PutDocumentsRequest.Builder() 1698 .addGenericDocuments(doc) 1699 .build())); 1700 assertThat(result.getSuccesses()).containsExactly("id", null); 1701 assertThat(result.getFailures()).isEmpty(); 1702 1703 // update to version 4. 1704 SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(MULTI_STEP_REQUEST).get(); 1705 assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA"); 1706 assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty(); 1707 assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA"); 1708 1709 // Create expected doc. Since we started at version 1 and migrated to version 4: 1710 // 1: A 'body' property should have been added with "new content for the newly added 'body' 1711 // property" 1712 // 2: The type should have been changed from 'TypeA' to 'TypeB' 1713 // 3: The 'subject' property should have been removed 1714 GenericDocument expected = 1715 new GenericDocument.Builder<>("namespace", "id", "TypeB") 1716 .setPropertyString( 1717 "body", "new content for the newly added 'body' property") 1718 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1719 .build(); 1720 assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected); 1721 } 1722 1723 @Test testSchemaMigration_multiStep2To4()1724 public void testSchemaMigration_multiStep2To4() throws Exception { 1725 // set version 2 to the database, only contain TypeA with a new property 1726 AppSearchSchema typeA = 1727 new AppSearchSchema.Builder("TypeA") 1728 .addProperty( 1729 new AppSearchSchema.StringPropertyConfig.Builder("subject") 1730 .setCardinality( 1731 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1732 .setIndexingType( 1733 AppSearchSchema.StringPropertyConfig 1734 .INDEXING_TYPE_PREFIXES) 1735 .setTokenizerType( 1736 AppSearchSchema.StringPropertyConfig 1737 .TOKENIZER_TYPE_PLAIN) 1738 .build()) 1739 .addProperty( 1740 new AppSearchSchema.StringPropertyConfig.Builder("body") 1741 .setCardinality( 1742 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1743 .setIndexingType( 1744 AppSearchSchema.StringPropertyConfig 1745 .INDEXING_TYPE_PREFIXES) 1746 .setTokenizerType( 1747 AppSearchSchema.StringPropertyConfig 1748 .TOKENIZER_TYPE_PLAIN) 1749 .build()) 1750 .build(); 1751 mDb.setSchemaAsync( 1752 new SetSchemaRequest.Builder() 1753 .addSchemas(typeA) 1754 .setForceOverride(true) 1755 .setVersion(2) 1756 .build()) 1757 .get(); 1758 1759 // save a doc to version 2. 1760 GenericDocument doc = 1761 new GenericDocument.Builder<>("namespace", "id", "TypeA") 1762 .setPropertyString("subject", "subject") 1763 .setPropertyString("body", "bodyFromA") 1764 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1765 .build(); 1766 AppSearchBatchResult<String, Void> result = 1767 checkIsBatchResultSuccess( 1768 mDb.putAsync( 1769 new PutDocumentsRequest.Builder() 1770 .addGenericDocuments(doc) 1771 .build())); 1772 assertThat(result.getSuccesses()).containsExactly("id", null); 1773 assertThat(result.getFailures()).isEmpty(); 1774 1775 // update to version 4. 1776 SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(MULTI_STEP_REQUEST).get(); 1777 assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA"); 1778 assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty(); 1779 assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA"); 1780 1781 // create expected doc, body exists in type A of version 2 1782 GenericDocument expected = 1783 new GenericDocument.Builder<>("namespace", "id", "TypeB") 1784 .setPropertyString("body", "bodyFromA") 1785 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1786 .build(); 1787 assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected); 1788 } 1789 1790 @Test testSchemaMigration_multiStep3To4()1791 public void testSchemaMigration_multiStep3To4() throws Exception { 1792 // set version 3 to the database, only contain TypeB 1793 AppSearchSchema typeA = 1794 new AppSearchSchema.Builder("TypeB") 1795 .addProperty( 1796 new AppSearchSchema.StringPropertyConfig.Builder("subject") 1797 .setCardinality( 1798 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1799 .setIndexingType( 1800 AppSearchSchema.StringPropertyConfig 1801 .INDEXING_TYPE_PREFIXES) 1802 .setTokenizerType( 1803 AppSearchSchema.StringPropertyConfig 1804 .TOKENIZER_TYPE_PLAIN) 1805 .build()) 1806 .addProperty( 1807 new AppSearchSchema.StringPropertyConfig.Builder("body") 1808 .setCardinality( 1809 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED) 1810 .setIndexingType( 1811 AppSearchSchema.StringPropertyConfig 1812 .INDEXING_TYPE_PREFIXES) 1813 .setTokenizerType( 1814 AppSearchSchema.StringPropertyConfig 1815 .TOKENIZER_TYPE_PLAIN) 1816 .build()) 1817 .build(); 1818 mDb.setSchemaAsync( 1819 new SetSchemaRequest.Builder() 1820 .addSchemas(typeA) 1821 .setForceOverride(true) 1822 .setVersion(3) 1823 .build()) 1824 .get(); 1825 1826 // save a doc to version 2. 1827 GenericDocument doc = 1828 new GenericDocument.Builder<>("namespace", "id", "TypeB") 1829 .setPropertyString("subject", "subject") 1830 .setPropertyString("body", "bodyFromB") 1831 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1832 .build(); 1833 AppSearchBatchResult<String, Void> result = 1834 checkIsBatchResultSuccess( 1835 mDb.putAsync( 1836 new PutDocumentsRequest.Builder() 1837 .addGenericDocuments(doc) 1838 .build())); 1839 assertThat(result.getSuccesses()).containsExactly("id", null); 1840 assertThat(result.getFailures()).isEmpty(); 1841 1842 // update to version 4. 1843 SetSchemaResponse setSchemaResponse = mDb.setSchemaAsync(MULTI_STEP_REQUEST).get(); 1844 assertThat(setSchemaResponse.getDeletedTypes()).isEmpty(); 1845 assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("TypeB"); 1846 assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeB"); 1847 1848 // create expected doc, body exists in type A of version 3 1849 GenericDocument expected = 1850 new GenericDocument.Builder<>("namespace", "id", "TypeB") 1851 .setPropertyString("body", "bodyFromB") 1852 .setCreationTimestampMillis(DOCUMENT_CREATION_TIME) 1853 .build(); 1854 assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected); 1855 } 1856 } 1857