/*
 * 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.drill.exec.physical.rowSet;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import java.util.List;

import org.apache.drill.categories.RowSetTest;
import org.apache.drill.common.types.TypeProtos.MajorType;
import org.apache.drill.common.types.TypeProtos.MinorType;
import org.apache.drill.exec.physical.rowSet.RowSet.ExtendableRowSet;
import org.apache.drill.exec.physical.rowSet.RowSet.SingleRowSet;
import org.apache.drill.exec.record.VectorContainer;
import org.apache.drill.exec.record.metadata.SchemaBuilder;
import org.apache.drill.exec.record.metadata.TupleMetadata;
import org.apache.drill.exec.vector.NullableBigIntVector;
import org.apache.drill.exec.vector.NullableFloat8Vector;
import org.apache.drill.exec.vector.NullableIntVector;
import org.apache.drill.exec.vector.NullableVarCharVector;
import org.apache.drill.exec.vector.ValueVector;
import org.apache.drill.exec.vector.accessor.ArrayReader;
import org.apache.drill.exec.vector.accessor.ArrayWriter;
import org.apache.drill.exec.vector.accessor.ObjectReader;
import org.apache.drill.exec.vector.accessor.ObjectType;
import org.apache.drill.exec.vector.accessor.ObjectWriter;
import org.apache.drill.exec.vector.accessor.ScalarReader;
import org.apache.drill.exec.vector.accessor.ScalarWriter;
import org.apache.drill.exec.vector.accessor.TupleReader;
import org.apache.drill.exec.vector.accessor.TupleWriter;
import org.apache.drill.exec.vector.accessor.VariantReader;
import org.apache.drill.exec.vector.accessor.VariantWriter;
import org.apache.drill.exec.vector.complex.ListVector;
import org.apache.drill.exec.vector.complex.MapVector;
import org.apache.drill.exec.vector.complex.UnionVector;
import org.apache.drill.test.SubOperatorTest;
import org.junit.Test;
import org.junit.experimental.categories.Category;

/**
 * Tests for readers and writers for union and list types.
 * <p>
 * Note that the union type is only partially supported in Drill.
 * The list type is unsupported. (However, the list type works in
 * the schema builder, row set writer, row set reader and the
 * result set builder. It does not, however, work in the Project
 * and other operators. Some assembly required for future use.)
 */

@Category(RowSetTest.class)
public class TestVariantAccessors extends SubOperatorTest {

  @Test
  public void testBuildRowSetUnion() {
    final TupleMetadata schema = new SchemaBuilder()

        // Union with simple and complex types

        .addUnion("u")
          .addType(MinorType.INT)
          .addMap()
            .addNullable("c", MinorType.BIGINT)
            .addNullable("d", MinorType.VARCHAR)
            .resumeUnion()
          .addList()
            .addType(MinorType.VARCHAR)
            .resumeUnion()
          .resumeSchema()
        .buildSchema();

    final ExtendableRowSet rowSet = fixture.rowSet(schema);
    final VectorContainer vc = rowSet.container();
    assertEquals(1, vc.getNumberOfColumns());

    // Single union

    final ValueVector vector = vc.getValueVector(0).getValueVector();
    assertTrue(vector instanceof UnionVector);
    final UnionVector union = (UnionVector) vector;

    final MapVector typeMap = union.getTypeMap();
    ValueVector member = typeMap.getChild(MinorType.INT.name());
    assertTrue(member instanceof NullableIntVector);

    // Inner map

    member = typeMap.getChild(MinorType.MAP.name());
    assertTrue(member instanceof MapVector);
    member = typeMap.getChild(MinorType.MAP.name());
    assertTrue(member instanceof MapVector);
    final MapVector childMap = (MapVector) member;
    ValueVector mapMember = childMap.getChild("c");
    assertNotNull(mapMember);
    assertTrue(mapMember instanceof NullableBigIntVector);
    mapMember = childMap.getChild("d");
    assertNotNull(mapMember);
    assertTrue(mapMember instanceof NullableVarCharVector);

    // Inner list

    member = typeMap.getChild(MinorType.LIST.name());
    assertTrue(member instanceof ListVector);
    final ListVector list = (ListVector) member;
    assertTrue(list.getDataVector() instanceof NullableVarCharVector);

    rowSet.clear();
  }

  /**
   * Test a variant (AKA "union vector") at the top level, using
   * just scalar values.
   */

  @Test
  public void testScalarVariant() {
    final TupleMetadata schema = new SchemaBuilder()
        .addUnion("u")
          .addType(MinorType.INT)
          .addType(MinorType.VARCHAR)
          .addType(MinorType.FLOAT8)
          .resumeSchema()
        .buildSchema();

    final ExtendableRowSet rs = fixture.rowSet(schema);
    final RowSetWriter writer = rs.writer();

    // Sanity check of writer structure

    final ObjectWriter wo = writer.column(0);
    assertEquals(ObjectType.VARIANT, wo.type());
    final VariantWriter vw = wo.variant();
    assertSame(vw, writer.variant(0));
    assertSame(vw, writer.variant("u"));
    assertTrue(vw.hasType(MinorType.INT));
    assertTrue(vw.hasType(MinorType.VARCHAR));
    assertTrue(vw.hasType(MinorType.FLOAT8));

    // Write values of different types

    vw.scalar(MinorType.INT).setInt(10);
    writer.save();

    vw.scalar(MinorType.VARCHAR).setString("fred");
    writer.save();

    // The entire variant is null

    vw.setNull();
    writer.save();

    vw.scalar(MinorType.FLOAT8).setDouble(123.45);
    writer.save();

    // Strange case: just the value is null, but the variant
    // is not null.

    vw.scalar(MinorType.INT).setNull();
    writer.save();

    // Marker to avoid fill-empty issues (fill-empties tested elsewhere.)

    vw.scalar(MinorType.INT).setInt(20);
    writer.save();

    final SingleRowSet result = writer.done();
    assertEquals(6, result.rowCount());

    // Read the values.

    final RowSetReader reader = result.reader();

    // Sanity check of structure

    final ObjectReader ro = reader.column(0);
    assertEquals(ObjectType.VARIANT, ro.type());
    final VariantReader vr = ro.variant();
    assertSame(vr, reader.variant(0));
    assertSame(vr, reader.variant("u"));
    for (final MinorType type : MinorType.values()) {
      if (type == MinorType.INT || type == MinorType.VARCHAR || type == MinorType.FLOAT8) {
        assertTrue(vr.hasType(type));
      } else {
        assertFalse(vr.hasType(type));
      }
    }

    // Can get readers up front

    final ScalarReader intReader = vr.scalar(MinorType.INT);
    final ScalarReader strReader = vr.scalar(MinorType.VARCHAR);
    final ScalarReader floatReader = vr.scalar(MinorType.FLOAT8);

    // Verify the data

    // Int 10

    assertTrue(reader.next());
    assertFalse(vr.isNull());
    assertSame(vr.dataType(), MinorType.INT);
    assertSame(intReader, vr.scalar());
    assertNotNull(vr.member());
    assertSame(vr.scalar(), vr.member().scalar());
    assertFalse(intReader.isNull());
    assertEquals(10, intReader.getInt());
    assertTrue(strReader.isNull());
    assertTrue(floatReader.isNull());

    // String "fred"

    assertTrue(reader.next());
    assertFalse(vr.isNull());
    assertSame(vr.dataType(), MinorType.VARCHAR);
    assertSame(strReader, vr.scalar());
    assertFalse(strReader.isNull());
    assertEquals("fred", strReader.getString());
    assertTrue(intReader.isNull());
    assertTrue(floatReader.isNull());

    // Null value

    assertTrue(reader.next());
    assertTrue(vr.isNull());
    assertNull(vr.dataType());
    assertNull(vr.scalar());
    assertTrue(intReader.isNull());
    assertTrue(strReader.isNull());
    assertTrue(floatReader.isNull());

    // Double 123.45

    assertTrue(reader.next());
    assertFalse(vr.isNull());
    assertSame(vr.dataType(), MinorType.FLOAT8);
    assertSame(floatReader, vr.scalar());
    assertFalse(floatReader.isNull());
    assertEquals(123.45, vr.scalar().getDouble(), 0.001);
    assertTrue(intReader.isNull());
    assertTrue(strReader.isNull());

    // Strange case: null int (but union is not null)

    assertTrue(reader.next());
    assertFalse(vr.isNull());
    assertSame(vr.dataType(), MinorType.INT);
    assertTrue(intReader.isNull());

    // Int 20

    assertTrue(reader.next());
    assertFalse(vr.isNull());
    assertFalse(intReader.isNull());
    assertEquals(20, intReader.getInt());

    assertFalse(reader.next());
    result.clear();
  }


  @Test
  public void testBuildRowSetScalarList() {
    final TupleMetadata schema = new SchemaBuilder()

        // Top-level single-element list

        .addList("list2")
          .addType(MinorType.VARCHAR)
          .resumeSchema()
        .buildSchema();

    final ExtendableRowSet rowSet = fixture.rowSet(schema);
    final VectorContainer vc = rowSet.container();
    assertEquals(1, vc.getNumberOfColumns());


    // Single-type list

    final ValueVector vector = vc.getValueVector(0).getValueVector();
    assertTrue(vector instanceof ListVector);
    final ListVector list = (ListVector) vector;
    assertTrue(list.getDataVector() instanceof NullableVarCharVector);

    rowSet.clear();
  }

  @Test
  public void testBuildRowSetUnionArray() {
    final TupleMetadata schema = new SchemaBuilder()

        // List with multiple types

        .addList("list1")
          .addType(MinorType.BIGINT)
          .addMap()
            .addNullable("a", MinorType.INT)
            .addNullable("b", MinorType.VARCHAR)
            .resumeUnion()

          // Nested single-element list

          .addList()
            .addType(MinorType.FLOAT8)
            .resumeUnion()
          .resumeSchema()
        .buildSchema();

    final ExtendableRowSet rowSet = fixture.rowSet(schema);
    final VectorContainer vc = rowSet.container();
    assertEquals(1, vc.getNumberOfColumns());

    // List with complex internal structure

    final ValueVector vector = vc.getValueVector(0).getValueVector();
    assertTrue(vector instanceof ListVector);
    final ListVector list = (ListVector) vector;
    assertTrue(list.getDataVector() instanceof UnionVector);
    final UnionVector union = (UnionVector) list.getDataVector();

    // Union inside the list

    final MajorType unionType = union.getField().getType();
    final List<MinorType> types = unionType.getSubTypeList();
    assertEquals(3, types.size());
    assertTrue(types.contains(MinorType.BIGINT));
    assertTrue(types.contains(MinorType.MAP));
    assertTrue(types.contains(MinorType.LIST));

    final MapVector typeMap = union.getTypeMap();
    ValueVector member = typeMap.getChild(MinorType.BIGINT.name());
    assertTrue(member instanceof NullableBigIntVector);

    // Map inside the list

    member = typeMap.getChild(MinorType.MAP.name());
    assertTrue(member instanceof MapVector);
    final MapVector childMap = (MapVector) member;
    ValueVector mapMember = childMap.getChild("a");
    assertNotNull(mapMember);
    assertTrue(mapMember instanceof NullableIntVector);
    mapMember = childMap.getChild("b");
    assertNotNull(mapMember);
    assertTrue(mapMember instanceof NullableVarCharVector);

    // Single-type list inside the outer list

    member = typeMap.getChild(MinorType.LIST.name());
    assertTrue(member instanceof ListVector);
    final ListVector childList = (ListVector) member;
    assertTrue(childList.getDataVector() instanceof NullableFloat8Vector);

    rowSet.clear();
  }

  /**
   * Test a variant (AKA "union vector") at the top level which
   * includes a map.
   */

  @Test
  public void testUnionWithMap() {
    final TupleMetadata schema = new SchemaBuilder()
        .addUnion("u")
          .addType(MinorType.VARCHAR)
          .addMap()
            .addNullable("a", MinorType.INT)
            .addNullable("b", MinorType.VARCHAR)
            .resumeUnion()
          .resumeSchema()
        .buildSchema();

    SingleRowSet result;

    // Write values

    {
      final ExtendableRowSet rs = fixture.rowSet(schema);
      final RowSetWriter writer = rs.writer();

      // Sanity check of writer structure

      final ObjectWriter wo = writer.column(0);
      assertEquals(ObjectType.VARIANT, wo.type());
      final VariantWriter vw = wo.variant();

      assertTrue(vw.hasType(MinorType.VARCHAR));
      final ObjectWriter strObj = vw.member(MinorType.VARCHAR);
      final ScalarWriter strWriter = strObj.scalar();
      assertSame(strWriter, vw.scalar(MinorType.VARCHAR));

      assertTrue(vw.hasType(MinorType.MAP));
      final ObjectWriter mapObj = vw.member(MinorType.MAP);
      final TupleWriter mWriter = mapObj.tuple();
      assertSame(mWriter, vw.tuple());

      final ScalarWriter aWriter = mWriter.scalar("a");
      final ScalarWriter bWriter = mWriter.scalar("b");

      // First row: string "first"

      vw.setType(MinorType.VARCHAR);
      strWriter.setString("first");
      writer.save();

      // Second row: a map

      vw.setType(MinorType.MAP);
      aWriter.setInt(20);
      bWriter.setString("fred");
      writer.save();

      // Third row: null

      vw.setNull();
      writer.save();

      // Fourth row: map with a null string

      vw.setType(MinorType.MAP);
      aWriter.setInt(40);
      bWriter.setNull();
      writer.save();

      // Fifth row: string "last"

      vw.setType(MinorType.VARCHAR);
      strWriter.setString("last");
      writer.save();

      result = writer.done();
      assertEquals(5, result.rowCount());
    }

    // Read the values.

    {
      final RowSetReader reader = result.reader();

      // Sanity check of structure

      final ObjectReader ro = reader.column(0);
      assertEquals(ObjectType.VARIANT, ro.type());
      final VariantReader vr = ro.variant();

      assertTrue(vr.hasType(MinorType.VARCHAR));
      final ObjectReader strObj = vr.member(MinorType.VARCHAR);
      final ScalarReader strReader = strObj.scalar();
      assertSame(strReader, vr.scalar(MinorType.VARCHAR));

      assertTrue(vr.hasType(MinorType.MAP));
      final ObjectReader mapObj = vr.member(MinorType.MAP);
      final TupleReader mReader = mapObj.tuple();
      assertSame(mReader, vr.tuple());

      final ScalarReader aReader = mReader.scalar("a");
      final ScalarReader bReader = mReader.scalar("b");

      // First row: string "first"

      assertTrue(reader.next());
      assertFalse(vr.isNull());
      assertEquals(MinorType.VARCHAR, vr.dataType());
      assertFalse(strReader.isNull());
      assertTrue(mReader.isNull());
      assertEquals("first", strReader.getString());

      // Second row: a map

      assertTrue(reader.next());
      assertFalse(vr.isNull());
      assertEquals(MinorType.MAP, vr.dataType());
      assertTrue(strReader.isNull());
      assertFalse(mReader.isNull());
      assertFalse(aReader.isNull());
      assertEquals(20, aReader.getInt());
      assertFalse(bReader.isNull());
      assertEquals("fred", bReader.getString());

      // Third row: null

      assertTrue(reader.next());
      assertTrue(vr.isNull());
      assertTrue(strReader.isNull());
      assertTrue(mReader.isNull());
      assertTrue(aReader.isNull());
      assertTrue(bReader.isNull());

      // Fourth row: map with a null string

      assertTrue(reader.next());
      assertEquals(MinorType.MAP, vr.dataType());
      assertEquals(40, aReader.getInt());
      assertTrue(bReader.isNull());

      // Fifth row: string "last"

      assertTrue(reader.next());
      assertEquals(MinorType.VARCHAR, vr.dataType());
      assertEquals("last", strReader.getString());

      assertFalse(reader.next());
    }

    result.clear();
  }

  /**
   * Test a scalar list. Should act just like a repeated type, with the
   * addition of allowing the list for a row to be null. But, a list
   * writer does not do auto-increment, so we must do that explicitly
   * after each write.
   */

  @Test
  public void testScalarList() {
    final TupleMetadata schema = new SchemaBuilder()
        .addList("list")
          .addType(MinorType.VARCHAR)
          .resumeSchema()
        .buildSchema();

    final ExtendableRowSet rowSet = fixture.rowSet(schema);
    final RowSetWriter writer = rowSet.writer();

    {
      final ObjectWriter listObj = writer.column(0);
      assertEquals(ObjectType.ARRAY, listObj.type());
      final ArrayWriter listArray = listObj.array();

      // The list contains only a scalar. But, because lists can,
      // in general, contain multiple contents, the list requires
      // an explicit save after each entry.

      final ObjectWriter itemObj = listArray.entry();
      assertEquals(ObjectType.SCALAR, itemObj.type());
      final ScalarWriter strWriter = itemObj.scalar();

      // First row: two strings and a null
      // Unlike a repeated type, a list can mark individual elements
      // as null.
      // List will automatically detect that data was written.

      strWriter.setString("fred");
      listArray.save();
      strWriter.setNull();
      listArray.save();
      strWriter.setString("wilma");
      listArray.save();
      writer.save();

      // Second row: null

      writer.save();

      // Third row: one string

      strWriter.setString("dino");
      listArray.save();
      writer.save();

      // Fourth row: empty array. Note that there is no trigger
      // to say that the column is not null, so we have to do it
      // explicitly.

      listArray.setNull(false);
      writer.save();

      // Last row: a null string and non-null

      strWriter.setNull();
      listArray.save();
      strWriter.setString("pebbles");
      listArray.save();
      writer.save();
    }

    final SingleRowSet result = writer.done();
    assertEquals(5, result.rowCount());

    {
      final RowSetReader reader = result.reader();

      final ObjectReader listObj = reader.column(0);
      assertEquals(ObjectType.ARRAY, listObj.type());
      final ArrayReader listArray = listObj.array();

      // The list is a repeated scalar

      assertEquals(ObjectType.SCALAR, listArray.entry().type());
      final ScalarReader strReader = listArray.scalar();

      // First row: two strings and a null

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertEquals(3, listArray.size());
      assertTrue(listArray.next());
      assertFalse(strReader.isNull());
      assertEquals("fred", strReader.getString());
      assertTrue(listArray.next());
      assertTrue(strReader.isNull());
      assertTrue(listArray.next());
      assertFalse(strReader.isNull());
      assertEquals("wilma", strReader.getString());
      assertFalse(listArray.next());

      // Second row: null

      assertTrue(reader.next());
      assertTrue(listArray.isNull());
      assertEquals(0, listArray.size());

      // Third row: one string

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertEquals(1, listArray.size());
      assertTrue(listArray.next());
      assertEquals("dino", strReader.getString());
      assertFalse(listArray.next());

      // Fourth row: empty array.

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertEquals(0, listArray.size());
      assertFalse(listArray.next());

      // Last row: a null string and non-null

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertEquals(2, listArray.size());
      assertTrue(listArray.next());
      assertTrue(strReader.isNull());
      assertTrue(listArray.next());
      assertFalse(strReader.isNull());
      assertEquals("pebbles", strReader.getString());
      assertFalse(listArray.next());

      assertFalse(reader.next());
    }

    result.clear();
  }

  /**
   * List of maps. Like a repeated map, but each list entry can be
   * null.
   */

  @Test
  public void testListOfMaps() {
    final TupleMetadata schema = new SchemaBuilder()
        .addList("list")
          .addMap()
            .addNullable("a", MinorType.INT)
            .addNullable("b", MinorType.VARCHAR)
            .resumeUnion()
          .resumeSchema()
        .buildSchema();

    final ExtendableRowSet rowSet = fixture.rowSet(schema);
    final RowSetWriter writer = rowSet.writer();

    {
      final ObjectWriter listObj = writer.column("list");
      assertEquals(ObjectType.ARRAY, listObj.type());
      final ArrayWriter listArray = listObj.array();
      final ObjectWriter itemObj = listArray.entry();
      assertEquals(ObjectType.TUPLE, itemObj.type());
      final TupleWriter mapWriter = itemObj.tuple();
      final ScalarWriter aWriter = mapWriter.scalar("a");
      final ScalarWriter bWriter = mapWriter.scalar("b");

      // First row:
      // {1, "fred"}, null, {3, null}

      aWriter.setInt(1);
      bWriter.setString("fred");

      listArray.save();
      // Can't mark the map as null. Instead, we simply skip
      // the map and the contained nullable members will automatically
      // back-fill each entry with a null value.
      listArray.save();

      aWriter.setInt(3);
      bWriter.setNull();
      listArray.save();
      writer.save();

      // Second row: null

      writer.save();

      // Third row: {null, "dino"}

      aWriter.setNull();
      bWriter.setString("dino");
      listArray.save();
      writer.save();

      // Fourth row: empty array. Note that there is no trigger
      // to say that the column is not null, so we have to do it
      // explicitly.

      listArray.setNull(false);
      writer.save();

      // Last row: {4, "pebbles"}

      aWriter.setInt(4);
      bWriter.setString("pebbles");
      listArray.save();
      writer.save();
    }

    final SingleRowSet result = writer.done();
    assertEquals(5, result.rowCount());

    {
      final RowSetReader reader = result.reader();

      final ObjectReader listObj = reader.column("list");
      assertEquals(ObjectType.ARRAY, listObj.type());
      final ArrayReader listArray = listObj.array();
      assertEquals(ObjectType.TUPLE, listArray.entry().type());
      final TupleReader mapReader = listArray.tuple();
      final ScalarReader aReader = mapReader.scalar("a");
      final ScalarReader bReader = mapReader.scalar("b");

      // First row:
      // {1, "fred"}, null, {3, null}

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertFalse(mapReader.isNull());
      assertEquals(3, listArray.size());

      assertTrue(listArray.next());
      assertFalse(aReader.isNull());
      assertEquals(1, aReader.getInt());
      assertFalse(bReader.isNull());
      assertEquals("fred", bReader.getString());

      assertTrue(listArray.next());
      // Awkward: the map has no null state, but its
      // members do.
      assertTrue(aReader.isNull());
      assertTrue(bReader.isNull());

      assertTrue(listArray.next());
      assertFalse(aReader.isNull());
      assertEquals(3, aReader.getInt());
      assertTrue(bReader.isNull());

      assertFalse(listArray.next());

      // Second row: null

      assertTrue(reader.next());
      assertTrue(listArray.isNull());
      assertEquals(0, listArray.size());

      // Third row: {null, "dino"}

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertEquals(1, listArray.size());
      assertTrue(listArray.next());
      assertTrue(aReader.isNull());
      assertFalse(bReader.isNull());
      assertEquals("dino", bReader.getString());
      assertFalse(listArray.next());

      // Fourth row: empty array.

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertEquals(0, listArray.size());
      assertFalse(listArray.next());

      // Last row: {4, "pebbles"}

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertEquals(1, listArray.size());
      assertTrue(listArray.next());
      assertEquals(4, aReader.getInt());
      assertEquals("pebbles", bReader.getString());
      assertFalse(listArray.next());

      assertFalse(reader.next());
    }

    result.clear();
  }

  /**
   * Test a union list.
   */

  @Test
  public void testListOfUnions() {
    final TupleMetadata schema = new SchemaBuilder()
        .addList("list")
          .addType(MinorType.INT)
          .addType(MinorType.VARCHAR)
          .resumeSchema()
        .buildSchema();

    final ExtendableRowSet rowSet = fixture.rowSet(schema);
    final RowSetWriter writer = rowSet.writer();

    {
      final ObjectWriter listObj = writer.column(0);
      assertEquals(ObjectType.ARRAY, listObj.type());
      final ArrayWriter listArray = listObj.array();
      final ObjectWriter itemObj = listArray.entry();
      assertEquals(ObjectType.VARIANT, itemObj.type());
      final VariantWriter variant = itemObj.variant();
      final ScalarWriter intWriter = variant.scalar(MinorType.INT);
      final ScalarWriter strWriter = variant.scalar(MinorType.VARCHAR);

      // First row: (1, "two", 3)

      variant.setType(MinorType.INT);
      intWriter.setInt(1);
      listArray.save();
      variant.setType(MinorType.VARCHAR);
      strWriter.setString("two");
      listArray.save();
      variant.setType(MinorType.INT);
      intWriter.setInt(3);
      listArray.save();
      writer.save();

      // Second row: null

      writer.save();

      // Third row: 4, null, "six", null int, null string

      variant.setType(MinorType.INT);
      intWriter.setInt(4);
      listArray.save();
      variant.setNull();
      listArray.save();
      variant.setType(MinorType.VARCHAR);
      strWriter.setString("six");
      listArray.save();
      variant.setType(MinorType.INT);
      intWriter.setNull();
      listArray.save();
      variant.setType(MinorType.VARCHAR);
      intWriter.setNull();
      listArray.save();
      writer.save();

      // Fourth row: empty array.

      listArray.setNull(false);
      writer.save();

      // Fifth row: 9

      variant.setType(MinorType.INT);
      intWriter.setInt(9);
      listArray.save();
      writer.save();
    }

    final SingleRowSet result = writer.done();
    assertEquals(5, result.rowCount());

    {
      final RowSetReader reader = result.reader();

      final ObjectReader listObj = reader.column(0);
      assertEquals(ObjectType.ARRAY, listObj.type());
      final ArrayReader listArray = listObj.array();
      assertEquals(ObjectType.VARIANT, listArray.entry().type());
      final VariantReader variant = listArray.variant();
      final ScalarReader intReader = variant.scalar(MinorType.INT);
      final ScalarReader strReader = variant.scalar(MinorType.VARCHAR);

      // First row: (1, "two", 3)

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertEquals(3, listArray.size());

      assertTrue(listArray.next());
      assertEquals(MinorType.INT, variant.dataType());
      assertFalse(intReader.isNull());
      assertTrue(strReader.isNull());
      assertEquals(1, intReader.getInt());
      assertEquals(1, variant.scalar().getInt());

      assertTrue(listArray.next());
      assertEquals(MinorType.VARCHAR, variant.dataType());
      assertTrue(intReader.isNull());
      assertFalse(strReader.isNull());
      assertEquals("two", strReader.getString());
      assertEquals("two", variant.scalar().getString());

      assertTrue(listArray.next());
      assertEquals(MinorType.INT, variant.dataType());
      assertEquals(3, intReader.getInt());

      assertFalse(listArray.next());

      // Second row: null

      assertTrue(reader.next());
      assertTrue(listArray.isNull());
      assertEquals(0, listArray.size());

      // Third row: 4, null, "six", null int, null string

      assertTrue(reader.next());
      assertEquals(5, listArray.size());

      assertTrue(listArray.next());
      assertEquals(4, intReader.getInt());

      assertTrue(listArray.next());
      assertTrue(variant.isNull());

      assertTrue(listArray.next());
      assertEquals("six", strReader.getString());

      assertTrue(listArray.next());
      assertEquals(MinorType.INT, variant.dataType());
      assertTrue(intReader.isNull());

      assertTrue(listArray.next());
      assertEquals(MinorType.VARCHAR, variant.dataType());
      assertTrue(strReader.isNull());

      assertFalse(listArray.next());

      // Fourth row: empty array.

      assertTrue(reader.next());
      assertFalse(listArray.isNull());
      assertEquals(0, listArray.size());
      assertFalse(listArray.next());

      // Fifth row: 9

      assertTrue(reader.next());
      assertEquals(1, listArray.size());

      assertTrue(listArray.next());
      assertEquals(9, intReader.getInt());
      assertFalse(listArray.next());

      assertFalse(reader.next());
    }

    result.clear();
  }

  /**
   * Test a variant (AKA "union vector") at the top level, using
   * just scalar values.
   */

  @Test
  public void testAddTypes() {
    final TupleMetadata batchSchema = new SchemaBuilder()
        .addNullable("v", MinorType.UNION)
        .buildSchema();

    final ExtendableRowSet rs = fixture.rowSet(batchSchema);
    final RowSetWriter writer = rs.writer();

    // Sanity check of writer structure

    final ObjectWriter wo = writer.column(0);
    assertEquals(ObjectType.VARIANT, wo.type());
    final VariantWriter vw = wo.variant();
    assertSame(vw, writer.variant(0));
    assertSame(vw, writer.variant("v"));
    for (final MinorType type : MinorType.values()) {
      assertFalse(vw.hasType(type));
    }

    // Write values of different types

    vw.scalar(MinorType.INT).setInt(10);
    assertTrue(vw.hasType(MinorType.INT));
    assertFalse(vw.hasType(MinorType.VARCHAR));
    writer.save();

    vw.scalar(MinorType.VARCHAR).setString("fred");
    assertTrue(vw.hasType(MinorType.VARCHAR));
    writer.save();

    vw.setNull();
    writer.save();

    vw.scalar(MinorType.FLOAT8).setDouble(123.45);
    assertTrue(vw.hasType(MinorType.INT));
    assertTrue(vw.hasType(MinorType.FLOAT8));
    writer.save();

    final SingleRowSet result = writer.done();

    assertEquals(4, result.rowCount());

    // Read the values.

    final RowSetReader reader = result.reader();

    // Sanity check of structure

    final ObjectReader ro = reader.column(0);
    assertEquals(ObjectType.VARIANT, ro.type());
    final VariantReader vr = ro.variant();
    assertSame(vr, reader.variant(0));
    assertSame(vr, reader.variant("v"));
    for (final MinorType type : MinorType.values()) {
      if (type == MinorType.INT || type == MinorType.VARCHAR || type == MinorType.FLOAT8) {
        assertTrue(vr.hasType(type));
      } else {
        assertFalse(vr.hasType(type));
      }
    }

    // Verify the data

    assertTrue(reader.next());
    assertFalse(vr.isNull());
    assertSame(vr.dataType(), MinorType.INT);
    assertSame(vr.scalar(MinorType.INT), vr.scalar());
    assertNotNull(vr.member());
    assertSame(vr.scalar(), vr.member().scalar());
    assertEquals(10, vr.scalar().getInt());

    assertTrue(reader.next());
    assertFalse(vr.isNull());
    assertSame(vr.dataType(), MinorType.VARCHAR);
    assertSame(vr.scalar(MinorType.VARCHAR), vr.scalar());
    assertEquals("fred", vr.scalar().getString());

    assertTrue(reader.next());
    assertTrue(vr.isNull());
    assertNull(vr.dataType());
    assertNull(vr.scalar());

    assertTrue(reader.next());
    assertFalse(vr.isNull());
    assertSame(vr.dataType(), MinorType.FLOAT8);
    assertSame(vr.scalar(MinorType.FLOAT8), vr.scalar());
    assertEquals(123.45, vr.scalar().getDouble(), 0.001);

    assertFalse(reader.next());
    result.clear();
  }

  /**
   * Test a variant (AKA "union vector") at the top level which includes
   * a list.
   */

  @Test
  public void testUnionWithList() {
    final TupleMetadata schema = new SchemaBuilder()
        .addUnion("u")
          .addType(MinorType.INT)
          .addList()
            .addType(MinorType.VARCHAR)
            .resumeUnion()
          .resumeSchema()
        .buildSchema();

    SingleRowSet result;

    // Write values

    {
      final ExtendableRowSet rs = fixture.rowSet(schema);
      final RowSetWriter writer = rs.writer();
      final VariantWriter vw = writer.variant("u");

      assertTrue(vw.hasType(MinorType.INT));
      final ScalarWriter intWriter = vw.scalar(MinorType.INT);

      assertTrue(vw.hasType(MinorType.LIST));
      final ArrayWriter aWriter = vw.array();
      final ScalarWriter strWriter = aWriter.scalar();

      // Row 1: 1, ["fred", "barney"]

      intWriter.setInt(1);
      strWriter.setString("fred");
      aWriter.save();
      strWriter.setString("barney");
      aWriter.save();
      writer.save();

      // Row 2, 2, ["wilma", "betty"]

      intWriter.setInt(2);
      strWriter.setString("wilma");
      aWriter.save();
      strWriter.setString("betty");
      aWriter.save();
      writer.save();

      result = writer.done();
      assertEquals(2, result.rowCount());
    }

    // Read the values.

    {
      final RowSetReader reader = result.reader();
      final VariantReader vr = reader.variant("u");

      assertTrue(vr.hasType(MinorType.INT));
      final ScalarReader intReader = vr.scalar(MinorType.INT);

      assertTrue(vr.hasType(MinorType.LIST));
      final ArrayReader aReader = vr.array();
      final ScalarReader strReader = aReader.scalar();

      assertTrue(reader.next());
      assertEquals(1, intReader.getInt());
      assertEquals(2, aReader.size());
      assertTrue(aReader.next());
      assertEquals("fred", strReader.getString());
      assertTrue(aReader.next());
      assertEquals("barney", strReader.getString());
      assertFalse(aReader.next());

      assertTrue(reader.next());
      assertEquals(2, intReader.getInt());
      assertEquals(2, aReader.size());
      assertTrue(aReader.next());
      assertEquals("wilma", strReader.getString());
      assertTrue(aReader.next());
      assertEquals("betty", strReader.getString());
      assertFalse(aReader.next());

      assertFalse(reader.next());
    }

    result.clear();
  }

  // TODO: Repeated list

}
