-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Fix date and timestamp transforms #1981
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
Changes from all commits
a0b8816
caad9b5
b4eb6e6
146ff9e
8162189
98a9f9b
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 |
|---|---|---|
|
|
@@ -21,14 +21,17 @@ | |
|
|
||
| import java.math.BigDecimal; | ||
| import java.math.BigInteger; | ||
| import java.util.Set; | ||
| import org.apache.iceberg.expressions.BoundLiteralPredicate; | ||
| import org.apache.iceberg.expressions.BoundPredicate; | ||
| import org.apache.iceberg.expressions.BoundSetPredicate; | ||
| import org.apache.iceberg.expressions.BoundTransform; | ||
| import org.apache.iceberg.expressions.Expression; | ||
| import org.apache.iceberg.expressions.Expressions; | ||
| import org.apache.iceberg.expressions.Literal; | ||
| import org.apache.iceberg.expressions.UnboundPredicate; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.Iterables; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.Sets; | ||
|
|
||
| import static org.apache.iceberg.expressions.Expressions.predicate; | ||
|
|
||
|
|
@@ -254,4 +257,145 @@ static <S, T> UnboundPredicate<T> transformSet(String fieldName, | |
| return predicate(predicate.op(), fieldName, | ||
| Iterables.transform(predicate.asSetPredicate().literalSet(), transform::apply)); | ||
| } | ||
|
|
||
| /** | ||
| * Fixes an inclusive projection to account for incorrectly transformed values. | ||
| * <p> | ||
| * A bug in 0.10.0 and earlier caused negative values to be incorrectly transformed by date and timestamp transforms | ||
| * to 1 larger than the correct value. For example, day(1969-12-31 10:00:00) produced 0 instead of -1. To read data | ||
| * written by versions with this bug, this method adjusts the inclusive projection. The current inclusive projection | ||
| * is correct, so this modifies the "correct" projection when needed. For example, < day(1969-12-31 10:00:00) will | ||
| * produce <= -1 (= 1969-12-31) and is adjusted to <= 0 (= 1969-01-01) because the incorrect transformed value was 0. | ||
| */ | ||
| static UnboundPredicate<Integer> fixInclusiveTimeProjection(UnboundPredicate<Integer> projected) { | ||
| if (projected == null) { | ||
| return projected; | ||
| } | ||
|
|
||
| // adjust the predicate for values that were 1 larger than the correct transformed value | ||
|
rdblue marked this conversation as resolved.
|
||
| switch (projected.op()) { | ||
| case LT: | ||
| if (projected.literal().value() < 0) { | ||
| return Expressions.lessThan(projected.term(), projected.literal().value() + 1); | ||
| } | ||
|
|
||
| return projected; | ||
|
|
||
| case LT_EQ: | ||
| if (projected.literal().value() < 0) { | ||
| return Expressions.lessThanOrEqual(projected.term(), projected.literal().value() + 1); | ||
| } | ||
|
|
||
| return projected; | ||
|
|
||
| case GT: | ||
| case GT_EQ: | ||
| // incorrect projected values are already greater than the bound for GT, GT_EQ | ||
| return projected; | ||
|
|
||
| case EQ: | ||
| if (projected.literal().value() < 0) { | ||
| // match either the incorrect value (projectedValue + 1) or the correct value (projectedValue) | ||
| return Expressions.in(projected.term(), projected.literal().value(), projected.literal().value() + 1); | ||
| } | ||
|
|
||
| return projected; | ||
|
|
||
| case IN: | ||
| Set<Integer> fixedSet = Sets.newHashSet(); | ||
| boolean hasNegativeValue = false; | ||
| for (Literal<Integer> lit : projected.literals()) { | ||
| Integer value = lit.value(); | ||
| fixedSet.add(value); | ||
| if (value < 0) { | ||
| hasNegativeValue = true; | ||
| fixedSet.add(value + 1); | ||
| } | ||
| } | ||
|
|
||
| if (hasNegativeValue) { | ||
| return Expressions.in(projected.term(), fixedSet); | ||
|
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. why not just always return this new expression? We build up fixedSet no matter what?
Contributor
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. If there is no negative value, then there is no need to fixup the expression and we can return the original. That avoids some object allocation? |
||
| } | ||
|
|
||
| return projected; | ||
|
|
||
| case NOT_IN: | ||
| case NOT_EQ: | ||
| // there is no inclusive projection for NOT_EQ and NOT_IN | ||
| return null; | ||
|
|
||
| default: | ||
| return projected; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Fixes a strict projection to account for incorrectly transformed values. | ||
| * <p> | ||
| * A bug in 0.10.0 and earlier caused negative values to be incorrectly transformed by date and timestamp transforms | ||
| * to 1 larger than the correct value. For example, day(1969-12-31 10:00:00) produced 0 instead of -1. To read data | ||
| * written by versions with this bug, this method adjusts the strict projection. | ||
| */ | ||
| static UnboundPredicate<Integer> fixStrictTimeProjection(UnboundPredicate<Integer> projected) { | ||
| if (projected == null) { | ||
| return null; | ||
| } | ||
|
|
||
| switch (projected.op()) { | ||
| case LT: | ||
| case LT_EQ: | ||
| // the correct bound is a correct strict projection for the incorrectly transformed values. | ||
| return projected; | ||
|
|
||
| case GT: | ||
| // GT and GT_EQ need to be adjusted because values that do not match the predicate may have been transformed | ||
| // into partition values that match the projected predicate. For example, >= month(1969-11-31) is > -2, but | ||
| // 1969-10-31 was previously transformed to month -2 instead of -3. This must use the more strict value. | ||
| if (projected.literal().value() <= 0) { | ||
| return Expressions.greaterThan(projected.term(), projected.literal().value() + 1); | ||
| } | ||
|
|
||
| return projected; | ||
|
|
||
| case GT_EQ: | ||
| if (projected.literal().value() <= 0) { | ||
| return Expressions.greaterThanOrEqual(projected.term(), projected.literal().value() + 1); | ||
| } | ||
|
|
||
| return projected; | ||
|
|
||
| case EQ: | ||
| case IN: | ||
| // there is no strict projection for EQ and IN | ||
| return null; | ||
|
|
||
| case NOT_EQ: | ||
| if (projected.literal().value() < 0) { | ||
| return Expressions.notIn(projected.term(), projected.literal().value(), projected.literal().value() + 1); | ||
| } | ||
|
|
||
| return projected; | ||
|
|
||
| case NOT_IN: | ||
| Set<Integer> fixedSet = Sets.newHashSet(); | ||
| boolean hasNegativeValue = false; | ||
| for (Literal<Integer> lit : projected.literals()) { | ||
| Integer value = lit.value(); | ||
| fixedSet.add(value); | ||
| if (value < 0) { | ||
| hasNegativeValue = true; | ||
| fixedSet.add(value + 1); | ||
| } | ||
| } | ||
|
|
||
| if (hasNegativeValue) { | ||
| return Expressions.notIn(projected.term(), fixedSet); | ||
| } | ||
|
|
||
| return projected; | ||
|
|
||
| default: | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,12 +52,23 @@ public Integer apply(Long timestampMicros) { | |
| return null; | ||
| } | ||
|
|
||
| // discards fractional seconds, not needed for calculation | ||
| OffsetDateTime timestamp = Instant | ||
| .ofEpochSecond(timestampMicros / 1_000_000) | ||
| .atOffset(ZoneOffset.UTC); | ||
|
|
||
| return (int) granularity.between(EPOCH, timestamp); | ||
| if (timestampMicros >= 0) { | ||
| OffsetDateTime timestamp = Instant | ||
| .ofEpochSecond( | ||
| Math.floorDiv(timestampMicros, 1_000_000), | ||
| Math.floorMod(timestampMicros, 1_000_000) * 1000) | ||
|
Contributor
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. nit: extract |
||
| .atOffset(ZoneOffset.UTC); | ||
| return (int) granularity.between(EPOCH, timestamp); | ||
| } else { | ||
| // add 1 micro to the value to account for the case where there is exactly 1 unit between the timestamp and epoch | ||
| // because the result will always be decremented. | ||
| OffsetDateTime timestamp = Instant | ||
| .ofEpochSecond( | ||
| Math.floorDiv(timestampMicros, 1_000_000), | ||
| Math.floorMod(timestampMicros + 1, 1_000_000) * 1000) | ||
| .atOffset(ZoneOffset.UTC); | ||
| return (int) granularity.between(EPOCH, timestamp) - 1; | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -101,11 +112,16 @@ public UnboundPredicate<Integer> project(String fieldName, BoundPredicate<Long> | |
|
|
||
| if (pred.isUnaryPredicate()) { | ||
| return Expressions.predicate(pred.op(), fieldName); | ||
|
|
||
| } else if (pred.isLiteralPredicate()) { | ||
| return ProjectionUtil.truncateLong(fieldName, pred.asLiteralPredicate(), this); | ||
| UnboundPredicate<Integer> projected = ProjectionUtil.truncateLong(fieldName, pred.asLiteralPredicate(), this); | ||
| return ProjectionUtil.fixInclusiveTimeProjection(projected); | ||
|
|
||
| } else if (pred.isSetPredicate() && pred.op() == Expression.Operation.IN) { | ||
| return ProjectionUtil.transformSet(fieldName, pred.asSetPredicate(), this); | ||
| UnboundPredicate<Integer> projected = ProjectionUtil.transformSet(fieldName, pred.asSetPredicate(), this); | ||
| return ProjectionUtil.fixInclusiveTimeProjection(projected); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
|
|
@@ -117,11 +133,17 @@ public UnboundPredicate<Integer> projectStrict(String fieldName, BoundPredicate< | |
|
|
||
| if (pred.isUnaryPredicate()) { | ||
| return Expressions.predicate(pred.op(), fieldName); | ||
|
|
||
| } else if (pred.isLiteralPredicate()) { | ||
| return ProjectionUtil.truncateLongStrict(fieldName, pred.asLiteralPredicate(), this); | ||
| UnboundPredicate<Integer> projected = ProjectionUtil.truncateLongStrict( | ||
| fieldName, pred.asLiteralPredicate(), this); | ||
| return ProjectionUtil.fixStrictTimeProjection(projected); | ||
|
|
||
| } else if (pred.isSetPredicate() && pred.op() == Expression.Operation.NOT_IN) { | ||
| return ProjectionUtil.transformSet(fieldName, pred.asSetPredicate(), this); | ||
| UnboundPredicate<Integer> projected = ProjectionUtil.transformSet(fieldName, pred.asSetPredicate(), this); | ||
| return ProjectionUtil.fixStrictTimeProjection(projected); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems the same logic (
if (condition) {return...} return ...) appears multiple times, can it be included inside a method, e.g. just embed it in the method defined inProjectionUtil?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are only 4 instance of this, and they call 2 different fix methods. I don't think it would be worth adding 2 methods just to dedup 4 lines here.