Skip to content

Tables tab: delete a Table from the row, published ones included - #2583

Merged
jh-RLI merged 2 commits into
developfrom
feature-2562-row-delete
Oct 2, 2026
Merged

jh-RLI merged 2 commits into
developfrom
feature-2562-row-delete

Conversation

@jh-RLI

@jh-RLI jh-RLI commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Closes #2562 (slice 10 of spec #2551).

What it does

Delete joins the table action service from #2561 as its third action, and the row's ⋯ menu as its last entry (red, behind a divider). It reuses the same path as publish and unpublish:

  • Role: Data maintainer or above (ROLE_GATES[DELETE] at DELETE_PERM). Below that, the entry stays in the menu, disabled, with the reason.
  • Draft: a plain confirmation.
  • Published Table: the dialog states what deleting breaks: the Datasets it leaves (your own by name, other people's by owner and name, since their owners are not told), its Review state, an active embargo, and that knowledge-graph links to it stop resolving. You confirm by typing the Table's name.
  • Batch (for Tables tab: selection, bulk bar and bulk publish/unpublish #2564's bulk bar, already handled by the service): you type the count when the batch holds a published Table or more than ten.
  • The server enforces the typed confirmation inside execute's transaction, against the Tables as they are at that moment. If a draft was published after the dialog opened, the request now asks for the name. A mismatch is the dialog's own input: 400, the error beside the field, no toast.

Order of work across the two databases

Table.delete is split into delete_record() (Django rows and cascades) and drop_oedb_table(). delete() still calls both, so the API is unchanged.

execute removes the Django rows inside its transaction, which holds the row lock. For delete that transaction is durable=True, so it cannot run nested inside another transaction. When the block is left, the rows are committed, and only then are the OEDB tables dropped, one Table at a time.

A failed drop:

  • is logged at WARNING with its traceback and drop=failed;
  • comes back in Outcome.drop_failed;
  • reaches the dashboard as tables-changed with warning: true. tables_tab.js shows that message as an assertive toast that stays until dismissed (dash-toast--warning) and names the Table, with its technical name. It never shows as a success.

Log line per Table: table_action … action=delete … datasets=<names left>|- published=yes|no drop=ok|failed.

The delete ceiling: 50

Production timeout. Read on the host today (2026-10-02 18:07 UTC, read-only):

  • Timeout 300 (httpd.conf:123);
  • socket-timeout=300 on both WSGIDaemonProcess groups;
  • no request-timeout.

This is unchanged since WF-21's capture on 2026-09-21.

Per-Table cost. Measured locally with the new benchmarks/tables_tab/delete_cost.py (Postgres 14, shared_buffers 128 MB, three rounds). Each run deletes a batch of real OEDB-backed Tables through execute:

rows per Table per Table, total of which the drop slowest single drop
0 (16 KB) 17–18 ms 9–10 ms 13 ms
100,000 (8 MB) 17–18 ms 10–11 ms 14 ms
1,000,000 (83 MB) 33–38 ms 22–24 ms 1,020 ms (once)
10,000,000 (0.8 GB) 0.64 s 0.62 s 1,126 ms

Safety factor 5. At a worst case of 1.2 s per Table, 50 Tables take 60 s, which is one fifth of the 300 s timeout. The margin is meant to cover production's OEDB sitting on another host and its larger buffer pool. A typical batch of 50 takes 1–2 s.

Where it is enforced. execute refuses more than 50 names before it reads anything (400, errors.table). The dialog states the limit ("Delete takes at most 50 tables at a time; you selected 412") and offers no confirm button. The reasoning sits next to CEILINGS in the code. Preflight.ceiling is now set, so #2564 adds its own ceilings to the same dict.

Points for you to rule on

  • A drop blocked on a lock is not covered by the ceiling. DROP TABLE waits for any session that holds a lock on that table, with no lock_timeout. The api_table DELETE has the same behaviour today. I left the shared drop_if_exists path alone. If you want a guard, the place is a SET LOCAL lock_timeout in the drop, and a timed-out drop would then report as drop=failed.
  • Peer reviews survive a delete. PeerReview.table is a name, not a foreign key, so it is not cascaded. The dialog says so ("The review stays on record without its table") rather than hiding it. This is unchanged behaviour. A new Table that reuses the name would inherit those reviews.
  • Knowledge-graph line: shown for published Tables only, as the spec says. No graph query is made, since the OEKG is never consulted on this path.

Tests

  • login/tests/test_table_delete.py: 22 tests through HTTP. They cover the role gate, consequences, typed name and typed count, a draft published since the dialog opened, refused-whole, the ceiling, and that rows are gone from both databases. One test checks that the drop runs after the rows are gone and outside the service's transaction. Another checks a failed drop: rows gone, data still in the OEDB, the warning naming the Table, and the WARNING log line. Plus the log fields and the re-fetch.
  • vitest: 3 new cases (warning toast, delete focus, lasting warning on tables-changed).
  • Results: login 323 green, vitest 161 green. The full suite ran 1,224 tests, green apart from the known LightImportTest, which fails only under the isolated-settings trick and passes alone.
  • The menu still costs no query: the 7-query budget is unchanged.

Browser check (headless Chrome, throwaway DB, 1440 px):

  • Delete a draft: plain confirm, the row is gone, focus moves to the list heading.
  • Delete a published Table in another user's Dataset: the consequences are shown. A wrong name keeps the dialog open with the error under the field. The right name deletes it.

No migration and no deploy step. This lifts the release gate for #2553 once it merges.

🤖 Generated with Claude Code

jh-RLI and others added 2 commits October 2, 2026 20:28
Delete joins the table action service as its third action (Data
maintainer or above) and the row's ⋯ menu as its last entry. A draft is
confirmed plainly; one published Table by typing its name, a batch with
a published Table or more than ten by typing the count, all enforced by
the server inside execute's transaction, against the Tables as they are
then. A mismatch is the dialog's own 400, no toast.

The dialog states what deleting breaks: the Datasets left (other
people's by owner), the Review state, an active embargo, and that
knowledge-graph links stop resolving.

Order of work: the Django rows go in execute's transaction, now durable
for delete so it cannot be nested; the OEDB tables are dropped after it
has committed, one at a time (Table.delete_record / drop_oedb_table). A
failed drop is logged at WARNING with drop=failed and comes back as a
lasting warning toast naming the Table, never as a success. Log fields:
datasets=<names left>|- published=yes|no drop=ok|failed.

Ceiling: 50 Tables per request, enforced in execute and stated in the
dialog. Production runs Timeout 300 / socket-timeout=300 (read on the
host 2026-10-02); locally a delete costs 17-38 ms per Table up to 1M rows
and at worst 1.13 s (10M rows), so 50 at 1.2 s is 60 s, a safety factor
of 5. benchmarks/tables_tab/delete_cost.py takes the measurement.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#2563 (PR #2582) landed first and touched the same seams. Resolved by
keeping both sides:

- table_actions.py: develop's Dataset actions plus delete. Delete's
  ceiling also blocks Preflight.confirmable, the property #2563 added
  for the confirm button, so the dialog reads one condition.
- table_action_dialog.html: delete joins #2563's elif chains (title,
  "Will be …", the body include, "Nothing to …", the confirm button,
  red for delete only).
- cells/menu.html: Delete stays last, after the Dataset entries.
- views.py: "confirm" joins TableActionView.PARAMS.
- test_table_actions: only Manage access is still absent from the menu.
- changelog: both entries, #2563's first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jh-RLI
jh-RLI merged commit e028e17 into develop Oct 2, 2026
4 of 5 checks passed
@jh-RLI
jh-RLI deleted the feature-2562-row-delete branch October 2, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tables tab: delete a Table from the row, including published ones

1 participant