-
Notifications
You must be signed in to change notification settings - Fork 4.6k
[Iceberg] Make timestamptz return new Timestamp.MICROS logical type #39344
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: master
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| { | ||
| "comment": "Modify this file in a trivial way to cause this test suite to run.", | ||
| "modification": 2 | ||
| "modification": 3 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ | |
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.time.LocalTime; | ||
| import java.time.OffsetDateTime; | ||
| import java.util.Arrays; | ||
| import java.util.Base64; | ||
| import java.util.Collections; | ||
|
|
@@ -1449,17 +1450,27 @@ public void testToBeamRow_timestampMicros_utcSuffix() { | |
|
|
||
| // BigQuery format with " UTC" suffix | ||
| String timestamp = "2024-08-10 16:52:07.123456 UTC"; | ||
|
|
||
| Row beamRow = BigQueryUtils.toBeamRow(schema, new TableRow().set("ts", timestamp)); | ||
|
|
||
| java.time.Instant actual = (java.time.Instant) beamRow.getValue("ts"); | ||
| assertEquals(2024, actual.atZone(java.time.ZoneOffset.UTC).getYear()); | ||
| assertEquals(8, actual.atZone(java.time.ZoneOffset.UTC).getMonthValue()); | ||
| assertEquals(10, actual.atZone(java.time.ZoneOffset.UTC).getDayOfMonth()); | ||
| assertEquals(16, actual.atZone(java.time.ZoneOffset.UTC).getHour()); | ||
| assertEquals(52, actual.atZone(java.time.ZoneOffset.UTC).getMinute()); | ||
| assertEquals(7, actual.atZone(java.time.ZoneOffset.UTC).getSecond()); | ||
| assertEquals(123456000, actual.getNano()); | ||
| String parsableTimestamp = "2024-08-10T16:52:07.123456Z"; | ||
|
Collaborator
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. Can we add a negative epoch and micros with leading zeros (e.g. .012345) test? |
||
| java.time.Instant instant = OffsetDateTime.parse(parsableTimestamp).toInstant(); | ||
| long seconds = instant.getEpochSecond(); | ||
| long micros = instant.getNano() / 1000; // BQ stores in micros precision | ||
| String value = seconds + "." + micros; | ||
|
|
||
| List<TableRow> testRows = | ||
| Arrays.asList(new TableRow().set("ts", timestamp), new TableRow().set("ts", value)); | ||
|
|
||
| for (TableRow row : testRows) { | ||
| Row beamRow = BigQueryUtils.toBeamRow(schema, row); | ||
|
|
||
| java.time.Instant actual = (java.time.Instant) beamRow.getValue("ts"); | ||
| assertEquals(2024, actual.atZone(java.time.ZoneOffset.UTC).getYear()); | ||
| assertEquals(8, actual.atZone(java.time.ZoneOffset.UTC).getMonthValue()); | ||
| assertEquals(10, actual.atZone(java.time.ZoneOffset.UTC).getDayOfMonth()); | ||
| assertEquals(16, actual.atZone(java.time.ZoneOffset.UTC).getHour()); | ||
| assertEquals(52, actual.atZone(java.time.ZoneOffset.UTC).getMinute()); | ||
| assertEquals(7, actual.atZone(java.time.ZoneOffset.UTC).getSecond()); | ||
| assertEquals(123456000, actual.getNano()); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -40,7 +40,9 @@ | |||||
| import org.apache.beam.sdk.schemas.logicaltypes.MicrosInstant; | ||||||
| import org.apache.beam.sdk.schemas.logicaltypes.PassThroughLogicalType; | ||||||
| import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; | ||||||
| import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; | ||||||
| import org.apache.beam.sdk.util.Preconditions; | ||||||
| import org.apache.beam.sdk.util.construction.TransformUpgrader; | ||||||
| import org.apache.beam.sdk.values.PCollection; | ||||||
| import org.apache.beam.sdk.values.Row; | ||||||
| import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; | ||||||
|
|
@@ -81,9 +83,11 @@ private IcebergUtils() {} | |||||
| .put(SqlTypes.DATETIME.getIdentifier(), Types.TimestampType.withoutZone()) | ||||||
| .put(SqlTypes.UUID.getIdentifier(), Types.UUIDType.get()) | ||||||
| .put(MicrosInstant.IDENTIFIER, Types.TimestampType.withZone()) | ||||||
| .put(Timestamp.IDENTIFIER, Types.TimestampType.withZone()) | ||||||
|
Collaborator
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. Will using timestamp logical type break xlang python reads?
Collaborator
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. What if it is a Timestamp.NANOS? Should we throw if the logical type is not explicitly MICROS? |
||||||
| .build(); | ||||||
|
|
||||||
| private static Schema.FieldType icebergTypeToBeamFieldType(final Type type) { | ||||||
| private static Schema.FieldType icebergTypeToBeamFieldType( | ||||||
| final Type type, @Nullable String updateCompatibilityVersion) { | ||||||
| switch (type.typeId()) { | ||||||
| case BOOLEAN: | ||||||
| return Schema.FieldType.BOOLEAN; | ||||||
|
|
@@ -102,7 +106,14 @@ private static Schema.FieldType icebergTypeToBeamFieldType(final Type type) { | |||||
| case TIMESTAMP: | ||||||
| Types.TimestampType ts = (Types.TimestampType) type.asPrimitiveType(); | ||||||
| if (ts.shouldAdjustToUTC()) { | ||||||
| return Schema.FieldType.DATETIME; | ||||||
| // timestamptz. The micros-precision Timestamp logical type preserves microseconds, while | ||||||
| // the legacy DATETIME (joda) mapping truncates to millis. Gated for update compatibility. | ||||||
| if (updateCompatibilityVersion != null | ||||||
| && !updateCompatibilityVersion.isEmpty() | ||||||
| && TransformUpgrader.compareVersions(updateCompatibilityVersion, "2.76.0") < 0) { | ||||||
| return Schema.FieldType.DATETIME; | ||||||
| } | ||||||
| return Schema.FieldType.logicalType(Timestamp.MICROS); | ||||||
| } | ||||||
| return Schema.FieldType.logicalType(SqlTypes.DATETIME); | ||||||
| case STRING: | ||||||
|
|
@@ -114,36 +125,51 @@ private static Schema.FieldType icebergTypeToBeamFieldType(final Type type) { | |||||
| case DECIMAL: | ||||||
| return Schema.FieldType.DECIMAL; | ||||||
| case STRUCT: | ||||||
| return Schema.FieldType.row(icebergStructTypeToBeamSchema(type.asStructType())); | ||||||
| return Schema.FieldType.row( | ||||||
| icebergStructTypeToBeamSchema(type.asStructType(), updateCompatibilityVersion)); | ||||||
| case LIST: | ||||||
| return Schema.FieldType.array(icebergTypeToBeamFieldType(type.asListType().elementType())); | ||||||
| return Schema.FieldType.array( | ||||||
| icebergTypeToBeamFieldType( | ||||||
| type.asListType().elementType(), updateCompatibilityVersion)); | ||||||
| case MAP: | ||||||
| return Schema.FieldType.map( | ||||||
| icebergTypeToBeamFieldType(type.asMapType().keyType()), | ||||||
| icebergTypeToBeamFieldType(type.asMapType().valueType())); | ||||||
| icebergTypeToBeamFieldType(type.asMapType().keyType(), updateCompatibilityVersion), | ||||||
| icebergTypeToBeamFieldType(type.asMapType().valueType(), updateCompatibilityVersion)); | ||||||
| default: | ||||||
| throw new RuntimeException("Unrecognized Iceberg Type: " + type.typeId()); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private static Schema.Field icebergFieldToBeamField(final Types.NestedField field) { | ||||||
| return Schema.Field.of(field.name(), icebergTypeToBeamFieldType(field.type())) | ||||||
| private static Schema.Field icebergFieldToBeamField( | ||||||
| final Types.NestedField field, @Nullable String updateCompatibilityVersion) { | ||||||
| return Schema.Field.of( | ||||||
| field.name(), icebergTypeToBeamFieldType(field.type(), updateCompatibilityVersion)) | ||||||
| .withNullable(field.isOptional()); | ||||||
| } | ||||||
|
|
||||||
| /** Converts an Iceberg {@link org.apache.iceberg.Schema} to a Beam {@link Schema}. */ | ||||||
| public static Schema icebergSchemaToBeamSchema(final org.apache.iceberg.Schema schema) { | ||||||
| return icebergSchemaToBeamSchema(schema, null); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Converts an Iceberg {@link org.apache.iceberg.Schema} to a Beam {@link Schema}, accounting for | ||||||
| * update compatibility. | ||||||
| */ | ||||||
| public static Schema icebergSchemaToBeamSchema( | ||||||
|
Collaborator
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 see some other call sites that do not use the update compat override beam/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCatalogConfig.java Line 205 in 2cd12e5
beam/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcOutputUtils.java Line 139 in 2cd12e5
|
||||||
| final org.apache.iceberg.Schema schema, @Nullable String updateCompatibilityVersion) { | ||||||
| Schema.Builder builder = Schema.builder(); | ||||||
| for (Types.NestedField f : schema.columns()) { | ||||||
| builder.addField(icebergFieldToBeamField(f)); | ||||||
| builder.addField(icebergFieldToBeamField(f, updateCompatibilityVersion)); | ||||||
| } | ||||||
| return builder.build(); | ||||||
| } | ||||||
|
|
||||||
| private static Schema icebergStructTypeToBeamSchema(final Types.StructType struct) { | ||||||
| private static Schema icebergStructTypeToBeamSchema( | ||||||
| final Types.StructType struct, @Nullable String updateCompatibilityVersion) { | ||||||
| Schema.Builder builder = Schema.builder(); | ||||||
| for (Types.NestedField f : struct.fields()) { | ||||||
| builder.addField(icebergFieldToBeamField(f)); | ||||||
| builder.addField(icebergFieldToBeamField(f, updateCompatibilityVersion)); | ||||||
| } | ||||||
| return builder.build(); | ||||||
| } | ||||||
|
|
@@ -613,21 +639,28 @@ private static Object getLogicalTypeValue(Object icebergValue, Schema.FieldType | |||||
| return LocalTime.parse(strValue); | ||||||
| } else if (type.isLogicalType(SqlTypes.DATETIME.getIdentifier())) { | ||||||
| return LocalDateTime.parse(strValue); | ||||||
| } else if (type.isLogicalType(Timestamp.IDENTIFIER)) { | ||||||
| return OffsetDateTime.parse(strValue).toInstant(); | ||||||
| } | ||||||
|
ahmedabu98 marked this conversation as resolved.
|
||||||
| } else if (icebergValue instanceof Long) { | ||||||
| if (type.isLogicalType(SqlTypes.TIME.getIdentifier())) { | ||||||
| return DateTimeUtil.timeFromMicros((Long) icebergValue); | ||||||
| } else if (type.isLogicalType(SqlTypes.DATETIME.getIdentifier())) { | ||||||
| return DateTimeUtil.timestampFromMicros((Long) icebergValue); | ||||||
| } else if (type.isLogicalType(Timestamp.IDENTIFIER)) { | ||||||
| // timestamptz stored as micros since epoch -> java.time.Instant (micros preserved). | ||||||
| return DateTimeUtil.timestamptzFromMicros((Long) icebergValue).toInstant(); | ||||||
| } | ||||||
| } else if (icebergValue instanceof Integer | ||||||
| && type.isLogicalType(SqlTypes.DATE.getIdentifier())) { | ||||||
| return DateTimeUtil.dateFromDays((Integer) icebergValue); | ||||||
| } else if (icebergValue instanceof OffsetDateTime | ||||||
| && type.isLogicalType(SqlTypes.DATETIME.getIdentifier())) { | ||||||
| return ((OffsetDateTime) icebergValue) | ||||||
| .withOffsetSameInstant(ZoneOffset.UTC) | ||||||
| .toLocalDateTime(); | ||||||
| } else if (icebergValue instanceof OffsetDateTime) { | ||||||
| OffsetDateTime odt = (OffsetDateTime) icebergValue; | ||||||
| if (type.isLogicalType(SqlTypes.DATETIME.getIdentifier())) { | ||||||
| return odt.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(); | ||||||
| } else if (type.isLogicalType(Timestamp.IDENTIFIER)) { | ||||||
| return odt.toInstant(); | ||||||
| } | ||||||
| } | ||||||
| // LocalDateTime, LocalDate, LocalTime | ||||||
| return icebergValue; | ||||||
|
|
||||||
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.
This is also used by load jobs and direct reads and both of them use the UTC string version right?
I am a bit concerned about cost of try to parse -> throw exception (expensive) -> try again for every single row in those cases.
Can we either do a conditional (check if ends with UTC or if contains :) to infer which approach we should take?
Maybe a future improvement we can make is the caller can pass the API they used to read so we know what type to expect.