decoder

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package decoder implements the debezium-json decoder for Kafka CDC messages. The wire format is the Debezium JSON envelope: a top-level object with "op" (c/u/d/r/t), "before", "after", "source" (with "ts_ms", "db", "table"), and "transaction" fields. Only c (create), u (update), and d (delete) map to pipeline changes; others are skipped.

Package decoder defines the Kafka message decoding contract. A decoder takes a raw Kafka record (from any wire format: debezium-json, avro, protobuf, …) and produces the pipeline's canonical Change events.

Field extraction turns a decoded payload document into the flat set of columns a table declared. It is shared by the raw and avro decoders: both end up holding a map[string]any and both need to project it onto the declared shape. What differs is the source of that map (a JSON parse versus an Avro unmarshal) and whether a raw blob accompanies it — neither of which this file knows about.

Raw lands the message envelope with the payload opaque by default: the record value is carried as a string, no JSON parsing, no schema inference. This is the bronze-landing decoder — the point is to NOT understand the content, so payloads that are not even valid JSON land verbatim. The transport envelope (topic, partition, offset, timestamp, key, headers) is attached by the reader, not here.

One Reader consumes every topic its spec tables name through a single shared decoder, so extraction is declared per topic: ByTopic switches a topic on when it has an entry, off (fully opaque) when it does not. Once a topic has an entry, its payload MUST parse as JSON — the two modes (byte passthrough vs. field extraction) are mutually exclusive per topic, since an unparseable payload extracting to all-NULL columns would look identical to a parseable one whose fields happen to be absent.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Extract added in v0.3.0

func Extract(doc map[string]any, fields []Field, miss MissFn) (map[string]any, error)

Extract projects doc onto fields. A declared field that the document does not carry lands as a nil entry (NULL) and reports a miss, unless it is Required, which fails the record instead.

The result always carries every declared column, present or not: a batch encodes against a fixed schema, so a column that appears only in some rows would make the shape depend on the data.

func OrderKey

func OrderKey(c *rowchange.Change, pk []string)

OrderKey rebuilds a change's key tuple in primary-key order, reading the values from the row image (after; before for deletes). Downstream consumers treat the key as positional — per-key collapse joins values into a map key, and the sink's equality deletes index the tuple by primary-key column — so a shuffled tuple duplicates rows and deletes the wrong ones.

Types

type Avro

type Avro struct {

	// ByTopic declares field selection per topic. A topic absent from the
	// map (including when ByTopic itself is nil, the zero value) keeps every
	// field the schema decodes.
	ByTopic map[string]TopicExtraction
	// Miss is called for every declared field a record does not carry (a
	// column declared that the registry schema does not have). Optional.
	Miss MissFn
	// contains filtered or unexported fields
}

Avro decodes Confluent-Avro messages: a 1-byte magic (0x00), a 4-byte big-endian schema id, then the binary Avro payload. The schema is resolved from the registry by id and cached (schemas are immutable per id) and the payload is decoded into the canonical value shape — records become map[string]any, arrays []any, scalars their Go types — which the sink's composite writer consumes directly. Every message is an insert: the record IS the row. (A Debezium-Avro envelope, where the message wraps before/after, is a future envelope layer on top.)

One Reader consumes every topic its spec tables name through a single shared decoder, so field selection is declared per topic: ByTopic projects the decoded record onto a subset for a topic that has an entry, and keeps every field for one that does not. The registry schema is the source of truth for a topic's record shape, and the spec is a projection of it, so a column present in the record but not declared is dropped rather than tripping schema drift downstream. There is no raw-blob equivalent of Raw's KeepPayload here — the pre-decode bytes are Confluent wire format (magic byte + schema id + binary), unreadable without the registry, so storing them would land a column nobody can query.

func NewAvroDecoder

func NewAvroDecoder(registry SchemaRegistry) *Avro

NewAvroDecoder builds an Avro decoder over a schema registry.

func (*Avro) Decode

func (d *Avro) Decode(rec *kgo.Record) ([]rowchange.Change, error)

type DebeziumJSON

type DebeziumJSON struct {
	// TopicToTable maps a Kafka topic name to the pipeline target table.
	// If empty, the decoder uses the source.db + source.table from the
	// envelope.
	TopicToTable map[string]string
}

DebeziumJSON decodes Kafka records in the Debezium JSON envelope format.

func (*DebeziumJSON) Decode

func (d *DebeziumJSON) Decode(record *kgo.Record) ([]rowchange.Change, error)

Decode implements Decoder.

type Decoder

type Decoder interface {
	Decode(record *kgo.Record) ([]rowchange.Change, error)
}

Decoder turns a single Kafka record into zero or more pipeline changes. The decoder is responsible for mapping the source message's operation type, key, and payload into the Change struct. A single record may produce multiple changes (e.g. a debezium outbox envelope wrapping a batch).

type ErrBadWireFormat

type ErrBadWireFormat struct{}

ErrBadWireFormat marks a value that does not carry the Confluent wire header — the topic is not Confluent-Avro, or the value is corrupt.

func (*ErrBadWireFormat) Error

func (e *ErrBadWireFormat) Error() string

type ErrFieldMissing added in v0.3.0

type ErrFieldMissing struct {
	Column string
	Path   string
}

ErrFieldMissing marks a required field the payload does not carry.

func (*ErrFieldMissing) Error added in v0.3.0

func (e *ErrFieldMissing) Error() string

type ErrNotJSON added in v0.3.0

type ErrNotJSON struct{ Err error }

ErrNotJSON marks a payload that could not be parsed as JSON when field extraction is declared. This is a hard failure, not a per-field miss: a payload extraction was declared for is presumed to be JSON, and one that is not means the topic does not hold what the spec says it does.

func (*ErrNotJSON) Error added in v0.3.0

func (e *ErrNotJSON) Error() string

func (*ErrNotJSON) Unwrap added in v0.3.0

func (e *ErrNotJSON) Unwrap() error

type ErrUnknownSchema

type ErrUnknownSchema struct {
	ID int
}

ErrUnknownSchema marks a schema id the registry does not know — the producer pointed at a different registry, or the id is corrupt. There is no schema to retry, so this is a needs-human condition, not a transient one.

func (*ErrUnknownSchema) Error

func (e *ErrUnknownSchema) Error() string

type Field added in v0.3.0

type Field struct {
	// Name is the destination column.
	Name string
	// Path is the payload path, dot-separated for nested documents. Empty
	// means Name is the path.
	Path string
	// Required fails the record when the field is absent, rather than
	// landing NULL.
	Required bool
}

Field is one declared column and where to read it from.

type HTTPRegistry

type HTTPRegistry struct {
	// contains filtered or unexported fields
}

HTTPRegistry is a Confluent-compatible schema registry client. Schemas are cached by id for the life of the process (immutable per id).

func NewHTTPRegistry

func NewHTTPRegistry(base string) *HTTPRegistry

NewHTTPRegistry dials the registry at base (no leading path requirement).

func (*HTTPRegistry) Get

func (r *HTTPRegistry) Get(ctx context.Context, id int) (avro.Schema, error)

Get resolves a schema by id, hitting the registry on a cache miss.

type MissFn added in v0.3.0

type MissFn func(column string)

MissFn is called when a non-required declared field is absent from a payload, so the caller can count it. An absent field is either contract optionality or a producer that changed shape, and the counter is what makes the second case visible. The decoders close over the record's topic before handing this to Extract, so the callback signature here stays a plain column name — the pure unit is "document + declared fields", the topic is the caller's context, not this function's.

type Raw

type Raw struct {
	// ByTopic declares field extraction per topic. A topic absent from the
	// map (including when ByTopic itself is nil, the zero value) stays fully
	// opaque: one "payload" column, the raw bytes, unconditionally.
	ByTopic map[string]TopicExtraction
	// Miss is called for every declared field a message does not carry — see
	// MissFn. Optional.
	Miss MissFn
}

Raw is the passthrough decoder. Every message is an insert — raw landing has no update or delete semantics, the log is the data. A tombstone (nil value) becomes a row with a NULL payload, which is the faithful record of the fact.

func (*Raw) Decode

func (d *Raw) Decode(r *kgo.Record) ([]rowchange.Change, error)

type SchemaRegistry

type SchemaRegistry interface {
	// Get returns the schema registered under id. A registry that does not
	// know the id returns ErrUnknownSchema.
	Get(ctx context.Context, id int) (avro.Schema, error)
}

SchemaRegistry resolves Avro schemas by Confluent schema id. Schemas are immutable once registered under an id — evolution publishes a NEW id and never rewrites an old one — so a per-id cache has no staleness window and no expiry.

type TopicExtraction added in v0.3.0

type TopicExtraction struct {
	Fields      []Field
	KeepPayload bool
}

TopicExtraction is one topic's field-extraction declaration: which fields to keep, and (for raw only) whether to also keep the full payload alongside them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL