feat: table properties, temporary tables, join completions, and MCP write access - #35
Merged
Merged
Conversation
Implements docs/table-properties-and-temporary-tables-plan.md. Properties pane - New `get_table_properties` (stats/table_properties.rs) answers per dialect in one round trip: information_schema.TABLES + SHOW CREATE/COLUMNS/INDEX on MySQL, pg_class/pg_stat_all_tables/pg_sequences on Postgres, sqlite_master + the PRAGMAs + dbstat on SQLite. It reads through execute_raw_sql_generic so a manual transaction routes it to the pinned connection, and every secondary lookup is .ok()-tolerant: a missing dbstat blanks one row, not the panel. - New `properties` view mode in DataGrid, so this is a workspace tab rather than a modal and two tables' properties can sit side by side. - Rows whose value is null are dropped instead of rendered as "-": "unknown" and "not applicable" are different answers. Temporary tables - New `get_temporary_tables` lists what the SESSION owns. Postgres matches pg_my_temp_schema() rather than nspname LIKE 'pg_temp%' — the catalogue holds every session's temp relations, and offering another session's would open a tab whose every query fails. MySQL's INNODB_TEMP_TABLE_INFO reports InnoDB's internal names, so those are filtered out and the section is usually empty there, which is the honest answer for a pooled connection. - The section renders only while the count is above zero and clears on a connection or database change. - TableItem.schema is threaded from the sidebar row through the tab to the reads, because a Postgres temp table lives in pg_temp_N and qualified() would look for it in the connection's schema. get_table_data, get_table_schema, drop_table and get_table_properties take an optional schema override that defaults to the connection's schema, so every pre-existing caller is unchanged. Also - SqlEditor dispatches `schema-changed` next to invalidateCatalog(), so DDL typed into a query tab refreshes the sidebar. It previously only reacted to renames and restores, so a CREATE TABLE left the list stale until a manual refresh. - 5 Rust unit tests, 66 i18n keys across en/vi/ja. Verified: tsc clean, 796 frontend tests pass, build keeps monaco out of the startup preload list, oxlint adds no new findings. The Rust is NOT compiler-verified — tauri dev holds the target dir, so only the pure helpers were checked via standalone rustc.
- The four headline figures are centred in their tracks (`align-items` plus `text-align`, and `justify-content` on the icon+label line). - Each label line takes one hue, set on `.tp-metric-head` and inherited by both children so a tile is one declaration: accent for storage, green for rows, violet for columns, amber for indexes — the meanings the Sidebar already attaches to those colours. The number stays `--win-text-primary`; four coloured numbers would compete with each other rather than stand out from the labels. - The hues are declared PER THEME as local `--tp-*` properties on `.tp-container`, not as literals in the rules that use them. A single hex is correct in at most one of the two themes, and amber forced the issue: `#f59e0b` on a 10px uppercase label over the light ground is roughly 2:1 contrast. Same fix applied to the three colours left over from earlier rounds (`.tp-badge-temp`, `.tp-badge-view`, `.tp-str`), so the file no longer holds a bare hex outside a theme block. Scoped to the panel deliberately: defining `--win-warning` globally would restyle the ~51 sites that reference it with a fallback.
`cargo test --lib` reports 130. The 131 in the previous commit came from grepping for `#[test]`/`#[tokio::test]`, which also counted a COMMENT in `database/ident.rs:94` that mentions the attribute in prose. The count is now the one an actual run produced, alongside `cargo clippy --all-targets` at zero warnings.
…ation - Transform Database Overview modal into a dense, spreadsheet-style data grid with detailed table and view metadata (schema, kind, charset, collation, engine, rows, sizes, comment). - Compact top summary cards into a slim 34px horizontal metric strip and merge filter pills with search/sort into a unified toolbar. - Add right-click context menu on Tables header in sidebar with 'Item overview' and 'Show diagram'. - Consolidate all component stylesheets (database_info, process_monitor, table_properties) into src/index.css and eliminate scattered CSS imports.
…bol border - Add subtle alternating even/odd row backgrounds for the Database Overview data grid. - Fix spurious vertical line on estimated_row approximate prefix by replacing td class with dbi-grid-approx.
… clause After `JOIN`, the completion list was the whole catalog ordered by usage frequency, and the FK knowledge only appeared once `ON` had been typed. A candidate the FK graph connects to a table already in scope now sorts into a new `1z_` tier above the plain table tier, says so in its detail line, and - unless an `ON` already follows the caret in the same statement - is inserted with the condition attached (`address a ON a.city_id = c.city_id`). Ranking reads the catalog cache only, so it costs no IPC and reports nothing until the background prime lands. `buildJoinConditions` gained an `fkOnly` option for it: the same-name fallback it uses at the caret would call every table joinable in a schema where each one has an `id`.
…JOIN The FK ranking added in the previous commit reads the catalog cache, and the first JOIN of a session is exactly when that cache is still cold: getTables() fires primeCatalog() and walks away, so the ranking silently did nothing until the user retyped. primeCatalog() now hands back the in-flight promise instead of a bare "already priming" return, and ensureCatalogPrimed() awaits it on that one path. The wait is capped at 1.2s with the prime left running past the cap, so a slow connection costs one frequency-ordered popup, never a popup that hangs. Drops the `priming` flag, which inFlight replaces, and clears inFlight in invalidateCatalog so nobody awaits a prime filling a discarded cache.
… an effect `npx oxlint` was exiting 1 on a single correctness error: the preview effect in ExportTableDialog wrote the picked rows into state synchronously, while the fetch path queues its loading flag in a microtask - so the two orderings had to be reasoned about together, and a comment explained why the early return had to sit where it did. Those rows are already in memory, so the whole branch is derivable. The effect now returns without touching state, and rows/totalRows/loading are computed from `pickedRows`. The ordering question is gone rather than documented. Renames the fetch-path state (fetchedRows/fetchedTotal/fetching) so the derived names stay the ones the JSX reads, and swaps `onlySelected` for `pickedRows` in the effect deps. Lint is back to exit 0, with 90 warnings left in the two groups CLAUDE.md triages.
feature-roadmap-plan.md listed 7 open items, 5 of which have real code: Quick Copy, Live Processlist, ER Diagram, Query Result Charts and .xlsx export. Only the Tauri auto-updater and DuckDB are actually unstarted. performance-optimization-guide.md was the same story for steps 1-3, and two details of its step 2 are now wrong: under rolldown an object `manualChunks` is ignored silently, and `drop: ['console']` is dead config without esbuild. CLAUDE.md's lint numbers had drifted too - ~190 warnings is now ~90, and the exhaustive-effect-dependencies backlog it described as untriaged is at zero. Each ticked item names where the code lives, so the next reader can check the claim instead of taking it.
… approval The plan required §3.5 to be decided before any code. Decided and recorded in the document: share the MECHANISM with Safe Mode, not the policy. Safe Mode cannot gate this even in principle - it lives in dbHelper's local invoke() on the frontend, and an MCP request arrives on an axum task in Rust without passing through it. So mcp/approval.rs builds the park-emit-await- resume machinery safeMode.ts deliberately skipped. The gate is always on and has no silent mode: "stop asking about my own statements" does not extend to a statement an outside service wrote, and a policy that cannot be configured is a policy that cannot drift from Safe Mode's. Three layers, none of them a substitute for another: - ConnEntry.mcp_write, a second per-connection tick, default off and cleared whenever the share tick goes off. Sharing a connection so a model can read the schema must not carry write access, or every shared connection becomes a source of dialogs - and fatigue is how a guard stops working. - policy::ensure_single_write: one statement, and not a read. A SELECT sent here would raise a dialog for something tablegrid_query answers without one. DDL passes: §0.3 already settled that keyword classification is the wrong mechanism, and the human reading the statement is the filter. - approval::ask: parks on a oneshot keyed by request id, emits to McpApprovalGate.tsx, refuses after 60s. The dialog says the write commits immediately and Rollback cannot reach it, because it runs pooled. reject_if_manual is asked AGAIN after approval - 60 seconds is long enough for the user to switch to manual-commit mode, and running then would issue BEGIN on a session they own. cargo check + clippy (zero warnings) + 131 lib tests; tsc, oxlint and 798 frontend tests. Remaining V2 item: the audit log to a file.
…teration The FK ranking added an await inside the table loop, taking oxlint from 89 warnings to 90. That warning did not belong to the triaged group: CLAUDE.md leaves no-await-in-loop alone because every site is a sequential DB read, and Promise.all there would fire N queries at the user's production database. This one reads getCachedSchema - a Map already in memory - so the rule's advice is right here and the exception did not apply. Aliases are now assigned in their own pass (each already used an isolated `taken` set, so the loop was order-independent already), the conditions are resolved with Promise.all, and the item loop reads both from a Map. 89 warnings, exit 0. Also corrects the count in CLAUDE.md, which I had just written as ~90 without noticing my own commit had moved it.
Closes the last V2 item. The in-memory log dies with the process, which is the wrong lifetime for the only record of a third party writing to a live database: "what did it change last Tuesday" is asked after something goes wrong, never during the same run. Encrypted because the entries carry the SQL verbatim, and SQL carries data - an INSERT names the values, a WHERE names the person. Plaintext on disk would hand that content to every backup agent, sync folder and search indexer on the machine, none of which knew it was handling database contents. AES-256-GCM, key in the OS keyring beside the bearer token. aes-gcm was already in Cargo.lock via pkcs5, so declaring it adds no crate to the tree - the same argument base64 carries. Per line, not per file: a log is append-only, so re-encrypting the whole file each request would be quadratic and a crash mid-rewrite would lose everything rather than one line. Each line's AAD is the previous line's tag, so removing, reordering or splicing lines makes every later line fail to open. Three limits are written into the module doc rather than left to be found: the key is readable by anything running as this user (a local app cannot hide a key from its own user - what this stops is the file travelling where the keyring did not), tail truncation stays undetectable, and line count and timing are not hidden. A dedicated writer thread, not spawn_blocking per entry: the chain makes write ORDER part of the format, and pool tasks have no order. The hot path only serialises and sends; an entry is dropped rather than allowed to block the request it records. Read back through mcp_audit_file_read, with a This run / Saved on disk switch in the Logs tab. Undecryptable lines are counted next to the list instead of being swallowed, and Clear is disabled over the disk view because it does not touch the file. cargo check + clippy (zero warnings) + 134 lib tests; tsc, oxlint 89/exit 0, 798 frontend tests.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files
|
CodeQL flagged two hard-coded cryptographic values on PR #35, both in this module's own tests: a [7u8; 32] key in the helper and a [9u8; 32] one in the wrong-key case. The alerts turned the PR's CodeQL check red. They are worth acting on rather than dismissing as test-only. Nothing at the call site tells a reader that a 32-byte literal is a test key, which is the same reason the rule exists - and no test here depends on the key's value. Generating one per cipher says that outright, and the wrong-key test now asks for a second generated key, which reads better than a second magic array. cargo fmt + clippy (zero warnings) + 134 lib tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds dedicated Table Properties and Temporary Tables panels, ranks joinable tables with automatic ON clause insertion in SQL Editor autocomplete, converts Database Overview into a dense data grid, and introduces MCP V2 write access with interactive approvals and encrypted hash-chained audit logging.
Type of change
Linked issue
Closes #
Key changes
get_table_propertiesbackend queries across MySQL, Postgres, and SQLite, surfaced as apropertiesview mode tab in DataGrid. Addedget_temporary_tablesfor session-scoped temporary table management.ONcondition expansion, plus catalog prime synchronization and batched condition resolution.Technical decisions
pg_my_temp_schema()rather thannspname LIKE 'pg_temp%'to avoid leaking other concurrent session relations.Out of scope / follow-ups
Testing
Verified:
Ran against:
How to re-verify:
SELECT * FROM orders JOINand verify candidate tables rank FK-related items first with automaticONclause suggestions.npx oxlint,npx tsc --noEmit,npm test, andcargo test --lib.Cross-cutting checks
Open if this PR crosses the Rust ↔ TypeScript boundary, or touches the editor / build config
Nothing enforces the pairs below. They drift silently, and the failure shows up as
wrong behaviour rather than as a build error.
#[tauri::command]→ registered insrc-tauri/src/app/handlers.rssrc/utils/dbHelper.tsredis_*command → classified insrc/utils/safeMode.tssrc-tauri/src/database/splitter.rsandsrc/sql/statements.tssrc/utils/backendErrors.tsredis_ssl_mode()andREDIS_SSL_MODESinConnectionManager.tsxdbHelper.tsvite.config.mts→ themonacochunk is still absent from themodulepreloadlist indist/index.htmlafternpm run build-frontendImpact & risk
Checklist
npx oxlint,npx tsc --noEmit,npm run testen.ts+vi.ts+ja.ts, never a literalconsole.log, throwawayunwrap()or stray TODONotes for the reviewer
stats/table_properties.rsandmcp/audit_file.rsfor the new backend features and security chaining.