From 22e5bf5d6ac9a36e8c3826779b1e8c53403d9efc Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Tue, 26 May 2026 14:38:28 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20ADR-0035=204i(a,b)=20=E2=80=94=20CREATE?= =?UTF-8?q?=20TABLE=20help/usage=20+=20describe=20table=20constraints;=20P?= =?UTF-8?q?hase=204=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (b) describe shows table-level constraints: TableDescription gains unique_constraints + check_constraints (populated by do_describe_table from read_schema), rendered in a new "Table constraints:" section — composite UNIQUE and table-level CHECK (named + unnamed). The per-column Constraints column already covered single-column NOT NULL/UNIQUE/PK/CHECK. (a) CREATE TABLE help/usage skeleton refreshed for the column DEFAULT/ CHECK/REFERENCES, table-level composite UNIQUE, table CHECK, and table-level FOREIGN KEY forms (4a.2/4a.3/4b) — engine-neutral, vocab-audit clean. With 4i's (c)/(d)/(e) already shipped, this completes sub-phase 4i — the verification sweep — and therefore ADR-0035 Phase 4 (4a–4i). ADR-0035 Status, §13 4i, the ADR index, and requirements.md Q1 updated to "Phase 4 complete". Tests: render_structure table-level-constraints unit test + e2e_describe_shows_table_level_constraints. Full suite 1917 passing / 0 failing / 1 ignored; clippy clean. --- docs/adr/0035-advanced-mode-sql-ddl.md | 80 +++++++++++++------------- docs/adr/README.md | 2 +- docs/requirements.md | 6 +- src/app.rs | 2 + src/db.rs | 9 +++ src/friendly/strings/en-US.yaml | 9 ++- src/output_render.rs | 62 ++++++++++++++++++++ src/ui.rs | 2 + tests/sql_alter_table.rs | 44 ++++++++++++++ tests/walking_skeleton.rs | 6 ++ 10 files changed, 176 insertions(+), 46 deletions(-) diff --git a/docs/adr/0035-advanced-mode-sql-ddl.md b/docs/adr/0035-advanced-mode-sql-ddl.md index 0f125b5..e044eea 100644 --- a/docs/adr/0035-advanced-mode-sql-ddl.md +++ b/docs/adr/0035-advanced-mode-sql-ddl.md @@ -4,17 +4,20 @@ Accepted. Design agreed with the user (2026-05-24); the approach is **validated end-to-end by sub-phases 4a / 4a.2 / 4a.3 / 4b / 4c / 4d / -4e / 4f / 4g / 4h** (`CREATE TABLE` with column- and table-level +4e / 4f / 4g / 4h / 4i** (`CREATE TABLE` with column- and table-level constraints and foreign keys, `DROP TABLE [IF EXISTS]`, `CREATE [UNIQUE] INDEX` / `DROP INDEX [IF EXISTS]`, `ALTER TABLE` add/drop/rename column, `ALTER TABLE … ALTER COLUMN TYPE`, `ALTER TABLE` -add/drop constraint + add foreign key, and `ALTER TABLE … RENAME TO`, -implemented 2026-05-25/26 — plans +add/drop constraint + add foreign key, `ALTER TABLE … RENAME TO`, and the +4i verification sweep — completion merge, simple/advanced completion +colour, describe of table-level constraints, self-ref FK indicator, and +the CREATE-TABLE help/usage refresh — implemented 2026-05-25/26 — plans `docs/plans/20260524-adr-0035-sql-ddl-4a.md`, `…-4a2.md`, `…-4a3.md`, `docs/plans/20260525-adr-0035-sql-ddl-4b.md`, `…-4c.md`, `…-4d.md`, `…-4e.md`, `…-4f.md`, `…-4g.md`, -`docs/plans/20260526-adr-0035-sql-ddl-4h.md`), so the decision is accepted -while the remaining sub-phase (**4i**, §13) continues. This is **Phase 4** of the ADR-0030 roadmap (the +`docs/plans/20260526-adr-0035-sql-ddl-4h.md`, +`docs/plans/20260526-adr-0035-sql-ddl-4i.md`). **Phase 4 is complete** +(4a–4i all shipped). This is **Phase 4** of the ADR-0030 roadmap (the advanced-mode SQL surface), the peer of ADR-0031 (expression grammar), ADR-0032 (`SELECT`), and ADR-0033 (DML). It **clarifies ADR-0030 §4** on how DDL is represented and executed. @@ -515,41 +518,38 @@ ADR-0033's structure: update; ADR-0035 §6 scope — user-confirmed; documented collision caveat). One undo step (the whole-project snapshot). Advanced-mode only; closes the rename half of `C1`. -- **4i — Verification sweep.** Typing-surface + matrix coverage, - engine-neutral error pass, undo-parity check (one step per - statement), `help`/usage for the new forms. **Carried in from earlier - slices:** (a) refresh the `CREATE TABLE` help/usage skeleton for the - 4a.2 `DEFAULT`/`CHECK`/composite-`UNIQUE`, 4a.3 table-`CHECK`, and 4b - FK forms (deferred from each) — **4d's index forms already carry their - own help/usage** (`ddl.sql_create_index` / `ddl.sql_drop_index` + the - `parse.usage.*` keys), since the nodes are new; (b) `describe` display - of table-level constraints (composite `UNIQUE` + table `CHECK`) — note - the **unique-*index* marker shipped in 4d** (`[unique]` in the - structure view + items panel), so only the table-level *constraint* - display remains here; (c) **4b self-ref - FK indicator** — a `CREATE TABLE` with a self-referencing FK - (`references `) parses + executes correctly, but the pre-submit - schema-existence diagnostic falsely flags the not-yet-created self - table as unknown (the FK parent slot is `IdentSource::Tables`). Make - the diagnostic treat a FK parent equal to the `CREATE TABLE` target as - valid, so the indicator stops lying for self-references. (d) **4c - shared-entry-word completion merge** — in advanced mode a shared entry - word surfaces only the SQL node's continuations, so `drop ` offers - only `table` (not the DSL `column`/`relationship`/`index`/`constraint`) - and a partial keyword like `drop rel` returns an *empty* list (a - mid-word dead end), even though the DSL drops still parse + execute via - fallback. Merge the expected sets of all candidate nodes for a shared - entry word so advanced completion offers every valid continuation - (`drop ` → table + column + relationship + index + constraint; `drop - rel` → relationship); verify `create`/`insert`/`update`/`delete` - completion stays sensible. **4d widened this:** `create` and `drop` - now each have *two* advanced nodes (table + index), so a shared entry - word's continuations now span two SQL shapes as well as the DSL ones — - the merge matters more. (e) **Discussion flag (user, 2026-05-25):** - before/with (d), discuss **visually distinguishing simple- vs - advanced-mode completions in the hint UI (likely by colour)** so a - learner can see which continuations are DSL and which are SQL — a UX - design conversation, not just the mechanical merge. +- **4i — Verification sweep (completes Phase 4).** *(Implemented + 2026-05-26 — plan `docs/plans/20260526-adr-0035-sql-ddl-4i.md`.)* + - **(a) `CREATE TABLE` help/usage skeleton** refreshed for the 4a.2 + `DEFAULT`/`CHECK`/composite-`UNIQUE`, 4a.3 table-`CHECK`, and 4b FK + forms (the index forms already carried their own since 4d). + - **(b) `describe` of table-level constraints** — `TableDescription` + gained `unique_constraints` + `check_constraints`, rendered in a + "Table constraints:" section (composite `UNIQUE`, table `CHECK` + incl. **named** CHECKs). The per-column `[unique]`-index marker + shipped in 4d. + - **(c) self-ref FK indicator** — `schema_existence_diagnostics` + collects the `CREATE TABLE` target(s) (`IdentSource::NewName`, role + `table_name`) and exempts a `Tables` reference matching one from the + unknown-table flag, so a self-referencing FK no longer pre-flags the + not-yet-created table; a FK to a genuinely-unknown *other* table + still flags. + - **(d) shared-entry-word completion merge** — at the advanced-mode + entry-word boundary, `completion_probe_in_mode` walks every candidate + node and unions the viable (`Incomplete`) ones' continuations, so + `drop ` offers `table·index·column·relationship·constraint` and `drop + rel` → `relationship` (was an empty dead-end). Completion-only (the + parse path is untouched); deeper positions keep the committed walk. + - **(e) simple-vs-advanced completion colour** — each continuation is + classified `Both`/`Advanced`/`Simple` and, **only when the candidate + list mixes modes**, coloured (`mode_advanced`/`mode_simple`, `Both` = + token-kind) and block-ordered `Both → Advanced → Simple` (user-chosen + design, 2026-05-26). Single-mode lists keep the token-kind colours. + - **Staples:** matrix/typing-surface coverage extended (completion + + describe tests); engine-neutral wording held (the vocab audit covers + the new strings); undo-parity is **N/A for 4i** — every change is + read-side (completion / diagnostics / describe / help), so no undo + steps are introduced. ## Consequences diff --git a/docs/adr/README.md b/docs/adr/README.md index 72f4cf4..506881a 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -40,4 +40,4 @@ This directory contains the project's ADRs, recorded per - [ADR-0032 — The full SQL `SELECT` grammar](0032-sql-select-grammar.md) — **Accepted**, the Phase-2 grammar commissioned by ADR-0030 §3: full `SELECT` with `INNER`/`LEFT`/`RIGHT`/`FULL OUTER`/`CROSS` joins, `GROUP BY`/`HAVING`, all four set ops (`UNION`/`UNION ALL`/`INTERSECT`/`EXCEPT`), `WITH` and `WITH RECURSIVE` CTEs, `LIMIT … OFFSET`, `DISTINCT`, `t.*`, and bare-alias projection (lifting Phase-1 §4.2); additive extensions to ADR-0031's `sql_expr` for scalar subqueries, `IN (SELECT …)`, `[NOT] EXISTS`, and qualified column refs (redeeming ADR-0031 §7 OOS-1/OOS-2); grammar-recursion via `Subgrammar(&SQL_SELECT_COMPOUND)` reuses ADR-0026's `MAX_SUBGRAMMAR_DEPTH = 64` cap unchanged; **softens ADR-0030 §8's "ambient assistance comes for free" claim**: completion scope needs new `WalkContext` accumulators (a `from_scope_stack` of `ScopeFrame`s holding `from_scope` / `cte_bindings` / `projection_aliases`), a **new walker node variant `Node::ScopedSubgrammar(&Node)`** as the push/pop trigger (existing `Node::Subgrammar` unchanged so DSL `Expr` and `sql_expr` recursion are unaffected), qualified-prefix completion narrowing, body-projection-derived CTE column resolution (so `SELECT *` and explicit-projection CTE bodies both yield real column completion past `cte_alias.|`), and a **post-walk fixup pass** that re-resolves projection-list identifier highlighting/validity once `FROM` is parsed (the projection-before-FROM problem); classifies every Phase-2 validation case against ADR-0027's ERROR/WARNING guideline (§11): five new `diagnostic.*` keys for parse-time-detectable cases (unknown qualifier, ambiguous column, projection-alias misplaced, CTE/compound arity mismatch) plus eight `engine.*` translation keys; a MatchedPath-walking predicate-warnings variant that closes the Phase-1 gap where SQL `WHERE` expressions emitted no `LIKE`-on-numeric / `= NULL` / type-mismatch warnings (ADR-0027 Amendment 1 finally extends to the SQL surface); adds a worker-side post-prepare type-resolution pass via engine column-origin metadata so bare column refs recover their playground type (partially lifting Phase-1 §4.5, the bool→0/1 case) — `Cargo.toml` gains `column_metadata` to rusqlite features (verified against pinned 0.39.0); `__rdbms_*` rejection extended to every new table-source slot; Amendment 1 narrows §12's resolution rule from a grammar-side structural classification to "trust the engine's column-origin metadata verbatim" after an empirical probe showed origin metadata follows through non-recursive CTEs, scalar subqueries, derived tables, set ops, and joins — the one structural exception is recursive CTE result columns, which return None and stay typeless; Amendment 2 records that §10.6's "rewrite the highlight class" prescription is realised via the two-pass schema-existence diagnostic + the renderer's diagnostic-overlay path (no separate per-byte rewrite step needed; no new HighlightClass variant), and that the projection-before-FROM completion narrowing has been improved by an `src/completion.rs` look-ahead probe when the leading walk's `from_scope` is empty but the full input parses - [ADR-0033 — The full SQL DML grammar (`INSERT` / `UPDATE` / `DELETE`)](0033-sql-dml-grammar.md) — **Accepted** (implemented + verified through sub-phase 3k, 2026-05-23; phase-exit report `docs/handoff/20260523-phase-3-verification.md`), the Phase-3 grammar commissioned by ADR-0030 §3: single- and multi-row `INSERT` (incl. `INSERT … SELECT` recursing through ADR-0032's `SQL_SELECT_COMPOUND`), `UPDATE` with `SET` assignment list, `DELETE`, all three optionally followed by `RETURNING projection_list`, plus full `ON CONFLICT … DO NOTHING / DO UPDATE` UPSERT on INSERT; **fixes the DSL-vs-SQL dispatch architecture for shared entry words (`insert`/`update`/`delete`)**: SQL-first / DSL-fallback in Advanced mode via a `Choice(SQL_shape, DSL_shape)` per shape, gated by a new walker capability `Node::Guard(fn)` — a zero-byte-consumption gating node that fails the enclosing Seq with a `ValidationError`; carries `Command::SqlInsert` / `SqlUpdate` / `SqlDelete` variants and `do_sql_*` worker handlers each of which knows the target table (for re-persistence) and the `returning: bool` flag (for DataResult routing); `shortid` auto-fill mirrors the DSL `do_insert` mechanism via worker post-fill; SQL DELETE produces the same per-relationship cascade summary the DSL DELETE does (ADR-0014 parity); three new walker diagnostics (`insert_arity_mismatch` ERROR, `auto_column_overridden` WARNING, `not_null_missing` WARNING) with positive + negative tests each; OOS list explicitly carves out `DEFAULT VALUES` (the project's planned seed feature), SQLite-specific `OR REPLACE` / `OR IGNORE` / `OR ABORT` / `OR FAIL` / `OR ROLLBACK` prefixes, `UPDATE FROM` multi-table updates, and WITH-prefixed DML; the `excluded` keyword inside `ON CONFLICT DO UPDATE` is a deliberate carve-out from ADR-0030 §7's engine-neutral posture (no standard-SQL UPSERT spelling exists that SQLite and PostgreSQL share); eleven phased sub-phases each with explicit exit gates + written DA gate, opening with the dispatch mechanism before any DML grammar lands; initial DA review recorded seven critiques that were resolved before status moved to Proposed; **Amendment 1 supersedes §2's dispatch mechanism**: the originally-chosen `Node::Guard(fn)` + `Choice(SQL_shape, DSL_shape)` was found during 3a to be unworkable as framed (any guard-in-`Choice` mechanism forces a `walk_choice` change — `walk_choice` only falls through on `NoMatch`, so Simple-mode valid-DSL would wrongly surface "this is SQL", and `walk_seq` treats a `NoMatch` past `idx 0` as a hard `Failed`, breaking Advanced-mode DSL fall-through); replaced by **category-grouped, mode-aware dispatch** in `walker::walk` (each `REGISTRY` entry tagged `CommandCategory::{Simple, Advanced}`, generalising the existing whole-command `is_advanced_only` gate), shared entry words carrying a node in both groups, no `Node::Guard` and no `walk_choice`/`walk_seq` change, advanced-mode completion SQL-first with DSL as a full-line fallback; **Amendment 2 (sub-phase 3f) supersedes §7's cascade mechanism**: the WHERE-injected per-child pre-count rested on a premise that was factually wrong about the DSL handler (which detects cascades by before/after row-count diffing inside a transaction, not by `Expr`-derived pre-count subqueries) and would have broken the §2 parity promise by reporting `SET NULL` the DSL path doesn't; replaced by mirroring `do_delete`'s count-diff exactly (verbatim DELETE executes, child-count diff observes the cascade — `ON DELETE CASCADE` row removals only, SET NULL deferred for both paths to preserve parity), which shares the render-layer formatter for free via `CommandOutcome::Delete` and **withdraws risk R2** (no WHERE-byte extraction, no N+1 subquery); **Amendment 3 (sub-phase 3j) records the command-identity model and defers the execution-mode side-channel**: a command is the typed outcome of a *mode-rooted* grammar path and its identity is intrinsic (Advanced mode tries SQL first, falls back to the *Simple* DSL command when no SQL branch matches a token, e.g. `delete … --all-rows`; note `update … --all-rows` does *not* fall back — the SQL `SET` expression eats `--all-rows`, harmless since the engine treats it as a comment); **Simple mode commits the DSL candidate for shared words** so the *real* DSL error surfaces, and when that line would also run in advanced mode the rendering layer **combines** them — DSL error **plus** an `advanced_mode.also_valid_sql` pointer ("… (valid as SQL in advanced mode)") — keeping the actionable DSL fix while pointing at advanced mode; bare "this is SQL" is reserved for entry words with no DSL form (`select`/`with`); a fully-overlapping input (`insert … values …`) legitimately yields *two distinct commands* (`Command::Insert` typed-AST vs `Command::SqlInsert` validated-text) that do the same thing but execute differently (ADR-0030 §4), so each is tested in the mode that produces it; **corrects the plan's 3j exit-gate premise** that the DSL DML tests run in Simple mode (they call `parse_command`, which defaults to Advanced) — the real invariant is "Simple-mode behaviour unchanged, Advanced mode SQL-first, DSL grammar tested in Simple mode, both variants tested in their producing mode", with §6/§7 parity keeping the paths observably equivalent; and **defers to its own future ADR** the execution-time mode side-channel (three-way `Mode`: simple/advanced/advanced-one-shot threaded through `Action`→worker, for mode-dependent *output* like echoing generated SQL) — today only the *rendering* side-channel `OutputLine.mode_at_submission` exists, and the three-way distinction is not required for Phase 3 dispatch correctness - [ADR-0034 — `history.log` as a complete command journal; replay reads success-only](0034-history-journal-and-replay-filter.md) — **Accepted**, resolves a three-way tension in `history.log`'s roles found while implementing ADR-0033 3f: (1) the persistent log is success-only while the in-memory Up/Down recall ring records *every* submission (success or failure, "so users can recall and edit typo'd commands"), and the ring is re-seeded from the log on project open — so **failed commands are recallable within a session but silently lost across sessions**; (2) replay wants the state-building (successful) commands while recall wants everything typed, which one success-only file cannot serve; (3) `replay history.log` never actually worked — `run_replay` parses each whole line through the DSL parser with no understanding of the `||` record shape, so a real log fails on line 1, and **no test ever fed the pipe format to replay** (the `replay_history_log_records_subcommands_only` test only checks what replay *writes*, never replays the log as input). Decision: `history.log` becomes a **complete journal** — every submission recorded, tagged `ok`/`err` via the status field the format already reserved (ADR-0015 §5) — and **each consumer filters**: hydration reads all records (cross-session recall matches in-session), replay reads `ok` only (and learns the journal format, while still accepting bare-command `.commands` scripts; detection by the leading timestamp+status prefix so a `|` inside a bare command isn't misread). Successful commands stay journalled transactionally by the worker; failed commands are journalled `err` best-effort from the runtime/app error path (a parse failure never reaches the worker). Amends ADR-0006's "successfully executed" wording and ADR-0015 §5 ("status always `ok`") / §12 (hydration). Code deferred to two tracked test-first sub-tasks (journal-failures+filtering; replay-parses-journal-format); existing all-`ok` logs need no migration; **implemented 2026-05-24** (plan `docs/plans/20260524-adr-0034-history-journal.md`); **Amendment 1 (2026-05-24): replay filters out app-lifecycle commands** — a working `replay history.log` (the §3 fix) exposed that the journal also records `save as`/`load`/`new`/`export`/`import`/`rebuild`/`mode` (which would panic the worker dispatch or abort the replay), so replay now re-applies **only** schema/data write commands and **skips** every `Command::App` + nested `Command::Replay`; **all skips continue** (never abort — reversing the prior nested-`replay` refusal, so a journal containing a once-run `replay` needs no hand-editing, and the infinite-loop footgun is closed by construction), with a `[skip]` **warning** on `import` and nested-`replay` skips (their omission can leave replayed state incomplete) and silent skips for the rest; `replay.error_nested` removed, `replay.skipped_import`/`replay.skipped_replay` added, `ReplayCompleted` carries `warnings` -- [ADR-0035 — Advanced-mode SQL DDL](0035-advanced-mode-sql-ddl.md) — **Accepted** (design agreed 2026-05-24; validated end-to-end by sub-phases 4a/4a.2/4a.3/4b `CREATE TABLE` (incl. foreign keys) + 4c `DROP TABLE [IF EXISTS]` + 4d `CREATE [UNIQUE] INDEX` / `DROP INDEX [IF EXISTS]` + 4e `ALTER TABLE` add/drop/rename column + 4f `ALTER TABLE … ALTER COLUMN TYPE` + 4g `ALTER TABLE` add/drop constraint + add FK + 4h `ALTER TABLE … RENAME TO`, implemented 2026-05-25/26; 4i pending), **Phase 4** of the ADR-0030 roadmap (peer of 0031/0032/0033) and **clarifies ADR-0030 §4**. Advanced-mode `CREATE`/`DROP`/`ALTER TABLE` + `CREATE`/`DROP INDEX` get their **own per-statement commands** (`SqlCreateTable`/`SqlAlterTable`/`SqlDropTable`/`SqlCreateIndex`/`SqlDropIndex`), like DML's `Sql*` set — but unlike DML they **execute *structurally*, not verbatim** (raw execution would lose the playground's types, named relationships, and `STRICT`; "verbatim" was a DML convenience, not a rule). Handlers **reuse the low-level schema/metadata helpers** where the operation matches simple mode and **stand alone where the SQL surface is richer** (clarity over forced refactoring); simple mode is untouched (additive). Dispatch: `create`/`drop` reuse ADR-0033 Amendment 1's category-grouped mode-aware dispatch (SQL-first, simple fallback); `alter` is a new advanced-only entry word. Full surface (no pre-emptive cuts, `Q4`): `CREATE TABLE` with column + table constraints, single/compound `PRIMARY KEY`, inline + table-level `FOREIGN KEY` → **named relationships** (one statement = one command = **one undo step**, ADR-0006); `ALTER TABLE` add/drop/rename column, `ALTER COLUMN TYPE`, add/drop constraint, add FK, **`RENAME TO`** (advanced-only table rename — new low-level op renaming the table + its CSV + the relationship and table-CHECK metadata, closing the rename half of `C1`); `CREATE [UNIQUE] INDEX` / `DROP INDEX`. Type slot accepts the ten playground keywords **and** standard-SQL aliases (`integer`→`int`, `varchar`→`text`, `timestamp`→`datetime`, …; length args accepted-and-ignored; no engine type names in/out — ADR-0030 §5). `CHECK`/`DEFAULT` reuse ADR-0031 `sql_expr`. **Pre-implementation `/runda` refinements (2026-05-24, user-confirmed):** `CREATE TABLE`/`DROP TABLE` **admit `IF [NOT] EXISTS`** (no-op-that-succeeds-with-a-note — a near-universal cross-vendor idiom, reclassified *into* scope, not engine-specific); `INTEGER PRIMARY KEY` maps to a **plain `int`** PK, *not* auto-increment (`serial` stays the sole auto-increment type). **Column-type-conversion is unified** (ADR-0017 engine, mode-appropriate policy): clean auto-converts and incompatible/own-type-static cases refuse in both modes, but a **lossy** change refuses-by-default in simple mode (`--force-conversion` opts in) while advanced mode **performs it with a loss note** and relies on **`undo` as the safety net** — no force flag, no dropping to simple mode (a payoff of shipping ADR-0006 first). OOS: views/triggers/txn-control/PRAGMA/etc. (ADR-0030 §3), the Postgres `USING` clause, and the DSL→SQL teaching echo (ADR-0030 Phase 5). Sub-phases 4a–4i, plus **4a.2** (per-column `CHECK`/`DEFAULT` via raw `sql_expr` text — `sql_expr` is validate-only, no `Expr` AST — + composite `UNIQUE(a,b)`; no new internal table) and **4a.3** (table-level/multi-column `CHECK`, landed via the new `__rdbms_playground_table_checks` metadata table because SQLite has no PRAGMA for CHECK; the builder tells a table-level CHECK from a column-level one by element position) and **4b** (foreign keys — inline `REFERENCES` + table-level `FOREIGN KEY` → ADR-0013 named relationships in the create transaction, one undo step; self-references + bare `REFERENCES ` supported, user-confirmed) and **4c** (`DROP TABLE [IF EXISTS]` → `SqlDropTable`, reusing `do_drop_table`; `IF EXISTS` is a no-op-with-note via `DropOutcome::Skipped`) and **4d** (`CREATE [UNIQUE] INDEX [IF NOT EXISTS] [] ON (cols)` → `SqlCreateIndex` and `DROP INDEX [IF EXISTS] ` → `SqlDropIndex`, reusing `do_add_index`/`do_drop_index`; **`CREATE UNIQUE INDEX` admitted** — ADR-0025 **Amendment 1** — via an additive `IndexSchema.unique` flag that round-trips through `project.yaml` and rebuild, with `[unique]` markers in the structure view + items panel, while simple-mode `add unique index` stays deferred; `IF [NOT] EXISTS` reuses the 4c skip path; `create`/`drop` each gain a *second* advanced node, exercising the all-candidates dispatch) and **4e** (`ALTER TABLE` add/drop/rename column → `SqlAlterTable`; `alter` is a new advanced-**only** entry word, runtime-decomposed to the existing `do_add_column`/`do_drop_column`/`do_rename_column` — no new worker layer; `do_add_column` extended to consume raw `default_sql`/`check_sql` so ADD COLUMN reaches CREATE-TABLE constraint parity; drop/rename refuse a column any CHECK references (table-level AND column-level, incl. a column's own self-check on rename) — the 4a.3 deferral, via a raw-CHECK-text tokenizer in the shared executors, so it guards both surfaces and fixes a latent rename-drift bug; SQL DROP COLUMN refuses an index-covered column with no `--cascade` spelling; the column executors + `do_add_index` gained an internal-`__rdbms_*`-table guard — all user-confirmed) and **4f** (`ALTER TABLE … ALTER COLUMN TYPE` → a fourth `AlterTableAction`, runtime-decomposed to the existing `change_column_type` with `ChangeColumnMode::ForceConversion` — which **is** the §7 advanced policy: lossy converts *with a note* (no force flag), incompatible + ADR-0017 static refusals (`↔ blob`, same-type, `date ↔ datetime`, non-`int → serial`) still refuse, while **`int → serial` is allowed** (auto-fills nulls + UNIQUE, ADR-0018 §8 — the §7 "→serial refused" summary is looser than the code); the builder discriminates the fourth branch by the **`type` keyword** (unique — ADD COLUMN's type is an ident), the type slot reuses `SQL_TYPE`; the internal-`__rdbms_*` guard was folded into `do_change_column_type`, closing the simple `change column` exposure too — user-confirmed) and **4g** (`ALTER TABLE … ADD [CONSTRAINT ] (CHECK | UNIQUE | FOREIGN KEY)` + `DROP CONSTRAINT `; ADD = CHECK + composite UNIQUE + FK, with `ADD PRIMARY KEY` and a *named* UNIQUE refused — composite UNIQUE is anonymous in our model; each ADD reuses a low-level path (table-CHECK/UNIQUE rebuild with a dry-run guard; FK → `add_relationship`, bare `REFERENCES

` → parent single-PK), DROP CONSTRAINT resolves the name to a table-CHECK then a child-side FK; **named table-CHECKs round-trip** via a nullable `name` column on `__rdbms_playground_table_checks` (**rebuild-only** arrival — pre-4g projects gain it on `rebuild`, a named add on an un-upgraded project is refused with a friendly "rebuild first" message) *and* a `project.yaml` `check_constraints` extension to an `{expr, name}` mapping (the bare-string form still reads); the internal-`__rdbms_*` guard was folded into `do_add_constraint`/`do_add_relationship`, completing that guard class — all user-confirmed) and **4h** (`ALTER TABLE … RENAME TO` — the one genuinely new low-level op, `do_rename_table`: a native engine rename plus one-transaction reconciliation of every metadata row naming the table (`__rdbms_playground_columns`, **both ends** of `__rdbms_playground_relationships`, `__rdbms_playground_table_checks`), the CSV file (the existing rewrite+delete path — no new persistence method), and **CHECK text that qualifies a column with the old table name** (`T.age`→`U.age`, a planning-`/runda` finding — the engine rewrites the live CHECK but the stored text would drift and break a fresh rebuild; `rewrite_check_table_qualifier` keeps them in step); grammar splits the `rename` verb into one branch with an inner Choice on a distinct second keyword (`column` vs `to`), the new-name slot mirroring the `CREATE TABLE` name slot; refuses same-name / existing-target / `__rdbms_*` / non-existent, with **case-insensitive** collision checks behind an engine-neutral pre-check (a finished-slice `/runda` finding — the engine matches names case-insensitively); auto-named indexes *and* relationships keep their stale names (only table-name columns update — §6 scope); one undo step; advanced-only, closing the rename half of `C1` — all user-confirmed); the remaining DDL forms stay "not yet supported" until their sub-phases land. Each sub-phase has exit + DA gates +- [ADR-0035 — Advanced-mode SQL DDL](0035-advanced-mode-sql-ddl.md) — **Accepted** (design agreed 2026-05-24; validated end-to-end by sub-phases 4a/4a.2/4a.3/4b `CREATE TABLE` (incl. foreign keys) + 4c `DROP TABLE [IF EXISTS]` + 4d `CREATE [UNIQUE] INDEX` / `DROP INDEX [IF EXISTS]` + 4e `ALTER TABLE` add/drop/rename column + 4f `ALTER TABLE … ALTER COLUMN TYPE` + 4g `ALTER TABLE` add/drop constraint + add FK + 4h `ALTER TABLE … RENAME TO` + 4i verification sweep (completion merge + simple/advanced completion colour + describe of table-level constraints + self-ref FK indicator + CREATE-TABLE help/usage), implemented 2026-05-25/26 — **Phase 4 complete**), **Phase 4** of the ADR-0030 roadmap (peer of 0031/0032/0033) and **clarifies ADR-0030 §4**. Advanced-mode `CREATE`/`DROP`/`ALTER TABLE` + `CREATE`/`DROP INDEX` get their **own per-statement commands** (`SqlCreateTable`/`SqlAlterTable`/`SqlDropTable`/`SqlCreateIndex`/`SqlDropIndex`), like DML's `Sql*` set — but unlike DML they **execute *structurally*, not verbatim** (raw execution would lose the playground's types, named relationships, and `STRICT`; "verbatim" was a DML convenience, not a rule). Handlers **reuse the low-level schema/metadata helpers** where the operation matches simple mode and **stand alone where the SQL surface is richer** (clarity over forced refactoring); simple mode is untouched (additive). Dispatch: `create`/`drop` reuse ADR-0033 Amendment 1's category-grouped mode-aware dispatch (SQL-first, simple fallback); `alter` is a new advanced-only entry word. Full surface (no pre-emptive cuts, `Q4`): `CREATE TABLE` with column + table constraints, single/compound `PRIMARY KEY`, inline + table-level `FOREIGN KEY` → **named relationships** (one statement = one command = **one undo step**, ADR-0006); `ALTER TABLE` add/drop/rename column, `ALTER COLUMN TYPE`, add/drop constraint, add FK, **`RENAME TO`** (advanced-only table rename — new low-level op renaming the table + its CSV + the relationship and table-CHECK metadata, closing the rename half of `C1`); `CREATE [UNIQUE] INDEX` / `DROP INDEX`. Type slot accepts the ten playground keywords **and** standard-SQL aliases (`integer`→`int`, `varchar`→`text`, `timestamp`→`datetime`, …; length args accepted-and-ignored; no engine type names in/out — ADR-0030 §5). `CHECK`/`DEFAULT` reuse ADR-0031 `sql_expr`. **Pre-implementation `/runda` refinements (2026-05-24, user-confirmed):** `CREATE TABLE`/`DROP TABLE` **admit `IF [NOT] EXISTS`** (no-op-that-succeeds-with-a-note — a near-universal cross-vendor idiom, reclassified *into* scope, not engine-specific); `INTEGER PRIMARY KEY` maps to a **plain `int`** PK, *not* auto-increment (`serial` stays the sole auto-increment type). **Column-type-conversion is unified** (ADR-0017 engine, mode-appropriate policy): clean auto-converts and incompatible/own-type-static cases refuse in both modes, but a **lossy** change refuses-by-default in simple mode (`--force-conversion` opts in) while advanced mode **performs it with a loss note** and relies on **`undo` as the safety net** — no force flag, no dropping to simple mode (a payoff of shipping ADR-0006 first). OOS: views/triggers/txn-control/PRAGMA/etc. (ADR-0030 §3), the Postgres `USING` clause, and the DSL→SQL teaching echo (ADR-0030 Phase 5). Sub-phases 4a–4i, plus **4a.2** (per-column `CHECK`/`DEFAULT` via raw `sql_expr` text — `sql_expr` is validate-only, no `Expr` AST — + composite `UNIQUE(a,b)`; no new internal table) and **4a.3** (table-level/multi-column `CHECK`, landed via the new `__rdbms_playground_table_checks` metadata table because SQLite has no PRAGMA for CHECK; the builder tells a table-level CHECK from a column-level one by element position) and **4b** (foreign keys — inline `REFERENCES` + table-level `FOREIGN KEY` → ADR-0013 named relationships in the create transaction, one undo step; self-references + bare `REFERENCES ` supported, user-confirmed) and **4c** (`DROP TABLE [IF EXISTS]` → `SqlDropTable`, reusing `do_drop_table`; `IF EXISTS` is a no-op-with-note via `DropOutcome::Skipped`) and **4d** (`CREATE [UNIQUE] INDEX [IF NOT EXISTS] [] ON (cols)` → `SqlCreateIndex` and `DROP INDEX [IF EXISTS] ` → `SqlDropIndex`, reusing `do_add_index`/`do_drop_index`; **`CREATE UNIQUE INDEX` admitted** — ADR-0025 **Amendment 1** — via an additive `IndexSchema.unique` flag that round-trips through `project.yaml` and rebuild, with `[unique]` markers in the structure view + items panel, while simple-mode `add unique index` stays deferred; `IF [NOT] EXISTS` reuses the 4c skip path; `create`/`drop` each gain a *second* advanced node, exercising the all-candidates dispatch) and **4e** (`ALTER TABLE` add/drop/rename column → `SqlAlterTable`; `alter` is a new advanced-**only** entry word, runtime-decomposed to the existing `do_add_column`/`do_drop_column`/`do_rename_column` — no new worker layer; `do_add_column` extended to consume raw `default_sql`/`check_sql` so ADD COLUMN reaches CREATE-TABLE constraint parity; drop/rename refuse a column any CHECK references (table-level AND column-level, incl. a column's own self-check on rename) — the 4a.3 deferral, via a raw-CHECK-text tokenizer in the shared executors, so it guards both surfaces and fixes a latent rename-drift bug; SQL DROP COLUMN refuses an index-covered column with no `--cascade` spelling; the column executors + `do_add_index` gained an internal-`__rdbms_*`-table guard — all user-confirmed) and **4f** (`ALTER TABLE … ALTER COLUMN TYPE` → a fourth `AlterTableAction`, runtime-decomposed to the existing `change_column_type` with `ChangeColumnMode::ForceConversion` — which **is** the §7 advanced policy: lossy converts *with a note* (no force flag), incompatible + ADR-0017 static refusals (`↔ blob`, same-type, `date ↔ datetime`, non-`int → serial`) still refuse, while **`int → serial` is allowed** (auto-fills nulls + UNIQUE, ADR-0018 §8 — the §7 "→serial refused" summary is looser than the code); the builder discriminates the fourth branch by the **`type` keyword** (unique — ADD COLUMN's type is an ident), the type slot reuses `SQL_TYPE`; the internal-`__rdbms_*` guard was folded into `do_change_column_type`, closing the simple `change column` exposure too — user-confirmed) and **4g** (`ALTER TABLE … ADD [CONSTRAINT ] (CHECK | UNIQUE | FOREIGN KEY)` + `DROP CONSTRAINT `; ADD = CHECK + composite UNIQUE + FK, with `ADD PRIMARY KEY` and a *named* UNIQUE refused — composite UNIQUE is anonymous in our model; each ADD reuses a low-level path (table-CHECK/UNIQUE rebuild with a dry-run guard; FK → `add_relationship`, bare `REFERENCES

` → parent single-PK), DROP CONSTRAINT resolves the name to a table-CHECK then a child-side FK; **named table-CHECKs round-trip** via a nullable `name` column on `__rdbms_playground_table_checks` (**rebuild-only** arrival — pre-4g projects gain it on `rebuild`, a named add on an un-upgraded project is refused with a friendly "rebuild first" message) *and* a `project.yaml` `check_constraints` extension to an `{expr, name}` mapping (the bare-string form still reads); the internal-`__rdbms_*` guard was folded into `do_add_constraint`/`do_add_relationship`, completing that guard class — all user-confirmed) and **4h** (`ALTER TABLE … RENAME TO` — the one genuinely new low-level op, `do_rename_table`: a native engine rename plus one-transaction reconciliation of every metadata row naming the table (`__rdbms_playground_columns`, **both ends** of `__rdbms_playground_relationships`, `__rdbms_playground_table_checks`), the CSV file (the existing rewrite+delete path — no new persistence method), and **CHECK text that qualifies a column with the old table name** (`T.age`→`U.age`, a planning-`/runda` finding — the engine rewrites the live CHECK but the stored text would drift and break a fresh rebuild; `rewrite_check_table_qualifier` keeps them in step); grammar splits the `rename` verb into one branch with an inner Choice on a distinct second keyword (`column` vs `to`), the new-name slot mirroring the `CREATE TABLE` name slot; refuses same-name / existing-target / `__rdbms_*` / non-existent, with **case-insensitive** collision checks behind an engine-neutral pre-check (a finished-slice `/runda` finding — the engine matches names case-insensitively); auto-named indexes *and* relationships keep their stale names (only table-name columns update — §6 scope); one undo step; advanced-only, closing the rename half of `C1` — all user-confirmed) and **4i** (the verification sweep that completes Phase 4: the shared-entry-word completion merge + the simple-vs-advanced completion colour-when-mixed with Both→Advanced→Simple block ordering; `describe` of table-level composite UNIQUE + table CHECK; the self-ref FK pre-submit indicator fix; and the CREATE-TABLE help/usage skeleton refresh). **All of Phase 4 (4a–4i) is shipped.** Each sub-phase has exit + DA gates diff --git a/docs/requirements.md b/docs/requirements.md index 7f5767e..17d658d 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -256,8 +256,10 @@ handoff-14 cleanup; 449 after B2/C2.) CHECK text that qualifies a column with the old table name** so a fresh rebuild round-trips; refuses same-name / existing-target / `__rdbms_*` / non-existent; auto-named indexes + relationships kept stale per §6 - scope; one undo step). Remaining: the 4i verification sweep per - ADR-0035 §13.)* + scope; one undo step), then the **4i verification sweep** (shared-entry- + word completion merge + simple/advanced completion colour; `describe` of + table-level constraints; self-ref FK pre-submit indicator; CREATE-TABLE + help/usage refresh). **ADR-0035 Phase 4 (4a–4i) is complete.**)* - [ ] **Q2** Non-standard syntax rejected with a clear message pointing at the supported subset. *(Design done — ADR-0030 §8: out-of-subset statements are diff --git a/src/app.rs b/src/app.rs index 02a29f3..a36bab5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2596,6 +2596,8 @@ mod tests { outbound_relationships: Vec::new(), inbound_relationships: Vec::new(), indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), } } diff --git a/src/db.rs b/src/db.rs index cbd1792..a08abf3 100644 --- a/src/db.rs +++ b/src/db.rs @@ -70,6 +70,13 @@ pub struct TableDescription { pub inbound_relationships: Vec, /// User-created indexes on this table (ADR-0025). pub indexes: Vec, + /// Table-level composite `UNIQUE (a, b, …)` constraints (ADR-0035 + /// §4a.2). Single-column UNIQUE rides on the column itself + /// (`ColumnDescription::unique`); these are the multi-column ones. + pub unique_constraints: Vec>, + /// Table-level `CHECK (…)` constraints with their optional name + /// (ADR-0035 §4a.3 / §4g), in declaration order. + pub check_constraints: Vec, } /// One user-created index on a table (ADR-0025). @@ -7390,6 +7397,8 @@ fn do_describe_table(conn: &Connection, name: &str) -> Result with pk [(), ...] — create a table sql_create_table: |- - create table [if not exists] ( [not null] [unique] [primary key], ... - [, primary key (, ...)]) — create a table (advanced SQL) + create table [if not exists] ( + [not null] [unique] [primary key] [default ] [check ()] [references

[()]], ... + [, primary key (, ...)] [, unique (, ...)] [, check ()] + [, [constraint ] foreign key () references

[()]]) + — create a table (advanced SQL) sql_drop_table: |- drop table [if exists] — remove a table (advanced SQL) sql_create_index: |- @@ -475,7 +478,7 @@ parse: # placeholders. ADR-0009's surface conventions apply. usage: create_table: "create table with pk [()[, ...]]" - sql_create_table: "create table [if not exists] ( [not null] [unique] [primary key], ... [, primary key (, ...)])" + sql_create_table: "create table [if not exists] ( [not null] [unique] [primary key] [default ] [check ()] [references [()]], ... [, primary key (, ...)] [, unique (, ...)] [, check ()] [, [constraint ] foreign key () references [()]])" sql_drop_table: "drop table [if exists] " sql_create_index: "create [unique] index [if not exists] [] on ([, ...])" sql_drop_index: "drop index [if exists] " diff --git a/src/output_render.rs b/src/output_render.rs index c42e512..1c1ae2f 100644 --- a/src/output_render.rs +++ b/src/output_render.rs @@ -152,6 +152,24 @@ pub fn render_structure(desc: &TableDescription) -> Vec { } } + // Table-level constraints (ADR-0035 §4i b): composite `UNIQUE (a, b)` + // and table `CHECK (…)` constraints. Single-column UNIQUE / NOT NULL / + // PK / column-level CHECK already show in the per-column "Constraints" + // column above; this section is the table-level constraints that span + // columns or stand alone. A named CHECK shows its name. + if !desc.unique_constraints.is_empty() || !desc.check_constraints.is_empty() { + out.push("Table constraints:".to_string()); + for cols in &desc.unique_constraints { + out.push(format!(" unique ({})", cols.join(", "))); + } + for chk in &desc.check_constraints { + match &chk.name { + Some(name) => out.push(format!(" check {name} ({})", chk.expr)), + None => out.push(format!(" check ({})", chk.expr)), + } + } + } + out } @@ -730,6 +748,8 @@ mod tests { outbound_relationships: Vec::new(), inbound_relationships: Vec::new(), indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; assert_snapshot!(render_structure(&desc).join("\n")); } @@ -749,6 +769,8 @@ mod tests { on_update: ReferentialAction::NoAction, }], indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; let out = render_structure(&desc).join("\n"); assert!( @@ -774,6 +796,8 @@ mod tests { outbound_relationships: Vec::new(), inbound_relationships: Vec::new(), indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; let out = render_structure(&desc).join("\n"); // PK appears for id, NOT NULL for name, blank for nick. @@ -796,6 +820,8 @@ mod tests { columns: vec!["Email".to_string()], unique: false, }], + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; let out = render_structure(&desc).join("\n"); assert!(out.contains("Indexes:"), "got:\n{out}"); @@ -821,11 +847,43 @@ mod tests { columns: vec!["Email".to_string()], unique: true, }], + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; let out = render_structure(&desc).join("\n"); assert!(out.contains("uidx_email (Email) [unique]"), "got:\n{out}"); } + #[test] + fn render_structure_shows_table_level_constraints() { + // ADR-0035 §4i (b): composite UNIQUE and table-level CHECK + // (named + unnamed) render in a "Table constraints:" section, + // distinct from the per-column "Constraints" column. + let desc = TableDescription { + name: "T".to_string(), + columns: vec![ + col("a", Type::Int, true, false), + col("b", Type::Int, false, false), + ], + outbound_relationships: Vec::new(), + inbound_relationships: Vec::new(), + indexes: Vec::new(), + unique_constraints: vec![vec!["a".to_string(), "b".to_string()]], + check_constraints: vec![ + crate::persistence::TableCheck { name: None, expr: "a < b".to_string() }, + crate::persistence::TableCheck { + name: Some("a_lt_b".to_string()), + expr: "a <> b".to_string(), + }, + ], + }; + let out = render_structure(&desc).join("\n"); + assert!(out.contains("Table constraints:"), "got:\n{out}"); + assert!(out.contains("unique (a, b)"), "got:\n{out}"); + assert!(out.contains("check (a < b)"), "unnamed check; got:\n{out}"); + assert!(out.contains("check a_lt_b (a <> b)"), "named check shows its name; got:\n{out}"); + } + #[test] fn render_structure_omits_indexes_section_when_none() { let desc = TableDescription { @@ -834,6 +892,8 @@ mod tests { outbound_relationships: Vec::new(), inbound_relationships: Vec::new(), indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; let out = render_structure(&desc).join("\n"); assert!(!out.contains("Indexes:"), "got:\n{out}"); @@ -1026,6 +1086,8 @@ mod tests { outbound_relationships: Vec::new(), inbound_relationships: Vec::new(), indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; let out = render_structure(&desc).join("\n"); // The lowercase form of the SQLite type should appear. diff --git a/src/ui.rs b/src/ui.rs index 03e6ae2..0aac888 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1310,6 +1310,8 @@ mod tests { outbound_relationships: Vec::new(), inbound_relationships: Vec::new(), indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; app.current_table = Some(desc); // Mirror what the App writes when a DSL command succeeds. diff --git a/tests/sql_alter_table.rs b/tests/sql_alter_table.rs index 72119d7..7136b22 100644 --- a/tests/sql_alter_table.rs +++ b/tests/sql_alter_table.rs @@ -653,6 +653,50 @@ fn e2e_named_check_metadata_survives_a_fresh_rebuild() { .expect("DROP CONSTRAINT after a fresh rebuild — the CHECK metadata was reconstructed"); } +// --- 4i (b): describe shows table-level constraints --------------------- + +#[test] +fn e2e_describe_shows_table_level_constraints() { + // ADR-0035 §4i (b): `describe` surfaces composite UNIQUE and + // table-level CHECK constraints (named + unnamed) — the executor + // populates them on TableDescription from the metadata. + let (project, db, _d) = open(); + let r = rt(); + std::fs::write( + project.path().join("d.commands"), + "create table T (a integer primary key, b integer, unique (a, b), check (a < b))\n\ + alter table T add constraint a_ne_b check (a <> b)\n", + ) + .expect("write script"); + let events = r.block_on(run_replay(&db, project.path(), "d.commands")); + assert!( + matches!(events.last(), Some(AppEvent::ReplayCompleted { .. })), + "events: {events:?}" + ); + + let desc = r.block_on(db.describe_table("T".to_string(), None)).expect("describe"); + assert_eq!( + desc.unique_constraints, + vec![vec!["a".to_string(), "b".to_string()]], + "composite UNIQUE surfaced" + ); + let checks: Vec<(Option, String)> = desc + .check_constraints + .iter() + .map(|c| (c.name.clone(), c.expr.clone())) + .collect(); + assert!( + checks.iter().any(|(n, e)| n.is_none() && e.contains("a < b")), + "unnamed table CHECK surfaced: {checks:?}" + ); + assert!( + checks + .iter() + .any(|(n, e)| n.as_deref() == Some("a_ne_b") && e.contains("a <> b")), + "named table CHECK surfaced with its name: {checks:?}" + ); +} + // --- 4h: ALTER TABLE … RENAME TO (ADR-0035 §6) -------------------------- /// Path to a table's CSV in the project data dir. diff --git a/tests/walking_skeleton.rs b/tests/walking_skeleton.rs index 6c8a040..73d5d15 100644 --- a/tests/walking_skeleton.rs +++ b/tests/walking_skeleton.rs @@ -269,6 +269,8 @@ fn fake_table(name: &str, columns: &[(&str, Type, bool)]) -> TableDescription { outbound_relationships: Vec::new(), inbound_relationships: Vec::new(), indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), } } @@ -443,6 +445,8 @@ fn add_relationship_flow_shows_parent_side_with_inbound_section() { on_update: ReferentialAction::NoAction, }], indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; app.update(AppEvent::DslSucceeded { command: Command::AddRelationship { @@ -495,6 +499,8 @@ fn add_relationship_flow_shows_inbound_section_on_parent() { on_update: ReferentialAction::NoAction, }], indexes: Vec::new(), + unique_constraints: Vec::new(), + check_constraints: Vec::new(), }; app.update(AppEvent::DslSucceeded { command: Command::AddColumn {