refactor(db): unwind vestigial worker source plumbing (ADR-0052 follow-up)

ADR-0052 moved success journaling out of the worker to the dispatch
layer, leaving the `source` that handlers threaded purely for the
worker's old history.log write dead. Remove it:

- drop `_source` from finalize_persistence and do_rebuild_from_text
- inline + delete the three read-only *_request wrappers
- drop the now-unused `source` param from the ~30 forwarding worker
  handlers (leaf + composite), compiler-guided
- remove the `source` field from the DescribeTable/QueryData/RunSelect
  requests and their DatabaseHandle methods (call sites updated)

The only worker `source` left is the snapshot/undo label
(snapshot_then / stage_pre_mutation / begin_batch). Purely mechanical,
no behaviour change. 2471 pass / 0 fail / 1 ignored, clippy clean.
This commit is contained in:
claude@clouddev1
2026-06-14 13:47:49 +00:00
parent ae73a4be85
commit e8fa859ab9
22 changed files with 231 additions and 384 deletions
@@ -189,9 +189,18 @@ over keeping journaling coupled in the worker (which would have needed the
no-op-skip / read-only sites no longer journal; success is journalled at
the dispatch layer (`spawn_dsl_dispatch` / `run_replay` / app-command
sites). The ring stays `Vec<String>`; `seed_history` / `ProjectSwitched`
are untouched. The vestigial worker `source` plumbing (the `_source`
param on `finalize_persistence` / `do_rebuild_from_text` and the thin
read-only `*_request` wrappers) is left in place — a clean follow-up.
are untouched. The vestigial worker `source` plumbing has since been
**fully unwound** (2026-06-14 follow-up): `_source` removed from
`finalize_persistence` / `do_rebuild_from_text`; the three read-only
`*_request` wrappers inlined and deleted; and — because the cascade ran
deeper than first estimated — the now-dead `source` param dropped from
the ~30 worker handlers (leaf + composite) that only forwarded it, plus
the `source` field removed from the `DescribeTable` / `QueryData` /
`RunSelect` requests and the matching `DatabaseHandle` method parameters
(the ~164 call-site churn was mostly tests). The only `source` left in
the worker is the snapshot/undo label (`snapshot_then` /
`stage_pre_mutation` / `begin_batch`), passed at the match-arm level.
Purely mechanical, compiler-guided, no behaviour change.
- **App commands recall bare.** Because they are dispatched outside the
`ExecuteDsl`/spawn path, app commands journal **simple** (`advanced =
false`) at their own sites, and `submit` excludes them from the ring's
+1 -1
View File
@@ -57,4 +57,4 @@ This directory contains the project's ADRs, recorded per
- [ADR-0049 — Input-field readline keymap: Esc-clear + Ctrl-A/E/W/K/U (I1b)](0049-input-field-readline-keymap.md) — **Accepted + implemented 2026-06-12 (issue #29)**, closes Gitea **#29** and the deferred **I1b** readline requirement. **Amends ADR-0046**, which listed "readline shortcuts (I1b)" as out-of-scope — that item is now in scope and decided here; orthogonal to ADR-0003's input-*mode* model and extends the I1a single-line cursor editing already shipped. Binds, in the input field (non-modal, non-nav, both modes): **`Esc`** clears a partly-typed command (empty buffer, cursor→0, scroll→0); **`Ctrl-A`/`Ctrl-E`** alias Home/End (line start/end); **`Ctrl-W`** deletes the previous word (readline-style — eats trailing whitespace then the preceding non-whitespace run, UTF-8-safe on char boundaries, only back to the cursor); **`Ctrl-K`** kills to end of line; **`Ctrl-U`** kills to start. **Esc precedence:** a live Tab-completion memo still wins (Esc undoes the completion first, ADR-0022; Esc clears only when no memo) — Esc-once backs out the completion, Esc-again clears. Forks all user-chosen: **single-Esc-clears** (not double-Esc — discoverable over accident-proof; an unsubmitted draft can be lost, a submitted line is always in history); the **full I1b set** (not just the issue's literal Ctrl-A/E + Esc); a **new ADR** (not an ADR-0046 amendment / no-ADR). Cursor-only keys (Ctrl-A/E) leave history navigation intact like Home/End; buffer-mutating keys (Esc-clear, Ctrl-W/K/U) end it like Backspace. Helpers `clear_input`/`delete_prev_word`/`kill_to_end`/`kill_to_start` in `src/app.rs`; **22 new Tier-1 tests, 2458 pass / 0 fail / 0 skip (1 ignored), clippy clean**. OOS: on-screen keybinding hints (issue #27 owns surfacing per-focus keybindings in the bottom status line — this ADR makes the keys *work*, #27 makes them *discoverable*); demo-mode badges for the new chords (ADR-0047 follow-up — Esc already badges `[ESC]`, the glyph-less Ctrl-chords are flagged but not added); multi-line input (I1); word-wise cursor motion (Alt-B/F) / transpose / yank
- [ADR-0050 — Incidental-DDL confirmations omit relationship info (structure-only)](0050-incidental-ddl-confirmations-omit-relationships.md) — **Accepted + implemented 2026-06-12 (issue #28)**, closes Gitea **#28**. **Supersedes** the incidental-DDL clause of **ADR-0044 §1** and the relationship-block half of **ADR-0016 §5**. Incidental-DDL confirmation echoes (`create table`, `add`/`drop`/`rename`/`change column`, `add`/`drop index`) now render **structure only** — header + column box + `Indexes:` + constraints — with **no `References:` / `Referenced by:` block** (neither prose nor diagram), even when the table carries relationships the user did not touch. Rationale (owner): a confirmation echo reports the change just made, not untouched relationships; ADR-0044's terse prose was the lesser of "prose vs diagram", but the right answer for these surfaces is **neither**. **Relationship-subject surfaces are unchanged**`show table`, `add`/`drop relationship`, `show relationship` still render ADR-0044 diagrams; relationships appear only when the user asks for (`show table`) or acts on (`add`/`drop relationship`) one, and are one `show table <T>` away — **no information lost**. Forks both user-chosen: **scope = all incidental DDL** (not just `add column` — the rationale is uniform, the mental model clean, and it's the simpler edit) and **delete the prose renderer** (not retain it dormant — no dead code). **Mechanism:** the `handle_dsl_success` `matches!` routing is unchanged (relationship-subject → diagrams; else → `render_structure`); the change is one line inside `render_structure` (`output_render.rs` — drop the relationship-block call) since all its callers are incidental DDL, plus deletion of the orphaned `relationship_prose_lines` + `cols_disp` helpers. The prose format survives in ADR-0016 §5 + git history for a future OOS-7 always-prose setting. **Tests:** the prose-presence unit test + its snapshot removed; a new unit test asserts `render_structure` on a description carrying **both** inbound and outbound relationships emits the box but no prose; the misnamed `add_relationship_flow_shows_inbound_section_on_parent` integration test (which sent an `AddColumn`) inverted + renamed to assert the add-column echo omits the prose; the diagram tests (`show table`, `add relationship`) unaffected. **2458 pass / 0 fail / 0 skip (1 ignored), clippy clean**. `requirements.md` unaffected (ADR-tracked refinement of a decided area, like ADR-0044 itself)
- [ADR-0051 — Bottom keybinding strip: context- and state-aware](0051-context-state-aware-keybinding-strip.md) — **Accepted + implemented 2026-06-13 (issue #27)**, closes Gitea **#27**. Repurposes the bottom status line into a **keystrokes-only, state-selected** strip (builds on ADR-0046 nav focus, ADR-0003 modes, ADR-0049 the #29 readline keys it now advertises, ADR-0022 the completion memo). A pure `status_bar_bindings(app) -> Vec<(key,label)>` chooses the strip by **priority, first match wins**: (1) **sidebar focus**`Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input`; (2) **completion memo live** (`last_completion`) → `Tab/Shift-Tab cycle · Esc cancel · Enter run`; (3) **history navigation** (new `App::is_browsing_history()` exposing the private `history_cursor`) → `↑↓ browse · Esc clear · Enter run`; (4) **editing** (input non-empty) → `Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run` (surfaces the #29 keys, closing ADR-0049's deferred advertisement); (5) **default** (empty) → `Ctrl-O sidebar · Tab complete · ↑ history · Enter run`. Priority is correct because Up clears the completion memo and Tab cancels history nav, so states 2/3 never co-occur, and the five are exhaustive for Input focus. **Typed-command words leave the strip** (`mode advanced`/`mode simple` switch, `:` one-shot) and **mode discovery moves to the empty-input hint** (`resolve_hint_lines`), **simple mode only**: `\`mode advanced\` for SQL` (the verb "type" omitted — the prompt implies it; advanced mode shows **no** pointer per a post-trial user decision — a switcher knows how they got there and `help` covers the way back). The one-shot's old `Backspace cancel one-shot` label is subsumed by the editing state (behaviour intact). Forks all user-chosen: **editing state shows the #29 keys** (vs unadvertised); **`Ctrl-C quit` omitted** from the strip (vs always shown); **no width-drop machinery** — the longest strip (~65 cols) fits all supported widths, so a **width-budget unit test** keeps it lean by construction instead (the user's own observation). Catalog: 12 new `shortcut.*` labels + the `panel.hint_mode_advanced` string added to `en-US.yaml`+`keys.rs` (validator-checked 1:1), 5 now-dead strip strings removed. **Modal-aware strip is OOS** (pre-existing: a modal owns the keyboard and carries its own hints; the strip under it is unchanged-in-kind, not worsened). Tests: 9 Tier-1 unit (per-state key sets — completion/history driven through real key events; width budget; mode-pointer presence/absence), 1 Tier-3 rewritten (`status_bar_is_keystroke_only_and_state_aware`), 15 full-panel snapshots re-accepted (reviewed — strip/hint only). **2467 pass / 0 fail / 0 skip (1 ignored), clippy clean.** OOS: modal-aware strip; a full-key cheatsheet overlay; Ctrl-K/U advertisement (editing strip shows the highest-value subset within the width budget)
- [ADR-0052 — Mode-tagged history for cross-mode recall](0052-mode-tagged-history-cross-mode-recall.md) — **Accepted + implemented 2026-06-13 (issue #30)**, closes Gitea **#30** — the feature (advanced history reusable in simple mode) **and** the bug in its comment (the `:` one-shot prefix lost across sessions). **Amends ADR-0034** (status field gains a `:adv` tag; **journaling moves from the worker to the dispatch layer**), **ADR-0015 §5/§6** (history.log leaves the worker transaction — `commit-db-last` now scopes yaml/csv/db only), and **ADR-0040** (a success-path journal-write failure is best-effort, not fatal); references ADR-0003. **Root cause:** history carried no mode, and the in-memory ring stored the raw `:select 1` while the worker journalled the *stripped* `select 1`, so the `:` was lost on disk. **Fix:** record the submission mode per entry as a **`:adv` suffix on the status token** (`ok`/`ok:adv`/`err`/`err:adv`) — `source` stays last + canonical so replay is unaffected; the in-memory ring (still `Vec<String>`) stores advanced entries in their `: `-prefixed simple-mode runnable form (a leading `:` unambiguously marks advanced since simple DSL never starts with `:`); recall **strips the `:` in advanced mode** (runs as bare SQL) and keeps it in simple mode (runs via the one-shot escape); hydration reconstructs the `: `-prefix from the tag, so cross-session = in-session. **The architectural turn (user's call):** the first draft kept journaling in the worker + threaded the mode down (~30-site plumbing); on review the user asked why the journal is written deep in the worker when the *failure* path already journals at the top of the chain — it shouldn't (history.log is a journal, not state). So **success journaling moved up** to `spawn_dsl_dispatch` / `run_replay` / the app-command sites (next to the failure path), the worker's `finalize_persistence` now writes only yaml/csv, and the journal write became **best-effort** (the command is already committed — consistent with the failure path; a rare disk-full leaves a committed command unjournalled, state intact). **App commands** journal simple (dispatched outside the spawn) and `submit` excludes them from the ring's advanced flag, so `undo`/`mode advanced` recall bare. Forks user-chosen: status-tag format (vs 4th field / `:`-in-source); unified scope; **dispatch-layer best-effort journaling** (vs worker-coupled-fatal). Two `/runda` passes (the second drove the relocation + app-command exclusion). Tests: the 15 worker-level journaling tests retired (worker no longer journals — yaml/csv/operation checks kept), re-covered at the new layer (history.rs status-tag + `:`-reconstruct; app.rs recall matrix; the #30 cross-session regression in `iteration6`; replay tests cover `run_replay` journaling). **2471 pass / 0 fail / 0 skip (1 ignored), clippy clean.** OOS: unwinding the now-vestigial worker `source` plumbing (`_source` params + thin `*_request` wrappers — a clean follow-up); replay re-journaling mode-fidelity (a replayed advanced line re-journals simple — not a regression)
- [ADR-0052 — Mode-tagged history for cross-mode recall](0052-mode-tagged-history-cross-mode-recall.md) — **Accepted + implemented 2026-06-13 (issue #30)**, closes Gitea **#30** — the feature (advanced history reusable in simple mode) **and** the bug in its comment (the `:` one-shot prefix lost across sessions). **Amends ADR-0034** (status field gains a `:adv` tag; **journaling moves from the worker to the dispatch layer**), **ADR-0015 §5/§6** (history.log leaves the worker transaction — `commit-db-last` now scopes yaml/csv/db only), and **ADR-0040** (a success-path journal-write failure is best-effort, not fatal); references ADR-0003. **Root cause:** history carried no mode, and the in-memory ring stored the raw `:select 1` while the worker journalled the *stripped* `select 1`, so the `:` was lost on disk. **Fix:** record the submission mode per entry as a **`:adv` suffix on the status token** (`ok`/`ok:adv`/`err`/`err:adv`) — `source` stays last + canonical so replay is unaffected; the in-memory ring (still `Vec<String>`) stores advanced entries in their `: `-prefixed simple-mode runnable form (a leading `:` unambiguously marks advanced since simple DSL never starts with `:`); recall **strips the `:` in advanced mode** (runs as bare SQL) and keeps it in simple mode (runs via the one-shot escape); hydration reconstructs the `: `-prefix from the tag, so cross-session = in-session. **The architectural turn (user's call):** the first draft kept journaling in the worker + threaded the mode down (~30-site plumbing); on review the user asked why the journal is written deep in the worker when the *failure* path already journals at the top of the chain — it shouldn't (history.log is a journal, not state). So **success journaling moved up** to `spawn_dsl_dispatch` / `run_replay` / the app-command sites (next to the failure path), the worker's `finalize_persistence` now writes only yaml/csv, and the journal write became **best-effort** (the command is already committed — consistent with the failure path; a rare disk-full leaves a committed command unjournalled, state intact). **App commands** journal simple (dispatched outside the spawn) and `submit` excludes them from the ring's advanced flag, so `undo`/`mode advanced` recall bare. Forks user-chosen: status-tag format (vs 4th field / `:`-in-source); unified scope; **dispatch-layer best-effort journaling** (vs worker-coupled-fatal). Two `/runda` passes (the second drove the relocation + app-command exclusion). Tests: the 15 worker-level journaling tests retired (worker no longer journals — yaml/csv/operation checks kept), re-covered at the new layer (history.rs status-tag + `:`-reconstruct; app.rs recall matrix; the #30 cross-session regression in `iteration6`; replay tests cover `run_replay` journaling). **2471 pass / 0 fail / 0 skip (1 ignored), clippy clean.** replay re-journaling mode-fidelity (a replayed advanced line re-journals simple — not a regression). **Follow-up done 2026-06-14:** the vestigial worker `source` plumbing was fully unwound (compiler-guided, no behaviour change) — `_source` removed from `finalize_persistence`/`do_rebuild_from_text`, the three `*_request` wrappers inlined+deleted, the dead `source` param dropped from the ~30 forwarding worker handlers, and the `source` field removed from the `DescribeTable`/`QueryData`/`RunSelect` requests + their `DatabaseHandle` methods (~164 mostly-test call sites); the only worker `source` left is the snapshot/undo label (see ADR-0052 *Consequences*)
+116 -272
View File
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -1190,7 +1190,7 @@ async fn build_schema_cache(database: &Database) -> crate::completion::SchemaCac
// miss leaves that table's columns unpopulated and the
// walker falls back to the schemaless value-literal list.
for name in cache.tables.clone() {
if let Ok(desc) = database.describe_table(name.clone(), None).await {
if let Ok(desc) = database.describe_table(name.clone()).await {
// Per-table indexes for the items panel (S2, ADR-0025).
// Carry uniqueness so the panel can mark a UNIQUE index
// (ADR-0035 §4d). Captured before `desc.columns` is
@@ -1650,7 +1650,7 @@ async fn build_show_data_echo(
limit: Some(_),
..
} => database
.describe_table(name.clone(), None)
.describe_table(name.clone())
.await
.map(|desc| {
desc.columns
@@ -1732,7 +1732,7 @@ async fn collect_echo_lookups(
Command::DropIndex {
selector: IndexSelector::Columns { table, columns },
} => {
if let Ok(desc) = database.describe_table(table.clone(), None).await
if let Ok(desc) = database.describe_table(table.clone()).await
&& let Some(idx) = desc.indexes.iter().find(|i| i.columns == *columns)
{
out.drop_index_name = Some(idx.name.clone());
@@ -1747,7 +1747,7 @@ async fn collect_echo_lookups(
child_column,
},
} => {
if let Ok(desc) = database.describe_table(child_table.clone(), None).await
if let Ok(desc) = database.describe_table(child_table.clone()).await
&& let Some(rel) = desc.outbound_relationships.iter().find(|r| {
// The Endpoints drop selector is single-column
// (ADR-0043 keeps DROP by-endpoints single-column;
@@ -1771,7 +1771,7 @@ async fn collect_echo_lookups(
// resolver API would be the next step if schemas grow.
if let Ok(tables) = database.list_tables().await {
for table in tables {
if let Ok(desc) = database.describe_table(table.clone(), None).await
if let Ok(desc) = database.describe_table(table.clone()).await
&& desc.outbound_relationships.iter().any(|r| r.name == *name)
{
out.drop_relationship = Some((name.clone(), table.clone()));
@@ -1795,8 +1795,8 @@ async fn collect_echo_lookups(
// *before* execution to know which `ADD COLUMN` lines to
// emit. The parent columns here are the explicit DSL list,
// paired positionally with the child list.
let parent_desc = database.describe_table(parent_table.clone(), None).await;
let child_desc = database.describe_table(child_table.clone(), None).await;
let parent_desc = database.describe_table(parent_table.clone()).await;
let child_desc = database.describe_table(child_table.clone()).await;
if let (Ok(parent_desc), Ok(child_desc)) = (parent_desc, child_desc) {
let mut new_columns: Vec<(String, crate::dsl::types::Type)> = Vec::new();
for (child_col, parent_col) in child_columns.iter().zip(parent_columns) {
@@ -2064,7 +2064,7 @@ async fn enrich_check_violation(
.await
.map(|v| v.to_string());
// The rule itself — the column's compiled CHECK expression.
if let Ok(desc) = database.describe_table(table.to_string(), None).await
if let Ok(desc) = database.describe_table(table.to_string()).await
&& let Some(col) = desc.columns.iter().find(|c| c.name == column)
{
facts.check_rule.clone_from(&col.check);
@@ -2272,7 +2272,7 @@ async fn user_value_for_column_with_schema(
} = command
{
let desc = database
.describe_table(table.to_string(), None)
.describe_table(table.to_string())
.await
.ok()?;
// Build the natural-order column list the same way
@@ -2311,7 +2311,7 @@ async fn user_value_for_column_with_schema(
&& literal_rows.len() == 1
{
let desc = database
.describe_table(table.to_string(), None)
.describe_table(table.to_string())
.await
.ok()?;
let idx = desc.columns.iter().position(|c| c.name == column)?;
@@ -2930,7 +2930,7 @@ async fn execute_command_typed(
.await
.map(|d| CommandOutcome::Schema(Some(d))),
Command::ShowTable { name } => database
.describe_table(name, src)
.describe_table(name)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
// ADR-0044: a named relationship renders as a diagram (App-side),
@@ -2983,14 +2983,14 @@ async fn execute_command_typed(
filter,
limit,
} => database
.query_data(name, filter, limit, src)
.query_data(name, filter, limit)
.await
.map(CommandOutcome::Query),
// A SQL `SELECT` (advanced mode; ADR-0030 §6, ADR-0031).
// The grammar walker has already validated `sql` is in
// the supported subset; the worker runs it as text.
Command::Select { sql } => database
.run_select(sql, src)
.run_select(sql)
.await
.map(CommandOutcome::Query),
// A SQL `INSERT` (advanced mode; ADR-0033 §1). Grammar-as-
+5 -5
View File
@@ -93,7 +93,7 @@ fn rename_column_with_case_variant_table_keeps_metadata_in_step() {
.expect("rename column via a case-variant table name");
let desc = r
.block_on(db.describe_table("Items".to_string(), None))
.block_on(db.describe_table("Items".to_string()))
.expect("describe Items");
let amount = desc
.columns
@@ -126,7 +126,7 @@ fn insert_with_case_variant_table_persists_and_survives_rebuild() {
let db = fresh_rebuild(db, &project, &r);
let rows = r
.block_on(db.query_data("Items".to_string(), None, None, None))
.block_on(db.query_data("Items".to_string(), None, None))
.expect("query")
.rows;
assert_eq!(rows.len(), 1, "the wrong-case insert survived the rebuild (no data loss)");
@@ -146,7 +146,7 @@ fn add_column_with_case_variant_table_survives_rebuild() {
);
let db = fresh_rebuild(db, &project, &r);
let desc = r.block_on(db.describe_table("Items".to_string(), None)).expect("describe");
let desc = r.block_on(db.describe_table("Items".to_string())).expect("describe");
let qty = desc.columns.iter().find(|c| c.name == "qty").expect("qty added");
assert_eq!(qty.user_type, Some(Type::Int), "qty's user-type survived the rebuild");
// The CHECK is intact too (a negative qty is refused under the real table).
@@ -224,12 +224,12 @@ fn add_relationship_with_case_variant_tables_survives_rebuild() {
add 1:n relationship from parent.id to child.parent_id\n",
);
// The parent's inbound relationship is visible under the stored case.
let p = r.block_on(db.describe_table("Parent".to_string(), None)).expect("describe Parent");
let p = r.block_on(db.describe_table("Parent".to_string())).expect("describe Parent");
assert_eq!(p.inbound_relationships.len(), 1, "relationship recorded under the stored case");
assert_eq!(p.inbound_relationships[0].other_table, "Child");
let db = fresh_rebuild(db, &project, &r);
let p = r.block_on(db.describe_table("Parent".to_string(), None)).expect("describe Parent");
let p = r.block_on(db.describe_table("Parent".to_string())).expect("describe Parent");
assert_eq!(p.inbound_relationships.len(), 1, "relationship survived the rebuild");
assert_eq!(p.inbound_relationships[0].other_table, "Child");
}
+5 -5
View File
@@ -276,7 +276,7 @@ fn compound_fk_declares_enforces_and_round_trips() {
);
// describe shows the compound endpoints symmetrically.
let city = db.describe_table("City".to_string(), None).await.unwrap();
let city = db.describe_table("City".to_string()).await.unwrap();
let outbound = &city.outbound_relationships[0];
assert_eq!(
outbound.local_columns,
@@ -329,7 +329,7 @@ fn compound_fk_create_fk_makes_both_child_columns() {
)
.await
.expect("add compound relationship with --create-fk");
let city = db.describe_table("City".to_string(), None).await.unwrap();
let city = db.describe_table("City".to_string()).await.unwrap();
for col in ["c_country", "c_code"] {
assert!(
city.columns.iter().any(|c| c.name == col),
@@ -527,7 +527,7 @@ fn compound_fk_survives_rebuild_from_text() {
.await;
assert!(bad.is_err(), "compound FK still enforced after rebuild from text");
// Endpoints survived the round-trip intact.
let city = db.describe_table("City".to_string(), None).await.unwrap();
let city = db.describe_table("City".to_string()).await.unwrap();
assert_eq!(
city.outbound_relationships[0].other_columns,
vec!["country".to_string(), "code".to_string()],
@@ -563,7 +563,7 @@ fn compound_fk_undo_removes_the_relationship() {
.await
.expect("add compound relationship");
assert_eq!(
db.describe_table("City".to_string(), None)
db.describe_table("City".to_string())
.await
.unwrap()
.outbound_relationships
@@ -573,7 +573,7 @@ fn compound_fk_undo_removes_the_relationship() {
// One undo step removes the whole relationship (ADR-0013/0006).
db.undo().await.unwrap().expect("undo applied");
assert!(
db.describe_table("City".to_string(), None)
db.describe_table("City".to_string())
.await
.unwrap()
.outbound_relationships
+4 -4
View File
@@ -76,7 +76,7 @@ fn rebuild_restores_schema_only_project() {
// Phase 4: confirm Customers exists with the right shape.
let desc = rt()
.block_on(async { db.describe_table("Customers".to_string(), None).await })
.block_on(async { db.describe_table("Customers".to_string()).await })
.expect("describe_table");
assert_eq!(desc.name, "Customers");
let cols: Vec<&str> = desc.columns.iter().map(|c| c.name.as_str()).collect();
@@ -143,7 +143,7 @@ fn rebuild_restores_rows_from_csv() {
});
let rows = rt()
.block_on(async { db.query_data("Customers".to_string(), None, None, None).await })
.block_on(async { db.query_data("Customers".to_string(), None, None).await })
.expect("query_data");
assert_eq!(rows.rows.len(), 2);
let names: Vec<Option<String>> = rows.rows.iter().map(|r| r[1].clone()).collect();
@@ -371,7 +371,7 @@ fn rebuild_preserves_created_at_from_yaml() {
// Trigger any successful command so project.yaml is
// rewritten from the now-rebuilt db state.
rt().block_on(async {
db.describe_table("T".to_string(), Some("show table T".to_string()))
db.describe_table("T".to_string())
.await
.unwrap();
// describe is read-only; force a rewrite by adding a column.
@@ -451,7 +451,7 @@ fn rebuild_restores_indexes() {
});
let desc = rt()
.block_on(async { db.describe_table("Customers".to_string(), None).await })
.block_on(async { db.describe_table("Customers".to_string()).await })
.expect("describe_table");
assert_eq!(desc.indexes.len(), 1, "index should survive rebuild");
assert_eq!(desc.indexes[0].name, "idx_email");
+1 -1
View File
@@ -173,7 +173,7 @@ fn rebuild_against_populated_db_wipes_and_reloads() {
.expect("rebuild");
});
let rows = rt()
.block_on(async { db.query_data("Customers".to_string(), None, None, None).await })
.block_on(async { db.query_data("Customers".to_string(), None, None).await })
.unwrap();
assert_eq!(rows.rows.len(), 1);
assert_eq!(rows.rows[0][1].as_deref(), Some("Edna"));
+1 -1
View File
@@ -362,7 +362,7 @@ fn end_to_end_export_then_import_real_project() {
// Round-trip: the inserted row is back.
let data_view = rt()
.block_on(async { imported_db.query_data("Customers".to_string(), None, None, None).await })
.block_on(async { imported_db.query_data("Customers".to_string(), None, None).await })
.expect("query data");
assert_eq!(data_view.rows.len(), 1);
// Serial id auto-filled to 1; Name was the inserted value.
+6 -6
View File
@@ -107,7 +107,7 @@ fn generates_junction_with_compound_pk_and_two_enforced_fks() {
assert!(tables.contains(&"Students_Courses".to_string()), "tables: {tables:?}");
// Two FK columns, both part of the compound PK.
let desc = db.describe_table("Students_Courses".to_string(), None).await.unwrap();
let desc = db.describe_table("Students_Courses".to_string()).await.unwrap();
let cols: Vec<(&str, bool)> =
desc.columns.iter().map(|c| (c.name.as_str(), c.primary_key)).collect();
assert_eq!(
@@ -191,7 +191,7 @@ fn compound_parent_pk_contributes_one_fk_column_each() {
.await
.expect("create m:n");
let desc = db.describe_table("Students_Sections".to_string(), None).await.unwrap();
let desc = db.describe_table("Students_Sections".to_string()).await.unwrap();
let names: Vec<&str> = desc.columns.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, vec!["Students_id", "Sections_course_id", "Sections_term"]);
// All three form the compound PK.
@@ -221,7 +221,7 @@ fn deleting_a_parent_cascades_to_the_junction() {
// Deleting the student cascades to the junction (ON DELETE CASCADE).
db.delete("Students".to_string(), RowFilter::AllRows, None).await.unwrap();
let rows = db.query_data("Students_Courses".to_string(), None, None, None).await.unwrap();
let rows = db.query_data("Students_Courses".to_string(), None, None).await.unwrap();
assert!(rows.rows.is_empty(), "junction rows should cascade-delete, got {:?}", rows.rows);
});
}
@@ -249,7 +249,7 @@ fn create_m2n_is_one_undo_step() {
let tables = db.list_tables().await.unwrap();
assert!(!tables.contains(&"Students_Courses".to_string()), "undo should remove the junction: {tables:?}");
// The parents' relationships are gone too (the junction held them).
let students = db.describe_table("Students".to_string(), None).await.unwrap();
let students = db.describe_table("Students".to_string()).await.unwrap();
assert!(students.inbound_relationships.is_empty(), "no leftover relationship after undo");
});
}
@@ -321,7 +321,7 @@ fn the_junction_can_be_renamed() {
assert!(tables.contains(&"Enrollments".to_string()), "tables: {tables:?}");
assert!(!tables.contains(&"Students_Courses".to_string()));
// Both relationships survive the rename (rebuild-preserving).
let desc = db.describe_table("Enrollments".to_string(), None).await.unwrap();
let desc = db.describe_table("Enrollments".to_string()).await.unwrap();
assert_eq!(desc.outbound_relationships.len(), 2, "FKs preserved across rename");
});
}
@@ -362,7 +362,7 @@ fn junction_survives_save_and_rebuild() {
db.rebuild_from_text(project.path().to_path_buf(), None).await.expect("rebuild");
let tables = db.list_tables().await.unwrap();
assert!(tables.contains(&"Students_Courses".to_string()), "junction survived: {tables:?}");
let desc = db.describe_table("Students_Courses".to_string(), None).await.unwrap();
let desc = db.describe_table("Students_Courses".to_string()).await.unwrap();
assert_eq!(desc.outbound_relationships.len(), 2, "both FKs reconstructed");
assert!(desc.columns.iter().all(|c| c.primary_key), "compound PK reconstructed");
});
+9 -9
View File
@@ -108,13 +108,13 @@ fn replay_runs_advanced_sql_create_table_as_a_write() {
// The SQL DDL line actually created the structural table…
let desc = rt()
.block_on(async { db.describe_table("Widget".to_string(), None).await })
.block_on(async { db.describe_table("Widget".to_string()).await })
.expect("describe");
let names: Vec<String> = desc.columns.iter().map(|c| c.name.clone()).collect();
assert_eq!(names, vec!["id".to_string(), "name".to_string()]);
// …and the following insert (serial id auto-filled) ran against it.
let rows = rt()
.block_on(async { db.query_data("Widget".to_string(), None, None, None).await })
.block_on(async { db.query_data("Widget".to_string(), None, None).await })
.expect("query")
.rows;
assert_eq!(rows.len(), 1);
@@ -139,7 +139,7 @@ fn replay_three_lines_dispatches_three_commands() {
// The dispatched commands actually mutated state.
let data_result = rt()
.block_on(async { db.query_data("T".to_string(), None, None, None).await })
.block_on(async { db.query_data("T".to_string(), None, None).await })
.expect("query_data");
assert_eq!(data_result.rows.len(), 1, "row inserted");
assert_eq!(data_result.rows[0][1].as_deref(), Some("Alice"));
@@ -174,7 +174,7 @@ fn replay_of_actual_history_log_runs_ok_commands_and_skips_err() {
assert_completed(&events, 3);
let data_result = rt()
.block_on(async { db.query_data("T".to_string(), None, None, None).await })
.block_on(async { db.query_data("T".to_string(), None, None).await })
.expect("query_data");
assert_eq!(data_result.rows.len(), 1, "only the ok INSERT applied");
assert_eq!(data_result.rows[0][1].as_deref(), Some("alpha"));
@@ -227,7 +227,7 @@ fn replay_skips_app_lifecycle_commands_silently() {
other => panic!("expected ReplayCompleted, got {other:?}"),
}
let data_result = rt()
.block_on(async { db.query_data("T".to_string(), None, None, None).await })
.block_on(async { db.query_data("T".to_string(), None, None).await })
.expect("query_data");
assert!(
data_result.columns.iter().any(|c| c == "v"),
@@ -401,14 +401,14 @@ fn replay_aborts_on_first_parse_failure_and_reports_line() {
// but earlier commands stayed applied (table T exists with
// the `name` column).
let desc = rt()
.block_on(async { db.describe_table("T".to_string(), None).await })
.block_on(async { db.describe_table("T".to_string()).await })
.expect("describe_table");
assert!(
desc.columns.iter().any(|c| c.name == "name"),
"earlier add column should have stayed applied"
);
let data_result = rt()
.block_on(async { db.query_data("T".to_string(), None, None, None).await })
.block_on(async { db.query_data("T".to_string(), None, None).await })
.expect("query_data");
assert!(
data_result.rows.is_empty(),
@@ -467,7 +467,7 @@ fn replay_rejects_wrong_type_value_in_a_hand_built_script() {
// The earlier two lines stayed applied; the failing insert
// did not run — state is intact.
let data_result = rt()
.block_on(async { db.query_data("T".to_string(), None, None, None).await })
.block_on(async { db.query_data("T".to_string(), None, None).await })
.expect("query_data");
assert!(
data_result.rows.is_empty(),
@@ -527,7 +527,7 @@ fn replay_skips_nested_replay_with_a_warning() {
other => panic!("expected ReplayCompleted (nested replay skipped), got {other:?}"),
}
// The nested file's table was NOT created (the replay was skipped).
let cols = rt().block_on(async { db.query_data("T".to_string(), None, None, None).await });
let cols = rt().block_on(async { db.query_data("T".to_string(), None, None).await });
assert!(cols.is_err(), "inner.commands' table T must not exist (nested replay skipped)");
}
+1 -1
View File
@@ -462,7 +462,7 @@ fn app_show_table_renders_relationships_as_compact_diagrams() {
rt.block_on(seed_schema(&db));
// Orders holds the FK to Customers — an outbound relationship.
let desc = rt
.block_on(db.describe_table("Orders".to_string(), None))
.block_on(db.describe_table("Orders".to_string()))
.expect("describe Orders");
let mut app = App::new();
+17 -17
View File
@@ -111,7 +111,7 @@ fn e2e_alter_drop_compound_primary_key_member_is_refused() {
/// The current user-facing type of column `name` in table `T`.
fn col_type(db: &Database, r: &tokio::runtime::Runtime, name: &str) -> Option<Type> {
r.block_on(db.describe_table("T".to_string(), None))
r.block_on(db.describe_table("T".to_string()))
.expect("describe")
.columns
.into_iter()
@@ -120,7 +120,7 @@ fn col_type(db: &Database, r: &tokio::runtime::Runtime, name: &str) -> Option<Ty
}
fn column_names(db: &Database, r: &tokio::runtime::Runtime) -> Vec<String> {
r.block_on(db.describe_table("T".to_string(), None))
r.block_on(db.describe_table("T".to_string()))
.expect("describe")
.columns
.into_iter()
@@ -163,7 +163,7 @@ fn e2e_alter_table_add_rename_drop_and_raw_default_check() {
// The DEFAULT backfilled the pre-existing row to qty = 0.
let rows = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query")
.rows;
assert_eq!(rows.len(), 1);
@@ -252,7 +252,7 @@ fn e2e_alter_column_type_clean_and_lossy_convert() {
}
let rows = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query")
.rows;
assert_eq!(rows.len(), 1);
@@ -292,7 +292,7 @@ fn e2e_alter_column_type_int_to_serial_is_allowed() {
}
assert_eq!(col_type(&db, &r, "n"), Some(Type::Serial), "int→serial converted the column");
let rows = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query")
.rows;
assert_eq!(rows[0][1].as_deref(), Some("100"), "the existing value is preserved");
@@ -635,7 +635,7 @@ fn e2e_drop_composite_unique_is_one_undo_step() {
.expect("write");
r.block_on(run_replay(&db, project.path(), "u.commands"));
let has_unique = || {
!r.block_on(db.describe_table("T".to_string(), None))
!r.block_on(db.describe_table("T".to_string()))
.expect("describe")
.unique_constraints
.is_empty()
@@ -878,7 +878,7 @@ fn e2e_describe_shows_table_level_constraints() {
"events: {events:?}"
);
let desc = r.block_on(db.describe_table("T".to_string(), None)).expect("describe");
let desc = r.block_on(db.describe_table("T".to_string())).expect("describe");
assert_eq!(
desc.unique_constraints,
vec![vec!["a".to_string(), "b".to_string()]],
@@ -976,7 +976,7 @@ fn e2e_rename_table_with_rows_csv_follows_and_survives_rebuild() {
assert!(!csv_path(&project, "Orders").exists(), "data/Orders.csv removed");
let rows = r
.block_on(db.query_data("Purchases".to_string(), None, None, None))
.block_on(db.query_data("Purchases".to_string(), None, None))
.expect("query")
.rows;
assert_eq!(rows.len(), 2);
@@ -991,7 +991,7 @@ fn e2e_rename_table_with_rows_csv_follows_and_survives_rebuild() {
"Purchases round-tripped through a fresh rebuild: {tables:?}"
);
let rows = r
.block_on(db.query_data("Purchases".to_string(), None, None, None))
.block_on(db.query_data("Purchases".to_string(), None, None))
.expect("query")
.rows;
assert_eq!(rows.len(), 2);
@@ -1077,7 +1077,7 @@ fn e2e_rename_fk_parent_updates_metadata_and_still_enforces() {
);
// The child's outbound relationship now points at the new parent name.
let c = r.block_on(db.describe_table("C".to_string(), None)).expect("describe C");
let c = r.block_on(db.describe_table("C".to_string())).expect("describe C");
assert_eq!(c.outbound_relationships.len(), 1);
assert_eq!(c.outbound_relationships[0].other_table, "Parent");
@@ -1129,7 +1129,7 @@ fn e2e_rename_fk_child_updates_metadata_and_still_enforces() {
);
// The parent's inbound relationship now names the renamed child.
let p = r.block_on(db.describe_table("P".to_string(), None)).expect("describe P");
let p = r.block_on(db.describe_table("P".to_string())).expect("describe P");
assert_eq!(p.inbound_relationships.len(), 1);
assert_eq!(p.inbound_relationships[0].other_table, "Child");
@@ -1168,7 +1168,7 @@ fn e2e_rename_self_referential_table_updates_both_ends() {
);
// Both ends of the self-reference now name `Tree`.
let t = r.block_on(db.describe_table("Tree".to_string(), None)).expect("describe Tree");
let t = r.block_on(db.describe_table("Tree".to_string())).expect("describe Tree");
assert_eq!(t.outbound_relationships[0].other_table, "Tree");
assert_eq!(t.inbound_relationships[0].other_table, "Tree");
@@ -1216,7 +1216,7 @@ fn e2e_rename_table_keeps_its_index_with_a_stale_name() {
"events: {events:?}"
);
let u = r.block_on(db.describe_table("Users".to_string(), None)).expect("describe Users");
let u = r.block_on(db.describe_table("Users".to_string())).expect("describe Users");
assert_eq!(u.indexes.len(), 1, "the index followed the rename");
assert_eq!(
u.indexes[0].name, "T_email_idx",
@@ -1226,7 +1226,7 @@ fn e2e_rename_table_keeps_its_index_with_a_stale_name() {
// Survives a fresh rebuild (recreated from IndexSchema on table Users).
let db = fresh_rebuild(db, &project, &r);
let u = r.block_on(db.describe_table("Users".to_string(), None)).expect("describe Users");
let u = r.block_on(db.describe_table("Users".to_string())).expect("describe Users");
assert_eq!(u.indexes.len(), 1);
assert_eq!(u.indexes[0].name, "T_email_idx");
}
@@ -1255,7 +1255,7 @@ fn e2e_rename_table_is_one_undo_step() {
"undo restored the old table name: {tables:?}"
);
assert_eq!(
r.block_on(db.query_data("Orders".to_string(), None, None, None)).expect("query").rows.len(),
r.block_on(db.query_data("Orders".to_string(), None, None)).expect("query").rows.len(),
1,
"the row is back under the old name"
);
@@ -1427,7 +1427,7 @@ fn e2e_alter_column_set_default_applies() {
))
.expect("insert omitting qty");
let rows = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query")
.rows;
assert_eq!(
@@ -1473,7 +1473,7 @@ fn e2e_alter_column_drop_default_removes_it() {
))
.expect("insert omitting qty");
let rows = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query")
.rows;
assert_eq!(
+1 -1
View File
@@ -55,7 +55,7 @@ fn insert_row(db: &Database, r: &tokio::runtime::Runtime, id: i64, email: &str)
}
fn index(db: &Database, r: &tokio::runtime::Runtime, name: &str) -> Option<(Vec<String>, bool)> {
r.block_on(db.describe_table("T".to_string(), None))
r.block_on(db.describe_table("T".to_string()))
.expect("describe")
.indexes
.into_iter()
+18 -18
View File
@@ -64,7 +64,7 @@ fn created_table_appears_with_playground_types() {
assert!(tables.contains(&"Widget".to_string()));
let desc = r
.block_on(db.describe_table("Widget".to_string(), None))
.block_on(db.describe_table("Widget".to_string()))
.expect("describe");
let types: Vec<(String, Option<Type>)> = desc
.columns
@@ -98,7 +98,7 @@ fn integer_primary_key_is_plain_int() {
))
.expect("create");
let desc = r
.block_on(db.describe_table("T".to_string(), None))
.block_on(db.describe_table("T".to_string()))
.expect("describe");
assert_eq!(desc.columns[0].user_type, Some(Type::Int));
}
@@ -137,7 +137,7 @@ fn serial_pk_autoincrements_in_multi_column_table() {
}
let data = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query");
let id_idx = data
.columns
@@ -220,7 +220,7 @@ fn table_without_primary_key_is_allowed() {
))
.expect("insert into PK-less table");
let data = r
.block_on(db.query_data("Notes".to_string(), None, None, None))
.block_on(db.query_data("Notes".to_string(), None, None))
.expect("query");
assert_eq!(data.rows.len(), 1);
}
@@ -299,7 +299,7 @@ fn default_is_applied_when_column_omitted() {
))
.expect("insert");
let data = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query");
let n_idx = data.columns.iter().position(|c| c == "n").expect("n column");
assert_eq!(data.rows[0][n_idx].as_deref(), Some("7"), "DEFAULT 7 applied");
@@ -381,7 +381,7 @@ fn check_default_and_composite_unique_survive_rebuild() {
// A valid row inserts; DEFAULT n=7 survived.
r.block_on(ins("1", "1", "5")).expect("valid row");
let data = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query");
let n_idx = data.columns.iter().position(|c| c == "n").expect("n column");
assert_eq!(data.rows[0][n_idx].as_deref(), Some("7"), "DEFAULT survived rebuild");
@@ -679,7 +679,7 @@ fn sql_create_table_is_one_undo_step() {
/// Sorted `id` column values of table `T`.
fn ids(db: &Database, r: &tokio::runtime::Runtime) -> Vec<Option<String>> {
let d = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query");
let idx = d.columns.iter().position(|c| c == "id").expect("id column");
let mut v: Vec<Option<String>> = d.rows.iter().map(|row| row[idx].clone()).collect();
@@ -801,7 +801,7 @@ fn dropping_a_column_a_table_check_references_fails_cleanly() {
// The table is intact: both columns survive (rollback) ...
let desc = r
.block_on(db.describe_table("T".to_string(), None))
.block_on(db.describe_table("T".to_string()))
.expect("describe still works");
assert_eq!(
desc.columns.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
@@ -925,14 +925,14 @@ fn foreign_key_creates_named_relationship_visible_in_describe() {
.expect("create child with FK");
// The child has an outbound relationship; the parent an inbound one.
let child = r.block_on(db.describe_table("child".to_string(), None)).expect("describe child");
let child = r.block_on(db.describe_table("child".to_string())).expect("describe child");
assert_eq!(child.outbound_relationships.len(), 1, "child references parent");
let rel = &child.outbound_relationships[0];
assert_eq!(rel.name, "parent_id_to_child_pid", "auto-named per ADR-0013");
assert_eq!(rel.other_table, "parent");
assert_eq!(rel.local_columns, vec!["pid".to_string()]);
let parent = r.block_on(db.describe_table("parent".to_string(), None)).expect("describe parent");
let parent = r.block_on(db.describe_table("parent".to_string())).expect("describe parent");
assert_eq!(parent.inbound_relationships.len(), 1, "parent is referenced by child");
}
@@ -954,7 +954,7 @@ fn explicit_constraint_name_is_used() {
Some("create table child (id serial primary key, pid int, constraint child_to_parent foreign key (pid) references parent(id))".to_string()),
))
.expect("create child with named FK");
let child = r.block_on(db.describe_table("child".to_string(), None)).expect("describe");
let child = r.block_on(db.describe_table("child".to_string())).expect("describe");
assert_eq!(child.outbound_relationships[0].name, "child_to_parent");
}
@@ -974,7 +974,7 @@ fn bare_references_resolves_to_parent_single_column_pk() {
Some("create table child (id serial primary key, pid int references parent)".to_string()),
))
.expect("create child with bare REFERENCES");
let child = r.block_on(db.describe_table("child".to_string(), None)).expect("describe");
let child = r.block_on(db.describe_table("child".to_string())).expect("describe");
assert_eq!(child.outbound_relationships[0].other_columns, vec!["id".to_string()], "resolved to parent PK");
}
@@ -1108,7 +1108,7 @@ fn create_table_with_fk_is_one_undo_step() {
// parent (now un-referenced) can be described without a dangling rel.
r.block_on(db.undo()).expect("undo").expect("a step was undone");
assert!(!r.block_on(db.list_tables()).unwrap().contains(&"child".to_string()));
let parent = r.block_on(db.describe_table("parent".to_string(), None)).expect("describe parent");
let parent = r.block_on(db.describe_table("parent".to_string())).expect("describe parent");
assert!(parent.inbound_relationships.is_empty(), "the relationship was undone with the table");
}
@@ -1152,7 +1152,7 @@ fn foreign_key_on_delete_cascade_takes_effect() {
))
.expect("delete parent");
let child_rows = r
.block_on(db.query_data("child".to_string(), None, None, None))
.block_on(db.query_data("child".to_string(), None, None))
.expect("query child");
assert!(child_rows.rows.is_empty(), "ON DELETE CASCADE removed the child row");
}
@@ -1232,7 +1232,7 @@ fn fk_survives_a_rebuild_triggering_column_add() {
.expect("add column via rebuild");
// The relationship still exists after the rebuild.
let child = r.block_on(db.describe_table("child".to_string(), None)).expect("describe");
let child = r.block_on(db.describe_table("child".to_string())).expect("describe");
assert_eq!(child.outbound_relationships.len(), 1, "FK survived the column-add rebuild");
// And the engine still enforces it (now and after a fresh rebuild).
insert_parent_row(&db, &r);
@@ -1275,7 +1275,7 @@ fn fk_referential_actions_survive_rebuild() {
))
.expect("create");
r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)).expect("rebuild");
let child = r.block_on(db.describe_table("child".to_string(), None)).expect("describe");
let child = r.block_on(db.describe_table("child".to_string())).expect("describe");
let rel = &child.outbound_relationships[0];
assert_eq!(rel.on_delete, ReferentialAction::Cascade, "ON DELETE survived rebuild");
assert_eq!(rel.on_update, ReferentialAction::SetNull, "ON UPDATE survived rebuild");
@@ -1299,7 +1299,7 @@ fn dropping_the_child_clears_the_fk_relationship() {
.expect("create");
r.block_on(db.drop_table("child".to_string(), Some("drop table child".to_string())))
.expect("drop child");
let parent = r.block_on(db.describe_table("parent".to_string(), None)).expect("describe parent");
let parent = r.block_on(db.describe_table("parent".to_string())).expect("describe parent");
assert!(parent.inbound_relationships.is_empty(), "dropping the child cleared the relationship");
}
@@ -1341,7 +1341,7 @@ fn bare_self_reference_resolves_to_own_pk() {
Some("create table emp (id int primary key, mgr int references emp)".to_string()),
))
.expect("create self-referential emp with a bare reference");
let emp = r.block_on(db.describe_table("emp".to_string(), None)).expect("describe");
let emp = r.block_on(db.describe_table("emp".to_string())).expect("describe");
assert_eq!(emp.outbound_relationships[0].other_columns, vec!["id".to_string()], "bare self-ref resolved to own PK");
// Enforced: a non-existent manager is rejected.
r.block_on(db.insert(
+4 -4
View File
@@ -154,7 +154,7 @@ fn delete_without_where_runs_across_all_rows() {
let csv = read_csv(&project, "t").unwrap_or_default();
assert!(!csv.contains('a') && !csv.contains('b') && !csv.contains('c'), "no rows left: {csv:?}");
let remaining = rt
.block_on(db.query_data("t".to_string(), None, None, None))
.block_on(db.query_data("t".to_string(), None, None))
.expect("query t");
assert!(remaining.rows.is_empty(), "table empty after unfiltered delete");
}
@@ -302,8 +302,8 @@ fn cascade_to_two_children_reports_both() {
assert_eq!(by_child.get("Orders"), Some(&2), "two orders cascaded");
assert_eq!(by_child.get("Reviews"), Some(&1), "one review cascaded");
// Both child CSVs re-persisted to the post-cascade (empty) state.
let orders = rt.block_on(db.query_data("Orders".to_string(), None, None, None)).unwrap();
let reviews = rt.block_on(db.query_data("Reviews".to_string(), None, None, None)).unwrap();
let orders = rt.block_on(db.query_data("Orders".to_string(), None, None)).unwrap();
let reviews = rt.block_on(db.query_data("Reviews".to_string(), None, None)).unwrap();
assert!(orders.rows.is_empty() && reviews.rows.is_empty(), "both children emptied");
let _ = &project;
}
@@ -361,7 +361,7 @@ fn delete_violating_fk_fails_and_persists_nothing() {
let result = run_delete(&db, &rt, input);
assert!(result.is_err(), "delete of a referenced parent must be rejected");
// Rolled back: Alice survives.
let customers = rt.block_on(db.query_data("Customers".to_string(), None, None, None)).unwrap();
let customers = rt.block_on(db.query_data("Customers".to_string(), None, None)).unwrap();
assert_eq!(customers.rows.len(), 1, "parent row preserved after rejected delete");
// No history line for the failed statement (written only on success).
let history = std::fs::read_to_string(project.path().join("history.log")).unwrap_or_default();
+1 -1
View File
@@ -149,7 +149,7 @@ fn seed(db: &Database, rt: &tokio::runtime::Runtime, sql: &str) {
}
fn query(db: &Database, rt: &tokio::runtime::Runtime, table: &str) -> Vec<Vec<Option<String>>> {
rt.block_on(db.query_data(table.to_string(), None, None, None))
rt.block_on(db.query_data(table.to_string(), None, None))
.unwrap_or_else(|e| panic!("query_data {table}: {e:?}"))
.rows
}
+1 -1
View File
@@ -55,7 +55,7 @@ fn make_t_with_index(db: &Database, r: &tokio::runtime::Runtime) -> String {
}
fn index_names(db: &Database, r: &tokio::runtime::Runtime) -> Vec<String> {
r.block_on(db.describe_table("T".to_string(), None))
r.block_on(db.describe_table("T".to_string()))
.expect("describe")
.indexes
.into_iter()
+1 -1
View File
@@ -150,7 +150,7 @@ fn drop_table_is_one_undo_step_and_restores_data() {
assert!(r.block_on(db.undo()).expect("undo").is_some(), "the drop was one undo step");
assert!(r.block_on(db.list_tables()).unwrap().contains(&"T".to_string()));
let data = r
.block_on(db.query_data("T".to_string(), None, None, None))
.block_on(db.query_data("T".to_string(), None, None))
.expect("query");
assert_eq!(data.rows.len(), 1, "the dropped row was restored by undo");
}
+10 -16
View File
@@ -215,7 +215,7 @@ fn decimal_aggregation_display_trims_ieee754_noise() {
// The reported case: the aggregate no longer leaks float noise.
let agg = rt
.block_on(db.run_select("select sum(price * qty) from Products".to_string(), None))
.block_on(db.run_select("select sum(price * qty) from Products".to_string()))
.expect("aggregate select");
assert_eq!(
agg.rows[0][0].as_deref(),
@@ -226,7 +226,7 @@ fn decimal_aggregation_display_trims_ieee754_noise() {
// Raw decimal column is still exact — TEXT storage preserves
// the input string verbatim, including the trailing zero.
let raw = rt
.block_on(db.run_select("select price from Products".to_string(), None))
.block_on(db.run_select("select price from Products".to_string()))
.expect("raw decimal select");
let prices: Vec<&str> = raw.rows.iter().map(|r| r[0].as_deref().unwrap()).collect();
assert_eq!(
@@ -240,10 +240,7 @@ fn decimal_aggregation_display_trims_ieee754_noise() {
fn database_run_select_constant_returns_a_single_row() {
let (_p, db, _dir) = open_project_db();
let data = rt()
.block_on(db.run_select(
"select 1".to_string(),
Some("select 1".to_string()),
))
.block_on(db.run_select("select 1".to_string()))
.expect("`select 1` runs clean");
assert_eq!(data.rows.len(), 1, "one result row");
assert_eq!(data.rows[0].len(), 1, "one column");
@@ -288,7 +285,7 @@ fn database_run_select_from_user_table_returns_inserted_rows() {
.expect("insert row");
});
let data = rt
.block_on(db.run_select("select Name from T".to_string(), None))
.block_on(db.run_select("select Name from T".to_string()))
.expect("SELECT runs");
assert_eq!(data.rows.len(), 1);
assert_eq!(data.rows[0][0].as_deref(), Some("Ada"));
@@ -336,7 +333,7 @@ fn database_run_select_recovers_bool_column_type() {
.expect("insert row");
});
let data = rt
.block_on(db.run_select("select Active from Products".to_string(), None))
.block_on(db.run_select("select Active from Products".to_string()))
.expect("SELECT runs");
assert_eq!(data.rows.len(), 2);
assert_eq!(data.column_types, vec![Some(Type::Bool)]);
@@ -374,7 +371,7 @@ fn database_run_select_recovers_text_type_through_alias() {
// playground type is recovered.
let data = rt
.block_on(
db.run_select("select Name as n from Users".to_string(), None),
db.run_select("select Name as n from Users".to_string()),
)
.expect("SELECT runs");
assert_eq!(data.columns, vec!["n".to_string()]);
@@ -402,7 +399,7 @@ fn database_run_select_computed_expression_stays_typeless() {
.expect("insert");
});
let data = rt
.block_on(db.run_select("select Score + 1 from T".to_string(), None))
.block_on(db.run_select("select Score + 1 from T".to_string()))
.expect("SELECT runs");
assert_eq!(data.column_types, vec![None]);
}
@@ -439,7 +436,6 @@ fn engine_aggregate_in_where_routes_through_catalog() {
let err = rt
.block_on(db.run_select(
"select id from T where count(score) > 0".to_string(),
None,
))
.expect_err("engine should reject aggregate in WHERE");
let DbError::Sqlite { .. } = &err else {
@@ -512,7 +508,6 @@ fn engine_group_by_missing_routes_through_catalog() {
let _ = rt
.block_on(db.run_select(
"select category, count(*) from T group by category".to_string(),
None,
))
.expect("benign GROUP BY query runs");
// Direct unit test on the matcher: ensure a message that
@@ -574,7 +569,6 @@ fn engine_scalar_subquery_too_many_rows_routes_through_catalog() {
let _ = rt
.block_on(db.run_select(
"select (select v from T) from T".to_string(),
None,
))
.expect("benign scalar subquery query runs");
let synthetic = DbError::Sqlite {
@@ -624,13 +618,13 @@ fn database_run_select_type_recovery_works_on_empty_table() {
});
// No INSERT — the table is empty.
let data_text = rt
.block_on(db.run_select("select col_text from Empty".to_string(), None))
.block_on(db.run_select("select col_text from Empty".to_string()))
.expect("SELECT runs even on empty table");
assert!(data_text.rows.is_empty());
assert_eq!(data_text.column_types, vec![Some(Type::Text)]);
let data_blob = rt
.block_on(db.run_select("select col_blob from Empty".to_string(), None))
.block_on(db.run_select("select col_blob from Empty".to_string()))
.expect("SELECT runs even on empty table");
assert!(data_blob.rows.is_empty());
assert_eq!(
@@ -723,7 +717,7 @@ fn database_run_select_recovers_all_ten_playground_types() {
for (col, expected_type) in cases {
let sql = format!("select {col} from AllTypes");
let data = rt
.block_on(db.run_select(sql.clone(), None))
.block_on(db.run_select(sql.clone()))
.expect("SELECT runs");
assert_eq!(
data.column_types,
+1 -1
View File
@@ -501,7 +501,7 @@ fn update_all_rows_flag_in_advanced_updates_every_row() {
"the --all-rows update replays through the DSL fall-back; events: {events:?}"
);
let rows = rt
.block_on(db.query_data("t".to_string(), None, None, None))
.block_on(db.query_data("t".to_string(), None, None))
.expect("query")
.rows;
assert_eq!(rows.len(), 2, "both rows present");
+3 -3
View File
@@ -63,7 +63,7 @@ async fn insert_named(db: &Database, name: &str) {
}
async fn row_count(db: &Database) -> usize {
db.query_data("Customers".to_string(), None, None, None)
db.query_data("Customers".to_string(), None, None)
.await
.unwrap()
.rows
@@ -306,7 +306,7 @@ async fn sql_delete(db: &Database, input: &str) {
}
async fn count_t(db: &Database) -> usize {
db.query_data("T".to_string(), None, None, None)
db.query_data("T".to_string(), None, None)
.await
.unwrap()
.rows
@@ -378,7 +378,7 @@ fn undo_restores_db_and_csv_consistently() {
// Both the database read model and the on-disk CSV are
// restored — the (db, csv) pair stays consistent.
assert_eq!(
db.query_data("T".to_string(), None, None, None)
db.query_data("T".to_string(), None, None)
.await
.unwrap()
.rows