Documentation
¶
Overview ¶
Arrow canonical extension types — registration, not implementation.
The Arrow Canonical Extension Types list was reviewed against the same admission criterion as the Kind set: a value enters the canonical system only when a supported source produces it AND a supported sink can hold it. Most of the list fails on one side or the other and is handled by the escape valve (KindUnknown + explicit cast). Three entries survived review; two were acted on, and the rest are registered here so the decision is conscious rather than a future surprise.
ADOPTED — uuid. KindUUID travels as FixedSizeBinary(16) big-endian with no interpretation of the bytes, exactly the Arrow canonical form, and carries the arrow.uuid extension label on the wire so it is distinguishable from a fixed(16) byte column. See transport/schema.go.
ADOPTED — json. KindJSON is stored as a string (the Arrow canonical storage) and its wire field carries the arrow.json extension label, so a consumer reading the RecordBatch directly knows the string is JSON without guessing.
SPIKED SEPARATELY, NOT IMPLEMENTED — Parquet Variant (a future KindVariant for free-shape payloads, distinct from rigid Struct/List/Map). iceberg-go can write it, but the project's discipline is to read a write back with Trino before trusting it, and the reference Trino is not v3-ready in this window. A dedicated spike (write Variant via iceberg-go, read it back with Trino on format_version=3, confirm per-field navigation) gates promotion.
REGISTERED, NO ACTION — timestamp_with_offset: each row keeps its own UTC offset, which no supported source produces (MySQL TIMESTAMP and Postgres timestamptz both normalize to UTC and discard the original offset). The target representation if a source ever preserves per-row offsets (Oracle TIMESTAMP WITH TIME ZONE, SQL Server datetimeoffset) is the Arrow struct{timestamp, offset_minutes} form — not a new invented one.
REGISTERED, NO ACTION — Bool8 is a physical encoding (a byte per bool for zero-copy compat with systems that store bool in one byte), not a new semantic type; KindBool already covers the meaning.
REGISTERED, NO ACTION — tensors (fixed/variable_shape_tensor) and GeoArrow: no supported source produces them natively (MySQL geometry already lands in the escape valve as opaque). Iceberg v3 is adding its own geometry types in the same maturation window as Variant; if those mature, geometry may gain a dedicated Kind via the same spike discipline. Until then it stays opaque.
Package core defines the canonical type system. This file implements the per-column cast: a closed override of one column's canonical type, applied between the source mapping and the sink. It is annotation, never expression — there is no function grammar anywhere. The matrix is closed: widening always, to string always, narrowing and parsing never (except the explicitly declared temporal reinterpretations).
Package core defines the canonical, sink-agnostic type system. This file adds the closed catalog of pipeline metadata columns that a target table may carry: values the engine already holds on every change (operation, commit time, position, source table, phase) and the moment the pipeline itself processed the event. Nothing here derives data — every key exposes a value that already exists.
Package core holds the canonical, sink-agnostic type system that crosses the source↔sink boundary. Sources map their native types into core.Kind; sinks map out of it. Neither side knows the other — that is what makes N sources × M sinks cost N+M mappings instead of N×M.
Index ¶
- Constants
- Variables
- func AsFloat64(v any) (float64, bool)
- func AsInt64(v any) (int64, bool)
- func AsUint64(v any) (uint64, bool)
- func CastWarning(from Kind, to CastTarget) string
- func CheckCast(from ColumnType, to CastTarget) error
- func MetadataCatalogNames() string
- func ParseInt64(b []byte) (int64, error)
- func ParseTimeOfDayText(s string) (int64, error)
- func ParseTimestampText(s string) (time.Time, error)
- func ResolveSchema(src Schema, cast CastPolicy, meta []MetadataColumn) (Schema, []Warning, error)
- func StringifyScalar(v any) (string, error)
- func ValidMetadataKey(k MetadataKey) bool
- type CastPolicy
- type CastTarget
- type Column
- type ColumnType
- type Kind
- type MetadataColumn
- type MetadataKey
- type OpaqueOrigin
- type Row
- type Schema
- type TableRef
- type Warning
Constants ¶
const ( PhaseSnapshot = "snapshot" // rows read by a DBLog chunk SELECT PhaseStream = "stream" // live CDC events PhaseIncremental = "incremental" // rows read by a cursor pass (#157) )
Phase values for the __phase wire column and the phase metadata column.
Variables ¶
var MetadataCatalog = []MetadataKey{ MetaOp, MetaCommitTS, MetaIngestTS, MetaPosition, MetaSourceTable, MetaPhase, MetaStream, MetaShard, MetaSeq, MetaMsgTS, MetaMsgKey, MetaHeaders, }
MetadataCatalog is the closed catalog, in the order the keys are declared above. It is the single source of truth: membership checks and the catalog list in error messages both derive from it.
Functions ¶
func AsFloat64 ¶ added in v0.4.0
AsFloat64 widens any Go float or integer to float64. Integers beyond 2^53 round to the nearest float, as any float comparison of them would.
func AsInt64 ¶ added in v0.4.0
AsInt64 widens any Go integer to int64. An unsigned value above MaxInt64 has no int64 form and is rejected, never wrapped.
func AsUint64 ¶ added in v0.4.0
AsUint64 widens any Go integer to uint64. A negative value is rejected.
func CastWarning ¶
func CastWarning(from Kind, to CastTarget) string
CastWarning returns the advisory warning for an allowed cast that loses information (timestamptz → timestamp), or "" for a clean cast.
func CheckCast ¶
func CheckCast(from ColumnType, to CastTarget) error
CheckCast validates that a cast from a source canonical column type to a target is in the closed matrix. It is the apply-time rule; nothing here runs at runtime beyond value conversion.
func MetadataCatalogNames ¶ added in v0.3.0
func MetadataCatalogNames() string
MetadataCatalogNames renders the catalog for an error message.
func ParseInt64 ¶ added in v0.4.0
ParseInt64 parses a base-10 integer rendered as text, the form a SQL driver returns a column in over a text protocol (e.g. go-sql-driver/mysql without column type information).
func ParseTimeOfDayText ¶
ParseTimeOfDayText parses "HH:MM:SS[.fraction]" into micros since midnight.
func ParseTimestampText ¶
ParseTimestampText parses a temporal text into a time.Time: an RFC3339 instant (zone preserved), a bare date (midnight), or a naive timestamp with an optional fraction.
func ResolveSchema ¶
func ResolveSchema(src Schema, cast CastPolicy, meta []MetadataColumn) (Schema, []Warning, error)
ResolveSchema applies the cast policy and appends the declared metadata columns (all nullable, in declaration order) to a source schema, producing the final canonical schema the sink is built from. A metadata destination must never collide with a source column: a silent collision would overwrite user data.
func StringifyScalar ¶
StringifyScalar renders a non-binary scalar to its canonical string form. It is shared by the value cast kernel and the columnar encoder so both agree on the Go-type → string mapping for the "to string always" matrix. Binary ([]byte) is deliberately absent: it needs an explicit encoding and is handled by castToString and the encoder separately.
func ValidMetadataKey ¶ added in v0.3.0
func ValidMetadataKey(k MetadataKey) bool
ValidMetadataKey reports whether k is a member of the closed catalog. The catalog is closed by design (package doc); an unknown key is a spec typo and must fail loudly, never become a silent string column.
Types ¶
type CastPolicy ¶
type CastPolicy struct {
Columns map[string]CastTarget
}
CastPolicy is the per-table map of source column name → target canonical type. Keys are source-side names, matching the introspection result.
func ParseCastPolicy ¶
func ParseCastPolicy(m map[string]string) (CastPolicy, error)
ParseCastPolicy builds a policy from the spec's textual map.
func (CastPolicy) Resolve ¶
func (p CastPolicy) Resolve(src Schema) (Schema, []Warning, error)
Resolve applies the policy to one source schema, producing the final canonical schema: cast columns in place, KindUnknown columns replaced by their cast or rejected, and the source primary key preserved.
func (CastPolicy) Target ¶
func (p CastPolicy) Target(name string) (CastTarget, bool)
Target returns the declared cast for a source column, if any.
type CastTarget ¶
type CastTarget struct {
Type ColumnType
// AssumeUTC marks timestamp → timestamptz(assume_utc): the naive
// literal is asserted to be UTC, never shifted. Required for that cast.
AssumeUTC bool
// Encoding names the explicit encoding for binary → string: "hex" or
// "base64". Plain binary → string is ambiguous and prohibited.
Encoding string
}
CastTarget is the resolved destination of a per-column cast: a canonical type plus the explicit rules the matrix requires for the ambiguous cases.
func ParseCastTarget ¶
func ParseCastTarget(s string) (CastTarget, error)
ParseCastTarget parses the textual cast value: a canonical kind name, decimal(p,s), timestamptz(assume_utc), or string(hex|base64).
func (CastTarget) Convert ¶
func (t CastTarget) Convert(from Kind, v any) (any, error)
Convert applies an allowed cast to one value. from is the source column's canonical Kind: the value kernel needs it to disambiguate representations that share a Go type ([]byte is binary or uuid; int32 is an integer or a date; int64 is an integer or a time-of-day). from is consultive — it only shapes the rendering; the policy switch is t.Type. It returns an error for a value the cast cannot represent (invalid UUID text, invalid JSON).
Convert is NOT a leftover from the pre-columnar design: it is the per-value cast kernel the sinks apply on their write paths (clickhouse, couchbase and iceberg coerce each column value through it before serialization), where the unit of work is one value, not a column. The dataplane's columnar Cast (transport.KindToArrow + compute) is the batch-level executor; the two coexist by design (W-1 / D-4: the matrix is the single policy, the arrow kernel and this value kernel are the two executors). Deleting Convert would strand the sinks.
func (CastTarget) String ¶
func (t CastTarget) String() string
String renders the target in the textual spec form.
type Column ¶
type Column struct {
Name string
Type ColumnType
}
Column is one named canonical column.
type ColumnType ¶
type ColumnType struct {
Kind Kind
Precision int
Scale int
FixedSize int
Nullable bool
// Opaque records the provenance of an unmappable source type: what it
// was and where it came from, so the validation error and any operator
// looking at it know what the escape valve is carrying. Non-nil only
// when Kind == KindUnknown.
Opaque *OpaqueOrigin
// Fields holds the ordered fields of a KindStruct column.
Fields []Column
// Elem is the element type of a KindList column.
Elem *ColumnType
// KeyType and ValueType are the key/value types of a KindMap column.
// Message-log map keys are almost always strings.
KeyType *ColumnType
ValueType *ColumnType
}
ColumnType is a canonical column type. Precision/Scale are only meaningful for KindDecimal; FixedSize only for KindFixedBinary; Opaque only when Kind is KindUnknown; the composite fields only when Kind is KindStruct (Fields), KindList (Elem) or KindMap (KeyType/ValueType). The composite fields are pointers so a Struct can nest a Struct, or a List hold a Struct, without a parallel type system.
func ParseColumnType ¶
func ParseColumnType(s string) (ColumnType, error)
ParseColumnType parses a textual canonical type string into a ColumnType. The syntax is the same as ParseCastTarget: "string", "int64", "decimal(20,4)", etc.
func (ColumnType) String ¶
func (t ColumnType) String() string
type Kind ¶
type Kind uint8
Kind is the canonical type.
The admission criterion is deliberate: a value enters the Kind set only if a supported source produces it natively AND a supported sink has a real representation for it. Physical encodings of the same logical type (dictionary, *_view, large_*) never become distinct Kinds; polymorphic types with no source or destination (union, extension) never enter; and the escape valve (KindUnknown + an explicit cast) is the deliberate alternative to accepting anything silently. Composite types (struct/list/map) are a separate dimension of the model — they touch the cast matrix, the Arrow mapping, Iceberg schema generation, the wire format and schema comparison — and land as a dedicated effort, not an enum addition.
const ( KindUnknown Kind = 0 KindBool Kind = 1 KindInt32 Kind = 2 KindInt64 Kind = 3 // KindUInt64 is an unsigned 64-bit integer; sources that natively emit // uint64 keep their exact width instead of silently widening to int64. KindUInt64 Kind = 4 KindFloat32 Kind = 5 KindFloat64 Kind = 6 // KindDecimal uses Precision and Scale. KindDecimal Kind = 7 KindString Kind = 8 KindBinary Kind = 9 // KindFixedBinary is Iceberg fixed(L) — a fixed-size byte sequence, // distinct from variable binary. KindFixedBinary Kind = 10 // KindDate is days since epoch. KindDate Kind = 11 // KindTime is micros since midnight. KindTime Kind = 12 // KindTimestamp is a naive wall clock, NO timezone (MySQL DATETIME). KindTimestamp Kind = 13 // KindTimestampTZ is a UTC instant (MySQL TIMESTAMP). KindTimestampTZ Kind = 14 KindUUID Kind = 15 // KindJSON is semantically JSON; physically a string in most sinks. KindJSON Kind = 16 // Composite kinds — a separate dimension of the model, entered only // because both sides of the boundary have a real representation for // them: message-log sources (Avro/Protobuf via schema registry) produce // nested values and Iceberg holds struct/list/map natively. KindStruct Kind = 17 KindList Kind = 18 KindMap Kind = 19 )
Kind values are explicit (not iota) because Kind crosses the wire in the Arrow extension metadata and in serialized schemas: inserting a value must never silently renumber the existing ones.
type MetadataColumn ¶
type MetadataColumn struct {
From MetadataKey `json:"from"`
As string `json:"as"`
}
MetadataColumn maps one metadata key to a destination column name. The destination name is explicit — there is no default, so an internal wire name never leaks into the user's schema.
type MetadataKey ¶
type MetadataKey string
MetadataKey identifies one pipeline-provided value that can be landed as a column in the target table. The catalog is closed: every key has a fixed canonical type and a fixed semantic, and there is no free-form field.
const ( // MetaOp is the row operation: "insert", "update" or "delete". In upsert // mode a delete removes the row entirely, so op never lands as "delete" // there — the row stops existing. In append mode every event becomes a // row and delete survives in op. MetaOp MetadataKey = "op" // MetaCommitTS is when the source committed the transaction that // produced the row. Snapshot rows carry NULL: the chunk SELECT does not // return the creating transaction's commit time. MetaCommitTS MetadataKey = "commit_ts" // MetaIngestTS is when the pipeline processed the event. The difference // between commit_ts and ingest_ts is the replication lag, measured per // row at the destination. MetaIngestTS MetadataKey = "ingest_ts" // MetaPosition is the row's source coordinate (GTID set, LSN, or Kafka // offset map). Snapshot rows carry the chunk's low watermark. MetaPosition MetadataKey = "position" // MetaSourceTable is the source table the row came from, e.g. // "shop.orders". MetaSourceTable MetadataKey = "source_table" // MetaPhase distinguishes backfill from live traffic: PhaseSnapshot for // rows read by the DBLog chunk SELECT, PhaseStream for live events. It is // an axis orthogonal to op — a snapshot row is semantically an insert. MetaPhase MetadataKey = "phase" // Transport metadata — the message-queue envelope (Kafka today, Kinesis // next). Names are transport-neutral so they do not need renaming as // sources are added. For CDC sources only MetaStream and MetaSequence // carry values (the source table and the GTID/LSN of the event); the // rest are NULL. MetaStream MetadataKey = "stream" // topic / stream name (CDC: source table) MetaShard MetadataKey = "shard" // partition / shard id (CDC: NULL) MetaSeq MetadataKey = "sequence" // offset / sequence number (CDC: GTID/LSN of the event) MetaMsgTS MetadataKey = "msg_ts" // message arrival time (CDC: NULL) MetaMsgKey MetadataKey = "msg_key" // message key (CDC: NULL) MetaHeaders MetadataKey = "headers" // JSON-serialized headers (CDC: NULL) )
func (MetadataKey) ColumnType ¶
func (k MetadataKey) ColumnType() ColumnType
ColumnType returns the canonical type the key lands as. Every metadata column is nullable: snapshot rows carry NULL for commit_ts, the transport envelope is NULL for CDC sources. The kind is the shape; nullability is a property of the value and is always true for a metadata column.
func (*MetadataKey) UnmarshalJSON ¶
func (k *MetadataKey) UnmarshalJSON(b []byte) error
UnmarshalJSON validates the key against the closed catalog at parse time, so a spec typo fails at load instead of at schema resolution. The spec loader round-trips YAML through JSON, so this is the effective hook.
func (*MetadataKey) UnmarshalText ¶
func (k *MetadataKey) UnmarshalText(b []byte) error
UnmarshalText is the YAML/text counterpart of UnmarshalJSON.
type OpaqueOrigin ¶
OpaqueOrigin names an unmappable source type and its vendor. It is the provenance half of the escape valve: KindUnknown says "this has no canonical form and needs an explicit cast", Opaque says what it was ("geometry" from "postgres", "point" from "mysql") instead of discarding that knowledge at the mapping boundary.
func (*OpaqueOrigin) String ¶
func (o *OpaqueOrigin) String() string
type Row ¶
Row is a decoded source row: column name → value. The Go type per Kind is the canonical contract both cast kernels (core.Convert and the columnar encoder) rely on:
KindBool bool KindInt32 int32 KindInt64 int64 KindUInt64 uint64 KindFloat32 float32 KindFloat64 float64 KindDecimal string (decimal text) KindString string KindBinary []byte KindFixedBinary []byte KindDate time.Time, int32 days, or "2006-01-02" text KindTime time.Time, int64 micros, or "15:04:05" text KindTimestamp time.Time or naive timestamp text KindTimestampTZ time.Time or RFC3339 text KindUUID string or 16 raw bytes KindJSON string, []byte, or a Go composite (map/slice)
A Kind may arrive in more than one representation — the DBLog snapshot and the live stream decode differently — so both kernels accept the union.
type Schema ¶
Schema is the canonical table shape — the ONLY schema type that crosses the source↔sink boundary.
func FilterSchemaColumns ¶ added in v0.3.0
FilterSchemaColumns keeps only the named columns, preserving order and the primary key. An empty list returns the schema unchanged. Shared by the SQL sources so a columnFilter narrows the introspected shape identically.
func WireSchema ¶
WireSchema returns the shape a source encodes and the wire carries.
Invariant: the wire preserves the SOURCE type — including its nullability — so a sink's Kind-aware cast sees the true origin (a date as Date32, binary as Binary, not the already-cast target). The resolved schema dictates only the KIND, and only for a KindUnknown column, which has no Arrow representation of its own; the sink then re-applies that cast idempotently.
func (Schema) KeyIndexes ¶
KeyIndexes resolves PrimaryKey into column positions, in key order.
type TableRef ¶
type TableRef struct {
Source string // source-side identifier, e.g. "shop.orders"
Target string // sink-side identifier, e.g. "raw.orders"
PrimaryKey []string // equality key; empty means "derive from source"
// Owner is the worker group that owns this table's partition
// ({pipeline}-{target}-{i}, spec.Table.WorkerGroupNames). Empty when the
// table is not partitioned. Sinks that persist a durable position per
// table use it to keep one position per partition instead of a single
// last-writer scalar (WK-001 §2.6). Derived from the spec, so it is
// stable across restarts and k8s rollouts — unlike the pod name.
Owner string
// OwnerCount is the number of partitions the table is split across
// (spec.Table.WorkerCount). A per-partition Position() read uses it to
// refuse a MinSafe over an incomplete owner set: an owner with no
// committed position would otherwise be resumed past. 0 or 1 means the
// collapsed single-owner path.
OwnerCount int
}
TableRef identifies one replicated table on both sides. This is the pipeline-wide table identity (replaces the per-source TableRef).