/* * FDBMetaDataStoreTest.java * * This source file is part of the FoundationDB open source project * * Copyright 2015-2018 Apple Inc. and the FoundationDB project authors * * 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 com.apple.foundationdb.record.provider.foundationdb; import com.apple.foundationdb.KeyValue; import com.apple.foundationdb.Transaction; import com.apple.foundationdb.record.ProtoVersionSupplier; import com.apple.foundationdb.record.RecordMetaData; import com.apple.foundationdb.record.RecordMetaDataBuilder; import com.apple.foundationdb.record.RecordMetaDataOptionsProto; import com.apple.foundationdb.record.RecordMetaDataProto; import com.apple.foundationdb.record.TestNoUnionEvolvedIllegalProto; import com.apple.foundationdb.record.TestNoUnionEvolvedProto; import com.apple.foundationdb.record.TestNoUnionEvolvedRenamedRecordTypeProto; import com.apple.foundationdb.record.TestNoUnionProto; import com.apple.foundationdb.record.TestRecords1EvolvedAgainProto; import com.apple.foundationdb.record.TestRecords1EvolvedProto; import com.apple.foundationdb.record.TestRecords1Proto; import com.apple.foundationdb.record.TestRecords3Proto; import com.apple.foundationdb.record.TestRecords4Proto; import com.apple.foundationdb.record.TestRecordsImplicitUsageNoUnionProto; import com.apple.foundationdb.record.TestRecordsImplicitUsageProto; import com.apple.foundationdb.record.TestRecordsDoubleNestedProto; import com.apple.foundationdb.record.TestRecordsImportProto; import com.apple.foundationdb.record.TestRecordsImportedAndNewProto; import com.apple.foundationdb.record.TestRecordsMultiProto; import com.apple.foundationdb.record.TestRecordsNestedAsRecord; import com.apple.foundationdb.record.TestRecordsOneOfProto; import com.apple.foundationdb.record.TestRecordsParentChildRelationshipProto; import com.apple.foundationdb.record.metadata.Index; import com.apple.foundationdb.record.metadata.IndexOptions; import com.apple.foundationdb.record.metadata.IndexTypes; import com.apple.foundationdb.record.metadata.Key; import com.apple.foundationdb.record.metadata.MetaDataEvolutionValidator; import com.apple.foundationdb.record.metadata.MetaDataException; import com.apple.foundationdb.record.metadata.MetaDataProtoTest; import com.apple.foundationdb.record.metadata.RecordType; import com.apple.foundationdb.record.metadata.expressions.KeyExpression; import com.apple.foundationdb.tuple.Tuple; import com.apple.test.BooleanSource; import com.apple.test.Tags; import com.google.common.collect.ImmutableSet; import com.google.protobuf.DescriptorProtos; import com.google.protobuf.Descriptors; import com.google.protobuf.ExtensionRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link FDBMetaDataStore}. */ @Tag(Tags.RequiresFDB) public class FDBMetaDataStoreTest extends FDBTestBase { FDBDatabase fdb; FDBMetaDataStore metaDataStore; public void openMetaDataStore(FDBRecordContext context) { metaDataStore = new FDBMetaDataStore(context, TestKeySpace.getKeyspacePath("record-test", "unit", "metadataStore")); metaDataStore.setDependencies(new Descriptors.FileDescriptor[] { RecordMetaDataOptionsProto.getDescriptor() }); } @BeforeEach public void setup() { fdb = FDBDatabaseFactory.instance().getDatabase(); fdb.run(context -> { openMetaDataStore(context); context.ensureActive().clear(metaDataStore.getSubspace().range()); return null; }); } @Test public void simple() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); MetaDataProtoTest.verifyEquals(metaData, metaDataStore.getRecordMetaData()); } } @Test public void manyTypes() { final int ntypes = 500; final int nfields = 10; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); DescriptorProtos.FileDescriptorProto.Builder fileBuilder = DescriptorProtos.FileDescriptorProto.newBuilder(); fileBuilder.addDependency(RecordMetaDataOptionsProto.getDescriptor().getName()); DescriptorProtos.DescriptorProto.Builder unionBuilder = fileBuilder.addMessageTypeBuilder(); unionBuilder.setName("RecordTypeUnion"); DescriptorProtos.MessageOptions.Builder unionMessageOptions = DescriptorProtos.MessageOptions.newBuilder(); RecordMetaDataOptionsProto.RecordTypeOptions.Builder unionOptions = RecordMetaDataOptionsProto.RecordTypeOptions.newBuilder(); unionOptions.setUsage(RecordMetaDataOptionsProto.RecordTypeOptions.Usage.UNION); unionMessageOptions.setExtension(RecordMetaDataOptionsProto.record, unionOptions.build()); unionBuilder.setOptions(unionMessageOptions); for (int ri = 1; ri <= ntypes; ri++) { DescriptorProtos.DescriptorProto.Builder messageBuilder = fileBuilder.addMessageTypeBuilder(); messageBuilder.setName("type_" + ri); for (int fi = 1; fi <= nfields; fi++) { messageBuilder.addFieldBuilder() .setName("field_" + fi) .setNumber(fi) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_STRING); } unionBuilder.addFieldBuilder() .setNumber(ri) .setName("_" + messageBuilder.getName()) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_MESSAGE) .setTypeName(messageBuilder.getName()); } RecordMetaDataProto.MetaData.Builder metaData = RecordMetaDataProto.MetaData.newBuilder(); metaData.setRecords(fileBuilder); for (int ri = 1; ri <= ntypes; ri++) { metaData.addRecordTypesBuilder() .setName("type_" + ri) .getPrimaryKeyBuilder().getFieldBuilder() .setFanType(RecordMetaDataProto.Field.FanType.SCALAR) .setFieldName("field_1"); } metaDataStore.saveRecordMetaData(metaData.build()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); for (int ri = 1; ri <= ntypes; ri++) { assertNotNull(metaDataStore.getRecordMetaData().getRecordType("type_" + ri)); } context.commit(); } } @Test public void historyCompat() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaDataProto.MetaData.Builder metaData = RecordMetaDataProto.MetaData.newBuilder(); metaData.setRecords(TestRecords1Proto.getDescriptor().toProto()); metaData.addRecordTypesBuilder() .setName("MySimpleRecord") .getPrimaryKeyBuilder().getFieldBuilder().setFieldName("rec_no").setFanType(RecordMetaDataProto.Field.FanType.SCALAR); metaData.addRecordTypesBuilder() .setName("MyOtherRecord") .getPrimaryKeyBuilder().getFieldBuilder().setFieldName("rec_no").setFanType(RecordMetaDataProto.Field.FanType.SCALAR); metaData.setVersion(101); metaDataStore.saveRecordMetaData(metaData.build()); { // Adjust to look like old format store by moving everything under CURRENT_KEY up under root. Transaction tr = context.ensureActive(); List<KeyValue> kvs = context.asyncToSync(FDBStoreTimer.Waits.WAIT_LOAD_META_DATA, tr.getRange(metaDataStore.getSubspace().range(FDBMetaDataStore.CURRENT_KEY)).asList()); context.ensureActive().clear(metaDataStore.getSubspace().range()); for (KeyValue kv : kvs) { Tuple tuple = Tuple.fromBytes(kv.getKey()); List<Object> items = tuple.getItems(); assertEquals(null, items.remove(items.size() - 2)); tuple = Tuple.fromList(items); tr.set(tuple.pack(), kv.getValue()); } } context.commit(); } RecordMetaData before; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); before = metaDataStore.getRecordMetaData(); context.commit(); } assertNotNull(before.getRecordType("MySimpleRecord")); assertFalse(before.hasIndex("MyIndex")); try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaDataProto.MetaData.Builder metaData = RecordMetaDataProto.MetaData.newBuilder(); metaData.setRecords(TestRecords1Proto.getDescriptor().toProto()); metaData.addRecordTypesBuilder() .setName("MySimpleRecord") .getPrimaryKeyBuilder().getFieldBuilder().setFieldName("rec_no").setFanType(RecordMetaDataProto.Field.FanType.SCALAR); metaData.addIndexesBuilder() .setName("MyIndex") .addRecordType("MySimpleRecord") .setAddedVersion(102) .setLastModifiedVersion(102) .getRootExpressionBuilder().getFieldBuilder() .setFieldName("num_value_2") .setFanType(RecordMetaDataProto.Field.FanType.SCALAR); metaData.addRecordTypesBuilder() .setName("MyOtherRecord") .getPrimaryKeyBuilder().getFieldBuilder().setFieldName("rec_no").setFanType(RecordMetaDataProto.Field.FanType.SCALAR); metaData.setVersion(102); metaDataStore.saveRecordMetaData(metaData.build()); context.commit(); } RecordMetaData after; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); after = metaDataStore.getRecordMetaData(); context.commit(); } assertNotNull(after.getRecordType("MySimpleRecord")); assertTrue(after.hasIndex("MyIndex")); RecordMetaData beforeAgain; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); beforeAgain = context.asyncToSync(FDBStoreTimer.Waits.WAIT_LOAD_META_DATA, metaDataStore.loadVersion(before.getVersion())); context.commit(); } assertEquals(before.getVersion(), beforeAgain.getVersion()); assertNotNull(beforeAgain.getRecordType("MySimpleRecord")); assertFalse(beforeAgain.hasIndex("MyIndex")); } @Test public void withToProto() { RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder().setRecords(TestRecordsParentChildRelationshipProto.getDescriptor()); metaDataBuilder.addIndex("MyChildRecord", "MyChildRecord$str_value", Key.Expressions.field("str_value")); metaDataBuilder.removeIndex("MyChildRecord$parent_rec_no"); metaDataBuilder.addIndex("MyChildRecord", new Index("MyChildRecord$parent&str", Key.Expressions.concatenateFields("parent_rec_no", "str_value"), Index.EMPTY_VALUE, IndexTypes.VALUE, IndexOptions.UNIQUE_OPTIONS)); metaDataBuilder.removeIndex("MyParentRecord$str_value_indexed"); metaDataBuilder.addIndex("MyParentRecord", "MyParentRecord$str&child", Key.Expressions.concat( Key.Expressions.field("str_value_indexed"), Key.Expressions.field("child_rec_nos", KeyExpression.FanType.FanOut))); metaDataBuilder.addMultiTypeIndex(Arrays.asList(metaDataBuilder.getRecordType("MyChildRecord"), metaDataBuilder.getRecordType("MyParentRecord")), new Index("all$rec_nos", Key.Expressions.field("rec_no"))); RecordMetaData metaData = metaDataBuilder.getRecordMetaData(); try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData retrievedMetaData = metaDataStore.getRecordMetaData(); MetaDataProtoTest.verifyEquals(metaData, retrievedMetaData); } } @Test public void withIncompatibleChange() { RecordMetaData metaData1 = RecordMetaData.build(TestRecords1Proto.getDescriptor()); try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.saveRecordMetaData(metaData1); context.commit(); } RecordMetaData metaData2 = RecordMetaData.build(metaData1.toProto().toBuilder().setVersion(metaData1.getVersion() + 1).clearIndexes().build()); try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.saveRecordMetaData(metaData2)); assertThat(e.getMessage(), containsString("index missing in new meta-data")); context.commit(); } // Using the builder, this change should be fine. RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder().setRecords(TestRecords1Proto.getDescriptor()); metaData1.getAllIndexes().forEach(index -> metaDataBuilder.removeIndex(index.getName())); RecordMetaData metaData3 = metaDataBuilder.getRecordMetaData(); try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.saveRecordMetaData(metaData3); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData retrievedMetaData = metaDataStore.getRecordMetaData(); MetaDataProtoTest.verifyEquals(metaData3, retrievedMetaData); } } @ParameterizedTest(name = "indexes [indexCounterBasedSubspaceKey = {0}]") @BooleanSource public void indexes(final boolean indexCounterBasedSubspaceKey) { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaDataBuilder builder = RecordMetaData.newBuilder(); if (indexCounterBasedSubspaceKey) { builder.enableCounterBasedSubspaceKeys(); } metaDataStore.saveRecordMetaData(builder.setRecords(TestRecords1Proto.getDescriptor()).getRecordMetaData()); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); metaDataStore.addIndex("MySimpleRecord", "testIndex", "rec_no"); assertNotNull(metaDataStore.getRecordMetaData().getIndex("testIndex")); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getIndex("testIndex")); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getIndex("testIndex")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.addIndex("MySimpleRecord", "testIndex", "rec_no")); assertEquals("Index testIndex already defined", e.getMessage()); metaDataStore.dropIndex("testIndex"); context.commit(); e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData().getIndex("testIndex")); assertEquals("Index testIndex not defined", e.getMessage()); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData().getIndex("testIndex")); assertEquals("Index testIndex not defined", e.getMessage()); e = assertThrows(MetaDataException.class, () -> metaDataStore.dropIndex("testIndex")); assertEquals("No index named testIndex defined", e.getMessage()); context.commit(); } } @Test public void withIndexesRequiringRebuild() { RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.saveRecordMetaData(metaData); context.commit(); } RecordMetaDataProto.MetaData.Builder protoBuilder = metaData.toProto().toBuilder().setVersion(metaData.getVersion() + 1); protoBuilder.getIndexesBuilderList().forEach(index -> { if (index.getName().equals("MySimpleRecord$str_value_indexed")) { index.addOptions(RecordMetaDataProto.Index.Option.newBuilder().setKey(IndexOptions.UNIQUE_OPTION).setValue("true")); index.setLastModifiedVersion(metaData.getVersion() + 1); } }); RecordMetaData metaData2 = RecordMetaData.build(protoBuilder.build()); try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.saveRecordMetaData(metaData2)); assertThat(e.getMessage(), containsString("last modified version of index changed")); MetaDataProtoTest.verifyEquals(metaData, metaDataStore.getRecordMetaData()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData retrievedMetaData = metaDataStore.getRecordMetaData(); assertThat(retrievedMetaData.getIndex("MySimpleRecord$str_value_indexed").isUnique(), is(false)); MetaDataProtoTest.verifyEquals(metaData, retrievedMetaData); } MetaDataEvolutionValidator laxerValidator = MetaDataEvolutionValidator.newBuilder() .setAllowIndexRebuilds(true) .build(); try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.setEvolutionValidator(laxerValidator); metaDataStore.saveRecordMetaData(metaData2); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData retrievedMetaData = metaDataStore.getRecordMetaData(); assertThat(retrievedMetaData.getIndex("MySimpleRecord$str_value_indexed").isUnique(), is(true)); MetaDataProtoTest.verifyEquals(metaData2, metaDataStore.getRecordMetaData()); } } @Test public void multiTypeIndex() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsMultiProto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.addUniversalIndex(FDBRecordStoreTestBase.COUNT_INDEX); metaDataStore.addMultiTypeIndex(Arrays.asList("MultiRecordOne", "MultiRecordTwo", "MultiRecordThree"), new Index("all$elements", Key.Expressions.field("element", KeyExpression.FanType.Concatenate), Index.EMPTY_VALUE, IndexTypes.VALUE, IndexOptions.UNIQUE_OPTIONS)); metaDataStore.addMultiTypeIndex(null, new Index("all$elements2", Key.Expressions.field("element", KeyExpression.FanType.Concatenate), Index.EMPTY_VALUE, IndexTypes.VALUE, IndexOptions.UNIQUE_OPTIONS)); metaDataStore.addMultiTypeIndex(Arrays.asList("MultiRecordTwo", "MultiRecordThree"), new Index("two&three$ego", Key.Expressions.field("ego"), Index.EMPTY_VALUE, IndexTypes.VALUE, IndexOptions.UNIQUE_OPTIONS)); metaDataStore.addMultiTypeIndex(Arrays.asList("MultiRecordOne"), new Index("one$name", Key.Expressions.field("name"), IndexTypes.VALUE)); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(5, metaData.getAllIndexes().size()); assertEquals(0, metaData.getFormerIndexes().size()); assertNotNull(metaData.getIndex("all$elements")); assertEquals(3, metaData.recordTypesForIndex(metaData.getIndex("all$elements")).size()); assertNotNull(metaData.getIndex("all$elements2")); assertEquals(3, metaData.recordTypesForIndex(metaData.getIndex("all$elements2")).size()); assertNotNull(metaData.getIndex("two&three$ego")); assertEquals(2, metaData.recordTypesForIndex(metaData.getIndex("two&three$ego")).size()); assertNotNull(metaData.getIndex("one$name")); assertEquals(1, metaData.recordTypesForIndex(metaData.getIndex("one$name")).size()); metaDataStore.dropIndex("one$name"); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData().getIndex("one$name")); assertEquals("Index one$name not defined", e.getMessage()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(4, metaData.getAllIndexes().size()); assertEquals(1, metaData.getFormerIndexes().size()); assertNotNull(metaDataStore.getRecordMetaData().getIndex("all$elements")); assertNotNull(metaDataStore.getRecordMetaData().getIndex("all$elements2")); assertNotNull(metaDataStore.getRecordMetaData().getIndex("two&three$ego")); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData().getIndex("one$name")); assertEquals("Index one$name not defined", e.getMessage()); } } @Test public void withoutBumpingVersion() { RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.saveRecordMetaData(metaData); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.saveRecordMetaData(metaData)); assertThat(e.getMessage(), containsString("meta-data version must increase")); context.commit(); } } @Test public void updateRecords() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); metaDataStore.updateRecords(TestRecords1EvolvedProto.getDescriptor()); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyOtherRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("AnotherRecord")); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyOtherRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("AnotherRecord")); context.commit(); } } @ParameterizedTest(name = "updateRecordsWithNewUnionField [reorderFields = {0}]") @BooleanSource public void updateRecordsWithNewUnionField(boolean reorderFields) { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData oldMetaData = metaDataStore.getRecordMetaData(); metaDataStore.mutateMetaData(metaDataProtoBuilder -> { final DescriptorProtos.FileDescriptorProto.Builder records = metaDataProtoBuilder.getRecordsBuilder(); records.getMessageTypeBuilderList().stream() .filter(message -> message.getName().equals(RecordMetaDataBuilder.DEFAULT_UNION_NAME)) .forEach(unionMessage -> { unionMessage.getFieldBuilderList().stream() .filter(field -> field.getName().equals("_MySimpleRecord")) .forEach(field -> field.setName("_MySimpleRecord_old")); int newFieldNumber = unionMessage.getFieldBuilderList().stream() .mapToInt(DescriptorProtos.FieldDescriptorProto.Builder::getNumber) .max() .orElse(0) + 1; DescriptorProtos.FieldDescriptorProto newField = DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_MESSAGE) .setTypeName("." + TestRecords1Proto.MySimpleRecord.getDescriptor().getFullName()) .setName("_MySimpleRecord_new") .setNumber(newFieldNumber) .build(); if (reorderFields) { List<DescriptorProtos.FieldDescriptorProto> fieldList = new ArrayList<>(unionMessage.getFieldBuilderList().size() + 1); fieldList.add(newField); fieldList.addAll(unionMessage.getFieldList()); unionMessage.clearField(); unionMessage.addAllField(fieldList); } else { unionMessage.addField(newField); } }); }); RecordMetaData newMetaData = metaDataStore.getRecordMetaData(); RecordType oldSimpleRecord = oldMetaData.getRecordType("MySimpleRecord"); assertEquals(TestRecords1EvolvedProto.RecordTypeUnion._MYSIMPLERECORD_FIELD_NUMBER, oldMetaData.getUnionFieldForRecordType(oldSimpleRecord).getNumber()); RecordType newSimpleRecord = newMetaData.getRecordType("MySimpleRecord"); assertSame(newMetaData.getUnionDescriptor().findFieldByName("_MySimpleRecord_new"), newMetaData.getUnionFieldForRecordType(newSimpleRecord)); assertThat(oldMetaData.getUnionFieldForRecordType(oldSimpleRecord).getNumber(), lessThan(newMetaData.getUnionFieldForRecordType(newSimpleRecord).getNumber())); assertEquals(oldSimpleRecord.getSinceVersion(), newSimpleRecord.getSinceVersion()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); RecordType simpleRecord = metaData.getRecordType("MySimpleRecord"); assertEquals("_MySimpleRecord_new", metaData.getUnionFieldForRecordType(simpleRecord).getName()); int newFieldNumber = TestRecords1Proto.RecordTypeUnion.getDescriptor().getFields().stream() .mapToInt(Descriptors.FieldDescriptor::getNumber) .max() .orElse(0) + 1; assertEquals(newFieldNumber, metaData.getUnionFieldForRecordType(simpleRecord).getNumber()); } } @Test public void updateRecordsWithExtensionOption() throws Descriptors.DescriptorValidationException { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } // Add an extension option specifying that a field should have a new index DescriptorProtos.FileDescriptorProto.Builder fileBuilder = TestRecords1Proto.getDescriptor().toProto().toBuilder(); fileBuilder.getMessageTypeBuilderList().forEach(messageType -> { if (messageType.getName().equals("MySimpleRecord")) { messageType.getFieldBuilderList().forEach(field -> { if (field.getName().equals("num_value_2")) { RecordMetaDataOptionsProto.FieldOptions isIndexedOption = RecordMetaDataOptionsProto.FieldOptions.newBuilder() .setIndex(RecordMetaDataOptionsProto.FieldOptions.IndexOption.newBuilder() .setType(IndexTypes.VALUE) .setUnique(true)) .build(); field.getOptionsBuilder().setExtension(RecordMetaDataOptionsProto.field, isIndexedOption); } }); } }); Descriptors.FileDescriptor newFileDescriptor = Descriptors.FileDescriptor.buildFrom(fileBuilder.build(), new Descriptors.FileDescriptor[]{RecordMetaDataOptionsProto.getDescriptor()}); // Validate that new extension option will result in new index when built from file. RecordMetaData metaDataFromFile = RecordMetaData.build(newFileDescriptor); Index newIndex = metaDataFromFile.getIndex("MySimpleRecord$num_value_2"); assertEquals(Key.Expressions.field("num_value_2"), newIndex.getRootExpression()); assertThat("newIndex not marked as unique", newIndex.isUnique()); // Update records. Validate that created meta-data does not add index. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.updateRecords(newFileDescriptor); RecordMetaData metaData = metaDataStore.getRecordMetaData(); // read from local cache MetaDataException e = assertThrows(MetaDataException.class, () -> metaData.getIndex("MySimpleRecord$num_value_2")); assertThat(e.getMessage(), containsString("Index MySimpleRecord$num_value_2 not defined")); context.commit(); } // Validate that reading the index back from database does not add the index. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.updateRecords(newFileDescriptor); RecordMetaData metaData = metaDataStore.getRecordMetaData(); // read from the database MetaDataException e = assertThrows(MetaDataException.class, () -> metaData.getIndex("MySimpleRecord$num_value_2")); assertThat(e.getMessage(), containsString("Index MySimpleRecord$num_value_2 not defined")); } } @Test public void updateRecordsWithLocalFileDescriptor() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); // Local file descriptor is not as evolved as the to-be updating records descriptor. It will fail. metaDataStore.setLocalFileDescriptor(TestRecords1Proto.getDescriptor()); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.updateRecords(TestRecords1EvolvedProto.getDescriptor())); assertEquals(e.getMessage(), "record type removed from union"); metaDataStore.setLocalFileDescriptor(TestRecords1EvolvedAgainProto.getDescriptor()); metaDataStore.updateRecords(TestRecords1EvolvedProto.getDescriptor()); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyOtherRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("AnotherRecord")); e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData().getRecordType("OneMoreRecord")); assertEquals(e.getMessage(), "Unknown record type OneMoreRecord"); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyOtherRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("AnotherRecord")); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData().getRecordType("OneMoreRecord")); assertEquals(e.getMessage(), "Unknown record type OneMoreRecord"); context.commit(); } } @Test public void extensionRegistry() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { // Default registry parses options openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); Descriptors.Descriptor mySimpleRecordDescriptor = metaData.getRecordType("MySimpleRecord").getDescriptor(); assertNotSame(mySimpleRecordDescriptor.getFile(), TestRecords1Proto.getDescriptor()); RecordMetaDataOptionsProto.FieldOptions recNoFieldOptions = mySimpleRecordDescriptor.findFieldByName("rec_no") .getOptions().getExtension(RecordMetaDataOptionsProto.field); assertNotNull(recNoFieldOptions); assertThat(recNoFieldOptions.getPrimaryKey(), is(true)); // Empty registry does not openMetaDataStore(context); metaDataStore.setExtensionRegistry(ExtensionRegistry.getEmptyRegistry()); metaData = metaDataStore.getRecordMetaData(); mySimpleRecordDescriptor = metaData.getRecordType("MySimpleRecord").getDescriptor(); assertNotSame(mySimpleRecordDescriptor.getFile(), TestRecords1Proto.getDescriptor()); assertThat(mySimpleRecordDescriptor.findFieldByName("rec_no").getOptions().hasExtension(RecordMetaDataOptionsProto.field), is(false)); // Null registry behaves like the empty registry in proto2 and throws an exception in proto3 openMetaDataStore(context); metaDataStore.setExtensionRegistry(null); if (ProtoVersionSupplier.getProtoVersion() == 2) { metaData = metaDataStore.getRecordMetaData(); mySimpleRecordDescriptor = metaData.getRecordType("MySimpleRecord").getDescriptor(); assertNotSame(mySimpleRecordDescriptor.getFile(), TestRecords1Proto.getDescriptor()); assertThat(mySimpleRecordDescriptor.findFieldByName("rec_no").getOptions().hasExtension(RecordMetaDataOptionsProto.field), is(false)); } else { final FDBMetaDataStore finalMetaDataStore = metaDataStore; assertThrows(NullPointerException.class, finalMetaDataStore::getRecordMetaData); } } } @Test public void extensionRegistryWithUnionDescriptor() { try (FDBRecordContext context = fdb.openContext()) { // test_records_3.proto relies on the union field annotation type openMetaDataStore(context); RecordMetaDataBuilder builder = RecordMetaData.newBuilder().setRecords(TestRecords3Proto.getDescriptor()); builder.getRecordType("MyHierarchicalRecord").setPrimaryKey(Key.Expressions.concatenateFields("parent_path", "child_name")); RecordMetaData metaData = builder.build(); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { // Default registry parses options openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertNotSame(metaData.getUnionDescriptor(), TestRecords3Proto.UnionDescriptor.getDescriptor()); assertEquals(metaData.getUnionDescriptor().toProto(), TestRecords3Proto.UnionDescriptor.getDescriptor().toProto()); // Empty registry does not openMetaDataStore(context); metaDataStore.setExtensionRegistry(ExtensionRegistry.getEmptyRegistry()); final FDBMetaDataStore finalMetaDataStore = metaDataStore; MetaDataException e = assertThrows(MetaDataException.class, finalMetaDataStore::getRecordMetaData); assertThat(e.getMessage(), containsString("Union descriptor is required")); // Null registry behaves like the empty registry in proto2 and throws an exception in proto3 openMetaDataStore(context); metaDataStore.setExtensionRegistry(null); if (ProtoVersionSupplier.getProtoVersion() == 2) { final FDBMetaDataStore secondFinalMetaDataStore = metaDataStore; e = assertThrows(MetaDataException.class, secondFinalMetaDataStore::getRecordMetaData); assertThat(e.getMessage(), containsString("Union descriptor is required")); } else { final FDBMetaDataStore secondFinalMetaDataStore = metaDataStore; assertThrows(NullPointerException.class, secondFinalMetaDataStore::getRecordMetaData); } } } private void addRecordType(@Nonnull DescriptorProtos.DescriptorProto newRecordType, @Nonnull KeyExpression primaryKey) { metaDataStore.mutateMetaData(metaDataProto -> MetaDataProtoEditor.addRecordType(metaDataProto, newRecordType, primaryKey)); } private void addRecordType(@Nonnull DescriptorProtos.DescriptorProto newRecordType, @Nonnull KeyExpression primaryKey, @Nonnull Index index) { metaDataStore.mutateMetaData(metaDataProto -> MetaDataProtoEditor.addRecordType(metaDataProto, newRecordType, primaryKey), recordMetaDataBuilder -> recordMetaDataBuilder.addIndex(newRecordType.getName(), index)); } private void deprecateRecordType(@Nonnull String recordType) { metaDataStore.mutateMetaData((metaDataProto) -> MetaDataProtoEditor.deprecateRecordType(metaDataProto, recordType)); } private void addField(@Nonnull String recordType, @Nonnull DescriptorProtos.FieldDescriptorProto field) { metaDataStore.mutateMetaData((metaDataProto) -> MetaDataProtoEditor.addField(metaDataProto, recordType, field)); } private void deprecateField(@Nonnull String recordType, @Nonnull String fieldName) { metaDataStore.mutateMetaData((metaDataProto) -> MetaDataProtoEditor.deprecateField(metaDataProto, recordType, fieldName)); } private void renameRecordType(@Nonnull String recordType, @Nonnull String newRecordTypeName) { metaDataStore.mutateMetaData((metaDataProto) -> MetaDataProtoEditor.renameRecordType(metaDataProto, recordType, newRecordTypeName)); } private static void assertDeprecated(@Nonnull RecordMetaData metaData, @Nonnull String recordType) { RecordType recordTypeObj = metaData.getRecordType(recordType); assertTrue(metaData.getUnionFieldForRecordType(recordTypeObj).getOptions().getDeprecated()); } @Test public void recordTypes() { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); version = metaData.getVersion(); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } // Add an existing record type. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version , metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.DescriptorProto newRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("MySimpleRecord") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .build(); MetaDataException e = assertThrows(MetaDataException.class, () -> addRecordType(newRecordType, Key.Expressions.field("rec_no"))); assertEquals(e.getMessage(), "Record type MySimpleRecord already exists"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); // version should not change context.commit(); } // Add a record type. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version , metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.DescriptorProto newRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("MyNewRecord") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .build(); addRecordType(newRecordType, Key.Expressions.field("rec_no")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecord")); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); assertEquals(version + 1, metaDataStore.getRecordMetaData().getRecordType("MyNewRecord").getSinceVersion().intValue()); context.commit(); } // The old local file descriptor does not have the new record type. Using it should fail. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.setLocalFileDescriptor(TestRecords1Proto.getDescriptor()); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData()); assertEquals("record type removed from union", e.getMessage()); context.commit(); } // Add a record type with index. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecord")); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.DescriptorProto newRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("MyNewRecordWithIndex") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .build(); addRecordType(newRecordType, Key.Expressions.field("rec_no"), new Index("MyNewRecordWithIndex$index", "rec_no")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecordWithIndex")); assertNotNull(metaDataStore.getRecordMetaData().getIndex("MyNewRecordWithIndex$index")); assertEquals(version + 2, metaDataStore.getRecordMetaData().getRecordType("MyNewRecordWithIndex").getSinceVersion().intValue()); assertEquals(version + 3, metaDataStore.getRecordMetaData().getVersion()); // +1 because of the index. context.commit(); } // Deprecate the just-added record types. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version + 3, metaDataStore.getRecordMetaData().getVersion()); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecordWithIndex")); deprecateRecordType("MyNewRecord"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertDeprecated(metaDataStore.getRecordMetaData(), "MyNewRecord"); assertEquals(version + 4, metaDataStore.getRecordMetaData().getVersion()); deprecateRecordType("MyNewRecordWithIndex"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecordWithIndex")); assertDeprecated(metaDataStore.getRecordMetaData(), "MyNewRecordWithIndex"); assertEquals(version + 5, metaDataStore.getRecordMetaData().getVersion()); context.commit(); } // Deprecate a record type from the original proto. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version + 5, metaDataStore.getRecordMetaData().getVersion()); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); deprecateRecordType("MySimpleRecord"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertDeprecated(metaDataStore.getRecordMetaData(), "MySimpleRecord"); assertEquals(version + 6, metaDataStore.getRecordMetaData().getVersion()); context.commit(); } // Deprecate a non-existent record type. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version + 6, metaDataStore.getRecordMetaData().getVersion()); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertDeprecated(metaDataStore.getRecordMetaData(), "MySimpleRecord"); MetaDataException e = assertThrows(MetaDataException.class, () -> deprecateRecordType("MyNonExistentRecord")); assertEquals(e.getMessage(), "Record type MyNonExistentRecord not found"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version + 6, metaDataStore.getRecordMetaData().getVersion()); context.commit(); } } @Test public void deprecateImportedRecordType() { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsImportProto.getDescriptor()); version = metaData.getVersion(); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } // Deprecate the record type try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(version, metaData.getVersion()); assertNotNull(metaData.getRecordType("MySimpleRecord")); assertNotNull(metaData.getRecordType("MyLongRecord")); MetaDataException e = assertThrows(MetaDataException.class, () -> deprecateRecordType("MySimpleRecord")); assertEquals("Record type MySimpleRecord not found", e.getMessage()); } } @Test public void deprecateWithNestedRecordType() { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsNestedAsRecord.getDescriptor()); version = metaData.getVersion(); metaDataStore.saveRecordMetaData(metaData); context.commit(); } // Deprecate OuterRecord try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(version, metaData.getVersion()); deprecateRecordType("OuterRecord"); RecordMetaData newMetaData = metaDataStore.getRecordMetaData(); assertThat(newMetaData.getVersion(), greaterThan(version)); assertDeprecated(newMetaData, "OuterRecord"); assertDeprecated(newMetaData, "InnerRecord"); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertThat(metaData.getVersion(), greaterThan(version)); assertDeprecated(metaData, "OuterRecord"); assertDeprecated(metaData, "InnerRecord"); } } @Test public void unionRecordTypes() { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); version = metaData.getVersion(); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } // Add a record type with default union name. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.DescriptorProto unionRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName(RecordMetaDataBuilder.DEFAULT_UNION_NAME) .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .build(); MetaDataException e = assertThrows(MetaDataException.class, () -> addRecordType(unionRecordType, Key.Expressions.field("rec_no"))); assertEquals(e.getMessage(), "Adding UNION record type not allowed"); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); // version must remain unchanged context.commit(); } // Add a record type with non-default union name but union usage. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.MessageOptions.Builder unionMessageOptions = DescriptorProtos.MessageOptions.newBuilder() .setExtension(RecordMetaDataOptionsProto.record, RecordMetaDataOptionsProto.RecordTypeOptions.newBuilder() .setUsage(RecordMetaDataOptionsProto.RecordTypeOptions.Usage.UNION).build()); DescriptorProtos.DescriptorProto unionRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("NonDefaultUnionRecord") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .setOptions(unionMessageOptions) .build(); MetaDataException e = assertThrows(MetaDataException.class, () -> addRecordType(unionRecordType, Key.Expressions.field("rec_no"))); assertEquals(e.getMessage(), "Adding UNION record type not allowed"); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); // version must remain unchanged context.commit(); } // Deprecate the record type union. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); MetaDataException e = assertThrows(MetaDataException.class, () -> deprecateRecordType(RecordMetaDataBuilder.DEFAULT_UNION_NAME)); assertEquals(e.getMessage(), "Cannot deprecate the union"); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); // version must remain unchanged context.commit(); } } @Test public void nonDefaultUnionRecordTypes() { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder().setRecords(TestRecords3Proto.getDescriptor()); metaDataBuilder.getOnlyRecordType().setPrimaryKey(Key.Expressions.concatenateFields("parent_path", "child_name")); RecordMetaData metaData = metaDataBuilder.getRecordMetaData(); version = metaData.getVersion(); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); } // Add a record type with the default union name to a record meta-data that has a non-default union name. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.DescriptorProto unionRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName(RecordMetaDataBuilder.DEFAULT_UNION_NAME) .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .build(); MetaDataException e = assertThrows(MetaDataException.class, () -> addRecordType(unionRecordType, Key.Expressions.field("rec_no"))); assertEquals(e.getMessage(), "Adding UNION record type not allowed"); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); // version must remain unchanged context.commit(); } // Add a second record type with non-default union name but union usage. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.MessageOptions.Builder unionMessageOptions = DescriptorProtos.MessageOptions.newBuilder() .setExtension(RecordMetaDataOptionsProto.record, RecordMetaDataOptionsProto.RecordTypeOptions.newBuilder() .setUsage(RecordMetaDataOptionsProto.RecordTypeOptions.Usage.UNION).build()); DescriptorProtos.DescriptorProto unionRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("SecondNonDefaultUnionRecord") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .setOptions(unionMessageOptions) .build(); MetaDataException e = assertThrows(MetaDataException.class, () -> addRecordType(unionRecordType, Key.Expressions.field("rec_no"))); assertEquals(e.getMessage(), "Adding UNION record type not allowed"); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); // version must remain unchanged context.commit(); } // Deprecate the record type union with non-default name. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); MetaDataException e = assertThrows(MetaDataException.class, () -> deprecateRecordType("UnionDescriptor")); assertEquals(e.getMessage(), "Cannot deprecate the union"); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); // version must remain unchanged context.commit(); } // Add a record type to a meta-data that has non-default union name. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version , metaDataStore.getRecordMetaData().getVersion()); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); DescriptorProtos.DescriptorProto newRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("MyNewRecord") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .build(); addRecordType(newRecordType, Key.Expressions.field("rec_no")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecord")); assertEquals(version + 1 , metaDataStore.getRecordMetaData().getVersion()); assertEquals(version + 1 , metaDataStore.getRecordMetaData().getRecordType("MyNewRecord").getSinceVersion().intValue()); context.commit(); } // Deprecate a record type from a meta-data that has non-default union name. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); deprecateRecordType("MyHierarchicalRecord"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); assertTrue(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("UnionDescriptor").findFieldByName("_MyHierarchicalRecord").getOptions().getDeprecated()); assertEquals(version + 2, metaDataStore.getRecordMetaData().getVersion()); // do not commit } // Validate that deprecation can happen when the type is fully qualified try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); deprecateRecordType(".com.apple.foundationdb.record.test3.MyHierarchicalRecord"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyHierarchicalRecord")); assertTrue(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("UnionDescriptor").findFieldByName("_MyHierarchicalRecord").getOptions().getDeprecated()); assertEquals(version + 2, metaDataStore.getRecordMetaData().getVersion()); context.commit(); } } private void addNestedRecordType(DescriptorProtos.DescriptorProto newRecordType) { metaDataStore.mutateMetaData((metaDataProto) -> MetaDataProtoEditor.addNestedRecordType(metaDataProto, newRecordType)); } @Test public void nestedRecordTypes() { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); version = metaData.getVersion(); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } // Adding an existing record type should fail try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version , metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.MessageOptions.Builder nestedMessageOptions = DescriptorProtos.MessageOptions.newBuilder() .setExtension(RecordMetaDataOptionsProto.record, RecordMetaDataOptionsProto.RecordTypeOptions.newBuilder() .setUsage(RecordMetaDataOptionsProto.RecordTypeOptions.Usage.NESTED).build()); DescriptorProtos.DescriptorProto newRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("MySimpleRecord") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .setOptions(nestedMessageOptions) .build(); MetaDataException e = assertThrows(MetaDataException.class, () -> addNestedRecordType(newRecordType)); assertEquals(e.getMessage(), "Record type MySimpleRecord already exists"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); // version should not change context.commit(); } // Add a nested record type. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.MessageOptions.Builder nestedMessageOptions = DescriptorProtos.MessageOptions.newBuilder() .setExtension(RecordMetaDataOptionsProto.record, RecordMetaDataOptionsProto.RecordTypeOptions.newBuilder() .setUsage(RecordMetaDataOptionsProto.RecordTypeOptions.Usage.NESTED).build()); DescriptorProtos.DescriptorProto newRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("MyNewNestedRecord") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .setOptions(nestedMessageOptions) .build(); // Use addRecordType should fail MetaDataException e = assertThrows(MetaDataException.class, () -> addRecordType(newRecordType, Key.Expressions.field("rec_no"))); assertEquals(e.getMessage(), "Use addNestedRecordType for adding NESTED record types"); // Use addNestedRecordType should succeed addNestedRecordType(newRecordType); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData().getRecordType("MyNewNestedRecord")); assertEquals(e.getMessage(), "Unknown record type MyNewNestedRecord"); assertNotNull(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("MyNewNestedRecord")); assertNull(metaDataStore.getRecordMetaData().getRecordsDescriptor() .findMessageTypeByName(RecordMetaDataBuilder.DEFAULT_UNION_NAME) .findFieldByName("_MyNewNestedRecord")); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); // addNestedRecordType is not idempotent! e = assertThrows(MetaDataException.class, () -> addNestedRecordType(newRecordType)); assertEquals(e.getMessage(), "Record type MyNewNestedRecord already exists"); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); context.commit(); } // Add nested type as a field try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("MyNewNestedRecord")); assertEquals(version + 1 , metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.FieldDescriptorProto field = DescriptorProtos.FieldDescriptorProto.newBuilder() .setName("newField") .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_MESSAGE) .setTypeName("MyNewNestedRecord") .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setNumber(10) .build(); addField("MySimpleRecord", field); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("MySimpleRecord").findFieldByName("newField")); assertEquals(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("MyNewNestedRecord"), metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("MySimpleRecord").findFieldByName("newField").getMessageType()); assertEquals(version + 2 , metaDataStore.getRecordMetaData().getVersion()); context.commit(); } // Deprecate a nested record type. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version + 2 , metaDataStore.getRecordMetaData().getVersion()); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("MyNewNestedRecord")); deprecateField("MySimpleRecord", "newField"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertTrue(metaDataStore.getRecordMetaData().getRecordsDescriptor() .findMessageTypeByName("MySimpleRecord") .findFieldByName("newField") .getOptions() .getDeprecated()); assertEquals(version + 3, metaDataStore.getRecordMetaData().getVersion()); context.commit(); } } @Test public void fields() { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); version = metaData.getVersion(); metaDataStore.saveRecordMetaData(metaData); context.commit(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); } // Add a new field try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version , metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.FieldDescriptorProto field = DescriptorProtos.FieldDescriptorProto.newBuilder() .setName("newField") .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setNumber(10) .build(); addField("MySimpleRecord", field); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("MySimpleRecord").findFieldByName("newField")); assertEquals(version + 1 , metaDataStore.getRecordMetaData().getVersion()); // Add it again should fail MetaDataException e = assertThrows(MetaDataException.class, () -> addField("MySimpleRecord", field)); assertEquals(e.getMessage(), "Field newField already exists in record type MySimpleRecord"); context.commit(); } // Add a field with non-existent record type try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.FieldDescriptorProto field = DescriptorProtos.FieldDescriptorProto.newBuilder() .setName("newFieldWithNonExistentRecordType") .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_MESSAGE) .setTypeName("NonExistentType") .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setNumber(10) .build(); MetaDataException e = assertThrows(MetaDataException.class, () -> addField("MySimpleRecord", field)); assertEquals(e.getMessage(), "Error converting from protobuf"); assertEquals(version + 1 , metaDataStore.getRecordMetaData().getVersion()); context.commit(); } // Deprecate field should fail if record type or field does not exist try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); MetaDataException e = assertThrows(MetaDataException.class, () -> deprecateField("NonExistentRecordType", "field")); assertEquals(e.getMessage(), "Record type NonExistentRecordType does not exist"); e = assertThrows(MetaDataException.class, () -> deprecateField("MySimpleRecord", "nonExistentField")); assertEquals(e.getMessage(), "Field nonExistentField not found in record type MySimpleRecord"); context.commit(); } // Deprecate a field try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("MySimpleRecord").findFieldByName("num_value_2")); assertFalse(metaDataStore.getRecordMetaData() .getRecordsDescriptor() .findMessageTypeByName("MySimpleRecord") .findFieldByName("num_value_2") .getOptions() .hasDeprecated()); assertEquals(version + 1 , metaDataStore.getRecordMetaData().getVersion()); deprecateField("MySimpleRecord", "num_value_2"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertTrue(metaDataStore.getRecordMetaData() .getRecordsDescriptor() .findMessageTypeByName("MySimpleRecord") .findFieldByName("num_value_2") .getOptions() .getDeprecated()); assertEquals(version + 2 , metaDataStore.getRecordMetaData().getVersion()); context.commit(); } // Deprecate the newly added field try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordsDescriptor().findMessageTypeByName("MySimpleRecord").findFieldByName("newField")); assertFalse(metaDataStore.getRecordMetaData() .getRecordsDescriptor() .findMessageTypeByName("MySimpleRecord") .findFieldByName("newField") .getOptions() .hasDeprecated()); assertEquals(version + 2 , metaDataStore.getRecordMetaData().getVersion()); deprecateField("MySimpleRecord", "newField"); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertTrue(metaDataStore.getRecordMetaData() .getRecordsDescriptor() .findMessageTypeByName("MySimpleRecord") .findFieldByName("newField") .getOptions() .getDeprecated()); assertEquals(version + 3 , metaDataStore.getRecordMetaData().getVersion()); context.commit(); } } @Test public void updateSchemaOptions() { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); version = metaDataStore.getRecordMetaData().getVersion(); context.commit(); } // Unset store record versions. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertTrue(metaDataStore.getRecordMetaData().isStoreRecordVersions()); metaDataStore.updateStoreRecordVersions(false); context.commit(); assertEquals(version + 1 , metaDataStore.getRecordMetaData().getVersion()); assertFalse(metaDataStore.getRecordMetaData().isStoreRecordVersions()); } // Set store record versions. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertFalse(metaDataStore.getRecordMetaData().isStoreRecordVersions()); metaDataStore.updateStoreRecordVersions(true); context.commit(); assertEquals(version + 2 , metaDataStore.getRecordMetaData().getVersion()); assertTrue(metaDataStore.getRecordMetaData().isStoreRecordVersions()); } // Enable split long records with default validator. It should fail. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertFalse(metaDataStore.getRecordMetaData().isSplitLongRecords()); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.enableSplitLongRecords()); assertEquals(e.getMessage(), "new meta-data splits long records"); context.commit(); assertEquals(version + 2 , metaDataStore.getRecordMetaData().getVersion()); assertTrue(metaDataStore.getRecordMetaData().isStoreRecordVersions()); } // Enable split long records try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.setEvolutionValidator(MetaDataEvolutionValidator.newBuilder().setAllowUnsplitToSplit(true).build()); assertFalse(metaDataStore.getRecordMetaData().isSplitLongRecords()); metaDataStore.enableSplitLongRecords(); context.commit(); assertEquals(version + 3 , metaDataStore.getRecordMetaData().getVersion()); assertTrue(metaDataStore.getRecordMetaData().isStoreRecordVersions()); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertTrue(metaDataStore.getRecordMetaData().isStoreRecordVersions()); assertEquals(version + 3 , metaDataStore.getRecordMetaData().getVersion()); context.commit(); } } @Test public void recordTypesWithOneOfUnion() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaDataBuilder metaData = RecordMetaData.newBuilder().setRecords(TestRecordsOneOfProto.getDescriptor()); final KeyExpression pkey = Key.Expressions.field("rec_no"); metaData.getRecordType("MySimpleRecord").setPrimaryKey(pkey); metaData.getRecordType("MyOtherRecord").setPrimaryKey(pkey); metaDataStore.saveRecordMetaData(metaData); context.commit(); } // Add a record type to oneOf. It should fail. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); DescriptorProtos.DescriptorProto newRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("MyNewRecord") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .build(); MetaDataException e = assertThrows(MetaDataException.class, () -> addRecordType(newRecordType, Key.Expressions.field("rec_no"))); assertEquals(e.getMessage(), "Adding record type to oneof is not allowed"); context.commit(); } } private enum TestProtoFiles { NO_UNION(TestNoUnionProto.getDescriptor()), DEFAULT_UNION(TestRecords1Proto.getDescriptor()), NON_DEFAULT_UNION(TestRecords4Proto.getDescriptor()); private Descriptors.FileDescriptor fileDescriptor; TestProtoFiles(Descriptors.FileDescriptor fileDescriptor) { this.fileDescriptor = fileDescriptor; } @Nonnull public Descriptors.FileDescriptor getFileDescriptor() { return fileDescriptor; } } @EnumSource(TestProtoFiles.class) @ParameterizedTest(name = "noUnion [protoFile = {0}]") public void noUnion(@Nonnull TestProtoFiles protoFile) { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.saveRecordMetaData(protoFile.getFileDescriptor()); context.commit(); version = metaDataStore.getRecordMetaData().getVersion(); } // Add a record type. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); DescriptorProtos.DescriptorProto newRecordType = DescriptorProtos.DescriptorProto.newBuilder() .setName("MyNewRecord") .addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_INT32) .setName("rec_no") .setNumber(1)) .build(); addRecordType(newRecordType, Key.Expressions.field("rec_no")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyNewRecord")); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); assertEquals(version + 1, metaDataStore.getRecordMetaData().getRecordType("MyNewRecord").getSinceVersion().intValue()); context.commit(); } } @ParameterizedTest(name = "noUnionUpdateRecords [repeatSaveOrDoUpdate = {0}]") @BooleanSource public void noUnionUpdateRecords(boolean repeatSaveOrDoUpdate) { int version; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.saveRecordMetaData(TestNoUnionProto.getDescriptor()); context.commit(); version = metaDataStore.getRecordMetaData().getVersion(); } // Update records with an evolved proto. A new record type is added earlier in the file. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); if (repeatSaveOrDoUpdate) { metaDataStore.updateRecords(TestNoUnionEvolvedProto.getDescriptor()); } else { metaDataStore.saveRecordMetaData(TestNoUnionEvolvedProto.getDescriptor()); } assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyOtherRecord")); assertEquals("_MySimpleRecord", metaDataStore.getRecordMetaData().getUnionDescriptor().findFieldByNumber(1).getName()); assertEquals("_MyOtherRecord", metaDataStore.getRecordMetaData().getUnionDescriptor().findFieldByNumber(2).getName()); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); context.commit(); } // Renaming a record type is not allowed try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); MetaDataException e; if (repeatSaveOrDoUpdate) { e = assertThrows(MetaDataException.class, () -> metaDataStore.updateRecords(TestNoUnionEvolvedRenamedRecordTypeProto.getDescriptor())); } else { e = assertThrows(MetaDataException.class, () -> metaDataStore.saveRecordMetaData(TestNoUnionEvolvedRenamedRecordTypeProto.getDescriptor())); } assertEquals("Record type MySimpleRecord removed", e.getMessage()); context.commit(); } } @ParameterizedTest(name = "noUnionImplicitUsage [repeatSaveOrDoUpdate = {0}]") @BooleanSource public void noUnionImplicitUsage(boolean repeatSaveOrDoUpdate) { int version; // MyOtherRecord has no explicit usage. The proto file has a union and does not include MyOtherRecord. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.saveRecordMetaData(TestRecordsImplicitUsageProto.getDescriptor()); context.commit(); version = metaDataStore.getRecordMetaData().getVersion(); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData().getRecordType("MyOtherRecord")); assertEquals(e.getMessage(), "Unknown record type MyOtherRecord"); } // The evolved proto no longer has a union. The record types with no explicit usage should now automatically show up in the union. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); assertEquals(version, metaDataStore.getRecordMetaData().getVersion()); if (repeatSaveOrDoUpdate) { metaDataStore.updateRecords(TestRecordsImplicitUsageNoUnionProto.getDescriptor()); } else { metaDataStore.saveRecordMetaData(TestRecordsImplicitUsageNoUnionProto.getDescriptor()); } assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MySimpleRecord")); assertNotNull(metaDataStore.getRecordMetaData().getRecordType("MyOtherRecord")); assertEquals(version + 1, metaDataStore.getRecordMetaData().getVersion()); context.commit(); } } @Test public void noUnionLocalFileDescriptor() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.saveRecordMetaData(TestNoUnionProto.getDescriptor()); context.commit(); } // The type cannot become NESTED in the local file descriptor try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.setLocalFileDescriptor(TestNoUnionEvolvedIllegalProto.getDescriptor()); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.getRecordMetaData()); assertEquals("record type removed from union", e.getMessage()); context.commit(); } // Deprecate a record type. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); deprecateRecordType(".com.apple.foundationdb.record.testnounion.MySimpleRecord"); context.commit(); } // Pass a local file descriptor and make sure MySimpleRecord's deprecated. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.setLocalFileDescriptor(TestNoUnionProto.getDescriptor()); Descriptors.FieldDescriptor deprecatedField = metaDataStore.getRecordMetaData().getUnionDescriptor().getFields().get(0); assertEquals("_MySimpleRecord", deprecatedField.getName()); assertTrue(deprecatedField.getOptions().getDeprecated()); context.commit(); } } /** * A basic test to verify that basic renaming works. */ @Test public void renameSimpleRecordType() { List<Index> simpleRecordIndexes; try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); simpleRecordIndexes = metaData.getRecordType("MySimpleRecord").getIndexes(); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType("MySimpleRecord", "MyNewSimpleRecord"); RecordMetaData metaData = metaDataStore.getRecordMetaData(); Descriptors.FieldDescriptor simpleField = metaData.getUnionDescriptor().findFieldByNumber(TestRecords1Proto.RecordTypeUnion._MYSIMPLERECORD_FIELD_NUMBER); assertSame(metaData.getRecordType("MyNewSimpleRecord").getDescriptor(), simpleField.getMessageType()); assertEquals("MyNewSimpleRecord", simpleField.getMessageType().getName()); assertEquals("_MyNewSimpleRecord", simpleField.getName()); assertEquals(ImmutableSet.of("MyNewSimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); MetaDataException e = assertThrows(MetaDataException.class, () -> metaData.getRecordType("MySimpleRecord")); assertEquals("Unknown record type MySimpleRecord", e.getMessage()); assertEquals(simpleRecordIndexes.stream().map(Index::getName).collect(Collectors.toSet()), metaData.getRecordType("MyNewSimpleRecord").getAllIndexes().stream().map(Index::getName).collect(Collectors.toSet())); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); Descriptors.FieldDescriptor simpleField = metaData.getUnionDescriptor().findFieldByNumber(TestRecords1Proto.RecordTypeUnion._MYSIMPLERECORD_FIELD_NUMBER); assertSame(metaData.getRecordType("MyNewSimpleRecord").getDescriptor(), simpleField.getMessageType()); assertEquals("MyNewSimpleRecord", simpleField.getMessageType().getName()); assertEquals("_MyNewSimpleRecord", simpleField.getName()); assertEquals(ImmutableSet.of("MyNewSimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); MetaDataException e = assertThrows(MetaDataException.class, () -> metaData.getRecordType("MySimpleRecord")); assertEquals("Unknown record type MySimpleRecord", e.getMessage()); assertEquals(simpleRecordIndexes.stream().map(Index::getName).collect(Collectors.toSet()), metaData.getRecordType("MyNewSimpleRecord").getAllIndexes().stream().map(Index::getName).collect(Collectors.toSet())); context.commit(); } } /** * Test whether fully qualifying a record type name works. */ @Test public void renameFullyQualifiedSimpleRecordType() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); // In theory, fully qualifying the name could work, but it doesn't as implemented. MetaDataException e = assertThrows(MetaDataException.class, () -> renameRecordType(".com.apple.foundationdb.record.test1.MySimpleRecord", "MyNewSimpleRecord")); assertThat(e.getMessage(), containsString("No record type found")); } } /** * Verify that if a record appears multiple times in the union that (1) all of the appearances are changed to the new * type and that (2) only one type is renamed. */ @Test public void renameSimpleWithMultipleUnionAppearances() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); metaDataStore.mutateMetaData(metaDataProtoBuilder -> metaDataProtoBuilder.getRecordsBuilder().getMessageTypeBuilderList().forEach(messageType -> { if (messageType.getName().equals(RecordMetaDataBuilder.DEFAULT_UNION_NAME)) { // Rename the current _MySimpleRecord field messageType.getFieldBuilderList().forEach(field -> { if (field.getName().equals("_MySimpleRecord")) { field.setName("_MySimpleRecord_v1"); } }); messageType.addField(DescriptorProtos.FieldDescriptorProto.newBuilder() .setLabel(DescriptorProtos.FieldDescriptorProto.Label.LABEL_OPTIONAL) .setType(DescriptorProtos.FieldDescriptorProto.Type.TYPE_MESSAGE) .setTypeName("." + TestRecords1Proto.MySimpleRecord.getDescriptor().getFullName()) .setName("_MySimpleRecord") .setNumber(messageType.getFieldBuilderList().stream().mapToInt(DescriptorProtos.FieldDescriptorProtoOrBuilder::getNumber).max().orElse(0) + 1) .build()); } }) ); metaData = metaDataStore.getRecordMetaData(); assertEquals(TestRecords1Proto.RecordTypeUnion._MYSIMPLERECORD_FIELD_NUMBER, metaData.getUnionDescriptor().findFieldByName("_MySimpleRecord_v1").getNumber()); assertEquals(metaData.getUnionFieldForRecordType(metaData.getRecordType("MySimpleRecord")).getNumber(), metaData.getUnionDescriptor().findFieldByName("_MySimpleRecord").getNumber()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType("MySimpleRecord", "MyNewSimpleRecord"); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(ImmutableSet.of("MyNewSimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); assertEquals(ImmutableSet.of("_MyNewSimpleRecord", "_MySimpleRecord_v1", "_MyOtherRecord"), metaData.getUnionDescriptor().getFields().stream().map(Descriptors.FieldDescriptor::getName).collect(Collectors.toSet())); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(ImmutableSet.of("MyNewSimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); assertEquals(ImmutableSet.of("_MyNewSimpleRecord", "_MySimpleRecord_v1", "_MyOtherRecord"), metaData.getUnionDescriptor().getFields().stream().map(Descriptors.FieldDescriptor::getName).collect(Collectors.toSet())); context.commit(); } } /** * This is somewhat of a weird case, but validate that if there is a union field for the new record type name that * looks like _NewRecordTypeName (for whatever reason), <em>don't</em> rename the union field to that because getting * the name looking right isn't worth throwing an error. */ @Test public void renameSimpleWhereUnionFieldIsAlreadyTaken() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); metaDataStore.mutateMetaData(metaDataProtoBuilder -> metaDataProtoBuilder.getRecordsBuilder().getMessageTypeBuilderList().forEach(messageType -> { if (messageType.getName().equals(RecordMetaDataBuilder.DEFAULT_UNION_NAME)) { // Rename the current _MySimpleRecord field messageType.getFieldBuilderList().forEach(field -> { if (field.getName().equals("_MyOtherRecord")) { field.setName("_MyNewSimpleRecord"); } }); } }) ); metaData = metaDataStore.getRecordMetaData(); assertEquals(TestRecords1Proto.RecordTypeUnion._MYSIMPLERECORD_FIELD_NUMBER, metaData.getUnionDescriptor().findFieldByName("_MySimpleRecord").getNumber()); assertEquals(TestRecords1Proto.RecordTypeUnion._MYSIMPLERECORD_FIELD_NUMBER, metaData.getUnionFieldForRecordType(metaData.getRecordType("MySimpleRecord")).getNumber()); assertEquals(TestRecords1Proto.RecordTypeUnion._MYOTHERRECORD_FIELD_NUMBER, metaData.getUnionDescriptor().findFieldByName("_MyNewSimpleRecord").getNumber()); assertEquals(TestRecords1Proto.RecordTypeUnion._MYOTHERRECORD_FIELD_NUMBER, metaData.getUnionFieldForRecordType(metaData.getRecordType("MyOtherRecord")).getNumber()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType("MySimpleRecord", "MyNewSimpleRecord"); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(ImmutableSet.of("MyNewSimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); assertEquals(ImmutableSet.of("_MySimpleRecord", "_MyNewSimpleRecord"), metaData.getUnionDescriptor().getFields().stream().map(Descriptors.FieldDescriptor::getName).collect(Collectors.toSet())); assertEquals("_MySimpleRecord", metaData.getUnionFieldForRecordType(metaData.getRecordType("MyNewSimpleRecord")).getName()); assertEquals("_MyNewSimpleRecord", metaData.getUnionFieldForRecordType(metaData.getRecordType("MyOtherRecord")).getName()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(ImmutableSet.of("MyNewSimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); assertEquals(ImmutableSet.of("_MySimpleRecord", "_MyNewSimpleRecord"), metaData.getUnionDescriptor().getFields().stream().map(Descriptors.FieldDescriptor::getName).collect(Collectors.toSet())); assertEquals("_MySimpleRecord", metaData.getUnionFieldForRecordType(metaData.getRecordType("MyNewSimpleRecord")).getName()); assertEquals("_MyNewSimpleRecord", metaData.getUnionFieldForRecordType(metaData.getRecordType("MyOtherRecord")).getName()); context.commit(); } } /** * Rename a {@code NESTED} record type. */ @Test public void renameNestedRecordType() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords4Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } // Rename a type that is marked as NESTED try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType("RestaurantTag", "RestoTag"); RecordMetaData metaData = metaDataStore.getRecordMetaData(); Descriptors.FileDescriptor recordsDescriptor = metaData.getRecordsDescriptor(); assertEquals(ImmutableSet.of("RestaurantReviewer", "ReviewerStats", "RestaurantReview", "RestoTag", "RestaurantRecord", "UnionDescriptor"), recordsDescriptor.getMessageTypes().stream().map(Descriptors.Descriptor::getName).collect(Collectors.toSet())); Descriptors.FieldDescriptor tagsField = metaData.getRecordType("RestaurantRecord").getDescriptor().findFieldByName("tags"); assertEquals("RestoTag", tagsField.getMessageType().getName()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); Descriptors.FileDescriptor recordsDescriptor = metaData.getRecordsDescriptor(); assertEquals(ImmutableSet.of("RestaurantReviewer", "ReviewerStats", "RestaurantReview", "RestoTag", "RestaurantRecord", "UnionDescriptor"), recordsDescriptor.getMessageTypes().stream().map(Descriptors.Descriptor::getName).collect(Collectors.toSet())); Descriptors.FieldDescriptor tagsField = metaData.getRecordType("RestaurantRecord").getDescriptor().findFieldByName("tags"); assertEquals("RestoTag", tagsField.getMessageType().getName()); } // Rename a nested type that doesn't have any explicit (record).usage try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType("ReviewerStats", "ReviewerStatistics"); RecordMetaData metaData = metaDataStore.getRecordMetaData(); Descriptors.FileDescriptor recordsDescriptor = metaData.getRecordsDescriptor(); assertEquals(ImmutableSet.of("RestaurantReviewer", "ReviewerStatistics", "RestaurantReview", "RestoTag", "RestaurantRecord", "UnionDescriptor"), recordsDescriptor.getMessageTypes().stream().map(Descriptors.Descriptor::getName).collect(Collectors.toSet())); Descriptors.FieldDescriptor stats = metaData.getRecordType("RestaurantReviewer").getDescriptor().findFieldByName("stats"); assertEquals("ReviewerStatistics", stats.getMessageType().getName()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); Descriptors.FileDescriptor recordsDescriptor = metaData.getRecordsDescriptor(); assertEquals(ImmutableSet.of("RestaurantReviewer", "ReviewerStatistics", "RestaurantReview", "RestoTag", "RestaurantRecord", "UnionDescriptor"), recordsDescriptor.getMessageTypes().stream().map(Descriptors.Descriptor::getName).collect(Collectors.toSet())); Descriptors.FieldDescriptor stats = metaData.getRecordType("RestaurantReviewer").getDescriptor().findFieldByName("stats"); assertEquals("ReviewerStatistics", stats.getMessageType().getName()); } } /** * Validate that the union can be renamed. */ @Test public void renameUnion() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } // Switch it from the default name to something else try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType(RecordMetaDataBuilder.DEFAULT_UNION_NAME, "RecordsOneUnion"); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals("RecordsOneUnion", metaData.getUnionDescriptor().getName()); assertEquals(ImmutableSet.of("MySimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals("RecordsOneUnion", metaData.getUnionDescriptor().getName()); assertEquals(ImmutableSet.of("MySimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); } // Switch it back to the default name try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType("RecordsOneUnion", RecordMetaDataBuilder.DEFAULT_UNION_NAME); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(RecordMetaDataBuilder.DEFAULT_UNION_NAME, metaData.getUnionDescriptor().getName()); assertEquals(ImmutableSet.of("MySimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(RecordMetaDataBuilder.DEFAULT_UNION_NAME, metaData.getUnionDescriptor().getName()); assertEquals(ImmutableSet.of("MySimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); } } private void unqualify(@Nonnull String packageName, @Nonnull String newPackage, @Nonnull DescriptorProtos.DescriptorProto.Builder messageTypeBuilder) { for (DescriptorProtos.FieldDescriptorProto.Builder fieldBuilder : messageTypeBuilder.getFieldBuilderList()) { if (fieldBuilder.hasTypeName()) { String withoutPrefix; if (fieldBuilder.getTypeName().startsWith("." + packageName + ".")) { withoutPrefix = fieldBuilder.getTypeName().substring(2 + packageName.length()); } else if (fieldBuilder.getTypeName().startsWith(packageName + ".")) { withoutPrefix = fieldBuilder.getTypeName().substring(1 + packageName.length()); } else { withoutPrefix = ""; } if (!withoutPrefix.isEmpty()) { String newTypeName; if (newPackage.isEmpty()) { newTypeName = withoutPrefix; } else { newTypeName = newPackage + "." + withoutPrefix; } fieldBuilder.setTypeName(newTypeName); } } } for (DescriptorProtos.DescriptorProto.Builder nestedTypeBuilder : messageTypeBuilder.getNestedTypeBuilderList()) { unqualify(packageName, newPackage, nestedTypeBuilder); } } private void unqualify(@Nonnull String newPackage, @Nonnull DescriptorProtos.FileDescriptorProto.Builder fileBuilder) { for (DescriptorProtos.DescriptorProto.Builder messageTypeBuilder : fileBuilder.getMessageTypeBuilderList()) { unqualify(fileBuilder.getPackage(), newPackage, messageTypeBuilder); } } @Test public void ambiguousTypes() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsDoubleNestedProto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); MetaDataProtoEditor.AmbiguousTypeNameException e = assertThrows(MetaDataProtoEditor.AmbiguousTypeNameException.class, () -> metaDataStore.mutateMetaData(protoBuilder -> { final DescriptorProtos.FileDescriptorProto.Builder fileBuilder = protoBuilder.getRecordsBuilder(); unqualify("", fileBuilder); MetaDataProtoEditor.renameRecordType(protoBuilder, "OtherRecord", "OtterRecord"); })); assertThat(e.getMessage(), containsString("might be of type .com.apple.foundationdb.record.test.doublenested.OtherRecord")); e = assertThrows(MetaDataProtoEditor.AmbiguousTypeNameException.class, () -> metaDataStore.mutateMetaData(protoBuilder -> { final DescriptorProtos.FileDescriptorProto.Builder fileBuilder = protoBuilder.getRecordsBuilder(); fileBuilder.getMessageTypeBuilderList().forEach(message -> { if (message.getName().equals("OtherRecord")) { unqualify(fileBuilder.getPackage(), "", message); } }); Optional<?> changedField = fileBuilder.getMessageTypeBuilderList().stream() .filter(message -> message.getName().equals("OtherRecord")) .flatMap(message -> message.getFieldBuilderList().stream()) .filter(field -> field.getTypeName().equals("OuterRecord")) .findAny(); assertTrue(changedField.isPresent()); MetaDataProtoEditor.renameRecordType(protoBuilder, "OuterRecord", "OtterRecord"); })); assertEquals("Field outer in message .com.apple.foundationdb.record.test.doublenested.OtherRecord of type OuterRecord might be of type .com.apple.foundationdb.record.test.doublenested.OuterRecord", e.getMessage()); } } /** * Make sure the type rename can go all the way down. */ @Test public void renameRecordTypeUsageInNested() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsDoubleNestedProto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType("OuterRecord", "OtterRecord"); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(ImmutableSet.of("OtterRecord", "MiddleRecord"), metaData.getRecordTypes().keySet()); final Descriptors.Descriptor otterDescriptor = metaData.getRecordsDescriptor().findMessageTypeByName("OtterRecord"); assertSame(otterDescriptor, metaData.getRecordType("OtterRecord").getDescriptor()); assertSame(otterDescriptor, metaData.getRecordType("OtterRecord").getDescriptor() .findNestedTypeByName("MiddleRecord") .findNestedTypeByName("InnerRecord") .findFieldByName("outer") .getMessageType()); assertSame(otterDescriptor, metaData.getRecordsDescriptor() .findMessageTypeByName("OtherRecord") .findFieldByName("outer") .getMessageType()); context.commit(); } } /** * If there are nested types with a similar name, do not rename the other nested types. */ @Test public void doNotRenameSimilarNestedType() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsDoubleNestedProto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.mutateMetaData(protoBuilder -> { // Unqualify the OtterRecord.MiddleRecord references in a way where they can be distinguished from globally-scoped MiddleRecords final DescriptorProtos.FileDescriptorProto.Builder fileBuilder = protoBuilder.getRecordsBuilder(); fileBuilder.getMessageTypeBuilderList().forEach(message -> { if (!message.getName().equals("MiddleRecord")) { message.getFieldBuilderList().forEach(field -> { if (field.getTypeName().equals(".com.apple.foundationdb.record.test.doublenested.OuterRecord.MiddleRecord")) { field.setTypeName("doublenested.OuterRecord.MiddleRecord"); } }); } }); // Verify that at least two fields are changed assertThat(fileBuilder.getMessageTypeBuilderList().stream() .flatMap(message -> message.getFieldBuilderList().stream()) .filter(field -> field.getTypeName().equals("doublenested.OuterRecord.MiddleRecord")) .count(), greaterThanOrEqualTo(1L)); MetaDataProtoEditor.renameRecordType(protoBuilder, "MiddleRecord", "MuddledRecord"); }); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertNotNull(metaData.getRecordsDescriptor().findMessageTypeByName("MuddledRecord")); assertEquals("MiddleRecord", metaData.getRecordsDescriptor().findMessageTypeByName("MuddledRecord").findFieldByName("other_middle").getMessageType().getName()); assertEquals("MiddleRecord", metaData.getRecordType("OuterRecord").getDescriptor().findFieldByName("middle").getMessageType().getName()); } } /** * Validate that a message type cannot be specified using "." syntax. */ @Test public void nestedRecordDefinition() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsDoubleNestedProto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); MetaDataException e = assertThrows(MetaDataException.class, () -> renameRecordType("OuterRecord.MiddleRecord", "OuterRecord.MiddlingRecord")); assertEquals("No record type found with name OuterRecord.MiddleRecord", e.getMessage()); } } /** * Validate that a message type cannot be renamed to something with a "." in it. */ @Test public void newNameSuggestsNestedType() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsDoubleNestedProto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); MetaDataException e = assertThrows(MetaDataException.class, () -> renameRecordType("OuterRecord", "OuterRecord.SomeNestedRecord")); // We rely on underlying Protobuf validation for this case assertEquals("Error converting from protobuf", e.getMessage()); } } /** * Validate that the identity rename works and changes nothing (except the meta-data version). */ @Test public void identityRename() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaDataProto.MetaData metaDataProto = metaDataStore.getRecordMetaData().toProto(); renameRecordType("MySimpleRecord", "MySimpleRecord"); assertThat(metaDataProto.getVersion(), lessThan(metaDataStore.getRecordMetaData().getVersion())); RecordMetaDataProto.MetaData mutatedMetaDataProto = metaDataStore.getRecordMetaData().toProto().toBuilder().setVersion(metaDataProto.getVersion()).build(); assertEquals(metaDataProto, mutatedMetaDataProto); MetaDataException e = assertThrows(MetaDataException.class, () -> renameRecordType("MyNonExistentRecord", "MyNonExistentRecord")); assertEquals("No record type found with name MyNonExistentRecord", e.getMessage()); context.commit(); } } /** * Validate that if a {@code NESTED} record with the same name as an imported record type * has its name changed, then the indexes do not change their record type. */ @Test public void dontRenameRecordTypeInIndexesWhenClashingWithImported() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsImportedAndNewProto.getDescriptor()); assertEquals(ImmutableSet.of("MySimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); assertSame(metaData.getRecordType("MySimpleRecord").getDescriptor(), TestRecords1Proto.MySimpleRecord.getDescriptor()); assertNotSame(metaData.getRecordType("MySimpleRecord").getDescriptor(), TestRecordsImportedAndNewProto.MySimpleRecord.getDescriptor()); assertSame(metaData.getRecordType("MyOtherRecord").getDescriptor(), TestRecordsImportedAndNewProto.MyOtherRecord.getDescriptor()); assertNotSame(metaData.getRecordType("MyOtherRecord").getDescriptor(), TestRecords1Proto.MyOtherRecord.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.mutateMetaData(metaDataProtoBuilder -> { MetaDataProtoEditor.renameRecordType(metaDataProtoBuilder, "MySimpleRecord", "MyNewSimpleRecord"); assertThat(metaDataProtoBuilder.getRecordTypesList().stream().map(RecordMetaDataProto.RecordType::getName).collect(Collectors.toList()), hasItem("MySimpleRecord")); for (RecordMetaDataProto.Index index : metaDataProtoBuilder.getIndexesList()) { assertThat(index.getRecordTypeList(), not(hasItem("MyNewSimpleRecord"))); } }); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(ImmutableSet.of("MySimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); assertEquals("MyNewSimpleRecord", metaData.getRecordsDescriptor() .findMessageTypeByName("MyOtherRecord") .findFieldByName("simple") .getMessageType() .getName()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = metaDataStore.getRecordMetaData(); assertEquals(ImmutableSet.of("MySimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); assertEquals("MyNewSimpleRecord", metaData.getRecordsDescriptor() .findMessageTypeByName("MyOtherRecord") .findFieldByName("simple") .getMessageType() .getName()); } } /** * Validate that if a {@code NESTED} record with the same name as an imported record type has its * name changed, then the record type in the record type list does not change. */ @Test public void dontRenameRecordTypeWhenClashingWithImported() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsImportedAndNewProto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); renameRecordType("MySimpleRecord", "MyLocalSimpleRecord"); // rename the nested record assertEquals(ImmutableSet.of("MySimpleRecord", "MyOtherRecord"), metaData.getRecordTypes().keySet()); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); MetaDataException e = assertThrows(MetaDataException.class, () -> renameRecordType("MyOtherRecord", "MySimpleRecord")); assertEquals("Cannot rename record type to MySimpleRecord as an imported record type of that name already exists", e.getMessage()); } } private static void validateInnerRecordsInRightPlaces(@Nonnull RecordMetaData metaData) { Descriptors.FileDescriptor recordsDescriptor = metaData.getRecordsDescriptor(); Descriptors.Descriptor innerRecord = recordsDescriptor.findMessageTypeByName("InnerRecord"); assertNotNull(innerRecord); Descriptors.Descriptor outerRecord = recordsDescriptor.findMessageTypeByName("OuterRecord"); assertNotNull(outerRecord); Descriptors.Descriptor middleRecord = outerRecord.findNestedTypeByName("MiddleRecord"); assertNotNull(middleRecord); Descriptors.Descriptor nestedInnerRecord = middleRecord.findNestedTypeByName("InnerRecord"); assertNotNull(nestedInnerRecord); Descriptors.FieldDescriptor innerField = outerRecord.findFieldByName("inner"); assertSame(nestedInnerRecord, innerField.getMessageType()); assertNotSame(innerRecord, innerField.getMessageType()); Descriptors.FieldDescriptor nestedInnerField = outerRecord.findFieldByName("inner"); assertSame(nestedInnerRecord, nestedInnerField.getMessageType()); assertNotSame(innerRecord, nestedInnerField.getMessageType()); } /** * Verify that if there is a nested type defined in a message, then changing a top-level record type * to that name won't cause any fields in that type to start pointing to the new type. */ @Test public void renameRecordTypeWithClashingNested() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecordsDoubleNestedProto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType("OtherRecord", "InnerRecord"); validateInnerRecordsInRightPlaces(metaDataStore.getRecordMetaData()); // Unqualify the inner record's field type name metaDataStore.mutateMetaData(metaDataProtoBuilder -> { DescriptorProtos.FileDescriptorProto.Builder fileBuilder = metaDataProtoBuilder.getRecordsBuilder(); DescriptorProtos.DescriptorProto.Builder outerBuilder = fileBuilder.getMessageTypeBuilderList().stream() .filter(messageBuilder -> messageBuilder.getName().equals("OuterRecord")) .findFirst() .get(); outerBuilder.getFieldBuilderList().stream() .filter(fieldBuilder -> fieldBuilder.getName().equals("inner")) .forEach(fieldBuilder -> fieldBuilder.setTypeName("MiddleRecord.InnerRecord")); outerBuilder.getNestedTypeBuilderList().stream() .filter(messageBuilder -> messageBuilder.getName().equals("MiddleRecord")) .flatMap(messageBuilder -> messageBuilder.getFieldBuilderList().stream()) .filter(fieldBuilder -> fieldBuilder.getName().equals("inner")) .forEach(fieldBuilder -> fieldBuilder.setTypeName("InnerRecord")); }); validateInnerRecordsInRightPlaces(metaDataStore.getRecordMetaData()); // do not commit } // Validate that this won't update a field of type OuterRecord.InnerRecord even if the field type // isn't fully qualified. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); metaDataStore.mutateMetaData(metaDataProtoBuilder -> { DescriptorProtos.FileDescriptorProto.Builder fileBuilder = metaDataProtoBuilder.getRecordsBuilder(); DescriptorProtos.DescriptorProto.Builder outerBuilder = fileBuilder.getMessageTypeBuilderList().stream() .filter(messageBuilder -> messageBuilder.getName().equals("OuterRecord")) .findFirst() .get(); outerBuilder.getFieldBuilderList().stream() .filter(fieldBuilder -> fieldBuilder.getName().equals("inner")) .forEach(fieldBuilder -> fieldBuilder.setTypeName("MiddleRecord.InnerRecord")); outerBuilder.getNestedTypeBuilderList().stream() .filter(messageBuilder -> messageBuilder.getName().equals("MiddleRecord")) .flatMap(messageBuilder -> messageBuilder.getFieldBuilderList().stream()) .filter(fieldBuilder -> fieldBuilder.getName().equals("inner")) .forEach(fieldBuilder -> fieldBuilder.setTypeName("InnerRecord")); }); renameRecordType("OtherRecord", "InnerRecord"); validateInnerRecordsInRightPlaces(metaDataStore.getRecordMetaData()); // do not commit } // Validate that if the field were unqualified in a way that introducing a new InnerRecord as a top-level record // would change the type that the Protobuf would not have built. try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); MetaDataException e = assertThrows(MetaDataException.class, () -> metaDataStore.mutateMetaData(metaDataProtoBuilder -> { DescriptorProtos.FileDescriptorProto.Builder fileBuilder = metaDataProtoBuilder.getRecordsBuilder(); fileBuilder.getMessageTypeBuilderList().stream() .filter(messageBuilder -> messageBuilder.getName().equals("OuterRecord")) .flatMap(messageBuilder -> messageBuilder.getFieldBuilderList().stream()) .filter(fieldBuilder -> fieldBuilder.getName().equals("inner")) .forEach(fieldBuilder -> fieldBuilder.setTypeName("InnerRecord")); })); assertEquals("Error converting from protobuf", e.getMessage()); assertNotNull(e.getCause()); assertThat(e.getCause(), instanceOf(Descriptors.DescriptorValidationException.class)); assertThat(e.getCause().getMessage(), containsString("\"InnerRecord\" is not defined.")); // do not commit } } /** * Verify that renaming a regular record type to the default union name throws an error. */ @Test public void dontRenameNonUnionToUnion() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); renameRecordType(RecordMetaDataBuilder.DEFAULT_UNION_NAME, "RecordOneUnion"); // to avoid conflicts MetaDataException e = assertThrows(MetaDataException.class, () -> renameRecordType("MySimpleRecord", RecordMetaDataBuilder.DEFAULT_UNION_NAME)); assertEquals("Cannot rename record type to the default union name", e.getMessage()); } } /** * Verify that renaming a non-existent record doesn't work. */ @Test public void tryRenameNonExistentRecord() { try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); RecordMetaData metaData = RecordMetaData.build(TestRecords1Proto.getDescriptor()); metaDataStore.saveRecordMetaData(metaData); context.commit(); } try (FDBRecordContext context = fdb.openContext()) { openMetaDataStore(context); MetaDataException e = assertThrows(MetaDataException.class, () -> renameRecordType("MyNonExistentRecord", "SomethingElse")); assertEquals("No record type found with name MyNonExistentRecord", e.getMessage()); } } }