You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@avro.apache.org by dk...@apache.org on 2020/05/21 15:25:00 UTC

[avro] branch master updated: AVRO-2648: Incorrect validation of numeric default values

This is an automated email from the ASF dual-hosted git repository.

dkulp pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/avro.git


The following commit(s) were added to refs/heads/master by this push:
     new 7347e25  AVRO-2648: Incorrect validation of numeric default values
7347e25 is described below

commit 7347e25381f170a28d6ab081e08ddf4b3354b8fe
Author: Jeffrey Mullins <jm...@bloomberg.net>
AuthorDate: Fri Dec 6 10:54:00 2019 -0500

    AVRO-2648: Incorrect validation of numeric default values
    
    Validation of numeric default values in Java is incorrect and results
    in API inconsistencies. Consider the following examples:
    
    Double values as int field default values:
    public void testDoubleAsIntDefaultValue() {
      Schema.Field field = new Schema.Field("myField",
              Schema.create(Schema.Type.INT), "doc", 1.1);
      field.hasDefaultValue(); // true
      field.defaultValue(); // internal DoubleNode (1.1)
      field.defaultVal(); // null
      GenericData.get().getDefaultValue(field); // Integer (1)
    
      field = new Schema.Field("myField",
              Schema.create(Schema.Type.INT), "doc", 1.0);
      field.hasDefaultValue(); // true
      field.defaultValue(); // internal DoubleNode (1.0)
      field.defaultVal(); // null
      GenericData.get().getDefaultValue(field); // Integer (1)
    }
    
    Invalid long value as int field default value:
    public void testInvalidLongAsIntDefault() {
      Schema.Field field = new Schema.Field("myField",
             Schema.create(Schema.Type.INT), "doc", Integer.MAX_VALUE + 1L);
      field.hasDefaultValue(); // true
      field.defaultValue(); // internal LongNode (2147483648)
      field.defaultVal(); // Long (2147483648)
      GenericData.get().getDefaultValue(field); // Integer (-2147483648)
    }
    
    This PR makes changes to invalidate incorrect default values for INT and
    LONG schemas, including all floating point values, e.g. 1.0.
    Additionally it contains changes to try and return the appropriate
    Object type given the schema type.
    
    This change is necessary for correctness and consitency but also
    because users cannot disable default value validation and handle
    these cases on their own since the underlying Field.defaultValue()
    is no longer public. Users only have access to default values
    mutated by Field.defaultVal() and GenericData.getDefaultValue().
    
    Notes on JacksonUtils.toObject():
     - This method is used to convert the underlying JsonNode default value
      to an Object when Field.defaultVal() is called. This method is
      invoked regardless of whether default value validation is true or
      false.
     - For LongNode values we continue to return Long values for INT
       schemas in the case we cannot safely convert to an Integer.
       This behavior, while maintained, is inconsistent with that
       of FloatNode / DoubleNode where null is returned for INT
       and LONG schemas. Additional changes may be needed for
       further consistency.
    
    Closes #739
    (cherry picked from commit 8b5c11ade7f73eb74a0f017bd52b9b485d68d42f)
---
 .../avro/src/main/java/org/apache/avro/Schema.java |   2 +
 .../apache/avro/util/internal/JacksonUtils.java    |  18 ++-
 .../src/test/java/org/apache/avro/TestSchema.java  | 140 +++++++++++++++++++++
 3 files changed, 159 insertions(+), 1 deletion(-)

diff --git a/lang/java/avro/src/main/java/org/apache/avro/Schema.java b/lang/java/avro/src/main/java/org/apache/avro/Schema.java
index 8e152ae..6a0f7da 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/Schema.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/Schema.java
@@ -1573,7 +1573,9 @@ public abstract class Schema extends JsonProperties implements Serializable {
     case FIXED:
       return defaultValue.isTextual();
     case INT:
+      return defaultValue.isIntegralNumber() && defaultValue.canConvertToInt();
     case LONG:
+      return defaultValue.isIntegralNumber() && defaultValue.canConvertToLong();
     case FLOAT:
     case DOUBLE:
       return defaultValue.isNumber();
diff --git a/lang/java/avro/src/main/java/org/apache/avro/util/internal/JacksonUtils.java b/lang/java/avro/src/main/java/org/apache/avro/util/internal/JacksonUtils.java
index 11f7fd1..1a82289 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/util/internal/JacksonUtils.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/util/internal/JacksonUtils.java
@@ -116,9 +116,25 @@ public class JacksonUtils {
         return jsonNode.asInt();
       } else if (schema.getType().equals(Schema.Type.LONG)) {
         return jsonNode.asLong();
+      } else if (schema.getType().equals(Schema.Type.FLOAT)) {
+        return (float) jsonNode.asDouble();
+      } else if (schema.getType().equals(Schema.Type.DOUBLE)) {
+        return jsonNode.asDouble();
       }
     } else if (jsonNode.isLong()) {
-      return jsonNode.asLong();
+      if (schema == null || schema.getType().equals(Schema.Type.LONG)) {
+        return jsonNode.asLong();
+      } else if (schema.getType().equals(Schema.Type.INT)) {
+        if (jsonNode.canConvertToInt()) {
+          return jsonNode.asInt();
+        } else {
+          return jsonNode.asLong();
+        }
+      } else if (schema.getType().equals(Schema.Type.FLOAT)) {
+        return (float) jsonNode.asDouble();
+      } else if (schema.getType().equals(Schema.Type.DOUBLE)) {
+        return jsonNode.asDouble();
+      }
     } else if (jsonNode.isDouble() || jsonNode.isFloat()) {
       if (schema == null || schema.getType().equals(Schema.Type.DOUBLE)) {
         return jsonNode.asDouble();
diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestSchema.java b/lang/java/avro/src/test/java/org/apache/avro/TestSchema.java
index 4899b0e..b7e0dbe 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestSchema.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestSchema.java
@@ -31,6 +31,7 @@ import java.util.List;
 
 import org.apache.avro.Schema.Field;
 import org.apache.avro.Schema.Type;
+import org.apache.avro.generic.GenericData;
 import org.junit.Test;
 
 public class TestSchema {
@@ -215,4 +216,143 @@ public class TestSchema {
     assertEquals(parent, parentWithoutInlinedChildReference);
   }
 
+  public void testIntDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.INT), "doc", 1);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1, field.defaultVal());
+    assertEquals(1, GenericData.get().getDefaultValue(field));
+
+    field = new Schema.Field("myField", Schema.create(Schema.Type.INT), "doc", Integer.MIN_VALUE);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(Integer.MIN_VALUE, field.defaultVal());
+    assertEquals(Integer.MIN_VALUE, GenericData.get().getDefaultValue(field));
+
+    field = new Schema.Field("myField", Schema.create(Schema.Type.INT), "doc", Integer.MAX_VALUE);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(Integer.MAX_VALUE, field.defaultVal());
+    assertEquals(Integer.MAX_VALUE, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test
+  public void testValidLongAsIntDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.INT), "doc", 1L);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1, field.defaultVal());
+    assertEquals(1, GenericData.get().getDefaultValue(field));
+
+    field = new Schema.Field("myField", Schema.create(Schema.Type.INT), "doc", Long.valueOf(Integer.MIN_VALUE));
+    assertTrue(field.hasDefaultValue());
+    assertEquals(Integer.MIN_VALUE, field.defaultVal());
+    assertEquals(Integer.MIN_VALUE, GenericData.get().getDefaultValue(field));
+
+    field = new Schema.Field("myField", Schema.create(Schema.Type.INT), "doc", Long.valueOf(Integer.MAX_VALUE));
+    assertTrue(field.hasDefaultValue());
+    assertEquals(Integer.MAX_VALUE, field.defaultVal());
+    assertEquals(Integer.MAX_VALUE, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test(expected = AvroTypeException.class)
+  public void testInvalidLongAsIntDefaultValue() {
+    new Schema.Field("myField", Schema.create(Schema.Type.INT), "doc", Integer.MAX_VALUE + 1L);
+  }
+
+  @Test(expected = AvroTypeException.class)
+  public void testDoubleAsIntDefaultValue() {
+    new Schema.Field("myField", Schema.create(Schema.Type.INT), "doc", 1.0);
+  }
+
+  @Test
+  public void testLongDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.LONG), "doc", 1L);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1L, field.defaultVal());
+    assertEquals(1L, GenericData.get().getDefaultValue(field));
+
+    field = new Schema.Field("myField", Schema.create(Schema.Type.LONG), "doc", Long.MIN_VALUE);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(Long.MIN_VALUE, field.defaultVal());
+    assertEquals(Long.MIN_VALUE, GenericData.get().getDefaultValue(field));
+
+    field = new Schema.Field("myField", Schema.create(Schema.Type.LONG), "doc", Long.MAX_VALUE);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(Long.MAX_VALUE, field.defaultVal());
+    assertEquals(Long.MAX_VALUE, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test
+  public void testIntAsLongDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.LONG), "doc", 1);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1L, field.defaultVal());
+    assertEquals(1L, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test(expected = AvroTypeException.class)
+  public void testDoubleAsLongDefaultValue() {
+    new Schema.Field("myField", Schema.create(Schema.Type.LONG), "doc", 1.0);
+  }
+
+  @Test
+  public void testDoubleDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.DOUBLE), "doc", 1.0);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1.0d, field.defaultVal());
+    assertEquals(1.0d, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test
+  public void testIntAsDoubleDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.DOUBLE), "doc", 1);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1.0d, field.defaultVal());
+    assertEquals(1.0d, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test
+  public void testLongAsDoubleDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.DOUBLE), "doc", 1L);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1.0d, field.defaultVal());
+    assertEquals(1.0d, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test
+  public void testFloatAsDoubleDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.DOUBLE), "doc", 1.0f);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1.0d, field.defaultVal());
+    assertEquals(1.0d, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test
+  public void testFloatDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.FLOAT), "doc", 1.0f);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1.0f, field.defaultVal());
+    assertEquals(1.0f, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test
+  public void testIntAsFloatDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.FLOAT), "doc", 1);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1.0f, field.defaultVal());
+    assertEquals(1.0f, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test
+  public void testLongAsFloatDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.FLOAT), "doc", 1L);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1.0f, field.defaultVal());
+    assertEquals(1.0f, GenericData.get().getDefaultValue(field));
+  }
+
+  @Test
+  public void testDoubleAsFloatDefaultValue() {
+    Schema.Field field = new Schema.Field("myField", Schema.create(Schema.Type.FLOAT), "doc", 1.0d);
+    assertTrue(field.hasDefaultValue());
+    assertEquals(1.0f, field.defaultVal());
+    assertEquals(1.0f, GenericData.get().getDefaultValue(field));
+  }
 }