v0.7.5: bundled PG tools, schema CRUD, object search, copy-as-DDL, object dependencies (#11)

* 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.
This commit is contained in:
2026-08-05 20:46:36 +08:00
committed by GitHub
parent cbc54119ed
commit 04e8ed3300
67 changed files with 2084 additions and 108 deletions
+11
View File
@@ -0,0 +1,11 @@
import { describe, it, expect } from "vitest";
import tauriConf from "../../src-tauri/tauri.conf.json";
describe("tauri bundle config (v0.7.5)", () => {
it("declares bundled pg_tools resources", () => {
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
});
it("version is 0.7.5", () => {
expect(tauriConf.version).toBe("0.7.5");
});
});
+41
View File
@@ -30,6 +30,12 @@ import {
clearRecentConnections,
getIndexes,
getConstraints,
createSchema,
renameSchema,
dropSchema,
searchObjects,
getObjectDdl,
getObjectDependencies,
} from "./commands";
import type { SchemaGraph } from "./types";
import type { QueryHistoryEntry } from "./commands";
@@ -291,4 +297,39 @@ describe("v0.5.0 command wrappers", () => {
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" });
});
});
+22 -1
View File
@@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import type { Connection, ConnectionInput, ConnectionTestResult, Folder, FolderInput, Tag, TagInput, Settings, ImportResult, TableInfo, QueryResult, BackupOptions, RestoreOptions, SyncOptions, PgToolStatus, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, SchemaGraph, IndexInfo, ConstraintInfo, RecentConnection } from "./types";
import type { Connection, ConnectionInput, ConnectionTestResult, Folder, FolderInput, Tag, TagInput, Settings, ImportResult, TableInfo, QueryResult, BackupOptions, RestoreOptions, SyncOptions, PgToolStatus, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, SchemaGraph, IndexInfo, ConstraintInfo, RecentConnection, ObjectSearchHit, DependencyInfo } from "./types";
import type { FilterRule, SortRule } from "../stores/dbViewerStore";
import type { ChangePayload } from "./changePayload";
@@ -298,4 +298,25 @@ export async function getIndexes(connectionId: string, schema?: string): Promise
export async function getConstraints(connectionId: string, schema?: string): Promise<ConstraintInfo[]> {
return invoke<ConstraintInfo[]>("get_constraints", { connectionId, schema });
}
// ─── v0.7.5: Object management (schemas, search, DDL, dependencies) ──
export async function createSchema(connectionId: string, name: string): Promise<void> {
return invoke<void>("create_schema", { connectionId, name });
}
export async function renameSchema(connectionId: string, oldName: string, newName: string): Promise<void> {
return invoke<void>("rename_schema", { connectionId, oldName, newName });
}
export async function dropSchema(connectionId: string, name: string, cascade: boolean): Promise<void> {
return invoke<void>("drop_schema", { connectionId, name, cascade });
}
export async function searchObjects(connectionId: string, schema: string, query: string): Promise<ObjectSearchHit[]> {
return invoke<ObjectSearchHit[]>("search_objects", { connectionId, schema, query });
}
export async function getObjectDdl(connectionId: string, schema: string, objectType: string, name: string): Promise<string> {
return invoke<string>("get_object_ddl", { connectionId, schema, objectType, name });
}
export async function getObjectDependencies(connectionId: string, schema: string, objectType: string, name: string): Promise<DependencyInfo[]> {
return invoke<DependencyInfo[]>("get_object_dependencies", { connectionId, schema, objectType, name });
}
+7
View File
@@ -50,4 +50,11 @@ describe("dbCapabilities", () => {
expect(getCapabilities("postgresql")).toBe(DB_CAPABILITIES.postgresql);
expect(getCapabilities("redis")).toBe(DB_CAPABILITIES.redis);
});
it("objects capability (search/ddl/dependencies) is PG-only", () => {
expect(getCapabilities("postgresql").objects).toBe(true);
expect(getCapabilities("mysql").objects).toBe(false);
expect(getCapabilities("sqlite").objects).toBe(false);
expect(getCapabilities("redis").objects).toBe(false);
});
});
+18 -3
View File
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";
import agents from "../../AGENTS.md?raw";
import readme from "../../README.md?raw";
describe("v0.7.0 docs coverage", () => {
describe("v0.7.5 docs coverage", () => {
it("AGENTS.md marks inline cell editing complete", () => {
expect(agents).toContain("Inline cell editing");
expect(agents).toMatch(/Inline cell editing \| ✅/);
@@ -24,8 +24,23 @@ describe("v0.7.0 docs coverage", () => {
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
});
it("README declares v0.7.0", () => {
expect(readme).toContain("0.7.0");
it("README declares v0.7.5", () => {
expect(readme).toContain("0.7.5");
});
it("AGENTS.md marks schema CRUD complete", () => {
expect(agents).toMatch(/Schema CRUD \| ✅/);
});
it("AGENTS.md marks global object search complete", () => {
expect(agents).toMatch(/Global object search \| ✅/);
});
it("AGENTS.md marks copy-as-DDL complete", () => {
expect(agents).toMatch(/Copy as DDL for any object \| ✅/);
});
it("AGENTS.md marks object dependencies complete", () => {
expect(agents).toMatch(/Object dependencies \| ✅/);
});
it("README notes bundled PostgreSQL tools", () => {
expect(readme.toLowerCase()).toContain("bundled");
});
it("README marks inline editing complete (not Upcoming)", () => {
// Key Features lists inline editing as a shipped feature
+28
View File
@@ -305,6 +305,34 @@ export interface PgToolStatus {
pg_restore_found: boolean;
pg_dump_version: string | null;
pg_restore_version: string | null;
pg_dump_source: string | null;
pg_restore_source: string | null;
}
export type PgObjectType =
| "table" | "view" | "materialized view" | "function" | "procedure"
| "trigger" | "sequence" | "enum" | "extension" | "index" | "constraint";
export type ObjectType =
| "functions"
| "triggers"
| "sequences"
| "enums"
| "extensions"
| "indexes"
| "constraints"
| "procedures";
export interface ObjectSearchHit {
name: string;
schema: string;
object_type: string; // TABLE | VIEW | MATERIALIZED VIEW | FUNCTION | PROCEDURE | TRIGGER | SEQUENCE | ENUM | EXTENSION | INDEX | CONSTRAINT
}
export interface DependencyInfo {
deptype: string;
class: string;
name: string;
}
export interface BackupJob {
+2 -2
View File
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import pkg from "../../package.json";
describe("version", () => {
it("declares v0.7.0 across the app shell", () => {
expect(pkg.version).toBe("0.7.0");
it("declares v0.7.5 across the app shell", () => {
expect(pkg.version).toBe("0.7.5");
});
});