Skip to content

Conversation

@yujiaaa2019
Copy link
Collaborator

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Adds support for mapping additional Java time/utility types (Instant, UUID, LocalDate) in HollowObjectTypeMapper along with corresponding read/write logic and tests. Introduces new MappedFieldType enum entries and special handling for multi-field reconstruction plus test coverage for serialization round-trips.

  • Add special wrapper handling for Instant, UUID, LocalDate (schema mapping, copy, parse).
  • Extend tests to validate new types individually and inside a composite type.
  • Introduce dedicated test POJO TypeWithSpecialTypes.

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
HollowObjectMapperTest.java Adds tests for Instant, UUID, and composite type containing special types.
HollowObjectMapperHollowRecordParserTest.java Extends wrapper types test to include Instant.
TypeWithSpecialTypes.java New test POJO holding Date, Instant, UUID, LocalDate.
HollowObjectTypeMapper.java Core changes: new special wrapper set, parsing/copy logic, new enum constants for multi-field types.

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines 415 to 418
int nanoPosInPojoSchema = schema.getPosition(MappedFieldType.INSTANT_SECONDS.getSpecialFieldName());
if(nanoPosInPojoSchema != -1) {
int nanoValue = objectNode.getFieldValueInt(MappedFieldType.INSTANT_SECONDS.getSpecialFieldName());
if(nanoValue != Long.MIN_VALUE) {
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nanos branch incorrectly uses INSTANT_SECONDS for both the position lookup and field access, and compares an int to Long.MIN_VALUE instead of Integer.MIN_VALUE. Use INSTANT_NANOS and compare against Integer.MIN_VALUE.

Suggested change
int nanoPosInPojoSchema = schema.getPosition(MappedFieldType.INSTANT_SECONDS.getSpecialFieldName());
if(nanoPosInPojoSchema != -1) {
int nanoValue = objectNode.getFieldValueInt(MappedFieldType.INSTANT_SECONDS.getSpecialFieldName());
if(nanoValue != Long.MIN_VALUE) {
int nanoPosInPojoSchema = schema.getPosition(MappedFieldType.INSTANT_NANOS.getSpecialFieldName());
if(nanoPosInPojoSchema != -1) {
int nanoValue = objectNode.getFieldValueInt(MappedFieldType.INSTANT_NANOS.getSpecialFieldName());
if(nanoValue != Integer.MIN_VALUE) {

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah this seems like a bug

return Long.valueOf(((Date)obj).getTime());
//TODO figure out when it's used
case UUID_LEAST_SIG_BITS:
return Long.valueOf(((UUID)obj).getLeastSignificantBits());
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only UUID_LEAST_SIG_BITS is handled in retrieveFieldValue; missing cases for UUID_MOST_SIG_BITS, INSTANT_SECONDS, INSTANT_NANOS, LOCAL_DATE_* leading to inconsistent behavior when these fields are requested (they will fall through to default). Add symmetrical handling (or document why not needed) for all newly introduced MappedFieldType entries.

Suggested change
return Long.valueOf(((UUID)obj).getLeastSignificantBits());
return Long.valueOf(((UUID)obj).getLeastSignificantBits());
case UUID_MOST_SIG_BITS:
return Long.valueOf(((UUID)obj).getMostSignificantBits());
case INSTANT_SECONDS:
return Long.valueOf(((Instant)obj).getEpochSecond());
case INSTANT_NANOS:
return Integer.valueOf(((Instant)obj).getNano());
case LOCAL_DATE_YEAR:
return Integer.valueOf(((LocalDate)obj).getYear());
case LOCAL_DATE_MONTH:
return Integer.valueOf(((LocalDate)obj).getMonthValue());
case LOCAL_DATE_DAY:
return Integer.valueOf(((LocalDate)obj).getDayOfMonth());

Copilot uses AI. Check for mistakes.
Comment on lines +261 to +262
}
else if (SPECIAL_WRAPPERS.contains(clazz)) {
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The special wrapper reconstruction logic for UUID, Instant, and LocalDate is duplicated later in parseFlatRecord; consider extracting a shared helper (e.g., rebuildSpecialWrapper(clazz, schema, accessor)) to reduce duplication and risk of divergence (already evident in the Instant nanos bug in the flat record path).

Copilot uses AI. Check for mistakes.
if (year != null && month != null && day != null) {
obj = LocalDate.of(year, month, day);
}
}
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The special wrapper reconstruction logic for UUID, Instant, and LocalDate is duplicated later in parseFlatRecord; consider extracting a shared helper (e.g., rebuildSpecialWrapper(clazz, schema, accessor)) to reduce duplication and risk of divergence (already evident in the Instant nanos bug in the flat record path).

Copilot uses AI. Check for mistakes.

long time = System.currentTimeMillis();
Date date = new Date(time);
// what if the DateTimeException is thrown?
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment is misleading—Instant.now() does not throw DateTimeException under normal circumstances. Remove or clarify to avoid confusion.

Suggested change
// what if the DateTimeException is thrown?

Copilot uses AI. Check for mistakes.
Comment on lines 265 to 269
String schemeText = baos.toString();
Assert.assertTrue(schemeText.contains("date"));
Assert.assertTrue(schemeText.contains("instant"));
Assert.assertTrue(schemeText.contains("uuid"));
Assert.assertTrue(schemeText.contains("localDate"));
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] schemeText appears to be a misspelling of schemaText used to hold schema contents; rename to schemaText for accuracy.

Suggested change
String schemeText = baos.toString();
Assert.assertTrue(schemeText.contains("date"));
Assert.assertTrue(schemeText.contains("instant"));
Assert.assertTrue(schemeText.contains("uuid"));
Assert.assertTrue(schemeText.contains("localDate"));
String schemaText = baos.toString();
Assert.assertTrue(schemaText.contains("date"));
Assert.assertTrue(schemaText.contains("instant"));
Assert.assertTrue(schemaText.contains("uuid"));
Assert.assertTrue(schemaText.contains("localDate"));

Copilot uses AI. Check for mistakes.
}
int leastSigBitsPosInPojoSchema = schema.getPosition(MappedFieldType.UUID_LEAST_SIG_BITS.getSpecialFieldName());
if(leastSigBitsPosInPojoSchema != -1) {
long value = hollowObject.getLong(MappedFieldType.UUID_LEAST_SIG_BITS.getSpecialFieldName());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not specifically for this PR, but a note on performance: since we already have a field position here I wonder if we can avoid the string match on field name again cc @eduardoramirez

leastSigBits = value;
}
}
if (mostSigBits != null && leastSigBits != null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there are 2 long components, having Long.MIN_VALUE as a sentinel effectively means certain UUIDs where one of the long blocks is 2^64 are usable. Effecitvely, 2^64 + 2^64 UUIDs will fall into this edge case, unlikely to ever hit this edge case but wonder if it can be handled better

ENUM_NAME(FieldType.STRING, "_name"),
DATE_TIME(FieldType.LONG, "value");
DATE_TIME(FieldType.LONG, "value"),
UUID_MOST_SIG_BITS(FieldType.LONG, "mostSigBits"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably namespace the long components of UUID into their own custom type name, otherwise it will undo the compression benefits for all other long values in the dataset.. since encoding a long normally only #bits required to encode the largest long value, so if UUID is also using the long type then #bits required to encode any long will be 64.

catch(Exception e) {
throw new RuntimeException(e);
}
} else if(clazz == UUID.class) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we plan to also implement these in the generated HollowAPI? Atm clients would need something like

com.netflix.foo.generated.UUID uuid = someHollowObject.getMyUuidField();
// System.out.println(uuid); // mostSigBits: -8232749396958297039 leastSigBits: -7121165017364359242
java.util.UUID javaUUID = new java.util.UUID(uuid.getMostSigBits(), uuid.getLeastSigBits());
System.out.println(javaUUID); // 8dbf660a-dd7d-4431-9d2c-8a25710f17b6

Comment on lines 415 to 418
int nanoPosInPojoSchema = schema.getPosition(MappedFieldType.INSTANT_SECONDS.getSpecialFieldName());
if(nanoPosInPojoSchema != -1) {
int nanoValue = objectNode.getFieldValueInt(MappedFieldType.INSTANT_SECONDS.getSpecialFieldName());
if(nanoValue != Long.MIN_VALUE) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah this seems like a bug

@yujiaaa2019 yujiaaa2019 requested a review from Sunjeet October 14, 2025 19:19
case FLOAT:
return (int)fixedLengthValue == HollowObjectWriteRecord.NULL_FLOAT_BITS;
case DOUBLE:
case UUID_LONG:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this field for avoiding making all LONGs to take 64 bits. But it will be backward incompatible for namespaces using UUID already.

  1. Old producer (LONG) → New consumer (UUID_LONG): The consumer will see a field type mismatch and throw IncompatibleSchemaException
  2. New producer (UUID_LONG) → Old consumer (LONG): The old consumer will see a field type mismatch and throw IncompatibleSchemaException

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants