CSV Files, Small Data Pipelines, and Simple Queries
Ruby source files are converted directly from the English source tree.
en/topic_11_csv_files_queries
Overview
Why this topic matters ๐ก
CSV work appears constantly in business software: imports, exports, admin reports, and integration handoffs. This topic teaches students to treat CSV handling as a small data pipeline: read data, write data, then query it in a focused service.
Learning outcomes ๐ฏ
By the end of this topic, students should be able to:
- read CSV files into Ruby data structures;
- write structured rows back to CSV format with explicit headers;
- explain why CSV handling should be separated into reading, writing, and query responsibilities;
- inject a reader dependency into a query service for easier testing;
- use Enumerable to express simple reporting-style queries over imported data.
Assessment focus โ
Students should be able to explain why file access and query logic should not be mixed into one large method.
Short Note
Students often first see CSV as a utility task: "open a file and loop over lines." That is too narrow. CSV work usually has three separate concerns:
- getting rows into memory;
- turning structured rows back into a file;
- asking business questions of the imported data.
Ruby is pleasant here because:
- the standard library already provides
CSV; - hashes and arrays make row-oriented data easy to inspect;
- Enumerable helps express lightweight reporting queries clearly.
Ruby beauty in this topic:
- small pipelines can stay very readable;
- the code moves naturally from file IO to structured data;
- query services can remain tiny but useful.
Ruby caution in this topic:
- CSV data is often messier than the exercise suggests;
- if parsing, writing, and querying all happen in one class, the code gets muddy fast.
Reflection prompt:
- Which part of the design changes if the source stops being a file and becomes an API response?
Worked Examples
Example 1: Importing inventory from a CSV file ๐ก
An inventory import is a strong teaching example because the file structure is easy to understand and the result maps naturally to arrays of hashes.
reader.load("inventory.csv")
# => [{"sku"=>"B100", "name"=>"Ruby Book", "price"=>"30", "category"=>"books"}]ruby
This is a good first sprint because it isolates file reading from later business logic.
Example 2: Querying imported rows ๐ก
Once rows are loaded, teams usually ask questions like:
- find a row by SKU;
- list all products in a category;
- calculate a simple average or total.
Why this is useful:
- it shows why parsing should happen once and querying should happen separately;
- it makes CSV feel like a data pipeline rather than a file-format exercise;
- it mirrors small reporting services in real applications.
Cheatsheet
Read CSV with headers
CSV.read(path, headers: true).map(&:to_h)ruby
Write CSV with headers
CSV.open(path, "w") do |csv|
csv << %w[sku name price]
rows.each { |row| csv << [row[:sku], row[:name], row[:price]] }
endruby
Query with Enumerable
rows.select { |row| row["category"] == "books" }
rows.find { |row| row["sku"] == "B100" }
rows.map { |row| row["name"] }ruby
Sprint mindset ๐
- Sprint 1: read rows
- Sprint 2: write rows
- Sprint 3: query imported data through a service
Study Guide
Topic purpose ๐ฏ
Learn to build a simple CSV workflow in small sprints. The emphasis is on design: read first, write second, query third. Students should feel the value of separating file access from business questions.
Study sequence ๐ช
- Read
overview.md. - Read
shortnote.md. - Study
worked_examples.md. - Keep
cheatsheet.mdopen while readingexample.rb. - Complete the basic writer exercise.
- Complete the advanced query exercise.
Sprint plan ๐
Sprint 1: Reader
- Read CSV rows from a file path.
- Use headers so the result is a collection of hashes.
- Keep the reader focused on loading data, not interpreting it.
Sprint 2: Writer
- Write structured rows to a CSV file.
- Preserve a stable header order.
- Return something useful to the caller, such as the path.
Sprint 3: Query service
- Ask simple questions about the imported data.
- Inject the reader so the service does not depend directly on file APIs.
- Use Enumerable to keep reporting logic clear.
Sprint 4: Optional extension ideas
- numeric casting for price and quantity;
- total inventory value report;
- invalid-row handling;
- alternative sources such as StringIO or API payloads.
Reflection questions ๐ค
- Why should the query service depend on a reader instead of calling
CSV.readdirectly? - What would make the writer hard to maintain?
- When should CSV parsing include type conversion, and when should that stay elsewhere?
Source Files and Tests
Ruby source files are converted directly from the English source tree.
# EXAMPLE CODE
# Topic: topic_11_csv_files_queries
#
# Purpose:
# - This file demonstrates a reference implementation for reading CSV data from a file.
# - It should pass tests from the beginning.
# - Read it before solving the writer and query exercises.
require "csv"
class InventoryCsvReader
def load(path)
CSV.read(path, headers: true).map(&:to_h)
end
end
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_11_csv_files_queries
#
# What to do:
# - Write structured rows to a CSV file.
# - Keep header order stable so tests and downstream readers are predictable.
# - Return the path so the caller knows what was written.
#
# Expected outcome:
# - You can export rows to CSV in a small, focused writer object.
require "csv"
class InventoryCsvWriter
HEADERS = %w[sku name price category].freeze
def write(path, rows)
CSV.open(path, "w") do |csv|
csv << HEADERS
rows.each do |row|
csv << [row[:sku], row[:name], row[:price], row[:category]]
end
end
path
end
end
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_11_csv_files_queries
#
# Academic purpose:
# - Practice turning imported CSV rows into a small query service.
# - Learn to keep file reading separate from business questions.
#
# Real-world use case:
# - Admin tools and back-office systems often import CSV files and then need quick lookups
# such as "find this SKU" or "list all rows in this category."
# - A query object keeps those questions away from low-level file handling.
#
# Why Ruby is beautiful here:
# - Arrays of hashes are easy to query with Enumerable.
# - An injected reader makes the service easier to test and replace later.
# - The code stays compact while still describing a real workflow.
#
# What to do:
# - Build a service that loads rows through the reader.
# - Provide a couple of focused query methods.
# - Keep the service about querying, not parsing.
#
# Expected outcome:
# - Advanced tests pass and you can explain the reader/query separation.
class InventoryQueryService
def initialize(reader:)
@reader = reader
end
def find_by_sku(path, sku)
@reader.load(path).find { |row| row["sku"] == sku }
end
def names_in_category(path, category)
@reader.load(path)
.select { |row| row["category"] == category }
.map { |row| row["name"] }
end
end
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_11_csv_files_queries
#
# Solution idea:
# - Use a dedicated writer object so file export stays separate from parsing and queries.
# - Fix the header order up front.
# - Write one row at a time from the hash values in that order.
require "csv"
class InventoryCsvWriter
HEADERS = %w[sku name price category].freeze
def write(path, rows)
CSV.open(path, "w") do |csv|
csv << HEADERS
rows.each do |row|
csv << [row[:sku], row[:name], row[:price], row[:category]]
end
end
path
end
end
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_11_csv_files_queries
#
# Solution idea:
# - Inject the reader so the query service asks for rows instead of reading files directly.
# - Use `find` for a single lookup and `select` + `map` for category listing.
# - Keep the methods small and clearly tied to one business question each.
class InventoryQueryService
def initialize(reader:)
@reader = reader
end
def find_by_sku(path, sku)
@reader.load(path).find { |row| row["sku"] == sku }
end
def names_in_category(path, category)
@reader.load(path)
.select { |row| row["category"] == category }
.map { |row| row["name"] }
end
end
Ruby course source
# This spec is your learning companion for topic_11_csv_files_queries.
#
# How to use this file:
# 1) Run tests and observe failures or successes.
# 2) Keep the EXAMPLE specs green from the beginning.
# 3) Implement the BASIC exercise as sprint 2: writing rows.
# 4) Implement the ADVANCED exercise as sprint 3: querying imported rows.
#
# Expected final result:
# - All examples in this file pass.
# - You understand CSV work as a small pipeline rather than isolated syntax.
require "tempfile"
require_relative "../example"
require_relative "../basic_exercise"
require_relative "../adv_exercise"
RSpec.describe "topic_11_csv_files_queries" do
describe "EXAMPLE purpose: read rows from a CSV file before adding more behavior" do
it "loads rows as hashes using headers" do
file = Tempfile.new(["inventory", ".csv"])
file.write("sku,name,price,category\nB100,Ruby Book,30,books\nA200,Desk Lamp,45,home\n")
file.flush
reader = InventoryCsvReader.new
expect(reader.load(file.path)).to eq(
[
{ "sku" => "B100", "name" => "Ruby Book", "price" => "30", "category" => "books" },
{ "sku" => "A200", "name" => "Desk Lamp", "price" => "45", "category" => "home" }
]
)
ensure
file.close!
end
end
describe "BASIC EXERCISE purpose: write structured rows in a small export sprint" do
it "writes rows with a stable header order" do
file = Tempfile.new(["inventory_export", ".csv"])
rows = [
{ sku: "B100", name: "Ruby Book", price: 30, category: "books" },
{ sku: "A200", name: "Desk Lamp", price: 45, category: "home" }
]
writer = InventoryCsvWriter.new
writer.write(file.path, rows)
expect(File.read(file.path)).to eq(
"sku,name,price,category\nB100,Ruby Book,30,books\nA200,Desk Lamp,45,home\n"
)
ensure
file.close!
end
end
describe "ADVANCED EXERCISE purpose: query imported data through a focused service" do
it "uses an injected reader to find rows and list names by category" do
file = Tempfile.new(["inventory_query", ".csv"])
file.write("sku,name,price,category\nB100,Ruby Book,30,books\nB200,Refactoring,50,books\nA200,Desk Lamp,45,home\n")
file.flush
service = InventoryQueryService.new(reader: InventoryCsvReader.new)
expect(service.find_by_sku(file.path, "B200")).to eq(
{ "sku" => "B200", "name" => "Refactoring", "price" => "50", "category" => "books" }
)
expect(service.names_in_category(file.path, "books")).to eq(["Ruby Book", "Refactoring"])
ensure
file.close!
end
end
end
Ruby course source
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
exec "${ROOT_DIR}/run_tests.sh" "$(basename "${SCRIPT_DIR}")"
Ruby course source
Study Prompts
Read the spec first and identify which behaviors belong to the example, basic exercise, and advanced exercise.
Attempt the exercises before opening the answer files, then compare the design choices.