diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidator.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidator.java
index 604dcc5ce3..d3e41581a0 100644
--- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidator.java
+++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidator.java
@@ -100,6 +100,8 @@ public class MetaDataEvolutionValidator {
private final boolean allowNoVersionChange;
private final boolean allowNoSinceVersion;
private final boolean allowFieldRenames;
+ private final boolean allowDeprecatedFieldRenames;
+ private final boolean allowUndeprecatingFields;
private final boolean allowIndexRebuilds;
private final boolean allowMissingFormerIndexNames;
private final boolean allowOlderFormerIndexAddedVersions;
@@ -111,6 +113,8 @@ private MetaDataEvolutionValidator() {
this.allowNoVersionChange = false;
this.allowNoSinceVersion = false;
this.allowFieldRenames = false;
+ this.allowDeprecatedFieldRenames = false;
+ this.allowUndeprecatingFields = false;
this.allowIndexRebuilds = false;
this.allowMissingFormerIndexNames = false;
this.allowOlderFormerIndexAddedVersions = false;
@@ -123,6 +127,8 @@ private MetaDataEvolutionValidator(@Nonnull Builder builder) {
this.allowNoVersionChange = builder.allowNoVersionChange;
this.allowNoSinceVersion = builder.allowNoSinceVersion;
this.allowFieldRenames = builder.allowFieldRenames;
+ this.allowDeprecatedFieldRenames = builder.allowDeprecatedFieldRenames;
+ this.allowUndeprecatingFields = builder.allowUndeprecatingFields;
this.allowIndexRebuilds = builder.allowIndexRebuilds;
this.allowMissingFormerIndexNames = builder.allowMissingFormerIndexNames;
this.allowOlderFormerIndexAddedVersions = builder.allowOlderFormerIndexAddedVersions;
@@ -271,13 +277,26 @@ private void validateProtoSyntax(@Nonnull Descriptors.Descriptor oldDescriptor,
private void validateField(@Nonnull FieldDescriptor oldFieldDescriptor, @Nonnull FieldDescriptor newFieldDescriptor,
@Nonnull Set
+ * Note that this is a strictly more permissive setting than + * {@linkplain #allowsDeprecatedFieldRenames() allowing deprecated field renames}. If this is set to {@code true}, + * then that other setting is effectively ignored. + *
+ * * @return whether this validator allows field names to change + * @see #allowsDeprecatedFieldRenames() */ public boolean allowsFieldRenames() { return allowFieldRenames; } + /** + * Whether this validator allows deprecated fields to be renamed. If this is {@code true}, then + * this behaves in an analogous fashion to if {@link #allowsFieldRenames()} returns {@code true}, but + * it only allows the change if the field has been deprecated. In general, removing fields is + * not allowed as it can result in unknown fields during Protobuf deserialization. For that reason, + * deprecating fields should be preferred to deleting them. Once a field is deprecated and no longer + * actively accessed, further modifications to the name of the now unused field may be safe. + * Note that this option will result in accepting a meta-data change if a field is renamed + * and deprecated in the same change. Additionally, if this validator {@link #allowsUndeprecatingFields()}, + * then this also allows deprecated fields to be renamed in the same change in which they are + * marked as no longer deprecated. + * + *+ * One reason that a user may want to allow renaming deprecating fields is to support recreating + * an existing field. For example, there are certain incompatible changes that a user may want + * to make to a field (e.g., modifying its Protobuf type from {@code sfixed32} to {@code sfixed64}). + * To accomplish this, the user can deprecate the old field, rename it, and then create a new field + * with the original field's name. This is not an operation that is without cost (the user must, + * for example, delete or at least bump the {@linkplain Index#getLastModifiedVersion() last modified version} of + * any index referencing the field, and they may need to populate the new field with data from the + * deprecated field), but it may be something that a user wants to do while iterating on a test + * meta-data. However, this should generally be avoided in production use cases. + *
+ * + *+ * Note that this is a strictly less permissive setting than more generally + * {@linkplain #allowsFieldRenames() allowing field renames}. If that configuration is set to {@code true}, then + * this configuration option is effectively ignored. + *
+ * + * @return whether this validator allows deprecated fields to be renamed + * @see #allowsFieldRenames() + */ + public boolean allowsDeprecatedFieldRenames() { + return allowDeprecatedFieldRenames; + } + + /** + * Whether this validator allows any kind of field renames. It captures whether either this validator + * {@link #allowsFieldRenames()} or {@link #allowsDeprecatedFieldRenames()}. If this is false, then + * this validator will throw if any kind of field changes its name. The exact set of field renames + * that are allowed will depend on the values of the other two configuration parameters. + * + * @return whether this validator allows any kind of field renames + * @see #allowsFieldRenames() + * @see #allowsDeprecatedFieldRenames() + */ + public boolean allowsAnyFieldRenames() { + return allowFieldRenames || allowDeprecatedFieldRenames; + } + + /** + * Whether this validator allows fields that were previously deprecated to be marked as not + * deprecated. Deprecating a field should generally be a one-way operation, as it represents + * deleting a field from the meta-data. By default, validators will therefore reject any meta-data + * change where a field goes from deprecated to not deprecated. However, especially when iterating + * on a meta-data used only in testing, there may be times when a user wants to un-delete a field. + * This option provides the flexibility for the user to allow that. + * + * @return whether this validator allows fields to be undeprecated + */ + public boolean allowsUndeprecatingFields() { + return allowUndeprecatingFields; + } + /** * Whether this validator allows new meta-data that require existing indexes to be rebuilt. * This can happen if an index is modified and its {@linkplain Index#getLastModifiedVersion() last modified version} @@ -905,6 +996,8 @@ public static class Builder { private boolean allowNoVersionChange; private boolean allowNoSinceVersion; private boolean allowFieldRenames; + private boolean allowDeprecatedFieldRenames; + private boolean allowUndeprecatingFields; private boolean allowIndexRebuilds; private boolean allowMissingFormerIndexNames; private boolean allowOlderFormerIndexAddedVersions; @@ -916,6 +1009,8 @@ private Builder(@Nonnull MetaDataEvolutionValidator validator) { this.allowNoVersionChange = validator.allowNoVersionChange; this.allowNoSinceVersion = validator.allowNoSinceVersion; this.allowFieldRenames = validator.allowFieldRenames; + this.allowDeprecatedFieldRenames = validator.allowDeprecatedFieldRenames; + this.allowUndeprecatingFields = validator.allowUndeprecatingFields; this.allowIndexRebuilds = validator.allowIndexRebuilds; this.allowMissingFormerIndexNames = validator.allowMissingFormerIndexNames; this.allowOlderFormerIndexAddedVersions = validator.allowOlderFormerIndexAddedVersions; @@ -1020,6 +1115,54 @@ public boolean allowsFieldRenames() { return allowFieldRenames; } + /** + * Set whether the validator will allow deprecated fields to be renamed. + * + * @param allowDeprecatedFieldRenames whether the validator will allow deprecated fields to be renamed + * @return this builder + * @see MetaDataEvolutionValidator#allowsDeprecatedFieldRenames() + */ + @CanIgnoreReturnValue + @Nonnull + public Builder setAllowDeprecatedFieldRenames(boolean allowDeprecatedFieldRenames) { + this.allowDeprecatedFieldRenames = allowDeprecatedFieldRenames; + return this; + } + + /** + * Whether the validator will allow deprecated fields to be renamed. + * + * @return whether the validator will allow deprecated fields to be renamed + * @see MetaDataEvolutionValidator#allowsDeprecatedFieldRenames() + */ + public boolean allowsDeprecatedFieldRenames() { + return allowDeprecatedFieldRenames; + } + + /** + * Set whether the validator will allow deprecated fields to be marked as not deprecated. + * + * @param allowUndeprecatingFields whether the validator will allow deprecated fields to be un-deprecated + * @return this builder + * @see MetaDataEvolutionValidator#allowsUndeprecatingFields() + */ + @CanIgnoreReturnValue + @Nonnull + public Builder setAllowUndeprecatingFields(boolean allowUndeprecatingFields) { + this.allowUndeprecatingFields = allowUndeprecatingFields; + return this; + } + + /** + * Whether the validator will allow deprecated fields to be marked as not deprecated. + * + * @return whether the validator will allow deprecated fields to be un-deprecated + * @see MetaDataEvolutionValidator#allowsUndeprecatingFields() + */ + public boolean allowsUndeprecatingFields() { + return allowUndeprecatingFields; + } + /** * Set whether the validator will allow changes to indexes that require rebuilds. * @param allowIndexRebuilds whether the validator will allow changes to indexes that require rebuilds diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidatorBuilderTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidatorBuilderTest.java index 69e1e1f362..f707e5bb77 100644 --- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidatorBuilderTest.java +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidatorBuilderTest.java @@ -110,6 +110,22 @@ void allowFieldRenames() { MetaDataEvolutionValidator::allowsFieldRenames); } + @Test + void allowDeprecatedFieldRenames() { + testSettingBooleanOption("allowDeprecatedFieldRenames", + MetaDataEvolutionValidator.Builder::setAllowDeprecatedFieldRenames, + MetaDataEvolutionValidator.Builder::allowsDeprecatedFieldRenames, + MetaDataEvolutionValidator::allowsDeprecatedFieldRenames); + } + + @Test + void allowUndeprecatingFields() { + testSettingBooleanOption("allowUndeprecatingFields", + MetaDataEvolutionValidator.Builder::setAllowUndeprecatingFields, + MetaDataEvolutionValidator.Builder::allowsUndeprecatingFields, + MetaDataEvolutionValidator::allowsUndeprecatingFields); + } + @Test void allowIndexRebuilds() { testSettingBooleanOption("allowIndexRebuilds", diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidatorTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidatorTest.java index b05236004d..d0339971d0 100644 --- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidatorTest.java +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/MetaDataEvolutionValidatorTest.java @@ -27,6 +27,7 @@ import com.apple.foundationdb.record.RecordMetaDataOptionsProto; import com.apple.foundationdb.record.RecordMetaDataProto; import com.apple.foundationdb.record.TestRecords1Proto; +import com.apple.foundationdb.record.TestRecords4Proto; import com.apple.foundationdb.record.TestRecordsEnumProto; import com.apple.foundationdb.record.TestRecordsIdenticalTypesProto; import com.apple.foundationdb.record.TestRecordsWithHeaderProto; @@ -44,13 +45,17 @@ import com.apple.foundationdb.record.provider.common.text.TextTokenizer; import com.apple.foundationdb.record.provider.foundationdb.IndexMaintainerFactoryRegistryImpl; import com.apple.foundationdb.tuple.Tuple; +import com.apple.test.ParameterizedTestUtils; import com.google.protobuf.ByteString; import com.google.protobuf.DescriptorProtos; import com.google.protobuf.Descriptors; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FileDescriptor; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -63,6 +68,7 @@ import java.util.function.Consumer; import java.util.function.UnaryOperator; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -111,6 +117,95 @@ static void assertInvalid(@Nonnull String errMsg, @Nonnull FileDescriptor oldFil assertInvalid(errMsg, oldFileDescriptor.findMessageTypeByName(RecordMetaDataBuilder.DEFAULT_UNION_NAME), newFileDescriptor.findMessageTypeByName(RecordMetaDataBuilder.DEFAULT_UNION_NAME)); } + private static class FieldRenameChecker { + @Nonnull + private final MetaDataEvolutionValidator baseValidator; + @Nonnull + private final MetaDataEvolutionValidator noRenamesValidator; + @Nonnull + private final MetaDataEvolutionValidator deprecatedOnlyValidator; + @Nonnull + private final MetaDataEvolutionValidator anyRenameValidator; + @Nonnull + private final MetaDataEvolutionValidator allRenamesValidator; + + public FieldRenameChecker(MetaDataEvolutionValidator baseValidator) { + this.baseValidator = baseValidator; + final MetaDataEvolutionValidator.Builder builder = baseValidator.asBuilder(); + + noRenamesValidator = builder + .setAllowDeprecatedFieldRenames(false) + .setAllowFieldRenames(false) + .build(); + assertFalse(noRenamesValidator.allowsAnyFieldRenames()); + assertFalse(noRenamesValidator.allowsDeprecatedFieldRenames()); + assertFalse(noRenamesValidator.allowsFieldRenames()); + + deprecatedOnlyValidator = builder + .setAllowDeprecatedFieldRenames(true) + .build(); + assertTrue(deprecatedOnlyValidator.allowsAnyFieldRenames()); + assertTrue(deprecatedOnlyValidator.allowsDeprecatedFieldRenames()); + assertFalse(deprecatedOnlyValidator.allowsFieldRenames()); + + anyRenameValidator = builder + .setAllowDeprecatedFieldRenames(false) + .setAllowFieldRenames(true) + .build(); + assertTrue(anyRenameValidator.allowsAnyFieldRenames()); + assertFalse(anyRenameValidator.allowsDeprecatedFieldRenames()); + assertTrue(anyRenameValidator.allowsFieldRenames()); + + // This should behave the same as anyRenameValidator, but it is included for completeness + allRenamesValidator = builder + .setAllowDeprecatedFieldRenames(true) + .build(); + assertTrue(allRenamesValidator.allowsAnyFieldRenames()); + assertTrue(allRenamesValidator.allowsDeprecatedFieldRenames()); + assertTrue(allRenamesValidator.allowsFieldRenames()); + } + + @Nonnull + public MetaDataEvolutionValidator getBaseValidator() { + return baseValidator; + } + + public void assertInvalidRenaming(@Nonnull String errMsg, boolean deprecatedOnly, @Nonnull RecordMetaData oldMetaData, @Nonnull RecordMetaData newMetaData) { + assertInvalid("field renamed", noRenamesValidator, oldMetaData, newMetaData); + assertInvalid(deprecatedOnly ? errMsg : "field renamed", deprecatedOnlyValidator, oldMetaData, newMetaData); + assertInvalid(errMsg, anyRenameValidator, oldMetaData, newMetaData); + assertInvalid(errMsg, allRenamesValidator, oldMetaData, newMetaData); + } + + public void assertValidRenaming(boolean deprecatedOnly, @Nonnull RecordMetaData oldMetaData, @Nonnull RecordMetaData newMetaData) { + assertInvalid("field renamed", noRenamesValidator, oldMetaData, newMetaData); + if (deprecatedOnly) { + deprecatedOnlyValidator.validate(oldMetaData, newMetaData); + } else { + assertInvalid("field renamed", deprecatedOnlyValidator, oldMetaData, newMetaData); + } + anyRenameValidator.validate(oldMetaData, newMetaData); + allRenamesValidator.validate(oldMetaData, newMetaData); + } + + public void assertValidRenaming(boolean deprecatedOnly, @Nonnull FileDescriptor oldFileDescriptor, @Nonnull FileDescriptor newFileDescriptor) { + assertValidRenaming(deprecatedOnly, oldFileDescriptor.findMessageTypeByName(RecordMetaDataBuilder.DEFAULT_UNION_NAME), newFileDescriptor.findMessageTypeByName(RecordMetaDataBuilder.DEFAULT_UNION_NAME)); + } + + public void assertValidRenaming(boolean deprecatedOnly, @Nonnull Descriptor oldUnionDescriptor, @Nonnull Descriptor newUnionDescriptor) { + assertInvalid("field renamed", noRenamesValidator, oldUnionDescriptor, newUnionDescriptor); + if (deprecatedOnly) { + deprecatedOnlyValidator.validateUnion(oldUnionDescriptor, newUnionDescriptor); + } else { + assertInvalid("field renamed", deprecatedOnlyValidator, oldUnionDescriptor, newUnionDescriptor); + } + anyRenameValidator.validateUnion(oldUnionDescriptor, newUnionDescriptor); + allRenamesValidator.validateUnion(oldUnionDescriptor, newUnionDescriptor); + } + } + + private final FieldRenameChecker fieldRenameChecker = new FieldRenameChecker(validator); + @Test void doNotChangeVersion() { // Check if a naive removal of the index without updating the version is checked @@ -218,6 +313,10 @@ static DescriptorProtos.FieldDescriptorProto.Builder addField(@Nonnull Descripto .setNumber(maxFieldNumber + 1); } + static void deprecateField(@Nonnull DescriptorProtos.FieldDescriptorProto.Builder field) { + field.getOptionsBuilder().setDeprecated(true); + } + @Test void changeSplitLongRecords() { RecordMetaData metaData1 = RecordMetaData.build(TestRecords1Proto.getDescriptor()); @@ -752,29 +851,112 @@ void dropField() { void renameField() { FileDescriptor updatedFile = mutateField("MySimpleRecord", "num_value_2", field -> field.setName("num_value_too")); + fieldRenameChecker.assertValidRenaming(false, TestRecords1Proto.getDescriptor(), updatedFile); - assertFalse(validator.allowsFieldRenames()); - assertInvalid("field renamed", TestRecords1Proto.getDescriptor(), updatedFile); RecordMetaData metaData1 = RecordMetaData.build(TestRecords1Proto.getDescriptor()); RecordMetaData metaData2 = replaceRecordsDescriptor(metaData1, updatedFile); - assertInvalid("field renamed", metaData1, metaData2); + fieldRenameChecker.assertValidRenaming(false, metaData1, metaData2); + } - final MetaDataEvolutionValidator laxerValidator = validator.asBuilder() - .setAllowFieldRenames(true) - .build(); - assertTrue(laxerValidator.allowsFieldRenames()); - laxerValidator.validateUnion(TestRecords1Proto.RecordTypeUnion.getDescriptor(), updatedFile.findMessageTypeByName(RecordMetaDataBuilder.DEFAULT_UNION_NAME)); + @Test + void renameDeprecatedField() { + FileDescriptor deprecatedFile = mutateField("MySimpleRecord", "num_value_2", + MetaDataEvolutionValidatorTest::deprecateField); + FileDescriptor renamedFile = mutateField("MySimpleRecord", "num_value_2", deprecatedFile, + field -> field.setName("num_value_too")); + fieldRenameChecker.assertValidRenaming(true, deprecatedFile, renamedFile); - laxerValidator.validate(metaData1, metaData2); + RecordMetaData metaData1 = RecordMetaData.build(deprecatedFile); + RecordMetaData metaData2 = replaceRecordsDescriptor(metaData1, renamedFile); + fieldRenameChecker.assertValidRenaming(true, metaData1, metaData2); } @Test - void renameFieldWithIndex() { + void renameFieldWhenMarkingDeprecated() { + FileDescriptor updatedFile = mutateField("MySimpleRecord", "num_value_2", field -> { + deprecateField(field); + field.setName("num_value_too"); + }); + fieldRenameChecker.assertValidRenaming(true, TestRecords1Proto.getDescriptor(), updatedFile); + + RecordMetaData metaData1 = RecordMetaData.build(TestRecords1Proto.getDescriptor()); + RecordMetaData metaData2 = replaceRecordsDescriptor(metaData1, updatedFile); + fieldRenameChecker.assertValidRenaming(true, metaData1, metaData2); + } + + @Test + void renameFieldWhenUndeprecating() { + // Change the field name at the same time we mark it as not deprecated + FileDescriptor deprecatedFile = mutateField("MySimpleRecord", "num_value_2", + MetaDataEvolutionValidatorTest::deprecateField); + FileDescriptor renamedFile = mutateField("MySimpleRecord", "num_value_2", deprecatedFile, + field -> field.clearOptions().setName("num_value_too")); + RecordMetaData metaData1 = RecordMetaData.build(deprecatedFile); + RecordMetaData metaData2 = replaceRecordsDescriptor(metaData1, renamedFile); + + // Under default settings, this is still not allowed (because we ban undeprecating fields) + assertFalse(fieldRenameChecker.getBaseValidator().allowsUndeprecatingFields()); + fieldRenameChecker.assertInvalidRenaming("field is no longer deprecated", true, metaData1, metaData2); + + // If un-deprecating fields is okay, then we get a valid renaming + final FieldRenameChecker laxerFieldRenameChecker = new FieldRenameChecker(fieldRenameChecker.getBaseValidator().asBuilder() + .setAllowUndeprecatingFields(true) + .build()); + assertTrue(laxerFieldRenameChecker.getBaseValidator().allowsUndeprecatingFields()); + laxerFieldRenameChecker.assertValidRenaming(true, metaData1, metaData2); + } + + @Test + void renameMixOfDeprecatedAndUndeprecatedFields() { + FileDescriptor deprecatedFile = mutateField("RestaurantTag", "weight", TestRecords4Proto.getDescriptor(), + MetaDataEvolutionValidatorTest::deprecateField); + deprecatedFile = mutateField("ReviewerStats", "hometown", deprecatedFile, + MetaDataEvolutionValidatorTest::deprecateField); + assertTrue(deprecatedFile.findMessageTypeByName("RestaurantTag").findFieldByName("weight").getOptions().getDeprecated()); + assertTrue(deprecatedFile.findMessageTypeByName("ReviewerStats").findFieldByName("hometown").getOptions().getDeprecated()); + final RecordMetaData metaData1 = RecordMetaData.build(deprecatedFile); + + // Rename one deprecated field + FileDescriptor renamedFile = mutateField("ReviewerStats", "hometown", deprecatedFile, + field -> field.setName("origin")); + assertTrue(renamedFile.findMessageTypeByName("ReviewerStats").findFieldByName("origin").getOptions().getDeprecated()); + fieldRenameChecker.assertValidRenaming(true, deprecatedFile.findMessageTypeByName("UnionDescriptor"), renamedFile.findMessageTypeByName("UnionDescriptor")); + fieldRenameChecker.assertValidRenaming(true, metaData1, replaceRecordsDescriptor(metaData1, renamedFile)); + + // Rename another deprecated field + renamedFile = mutateField("RestaurantTag", "weight", renamedFile, + field -> field.setName("weighting")); + assertTrue(renamedFile.findMessageTypeByName("RestaurantTag").findFieldByName("weighting").getOptions().getDeprecated()); + fieldRenameChecker.assertValidRenaming(true, deprecatedFile.findMessageTypeByName("UnionDescriptor"), renamedFile.findMessageTypeByName("UnionDescriptor")); + fieldRenameChecker.assertValidRenaming(true, metaData1, replaceRecordsDescriptor(metaData1, renamedFile)); + + // Rename a non deprecated field. Now, the fieldRenameChecker should only consider this valid if it allows + // all field renames + renamedFile = mutateField("RestaurantRecord", "reviews", renamedFile, + field -> field.setName("review_list")); + assertFalse(renamedFile.findMessageTypeByName("RestaurantRecord").findFieldByName("review_list").getOptions().getDeprecated()); + fieldRenameChecker.assertValidRenaming(false, deprecatedFile.findMessageTypeByName("UnionDescriptor"), renamedFile.findMessageTypeByName("UnionDescriptor")); + fieldRenameChecker.assertValidRenaming(false, metaData1, replaceRecordsDescriptor(metaData1, renamedFile)); + } + + @Nonnull + static Stream