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 why a freshly-read CSV record is entirely
Value::String, and what later turns a string into a number. - 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 nine shapes (null, bool, integer, float, string, date, datetime,
array, map):
clinker-record ·value.rs ·Value type @19acdcb4
pub enum Value { Null, Bool(bool), Integer(i64), Float(f64), String(FieldStr), Date(NaiveDate), DateTime(NaiveDateTime), Array(Vec<Value>), Map(Box<IndexMap<Box<str>, Value>>),}It’s a closed set: a cell is exactly one of these nine, never something else. When the
CSV source reads Alice’s row, it doesn’t guess types; it reads every field as a
string (Value::String), exactly as you predicted. So right after reading, Alice is a
row of strings; coercing "15200" to a number is a later transform’s job.
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 @19acdcb4
The real definition, trimmed to the part that matters now:
pub struct Record { schema: Arc<Schema>, // the column names + order, shared by every row of a source values: Vec<Value>, // 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 Vec<Value> 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 @19acdcb4
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 Vec<Value> 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 Arc<Schema> + Vec<Value>.
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: Vec<Value> = 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 Vec<Value> 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 Vec<Value> 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: Vec<Value> = 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 Arc<Schema>?
Section titled “Why-bridge: why a positional Vec and a shared Arc<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.