| /* |
| * 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.collection.IsMapContaining; |
| import org.junit.jupiter.api.Test; |
| |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.hamcrest.Matchers.equalTo; |
| import static org.hamcrest.Matchers.instanceOf; |
| import static org.junit.jupiter.api.Assertions.*; |
| |
| public class TestLogicalType { |
| |
| @Test |
| void decimalFromSchema() { |
| 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); |
| |
| assertTrue(logicalType instanceof LogicalTypes.Decimal, "Should be a Decimal"); |
| LogicalTypes.Decimal decimal = (LogicalTypes.Decimal) logicalType; |
| assertEquals(9, decimal.getPrecision(), "Should have correct precision"); |
| assertEquals(2, decimal.getScale(), "Should have correct scale"); |
| } |
| |
| @Test |
| void invalidLogicalTypeIgnored() { |
| final Schema schema = Schema.createFixed("aFixed", null, null, 2); |
| schema.addProp("logicalType", "decimal"); |
| schema.addProp("precision", 9); |
| schema.addProp("scale", 2); |
| |
| assertNull(LogicalTypes.fromSchemaIgnoreInvalid(schema), "Should ignore invalid logical type"); |
| } |
| |
| @Test |
| void decimalWithNonByteArrayTypes() { |
| 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 |
| void unknownFromJsonNode() { |
| Schema schema = Schema.create(Schema.Type.STRING); |
| schema.addProp("logicalType", "unknown"); |
| schema.addProp("someProperty", 34); |
| LogicalType logicalType = LogicalTypes.fromSchemaIgnoreInvalid(schema); |
| assertNull(logicalType, "Should not return a LogicalType instance"); |
| } |
| |
| @Test |
| void decimalBytesHasNoPrecisionLimit() { |
| Schema schema = Schema.create(Schema.Type.BYTES); |
| // precision is not limited for bytes |
| LogicalTypes.decimal(Integer.MAX_VALUE).addToSchema(schema); |
| assertEquals(Integer.MAX_VALUE, |
| ((LogicalTypes.Decimal) LogicalTypes.fromSchemaIgnoreInvalid(schema)).getPrecision(), |
| "Precision should be an Integer.MAX_VALUE"); |
| } |
| |
| @Test |
| void decimalFixedPrecisionLimit() { |
| // 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; |
| }); |
| assertNull(LogicalTypes.fromSchemaIgnoreInvalid(schema), "Invalid logical type should not be set on 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; |
| }); |
| assertNull(LogicalTypes.fromSchemaIgnoreInvalid(schema129), "Invalid logical type should not be set on schema"); |
| } |
| |
| @Test |
| void decimalFailsWithZeroPrecision() { |
| 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; |
| }); |
| assertNull(LogicalTypes.fromSchemaIgnoreInvalid(schema), "Invalid logical type should not be set on schema"); |
| } |
| |
| @Test |
| void decimalFailsWithNegativePrecision() { |
| 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; |
| }); |
| assertNull(LogicalTypes.fromSchemaIgnoreInvalid(schema), "Invalid logical type should not be set on schema"); |
| } |
| |
| @Test |
| void decimalScaleBoundedByPrecision() { |
| 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; |
| }); |
| assertNull(LogicalTypes.fromSchemaIgnoreInvalid(schema), "Invalid logical type should not be set on schema"); |
| } |
| |
| @Test |
| void decimalFailsWithNegativeScale() { |
| 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; |
| }); |
| assertNull(LogicalTypes.fromSchemaIgnoreInvalid(schema), "Invalid logical type should not be set on schema"); |
| } |
| |
| @Test |
| void schemaRejectsSecondLogicalType() { |
| 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; |
| }); |
| assertEquals(LogicalTypes.decimal(9), LogicalTypes.fromSchemaIgnoreInvalid(schema), |
| "First logical type should still be set on schema"); |
| } |
| |
| @Test |
| void decimalDefaultScale() { |
| Schema schema = Schema.createFixed("aDecimal", null, null, 4); |
| // 4 bytes can hold up to 9 digits of precision |
| LogicalTypes.decimal(9).addToSchema(schema); |
| assertEquals(0, ((LogicalTypes.Decimal) LogicalTypes.fromSchemaIgnoreInvalid(schema)).getScale(), |
| "Scale should be a 0"); |
| } |
| |
| @Test |
| void fixedDecimalToFromJson() { |
| Schema schema = Schema.createFixed("aDecimal", null, null, 4); |
| LogicalTypes.decimal(9, 2).addToSchema(schema); |
| Schema parsed = new Schema.Parser().parse(schema.toString(true)); |
| assertEquals(schema, parsed, "Constructed and parsed schemas should match"); |
| } |
| |
| @Test |
| void bytesDecimalToFromJson() { |
| Schema schema = Schema.create(Schema.Type.BYTES); |
| LogicalTypes.decimal(9, 2).addToSchema(schema); |
| Schema parsed = new Schema.Parser().parse(schema.toString(true)); |
| assertEquals(schema, parsed, "Constructed and parsed schemas should match"); |
| } |
| |
| @Test |
| void logicalTypeEquals() { |
| 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 |
| void logicalTypeInSchemaEquals() { |
| Schema schema1 = Schema.createFixed("aDecimal", null, null, 4); |
| Schema schema2 = Schema.createFixed("aDecimal", null, null, 4); |
| Schema schema3 = Schema.createFixed("aDecimal", null, null, 4); |
| assertNotSame(schema1, schema2); |
| 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 |
| void registerLogicalTypeThrowsIfTypeNameNotProvided() { |
| 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 |
| void registerLogicalTypeWithName() { |
| 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); |
| |
| assertThat(LogicalTypes.getCustomRegisteredTypes(), IsMapContaining.hasEntry("registered", factory)); |
| } |
| |
| @Test |
| void registerLogicalTypeWithFactoryName() { |
| 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); |
| |
| assertThat(LogicalTypes.getCustomRegisteredTypes(), IsMapContaining.hasEntry("factory", factory)); |
| } |
| |
| @Test |
| void registerLogicalTypeWithFactoryNameNotProvided() { |
| final LogicalTypes.LogicalTypeFactory factory = schema -> LogicalTypes.date(); |
| |
| LogicalTypes.register("logicalTypeName", factory); |
| |
| assertThat(LogicalTypes.getCustomRegisteredTypes(), IsMapContaining.hasEntry("logicalTypeName", factory)); |
| } |
| |
| @Test |
| void registerLogicalTypeFactoryByServiceLoader() { |
| assertThat(LogicalTypes.getCustomRegisteredTypes(), |
| IsMapContaining.hasEntry(equalTo("service-example"), instanceOf(LogicalTypes.LogicalTypeFactory.class))); |
| } |
| |
| public static void assertEqualsTrue(String message, Object o1, Object o2) { |
| assertEquals(o1, o2, "Should be equal (forward): " + message); |
| assertEquals(o2, o1, "Should be equal (reverse): " + message); |
| } |
| |
| public static void assertEqualsFalse(String message, Object o1, Object o2) { |
| assertNotEquals(o1, o2, "Should be equal (forward): " + message); |
| assertNotEquals(o2, o1, "Should be equal (reverse): " + message); |
| } |
| |
| /** |
| * 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(); |
| fail("No exception was thrown (" + message + "), expected: " + expected.getName()); |
| } catch (Exception actual) { |
| assertEquals(expected, actual.getClass(), message); |
| assertTrue(actual.getMessage().contains(containedInMessage), |
| "Expected exception message (" + containedInMessage + ") missing: " + actual.getMessage()); |
| } |
| } |
| } |