feat(seed): command plumbing + walking skeleton (ADR-0048 P1.2)
End-to-end `seed <table> [count]` path, both modes: - Command::Seed AST + grammar node (show-data table slot + optional positional count) + REGISTRY registration + build_seed. - Runtime dispatch -> Database::seed -> Request::Seed worker arm -> do_seed. - do_seed (Phase-1 skeleton): generates whole rows for non-FK, non-autogen columns via the seed library and inserts them one at a time through do_insert (reusing validation / autogen autofill / FK-error / persistence). One undo step (snapshot_then wraps it) and one history.log line (only the first row carries the source); default count 20. - help (`help seed`) + parse-usage catalog entries. - Reuses CommandOutcome::Insert for the auto-show; a dedicated SeedResult (capped preview + advisory) replaces it in P1.3. 5 Tier-3 integration tests (parse, populate+persist, default-20, reproducible --seed, one history line). 2327 pass / 0 fail / 0 skip, clippy all-targets clean. Deferred to P1.3: FK sampling, identifier/constraint uniqueness, CHECK derivation, block guard, capped preview, advisory, multi-row path. Deferred to P1.4: completion/highlight/hint/validity wiring + --seed flag.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
//! Tier-3 integration tests for the `seed` command (ADR-0048, the
|
||||
//! Phase-1 walking skeleton). Covers the parse path (grammar → AST),
|
||||
//! the worker round-trip (rows generated + persisted to CSV),
|
||||
//! reproducibility via a fixed `--seed`, and the single `history.log`
|
||||
//! line for the whole command (ADR-0048 D15 / U3).
|
||||
|
||||
use rdbms_playground::db::Database;
|
||||
use rdbms_playground::dsl::{ColumnSpec, Command, Type, parse_command};
|
||||
use rdbms_playground::persistence::Persistence;
|
||||
use rdbms_playground::project;
|
||||
|
||||
fn rt() -> tokio::runtime::Runtime {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio rt")
|
||||
}
|
||||
|
||||
fn open_project_db() -> (project::Project, Database, tempfile::TempDir) {
|
||||
let dir = tempfile::tempdir().expect("create tempdir");
|
||||
let project =
|
||||
project::open_or_create(None, Some(dir.path())).expect("open or create project");
|
||||
let persistence = Persistence::new(project.path().to_path_buf());
|
||||
let db = Database::open_with_persistence(project.db_path(), persistence)
|
||||
.expect("open db with persistence");
|
||||
(project, db, dir)
|
||||
}
|
||||
|
||||
fn read_csv(project: &project::Project, table: &str) -> Option<String> {
|
||||
std::fs::read_to_string(project.path().join("data").join(format!("{table}.csv"))).ok()
|
||||
}
|
||||
|
||||
/// `People(id serial pk, name text, email text)` — `id` is autogen
|
||||
/// (excluded from generation, so no PK collisions), `name`/`email`
|
||||
/// are generated.
|
||||
fn create_people(db: &Database, rt: &tokio::runtime::Runtime) {
|
||||
rt.block_on(db.create_table(
|
||||
"People".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id", Type::Serial),
|
||||
ColumnSpec::new("name", Type::Text),
|
||||
ColumnSpec::new("email", Type::Text),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
))
|
||||
.expect("create People");
|
||||
}
|
||||
|
||||
/// Data rows in a CSV = non-empty lines minus the header.
|
||||
fn data_row_count(csv: &str) -> usize {
|
||||
csv.lines()
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.count()
|
||||
.saturating_sub(1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_parses_with_and_without_count() {
|
||||
match parse_command("seed People 5").expect("`seed People 5` parses") {
|
||||
Command::Seed {
|
||||
table,
|
||||
count,
|
||||
rng_seed,
|
||||
} => {
|
||||
assert_eq!(table, "People");
|
||||
assert_eq!(count, Some(5));
|
||||
assert_eq!(rng_seed, None);
|
||||
}
|
||||
other => panic!("expected Command::Seed, got {other:?}"),
|
||||
}
|
||||
match parse_command("seed People").expect("`seed People` parses") {
|
||||
Command::Seed { table, count, .. } => {
|
||||
assert_eq!(table, "People");
|
||||
assert_eq!(count, None, "omitted count is None (executor defaults to 20)");
|
||||
}
|
||||
other => panic!("expected Command::Seed, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_populates_a_table_and_persists_rows() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_people(&db, &rt);
|
||||
|
||||
let result = rt
|
||||
.block_on(db.seed("People".into(), Some(7), Some(42), Some("seed People 7".into())))
|
||||
.expect("seed succeeds");
|
||||
assert_eq!(result.rows_affected, 7);
|
||||
|
||||
let csv = read_csv(&project, "People").expect("People CSV exists after seed");
|
||||
assert_eq!(
|
||||
data_row_count(&csv),
|
||||
7,
|
||||
"CSV should hold 7 generated rows:\n{csv}"
|
||||
);
|
||||
// The generated `email` column produces address-shaped values.
|
||||
assert!(csv.contains('@'), "seeded emails should appear in the CSV:\n{csv}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_count_defaults_to_twenty() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_people(&db, &rt);
|
||||
|
||||
let result = rt
|
||||
.block_on(db.seed("People".into(), None, Some(1), Some("seed People".into())))
|
||||
.expect("seed succeeds");
|
||||
assert_eq!(result.rows_affected, 20, "omitted count defaults to 20");
|
||||
let csv = read_csv(&project, "People").expect("People CSV exists");
|
||||
assert_eq!(data_row_count(&csv), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_is_reproducible_with_a_fixed_seed() {
|
||||
let (p1, db1, _d1) = open_project_db();
|
||||
let (p2, db2, _d2) = open_project_db();
|
||||
let rt = rt();
|
||||
create_people(&db1, &rt);
|
||||
create_people(&db2, &rt);
|
||||
|
||||
rt.block_on(db1.seed("People".into(), Some(4), Some(123), Some("seed People 4".into())))
|
||||
.expect("seed run 1");
|
||||
rt.block_on(db2.seed("People".into(), Some(4), Some(123), Some("seed People 4".into())))
|
||||
.expect("seed run 2");
|
||||
|
||||
let csv1 = read_csv(&p1, "People").expect("csv 1");
|
||||
let csv2 = read_csv(&p2, "People").expect("csv 2");
|
||||
assert_eq!(csv1, csv2, "the same --seed must reproduce identical data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_writes_exactly_one_history_line() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_people(&db, &rt);
|
||||
|
||||
rt.block_on(db.seed("People".into(), Some(5), Some(1), Some("seed People 5".into())))
|
||||
.expect("seed succeeds");
|
||||
|
||||
let history = std::fs::read_to_string(project.path().join("history.log"))
|
||||
.expect("history.log exists");
|
||||
let seed_lines = history.lines().filter(|l| l.contains("seed People 5")).count();
|
||||
assert_eq!(
|
||||
seed_lines, 1,
|
||||
"a seed of 5 rows must write exactly one history line:\n{history}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user