Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ All notable changes to QueryDen are documented here. This project adheres to [Se
- **PostgreSQL FK relationships now render in the ER diagram.** The `schemaItems.foreignKeys` list is sometimes null when schema loading fails silently. Added direct `fetchPostgresForeignKeys()` and `fetchMySQLForeignKeys()` fallbacks that query system catalogs directly, plus a `normalizeTableName()` helper to reconcile the `public.` prefix format difference between PostgreSQL's `regclass::text` and the schema items table list.
- **PostgreSQL columns in `public` schema no longer missing from the ER diagram.** Tables with many columns were showing as header-only because the column map keyed by `public.tablename` didn't match the schema items' bare `tablename` format. Stripping the `public.` prefix in the column lookup fixed it.
- **ER diagram no longer freezes when opening on databases with many tables.** The table selector dialog now appears first — users pick which tables to include before any schema data is fetched or dagre layout runs. Selected-only data keeps both the query batch and the layout computation scoped to what's visible.
- **Save no longer replaces custom query results with a full table scan.** After saving edits, `handleSave` re-runs the user's original SELECT query (captured from `lastSelectQueryRef`) instead of a generic `SELECT * FROM table LIMIT 1000`. Column selection, ORDER BY, and LIMIT are preserved — the grid stays on the user's custom result set.
- **Schema-qualified table PK candidates now use the base table name.** `handleSave` and `handleUpdateRow` derive table-specific PK candidates from the unqualified table name (e.g. `config_id` for `ansible.config`) instead of the full schema-qualified name, fixing PK detection on multi-schema databases.
- **Fallback UPDATE WHERE clause uses pre-edit values.** When no PK column is found, `handleSave` reads from the `_original` row snapshot (captured at edit time) rather than the current edited values, preventing 0-row-affect UPDATEs.

### Changed
- **ER diagram compact view is now the default.** The diagram opens in compact mode (PK/FK columns only) instead of showing all columns, reducing visual clutter on first open. Users can toggle back to full-column view via the toolbar button.
- **PostgreSQL FK introspection now uses `pg_catalog.pg_constraint` instead of `information_schema`.** The Database Explorer and post-SELECT FK auto-loading both query `pg_constraint` directly, correctly handling cross-schema foreign keys, composite (multi-column) FKs, and environments where `information_schema` permissions are restricted. Cross-schema FK references are schema-qualified in the UI (e.g. `"billing"."invoices"`).
- **FK metadata is now auto-loaded after SELECT queries.** When the results grid detects a table-backed SELECT, it fetches FK constraints from the relevant engine (PostgreSQL/Supabase/CockroachDB via `information_schema`, MySQL/MariaDB via `information_schema`, SQLite via `PRAGMA foreign_key_list`) and caches them per `schema.table` so subsequent queries on the same table are instant. FK columns then render as link cells without requiring a prior tree-expand.
- **Webview default right-click context menu re-enabled.** The document-level `contextmenu` `preventDefault()` that suppressed the browser-native Reload/Inspect Element menu on Linux WebKitGTK has been removed. The app's own context menus (tab strip, Monaco editor, grid) stop propagation and remain unaffected.
- **PK detection now prefers real database metadata over name heuristics.** `loadTableDetails` queries actual PK columns from `information_schema.table_constraints` (PostgreSQL/MySQL) or `PRAGMA table_info` (SQLite). The post-SELECT FK auto-load path also detects PKs in parallel. All CRUD operations use a unified `getPrimaryKey` helper that checks `tableSchema.primaryKeys` first, then falls back to expanded name heuristics (`id`, `uuid`, `uid`, `pk`, `row_id`, `object_id`, `key`, `code`, `_id`, `{table}_id`).

### Added
- **FK-aware inline navigation in the results grid.** FK columns render as styled link cells with an external-link icon. Clicking the icon fires a `SELECT * FROM refTable WHERE refCol = value` query, navigating directly to the referenced row — enabling quick parent-row lookup from any query result.
Expand All @@ -63,7 +67,10 @@ All notable changes to QueryDen are documented here. This project adheres to [Se
- **PostgreSQL operators enabled by default in schema tree.** The `showOperators` setting now defaults to `true`, so operator nodes appear in the schema tree without having to toggle the setting first.
- **ER Diagram visualization for PostgreSQL, MySQL/MariaDB, and SQLite.** A new ER Diagram dialog (toolbar <Table /> button) renders the current database as an interactive entity-relationship diagram powered by `@xyflow/react` v12 and `dagre` auto-layout. Tables are shown as cards with PK (key icon) and FK (link icon) column markers; relationships are drawn as smooth-step edges with cardinality labels (`*` at the FK end, `1` at the PK end). The dialog opens with a table selector so users pick which tables to include — preventing the freeze that would occur when auto-introspecting many tables. A toolbar provides schema multi-select (PostgreSQL), search/filter with related-table expansion, compact/PK-only column mode, refresh, and SVG export (Tauri `save()` dialog + `writeTextFile`). Schema data is cached with a 30-second TTL so re-opening the same tables is instant. Provider-specific introspection: PostgreSQL queries `pg_attribute`/`pg_index`/`pg_constraint` in bulk, MySQL uses `information_schema`, SQLite uses batched `PRAGMA table_info`/`PRAGMA foreign_key_list` via `Promise.all`.
- **ER Diagram now opens on the correct database.** Previously the diagram always read the global `activeConnection` — if the active editor tab targeted a different database, the diagram showed the wrong schema. The toolbar button now switches the connection to match the tab's target before opening.
- **Grid scrolls to the saved record after save.** After saving changes (via `handleSave`), the grid dispatches `grid-scroll-to-row` to scroll to the modified row matched by PK value, or `grid-scroll-to-bottom` for newly inserted rows — keeping the user's viewport focused on the record they just edited.

### Removed
- **Roadmap items removed as addressed.** "Vault crypto hardening" (#14, #15), "Schema-aware row editor" (#50), and "ER Diagram" (#51) removed from the website roadmap — vault hardening, schema-aware FK dropdowns, NOT NULL validation, and ER diagram support in this release subsume those feature requests.
Comment on lines +72 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate heading level under Unreleased (### Removed).

Line 72 introduces a second ### Removed heading in the same section, which triggers MD024 and can break docs tooling/navigation.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 72-72: Multiple headings with the same content

(MD024, no-duplicate-heading)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 72 - 73, There are two identical "### Removed"
headings under the "Unreleased" section causing MD024; locate the duplicate "###
Removed" heading (the second occurrence) and either remove it or merge its
bullet(s) into the first "### Removed" list so only one "### Removed" heading
remains; ensure the bullets mentioning "Vault crypto hardening", "Schema-aware
row editor", and "ER Diagram" are preserved under the single heading.


## [1.0.23] - 2026-05-26

Expand Down
32 changes: 31 additions & 1 deletion src/components/explorer/DatabaseExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface TableDetails {
foreignKeys: { columns: string[]; refTable: string; refColumns: string[] }[];
indexes: { name: string; columns: string[]; unique: boolean }[];
triggers: string[];
primaryKeys: string[];
}

interface DatabaseExplorerProps {
Expand Down Expand Up @@ -590,7 +591,7 @@ export function DatabaseExplorer({ isAddConnectionDialogOpen = false }: Database
setLoadingTableDetails(prev => new Set(prev).add(key));

try {
const details: TableDetails = { columns: [], constraints: [], foreignKeys: [], indexes: [], triggers: [] };
const details: TableDetails = { columns: [], constraints: [], foreignKeys: [], indexes: [], triggers: [], primaryKeys: [] };

if (["postgres", "supabase", "cockroach"].includes(activeConnection.type)) {
// Load columns
Expand All @@ -607,6 +608,20 @@ export function DatabaseExplorer({ isAddConnectionDialogOpen = false }: Database
default: c.column_default
}));

// Load primary keys
const pks = await currentDb.select(`
SELECT kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema = $1
AND tc.table_name = $2
AND tc.constraint_type = 'PRIMARY KEY'
ORDER BY kcu.ordinal_position
`, [schemaName, tableName]);
details.primaryKeys = pks.map((p: any) => p.column_name);

// Load indexes
const idxs = await currentDb.select(`
SELECT indexname, indexdef
Expand Down Expand Up @@ -688,6 +703,16 @@ export function DatabaseExplorer({ isAddConnectionDialogOpen = false }: Database
default: c.column_default,
}));

const pks = await currentDb.select(`
SELECT column_name
FROM information_schema.key_column_usage
WHERE table_schema = DATABASE()
AND table_name = ?
AND constraint_name = 'PRIMARY'
ORDER BY ordinal_position
`, [tableName]);
details.primaryKeys = pks.map((p: any) => p.column_name);

const idxs = await currentDb.select(`SHOW INDEX FROM ${quoteIdentifier(tableName, activeConnection.type)}`);
const idxMap: Record<string, { columns: string[]; unique: boolean }> = {};
for (const idx of idxs) {
Expand Down Expand Up @@ -747,6 +772,11 @@ export function DatabaseExplorer({ isAddConnectionDialogOpen = false }: Database
nullable: c.notnull === 0,
default: c.dflt_value,
}));
// PRAGMA table_info returns `pk` as the 1-based PK ordinal (0 = not PK)
details.primaryKeys = cols
.filter((c: any) => c.pk > 0)
.sort((a: any, b: any) => a.pk - b.pk)
.map((c: any) => c.name);

const idxs = await currentDb.select(`PRAGMA index_list(${quotedTable})`);
for (const idx of idxs) {
Expand Down
Loading