Files
rdbms-playground/src/dsl/shortid.rs
T
claude@clouddev1 305e5083d5 INSERT/UPDATE/DELETE + value model + auto-show, with polish
DSL data operations (ADR-0014):
- insert into T [(cols)] values (vals); short form
  insert into T (vals) omits values keyword for friendlier
  syntax.
- update T set ... where col=val | --all-rows; delete from T
  where col=val | --all-rows; show data T.
- Value AST (Number/Text/Bool/Null) with per-column-type
  validation in the executor: int/real/decimal/bool/date/
  datetime/shortid each accept a documented literal shape
  and produce friendly format errors naming the column.
- INSERT short form fills non-auto-generated columns in
  schema order; auto-fills serial via SQLite and shortid
  via the new generator (T2).
- `add column [to table] T: c (type)` -- `to table` now
  optional.

Database:
- insert/update/delete via prepared statements with bound
  rusqlite::types::Value parameters.
- InsertResult/UpdateResult/DeleteResult: writes return
  rows_affected plus the affected row(s) only (not the whole
  table), so users see exactly what changed.
- INSERT shows the just-inserted row via last_insert_rowid.
- UPDATE captures matching rowids up-front and fetches them
  post-update -- works even if the UPDATE changed the WHERE
  column.
- DELETE reports per-relationship cascade effects by row-
  count diffing inbound child tables; UPDATE-side cascades
  are not yet detected (would need value diffing).
- query_data formats cells (booleans true/false, NULLs as
  None).

FK error enrichment:
- Now lists both outbound (INSERT/UPDATE relevance) and
  inbound (DELETE/UPDATE on parent relevance) FKs from the
  metadata, so RESTRICT errors point at the children
  blocking the delete.
- RelationshipSelector has a proper Display impl -- "no
  such relationship" reads cleanly.

Relationship display:
- target_table for AddRelationship/DropRelationship now
  returns the parent (1-side); structure rendering after
  add/drop shows that side's "Referenced by:" entry,
  matching the `from <Parent>` direction of the command.
- [ok] summary uses display_subject so relationship
  commands show both endpoints (`from P.col to C.col`)
  rather than a single misleading table name.
- Auto-name format `<Parent>_<pcol>_to_<Child>_<ccol>`
  (matches the from..to direction).

Output rendering and scrolling:
- Wrap-aware scroll: renderer reports both visible-row
  count and total wrapped-row count to App; scroll math
  caps against actual displayable rows. Long lines wrap;
  the bottom line is always reachable; PageUp/PageDown work
  correctly even after paging past the buffer top.
- Multi-line messages (FK error enrichment, cascade summary)
  split into single-line OutputLines at creation time so
  wrap/scroll math agree.

Runtime / events:
- New AppEvent variants for Insert/Update/Delete success
  carrying typed result structs; DslDataSucceeded reserved
  for show-data queries.

Docs:
- ADR-0014 covers data-op grammar, value model, --all-rows
  safety, auto-show.
- requirements.md: C5 done, T2 done, V2 partial (basic data
  view), V5 partial (show data added). New entries: C5a
  complex WHERE expressions; H1 progress note for FK
  enrichment; H1a (strong syntax-help in parse errors).

Tests: 200 passing (183 lib + 17 integration), 0 skipped.
Includes parser, type-validation, DB write/read, FK-failure
enrichment, cascade-delete propagation, focused-auto-show
behaviour, scroll-cap invariants. Clippy clean with nursery
enabled.
2026-05-07 16:33:25 +00:00

116 lines
3.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Shortid generation and validation.
//!
//! Per ADR-0005, the `shortid` user-facing type is a base58
//! random identifier of 1012 characters with no ambiguous
//! glyphs (no `0`/`O`/`I`/`l`). The generator is small enough
//! to live in the DSL crate alongside the type definition.
use rand::RngExt;
/// Base58 alphabet — Bitcoin-style. 0 / O / I / l are excluded
/// because they are easily confused in print.
const ALPHABET: &[u8; 58] =
b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const DEFAULT_LEN: usize = 10;
/// Length bounds accepted on user-supplied shortid values.
pub const MIN_LEN: usize = 10;
pub const MAX_LEN: usize = 12;
/// Generate a fresh shortid using thread-local RNG.
#[must_use]
pub fn generate() -> String {
generate_len(DEFAULT_LEN)
}
#[must_use]
fn generate_len(len: usize) -> String {
let mut rng = rand::rng();
let mut out = String::with_capacity(len);
for _ in 0..len {
let idx = rng.random_range(0..ALPHABET.len());
out.push(ALPHABET[idx] as char);
}
out
}
/// Validate a user-supplied shortid value.
pub fn validate(value: &str) -> Result<(), String> {
if value.len() < MIN_LEN || value.len() > MAX_LEN {
return Err(format!(
"shortid must be {MIN_LEN}{MAX_LEN} characters; got {} character(s)",
value.chars().count()
));
}
for c in value.chars() {
if !ALPHABET.contains(&(c as u8)) {
return Err(format!(
"shortid contains '{c}', which is not in the base58 alphabet \
(no 0, O, I, or l; ASCII letters and digits otherwise)"
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn generated_ids_have_default_length() {
let id = generate();
assert_eq!(id.len(), DEFAULT_LEN);
}
#[test]
fn generated_ids_use_only_base58_alphabet() {
for _ in 0..100 {
let id = generate();
for c in id.chars() {
assert!(
ALPHABET.contains(&(c as u8)),
"char {c:?} not in base58 alphabet"
);
}
}
}
#[test]
fn generated_ids_are_not_all_identical() {
// Probabilistically extremely unlikely with a good RNG;
// catches a wholly broken generator (constant output).
let a = generate();
let b = generate();
let c = generate();
assert!(
a != b || b != c,
"all three generated ids were identical: {a}, {b}, {c}"
);
}
#[test]
fn validate_accepts_well_formed_values() {
assert!(validate("23456789Ab").is_ok()); // 10 chars
assert!(validate("23456789AbCD").is_ok()); // 12 chars
}
#[test]
fn validate_rejects_too_short_or_too_long() {
let err = validate("short").unwrap_err();
assert!(err.contains("characters"));
let err = validate("waytoolongafornow").unwrap_err();
assert!(err.contains("characters"));
}
#[test]
fn validate_rejects_ambiguous_glyphs() {
for bad in ["0aaaaaaaaa", "Oaaaaaaaaa", "Iaaaaaaaaa", "laaaaaaaaa"] {
let err = validate(bad).unwrap_err();
assert!(err.contains("base58"), "for {bad}: {err}");
}
}
}