blob: deadb2efc587df226bb0a3e5faeff5ab6997da04 [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
*
* https://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.avro;
import java.util.Arrays;
import java.util.concurrent.Callable;
import org.hamcrest.MatcherAssert;
import org.hamcrest.collection.IsMapContaining;
import org.junit.Assert;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
public class TestLogicalType {
@Test
public void testDecimalFromSchema() {
Schema schema = Schema.createFixed("aFixed", null, null, 4);
schema.addProp("logicalType", "decimal");
schema.addProp("precision", 9);
schema.addProp("scale", 2);
LogicalType logicalType = LogicalTypes.fromSchemaIgnoreInvalid(schema);
Assert.assertTrue("Should be a Decimal", logicalType instanceof LogicalTypes.Decimal);
LogicalTypes.Decimal decimal = (LogicalTypes.Decimal) logicalType;
Assert.assertEquals("Should have correct precision", 9, decimal.getPrecision());
Assert.assertEquals("Should have correct scale", 2, decimal.getScale());
}
@Test
public void testInvalidLogicalTypeIgnored() {
final Schema schema = Schema.createFixed("aFixed", null, null, 2);
schema.addProp("logicalType", "decimal");
schema.addProp("precision", 9);
schema.addProp("scale", 2);
Assert.assertNull("Should ignore invalid logical type", LogicalTypes.fromSchemaIgnoreInvalid(schema));
}
@Test
public void testDecimalWithNonByteArrayTypes() {
final LogicalType decimal = LogicalTypes.decimal(5, 2);
// test simple types
Schema[] nonBytes = new Schema[] { Schema.createRecord("Record", null, null, false),
Schema.createArray(Schema.create(Schema.Type.BYTES)), Schema.createMap(Schema.create(Schema.Type.BYTES)),
Schema.createEnum("Enum", null, null, Arrays.asList("a", "b")),
Schema.createUnion(Arrays.asList(Schema.create(Schema.Type.BYTES), Schema.createFixed("fixed", null, null, 4))),
Schema.create(Schema.Type.BOOLEAN), Schema.create(Schema.Type.INT), Schema.create(Schema.Type.LONG),
Schema.create(Schema.Type.FLOAT), Schema.create(Schema.Type.DOUBLE), Schema.create(Schema.Type.NULL),
Schema.create(Schema.Type.STRING) };
for (final Schema schema : nonBytes) {
assertThrows("Should reject type: " + schema.getType(), IllegalArgumentException.class,
"Logical type decimal must be backed by fixed or bytes", () -> {
decimal.addToSchema(schema);
return null;
});
}
}
@Test
public void testUnknownFromJsonNode() {
Schema schema = Schema.create(Schema.Type.STRING);
schema.addProp("logicalType", "unknown");
schema.addProp("someProperty", 34);
LogicalType logicalType = LogicalTypes.fromSchemaIgnoreInvalid(schema);
Assert.assertNull("Should not return a LogicalType instance", logicalType);
}
@Test
public void testDecimalBytesHasNoPrecisionLimit() {
Schema schema = Schema.create(Schema.Type.BYTES);
// precision is not limited for bytes
LogicalTypes.decimal(Integer.MAX_VALUE).addToSchema(schema);
Assert.assertEquals("Precision should be an Integer.MAX_VALUE", Integer.MAX_VALUE,
((LogicalTypes.Decimal) LogicalTypes.fromSchemaIgnoreInvalid(schema)).getPrecision());
}
@Test
public void testDecimalFixedPrecisionLimit() {
// 4 bytes can hold up to 9 digits of precision
final Schema schema = Schema.createFixed("aDecimal", null, null, 4);
assertThrows("Should reject precision", IllegalArgumentException.class, "fixed(4) cannot store 10 digits (max 9)",
() -> {
LogicalTypes.decimal(10).addToSchema(schema);
return null;
});
Assert.assertNull("Invalid logical type should not be set on schema", LogicalTypes.fromSchemaIgnoreInvalid(schema));
// 129 bytes can hold up to 310 digits of precision
final Schema schema129 = Schema.createFixed("aDecimal", null, null, 129);
assertThrows("Should reject precision", IllegalArgumentException.class,
"fixed(129) cannot store 311 digits (max 310)", () -> {
LogicalTypes.decimal(311).addToSchema(schema129);
return null;
});
Assert.assertNull("Invalid logical type should not be set on schema",
LogicalTypes.fromSchemaIgnoreInvalid(schema129));
}
@Test
public void testDecimalFailsWithZeroPrecision() {
final Schema schema = Schema.createFixed("aDecimal", null, null, 4);
assertThrows("Should reject precision", IllegalArgumentException.class,
"Invalid decimal precision: 0 (must be positive)", () -> {
LogicalTypes.decimal(0).addToSchema(schema);
return null;
});
Assert.assertNull("Invalid logical type should not be set on schema", LogicalTypes.fromSchemaIgnoreInvalid(schema));
}
@Test
public void testDecimalFailsWithNegativePrecision() {
final Schema schema = Schema.createFixed("aDecimal", null, null, 4);
assertThrows("Should reject precision", IllegalArgumentException.class,
"Invalid decimal precision: -9 (must be positive)", () -> {
LogicalTypes.decimal(-9).addToSchema(schema);
return null;
});
Assert.assertNull("Invalid logical type should not be set on schema", LogicalTypes.fromSchemaIgnoreInvalid(schema));
}
@Test
public void testDecimalScaleBoundedByPrecision() {
final Schema schema = Schema.createFixed("aDecimal", null, null, 4);
assertThrows("Should reject precision", IllegalArgumentException.class,
"Invalid decimal scale: 10 (greater than precision: 9)", () -> {
LogicalTypes.decimal(9, 10).addToSchema(schema);
return null;
});
Assert.assertNull("Invalid logical type should not be set on schema", LogicalTypes.fromSchemaIgnoreInvalid(schema));
}
@Test
public void testDecimalFailsWithNegativeScale() {
final Schema schema = Schema.createFixed("aDecimal", null, null, 4);
assertThrows("Should reject precision", IllegalArgumentException.class,
"Invalid decimal scale: -2 (must be positive)", () -> {
LogicalTypes.decimal(9, -2).addToSchema(schema);
return null;
});
Assert.assertNull("Invalid logical type should not be set on schema", LogicalTypes.fromSchemaIgnoreInvalid(schema));
}
@Test
public void testSchemaRejectsSecondLogicalType() {
final Schema schema = Schema.createFixed("aDecimal", null, null, 4);
LogicalTypes.decimal(9).addToSchema(schema);
assertThrows("Should reject second logical type", AvroRuntimeException.class, "Can't overwrite property: scale",
() -> {
LogicalTypes.decimal(9, 2).addToSchema(schema);
return null;
});
Assert.assertEquals("First logical type should still be set on schema", LogicalTypes.decimal(9),
LogicalTypes.fromSchemaIgnoreInvalid(schema));
}
@Test
public void testDecimalDefaultScale() {
Schema schema = Schema.createFixed("aDecimal", null, null, 4);
// 4 bytes can hold up to 9 digits of precision
LogicalTypes.decimal(9).addToSchema(schema);
Assert.assertEquals("Scale should be a 0", 0,
((LogicalTypes.Decimal) LogicalTypes.fromSchemaIgnoreInvalid(schema)).getScale());
}
@Test
public void testFixedDecimalToFromJson() {
Schema schema = Schema.createFixed("aDecimal", null, null, 4);
LogicalTypes.decimal(9, 2).addToSchema(schema);
Schema parsed = new Schema.Parser().parse(schema.toString(true));
Assert.assertEquals("Constructed and parsed schemas should match", schema, parsed);
}
@Test
public void testBytesDecimalToFromJson() {
Schema schema = Schema.create(Schema.Type.BYTES);
LogicalTypes.decimal(9, 2).addToSchema(schema);
Schema parsed = new Schema.Parser().parse(schema.toString(true));
Assert.assertEquals("Constructed and parsed schemas should match", schema, parsed);
}
@Test
public void testLogicalTypeEquals() {
LogicalTypes.Decimal decimal90 = LogicalTypes.decimal(9);
LogicalTypes.Decimal decimal80 = LogicalTypes.decimal(8);
LogicalTypes.Decimal decimal92 = LogicalTypes.decimal(9, 2);
assertEqualsTrue("Same decimal", LogicalTypes.decimal(9, 0), decimal90);
assertEqualsTrue("Same decimal", LogicalTypes.decimal(8, 0), decimal80);
assertEqualsTrue("Same decimal", LogicalTypes.decimal(9, 2), decimal92);
assertEqualsFalse("Different logical type", LogicalTypes.uuid(), decimal90);
assertEqualsFalse("Different precision", decimal90, decimal80);
assertEqualsFalse("Different scale", decimal90, decimal92);
}
@Test
public void testLogicalTypeInSchemaEquals() {
Schema schema1 = Schema.createFixed("aDecimal", null, null, 4);
Schema schema2 = Schema.createFixed("aDecimal", null, null, 4);
Schema schema3 = Schema.createFixed("aDecimal", null, null, 4);
Assert.assertNotSame(schema1, schema2);
Assert.assertNotSame(schema1, schema3);
assertEqualsTrue("No logical types", schema1, schema2);
assertEqualsTrue("No logical types", schema1, schema3);
LogicalTypes.decimal(9).addToSchema(schema1);
assertEqualsFalse("Two has no logical type", schema1, schema2);
LogicalTypes.decimal(9).addToSchema(schema2);
assertEqualsTrue("Same logical types", schema1, schema2);
LogicalTypes.decimal(9, 2).addToSchema(schema3);
assertEqualsFalse("Different logical type", schema1, schema3);
}
@Test
public void testRegisterLogicalTypeThrowsIfTypeNameNotProvided() {
assertThrows("Should error if type name was not provided", UnsupportedOperationException.class,
"LogicalTypeFactory TypeName has not been provided", () -> {
LogicalTypes.register(schema -> LogicalTypes.date());
return null;
});
}
@Test
public void testRegisterLogicalTypeWithName() {
final LogicalTypes.LogicalTypeFactory factory = new LogicalTypes.LogicalTypeFactory() {
@Override
public LogicalType fromSchema(Schema schema) {
return LogicalTypes.date();
}
@Override
public String getTypeName() {
return "typename";
}
};
LogicalTypes.register("registered", factory);
MatcherAssert.assertThat(LogicalTypes.getCustomRegisteredTypes(), IsMapContaining.hasEntry("registered", factory));
}
@Test
public void testRegisterLogicalTypeWithFactoryName() {
final LogicalTypes.LogicalTypeFactory factory = new LogicalTypes.LogicalTypeFactory() {
@Override
public LogicalType fromSchema(Schema schema) {
return LogicalTypes.date();
}
@Override
public String getTypeName() {
return "factory";
}
};
LogicalTypes.register(factory);
MatcherAssert.assertThat(LogicalTypes.getCustomRegisteredTypes(), IsMapContaining.hasEntry("factory", factory));
}
@Test
public void testRegisterLogicalTypeWithFactoryNameNotProvided() {
final LogicalTypes.LogicalTypeFactory factory = schema -> LogicalTypes.date();
LogicalTypes.register("logicalTypeName", factory);
MatcherAssert.assertThat(LogicalTypes.getCustomRegisteredTypes(),
IsMapContaining.hasEntry("logicalTypeName", factory));
}
@Test
public void testRegisterLogicalTypeFactoryByServiceLoader() {
MatcherAssert.assertThat(LogicalTypes.getCustomRegisteredTypes(),
IsMapContaining.hasEntry(equalTo("service-example"), instanceOf(LogicalTypes.LogicalTypeFactory.class)));
}
public static void assertEqualsTrue(String message, Object o1, Object o2) {
Assert.assertEquals("Should be equal (forward): " + message, o1, o2);
Assert.assertEquals("Should be equal (reverse): " + message, o2, o1);
}
public static void assertEqualsFalse(String message, Object o1, Object o2) {
Assert.assertNotEquals("Should be equal (forward): " + message, o1, o2);
Assert.assertNotEquals("Should be equal (reverse): " + message, o2, o1);
}
/**
* A convenience method to avoid a large number of @Test(expected=...) tests
*
* @param message A String message to describe this assertion
* @param expected An Exception class that the Runnable should throw
* @param containedInMessage A String that should be contained by the thrown
* exception's message
* @param callable A Callable that is expected to throw the exception
*/
public static void assertThrows(String message, Class<? extends Exception> expected, String containedInMessage,
Callable<?> callable) {
try {
callable.call();
Assert.fail("No exception was thrown (" + message + "), expected: " + expected.getName());
} catch (Exception actual) {
Assert.assertEquals(message, expected, actual.getClass());
Assert.assertTrue("Expected exception message (" + containedInMessage + ") missing: " + actual.getMessage(),
actual.getMessage().contains(containedInMessage));
}
}
}