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:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user