Skip to content

[adapters] Parse Debezium logical types in the Avro input connector#6620

Merged
ryzhyk merged 1 commit into
mainfrom
debezium-avro
Jul 14, 2026
Merged

[adapters] Parse Debezium logical types in the Avro input connector#6620
ryzhyk merged 1 commit into
mainfrom
debezium-avro

Conversation

@ryzhyk

@ryzhyk ryzhyk commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Debezium encodes temporal and variable-scale-decimal columns in forms that differ from the standard Avro types, tagging them with a connect.name attribute rather than a native Avro logical type.

This commit adds support for several such types:

  • Temporal: Time, MicroTime, NanoTime, ZonedTime, Timestamp, MicroTimestamp, NanoTimestamp, ZonedTimestamp, and the org.apache.kafka.connect.data.* aliases from time.precision.mode=connect, parsed into TIME/TIMESTAMP. Date already deserialized correctly as a plain day count and needs no conversion.
  • io.debezium.data.VariableScaleDecimal, a {scale, value} record used for NUMERIC/DECIMAL columns without a fixed scale, parsed into DECIMAL.

The implementation could have been simpler if it didn't have to fight with the apache-avro crate, which drops attributes on primitive schemas, so these annotations were lost.

Describe Manual Test Plan

Checklist

  • Unit tests added/updated
  • Integration tests added/updated
  • Documentation updated
  • Changelog updated

Breaking Changes?

Mark if you think the answer is yes for any of these components:

Describe Incompatible Changes

@ryzhyk ryzhyk added connectors Issues related to the adapters/connectors crate user-reported Reported by a user or customer labels Jul 11, 2026

@mythical-fred mythical-fred left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice separation: the generic Coercion layer in coercion.rs stays source-agnostic and delegates every Debezium-specific bit (temporal enum, VariableScaleDecimal parsing) to debezium.rs. Tests cover the wire format for every temporal type (millis/micros/nanos + ISO zoned + org.apache.kafka.connect.data.*), nullable timestamp, wraparound zoned time across midnight, VariableScaleDecimal with a nullable column and a by-name reference, plus a schema-mismatch rejection path. hoist_coercible_types correctly leaves named types alone (they preserve attributes natively) and only rewrites primitives. The time.precision.mode=connect aliases and Date (which stays a plain day count) are handled the way the Debezium docs describe them. Signed-off-by, no AI trailers. Approve.

A couple of small non-blocking observations:

  1. Coercion::validate treats every timestamp coercion as compatible with either SqlType::Timestamp or SqlType::TimestampTz. That's semantically fine (a plain io.debezium.time.Timestamp is UTC epoch millis, so loading it into a TIMESTAMP WITH TIME ZONE column reads it as UTC), but worth a docstring note on is_timestamp so future readers don't puzzle over it — especially since ZonedTime maps to a non-zoned TIME column intentionally.

  2. variable_scale_decimal_to_string doesn't reject negative scale. The Debezium spec constrains it to >= 0, and BigDecimal::new(unscaled, scale as i64) will happily interpret a negative scale as a multiplier. Not a real-world concern — Debezium won't emit that — but a scale >= 0 check would fail loudly on a corrupted schema-registry payload.

  3. The println!("Debezium Avro schema: {schema_str}") in debezium_avro_schema (the helper that only exists in the test file, so no production impact) is debug output that survives PR merges; consider dropping or eprintln!-ing behind a flag.

Comment thread crates/adapters/src/format/avro/coercion.rs Outdated
Str(String),
}

impl Coercion {

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 file is in a directory called Avro, yet it talks about Kafka.
I can't make a general picture of the code organization.
Moreover, the function name does not mention Kafka at all.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kafka Connect (not Kafka) has its own Avro extensions, which is what this PR supports. I agree this is all in bad taste, but we don't control the ecosystem.

/// Rewrite an Avro schema so that coercion annotations on primitive types
/// survive parsing by `apache-avro`.
///
/// `apache-avro` discards attributes on primitive schemas, so a `connect.name`

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.

Is this a bug in apache-avro? Can we contribute this fix?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

apache-avro is not a well designed crate. I consider this a design bug.

Can we contribute this fix?
Ideally, yes, but this is a time-sensitive feature.

/// Return an object schema's `connect.name` if it is a recognized coercion on a
/// primitive type. Named types keep their attributes natively, so they are not
/// hoisted.
fn object_hoistable_connect_name(obj: &Map<String, JsonValue>) -> Option<String> {

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 making me dizzy
who designed this stuff?

/// | `io.debezium.time.Timestamp` | `long` milliseconds since epoch | `TIMESTAMP` |
/// | `io.debezium.time.MicroTimestamp` | `long` microseconds since epoch | `TIMESTAMP` |
/// | `io.debezium.time.NanoTimestamp` | `long` nanoseconds since epoch | `TIMESTAMP` |
/// | `io.debezium.time.ZonedTimestamp` | ISO-8601 `string` | `TIMESTAMP` |

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.

none of these produces a TIME ZONE?
Note that we don't support TIME WITH TIME ZONE (which I don't think makes sense, but many SQL dialects do), but we do support TIMESTAMP WITH TIME ZONE

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ZonedTimestamp actually deserialized to Timestamp With Timezone. I'll update the docs.

.ok_or_else(|| "VariableScaleDecimal record is missing the 'value' field".to_string())?;

let unscaled = BigInt::from_signed_bytes_be(unscaled);
Ok(BigDecimal::new(unscaled, scale as i64).to_string())

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.

Can't we use our own Decimal? Or is the precision arbitrary?

Comment thread crates/adapters/src/format/avro/debezium.rs Outdated
Comment thread crates/adapters/src/format/avro/debezium.rs

/// Debezium message Avro schema with the specified inner record schema.
fn debezium_avro_schema(value_schema: &str, value_type_name: &str) -> AvroSchema {
let schema_str = debezium_avro_schema_str(value_schema, value_type_name);

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.

Why so many empty lines?

Comment thread crates/adapters/src/format/avro/test.rs
Debezium encodes temporal and variable-scale-decimal columns in forms that
differ from the standard Avro types, tagging them with a `connect.name`
attribute rather than a native Avro logical type.

This commit adds support for several such types:
- Temporal: Time, MicroTime, NanoTime, ZonedTime, Timestamp,
  MicroTimestamp, NanoTimestamp, ZonedTimestamp, and the
  org.apache.kafka.connect.data.* aliases from time.precision.mode=connect,
  parsed into TIME/TIMESTAMP. Date already deserialized correctly as a
  plain day count and needs no conversion.
- io.debezium.data.VariableScaleDecimal, a {scale, value} record used for
  NUMERIC/DECIMAL columns without a fixed scale, parsed into DECIMAL.

The implementation could have been simpler if it didn't have to fight with
the `apache-avro` crate, which drops attributes on primitive schemas, so these
annotations were lost.

Signed-off-by: Leonid Ryzhyk <ryzhyk@gmail.com>
@ryzhyk
ryzhyk enabled auto-merge July 14, 2026 03:34
@ryzhyk
ryzhyk added this pull request to the merge queue Jul 14, 2026
Merged via the queue into main with commit 60f5342 Jul 14, 2026
1 check passed
@ryzhyk
ryzhyk deleted the debezium-avro branch July 14, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

connectors Issues related to the adapters/connectors crate user-reported Reported by a user or customer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants