mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
New query, data and schema tools for the Database feature (SQLite, PostgreSQL, MySQL). ## Query console - **Explain Plan** tab: SQLite `EXPLAIN QUERY PLAN`, PostgreSQL `EXPLAIN (FORMAT JSON)`, MySQL `EXPLAIN`, parsed in `ide-db::plan` into an operation tree (Full Scan / Index Scan / Group By / Order By …) under a statement root, with a properties panel. - **Query history**: `ide-db::history` keeps executed statements per project and source in `db-history.json` (deduplicated, newest first, 500 max). The Browse Query History dialog has search, a highlighted preview, Paste and Delete. - Output reports `(execution: X ms, fetching: Y ms)`. ## Results and table editor - Local column filter (value list with counts) and local sort. - Record / Value / Aggregates side panel. - Chart view (bar / line) with X / Y / aggregation settings. - Export Data dialog: extractor, Transpose, headers, preview, file or clipboard. An extractor drop-down sits on the data toolbars. - Import Data from a CSV / TSV file. - Table editor gets a WHERE | ORDER BY row. Column headers fit their names, with filter and sort indicators. Whole SQLite REALs display as `3108`, not `3108.0`. ## Database tree - Triggers under tables. Routines, sequences and materialized views under schemas, with DDL, Quick Documentation and Drop. - Data source options: object filter, show template databases, single database mode (PostgreSQL). - New ▸ Import from Clipboard (connection URLs). ## IPC New commands: `db_schema_objects`, `db_object_ddl`, `db_explain`, `db_history`, `db_remove_history`. Bindings are regenerated. ## Testing - `cargo test -p ide-db`: plan parsing, history store, trigger listing and DDL, explain without executing. - `cargo clippy -p ide-db -p openidea --all-targets -- -D warnings` is clean. - `pnpm typecheck` and eslint pass. `pnpm vitest run src/features/database src/ui`: 226 passed. - Checked in the running app against a SQLite database: - tree with triggers - result grid, local filter, side panel - chart - Explain Plan - history dialog - Export Data dialog - table editor ORDER BY - CSV import Merge follow-up: grouped the new `--chart-series` token under a section comment and added it to the theme plugin template (theme.test.ts on main), regenerated docs/plugins/themes.md. Re-verified after merging main on macOS with a SQLite source: query + result grid with filter/sort headers, Explain Plan tab (Full Scan / Index Scan / Group By / Order By), Query History dialog; `cargo nextest run -p ide-db` 45 passed, bindings unchanged after regeneration. https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
99 lines
4.2 KiB
TypeScript
99 lines
4.2 KiB
TypeScript
/** Nodes of the Database tool window tree and the objects they stand for. */
|
|
import type {
|
|
DataSource,
|
|
DbColumnInfo,
|
|
DbForeignKey,
|
|
DbIndexInfo,
|
|
DbObjectKind as DbSchemaObjectKind,
|
|
DbRoutineInfo,
|
|
DbSequenceInfo,
|
|
DbTableInfo,
|
|
DbTableKind,
|
|
DbTriggerInfo,
|
|
} from "@/platform/ipc";
|
|
import type { DbObjectRef } from "./objects";
|
|
|
|
export type Group = "columns" | "keys" | "foreignKeys" | "indexes" | "triggers";
|
|
|
|
/** Folders under a schema: IDEA's `tables`, `views`, `materialized views`, `routines`, `sequences`. */
|
|
export type Folder = "tables" | "views" | "materializedViews" | "routines" | "sequences";
|
|
|
|
/** The folder a table, view or materialized view is listed in. */
|
|
export function tableFolder(kind: DbTableKind): Folder {
|
|
return kind === "view" ? "views" : kind === "materializedView" ? "materializedViews" : "tables";
|
|
}
|
|
|
|
/** Where a table-level node lives: connection target, schema, table. */
|
|
interface InTable {
|
|
id: string;
|
|
schema: string;
|
|
table: string;
|
|
}
|
|
|
|
export type DbNode =
|
|
| { kind: "source"; key: string; source: DataSource }
|
|
/** A database of a PostgreSQL server; `id` is its connection target. */
|
|
| { kind: "database"; key: string; id: string; name: string; current: boolean }
|
|
| { kind: "schema"; key: string; id: string; schema: string }
|
|
| { kind: "folder"; key: string; id: string; schema: string; folder: Folder }
|
|
| { kind: "table"; key: string; id: string; schema: string; table: DbTableInfo }
|
|
| { kind: "routine"; key: string; id: string; schema: string; routine: DbRoutineInfo }
|
|
| { kind: "sequence"; key: string; id: string; schema: string; sequence: DbSequenceInfo }
|
|
| ({ kind: "group"; key: string; group: Group } & InTable)
|
|
| ({ kind: "column"; key: string; column: DbColumnInfo; foreignKey: boolean } & InTable)
|
|
| ({ kind: "key"; key: string; columns: string[] } & InTable)
|
|
| ({ kind: "foreignKey"; key: string; fk: DbForeignKey } & InTable)
|
|
| ({ kind: "index"; key: string; index: DbIndexInfo } & InTable)
|
|
| ({ kind: "trigger"; key: string; trigger: DbTriggerInfo } & InTable)
|
|
| { kind: "message"; key: string; text: string; error?: boolean };
|
|
|
|
/** The connection target a node belongs to. */
|
|
export function nodeTarget(n: DbNode): string | null {
|
|
if (n.kind === "source") return n.source.id ?? null;
|
|
if ("id" in n && typeof n.id === "string") return n.id;
|
|
return n.key.split("/")[0] ?? null;
|
|
}
|
|
|
|
/** The table (or view) a node is, or is inside of. */
|
|
export function tableOf(
|
|
n: DbNode,
|
|
): { id: string; schema: string; table: string; view: boolean; kind: DbTableKind } | null {
|
|
if (n.kind === "table")
|
|
return { id: n.id, schema: n.schema, table: n.table.name, view: n.table.kind !== "table", kind: n.table.kind };
|
|
if ("table" in n && typeof n.table === "string")
|
|
return { id: n.id, schema: n.schema, table: n.table, view: false, kind: "table" };
|
|
return null;
|
|
}
|
|
|
|
/** A routine, trigger or sequence node: what Go to DDL / Quick Documentation read. */
|
|
export function schemaObjectOf(
|
|
n: DbNode,
|
|
): { id: string; schema: string; kind: DbSchemaObjectKind; name: string } | null {
|
|
if (n.kind === "routine") return { id: n.id, schema: n.schema, kind: n.routine.kind, name: n.routine.name };
|
|
if (n.kind === "sequence") return { id: n.id, schema: n.schema, kind: "sequence", name: n.sequence.name };
|
|
if (n.kind === "trigger") return { id: n.id, schema: n.schema, kind: "trigger", name: n.trigger.name };
|
|
return null;
|
|
}
|
|
|
|
/** The schema object a node stands for (Rename, Drop, Copy Reference). */
|
|
export function objectOf(n: DbNode): DbObjectRef | null {
|
|
switch (n.kind) {
|
|
case "schema":
|
|
return { kind: "schema", schema: n.schema, name: n.schema };
|
|
case "table":
|
|
return { kind: n.table.kind, schema: n.schema, name: n.table.name };
|
|
case "column":
|
|
return { kind: "column", schema: n.schema, name: n.table, column: n.column.name };
|
|
case "index":
|
|
return { kind: "index", schema: n.schema, name: n.index.name, table: n.table };
|
|
case "routine":
|
|
return { kind: n.routine.kind, schema: n.schema, name: n.routine.name, arguments: n.routine.arguments };
|
|
case "sequence":
|
|
return { kind: "sequence", schema: n.schema, name: n.sequence.name };
|
|
case "trigger":
|
|
return { kind: "trigger", schema: n.schema, name: n.trigger.name, table: n.table };
|
|
default:
|
|
return null;
|
|
}
|
|
}
|