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