Skip to content

Data Representation Contracts

Status and scope

The tabular protocols are named TabularReader and TabularWriter. They read and write ETLRelay batches. KeyValueReader remains a separate protocol for reading mapping-shaped documents, with YamlReader as the existing implementation.

For v1.0, mapping-document writing and conversion between tabular batches and row records are deferred. Neither capability has a demonstrated requirement within the frozen feature set.

Three distinct representations

Tabular batches

ETLRelay uses Batch objects, backed by PyArrow tables, as the common in-memory tabular representation. The current TabularReader protocol reads a binary stream and returns an iterable of batches; TabularWriter serializes an iterable of batches to a binary stream.

from collections.abc import Iterable
from typing import BinaryIO, Protocol

from etlrelay.core import Batch


class TabularReader(Protocol):
    def read(self, stream: BinaryIO) -> Iterable[Batch]:
        ...


class TabularWriter(Protocol):
    def write(self, batches: Iterable[Batch], stream: BinaryIO) -> None:
        ...

Mapping-shaped documents

KeyValueReader reads one document mapping from a binary stream. YamlReader is the current implementation, using a safe loader and checking that the document root is a mapping with string keys. It rejects duplicate keys and multiple documents. It does not know the application schema.

from collections.abc import Mapping
from typing import Any, BinaryIO, Protocol


class KeyValueReader(Protocol):
    def read(self, stream: BinaryIO) -> Mapping[str, Any]:
        ...

ConfigLoader depends on KeyValueReader, not directly on YamlReader. ConfigValidator then applies the ETLRelay versioned schema, while PipelineBuilder consumes the validated configuration and constructs runtime components. The YAML reader never produces Batch objects and is not part of the tabular format registry.

Row records

A row record is an in-memory mapping such as {"id": 1, "name": "Ada"}. A sequence of such mappings is different from one configuration mapping and from an iterable of Batch objects. ETLRelay does not introduce a general conversion framework between these representations in the frozen v1.0 scope.

Configuration loading boundary

Config file
    │
    ▼
FileSystem.open_read(path)
    │
    ▼
Binary stream
    │
    ▼
KeyValueReader.read(stream)  ──> one root Mapping[str, Any]
    │
    ▼
ConfigValidator.validate(mapping)
    │
    ▼
Validated mapping
    │
    ▼
PipelineBuilder.build(mapping)  ──> Pipeline

The loader must not construct sources, processors, sinks, SQLAlchemy engines, or pipelines. The builder owns component construction and SQL environment preflight. Schema validation and preflight occur before configured source/sink I/O starts.

Format registry boundaries

TabularFileFormatRegistry contains reader and writer factories for tabular formats such as CSV and Parquet. It models reading and writing independently, because a format can be read-only or write-only. The default catalog registers CSV and Parquet readers/writers.

YamlReader is not a tabular format reader. It implements KeyValueReader and does not belong in the tabular registry. A future JsonReader could satisfy the same document-reader contract without changing the configuration schema, but JSON support is not included in the frozen v1.0 scope.

Tabular/record conversion — deferred

Tabular-to-records and records-to-tabular conversion are not required for v1.0. No conversion protocol or generic conversion framework is introduced. Before revisiting this decision, define a concrete consumer, eager/lazy row iteration, schema inference versus explicit schema, column order, missing/extra fields, empty input, null and Arrow-specific values, batching, and cleanup. Mapping-document parsing does not establish those row-conversion semantics.

Protocol naming and mapping-document writing

TabularReader and TabularWriter explicitly identify the batch-oriented contracts. Concrete format implementations retain their existing names: CsvReader, ParquetReader, CsvWriter, and ParquetWriter.

KeyValueReader remains separate because one mapping-shaped document is a different representation from tabular batches or an iterable of row records.

KeyValueWriter is deferred beyond v1.0. Configuration loading requires document reading, and no mapping-document serialization use case currently justifies a writer protocol.

Class diagram: tabular data versus mapping-shaped documents

The diagram below summarizes the principal types and dependencies described on this page. Existing flow, sequence, and data-flow diagrams remain part of this document.

classDiagram
    direction LR

    class Batch {
        +table: pyarrow.Table
    }
    class TabularReader {
        <<protocol>>
        +read(stream) Iterable~Batch~
    }
    class TabularWriter {
        <<protocol>>
        +write(batches, stream) None
    }
    class CsvReader
    class ParquetReader
    class CsvWriter
    class ParquetWriter
    class KeyValueReader {
        <<protocol>>
        +read(stream) Mapping
    }
    class YamlReader {
        +read(stream) Mapping
    }
    class ConfigLoader {
        +load(path) Mapping
    }
    class ConfigValidator {
        +validate(config) None
    }

    TabularReader <|.. CsvReader
    TabularReader <|.. ParquetReader
    TabularWriter <|.. CsvWriter
    TabularWriter <|.. ParquetWriter
    KeyValueReader <|.. YamlReader
    ConfigLoader --> KeyValueReader : reads one document
    ConfigLoader --> ConfigValidator : checks schema
    TabularReader ..> Batch : produces
    TabularWriter ..> Batch : serializes