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:
+32
@@ -771,6 +771,10 @@ impl App {
|
||||
self.handle_dsl_insert_success(&command, &result);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslSeedSucceeded { command, result } => {
|
||||
self.handle_dsl_seed_success(&command, &result);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslUpdateSucceeded {
|
||||
command,
|
||||
result,
|
||||
@@ -2072,6 +2076,34 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a successful `seed` (ADR-0048): the ✓ echo, the seeded-row
|
||||
/// count (with a cap note when the unique-value space ran out), the
|
||||
/// capped preview table (D18), and a Hint-styled advisory naming
|
||||
/// columns filled with generic text that look like fixed value sets
|
||||
/// (D12/D13).
|
||||
fn handle_dsl_seed_success(&mut self, command: &Command, result: &crate::db::SeedResult) {
|
||||
self.note_ok_summary(command);
|
||||
let mut summary = crate::t!(
|
||||
"ok.rows_seeded",
|
||||
count = result.produced,
|
||||
table = result.table
|
||||
);
|
||||
if result.produced < result.requested {
|
||||
summary.push(' ');
|
||||
summary.push_str(&crate::t!("seed.capped", requested = result.requested));
|
||||
}
|
||||
self.note_system(summary);
|
||||
for line in crate::output_render::render_data_table(&result.data) {
|
||||
self.note_system(line);
|
||||
}
|
||||
if !result.advisory_columns.is_empty() {
|
||||
self.push_category_three_prose(crate::t!(
|
||||
"seed.advisory_generic",
|
||||
columns = result.advisory_columns.join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_dsl_update_success(&mut self, command: &Command, result: &UpdateResult) {
|
||||
self.note_ok_summary(command);
|
||||
self.note_system(crate::t!("ok.rows_updated", count = result.rows_affected));
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,10 @@ pub enum AppEvent {
|
||||
command: Command,
|
||||
result: InsertResult,
|
||||
},
|
||||
DslSeedSucceeded {
|
||||
command: Command,
|
||||
result: crate::db::SeedResult,
|
||||
},
|
||||
DslUpdateSucceeded {
|
||||
command: Command,
|
||||
result: UpdateResult,
|
||||
|
||||
@@ -550,7 +550,10 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
||||
("ok.index_dropped_with_column", &["index"]),
|
||||
("ok.rows_deleted", &["count"]),
|
||||
("ok.rows_inserted", &["count"]),
|
||||
("ok.rows_seeded", &["count", "table"]),
|
||||
("ok.rows_updated", &["count"]),
|
||||
("seed.capped", &["requested"]),
|
||||
("seed.advisory_generic", &["columns"]),
|
||||
// ---- Client-side success notes (ADR-0017 §6, ADR-0018 §9) ----
|
||||
("client_side.auto_fill_add_serial", &["count"]),
|
||||
("client_side.auto_fill_add_shortid", &["count"]),
|
||||
|
||||
@@ -983,6 +983,13 @@ db:
|
||||
# template couldn't provide. Re-introduce a key here if a non-English
|
||||
# locale lands.)
|
||||
|
||||
# Seed-command notes (ADR-0048): the cap note when the unique-value
|
||||
# space is exhausted, and the advisory that flags columns filled with
|
||||
# generic text that look like fixed value sets.
|
||||
seed:
|
||||
capped: "(of {requested} requested — ran out of distinct value combinations)"
|
||||
advisory_generic: "{columns} filled with generic text — they look like fixed value sets."
|
||||
|
||||
ok:
|
||||
# ADR-0040: the generic `[ok] <verb> <subject>` summary line was
|
||||
# retired — a successful command's echo line now carries a ✓
|
||||
@@ -990,6 +997,7 @@ ok:
|
||||
# per-operation row-count footers below still convey real payload
|
||||
# and are unchanged.
|
||||
rows_inserted: " {count} row(s) inserted"
|
||||
rows_seeded: " {count} row(s) seeded into {table}"
|
||||
rows_updated: " {count} row(s) updated"
|
||||
rows_deleted: " {count} row(s) deleted"
|
||||
# Shown beneath a `drop column --cascade` summary, once per
|
||||
|
||||
+7
-4
@@ -1492,6 +1492,10 @@ fn spawn_dsl_dispatch(
|
||||
command: command.clone(),
|
||||
result,
|
||||
},
|
||||
Ok(CommandOutcome::Seed(result)) => AppEvent::DslSeedSucceeded {
|
||||
command: command.clone(),
|
||||
result,
|
||||
},
|
||||
Ok(CommandOutcome::Update(result)) => AppEvent::DslUpdateSucceeded {
|
||||
command: command.clone(),
|
||||
result,
|
||||
@@ -2364,6 +2368,7 @@ enum CommandOutcome {
|
||||
ShowRelationship(Option<Box<crate::db::RelationshipDiagramData>>),
|
||||
QueryPlan(QueryPlan),
|
||||
Insert(InsertResult),
|
||||
Seed(crate::db::SeedResult),
|
||||
Update(UpdateResult),
|
||||
Delete(DeleteResult),
|
||||
ChangeColumn(ChangeColumnTypeResult),
|
||||
@@ -2911,9 +2916,7 @@ async fn execute_command_typed(
|
||||
.insert(table, columns, values, src)
|
||||
.await
|
||||
.map(CommandOutcome::Insert),
|
||||
// ADR-0048 (SD1). Phase 1 reuses the insert outcome for the
|
||||
// auto-show; a dedicated `SeedResult` (capped preview +
|
||||
// enum/CHECK advisory) replaces this in a later phase.
|
||||
// ADR-0048 (SD1).
|
||||
Command::Seed {
|
||||
table,
|
||||
count,
|
||||
@@ -2921,7 +2924,7 @@ async fn execute_command_typed(
|
||||
} => database
|
||||
.seed(table, count, rng_seed, src)
|
||||
.await
|
||||
.map(CommandOutcome::Insert),
|
||||
.map(CommandOutcome::Seed),
|
||||
Command::Update {
|
||||
table,
|
||||
assignments,
|
||||
|
||||
Reference in New Issue
Block a user