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:
claude@clouddev1
2026-05-08 14:32:19 +00:00
parent 7dfa718c6e
commit 5bb0a147f0
10 changed files with 1718 additions and 97 deletions
+8 -3
View File
@@ -29,8 +29,8 @@ use crate::action::Action;
use crate::app::App;
use crate::cli::Args;
use crate::db::{
ChangeColumnTypeResult, DataResult, Database, DbError, DeleteResult, InsertResult,
TableDescription, UpdateResult,
AddColumnResult, ChangeColumnTypeResult, DataResult, Database, DbError, DeleteResult,
InsertResult, TableDescription, UpdateResult,
};
use crate::dsl::Command;
use crate::event::AppEvent;
@@ -951,6 +951,10 @@ fn spawn_dsl_dispatch(
command: command.clone(),
result,
},
Ok(CommandOutcome::AddColumn(result)) => AppEvent::DslAddColumnSucceeded {
command: command.clone(),
result,
},
Err(DbError::PersistenceFatal {
operation,
path,
@@ -988,6 +992,7 @@ enum CommandOutcome {
Update(UpdateResult),
Delete(DeleteResult),
ChangeColumn(ChangeColumnTypeResult),
AddColumn(AddColumnResult),
}
/// Execute a parsed user command and return either a typed
@@ -1016,7 +1021,7 @@ async fn execute_command_typed(
Command::AddColumn { table, column, ty } => database
.add_column(table, column, ty, src)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
.map(CommandOutcome::AddColumn),
Command::DropColumn { table, column } => database
.drop_column(table, column, src)
.await