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:
@@ -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
|
no-op-skip / read-only sites no longer journal; success is journalled at
|
||||||
the dispatch layer (`spawn_dsl_dispatch` / `run_replay` / app-command
|
the dispatch layer (`spawn_dsl_dispatch` / `run_replay` / app-command
|
||||||
sites). The ring stays `Vec<String>`; `seed_history` / `ProjectSwitched`
|
sites). The ring stays `Vec<String>`; `seed_history` / `ProjectSwitched`
|
||||||
are untouched. The vestigial worker `source` plumbing (the `_source`
|
are untouched. The vestigial worker `source` plumbing has since been
|
||||||
param on `finalize_persistence` / `do_rebuild_from_text` and the thin
|
**fully unwound** (2026-06-14 follow-up): `_source` removed from
|
||||||
read-only `*_request` wrappers) is left in place — a clean follow-up.
|
`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
|
- **App commands recall bare.** Because they are dispatched outside the
|
||||||
`ExecuteDsl`/spawn path, app commands journal **simple** (`advanced =
|
`ExecuteDsl`/spawn path, app commands journal **simple** (`advanced =
|
||||||
false`) at their own sites, and `submit` excludes them from the ring's
|
false`) at their own sites, and `submit` excludes them from the ring's
|
||||||
|
|||||||
+1
-1
@@ -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-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-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-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*)
|
||||||
|
|||||||
+13
-13
@@ -1190,7 +1190,7 @@ async fn build_schema_cache(database: &Database) -> crate::completion::SchemaCac
|
|||||||
// miss leaves that table's columns unpopulated and the
|
// miss leaves that table's columns unpopulated and the
|
||||||
// walker falls back to the schemaless value-literal list.
|
// walker falls back to the schemaless value-literal list.
|
||||||
for name in cache.tables.clone() {
|
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).
|
// Per-table indexes for the items panel (S2, ADR-0025).
|
||||||
// Carry uniqueness so the panel can mark a UNIQUE index
|
// Carry uniqueness so the panel can mark a UNIQUE index
|
||||||
// (ADR-0035 §4d). Captured before `desc.columns` is
|
// (ADR-0035 §4d). Captured before `desc.columns` is
|
||||||
@@ -1650,7 +1650,7 @@ async fn build_show_data_echo(
|
|||||||
limit: Some(_),
|
limit: Some(_),
|
||||||
..
|
..
|
||||||
} => database
|
} => database
|
||||||
.describe_table(name.clone(), None)
|
.describe_table(name.clone())
|
||||||
.await
|
.await
|
||||||
.map(|desc| {
|
.map(|desc| {
|
||||||
desc.columns
|
desc.columns
|
||||||
@@ -1732,7 +1732,7 @@ async fn collect_echo_lookups(
|
|||||||
Command::DropIndex {
|
Command::DropIndex {
|
||||||
selector: IndexSelector::Columns { table, columns },
|
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)
|
&& let Some(idx) = desc.indexes.iter().find(|i| i.columns == *columns)
|
||||||
{
|
{
|
||||||
out.drop_index_name = Some(idx.name.clone());
|
out.drop_index_name = Some(idx.name.clone());
|
||||||
@@ -1747,7 +1747,7 @@ async fn collect_echo_lookups(
|
|||||||
child_column,
|
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| {
|
&& let Some(rel) = desc.outbound_relationships.iter().find(|r| {
|
||||||
// The Endpoints drop selector is single-column
|
// The Endpoints drop selector is single-column
|
||||||
// (ADR-0043 keeps DROP by-endpoints 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.
|
// resolver API would be the next step if schemas grow.
|
||||||
if let Ok(tables) = database.list_tables().await {
|
if let Ok(tables) = database.list_tables().await {
|
||||||
for table in tables {
|
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)
|
&& desc.outbound_relationships.iter().any(|r| r.name == *name)
|
||||||
{
|
{
|
||||||
out.drop_relationship = Some((name.clone(), table.clone()));
|
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
|
// *before* execution to know which `ADD COLUMN` lines to
|
||||||
// emit. The parent columns here are the explicit DSL list,
|
// emit. The parent columns here are the explicit DSL list,
|
||||||
// paired positionally with the child list.
|
// paired positionally with the child list.
|
||||||
let parent_desc = database.describe_table(parent_table.clone(), None).await;
|
let parent_desc = database.describe_table(parent_table.clone()).await;
|
||||||
let child_desc = database.describe_table(child_table.clone(), None).await;
|
let child_desc = database.describe_table(child_table.clone()).await;
|
||||||
if let (Ok(parent_desc), Ok(child_desc)) = (parent_desc, child_desc) {
|
if let (Ok(parent_desc), Ok(child_desc)) = (parent_desc, child_desc) {
|
||||||
let mut new_columns: Vec<(String, crate::dsl::types::Type)> = Vec::new();
|
let mut new_columns: Vec<(String, crate::dsl::types::Type)> = Vec::new();
|
||||||
for (child_col, parent_col) in child_columns.iter().zip(parent_columns) {
|
for (child_col, parent_col) in child_columns.iter().zip(parent_columns) {
|
||||||
@@ -2064,7 +2064,7 @@ async fn enrich_check_violation(
|
|||||||
.await
|
.await
|
||||||
.map(|v| v.to_string());
|
.map(|v| v.to_string());
|
||||||
// The rule itself — the column's compiled CHECK expression.
|
// 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)
|
&& let Some(col) = desc.columns.iter().find(|c| c.name == column)
|
||||||
{
|
{
|
||||||
facts.check_rule.clone_from(&col.check);
|
facts.check_rule.clone_from(&col.check);
|
||||||
@@ -2272,7 +2272,7 @@ async fn user_value_for_column_with_schema(
|
|||||||
} = command
|
} = command
|
||||||
{
|
{
|
||||||
let desc = database
|
let desc = database
|
||||||
.describe_table(table.to_string(), None)
|
.describe_table(table.to_string())
|
||||||
.await
|
.await
|
||||||
.ok()?;
|
.ok()?;
|
||||||
// Build the natural-order column list the same way
|
// 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
|
&& literal_rows.len() == 1
|
||||||
{
|
{
|
||||||
let desc = database
|
let desc = database
|
||||||
.describe_table(table.to_string(), None)
|
.describe_table(table.to_string())
|
||||||
.await
|
.await
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let idx = desc.columns.iter().position(|c| c.name == column)?;
|
let idx = desc.columns.iter().position(|c| c.name == column)?;
|
||||||
@@ -2930,7 +2930,7 @@ async fn execute_command_typed(
|
|||||||
.await
|
.await
|
||||||
.map(|d| CommandOutcome::Schema(Some(d))),
|
.map(|d| CommandOutcome::Schema(Some(d))),
|
||||||
Command::ShowTable { name } => database
|
Command::ShowTable { name } => database
|
||||||
.describe_table(name, src)
|
.describe_table(name)
|
||||||
.await
|
.await
|
||||||
.map(|d| CommandOutcome::Schema(Some(d))),
|
.map(|d| CommandOutcome::Schema(Some(d))),
|
||||||
// ADR-0044: a named relationship renders as a diagram (App-side),
|
// ADR-0044: a named relationship renders as a diagram (App-side),
|
||||||
@@ -2983,14 +2983,14 @@ async fn execute_command_typed(
|
|||||||
filter,
|
filter,
|
||||||
limit,
|
limit,
|
||||||
} => database
|
} => database
|
||||||
.query_data(name, filter, limit, src)
|
.query_data(name, filter, limit)
|
||||||
.await
|
.await
|
||||||
.map(CommandOutcome::Query),
|
.map(CommandOutcome::Query),
|
||||||
// A SQL `SELECT` (advanced mode; ADR-0030 §6, ADR-0031).
|
// A SQL `SELECT` (advanced mode; ADR-0030 §6, ADR-0031).
|
||||||
// The grammar walker has already validated `sql` is in
|
// The grammar walker has already validated `sql` is in
|
||||||
// the supported subset; the worker runs it as text.
|
// the supported subset; the worker runs it as text.
|
||||||
Command::Select { sql } => database
|
Command::Select { sql } => database
|
||||||
.run_select(sql, src)
|
.run_select(sql)
|
||||||
.await
|
.await
|
||||||
.map(CommandOutcome::Query),
|
.map(CommandOutcome::Query),
|
||||||
// A SQL `INSERT` (advanced mode; ADR-0033 §1). Grammar-as-
|
// A SQL `INSERT` (advanced mode; ADR-0033 §1). Grammar-as-
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ fn rename_column_with_case_variant_table_keeps_metadata_in_step() {
|
|||||||
.expect("rename column via a case-variant table name");
|
.expect("rename column via a case-variant table name");
|
||||||
|
|
||||||
let desc = r
|
let desc = r
|
||||||
.block_on(db.describe_table("Items".to_string(), None))
|
.block_on(db.describe_table("Items".to_string()))
|
||||||
.expect("describe Items");
|
.expect("describe Items");
|
||||||
let amount = desc
|
let amount = desc
|
||||||
.columns
|
.columns
|
||||||
@@ -126,7 +126,7 @@ fn insert_with_case_variant_table_persists_and_survives_rebuild() {
|
|||||||
|
|
||||||
let db = fresh_rebuild(db, &project, &r);
|
let db = fresh_rebuild(db, &project, &r);
|
||||||
let rows = 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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(rows.len(), 1, "the wrong-case insert survived the rebuild (no data loss)");
|
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 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");
|
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");
|
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).
|
// 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",
|
add 1:n relationship from parent.id to child.parent_id\n",
|
||||||
);
|
);
|
||||||
// The parent's inbound relationship is visible under the stored case.
|
// 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.len(), 1, "relationship recorded under the stored case");
|
||||||
assert_eq!(p.inbound_relationships[0].other_table, "Child");
|
assert_eq!(p.inbound_relationships[0].other_table, "Child");
|
||||||
|
|
||||||
let db = fresh_rebuild(db, &project, &r);
|
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.len(), 1, "relationship survived the rebuild");
|
||||||
assert_eq!(p.inbound_relationships[0].other_table, "Child");
|
assert_eq!(p.inbound_relationships[0].other_table, "Child");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ fn compound_fk_declares_enforces_and_round_trips() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// describe shows the compound endpoints symmetrically.
|
// 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];
|
let outbound = &city.outbound_relationships[0];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
outbound.local_columns,
|
outbound.local_columns,
|
||||||
@@ -329,7 +329,7 @@ fn compound_fk_create_fk_makes_both_child_columns() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("add compound relationship with --create-fk");
|
.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"] {
|
for col in ["c_country", "c_code"] {
|
||||||
assert!(
|
assert!(
|
||||||
city.columns.iter().any(|c| c.name == col),
|
city.columns.iter().any(|c| c.name == col),
|
||||||
@@ -527,7 +527,7 @@ fn compound_fk_survives_rebuild_from_text() {
|
|||||||
.await;
|
.await;
|
||||||
assert!(bad.is_err(), "compound FK still enforced after rebuild from text");
|
assert!(bad.is_err(), "compound FK still enforced after rebuild from text");
|
||||||
// Endpoints survived the round-trip intact.
|
// 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!(
|
assert_eq!(
|
||||||
city.outbound_relationships[0].other_columns,
|
city.outbound_relationships[0].other_columns,
|
||||||
vec!["country".to_string(), "code".to_string()],
|
vec!["country".to_string(), "code".to_string()],
|
||||||
@@ -563,7 +563,7 @@ fn compound_fk_undo_removes_the_relationship() {
|
|||||||
.await
|
.await
|
||||||
.expect("add compound relationship");
|
.expect("add compound relationship");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.describe_table("City".to_string(), None)
|
db.describe_table("City".to_string())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.outbound_relationships
|
.outbound_relationships
|
||||||
@@ -573,7 +573,7 @@ fn compound_fk_undo_removes_the_relationship() {
|
|||||||
// One undo step removes the whole relationship (ADR-0013/0006).
|
// One undo step removes the whole relationship (ADR-0013/0006).
|
||||||
db.undo().await.unwrap().expect("undo applied");
|
db.undo().await.unwrap().expect("undo applied");
|
||||||
assert!(
|
assert!(
|
||||||
db.describe_table("City".to_string(), None)
|
db.describe_table("City".to_string())
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.outbound_relationships
|
.outbound_relationships
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ fn rebuild_restores_schema_only_project() {
|
|||||||
|
|
||||||
// Phase 4: confirm Customers exists with the right shape.
|
// Phase 4: confirm Customers exists with the right shape.
|
||||||
let desc = rt()
|
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");
|
.expect("describe_table");
|
||||||
assert_eq!(desc.name, "Customers");
|
assert_eq!(desc.name, "Customers");
|
||||||
let cols: Vec<&str> = desc.columns.iter().map(|c| c.name.as_str()).collect();
|
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()
|
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");
|
.expect("query_data");
|
||||||
assert_eq!(rows.rows.len(), 2);
|
assert_eq!(rows.rows.len(), 2);
|
||||||
let names: Vec<Option<String>> = rows.rows.iter().map(|r| r[1].clone()).collect();
|
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
|
// Trigger any successful command so project.yaml is
|
||||||
// rewritten from the now-rebuilt db state.
|
// rewritten from the now-rebuilt db state.
|
||||||
rt().block_on(async {
|
rt().block_on(async {
|
||||||
db.describe_table("T".to_string(), Some("show table T".to_string()))
|
db.describe_table("T".to_string())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// describe is read-only; force a rewrite by adding a column.
|
// describe is read-only; force a rewrite by adding a column.
|
||||||
@@ -451,7 +451,7 @@ fn rebuild_restores_indexes() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let desc = rt()
|
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");
|
.expect("describe_table");
|
||||||
assert_eq!(desc.indexes.len(), 1, "index should survive rebuild");
|
assert_eq!(desc.indexes.len(), 1, "index should survive rebuild");
|
||||||
assert_eq!(desc.indexes[0].name, "idx_email");
|
assert_eq!(desc.indexes[0].name, "idx_email");
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ fn rebuild_against_populated_db_wipes_and_reloads() {
|
|||||||
.expect("rebuild");
|
.expect("rebuild");
|
||||||
});
|
});
|
||||||
let rows = rt()
|
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();
|
.unwrap();
|
||||||
assert_eq!(rows.rows.len(), 1);
|
assert_eq!(rows.rows.len(), 1);
|
||||||
assert_eq!(rows.rows[0][1].as_deref(), Some("Edna"));
|
assert_eq!(rows.rows[0][1].as_deref(), Some("Edna"));
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ fn end_to_end_export_then_import_real_project() {
|
|||||||
|
|
||||||
// Round-trip: the inserted row is back.
|
// Round-trip: the inserted row is back.
|
||||||
let data_view = rt()
|
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");
|
.expect("query data");
|
||||||
assert_eq!(data_view.rows.len(), 1);
|
assert_eq!(data_view.rows.len(), 1);
|
||||||
// Serial id auto-filled to 1; Name was the inserted value.
|
// Serial id auto-filled to 1; Name was the inserted value.
|
||||||
|
|||||||
+6
-6
@@ -107,7 +107,7 @@ fn generates_junction_with_compound_pk_and_two_enforced_fks() {
|
|||||||
assert!(tables.contains(&"Students_Courses".to_string()), "tables: {tables:?}");
|
assert!(tables.contains(&"Students_Courses".to_string()), "tables: {tables:?}");
|
||||||
|
|
||||||
// Two FK columns, both part of the compound PK.
|
// 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)> =
|
let cols: Vec<(&str, bool)> =
|
||||||
desc.columns.iter().map(|c| (c.name.as_str(), c.primary_key)).collect();
|
desc.columns.iter().map(|c| (c.name.as_str(), c.primary_key)).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -191,7 +191,7 @@ fn compound_parent_pk_contributes_one_fk_column_each() {
|
|||||||
.await
|
.await
|
||||||
.expect("create m:n");
|
.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();
|
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"]);
|
assert_eq!(names, vec!["Students_id", "Sections_course_id", "Sections_term"]);
|
||||||
// All three form the compound PK.
|
// 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).
|
// Deleting the student cascades to the junction (ON DELETE CASCADE).
|
||||||
db.delete("Students".to_string(), RowFilter::AllRows, None).await.unwrap();
|
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);
|
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();
|
let tables = db.list_tables().await.unwrap();
|
||||||
assert!(!tables.contains(&"Students_Courses".to_string()), "undo should remove the junction: {tables:?}");
|
assert!(!tables.contains(&"Students_Courses".to_string()), "undo should remove the junction: {tables:?}");
|
||||||
// The parents' relationships are gone too (the junction held them).
|
// 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");
|
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(&"Enrollments".to_string()), "tables: {tables:?}");
|
||||||
assert!(!tables.contains(&"Students_Courses".to_string()));
|
assert!(!tables.contains(&"Students_Courses".to_string()));
|
||||||
// Both relationships survive the rename (rebuild-preserving).
|
// 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");
|
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");
|
db.rebuild_from_text(project.path().to_path_buf(), None).await.expect("rebuild");
|
||||||
let tables = db.list_tables().await.unwrap();
|
let tables = db.list_tables().await.unwrap();
|
||||||
assert!(tables.contains(&"Students_Courses".to_string()), "junction survived: {tables:?}");
|
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_eq!(desc.outbound_relationships.len(), 2, "both FKs reconstructed");
|
||||||
assert!(desc.columns.iter().all(|c| c.primary_key), "compound PK reconstructed");
|
assert!(desc.columns.iter().all(|c| c.primary_key), "compound PK reconstructed");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,13 +108,13 @@ fn replay_runs_advanced_sql_create_table_as_a_write() {
|
|||||||
|
|
||||||
// The SQL DDL line actually created the structural table…
|
// The SQL DDL line actually created the structural table…
|
||||||
let desc = rt()
|
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");
|
.expect("describe");
|
||||||
let names: Vec<String> = desc.columns.iter().map(|c| c.name.clone()).collect();
|
let names: Vec<String> = desc.columns.iter().map(|c| c.name.clone()).collect();
|
||||||
assert_eq!(names, vec!["id".to_string(), "name".to_string()]);
|
assert_eq!(names, vec!["id".to_string(), "name".to_string()]);
|
||||||
// …and the following insert (serial id auto-filled) ran against it.
|
// …and the following insert (serial id auto-filled) ran against it.
|
||||||
let rows = rt()
|
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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(rows.len(), 1);
|
assert_eq!(rows.len(), 1);
|
||||||
@@ -139,7 +139,7 @@ fn replay_three_lines_dispatches_three_commands() {
|
|||||||
|
|
||||||
// The dispatched commands actually mutated state.
|
// The dispatched commands actually mutated state.
|
||||||
let data_result = rt()
|
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");
|
.expect("query_data");
|
||||||
assert_eq!(data_result.rows.len(), 1, "row inserted");
|
assert_eq!(data_result.rows.len(), 1, "row inserted");
|
||||||
assert_eq!(data_result.rows[0][1].as_deref(), Some("Alice"));
|
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);
|
assert_completed(&events, 3);
|
||||||
|
|
||||||
let data_result = rt()
|
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");
|
.expect("query_data");
|
||||||
assert_eq!(data_result.rows.len(), 1, "only the ok INSERT applied");
|
assert_eq!(data_result.rows.len(), 1, "only the ok INSERT applied");
|
||||||
assert_eq!(data_result.rows[0][1].as_deref(), Some("alpha"));
|
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:?}"),
|
other => panic!("expected ReplayCompleted, got {other:?}"),
|
||||||
}
|
}
|
||||||
let data_result = rt()
|
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");
|
.expect("query_data");
|
||||||
assert!(
|
assert!(
|
||||||
data_result.columns.iter().any(|c| c == "v"),
|
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
|
// but earlier commands stayed applied (table T exists with
|
||||||
// the `name` column).
|
// the `name` column).
|
||||||
let desc = rt()
|
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");
|
.expect("describe_table");
|
||||||
assert!(
|
assert!(
|
||||||
desc.columns.iter().any(|c| c.name == "name"),
|
desc.columns.iter().any(|c| c.name == "name"),
|
||||||
"earlier add column should have stayed applied"
|
"earlier add column should have stayed applied"
|
||||||
);
|
);
|
||||||
let data_result = rt()
|
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");
|
.expect("query_data");
|
||||||
assert!(
|
assert!(
|
||||||
data_result.rows.is_empty(),
|
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
|
// The earlier two lines stayed applied; the failing insert
|
||||||
// did not run — state is intact.
|
// did not run — state is intact.
|
||||||
let data_result = rt()
|
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");
|
.expect("query_data");
|
||||||
assert!(
|
assert!(
|
||||||
data_result.rows.is_empty(),
|
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:?}"),
|
other => panic!("expected ReplayCompleted (nested replay skipped), got {other:?}"),
|
||||||
}
|
}
|
||||||
// The nested file's table was NOT created (the replay was skipped).
|
// 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)");
|
assert!(cols.is_err(), "inner.commands' table T must not exist (nested replay skipped)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ fn app_show_table_renders_relationships_as_compact_diagrams() {
|
|||||||
rt.block_on(seed_schema(&db));
|
rt.block_on(seed_schema(&db));
|
||||||
// Orders holds the FK to Customers — an outbound relationship.
|
// Orders holds the FK to Customers — an outbound relationship.
|
||||||
let desc = rt
|
let desc = rt
|
||||||
.block_on(db.describe_table("Orders".to_string(), None))
|
.block_on(db.describe_table("Orders".to_string()))
|
||||||
.expect("describe Orders");
|
.expect("describe Orders");
|
||||||
|
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|||||||
+17
-17
@@ -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`.
|
/// The current user-facing type of column `name` in table `T`.
|
||||||
fn col_type(db: &Database, r: &tokio::runtime::Runtime, name: &str) -> Option<Type> {
|
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")
|
.expect("describe")
|
||||||
.columns
|
.columns
|
||||||
.into_iter()
|
.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> {
|
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")
|
.expect("describe")
|
||||||
.columns
|
.columns
|
||||||
.into_iter()
|
.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.
|
// The DEFAULT backfilled the pre-existing row to qty = 0.
|
||||||
let rows = r
|
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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(rows.len(), 1);
|
assert_eq!(rows.len(), 1);
|
||||||
@@ -252,7 +252,7 @@ fn e2e_alter_column_type_clean_and_lossy_convert() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let rows = r
|
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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(rows.len(), 1);
|
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");
|
assert_eq!(col_type(&db, &r, "n"), Some(Type::Serial), "int→serial converted the column");
|
||||||
let rows = r
|
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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(rows[0][1].as_deref(), Some("100"), "the existing value is preserved");
|
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");
|
.expect("write");
|
||||||
r.block_on(run_replay(&db, project.path(), "u.commands"));
|
r.block_on(run_replay(&db, project.path(), "u.commands"));
|
||||||
let has_unique = || {
|
let has_unique = || {
|
||||||
!r.block_on(db.describe_table("T".to_string(), None))
|
!r.block_on(db.describe_table("T".to_string()))
|
||||||
.expect("describe")
|
.expect("describe")
|
||||||
.unique_constraints
|
.unique_constraints
|
||||||
.is_empty()
|
.is_empty()
|
||||||
@@ -878,7 +878,7 @@ fn e2e_describe_shows_table_level_constraints() {
|
|||||||
"events: {events:?}"
|
"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!(
|
assert_eq!(
|
||||||
desc.unique_constraints,
|
desc.unique_constraints,
|
||||||
vec![vec!["a".to_string(), "b".to_string()]],
|
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");
|
assert!(!csv_path(&project, "Orders").exists(), "data/Orders.csv removed");
|
||||||
|
|
||||||
let rows = r
|
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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(rows.len(), 2);
|
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:?}"
|
"Purchases round-tripped through a fresh rebuild: {tables:?}"
|
||||||
);
|
);
|
||||||
let rows = r
|
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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(rows.len(), 2);
|
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.
|
// 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.len(), 1);
|
||||||
assert_eq!(c.outbound_relationships[0].other_table, "Parent");
|
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.
|
// 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.len(), 1);
|
||||||
assert_eq!(p.inbound_relationships[0].other_table, "Child");
|
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`.
|
// 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.outbound_relationships[0].other_table, "Tree");
|
||||||
assert_eq!(t.inbound_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:?}"
|
"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.len(), 1, "the index followed the rename");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
u.indexes[0].name, "T_email_idx",
|
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).
|
// Survives a fresh rebuild (recreated from IndexSchema on table Users).
|
||||||
let db = fresh_rebuild(db, &project, &r);
|
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.len(), 1);
|
||||||
assert_eq!(u.indexes[0].name, "T_email_idx");
|
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:?}"
|
"undo restored the old table name: {tables:?}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
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,
|
1,
|
||||||
"the row is back under the old name"
|
"the row is back under the old name"
|
||||||
);
|
);
|
||||||
@@ -1427,7 +1427,7 @@ fn e2e_alter_column_set_default_applies() {
|
|||||||
))
|
))
|
||||||
.expect("insert omitting qty");
|
.expect("insert omitting qty");
|
||||||
let rows = r
|
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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1473,7 +1473,7 @@ fn e2e_alter_column_drop_default_removes_it() {
|
|||||||
))
|
))
|
||||||
.expect("insert omitting qty");
|
.expect("insert omitting qty");
|
||||||
let rows = r
|
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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -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)> {
|
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")
|
.expect("describe")
|
||||||
.indexes
|
.indexes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ fn created_table_appears_with_playground_types() {
|
|||||||
assert!(tables.contains(&"Widget".to_string()));
|
assert!(tables.contains(&"Widget".to_string()));
|
||||||
|
|
||||||
let desc = r
|
let desc = r
|
||||||
.block_on(db.describe_table("Widget".to_string(), None))
|
.block_on(db.describe_table("Widget".to_string()))
|
||||||
.expect("describe");
|
.expect("describe");
|
||||||
let types: Vec<(String, Option<Type>)> = desc
|
let types: Vec<(String, Option<Type>)> = desc
|
||||||
.columns
|
.columns
|
||||||
@@ -98,7 +98,7 @@ fn integer_primary_key_is_plain_int() {
|
|||||||
))
|
))
|
||||||
.expect("create");
|
.expect("create");
|
||||||
let desc = r
|
let desc = r
|
||||||
.block_on(db.describe_table("T".to_string(), None))
|
.block_on(db.describe_table("T".to_string()))
|
||||||
.expect("describe");
|
.expect("describe");
|
||||||
assert_eq!(desc.columns[0].user_type, Some(Type::Int));
|
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
|
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");
|
.expect("query");
|
||||||
let id_idx = data
|
let id_idx = data
|
||||||
.columns
|
.columns
|
||||||
@@ -220,7 +220,7 @@ fn table_without_primary_key_is_allowed() {
|
|||||||
))
|
))
|
||||||
.expect("insert into PK-less table");
|
.expect("insert into PK-less table");
|
||||||
let data = r
|
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");
|
.expect("query");
|
||||||
assert_eq!(data.rows.len(), 1);
|
assert_eq!(data.rows.len(), 1);
|
||||||
}
|
}
|
||||||
@@ -299,7 +299,7 @@ fn default_is_applied_when_column_omitted() {
|
|||||||
))
|
))
|
||||||
.expect("insert");
|
.expect("insert");
|
||||||
let data = r
|
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");
|
.expect("query");
|
||||||
let n_idx = data.columns.iter().position(|c| c == "n").expect("n column");
|
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");
|
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.
|
// A valid row inserts; DEFAULT n=7 survived.
|
||||||
r.block_on(ins("1", "1", "5")).expect("valid row");
|
r.block_on(ins("1", "1", "5")).expect("valid row");
|
||||||
let data = r
|
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");
|
.expect("query");
|
||||||
let n_idx = data.columns.iter().position(|c| c == "n").expect("n column");
|
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");
|
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`.
|
/// Sorted `id` column values of table `T`.
|
||||||
fn ids(db: &Database, r: &tokio::runtime::Runtime) -> Vec<Option<String>> {
|
fn ids(db: &Database, r: &tokio::runtime::Runtime) -> Vec<Option<String>> {
|
||||||
let d = r
|
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");
|
.expect("query");
|
||||||
let idx = d.columns.iter().position(|c| c == "id").expect("id column");
|
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();
|
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) ...
|
// The table is intact: both columns survive (rollback) ...
|
||||||
let desc = r
|
let desc = r
|
||||||
.block_on(db.describe_table("T".to_string(), None))
|
.block_on(db.describe_table("T".to_string()))
|
||||||
.expect("describe still works");
|
.expect("describe still works");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
desc.columns.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
|
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");
|
.expect("create child with FK");
|
||||||
|
|
||||||
// The child has an outbound relationship; the parent an inbound one.
|
// 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");
|
assert_eq!(child.outbound_relationships.len(), 1, "child references parent");
|
||||||
let rel = &child.outbound_relationships[0];
|
let rel = &child.outbound_relationships[0];
|
||||||
assert_eq!(rel.name, "parent_id_to_child_pid", "auto-named per ADR-0013");
|
assert_eq!(rel.name, "parent_id_to_child_pid", "auto-named per ADR-0013");
|
||||||
assert_eq!(rel.other_table, "parent");
|
assert_eq!(rel.other_table, "parent");
|
||||||
assert_eq!(rel.local_columns, vec!["pid".to_string()]);
|
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");
|
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()),
|
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");
|
.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");
|
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()),
|
Some("create table child (id serial primary key, pid int references parent)".to_string()),
|
||||||
))
|
))
|
||||||
.expect("create child with bare REFERENCES");
|
.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");
|
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.
|
// parent (now un-referenced) can be described without a dangling rel.
|
||||||
r.block_on(db.undo()).expect("undo").expect("a step was undone");
|
r.block_on(db.undo()).expect("undo").expect("a step was undone");
|
||||||
assert!(!r.block_on(db.list_tables()).unwrap().contains(&"child".to_string()));
|
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");
|
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");
|
.expect("delete parent");
|
||||||
let child_rows = r
|
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");
|
.expect("query child");
|
||||||
assert!(child_rows.rows.is_empty(), "ON DELETE CASCADE removed the child row");
|
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");
|
.expect("add column via rebuild");
|
||||||
|
|
||||||
// The relationship still exists after the 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");
|
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).
|
// And the engine still enforces it (now and after a fresh rebuild).
|
||||||
insert_parent_row(&db, &r);
|
insert_parent_row(&db, &r);
|
||||||
@@ -1275,7 +1275,7 @@ fn fk_referential_actions_survive_rebuild() {
|
|||||||
))
|
))
|
||||||
.expect("create");
|
.expect("create");
|
||||||
r.block_on(db.rebuild_from_text(p.path().to_path_buf(), None)).expect("rebuild");
|
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];
|
let rel = &child.outbound_relationships[0];
|
||||||
assert_eq!(rel.on_delete, ReferentialAction::Cascade, "ON DELETE survived rebuild");
|
assert_eq!(rel.on_delete, ReferentialAction::Cascade, "ON DELETE survived rebuild");
|
||||||
assert_eq!(rel.on_update, ReferentialAction::SetNull, "ON UPDATE 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");
|
.expect("create");
|
||||||
r.block_on(db.drop_table("child".to_string(), Some("drop table child".to_string())))
|
r.block_on(db.drop_table("child".to_string(), Some("drop table child".to_string())))
|
||||||
.expect("drop child");
|
.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");
|
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()),
|
Some("create table emp (id int primary key, mgr int references emp)".to_string()),
|
||||||
))
|
))
|
||||||
.expect("create self-referential emp with a bare reference");
|
.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");
|
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.
|
// Enforced: a non-existent manager is rejected.
|
||||||
r.block_on(db.insert(
|
r.block_on(db.insert(
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ fn delete_without_where_runs_across_all_rows() {
|
|||||||
let csv = read_csv(&project, "t").unwrap_or_default();
|
let csv = read_csv(&project, "t").unwrap_or_default();
|
||||||
assert!(!csv.contains('a') && !csv.contains('b') && !csv.contains('c'), "no rows left: {csv:?}");
|
assert!(!csv.contains('a') && !csv.contains('b') && !csv.contains('c'), "no rows left: {csv:?}");
|
||||||
let remaining = rt
|
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");
|
.expect("query t");
|
||||||
assert!(remaining.rows.is_empty(), "table empty after unfiltered delete");
|
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("Orders"), Some(&2), "two orders cascaded");
|
||||||
assert_eq!(by_child.get("Reviews"), Some(&1), "one review cascaded");
|
assert_eq!(by_child.get("Reviews"), Some(&1), "one review cascaded");
|
||||||
// Both child CSVs re-persisted to the post-cascade (empty) state.
|
// 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 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, 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");
|
assert!(orders.rows.is_empty() && reviews.rows.is_empty(), "both children emptied");
|
||||||
let _ = &project;
|
let _ = &project;
|
||||||
}
|
}
|
||||||
@@ -361,7 +361,7 @@ fn delete_violating_fk_fails_and_persists_nothing() {
|
|||||||
let result = run_delete(&db, &rt, input);
|
let result = run_delete(&db, &rt, input);
|
||||||
assert!(result.is_err(), "delete of a referenced parent must be rejected");
|
assert!(result.is_err(), "delete of a referenced parent must be rejected");
|
||||||
// Rolled back: Alice survives.
|
// 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");
|
assert_eq!(customers.rows.len(), 1, "parent row preserved after rejected delete");
|
||||||
// No history line for the failed statement (written only on success).
|
// 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();
|
let history = std::fs::read_to_string(project.path().join("history.log")).unwrap_or_default();
|
||||||
|
|||||||
@@ -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>>> {
|
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:?}"))
|
.unwrap_or_else(|e| panic!("query_data {table}: {e:?}"))
|
||||||
.rows
|
.rows
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
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")
|
.expect("describe")
|
||||||
.indexes
|
.indexes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -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.undo()).expect("undo").is_some(), "the drop was one undo step");
|
||||||
assert!(r.block_on(db.list_tables()).unwrap().contains(&"T".to_string()));
|
assert!(r.block_on(db.list_tables()).unwrap().contains(&"T".to_string()));
|
||||||
let data = r
|
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");
|
.expect("query");
|
||||||
assert_eq!(data.rows.len(), 1, "the dropped row was restored by undo");
|
assert_eq!(data.rows.len(), 1, "the dropped row was restored by undo");
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-16
@@ -215,7 +215,7 @@ fn decimal_aggregation_display_trims_ieee754_noise() {
|
|||||||
|
|
||||||
// The reported case: the aggregate no longer leaks float noise.
|
// The reported case: the aggregate no longer leaks float noise.
|
||||||
let agg = rt
|
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");
|
.expect("aggregate select");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
agg.rows[0][0].as_deref(),
|
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
|
// Raw decimal column is still exact — TEXT storage preserves
|
||||||
// the input string verbatim, including the trailing zero.
|
// the input string verbatim, including the trailing zero.
|
||||||
let raw = rt
|
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");
|
.expect("raw decimal select");
|
||||||
let prices: Vec<&str> = raw.rows.iter().map(|r| r[0].as_deref().unwrap()).collect();
|
let prices: Vec<&str> = raw.rows.iter().map(|r| r[0].as_deref().unwrap()).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -240,10 +240,7 @@ fn decimal_aggregation_display_trims_ieee754_noise() {
|
|||||||
fn database_run_select_constant_returns_a_single_row() {
|
fn database_run_select_constant_returns_a_single_row() {
|
||||||
let (_p, db, _dir) = open_project_db();
|
let (_p, db, _dir) = open_project_db();
|
||||||
let data = rt()
|
let data = rt()
|
||||||
.block_on(db.run_select(
|
.block_on(db.run_select("select 1".to_string()))
|
||||||
"select 1".to_string(),
|
|
||||||
Some("select 1".to_string()),
|
|
||||||
))
|
|
||||||
.expect("`select 1` runs clean");
|
.expect("`select 1` runs clean");
|
||||||
assert_eq!(data.rows.len(), 1, "one result row");
|
assert_eq!(data.rows.len(), 1, "one result row");
|
||||||
assert_eq!(data.rows[0].len(), 1, "one column");
|
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");
|
.expect("insert row");
|
||||||
});
|
});
|
||||||
let data = rt
|
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");
|
.expect("SELECT runs");
|
||||||
assert_eq!(data.rows.len(), 1);
|
assert_eq!(data.rows.len(), 1);
|
||||||
assert_eq!(data.rows[0][0].as_deref(), Some("Ada"));
|
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");
|
.expect("insert row");
|
||||||
});
|
});
|
||||||
let data = rt
|
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");
|
.expect("SELECT runs");
|
||||||
assert_eq!(data.rows.len(), 2);
|
assert_eq!(data.rows.len(), 2);
|
||||||
assert_eq!(data.column_types, vec![Some(Type::Bool)]);
|
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.
|
// playground type is recovered.
|
||||||
let data = rt
|
let data = rt
|
||||||
.block_on(
|
.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");
|
.expect("SELECT runs");
|
||||||
assert_eq!(data.columns, vec!["n".to_string()]);
|
assert_eq!(data.columns, vec!["n".to_string()]);
|
||||||
@@ -402,7 +399,7 @@ fn database_run_select_computed_expression_stays_typeless() {
|
|||||||
.expect("insert");
|
.expect("insert");
|
||||||
});
|
});
|
||||||
let data = rt
|
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");
|
.expect("SELECT runs");
|
||||||
assert_eq!(data.column_types, vec![None]);
|
assert_eq!(data.column_types, vec![None]);
|
||||||
}
|
}
|
||||||
@@ -439,7 +436,6 @@ fn engine_aggregate_in_where_routes_through_catalog() {
|
|||||||
let err = rt
|
let err = rt
|
||||||
.block_on(db.run_select(
|
.block_on(db.run_select(
|
||||||
"select id from T where count(score) > 0".to_string(),
|
"select id from T where count(score) > 0".to_string(),
|
||||||
None,
|
|
||||||
))
|
))
|
||||||
.expect_err("engine should reject aggregate in WHERE");
|
.expect_err("engine should reject aggregate in WHERE");
|
||||||
let DbError::Sqlite { .. } = &err else {
|
let DbError::Sqlite { .. } = &err else {
|
||||||
@@ -512,7 +508,6 @@ fn engine_group_by_missing_routes_through_catalog() {
|
|||||||
let _ = rt
|
let _ = rt
|
||||||
.block_on(db.run_select(
|
.block_on(db.run_select(
|
||||||
"select category, count(*) from T group by category".to_string(),
|
"select category, count(*) from T group by category".to_string(),
|
||||||
None,
|
|
||||||
))
|
))
|
||||||
.expect("benign GROUP BY query runs");
|
.expect("benign GROUP BY query runs");
|
||||||
// Direct unit test on the matcher: ensure a message that
|
// 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
|
let _ = rt
|
||||||
.block_on(db.run_select(
|
.block_on(db.run_select(
|
||||||
"select (select v from T) from T".to_string(),
|
"select (select v from T) from T".to_string(),
|
||||||
None,
|
|
||||||
))
|
))
|
||||||
.expect("benign scalar subquery query runs");
|
.expect("benign scalar subquery query runs");
|
||||||
let synthetic = DbError::Sqlite {
|
let synthetic = DbError::Sqlite {
|
||||||
@@ -624,13 +618,13 @@ fn database_run_select_type_recovery_works_on_empty_table() {
|
|||||||
});
|
});
|
||||||
// No INSERT — the table is empty.
|
// No INSERT — the table is empty.
|
||||||
let data_text = rt
|
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");
|
.expect("SELECT runs even on empty table");
|
||||||
assert!(data_text.rows.is_empty());
|
assert!(data_text.rows.is_empty());
|
||||||
assert_eq!(data_text.column_types, vec![Some(Type::Text)]);
|
assert_eq!(data_text.column_types, vec![Some(Type::Text)]);
|
||||||
|
|
||||||
let data_blob = rt
|
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");
|
.expect("SELECT runs even on empty table");
|
||||||
assert!(data_blob.rows.is_empty());
|
assert!(data_blob.rows.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -723,7 +717,7 @@ fn database_run_select_recovers_all_ten_playground_types() {
|
|||||||
for (col, expected_type) in cases {
|
for (col, expected_type) in cases {
|
||||||
let sql = format!("select {col} from AllTypes");
|
let sql = format!("select {col} from AllTypes");
|
||||||
let data = rt
|
let data = rt
|
||||||
.block_on(db.run_select(sql.clone(), None))
|
.block_on(db.run_select(sql.clone()))
|
||||||
.expect("SELECT runs");
|
.expect("SELECT runs");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
data.column_types,
|
data.column_types,
|
||||||
|
|||||||
@@ -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:?}"
|
"the --all-rows update replays through the DSL fall-back; events: {events:?}"
|
||||||
);
|
);
|
||||||
let rows = rt
|
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")
|
.expect("query")
|
||||||
.rows;
|
.rows;
|
||||||
assert_eq!(rows.len(), 2, "both rows present");
|
assert_eq!(rows.len(), 2, "both rows present");
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ async fn insert_named(db: &Database, name: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn row_count(db: &Database) -> usize {
|
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
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.rows
|
.rows
|
||||||
@@ -306,7 +306,7 @@ async fn sql_delete(db: &Database, input: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn count_t(db: &Database) -> usize {
|
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
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.rows
|
.rows
|
||||||
@@ -378,7 +378,7 @@ fn undo_restores_db_and_csv_consistently() {
|
|||||||
// Both the database read model and the on-disk CSV are
|
// Both the database read model and the on-disk CSV are
|
||||||
// restored — the (db, csv) pair stays consistent.
|
// restored — the (db, csv) pair stays consistent.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.query_data("T".to_string(), None, None, None)
|
db.query_data("T".to_string(), None, None)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.rows
|
.rows
|
||||||
|
|||||||
Reference in New Issue
Block a user