Skip to content
Closed
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 @@ -85,6 +85,11 @@ public abstract class TimestampConverter<R extends ConnectRecord<R>> implements

private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

public static final Schema OPTIONAL_DATE_SCHEMA = org.apache.kafka.connect.data.Date.builder().optional().schema();
public static final Schema OPTIONAL_TIMESTAMP_SCHEMA = Timestamp.builder().optional().schema();
public static final Schema OPTIONAL_TIME_SCHEMA = Time.builder().optional().schema();


private interface TimestampTranslator {
/**
* Convert from the type-specific format to the universal java.util.Date format
Expand All @@ -94,7 +99,7 @@ private interface TimestampTranslator {
/**
* Get the schema for this format.
*/
Schema typeSchema();
Schema typeSchema(boolean isOptional);

/**
* Convert from the universal java.util.Date format to the type-specific format
Expand All @@ -118,8 +123,8 @@ public Date toRaw(Config config, Object orig) {
}

@Override
public Schema typeSchema() {
return Schema.STRING_SCHEMA;
public Schema typeSchema(boolean isOptional) {
return isOptional ? Schema.OPTIONAL_STRING_SCHEMA : Schema.STRING_SCHEMA;
}

@Override
Expand All @@ -139,8 +144,8 @@ public Date toRaw(Config config, Object orig) {
}

@Override
public Schema typeSchema() {
return Schema.INT64_SCHEMA;
public Schema typeSchema(boolean isOptional) {
return isOptional ? Schema.OPTIONAL_INT64_SCHEMA : Schema.INT64_SCHEMA;
}

@Override
Expand All @@ -159,8 +164,8 @@ public Date toRaw(Config config, Object orig) {
}

@Override
public Schema typeSchema() {
return org.apache.kafka.connect.data.Date.SCHEMA;
public Schema typeSchema(boolean isOptional) {
return isOptional ? OPTIONAL_DATE_SCHEMA : org.apache.kafka.connect.data.Date.SCHEMA;
}

@Override
Expand All @@ -185,8 +190,8 @@ public Date toRaw(Config config, Object orig) {
}

@Override
public Schema typeSchema() {
return Time.SCHEMA;
public Schema typeSchema(boolean isOptional) {
return isOptional ? OPTIONAL_TIME_SCHEMA : Time.SCHEMA;
}

@Override
Expand All @@ -212,8 +217,8 @@ public Date toRaw(Config config, Object orig) {
}

@Override
public Schema typeSchema() {
return Timestamp.SCHEMA;
public Schema typeSchema(boolean isOptional) {
return isOptional ? OPTIONAL_TIMESTAMP_SCHEMA : Timestamp.SCHEMA;
}

@Override
Expand Down Expand Up @@ -245,7 +250,7 @@ public void configure(Map<String, ?> configs) {
final String field = simpleConfig.getString(FIELD_CONFIG);
final String type = simpleConfig.getString(TARGET_TYPE_CONFIG);
String formatPattern = simpleConfig.getString(FORMAT_CONFIG);
schemaUpdateCache = new SynchronizedCache<>(new LRUCache<Schema, Schema>(16));
schemaUpdateCache = new SynchronizedCache<>(new LRUCache<>(16));

if (!VALID_TYPES.contains(type)) {
throw new ConfigException("Unknown timestamp type in TimestampConverter: " + type + ". Valid values are "
Expand Down Expand Up @@ -326,38 +331,47 @@ protected R newRecord(R record, Schema updatedSchema, Object updatedValue) {
protected abstract R newRecord(R record, Schema updatedSchema, Object updatedValue);

private R applyWithSchema(R record) {
final Schema schema = operatingSchema(record);
final Schema originalSchema = operatingSchema(record);
final Object rawValue = operatingValue(record);
// Entire value is timestamp
if (config.field.isEmpty()) {
Object value = operatingValue(record);
// New schema is determined by the requested target timestamp type
Schema updatedSchema = TRANSLATORS.get(config.type).typeSchema();
return newRecord(record, updatedSchema, convertTimestamp(value, timestampTypeFromSchema(schema)));
} else {
final Struct value = requireStruct(operatingValue(record), PURPOSE);
Schema updatedSchema = schemaUpdateCache.get(value.schema());
if (updatedSchema == null) {
SchemaBuilder builder = SchemaUtil.copySchemaBasics(schema, SchemaBuilder.struct());
for (Field field : schema.fields()) {
if (field.name().equals(config.field)) {
builder.field(field.name(), TRANSLATORS.get(config.type).typeSchema());
} else {
builder.field(field.name(), field.schema());
}
}
if (schema.isOptional())
builder.optional();
if (schema.defaultValue() != null) {
Struct updatedDefaultValue = applyValueWithSchema((Struct) schema.defaultValue(), builder);
builder.defaultValue(updatedDefaultValue);
}
Schema updatedSchema = TRANSLATORS.get(config.type).typeSchema(originalSchema.isOptional());
return newRecord(record, updatedSchema, convertTimestamp(rawValue, timestampTypeFromSchema(originalSchema)));
}
// Value is Struct, only its single field should be converted
if (rawValue == null) {
Schema updatedSchema = updateSchema(originalSchema);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not using a cached schema, which means that a new schema has to be built for every record that uses some original schema, A, but that also has a null timestamp field. Wouldn't it make sense to cache this updated schema? And, since the updates schema is not a function of the record, we should be able to look for the updated schema in the cache or update the schema and cache it before dealing with the raw value.

This might also mean that there are fewer lines changed, especially if we keep the else block and previous indentation (as mentioned before, even though strictly speaking the else is unnecessary), which might help us ensure that the behavior doesn't change for the non-null timestamps.

schemaUpdateCache.put(originalSchema, updatedSchema);
return newRecord(record, updatedSchema, null);
}

updatedSchema = builder.build();
schemaUpdateCache.put(schema, updatedSchema);
}
final Struct struct = requireStruct(rawValue, PURPOSE);
Schema updatedSchema = schemaUpdateCache.get(struct.schema());
if (updatedSchema == null) {
updatedSchema = updateSchema(originalSchema);
schemaUpdateCache.put(originalSchema, updatedSchema);
}
Struct updatedValue = applyValueWithSchema(struct, updatedSchema);
return newRecord(record, updatedSchema, updatedValue);
}

Struct updatedValue = applyValueWithSchema(value, updatedSchema);
return newRecord(record, updatedSchema, updatedValue);
private Schema updateSchema(Schema originalSchema) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Again, if we're only going to call this from one location (see previous comment), maybe it's better to not pull this logic out to minimize changes.

SchemaBuilder builder = SchemaUtil.copySchemaBasics(originalSchema, SchemaBuilder.struct());
for (Field field : originalSchema.fields()) {
if (field.name().equals(config.field)) {
builder.field(field.name(), TRANSLATORS.get(config.type).typeSchema(field.schema().isOptional()));
} else {
builder.field(field.name(), field.schema());
}
}
if (originalSchema.isOptional())
builder.optional();
if (originalSchema.defaultValue() != null) {
Struct updatedDefaultValue = applyValueWithSchema((Struct) originalSchema.defaultValue(), builder);
builder.defaultValue(updatedDefaultValue);
}
return builder.build();
}

private Struct applyValueWithSchema(Struct value, Schema updatedSchema) {
Expand All @@ -375,15 +389,15 @@ private Struct applyValueWithSchema(Struct value, Schema updatedSchema) {
}

private R applySchemaless(R record) {
if (config.field.isEmpty()) {
Object value = operatingValue(record);
return newRecord(record, null, convertTimestamp(value));
} else {
final Map<String, Object> value = requireMap(operatingValue(record), PURPOSE);
final HashMap<String, Object> updatedValue = new HashMap<>(value);
updatedValue.put(config.field, convertTimestamp(value.get(config.field)));
return newRecord(record, null, updatedValue);
Object rawValue = operatingValue(record);
if (rawValue == null || config.field.isEmpty()) {
Object value = convertTimestamp(rawValue);
return newRecord(record, null, value);
}
final Map<String, Object> value = requireMap(rawValue, PURPOSE);
final HashMap<String, Object> updatedValue = new HashMap<>(value);
updatedValue.put(config.field, convertTimestamp(value.get(config.field)));
return newRecord(record, null, updatedValue);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I understand that the if block always returns, and so an else block is unnecessary. But because we likely want to backport this, and because we'd like to minimize the changes to help ensure the behavior remains the same for non-null timestamps, it's probably worth it to not remove the else block and keep the original indentation.

}

/**
Expand Down Expand Up @@ -424,11 +438,14 @@ private String inferTimestampType(Object timestamp) {

/**
* Convert the given timestamp to the target timestamp format.
* @param timestamp the input timestamp
* @param timestamp the input timestamp, may be null
* @param timestampFormat the format of the timestamp, or null if the format should be inferred
* @return the converted timestamp
*/
private Object convertTimestamp(Object timestamp, String timestampFormat) {
if (timestamp == null) {
return null;
}
if (timestampFormat == null) {
timestampFormat = inferTimestampType(timestamp);
}
Expand Down
Loading