Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ All notable changes to QueryDen are documented here. This project adheres to [Se
- **[#15](https://github.com/openidle-dev/queryden/issues/15) — Argon2id parameters are now explicitly locked.** Replaced `Argon2::default()` with an explicit `Params::new(19456, 2, 1, None)` — 19 MiB memory cost, 2 iterations, 1 parallel thread, using Argon2id v1.3. These match the actual defaults of the argon2 0.5 crate and prevent silent parameter drift across crate version bumps.

### Fixed
- **PSQL Console error messages no longer flash and disappear.** Errors in the CLI execution path (PostgreSQL version unknown, download cancelled, download failed, psql not found, `\watch` errors) are now committed as persistent `psqlConsoleEntry` entries before the live-output grace period expires, preventing the output from going blank after 300ms. The `\watch` loop-end path also commits accumulated output. Fixes Windows WebView2 race where `showLiveGrace` expired without entries.
- **"Open in Explorer" now works reliably on Windows.** Empty tabs no longer silently fail — the auto-save directory is resolved as a fallback parent path. All catch blocks now show toast feedback (`showToastMessage`) instead of silent failure.
- **Error feedback added to 4 previously-silent catch blocks in DatabaseExplorer, MainContent, and ERDDialog.** Drop Role failure, DDL load failures (double-click and keyboard shortcut), ER Diagram connection failure, and SVG export failure now show error dialogs or toasts instead of silently swallowing errors.
- **Tab right-click context menu.** Right-clicking a query editor tab now shows a DataGrip-style menu: Close, Close Others, Close All, Rename, Duplicate, Copy File Path, Open in Explorer. Copy File Path copies the tab's auto-save `.sql` path to the clipboard with fallback (textarea + `execCommand`) for Linux WebKitGTK where the async clipboard API is unavailable. Open in Explorer creates the auto-save directory if it doesn't exist, then reveals the file via `revealItemInDir` with an `openPath` fallback on all platforms (Windows, Linux, macOS).
- **Inline editing SQL now quotes table/column names.** When inserting, updating, or deleting rows from the results grid, the generated `INSERT`/`UPDATE`/`DELETE` SQL uses `quoteIdentifier()` for all table names and column names — handling mixed-case identifiers, reserved words, and schema-qualified names (`"schema"."table"`) consistently across PostgreSQL, MySQL/MariaDB, SQLite, and CockroachDB.
- **Session restore tabs now visible without a connection.** Restored query tabs (from `sessions.json`) render immediately on launch — the tab strip and Monaco editor appear even before the user selects a database from the sidebar. Previously all tabs were hidden behind the `isDatabaseReady` gate, requiring a manual connection click. The toolbar (Run/Save/Format buttons) stays gated as those actions require a live database.
Expand Down
11 changes: 8 additions & 3 deletions src/components/explorer/DatabaseExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1625,7 +1625,8 @@ export function DatabaseExplorer({ isAddConnectionDialogOpen = false }: Database
}));
}
} catch (e) {
console.error("Failed to get DDL:", e);
const errMsg = typeof e === 'string' ? e : (e as any)?.message || "Failed to get DDL";
confirmDialog.dialog({ title: "Failed to Load DDL", message: errMsg, confirmLabel: "OK", type: "danger" });
}
}
if (hasChildren || isFolder || node.icon === "server") toggleExpand(node.id);
Expand Down Expand Up @@ -1771,7 +1772,10 @@ export function DatabaseExplorer({ isAddConnectionDialogOpen = false }: Database
detail: { query: ddl, name: `DDL ${node.name}` }
}));
}
} catch (e) {}
} catch (e) {
const errMsg = typeof e === 'string' ? e : (e as any)?.message || "Failed to get DDL";
confirmDialog.dialog({ title: "Failed to Load DDL", message: errMsg, confirmLabel: "OK", type: "danger" });
}
})();
}
}
Expand Down Expand Up @@ -2612,7 +2616,8 @@ export function DatabaseExplorer({ isAddConnectionDialogOpen = false }: Database
try {
await dropRole(roleName);
} catch (e: any) {
console.error("Drop role failed:", e);
const errMsg = typeof e === 'string' ? e : e?.message || String(e);
confirmDialog.dialog({ title: "Drop Role Failed", message: errMsg, confirmLabel: "OK", type: "danger" });
}
closeContextMenu();
}}
Expand Down
180 changes: 148 additions & 32 deletions src/components/layout/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -405,33 +405,52 @@ export function MainContent() {
}

async function openTabFileInExplorer(tab: QueryTab): Promise<void> {
const path = await getTabAutoSavePath(tab);
if (!path) return;
// Ensure parent directory exists on disk so the file manager can navigate to it
// Resolve path and parent directory; handle empty tabs by using the
// auto-save directory as fallback parent.
let path: string | null = null;
let parentDir: string | null = null;

try {
const { dirname } = await import("@tauri-apps/api/path");
parentDir = await dirname(path);
const { join, dirname, appDataDir } = await import("@tauri-apps/api/path");

path = await getTabAutoSavePath(tab);
if (path) {
parentDir = await dirname(path);
} else {
parentDir = settings.autoSavePath
? settings.autoSavePath
: await appDataDir().then((b: string) => join(b, "auto-save"));
}

const { mkdir } = await import("@tauri-apps/plugin-fs");
await mkdir(parentDir, { recursive: true });
if (parentDir) {
await mkdir(parentDir, { recursive: true });
}
} catch {
// can't create dir, try to open anyway
showToastMessage("Could not prepare file manager path");
}
try {
const { revealItemInDir } = await import("@tauri-apps/plugin-opener");
await revealItemInDir(path);
} catch {
// revealItemInDir failed (file doesn't exist on disk yet) — open the
// parent directory in the file manager instead
if (parentDir) {
try {
const { openPath } = await import("@tauri-apps/plugin-opener");
await openPath(parentDir);
} catch {
// swallow — can't open explorer
}

if (path) {
try {
const { revealItemInDir } = await import("@tauri-apps/plugin-opener");
await revealItemInDir(path);
return;
} catch {
showToastMessage("Could not reveal file, opening parent folder");
}
}

if (parentDir) {
try {
const { openPath } = await import("@tauri-apps/plugin-opener");
await openPath(parentDir);
return;
} catch {
showToastMessage("Could not open file manager");
}
} else {
showToastMessage("Could not open file manager");
}
}

// Close tab context menu on outside click / Escape
Expand Down Expand Up @@ -974,6 +993,21 @@ const executeQuery = useCallback(async (specificQuery?: any, statementInfo?: { l
const msg = "PostgreSQL version unknown. Connect to the database first so QueryDen can detect the server version.";
appendPsqlOutput([`ERROR: ${msg}`]);
setError(msg);
if (currentTabId) {
const errEntry: PsqlConsoleEntry = {
id: crypto.randomUUID(),
command: runningCmdRef.current || queryToRun,
outputLines: psqlOutputRef.current.length > 0 ? [...psqlOutputRef.current] : [`ERROR: ${msg}`],
hasErrors: true,
executionTime: 0,
};
updateTabState(currentTabId, {
psqlEntries: [...(currentTab?.psqlEntries || []), errEntry],
psqlOutput: [],
});
psqlOutputRef.current = [];
setPsqlOutput([]);
}
Comment thread
kix007 marked this conversation as resolved.
setIsExecuting(false);
isExecutingRef.current = false;
return;
Expand All @@ -993,8 +1027,24 @@ const executeQuery = useCallback(async (specificQuery?: any, statementInfo?: { l
type: "info",
});
if (!confirmed) {
appendPsqlOutput([`ERROR: CLI tool download cancelled.`]);
setError("CLI tool download cancelled.");
const errMsg = "CLI tool download cancelled.";
appendPsqlOutput([`ERROR: ${errMsg}`]);
setError(errMsg);
if (currentTabId) {
const errEntry: PsqlConsoleEntry = {
id: crypto.randomUUID(),
command: runningCmdRef.current || queryToRun,
outputLines: psqlOutputRef.current.length > 0 ? [...psqlOutputRef.current] : [`ERROR: ${errMsg}`],
hasErrors: true,
executionTime: 0,
};
updateTabState(currentTabId, {
psqlEntries: [...(currentTab?.psqlEntries || []), errEntry],
psqlOutput: [],
});
psqlOutputRef.current = [];
setPsqlOutput([]);
}
setIsExecuting(false);
isExecutingRef.current = false;
return;
Expand All @@ -1007,6 +1057,21 @@ const executeQuery = useCallback(async (specificQuery?: any, statementInfo?: { l
const msg = `Download failed: ${dlErr.message || String(dlErr)}`;
appendPsqlOutput([`ERROR: ${msg}`]);
setError(msg);
if (currentTabId) {
const errEntry: PsqlConsoleEntry = {
id: crypto.randomUUID(),
command: runningCmdRef.current || queryToRun,
outputLines: psqlOutputRef.current.length > 0 ? [...psqlOutputRef.current] : [`ERROR: ${msg}`],
hasErrors: true,
executionTime: 0,
};
updateTabState(currentTabId, {
psqlEntries: [...(currentTab?.psqlEntries || []), errEntry],
psqlOutput: [],
});
psqlOutputRef.current = [];
setPsqlOutput([]);
}
setIsExecuting(false);
isExecutingRef.current = false;
return;
Expand All @@ -1033,6 +1098,21 @@ const executeQuery = useCallback(async (specificQuery?: any, statementInfo?: { l
}
appendPsqlOutput([`ERROR: ${installHint}`]);
setError("psql not found");
if (currentTabId) {
const errEntry: PsqlConsoleEntry = {
id: crypto.randomUUID(),
command: runningCmdRef.current || queryToRun,
outputLines: psqlOutputRef.current.length > 0 ? [...psqlOutputRef.current] : [`ERROR: ${installHint}`],
hasErrors: true,
executionTime: 0,
};
updateTabState(currentTabId, {
psqlEntries: [...(currentTab?.psqlEntries || []), errEntry],
psqlOutput: [],
});
psqlOutputRef.current = [];
setPsqlOutput([]);
}
setIsExecuting(false);
isExecutingRef.current = false;
return;
Expand Down Expand Up @@ -1263,6 +1343,21 @@ const executeQuery = useCallback(async (specificQuery?: any, statementInfo?: { l
const msg = "\\watch cannot be used with an empty query on the PSQLWindow / console. Run a query first (e.g. SELECT 1) and then use \\watch.";
appendPsqlOutput([`ERROR: ${msg}`]);
setError(msg);
if (currentTabId) {
const errEntry: PsqlConsoleEntry = {
id: crypto.randomUUID(),
command: runningCmdRef.current || queryToRun,
outputLines: psqlOutputRef.current.length > 0 ? [...psqlOutputRef.current] : [`ERROR: ${msg}`],
hasErrors: true,
executionTime: 0,
};
updateTabState(currentTabId, {
psqlEntries: [...(currentTab?.psqlEntries || []), errEntry],
psqlOutput: [],
});
psqlOutputRef.current = [];
setPsqlOutput([]);
}
setIsExecuting(false);
isExecutingRef.current = false;
return;
Expand All @@ -1287,6 +1382,24 @@ const executeQuery = useCallback(async (specificQuery?: any, statementInfo?: { l
}
}

// Commit accumulated watch output as a persistent entry
if (currentTabId) {
const watchCmd = runningCmdRef.current || queryToRun;
const watchOutput = psqlOutputRef.current;
const watchEntry: PsqlConsoleEntry = {
id: crypto.randomUUID(),
command: watchCmd,
outputLines: watchOutput.length > 0 ? [...watchOutput] : ["(watch cancelled)"],
hasErrors: watchOutput.some(l => l.startsWith("ERROR:") || l.startsWith("FATAL:")),
executionTime: Date.now() - startTime,
};
updateTabState(currentTabId, {
psqlEntries: [...(currentTab?.psqlEntries || []), watchEntry],
psqlOutput: [],
});
psqlOutputRef.current = [];
setPsqlOutput([]);
}
setIsExecuting(false);
isExecutingRef.current = false;
return;
Expand All @@ -1308,7 +1421,8 @@ const executeQuery = useCallback(async (specificQuery?: any, statementInfo?: { l
psqlEntries: [...(currentTab?.psqlEntries || []), newEntry],
psqlOutput: [],
});
clearPsqlOutput();
psqlOutputRef.current = [];
setPsqlOutput([]);
}
setIsExecuting(false);
isExecutingRef.current = false;
Expand Down Expand Up @@ -1347,15 +1461,16 @@ const executeQuery = useCallback(async (specificQuery?: any, statementInfo?: { l
hasErrors: true,
executionTime: Date.now() - startTime,
};
updateTabState(currentTabId, {
psqlEntries: [...(currentTab?.psqlEntries || []), errEntry],
psqlOutput: [],
});
clearPsqlOutput();
}
setIsExecuting(false);
isExecutingRef.current = false;
return;
updateTabState(currentTabId, {
psqlEntries: [...(currentTab?.psqlEntries || []), errEntry],
psqlOutput: [],
});
psqlOutputRef.current = [];
setPsqlOutput([]);
}
setIsExecuting(false);
isExecutingRef.current = false;
return;
}
if (currentTabId) updateTabState(currentTabId, { statementResults });
}
Expand Down Expand Up @@ -2773,6 +2888,7 @@ const executeQuery = useCallback(async (specificQuery?: any, statementInfo?: { l
try {
await connectToDatabase(activeTab.target.connectionId, activeTab.target.database);
} catch {
showToastMessage("Failed to connect — cannot open ER Diagram");
return;
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/components/tools/ERDDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "lucide-react";
import { useConnections } from "../../contexts/useConnections";
import { Dialog } from "../ui/Dialog";
import { useConfirmDialog } from "../ui/ConfirmDialog";
import { IconButton } from "../ui/IconButton";
import { Input } from "../ui/Input";
import { ERDCanvas } from "./ERDCanvas";
Expand Down Expand Up @@ -88,6 +89,7 @@ function getRelatedTableIds(
export function ERDDialog({ isOpen, onClose }: ERDDialogProps) {
const { currentDb, activeConnection, selectedDatabase, schemaItems } =
useConnections();
const confirmDialog = useConfirmDialog();

const [selectedTables, setSelectedTables] = useState<string[]>([]);
const [showSelector, setShowSelector] = useState(true);
Expand Down Expand Up @@ -272,8 +274,9 @@ export function ERDDialog({ isOpen, onClose }: ERDDialogProps) {
if (path) {
await writeTextFile(path, svgData);
}
} catch {
// export failed silently
} catch (e: any) {
const errMsg = e?.message || String(e);
confirmDialog.dialog({ title: "Export Failed", message: errMsg, confirmLabel: "OK", type: "danger" });
}
}, [selectedDatabase]);

Expand Down