Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -469,14 +474,59 @@ private static boolean isValidPartitionTime(
AutoPartitionTimeUnit timeUnit,
AutoPartitionStrategy autoPartitionStrategy) {
try {
DateTimeFormatter.ofPattern(getPartitionTimeFormat(timeUnit, autoPartitionStrategy))
.parse(time);
return true;
TemporalAccessor parsed =
buildPartitionTimeParser(
getPartitionTimeFormat(timeUnit, autoPartitionStrategy))
.parse(time);
// a quarter without a day is not range-checked by the resolver
return !parsed.isSupported(IsoFields.QUARTER_OF_YEAR)
|| IsoFields.QUARTER_OF_YEAR
.range()
.isValidValue(parsed.getLong(IsoFields.QUARTER_OF_YEAR));
} catch (DateTimeParseException e) {
return false;
}
}

/**
* Builds a strict parser for the given partition time pattern.
*
* <p>A single-letter quarter field ('Q' or 'q') is parsed as a fixed-width value, because the
* variable-width quarter of {@link DateTimeFormatter#ofPattern(String)} cannot follow an
* adjacent year field, e.g. "yyyyQ" fails to parse "20244". The {@link ResolverStyle#STRICT}
* resolver rejects non-existent times such as "20240230", and the default era lets the
* year-of-era field ('y') resolve under it.
*/
private static DateTimeFormatter buildPartitionTimeParser(String pattern) {
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
StringBuilder pendingPattern = new StringBuilder();
boolean inLiteral = false;
for (int index = 0; index < pattern.length(); index++) {
char patternChar = pattern.charAt(index);
if (patternChar == '\'') {
inLiteral = !inLiteral;
} else if (!inLiteral
&& (patternChar == 'Q' || patternChar == 'q')
&& (index == 0 || pattern.charAt(index - 1) != patternChar)
&& (index + 1 == pattern.length()
|| pattern.charAt(index + 1) != patternChar)) {
if (pendingPattern.length() > 0) {
builder.appendPattern(pendingPattern.toString());
pendingPattern.setLength(0);
}
builder.appendValue(IsoFields.QUARTER_OF_YEAR, 1);
continue;
}
pendingPattern.append(patternChar);
}
if (pendingPattern.length() > 0) {
builder.appendPattern(pendingPattern.toString());
}
return builder.parseDefaulting(ChronoField.ERA, 1)
.toFormatter()
.withResolverStyle(ResolverStyle.STRICT);
}

private static String getFormattedTime(ZonedDateTime zonedDateTime, String format) {
return DateTimeFormatter.ofPattern(format).format(zonedDateTime);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,95 @@ void testValidateConfiguredTimeFormatWhenAutoPartitionIsDisabled() {
.hasMessageContaining("yyyy-MM-dd");
}

@Test
void testValidateAutoPartitionTimeWithQuarterFormat() {
Configuration quarterConf = new Configuration();
quarterConf.setString(ConfigOptions.TABLE_AUTO_PARTITION_TIME_FORMAT, "yyyyQ");
AutoPartitionStrategy quarterStrategy = AutoPartitionStrategy.from(quarterConf);

for (String partitionTime : Arrays.asList("20241", "20244")) {
assertThatNoException()
.isThrownBy(
() ->
validateAutoPartitionTime(
new PartitionSpec(
Collections.singletonMap("dt", partitionTime)),
Collections.singletonList("dt"),
quarterStrategy));
}
for (String partitionTime : Arrays.asList("20240", "20245", "2024", "202441")) {
assertThatThrownBy(
() ->
validateAutoPartitionTime(
new PartitionSpec(
Collections.singletonMap("dt", partitionTime)),
Collections.singletonList("dt"),
quarterStrategy))
.isInstanceOf(InvalidPartitionException.class)
.hasMessageContaining(partitionTime);
}

// the default quarter format is also "yyyyQ"
Configuration defaultQuarterConf = new Configuration();
defaultQuarterConf.set(
ConfigOptions.TABLE_AUTO_PARTITION_TIME_UNIT, AutoPartitionTimeUnit.QUARTER);
defaultQuarterConf.setString(ConfigOptions.TABLE_AUTO_PARTITION_TIMEZONE, "UTC");
AutoPartitionStrategy defaultQuarterStrategy =
AutoPartitionStrategy.from(defaultQuarterConf);
Instant now = Instant.parse("2024-11-11T00:00:00Z");
assertThat(isPastAutoPartition("20243", defaultQuarterStrategy, now)).isTrue();
assertThat(isPastAutoPartition("20244", defaultQuarterStrategy, now)).isFalse();
assertThat(isPastAutoPartition("20251", defaultQuarterStrategy, now)).isFalse();
}

@Test
void testValidateAutoPartitionTimeRejectsNonExistentDates() {
Configuration dayConf = new Configuration();
dayConf.setString(ConfigOptions.TABLE_AUTO_PARTITION_TIME_FORMAT, "yyyyMMdd");
AutoPartitionStrategy dayStrategy = AutoPartitionStrategy.from(dayConf);
Configuration hourConf = new Configuration();
hourConf.setString(ConfigOptions.TABLE_AUTO_PARTITION_TIME_FORMAT, "yyyyMMddHH");
AutoPartitionStrategy hourStrategy = AutoPartitionStrategy.from(hourConf);

assertThatNoException()
.isThrownBy(
() ->
validateAutoPartitionTime(
new PartitionSpec(
Collections.singletonMap("dt", "20240229")),
Collections.singletonList("dt"),
dayStrategy));
for (String partitionTime : Arrays.asList("20240230", "20230229", "20240431")) {
assertThatThrownBy(
() ->
validateAutoPartitionTime(
new PartitionSpec(
Collections.singletonMap("dt", partitionTime)),
Collections.singletonList("dt"),
dayStrategy))
.isInstanceOf(InvalidPartitionException.class)
.hasMessageContaining(partitionTime);
}

assertThatNoException()
.isThrownBy(
() ->
validateAutoPartitionTime(
new PartitionSpec(
Collections.singletonMap("dt", "2024010123")),
Collections.singletonList("dt"),
hourStrategy));
assertThatThrownBy(
() ->
validateAutoPartitionTime(
new PartitionSpec(
Collections.singletonMap("dt", "2024010124")),
Collections.singletonList("dt"),
hourStrategy))
.isInstanceOf(InvalidPartitionException.class)
.hasMessageContaining("2024010124");
}

@Test
void testValidateAutoPartitionTimeRetentionBoundaryWithConfiguredDayFormat() {
Configuration dashedDayConf = new Configuration();
Expand Down
Loading