blob: a87a1ac617523dbfb6eb263939b4bfe46313eb09 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.appsearch.cts.app;
import static android.app.appsearch.AppSearchResult.RESULT_NOT_FOUND;
import static com.android.server.appsearch.testing.AppSearchTestUtils.checkIsBatchResultSuccess;
import static com.android.server.appsearch.testing.AppSearchTestUtils.convertSearchResultsToDocuments;
import static com.android.server.appsearch.testing.AppSearchTestUtils.doGet;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.AppSearchSessionShim;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.Migrator;
import android.app.appsearch.PutDocumentsRequest;
import android.app.appsearch.SearchResultsShim;
import android.app.appsearch.SearchSpec;
import android.app.appsearch.SetSchemaRequest;
import android.app.appsearch.SetSchemaResponse;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
/*
* For schema migration, we have 4 factors
* A. is ForceOverride set to true?
* B. is the schema change backwards compatible?
* C. is shouldTrigger return true?
* D. is there a migration triggered for each incompatible type and no deleted types?
* If B is true then D could never be false, so that will give us 12 combinations.
*
* Trigger Delete First Second
* A B C D Migration Types SetSchema SetSchema
* TRUE TRUE TRUE TRUE Yes succeeds succeeds(noop)
* TRUE TRUE FALSE TRUE succeeds succeeds(noop)
* TRUE FALSE TRUE TRUE Yes fail succeeds
* TRUE FALSE TRUE FALSE Yes Yes fail succeeds
* TRUE FALSE FALSE TRUE Yes fail succeeds
* TRUE FALSE FALSE FALSE Yes fail succeeds
* FALSE TRUE TRUE TRUE Yes succeeds succeeds(noop)
* FALSE TRUE FALSE TRUE succeeds succeeds(noop)
* FALSE FALSE TRUE TRUE Yes fail succeeds
* FALSE FALSE TRUE FALSE Yes fail throw error
* FALSE FALSE FALSE TRUE Impossible case, migrators are inactivity
* FALSE FALSE FALSE FALSE fail throw error
*/
public abstract class AppSearchSchemaMigrationCtsTestBase {
private static final String DB_NAME = "";
private static final long DOCUMENT_CREATION_TIME = 12345L;
private static final Migrator ACTIVE_NOOP_MIGRATOR =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion, int finalVersion, @NonNull GenericDocument document) {
return document;
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion, int finalVersion, @NonNull GenericDocument document) {
return document;
}
};
private static final Migrator INACTIVE_MIGRATOR =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return false;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion, int finalVersion, @NonNull GenericDocument document) {
return document;
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion, int finalVersion, @NonNull GenericDocument document) {
return document;
}
};
private AppSearchSessionShim mDb;
protected abstract ListenableFuture<AppSearchSessionShim> createSearchSession(
@NonNull String dbName);
@Before
public void setUp() throws Exception {
mDb = createSearchSession(DB_NAME).get();
// Cleanup whatever documents may still exist in these databases. This is needed in
// addition to tearDown in case a test exited without completing properly.
AppSearchSchema schema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(schema)
.setForceOverride(true)
.build())
.get();
GenericDocument doc =
new GenericDocument.Builder<>("namespace", "id0", "testSchema")
.setPropertyString("subject", "testPut example1")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc)
.build()));
assertThat(result.getSuccesses()).containsExactly("id0", null);
assertThat(result.getFailures()).isEmpty();
}
@After
public void tearDown() throws Exception {
// Cleanup whatever documents may still exist in these databases.
mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
}
@Test
public void testSchemaMigration_A_B_C_D() throws Exception {
// create a backwards compatible schema and update the version
AppSearchSchema B_C_Schema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(B_C_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.setVersion(2) // upgrade version
.build())
.get();
}
@Test
public void testSchemaMigration_A_B_NC_D() throws Exception {
// create a backwards compatible schema but don't update the version
AppSearchSchema B_NC_Schema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(B_NC_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.build())
.get();
}
@Test
public void testSchemaMigration_A_NB_C_D() throws Exception {
// create a backwards incompatible schema and update the version
AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema").build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(NB_C_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.setVersion(2) // upgrade version
.build())
.get();
}
@Test
public void testSchemaMigration_A_NB_C_ND() throws Exception {
// create a backwards incompatible schema and update the version
AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema").build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(NB_C_Schema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) // ND
.setForceOverride(true)
.setVersion(2) // upgrade version
.build())
.get();
}
@Test
public void testSchemaMigration_A_NB_NC_D() throws Exception {
// create a backwards incompatible schema but don't update the version
AppSearchSchema NB_NC_Schema = new AppSearchSchema.Builder("testSchema").build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(NB_NC_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.build())
.get();
}
@Test
public void testSchemaMigration_A_NB_NC_ND() throws Exception {
// create a backwards incompatible schema but don't update the version
AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema").build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas($B_$C_Schema)
.setMigrator("testSchema", INACTIVE_MIGRATOR) // ND
.setForceOverride(true)
.build())
.get();
}
@Test
public void testSchemaMigration_NA_B_C_D() throws Exception {
// create a backwards compatible schema and update the version
AppSearchSchema B_C_Schema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(B_C_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setVersion(2) // upgrade version
.build())
.get();
}
@Test
public void testSchemaMigration_NA_B_NC_D() throws Exception {
// create a backwards compatible schema but don't update the version
AppSearchSchema B_NC_Schema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(B_NC_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setForceOverride(true)
.build())
.get();
}
@Test
public void testSchemaMigration_NA_NB_C_D() throws Exception {
// create a backwards incompatible schema and update the version
AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema").build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(NB_C_Schema)
.setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
.setVersion(2) // upgrade version
.build())
.get();
}
@Test
public void testSchemaMigration_NA_NB_C_ND() throws Exception {
// create a backwards incompatible schema and update the version
AppSearchSchema $B_C_Schema = new AppSearchSchema.Builder("testSchema").build();
ExecutionException exception =
assertThrows(
ExecutionException.class,
() ->
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas($B_C_Schema)
.setMigrator(
"testSchema",
INACTIVE_MIGRATOR) // ND
.setVersion(2) // upgrade version
.build())
.get());
assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
}
@Test
public void testSchemaMigration_NA_NB_NC_ND() throws Exception {
// create a backwards incompatible schema but don't update the version
AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema").build();
ExecutionException exception =
assertThrows(
ExecutionException.class,
() ->
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas($B_$C_Schema)
.setMigrator(
"testSchema",
INACTIVE_MIGRATOR) // ND
.build())
.get());
assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
}
@Test
public void testSchemaMigration() throws Exception {
AppSearchSchema schema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("To")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(schema)
.setForceOverride(true)
.build())
.get();
GenericDocument doc1 =
new GenericDocument.Builder<>("namespace", "id1", "testSchema")
.setPropertyString("subject", "testPut example1")
.setPropertyString("To", "testTo example1")
.build();
GenericDocument doc2 =
new GenericDocument.Builder<>("namespace", "id2", "testSchema")
.setPropertyString("subject", "testPut example2")
.setPropertyString("To", "testTo example2")
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc1, doc2)
.build()));
assertThat(result.getSuccesses()).containsExactly("id1", null, "id2", null);
assertThat(result.getFailures()).isEmpty();
// create new schema type and upgrade the version number
AppSearchSchema newSchema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
// set the new schema to AppSearch, the first document will be migrated successfully but the
// second one will be failed.
Migrator migrator =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return currentVersion != finalVersion;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
if (document.getId().equals("id2")) {
return new GenericDocument.Builder<>(
document.getNamespace(),
document.getId(),
document.getSchemaType())
.setPropertyString("subject", "testPut example2")
.setPropertyString(
"to", "Expect to fail, property not in the schema")
.build();
}
return new GenericDocument.Builder<>(
document.getNamespace(),
document.getId(),
document.getSchemaType())
.setPropertyString("subject", "testPut example1 migrated")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
throw new IllegalStateException(
"Downgrade should not be triggered for this test");
}
};
SetSchemaResponse setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(2) // upgrade version
.build())
.get();
// Check the schema has been saved
assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("testSchema");
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("testSchema");
// Check migrate the first document is success
GenericDocument expected =
new GenericDocument.Builder<>("namespace", "id1", "testSchema")
.setPropertyString("subject", "testPut example1 migrated")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected);
// Check migrate the second document is fail.
assertThat(setSchemaResponse.getMigrationFailures()).hasSize(1);
SetSchemaResponse.MigrationFailure migrationFailure =
setSchemaResponse.getMigrationFailures().get(0);
assertThat(migrationFailure.getNamespace()).isEqualTo("namespace");
assertThat(migrationFailure.getSchemaType()).isEqualTo("testSchema");
assertThat(migrationFailure.getDocumentId()).isEqualTo("id2");
AppSearchResult<Void> actualResult = migrationFailure.getAppSearchResult();
assertThat(actualResult.isSuccess()).isFalse();
assertThat(actualResult.getResultCode()).isEqualTo(RESULT_NOT_FOUND);
assertThat(actualResult.getErrorMessage())
.contains("Property config 'to' not found for key");
}
@Test
public void testSchemaMigration_downgrade() throws Exception {
AppSearchSchema schema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("To")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(schema)
.setForceOverride(true)
.setVersion(3)
.build())
.get();
GenericDocument doc1 =
new GenericDocument.Builder<>("namespace", "id1", "testSchema")
.setPropertyString("subject", "testPut example1")
.setPropertyString("To", "testTo example1")
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc1)
.build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
// create new schema type and upgrade the version number
AppSearchSchema newSchema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
// set the new schema to AppSearch
Migrator migrator =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return currentVersion != finalVersion;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
throw new IllegalStateException(
"Upgrade should not be triggered for this test");
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return new GenericDocument.Builder<>(
document.getNamespace(),
document.getId(),
document.getSchemaType())
.setPropertyString("subject", "testPut example1 migrated")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
}
};
SetSchemaResponse setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(1) // downgrade version
.build())
.get();
// Check the schema has been saved
assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("testSchema");
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("testSchema");
// Check migrate is success
GenericDocument expected =
new GenericDocument.Builder<>("namespace", "id1", "testSchema")
.setPropertyString("subject", "testPut example1 migrated")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected);
}
@Test
public void testSchemaMigration_sameVersion() throws Exception {
AppSearchSchema schema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("To")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(schema)
.setForceOverride(true)
.setVersion(3)
.build())
.get();
GenericDocument doc1 =
new GenericDocument.Builder<>("namespace", "id1", "testSchema")
.setPropertyString("subject", "testPut example1")
.setPropertyString("To", "testTo example1")
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc1)
.build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
// create new schema type with the same version number
AppSearchSchema newSchema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
// set the new schema to AppSearch
Migrator migrator =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return currentVersion != finalVersion;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
throw new IllegalStateException(
"Upgrade should not be triggered for this test");
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
throw new IllegalStateException(
"Downgrade should not be triggered for this test");
}
};
// SetSchema with forceOverride=false
ExecutionException exception =
assertThrows(
ExecutionException.class,
() ->
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(3) // same version
.build())
.get());
assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
// SetSchema with forceOverride=true
SetSchemaResponse setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(3) // same version
.setForceOverride(true)
.build())
.get();
assertThat(mDb.getSchema().get().getSchemas()).containsExactly(newSchema);
assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("testSchema");
assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
}
@Test
public void testSchemaMigration_noMigration() throws Exception {
AppSearchSchema schema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("To")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(schema)
.setForceOverride(true)
.setVersion(2)
.build())
.get();
GenericDocument doc1 =
new GenericDocument.Builder<>("namespace", "id1", "testSchema")
.setPropertyString("subject", "testPut example1")
.setPropertyString("To", "testTo example1")
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc1)
.build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
// create new schema type and upgrade the version number
AppSearchSchema newSchema =
new AppSearchSchema.Builder("testSchema")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
// Set start version to be 3 means we won't trigger migration for 2.
Migrator migrator =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return currentVersion > 2 && currentVersion != finalVersion;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
throw new IllegalStateException(
"Upgrade should not be triggered for this test");
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
throw new IllegalStateException(
"Downgrade should not be triggered for this test");
}
};
// SetSchema with forceOverride=false
ExecutionException exception =
assertThrows(
ExecutionException.class,
() ->
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(newSchema)
.setMigrator("testSchema", migrator)
.setVersion(4) // upgrade version
.build())
.get());
assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
}
@Test
public void testSchemaMigration_sourceToNowhere() throws Exception {
// set the source schema to AppSearch
AppSearchSchema schema = new AppSearchSchema.Builder("sourceSchema").build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(schema)
.setForceOverride(true)
.build())
.get();
// save a doc to the source type
GenericDocument doc =
new GenericDocument.Builder<>("namespace", "id1", "sourceSchema")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc)
.build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
Migrator migrator_sourceToNowhere =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return new GenericDocument.Builder<>(
"zombieNamespace", "zombieId", "nonExistSchema")
.build();
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return document;
}
};
// SetSchema with forceOverride=false
// Source type exist, destination type doesn't exist.
ExecutionException exception =
assertThrows(
ExecutionException.class,
() ->
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder(
"emptySchema")
.build())
.setMigrator(
"sourceSchema",
migrator_sourceToNowhere)
.setVersion(2)
.build()) // upgrade version
.get());
assertThat(exception)
.hasMessageThat()
.contains(
"Receive a migrated document with schema type: nonExistSchema. "
+ "But the schema types doesn't exist in the request");
// SetSchema with forceOverride=true
// Source type exist, destination type doesn't exist.
exception =
assertThrows(
ExecutionException.class,
() ->
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder(
"emptySchema")
.build())
.setMigrator(
"sourceSchema",
migrator_sourceToNowhere)
.setForceOverride(true)
.setVersion(2)
.build()) // upgrade version
.get());
assertThat(exception)
.hasMessageThat()
.contains(
"Receive a migrated document with schema type: nonExistSchema. "
+ "But the schema types doesn't exist in the request");
}
@Test
public void testSchemaMigration_nowhereToDestination() throws Exception {
// set the destination schema to AppSearch
AppSearchSchema destinationSchema =
new AppSearchSchema.Builder("destinationSchema").build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(destinationSchema)
.setForceOverride(true)
.build())
.get();
Migrator migrator_nowhereToDestination =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return document;
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return document;
}
};
// Source type doesn't exist, destination type exist. Since source type doesn't exist,
// no matter force override or not, the migrator won't be invoked
// SetSchema with forceOverride=false
SetSchemaResponse setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(destinationSchema)
.addSchemas(
new AppSearchSchema.Builder("emptySchema").build())
.setMigrator(
"nonExistSchema", migrator_nowhereToDestination)
.setVersion(2) // upgrade version
.build())
.get();
assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
// SetSchema with forceOverride=true
setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(destinationSchema)
.addSchemas(
new AppSearchSchema.Builder("emptySchema").build())
.setMigrator(
"nonExistSchema", migrator_nowhereToDestination)
.setVersion(2) // upgrade version
.setForceOverride(true)
.build())
.get();
assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
}
@Test
public void testSchemaMigration_nowhereToNowhere() throws Exception {
// set empty schema
mDb.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
Migrator migrator_nowhereToNowhere =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return document;
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return document;
}
};
// Source type doesn't exist, destination type exist. Since source type doesn't exist,
// no matter force override or not, the migrator won't be invoked
// SetSchema with forceOverride=false
SetSchemaResponse setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("emptySchema").build())
.setMigrator("nonExistSchema", migrator_nowhereToNowhere)
.setVersion(2) // upgrade version
.build())
.get();
assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
// SetSchema with forceOverride=true
setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("emptySchema").build())
.setMigrator("nonExistSchema", migrator_nowhereToNowhere)
.setVersion(2) // upgrade version
.setForceOverride(true)
.build())
.get();
assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
}
@Test
public void testSchemaMigration_toAnotherType() throws Exception {
// set the source schema to AppSearch
AppSearchSchema sourceSchema = new AppSearchSchema.Builder("sourceSchema").build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(sourceSchema)
.setForceOverride(true)
.build())
.get();
// save a doc to the source type
GenericDocument doc =
new GenericDocument.Builder<>("namespace", "id1", "sourceSchema").build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc)
.build()));
assertThat(result.getSuccesses()).containsExactly("id1", null);
assertThat(result.getFailures()).isEmpty();
// create the destination type and migrator
AppSearchSchema destinationSchema =
new AppSearchSchema.Builder("destinationSchema").build();
Migrator migrator =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return new GenericDocument.Builder<>(
"namespace", document.getId(), "destinationSchema")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return document;
}
};
// SetSchema with forceOverride=false and increase overall version
SetSchemaResponse setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(destinationSchema)
.setMigrator("sourceSchema", migrator)
.setForceOverride(false)
.setVersion(2) // upgrade version
.build())
.get();
assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("sourceSchema");
assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("sourceSchema");
// Check successfully migrate the doc to the destination type
GenericDocument expected =
new GenericDocument.Builder<>("namespace", "id1", "destinationSchema")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
assertThat(doGet(mDb, "namespace", "id1")).containsExactly(expected);
}
@Test
public void testSchemaMigration_toMultipleDestinationType() throws Exception {
// set the source schema to AppSearch
AppSearchSchema sourceSchema =
new AppSearchSchema.Builder("Person")
.addProperty(
new AppSearchSchema.LongPropertyConfig.Builder("Age")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(sourceSchema)
.setForceOverride(true)
.build())
.get();
// save a child and an adult to the Person type
GenericDocument childDoc =
new GenericDocument.Builder<>("namespace", "Person1", "Person")
.setPropertyLong("Age", 6)
.build();
GenericDocument adultDoc =
new GenericDocument.Builder<>("namespace", "Person2", "Person")
.setPropertyLong("Age", 36)
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(childDoc, adultDoc)
.build()));
assertThat(result.getSuccesses()).containsExactly("Person1", null, "Person2", null);
assertThat(result.getFailures()).isEmpty();
// create the migrator
Migrator migrator =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
if (document.getPropertyLong("Age") < 21) {
return new GenericDocument.Builder<>("namespace", "child-id", "Child")
.setPropertyLong("Age", document.getPropertyLong("Age"))
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
} else {
return new GenericDocument.Builder<>("namespace", "adult-id", "Adult")
.setPropertyLong("Age", document.getPropertyLong("Age"))
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
}
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return document;
}
};
// create adult and child schema
AppSearchSchema adultSchema =
new AppSearchSchema.Builder("Adult")
.addProperty(
new AppSearchSchema.LongPropertyConfig.Builder("Age")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
AppSearchSchema childSchema =
new AppSearchSchema.Builder("Child")
.addProperty(
new AppSearchSchema.LongPropertyConfig.Builder("Age")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
// SetSchema with forceOverride=false and increase overall version
SetSchemaResponse setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(adultSchema, childSchema)
.setMigrator("Person", migrator)
.setForceOverride(false)
.setVersion(2) // upgrade version
.build())
.get();
assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("Person");
assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("Person");
// Check successfully migrate the child doc
GenericDocument expectedInChild =
new GenericDocument.Builder<>("namespace", "child-id", "Child")
.setPropertyLong("Age", 6)
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
assertThat(doGet(mDb, "namespace", "child-id")).containsExactly(expectedInChild);
// Check successfully migrate the adult doc
GenericDocument expectedInAdult =
new GenericDocument.Builder<>("namespace", "adult-id", "Adult")
.setPropertyLong("Age", 36)
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
assertThat(doGet(mDb, "namespace", "adult-id")).containsExactly(expectedInAdult);
}
@Test
public void testSchemaMigration_loadTest() throws Exception {
// set the two source type A & B to AppSearch
AppSearchSchema sourceSchemaA =
new AppSearchSchema.Builder("schemaA")
.addProperty(
new AppSearchSchema.LongPropertyConfig.Builder("num")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
AppSearchSchema sourceSchemaB =
new AppSearchSchema.Builder("schemaB")
.addProperty(
new AppSearchSchema.LongPropertyConfig.Builder("num")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(sourceSchemaA, sourceSchemaB)
.setForceOverride(true)
.build())
.get();
// save 100 docs to each type
PutDocumentsRequest.Builder putRequestBuilder = new PutDocumentsRequest.Builder();
for (int i = 0; i < 100; i++) {
GenericDocument docInA =
new GenericDocument.Builder<>("namespace", "idA-" + i, "schemaA")
.setPropertyLong("num", i)
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
GenericDocument docInB =
new GenericDocument.Builder<>("namespace", "idB-" + i, "schemaB")
.setPropertyLong("num", i)
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
putRequestBuilder.addGenericDocuments(docInA, docInB);
}
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(mDb.put(putRequestBuilder.build()));
assertThat(result.getFailures()).isEmpty();
// create three destination types B, C & D
AppSearchSchema destinationSchemaB =
new AppSearchSchema.Builder("schemaB")
.addProperty(
new AppSearchSchema.LongPropertyConfig.Builder("numNewProperty")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
AppSearchSchema destinationSchemaC =
new AppSearchSchema.Builder("schemaC")
.addProperty(
new AppSearchSchema.LongPropertyConfig.Builder("num")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
AppSearchSchema destinationSchemaD =
new AppSearchSchema.Builder("schemaD")
.addProperty(
new AppSearchSchema.LongPropertyConfig.Builder("num")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.build())
.build();
// Create an active migrator for type A which will migrate first 50 docs to C and second
// 50 docs to D
Migrator migratorA =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
if (document.getPropertyLong("num") < 50) {
return new GenericDocument.Builder<>(
"namespace", document.getId() + "-destC", "schemaC")
.setPropertyLong("num", document.getPropertyLong("num"))
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
} else {
return new GenericDocument.Builder<>(
"namespace", document.getId() + "-destD", "schemaD")
.setPropertyLong("num", document.getPropertyLong("num"))
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
}
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return document;
}
};
// Create an active migrator for type B which will migrate first 50 docs to B and second
// 50 docs to D
Migrator migratorB =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return true;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
if (document.getPropertyLong("num") < 50) {
return new GenericDocument.Builder<>(
"namespace", document.getId() + "-destB", "schemaB")
.setPropertyLong(
"numNewProperty", document.getPropertyLong("num"))
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
} else {
return new GenericDocument.Builder<>(
"namespace", document.getId() + "-destD", "schemaD")
.setPropertyLong("num", document.getPropertyLong("num"))
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
}
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion,
int finalVersion,
@NonNull GenericDocument document) {
return document;
}
};
// SetSchema with forceOverride=false and increase overall version
SetSchemaResponse setSchemaResponse =
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(
destinationSchemaB,
destinationSchemaC,
destinationSchemaD)
.setMigrator("schemaA", migratorA)
.setMigrator("schemaB", migratorB)
.setForceOverride(false)
.setVersion(2) // upgrade version
.build())
.get();
assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("schemaA");
assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("schemaB");
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("schemaA", "schemaB");
// generate expected documents
List<GenericDocument> expectedDocs = new ArrayList<>();
for (int i = 0; i < 50; i++) {
GenericDocument docAToC =
new GenericDocument.Builder<>("namespace", "idA-" + i + "-destC", "schemaC")
.setPropertyLong("num", i)
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
GenericDocument docBToB =
new GenericDocument.Builder<>("namespace", "idB-" + i + "-destB", "schemaB")
.setPropertyLong("numNewProperty", i)
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
expectedDocs.add(docAToC);
expectedDocs.add(docBToB);
}
for (int i = 50; i < 100; i++) {
GenericDocument docAToD =
new GenericDocument.Builder<>("namespace", "idA-" + i + "-destD", "schemaD")
.setPropertyLong("num", i)
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
GenericDocument docBToD =
new GenericDocument.Builder<>("namespace", "idB-" + i + "-destD", "schemaD")
.setPropertyLong("num", i)
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
expectedDocs.add(docAToD);
expectedDocs.add(docBToD);
}
// query all documents and compare
SearchResultsShim searchResults =
mDb.search(
"",
new SearchSpec.Builder()
.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
.build());
List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
assertThat(documents).containsExactlyElementsIn(expectedDocs);
}
// *************************** Multi-step migration tests ******************************
// Version structure and how version bumps:
// Version 1: Start - typeA docs contains "subject" property.
// Version 2: typeA docs get new "body" property, contains "subject" and "body" now.
// Version 3: typeA docs is migrated to typeB, typeA docs got removed, typeB doc contains
// "subject" and "body" property.
// Version 4: typeB docs remove "subject" property, contains only "body" now.
// Create a multi-step migrator for A, which could migrate version 1-3 to 4.
private static final Migrator MULTI_STEP_MIGRATOR_A =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return currentVersion < 3;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion, int finalVersion, @NonNull GenericDocument document) {
GenericDocument.Builder docBuilder =
new GenericDocument.Builder<>("namespace", "id", "TypeB")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME);
if (currentVersion == 2) {
docBuilder.setPropertyString("body", document.getPropertyString("body"));
} else {
docBuilder.setPropertyString(
"body", "new content for the newly added 'body' property");
}
return docBuilder.build();
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion, int finalVersion, @NonNull GenericDocument document) {
return document;
}
};
// create a multi-step migrator for B, which could migrate version 1-3 to 4.
private static final Migrator MULTI_STEP_MIGRATOR_B =
new Migrator() {
@Override
public boolean shouldMigrate(int currentVersion, int finalVersion) {
return currentVersion == 3;
}
@NonNull
@Override
public GenericDocument onUpgrade(
int currentVersion, int finalVersion, @NonNull GenericDocument document) {
return new GenericDocument.Builder<>("namespace", "id", "TypeB")
.setPropertyString("body", document.getPropertyString("body"))
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
}
@NonNull
@Override
public GenericDocument onDowngrade(
int currentVersion, int finalVersion, @NonNull GenericDocument document) {
return document;
}
};
// create a setSchemaRequest, which could migrate version 1-3 to 4.
private static final SetSchemaRequest MULTI_STEP_REQUEST =
new SetSchemaRequest.Builder()
.addSchemas(
new AppSearchSchema.Builder("TypeB")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("body")
.setCardinality(
AppSearchSchema.PropertyConfig
.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build())
.setMigrator("TypeA", MULTI_STEP_MIGRATOR_A)
.setMigrator("TypeB", MULTI_STEP_MIGRATOR_B)
.setVersion(4)
.build();
@Test
public void testSchemaMigration_multiStep1To4() throws Exception {
// set version 1 to the database, only contain TypeA
AppSearchSchema typeA =
new AppSearchSchema.Builder("TypeA")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(typeA)
.setForceOverride(true)
.setVersion(1)
.build())
.get();
// save a doc to version 1.
GenericDocument doc =
new GenericDocument.Builder<>("namespace", "id", "TypeA")
.setPropertyString("subject", "subject")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc)
.build()));
assertThat(result.getSuccesses()).containsExactly("id", null);
assertThat(result.getFailures()).isEmpty();
// update to version 4.
SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
// Create expected doc. Since we started at version 1 and migrated to version 4:
// 1: A 'body' property should have been added with "new content for the newly added 'body'
// property"
// 2: The type should have been changed from 'TypeA' to 'TypeB'
// 3: The 'subject' property should have been removed
GenericDocument expected =
new GenericDocument.Builder<>("namespace", "id", "TypeB")
.setPropertyString(
"body", "new content for the newly added 'body' property")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected);
}
@Test
public void testSchemaMigration_multiStep2To4() throws Exception {
// set version 2 to the database, only contain TypeA with a new property
AppSearchSchema typeA =
new AppSearchSchema.Builder("TypeA")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("body")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(typeA)
.setForceOverride(true)
.setVersion(2)
.build())
.get();
// save a doc to version 2.
GenericDocument doc =
new GenericDocument.Builder<>("namespace", "id", "TypeA")
.setPropertyString("subject", "subject")
.setPropertyString("body", "bodyFromA")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc)
.build()));
assertThat(result.getSuccesses()).containsExactly("id", null);
assertThat(result.getFailures()).isEmpty();
// update to version 4.
SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
assertThat(setSchemaResponse.getDeletedTypes()).containsExactly("TypeA");
assertThat(setSchemaResponse.getIncompatibleTypes()).isEmpty();
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeA");
// create expected doc, body exists in type A of version 2
GenericDocument expected =
new GenericDocument.Builder<>("namespace", "id", "TypeB")
.setPropertyString("body", "bodyFromA")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected);
}
@Test
public void testSchemaMigration_multiStep3To4() throws Exception {
// set version 3 to the database, only contain TypeB
AppSearchSchema typeA =
new AppSearchSchema.Builder("TypeB")
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("subject")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.addProperty(
new AppSearchSchema.StringPropertyConfig.Builder("body")
.setCardinality(
AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setIndexingType(
AppSearchSchema.StringPropertyConfig
.INDEXING_TYPE_PREFIXES)
.setTokenizerType(
AppSearchSchema.StringPropertyConfig
.TOKENIZER_TYPE_PLAIN)
.build())
.build();
mDb.setSchema(
new SetSchemaRequest.Builder()
.addSchemas(typeA)
.setForceOverride(true)
.setVersion(3)
.build())
.get();
// save a doc to version 2.
GenericDocument doc =
new GenericDocument.Builder<>("namespace", "id", "TypeB")
.setPropertyString("subject", "subject")
.setPropertyString("body", "bodyFromB")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
AppSearchBatchResult<String, Void> result =
checkIsBatchResultSuccess(
mDb.put(
new PutDocumentsRequest.Builder()
.addGenericDocuments(doc)
.build()));
assertThat(result.getSuccesses()).containsExactly("id", null);
assertThat(result.getFailures()).isEmpty();
// update to version 4.
SetSchemaResponse setSchemaResponse = mDb.setSchema(MULTI_STEP_REQUEST).get();
assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
assertThat(setSchemaResponse.getIncompatibleTypes()).containsExactly("TypeB");
assertThat(setSchemaResponse.getMigratedTypes()).containsExactly("TypeB");
// create expected doc, body exists in type A of version 3
GenericDocument expected =
new GenericDocument.Builder<>("namespace", "id", "TypeB")
.setPropertyString("body", "bodyFromB")
.setCreationTimestampMillis(DOCUMENT_CREATION_TIME)
.build();
assertThat(doGet(mDb, "namespace", "id")).containsExactly(expected);
}
}