FrameTest.java

/*
 * 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.calcite.avatica;

import org.apache.calcite.avatica.Meta.Frame;
import org.apache.calcite.avatica.proto.Common;
import org.apache.calcite.avatica.proto.Common.ColumnValue;
import org.apache.calcite.avatica.proto.Common.TypedValue;

import org.junit.Test;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Tests serialization of {@link Frame}.
 */
public class FrameTest {

  private static final TypedValue NUMBER_VALUE = TypedValue.newBuilder().setNumberValue(1)
      .setType(Common.Rep.LONG).build();

  private void serializeAndTestEquality(Frame frame) {
    Frame frameCopy = Frame.fromProto(frame.toProto());

    assertEquals(frame.done, frameCopy.done);
    assertEquals(frame.offset, frameCopy.offset);

    Iterable<Object> origRows = frame.rows;
    Iterable<Object> copiedRows = frameCopy.rows;

    assertEquals("Expected rows to both be null, or both be non-null",
        origRows == null, copiedRows == null);

    Iterator<Object> origIter = origRows.iterator();
    Iterator<Object> copiedIter = copiedRows.iterator();
    while (origIter.hasNext() && copiedIter.hasNext()) {
      Object orig = origIter.next();
      Object copy = copiedIter.next();

      assertEquals(orig == null, copy == null);

      // This is goofy, but it seems like an Array comes from the JDBC implementation but then
      // the resulting Frame has a List to support the Avatica typed Accessors
      assertEquals(Object[].class, orig.getClass());
      assertTrue("Expected List but got " + copy.getClass(), copy instanceof List);

      @SuppressWarnings("unchecked")
      List<Object> copyList = (List<Object>) copy;

      assertArrayEquals((Object[]) orig, copyList.toArray(new Object[0]));
    }

    assertEquals(origIter.hasNext(), copiedIter.hasNext());
  }

  @Test
  public void testEmpty() {
    serializeAndTestEquality(Frame.EMPTY);
  }

  @Test
  public void testSingleRow() {
    ArrayList<Object> rows = new ArrayList<>();
    rows.add(new Object[] {"string", Integer.MAX_VALUE, new Date().getTime()});

    Frame singleRow = new Frame(0, true, rows);

    serializeAndTestEquality(singleRow);
  }

  @Test
  public void testMultipleRows() {
    ArrayList<Object> rows = new ArrayList<>();
    rows.add(new Object[] {"string", Integer.MAX_VALUE, new Date().getTime()});
    rows.add(new Object[] {"gnirts", 0, Long.MIN_VALUE});
    rows.add(new Object[] {"", null, Long.MAX_VALUE});

    Frame singleRow = new Frame(0, true, rows);

    serializeAndTestEquality(singleRow);
  }

  protected void testValueRoundTrip(Object value) {
    List<Object> rows = Collections.singletonList(new Object[]{value});
    serializeAndTestEquality(new Frame(0, true, rows));
  }

  @Test
  public void testInteger() {
    testValueRoundTrip(Integer.MIN_VALUE);
    testValueRoundTrip(Integer.MAX_VALUE);
  }

  @Test
  public void testLong() {
    testValueRoundTrip(1L);
    testValueRoundTrip(0L);
    testValueRoundTrip(Long.MIN_VALUE);
    testValueRoundTrip(Long.MAX_VALUE);
  }

  @Test
  public void testFloat() {
    testValueRoundTrip(Float.MIN_VALUE);
    testValueRoundTrip(Float.MAX_VALUE);
    testValueRoundTrip(Float.MIN_NORMAL);
  }

  @Test
  public void testDouble() {
    testValueRoundTrip(Double.MIN_VALUE);
    testValueRoundTrip(Double.MAX_VALUE);
    testValueRoundTrip(Double.MIN_NORMAL);
  }

  @Test
  public void testString() {
    testValueRoundTrip("example-value");
    testValueRoundTrip("");
  }

  @Test
  public void testChar() {
    testValueRoundTrip('a');
    testValueRoundTrip('\0');
  }

  @Test
  public void testByte() {
    testValueRoundTrip(Byte.MAX_VALUE);
    testValueRoundTrip(Byte.MIN_VALUE);
  }

  @Test
  public void testByteArray() {
    testValueRoundTrip(new byte[] {1, 0});
  }

  @Test
  public void testBoolean() {
    testValueRoundTrip(true);
    testValueRoundTrip(false);
  }

  @Test
  public void testShort() {
    testValueRoundTrip(Short.MAX_VALUE);
    testValueRoundTrip(Short.MIN_VALUE);
  }

  @Test
  public void testBigDecimal() {
    testValueRoundTrip(BigDecimal.valueOf(0));
    testValueRoundTrip(BigDecimal.valueOf(1));
    testValueRoundTrip(BigDecimal // make a non-integer value larger than Long.MAX_VALUE
        .valueOf(Long.MAX_VALUE)
        .multiply(BigDecimal.TEN)
        .add(BigDecimal.valueOf(0.12345d)));
  }

  @Test public void testMalformedColumnValue() {
    // Invalid ColumnValue: has an array and scalar
    final ColumnValue bothAttributesColumnValue = ColumnValue.newBuilder().setHasArrayValue(true)
        .setScalarValue(NUMBER_VALUE).build();
    // Note omission of setScalarValue(TypedValue).
    final ColumnValue neitherAttributeColumnValue = ColumnValue.newBuilder().setHasArrayValue(false)
        .build();

    try {
      Frame.validateColumnValue(bothAttributesColumnValue);
      fail("Validating the ColumnValue should have failed as it has an array and scalar");
    } catch (IllegalArgumentException e) {
      // Pass
    }

    try {
      Frame.validateColumnValue(neitherAttributeColumnValue);
      fail("Validating the ColumnValue should have failed as it has neither an array nor scalar");
    } catch (IllegalArgumentException e) {
      // Pass
    }
  }

  @Test public void testColumnValueBackwardsCompatibility() {
    // 1
    final ColumnValue oldStyleScalarValue = ColumnValue.newBuilder().addValue(NUMBER_VALUE).build();
    // [1, 1]
    final ColumnValue oldStyleArrayValue = ColumnValue.newBuilder().addValue(NUMBER_VALUE)
        .addValue(NUMBER_VALUE).build();

    assertFalse(Frame.isNewStyleColumn(oldStyleScalarValue));
    assertFalse(Frame.isNewStyleColumn(oldStyleArrayValue));

    Object scalar = Frame.parseOldStyleColumn(oldStyleScalarValue);
    assertEquals(1L, scalar);

    Object array = Frame.parseOldStyleColumn(oldStyleArrayValue);
    assertEquals(Arrays.asList(1L, 1L), array);
  }

  @Test public void testColumnValueParsing() {
    // 1
    final ColumnValue scalarValue = ColumnValue.newBuilder().setScalarValue(NUMBER_VALUE).build();
    // [1, 1]
    final ColumnValue arrayValue = ColumnValue.newBuilder().addArrayValue(NUMBER_VALUE)
        .addArrayValue(NUMBER_VALUE).setHasArrayValue(true).build();

    assertTrue(Frame.isNewStyleColumn(scalarValue));
    assertTrue(Frame.isNewStyleColumn(arrayValue));

    Object scalar = Frame.parseColumn(scalarValue);
    assertEquals(1L, scalar);

    Object array = Frame.parseColumn(arrayValue);
    assertEquals(Arrays.asList(1L, 1L), array);
  }

  @Test public void testDeprecatedValueAttributeForScalars() {
    // Create a row with schema: [VARCHAR, INTEGER, DATE]
    List<Object> rows = Collections.<Object>singletonList(new Object[] {"string", Integer.MAX_VALUE,
        new Date().getTime()});
    Meta.Frame frame = Meta.Frame.create(0, true, rows);
    // Convert it to a protobuf
    Common.Frame protoFrame = frame.toProto();
    assertEquals(1, protoFrame.getRowsCount());
    // Get that row we created
    Common.Row protoRow = protoFrame.getRows(0);
    // One row has many columns
    List<Common.ColumnValue> protoColumns = protoRow.getValueList();
    assertEquals(3, protoColumns.size());
    // Verify that the scalar value is also present in the deprecated values attributes.
    List<Common.TypedValue> deprecatedValues = protoColumns.get(0).getValueList();
    assertEquals(1, deprecatedValues.size());
    Common.TypedValue scalarValue = protoColumns.get(0).getScalarValue();
    assertEquals(deprecatedValues.get(0), scalarValue);
  }

  @Test public void testDeprecatedValueAttributeForArrays() {
    // Create a row with schema: [VARCHAR, ARRAY]
    List<Object> rows = Collections.<Object>singletonList(new Object[] {"string",
        Arrays.asList(1, 2, 3)});
    Meta.Frame frame = Meta.Frame.create(0, true, rows);
    // Convert it to a protobuf
    Common.Frame protoFrame = frame.toProto();
    assertEquals(1, protoFrame.getRowsCount());
    // Get that row we created
    Common.Row protoRow = protoFrame.getRows(0);
    // One row has many columns
    List<Common.ColumnValue> protoColumns = protoRow.getValueList();
    // We should have two columns
    assertEquals(2, protoColumns.size());
    // Fetch the ARRAY column
    Common.ColumnValue protoColumn = protoColumns.get(1);
    // We should have the 3 ARRAY elements in the array_values attribute as well as the deprecated
    // values attribute.
    List<Common.TypedValue> deprecatedValues = protoColumn.getValueList();
    assertEquals(3, deprecatedValues.size());
    assertTrue("Column 2 should have an array_value", protoColumns.get(1).getHasArrayValue());
    List<Common.TypedValue> arrayValues = protoColumns.get(1).getArrayValueList();
    assertEquals(arrayValues, deprecatedValues);
  }

  @Test public void testNestedArraySerialization() {
    List<Object> rows = new ArrayList<>();
    // [ "pk", [[1,2], [3,4]] ]
    rows.add(Arrays.asList("pk", Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4))));
    Frame frame = new Frame(0, true, rows);
    // Parse back the list in serialized form
    Common.Frame protoFrame = frame.toProto();
    Common.Row protoRow = protoFrame.getRows(0);
    Common.ColumnValue protoColumn = protoRow.getValue(1);
    assertTrue(protoColumn.getHasArrayValue());
    int value = 1;
    for (Common.TypedValue arrayElement : protoColumn.getArrayValueList()) {
      assertEquals(Common.Rep.ARRAY, arrayElement.getType());
      for (Common.TypedValue nestedArrayElement : arrayElement.getArrayValueList()) {
        assertEquals(Common.Rep.INTEGER, nestedArrayElement.getType());
        assertEquals(value++, nestedArrayElement.getNumberValue());
      }
    }

    Frame newFrame = Frame.fromProto(protoFrame);
    @SuppressWarnings("unchecked")
    List<Object> newRow = (List<Object>) newFrame.rows.iterator().next();
    @SuppressWarnings("unchecked")
    List<Object> expectedRow = (List<Object>) rows.get(0);
    assertEquals(expectedRow.get(0), newRow.get(0));
    assertEquals(expectedRow.get(1), newRow.get(1));
  }
}

// End FrameTest.java