blob: 70917110a52adb92d404bc4a848f6b413fd14477 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.parquet.thrift.struct;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import org.apache.parquet.thrift.projection.FieldsPath;
import org.apache.parquet.thrift.struct.ThriftType.BoolType;
import org.apache.parquet.thrift.struct.ThriftType.ByteType;
import org.apache.parquet.thrift.struct.ThriftType.DoubleType;
import org.apache.parquet.thrift.struct.ThriftType.EnumType;
import org.apache.parquet.thrift.struct.ThriftType.I16Type;
import org.apache.parquet.thrift.struct.ThriftType.I32Type;
import org.apache.parquet.thrift.struct.ThriftType.I64Type;
import org.apache.parquet.thrift.struct.ThriftType.StringType;
/**
* A checker for thrift struct to enforce its backward compatibility, returns compatibility report based on following rules:
* 1. Should not add new REQUIRED field in new thrift struct. Adding optional field is OK
* 2. Should not change field type for an existing field
* 3. Should not delete existing field
* 4. Should not make requirement type more restrictive for a field in new thrift struct
*/
public class CompatibilityChecker {
public CompatibilityReport checkCompatibility(ThriftType.StructType oldStruct, ThriftType.StructType newStruct) {
CompatibleCheckerVisitor visitor = new CompatibleCheckerVisitor();
newStruct.accept(visitor, new State(oldStruct,new FieldsPath()));
return visitor.getReport();
}
}
class CompatibilityReport {
boolean isCompatible = true;
boolean hasEmptyStruct = false;
List<String> messages = new ArrayList<String>();
public boolean isCompatible() {
return isCompatible;
}
public boolean hasEmptyStruct() {
return hasEmptyStruct;
}
public void fail(String message) {
messages.add(message);
isCompatible = false;
}
public void emptyStruct(String message) {
messages.add(message);
hasEmptyStruct = true;
}
public List<String> getMessages() {
return messages;
}
public String prettyMessages() {
return String.join("\n", messages);
}
@Override
public String toString() {
return "CompatibilityReport{" +
"isCompatible=" + isCompatible +
", hasEmptyStruct=" + hasEmptyStruct +
", messages=\n" + prettyMessages() +
'}';
}
}
class State {
FieldsPath path;
ThriftType oldType;
public State(ThriftType oldType, FieldsPath fieldsPath) {
this.path = fieldsPath;
this.oldType = oldType;
}
}
class CompatibleCheckerVisitor implements ThriftType.StateVisitor<Void, State> {
CompatibilityReport report = new CompatibilityReport();
public CompatibilityReport getReport() {
return report;
}
@Override
public Void visit(ThriftType.MapType mapType, State state) {
ThriftType.MapType oldMapType = ((ThriftType.MapType) state.oldType);
ThriftField oldKeyField = oldMapType.getKey();
ThriftField newKeyField = mapType.getKey();
ThriftField newValueField = mapType.getValue();
ThriftField oldValueField = oldMapType.getValue();
checkField(oldKeyField, newKeyField, state.path);
checkField(oldValueField, newValueField, state.path);
return null;
}
@Override
public Void visit(ThriftType.SetType setType, State state) {
ThriftType.SetType oldSetType = ((ThriftType.SetType) state.oldType);
ThriftField oldField = oldSetType.getValues();
ThriftField newField = setType.getValues();
checkField(oldField, newField, state.path);
return null;
}
@Override
public Void visit(ThriftType.ListType listType, State state) {
ThriftType.ListType currentOldType = ((ThriftType.ListType) state.oldType);
ThriftField oldField = currentOldType.getValues();
ThriftField newField = listType.getValues();
checkField(oldField, newField, state.path);
return null;
}
public void incompatible(String message, FieldsPath path) {
report.fail("at " + path + ":" +message);
}
private void checkField(ThriftField oldField, ThriftField newField, FieldsPath path) {
if (!newField.getType().getType().equals(oldField.getType().getType())) {
incompatible("type is not compatible: " + oldField.getType().getType() + " vs " + newField.getType().getType(), path);
return;
}
if (!newField.getName().equals(oldField.getName())) {
incompatible("field names are different: " + oldField.getName() + " vs " + newField.getName(), path);
return;
}
if (firstIsMoreRestirctive(newField.getRequirement(), oldField.getRequirement())) {
incompatible("new field is more restrictive: " + newField.getName(), path);
return;
}
newField.getType().accept(this, new State(oldField.getType(),path.push(newField)));
}
private boolean firstIsMoreRestirctive(ThriftField.Requirement firstReq, ThriftField.Requirement secReq) {
if (firstReq == ThriftField.Requirement.REQUIRED && secReq != ThriftField.Requirement.REQUIRED) {
return true;
} else {
return false;
}
}
@Override
public Void visit(ThriftType.StructType newStruct, State state) {
ThriftType.StructType oldStructType = ((ThriftType.StructType) state.oldType);
short oldMaxId = 0;
if (newStruct.getChildren().isEmpty()) {
report.emptyStruct("encountered an empty struct: " + state.path);
}
for (ThriftField oldField : oldStructType.getChildren()) {
short fieldId = oldField.getFieldId();
if (fieldId > oldMaxId) {
oldMaxId = fieldId;
}
ThriftField newField = newStruct.getChildById(fieldId);
if (newField == null) {
incompatible("can not find index in new Struct: " + fieldId + " in " + newStruct, state.path);
return null;
}
checkField(oldField, newField, state.path);
}
//check for new added
for (ThriftField newField : newStruct.getChildren()) {
//can not add required
if (newField.getRequirement() != ThriftField.Requirement.REQUIRED)
continue;//can add optional field
short newFieldId = newField.getFieldId();
if (newFieldId > oldMaxId) {
incompatible("new required field " + newField.getName() + " is added", state.path);
return null;
}
if (newFieldId < oldMaxId && oldStructType.getChildById(newFieldId) == null) {
incompatible("new required field " + newField.getName() + " is added", state.path);
return null;
}
}
return null;
}
@Override
public Void visit(EnumType enumType, State state) {
return null;
}
@Override
public Void visit(BoolType boolType, State state) {
return null;
}
@Override
public Void visit(ByteType byteType, State state) {
return null;
}
@Override
public Void visit(DoubleType doubleType, State state) {
return null;
}
@Override
public Void visit(I16Type i16Type, State state) {
return null;
}
@Override
public Void visit(I32Type i32Type, State state) {
return null;
}
@Override
public Void visit(I64Type i64Type, State state) {
return null;
}
@Override
public Void visit(StringType stringType, State state) {
return null;
}
}