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);
|
self.handle_dsl_insert_success(&command, &result);
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
AppEvent::DslSeedSucceeded { command, result } => {
|
||||||
|
self.handle_dsl_seed_success(&command, &result);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
AppEvent::DslUpdateSucceeded {
|
AppEvent::DslUpdateSucceeded {
|
||||||
command,
|
command,
|
||||||
result,
|
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) {
|
fn handle_dsl_update_success(&mut self, command: &Command, result: &UpdateResult) {
|
||||||
self.note_ok_summary(command);
|
self.note_ok_summary(command);
|
||||||
self.note_system(crate::t!("ok.rows_updated", count = result.rows_affected));
|
self.note_system(crate::t!("ok.rows_updated", count = result.rows_affected));
|
||||||
|
|||||||
@@ -287,6 +287,23 @@ pub struct InsertResult {
|
|||||||
pub data: DataResult,
|
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 …`.
|
/// Outcome of a successful `add column …`.
|
||||||
///
|
///
|
||||||
/// Carries the post-add structure (used for the auto-show that
|
/// Carries the post-add structure (used for the auto-show that
|
||||||
@@ -709,7 +726,7 @@ enum Request {
|
|||||||
count: Option<u64>,
|
count: Option<u64>,
|
||||||
rng_seed: Option<u64>,
|
rng_seed: Option<u64>,
|
||||||
source: Option<String>,
|
source: Option<String>,
|
||||||
reply: oneshot::Sender<Result<InsertResult, DbError>>,
|
reply: oneshot::Sender<Result<SeedResult, DbError>>,
|
||||||
},
|
},
|
||||||
Update {
|
Update {
|
||||||
table: String,
|
table: String,
|
||||||
@@ -1507,7 +1524,7 @@ impl Database {
|
|||||||
count: Option<u64>,
|
count: Option<u64>,
|
||||||
rng_seed: Option<u64>,
|
rng_seed: Option<u64>,
|
||||||
source: Option<String>,
|
source: Option<String>,
|
||||||
) -> Result<InsertResult, DbError> {
|
) -> Result<SeedResult, DbError> {
|
||||||
let (reply, recv) = oneshot::channel();
|
let (reply, recv) = oneshot::channel();
|
||||||
self.send(Request::Seed {
|
self.send(Request::Seed {
|
||||||
table,
|
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).
|
/// Default row count when `seed <T>` omits the count (ADR-0048 D6).
|
||||||
const DEFAULT_SEED_COUNT: u64 = 20;
|
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.
|
/// How a single column's value is produced for each seeded row.
|
||||||
enum SeedColPlan {
|
enum SeedColPlan {
|
||||||
/// Generated from the seed library (the generator is chosen once;
|
/// Generated from the seed library (the generator is chosen once;
|
||||||
@@ -8802,7 +8827,7 @@ fn do_seed(
|
|||||||
table: &str,
|
table: &str,
|
||||||
count: Option<u64>,
|
count: Option<u64>,
|
||||||
rng_seed: Option<u64>,
|
rng_seed: Option<u64>,
|
||||||
) -> Result<InsertResult, DbError> {
|
) -> Result<SeedResult, DbError> {
|
||||||
use crate::seed;
|
use crate::seed;
|
||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
|
|
||||||
@@ -8810,6 +8835,12 @@ fn do_seed(
|
|||||||
let table = canonical_table.as_str();
|
let table = canonical_table.as_str();
|
||||||
let n = count.unwrap_or(DEFAULT_SEED_COUNT);
|
let n = count.unwrap_or(DEFAULT_SEED_COUNT);
|
||||||
debug!(table = %table, count = n, "seed");
|
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)?;
|
let schema = read_schema(conn, table)?;
|
||||||
|
|
||||||
@@ -8839,9 +8870,11 @@ fn do_seed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the per-column generation plan, skipping autogen and
|
// 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 col_names: Vec<String> = Vec::new();
|
||||||
let mut plans: Vec<SeedColPlan> = Vec::new();
|
let mut plans: Vec<SeedColPlan> = Vec::new();
|
||||||
|
let mut advisory_columns: Vec<String> = Vec::new();
|
||||||
for c in &schema.columns {
|
for c in &schema.columns {
|
||||||
let ty = c.user_type.unwrap_or(Type::Text);
|
let ty = c.user_type.unwrap_or(Type::Text);
|
||||||
// serial/shortid auto-fill in `do_insert`; omit them.
|
// serial/shortid auto-fill in `do_insert`; omit them.
|
||||||
@@ -8879,6 +8912,15 @@ fn do_seed(
|
|||||||
check_in_values,
|
check_in_values,
|
||||||
};
|
};
|
||||||
let generator = seed::choose_generator(table, &spec);
|
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 });
|
plans.push(SeedColPlan::Generated { generator, ty });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8957,8 +8999,12 @@ fn do_seed(
|
|||||||
const MAX_ATTEMPTS: u32 = 200;
|
const MAX_ATTEMPTS: u32 = 200;
|
||||||
|
|
||||||
let mut rng = seed::make_rng(rng_seed);
|
let mut rng = seed::make_rng(rng_seed);
|
||||||
let mut rows_affected = 0usize;
|
let mut preview = DataResult {
|
||||||
let mut last_data: Option<DataResult> = None;
|
table_name: table.to_string(),
|
||||||
|
columns: Vec::new(),
|
||||||
|
column_types: Vec::new(),
|
||||||
|
rows: Vec::new(),
|
||||||
|
};
|
||||||
let mut accepted: u64 = 0;
|
let mut accepted: u64 = 0;
|
||||||
let mut capped = false;
|
let mut capped = false;
|
||||||
|
|
||||||
@@ -9024,8 +9070,15 @@ fn do_seed(
|
|||||||
};
|
};
|
||||||
match inserted {
|
match inserted {
|
||||||
Some(result) => {
|
Some(result) => {
|
||||||
rows_affected += result.rows_affected;
|
// Accumulate the capped preview (D18).
|
||||||
last_data = Some(result.data);
|
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;
|
accepted += 1;
|
||||||
}
|
}
|
||||||
None => break,
|
None => break,
|
||||||
@@ -9037,21 +9090,16 @@ fn do_seed(
|
|||||||
table = %table,
|
table = %table,
|
||||||
requested = n,
|
requested = n,
|
||||||
produced = accepted,
|
produced = accepted,
|
||||||
"seed capped: ran out of distinct unique-value combinations before the \
|
"seed capped: ran out of distinct unique-value combinations before the requested count"
|
||||||
requested count (user-facing note arrives with the advisory in P1.3c)"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(InsertResult {
|
Ok(SeedResult {
|
||||||
rows_affected,
|
table: table.to_string(),
|
||||||
// `None` only when count was 0 — an empty result for the
|
requested: n,
|
||||||
// auto-show (the zero-no-op refinement lands in a later phase).
|
produced: accepted,
|
||||||
data: last_data.unwrap_or_else(|| DataResult {
|
data: preview,
|
||||||
table_name: table.to_string(),
|
advisory_columns,
|
||||||
columns: Vec::new(),
|
|
||||||
column_types: Vec::new(),
|
|
||||||
rows: Vec::new(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ pub enum AppEvent {
|
|||||||
command: Command,
|
command: Command,
|
||||||
result: InsertResult,
|
result: InsertResult,
|
||||||
},
|
},
|
||||||
|
DslSeedSucceeded {
|
||||||
|
command: Command,
|
||||||
|
result: crate::db::SeedResult,
|
||||||
|
},
|
||||||
DslUpdateSucceeded {
|
DslUpdateSucceeded {
|
||||||
command: Command,
|
command: Command,
|
||||||
result: UpdateResult,
|
result: UpdateResult,
|
||||||
|
|||||||
@@ -550,7 +550,10 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("ok.index_dropped_with_column", &["index"]),
|
("ok.index_dropped_with_column", &["index"]),
|
||||||
("ok.rows_deleted", &["count"]),
|
("ok.rows_deleted", &["count"]),
|
||||||
("ok.rows_inserted", &["count"]),
|
("ok.rows_inserted", &["count"]),
|
||||||
|
("ok.rows_seeded", &["count", "table"]),
|
||||||
("ok.rows_updated", &["count"]),
|
("ok.rows_updated", &["count"]),
|
||||||
|
("seed.capped", &["requested"]),
|
||||||
|
("seed.advisory_generic", &["columns"]),
|
||||||
// ---- Client-side success notes (ADR-0017 §6, ADR-0018 §9) ----
|
// ---- Client-side success notes (ADR-0017 §6, ADR-0018 §9) ----
|
||||||
("client_side.auto_fill_add_serial", &["count"]),
|
("client_side.auto_fill_add_serial", &["count"]),
|
||||||
("client_side.auto_fill_add_shortid", &["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
|
# template couldn't provide. Re-introduce a key here if a non-English
|
||||||
# locale lands.)
|
# 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:
|
ok:
|
||||||
# ADR-0040: the generic `[ok] <verb> <subject>` summary line was
|
# ADR-0040: the generic `[ok] <verb> <subject>` summary line was
|
||||||
# retired — a successful command's echo line now carries a ✓
|
# 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
|
# per-operation row-count footers below still convey real payload
|
||||||
# and are unchanged.
|
# and are unchanged.
|
||||||
rows_inserted: " {count} row(s) inserted"
|
rows_inserted: " {count} row(s) inserted"
|
||||||
|
rows_seeded: " {count} row(s) seeded into {table}"
|
||||||
rows_updated: " {count} row(s) updated"
|
rows_updated: " {count} row(s) updated"
|
||||||
rows_deleted: " {count} row(s) deleted"
|
rows_deleted: " {count} row(s) deleted"
|
||||||
# Shown beneath a `drop column --cascade` summary, once per
|
# Shown beneath a `drop column --cascade` summary, once per
|
||||||
|
|||||||
+7
-4
@@ -1492,6 +1492,10 @@ fn spawn_dsl_dispatch(
|
|||||||
command: command.clone(),
|
command: command.clone(),
|
||||||
result,
|
result,
|
||||||
},
|
},
|
||||||
|
Ok(CommandOutcome::Seed(result)) => AppEvent::DslSeedSucceeded {
|
||||||
|
command: command.clone(),
|
||||||
|
result,
|
||||||
|
},
|
||||||
Ok(CommandOutcome::Update(result)) => AppEvent::DslUpdateSucceeded {
|
Ok(CommandOutcome::Update(result)) => AppEvent::DslUpdateSucceeded {
|
||||||
command: command.clone(),
|
command: command.clone(),
|
||||||
result,
|
result,
|
||||||
@@ -2364,6 +2368,7 @@ enum CommandOutcome {
|
|||||||
ShowRelationship(Option<Box<crate::db::RelationshipDiagramData>>),
|
ShowRelationship(Option<Box<crate::db::RelationshipDiagramData>>),
|
||||||
QueryPlan(QueryPlan),
|
QueryPlan(QueryPlan),
|
||||||
Insert(InsertResult),
|
Insert(InsertResult),
|
||||||
|
Seed(crate::db::SeedResult),
|
||||||
Update(UpdateResult),
|
Update(UpdateResult),
|
||||||
Delete(DeleteResult),
|
Delete(DeleteResult),
|
||||||
ChangeColumn(ChangeColumnTypeResult),
|
ChangeColumn(ChangeColumnTypeResult),
|
||||||
@@ -2911,9 +2916,7 @@ async fn execute_command_typed(
|
|||||||
.insert(table, columns, values, src)
|
.insert(table, columns, values, src)
|
||||||
.await
|
.await
|
||||||
.map(CommandOutcome::Insert),
|
.map(CommandOutcome::Insert),
|
||||||
// ADR-0048 (SD1). Phase 1 reuses the insert outcome for the
|
// ADR-0048 (SD1).
|
||||||
// auto-show; a dedicated `SeedResult` (capped preview +
|
|
||||||
// enum/CHECK advisory) replaces this in a later phase.
|
|
||||||
Command::Seed {
|
Command::Seed {
|
||||||
table,
|
table,
|
||||||
count,
|
count,
|
||||||
@@ -2921,7 +2924,7 @@ async fn execute_command_typed(
|
|||||||
} => database
|
} => database
|
||||||
.seed(table, count, rng_seed, src)
|
.seed(table, count, rng_seed, src)
|
||||||
.await
|
.await
|
||||||
.map(CommandOutcome::Insert),
|
.map(CommandOutcome::Seed),
|
||||||
Command::Update {
|
Command::Update {
|
||||||
table,
|
table,
|
||||||
assignments,
|
assignments,
|
||||||
|
|||||||
+69
-8
@@ -87,7 +87,7 @@ fn seed_populates_a_table_and_persists_rows() {
|
|||||||
let result = rt
|
let result = rt
|
||||||
.block_on(db.seed("People".into(), Some(7), Some(42), Some("seed People 7".into())))
|
.block_on(db.seed("People".into(), Some(7), Some(42), Some("seed People 7".into())))
|
||||||
.expect("seed succeeds");
|
.expect("seed succeeds");
|
||||||
assert_eq!(result.rows_affected, 7);
|
assert_eq!(result.produced, 7);
|
||||||
|
|
||||||
let csv = read_csv(&project, "People").expect("People CSV exists after seed");
|
let csv = read_csv(&project, "People").expect("People CSV exists after seed");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -108,7 +108,7 @@ fn seed_count_defaults_to_twenty() {
|
|||||||
let result = rt
|
let result = rt
|
||||||
.block_on(db.seed("People".into(), None, Some(1), Some("seed People".into())))
|
.block_on(db.seed("People".into(), None, Some(1), Some("seed People".into())))
|
||||||
.expect("seed succeeds");
|
.expect("seed succeeds");
|
||||||
assert_eq!(result.rows_affected, 20, "omitted count defaults to 20");
|
assert_eq!(result.produced, 20, "omitted count defaults to 20");
|
||||||
let csv = read_csv(&project, "People").expect("People CSV exists");
|
let csv = read_csv(&project, "People").expect("People CSV exists");
|
||||||
assert_eq!(data_row_count(&csv), 20);
|
assert_eq!(data_row_count(&csv), 20);
|
||||||
}
|
}
|
||||||
@@ -217,7 +217,7 @@ fn seed_fills_foreign_keys_from_existing_parents() {
|
|||||||
let res = rt
|
let res = rt
|
||||||
.block_on(db.seed("Orders".into(), Some(10), Some(2), Some("seed Orders 10".into())))
|
.block_on(db.seed("Orders".into(), Some(10), Some(2), Some("seed Orders 10".into())))
|
||||||
.expect("seed Orders");
|
.expect("seed Orders");
|
||||||
assert_eq!(res.rows_affected, 10, "every child row must insert (valid FK)");
|
assert_eq!(res.produced, 10, "every child row must insert (valid FK)");
|
||||||
|
|
||||||
let csv = read_csv(&project, "Orders").expect("Orders CSV");
|
let csv = read_csv(&project, "Orders").expect("Orders CSV");
|
||||||
let valid: std::collections::HashSet<String> = (1..=5).map(|i| i.to_string()).collect();
|
let valid: std::collections::HashSet<String> = (1..=5).map(|i| i.to_string()).collect();
|
||||||
@@ -294,7 +294,7 @@ fn seed_omits_a_nullable_blob_column() {
|
|||||||
let res = rt
|
let res = rt
|
||||||
.block_on(db.seed("Files".into(), Some(3), Some(1), Some("seed Files 3".into())))
|
.block_on(db.seed("Files".into(), Some(3), Some(1), Some("seed Files 3".into())))
|
||||||
.expect("seed succeeds despite the nullable blob");
|
.expect("seed succeeds despite the nullable blob");
|
||||||
assert_eq!(res.rows_affected, 3);
|
assert_eq!(res.produced, 3);
|
||||||
let csv = read_csv(&project, "Files").expect("Files CSV");
|
let csv = read_csv(&project, "Files").expect("Files CSV");
|
||||||
assert_eq!(data_row_count(&csv), 3);
|
assert_eq!(data_row_count(&csv), 3);
|
||||||
}
|
}
|
||||||
@@ -328,7 +328,7 @@ fn seed_keeps_unique_columns_distinct() {
|
|||||||
let res = rt
|
let res = rt
|
||||||
.block_on(db.seed("Tags".into(), Some(8), Some(3), Some("seed Tags 8".into())))
|
.block_on(db.seed("Tags".into(), Some(8), Some(3), Some("seed Tags 8".into())))
|
||||||
.expect("seed");
|
.expect("seed");
|
||||||
assert_eq!(res.rows_affected, 8);
|
assert_eq!(res.produced, 8);
|
||||||
|
|
||||||
let csv = read_csv(&project, "Tags").expect("Tags CSV");
|
let csv = read_csv(&project, "Tags").expect("Tags CSV");
|
||||||
let labels = nth_column_values(&csv, 1);
|
let labels = nth_column_values(&csv, 1);
|
||||||
@@ -357,7 +357,7 @@ fn seed_sequences_identifier_int_columns() {
|
|||||||
let res = rt
|
let res = rt
|
||||||
.block_on(db.seed("Items".into(), Some(5), Some(1), Some("seed Items 5".into())))
|
.block_on(db.seed("Items".into(), Some(5), Some(1), Some("seed Items 5".into())))
|
||||||
.expect("seed");
|
.expect("seed");
|
||||||
assert_eq!(res.rows_affected, 5);
|
assert_eq!(res.produced, 5);
|
||||||
|
|
||||||
let csv = read_csv(&project, "Items").expect("Items CSV");
|
let csv = read_csv(&project, "Items").expect("Items CSV");
|
||||||
let codes: Vec<i64> = nth_column_values(&csv, 1)
|
let codes: Vec<i64> = nth_column_values(&csv, 1)
|
||||||
@@ -431,7 +431,8 @@ fn seed_junction_produces_distinct_combinations_and_caps() {
|
|||||||
.seed("J".into(), Some(10), Some(7), Some("seed J 10".into()))
|
.seed("J".into(), Some(10), Some(7), Some("seed J 10".into()))
|
||||||
.await
|
.await
|
||||||
.expect("seed J");
|
.expect("seed J");
|
||||||
assert_eq!(res.rows_affected, 4, "junction caps at available combos");
|
assert_eq!(res.produced, 4, "junction caps at available combos");
|
||||||
|
assert_eq!(res.requested, 10, "the requested count is reported for the cap note");
|
||||||
});
|
});
|
||||||
|
|
||||||
let csv = read_csv(&project, "J").expect("J CSV");
|
let csv = read_csv(&project, "J").expect("J CSV");
|
||||||
@@ -463,7 +464,7 @@ fn seed_draws_enum_values_from_an_in_check() {
|
|||||||
let res = rt
|
let res = rt
|
||||||
.block_on(db.seed("Tickets".into(), Some(12), Some(2), Some("seed Tickets 12".into())))
|
.block_on(db.seed("Tickets".into(), Some(12), Some(2), Some("seed Tickets 12".into())))
|
||||||
.expect("seed");
|
.expect("seed");
|
||||||
assert_eq!(res.rows_affected, 12, "all rows insert — values satisfy the CHECK");
|
assert_eq!(res.produced, 12, "all rows insert — values satisfy the CHECK");
|
||||||
|
|
||||||
let csv = read_csv(&project, "Tickets").expect("Tickets CSV");
|
let csv = read_csv(&project, "Tickets").expect("Tickets CSV");
|
||||||
for v in nth_column_values(&csv, 1) {
|
for v in nth_column_values(&csv, 1) {
|
||||||
@@ -472,4 +473,64 @@ fn seed_draws_enum_values_from_an_in_check() {
|
|||||||
"status `{v}` was not drawn from the IN check:\n{csv}"
|
"status `{v}` was not drawn from the IN check:\n{csv}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// The IN-check column is derived, not generic, so it is NOT flagged.
|
||||||
|
assert!(
|
||||||
|
res.advisory_columns.is_empty(),
|
||||||
|
"an IN-check column should not be flagged: {:?}",
|
||||||
|
res.advisory_columns
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seed_advises_on_enum_ish_columns() {
|
||||||
|
let (_project, db, _dir) = open_project_db();
|
||||||
|
let rt = rt();
|
||||||
|
// `status` has no CHECK and no name heuristic → generic text, so it
|
||||||
|
// is flagged for the advisory (D12/D13).
|
||||||
|
rt.block_on(db.create_table(
|
||||||
|
"Tasks".to_string(),
|
||||||
|
vec![
|
||||||
|
ColumnSpec::new("id", Type::Serial),
|
||||||
|
ColumnSpec::new("status", Type::Text),
|
||||||
|
],
|
||||||
|
vec!["id".to_string()],
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.expect("create Tasks");
|
||||||
|
|
||||||
|
let res = rt
|
||||||
|
.block_on(db.seed("Tasks".into(), Some(3), Some(1), Some("seed Tasks 3".into())))
|
||||||
|
.expect("seed");
|
||||||
|
assert!(
|
||||||
|
res.advisory_columns.contains(&"status".to_string()),
|
||||||
|
"enum-ish `status` should be flagged: {:?}",
|
||||||
|
res.advisory_columns
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seed_refuses_an_excessive_count() {
|
||||||
|
let (_project, db, _dir) = open_project_db();
|
||||||
|
let rt = rt();
|
||||||
|
create_people(&db, &rt);
|
||||||
|
let err = rt
|
||||||
|
.block_on(db.seed("People".into(), Some(1_000_000), Some(1), Some("seed People 1000000".into())))
|
||||||
|
.expect_err("an excessive count must be refused");
|
||||||
|
assert!(
|
||||||
|
err.to_string().to_lowercase().contains("maximum"),
|
||||||
|
"error should mention the maximum: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seed_preview_is_capped_but_count_is_full() {
|
||||||
|
let (_project, db, _dir) = open_project_db();
|
||||||
|
let rt = rt();
|
||||||
|
create_people(&db, &rt);
|
||||||
|
|
||||||
|
let res = rt
|
||||||
|
.block_on(db.seed("People".into(), Some(25), Some(1), Some("seed People 25".into())))
|
||||||
|
.expect("seed");
|
||||||
|
assert_eq!(res.produced, 25, "the full count is produced");
|
||||||
|
assert_eq!(res.data.rows.len(), 20, "the preview is capped at 20 rows");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user