дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
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
Цей коміт міститься в:
1 джерело
580f0efc28
коміт
575749f2ac
48 files changed
+4268
-289
No files matched your search
@@ -130,7 +130,7 @@ export function sqlCandidates({ model, analysis: a, driver, sourceName }: Comple
|
||||
eq(t.schema, defaultSchema) || driver === "sqlite" ? q(t.name) : `${q(t.schema)}.${q(t.name)}`;
|
||||
const tableCandidate = (t: DbCompletionTable, boost: number, qualified = false): SqlCandidate => ({
|
||||
label: t.name,
|
||||
kind: t.kind === "view" ? "view" : "table",
|
||||
kind: t.kind !== "table" ? "view" : "table",
|
||||
tail: t.schema ? `(${t.schema})` : undefined,
|
||||
type: sourceName,
|
||||
insert: qualified ? `${q(t.schema)}.${q(t.name)}` : tableText(t),
|
||||
@@ -192,7 +192,7 @@ export function sqlCandidates({ model, analysis: a, driver, sourceName }: Comple
|
||||
for (const t of tables) {
|
||||
schemas.add(t.schema);
|
||||
const local = eq(t.schema, defaultSchema) || driver === "sqlite";
|
||||
out.push(tableCandidate(t, local ? (t.kind === "view" ? 28 : 30) : 0, !local));
|
||||
out.push(tableCandidate(t, local ? (t.kind !== "table" ? 28 : 30) : 0, !local));
|
||||
}
|
||||
for (const s of schemas)
|
||||
if (!eq(s, defaultSchema) || schemas.size > 1)
|
||||
@@ -204,7 +204,9 @@ export function sqlCandidates({ model, analysis: a, driver, sourceName }: Comple
|
||||
case "afterTable": {
|
||||
if (!ctx.ref.alias && ctx.ref.kind === "table" && a.verb !== "INSERT" && a.verb !== "REPLACE") {
|
||||
// IDEA suggests an alias first.
|
||||
const taken = new Set(a.refs.filter((r) => r !== ctx.ref).flatMap((r) => [refName(r).toLowerCase(), r.name.toLowerCase()]));
|
||||
const taken = new Set(
|
||||
a.refs.filter((r) => r !== ctx.ref).flatMap((r) => [refName(r).toLowerCase(), r.name.toLowerCase()]),
|
||||
);
|
||||
out.push({ label: aliasFor(ctx.ref.name, taken), kind: "alias", boost: 45 });
|
||||
}
|
||||
if (a.verb === "UPDATE") keywords(["SET", "AS"], 20);
|
||||
@@ -237,7 +239,15 @@ export function sqlCandidates({ model, analysis: a, driver, sourceName }: Comple
|
||||
// The columns an INSERT needs (no default), then all of them.
|
||||
const required = all.filter((c) => !c.hasDefault).map((c) => c.name);
|
||||
if (required.length > 1) out.push(columnsCandidate(ctx.target, required, 60, false));
|
||||
if (all.length > 1 && all.length !== required.length) out.push(columnsCandidate(ctx.target, all.map((c) => c.name), 55, false));
|
||||
if (all.length > 1 && all.length !== required.length)
|
||||
out.push(
|
||||
columnsCandidate(
|
||||
ctx.target,
|
||||
all.map((c) => c.name),
|
||||
55,
|
||||
false,
|
||||
),
|
||||
);
|
||||
out.push(...all.map((c) => columnCandidate(c, ownerOf(ctx.target), t, 50)));
|
||||
keywords(["SELECT", "VALUES"], 5);
|
||||
return out;
|
||||
@@ -271,14 +281,21 @@ export function sqlCandidates({ model, analysis: a, driver, sourceName }: Comple
|
||||
seen.add(key);
|
||||
const ambiguous = r.depth > 0 || (count.get(c.name.toLowerCase()) ?? 0) > 1;
|
||||
out.push(
|
||||
columnCandidate(c, owner, t, r.depth === 0 ? 40 : 20, ambiguous ? `${q(refName(r))}.${q(c.name)}` : undefined),
|
||||
columnCandidate(
|
||||
c,
|
||||
owner,
|
||||
t,
|
||||
r.depth === 0 ? 40 : 20,
|
||||
ambiguous ? `${q(refName(r))}.${q(c.name)}` : undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (assigning) continue;
|
||||
out.push({ label: refName(r), kind: "alias", boost: 35 });
|
||||
// The whole column list (IDEA's "expand *" item in the select list).
|
||||
const names = columnsOf(r).map((c) => c.name);
|
||||
if (clause === "select" && r.depth === 0 && names.length > 1) out.push(columnsCandidate(r, names, 30, inner.length > 1));
|
||||
if (clause === "select" && r.depth === 0 && names.length > 1)
|
||||
out.push(columnsCandidate(r, names, 30, inner.length > 1));
|
||||
}
|
||||
if (assigning) return;
|
||||
if (!inner.length && a.fromInsert !== null && clause === "select") {
|
||||
@@ -478,5 +495,5 @@ function tableDoc(t: DbCompletionTable): string {
|
||||
const cols = t.columns.map(
|
||||
(c) => ` ${c.name.padEnd(width)} ${c.dataType}${c.nullable ? "" : " NOT NULL"}${c.primaryKey ? " PK" : ""}`,
|
||||
);
|
||||
return `${t.kind === "view" ? "VIEW" : "TABLE"} ${t.schema}.${t.name}\n${cols.join("\n")}`;
|
||||
return `${t.kind === "materializedView" ? "MATERIALIZED VIEW" : t.kind === "view" ? "VIEW" : "TABLE"} ${t.schema}.${t.name}\n${cols.join("\n")}`;
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача