Skip to content

feat(kafka): support Avro-encoded message key via schema registry#1201

Merged
yuzifeng1984 merged 8 commits into
timeplus-io:developfrom
burnerlee:feat/kafka-avro-message-key
Jul 1, 2026
Merged

feat(kafka): support Avro-encoded message key via schema registry#1201
yuzifeng1984 merged 8 commits into
timeplus-io:developfrom
burnerlee:feat/kafka-avro-message-key

Conversation

@burnerlee

@burnerlee burnerlee commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

PR checklist:

  • Did you run ClangFormat? — new code follows surrounding style; all edited files are in Proton-specific paths
  • Did you separate headers to a different section in existing community code base? — new includes added in alphabetical order within existing include blocks
  • Did you surround proton: starts/ends for new code in existing community code base? — not applicable, all changes are in src/Storages/ExternalStream/Kafka/ which is Proton-specific code

Please write user-readable short description of the changes:

Adds a new message_key_schema_name setting to Kafka external streams. When set, Proton decodes the Kafka message key using the Confluent Avro wire format (5-byte wire prefix: magic byte + 4-byte schema ID) and emits the decoded value as a JSON string into _tp_message_key.

Motivation: Producers that use Confluent Schema Registry commonly Avro-encode message keys, not just values. Reading _tp_message_key on such topics previously returned raw bytes.

Behaviour:

  • message_key_schema_name names an Avro schema subject in the schema registry
  • Requires kafka_schema_registry_url (validated at CREATE EXTERNAL STREAM time)
  • _tp_message_key column must be String or Nullable(String) in Avro key mode (validated at CREATE time)
  • Schema lookup is cached via the existing KafkaSchemaRegistryForAvro mechanism
  • On read: Avro binary key is decoded into a GenericDatum, then re-encoded as Avro JSON (not plain JSON — union types use Avro union encoding, bytes fields are base64). The result is stored as a string in _tp_message_key
  • On write: throws NOT_IMPLEMENTED if message_key_schema_name is set — Avro-encoding keys on write is not yet supported. Plain string keys via _tp_message_key without message_key_schema_name continue to work
  • Empty key (key_len == 0) returns NULL for Nullable(String) columns, empty string for String columns

Example:

CREATE EXTERNAL STREAM orders (
    _tp_message_key String,
    payload         String
) SETTINGS
    type                       = 'kafka',
    brokers                    = 'localhost:9092',
    topic                      = 'orders',
    data_format                = 'JSONEachRow',
    kafka_schema_registry_url  = 'http://localhost:8081',
    message_key_schema_name    = 'orders-key';

SELECT _tp_message_key FROM orders;
-- Returns Avro JSON string, e.g. {"user_id": 42, "tenant": "acme"}

Closes #915

Added a new setting `message_key_schema_name` to Kafka external streams.
When set, the Kafka message key is decoded using the Confluent Avro wire
format (5-byte prefix: 0x00 magic + 4-byte schema ID) against the named
subject in the configured schema registry, and the result is emitted as
a JSON string into the `_tp_message_key` virtual column.

Changes:
- ExternalStreamSettings.h: add `message_key_schema_name` String setting
- Kafka.cpp: validate setting requires `kafka_schema_registry_url`; restrict
  `_tp_message_key` column to String/FixedString when Avro key mode is active;
  build and store a `KafkaSchemaRegistryForAvro` for the key subject in the
  constructor; pass registry and subject name to each KafkaSource
- Kafka.h: add `avro_key_schema_registry` member; include KafkaSchemaRegistryForAvro
- KafkaSource.h: add `avro_key_schema_registry` and `avro_key_schema_subject`
  members; add constructor params; declare `decodeAvroKey()` private method
- KafkaSource.cpp: implement `decodeAvroKey()` using AvroDeserializer against a
  single String column header; branch both the typed (String/FixedString switch
  case) and untyped (RESERVED_MESSAGE_KEY fallback) virtual column paths to call
  `decodeAvroKey()` when the registry is present, raw bytes otherwise
@chenziliang chenziliang requested a review from yuzifeng1984 June 21, 2026 17:10
Comment thread src/Storages/ExternalStream/Kafka/KafkaSource.cpp Outdated
Comment thread src/Storages/ExternalStream/Kafka/KafkaSource.h Outdated
Comment thread src/Storages/ExternalStream/Kafka/KafkaSource.cpp Outdated
@yuzifeng1984

Copy link
Copy Markdown
Collaborator

Need implement KafkaSink related things to encode message key or throw an exception on write if currently not supported.

Comment thread src/Storages/ExternalStream/Kafka/Kafka.cpp Outdated
- Return empty string (not decode) when key_len == 0 and column is non-nullable
- Remove unused avro_key_schema_subject param/member — schema ID comes from key bytes
- Use angle-bracket includes for Formats headers in Kafka.cpp
Replace broken AvroDeserializer approach (which mapped Avro fields by
column name and always returned empty string) with GenericDatum round-trip:
binary Avro -> GenericDatum -> Avro JSON string.

Also fix include style in KafkaSource.h (angle brackets).
Writing _tp_message_key as Confluent Avro binary is not yet implemented —
only decoding on read is supported. Throw a clear error on write with
guidance to use a plain string key instead.
@burnerlee

Copy link
Copy Markdown
Contributor Author

@yuzifeng1984 how can I test my changes in local - can you share your testing setup?

@yuzifeng1984

yuzifeng1984 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Hi @burnerlee, I use Redpanda for local testing. You may use its web UI to configure Kafka things including schema
https://docs.redpanda.com/labs/docker-compose/single-broker/

@burnerlee

Copy link
Copy Markdown
Contributor Author

Hi @burnerlee, I use Redpanda for local testing. You may use its web UI to configure Kafka things including schema

https://docs.redpanda.com/labs/docker-compose/single-broker/

Thanks

@yuzifeng1984

Copy link
Copy Markdown
Collaborator

@burnerlee The PR looks good.

Can you also update the the kafka_schema_registry_url setting? Just in case it is only used for key but not message body.

Blocker — Format validation conflict makes the documented use case impossible

File: src/Storages/ExternalStream/Kafka/Kafka.cpp:264-279 (verifySettings)

The pre-existing check unconditionally rejects any kafka_schema_registry_url value when data_format is not 'Avro' or 'ProtobufSingle':

if (!new_settings->kafka_schema_registry_url.value.empty())
{
const auto & format = new_settings->data_format.value;
const bool format_supported = format == "ProtobufSingle" || format == "Avro";
if (!format_supported)
throw Exception(...);
}

The PR's own example in the description uses data_format = 'JSONEachRow' with kafka_schema_registry_url set — exactly the case where a user wants Avro-encoded keys but JSON-encoded values. This CREATE EXTERNAL STREAM would throw at creation time, before reaching any of the new code.

Fix: Condition the format check on whether the registry is used for value decoding. When message_key_schema_name is non-empty and data_format is not itself Avro/Protobuf, the registry is used solely for key decoding and the format
constraint should not apply. For example:

const bool using_registry_for_value = new_settings->message_key_schema_name.value.empty();
if (!new_settings->kafka_schema_registry_url.value.empty() && using_registry_for_value)
{
// existing format check ...
}

@burnerlee

Copy link
Copy Markdown
Contributor Author

@burnerlee The PR looks good.

Can you also update the the kafka_schema_registry_url setting? Just in case it is only used for key but not message body.

Blocker — Format validation conflict makes the documented use case impossible

File: src/Storages/ExternalStream/Kafka/Kafka.cpp:264-279 (verifySettings)

The pre-existing check unconditionally rejects any kafka_schema_registry_url value when data_format is not 'Avro' or 'ProtobufSingle':

if (!new_settings->kafka_schema_registry_url.value.empty())

{

const auto & format = new_settings->data_format.value;

const bool format_supported = format == "ProtobufSingle" || format == "Avro";

if (!format_supported)

  throw Exception(...);

}

The PR's own example in the description uses data_format = 'JSONEachRow' with kafka_schema_registry_url set — exactly the case where a user wants Avro-encoded keys but JSON-encoded values. This CREATE EXTERNAL STREAM would throw at creation time, before reaching any of the new code.

Fix: Condition the format check on whether the registry is used for value decoding. When message_key_schema_name is non-empty and data_format is not itself Avro/Protobuf, the registry is used solely for key decoding and the format

constraint should not apply. For example:

const bool using_registry_for_value = new_settings->message_key_schema_name.value.empty();

if (!new_settings->kafka_schema_registry_url.value.empty() && using_registry_for_value)

{

  // existing format check ...

}

Sure let me update

Previously `kafka_schema_registry_url` required the message body format
to be Avro or ProtobufSingle. With Avro-encoded keys, users may want a
non-Avro body format (e.g. JSONEachRow) but still need the schema
registry for the key. Allow the registry URL when either the body format
needs it or `message_key_schema_name` is set.

Also fix a latent bug where the post-update validation used the old
`settings` member instead of `new_settings` when checking
`message_key_schema_name`.
@burnerlee burnerlee force-pushed the feat/kafka-avro-message-key branch from 3c0661d to 6cde6ce Compare June 30, 2026 08:41
@burnerlee

Copy link
Copy Markdown
Contributor Author

@yuzifeng1984 updated. Could you check?

@yuzifeng1984 yuzifeng1984 merged commit b73dba7 into timeplus-io:develop Jul 1, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kafka External Stream to support read the Avro-encoded message key

2 participants