Checkpoint 2: Add Nullable Column Views
Checkpoint 1 gave you owned arrays. Now let an expression borrow those arrays in three useful shapes without copying their values:
- an
Arrayview reads rows in their original order; - a
Constantrepeats one value or typed null for a requested length; and - an
Indexedview remaps rows through a borrowed index slice.
Begin from your completed Checkpoint 1 workspace. Copy the cumulative tests, then run only the new Chapter 2 cases once:
cargo x copy-test --chapter 2
cargo test -p type-exercise-starter-supplied-tests chapter_2 --locked
That focused test should fail because ColumnViewImpl and ColumnView do not exist yet, while the
Chapter 1 implementation should still compile. Keep the copied tests unchanged.
Enable the learner-owned module
Open type-exercise-starter/core/src/lib.rs and enable the existing column module and export.
Then implement type-exercise-starter/core/src/column.rs.
The erased view must accept every Checkpoint 1 physical family. Its representation stays private; callers create each shape through checked constructors:
let values: ArrayImpl = I32Array::from_slice(&[Some(10), None, Some(30)]).into();
let array = ColumnViewImpl::array(&values);
let constant = ColumnViewImpl::constant(ScalarRefImpl::Int32(7), 3);
let nulls = ColumnViewImpl::null(PhysicalType::Int32, 3);
let indices = [2, 1, 2, 0];
let indexed = ColumnViewImpl::indexed(&indices, &values)?;
All three forms answer the same questions: len, is_empty, physical_type, and get. They
borrow their inputs for lifetime 'a, so construction does not allocate another array.
Preserve nulls and physical types
An array view delegates its length, physical type, and row read to the borrowed ArrayImpl. A
constant stores one Option<ScalarRefImpl<'a>> and a length:
constant(value, len)recordsvalue.physical_type()and returns that same value for every row;null(physical_type, len)records the supplied type and returnsNonefor every row.
The explicit type on a null constant is essential: None carries no scalar variant, but later
code must still distinguish a null Int64 column from a null String column.
Treat row < len as the public precondition for get, matching array access in this course.
Assert that bound before reading the private representation. Inside a valid range, None means a
SQL null rather than an out-of-bounds sentinel.
Validate indexed views once
An indexed view borrows &[u32] and an &ArrayImpl. Its output length is the number of indices,
and its physical type is the values array’s type. Output row r reads
values.get(indices[r] as usize).
Validate every index in ColumnViewImpl::indexed. If an index falls outside the values array,
return an error that identifies the bad index and its output row. Once construction succeeds,
every output row is safe to read without repeating index validation or materializing a gathered
array.
For example, values ["zero", NULL, "two"] with indices [2, 1, 2, 0] read as
["two", NULL, "two", "zero"]. The two appearances of "two" borrow the same underlying
string bytes.
Check the scalar family once
ColumnViewImpl is appropriate when a planner knows the physical type only at runtime. Generic
code often wants a concrete scalar family. Add ColumnView<'a, S: Scalar> with the same three
private forms and implement:
TryFrom<ColumnViewImpl<'a>> for ColumnView<'a, S>
Compare the erased view’s physical_type() with S::PHYSICAL_TYPE before converting its private
state. Then downcast the borrowed array, constant scalar, or indexed values array through the
checked conversions from Checkpoint 1. A mismatched family returns TypeMismatch before any row
is read.
After that one conversion, ColumnView<'a, S>::get returns Option<S::RefType<'a>> directly. For
ColumnView<'_, String>, the returned &str still borrows the original StringArray bytes.
Decimal remains available through ColumnViewImpl; its precision and scale are runtime metadata,
so it does not use the static Scalar relationship.
Run both checkpoints
Run the focused Chapter 2 cases, then the full cumulative package:
cargo test -p type-exercise-starter-supplied-tests chapter_2 --locked
cargo test -p type-exercise-starter-supplied-tests --locked
The cumulative run should pass nine tests: five from Checkpoint 1 and four from Checkpoint 2. You can run the completed snapshot independently:
cargo test -p type-exercise-checkpoint-02-supplied-tests --locked
cargo check -p type-exercise-checkpoint-02-core --locked
You are done when array views preserve null positions, constants repeat values and typed nulls, indexed views validate and remap rows, and typed views reject the wrong family before returning borrowed scalar references.
The next checkpoint will put these views underneath a shared expression loop.