feat(seed): SeedResult outcome, capped preview, advisory, count cap (ADR-0048 P1.3c)

A dedicated SeedResult replaces the borrowed insert outcome (X5):
- CommandOutcome::Seed + DslSeedSucceeded event + handle_dsl_seed_success
  render: the echo, "N row(s) seeded into T", a capped preview table
  (D18, first 20 rows; full count always reported), and a Hint-styled
  advisory naming enum-ish / un-derivable-CHECK columns filled with
  generic text (D12/D13, Phase-1 wording).
- SeedResult carries requested vs produced, so a junction cap is now
  reported to the user, not only logged.
- Count cap (D6): a seed over 10000 rows is refused with a friendly error.
- Catalog keys ok.rows_seeded / seed.capped / seed.advisory_generic.

4 new tests (advisory flag, IN-check not flagged, preview cap, excess
count). 2346 pass / 0 fail / 0 skip, clippy clean.
This commit is contained in:
claude@clouddev1
2026-06-11 19:11:18 +00:00
parent 9c135010ba
commit 0b3ab3cc13
7 changed files with 191 additions and 32 deletions
+68 -20
View File
@@ -287,6 +287,23 @@ pub struct InsertResult {
pub data: DataResult,
}
/// Outcome of a successful `seed` (ADR-0048).
///
/// `produced` is below `requested` when the unique-value space ran out
/// (D14 cap). `data` is a **capped preview** of the seeded rows (D18,
/// not the whole batch). `advisory_columns` names columns that were
/// filled with generic text but look like fixed value sets — enum-ish
/// names or un-derivable CHECKs (D12/D13) — so the render can nudge the
/// user toward choosing those values deliberately.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeedResult {
pub table: String,
pub requested: u64,
pub produced: u64,
pub data: DataResult,
pub advisory_columns: Vec<String>,
}
/// Outcome of a successful `add column …`.
///
/// Carries the post-add structure (used for the auto-show that
@@ -709,7 +726,7 @@ enum Request {
count: Option<u64>,
rng_seed: Option<u64>,
source: Option<String>,
reply: oneshot::Sender<Result<InsertResult, DbError>>,
reply: oneshot::Sender<Result<SeedResult, DbError>>,
},
Update {
table: String,
@@ -1507,7 +1524,7 @@ impl Database {
count: Option<u64>,
rng_seed: Option<u64>,
source: Option<String>,
) -> Result<InsertResult, DbError> {
) -> Result<SeedResult, DbError> {
let (reply, recv) = oneshot::channel();
self.send(Request::Seed {
table,
@@ -8686,6 +8703,14 @@ fn count_rows(conn: &Connection, table: &str) -> Result<i64, DbError> {
/// Default row count when `seed <T>` omits the count (ADR-0048 D6).
const DEFAULT_SEED_COUNT: u64 = 20;
/// Upper bound on a single `seed` (ADR-0048 D6) — a typo like
/// `seed t 1000000` is refused rather than left to hang the app.
const MAX_SEED_COUNT: u64 = 10_000;
/// Cap on rows shown in the post-seed auto-show preview (ADR-0048 D18).
/// The full count is always reported; only the rendered table is capped.
const SEED_PREVIEW_CAP: usize = 20;
/// How a single column's value is produced for each seeded row.
enum SeedColPlan {
/// Generated from the seed library (the generator is chosen once;
@@ -8802,7 +8827,7 @@ fn do_seed(
table: &str,
count: Option<u64>,
rng_seed: Option<u64>,
) -> Result<InsertResult, DbError> {
) -> Result<SeedResult, DbError> {
use crate::seed;
use rand::RngExt;
@@ -8810,6 +8835,12 @@ fn do_seed(
let table = canonical_table.as_str();
let n = count.unwrap_or(DEFAULT_SEED_COUNT);
debug!(table = %table, count = n, "seed");
if n > MAX_SEED_COUNT {
return Err(DbError::Unsupported(format!(
"cannot seed {n} rows at once: the maximum is {MAX_SEED_COUNT}. \
Seed in smaller batches."
)));
}
let schema = read_schema(conn, table)?;
@@ -8839,9 +8870,11 @@ fn do_seed(
}
// Build the per-column generation plan, skipping autogen and
// un-generatable columns.
// un-generatable columns. `advisory_columns` collects columns
// filled with generic text that look like fixed value sets (D12/D13).
let mut col_names: Vec<String> = Vec::new();
let mut plans: Vec<SeedColPlan> = Vec::new();
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.
@@ -8879,6 +8912,15 @@ fn do_seed(
check_in_values,
};
let generator = seed::choose_generator(table, &spec);
// Flag columns that fell through to generic text but look
// like a fixed value set (enum-ish name, or a CHECK we
// could not derive values from) — D12/D13.
if matches!(generator, crate::seed::Generator::Generic)
&& (seed::is_enum_ish(&c.name)
|| (c.check.is_some() && spec.check_in_values.is_none()))
{
advisory_columns.push(c.name.clone());
}
plans.push(SeedColPlan::Generated { generator, ty });
}
}
@@ -8957,8 +8999,12 @@ fn do_seed(
const MAX_ATTEMPTS: u32 = 200;
let mut rng = seed::make_rng(rng_seed);
let mut rows_affected = 0usize;
let mut last_data: Option<DataResult> = None;
let mut preview = DataResult {
table_name: table.to_string(),
columns: Vec::new(),
column_types: Vec::new(),
rows: Vec::new(),
};
let mut accepted: u64 = 0;
let mut capped = false;
@@ -9024,8 +9070,15 @@ fn do_seed(
};
match inserted {
Some(result) => {
rows_affected += result.rows_affected;
last_data = Some(result.data);
// Accumulate the capped preview (D18).
if preview.columns.is_empty() {
preview.columns = result.data.columns;
preview.column_types = result.data.column_types;
}
if preview.rows.len() < SEED_PREVIEW_CAP {
preview.rows.extend(result.data.rows);
preview.rows.truncate(SEED_PREVIEW_CAP);
}
accepted += 1;
}
None => break,
@@ -9037,21 +9090,16 @@ fn do_seed(
table = %table,
requested = n,
produced = accepted,
"seed capped: ran out of distinct unique-value combinations before the \
requested count (user-facing note arrives with the advisory in P1.3c)"
"seed capped: ran out of distinct unique-value combinations before the requested count"
);
}
Ok(InsertResult {
rows_affected,
// `None` only when count was 0 — an empty result for the
// auto-show (the zero-no-op refinement lands in a later phase).
data: last_data.unwrap_or_else(|| DataResult {
table_name: table.to_string(),
columns: Vec::new(),
column_types: Vec::new(),
rows: Vec::new(),
}),
Ok(SeedResult {
table: table.to_string(),
requested: n,
produced: accepted,
data: preview,
advisory_columns,
})
}