core

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: 11 Imported by: 0

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

View Source
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

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

func AsFloat64(v any) (float64, bool)

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

func AsInt64(v any) (int64, bool)

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

func AsUint64(v any) (uint64, bool)

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

func ParseInt64(b []byte) (int64, error)

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

func ParseTimeOfDayText(s string) (int64, error)

ParseTimeOfDayText parses "HH:MM:SS[.fraction]" into micros since midnight.

func ParseTimestampText

func ParseTimestampText(s string) (time.Time, error)

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

func StringifyScalar(v any) (string, error)

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.

func (Kind) String

func (k Kind) String() string

String renders the kind name for errors and diagnostics.

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) String

func (k MetadataKey) String() string

String renders the key name.

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

type OpaqueOrigin struct {
	TypeName   string
	VendorName string
}

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

type Row map[string]any

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

type Schema struct {
	Columns    []Column
	PrimaryKey []string // column names, in key order
}

Schema is the canonical table shape — the ONLY schema type that crosses the source↔sink boundary.

func FilterSchemaColumns added in v0.3.0

func FilterSchemaColumns(cs Schema, columns []string) Schema

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

func WireSchema(source, resolved Schema) Schema

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) Column

func (s Schema) Column(name string) (Column, bool)

Column returns the column by name, if present.

func (Schema) KeyIndexes

func (s Schema) KeyIndexes() ([]int, error)

KeyIndexes resolves PrimaryKey into column positions, in key order.

func (Schema) Validate

func (s Schema) Validate() error

Validate reports whether the schema is internally coherent: every column is named, names are unique, and the primary key names existing columns without repetition. An empty schema is valid.

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).

type Warning

type Warning struct {
	Message string
}

Warning is a validation outcome that does not reject the spec but must be surfaced to the operator (eventlog, status) — never only in logs.

Jump to

Keyboard shortcuts

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