001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.types;
019
020import static org.junit.Assert.assertArrayEquals;
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertNull;
023
024import java.math.BigDecimal;
025import java.util.Arrays;
026import org.apache.hadoop.hbase.HBaseClassTestRule;
027import org.apache.hadoop.hbase.testclassification.MiscTests;
028import org.apache.hadoop.hbase.testclassification.SmallTests;
029import org.apache.hadoop.hbase.util.Order;
030import org.apache.hadoop.hbase.util.PositionedByteRange;
031import org.apache.hadoop.hbase.util.SimplePositionedMutableByteRange;
032import org.junit.ClassRule;
033import org.junit.Test;
034import org.junit.experimental.categories.Category;
035
036@Category({ MiscTests.class, SmallTests.class })
037public class TestStructNullExtension {
038
039  @ClassRule
040  public static final HBaseClassTestRule CLASS_RULE =
041    HBaseClassTestRule.forClass(TestStructNullExtension.class);
042
043  /**
044   * Verify null extension respects the type's isNullable field.
045   */
046  @Test(expected = NullPointerException.class)
047  public void testNonNullableNullExtension() {
048    Struct s = new StructBuilder().add(new RawStringTerminated("|")) // not nullable
049      .toStruct();
050    PositionedByteRange buf = new SimplePositionedMutableByteRange(4);
051    s.encode(buf, new Object[1]);
052  }
053
054  /**
055   * Positive cases for null extension.
056   */
057  @Test
058  public void testNullableNullExtension() {
059    // the following field members are used because they're all nullable
060    final StructBuilder builder = new StructBuilder().add(new OrderedNumeric(Order.ASCENDING))
061      .add(new OrderedString(Order.ASCENDING));
062    Struct shorter = builder.toStruct();
063    final Struct longer = builder
064      // intentionally include a wrapped instance to test wrapper behavior.
065      .add(new TerminatedWrapper<>(new OrderedString(Order.ASCENDING), "/"))
066      .add(new OrderedNumeric(Order.ASCENDING)).toStruct();
067
068    PositionedByteRange buf1 = new SimplePositionedMutableByteRange(7);
069    Object[] val1 = new Object[] { BigDecimal.ONE, "foo" }; // => 2 bytes + 5 bytes
070    assertEquals("Encoding shorter value wrote a surprising number of bytes.", buf1.getLength(),
071      shorter.encode(buf1, val1));
072    int shortLen = buf1.getLength();
073
074    // test iterator
075    buf1.setPosition(0);
076    StructIterator it = longer.iterator(buf1);
077    it.skip();
078    it.skip();
079    assertEquals("Position should be at end. Broken test.", buf1.getLength(), buf1.getPosition());
080    assertEquals("Failed to skip null element with extended struct.", 0, it.skip());
081    assertEquals("Failed to skip null element with extended struct.", 0, it.skip());
082
083    buf1.setPosition(0);
084    it = longer.iterator(buf1);
085    assertEquals(BigDecimal.ONE, it.next());
086    assertEquals("foo", it.next());
087    assertEquals("Position should be at end. Broken test.", buf1.getLength(), buf1.getPosition());
088    assertNull("Failed to skip null element with extended struct.", it.next());
089    assertNull("Failed to skip null element with extended struct.", it.next());
090
091    // test Struct
092    buf1.setPosition(0);
093    assertArrayEquals("Simple struct decoding is broken.", val1, shorter.decode(buf1));
094
095    buf1.setPosition(0);
096    assertArrayEquals("Decoding short value with extended struct should append null elements.",
097      Arrays.copyOf(val1, 4), longer.decode(buf1));
098
099    // test omission of trailing members
100    PositionedByteRange buf2 = new SimplePositionedMutableByteRange(7);
101    buf1.setPosition(0);
102    assertEquals(
103      "Encoding a short value with extended struct should have same result as using short struct.",
104      shortLen, longer.encode(buf2, val1));
105    assertArrayEquals(
106      "Encoding a short value with extended struct should have same result as using short struct",
107      buf1.getBytes(), buf2.getBytes());
108
109    // test null trailing members
110    // all fields are nullable, so nothing should hit the buffer.
111    val1 = new Object[] { null, null, null, null }; // => 0 bytes
112    buf1.set(0);
113    buf2.set(0);
114    assertEquals("Encoding null-truncated value wrote a surprising number of bytes.",
115      buf1.getLength(), longer.encode(buf1, new Object[0]));
116    assertEquals("Encoding null-extended value wrote a surprising number of bytes.",
117      buf1.getLength(), longer.encode(buf1, val1));
118    assertArrayEquals("Encoded unexpected result.", buf1.getBytes(), buf2.getBytes());
119    assertArrayEquals("Decoded unexpected result.", val1, longer.decode(buf2));
120
121    // all fields are nullable, so only 1 should hit the buffer.
122    Object[] val2 = new Object[] { BigDecimal.ONE, null, null, null }; // => 2 bytes
123    buf1.set(2);
124    buf2.set(2);
125    assertEquals("Encoding null-truncated value wrote a surprising number of bytes.",
126      buf1.getLength(), longer.encode(buf1, Arrays.copyOf(val2, 1)));
127    assertEquals("Encoding null-extended value wrote a surprising number of bytes.",
128      buf2.getLength(), longer.encode(buf2, val2));
129    assertArrayEquals("Encoded unexpected result.", buf1.getBytes(), buf2.getBytes());
130    buf2.setPosition(0);
131    assertArrayEquals("Decoded unexpected result.", val2, longer.decode(buf2));
132
133    // all fields are nullable, so only 1, null, "foo" should hit the buffer.
134    // => 2 bytes + 1 byte + 6 bytes
135    Object[] val3 = new Object[] { BigDecimal.ONE, null, "foo", null };
136    buf1.set(9);
137    buf2.set(9);
138    assertEquals("Encoding null-truncated value wrote a surprising number of bytes.",
139      buf1.getLength(), longer.encode(buf1, Arrays.copyOf(val3, 3)));
140    assertEquals("Encoding null-extended value wrote a surprising number of bytes.",
141      buf2.getLength(), longer.encode(buf2, val3));
142    assertArrayEquals("Encoded unexpected result.", buf1.getBytes(), buf2.getBytes());
143    buf2.setPosition(0);
144    assertArrayEquals("Decoded unexpected result.", val3, longer.decode(buf2));
145  }
146}