blob: 4a5805742443428f52997c2ec11a8087a5177efb [file] [log] [blame]
/*
* 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.iceberg.util;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
/**
* Within Z-Ordering the byte representations of objects being compared must be ordered, this
* requires several types to be transformed when converted to bytes. The goal is to map object's
* whose byte representation are not lexicographically ordered into representations that are
* lexicographically ordered. Bytes produced should be compared lexicographically as unsigned bytes,
* big-endian.
*
* <p>All types except for String are stored within an 8 Byte Buffer
*
* <p>Most of these techniques are derived from
* https://aws.amazon.com/blogs/database/z-order-indexing-for-multifaceted-queries-in-amazon-dynamodb-part-2/
*
* <p>Some implementation is taken from
* https://github.com/apache/hbase/blob/master/hbase-common/src/main/java/org/apache/hadoop/hbase/util/OrderedBytes.java
*/
public class ZOrderByteUtils {
public static final int PRIMITIVE_BUFFER_SIZE = 8;
private ZOrderByteUtils() {}
static ByteBuffer allocatePrimitiveBuffer() {
return ByteBuffer.allocate(PRIMITIVE_BUFFER_SIZE);
}
/**
* Signed ints do not have their bytes in magnitude order because of the sign bit. To fix this,
* flip the sign bit so that all negatives are ordered before positives. This essentially shifts
* the 0 value so that we don't break our ordering when we cross the new 0 value.
*/
public static ByteBuffer intToOrderedBytes(int val, ByteBuffer reuse) {
ByteBuffer bytes = ByteBuffers.reuse(reuse, PRIMITIVE_BUFFER_SIZE);
bytes.putLong(((long) val) ^ 0x8000000000000000L);
return bytes;
}
/**
* Signed longs are treated the same as the signed ints in {@link #intToOrderedBytes(int,
* ByteBuffer)}
*/
public static ByteBuffer longToOrderedBytes(long val, ByteBuffer reuse) {
ByteBuffer bytes = ByteBuffers.reuse(reuse, PRIMITIVE_BUFFER_SIZE);
bytes.putLong(val ^ 0x8000000000000000L);
return bytes;
}
/**
* Signed shorts are treated the same as the signed ints in {@link #intToOrderedBytes(int,
* ByteBuffer)}
*/
public static ByteBuffer shortToOrderedBytes(short val, ByteBuffer reuse) {
return intToOrderedBytes(val, reuse);
}
/**
* Signed tiny ints are treated the same as the signed ints in {@link #intToOrderedBytes(int,
* ByteBuffer)}
*/
public static ByteBuffer tinyintToOrderedBytes(byte val, ByteBuffer reuse) {
return intToOrderedBytes(val, reuse);
}
/**
* IEEE 754 : “If two floating-point numbers in the same format are ordered (say, x {@literal <}
* y), they are ordered the same way when their bits are reinterpreted as sign-magnitude
* integers.”
*
* <p>Which means floats can be treated as sign magnitude integers which can then be converted
* into lexicographically comparable bytes
*/
public static ByteBuffer floatToOrderedBytes(float val, ByteBuffer reuse) {
return doubleToOrderedBytes(val, reuse);
}
/** Doubles are treated the same as floats in {@link #floatToOrderedBytes(float, ByteBuffer)} */
public static ByteBuffer doubleToOrderedBytes(double val, ByteBuffer reuse) {
ByteBuffer bytes = ByteBuffers.reuse(reuse, PRIMITIVE_BUFFER_SIZE);
long lval = Double.doubleToLongBits(val);
lval ^= ((lval >> (Integer.SIZE - 1)) | Long.MIN_VALUE);
bytes.putLong(lval);
return bytes;
}
/**
* Strings are lexicographically sortable BUT if different byte array lengths will ruin the
* Z-Ordering. (ZOrder requires that a given column contribute the same number of bytes every
* time). This implementation just uses a set size to for all output byte representations.
* Truncating longer strings and right padding 0 for shorter strings.
*/
@SuppressWarnings("ByteBufferBackingArray")
public static ByteBuffer stringToOrderedBytes(
String val, int length, ByteBuffer reuse, CharsetEncoder encoder) {
Preconditions.checkArgument(
encoder.charset().equals(StandardCharsets.UTF_8),
"Cannot use an encoder not using UTF_8 as it's Charset");
ByteBuffer bytes = ByteBuffers.reuse(reuse, length);
Arrays.fill(bytes.array(), 0, length, (byte) 0x00);
if (val != null) {
CharBuffer inputBuffer = CharBuffer.wrap(val);
encoder.encode(inputBuffer, bytes, true);
}
return bytes;
}
/**
* Return a bytebuffer with the given bytes truncated to length, or filled with 0's to length
* depending on whether the given bytes are larger or smaller than the given length.
*/
@SuppressWarnings("ByteBufferBackingArray")
public static ByteBuffer byteTruncateOrFill(byte[] val, int length, ByteBuffer reuse) {
ByteBuffer bytes = ByteBuffers.reuse(reuse, length);
if (val == null) {
Arrays.fill(bytes.array(), 0, length, (byte) 0x00);
return bytes;
}
if (val.length < length) {
bytes.put(val, 0, val.length);
Arrays.fill(bytes.array(), val.length, length, (byte) 0x00);
} else {
bytes.put(val, 0, length);
}
return bytes;
}
static byte[] interleaveBits(byte[][] columnsBinary, int interleavedSize) {
return interleaveBits(columnsBinary, interleavedSize, ByteBuffer.allocate(interleavedSize));
}
/**
* Interleave bits using a naive loop. Variable length inputs are allowed but to get a consistent
* ordering it is required that every column contribute the same number of bytes in each
* invocation. Bits are interleaved from all columns that have a bit available at that position.
* Once a Column has no more bits to produce it is skipped in the interleaving.
*
* @param columnsBinary an array of ordered byte representations of the columns being ZOrdered
* @param interleavedSize the number of bytes to use in the output
* @return the columnbytes interleaved
*/
// NarrowingCompoundAssignment is intended here. See
// https://github.com/apache/iceberg/pull/5200#issuecomment-1176226163
@SuppressWarnings({"ByteBufferBackingArray", "NarrowingCompoundAssignment"})
public static byte[] interleaveBits(
byte[][] columnsBinary, int interleavedSize, ByteBuffer reuse) {
byte[] interleavedBytes = reuse.array();
Arrays.fill(interleavedBytes, 0, interleavedSize, (byte) 0x00);
int sourceColumn = 0;
int sourceByte = 0;
int sourceBit = 7;
int interleaveByte = 0;
int interleaveBit = 7;
while (interleaveByte < interleavedSize) {
// Take the source bit from source byte and move it to the output bit position
interleavedBytes[interleaveByte] |=
(columnsBinary[sourceColumn][sourceByte] & 1 << sourceBit) >>> sourceBit << interleaveBit;
--interleaveBit;
// Check if an output byte has been completed
if (interleaveBit == -1) {
// Move to the next output byte
interleaveByte++;
// Move to the highest order bit of the new output byte
interleaveBit = 7;
}
// Check if the last output byte has been completed
if (interleaveByte == interleavedSize) {
break;
}
// Find the next source bit to interleave
do {
// Move to next column
++sourceColumn;
if (sourceColumn == columnsBinary.length) {
// If the last source column was used, reset to next bit of first column
sourceColumn = 0;
--sourceBit;
if (sourceBit == -1) {
// If the last bit of the source byte was used, reset to the highest bit of the next
// byte
sourceByte++;
sourceBit = 7;
}
}
} while (columnsBinary[sourceColumn].length <= sourceByte);
}
return interleavedBytes;
}
}