дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02: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
100 рядки
3.8 KiB
TypeScript
100 рядки
3.8 KiB
TypeScript
/**
|
|
* Result-set export formats (IDEA's data extractors): CSV, TSV, JSON, SQL
|
|
* INSERT statements and a Markdown table. Values arrive as text; numeric
|
|
* columns are written unquoted where the format distinguishes them.
|
|
*/
|
|
import type { DbDriver } from "@/platform/ipc";
|
|
import { isNumericColumn } from "./Grid";
|
|
import { toCsv, toTsv, type Cell } from "./model";
|
|
import { quoteIdent } from "./sql/complete";
|
|
|
|
export type ExportFormat = "csv" | "tsv" | "json" | "sql" | "markdown";
|
|
|
|
export const EXPORT_FORMATS: ReadonlyArray<{ id: ExportFormat; label: string; extension: string }> = [
|
|
{ id: "csv", label: "CSV", extension: "csv" },
|
|
{ id: "tsv", label: "TSV", extension: "tsv" },
|
|
{ id: "json", label: "JSON", extension: "json" },
|
|
{ id: "sql", label: "SQL Inserts", extension: "sql" },
|
|
{ id: "markdown", label: "Markdown", extension: "md" },
|
|
];
|
|
|
|
export interface ExportData {
|
|
columns: ReadonlyArray<{ name: string; type: string }>;
|
|
rows: ReadonlyArray<readonly Cell[]>;
|
|
/** Target of SQL inserts (the query's table, when it reads one). */
|
|
table: string;
|
|
driver: DbDriver;
|
|
}
|
|
|
|
/** The Export Data dialog's options (CSV, TSV and Markdown; JSON and SQL keep their shape). */
|
|
export interface ExportOptions {
|
|
columnHeader: boolean;
|
|
rowHeader: boolean;
|
|
transpose: boolean;
|
|
}
|
|
|
|
export const DEFAULT_EXPORT_OPTIONS: ExportOptions = { columnHeader: true, rowHeader: false, transpose: false };
|
|
|
|
/** Header and rows of a delimited export after the row header / transpose options. */
|
|
function laidOut(d: ExportData, o: ExportOptions): { head: string[] | null; rows: Cell[][] } {
|
|
let head = d.columns.map((c) => c.name);
|
|
let rows = d.rows.map((r) => [...r]);
|
|
if (o.rowHeader) {
|
|
head = ["#", ...head];
|
|
rows = rows.map((r, i) => [String(i + 1), ...r]);
|
|
}
|
|
if (!o.transpose) return { head: o.columnHeader ? head : null, rows };
|
|
return { head: null, rows: head.map((h, c) => [...(o.columnHeader ? [h] : []), ...rows.map((r) => r[c] ?? null)]) };
|
|
}
|
|
|
|
export function exportData(
|
|
format: ExportFormat,
|
|
d: ExportData,
|
|
options: ExportOptions = DEFAULT_EXPORT_OPTIONS,
|
|
): string {
|
|
const names = d.columns.map((c) => c.name);
|
|
const numeric = d.columns.map((c, i) =>
|
|
isNumericColumn(
|
|
c.type,
|
|
d.rows.slice(0, 50).map((r) => r[i] ?? null),
|
|
),
|
|
);
|
|
switch (format) {
|
|
case "csv": {
|
|
const t = laidOut(d, options);
|
|
return toCsv(t.head, t.rows);
|
|
}
|
|
case "tsv": {
|
|
const t = laidOut(d, options);
|
|
return toTsv(t.head, t.rows);
|
|
}
|
|
case "json": {
|
|
const value = (v: Cell, i: number) =>
|
|
v === null ? null : numeric[i] && Number.isFinite(Number(v)) ? Number(v) : v;
|
|
return JSON.stringify(
|
|
d.rows.map((r) => Object.fromEntries(names.map((n, i) => [n, value(r[i] ?? null, i)]))),
|
|
null,
|
|
2,
|
|
);
|
|
}
|
|
case "sql": {
|
|
const literal = (v: Cell, i: number) =>
|
|
v === null ? "NULL" : numeric[i] && Number.isFinite(Number(v)) ? v : `'${v.replace(/'/g, "''")}'`;
|
|
const head = `INSERT INTO ${quoteIdent(d.table, d.driver)} (${names.map((n) => quoteIdent(n, d.driver)).join(", ")}) VALUES`;
|
|
return d.rows.map((r) => `${head} (${r.map(literal).join(", ")});`).join("\n");
|
|
}
|
|
case "markdown": {
|
|
const cell = (v: Cell) => (v ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
const t = laidOut(d, options);
|
|
const width = t.head?.length ?? t.rows[0]?.length ?? 0;
|
|
// A Markdown table always has a header row: blank when left out.
|
|
const head = t.head ?? Array.from({ length: width }, () => "");
|
|
const align =
|
|
options.transpose || t.head === null
|
|
? head.map(() => "---")
|
|
: [...(options.rowHeader ? ["--:"] : []), ...numeric.map((n) => (n ? "--:" : "---"))];
|
|
return [head.map(cell), align, ...t.rows.map((r) => r.map(cell))].map((r) => `| ${r.join(" | ")} |`).join("\n");
|
|
}
|
|
}
|
|
}
|