Файли
openidea/src/features/database/nodes.ts
T
l0ng-ai 575749f2ac feat(database): Add query plans, history, data tools and schema objects (#63)
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
2026-10-27 13:31:29 +08:00

99 рядки
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;
}
}