• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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