Skip to content

Kafka Connect converter - DO NOT MERGE#4548

Open
prakhargarg105 wants to merge 73 commits into
redpanda-data:mainfrom
prakhargarg105:kafka_converter
Open

Kafka Connect converter - DO NOT MERGE#4548
prakhargarg105 wants to merge 73 commits into
redpanda-data:mainfrom
prakhargarg105:kafka_converter

Conversation

@prakhargarg105

Copy link
Copy Markdown
Contributor

No description provided.

prakhargarg105 and others added 30 commits June 22, 2026 11:03
Approved brainstorming design for a deterministic Go conversion engine
(internal/connect_converter) plus thin CLI. v1 covers mirror, Snowflake,
BigQuery, S3, GCS, and JDBC connectors, common converters and SMTs, with
best-effort YAML output and inline # TODO markers for unmapped content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15-task TDD plan for internal/connect_converter engine + CLI convert
subcommand. Covers parser, MapCtx, yaml builders, registry/fallback,
assemble/render, six connector mappers, converters, SMTs, golden tests,
and CLI wiring. Each task validates output via the benthos linter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eton

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… assemble invariant note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sistently

Every required snowflake_streaming field now emits a TODO stub *and* calls
ctx.Warn when the corresponding KC key is absent, fixing the silent drop of
`user` and the missing Warn calls on `account`/`role`/`database`/`schema`.
Optional fields (private_key, private_key_file) are mapped-only with no stub.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…outer)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename unused interface-method params to _, add const-block doc comment,
apply SplitSeq modernize fix, and use assert.Empty for blank-string checks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ld value coercion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Usage (CLI + getting the Kafka Connect JSON via the Connect REST API),
supported connectors/converters/SMTs, output model, v1 limitations,
architecture, how to add a mapper, and the linter-backed test approach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds design for ExtractField, HoistField, MaskField, Cast,
TimestampConverter, ValueToKey SMT mappers. Purely additive to the
existing SMTMapper/registry pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…imestampConverter/ValueToKey SMTs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
prakhargarg105 and others added 29 commits June 23, 2026 08:57
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…class

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or aliases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Strip // line comments (JSONC-style) before JSON unmarshaling so users
can paste documented configs. Stripping is string-aware: // inside
a JSON string value (e.g. jdbc:postgresql://host/db URLs) is preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nk connectors

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Maps Debezium postgres/mysql/sqlserver/oracle connectors to the
corresponding *_cdc inputs (JDBC source stays sql_select). MongoDB has
no RPCN equivalent and stays unmapped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ine; oracle dsn + DRY tidy

- Stop consuming topic.prefix and database.server.name in all four Debezium
  mappers (postgres/mysql/sqlserver/oracle) so they fall through to the
  Unmapped() sweep and appear as inline YAML TODO comments, matching the
  JDBC convention.
- Oracle: annotate connection_string with a TODO when service name is empty.
- MySQL: remove spurious "TODO: " prefix from database.server.id warn message.
- SQL Server + Oracle: replace inlined table-emit logic with debeziumTables()
  (same source key table.include.list, same CSV split, same field name).
- Regenerate debezium_postgres and debezium_mysql goldens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t, insertfield, replacefield, regexrouter)

Gap 1: TimestampConverter now reads the 'format' property and translates
Joda/SimpleDateFormat patterns to Go layouts via jodaToGoLayout(); falls back
to RFC3339 + TODO warn only when 'format' is absent.

Gap 3: castMethod maps int8/16/32/64 to .int64() instead of .number() so
integer columns remain integers.

Gap 7: Cast with a bare type in 'spec' (no colon) is now treated as a
whole-value cast: emits 'root = this.<method>()'.

Gap 5: InsertField now handles topic.field (@kafka_topic), timestamp.field
(kafka_timestamp_ms metadata), partition.field, and offset.field in addition
to static.field/static.value; multiple fields emit one assignment line each.

Gap 6: ReplaceField now supports 'blacklist' as a legacy alias for 'exclude'
and 'include'/'whitelist' projection (root = {} + root.<f> = this.<f>).

Gap 18: ReplaceField passthrough stub comment now accurately names which
properties were looked for instead of hard-coding "include/whitelist semantics".

Gap 17: RegexRouter changed from metadata("kafka_topic").re_replace_all(...) to
@kafka_topic.re_replace_all(...) shorthand; smt_chain golden regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Matches/HasHeaderKey predicates

Adds smt_filter.go with full support for Kafka Connect's Filter SMT and the
predicates framework. The mapper resolves the named predicate type, builds
the corresponding Bloblang conditional-delete expression, and consumes all
predicates.* keys so they do not surface as unmapped-field warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…DC; recognize ExtractNewRecordState

Gap 2: consumeDebeziumCommon now marks value.converter/key.converter (and their
sub-keys) consumed so mapConverters() skips emitting schema_registry_decode —
*_cdc inputs read structured rows directly from WAL/binlog, not Avro/JSON bytes.

Gap 9: new smt_debezium.go registers ExtractNewRecordState as a no-op SMT with
an informational warning instead of the generic "unsupported — map manually" stub.

debezium_postgres golden input updated to include Avro value.converter to
exercise and lock in the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add injectUserInfo helper that inserts user[:password]@ after the first
"://" in the DSN. driverAndDSN now reads and consumes connection.user and
connection.password so they no longer surface as unmapped-field warnings.
A secret-hygiene TODO comment is appended to the DSN when a password is
inlined, matching the Debezium pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…view)

Replace double-# TODO concatenation with a single clean semicolon-separated
sentence; add special-character caveat on injectUserInfo; pin comment shape
in tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er, compression, time partitioner

- objectFormatExtension: fall back to Aiven format.output.type
  (json/jsonl/csv/parquet/avro) when format.class is absent; consume both keys
- mapBatching: take countKeys []string (first-present wins, all consumed); S3
  and GCS pass [flush.size, file.max.records] so Aiven file.max.records maps to
  batching.count
- file.compression.type: append .gz/.snappy/.zst to the object path extension
  plus an inline TODO to add a matching compress processor (connector layer
  cannot inject one)
- TimeBasedPartitioner: translate path.format Joda pattern into a time-bucketed
  path prefix via jodaToGoLayoutForPath, reusing jodaToGoLayout, emitting
  ${! metadata("kafka_timestamp_ms").number().ts_format(...) } interpolations
  (verified against the benthos linter); consume partitioner.class,
  path.format, partition.duration.ms, locale, timezone; best-effort TODO when
  path.format is absent/untranslatable
- update remaining mapBatching callers (bigquery, jdbc) to the slice signature

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…regex topics, JSON-Schema converter

Gap 13: emit Bloblang match expression over @kafka_topic for multi-entry
snowflake.topic2table.map (table field is InterpolatedString; single entry
stays literal). Gap 14: add regexp_topics: true to mirror input so Java
regex patterns like orders.* are honoured. Gap 10: register
io.confluent.connect.json.JsonSchemaConverter as schemaRegistryConverter
(same as Avro/Protobuf).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…for sinks

When a sink connector specifies topics.regex instead of a literal topics
CSV, sinkInputFromTopics now builds a redpanda input with the regex pattern
in the topics list and regexp_topics: true, matching how the mirror connector
handles regex-based topic selection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ms→seconds) (review)

Switch jodaToGoLayoutForPath from kafka_timestamp_ms (epoch ms) to
kafka_timestamp_unix (epoch seconds). Bloblang ts_format interprets a
bare number as epoch SECONDS, so feeding it epoch milliseconds produced
wildly wrong years (~55000 instead of ~2024). kafka_timestamp_unix is
set by record.Timestamp.Unix() in the franz reader and is genuinely
epoch seconds. Add hazard comments at the doc and emission site. Pin the
corrected expression in TimeBasedPartitioner tests for both S3 and GCS
and assert the old broken form is not emitted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ractTopic, ByLogicalTableRouter, etc.)

Add smt_routing.go with five topic-routing SMT mappers:
- org.apache.kafka.connect.transforms.TimestampRouter
- io.confluent.connect.transforms.MessageTimestampRouter (with message.timestamp.keys TODO/warn)
- io.confluent.connect.transforms.ExtractTopic$Value/$Key/$Header
- io.confluent.connect.cloud.transforms.TopicRegexRouter (delegates to regexRouterSMT)
- io.debezium.transforms.ByLogicalTableRouter (with key.field.* TODO/warn)

All emit `meta kafka_topic = <expr>` via mappingProc. TimestampRouter/MessageTimestampRouter
build Bloblang concat from topic.format placeholders using kafka_timestamp_unix + ts_format().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…HeaderFrom, HeaderToValue)

Adds smt_headers.go with four header-manipulation SMT mappers:
- InsertHeader → meta <name> = "<literal>"
- DropHeaders → meta <name> = deleted() per header
- HeaderFrom$Value/$Key → meta <header> = this.<field> (copy); also root.<field> = deleted() on move
- io.debezium.transforms.HeaderToValue → root.<field> = metadata("<header>") (copy/move inverse)

Quoted meta key helper added for hyphenated header names (Bloblang parses bare x-y as subtraction).
Mismatched fields/headers lengths emit a TODO + ctx.Warn for the common prefix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tten, GzipDecompress, Drop, TombstoneHandler, FromXml) + Confluent ReplaceField alias

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ox, scripting, partitions, mongodb, geometry) with tailored guidance

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rocessor

When a non-Filter SMT carries transforms.<alias>.predicate (and optional
negate), wrap its processor nodes in a benthos switch processor so the
transform applies only to records matching the predicate — matching KC
conditional-transform semantics generically.

Refactored resolvePredicate → resolvePredicateExpr (shared helper); both
filterSMT and the new applyPredicateGating in convert.go call it.
consumeAllPredicateKeys is called by applyPredicateGating so predicate
keys never surface as unmapped fields. Filter SMT (the only KC built-in
that handles predicates internally) is exempt from double-gating.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…loblang paths (review)

Add fieldPath(base, name) helper in smt_helpers.go that splits KC field
names on "." and %q-quotes any segment that is not a safe Bloblang
identifier (hyphens, leading digits, etc.). Move isSafeIdentifier there
from smt_headers.go (one canonical location). Apply fieldPath to every
this.<field> / root.<field> interpolation across all SMT mappers:
smt_cast, smt_extractfield, smt_headers, smt_insertfield, smt_maskfield,
smt_replacefield, smt_routing, smt_timestampconverter, smt_valuetokey.

Safe-identifier names remain bare, so all 18 goldens are unchanged.
New tests in smt_fieldpath_test.go cover the helper unit and representative
SMTs (ExtractField, InsertField, Cast, ReplaceField, HeaderFrom, ValueToKey)
with a hyphenated field "event-type" verifying assertValidRPCN + quoted form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blind per-connector sweep (10 real-world KC configs × 11 connectors = 110,
authored without reference to converter support) found 6 lint-breaking configs
and a set of semantic/noise gaps. All are now fixed; re-running the sweep gives
0 convert errors and 0 lint failures.

Lint-breakers:
- G1: recognize Oracle JDBC URLs → `oracle` driver + `oracle://` DSN (was
  driver:"" which fails the sql_* driver enum); thin service & SID forms.
- G2: Debezium *_cdc emits a non-empty `tables` placeholder when capture is
  defined by exclude-list / DB-level / regex (empty entry failed the
  "at least one table" lint).

Correctness:
- G3: JDBC source no longer emits a spurious schema_registry_decode (sql_select
  reads DB rows directly) — consumes value/key.converter like the CDC path.
- G4: JDBC sink translates insert.mode=upsert/update into a dialect-aware
  ON CONFLICT / ON DUPLICATE KEY suffix and derives columns/args_mapping from
  fields.whitelist (MERGE dialects get a clear manual TODO).
- G5: ExtractNewRecordState warning now enumerates the present row-shaping
  options (add.fields/add.headers/delete.handling.mode/drop.tombstones).
- G6: JDBC source mode → ORDER BY <cursor> suffix instead of an empty stub.
- G7: MirrorMaker honors replication.policy.class — DefaultReplicationPolicy
  prefixes the target topic with the source alias; Identity keeps the name.
- G8: BigQuery derives a topic-based `table` (topic2TableMap match or
  ${! @kafka_topic }) instead of an empty stub; warns on upsert/delete.

Coverage / noise:
- G9: S3/GCS handle Daily/Hourly partitioners (time-bucket prefix) and
  FieldPartitioner (field=${! this.field }/ prefix); consume parquet.codec,
  file.name.*, format.output.fields, timestamp.extractor.
- G10: register ByteArrayConverter as a no-op converter (MM2 default / raw).
- G11: consume topic.prefix / database.server.name (no *_cdc equivalent).
- G12: fold sslmode (PG DSN) and encrypt/trustServerCertificate (SQL Server)
  into the connection string; surface Oracle PDB; consume adapter/url/log.mining.
- G13: consume schema.history.internal.* / database.history.* families.
- G14: warn on Snowflake schematization; consume streaming/role override opts.
- G15: warn on MirrorMaker topics.exclude; consume groups/offset-syncs/policy.

Adds gap_fixes_test.go regression tests; regenerates affected golden fixtures.
Includes the per-connector gap analysis doc that motivated the work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…connectors

Two new supported connectors, with class names/aliases/config keys taken from
current official docs (Apache Iceberg, Tabular, Confluent Replicator, MM2).

Iceberg sink → `iceberg` output (REST catalog):
- Classes: org.apache.iceberg.connect.IcebergSinkConnector (current) and the
  legacy alias io.tabular.iceberg.connect.IcebergSinkConnector. No distinct
  Confluent/Aiven class (both ship the upstream connector).
- iceberg.catalog.uri/warehouse → catalog.url/warehouse; credential (client:secret)
  → catalog.auth.oauth2; token → catalog.auth.bearer.
- AWS Glue (catalog-impl=GlueCatalog) → the Glue REST endpoint + catalog.auth.aws_sigv4
  (service=glue) + a derived schema_evolution.table_location (Glue assigns none).
- Non-REST catalogs (hive/hadoop/nessie/jdbc) → stub URL + warning (RPCN iceberg is
  REST-only).
- iceberg.tables → namespace + table (last dot splits); multiple tables / dynamic
  routing (dynamic-enabled + route-field) → table interpolation + TODO.
- storage backend (aws_s3/gcp_cloud_storage/azure_blob_storage) inferred from io-impl
  and the warehouse URI scheme; bucket/container parsed from the warehouse location.
- evolve-schema-enabled → schema_evolution.enabled; control.commit.interval-ms →
  batching.period; upsert-mode-enabled/cdc-field → warning (iceberg appends).
- value.converter left to the converter pass (a sink decodes Kafka bytes); the
  redpanda input is synthesized from topics/topics.regex by the engine.

Kafka→Kafka (redpanda→redpanda):
- io.confluent.connect.replicator.ReplicatorSourceConnector → redpanda input+output.
  src/dest.kafka.bootstrap.servers → seed_brokers; topic.whitelist/topic.regex →
  topics (+regexp_topics); src.consumer.group.id → consumer_group; topic.rename.format
  ${topic} → ${! @kafka_topic } interpolation; src/dest.kafka.security.protocol →
  tls/sasl TODO stubs; topic.blacklist → warning; converters consumed (byte copy, no
  schema_registry_decode).
- MM2 MirrorCheckpointConnector / MirrorHeartbeatConnector recognized as operational
  (drop stub + informational warning), not data pipelines.

Adds conn_iceberg.go/conn_replicator.go (+ tests), 4 golden fixtures, registers the
iceberg component bundle in the lint harness, and updates the README connector table
(also fixes the stale MirrorMaker kafka_franz→redpanda entry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nd-trip

Make the converter handle both sides of serialization explicitly instead of
leaving gaps as TODO comments.

Decode (Kafka record → structured):
- JsonConverter is now a real converter, not a silent no-op. schemas.enable=true
  emits `root = this.payload` to unwrap Kafka Connect's {schema,payload} envelope;
  schemas.enable=false emits nothing (RPCN auto-parses JSON bytes on structured
  access); absent → a warning noting the Connect default is true.
- Avro/Protobuf/JSON-Schema continue to emit schema_registry_decode.

Encode (structured → sink file format) for object-store sinks:
- Component gains an Encode []*yaml.Node slot, appended to the pipeline after
  converters + SMTs (runs last, just before the output).
- format.class/format.output.type=Avro → an `avro` (operator: from_json) encode
  processor with a schema TODO, replacing the old "add an encode step" comment.
- Parquet → a path TODO to add parquet_encode (it can't lint without a schema,
  which the converter can't infer).
- JSON/JSONL/CSV/bytes need no encode (the output serializes structured records).

Registers the avro component bundle in the lint harness; regenerates the
s3_sink_full / gcs_sink_full goldens (now carry the avro encode step); adds
decode_encode_test.go and updates the README converter/serialization tables.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure reorganization — no behavior change. The package was a flat list of ~60
files with tests interleaved and no obvious entry point. Group by name prefix so
the directory listing clusters by role, and add a map so a newcomer knows where
to start and how to debug.

Renames (git mv, same package — no import changes):
- Pipeline core → engine_*: convert/parse/assemble/render (+ matching tests).
- yamlutil.go → yaml.go; smt_helpers.go → helpers_smt.go;
  field_helpers.go → helpers_field.go (+ matching tests).
- linter_test.go → testsupport_test.go (the shared test harness) and moved the
  gapConvert helper into it alongside assertValidRPCN so all shared test
  infrastructure lives in one place.

Adds doc.go: a package overview that walks the Convert() pipeline, maps every
file group to its role, and gives a step-by-step debug flow (golden tests +
-update, assertValidRPCN, warnings/TODO markers). The README "Architecture"
section gains a repository-layout table and the stale filename references
(parse.go/assemble.go/render.go/yamlutil.go/linter_test.go) are corrected.

Tests stay colocated with their source (Go requires same-package tests for the
unexported API); connectors/converters/SMTs keep their already-consistent
conn_*/conv_*/smt_* prefixes. lint 0, all converter + CLI tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant