diff --git a/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/AvroSchemaUtil.java b/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/AvroSchemaUtil.java index 788ad025b..19de99522 100644 --- a/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/AvroSchemaUtil.java +++ b/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/AvroSchemaUtil.java @@ -22,6 +22,20 @@ public static void traverseSchema(Schema schema, SchemaVisitor visitor) { traverseSchema(schema, visitor, visited); } + /** + * Returns true if a null value is allowed as the default value for a field + * (given its schema). It is valid if and only if: + * (1) The field's type is null, or + * (2) The field is a union, where the first alternative type is null. + */ + public static boolean isNullAValidDefaultForSchema(Schema schema) { + return schema != null && + (schema.getType() == Schema.Type.NULL || + schema.getType() == Schema.Type.UNION && + !schema.getTypes().isEmpty() && + schema.getTypes().get(0).getType() == Schema.Type.NULL); + } + /** * given a (parent) schema, and a field name, find the schema for that field. * if the field is a union, returns the (only) non-null branch of the union diff --git a/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/Jackson1Utils.java b/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/Jackson1Utils.java index abd9440a3..03ba0dbcb 100644 --- a/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/Jackson1Utils.java +++ b/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/Jackson1Utils.java @@ -26,7 +26,9 @@ public class Jackson1Utils { */ public static JsonNode toJsonNode(Object datum) { if (datum == null) { - return JsonNodeFactory.instance.nullNode(); + return null; + } else if (datum instanceof JsonNode) { + return (JsonNode) datum; } else if (datum instanceof byte[]) { try { return JsonNodeFactory.instance.textNode(new String((byte[]) datum, BYTES_CHARSET)); diff --git a/helper/impls/helper-impl-110/src/main/java/com/linkedin/avroutil1/compatibility/avro110/FieldBuilder110.java b/helper/impls/helper-impl-110/src/main/java/com/linkedin/avroutil1/compatibility/avro110/FieldBuilder110.java index 45caf53f0..7c8b0c06b 100644 --- a/helper/impls/helper-impl-110/src/main/java/com/linkedin/avroutil1/compatibility/avro110/FieldBuilder110.java +++ b/helper/impls/helper-impl-110/src/main/java/com/linkedin/avroutil1/compatibility/avro110/FieldBuilder110.java @@ -6,6 +6,7 @@ package com.linkedin.avroutil1.compatibility.avro110; +import com.linkedin.avroutil1.compatibility.AvroSchemaUtil; import com.linkedin.avroutil1.compatibility.FieldBuilder; import org.apache.avro.Schema; import org.apache.avro.Schema.Field.Order; @@ -37,6 +38,10 @@ public FieldBuilder110(String name) { @Override public FieldBuilder setSchema(Schema schema) { _schema = schema; + if (_defaultVal == Schema.Field.NULL_DEFAULT_VALUE) { + // Check if null is still a valid default for the schema. + setDefault(null); + } return this; } @@ -48,6 +53,23 @@ public FieldBuilder setDoc(String doc) { @Override public FieldBuilder setDefault(Object defaultValue) { + // If defaultValue is null, it's ambiguous. It could mean either of these: + // (1) The default value was not specified, or + // (2) The default value was specified to be null. + // + // To disambiguate, we check to see if null is a valid value for the + // field's schema. If it is, we convert it into a special object (marker) + // that's known to Avro. If it's not, it's case (1); we leave it as is. + // + // This means there's no way (using the helper) to create a field whose + // schema allows null as a default, but you want to say "no default was + // specified". That's a small price to pay for not bloating the helper API. + // + // Note that we don't validate all possible default values against the + // schema. That's Avro's job. We only check for the ambiguous case here. + if (defaultValue == null && AvroSchemaUtil.isNullAValidDefaultForSchema(_schema)) { + defaultValue = Schema.Field.NULL_DEFAULT_VALUE; + } _defaultVal = defaultValue; return this; } diff --git a/helper/impls/helper-impl-14/src/main/java/com/linkedin/avroutil1/compatibility/avro14/FieldBuilder14.java b/helper/impls/helper-impl-14/src/main/java/com/linkedin/avroutil1/compatibility/avro14/FieldBuilder14.java index 91f57703e..7e7595107 100644 --- a/helper/impls/helper-impl-14/src/main/java/com/linkedin/avroutil1/compatibility/avro14/FieldBuilder14.java +++ b/helper/impls/helper-impl-14/src/main/java/com/linkedin/avroutil1/compatibility/avro14/FieldBuilder14.java @@ -6,11 +6,13 @@ package com.linkedin.avroutil1.compatibility.avro14; +import com.linkedin.avroutil1.compatibility.AvroSchemaUtil; import com.linkedin.avroutil1.compatibility.FieldBuilder; import com.linkedin.avroutil1.compatibility.Jackson1Utils; import org.apache.avro.Schema; import org.apache.avro.Schema.Field.Order; import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.node.NullNode; import java.lang.reflect.Field; import java.util.Map; @@ -52,6 +54,10 @@ public FieldBuilder14(String name) { @Override public FieldBuilder setSchema(Schema schema) { _schema = schema; + if (_defaultVal == NullNode.getInstance()) { + // Check if null is still a valid default for the schema. + setDefault((Object) null); + } return this; } @@ -63,6 +69,23 @@ public FieldBuilder setDoc(String doc) { @Override public FieldBuilder setDefault(Object defaultValue) { + // If defaultValue is null, it's ambiguous. It could mean either of these: + // (1) The default value was not specified, or + // (2) The default value was specified to be null. + // + // To disambiguate, we check to see if null is a valid value for the + // field's schema. If it is, we convert it into a special object (marker) + // that's known to Avro. If it's not, it's case (1); we leave it as is. + // + // This means there's no way (using the helper) to create a field whose + // schema allows null as a default, but you want to say "no default was + // specified". That's a small price to pay for not bloating the helper API. + // + // Note that we don't validate all possible default values against the + // schema. That's Avro's job. We only check for the ambiguous case here. + if (defaultValue == null && AvroSchemaUtil.isNullAValidDefaultForSchema(_schema)) { + return setDefault(NullNode.getInstance()); + } return setDefault(Jackson1Utils.toJsonNode(defaultValue)); } diff --git a/helper/impls/helper-impl-15/src/main/java/com/linkedin/avroutil1/compatibility/avro15/FieldBuilder15.java b/helper/impls/helper-impl-15/src/main/java/com/linkedin/avroutil1/compatibility/avro15/FieldBuilder15.java index d41f6bbaa..e9a6674b8 100644 --- a/helper/impls/helper-impl-15/src/main/java/com/linkedin/avroutil1/compatibility/avro15/FieldBuilder15.java +++ b/helper/impls/helper-impl-15/src/main/java/com/linkedin/avroutil1/compatibility/avro15/FieldBuilder15.java @@ -6,11 +6,13 @@ package com.linkedin.avroutil1.compatibility.avro15; +import com.linkedin.avroutil1.compatibility.AvroSchemaUtil; import com.linkedin.avroutil1.compatibility.FieldBuilder; import com.linkedin.avroutil1.compatibility.Jackson1Utils; import org.apache.avro.Schema; import org.apache.avro.Schema.Field.Order; import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.node.NullNode; import java.lang.reflect.Field; import java.util.Map; @@ -52,6 +54,10 @@ public FieldBuilder15(String name) { @Override public FieldBuilder setSchema(Schema schema) { _schema = schema; + if (_defaultVal == NullNode.getInstance()) { + // Check if null is still a valid default for the schema. + setDefault((Object) null); + } return this; } @@ -63,6 +69,23 @@ public FieldBuilder setDoc(String doc) { @Override public FieldBuilder setDefault(Object defaultValue) { + // If defaultValue is null, it's ambiguous. It could mean either of these: + // (1) The default value was not specified, or + // (2) The default value was specified to be null. + // + // To disambiguate, we check to see if null is a valid value for the + // field's schema. If it is, we convert it into a special object (marker) + // that's known to Avro. If it's not, it's case (1); we leave it as is. + // + // This means there's no way (using the helper) to create a field whose + // schema allows null as a default, but you want to say "no default was + // specified". That's a small price to pay for not bloating the helper API. + // + // Note that we don't validate all possible default values against the + // schema. That's Avro's job. We only check for the ambiguous case here. + if (defaultValue == null && AvroSchemaUtil.isNullAValidDefaultForSchema(_schema)) { + return setDefault(NullNode.getInstance()); + } return setDefault(Jackson1Utils.toJsonNode(defaultValue)); } diff --git a/helper/impls/helper-impl-16/src/main/java/com/linkedin/avroutil1/compatibility/avro16/FieldBuilder16.java b/helper/impls/helper-impl-16/src/main/java/com/linkedin/avroutil1/compatibility/avro16/FieldBuilder16.java index 948d00ba1..f0a9a329c 100644 --- a/helper/impls/helper-impl-16/src/main/java/com/linkedin/avroutil1/compatibility/avro16/FieldBuilder16.java +++ b/helper/impls/helper-impl-16/src/main/java/com/linkedin/avroutil1/compatibility/avro16/FieldBuilder16.java @@ -6,11 +6,13 @@ package com.linkedin.avroutil1.compatibility.avro16; +import com.linkedin.avroutil1.compatibility.AvroSchemaUtil; import com.linkedin.avroutil1.compatibility.FieldBuilder; import com.linkedin.avroutil1.compatibility.Jackson1Utils; import org.apache.avro.Schema; import org.apache.avro.Schema.Field.Order; import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.node.NullNode; import java.util.Map; @@ -39,6 +41,10 @@ public FieldBuilder16(String name) { @Override public FieldBuilder setSchema(Schema schema) { _schema = schema; + if (_defaultVal == NullNode.getInstance()) { + // Check if null is still a valid default for the schema. + setDefault((Object) null); + } return this; } @@ -50,6 +56,23 @@ public FieldBuilder setDoc(String doc) { @Override public FieldBuilder setDefault(Object defaultValue) { + // If defaultValue is null, it's ambiguous. It could mean either of these: + // (1) The default value was not specified, or + // (2) The default value was specified to be null. + // + // To disambiguate, we check to see if null is a valid value for the + // field's schema. If it is, we convert it into a special object (marker) + // that's known to Avro. If it's not, it's case (1); we leave it as is. + // + // This means there's no way (using the helper) to create a field whose + // schema allows null as a default, but you want to say "no default was + // specified". That's a small price to pay for not bloating the helper API. + // + // Note that we don't validate all possible default values against the + // schema. That's Avro's job. We only check for the ambiguous case here. + if (defaultValue == null && AvroSchemaUtil.isNullAValidDefaultForSchema(_schema)) { + return setDefault(NullNode.getInstance()); + } return setDefault(Jackson1Utils.toJsonNode(defaultValue)); } diff --git a/helper/impls/helper-impl-17/src/main/java/com/linkedin/avroutil1/compatibility/avro17/FieldBuilder17.java b/helper/impls/helper-impl-17/src/main/java/com/linkedin/avroutil1/compatibility/avro17/FieldBuilder17.java index fa078a83d..692b527d1 100644 --- a/helper/impls/helper-impl-17/src/main/java/com/linkedin/avroutil1/compatibility/avro17/FieldBuilder17.java +++ b/helper/impls/helper-impl-17/src/main/java/com/linkedin/avroutil1/compatibility/avro17/FieldBuilder17.java @@ -6,11 +6,13 @@ package com.linkedin.avroutil1.compatibility.avro17; +import com.linkedin.avroutil1.compatibility.AvroSchemaUtil; import com.linkedin.avroutil1.compatibility.FieldBuilder; import com.linkedin.avroutil1.compatibility.Jackson1Utils; import org.apache.avro.Schema; import org.apache.avro.Schema.Field.Order; import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.node.NullNode; import java.util.Map; @@ -39,6 +41,10 @@ public FieldBuilder17(String name) { @Override public FieldBuilder setSchema(Schema schema) { _schema = schema; + if (_defaultVal == NullNode.getInstance()) { + // Check if null is still a valid default for the schema. + setDefault((Object) null); + } return this; } @@ -50,6 +56,23 @@ public FieldBuilder setDoc(String doc) { @Override public FieldBuilder setDefault(Object defaultValue) { + // If defaultValue is null, it's ambiguous. It could mean either of these: + // (1) The default value was not specified, or + // (2) The default value was specified to be null. + // + // To disambiguate, we check to see if null is a valid value for the + // field's schema. If it is, we convert it into a special object (marker) + // that's known to Avro. If it's not, it's case (1); we leave it as is. + // + // This means there's no way (using the helper) to create a field whose + // schema allows null as a default, but you want to say "no default was + // specified". That's a small price to pay for not bloating the helper API. + // + // Note that we don't validate all possible default values against the + // schema. That's Avro's job. We only check for the ambiguous case here. + if (defaultValue == null && AvroSchemaUtil.isNullAValidDefaultForSchema(_schema)) { + return setDefault(NullNode.getInstance()); + } return setDefault(Jackson1Utils.toJsonNode(defaultValue)); } diff --git a/helper/impls/helper-impl-18/src/main/java/com/linkedin/avroutil1/compatibility/avro18/FieldBuilder18.java b/helper/impls/helper-impl-18/src/main/java/com/linkedin/avroutil1/compatibility/avro18/FieldBuilder18.java index ec5eadc7e..5a417b5d3 100644 --- a/helper/impls/helper-impl-18/src/main/java/com/linkedin/avroutil1/compatibility/avro18/FieldBuilder18.java +++ b/helper/impls/helper-impl-18/src/main/java/com/linkedin/avroutil1/compatibility/avro18/FieldBuilder18.java @@ -6,11 +6,13 @@ package com.linkedin.avroutil1.compatibility.avro18; +import com.linkedin.avroutil1.compatibility.AvroSchemaUtil; import com.linkedin.avroutil1.compatibility.FieldBuilder; import com.linkedin.avroutil1.compatibility.Jackson1Utils; import org.apache.avro.Schema; import org.apache.avro.Schema.Field.Order; import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.node.NullNode; import java.util.Map; @@ -42,6 +44,10 @@ public FieldBuilder18(String name) { @Override public FieldBuilder setSchema(Schema schema) { _schema = schema; + if (_defaultVal == NullNode.getInstance()) { + // Check if null is still a valid default for the schema. + setDefault((Object) null); + } return this; } @@ -53,6 +59,23 @@ public FieldBuilder setDoc(String doc) { @Override public FieldBuilder setDefault(Object defaultValue) { + // If defaultValue is null, it's ambiguous. It could mean either of these: + // (1) The default value was not specified, or + // (2) The default value was specified to be null. + // + // To disambiguate, we check to see if null is a valid value for the + // field's schema. If it is, we convert it into a special object (marker) + // + // This means there's no way (using the helper) to create a field whose + // schema allows null as a default, but you want to say "no default was + // specified". That's a small price to pay for not bloating the helper API. + // that's known to Avro. If it's not, it's case (1); we leave it as is. + // + // Note that we don't validate all possible default values against the + // schema. That's Avro's job. We only check for the ambiguous case here. + if (defaultValue == null && AvroSchemaUtil.isNullAValidDefaultForSchema(_schema)) { + return setDefault(NullNode.getInstance()); + } return setDefault(Jackson1Utils.toJsonNode(defaultValue)); } diff --git a/helper/impls/helper-impl-19/src/main/java/com/linkedin/avroutil1/compatibility/avro19/FieldBuilder19.java b/helper/impls/helper-impl-19/src/main/java/com/linkedin/avroutil1/compatibility/avro19/FieldBuilder19.java index 749cc0091..f9b0d0e97 100644 --- a/helper/impls/helper-impl-19/src/main/java/com/linkedin/avroutil1/compatibility/avro19/FieldBuilder19.java +++ b/helper/impls/helper-impl-19/src/main/java/com/linkedin/avroutil1/compatibility/avro19/FieldBuilder19.java @@ -6,6 +6,7 @@ package com.linkedin.avroutil1.compatibility.avro19; +import com.linkedin.avroutil1.compatibility.AvroSchemaUtil; import com.linkedin.avroutil1.compatibility.FieldBuilder; import org.apache.avro.Schema; import org.apache.avro.Schema.Field.Order; @@ -37,6 +38,10 @@ public FieldBuilder19(String name) { @Override public FieldBuilder setSchema(Schema schema) { _schema = schema; + if (_defaultVal == Schema.Field.NULL_DEFAULT_VALUE) { + // Check if null is still a valid default for the schema. + setDefault(null); + } return this; } @@ -48,6 +53,23 @@ public FieldBuilder setDoc(String doc) { @Override public FieldBuilder setDefault(Object defaultValue) { + // If defaultValue is null, it's ambiguous. It could mean either of these: + // (1) The default value was not specified, or + // (2) The default value was specified to be null. + // + // To disambiguate, we check to see if null is a valid value for the + // field's schema. If it is, we convert it into a special object (marker) + // that's known to Avro. If it's not, it's case (1); we leave it as is. + // + // This means there's no way (using the helper) to create a field whose + // schema allows null as a default, but you want to say "no default was + // specified". That's a small price to pay for not bloating the helper API. + // + // Note that we don't validate all possible default values against the + // schema. That's Avro's job. We only check for the ambiguous case here. + if (defaultValue == null && AvroSchemaUtil.isNullAValidDefaultForSchema(_schema)) { + defaultValue = Schema.Field.NULL_DEFAULT_VALUE; + } _defaultVal = defaultValue; return this; } diff --git a/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/Avro110FieldBuilderTest.java b/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/Avro110FieldBuilderTest.java new file mode 100644 index 000000000..7041c84be --- /dev/null +++ b/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/Avro110FieldBuilderTest.java @@ -0,0 +1,159 @@ +/* + * Copyright 2021 LinkedIn Corp. + * Licensed under the BSD 2-Clause License (the "License"). + * See License in the project root for license information. + */ + +package com.linkedin.avroutil1.compatibility.avro110; + +import com.fasterxml.jackson.databind.node.NullNode; +import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; +import com.linkedin.avroutil1.compatibility.FieldBuilder; +import com.linkedin.avroutil1.testcommon.TestUtil; +import org.apache.avro.AvroRuntimeException; +import org.apache.avro.AvroTypeException; +import org.apache.avro.JsonProperties; +import org.apache.avro.Schema; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class Avro110FieldBuilderTest { + + @Test + public void testNullDefaultForNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("nullWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultVal()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + + // Explicit null marker object. + builder.setDefault(JsonProperties.NULL_VALUE); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + + // Wrong marker object. + builder.setDefault(NullNode.getInstance()); + try { + builder.build(); + } catch (AvroRuntimeException expected) { + Assert.assertEquals(expected.getMessage(), "Unknown datum class: class com.fasterxml.jackson.databind.node.NullNode"); + } + + // Arbitrary object not valid as the default. + builder.setDefault("invalid"); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field nullWithoutDefault: \"invalid\" not a \"null\""); + } + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field nullWithoutDefault: \"invalid\" not a \"boolean\""); + } + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultVal()); + } + + @Test + public void testNullDefaultForUnionWithNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("unionWithNullNoDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultVal()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + + // Explicit null marker object. + builder.setDefault(JsonProperties.NULL_VALUE); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + + // Wrong marker object. + builder.setDefault(NullNode.getInstance()); + try { + builder.build(); + } catch (AvroRuntimeException expected) { + Assert.assertEquals(expected.getMessage(), "Unknown datum class: class com.fasterxml.jackson.databind.node.NullNode"); + } + + // Arbitrary object not valid as the default. + builder.setDefault("invalid"); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field unionWithNullNoDefault: \"invalid\" not a [\"null\",\"string\"]"); + } + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field unionWithNullNoDefault: \"invalid\" not a \"boolean\""); + } + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultVal()); + } + + @Test + public void testNullDefaultForBoolField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("boolWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultVal()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertNull(builder.build().defaultVal()); + + // Explicit null marker object. + builder.setDefault(JsonProperties.NULL_VALUE); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field boolWithoutDefault: null not a \"boolean\""); + } + + // Wrong marker object. + builder.setDefault(NullNode.getInstance()); + try { + builder.build(); + } catch (AvroRuntimeException expected) { + Assert.assertEquals(expected.getMessage(), "Unknown datum class: class com.fasterxml.jackson.databind.node.NullNode"); + } + + // Arbitrary object not valid as the default. + builder.setDefault("invalid"); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field boolWithoutDefault: \"invalid\" not a \"boolean\""); + } + } +} diff --git a/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/AvroCompatibilityHelperAvro110Test.java b/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/AvroCompatibilityHelperAvro110Test.java index 57897e0c3..473a1b929 100644 --- a/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/AvroCompatibilityHelperAvro110Test.java +++ b/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/AvroCompatibilityHelperAvro110Test.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import org.apache.avro.JsonProperties; import org.apache.avro.Schema; import org.mockito.Mockito; import org.testng.Assert; @@ -51,7 +52,7 @@ public void testCreateSchemaFieldWithProvidedDefaultValue() throws IOException { Schema schema = Schema.parse(TestUtil.load("RecordWithRecursiveTypesAndDefaults.avsc")); // Test null default value Schema.Field field = schema.getField("unionWithNullDefault"); - Assert.assertNull(AvroCompatibilityHelper.createSchemaField("unionWithNullDefault", field.schema(), "", null).defaultVal()); + Assert.assertEquals(AvroCompatibilityHelper.createSchemaField("unionWithNullDefault", field.schema(), "", null).defaultVal(), JsonProperties.NULL_VALUE); // Test primitive default value field = schema.getField("doubleFieldWithDefault"); Assert.assertEquals(AvroCompatibilityHelper.createSchemaField("doubleFieldWithDefault", field.schema(), "", field.defaultVal()).defaultVal(), 1.0); diff --git a/helper/tests/helper-tests-14/src/test/java/com/linkedin/avroutil1/compatibility/avro14/Avro14FieldBuilderTest.java b/helper/tests/helper-tests-14/src/test/java/com/linkedin/avroutil1/compatibility/avro14/Avro14FieldBuilderTest.java new file mode 100644 index 000000000..483bcba74 --- /dev/null +++ b/helper/tests/helper-tests-14/src/test/java/com/linkedin/avroutil1/compatibility/avro14/Avro14FieldBuilderTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2021 LinkedIn Corp. + * Licensed under the BSD 2-Clause License (the "License"). + * See License in the project root for license information. + */ + +package com.linkedin.avroutil1.compatibility.avro14; + +import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; +import com.linkedin.avroutil1.compatibility.FieldBuilder; +import com.linkedin.avroutil1.testcommon.TestUtil; +import org.apache.avro.Schema; +import org.codehaus.jackson.node.NullNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class Avro14FieldBuilderTest { + + @Test + public void testNullDefaultForNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("nullWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema, but Avro 1.4 doesn't mind. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForUnionWithNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("unionWithNullNoDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema, but Avro 1.4 doesn't mind. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForBoolField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("boolWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertNull(builder.build().defaultValue()); + + // Explicit null marker object. Not valid per the schema, but Avro 1.4 + // doesn't mind. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema, but Avro 1.4 doesn't mind. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + } +} diff --git a/helper/tests/helper-tests-15/src/test/java/com/linkedin/avroutil1/compatibility/avro15/Avro15FieldBuilderTest.java b/helper/tests/helper-tests-15/src/test/java/com/linkedin/avroutil1/compatibility/avro15/Avro15FieldBuilderTest.java new file mode 100644 index 000000000..75bfd0759 --- /dev/null +++ b/helper/tests/helper-tests-15/src/test/java/com/linkedin/avroutil1/compatibility/avro15/Avro15FieldBuilderTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2021 LinkedIn Corp. + * Licensed under the BSD 2-Clause License (the "License"). + * See License in the project root for license information. + */ + +package com.linkedin.avroutil1.compatibility.avro15; + +import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; +import com.linkedin.avroutil1.compatibility.FieldBuilder; +import com.linkedin.avroutil1.testcommon.TestUtil; +import org.apache.avro.Schema; +import org.codehaus.jackson.node.NullNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class Avro15FieldBuilderTest { + + @Test + public void testNullDefaultForNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("nullWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema, but Avro 1.5 doesn't mind. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForUnionWithNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("unionWithNullNoDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema, but Avro 1.5 doesn't mind. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForBoolField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("boolWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertNull(builder.build().defaultValue()); + + // Explicit null marker object. Not valid per the schema, but Avro 1.5 + // doesn't mind. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema, but Avro 1.5 doesn't mind. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + } +} diff --git a/helper/tests/helper-tests-16/src/test/java/com/linkedin/avroutil1/compatibility/avro16/Avro16FieldBuilderTest.java b/helper/tests/helper-tests-16/src/test/java/com/linkedin/avroutil1/compatibility/avro16/Avro16FieldBuilderTest.java new file mode 100644 index 000000000..d6ab80c94 --- /dev/null +++ b/helper/tests/helper-tests-16/src/test/java/com/linkedin/avroutil1/compatibility/avro16/Avro16FieldBuilderTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2021 LinkedIn Corp. + * Licensed under the BSD 2-Clause License (the "License"). + * See License in the project root for license information. + */ + +package com.linkedin.avroutil1.compatibility.avro16; + +import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; +import com.linkedin.avroutil1.compatibility.FieldBuilder; +import com.linkedin.avroutil1.testcommon.TestUtil; +import org.apache.avro.Schema; +import org.codehaus.jackson.node.NullNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class Avro16FieldBuilderTest { + + @Test + public void testNullDefaultForNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("nullWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema, but Avro 1.6 doesn't mind. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForUnionWithNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("unionWithNullNoDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema, but Avro 1.6 doesn't mind. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForBoolField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("boolWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertNull(builder.build().defaultValue()); + + // Explicit null marker object. Not valid per the schema, but Avro 1.6 + // doesn't mind. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema, but Avro 1.6 doesn't mind. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + } +} diff --git a/helper/tests/helper-tests-17/src/test/java/com/linkedin/avroutil1/compatibility/avro17/Avro17FieldBuilderTest.java b/helper/tests/helper-tests-17/src/test/java/com/linkedin/avroutil1/compatibility/avro17/Avro17FieldBuilderTest.java new file mode 100644 index 000000000..5cf64bc1b --- /dev/null +++ b/helper/tests/helper-tests-17/src/test/java/com/linkedin/avroutil1/compatibility/avro17/Avro17FieldBuilderTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2021 LinkedIn Corp. + * Licensed under the BSD 2-Clause License (the "License"). + * See License in the project root for license information. + */ + +package com.linkedin.avroutil1.compatibility.avro17; + +import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; +import com.linkedin.avroutil1.compatibility.FieldBuilder; +import com.linkedin.avroutil1.testcommon.TestUtil; +import org.apache.avro.Schema; +import org.codehaus.jackson.node.NullNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class Avro17FieldBuilderTest { + + @Test + public void testNullDefaultForNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("nullWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema; Avro 1.7 warns, but no error. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForUnionWithNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("unionWithNullNoDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema; Avro 1.7 warns, but no error. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForBoolField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("boolWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertNull(builder.build().defaultValue()); + + // Explicit null marker object. Not valid per the schema; Avro 1.7 + // warns, but not an error. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Arbitrary object. Not valid per the schema; Avro 1.7 warns, but no error. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + } +} diff --git a/helper/tests/helper-tests-18/src/test/java/com/linkedin/avroutil1/compatibility/avro18/Avro18FieldBuilderTest.java b/helper/tests/helper-tests-18/src/test/java/com/linkedin/avroutil1/compatibility/avro18/Avro18FieldBuilderTest.java new file mode 100644 index 000000000..7596d57ae --- /dev/null +++ b/helper/tests/helper-tests-18/src/test/java/com/linkedin/avroutil1/compatibility/avro18/Avro18FieldBuilderTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2021 LinkedIn Corp. + * Licensed under the BSD 2-Clause License (the "License"). + * See License in the project root for license information. + */ + +package com.linkedin.avroutil1.compatibility.avro18; + +import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; +import com.linkedin.avroutil1.compatibility.FieldBuilder; +import com.linkedin.avroutil1.testcommon.TestUtil; +import org.apache.avro.AvroRuntimeException; +import org.apache.avro.JsonProperties; +import org.apache.avro.Schema; +import org.codehaus.jackson.node.NullNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class Avro18FieldBuilderTest { + + @Test + public void testNullDefaultForNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("nullWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Wrong marker object. + try { + builder.setDefault(JsonProperties.NULL_VALUE); + } catch (AvroRuntimeException expected) { + Assert.assertEquals(expected.getMessage(), "Unknown datum class: class org.apache.avro.JsonProperties$Null"); + } + + // Arbitrary object. Not valid per the schema; Avro 1.8 warns, but no error. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForUnionWithNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("unionWithNullNoDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Explicit null marker object. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Wrong marker object. + try { + builder.setDefault(JsonProperties.NULL_VALUE); + } catch (AvroRuntimeException expected) { + Assert.assertEquals(expected.getMessage(), "Unknown datum class: class org.apache.avro.JsonProperties$Null"); + } + + // Arbitrary object. Not valid per the schema; Avro 1.8 warns, but no error. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertTrue(builder.build().defaultValue().isNull()); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultValue()); + } + + @Test + public void testNullDefaultForBoolField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("boolWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultValue()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertNull(builder.build().defaultValue()); + + // Explicit null marker object. Not valid per the schema; Avro 1.8 + // warns, but not an error. + builder.setDefault(NullNode.getInstance()); + Assert.assertTrue(builder.build().defaultValue().isNull()); + + // Wrong marker object. + try { + builder.setDefault(JsonProperties.NULL_VALUE); + } catch (AvroRuntimeException expected) { + Assert.assertEquals(expected.getMessage(), "Unknown datum class: class org.apache.avro.JsonProperties$Null"); + } + + // Arbitrary object. Not valid per the schema; Avro 1.8 warns, but no error. + builder.setDefault("invalid"); + Assert.assertEquals(builder.build().defaultValue().getTextValue(), "invalid"); + } +} diff --git a/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/Avro19FieldBuilderTest.java b/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/Avro19FieldBuilderTest.java new file mode 100644 index 000000000..7d150f14a --- /dev/null +++ b/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/Avro19FieldBuilderTest.java @@ -0,0 +1,159 @@ +/* + * Copyright 2021 LinkedIn Corp. + * Licensed under the BSD 2-Clause License (the "License"). + * See License in the project root for license information. + */ + +package com.linkedin.avroutil1.compatibility.avro19; + +import com.fasterxml.jackson.databind.node.NullNode; +import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; +import com.linkedin.avroutil1.compatibility.FieldBuilder; +import com.linkedin.avroutil1.testcommon.TestUtil; +import org.apache.avro.AvroRuntimeException; +import org.apache.avro.AvroTypeException; +import org.apache.avro.JsonProperties; +import org.apache.avro.Schema; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class Avro19FieldBuilderTest { + + @Test + public void testNullDefaultForNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("nullWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultVal()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + + // Explicit null marker object. + builder.setDefault(JsonProperties.NULL_VALUE); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + + // Wrong marker object. + builder.setDefault(NullNode.getInstance()); + try { + builder.build(); + } catch (AvroRuntimeException expected) { + Assert.assertEquals(expected.getMessage(), "Unknown datum class: class com.fasterxml.jackson.databind.node.NullNode"); + } + + // Arbitrary object not valid as the default. + builder.setDefault("invalid"); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field nullWithoutDefault: \"invalid\" not a \"null\""); + } + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field nullWithoutDefault: \"invalid\" not a \"boolean\""); + } + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultVal()); + } + + @Test + public void testNullDefaultForUnionWithNullField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("unionWithNullNoDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultVal()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + + // Explicit null marker object. + builder.setDefault(JsonProperties.NULL_VALUE); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + + // Wrong marker object. + builder.setDefault(NullNode.getInstance()); + try { + builder.build(); + } catch (AvroRuntimeException expected) { + Assert.assertEquals(expected.getMessage(), "Unknown datum class: class com.fasterxml.jackson.databind.node.NullNode"); + } + + // Arbitrary object not valid as the default. + builder.setDefault("invalid"); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field unionWithNullNoDefault: \"invalid\" not a [\"null\",\"string\"]"); + } + + // Change the schema. The default is not reset, because it's not null. + Schema.Field anotherField = schema.getField("boolWithoutDefault"); + builder.setSchema(anotherField.schema()); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field unionWithNullNoDefault: \"invalid\" not a \"boolean\""); + } + + // Change the schema after setting the default to null. + builder.setSchema(field.schema()); + builder.setDefault(null); + Assert.assertEquals(builder.build().defaultVal(), JsonProperties.NULL_VALUE); + builder.setSchema(anotherField.schema()); + Assert.assertNull(builder.build().defaultVal()); + } + + @Test + public void testNullDefaultForBoolField() throws Exception { + Schema schema = Schema.parse(TestUtil.load("RecordWithDefaults.avsc")); + Schema.Field field = schema.getField("boolWithoutDefault"); + FieldBuilder builder = AvroCompatibilityHelper.cloneSchemaField(field); + + // No default value specified; cloneSchemaField() doesn't alter that. + Assert.assertNull(builder.build().defaultVal()); + + // Explicitly set null as the default value. + builder.setDefault(null); + Assert.assertNull(builder.build().defaultVal()); + + // Explicit null marker object. + builder.setDefault(JsonProperties.NULL_VALUE); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field boolWithoutDefault: null not a \"boolean\""); + } + + // Wrong marker object. + builder.setDefault(NullNode.getInstance()); + try { + builder.build(); + } catch (AvroRuntimeException expected) { + Assert.assertEquals(expected.getMessage(), "Unknown datum class: class com.fasterxml.jackson.databind.node.NullNode"); + } + + // Arbitrary object not valid as the default. + builder.setDefault("invalid"); + try { + builder.build(); + } catch (AvroTypeException expected) { + Assert.assertEquals(expected.getMessage(), "Invalid default for field boolWithoutDefault: \"invalid\" not a \"boolean\""); + } + } +} diff --git a/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/AvroCompatibilityHelperAvro19Test.java b/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/AvroCompatibilityHelperAvro19Test.java index e0473693b..efdff491e 100644 --- a/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/AvroCompatibilityHelperAvro19Test.java +++ b/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/AvroCompatibilityHelperAvro19Test.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import org.apache.avro.JsonProperties; import org.apache.avro.Schema; import org.mockito.Mockito; import org.testng.Assert; @@ -51,7 +52,7 @@ public void testCreateSchemaFieldWithProvidedDefaultValue() throws IOException { Schema schema = Schema.parse(TestUtil.load("RecordWithRecursiveTypesAndDefaults.avsc")); // Test null default value Schema.Field field = schema.getField("unionWithNullDefault"); - Assert.assertNull(AvroCompatibilityHelper.createSchemaField("unionWithNullDefault", field.schema(), "", null).defaultVal()); + Assert.assertEquals(AvroCompatibilityHelper.createSchemaField("unionWithNullDefault", field.schema(), "", null).defaultVal(), JsonProperties.NULL_VALUE); // Test primitive default value field = schema.getField("doubleFieldWithDefault"); Assert.assertEquals(AvroCompatibilityHelper.createSchemaField("doubleFieldWithDefault", field.schema(), "", field.defaultVal()).defaultVal(), 1.0); diff --git a/helper/tests/helper-tests-allavro/src/test/java/com/linkedin/avroutil1/compatibility/AvroCompatibilityHelperDefaultsTest.java b/helper/tests/helper-tests-allavro/src/test/java/com/linkedin/avroutil1/compatibility/AvroCompatibilityHelperDefaultsTest.java index a1279396e..38115fc2a 100644 --- a/helper/tests/helper-tests-allavro/src/test/java/com/linkedin/avroutil1/compatibility/AvroCompatibilityHelperDefaultsTest.java +++ b/helper/tests/helper-tests-allavro/src/test/java/com/linkedin/avroutil1/compatibility/AvroCompatibilityHelperDefaultsTest.java @@ -85,17 +85,17 @@ public void testGetDefaultsForFieldsWithoutDefaults() throws Exception { } try { - AvroCompatibilityHelper.getGenericDefaultValue(schema.getField("unionWithNoDefault")); + AvroCompatibilityHelper.getGenericDefaultValue(schema.getField("unionWithStringNoDefault")); } catch (AvroRuntimeException expected) { Throwable root = Throwables.getRootCause(expected); - Assert.assertTrue(root.getMessage().contains("unionWithNoDefault")); + Assert.assertTrue(root.getMessage().contains("unionWithStringNoDefault")); } try { - AvroCompatibilityHelper.getSpecificDefaultValue(schema.getField("unionWithNoDefault")); + AvroCompatibilityHelper.getSpecificDefaultValue(schema.getField("unionWithStringNoDefault")); } catch (AvroRuntimeException expected) { Throwable root = Throwables.getRootCause(expected); - Assert.assertTrue(root.getMessage().contains("unionWithNoDefault")); + Assert.assertTrue(root.getMessage().contains("unionWithStringNoDefault")); } } diff --git a/helper/tests/helper-tests-common/src/main/resources/RecordWithDefaults.avsc b/helper/tests/helper-tests-common/src/main/resources/RecordWithDefaults.avsc index e2f4b0c28..fbf818268 100644 --- a/helper/tests/helper-tests-common/src/main/resources/RecordWithDefaults.avsc +++ b/helper/tests/helper-tests-common/src/main/resources/RecordWithDefaults.avsc @@ -31,14 +31,18 @@ "type": ["null", "string"], "default": null }, + { + "name": "unionWithNullNoDefault", + "type": ["null", "string"] + }, { "name": "unionWithStringDefault", "type": ["string", "null"], "default": "def" }, { - "name": "unionWithNoDefault", + "name": "unionWithStringNoDefault", "type": ["string", "null"] } ] -} \ No newline at end of file +}