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:
@@ -8724,6 +8724,10 @@ enum SeedColPlan {
|
||||
/// column's slot within the parent key tuple (so a compound FK's
|
||||
/// child columns all read from the *same* sampled parent row).
|
||||
ForeignKey { fk_idx: usize, pos: usize },
|
||||
/// A `shortid` column: a base58 id from seed's *seeded* RNG so it
|
||||
/// reproduces under `--seed` (ADR-0048 D4). Always forced — a
|
||||
/// `shortid` column needs an id, never a name-heuristic value.
|
||||
ShortId,
|
||||
}
|
||||
|
||||
/// Collision key for a positional list of seeded values, used to keep
|
||||
@@ -8771,8 +8775,11 @@ fn sample_parent_key_tuples(
|
||||
.map(|c| format!("\"{}\"", c.replace('"', "\"\"")))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
// `ORDER BY` the key columns so the sampled order is deterministic
|
||||
// (ADR-0048 D4): `--seed` reproducibility must not depend on
|
||||
// SQLite's unspecified `DISTINCT` row order.
|
||||
let sql = format!(
|
||||
"SELECT DISTINCT {cols} FROM \"{}\"",
|
||||
"SELECT DISTINCT {cols} FROM \"{}\" ORDER BY {cols}",
|
||||
parent_table.replace('"', "\"\"")
|
||||
);
|
||||
let n = parent_columns.len();
|
||||
@@ -8877,8 +8884,9 @@ fn do_seed(
|
||||
let mut advisory_columns: Vec<String> = Vec::new();
|
||||
for c in &schema.columns {
|
||||
let ty = c.user_type.unwrap_or(Type::Text);
|
||||
// serial/shortid auto-fill in `do_insert`; omit them.
|
||||
if matches!(ty, Type::Serial | Type::ShortId) {
|
||||
// serial auto-fills deterministically in `do_insert` (rowid /
|
||||
// MAX+1) — omit it. shortid is handled below from the seeded RNG.
|
||||
if matches!(ty, Type::Serial) {
|
||||
continue;
|
||||
}
|
||||
// blob has no DSL value path: refuse if required (D1), else omit.
|
||||
@@ -8895,6 +8903,10 @@ fn do_seed(
|
||||
col_names.push(c.name.clone());
|
||||
if let Some(&(fk_idx, pos)) = fk_child_pos.get(c.name.as_str()) {
|
||||
plans.push(SeedColPlan::ForeignKey { fk_idx, pos });
|
||||
} else if matches!(ty, Type::ShortId) {
|
||||
// Always the shortid generator (never a name heuristic — a
|
||||
// shortid column needs a base58 id, not e.g. an email).
|
||||
plans.push(SeedColPlan::ShortId);
|
||||
} else {
|
||||
// A simple `col IN ('a','b')` CHECK becomes the value source
|
||||
// (D17) so the enum-as-CHECK pattern just works.
|
||||
@@ -9028,6 +9040,10 @@ fn do_seed(
|
||||
SeedColPlan::ForeignKey { fk_idx, pos } => {
|
||||
fk_samples[*fk_idx][fk_choice[*fk_idx]][*pos].clone()
|
||||
}
|
||||
// Seeded base58 id → reproducible under `--seed` (D4).
|
||||
SeedColPlan::ShortId => {
|
||||
Value::Text(crate::dsl::shortid::generate_with_rng(&mut rng))
|
||||
}
|
||||
SeedColPlan::Generated { generator, ty }
|
||||
if matches!(generator, crate::seed::Generator::IdentitySequential)
|
||||
&& matches!(ty, Type::Int) =>
|
||||
|
||||
Reference in New Issue
Block a user