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