/** * 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) ); } } }