дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +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
@@ -141,9 +141,15 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
/* Gives way before the name does. */
|
||||
.treeDetail {
|
||||
color: var(--fg-muted);
|
||||
margin-left: 6px;
|
||||
min-width: 0;
|
||||
flex-shrink: 100;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* IDEA: counts after folders, column types in a smaller grey face, key rows in grey. */
|
||||
.treeCount {
|
||||
@@ -291,9 +297,28 @@
|
||||
}
|
||||
|
||||
/* Table editor. */
|
||||
.filter {
|
||||
/* The table editor's WHERE | ORDER BY row (IDEA). */
|
||||
.filterBar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
height: var(--h-header);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.filterField {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.filterField + .filterField {
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.filterField > input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.pager {
|
||||
display: flex;
|
||||
@@ -663,27 +688,38 @@
|
||||
user-select: text;
|
||||
}
|
||||
.sqlPreview :global(.tok-keyword),
|
||||
.sqlPreview :global(.tok-operatorKeyword) {
|
||||
.codeText :global(.tok-keyword),
|
||||
.sqlPreview :global(.tok-operatorKeyword),
|
||||
.codeText :global(.tok-operatorKeyword) {
|
||||
color: var(--syn-keyword);
|
||||
}
|
||||
.sqlPreview :global(.tok-string),
|
||||
.sqlPreview :global(.tok-string2) {
|
||||
.codeText :global(.tok-string),
|
||||
.sqlPreview :global(.tok-string2),
|
||||
.codeText :global(.tok-string2) {
|
||||
color: var(--syn-string);
|
||||
}
|
||||
.sqlPreview :global(.tok-number),
|
||||
.codeText :global(.tok-number),
|
||||
.sqlPreview :global(.tok-bool),
|
||||
.sqlPreview :global(.tok-atom) {
|
||||
.codeText :global(.tok-bool),
|
||||
.sqlPreview :global(.tok-atom),
|
||||
.codeText :global(.tok-atom) {
|
||||
color: var(--syn-number);
|
||||
}
|
||||
.sqlPreview :global(.tok-typeName) {
|
||||
.sqlPreview :global(.tok-typeName),
|
||||
.codeText :global(.tok-typeName) {
|
||||
color: var(--syn-keyword);
|
||||
}
|
||||
.sqlPreview :global(.tok-comment) {
|
||||
.sqlPreview :global(.tok-comment),
|
||||
.codeText :global(.tok-comment) {
|
||||
color: var(--syn-comment);
|
||||
font-style: italic;
|
||||
}
|
||||
.sqlPreview :global(.tok-punctuation),
|
||||
.sqlPreview :global(.tok-operator) {
|
||||
.codeText :global(.tok-punctuation),
|
||||
.sqlPreview :global(.tok-operator),
|
||||
.codeText :global(.tok-operator) {
|
||||
color: var(--syn-punct);
|
||||
}
|
||||
.modifyBody {
|
||||
@@ -776,3 +812,393 @@
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Result views: grid + Record / Value / Aggregates panel. */
|
||||
.dataView {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
.dataView > .fill {
|
||||
min-width: 0;
|
||||
}
|
||||
.headerFilter {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--radius-small);
|
||||
background: transparent;
|
||||
color: var(--fg-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.headerFilter:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.headerFilterOn {
|
||||
color: var(--accent);
|
||||
}
|
||||
.filterPill {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-small);
|
||||
background: var(--bg-popup);
|
||||
color: var(--fg-secondary);
|
||||
font-size: var(--fs-small);
|
||||
box-shadow: var(--shadow-popup);
|
||||
}
|
||||
.filterPillClear {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--link);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.filterPopup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 280px;
|
||||
padding: 8px;
|
||||
}
|
||||
.filterValues {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
.filterValue {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: var(--h-row);
|
||||
}
|
||||
.filterValue > :first-child {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.filterCount {
|
||||
color: var(--fg-muted);
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
.filterEmpty {
|
||||
padding: 12px;
|
||||
color: var(--fg-muted);
|
||||
text-align: center;
|
||||
}
|
||||
.filterButtons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.sidePanel {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 320px;
|
||||
min-height: 0;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.sidePanelHead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.sidePanelBody {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.recordFields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
.recordField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.recordLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.valueView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.valueText {
|
||||
flex: 1;
|
||||
min-height: 120px;
|
||||
resize: none;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--bg-input);
|
||||
color: var(--fg-strong);
|
||||
font-family: var(--openidea-editor-font, var(--font-mono));
|
||||
font-size: var(--fs-code);
|
||||
}
|
||||
.valueText:focus {
|
||||
outline: none;
|
||||
border-color: var(--focus-ring);
|
||||
}
|
||||
.aggregates {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.aggregates th,
|
||||
.aggregates td {
|
||||
padding: 4px 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
}
|
||||
.aggregates th {
|
||||
color: var(--fg-secondary);
|
||||
width: 40%;
|
||||
}
|
||||
.aggregates td {
|
||||
font-family: var(--openidea-editor-font, var(--font-mono));
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Chart. */
|
||||
.chartBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: var(--h-header);
|
||||
padding: 0 8px;
|
||||
flex: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.chartHost {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chartSettings {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
}
|
||||
.chartGrid {
|
||||
stroke: var(--border);
|
||||
}
|
||||
.chartAxis {
|
||||
stroke: var(--fg-muted);
|
||||
}
|
||||
.chartTick {
|
||||
fill: var(--fg-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
.chartAxisName {
|
||||
fill: var(--fg-secondary);
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
.chartBarMark {
|
||||
fill: var(--chart-series);
|
||||
fill-opacity: 0.9;
|
||||
}
|
||||
.chartBarMark:hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
.chartLine {
|
||||
fill: none;
|
||||
stroke: var(--chart-series);
|
||||
stroke-width: 2;
|
||||
}
|
||||
.chartDot {
|
||||
fill: var(--chart-series);
|
||||
}
|
||||
|
||||
/* Query plan. */
|
||||
.planView {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
.planTree {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.planHeader,
|
||||
.planRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 1fr) 1fr;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
.planHeader {
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.planOperation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.planParams {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.planProperties {
|
||||
flex: none;
|
||||
width: 320px;
|
||||
align-self: flex-start;
|
||||
border-collapse: collapse;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.planProperties th,
|
||||
.planProperties td {
|
||||
padding: 4px 8px;
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid var(--border);
|
||||
word-break: break-word;
|
||||
}
|
||||
.planProperties th {
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
|
||||
/* Export / history / import dialogs. */
|
||||
.exportDialog {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
height: 420px;
|
||||
padding: 14px;
|
||||
}
|
||||
.exportOptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 300px;
|
||||
flex: none;
|
||||
}
|
||||
.exportOptions .formLabel,
|
||||
.exportPreview .formLabel {
|
||||
text-align: left;
|
||||
}
|
||||
.exportPreview {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.exportPreviewText,
|
||||
.historyPreview {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.codePreview {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--bg-editor);
|
||||
font-family: var(--openidea-editor-font, var(--font-mono));
|
||||
font-size: var(--fs-code);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.codeGutter {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
flex: none;
|
||||
min-width: 36px;
|
||||
min-height: 100%;
|
||||
padding: 8px 10px 8px 8px;
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--bg-editor);
|
||||
color: var(--fg-muted);
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
}
|
||||
.codeText {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
color: var(--fg-editor, var(--fg-strong));
|
||||
font: inherit;
|
||||
white-space: pre;
|
||||
user-select: text;
|
||||
}
|
||||
.historyDialog {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: 440px;
|
||||
padding: 14px;
|
||||
}
|
||||
.historyList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 300px;
|
||||
flex: none;
|
||||
min-height: 0;
|
||||
}
|
||||
.historyItem {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.historyText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--openidea-editor-font, var(--font-mono));
|
||||
}
|
||||
.importPreview {
|
||||
max-height: 200px;
|
||||
margin: 0 14px 14px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
.importPreview table {
|
||||
border-collapse: collapse;
|
||||
font-family: var(--openidea-editor-font, var(--font-mono));
|
||||
font-size: var(--fs-code);
|
||||
}
|
||||
.importPreview td {
|
||||
padding: 2px 8px;
|
||||
border-right: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.importHeader td {
|
||||
color: var(--fg-strong);
|
||||
font-weight: 600;
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача