@VisibleForTesting
    public static DataType inferWiderType(DataType lType, DataType rType) {
        // Ignore nullability during data type merge
        boolean nullable = lType.isNullable() || rType.isNullable();
        lType = lType.notNull();
        rType = rType.notNull();

        DataType mergedType;
        if (lType.equals(rType)) {
            // identical type
            mergedType = rType;
        } else if (lType.is(DataTypeFamily.INTEGER_NUMERIC)
                && rType.is(DataTypeFamily.INTEGER_NUMERIC)) {
            mergedType = DataTypes.BIGINT();
        } else if (lType.is(DataTypeFamily.CHARACTER_STRING)
                && rType.is(DataTypeFamily.CHARACTER_STRING)) {
            mergedType = DataTypes.STRING();
        } else if (lType.is(DataTypeFamily.APPROXIMATE_NUMERIC)
                && rType.is(DataTypeFamily.APPROXIMATE_NUMERIC)) {
            mergedType = DataTypes.DOUBLE();
        } else if (lType.is(DataTypeRoot.DECIMAL) && rType.is(DataTypeRoot.DECIMAL)) {
            // Merge two decimal types
            DecimalType lhsDecimal = (DecimalType) lType;
            DecimalType rhsDecimal = (DecimalType) rType;
            int resultIntDigits =
                    Math.max(
                            lhsDecimal.getPrecision() - lhsDecimal.getScale(),
                            rhsDecimal.getPrecision() - rhsDecimal.getScale());
            int resultScale = Math.max(lhsDecimal.getScale(), rhsDecimal.getScale());
            mergedType = DataTypes.DECIMAL(resultIntDigits + resultScale, resultScale);
        } else if (lType.is(DataTypeRoot.DECIMAL) && rType.is(DataTypeFamily.EXACT_NUMERIC)) {
            // Merge decimal and int
            DecimalType lhsDecimal = (DecimalType) lType;
            mergedType =
                    DataTypes.DECIMAL(
                            Math.max(
                                    lhsDecimal.getPrecision(),
                                    lhsDecimal.getScale() + getNumericPrecision(rType)),
                            lhsDecimal.getScale());
        } else if (rType.is(DataTypeRoot.DECIMAL) && lType.is(DataTypeFamily.EXACT_NUMERIC)) {
            // Merge decimal and int
            DecimalType rhsDecimal = (DecimalType) rType;
            mergedType =
                    DataTypes.DECIMAL(
                            Math.max(
                                    rhsDecimal.getPrecision(),
                                    rhsDecimal.getScale() + getNumericPrecision(lType)),
                            rhsDecimal.getScale());
        } else {
            throw new IllegalStateException(
                    String.format("Incompatible types: \"%s\" and \"%s\"", lType, rType));
        }

        if (nullable) {
            return mergedType.nullable();
        } else {
            return mergedType.notNull();
        }
    }
@Test
    public void testInferWiderType() {
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(DataTypes.BINARY(17), DataTypes.BINARY(17)))
                .isEqualTo(DataTypes.BINARY(17));
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.VARBINARY(17), DataTypes.VARBINARY(17)))
                .isEqualTo(DataTypes.VARBINARY(17));
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.BYTES(), DataTypes.BYTES()))
                .isEqualTo(DataTypes.BYTES());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.BOOLEAN(), DataTypes.BOOLEAN()))
                .isEqualTo(DataTypes.BOOLEAN());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.INT(), DataTypes.INT()))
                .isEqualTo(DataTypes.INT());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.TINYINT(), DataTypes.TINYINT()))
                .isEqualTo(DataTypes.TINYINT());
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(DataTypes.SMALLINT(), DataTypes.SMALLINT()))
                .isEqualTo(DataTypes.SMALLINT());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.BIGINT(), DataTypes.BIGINT()))
                .isEqualTo(DataTypes.BIGINT());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.FLOAT(), DataTypes.FLOAT()))
                .isEqualTo(DataTypes.FLOAT());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.DOUBLE(), DataTypes.DOUBLE()))
                .isEqualTo(DataTypes.DOUBLE());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.CHAR(17), DataTypes.CHAR(17)))
                .isEqualTo(DataTypes.CHAR(17));
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(DataTypes.VARCHAR(17), DataTypes.VARCHAR(17)))
                .isEqualTo(DataTypes.VARCHAR(17));
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.STRING(), DataTypes.STRING()))
                .isEqualTo(DataTypes.STRING());
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.DECIMAL(17, 7), DataTypes.DECIMAL(17, 7)))
                .isEqualTo(DataTypes.DECIMAL(17, 7));
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.DATE(), DataTypes.DATE()))
                .isEqualTo(DataTypes.DATE());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.TIME(), DataTypes.TIME()))
                .isEqualTo(DataTypes.TIME());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.TIME(6), DataTypes.TIME(6)))
                .isEqualTo(DataTypes.TIME(6));
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(DataTypes.TIMESTAMP(), DataTypes.TIMESTAMP()))
                .isEqualTo(DataTypes.TIMESTAMP());
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(DataTypes.TIMESTAMP(3), DataTypes.TIMESTAMP(3)))
                .isEqualTo(DataTypes.TIMESTAMP(3));
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.TIMESTAMP_TZ(), DataTypes.TIMESTAMP_TZ()))
                .isEqualTo(DataTypes.TIMESTAMP_TZ());
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.TIMESTAMP_TZ(3), DataTypes.TIMESTAMP_TZ(3)))
                .isEqualTo(DataTypes.TIMESTAMP_TZ(3));
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.TIMESTAMP_LTZ(), DataTypes.TIMESTAMP_LTZ()))
                .isEqualTo(DataTypes.TIMESTAMP_LTZ());
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.TIMESTAMP_LTZ(3), DataTypes.TIMESTAMP_LTZ(3)))
                .isEqualTo(DataTypes.TIMESTAMP_LTZ(3));
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.ARRAY(DataTypes.INT()), DataTypes.ARRAY(DataTypes.INT())))
                .isEqualTo(DataTypes.ARRAY(DataTypes.INT()));
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.MAP(DataTypes.INT(), DataTypes.STRING()),
                                DataTypes.MAP(DataTypes.INT(), DataTypes.STRING())))
                .isEqualTo(DataTypes.MAP(DataTypes.INT(), DataTypes.STRING()));

        // Test compatible widening cast
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.INT(), DataTypes.BIGINT()))
                .isEqualTo(DataTypes.BIGINT());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.VARCHAR(17), DataTypes.STRING()))
                .isEqualTo(DataTypes.STRING());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.FLOAT(), DataTypes.DOUBLE()))
                .isEqualTo(DataTypes.DOUBLE());
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.INT(), DataTypes.DECIMAL(4, 0)))
                .isEqualTo(DataTypes.DECIMAL(10, 0));
        Assertions.assertThat(SchemaUtils.inferWiderType(DataTypes.INT(), DataTypes.DECIMAL(10, 5)))
                .isEqualTo(DataTypes.DECIMAL(15, 5));
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(DataTypes.BIGINT(), DataTypes.DECIMAL(10, 5)))
                .isEqualTo(DataTypes.DECIMAL(24, 5));
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.DECIMAL(5, 4), DataTypes.DECIMAL(10, 2)))
                .isEqualTo(DataTypes.DECIMAL(12, 4));

        // Test merging with nullability
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.INT().notNull(), DataTypes.INT().notNull()))
                .isEqualTo(DataTypes.INT().notNull());
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.INT().nullable(), DataTypes.INT().notNull()))
                .isEqualTo(DataTypes.INT().nullable());
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.INT().notNull(), DataTypes.INT().nullable()))
                .isEqualTo(DataTypes.INT().nullable());
        Assertions.assertThat(
                        SchemaUtils.inferWiderType(
                                DataTypes.INT().nullable(), DataTypes.INT().nullable()))
                .isEqualTo(DataTypes.INT().nullable());

        // incompatible type merges test
        Assertions.assertThatThrownBy(
                        () -> SchemaUtils.inferWiderType(DataTypes.INT(), DataTypes.DOUBLE()))
                .isExactlyInstanceOf(IllegalStateException.class);

        Assertions.assertThatThrownBy(
                        () ->
                                SchemaUtils.inferWiderType(
                                        DataTypes.DECIMAL(17, 0), DataTypes.DOUBLE()))
                .isExactlyInstanceOf(IllegalStateException.class);
        Assertions.assertThatThrownBy(
                        () -> SchemaUtils.inferWiderType(DataTypes.INT(), DataTypes.STRING()))
                .isExactlyInstanceOf(IllegalStateException.class);
    }