What is a record?
Back in Orientation you ran customer_etl and Clinker reported
“5 records.” A Record’s Journey follows one of those records all the way through
the engine. But first: what is a record? It’s the unit of data that flows through every
pipeline, and it’s built from three vocabulary types you’ll see everywhere: a cell, a
row, and the columns that name the cells.
What you’ll be able to do
Section titled “What you’ll be able to do”- Name the three types that make up a record (
Valueis a cell,Recorda row,Schemathe columns) and say what each one holds. - Explain how the declared schema turns decoded CSV text into typed values before downstream processing.
- Trace how a cell is located: how
values[i]finds the right column through the sharedSchema. - Write a miniature record (a schema plus a positional
Vecof values) and print each cell against its column name.
(We meet these types shallowly here; Data & Representation opens
each one up: the optimized string type, the Arc sharing, the per-record context.)
New terms in this lesson (each is named and defined here before it appears below):
Value: one cell of data.Record, one row: a list of cells bound to a schema.Schema: the column names and their order.Arc, a shared handle, so every row points at the same schema.
Predict first
Section titled “Predict first”A cell is a Value
Section titled “A cell is a Value”Every single cell of data in Clinker is one
Value,
a closed set of ten shapes (null, bool, integer, float, decimal, string, date, datetime,
array, map):
clinker-record ·value.rs ·Value type @3b343a4e
pub enum Value { Null, Bool(bool), Integer(i64), Float(f64), Decimal(Decimal), String(FieldStr), Date(NaiveDate), DateTime(NaiveDateTime), Array(OwnedValues), Map(OwnedMap),}It’s a closed set: a cell is exactly one of these ten, never something else. The CSV decoder produces text, and source admission applies the declared schema. Alice reaches the next node as a row of strings because this example deliberately declares string columns. A declared numeric column would already hold a parsed number at that boundary.
A row is a Record
Section titled “A row is a Record”A Record
is one row: a list of Values lined up against a Schema that names the columns.
clinker-record ·mod.rs ·Record type @3b343a4e
The real definition, trimmed to the part that matters now:
pub struct Record { schema: SharedStorage<Schema>, // the column names + order, shared by every row of a source values: OwnedValues, // the cells, positional — values[i] belongs to column i // ... plus per-record scoped vars and document context, for later}Two ideas to take away. First, the cells are a plain OwnedValues indexed by
position; there’s no name stored next to each cell. Second, the schema is held behind an
Arc,
a shared handle, so a million rows from one source all point at the same schema
instead of each carrying their own copy. (Why Arc, and what that costs, is a
Data & Representation question.)
The columns are a Schema
Section titled “The columns are a Schema”The Schema
is the list of column names and their order; it’s what lets values[5] mean
“lifetime_value”:
clinker-record ·schema.rs ·Schema type @3b343a4e
pub struct Schema { columns: Vec<Box<str>>, // column names, in order field_metadata: Vec<Option<FieldMetadata>>, index: HashMap<Box<str>, usize>, // name -> position, for O(1) lookup}This is the join that makes a positional OwnedValues readable. To find “lifetime_value”,
the schema’s index maps the name to a position (say 5); then values[5] is the cell.
The names live once, in the schema; the cells are bare values keyed by their slot. In
customer_etl, the source declared the schema right in the YAML:
customer_id, first_name, last_name, email, status, lifetime_value, zip_code. That’s the
schema every customer record is bound to.
Worked → completion → faded
Section titled “Worked → completion → faded”Now build a record yourself, scaffolded down from a fully worked model to one you write from scratch. Each rung adds exactly one new idea.
Worked: a record, modeled in miniature
Section titled “Worked: a record, modeled in miniature”Here’s Alice’s row as the reader first sees it: column names plus a Vec of values,
every cell a string. Run it and read the output, each column name printed against its
positional cell.
> output appears here — press Run
The zip is the whole point in miniature: the names and the cells are stored apart
(one schema, one Vec) and re-joined by position when you need them, exactly how the
real Record uses SharedStorage<Schema> + OwnedValues.
Completion: add the missing column
Section titled “Completion: add the missing column”Below, the same record is missing its last column. The schema already lists zip_code,
but alice has only four cells, so the zip stops short and zip_code never prints.
Add the fifth cell so every column has a value. (Like the CSV reader, make it a
Value::Str, not an integer.)
> output appears here — press Run
💡 Hint 1
zip pairs by position and stops at the shorter side. The schema has five names; alice has four cells, so zip_code is dropped. Push one more Value::Str(...) onto the Vec: a zip code like "94103", read as a string, just like every other freshly-read cell.Show solution
let alice: OwnedValues = vec![ Value::Str("1001".to_string()), Value::Str("Alice".to_string()), Value::Str("active".to_string()), Value::Str("15200".to_string()), Value::Str("94103".to_string()), // zip_code — a string, like every read cell];A record’s OwnedValues must have exactly one cell per schema column, in order. The
zip code is text on disk, so it arrives as Value::Str, not Value::Integer, even
though it looks numeric. The schema declares which columns exist; the Vec supplies
one positional cell for each.
Faded: build the record from scratch
Section titled “Faded: build the record from scratch”Your turn with much less scaffolding. The schema is given. Build the OwnedValues for
Bob’s row (1002, Bob, active, 8400, 10001) as the CSV reader would first produce
it (every cell a string), then print each column against its cell with the same zip
loop. The Value enum and main skeleton are provided; you write the Vec and the
loop.
> output appears here — press Run
Show solution
fn main() { let schema = ["customer_id", "first_name", "status", "lifetime_value", "zip_code"];
let bob: OwnedValues = vec![ Value::Str("1002".to_string()), Value::Str("Bob".to_string()), Value::Str("active".to_string()), Value::Str("8400".to_string()), Value::Str("10001".to_string()), ];
for (column, value) in schema.iter().zip(&bob) { println!("{column:>15} = {value:?}"); }}Five columns, five string cells, joined by position. Note that lifetime_value is
"8400" (a string) here, not 8400 (an integer); coercion is the next transform’s job,
not the reader’s. You just built, by hand, the exact shape the CSV source hands the rest
of the pipeline.
Why-bridge: why a positional Vec and a shared SharedStorage<Schema>?
Section titled “Why-bridge: why a positional Vec and a shared SharedStorage<Schema>?”Two design choices in the real Record are worth a sentence of why, since you’ll lean
on them all through Data & Representation.
- Cells are positional, not named per cell. Storing the column name next to every
cell would repeat the same seven strings on every one of a million rows. Instead the
names live once in the
Schema, and a cell is found by its slot:values[i]. The cost is that a bare cell is meaningless without its schema, which is why aRecordalways carries one. - The schema is shared behind
Arc, not copied. Every row of one source has the same columns, so copying the schema per row would be pure waste.Arclets a million records share one schema: cloning a record’s schema handle bumps a reference count, it doesn’t duplicate the column list.
Retrieval checkpoint
Section titled “Retrieval checkpoint”Connections
Section titled “Connections”You now have the vocabulary of a record: a Value is a cell, a Record is a positional
row of cells, and a Schema names the columns once and is shared via Arc. Next:
how the YAML pipeline you wrote becomes a plan the engine
can actually run.
See it from the pipeline author’s side (optional, one-directional; the schema and
record concept where you declare the schema: in YAML rather than read the Rust types):
Glossary terms used: Value, Record, Schema, Arc.