ADR-0018 implementation: auto-fill contracts for serial and shortid
Generalises serial and shortid beyond their previous restricted forms: - `serial` is no longer restricted to single-column PK. Non-PK serial columns get an emitted UNIQUE constraint and use application-side MAX(col)+1 at INSERT time (rowid alias still drives the PK case for free; per ADR-0010 worker-thread serialisation, the read-then-insert sequence is safe). - `shortid` columns auto-fill existing null cells when the column is materialised — `add column T: x (shortid)` on a non-empty table no longer leaves rows in a not-really-valid NULL state. - `int -> serial` joins the type-change matrix as always-clean identity (closes the asymmetry vs `text -> shortid`); other sources are refused with a route-via-int hint. - `change column T: x (serial|shortid)` fills null source cells with sequence / generated values in the same rebuild transaction. Internal infrastructure: - ReadColumn gains `unique: bool`; read_schema detects single- column UNIQUE indexes via pragma_index_list / pragma_index_info; schema_to_ddl emits inline UNIQUE for non-PK columns. - ColumnSchema (persistence) gains `unique: bool` so the flag survives YAML round-trip and rebuild-from-text reconstructs it faithfully — preserves the "serial -> int leaves UNIQUE in place" promise across save/load cycles. - ChangeColumnTypeResult.client_side now carries `auto_filled` + `auto_fill_kind` alongside `transformed` + `lossy`; the app handler renders separate note lines when both apply. - AddColumnResult is a new return type carrying pre-rendered [client-side] note lines for the auto-fill paths. Tests: 519 -> 534 (+15). Clippy clean.
This commit is contained in:
+64
-24
@@ -13,8 +13,8 @@ use tracing::{trace, warn};
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::db::{
|
||||
CascadeEffect, ChangeColumnTypeResult, DataResult, DeleteResult, InsertResult,
|
||||
TableDescription, UpdateResult,
|
||||
AddColumnResult, CascadeEffect, ChangeColumnTypeResult, DataResult, DeleteResult,
|
||||
InsertResult, TableDescription, UpdateResult,
|
||||
};
|
||||
use crate::dsl::{Command, ParseError, parse_command};
|
||||
use crate::event::AppEvent;
|
||||
@@ -317,6 +317,10 @@ impl App {
|
||||
self.handle_dsl_change_column_success(&command, result);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslAddColumnSucceeded { command, result } => {
|
||||
self.handle_dsl_add_column_success(&command, result);
|
||||
Vec::new()
|
||||
}
|
||||
AppEvent::DslFailed { command, error } => {
|
||||
self.handle_dsl_failure(&command, &error);
|
||||
Vec::new()
|
||||
@@ -782,6 +786,26 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_dsl_add_column_success(
|
||||
&mut self,
|
||||
command: &Command,
|
||||
result: AddColumnResult,
|
||||
) {
|
||||
let summary = format!("[ok] {} {}", command.verb(), command.display_subject());
|
||||
self.note_system(summary);
|
||||
// ADR-0018 §9: emit auto-fill note(s) before the
|
||||
// structure render, so the pedagogical "the tool did
|
||||
// this for you" line is in the user's eye-line next to
|
||||
// the success summary.
|
||||
for note in result.client_side_notes {
|
||||
self.note_system(note);
|
||||
}
|
||||
for line in crate::output_render::render_structure(&result.description) {
|
||||
self.note_system(line);
|
||||
}
|
||||
self.current_table = Some(result.description);
|
||||
}
|
||||
|
||||
fn handle_dsl_change_column_success(
|
||||
&mut self,
|
||||
command: &Command,
|
||||
@@ -790,28 +814,44 @@ impl App {
|
||||
let summary = format!("[ok] {} {}", command.verb(), command.display_subject());
|
||||
self.note_system(summary);
|
||||
if let Some(note) = result.client_side {
|
||||
// ADR-0017 §6: pedagogical hook telling the learner
|
||||
// "the tool did this for you; raw SQL would need a
|
||||
// CAST or application-level code." Lossy variant
|
||||
// adds the lossy count when --force-conversion was
|
||||
// used.
|
||||
let line = if note.lossy > 0 {
|
||||
format!(
|
||||
"[client-side] {n} row(s) transformed; {l} of those discarded \
|
||||
information (lossy). In raw SQL this would need an explicit \
|
||||
`CAST` or application-level code.",
|
||||
n = note.transformed,
|
||||
l = note.lossy
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"[client-side] {n} row(s) were transformed before being stored. \
|
||||
In raw SQL this would need an explicit `CAST` or \
|
||||
application-level code.",
|
||||
n = note.transformed
|
||||
)
|
||||
};
|
||||
self.note_system(line);
|
||||
// ADR-0017 §6 + ADR-0018 §9: pedagogical hook
|
||||
// telling the learner "the tool did this for you;
|
||||
// raw SQL would need explicit CAST / UPDATE / etc."
|
||||
//
|
||||
// When both transformations and auto-fills happen
|
||||
// in the same operation, both note lines are
|
||||
// emitted in order (ADR-0018 §9 explicit rule).
|
||||
if note.transformed > 0 {
|
||||
let line = if note.lossy > 0 {
|
||||
format!(
|
||||
"[client-side] {n} row(s) transformed; {l} of those discarded \
|
||||
information (lossy). In raw SQL this would need an explicit \
|
||||
`CAST` or application-level code.",
|
||||
n = note.transformed,
|
||||
l = note.lossy
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"[client-side] {n} row(s) were transformed before being stored. \
|
||||
In raw SQL this would need an explicit `CAST` or \
|
||||
application-level code.",
|
||||
n = note.transformed
|
||||
)
|
||||
};
|
||||
self.note_system(line);
|
||||
}
|
||||
if note.auto_filled > 0 {
|
||||
let kind = match note.auto_fill_kind {
|
||||
Some(crate::db::AutoFillKind::Serial) => "serial",
|
||||
Some(crate::db::AutoFillKind::ShortId) => "shortid",
|
||||
None => "auto-generated",
|
||||
};
|
||||
self.note_system(format!(
|
||||
"[client-side] {m} null cell(s) given auto-generated {kind} values. \
|
||||
In raw SQL this would need an explicit UPDATE to populate.",
|
||||
m = note.auto_filled,
|
||||
));
|
||||
}
|
||||
}
|
||||
for line in crate::output_render::render_structure(&result.description) {
|
||||
self.note_system(line);
|
||||
|
||||
Reference in New Issue
Block a user