DSL parser, async DB worker, types, history, metadata, polish

Track 1 implementation plus polish round.

Parser (chumsky):
- Grammar-based DSL producing a typed Command AST.
- create table X with pk [name:type[,name:type...]] supports
  arbitrary names, any user type, compound PKs natively. Bare
  form errors with a friendly hint pointing at `with pk`.
- add column to table X: Name (type); drop table X.
- Required clauses use keyword grammar; -- reserved for opt-in
  flags (ADR-0009). Custom Rich reasons preferred when surfacing
  chumsky errors so unknown-type messages list valid alternatives.

Database (ADR-0010, ADR-0012):
- rusqlite + STRICT tables + foreign_keys=ON.
- Dedicated worker thread; mpsc Request inbox, oneshot replies.
- Typed DbError with friendly_message() hook for H1.
- Internal __rdbms_playground_columns metadata table preserves
  user-facing types across schema reads, atomically maintained
  alongside DDL via Connection transactions. list_tables hides
  it via the new __rdbms_ internal-table convention.

Types (ADR-0005, ADR-0011):
- All ten user-facing types: text, int, real, decimal, bool,
  date, datetime, blob, serial, shortid.
- Type::fk_target_type() for FK-side column-type rule
  (Serial->Int, ShortId->Text, others identity) -- foundation
  for the FK iteration.

App / Runtime / UI:
- update() stays pure-sync; runtime dispatches DSL via spawned
  tasks, results post back as AppEvent::Dsl*.
- Items panel renders live tables list; output panel shows the
  user-facing structure of the current table after each DDL.
- In-memory command history (Up/Down, draft preservation,
  consecutive-duplicate dedup) -- I2 partial.
- Mouse capture removed; terminal native text selection
  restored (toggle approach revisited when scroll/click
  features land).

Docs:
- ADRs 0009 (DSL syntax conventions), 0010 (DB worker),
  0011 (FK type compat), 0012 (internal metadata table).
- requirements.md progress notes; new V4 entry for the
  scrollable session-log + inline rich rendering + Markdown
  export direction.

Tests: 103 passing (91 lib + 12 integration), 0 skipped.
Clippy clean with nursery enabled.
This commit is contained in:
claude@clouddev1
2026-05-07 13:32:19 +00:00
parent 25a0f1260f
commit c1e52920eb
21 changed files with 3186 additions and 120 deletions
+67
View File
@@ -0,0 +1,67 @@
//! The Command AST.
//!
//! `Command` is the parser's output and the database worker's
//! input. Each variant carries fully validated data — the parser
//! is responsible for shape, the database worker for semantics
//! (e.g. "table does not exist").
//!
//! The shape supports compound primary keys natively even though
//! only the dedicated `with pk a:int,b:int` grammar exposes them
//! today. Future grammar extensions (inline column specs, `set
//! primary key`, junction-table convenience commands) emit into
//! the same shape.
use crate::dsl::types::Type;
/// A column at table-creation time: a name and a user-facing
/// type. Constraints beyond `PRIMARY KEY` (NOT NULL, UNIQUE,
/// CHECK, DEFAULT) come in later iterations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnSpec {
pub name: String,
pub ty: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
CreateTable {
name: String,
/// Columns to create, in declaration order.
columns: Vec<ColumnSpec>,
/// Names of columns forming the primary key. Length 1 is
/// a single PK; length >= 2 is a compound PK; length 0
/// indicates no primary key (a future grammar option,
/// not produced by today's parser).
primary_key: Vec<String>,
},
DropTable {
name: String,
},
AddColumn {
table: String,
column: String,
ty: Type,
},
}
impl Command {
/// Short label for log output and result rendering.
#[must_use]
pub const fn verb(&self) -> &'static str {
match self {
Self::CreateTable { .. } => "create table",
Self::DropTable { .. } => "drop table",
Self::AddColumn { .. } => "add column",
}
}
/// The table this command targets — every Command in this
/// iteration operates on exactly one table.
#[must_use]
pub fn target_table(&self) -> &str {
match self {
Self::CreateTable { name, .. } | Self::DropTable { name } => name,
Self::AddColumn { table, .. } => table,
}
}
}