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.jupiter.api.Assertions.assertArrayEquals;
021import static org.junit.jupiter.api.Assertions.assertEquals;
022import static org.junit.jupiter.api.Assertions.assertNull;
023import static org.junit.jupiter.api.Assertions.assertThrows;
024
025import java.math.BigDecimal;
026import java.util.Arrays;
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.jupiter.api.Tag;
033import org.junit.jupiter.api.Test;
034
035@Tag(MiscTests.TAG)
036@Tag(SmallTests.TAG)
037public class TestStructNullExtension {
038
039  /**
040   * Verify null extension respects the type's isNullable field.
041   */
042  @Test
043  public void testNonNullableNullExtension() {
044    // not nullable
045    Struct s = new StructBuilder().add(new RawStringTerminated("|")).toStruct();
046    PositionedByteRange buf = new SimplePositionedMutableByteRange(4);
047    assertThrows(NullPointerException.class, () -> {
048      s.encode(buf, new Object[1]);
049    });
050  }
051
052  /**
053   * Positive cases for null extension.
054   */
055  @Test
056  public void testNullableNullExtension() {
057    // the following field members are used because they're all nullable
058    final StructBuilder builder = new StructBuilder().add(new OrderedNumeric(Order.ASCENDING))
059      .add(new OrderedString(Order.ASCENDING));
060    Struct shorter = builder.toStruct();
061    final Struct longer = builder
062      // intentionally include a wrapped instance to test wrapper behavior.
063      .add(new TerminatedWrapper<>(new OrderedString(Order.ASCENDING), "/"))
064      .add(new OrderedNumeric(Order.ASCENDING)).toStruct();
065
066    PositionedByteRange buf1 = new SimplePositionedMutableByteRange(7);
067    Object[] val1 = new Object[] { BigDecimal.ONE, "foo" }; // => 2 bytes + 5 bytes
068    assertEquals(buf1.getLength(), shorter.encode(buf1, val1),
069      "Encoding shorter value wrote a surprising number of bytes.");
070    int shortLen = buf1.getLength();
071
072    // test iterator
073    buf1.setPosition(0);
074    StructIterator it = longer.iterator(buf1);
075    it.skip();
076    it.skip();
077    assertEquals(buf1.getLength(), buf1.getPosition(), "Position should be at end. Broken test.");
078    assertEquals(0, it.skip(), "Failed to skip null element with extended struct.");
079    assertEquals(0, it.skip(), "Failed to skip null element with extended struct.");
080
081    buf1.setPosition(0);
082    it = longer.iterator(buf1);
083    assertEquals(BigDecimal.ONE, it.next());
084    assertEquals("foo", it.next());
085    assertEquals(buf1.getLength(), buf1.getPosition(), "Position should be at end. Broken test.");
086    assertNull(it.next(), "Failed to skip null element with extended struct.");
087    assertNull(it.next(), "Failed to skip null element with extended struct.");
088
089    // test Struct
090    buf1.setPosition(0);
091    assertArrayEquals(val1, shorter.decode(buf1), "Simple struct decoding is broken.");
092
093    buf1.setPosition(0);
094    assertArrayEquals(Arrays.copyOf(val1, 4), longer.decode(buf1),
095      "Decoding short value with extended struct should append null elements.");
096
097    // test omission of trailing members
098    PositionedByteRange buf2 = new SimplePositionedMutableByteRange(7);
099    buf1.setPosition(0);
100    assertEquals(shortLen, longer.encode(buf2, val1),
101      "Encoding a short value with extended struct should have same result as using short struct.");
102    assertArrayEquals(buf1.getBytes(), buf2.getBytes(),
103      "Encoding a short value with extended struct should have same result as using short struct");
104
105    // test null trailing members
106    // all fields are nullable, so nothing should hit the buffer.
107    val1 = new Object[] { null, null, null, null }; // => 0 bytes
108    buf1.set(0);
109    buf2.set(0);
110    assertEquals(buf1.getLength(), longer.encode(buf1, new Object[0]),
111      "Encoding null-truncated value wrote a surprising number of bytes.");
112    assertEquals(buf1.getLength(), longer.encode(buf1, val1),
113      "Encoding null-extended value wrote a surprising number of bytes.");
114    assertArrayEquals(buf1.getBytes(), buf2.getBytes(), "Encoded unexpected result.");
115    assertArrayEquals(val1, longer.decode(buf2), "Decoded unexpected result.");
116
117    // all fields are nullable, so only 1 should hit the buffer.
118    Object[] val2 = new Object[] { BigDecimal.ONE, null, null, null }; // => 2 bytes
119    buf1.set(2);
120    buf2.set(2);
121    assertEquals(buf1.getLength(), longer.encode(buf1, Arrays.copyOf(val2, 1)),
122      "Encoding null-truncated value wrote a surprising number of bytes.");
123    assertEquals(buf2.getLength(), longer.encode(buf2, val2),
124      "Encoding null-extended value wrote a surprising number of bytes.");
125    assertArrayEquals(buf1.getBytes(), buf2.getBytes(), "Encoded unexpected result.");
126    buf2.setPosition(0);
127    assertArrayEquals(val2, longer.decode(buf2), "Decoded unexpected result.");
128
129    // all fields are nullable, so only 1, null, "foo" should hit the buffer.
130    // => 2 bytes + 1 byte + 6 bytes
131    Object[] val3 = new Object[] { BigDecimal.ONE, null, "foo", null };
132    buf1.set(9);
133    buf2.set(9);
134    assertEquals(buf1.getLength(), longer.encode(buf1, Arrays.copyOf(val3, 3)),
135      "Encoding null-truncated value wrote a surprising number of bytes.");
136    assertEquals(buf2.getLength(), longer.encode(buf2, val3),
137      "Encoding null-extended value wrote a surprising number of bytes.");
138    assertArrayEquals(buf1.getBytes(), buf2.getBytes(), "Encoded unexpected result.");
139    buf2.setPosition(0);
140    assertArrayEquals(val3, longer.decode(buf2), "Decoded unexpected result.");
141  }
142}