Documentation
¶
Overview ¶
Columnar decoding: the direct-to-Arrow sibling of Decoder (#733). A decoder that can write a record's fields straight into a transport.RowEncoder implements ColumnarDecoder and the Kafka reader prefers it, so the live path never materializes a per-record rowchange.Change or re-encodes one through the source puller. The decode cores are shared: the map path stays intact for callers that still need []rowchange.Change.
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; the truncate and message ops (t/m) are skipped. A missing or unknown op is ErrNotEnvelope.
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 ¶
- Variables
- func Extract(doc map[string]any, fields []Field, miss MissFn) (map[string]any, error)
- func OrderKey(c *rowchange.Change, pk []string)
- type Avro
- type ColumnarDecoder
- type DebeziumJSON
- type Decoder
- type Encoder
- type ErrBadWireFormat
- type ErrFieldMissing
- type ErrNotEnvelope
- type ErrNotJSON
- type ErrUnknownSchema
- type Field
- type HTTPRegistry
- type MissFn
- type Raw
- type SchemaRegistry
- type TopicExtraction
Constants ¶
This section is empty.
Variables ¶
var ErrEncode = errors.New("decoder: value cannot be encoded")
ErrEncode marks a value a declared column's Arrow builder rejected. It is not a decode error — the record parsed — so the caller must fail the run rather than skip the record, exactly as the row puller's RecordFromChanges fails it.
var ErrShapeDrift = errors.New("decoder: record needs the map path")
ErrShapeDrift marks a record the direct path cannot encode without changing behavior. The caller re-encodes it on the map path, which gates and merges schema drift, backfills a delete's key and guards a delete with no primary key exactly as the row puller did.
Functions ¶
func Extract ¶ added in v0.3.0
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 ¶
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) DecodeInto ¶ added in v0.5.0
DecodeInto implements ColumnarDecoder: a projected record's declared fields are written straight into the target's Arrow builders, no rowchange.Change between. An unprojected topic keeps the whole registry record, whose shape may exceed the canonical schema, so it returns ErrShapeDrift and the reader re-encodes that record on the map path. Every message is an insert: the record IS the row.
type ColumnarDecoder ¶ added in v0.5.0
type ColumnarDecoder interface {
DecodeInto(rec *kgo.Record, enc Encoder, position string) (int, error)
}
ColumnarDecoder is the optional direct-to-Arrow sibling of Decoder (#733). DecodeInto resolves the record's source key (a Debezium envelope source, or "" for a topic-keyed decoder), asks enc for that table's encoder, and appends each decoded row straight into its Arrow builders — no per-row map[string]any buffered between decode and encode.
It returns the number of rows appended: 0 for a tombstone, a skipped op, or an unmapped source. Every row is stamped with its operation, commit time and the given position. A record the direct path cannot encode without changing behavior (a shape the schema cannot represent, a missing target schema, or a delete that needs the map path's key backfill) returns ErrShapeDrift, and the caller re-encodes that record on the map path — which reproduces the row puller exactly.
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) DecodeInto ¶ added in v0.5.0
DecodeInto implements ColumnarDecoder: the decoded after image is written straight into the target's Arrow builders, no rowchange.Change between. Inserts and updates encode directly; a delete needs the map path's key backfill and primary-key guard, so it returns ErrShapeDrift and the reader re-encodes that record through the puller.
type Decoder ¶
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 Encoder ¶ added in v0.5.0
type Encoder func(sourceKey string) (enc *transport.RowEncoder, mapped bool, err error)
Encoder resolves a decoded record's source key to the Arrow encoder for its target table, built from that table's canonical schema. It reports whether the source maps to a configured target (mapped=false means the record is skipped, as the row path skips an unmapped change). A nil encoder with mapped=true means the target has no canonical schema; the decoder then returns ErrShapeDrift so the caller falls back to the map path, which infers the shape from the rows.
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
ErrFieldMissing marks a required field the payload does not carry.
func (*ErrFieldMissing) Error ¶ added in v0.3.0
func (e *ErrFieldMissing) Error() string
type ErrNotEnvelope ¶ added in v0.5.0
ErrNotEnvelope marks a message that is not a Debezium envelope — an empty op, an unknown op, or a payload that does not parse. It is fatal: the topic's format is an assertion, and silently skipping every record is total data loss.
func (*ErrNotEnvelope) Error ¶ added in v0.5.0
func (e *ErrNotEnvelope) Error() string
func (*ErrNotEnvelope) Unwrap ¶ added in v0.5.0
func (e *ErrNotEnvelope) Unwrap() error
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 NewHTTPRegistryWithAuth ¶ added in v0.5.0
func NewHTTPRegistryWithAuth(base, username, password string, tlsCfg *tls.Config) *HTTPRegistry
NewHTTPRegistryWithAuth dials the registry with HTTP basic auth and an optional TLS config (issue #604). An empty username means no auth; a nil tlsCfg means the default transport.
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) DecodeInto ¶ added in v0.5.0
DecodeInto implements ColumnarDecoder: the raw payload is written straight into the target's Arrow builders, with no rowchange.Change between. An opaque topic lands its single "payload" column; an extracting topic projects the declared fields. Every message is an insert — raw landing has no update or delete semantics.
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
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.