/**
* 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.avro;
import java.util.Arrays;
import java.util.concurrent.Callable;
import org.junit.Assert;
import org.junit.Test;
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", new Callable() {
@Override
public Object call() throws Exception {
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)", new Callable() {
@Override
public Object call() throws Exception {
LogicalTypes.decimal(10).addToSchema(schema);
return null;
}
}
);
Assert.assertNull("Invalid logical type should not be set on schema",
LogicalTypes.fromSchemaIgnoreInvalid(schema));
}
@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)", new Callable() {
@Override
public Object call() throws Exception {
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)", new Callable() {
@Override
public Object call() throws Exception {
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)",
new Callable() {
@Override
public Object call() throws Exception {
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)", new Callable() {
@Override
public Object call() throws Exception {
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", new Callable() {
@Override
public Object call() throws Exception {
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);
}
public static void assertEqualsTrue(String message, Object o1, Object o2) {
Assert.assertTrue("Should be equal (forward): " + message, o1.equals(o2));
Assert.assertTrue("Should be equal (reverse): " + message, o2.equals(o1));
}
public static void assertEqualsFalse(String message, Object o1, Object o2) {
Assert.assertFalse("Should be equal (forward): " + message, o1.equals(o2));
Assert.assertFalse("Should be equal (reverse): " + message, o2.equals(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)
);
}
}
}