* chore: bump version to 0.7.5 (Task 1.1) * feat(db): pure object DDL/search/depend builders (Task 1.2) * feat(models): ObjectSearchHit, DependencyInfo, PgToolPaths, tool source fields (Task 1.3) * feat(backup): bundle-aware pg tool resolution, system-first fallback (Task 2.1) * feat(objects): schema CRUD commands + integration test (Task 2.2) * feat(objects): search_objects command (current-schema, all types) (Task 2.3) * feat(objects): get_object_ddl for all browsable types (Task 2.4) * feat(objects): pg_depend object dependencies + schema contents (Task 2.5) * feat(ipc): register object management commands (Task 3.1) * feat(ipc): frontend wrappers + types for object management (Task 3.2) * feat(build): declare bundled pg_tools resources (Task 3.3) * test(capabilities): lock objects capability PG-only for search/ddl/dependencies (Task 3.4) * feat(ui): Cmd+K object search palette in DB viewer (Task 4.1) * feat(ui): schema CRUD menu + dependency dialog (Task 4.2) * feat(ui): copy-as-DDL + dependency view context menu in Objects (Task 4.3) * feat(ui): dependency check before table drop + bundled-tool status (Task 4.4) * docs: v0.7.5 release notes + status table + bundled-tools (Task 5.1) * ci: build/verify bundled pg client tools per platform before tauri build (Task 5.2) * fix(objects): report pg_rewrite dependencies as the dependent view (pg_class) pg_depend records view dependencies via the view's internal rewrite rule (classid = pg_rewrite). The user-facing dependent object is the VIEW itself, so map that classid to pg_class (name still resolved through ev_class). Fixes the live-PG integration test object_dependencies_for_table_includes_view and makes the dependency dialog readable for view dependents. * test: add idempotent PG integration-test seed script Seeds the first PG test db (GRIDLINE_TEST_SRC) with the objects the #[ignore] integration tests assert against: users (+users_id_seq), products (3 rows), orders (3 rows), order_summary view, audit_log + get_user functions, user_role enum. Idempotent: DROP + recreate. * fix(build): regenerate multi-resolution icons + roadmap Keychain item - icon.ico previously contained a single 16x16 frame (Windows scaled it up -> blurry taskbar/start-menu icon). Regenerated from the 512px source via 'tauri icon': ICO now has 16/24/32/48/64/256 frames, icns has full @2x coverage up to 1024px, PNGs re-rendered from the same source. - Added icons/icon.png (512px) to bundle.icon so Linux hicolor installs a high-DPI entry. - ROADMAP: log the 'Enable Keychain' toggle (currently a form-only placeholder) under Next up -> Connection & credentials. * docs(readme): dynamic version-free download links + 3-col table - release.yml: set releaseAssetNamePattern to '[name]_[platform]_[arch][setup][ext]' (version-free), so asset filenames are stable across releases: Gridline_darwin_aarch64.dmg, Gridline_windows_x64-setup.exe, Gridline_linux_amd64.deb, Gridline_linux_x86_64.rpm, etc. - README: both download tables now 3-column (OS | Architecture | Download) and link via GitHub's releases/latest/download/<file> redirect — they always point at the newest published release, no per-release edits. - AGENTS.md: releases checklist updated — download links stay version-free. * docs(readme): platform-per-column download table (macOS | Windows | Linux) Table now mirrors the release layout: one column per OS with a logo row and a download-links row underneath. Links stay version-free via releases/latest/download/<file> (releaseAssetNamePattern in release.yml). * docs(readme): revert download table to clean image-less 3-col layout Logo row was hard to read on GitHub dark mode; the plain OS | Architecture | Download table is cleaner and still uses dynamic releases/latest links.
335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
|
|
vi.mock("@tauri-apps/api/core", () => ({
|
|
invoke: vi.fn().mockResolvedValue({ tables: [], relationships: [] }),
|
|
}));
|
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import {
|
|
testConnection,
|
|
dbConnect,
|
|
dbDisconnect,
|
|
getDatabases,
|
|
getSchemas,
|
|
getTables,
|
|
getTableData,
|
|
executeChange,
|
|
refreshConnection,
|
|
getSchemaGraph,
|
|
executeQuery,
|
|
getQueryHistory,
|
|
clearQueryHistory,
|
|
setHistoryFavorite,
|
|
saveQuery,
|
|
getSavedQueries,
|
|
updateSavedQuery,
|
|
deleteSavedQuery,
|
|
setConnectionFavorite,
|
|
recordRecentConnection,
|
|
getRecentConnections,
|
|
clearRecentConnections,
|
|
getIndexes,
|
|
getConstraints,
|
|
createSchema,
|
|
renameSchema,
|
|
dropSchema,
|
|
searchObjects,
|
|
getObjectDdl,
|
|
getObjectDependencies,
|
|
} from "./commands";
|
|
import type { SchemaGraph } from "./types";
|
|
import type { QueryHistoryEntry } from "./commands";
|
|
|
|
describe("commands", () => {
|
|
it("testConnection has correct signature", () => {
|
|
expect(typeof testConnection).toBe("function");
|
|
});
|
|
|
|
it("dbConnect returns void promise", () => {
|
|
expect(typeof dbConnect).toBe("function");
|
|
});
|
|
|
|
it("dbDisconnect returns void promise", () => {
|
|
expect(typeof dbDisconnect).toBe("function");
|
|
});
|
|
|
|
it("getDatabases returns string array promise", () => {
|
|
expect(typeof getDatabases).toBe("function");
|
|
});
|
|
|
|
it("getSchemas returns string array promise", () => {
|
|
expect(typeof getSchemas).toBe("function");
|
|
});
|
|
|
|
it("getTables returns TableInfo array promise", () => {
|
|
expect(typeof getTables).toBe("function");
|
|
});
|
|
|
|
it("getTableData returns QueryResult-shaped promise", () => {
|
|
expect(typeof getTableData).toBe("function");
|
|
});
|
|
|
|
it("executeChange returns void promise", () => {
|
|
expect(typeof executeChange).toBe("function");
|
|
});
|
|
|
|
it("refreshConnection returns full tree promise", () => {
|
|
expect(typeof refreshConnection).toBe("function");
|
|
});
|
|
|
|
describe("getSchemaGraph", () => {
|
|
it("is a callable function with correct signature", () => {
|
|
expect(typeof getSchemaGraph).toBe("function");
|
|
const result: Promise<SchemaGraph> = getSchemaGraph("conn-1", "public");
|
|
expect(result).toBeInstanceOf(Promise);
|
|
});
|
|
|
|
it("accepts schema as optional", () => {
|
|
const result: Promise<SchemaGraph> = getSchemaGraph("conn-1");
|
|
expect(result).toBeInstanceOf(Promise);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("query commands", () => {
|
|
it("executeQuery calls invoke with correct args", async () => {
|
|
const mockResult = { columns: [], rows: [], total_rows: 0, page: 1, page_size: 50 };
|
|
vi.mocked(invoke).mockResolvedValueOnce(mockResult);
|
|
|
|
const result = await executeQuery("conn-1", "SELECT 1", 1, 50);
|
|
|
|
expect(invoke).toHaveBeenCalledWith("execute_query", {
|
|
connectionId: "conn-1",
|
|
query: "SELECT 1",
|
|
page: 1,
|
|
pageSize: 50,
|
|
});
|
|
expect(result).toEqual(mockResult);
|
|
});
|
|
|
|
it("getQueryHistory calls invoke", async () => {
|
|
const mockHistory = [{ id: "h1", connection_id: "c1", query_text: "SELECT 1", status: "success", executed_at: "2025-01-01" }];
|
|
vi.mocked(invoke).mockResolvedValueOnce(mockHistory);
|
|
const result = await getQueryHistory("conn-1", 10, 0);
|
|
expect(invoke).toHaveBeenCalledWith("get_query_history", { connectionId: "conn-1", limit: 10, offset: 0 });
|
|
expect(result).toEqual(mockHistory);
|
|
});
|
|
|
|
it("clearQueryHistory calls invoke", async () => {
|
|
vi.mocked(invoke).mockResolvedValueOnce(undefined);
|
|
await clearQueryHistory("conn-1");
|
|
expect(invoke).toHaveBeenCalledWith("clear_query_history", { connectionId: "conn-1" });
|
|
});
|
|
|
|
it("getQueryHistory with null calls invoke with null connectionId", async () => {
|
|
vi.mocked(invoke).mockResolvedValueOnce([]);
|
|
await getQueryHistory(null, 50, 0);
|
|
expect(invoke).toHaveBeenCalledWith("get_query_history", {
|
|
connectionId: null,
|
|
limit: 50,
|
|
offset: 0,
|
|
});
|
|
});
|
|
|
|
it("clearQueryHistory with null clears all", async () => {
|
|
vi.mocked(invoke).mockResolvedValueOnce(undefined);
|
|
await clearQueryHistory(null);
|
|
expect(invoke).toHaveBeenCalledWith("clear_query_history", {
|
|
connectionId: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Query History — v6", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("QueryHistoryEntry includes favorite field", () => {
|
|
const entry: QueryHistoryEntry = {
|
|
id: "h1",
|
|
connection_id: "c1",
|
|
query_text: "SELECT 1",
|
|
execution_time_ms: 42,
|
|
row_count: 5,
|
|
status: "success",
|
|
error_message: null,
|
|
executed_at: "2026-01-01T00:00:00Z",
|
|
favorite: false, // NEW — must be accepted
|
|
};
|
|
expect(entry.favorite).toBe(false);
|
|
});
|
|
|
|
it("setHistoryFavorite calls invoke with correct params", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await setHistoryFavorite("entry-id-1", "conn-abc");
|
|
expect(mockInvoke).toHaveBeenCalledWith("set_history_favorite", {
|
|
id: "entry-id-1",
|
|
connectionId: "conn-abc",
|
|
});
|
|
});
|
|
|
|
it("saveQuery calls invoke with correct params", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await saveQuery({
|
|
connectionId: "conn-xyz",
|
|
name: "My Saved Query",
|
|
queryText: "SELECT * FROM users",
|
|
folder: "reports",
|
|
});
|
|
expect(mockInvoke).toHaveBeenCalledWith("save_query", {
|
|
connectionId: "conn-xyz",
|
|
name: "My Saved Query",
|
|
queryText: "SELECT * FROM users",
|
|
folder: "reports",
|
|
});
|
|
});
|
|
|
|
it("saveQuery accepts null connectionId for global queries", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await saveQuery({
|
|
connectionId: null,
|
|
name: "Global query",
|
|
queryText: "SELECT 1",
|
|
folder: "",
|
|
});
|
|
expect(mockInvoke).toHaveBeenCalledWith("save_query", {
|
|
connectionId: null,
|
|
name: "Global query",
|
|
queryText: "SELECT 1",
|
|
folder: "",
|
|
});
|
|
});
|
|
|
|
it("getSavedQueries calls invoke with correct params", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue([]);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await getSavedQueries("conn-123");
|
|
expect(mockInvoke).toHaveBeenCalledWith("get_saved_queries", {
|
|
connectionId: "conn-123",
|
|
});
|
|
});
|
|
|
|
it("getSavedQueries accepts null for all-connections", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue([]);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await getSavedQueries(null);
|
|
expect(mockInvoke).toHaveBeenCalledWith("get_saved_queries", {
|
|
connectionId: null,
|
|
});
|
|
});
|
|
|
|
it("updateSavedQuery calls invoke with correct params", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await updateSavedQuery("q-id", { name: "Renamed" });
|
|
expect(mockInvoke).toHaveBeenCalledWith("update_saved_query", {
|
|
id: "q-id",
|
|
patch: { name: "Renamed" },
|
|
});
|
|
});
|
|
|
|
it("deleteSavedQuery calls invoke with correct params", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await deleteSavedQuery("q-id");
|
|
expect(mockInvoke).toHaveBeenCalledWith("delete_saved_query", {
|
|
id: "q-id",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("v0.5.0 command wrappers", () => {
|
|
it("setConnectionFavorite invokes set_connection_favorite with camelCase", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await setConnectionFavorite("c1", true);
|
|
expect(mockInvoke).toHaveBeenCalledWith("set_connection_favorite", { connectionId: "c1", favorite: true });
|
|
});
|
|
|
|
it("recordRecentConnection invokes record_recent_connection", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await recordRecentConnection("c1");
|
|
expect(mockInvoke).toHaveBeenCalledWith("record_recent_connection", { connectionId: "c1" });
|
|
});
|
|
|
|
it("getRecentConnections invokes get_recent_connections with limit", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue([]);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await getRecentConnections(8);
|
|
expect(mockInvoke).toHaveBeenCalledWith("get_recent_connections", { limit: 8 });
|
|
});
|
|
|
|
it("clearRecentConnections invokes clear_recent_connections", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue(undefined);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await clearRecentConnections();
|
|
expect(mockInvoke).toHaveBeenCalledWith("clear_recent_connections", {});
|
|
});
|
|
|
|
it("getIndexes invokes get_indexes with connectionId + schema", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue([]);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await getIndexes("c1", "public");
|
|
expect(mockInvoke).toHaveBeenCalledWith("get_indexes", { connectionId: "c1", schema: "public" });
|
|
});
|
|
|
|
it("getConstraints invokes get_constraints with connectionId + schema", async () => {
|
|
const mockInvoke = vi.fn().mockResolvedValue([]);
|
|
vi.mocked(invoke).mockImplementation(mockInvoke);
|
|
|
|
await getConstraints("c1", "public");
|
|
expect(mockInvoke).toHaveBeenCalledWith("get_constraints", { connectionId: "c1", schema: "public" });
|
|
});
|
|
});
|
|
|
|
describe("object management commands (v0.7.5)", () => {
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
it("createSchema calls invoke with name", async () => {
|
|
vi.mocked(invoke).mockResolvedValueOnce(undefined);
|
|
await createSchema("c1", "my_schema");
|
|
expect(invoke).toHaveBeenCalledWith("create_schema", { connectionId: "c1", name: "my_schema" });
|
|
});
|
|
it("renameSchema maps old/new names", async () => {
|
|
vi.mocked(invoke).mockResolvedValueOnce(undefined);
|
|
await renameSchema("c1", "old", "new");
|
|
expect(invoke).toHaveBeenCalledWith("rename_schema", { connectionId: "c1", oldName: "old", newName: "new" });
|
|
});
|
|
it("dropSchema passes cascade", async () => {
|
|
vi.mocked(invoke).mockResolvedValueOnce(undefined);
|
|
await dropSchema("c1", "s", true);
|
|
expect(invoke).toHaveBeenCalledWith("drop_schema", { connectionId: "c1", name: "s", cascade: true });
|
|
});
|
|
it("searchObjects maps query+schema", async () => {
|
|
vi.mocked(invoke).mockResolvedValueOnce([]);
|
|
await searchObjects("c1", "public", "user");
|
|
expect(invoke).toHaveBeenCalledWith("search_objects", { connectionId: "c1", schema: "public", query: "user" });
|
|
});
|
|
it("getObjectDdl maps objectType+name", async () => {
|
|
vi.mocked(invoke).mockResolvedValueOnce("CREATE SEQUENCE ...");
|
|
await getObjectDdl("c1", "public", "sequence", "users_id_seq");
|
|
expect(invoke).toHaveBeenCalledWith("get_object_ddl", { connectionId: "c1", schema: "public", objectType: "sequence", name: "users_id_seq" });
|
|
});
|
|
it("getObjectDependencies maps objectType+name", async () => {
|
|
vi.mocked(invoke).mockResolvedValueOnce([]);
|
|
await getObjectDependencies("c1", "public", "table", "orders");
|
|
expect(invoke).toHaveBeenCalledWith("get_object_dependencies", { connectionId: "c1", schema: "public", objectType: "table", name: "orders" });
|
|
});
|
|
}); |