Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Ensure all modules are included in INTEG_TEST testcluster distribution ([#20241](https://github.com/opensearch-project/OpenSearch/pull/20241))
- Cleanup HttpServerTransport.Dispatcher in Netty tests ([#20160](https://github.com/opensearch-project/OpenSearch/pull/20160))
- Add `cluster.initial_cluster_manager_nodes` to testClusters OVERRIDABLE_SETTINGS ([#20348](https://github.com/opensearch-project/OpenSearch/pull/20348))
- Add BigInteger support for unsigned_long fields in gRPC transport ([#20346](https://github.com/opensearch-project/OpenSearch/pull/20346))

### Fixed
- Fix bug of warm index: FullFileCachedIndexInput was closed error ([#20055](https://github.com/opensearch-project/OpenSearch/pull/20055))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
*/
package org.opensearch.transport.grpc.proto.response.common;

import org.opensearch.common.Numbers;
import org.opensearch.protobufs.FieldValue;

import java.math.BigInteger;

import static org.opensearch.index.query.AbstractQueryBuilder.maybeConvertToBytesRef;

/**
Expand Down Expand Up @@ -40,7 +43,7 @@ public static FieldValue toProto(Object javaObject) {

/**
* Converts a generic Java Object to its Protocol Buffer FieldValue representation.
* It handles various Java types (Integer, Long, Double, Float, String, Boolean, Enum, Map)
* It handles various Java types (Integer, Long, Double, Float, String, Boolean, Enum, BigInteger, Map)
* and converts them to the appropriate FieldValue type.
*
* @param javaObject The Java object to convert
Expand Down Expand Up @@ -76,6 +79,16 @@ public static void toProto(Object javaObject, FieldValue.Builder fieldValueBuild
}
case Boolean b -> fieldValueBuilder.setBool(b);
case Enum<?> e -> fieldValueBuilder.setString(e.toString());
case BigInteger bi -> {
// BigInteger is used for unsigned_long fields in OpenSearch
// Validate that the value is within unsigned long range (0 to 2^64-1)
Numbers.toUnsignedLongExact(bi);
// Convert to long for protobuf uint64 representation
// bi.longValue() preserves the bit pattern for correct uint64 encoding
org.opensearch.protobufs.GeneralNumber.Builder num = org.opensearch.protobufs.GeneralNumber.newBuilder();
num.setUint64Value(bi.longValue());
fieldValueBuilder.setGeneralNumber(num.build());
}
default -> throw new IllegalArgumentException("Cannot convert " + javaObject + " to FieldValue");
}
}
Expand Down Expand Up @@ -121,7 +134,16 @@ public static Object fromProto(FieldValue fieldValue, boolean convertStringsToBy
case DOUBLE_VALUE:
return generalNumber.getDoubleValue();
case UINT64_VALUE:
return generalNumber.getUint64Value();
long uint64Value = generalNumber.getUint64Value();
// If the value doesn't fit in a signed long (i.e., it's negative when interpreted as signed),
// return BigInteger to preserve the unsigned value. Otherwise return Long for efficiency.
if (uint64Value < 0) {
// Value exceeds Long.MAX_VALUE, convert to BigInteger using unsigned interpretation
return Numbers.toUnsignedBigInteger(uint64Value);
} else {
// Value fits in signed long range, return as Long
return uint64Value;
}
default:
throw new IllegalArgumentException("Unsupported general number type: " + generalNumber.getValueCase());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.opensearch.protobufs.FieldValue;
import org.opensearch.test.OpenSearchTestCase;

import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -183,6 +184,86 @@ public void testFromProtoWithUint64Value() {
assertEquals("Uint64 value should match", uint64Value, result);
}

public void testFromProtoWithUint64ValueExceedingLongMax() {
// Create a FieldValue with UINT64_VALUE that exceeds Long.MAX_VALUE
// When a uint64 value > Long.MAX_VALUE is stored, it appears as a negative long
// (e.g., max uint64 = 2^64-1 appears as -1L in two's complement)
// The expected behavior is to return BigInteger for values that don't fit in signed long
long uint64ValueExceedingLongMax = -1L; // This represents 2^64-1 (max unsigned long)
org.opensearch.protobufs.GeneralNumber generalNumber = org.opensearch.protobufs.GeneralNumber.newBuilder()
.setUint64Value(uint64ValueExceedingLongMax)
.build();
FieldValue fieldValue = FieldValue.newBuilder().setGeneralNumber(generalNumber).build();

// Convert from Protocol Buffer
Object result = FieldValueProtoUtils.fromProto(fieldValue);

// Verify the conversion - should return BigInteger for values exceeding Long.MAX_VALUE
assertNotNull("Result should not be null", result);
assertTrue("Result should be BigInteger when value exceeds Long.MAX_VALUE", result instanceof BigInteger);
BigInteger expectedValue = new BigInteger("18446744073709551615"); // 2^64 - 1
assertEquals("Uint64 value should match max unsigned long", expectedValue, result);
}

public void testToProtoWithBigInteger() {
// Test with a BigInteger that represents an unsigned_long value
BigInteger bigIntValue = new BigInteger("18446744073709551615"); // Max unsigned long
FieldValue fieldValue = FieldValueProtoUtils.toProto(bigIntValue);

assertNotNull("FieldValue should not be null", fieldValue);
assertTrue("FieldValue should have general number", fieldValue.hasGeneralNumber());
assertTrue("GeneralNumber should have uint64 value", fieldValue.getGeneralNumber().hasUint64Value());
assertEquals("Uint64 value should match", -1L, fieldValue.getGeneralNumber().getUint64Value()); // -1L is max unsigned
}

public void testToProtoWithBigIntegerSmallValue() {
// Test with a BigInteger that has a value within signed long range
BigInteger bigIntValue = new BigInteger("2147395412");
FieldValue fieldValue = FieldValueProtoUtils.toProto(bigIntValue);

assertNotNull("FieldValue should not be null", fieldValue);
assertTrue("FieldValue should have general number", fieldValue.hasGeneralNumber());
assertTrue("GeneralNumber should have uint64 value", fieldValue.getGeneralNumber().hasUint64Value());
assertEquals("Uint64 value should match", 2147395412L, fieldValue.getGeneralNumber().getUint64Value());
}

public void testRoundTripBigInteger() {
// Test round-trip conversion for BigInteger values
BigInteger[] testValues = {
new BigInteger("0"),
new BigInteger("2147395412"),
new BigInteger("9223372036854775807"), // Max signed long
BigInteger.valueOf(Long.MAX_VALUE) };

for (BigInteger original : testValues) {
FieldValue fieldValue = FieldValueProtoUtils.toProto(original);
Object result = FieldValueProtoUtils.fromProto(fieldValue);

assertNotNull("Result should not be null for " + original, result);
assertTrue("Result should be Long for " + original, result instanceof Long);
assertEquals("Value should match for " + original, original.longValue(), ((Long) result).longValue());
}
}

public void testToProtoWithBigIntegerOutOfRange() {
// Test that BigInteger values outside unsigned long range throw exception
BigInteger tooLarge = new BigInteger("18446744073709551616"); // 2^64 (exceeds max unsigned long)
BigInteger negative = new BigInteger("-1");

// Should throw IllegalArgumentException for values out of range
IllegalArgumentException exception1 = expectThrows(IllegalArgumentException.class, () -> FieldValueProtoUtils.toProto(tooLarge));
assertTrue(
"Exception should mention out of range",
exception1.getMessage().contains("out of range") || exception1.getMessage().contains("unsigned long")
);

IllegalArgumentException exception2 = expectThrows(IllegalArgumentException.class, () -> FieldValueProtoUtils.toProto(negative));
assertTrue(
"Exception should mention out of range",
exception2.getMessage().contains("out of range") || exception2.getMessage().contains("unsigned long")
);
}

// Test enum for testing enum conversion
private enum TestEnum {
TEST_VALUE
Expand Down
Loading