ADR-0019 §9 sweep (2/2): help blocks + modals + system notes
Final pass of the i18n migration sweep. Every user-visible
string in `src/` now flows through the catalog via `t!()`.
## Categories migrated in this commit
- **help.cli_banner** — the entire `cli::HELP_TEXT` const,
formerly a 40-line `&'static str`, is now a YAML block in
the catalog. The const is replaced by a thin
`cli::help_text() -> String` wrapper that performs the
catalog lookup. `main.rs` calls `help_text()` for both
`--help` output and the args-parse error path. The two
integration tests that referenced `HELP_TEXT` directly are
updated.
- **help.in_app_body** — the in-app `help` command's body is
one YAML block; `note_help` becomes 5 lines that iterate
the lines and emit each as its own output row (preserving
the renderer's "one logical line = one display row"
invariant for accurate scroll math).
- **modal.*** — load picker, rebuild confirm, and save-as
path-entry strings: rebuild_cancelled, load_cancelled,
generic_cancelled, load_picker_nothing,
path_entry_empty_name, path_entry_empty_path.
- **dsl.failed** — the `"<verb> <subject>" failed: <rendered>`
wrapper around the friendly-error layer's translated
message.
- **dsl.running** — the `running: <input>` echo line shown
above each command's response. (Note: the en-US prefix
"running: " is hardcoded in the parse-error caret-padding
calculation. Translators changing the prefix must keep the
width consistent — documented inline.)
- **advanced_mode.not_implemented** — the placeholder echo
shown when SQL hits the unimplemented advanced-mode path
(Q1 territory).
- **fatal.persistence** — the FATAL banner for
PersistenceFatal events (ADR-0015 §8).
- **project.{load_path_missing,saveas_target_exists,**
**import_zip_missing}** — runtime-side project-switch
validation errors that surface via ProjectSwitchFailed.
## Catalog start-up ordering
`main.rs` now calls `friendly::catalog()` at the very top
(before args parsing) so `help_text()` works in both the
success path and the args-error path. A corrupted build
artefact still fails loudly with a useful panic; the
practical risk is essentially zero since the catalog is
`include_str!`'d at compile time and validated by the unit
test before shipping.
## Remaining literals
The only `note_*` calls in `src/` that still pass plain
strings are inside `#[cfg(test)]` modules — synthetic test
fixtures, not user-visible. The codebase passes the "every
user-visible string flows through the catalog" bar.
## Tally
610 tests passing (no change in count — pure refactor).
Clippy clean with nursery lints.
## What this closes
ADR-0019 §9 (migration sweep) — done.
ADR-0019 itself is now fully implemented:
- §1-§5: catalog + translator + voice + verbosity ✓ (`eac7e5b`)
- §6: row pinpointing + schema enrichment ✓ (`431645a`)
- §9: migration sweep ✓ (this + `aff528a`)
- §10: anchor phrases preserved throughout ✓
- The five "Out of scope" items remain explicitly bounded
to future ADRs (advanced-mode SQL, settings persistence,
pluralisation, runtime locale, value formatting,
constraint management).
This commit is contained in:
+38
-66
@@ -344,10 +344,11 @@ impl App {
|
|||||||
path,
|
path,
|
||||||
message,
|
message,
|
||||||
} => {
|
} => {
|
||||||
let banner = format!(
|
let banner = crate::t!(
|
||||||
"FATAL: failed to {operation} `{}` — {message}. \
|
"fatal.persistence",
|
||||||
Quitting; investigate and restart.",
|
operation = operation,
|
||||||
path.display(),
|
path = path.display(),
|
||||||
|
message = message
|
||||||
);
|
);
|
||||||
self.note_error(banner.clone());
|
self.note_error(banner.clone());
|
||||||
self.fatal_message = Some(banner);
|
self.fatal_message = Some(banner);
|
||||||
@@ -754,8 +755,9 @@ impl App {
|
|||||||
// until the advanced-mode SQL path lands. Once it does,
|
// until the advanced-mode SQL path lands. Once it does,
|
||||||
// this branch parses with sqlparser-rs and dispatches
|
// this branch parses with sqlparser-rs and dispatches
|
||||||
// analogously to the DSL path below.
|
// analogously to the DSL path below.
|
||||||
self.note_system(format!(
|
self.note_system(crate::t!(
|
||||||
"advanced mode SQL not implemented yet — echo: {effective_input}"
|
"advanced_mode.not_implemented",
|
||||||
|
input = effective_input
|
||||||
));
|
));
|
||||||
self.push_output(OutputLine {
|
self.push_output(OutputLine {
|
||||||
text: effective_input,
|
text: effective_input,
|
||||||
@@ -785,7 +787,7 @@ impl App {
|
|||||||
// keeping the worker out of the loop and the
|
// keeping the worker out of the loop and the
|
||||||
// history.log clean.
|
// history.log clean.
|
||||||
self.push_output(OutputLine {
|
self.push_output(OutputLine {
|
||||||
text: format!("running: {input}"),
|
text: crate::t!("dsl.running", input = input),
|
||||||
kind: OutputKind::Echo,
|
kind: OutputKind::Echo,
|
||||||
mode_at_submission: submission_mode,
|
mode_at_submission: submission_mode,
|
||||||
});
|
});
|
||||||
@@ -793,7 +795,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
Ok(cmd) => {
|
Ok(cmd) => {
|
||||||
self.push_output(OutputLine {
|
self.push_output(OutputLine {
|
||||||
text: format!("running: {input}"),
|
text: crate::t!("dsl.running", input = input),
|
||||||
kind: OutputKind::Echo,
|
kind: OutputKind::Echo,
|
||||||
mode_at_submission: submission_mode,
|
mode_at_submission: submission_mode,
|
||||||
});
|
});
|
||||||
@@ -807,13 +809,19 @@ impl App {
|
|||||||
// Echo the source line so the user can see what
|
// Echo the source line so the user can see what
|
||||||
// got submitted (and copy-paste it back to fix).
|
// got submitted (and copy-paste it back to fix).
|
||||||
self.push_output(OutputLine {
|
self.push_output(OutputLine {
|
||||||
text: format!("running: {input}"),
|
text: crate::t!("dsl.running", input = input),
|
||||||
kind: OutputKind::Echo,
|
kind: OutputKind::Echo,
|
||||||
mode_at_submission: submission_mode,
|
mode_at_submission: submission_mode,
|
||||||
});
|
});
|
||||||
// Caret pointer at the failure position, when we
|
// Caret pointer at the failure position, when we
|
||||||
// have one. Aligned to the "running: " prefix so
|
// have one. Aligned to the "running: " prefix so
|
||||||
// the caret sits under the offending character.
|
// the caret sits under the offending character.
|
||||||
|
//
|
||||||
|
// Note: the prefix length is hardcoded against
|
||||||
|
// the en-US `dsl.running` template ("running:
|
||||||
|
// {input}"). A translator changing that prefix
|
||||||
|
// must update this width too — the constraint is
|
||||||
|
// captured in the catalog comment block.
|
||||||
if let ParseError::Invalid { position, .. } = &err {
|
if let ParseError::Invalid { position, .. } = &err {
|
||||||
let prefix = "running: ";
|
let prefix = "running: ";
|
||||||
let trimmed_offset = leading_trim_offset(input);
|
let trimmed_offset = leading_trim_offset(input);
|
||||||
@@ -976,10 +984,11 @@ impl App {
|
|||||||
// `note_error` splits on newlines internally — refusal
|
// `note_error` splits on newlines internally — refusal
|
||||||
// diagnostics from `change column …` (ADR-0017 §7) flow
|
// diagnostics from `change column …` (ADR-0017 §7) flow
|
||||||
// through as a multi-line bordered table.
|
// through as a multi-line bordered table.
|
||||||
self.note_error(format!(
|
self.note_error(crate::t!(
|
||||||
"\"{} {}\" failed: {rendered}",
|
"dsl.failed",
|
||||||
command.verb(),
|
verb = command.verb(),
|
||||||
command.display_subject()
|
subject = command.display_subject(),
|
||||||
|
rendered = rendered
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1164,7 +1173,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||||
self.modal = None;
|
self.modal = None;
|
||||||
self.note_system("rebuild cancelled");
|
self.note_system(crate::t!("modal.rebuild_cancelled"));
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
@@ -1179,13 +1188,16 @@ impl App {
|
|||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
self.modal = None;
|
self.modal = None;
|
||||||
self.note_system(format!("{} cancelled", state.title.to_lowercase()));
|
self.note_system(crate::t!(
|
||||||
|
"modal.generic_cancelled",
|
||||||
|
title = state.title.to_lowercase()
|
||||||
|
));
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let target = state.input.trim().to_string();
|
let target = state.input.trim().to_string();
|
||||||
if target.is_empty() {
|
if target.is_empty() {
|
||||||
self.note_error("path entry: empty name");
|
self.note_error(crate::t!("modal.path_entry_empty_name"));
|
||||||
self.modal = Some(Modal::PathEntry(state));
|
self.modal = Some(Modal::PathEntry(state));
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
@@ -1263,7 +1275,7 @@ impl App {
|
|||||||
LoadPickerSubMode::List => match key.code {
|
LoadPickerSubMode::List => match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
self.modal = None;
|
self.modal = None;
|
||||||
self.note_system("load cancelled");
|
self.note_system(crate::t!("modal.load_cancelled"));
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
@@ -1288,7 +1300,7 @@ impl App {
|
|||||||
source: "load".to_string(),
|
source: "load".to_string(),
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
self.note_error("nothing to load");
|
self.note_error(crate::t!("modal.load_picker_nothing"));
|
||||||
self.modal = Some(Modal::LoadPicker(state));
|
self.modal = Some(Modal::LoadPicker(state));
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
@@ -1314,7 +1326,7 @@ impl App {
|
|||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let target = input.trim().to_string();
|
let target = input.trim().to_string();
|
||||||
if target.is_empty() {
|
if target.is_empty() {
|
||||||
self.note_error("path entry: empty path");
|
self.note_error(crate::t!("modal.path_entry_empty_path"));
|
||||||
self.modal = Some(Modal::LoadPicker(state));
|
self.modal = Some(Modal::LoadPicker(state));
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
@@ -1376,53 +1388,13 @@ impl App {
|
|||||||
/// always accurate against the build they're running. As
|
/// always accurate against the build they're running. As
|
||||||
/// new commands land, append them here.
|
/// new commands land, append them here.
|
||||||
fn note_help(&mut self) {
|
fn note_help(&mut self) {
|
||||||
self.note_system("Supported commands:");
|
// Body lives in the i18n catalog (`help.in_app_body`).
|
||||||
for line in [
|
// Each YAML line becomes its own `OutputLine` so the
|
||||||
" quit / q — exit",
|
// scroll-position math (one logical line = one display
|
||||||
" help — this list",
|
// row) stays accurate per the renderer's invariant.
|
||||||
" mode simple|advanced — switch input mode",
|
let body = crate::t!("help.in_app_body");
|
||||||
" messages — show current verbosity",
|
for line in body.lines() {
|
||||||
" messages short|verbose— switch error wording (verbose is the default)",
|
self.note_system(line.to_string());
|
||||||
" rebuild — rebuild .db from project.yaml + data/ (with confirmation)",
|
|
||||||
" save — save current temp project under a name",
|
|
||||||
" save as — copy current project to a new name/path",
|
|
||||||
" new — close current, start a fresh temp project",
|
|
||||||
" load — open the project picker",
|
|
||||||
" export [<path>] — write a zip of project.yaml + data/ (excludes .db, history.log)",
|
|
||||||
" import <zip> [as <t>] — unpack a zip and switch to the new project",
|
|
||||||
"DSL data commands (in simple mode):",
|
|
||||||
" create table <T> with pk [<col>:<type>...]",
|
|
||||||
" drop table <T>",
|
|
||||||
" add column [to] [table] <T>: <col> (<type>)",
|
|
||||||
" (for serial/shortid on a non-empty table: existing rows auto-filled)",
|
|
||||||
" drop column [from] [table] <T>: <col>",
|
|
||||||
" rename column [in] [table] <T>: <old> to <new>",
|
|
||||||
" change column [in] [table] <T>: <col> (<newtype>)",
|
|
||||||
" [--force-conversion | --dont-convert]",
|
|
||||||
" (to serial/shortid: null cells auto-filled with generated values)",
|
|
||||||
" add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>",
|
|
||||||
" [on delete <action>] [on update <action>] [--create-fk]",
|
|
||||||
" drop relationship <name>",
|
|
||||||
" insert into <T> [(cols)] [values] (vals)",
|
|
||||||
" update <T> set <c>=<v>... where <c>=<v> | --all-rows",
|
|
||||||
" delete from <T> where <c>=<v> | --all-rows",
|
|
||||||
" show table <T>",
|
|
||||||
" show data <T>",
|
|
||||||
" replay <path> — run each non-blank, non-`#`-comment line",
|
|
||||||
" of <path> as a command. Stops at the first",
|
|
||||||
" error (no rollback). Relative paths resolve",
|
|
||||||
" under the current project's directory.",
|
|
||||||
"Types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid",
|
|
||||||
"Auto-generated types (serial, shortid):",
|
|
||||||
" serial — integer that auto-fills with the next sequence value",
|
|
||||||
" (MAX(col)+1) on insert. Outside a primary key it carries",
|
|
||||||
" a UNIQUE contract.",
|
|
||||||
" shortid — short base58 identifier auto-filled at insert time. Always",
|
|
||||||
" carries a UNIQUE contract.",
|
|
||||||
" Adding or changing-to either type on a non-empty table auto-fills",
|
|
||||||
" existing/null cells in the same operation.",
|
|
||||||
] {
|
|
||||||
self.note_system(line);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-45
@@ -31,51 +31,16 @@ pub struct Args {
|
|||||||
pub help: bool,
|
pub help: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Usage banner printed by `--help`. Kept as one block so the
|
/// Usage banner printed by `--help`.
|
||||||
/// formatting is reviewable on its own.
|
///
|
||||||
pub const HELP_TEXT: &str = "\
|
/// Wraps the catalog lookup (`help.cli_banner`) so callers
|
||||||
rdbms-playground — a TUI playground for relational database concepts
|
/// don't have to spell out the key. The catalog body is the
|
||||||
|
/// single source of truth — see
|
||||||
Usage:
|
/// `src/friendly/strings/en-US.yaml`.
|
||||||
rdbms-playground [options] [<project-path>]
|
#[must_use]
|
||||||
|
pub fn help_text() -> String {
|
||||||
Arguments:
|
crate::t!("help.cli_banner")
|
||||||
<project-path> Path to an existing project directory.
|
}
|
||||||
Without this, a fresh auto-named temp
|
|
||||||
project is created in the data dir.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h, --help Print this help and exit.
|
|
||||||
--theme <light|dark> Override theme (default: auto-detect).
|
|
||||||
--data-dir <PATH> Use PATH as the data root instead of
|
|
||||||
the OS-standard location for this run.
|
|
||||||
--log-file <PATH> Write tracing output to PATH.
|
|
||||||
--resume Open the most-recently-used project
|
|
||||||
(path tracked under <data-root>/last_project).
|
|
||||||
Errors out if no previous project is
|
|
||||||
recorded. Mutually exclusive with
|
|
||||||
<project-path>.
|
|
||||||
|
|
||||||
App-level commands (typed inside the app, available in both modes):
|
|
||||||
quit / q Exit cleanly.
|
|
||||||
mode simple|advanced Switch input mode.
|
|
||||||
help Show this list of commands in-app.
|
|
||||||
save Save the current temp project under a
|
|
||||||
chosen name (or `save as` to copy a
|
|
||||||
named project to a new location).
|
|
||||||
save as Always prompt for a target name/path.
|
|
||||||
new Close current, create a fresh temp.
|
|
||||||
load Open the project picker.
|
|
||||||
rebuild Rebuild playground.db from project.yaml
|
|
||||||
+ data/, with confirmation.
|
|
||||||
export [<path>] Write a zip of project.yaml + data/ to
|
|
||||||
<path> (relative paths under the data
|
|
||||||
root). Excludes playground.db and
|
|
||||||
history.log.
|
|
||||||
import <zip> [as <t>] Unpack <zip> into a new project and
|
|
||||||
switch to it. <t> overrides the target
|
|
||||||
name (else taken from the zip).
|
|
||||||
";
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ArgsError {
|
pub enum ArgsError {
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
"error.type_mismatch.update.hint",
|
"error.type_mismatch.update.hint",
|
||||||
&["table", "column", "expected_type"],
|
&["table", "column", "expected_type"],
|
||||||
),
|
),
|
||||||
|
// ---- Help text ----
|
||||||
|
("help.cli_banner", &[]),
|
||||||
|
("help.in_app_body", &[]),
|
||||||
// ---- Parse error rendering ----
|
// ---- Parse error rendering ----
|
||||||
("parse.caret", &["padding"]),
|
("parse.caret", &["padding"]),
|
||||||
("parse.empty", &[]),
|
("parse.empty", &[]),
|
||||||
@@ -128,10 +131,27 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("project.export_usage", &[]),
|
("project.export_usage", &[]),
|
||||||
("project.import_empty_target", &[]),
|
("project.import_empty_target", &[]),
|
||||||
("project.import_usage", &[]),
|
("project.import_usage", &[]),
|
||||||
|
("project.import_zip_missing", &["path"]),
|
||||||
|
("project.load_path_missing", &["path"]),
|
||||||
|
("project.saveas_target_exists", &["path"]),
|
||||||
("project.rebuild_failed", &["error"]),
|
("project.rebuild_failed", &["error"]),
|
||||||
("project.rebuild_ok", &["summary"]),
|
("project.rebuild_ok", &["summary"]),
|
||||||
("project.switch_failed", &["error"]),
|
("project.switch_failed", &["error"]),
|
||||||
("project.switched_ok", &["display_name"]),
|
("project.switched_ok", &["display_name"]),
|
||||||
|
// ---- Advanced-mode placeholder ----
|
||||||
|
("advanced_mode.not_implemented", &["input"]),
|
||||||
|
// ---- DSL failure wrapper / running echo ----
|
||||||
|
("dsl.failed", &["verb", "subject", "rendered"]),
|
||||||
|
("dsl.running", &["input"]),
|
||||||
|
// ---- Persistence-fatal banner ----
|
||||||
|
("fatal.persistence", &["operation", "path", "message"]),
|
||||||
|
// ---- Modal labels ----
|
||||||
|
("modal.generic_cancelled", &["title"]),
|
||||||
|
("modal.load_cancelled", &[]),
|
||||||
|
("modal.load_picker_nothing", &[]),
|
||||||
|
("modal.path_entry_empty_name", &[]),
|
||||||
|
("modal.path_entry_empty_path", &[]),
|
||||||
|
("modal.rebuild_cancelled", &[]),
|
||||||
// ---- mode / messages banners ----
|
// ---- mode / messages banners ----
|
||||||
("messages.set_short", &[]),
|
("messages.set_short", &[]),
|
||||||
("messages.set_verbose", &[]),
|
("messages.set_verbose", &[]),
|
||||||
|
|||||||
@@ -152,6 +152,103 @@ error:
|
|||||||
empty_update:
|
empty_update:
|
||||||
headline: "UPDATE requires at least one assignment."
|
headline: "UPDATE requires at least one assignment."
|
||||||
|
|
||||||
|
# ---- Help text (CLI banner + in-app `help` command) ------------------
|
||||||
|
help:
|
||||||
|
# CLI usage banner. Printed by `--help` / `-h` and on
|
||||||
|
# argument-parse failure. Multi-line block; consumers
|
||||||
|
# iterate lines or print as-is.
|
||||||
|
cli_banner: |
|
||||||
|
rdbms-playground — a TUI playground for relational database concepts
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
rdbms-playground [options] [<project-path>]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<project-path> Path to an existing project directory.
|
||||||
|
Without this, a fresh auto-named temp
|
||||||
|
project is created in the data dir.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Print this help and exit.
|
||||||
|
--theme <light|dark> Override theme (default: auto-detect).
|
||||||
|
--data-dir <PATH> Use PATH as the data root instead of
|
||||||
|
the OS-standard location for this run.
|
||||||
|
--log-file <PATH> Write tracing output to PATH.
|
||||||
|
--resume Open the most-recently-used project
|
||||||
|
(path tracked under <data-root>/last_project).
|
||||||
|
Errors out if no previous project is
|
||||||
|
recorded. Mutually exclusive with
|
||||||
|
<project-path>.
|
||||||
|
|
||||||
|
App-level commands (typed inside the app, available in both modes):
|
||||||
|
quit / q Exit cleanly.
|
||||||
|
mode simple|advanced Switch input mode.
|
||||||
|
help Show this list of commands in-app.
|
||||||
|
save Save the current temp project under a
|
||||||
|
chosen name (or `save as` to copy a
|
||||||
|
named project to a new location).
|
||||||
|
save as Always prompt for a target name/path.
|
||||||
|
new Close current, create a fresh temp.
|
||||||
|
load Open the project picker.
|
||||||
|
rebuild Rebuild playground.db from project.yaml
|
||||||
|
+ data/, with confirmation.
|
||||||
|
export [<path>] Write a zip of project.yaml + data/ to
|
||||||
|
<path> (relative paths under the data
|
||||||
|
root). Excludes playground.db and
|
||||||
|
history.log.
|
||||||
|
import <zip> [as <t>] Unpack <zip> into a new project and
|
||||||
|
switch to it. <t> overrides the target
|
||||||
|
name (else taken from the zip).
|
||||||
|
# In-app `help` command output. Same shape as
|
||||||
|
# `cli_banner` — multi-line block, consumers iterate
|
||||||
|
# lines and emit each as its own output row so scroll
|
||||||
|
# math stays accurate.
|
||||||
|
in_app_body: |
|
||||||
|
Supported commands:
|
||||||
|
quit / q — exit
|
||||||
|
help — this list
|
||||||
|
mode simple|advanced — switch input mode
|
||||||
|
messages — show current verbosity
|
||||||
|
messages short|verbose— switch error wording (verbose is the default)
|
||||||
|
rebuild — rebuild .db from project.yaml + data/ (with confirmation)
|
||||||
|
save — save current temp project under a name
|
||||||
|
save as — copy current project to a new name/path
|
||||||
|
new — close current, start a fresh temp project
|
||||||
|
load — open the project picker
|
||||||
|
export [<path>] — write a zip of project.yaml + data/ (excludes .db, history.log)
|
||||||
|
import <zip> [as <t>] — unpack a zip and switch to the new project
|
||||||
|
DSL data commands (in simple mode):
|
||||||
|
create table <T> with pk [<col>:<type>...]
|
||||||
|
drop table <T>
|
||||||
|
add column [to] [table] <T>: <col> (<type>)
|
||||||
|
(for serial/shortid on a non-empty table: existing rows auto-filled)
|
||||||
|
drop column [from] [table] <T>: <col>
|
||||||
|
rename column [in] [table] <T>: <old> to <new>
|
||||||
|
change column [in] [table] <T>: <col> (<newtype>)
|
||||||
|
[--force-conversion | --dont-convert]
|
||||||
|
(to serial/shortid: null cells auto-filled with generated values)
|
||||||
|
add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>
|
||||||
|
[on delete <action>] [on update <action>] [--create-fk]
|
||||||
|
drop relationship <name>
|
||||||
|
insert into <T> [(cols)] [values] (vals)
|
||||||
|
update <T> set <c>=<v>... where <c>=<v> | --all-rows
|
||||||
|
delete from <T> where <c>=<v> | --all-rows
|
||||||
|
show table <T>
|
||||||
|
show data <T>
|
||||||
|
replay <path> — run each non-blank, non-`#`-comment line
|
||||||
|
of <path> as a command. Stops at the first
|
||||||
|
error (no rollback). Relative paths resolve
|
||||||
|
under the current project's directory.
|
||||||
|
Types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid
|
||||||
|
Auto-generated types (serial, shortid):
|
||||||
|
serial — integer that auto-fills with the next sequence value
|
||||||
|
(MAX(col)+1) on insert. Outside a primary key it carries
|
||||||
|
a UNIQUE contract.
|
||||||
|
shortid — short base58 identifier auto-filled at insert time. Always
|
||||||
|
carries a UNIQUE contract.
|
||||||
|
Adding or changing-to either type on a non-empty table auto-fills
|
||||||
|
existing/null cells in the same operation.
|
||||||
|
|
||||||
# ---- DSL parse error rendering --------------------------------------
|
# ---- DSL parse error rendering --------------------------------------
|
||||||
parse:
|
parse:
|
||||||
# Wrapper around chumsky's structural error message. The
|
# Wrapper around chumsky's structural error message. The
|
||||||
@@ -179,6 +276,39 @@ project:
|
|||||||
export_usage: "usage: export [<path>]"
|
export_usage: "usage: export [<path>]"
|
||||||
import_usage: "usage: import <zip-path> [as <target>]"
|
import_usage: "usage: import <zip-path> [as <target>]"
|
||||||
import_empty_target: "import: empty target after `as`"
|
import_empty_target: "import: empty target after `as`"
|
||||||
|
# Project-switch validation failures (load / save-as /
|
||||||
|
# import). Returned from the runtime as Err(String) and
|
||||||
|
# surfaced via project.switch_failed.
|
||||||
|
load_path_missing: "path `{path}` does not exist"
|
||||||
|
saveas_target_exists: "`{path}` already exists; pick a different name or remove it first"
|
||||||
|
import_zip_missing: "zip `{path}` does not exist"
|
||||||
|
|
||||||
|
# ---- DSL failure wrapper + advanced-mode placeholder + fatal --------
|
||||||
|
dsl:
|
||||||
|
# Wrapper around the friendly-error layer's translated
|
||||||
|
# message, surfaced as `"<verb> <subject>" failed: <rendered>`.
|
||||||
|
failed: '"{verb} {subject}" failed: {rendered}'
|
||||||
|
# Echo line `running: <input>` shown above each command's
|
||||||
|
# response so the user has on-screen context for the
|
||||||
|
# output that follows.
|
||||||
|
running: "running: {input}"
|
||||||
|
|
||||||
|
# ---- Advanced-mode placeholder until SQL parser lands (Q1) ----------
|
||||||
|
advanced_mode:
|
||||||
|
not_implemented: "advanced mode SQL not implemented yet — echo: {input}"
|
||||||
|
|
||||||
|
# ---- Persistence-fatal banner (ADR-0015 §8) -------------------------
|
||||||
|
fatal:
|
||||||
|
persistence: "FATAL: failed to {operation} `{path}` — {message}. Quitting; investigate and restart."
|
||||||
|
|
||||||
|
# ---- Modal labels (load picker, rebuild confirm, save-as path) ------
|
||||||
|
modal:
|
||||||
|
rebuild_cancelled: "rebuild cancelled"
|
||||||
|
load_cancelled: "load cancelled"
|
||||||
|
generic_cancelled: "{title} cancelled"
|
||||||
|
path_entry_empty_name: "path entry: empty name"
|
||||||
|
path_entry_empty_path: "path entry: empty path"
|
||||||
|
load_picker_nothing: "nothing to load"
|
||||||
|
|
||||||
# ---- mode / messages banners (app-level commands) -------------------
|
# ---- mode / messages banners (app-level commands) -------------------
|
||||||
mode:
|
mode:
|
||||||
|
|||||||
+12
-10
@@ -1,20 +1,29 @@
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use rdbms_playground::cli::{Args, HELP_TEXT};
|
use rdbms_playground::cli::{help_text, Args};
|
||||||
use rdbms_playground::{logging, runtime};
|
use rdbms_playground::{logging, runtime};
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
|
// ADR-0019 §8.6: parse the embedded message catalog up
|
||||||
|
// front so a corrupted build artefact fails loudly here
|
||||||
|
// rather than at the first `t!()` call deep inside the
|
||||||
|
// event loop. Cheap — the catalog is small and `OnceLock`
|
||||||
|
// memoises the parse for every subsequent caller.
|
||||||
|
// Doing this first also means the args-parse error path
|
||||||
|
// below can pull `help.cli_banner` from the catalog.
|
||||||
|
let _ = rdbms_playground::friendly::catalog();
|
||||||
|
|
||||||
let args = match Args::from_env() {
|
let args = match Args::from_env() {
|
||||||
Ok(args) => args,
|
Ok(args) => args,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("rdbms-playground: {e}");
|
eprintln!("rdbms-playground: {e}");
|
||||||
eprintln!("\n{HELP_TEXT}");
|
eprintln!("\n{}", help_text());
|
||||||
return ExitCode::from(2);
|
return ExitCode::from(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if args.help {
|
if args.help {
|
||||||
print!("{HELP_TEXT}");
|
print!("{}", help_text());
|
||||||
return ExitCode::SUCCESS;
|
return ExitCode::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,13 +32,6 @@ fn main() -> ExitCode {
|
|||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ADR-0019 §8.6: parse the embedded message catalog up
|
|
||||||
// front so a corrupted build artefact fails loudly here
|
|
||||||
// rather than at the first `t!()` call deep inside the
|
|
||||||
// event loop. Cheap — the catalog is small and `OnceLock`
|
|
||||||
// memoises the parse for every subsequent caller.
|
|
||||||
let _ = rdbms_playground::friendly::catalog();
|
|
||||||
|
|
||||||
let tokio_rt = match tokio::runtime::Runtime::new() {
|
let tokio_rt = match tokio::runtime::Runtime::new() {
|
||||||
Ok(rt) => rt,
|
Ok(rt) => rt,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
+11
-5
@@ -514,16 +514,19 @@ async fn perform_switch(
|
|||||||
let resolved_target: Option<std::path::PathBuf> = match &req {
|
let resolved_target: Option<std::path::PathBuf> = match &req {
|
||||||
SwitchRequest::Load { path } => {
|
SwitchRequest::Load { path } => {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err(format!("path `{}` does not exist", path.display()));
|
return Err(crate::t!(
|
||||||
|
"project.load_path_missing",
|
||||||
|
path = path.display()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Some(path.clone())
|
Some(path.clone())
|
||||||
}
|
}
|
||||||
SwitchRequest::SaveAs { target } => {
|
SwitchRequest::SaveAs { target } => {
|
||||||
let p = resolve_save_target(target, &session.data_root);
|
let p = resolve_save_target(target, &session.data_root);
|
||||||
if p.exists() {
|
if p.exists() {
|
||||||
return Err(format!(
|
return Err(crate::t!(
|
||||||
"`{}` already exists; pick a different name or remove it first",
|
"project.saveas_target_exists",
|
||||||
p.display(),
|
path = p.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Some(p)
|
Some(p)
|
||||||
@@ -531,7 +534,10 @@ async fn perform_switch(
|
|||||||
SwitchRequest::NewTemp => None,
|
SwitchRequest::NewTemp => None,
|
||||||
SwitchRequest::Import { zip_path, as_target } => {
|
SwitchRequest::Import { zip_path, as_target } => {
|
||||||
if !zip_path.exists() {
|
if !zip_path.exists() {
|
||||||
return Err(format!("zip `{}` does not exist", zip_path.display()));
|
return Err(crate::t!(
|
||||||
|
"project.import_zip_missing",
|
||||||
|
path = zip_path.display()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// Validate the zip up front so we don't drop the
|
// Validate the zip up front so we don't drop the
|
||||||
// current project for an unimportable file.
|
// current project for an unimportable file.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
|
|
||||||
use rdbms_playground::app::App;
|
use rdbms_playground::app::App;
|
||||||
use rdbms_playground::cli::HELP_TEXT;
|
use rdbms_playground::cli::help_text;
|
||||||
use rdbms_playground::db::{DbError, SqliteErrorKind};
|
use rdbms_playground::db::{DbError, SqliteErrorKind};
|
||||||
use rdbms_playground::dsl::parse_command;
|
use rdbms_playground::dsl::parse_command;
|
||||||
use rdbms_playground::event::AppEvent;
|
use rdbms_playground::event::AppEvent;
|
||||||
@@ -87,7 +87,7 @@ fn collect_output(app: &App) -> String {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_help_text_uses_no_engine_vocabulary() {
|
fn cli_help_text_uses_no_engine_vocabulary() {
|
||||||
assert_clean("CLI HELP_TEXT", HELP_TEXT);
|
assert_clean("CLI help_text()", &help_text());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ fn args_resume_after_positional_path_also_errors() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn args_help_listing_mentions_resume() {
|
fn args_help_listing_mentions_resume() {
|
||||||
assert!(rdbms_playground::cli::HELP_TEXT.contains("--resume"));
|
assert!(rdbms_playground::cli::help_text().contains("--resume"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- last_project read/write ----------------------------------
|
// --- last_project read/write ----------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user