-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Parquet, Spark, Flink: Type uniformity for variant shredding #17424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
c8454fd
4f717b6
63a1d4c
69d6e88
c566dcc
88731f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,10 +51,10 @@ | |
| * | ||
| * <ul> | ||
| * <li>Object fields are emitted in alphabetical order in the shredded schema. | ||
| * <li>Type selection picks the most common type with explicit tie-break priority (see {@link | ||
| * FieldInfo#TIE_BREAK_PRIORITY}), not enum ordinal. | ||
| * <li>A field is admitted only if all observations fall into a single type family after numeric | ||
| * widening; mixed-type fields remain in the residual {@code value}. | ||
| * <li>Integer types (INT8/16/32/64) and decimal types (DECIMAL4/8/16) are each promoted to the | ||
| * widest observed before competing with other types. | ||
| * widest observed within their family. | ||
| * <li>Fields below {@code MIN_FIELD_FREQUENCY} are pruned. Above {@code MAX_SHREDDED_FIELDS}, the | ||
|
nssalian marked this conversation as resolved.
|
||
| * most frequent are kept with alphabetical tie-breaking. | ||
| * <li>Recursion into nested objects/arrays stops at {@code MAX_SHREDDING_DEPTH} (default 50). | ||
|
|
@@ -96,7 +96,7 @@ public Type analyzeAndCreateSchema(List<T> bufferedRows, int variantFieldIndex) | |
| } | ||
|
|
||
| PathNode root = buildPathTree(variantValues); | ||
| PhysicalType rootType = root.info.getMostCommonType(); | ||
| PhysicalType rootType = root.info.admittedType(); | ||
| if (rootType == null) { | ||
| return null; | ||
| } | ||
|
|
@@ -251,12 +251,12 @@ private static void traverseArray(PathNode node, VariantArray array, int depth) | |
| } | ||
|
|
||
| private static Type buildFieldGroup(PathNode node) { | ||
| PhysicalType commonType = node.info.getMostCommonType(); | ||
| if (commonType == null) { | ||
| PhysicalType admittedType = node.info.admittedType(); | ||
| if (admittedType == null) { | ||
| return null; | ||
| } | ||
|
|
||
| Type typedValue = buildTypedValue(node, commonType); | ||
| Type typedValue = buildTypedValue(node, admittedType); | ||
| if (typedValue == null) { | ||
| return null; | ||
| } | ||
|
|
@@ -303,7 +303,7 @@ private static Type createArrayTypedValue(PathNode node) { | |
| if (elementNode == null) { | ||
| return null; | ||
| } | ||
| PhysicalType elementType = elementNode.info.getMostCommonType(); | ||
| PhysicalType elementType = elementNode.info.admittedType(); | ||
| if (elementType == null) { | ||
| return null; | ||
| } | ||
|
|
@@ -425,8 +425,8 @@ private static class FieldInfo { | |
| private int maxDecimalScale = 0; | ||
| private int maxDecimalIntegerDigits = 0; | ||
| private int observationCount = 0; | ||
| private boolean mostCommonComputed = false; | ||
| private PhysicalType mostCommonCached = null; | ||
| private boolean admittedTypeComputed = false; | ||
| private PhysicalType admittedTypeCached = null; | ||
|
|
||
| private static final Map<PhysicalType, Integer> INTEGER_PRIORITY = | ||
| ImmutableMap.of( | ||
|
|
@@ -441,32 +441,8 @@ private static class FieldInfo { | |
| PhysicalType.DECIMAL8, 1, | ||
| PhysicalType.DECIMAL16, 2); | ||
|
|
||
| /** Tie-break ordering when two physical types have equal counts. Higher value wins. */ | ||
| private static final Map<PhysicalType, Integer> TIE_BREAK_PRIORITY = | ||
| ImmutableMap.<PhysicalType, Integer>builder() | ||
| .put(PhysicalType.BOOLEAN_TRUE, 0) | ||
| .put(PhysicalType.INT8, 1) | ||
| .put(PhysicalType.INT16, 2) | ||
| .put(PhysicalType.INT32, 3) | ||
| .put(PhysicalType.INT64, 4) | ||
| .put(PhysicalType.FLOAT, 5) | ||
| .put(PhysicalType.DOUBLE, 6) | ||
| .put(PhysicalType.DECIMAL4, 7) | ||
| .put(PhysicalType.DECIMAL8, 8) | ||
| .put(PhysicalType.DECIMAL16, 9) | ||
| .put(PhysicalType.DATE, 10) | ||
| .put(PhysicalType.TIME, 11) | ||
| .put(PhysicalType.TIMESTAMPTZ, 12) | ||
| .put(PhysicalType.TIMESTAMPNTZ, 13) | ||
| .put(PhysicalType.BINARY, 14) | ||
| .put(PhysicalType.STRING, 15) | ||
| .put(PhysicalType.TIMESTAMPTZ_NANOS, 16) | ||
| .put(PhysicalType.TIMESTAMPNTZ_NANOS, 17) | ||
| .put(PhysicalType.UUID, 18) | ||
| .buildOrThrow(); | ||
|
|
||
| void observe(VariantValue value) { | ||
| mostCommonComputed = false; | ||
| admittedTypeComputed = false; | ||
| observationCount++; | ||
| // Use BOOLEAN_TRUE for both TRUE/FALSE values | ||
| PhysicalType type = | ||
|
|
@@ -483,62 +459,56 @@ void observe(VariantValue value) { | |
| } | ||
| } | ||
|
|
||
| PhysicalType getMostCommonType() { | ||
| if (mostCommonComputed) { | ||
| return mostCommonCached; | ||
| PhysicalType admittedType() { | ||
| if (admittedTypeComputed) { | ||
| return admittedTypeCached; | ||
| } | ||
|
|
||
| Map<PhysicalType, Integer> combinedCounts = Maps.newHashMap(); | ||
|
|
||
| int integerTotalCount = 0; | ||
| PhysicalType mostCapableInteger = null; | ||
|
|
||
| int decimalTotalCount = 0; | ||
| PhysicalType mostCapableDecimal = null; | ||
| Set<PhysicalType> families = Sets.newHashSet(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I generally like to avoid having multiple local variables that only work if they are set in certain combinations and I think we can make this a bit tighter if instead of tracking all these things separately we do something like. if admitted not set - set here is a quick draft i had the llm do PhysicalType admittedType() {
PhysicalType admitted = null;
for (int i = 0; i < typeCounts.length; i++) {
if (typeCounts[i] == 0) {
continue;
}
PhysicalType merged = mergeFamily(admitted, PHYSICAL_TYPES[i]);
if (merged == null) {
// Mixed type families: do not shred this field.
return null;
}
admitted = merged;
}
return admitted;
}
/**
* Merges {@code candidate} into the currently admitted type.
*
* <p>Returns the wider type when both are in the same integer or decimal family, {@code
* candidate} when nothing is admitted yet, {@code current} when the types are identical, and
* null when the types are from incompatible families (including FLOAT vs DOUBLE).
*/
private static PhysicalType mergeFamily(PhysicalType current, PhysicalType candidate) {
if (current == null) {
return candidate;
}
if (current == candidate) {
return current;
}
if (isIntegerType(current) && isIntegerType(candidate)) {
return integerRank(current) >= integerRank(candidate) ? current : candidate;
}
if (isDecimalType(current) && isDecimalType(candidate)) {
return decimalRank(current) >= decimalRank(candidate) ? current : candidate;
}
return null;
}
private static int integerRank(PhysicalType type) {
return switch (type) {
case INT8 -> 0;
case INT16 -> 1;
case INT32 -> 2;
case INT64 -> 3;
default -> throw new IllegalArgumentException("Not an integer type: " + type);
};
}
private static int decimalRank(PhysicalType type) {
return switch (type) {
case DECIMAL4 -> 0;
case DECIMAL8 -> 1;
case DECIMAL16 -> 2;
default -> throw new IllegalArgumentException("Not a decimal type: " + type);
};
}
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure on the ranking code there, originally I had it with enum ordinal but thats a spotless issue. Maybe we just have static ordered type lists for each family?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would try iterating on that though ... see if we can remove all the state here we don't need
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah let me see if this is complete. I'll noodle on this more and update.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dropped widestInteger, widestDecimal, admittedTypeCached, and the families Set. Remaining state (typeCounts, decimal scale/digits, observation count) is used for schema build and pruning. |
||
| PhysicalType widestInteger = null; | ||
| PhysicalType widestDecimal = null; | ||
|
|
||
| for (int i = 0; i < typeCounts.length; i++) { | ||
| int count = typeCounts[i]; | ||
| if (count == 0) { | ||
| if (typeCounts[i] == 0) { | ||
| continue; | ||
| } | ||
| PhysicalType type = PHYSICAL_TYPES[i]; | ||
|
|
||
| if (isIntegerType(type)) { | ||
| integerTotalCount += count; | ||
| if (mostCapableInteger == null | ||
| || INTEGER_PRIORITY.get(type) > INTEGER_PRIORITY.get(mostCapableInteger)) { | ||
| mostCapableInteger = type; | ||
| } | ||
| widestInteger = widerType(widestInteger, type, INTEGER_PRIORITY); | ||
| } else if (isDecimalType(type)) { | ||
| decimalTotalCount += count; | ||
| if (mostCapableDecimal == null | ||
| || DECIMAL_PRIORITY.get(type) > DECIMAL_PRIORITY.get(mostCapableDecimal)) { | ||
| mostCapableDecimal = type; | ||
| } | ||
| widestDecimal = widerType(widestDecimal, type, DECIMAL_PRIORITY); | ||
| } else { | ||
| combinedCounts.put(type, count); | ||
| families.add(type); | ||
| } | ||
| } | ||
|
|
||
| if (mostCapableInteger != null) { | ||
| combinedCounts.put(mostCapableInteger, integerTotalCount); | ||
| if (widestInteger != null) { | ||
| families.add(widestInteger); | ||
| } | ||
|
|
||
| if (mostCapableDecimal != null) { | ||
| combinedCounts.put(mostCapableDecimal, decimalTotalCount); | ||
| if (widestDecimal != null) { | ||
| families.add(widestDecimal); | ||
| } | ||
|
|
||
| // Pick the most common type with tie-breaking | ||
| mostCommonCached = | ||
| combinedCounts.entrySet().stream() | ||
| .max( | ||
| Map.Entry.<PhysicalType, Integer>comparingByValue() | ||
| .thenComparingInt( | ||
| entry -> TIE_BREAK_PRIORITY.getOrDefault(entry.getKey(), -1))) | ||
| .map(Map.Entry::getKey) | ||
| .orElse(null); | ||
| mostCommonComputed = true; | ||
| return mostCommonCached; | ||
| // Type-uniformity: admit only if exactly one family remains after widening. | ||
| if (families.size() != 1) { | ||
| admittedTypeCached = null; | ||
| admittedTypeComputed = true; | ||
| return null; | ||
| } | ||
|
|
||
| admittedTypeCached = families.iterator().next(); | ||
| admittedTypeComputed = true; | ||
| return admittedTypeCached; | ||
| } | ||
|
|
||
| private static PhysicalType widerType( | ||
| PhysicalType current, PhysicalType candidate, Map<PhysicalType, Integer> priority) { | ||
| if (current == null) { | ||
| return candidate; | ||
| } | ||
| return priority.get(candidate) > priority.get(current) ? candidate : current; | ||
| } | ||
|
|
||
| private static boolean isIntegerType(PhysicalType type) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.