feat(seed): --seed flag, ambient wiring, and /runda hardening (ADR-0048 P1.4 + DA)
P1.4 — user-visible surface: - Grammar: `seed <table> [count] [--seed <n>]` (the first DSL flag with a value); build_seed disambiguates the seed value from the positional count. - Verified the auto-wired surface: table-name completion, --seed offered as a candidate, validity consistent with `show data`, an ADR-0042 near-miss row for bare `seed`, and render tests for the seed outcome. /runda hardening — eight DA findings, all resolved: - FK sampling now uses ORDER BY so --seed reproducibility no longer relies on SQLite's unspecified DISTINCT order (D4). - shortid columns now generate from seed's seeded RNG (new shortid::generate_with_rng) — D4 now holds with no exceptions. - Added the missing coverage the DA flagged: undo-one-step (D15), replay re-runs a seed line (D16), advanced-mode (D5), atomic rollback on a constraint failure, seed 0 no-op, complex-CHECK advisory (D17), and FK + shortid reproducibility. 2358 pass / 0 fail / 0 skip, clippy all-targets clean.
This commit is contained in:
+49
-27
@@ -430,16 +430,26 @@ const LIMIT_CLAUSE: Node = Node::Seq(LIMIT_CLAUSE_NODES);
|
||||
// =================================================================
|
||||
|
||||
/// Optional positional row count. Reuses `LIMIT_VALIDATOR` (a
|
||||
/// non-negative integer). Phase 1 has no `--seed` flag, `set` clause,
|
||||
/// or `<table>.<column>` column-fill form yet.
|
||||
/// non-negative integer).
|
||||
const SEED_COUNT: Node = Node::NumberLit {
|
||||
validator: Some(LIMIT_VALIDATOR),
|
||||
};
|
||||
/// `--seed <n>` — a reproducible-generation flag carrying a numeric
|
||||
/// seed (ADR-0048 D4). The only flag in the DSL that takes a value;
|
||||
/// `build_seed` reads the number immediately after the flag.
|
||||
const SEED_FLAG_NODES: &[Node] = &[
|
||||
Node::Flag("seed"),
|
||||
Node::NumberLit {
|
||||
validator: Some(LIMIT_VALIDATOR),
|
||||
},
|
||||
];
|
||||
const SEED_FLAG: Node = Node::Seq(SEED_FLAG_NODES);
|
||||
const SEED_NODES: &[Node] = &[
|
||||
// `writes_table` so a future `set <col>=…` clause's column slots
|
||||
// can resolve against this table.
|
||||
TABLE_NAME_WRITES,
|
||||
Node::Optional(&SEED_COUNT),
|
||||
Node::Optional(&SEED_FLAG),
|
||||
];
|
||||
const SEED_SHAPE: Node = Node::Seq(SEED_NODES);
|
||||
|
||||
@@ -726,36 +736,48 @@ fn build_show_limit(path: &MatchedPath) -> Result<Option<u64>, ValidationError>
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a `seed <T> [<count>]` command (ADR-0048). The only
|
||||
/// `NumberLit` in a `seed` path is the optional count.
|
||||
/// Build a `seed <T> [<count>] [--seed <n>]` command (ADR-0048). The
|
||||
/// `--seed` flag's value is the `NumberLit` right after the flag; the
|
||||
/// positional count is the `NumberLit` *before* the flag (or the only
|
||||
/// one when no flag is present).
|
||||
fn build_seed(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
let table = require_ident(path, "table_name")?;
|
||||
let flag_idx = path
|
||||
.items
|
||||
.iter()
|
||||
.position(|i| matches!(&i.kind, MatchedKind::Flag("seed")));
|
||||
|
||||
let rng_seed = flag_idx
|
||||
.and_then(|fi| path.items.get(fi + 1))
|
||||
.filter(|i| matches!(i.kind, MatchedKind::NumberLit))
|
||||
.map(|i| parse_seed_u64(&i.text))
|
||||
.transpose()?;
|
||||
|
||||
let count = path
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(idx, i)| {
|
||||
matches!(i.kind, MatchedKind::NumberLit) && flag_idx.is_none_or(|fi| *idx < fi)
|
||||
})
|
||||
.map(|(_, i)| parse_seed_u64(&i.text))
|
||||
.transpose()?;
|
||||
|
||||
Ok(Command::Seed {
|
||||
table: require_ident(path, "table_name")?,
|
||||
count: build_seed_count(path)?,
|
||||
// `--seed <n>` is added in a later phase; reproducibility off
|
||||
// for now.
|
||||
rng_seed: None,
|
||||
table,
|
||||
count,
|
||||
rng_seed,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_seed_count(path: &MatchedPath) -> Result<Option<u64>, ValidationError> {
|
||||
let Some(item) = path
|
||||
.items
|
||||
.iter()
|
||||
.find(|i| matches!(i.kind, MatchedKind::NumberLit))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
item.text
|
||||
.parse::<u64>()
|
||||
.map(Some)
|
||||
.map_err(|_| ValidationError {
|
||||
message_key: "parse.custom.bind_type_mismatch",
|
||||
args: vec![
|
||||
("found", item.text.clone()),
|
||||
("expected", "non-negative integer".to_string()),
|
||||
],
|
||||
})
|
||||
fn parse_seed_u64(text: &str) -> Result<u64, ValidationError> {
|
||||
text.parse::<u64>().map_err(|_| ValidationError {
|
||||
message_key: "parse.custom.bind_type_mismatch",
|
||||
args: vec![
|
||||
("found", text.to_string()),
|
||||
("expected", "non-negative integer".to_string()),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
fn build_insert(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||
|
||||
Reference in New Issue
Block a user