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
2 changes: 1 addition & 1 deletion .github/trigger_files/IO_Iceberg_Integration_Tests.json
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
}
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
## Breaking Changes

* (Python) Removed `google-perftools` from the SDK container images. Users who wish to use `--profiler_agent=tcmalloc` should install google-perftools APT package in their custom container images separately ([#39323](https://github.com/apache/beam/issues/39323)).
* [IcebergIO] Reading a `timestamptz` column will now return a `Timestamp.MICROS` Beam logical type, equivalent to a
`java.time.Instant` object, in order to preserve microseconds (the old Beam `Schema.FieldType#DATETIME` primitive type
truncates past milliseconds). Use pipeline option `--updateCompatibilityVersion=2.75.0` (or any older version) to
keep the old behavior ([#39344](https://github.com/apache/beam/issues/39344)).

## Deprecations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,14 @@ public static Row toBeamRow(Schema rowSchema, TableSchema bqSchema, TableRow jso
return java.time.Instant.parse(jsonBQString);
}
} else if (fieldType.isLogicalType(Timestamp.IDENTIFIER)) {
return VAR_PRECISION_FORMATTER.parse(jsonBQString, java.time.Instant::from);
try {

Copy link
Copy Markdown
Collaborator

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.

BigDecimal bd = new BigDecimal(jsonBQString);
long seconds = bd.longValue();
long nanos = bd.remainder(BigDecimal.ONE).movePointRight(9).longValue();
return java.time.Instant.ofEpochSecond(seconds, nanos);
} catch (NumberFormatException e) {
return VAR_PRECISION_FORMATTER.parse(jsonBQString, java.time.Instant::from);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Map;
import org.apache.beam.sdk.annotations.Internal;
import org.apache.beam.sdk.io.Read;
import org.apache.beam.sdk.options.StreamingOptions;
import org.apache.beam.sdk.schemas.Schema;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.values.PBegin;
Expand Down Expand Up @@ -668,12 +669,23 @@ public PCollection<Row> expand(PBegin input) {

Table table = TableCache.get(getCatalogConfig(), tableId);

@Nullable
String updateCompatibilityVersion =
input
.getPipeline()
.getOptions()
.as(StreamingOptions.class)
.getUpdateCompatibilityVersion();

IcebergScanConfig scanConfig =
IcebergScanConfig.builder()
.setCatalogConfig(getCatalogConfig())
.setScanType(IcebergScanConfig.ScanType.TABLE)
.setTableIdentifier(tableId)
.setSchema(IcebergUtils.icebergSchemaToBeamSchema(table.schema()))
.setSchema(
IcebergUtils.icebergSchemaToBeamSchema(
table.schema(), updateCompatibilityVersion))
.setUpdateCompatibilityVersion(updateCompatibilityVersion)
.setFromSnapshotInclusive(getFromSnapshot())
.setToSnapshot(getToSnapshot())
.setFromTimestamp(getFromTimestamp())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ public org.apache.iceberg.Schema recordIdSchema() {

public Schema rowIdBeamSchema() {
if (cachedRowIdBeamSchema == null) {
cachedRowIdBeamSchema = icebergSchemaToBeamSchema(recordIdSchema());
cachedRowIdBeamSchema =
icebergSchemaToBeamSchema(recordIdSchema(), getUpdateCompatibilityVersion());
}
return cachedRowIdBeamSchema;
}
Expand Down Expand Up @@ -237,6 +238,9 @@ public Expression getFilter() {
@Pure
public abstract boolean getUseCdc();

@Pure
public abstract @Nullable String getUpdateCompatibilityVersion();

@Pure
public abstract @Nullable Boolean getStreaming();

Expand Down Expand Up @@ -335,6 +339,9 @@ public Builder setTableIdentifier(String... names) {

public abstract Builder setUseCdc(boolean useCdc);

public abstract Builder setUpdateCompatibilityVersion(
@Nullable String updateCompatibilityVersion);

public abstract Builder setStreaming(@Nullable Boolean streaming);

public abstract Builder setPollInterval(@Nullable Duration pollInterval);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Will using timestamp logical type break xlang python reads?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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;
Expand All @@ -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:
Expand All @@ -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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

return IcebergUtils.icebergSchemaToBeamSchema(table.schema());

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();
}
Expand Down Expand Up @@ -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();
}
Comment thread
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ public PCollection<Row> expand(PBegin input) {
.setCoder(KvCoder.of(ReadTaskDescriptor.getCoder(), ReadTask.getCoder()))
.apply(Redistribute.arbitrarily())
.apply("Read Rows From Tasks", ParDo.of(new ReadFromTasks(scanConfig)))
.setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(scanConfig.getProjectedSchema()));
.setRowSchema(
IcebergUtils.icebergSchemaToBeamSchema(
scanConfig.getProjectedSchema(), scanConfig.getUpdateCompatibilityVersion()));
}

/** Continuously watches for new snapshots. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ public void process(
return;
}
FileScanTask task = fileScanTasks.get((int) l);
Schema beamSchema = IcebergUtils.icebergSchemaToBeamSchema(scanConfig.getProjectedSchema());
Schema beamSchema =
IcebergUtils.icebergSchemaToBeamSchema(
scanConfig.getProjectedSchema(), scanConfig.getUpdateCompatibilityVersion());
try (CloseableIterable<Record> reader = ReadUtils.createReader(task, table, scanConfig)) {

for (Record record : reader) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ public void populateDisplayData(DisplayData.Builder builder) {

@Override
public Coder<Row> getOutputCoder() {
return RowCoder.of(IcebergUtils.icebergSchemaToBeamSchema(scanConfig.getProjectedSchema()));
return RowCoder.of(
IcebergUtils.icebergSchemaToBeamSchema(
scanConfig.getProjectedSchema(), scanConfig.getUpdateCompatibilityVersion()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ class ScanTaskReader extends BoundedSource.BoundedReader<Row> {

public ScanTaskReader(ScanTaskSource source) {
this.source = source;
this.beamSchema = icebergSchemaToBeamSchema(source.getScanConfig().getProjectedSchema());
this.beamSchema =
icebergSchemaToBeamSchema(
source.getScanConfig().getProjectedSchema(),
source.getScanConfig().getUpdateCompatibilityVersion());
}

@Override
Expand Down
Loading
Loading