Compare commits
5
Commits
35ca108fa1
...
64818c08f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64818c08f6 | ||
|
|
3ad4affef2 | ||
|
|
e88fa79f09 | ||
|
|
07575da983 | ||
|
|
010dbf8e9e |
@@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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
|
- Install via **Scoop**, **Homebrew**, and **winget** in addition to the
|
||||||
existing channels.
|
existing channels.
|
||||||
- Tier-4 end-to-end test suite that exercises the real application in a
|
- Tier-4 end-to-end test suite that exercises the real application in a
|
||||||
@@ -18,6 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
on each build.
|
on each build.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Pasting or scripting several commands at once no longer occasionally
|
||||||
|
rejects a valid simple-mode `insert` — submitted right after adding a
|
||||||
|
column — as though it were advanced-mode SQL.
|
||||||
- **Light theme:** string-literal and flag colours in syntax highlighting
|
- **Light theme:** string-literal and flag colours in syntax highlighting
|
||||||
were below the WCAG-AA contrast bar; both are now legible. Two dark-theme
|
were below the WCAG-AA contrast bar; both are now legible. Two dark-theme
|
||||||
token colours that were hard to tell apart have been separated.
|
token colours that were hard to tell apart have been separated.
|
||||||
|
|||||||
@@ -246,6 +246,20 @@ Key invariants in the code:
|
|||||||
the specific product (SQLite, STRICT, rusqlite, PRAGMA).
|
the specific product (SQLite, STRICT, rusqlite, PRAGMA).
|
||||||
ADR-internal prose and code comments may name it where
|
ADR-internal prose and code comments may name it where
|
||||||
technically necessary for precision.
|
technically necessary for precision.
|
||||||
|
- **Changelog discipline.** `CHANGELOG.md` (repo root) tracks
|
||||||
|
notable **user-facing** changes (Keep a Changelog + SemVer).
|
||||||
|
Update it in the **same change** that introduces a user-facing
|
||||||
|
behaviour change: add or amend a bullet under `[Unreleased]`
|
||||||
|
in the right category (Added / Changed / Deprecated / Removed /
|
||||||
|
Fixed / Security), phrased for end users under the two copy
|
||||||
|
rules above (no engine name; no "DSL" — say "simple mode" /
|
||||||
|
"advanced mode"). A change with no user-visible effect (pure
|
||||||
|
refactor, internal tests, CI plumbing) gets **no** entry — and
|
||||||
|
the judgement of "user-facing" includes scripted / pasted /
|
||||||
|
power-user paths, not just the interactive happy path. At
|
||||||
|
release time, rename `[Unreleased]` to the new version + date,
|
||||||
|
add the compare link, and **sweep the commits/handoffs since
|
||||||
|
the last tag** as a backstop for anything missed.
|
||||||
- **Confirm commits.** Per the user's global rules, every
|
- **Confirm commits.** Per the user's global rules, every
|
||||||
`git commit` is preceded by an explicit message proposal
|
`git commit` is preceded by an explicit message proposal
|
||||||
and user approval. No AI attribution in commit messages.
|
and user approval. No AI attribution in commit messages.
|
||||||
|
|||||||
@@ -824,6 +824,63 @@ of issue #4; no `AmbientHint` / renderer change. Covered by
|
|||||||
seed_count_hint_does_not_leak_once_the_count_or_a_clause_is_given,
|
seed_count_hint_does_not_leak_once_the_count_or_a_clause_is_given,
|
||||||
seed_count_hint_also_fires_after_a_column_fill_target}`.
|
seed_count_hint_also_fires_after_a_column_fill_target}`.
|
||||||
|
|
||||||
|
## Amendment 8 — submission gate: hold input until the schema-cache refresh lands (2026-06-22)
|
||||||
|
|
||||||
|
§9 established the schema cache as the source of truth for the
|
||||||
|
walker's schema-aware dispatch, "refreshed by the runtime … after
|
||||||
|
successful DDL". That refresh is **asynchronous**: the runtime applies
|
||||||
|
a command on the worker thread (ADR-0010) and only afterwards posts a
|
||||||
|
`SchemaCacheRefreshed` event back through the **same FIFO channel that
|
||||||
|
carries key events**. Between dispatching a DDL command and that event
|
||||||
|
landing, `schema_cache` is stale w.r.t. the command just run.
|
||||||
|
|
||||||
|
Validation in `App::update` is pure-sync (a core invariant — it cannot
|
||||||
|
do a DB round-trip), so it can only consult that cache. Under
|
||||||
|
**faster-than-human input** (paste, scripted input, an unpaced PTY
|
||||||
|
driver), the next submission's Enter is already queued *ahead* of the
|
||||||
|
refresh event, so it is validated against the stale cache. A simple-mode
|
||||||
|
**Form-B insert** (`insert into T values (…)`, columns derived from the
|
||||||
|
cache) submitted right after `add column` then sees the pre-DDL columns,
|
||||||
|
its value arity can't match, and the friendly layer tags it *"trying to
|
||||||
|
write SQL?"* — even though the identical line succeeds at human speed
|
||||||
|
(issue **#39**).
|
||||||
|
|
||||||
|
**Decision — gate submission on the pending refresh.** `App` carries
|
||||||
|
`awaiting_schema_refresh: bool` + a `held_submissions` FIFO queue.
|
||||||
|
`dispatch_dsl` arms the flag on **every** `ExecuteDsl` dispatch; while
|
||||||
|
armed, a new DSL submission is **held** (queued in submission order)
|
||||||
|
rather than validated. The `SchemaCacheRefreshed` handler clears the
|
||||||
|
flag and **drains** the queue against the now-fresh cache, stopping as
|
||||||
|
soon as a drained command re-arms the gate (so the remainder wait for
|
||||||
|
*its* refresh — order preserved). A held command that doesn't dispatch
|
||||||
|
(parse error / pre-flight rejection) leaves the gate open and the loop
|
||||||
|
continues. App-lifecycle commands (`quit` / `help` / `load` / `undo` /
|
||||||
|
`rebuild`) route through `dispatch_app_command` *before* `dispatch_dsl`
|
||||||
|
and so are never held.
|
||||||
|
|
||||||
|
**Why arm on every dispatch, not just DDL.** It keeps at most one DSL
|
||||||
|
command in flight, so refreshes are strictly one-per-dispatch and in
|
||||||
|
order — making the gate a provably-correct boolean. Arming only on
|
||||||
|
schema-mutating commands would let a *preceding* non-DDL command's
|
||||||
|
refresh clear the gate early and drain a held Form-B insert against a
|
||||||
|
cache that predates the DDL — reintroducing the bug in a corner case.
|
||||||
|
(App-lifecycle commands also refresh but bypass the gate; they are
|
||||||
|
modal/picker-gated and so cannot overlap a rapid DSL paste, the only
|
||||||
|
thing this guards.)
|
||||||
|
|
||||||
|
**Scope.** Interactive input only. The `replay` / history-log / startup
|
||||||
|
rebuild-from-text batch path already re-snapshots the schema
|
||||||
|
**synchronously, inline, before every line** (`run_replay`,
|
||||||
|
`build_schema_cache`), so it has always had this ordering guarantee;
|
||||||
|
this amendment brings the interactive path in line with it. No
|
||||||
|
interactive-user impact (the gate clears in milliseconds); held input is
|
||||||
|
never lost because the runtime sends a `SchemaCacheRefreshed` after
|
||||||
|
*every* dispatch, success or failure. Covered by
|
||||||
|
`app::tests::form_b_insert_after_ddl_is_held_until_refresh_then_dispatched`
|
||||||
|
(Tier-1, deterministic event ordering) and the Tier-4 PTY regression
|
||||||
|
`e2e_pty::back_to_back_insert_after_ddl_still_succeeds` (the unpaced
|
||||||
|
inverse of flow 3).
|
||||||
|
|
||||||
## Out of scope
|
## Out of scope
|
||||||
|
|
||||||
Deliberately deferred to keep this ADR shippable as a single
|
Deliberately deferred to keep this ADR shippable as a single
|
||||||
|
|||||||
@@ -706,6 +706,56 @@ documentation is still hand-curated for round 1.
|
|||||||
table-ident from new-name-ident visually is a future
|
table-ident from new-name-ident visually is a future
|
||||||
enhancement.
|
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.<id>` block per `Some` id) and the
|
||||||
|
`help <topic>` **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 <topic>` 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 <topic>` 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
|
## References
|
||||||
|
|
||||||
- ADR-0023 — Unified declarative grammar tree (Proposed direction). Superseded by this ADR for execution detail.
|
- ADR-0023 — Unified declarative grammar tree (Proposed direction). Superseded by this ADR for execution detail.
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -7,18 +7,22 @@ binaries ship: **TT4** (Tier-4 PTY tests), **NFR verification**, and a
|
|||||||
|
|
||||||
## §1. State
|
## §1. State
|
||||||
|
|
||||||
**Branch `main`.** Three commits this session:
|
**Branch `main`.** Commits this session (all on `main`, **not pushed** — push
|
||||||
|
is the user's step):
|
||||||
- `65eab71` `fix(theme)` — WCAG-AA palette fix + contrast/ΔE2000 gates +
|
- `65eab71` `fix(theme)` — WCAG-AA palette fix + contrast/ΔE2000 gates +
|
||||||
`scripts/palette-preview.py`.
|
`scripts/palette-preview.py`.
|
||||||
- `fd63de3` `test(tt4)` — Tier-4 PTY end-to-end suite (`tests/e2e_pty.rs`).
|
- `fd63de3` `test(tt4)` — Tier-4 PTY end-to-end suite (`tests/e2e_pty.rs`).
|
||||||
- **(pending)** a docs commit — ADR-0008 Amendment 1, ADR-0057, README index,
|
- `88204f2` `docs(tt4,nfr)` — ADR-0008 Amendment 1, ADR-0057, README index,
|
||||||
`requirements.md`, `CHANGELOG.md`, this handoff, and the plan doc
|
`requirements.md`, `CHANGELOG.md`, handoff-76, plan doc.
|
||||||
`docs/plans/20260622-tt4-nfr-changelog.md`. *(Propose + confirm before
|
- `1ffe11c` `fix(tt4)` — drop the dead `pid` helper (macOS-only dead-code
|
||||||
committing; not yet committed at time of writing.)*
|
warning the Linux gate can't see; found via the user's macOS run).
|
||||||
|
- `35ca108` `docs(tt5)` — macOS Tier-4 confirmation + Windows verification
|
||||||
|
stance (and the §7 below was added in a follow-up docs commit).
|
||||||
|
|
||||||
**Test baseline: 2519 passed / 0 failed / 1 ignored** (was 2509; +4 theme
|
**Test baseline: 2519 passed / 0 failed / 1 ignored** (was 2509; +4 theme
|
||||||
gates, +6 e2e_pty). `clippy --all-targets -D warnings` + `fmt --check` clean.
|
gates, +6 e2e_pty). `clippy --all-targets -D warnings` + `fmt --check` clean.
|
||||||
Full suite verified green 3× under parallel load.
|
Full suite verified green 3× under parallel load on Linux, and a full native
|
||||||
|
run on **macOS** (5 e2e_pty there — Linux-only RSS test cfg-skipped).
|
||||||
|
|
||||||
## §2. What shipped
|
## §2. What shipped
|
||||||
|
|
||||||
@@ -83,10 +87,10 @@ managers + TT4 are post-tag → Unreleased.
|
|||||||
- **TT5 remaining:** only a **Windows execution runner** now (macOS + Tier-4
|
- **TT5 remaining:** only a **Windows execution runner** now (macOS + Tier-4
|
||||||
gaps closed this session). First real CI run is the final confirmation that
|
gaps closed this session). First real CI run is the final confirmation that
|
||||||
the PTY tests pass in the Gitea container (validated locally; very low risk).
|
the PTY tests pass in the Gitea container (validated locally; very low risk).
|
||||||
- Untouched larger items from the "what's next" survey: hint/help issues
|
- **Next focus is the open issues — see §7** (user direction 2026-06-22: clear
|
||||||
**#36/#37/#38**; the big features (V4 session journal, TU1 tutorial). The
|
the open issues before resuming feature work). Larger features (V4 session
|
||||||
CHANGELOG can later feed `--release-notes-url` into the CI winget job
|
journal, TU1 tutorial) come after. The CHANGELOG can later feed
|
||||||
(handoff-75 §6).
|
`--release-notes-url` into the CI winget job (handoff-75 §6).
|
||||||
|
|
||||||
## §5. Process pins
|
## §5. Process pins
|
||||||
|
|
||||||
@@ -119,3 +123,28 @@ managers + TT4 are post-tag → Unreleased.
|
|||||||
note was reworded to say this plainly and welcomingly; it stays `[/]` by
|
note was reworded to say this plainly and welcomingly; it stays `[/]` by
|
||||||
deliberate scope. (Virtualization options explored for a future runner are not
|
deliberate scope. (Virtualization options explored for a future runner are not
|
||||||
recorded here — they hinge on host specifics rather than project constraints.)
|
recorded here — they hinge on host specifics rather than project constraints.)
|
||||||
|
|
||||||
|
## §7. Next session — start with the open issues
|
||||||
|
|
||||||
|
Per user direction (2026-06-22), the next session should **work the open Gitea
|
||||||
|
issues before resuming feature work** (V4 journal, TU1 tutorial, etc.). There
|
||||||
|
are **four open** (`tea issues list --state open --limit 100`; read a body with
|
||||||
|
`tea issue <n> --fields body --output json < /dev/null | jq -r '.body'`, and
|
||||||
|
comments with `tea issue <n> --comments < /dev/null`):
|
||||||
|
|
||||||
|
| # | Label | One-line | This-session read on scope |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **#36** | enhancement | `help <sql-form>` shows no distinct content — the seven advanced-mode SQL nodes carry `help_id: None` (a dedup hack for the `help` list; `src/dsl/grammar/mod.rs:915-918`), so e.g. `help select` resolves to nothing. The parse-error/usage layer (ADR-0042/H1a) already distinguishes them; only the `help` command lags. | **Contained.** Good first pick — a real, bounded gap. Touches H3/`help`; an ADR amendment may apply. |
|
||||||
|
| **#37** | enhancement | Clause-concept hints — deeper teaching when the cursor sits inside a recognized *clause* (`on delete ⟨cascade\|set null\|restrict⟩`, the `create table` constraint slots, `with pk`, `1:n`/`m:n`), between tier-2's candidate list and the whole-form tier-3 block. Deferred extension of **ADR-0053** (H2). | **Medium scope, richest teaching value** — squarely on the pedagogy mission. Likely an ADR-0053 amendment. |
|
||||||
|
| **#38** | enhancement | Pre-submit-diagnostic F1 route + ~33 `diagnostic.*` tier-3 blocks. Needs a `class`/`message_key` field threaded through **every** diagnostic-creation site (walker + validators). Deferred from ADR-0053 Phase C. | **Broad mechanism change for the most marginal value** (tier-2 already surfaces these). The issue itself says so. **Decision needed:** do, defer, or close as wontfix — escalate to the user. |
|
||||||
|
| **#39** | bug | Simple-mode **Form-B insert** (`insert into T values (…)`) submitted faster than the post-DDL **schema-cache refresh** validates against stale schema → misparsed as SQL. Filed this session; repro + diagnosis (incl. `SchemaCache` at `src/completion.rs:53`, Form-B handling `src/dsl/walker/context.rs:155-157`) in the issue. | **Low impact** (no interactive-user hit; cast driver + tests pace). Fix is its own focused change (sequence the cache refresh with command execution, or validate against the authoritative schema). |
|
||||||
|
|
||||||
|
**Suggested order (a recommendation, not a mandate — confirm with the user):**
|
||||||
|
#36 (contained warm-up) → #37 (highest on-mission value) → #39 (bug; size the fix
|
||||||
|
first) → #38 (get the user's do/defer/close call before investing in the broad
|
||||||
|
threading change). All four are hint/help/parse-adjacent except #39; reading
|
||||||
|
**ADR-0053** (the contextual-hint design) first will orient #37 and #38.
|
||||||
|
|
||||||
|
No new labels needed (`bug`/`enhancement` cover them; ask the user before
|
||||||
|
creating any). Issue-tracker etiquette + `tea` gotchas are in the project
|
||||||
|
`CLAUDE.md` ("Issue tracking — Gitea via `tea`").
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Session handoff — 2026-06-22 (77)
|
||||||
|
|
||||||
|
Continues from handoff-76. Per the user's direction (handoff-76 §7: clear the
|
||||||
|
open Gitea issues before resuming feature work), this session took the **bug**
|
||||||
|
of the four open issues — **#39** (Form-B insert misparse against a stale schema
|
||||||
|
cache after fast DDL) — and fixed it end-to-end. Three open issues remain
|
||||||
|
(#36/#37/#38, all enhancements); see §3.
|
||||||
|
|
||||||
|
## §1. State
|
||||||
|
|
||||||
|
**Branch `main`.** Commits this session (on `main`, **not pushed** — push is the
|
||||||
|
user's step):
|
||||||
|
|
||||||
|
- `07575da` `fix(app)` — schema-refresh submission gate (#39) + Tier-1 and
|
||||||
|
Tier-4 PTY regression tests + ADR-0022 Amendment 8 + README index.
|
||||||
|
- this `docs(handoff-77)` commit — folds in the `CHANGELOG.md` `[Unreleased]`
|
||||||
|
Fixed bullet for #39 **and** a new **changelog-discipline rule** in project
|
||||||
|
`CLAUDE.md` (see §4).
|
||||||
|
|
||||||
|
**Test baseline: 2521 passed / 0 failed / 1 ignored** (was 2519; +1 lib test,
|
||||||
|
+1 e2e_pty test → e2e_pty now 7 on Linux). `clippy --all-targets -D warnings` +
|
||||||
|
`fmt --check` clean. The new PTY test was confirmed to **time out without the
|
||||||
|
fix** (genuine guard), green with it.
|
||||||
|
|
||||||
|
**Issue #39 is closed** (resolution comment + diagnosis recorded on the issue).
|
||||||
|
|
||||||
|
## §2. What shipped — issue #39
|
||||||
|
|
||||||
|
**Root cause.** `App::update` is pure-sync and validates submissions against
|
||||||
|
`App::schema_cache`. The runtime refreshes that cache **asynchronously** after
|
||||||
|
the worker applies a command — a `SchemaCacheRefreshed` event posted on the
|
||||||
|
**same FIFO channel as key events** (`runtime.rs:1591`, applied `app.rs`
|
||||||
|
`SchemaCacheRefreshed` arm). Under faster-than-human input (paste / script /
|
||||||
|
unpaced PTY) the next Enter is processed *before* the refresh lands, so a
|
||||||
|
simple-mode **Form-B insert** (`insert into T values (…)`, columns derived from
|
||||||
|
the cache) submitted right after `add column` sees the pre-DDL columns, its
|
||||||
|
arity can't match, and the friendly layer tags it *"trying to write SQL?"*. The
|
||||||
|
worker itself is never wrong (it runs commands serially); the bug was purely in
|
||||||
|
client-side pre-validation racing the refresh.
|
||||||
|
|
||||||
|
**Fix — submission gate (`src/app.rs`).** Two new private `App` fields:
|
||||||
|
`awaiting_schema_refresh: bool` + `held_submissions: VecDeque<(String,
|
||||||
|
EffectiveMode)>`. `dispatch_dsl` arms the flag on **every** `ExecuteDsl`
|
||||||
|
dispatch; while armed, a new DSL submission is **held** (queued in submission
|
||||||
|
order) rather than validated. The `SchemaCacheRefreshed` handler clears the flag
|
||||||
|
and **drains** the queue against the now-fresh cache, stopping as soon as a
|
||||||
|
drained command re-arms the gate (the rest then wait for *its* refresh — order
|
||||||
|
preserved). App-lifecycle commands route through `dispatch_app_command` *before*
|
||||||
|
`dispatch_dsl`, so `quit`/`help`/`load`/`undo`/`rebuild` are never held.
|
||||||
|
|
||||||
|
**Why arm on every dispatch, not just DDL** — it keeps at most one DSL command
|
||||||
|
in flight, so refreshes are strictly one-per-dispatch and in order, making the
|
||||||
|
boolean provably correct. Arming only on DDL would let a *preceding* non-DDL
|
||||||
|
command's refresh clear the gate early and drain a held insert against a
|
||||||
|
pre-DDL cache (a real corner case). Cost was one existing test
|
||||||
|
(`walking_skeleton::colon_escape_in_simple_mode_is_one_shot`) updated to model
|
||||||
|
the post-dispatch refresh — faithful, since it had been assuming it.
|
||||||
|
|
||||||
|
**Scope = interactive only.** The `replay` / history-log / startup
|
||||||
|
rebuild-from-text batch path already re-snapshots the schema **synchronously,
|
||||||
|
inline, before every line** (`run_replay` → `build_schema_cache`,
|
||||||
|
`runtime.rs:2514`), so it always had this ordering guarantee; the fix brings the
|
||||||
|
interactive path in line with it. No interactive-user impact (the gate clears in
|
||||||
|
ms); held input is never lost because the runtime sends a `SchemaCacheRefreshed`
|
||||||
|
after **every** dispatch, success or failure (the unconditional post-match block
|
||||||
|
in `spawn_dsl_dispatch`).
|
||||||
|
|
||||||
|
**Tests** (both verified red→green):
|
||||||
|
- `app::tests::form_b_insert_after_ddl_is_held_until_refresh_then_dispatched` —
|
||||||
|
Tier-1, deterministic: stale cache → submit DDL (arms) → submit insert (held,
|
||||||
|
no error) → deliver fresh `SchemaCacheRefreshed` → insert dispatches.
|
||||||
|
- `e2e_pty::back_to_back_insert_after_ddl_still_succeeds` — Tier-4 PTY, the
|
||||||
|
unpaced inverse of flow 3; asserts the insert's own `('Alice') ✓` echo.
|
||||||
|
|
||||||
|
**Docs.** ADR-0022 **Amendment 8** records the gate (the §9 schema-cache
|
||||||
|
refresh-vs-validation timing contract); README index updated in the same edit
|
||||||
|
(ADR-0000 rule).
|
||||||
|
|
||||||
|
## §3. Open / follow-ups
|
||||||
|
|
||||||
|
Per the user's direction the open issues come before feature work. After closing
|
||||||
|
#39, **four** issues are open. The hint/help trio (#36/#37/#38) are all
|
||||||
|
**enhancements** on the pedagogy mission (read handoff-76 §7's table for fuller
|
||||||
|
scope; read **ADR-0053** first — it orients #37/#38). **#40** is a CI/packaging
|
||||||
|
follow-up filed this session.
|
||||||
|
|
||||||
|
| # | One-line | read |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **#36** | `help <sql-form>` shows no distinct content — the 7 advanced-mode SQL nodes share `help_id: None` (`src/dsl/grammar/mod.rs:915-918`) | **Contained.** Good next pick; touches H3/`help`, maybe an ADR amendment. |
|
||||||
|
| **#37** | Clause-concept hints (cursor inside `on delete …`, `with pk`, `1:n`/`m:n`, create-table constraint slots) — deferred ADR-0053 extension | **Medium scope, richest teaching value.** Likely an ADR-0053 amendment. |
|
||||||
|
| **#38** | Pre-submit-diagnostic F1 route + ~33 `diagnostic.*` tier-3 blocks; needs a `class`/`message_key` threaded through every diagnostic site | **Broad mechanism, most marginal value.** Get the user's do/defer/close call before investing. |
|
||||||
|
| **#40** | Wire `CHANGELOG.md` into the winget release notes (`komac --release-notes-url`); ADR-0056 area | Filed this session (the changelog→winget thread, see §4). Small, deferred-by-decision originally. |
|
||||||
|
|
||||||
|
**Suggested order (confirm with the user):** #36 (contained) → #37 (highest
|
||||||
|
on-mission value) → #38 (decision first: do / defer / close). #40 is independent
|
||||||
|
(release pipeline) and can slot in whenever. #38 in particular should be
|
||||||
|
escalated for a do-or-close call rather than silently built.
|
||||||
|
|
||||||
|
## §4. Changelog discipline — new rule + open winget thread
|
||||||
|
|
||||||
|
The session surfaced that **no rule existed** for keeping `CHANGELOG.md` current
|
||||||
|
(it was created in handoff-76's plan but never given a maintenance process), so
|
||||||
|
the #39 fix wasn't logged until the user asked. Decided with the user:
|
||||||
|
|
||||||
|
- **New `CLAUDE.md` rule (this commit):** update `[Unreleased]` in the **same
|
||||||
|
change** that alters user-facing behaviour (incl. scripted/pasted/power-user
|
||||||
|
paths, not just the interactive happy path), under the two copy rules; no entry
|
||||||
|
for refactor/test/CI-only changes; at release time rename `[Unreleased]` and
|
||||||
|
**sweep commits/handoffs since the last tag** as a backstop.
|
||||||
|
- **#39 entry added** under `[Unreleased] → Fixed`.
|
||||||
|
- **winget release notes → issue #40.** komac supports `--release-notes-url` /
|
||||||
|
`--release-notes`; the `winget` job (`publish.yaml` ≈ L276) passes neither.
|
||||||
|
Tracked, not done (it's a release-pipeline / ADR-0056 change, kept out of this
|
||||||
|
bug-fix session by the user's call).
|
||||||
|
|
||||||
|
## §5. Process pins
|
||||||
|
|
||||||
|
- Commit user-confirmed, no AI attribution, append-only, on `main`; **push is
|
||||||
|
the user's step** (this commit is unpushed).
|
||||||
|
- Test-first honored: both regression tests were confirmed RED before the fix
|
||||||
|
(the PTY one via a `git stash` of `src/app.rs`), GREEN after. A written
|
||||||
|
Devil's-Advocate pass on the implementation surfaced one comment imprecision
|
||||||
|
(fixed) and no behavioral findings.
|
||||||
|
- `cargo sweep` not run this session; the build grew modestly (one extra PTY
|
||||||
|
test binary). Consider a sweep at the next milestone.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Session handoff — 2026-06-22 (78)
|
||||||
|
|
||||||
|
Continues from handoff-77 (issue #39 + changelog rule). This chunk cleared
|
||||||
|
**issue #36** — advanced-mode SQL forms now have distinct `help` content — the
|
||||||
|
first of the enhancement issues the user is working through. Three issues remain
|
||||||
|
open (#37, #38, #40).
|
||||||
|
|
||||||
|
## §1. State
|
||||||
|
|
||||||
|
**Branch `main`.** Work for #36 is staged but **not yet committed** at the time
|
||||||
|
of writing (two commits proposed: a `feat(help)` for the change + a
|
||||||
|
`docs(handoff-78)`). **Not pushed** — push is the user's step.
|
||||||
|
|
||||||
|
**Test baseline: 2525 passed / 0 failed / 1 ignored** (was 2521; +4 new
|
||||||
|
`help_command` integration tests). `clippy --all-targets -D warnings` +
|
||||||
|
`fmt --check` clean. Issue #36 to be **closed** on commit.
|
||||||
|
|
||||||
|
## §2. What shipped — issue #36
|
||||||
|
|
||||||
|
**Problem.** The in-app `help` command gave no distinct content for the six
|
||||||
|
advanced-mode SQL **DML/query** forms (`SELECT`, `WITH`, `SQL_INSERT`,
|
||||||
|
`SQL_UPDATE`, `SQL_DELETE`, `EXPLAIN_SQL`): they carried `help_id: None` (a
|
||||||
|
list-dedup shortcut), so `help select` / `help with` resolved to the
|
||||||
|
unknown-topic note and `help insert` showed only the simple form. (The advanced
|
||||||
|
SQL **DDL** forms already had `help.ddl.sql_*` pages, so the gap was
|
||||||
|
inconsistent too.)
|
||||||
|
|
||||||
|
**Decisions taken with the user** (three forks, all confirmed):
|
||||||
|
1. `help <topic>` shows **both forms** for a shared entry word, mode-blind.
|
||||||
|
2. `select` / `with` get `help_id`s and are **listed** too (consistent with the
|
||||||
|
already-listed SQL DDL forms).
|
||||||
|
3. The `help` **list** is **split by mode** into "Simple-mode commands:" and
|
||||||
|
"Advanced-mode (SQL) commands:" sections — fixing a pre-existing header bug
|
||||||
|
(next item).
|
||||||
|
|
||||||
|
**Fix (near-zero logic):**
|
||||||
|
- Gave all six advanced forms distinct `help_id`s (`data.select`, `data.with`,
|
||||||
|
`data.sql_insert`, `data.sql_update`, `data.sql_delete`, `data.explain_sql`)
|
||||||
|
with hand-curated terse `help.data.*` catalog pages (`src/dsl/grammar/data.rs`,
|
||||||
|
`src/friendly/strings/en-US.yaml`). Distinct strings ⇒ the dedup invariant
|
||||||
|
(`no_two_registered_commands_share_a_help_id`) is untouched. `note_help_topic`
|
||||||
|
needed **no** change — the forms now resolve automatically (so `help insert`
|
||||||
|
shows the simple block + the `sql_insert` block, like `help create` already
|
||||||
|
did).
|
||||||
|
- `note_help` (`src/app.rs`) now **groups by `CommandCategory`**: `app.*`
|
||||||
|
commands first (unlabelled, under the intro — they work in either mode), then
|
||||||
|
a simple-mode group and an advanced-mode (SQL) group. New catalog keys
|
||||||
|
`help.simple_section` / `help.advanced_section` replace the old
|
||||||
|
`help.dsl_section`.
|
||||||
|
- Trimmed `help.data.explain`'s advanced lines (the `explain_sql` page now owns
|
||||||
|
that). Updated `src/friendly/keys.rs` (catalog-key registry) and the stale
|
||||||
|
`help_id`-rationale comments in `src/dsl/grammar/{mod,data}.rs`.
|
||||||
|
|
||||||
|
**Copy-rule fix (bonus, in scope).** The old list header
|
||||||
|
`"DSL data commands (in simple mode):"` violated the project copy rule (the
|
||||||
|
banned word **"DSL"**) *and* mis-labelled the advanced SQL forms it already
|
||||||
|
contained as "simple mode". The split headers fix both. Verified the full `help`
|
||||||
|
output no longer contains "DSL".
|
||||||
|
|
||||||
|
**Tests** (all four red→green): `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}`. Rendered output eyeballed
|
||||||
|
(alignment, both-forms, the three list groups).
|
||||||
|
|
||||||
|
**Docs.** ADR-0024 **Amendment 1** (owns `help_id`); README index updated same
|
||||||
|
edit; CHANGELOG `[Unreleased] → Added` entry (per handoff-77's new changelog
|
||||||
|
rule — landed *with* the change this time).
|
||||||
|
|
||||||
|
## §3. Copy-rule audit finding (flagged, NOT fixed) — needs a triage call
|
||||||
|
|
||||||
|
The user asked for a "DSL" sweep of help/hint strings. Result: the only
|
||||||
|
user-facing help/hint violation was the list header (fixed above). The other
|
||||||
|
catalog "DSL" hits are comments / YAML keys (internal — the rule allows those).
|
||||||
|
|
||||||
|
**One bonus finding outside help/hint, deliberately left for the user to
|
||||||
|
triage:** `src/dsl/value.rs:106` returns the error message *"literal `blob`
|
||||||
|
values are not supported in **DSL** yet"* — a likely user-facing copy-rule
|
||||||
|
violation (and it also surfaces an internal term). Not touched (out of #36
|
||||||
|
scope). **Decide: fix now / file an issue / leave.** Worth a quick check of
|
||||||
|
whether that message reaches the user raw or is wrapped by the friendly layer.
|
||||||
|
|
||||||
|
## §4. Open / follow-ups — three issues remain
|
||||||
|
|
||||||
|
| # | One-line | read |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **#37** | Clause-concept hints (cursor inside `on delete …`, `with pk`, `1:n`/`m:n`, create-table constraint slots) — deferred ADR-0053 extension | **Medium scope, richest teaching value.** Likely an ADR-0053 amendment; read ADR-0053 first. |
|
||||||
|
| **#38** | Pre-submit-diagnostic F1 route + ~33 `diagnostic.*` tier-3 blocks; needs a `class`/`message_key` threaded through every diagnostic site | **Broad mechanism, most marginal value.** Get the user's do/defer/close call before building. |
|
||||||
|
| **#40** | Wire `CHANGELOG.md` into the winget release notes (`komac --release-notes-url`); ADR-0056 area | Filed handoff-77; independent release-pipeline work. |
|
||||||
|
| (new?) | `value.rs:106` "DSL" copy-rule violation — see §3 | Not filed yet; awaiting the user's triage call. |
|
||||||
|
|
||||||
|
**Suggested next:** #37 (highest on-mission value) → #38 (escalate do/defer/close
|
||||||
|
first) → #40 (independent). Plus the §3 `value.rs` triage.
|
||||||
|
|
||||||
|
## §5. Process pins
|
||||||
|
|
||||||
|
- Test-first honoured: 4 help tests confirmed RED before the fix, GREEN after.
|
||||||
|
- Written Devil's-Advocate pass on the implementation: no blocking findings
|
||||||
|
(the dedup invariant holds with distinct ids; `note_help_topic` unchanged;
|
||||||
|
category split verified in the rendered output).
|
||||||
|
- Commits user-confirmed, no AI attribution, append-only, on `main`; push is the
|
||||||
|
user's step.
|
||||||
+198
-18
@@ -377,6 +377,31 @@ pub struct App {
|
|||||||
/// by default; refreshed by the runtime on project load
|
/// by default; refreshed by the runtime on project load
|
||||||
/// and after successful DDL.
|
/// and after successful DDL.
|
||||||
pub schema_cache: crate::completion::SchemaCache,
|
pub schema_cache: crate::completion::SchemaCache,
|
||||||
|
/// Issue #39: `true` while a dispatched DSL command's async
|
||||||
|
/// schema-cache refresh is still in flight. The runtime applies a
|
||||||
|
/// command on the worker thread and only *afterwards* sends back a
|
||||||
|
/// `SchemaCacheRefreshed` event; until that lands, `schema_cache` is
|
||||||
|
/// stale w.r.t. the command just dispatched. Validating a follow-up
|
||||||
|
/// submission against that stale cache is the issue #39 bug (a Form-B
|
||||||
|
/// insert after `add column` sees the pre-DDL columns and is wrongly
|
||||||
|
/// rejected as "trying to write SQL?"). While this flag is set, new
|
||||||
|
/// `dispatch_dsl` submissions are *held* in `held_submissions` rather
|
||||||
|
/// than validated, then drained when the refresh arrives. Armed on
|
||||||
|
/// every `ExecuteDsl` dispatch, so at most one *DSL* command is ever in
|
||||||
|
/// flight — refreshes are then strictly one-per-dispatch and in order,
|
||||||
|
/// keeping the gate a simple, provably-correct boolean. (App-lifecycle
|
||||||
|
/// commands — `load` / `undo` / `rebuild` — also refresh the cache but
|
||||||
|
/// bypass this gate; they are modal/picker-gated and so cannot overlap a
|
||||||
|
/// rapid DSL paste, the only thing this guards.) Cleared in the
|
||||||
|
/// `SchemaCacheRefreshed` handler.
|
||||||
|
awaiting_schema_refresh: bool,
|
||||||
|
/// Issue #39: submissions deferred while `awaiting_schema_refresh` is
|
||||||
|
/// set, in submission order. Each is the canonical input line plus the
|
||||||
|
/// effective mode it was submitted under; drained through
|
||||||
|
/// `dispatch_dsl` (re-validated against the now-fresh cache) when the
|
||||||
|
/// pending refresh lands. `push_history` already ran for these at
|
||||||
|
/// `submit` time, so draining re-enters at `dispatch_dsl`, not `submit`.
|
||||||
|
held_submissions: std::collections::VecDeque<(String, EffectiveMode)>,
|
||||||
/// Whether the undo/snapshot machinery is active this session
|
/// Whether the undo/snapshot machinery is active this session
|
||||||
/// (ADR-0006 Amendment 1). `false` under the `--no-undo` CLI
|
/// (ADR-0006 Amendment 1). `false` under the `--no-undo` CLI
|
||||||
/// flag; the `undo` / `redo` commands then report undo is off
|
/// flag; the `undo` / `redo` commands then report undo is off
|
||||||
@@ -596,6 +621,10 @@ impl App {
|
|||||||
modal: None,
|
modal: None,
|
||||||
last_completion: None,
|
last_completion: None,
|
||||||
schema_cache: crate::completion::SchemaCache::default(),
|
schema_cache: crate::completion::SchemaCache::default(),
|
||||||
|
// Issue #39: no command is in flight at construction; the
|
||||||
|
// schema-refresh gate starts open with an empty hold queue.
|
||||||
|
awaiting_schema_refresh: false,
|
||||||
|
held_submissions: std::collections::VecDeque::new(),
|
||||||
// Undo is on by default; the runtime flips this off for
|
// Undo is on by default; the runtime flips this off for
|
||||||
// a `--no-undo` session (ADR-0006 Amendment 1).
|
// a `--no-undo` session (ADR-0006 Amendment 1).
|
||||||
undo_enabled: true,
|
undo_enabled: true,
|
||||||
@@ -912,7 +941,24 @@ impl App {
|
|||||||
"schema cache refreshed",
|
"schema cache refreshed",
|
||||||
);
|
);
|
||||||
self.schema_cache = cache;
|
self.schema_cache = cache;
|
||||||
Vec::new()
|
// Issue #39: the in-flight command's refresh has landed, so
|
||||||
|
// the gate opens. Drain any submissions held while it was in
|
||||||
|
// flight, re-validating each against the now-fresh cache.
|
||||||
|
// Stop as soon as a drained command dispatches (re-arming the
|
||||||
|
// gate via its own `ExecuteDsl`): the remaining held commands
|
||||||
|
// then wait for *its* refresh, preserving submission order. A
|
||||||
|
// held command that does not dispatch (parse error / pre-flight
|
||||||
|
// rejection) leaves the gate open, so the loop continues to the
|
||||||
|
// next held submission.
|
||||||
|
self.awaiting_schema_refresh = false;
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
while !self.awaiting_schema_refresh {
|
||||||
|
let Some((input, submission_mode)) = self.held_submissions.pop_front() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
actions.extend(self.dispatch_dsl(&input, submission_mode));
|
||||||
|
}
|
||||||
|
actions
|
||||||
}
|
}
|
||||||
AppEvent::RelationshipsRefreshed(relationships) => {
|
AppEvent::RelationshipsRefreshed(relationships) => {
|
||||||
trace!(count = relationships.len(), "relationships refreshed");
|
trace!(count = relationships.len(), "relationships refreshed");
|
||||||
@@ -1981,6 +2027,20 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_dsl(&mut self, input: &str, submission_mode: EffectiveMode) -> Vec<Action> {
|
fn dispatch_dsl(&mut self, input: &str, submission_mode: EffectiveMode) -> Vec<Action> {
|
||||||
|
// Issue #39: if a previously-dispatched command's schema-cache
|
||||||
|
// refresh is still in flight, `schema_cache` is stale w.r.t. that
|
||||||
|
// command. Validating this submission now would race it (the bug:
|
||||||
|
// a Form-B insert after `add column` rejected against the pre-DDL
|
||||||
|
// schema). Hold it in submission order; the `SchemaCacheRefreshed`
|
||||||
|
// handler drains the queue once the fresh schema lands. App-level
|
||||||
|
// commands (`quit`, `help`, `load`, …) route through
|
||||||
|
// `dispatch_app_command` *before* here, so they are never held.
|
||||||
|
if self.awaiting_schema_refresh {
|
||||||
|
debug!(input, "holding submission until schema cache refresh lands");
|
||||||
|
self.held_submissions
|
||||||
|
.push_back((input.to_string(), submission_mode));
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
// The two-way mode the walker + the `[mode]` render tag read; the
|
// The two-way mode the walker + the `[mode]` render tag read; the
|
||||||
// three-way `submission_mode` (ADR-0037) rides on `ExecuteDsl` for
|
// three-way `submission_mode` (ADR-0037) rides on `ExecuteDsl` for
|
||||||
// the runtime's echo gate (ADR-0038).
|
// the runtime's echo gate (ADR-0038).
|
||||||
@@ -2069,6 +2129,14 @@ impl App {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
self.push_output(OutputLine::echo(input, mode));
|
self.push_output(OutputLine::echo(input, mode));
|
||||||
|
// Issue #39: a command is now in flight; its schema-cache
|
||||||
|
// refresh will land asynchronously. Arm the gate so any
|
||||||
|
// follow-up submission waits for the fresh schema rather
|
||||||
|
// than racing the stale cache. Armed on every dispatch (not
|
||||||
|
// just DDL) so only one command is ever in flight, which
|
||||||
|
// keeps the gate a simple boolean — refreshes are then
|
||||||
|
// strictly one-per-dispatch and in order.
|
||||||
|
self.awaiting_schema_refresh = true;
|
||||||
vec![Action::ExecuteDsl {
|
vec![Action::ExecuteDsl {
|
||||||
command: cmd,
|
command: cmd,
|
||||||
source: input.to_string(),
|
source: input.to_string(),
|
||||||
@@ -2979,34 +3047,52 @@ impl App {
|
|||||||
/// output panel.
|
/// output panel.
|
||||||
///
|
///
|
||||||
/// Assembled from the command REGISTRY (ADR-0024 §help_id):
|
/// Assembled from the command REGISTRY (ADR-0024 §help_id):
|
||||||
/// the framing (`help.intro`, `help.dsl_section`,
|
/// the framing (`help.intro`, `help.simple_section`,
|
||||||
/// `help.types_reference`) comes from the catalog, and each
|
/// `help.advanced_section`, `help.types_reference`) comes from
|
||||||
/// command's body is the catalog entry named by its
|
/// the catalog, and each command's body is the catalog entry
|
||||||
/// `help_id`. A newly-registered command appears here
|
/// named by its `help_id`. A newly-registered command appears
|
||||||
/// automatically — no edit to this function or a hand-kept
|
/// here automatically — no edit to this function or a hand-kept
|
||||||
/// list. Each catalog line becomes its own `OutputLine` so
|
/// list. Each catalog line becomes its own `OutputLine` so
|
||||||
/// the scroll-position math (one logical line = one display
|
/// the scroll-position math (one logical line = one display
|
||||||
/// row) stays accurate per the renderer's invariant.
|
/// 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) {
|
fn note_help(&mut self) {
|
||||||
use crate::dsl::grammar::REGISTRY;
|
use crate::dsl::grammar::{CommandCategory, REGISTRY};
|
||||||
|
|
||||||
let mut lines: Vec<String> = Vec::new();
|
let mut lines: Vec<String> = Vec::new();
|
||||||
lines.push(crate::t!("help.intro"));
|
lines.push(crate::t!("help.intro"));
|
||||||
// REGISTRY is ordered app-commands first; emit the
|
|
||||||
// "DSL data commands" sub-header at the first command
|
let mut simple: Vec<String> = Vec::new();
|
||||||
// whose help_id leaves the `app.` namespace.
|
let mut advanced: Vec<String> = Vec::new();
|
||||||
let mut dsl_header_done = false;
|
for (command, category) in REGISTRY {
|
||||||
for (command, _category) in REGISTRY {
|
|
||||||
let Some(help_id) = command.help_id else {
|
let Some(help_id) = command.help_id else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !dsl_header_done && !help_id.starts_with("app.") {
|
let body = crate::friendly::translate(&format!("help.{help_id}"), &[]);
|
||||||
lines.push(crate::t!("help.dsl_section"));
|
let block: Vec<String> = body.lines().map(str::to_string).collect();
|
||||||
dsl_header_done = true;
|
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, &[]);
|
if !simple.is_empty() {
|
||||||
lines.extend(body.lines().map(str::to_string));
|
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(
|
lines.extend(
|
||||||
crate::t!("help.types_reference")
|
crate::t!("help.types_reference")
|
||||||
@@ -4614,6 +4700,100 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_b_insert_after_ddl_is_held_until_refresh_then_dispatched() {
|
||||||
|
// Issue #39: a simple-mode Form-B insert (`insert into T values
|
||||||
|
// (…)`, no column list) submitted faster than the post-DDL
|
||||||
|
// schema-cache refresh must NOT be validated against the *stale*
|
||||||
|
// cache — doing so wrongly rejects it as "trying to write SQL?".
|
||||||
|
//
|
||||||
|
// This reproduces, deterministically, the event ordering the async
|
||||||
|
// runtime produces under fast input: a schema-mutating command is
|
||||||
|
// dispatched (arming the gate while its cache refresh is in flight),
|
||||||
|
// a follow-up insert is submitted before the refresh lands, and only
|
||||||
|
// then does the `SchemaCacheRefreshed` event arrive. Pre-fix the
|
||||||
|
// insert is validated against the pre-DDL schema and rejected; with
|
||||||
|
// the gate it is held and dispatched against the fresh schema.
|
||||||
|
use crate::completion::{SchemaCache, TableColumn};
|
||||||
|
use crate::dsl::types::Type;
|
||||||
|
|
||||||
|
// Pre-DDL schema: `Customers` has only the auto `id` (serial). This
|
||||||
|
// is the stale cache a racing Form-B insert would otherwise see —
|
||||||
|
// zero user-fillable columns, so `values ('Alice')` can't match.
|
||||||
|
let mut app = App::new();
|
||||||
|
let mut stale = SchemaCache::default();
|
||||||
|
stale.tables.push("Customers".to_string());
|
||||||
|
stale.columns.push("id".to_string());
|
||||||
|
stale.table_columns.insert(
|
||||||
|
"Customers".to_string(),
|
||||||
|
vec![TableColumn {
|
||||||
|
name: "id".to_string(),
|
||||||
|
user_type: Type::Serial,
|
||||||
|
not_null: true,
|
||||||
|
has_default: false,
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
app.schema_cache = stale;
|
||||||
|
|
||||||
|
// 1. Submit the DDL. It dispatches and arms the gate: a schema
|
||||||
|
// refresh is now (conceptually) in flight.
|
||||||
|
type_str(&mut app, "add column to Customers: Name (text)");
|
||||||
|
let ddl_actions = submit(&mut app);
|
||||||
|
assert!(
|
||||||
|
matches!(ddl_actions.as_slice(), [Action::ExecuteDsl { .. }]),
|
||||||
|
"the DDL should dispatch; got {ddl_actions:?}",
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Submit the Form-B insert *before* the refresh lands. It must be
|
||||||
|
// held — not dispatched, and crucially not rejected against the
|
||||||
|
// stale cache (no error note such as "trying to write SQL?").
|
||||||
|
type_str(&mut app, "insert into Customers values ('Alice')");
|
||||||
|
let held_actions = submit(&mut app);
|
||||||
|
assert!(
|
||||||
|
held_actions.is_empty(),
|
||||||
|
"the insert must be held while the refresh is in flight, \
|
||||||
|
not dispatched or rejected; got {held_actions:?}",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!app.output.iter().any(|l| l.kind == OutputKind::Error),
|
||||||
|
"a held insert must produce no error note (e.g. the \
|
||||||
|
'trying to write SQL?' pointer); errors:\n{}",
|
||||||
|
error_lines(&app),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. The DDL's schema refresh lands carrying the fresh schema
|
||||||
|
// (`Customers` now has `id` + `Name`). The held insert drains and
|
||||||
|
// dispatches, validated against the up-to-date cache.
|
||||||
|
let mut fresh = SchemaCache::default();
|
||||||
|
fresh.tables.push("Customers".to_string());
|
||||||
|
fresh.columns.push("id".to_string());
|
||||||
|
fresh.columns.push("Name".to_string());
|
||||||
|
fresh.table_columns.insert(
|
||||||
|
"Customers".to_string(),
|
||||||
|
vec![
|
||||||
|
TableColumn {
|
||||||
|
name: "id".to_string(),
|
||||||
|
user_type: Type::Serial,
|
||||||
|
not_null: true,
|
||||||
|
has_default: false,
|
||||||
|
},
|
||||||
|
TableColumn::new("Name", Type::Text),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let drained = app.update(AppEvent::SchemaCacheRefreshed(fresh));
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
drained.as_slice(),
|
||||||
|
[Action::ExecuteDsl {
|
||||||
|
command: Command::Insert { .. },
|
||||||
|
..
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
"the held insert must dispatch once the fresh schema lands; \
|
||||||
|
got {drained:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple_mode_submit_of_pure_dsl_error_has_no_advanced_pointer() {
|
fn simple_mode_submit_of_pure_dsl_error_has_no_advanced_pointer() {
|
||||||
// A DSL error that is *not* valid SQL either (unknown command)
|
// A DSL error that is *not* valid SQL either (unknown command)
|
||||||
|
|||||||
+13
-13
@@ -1886,12 +1886,12 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode {
|
|||||||
entry: Word::keyword("explain"),
|
entry: Word::keyword("explain"),
|
||||||
shape: EXPLAIN_SQL_SHAPE,
|
shape: EXPLAIN_SQL_SHAPE,
|
||||||
ast_builder: build_explain_sql,
|
ast_builder: build_explain_sql,
|
||||||
// No `help_id` / `usage_ids` — this is the `Advanced` half of the
|
// Issue #36: its own `help` page (`data.explain_sql`), listed under the
|
||||||
// shared `explain` entry word, so it defers to the `Simple`
|
// advanced-mode (SQL) section and shown by `help explain` next to the
|
||||||
// `EXPLAIN` node's help/usage (which now covers the SQL forms
|
// simple `EXPLAIN` form (distinct help_id ⇒ no dedup clash). `usage_ids`
|
||||||
// too). Mirrors the `SQL_INSERT`/`SQL_UPDATE`/`SQL_DELETE`
|
// stays empty — the `Simple` `EXPLAIN` node's usage block already covers
|
||||||
// precedent; otherwise `note_help` would print `explain` twice.
|
// the shared `explain` entry word.
|
||||||
help_id: None,
|
help_id: Some("data.explain_sql"),
|
||||||
hint_ids: &["explain_sql"],
|
hint_ids: &["explain_sql"],
|
||||||
usage_ids: &[],
|
usage_ids: &[],
|
||||||
};
|
};
|
||||||
@@ -1902,13 +1902,13 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode {
|
|||||||
/// The shape is the post-`SELECT` portion of a top-level
|
/// The shape is the post-`SELECT` portion of a top-level
|
||||||
/// statement; the registry's entry-word dispatch consumes the
|
/// statement; the registry's entry-word dispatch consumes the
|
||||||
/// leading `SELECT` keyword before the shape walks (sub-phase
|
/// leading `SELECT` keyword before the shape walks (sub-phase
|
||||||
/// 2c migration). `help_id` is `None` until the `help sql`
|
/// 2c migration). Carries its own `help` page (`data.select`),
|
||||||
/// page lands (ADR-0030 Phase 6).
|
/// listed under the advanced-mode (SQL) section (issue #36).
|
||||||
pub static SELECT: CommandNode = CommandNode {
|
pub static SELECT: CommandNode = CommandNode {
|
||||||
entry: Word::keyword("select"),
|
entry: Word::keyword("select"),
|
||||||
shape: Node::Subgrammar(&sql_select::SQL_SELECT_TAIL),
|
shape: Node::Subgrammar(&sql_select::SQL_SELECT_TAIL),
|
||||||
ast_builder: build_select,
|
ast_builder: build_select,
|
||||||
help_id: None,
|
help_id: Some("data.select"),
|
||||||
hint_ids: &["select"],
|
hint_ids: &["select"],
|
||||||
usage_ids: &["parse.usage.select"],
|
usage_ids: &["parse.usage.select"],
|
||||||
};
|
};
|
||||||
@@ -1924,7 +1924,7 @@ pub static WITH: CommandNode = CommandNode {
|
|||||||
entry: Word::keyword("with"),
|
entry: Word::keyword("with"),
|
||||||
shape: Node::Subgrammar(&sql_select::SQL_WITH_TAIL),
|
shape: Node::Subgrammar(&sql_select::SQL_WITH_TAIL),
|
||||||
ast_builder: build_select,
|
ast_builder: build_select,
|
||||||
help_id: None,
|
help_id: Some("data.with"), // issue #36: own help page, advanced section
|
||||||
hint_ids: &["with"],
|
hint_ids: &["with"],
|
||||||
usage_ids: &["parse.usage.with"],
|
usage_ids: &["parse.usage.with"],
|
||||||
};
|
};
|
||||||
@@ -1943,7 +1943,7 @@ pub static SQL_INSERT: CommandNode = CommandNode {
|
|||||||
entry: Word::keyword("insert"),
|
entry: Word::keyword("insert"),
|
||||||
shape: Node::Subgrammar(&sql_insert::SQL_INSERT_SHAPE),
|
shape: Node::Subgrammar(&sql_insert::SQL_INSERT_SHAPE),
|
||||||
ast_builder: build_sql_insert,
|
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"],
|
hint_ids: &["sql_insert"],
|
||||||
usage_ids: &[],
|
usage_ids: &[],
|
||||||
};
|
};
|
||||||
@@ -1957,7 +1957,7 @@ pub static SQL_UPDATE: CommandNode = CommandNode {
|
|||||||
entry: Word::keyword("update"),
|
entry: Word::keyword("update"),
|
||||||
shape: Node::Subgrammar(&sql_update::SQL_UPDATE_SHAPE),
|
shape: Node::Subgrammar(&sql_update::SQL_UPDATE_SHAPE),
|
||||||
ast_builder: build_sql_update,
|
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"],
|
hint_ids: &["sql_update"],
|
||||||
usage_ids: &[],
|
usage_ids: &[],
|
||||||
};
|
};
|
||||||
@@ -1973,7 +1973,7 @@ pub static SQL_DELETE: CommandNode = CommandNode {
|
|||||||
entry: Word::keyword("delete"),
|
entry: Word::keyword("delete"),
|
||||||
shape: Node::Subgrammar(&sql_delete::SQL_DELETE_SHAPE),
|
shape: Node::Subgrammar(&sql_delete::SQL_DELETE_SHAPE),
|
||||||
ast_builder: build_sql_delete,
|
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"],
|
hint_ids: &["sql_delete"],
|
||||||
usage_ids: &[],
|
usage_ids: &[],
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-6
@@ -537,8 +537,11 @@ pub struct CommandNode {
|
|||||||
/// block). `hint_key_for_input_in_mode` disambiguates by the form
|
/// block). `hint_key_for_input_in_mode` disambiguates by the form
|
||||||
/// word, reusing `usage_key_for_input_in_mode`'s logic. Empty
|
/// word, reusing `usage_key_for_input_in_mode`'s logic. Empty
|
||||||
/// until a form's tier-3 block is authored (the surface falls back
|
/// 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
|
/// to tier-2 ambient/error text). Parallel to `help_id` but
|
||||||
/// `None` on advanced-SQL forms purely to dedup the `help` list).
|
/// finer-grained: every form (simple and advanced) carries a
|
||||||
|
/// `hint_id`, whereas the `help <topic>` 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],
|
pub hint_ids: &'static [&'static str],
|
||||||
/// Catalog keys under `parse.usage.*` to render in the
|
/// Catalog keys under `parse.usage.*` to render in the
|
||||||
/// "usage:" block when a parse error fires for this command
|
/// "usage:" block when a parse error fires for this command
|
||||||
@@ -1156,10 +1159,12 @@ mod usage_key_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn no_two_registered_commands_share_a_help_id() {
|
fn no_two_registered_commands_share_a_help_id() {
|
||||||
// `note_help` emits one help block per `help_id: Some(_)`
|
// `note_help` emits one help block per `help_id: Some(_)`
|
||||||
// with no dedup, so a duplicate help_id prints the same
|
// with no dedup, so a duplicate help_id string prints the same
|
||||||
// command twice in `help`. Shared-entry-word `Advanced`
|
// block twice. Distinct help_ids are fine — a shared-entry-word
|
||||||
// nodes (SQL_INSERT, …, EXPLAIN_SQL) therefore carry
|
// simple + SQL pair (e.g. `data.insert` + `data.sql_insert`,
|
||||||
// `help_id: None` and defer to their `Simple` sibling.
|
// issue #36) each get their own block, grouped under one topic
|
||||||
|
// by `help <topic>` and split across the simple/advanced
|
||||||
|
// sections of the full list.
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
for (command, _category) in super::REGISTRY {
|
for (command, _category) in super::REGISTRY {
|
||||||
if let Some(id) = command.help_id {
|
if let Some(id) = command.help_id {
|
||||||
|
|||||||
@@ -180,7 +180,8 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
// In-app `help` — framing + per-command entries keyed by
|
// In-app `help` — framing + per-command entries keyed by
|
||||||
// each CommandNode's `help_id` (ADR-0024 §help_id).
|
// each CommandNode's `help_id` (ADR-0024 §help_id).
|
||||||
("help.intro", &[]),
|
("help.intro", &[]),
|
||||||
("help.dsl_section", &[]),
|
("help.simple_section", &[]),
|
||||||
|
("help.advanced_section", &[]),
|
||||||
("help.types_reference", &[]),
|
("help.types_reference", &[]),
|
||||||
("help.detail_hint", &[]),
|
("help.detail_hint", &[]),
|
||||||
("help.unknown_topic", &["topic"]),
|
("help.unknown_topic", &["topic"]),
|
||||||
@@ -223,6 +224,13 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("help.data.delete", &[]),
|
("help.data.delete", &[]),
|
||||||
("help.data.replay", &[]),
|
("help.data.replay", &[]),
|
||||||
("help.data.explain", &[]),
|
("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 panel ambient typing assistance (ADR-0022 §6) ----
|
||||||
("hint.ambient_complete", &[]),
|
("hint.ambient_complete", &[]),
|
||||||
("hint.ambient_error_with_usage", &["message", "usage"]),
|
("hint.ambient_error_with_usage", &["message", "usage"]),
|
||||||
|
|||||||
@@ -246,7 +246,13 @@ help:
|
|||||||
# are multi-line-capable — the renderer emits one output row
|
# are multi-line-capable — the renderer emits one output row
|
||||||
# per line so scroll math stays accurate.
|
# per line so scroll math stays accurate.
|
||||||
intro: "Supported commands:"
|
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
|
# H3: footer on the full `help` list, and the not-found note
|
||||||
# for `help <topic>`. `{topic}` is the word the user typed.
|
# for `help <topic>`. `{topic}` is the word the user typed.
|
||||||
detail_hint: "Type `help <command>` for detail on one command (e.g. `help insert`), or `help types` for the type reference."
|
detail_hint: "Type `help <command>` for detail on one command (e.g. `help insert`), or `help types` for the type reference."
|
||||||
@@ -368,8 +374,25 @@ help:
|
|||||||
explain show data <T> | explain update <T> ... | explain delete from <T> ...
|
explain show data <T> | explain update <T> ... | explain delete from <T> ...
|
||||||
— show how the database would run a query, without
|
— show how the database would run a query, without
|
||||||
running it (safe even for update / delete)
|
running it (safe even for update / delete)
|
||||||
explain <select|with|insert|update|delete …> (advanced mode)
|
# Issue #36: advanced-mode (SQL) forms. Each has its own help page, listed
|
||||||
— the same plan for the SQL you wrote
|
# under the "Advanced-mode (SQL) commands:" section and shown by
|
||||||
|
# `help <topic>` alongside its simple-mode sibling — so `help insert` shows
|
||||||
|
# both the simple form and `sql_insert` (like `help create` already does).
|
||||||
|
select: |-
|
||||||
|
select <cols> | * from <T> [where …] [group by …] [order by …] [limit n]
|
||||||
|
— query rows (advanced SQL)
|
||||||
|
with: |-
|
||||||
|
with <name> as (<select>) [, …] <select> — query through a named
|
||||||
|
sub-query / CTE (advanced SQL)
|
||||||
|
sql_insert: |-
|
||||||
|
insert into <T> (col, …) values (val, …) — add a row (advanced SQL)
|
||||||
|
sql_update: |-
|
||||||
|
update <T> set <col> = <val>, … where <expr> — change matching rows (advanced SQL)
|
||||||
|
sql_delete: |-
|
||||||
|
delete from <T> where <expr> — remove matching rows (advanced SQL)
|
||||||
|
explain_sql: |-
|
||||||
|
explain <select|with|insert|update|delete …> — show the plan for a SQL statement,
|
||||||
|
without running it (advanced SQL)
|
||||||
# Type reference, appended after the command list.
|
# Type reference, appended after the command list.
|
||||||
types_reference: |
|
types_reference: |
|
||||||
Types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid
|
Types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid
|
||||||
|
|||||||
@@ -377,6 +377,33 @@ fn undo_after_drop_table_restores_it() {
|
|||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flow 5 — issue #39 regression: commands submitted **back-to-back**, with
|
||||||
|
/// no wait between them, must still execute correctly. This is the inverse of
|
||||||
|
/// flow 3 (which paces each command on purpose). Pre-fix, a Form-B insert sent
|
||||||
|
/// immediately after `add column` was validated against the *stale* schema
|
||||||
|
/// cache — the worker hadn't yet refreshed it — and wrongly rejected as
|
||||||
|
/// "trying to write SQL?", so the row never landed. The schema-refresh gate
|
||||||
|
/// (`App::awaiting_schema_refresh`) now holds each submission until the prior
|
||||||
|
/// command's refresh arrives, making the outcome independent of input speed.
|
||||||
|
#[test]
|
||||||
|
fn back_to_back_insert_after_ddl_still_succeeds() {
|
||||||
|
let mut app = PtyApp::launch(&[]);
|
||||||
|
app.wait_for_no_tables(); // fresh project
|
||||||
|
|
||||||
|
// Fire all three with no readiness wait in between — the faster-than-human
|
||||||
|
// input that triggered issue #39 (paste / script / unpaced driver).
|
||||||
|
app.submit("create table Customers with pk id(serial)");
|
||||||
|
app.submit("add column to Customers: Name (text)");
|
||||||
|
app.submit("insert into Customers values ('Alice')");
|
||||||
|
|
||||||
|
// The insert's OWN success echo (value + ✓) — proof the row reached the
|
||||||
|
// database, not the "trying to write SQL?" rejection. If the gate
|
||||||
|
// regressed, the insert would misparse against the stale schema and this
|
||||||
|
// would time out.
|
||||||
|
app.wait_for("('Alice') ✓");
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== NFR perf (measured, generous) ===================
|
// ===================== NFR perf (measured, generous) ===================
|
||||||
//
|
//
|
||||||
// These run against the DEBUG binary, so the bounds are loose
|
// These run against the DEBUG binary, so the bounds are loose
|
||||||
|
|||||||
@@ -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]
|
#[test]
|
||||||
fn help_types_renders_the_type_reference() {
|
fn help_types_renders_the_type_reference() {
|
||||||
let out = output_for("help types");
|
let out = output_for("help types");
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ fn colon_escape_in_simple_mode_is_one_shot() {
|
|||||||
echoed.text,
|
echoed.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Issue #39: dispatching `:select 1` arms the schema-refresh gate (a
|
||||||
|
// cache refresh is conceptually in flight). In the real runtime that
|
||||||
|
// refresh lands before the user types the next line; model it here so
|
||||||
|
// the follow-up submission is processed rather than held.
|
||||||
|
app.update(AppEvent::SchemaCacheRefreshed(app.schema_cache.clone()));
|
||||||
|
|
||||||
// Subsequent submission (unrecognised in simple mode) parse-errors,
|
// Subsequent submission (unrecognised in simple mode) parse-errors,
|
||||||
// not echoes — confirming the mode reverted.
|
// not echoes — confirming the mode reverted.
|
||||||
type_str(&mut app, "list things");
|
type_str(&mut app, "list things");
|
||||||
|
|||||||
Reference in New Issue
Block a user