From 3ad4affef24acb84f952559af9a1f2bdd9bf0934 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Mon, 22 Jun 2026 19:01:27 +0000 Subject: [PATCH] feat(help): distinct help for advanced-mode SQL forms; split list by mode (#36) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The six advanced SQL DML/query forms (SELECT, WITH, SQL_INSERT, SQL_UPDATE, SQL_DELETE, EXPLAIN_SQL) carried help_id: None, so `help select`/`help with` resolved to nothing and `help insert` showed only the simple form. Give each its own distinct help_id (data.select, data.sql_insert, …) with a hand-curated help.data.* page. Distinct strings keep the dedup invariant intact, and note_help_topic needs no change — `help insert` now shows the simple block and the sql_insert block (like `help create` already did), and the advanced-only forms resolve. note_help now groups the list by CommandCategory: app-lifecycle commands first (unlabelled), then "Simple-mode commands:" and "Advanced-mode (SQL) commands:" sections — replacing the old single "DSL data commands (in simple mode):" header, which used the banned "DSL" term and mis-labelled the advanced SQL forms it already contained. Four new help_command tests (red→green). Recorded as ADR-0024 Amendment 1; CHANGELOG updated. --- CHANGELOG.md | 4 + ...024-unified-grammar-tree-execution-plan.md | 50 +++++++++++++ docs/adr/README.md | 2 +- src/app.rs | 52 ++++++++----- src/dsl/grammar/data.rs | 26 +++---- src/dsl/grammar/mod.rs | 17 +++-- src/friendly/keys.rs | 10 ++- src/friendly/strings/en-US.yaml | 29 +++++++- tests/it/help_command.rs | 74 +++++++++++++++++++ 9 files changed, 223 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61354c9..63ec520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- `help` now covers the advanced-mode SQL commands: `help select`, `help with`, + and the SQL forms of `insert` / `update` / `delete` / `explain` show their own + syntax, and the full command list is grouped into "Simple-mode commands" and + "Advanced-mode (SQL) commands" sections. - Install via **Scoop**, **Homebrew**, and **winget** in addition to the existing channels. - Tier-4 end-to-end test suite that exercises the real application in a diff --git a/docs/adr/0024-unified-grammar-tree-execution-plan.md b/docs/adr/0024-unified-grammar-tree-execution-plan.md index 7c2b29b..23742ac 100644 --- a/docs/adr/0024-unified-grammar-tree-execution-plan.md +++ b/docs/adr/0024-unified-grammar-tree-execution-plan.md @@ -706,6 +706,56 @@ documentation is still hand-curated for round 1. table-ident from new-name-ident visually is a future enhancement. +## Amendment 1 — advanced-mode SQL forms get their own `help` pages, and the list groups by mode (2026-06-22, issue #36) + +`help_id` (§Node taxonomy) drives two surfaces: the `help` **list** +(`note_help` emits one `help.` block per `Some` id) and the +`help ` **lookup** (`note_help_topic` shows the block of every +node whose entry word matches the topic). The dedup rule is that +`help_id` *strings* are unique (one block ⇒ printed once). + +Originally the six advanced-mode SQL **DML/query** forms — `SELECT`, +`WITH`, `SQL_INSERT`, `SQL_UPDATE`, `SQL_DELETE`, `EXPLAIN_SQL` — +carried `help_id: None`. That was a list-formatting shortcut (avoid a +second `insert` entry), but it had a side effect: `help select` / +`help with` resolved to **nothing** (the unknown-topic note), and +`help insert` showed only the simple form — even though the SQL surface +is genuinely different and advanced mode exists precisely for learners +moving to raw SQL. (The advanced SQL **DDL** forms — `sql_create_table` +etc. — already had their own `help.ddl.sql_*` pages, so the gap was +inconsistent as well as a pedagogy hole.) + +**Decision.** Give every advanced SQL form its **own** `help_id` +(`data.select`, `data.with`, `data.sql_insert`, `data.sql_update`, +`data.sql_delete`, `data.explain_sql`) with a hand-curated +`help.data.*` page (the catalog body stays hand-written, per "What's +out of scope" above — this amendment doesn't change that). Because the +ids are **distinct strings**, the dedup invariant +(`no_two_registered_commands_share_a_help_id`) is untouched, and: + +- **`help ` shows every form sharing the entry word** — so + `help insert` shows the simple block *and* the `sql_insert` block + (exactly as `help create` already showed the simple + SQL create + forms). Advanced-only `help select` / `help with` now resolve. +- **The list groups by mode.** `note_help` now splits the REGISTRY by + [`CommandCategory`]: app-lifecycle commands (ids in the `app.*` + namespace, usable in either mode) list first, unlabelled, under the + intro; then a **`help.simple_section`** ("Simple-mode commands:") + group and a **`help.advanced_section`** ("Advanced-mode (SQL) + commands:") group. This replaces the single `help.dsl_section` header + ("DSL data commands (in simple mode):"), which both used the banned + "DSL" term (ADR-0002 user-facing posture) and wrongly claimed simple + mode for the advanced SQL forms the section already held. + +This partially realises ADR-0030 §6's "Polish" item (a `help sql` +page): rather than one combined page, each form has its own, reached +through the normal `help ` surface and discoverable in the +advanced-mode list section. `note_help_topic` needed **no** change — +the new `help_id`s make the forms resolve automatically. Covered by +`help_command::{help_select_renders_the_sql_select_block, +help_with_renders_the_cte_block, help_insert_shows_both_simple_and_sql_forms, +help_list_splits_simple_and_advanced_sections}`. + ## References - ADR-0023 — Unified declarative grammar tree (Proposed direction). Superseded by this ADR for execution detail. diff --git a/docs/adr/README.md b/docs/adr/README.md index ff51058..e938949 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -36,7 +36,7 @@ This directory contains the project's ADRs, recorded per - [ADR-0021 — Parser-as-source-of-truth for H1a (per-command usage in parse errors)](0021-parser-as-source-of-truth-for-h1a.md) — **Mechanism superseded by ADR-0024; H1a scope continued in ADR-0042.** The *intent* (show the command's grammar at the point of error) shipped — `usage_ids` on each `CommandNode`, the `parse.usage.*` templates, and the `available_commands` fallback all exist — but via grammar nodes, not the `chumsky` `UsageEntry` registry / `parse.token.*` keys described here (which were never built). - [ADR-0022 — Ambient typing assistance: colour, hint panel, completion (I3 + I4)](0022-ambient-typing-assistance.md) — **Amendment 1 supersedes §12's simple-mode-only carve-out**: the unified mode-aware walker (ADR-0030/0031/0032) now speaks SQL, so advanced-mode ambient assistance is re-enabled. `ambient_hint_in_mode` + `hint_resolution_at_input_in_mode` + `expected_for_hint_snapshot` thread `Mode`; `render_hint_panel` calls ambient for all modes (no more advanced-mode `None`); the one-shot `:` sigil is stripped before the ambient walk. Fixes a live bug where advanced-mode SQL hinting/completion-preview were dead despite Phase 2 marking them green (validated at the engine layer, not the UI). Simple-mode gating, highlighting, and the §13 performance posture are unchanged; covered by an app-level render test plus ambient-layer regression locks; **Amendment 2 reverses the handoff-14 keywords-first candidate ordering** — schema identifiers (table/column/relationship names) now sort *before* keywords so a name the user would have to look up stays visible in the single-row, window-scrolled candidate line (keywords are learned over time; the `tok_identifier`/`tok_keyword` colour split marks the boundary); shipped with a `walk_repeated` fix that surfaces a list item's trailing optionals at a clean boundary (`order by Name ` → `asc`/`desc`, `select Name ` → `as`, `create table … Code(text) ` → `not`/`unique`/`default`/`check`; the `,` separator deliberately not surfaced); records a deferred two-line hint box for growing lists; **Amendment 3 makes the ambient-hint fallback rung schema-aware** — Amendment 1's bottom-rung `parse_command_in_mode` was schemaless while every earlier rung was not, so between-values insert hints pointed at `)` (type-blind close) instead of `,` and wrong-arity closed tuples read "submit with Enter" for an input the schema-aware parse rejects (issue #2); now uses `parse_command_with_schema_in_mode`, no extra walk, with the friendly arity diagnostic still winning at its higher rung; **Amendment 4 gives column types a dedicated highlight class** — both `Node::Ident.highlight_override` *and* the `Word.highlight_override` field were dead (driver destructured the former to `_`, `walk_word` hardcoded `Keyword`); now both wired through, with a new `HighlightClass::Type` + eighth `Theme` field `tok_type` (a pink/deep-magenta distinct from both keyword purple and identifier teal) so types no longer render identically to identifiers (issue #8); the three `IdentSource::Types` slots opt in via `Some(Type)` (advanced-mode single-word SQL aliases — `float`, `varchar`, … per ADR-0035 §3 — ride along for free), and the two-word `double precision` alias opts in via the new `Word::type_keyword` constructor so it matches its synonyms; **Amendment 5 lets the hint panel grow for long prose hints** — a fixed one-row panel clipped long field-value/usage hints past the first line (issue #12); `resolve_hint_lines` now pre-wraps prose and `render_right_column` sizes the panel to the line count (1 row default, up to `MAX_HINT_ROWS`=3, reclaimed when short) with a `clamp_wrapped` ellipsis backstop; the candidate list still scrolls horizontally on one row (Amendment 2's deferred two-line candidate box stays deferred); also shortens the 299-char `parse.usage.sql_create_table` synopsis to a terse one-liner (full grammar remains in `help.ddl.sql_create_table`); **Amendment 6 adds a curated SQL function-name list** (`src/dsl/sql_functions.rs`, `KNOWN_SQL_FUNCTIONS` — aggregates + common + broader scalars; `cast` deliberately excluded as its `CAST(x AS type)` syntax isn't a plain-call shape) as the single source of truth shared by two consumers at the `sql_expr_ident` slot (ADR-0031 §1): **issue #15** offers the functions as Tab candidates under a new `CandidateKind::Function` + ninth `Theme` colour `tok_function` (a blue distinct from keyword/identifier/type, parallel to Amendment 4's `tok_type`) so a learner discovers `sum`/`upper`/…; **issue #16** restores the typing-time column-typo flag the issue-#6 fix had dropped wholesale at this slot — `invalid_ident_at_cursor` now bails only when the partial prefix-matches a known function, else falls through to the schema-column check, so `select Agx` warns again at typing time while `select sum` does not (the issue-#6 lockdown tests + the submit-time `unknown_column` diagnostic path are untouched, and the no-validation-allowlist posture stands); see ADR-0031's status note for the grammar-side anchor; **Amendment 7 surfaces optional positional args in the hint panel** (issue #26): at `seed ▮` the optional row count (a bare `NumberLit` with no candidate) was invisible next to the `set`/`--seed` chips, and the resolver short-circuits on the already-complete command. Extends the issue-#4 `IntroProse` `HintMode` (ADR-0024 §HintMode-per-node) to survive trailing optionals: `walk_optional` stashes a skipped inner's `IntroProse` key into a new `WalkContext.surviving_intro_hint` (key + position) before the empty match clears `pending_hint_mode`, and the snapshot keeps it only when the skip position is the cursor (so it never leaks past a later-consumed `set …` clause or once the count is given); the resolver returns it ahead of the empty-expected short-circuit. The seed count is wrapped `Hinted{IntroProse("hint.seed_count")}`; prose names the count (default 20), the `.column` column-fill form, and `set`/`--seed` (user-chosen scope). Only `IntroProse` is carried; `ProseOnly`/`ForceProse` and the CREATE-TABLE element (a required `Repeated`) are untouched; no `AmbientHint`/renderer change; **Amendment 8 adds a submission gate so interactive input is held until the schema-cache refresh lands** (issue #39): §9's schema-cache refresh is asynchronous (the worker applies the command, then posts `SchemaCacheRefreshed` on the same FIFO channel as key events), so under faster-than-human input a follow-up submission was validated against the *stale* cache — a Form-B `insert into T values (…)` right after `add column` saw the pre-DDL columns and was wrongly rejected as "trying to write SQL?". `App` now arms `awaiting_schema_refresh` on every `ExecuteDsl` dispatch and holds further DSL submissions in a `held_submissions` FIFO, draining them against the fresh cache when the refresh arrives (order-preserving; app-lifecycle commands bypass the gate; arm-on-every-dispatch keeps exactly one DSL command in flight so the boolean is provably correct); interactive-only (the `replay`/batch path already re-snapshots synchronously per line), no interactive-user impact, held input never lost (a refresh always follows a dispatch); covered by a Tier-1 deterministic ordering test + a Tier-4 PTY back-to-back regression - [ADR-0023 — Unified declarative grammar tree](0023-unified-grammar-tree.md) — direction (superseded for execution detail by ADR-0024) -- [ADR-0024 — Unified grammar tree: execution plan](0024-unified-grammar-tree-execution-plan.md) — **Accepted**, the executable spec — implemented (Phases A–F; Phase F shipped "minimal", `parser.rs` retained as the router — see the ADR's Phase F implementation note) +- [ADR-0024 — Unified grammar tree: execution plan](0024-unified-grammar-tree-execution-plan.md) — **Accepted**, the executable spec — implemented (Phases A–F; Phase F shipped "minimal", `parser.rs` retained as the router — see the ADR's Phase F implementation note); **Amendment 1 gives advanced-mode SQL forms their own `help` pages and groups the list by mode** (issue #36): the six advanced DML/query forms (`SELECT`/`WITH`/`SQL_INSERT`/`SQL_UPDATE`/`SQL_DELETE`/`EXPLAIN_SQL`) carried `help_id: None`, so `help select` resolved to nothing and `help insert` showed only the simple form; they now carry distinct `help_id`s (`data.select`, `data.sql_insert`, …) with hand-curated `help.data.*` pages, so `help ` shows every form sharing an entry word (simple + SQL, like `help create` already did) and the advanced-only forms resolve; `note_help` now splits the list by `CommandCategory` into a "Simple-mode commands:" and an "Advanced-mode (SQL) commands:" section (app-lifecycle commands list first, unlabelled), replacing the old single `help.dsl_section` header which used the banned "DSL" term and mis-claimed simple mode for the advanced forms it held; dedup invariant (unique `help_id` strings) and `note_help_topic` unchanged; covered by four new `help_command` tests - [ADR-0025 — Indexes](0025-indexes.md) — **Accepted** (**Amendment 1, 2026-05-25**: UNIQUE indexes admitted on the **advanced-mode** surface via `CREATE UNIQUE INDEX` — ADR-0035 §4d; the `IndexSchema.unique` flag round-trips through `project.yaml` with no new metadata table since the engine reports uniqueness natively; simple-mode `add unique index` stays deferred), `add index` / `drop index`, persistence, rebuild-table preservation, and items-list display (`C3` index portion + `S2`) - [ADR-0026 — Complex WHERE expressions](0026-complex-where-expressions.md) — **Accepted**, stratified recursive expression grammar (`AND`/`OR`/`NOT`, comparisons, `LIKE`, `IS NULL`, `IN`, `BETWEEN`) for `update` / `delete` / `show data` filters; `show data` gains `where` + `limit`; adds the `Subgrammar` node and a recursive `Expr` AST (`C5a`) - [ADR-0027 — Input-field validity indicator](0027-input-validity-indicator.md) — **Accepted**, a debounced `[ERR]` / `[WRN]` marker at the input row's right edge, backed by a walker diagnostics-severity model (parse-outcome + schema-existence); advisory, never blocks submission (`S6`); Amendment 1 adds a `LIKE`-on-numeric-column WARNING diff --git a/src/app.rs b/src/app.rs index 1cf33a7..0a37980 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3047,34 +3047,52 @@ impl App { /// output panel. /// /// Assembled from the command REGISTRY (ADR-0024 §help_id): - /// the framing (`help.intro`, `help.dsl_section`, - /// `help.types_reference`) comes from the catalog, and each - /// command's body is the catalog entry named by its - /// `help_id`. A newly-registered command appears here - /// automatically — no edit to this function or a hand-kept + /// the framing (`help.intro`, `help.simple_section`, + /// `help.advanced_section`, `help.types_reference`) comes from + /// the catalog, and each command's body is the catalog entry + /// named by its `help_id`. A newly-registered command appears + /// here automatically — no edit to this function or a hand-kept /// list. Each catalog line becomes its own `OutputLine` so /// the scroll-position math (one logical line = one display /// row) stays accurate per the renderer's invariant. + /// + /// Issue #36: the commands group by mode. App-lifecycle commands + /// (`help_id` in the `app.*` namespace) work in either mode and + /// list first, unlabelled, under the intro. The rest split by + /// [`CommandCategory`] into a simple-mode group and an + /// advanced-mode (SQL) group — replacing the old single "DSL data + /// commands (in simple mode)" header, which both used the banned + /// "DSL" term and wrongly claimed simple mode for the advanced SQL + /// forms the section already contained. fn note_help(&mut self) { - use crate::dsl::grammar::REGISTRY; + use crate::dsl::grammar::{CommandCategory, REGISTRY}; let mut lines: Vec = Vec::new(); lines.push(crate::t!("help.intro")); - // REGISTRY is ordered app-commands first; emit the - // "DSL data commands" sub-header at the first command - // whose help_id leaves the `app.` namespace. - let mut dsl_header_done = false; - for (command, _category) in REGISTRY { + + let mut simple: Vec = Vec::new(); + let mut advanced: Vec = Vec::new(); + for (command, category) in REGISTRY { let Some(help_id) = command.help_id else { continue; }; - if !dsl_header_done && !help_id.starts_with("app.") { - lines.push(crate::t!("help.dsl_section")); - dsl_header_done = true; + let body = crate::friendly::translate(&format!("help.{help_id}"), &[]); + let block: Vec = body.lines().map(str::to_string).collect(); + if help_id.starts_with("app.") { + lines.extend(block); + } else if matches!(category, CommandCategory::Advanced) { + advanced.extend(block); + } else { + simple.extend(block); } - let key = format!("help.{help_id}"); - let body = crate::friendly::translate(&key, &[]); - lines.extend(body.lines().map(str::to_string)); + } + if !simple.is_empty() { + lines.push(crate::t!("help.simple_section")); + lines.extend(simple); + } + if !advanced.is_empty() { + lines.push(crate::t!("help.advanced_section")); + lines.extend(advanced); } lines.extend( crate::t!("help.types_reference") diff --git a/src/dsl/grammar/data.rs b/src/dsl/grammar/data.rs index 7e17eca..5e2d336 100644 --- a/src/dsl/grammar/data.rs +++ b/src/dsl/grammar/data.rs @@ -1886,12 +1886,12 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode { entry: Word::keyword("explain"), shape: EXPLAIN_SQL_SHAPE, ast_builder: build_explain_sql, - // No `help_id` / `usage_ids` — this is the `Advanced` half of the - // shared `explain` entry word, so it defers to the `Simple` - // `EXPLAIN` node's help/usage (which now covers the SQL forms - // too). Mirrors the `SQL_INSERT`/`SQL_UPDATE`/`SQL_DELETE` - // precedent; otherwise `note_help` would print `explain` twice. - help_id: None, + // Issue #36: its own `help` page (`data.explain_sql`), listed under the + // advanced-mode (SQL) section and shown by `help explain` next to the + // simple `EXPLAIN` form (distinct help_id ⇒ no dedup clash). `usage_ids` + // stays empty — the `Simple` `EXPLAIN` node's usage block already covers + // the shared `explain` entry word. + help_id: Some("data.explain_sql"), hint_ids: &["explain_sql"], usage_ids: &[], }; @@ -1902,13 +1902,13 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode { /// The shape is the post-`SELECT` portion of a top-level /// statement; the registry's entry-word dispatch consumes the /// leading `SELECT` keyword before the shape walks (sub-phase -/// 2c migration). `help_id` is `None` until the `help sql` -/// page lands (ADR-0030 Phase 6). +/// 2c migration). Carries its own `help` page (`data.select`), +/// listed under the advanced-mode (SQL) section (issue #36). pub static SELECT: CommandNode = CommandNode { entry: Word::keyword("select"), shape: Node::Subgrammar(&sql_select::SQL_SELECT_TAIL), ast_builder: build_select, - help_id: None, + help_id: Some("data.select"), hint_ids: &["select"], usage_ids: &["parse.usage.select"], }; @@ -1924,7 +1924,7 @@ pub static WITH: CommandNode = CommandNode { entry: Word::keyword("with"), shape: Node::Subgrammar(&sql_select::SQL_WITH_TAIL), ast_builder: build_select, - help_id: None, + help_id: Some("data.with"), // issue #36: own help page, advanced section hint_ids: &["with"], usage_ids: &["parse.usage.with"], }; @@ -1943,7 +1943,7 @@ pub static SQL_INSERT: CommandNode = CommandNode { entry: Word::keyword("insert"), shape: Node::Subgrammar(&sql_insert::SQL_INSERT_SHAPE), ast_builder: build_sql_insert, - help_id: None, + help_id: Some("data.sql_insert"), // issue #36: own help page, advanced section hint_ids: &["sql_insert"], usage_ids: &[], }; @@ -1957,7 +1957,7 @@ pub static SQL_UPDATE: CommandNode = CommandNode { entry: Word::keyword("update"), shape: Node::Subgrammar(&sql_update::SQL_UPDATE_SHAPE), ast_builder: build_sql_update, - help_id: None, + help_id: Some("data.sql_update"), // issue #36: own help page, advanced section hint_ids: &["sql_update"], usage_ids: &[], }; @@ -1973,7 +1973,7 @@ pub static SQL_DELETE: CommandNode = CommandNode { entry: Word::keyword("delete"), shape: Node::Subgrammar(&sql_delete::SQL_DELETE_SHAPE), ast_builder: build_sql_delete, - help_id: None, + help_id: Some("data.sql_delete"), // issue #36: own help page, advanced section hint_ids: &["sql_delete"], usage_ids: &[], }; diff --git a/src/dsl/grammar/mod.rs b/src/dsl/grammar/mod.rs index 6122e09..e5ea49d 100644 --- a/src/dsl/grammar/mod.rs +++ b/src/dsl/grammar/mod.rs @@ -537,8 +537,11 @@ pub struct CommandNode { /// block). `hint_key_for_input_in_mode` disambiguates by the form /// word, reusing `usage_key_for_input_in_mode`'s logic. Empty /// until a form's tier-3 block is authored (the surface falls back - /// to tier-2 ambient/error text). Distinct from `help_id` (which is - /// `None` on advanced-SQL forms purely to dedup the `help` list). + /// to tier-2 ambient/error text). Parallel to `help_id` but + /// finer-grained: every form (simple and advanced) carries a + /// `hint_id`, whereas the `help ` view groups forms by entry + /// word (so a shared-entry simple + SQL pair both surface under e.g. + /// `help insert`). pub hint_ids: &'static [&'static str], /// Catalog keys under `parse.usage.*` to render in the /// "usage:" block when a parse error fires for this command @@ -1156,10 +1159,12 @@ mod usage_key_tests { #[test] fn no_two_registered_commands_share_a_help_id() { // `note_help` emits one help block per `help_id: Some(_)` - // with no dedup, so a duplicate help_id prints the same - // command twice in `help`. Shared-entry-word `Advanced` - // nodes (SQL_INSERT, …, EXPLAIN_SQL) therefore carry - // `help_id: None` and defer to their `Simple` sibling. + // with no dedup, so a duplicate help_id string prints the same + // block twice. Distinct help_ids are fine — a shared-entry-word + // simple + SQL pair (e.g. `data.insert` + `data.sql_insert`, + // issue #36) each get their own block, grouped under one topic + // by `help ` and split across the simple/advanced + // sections of the full list. let mut seen = std::collections::HashSet::new(); for (command, _category) in super::REGISTRY { if let Some(id) = command.help_id { diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index 21af9f3..64b65b9 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -180,7 +180,8 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ // In-app `help` — framing + per-command entries keyed by // each CommandNode's `help_id` (ADR-0024 §help_id). ("help.intro", &[]), - ("help.dsl_section", &[]), + ("help.simple_section", &[]), + ("help.advanced_section", &[]), ("help.types_reference", &[]), ("help.detail_hint", &[]), ("help.unknown_topic", &["topic"]), @@ -223,6 +224,13 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("help.data.delete", &[]), ("help.data.replay", &[]), ("help.data.explain", &[]), + // Issue #36: advanced-mode (SQL) help pages. + ("help.data.select", &[]), + ("help.data.with", &[]), + ("help.data.sql_insert", &[]), + ("help.data.sql_update", &[]), + ("help.data.sql_delete", &[]), + ("help.data.explain_sql", &[]), // ---- Hint panel ambient typing assistance (ADR-0022 §6) ---- ("hint.ambient_complete", &[]), ("hint.ambient_error_with_usage", &["message", "usage"]), diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index 41caaf8..66a925d 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -246,7 +246,13 @@ help: # are multi-line-capable — the renderer emits one output row # per line so scroll math stays accurate. intro: "Supported commands:" - dsl_section: "DSL data commands (in simple mode):" + # Issue #36: the command list groups by mode. App-lifecycle commands list + # first (unlabelled, under the intro — they work in either mode); the rest + # split into these two sections by command category. (Replaces the old + # single "DSL data commands (in simple mode):" header, which used the banned + # "DSL" term and mis-labelled the advanced SQL forms it already contained.) + simple_section: "Simple-mode commands:" + advanced_section: "Advanced-mode (SQL) commands:" # H3: footer on the full `help` list, and the not-found note # for `help `. `{topic}` is the word the user typed. detail_hint: "Type `help ` for detail on one command (e.g. `help insert`), or `help types` for the type reference." @@ -368,8 +374,25 @@ help: explain show data | explain update ... | explain delete from ... — show how the database would run a query, without running it (safe even for update / delete) - explain (advanced mode) - — the same plan for the SQL you wrote + # Issue #36: advanced-mode (SQL) forms. Each has its own help page, listed + # under the "Advanced-mode (SQL) commands:" section and shown by + # `help ` alongside its simple-mode sibling — so `help insert` shows + # both the simple form and `sql_insert` (like `help create` already does). + select: |- + select | * from [where …] [group by …] [order by …] [limit n] + — query rows (advanced SQL) + with: |- + with as ( — query through a named + sub-query / CTE (advanced SQL) + sql_insert: |- + insert into (col, …) values (val, …) — add a row (advanced SQL) + sql_update: |- + update set = , … where — change matching rows (advanced SQL) + sql_delete: |- + delete from where — remove matching rows (advanced SQL) + explain_sql: |- + explain — show the plan for a SQL statement, + without running it (advanced SQL) # Type reference, appended after the command list. types_reference: | Types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid diff --git a/tests/it/help_command.rs b/tests/it/help_command.rs index 261611e..75a91ce 100644 --- a/tests/it/help_command.rs +++ b/tests/it/help_command.rs @@ -115,6 +115,80 @@ fn help_create_covers_every_form_sharing_the_entry_word() { ); } +// ----- issue #36: advanced-mode SQL forms get distinct help content ----- + +#[test] +fn help_select_renders_the_sql_select_block() { + // `select` is advanced-only (no simple sibling) and used to have + // `help_id: None`, so `help select` produced the unknown-topic note. + // It now carries its own help page. + let out = output_for("help select"); + let joined = out.join("\n").to_lowercase(); + assert!( + joined.contains("select") && joined.contains("from"), + "help select shows the SQL select form: {out:?}", + ); + assert!( + !out.iter().any(|l| l.contains("No help for")), + "help select resolves to content, not the unknown-topic note: {out:?}", + ); +} + +#[test] +fn help_with_renders_the_cte_block() { + let out = output_for("help with"); + let joined = out.join("\n").to_lowercase(); + assert!( + joined.contains("with") && joined.contains("as ("), + "help with shows the CTE form: {out:?}", + ); + assert!( + !out.iter().any(|l| l.contains("No help for")), + "help with resolves to content: {out:?}", + ); +} + +#[test] +fn help_insert_shows_both_simple_and_sql_forms() { + // `help insert` now covers the simple form AND the advanced SQL form + // (two clearly-labelled blocks, like `help create` already does). + let out = output_for("help insert"); + let joined = out.join("\n").to_lowercase(); + assert!( + joined.contains("insert into"), + "simple insert form shown: {out:?}", + ); + assert!( + joined.contains("advanced"), + "advanced SQL insert form shown alongside the simple one: {out:?}", + ); +} + +#[test] +fn help_list_splits_simple_and_advanced_sections() { + let out = output_for("help"); + let joined = out.join("\n"); + assert!( + out.iter().any(|l| l.contains("Simple-mode commands")), + "simple-mode section header present: {out:?}", + ); + assert!( + out.iter() + .any(|l| l.contains("Advanced-mode") && l.contains("SQL")), + "advanced-mode (SQL) section header present: {out:?}", + ); + // Copy rule: never say "DSL" in user-facing text (the old header did). + assert!( + !joined.contains("DSL"), + "help output must not contain 'DSL': {out:?}", + ); + // The advanced query commands are now discoverable in the list. + assert!( + joined.to_lowercase().contains("select"), + "select is listed in help: {out:?}", + ); +} + #[test] fn help_types_renders_the_type_reference() { let out = output_for("help types");