0.8.0: MariaDB + PlanetScale, TimescaleDB, multi-result sets, streaming export, schema diff #27
@@ -281,8 +281,8 @@ describe("DbViewerScreen", () => {
|
|||||||
|
|
||||||
it("executes a non-destructive query when Run is clicked", async () => {
|
it("executes a non-destructive query when Run is clicked", async () => {
|
||||||
const executeQuery = vi
|
const executeQuery = vi
|
||||||
.spyOn(commands, "executeQuery")
|
.spyOn(commands, "executeQueryMulti")
|
||||||
.mockResolvedValue(mockQueryResult as any);
|
.mockResolvedValue({ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any);
|
||||||
render(
|
render(
|
||||||
<DbViewerScreen
|
<DbViewerScreen
|
||||||
connectionId="c1"
|
connectionId="c1"
|
||||||
@@ -306,13 +306,11 @@ describe("DbViewerScreen", () => {
|
|||||||
|
|
||||||
it("shows a destructive-query confirmation dialog and executes on confirm", async () => {
|
it("shows a destructive-query confirmation dialog and executes on confirm", async () => {
|
||||||
const executeQuery = vi
|
const executeQuery = vi
|
||||||
.spyOn(commands, "executeQuery")
|
.spyOn(commands, "executeQueryMulti")
|
||||||
.mockResolvedValue({
|
.mockResolvedValue({
|
||||||
columns: [],
|
result_sets: [{ columns: [], rows: [], total_rows: 0, page: 1, page_size: 50 }],
|
||||||
rows: [],
|
notices: [],
|
||||||
total_rows: 0,
|
execution_time_ms: 42,
|
||||||
page: 1,
|
|
||||||
page_size: 50,
|
|
||||||
} as any);
|
} as any);
|
||||||
render(
|
render(
|
||||||
<DbViewerScreen
|
<DbViewerScreen
|
||||||
@@ -345,8 +343,8 @@ describe("DbViewerScreen", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("refreshes the schema tree after a DDL query runs", async () => {
|
it("refreshes the schema tree after a DDL query runs", async () => {
|
||||||
vi.spyOn(commands, "executeQuery").mockResolvedValue(
|
vi.spyOn(commands, "executeQueryMulti").mockResolvedValue(
|
||||||
mockQueryResult as any,
|
{ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any,
|
||||||
);
|
);
|
||||||
const getSchemas = vi
|
const getSchemas = vi
|
||||||
.spyOn(commands, "getSchemas")
|
.spyOn(commands, "getSchemas")
|
||||||
@@ -377,8 +375,8 @@ describe("DbViewerScreen", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not refresh the schema tree after a SELECT", async () => {
|
it("does not refresh the schema tree after a SELECT", async () => {
|
||||||
vi.spyOn(commands, "executeQuery").mockResolvedValue(
|
vi.spyOn(commands, "executeQueryMulti").mockResolvedValue(
|
||||||
mockQueryResult as any,
|
{ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any,
|
||||||
);
|
);
|
||||||
const getSchemas = vi
|
const getSchemas = vi
|
||||||
.spyOn(commands, "getSchemas")
|
.spyOn(commands, "getSchemas")
|
||||||
@@ -396,7 +394,7 @@ describe("DbViewerScreen", () => {
|
|||||||
);
|
);
|
||||||
fireEvent.change(textarea, { target: { value: "SELECT 1" } });
|
fireEvent.change(textarea, { target: { value: "SELECT 1" } });
|
||||||
fireEvent.click(screen.getByRole("button", { name: /run query/i }));
|
fireEvent.click(screen.getByRole("button", { name: /run query/i }));
|
||||||
await waitFor(() => expect(commands.executeQuery).toHaveBeenCalled());
|
await waitFor(() => expect(commands.executeQueryMulti).toHaveBeenCalled());
|
||||||
expect(getSchemas).not.toHaveBeenCalled();
|
expect(getSchemas).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -423,8 +421,8 @@ describe("DbViewerScreen", () => {
|
|||||||
|
|
||||||
it("runs the current query when the Cmd+Enter action fires", async () => {
|
it("runs the current query when the Cmd+Enter action fires", async () => {
|
||||||
const executeQuery = vi
|
const executeQuery = vi
|
||||||
.spyOn(commands, "executeQuery")
|
.spyOn(commands, "executeQueryMulti")
|
||||||
.mockResolvedValue(mockQueryResult as any);
|
.mockResolvedValue({ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any);
|
||||||
render(
|
render(
|
||||||
<DbViewerScreen
|
<DbViewerScreen
|
||||||
connectionId="c1"
|
connectionId="c1"
|
||||||
@@ -452,7 +450,7 @@ describe("DbViewerScreen", () => {
|
|||||||
const pending = new Promise<unknown>((r) => {
|
const pending = new Promise<unknown>((r) => {
|
||||||
resolveRun = r;
|
resolveRun = r;
|
||||||
});
|
});
|
||||||
vi.spyOn(commands, "executeQuery").mockReturnValue(pending as any);
|
vi.spyOn(commands, "executeQueryMulti").mockReturnValue(pending as any);
|
||||||
render(
|
render(
|
||||||
<DbViewerScreen
|
<DbViewerScreen
|
||||||
connectionId="c1"
|
connectionId="c1"
|
||||||
@@ -469,7 +467,7 @@ describe("DbViewerScreen", () => {
|
|||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId("query-run-pulse")).toBeInTheDocument(),
|
expect(screen.getByTestId("query-run-pulse")).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
resolveRun(mockQueryResult);
|
resolveRun({ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 });
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.queryByTestId("query-run-pulse")).toBeNull(),
|
expect(screen.queryByTestId("query-run-pulse")).toBeNull(),
|
||||||
);
|
);
|
||||||
@@ -477,8 +475,8 @@ describe("DbViewerScreen", () => {
|
|||||||
|
|
||||||
it("collapses and re-expands the query results via the caret", async () => {
|
it("collapses and re-expands the query results via the caret", async () => {
|
||||||
const executeQuery = vi
|
const executeQuery = vi
|
||||||
.spyOn(commands, "executeQuery")
|
.spyOn(commands, "executeQueryMulti")
|
||||||
.mockResolvedValue(mockQueryResult as any);
|
.mockResolvedValue({ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any);
|
||||||
render(
|
render(
|
||||||
<DbViewerScreen
|
<DbViewerScreen
|
||||||
connectionId="c1"
|
connectionId="c1"
|
||||||
@@ -505,8 +503,8 @@ describe("DbViewerScreen", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("resizes the results panel with a drag handle, clamped to min/max", async () => {
|
it("resizes the results panel with a drag handle, clamped to min/max", async () => {
|
||||||
vi.spyOn(commands, "executeQuery").mockResolvedValue(
|
vi.spyOn(commands, "executeQueryMulti").mockResolvedValue(
|
||||||
mockQueryResult as any,
|
{ result_sets: [mockQueryResult], notices: [], execution_time_ms: 42 } as any,
|
||||||
);
|
);
|
||||||
render(
|
render(
|
||||||
<DbViewerScreen
|
<DbViewerScreen
|
||||||
@@ -930,7 +928,7 @@ describe("DbViewerScreen", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders an objectForm tab with the Visual/SQL toggle in the objects view", async () => {
|
it("renders an objectForm tab with the Visual/SQL toggle in the objects view", async () => {
|
||||||
vi.spyOn(commands, "executeQuery").mockResolvedValue({ columns: [], rows: [], total_rows: 0, page: 1, page_size: 50 } as any);
|
vi.spyOn(commands, "executeQueryMulti").mockResolvedValue({ result_sets: [{ columns: [], rows: [], total_rows: 0, page: 1, page_size: 50 }], notices: [], execution_time_ms: 42 } as any);
|
||||||
vi.spyOn(commands, "getSchemas").mockResolvedValue(["public"]);
|
vi.spyOn(commands, "getSchemas").mockResolvedValue(["public"]);
|
||||||
vi.spyOn(commands, "getDatabases").mockResolvedValue(["mydb"]);
|
vi.spyOn(commands, "getDatabases").mockResolvedValue(["mydb"]);
|
||||||
vi.spyOn(commands, "getTables").mockResolvedValue([] as any);
|
vi.spyOn(commands, "getTables").mockResolvedValue([] as any);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { TooltipProvider } from "../ui/Tooltip";
|
|||||||
import { DbViewerSidebar, NAV_CAPABILITY_KEY } from "./DbViewerSidebar";
|
import { DbViewerSidebar, NAV_CAPABILITY_KEY } from "./DbViewerSidebar";
|
||||||
import { DbViewerToolbar } from "./DbViewerToolbar";
|
import { DbViewerToolbar } from "./DbViewerToolbar";
|
||||||
import { isDestructiveQuery, isSchemaModifyingQuery } from "../../lib/utils";
|
import { isDestructiveQuery, isSchemaModifyingQuery } from "../../lib/utils";
|
||||||
import { executeQuery, cancelQuery } from "../../lib/commands";
|
import { executeQueryMulti, cancelQuery } from "../../lib/commands";
|
||||||
import { getCapabilities } from "../../lib/dbCapabilities";
|
import { getCapabilities } from "../../lib/dbCapabilities";
|
||||||
|
|
||||||
const QueryEditor = lazy(() => import("../editor/QueryEditor").then((m) => ({ default: m.QueryEditor })));
|
const QueryEditor = lazy(() => import("../editor/QueryEditor").then((m) => ({ default: m.QueryEditor })));
|
||||||
@@ -19,6 +19,7 @@ import { ObjectDetail, type AnyObject } from "./objects/ObjectDetail";
|
|||||||
import { ObjectFormTab } from "./objects/ObjectFormTab";
|
import { ObjectFormTab } from "./objects/ObjectFormTab";
|
||||||
import { TabBar } from "./TabBar";
|
import { TabBar } from "./TabBar";
|
||||||
import { VirtualDataGrid } from "../grid/VirtualDataGrid";
|
import { VirtualDataGrid } from "../grid/VirtualDataGrid";
|
||||||
|
import { ResultSetPanels } from "./ResultSetPanels";
|
||||||
import { RowDetailDrawer } from "../grid/RowDetailDrawer";
|
import { RowDetailDrawer } from "../grid/RowDetailDrawer";
|
||||||
import { TableControls } from "./TableControls";
|
import { TableControls } from "./TableControls";
|
||||||
import { EditConnectionModal } from "./EditConnectionModal";
|
import { EditConnectionModal } from "./EditConnectionModal";
|
||||||
@@ -260,6 +261,7 @@ export function DbViewerScreen({
|
|||||||
|
|
||||||
const setTabData = useDbViewerStore((s) => s.setTabData);
|
const setTabData = useDbViewerStore((s) => s.setTabData);
|
||||||
const setTabError = useDbViewerStore((s) => s.setTabError);
|
const setTabError = useDbViewerStore((s) => s.setTabError);
|
||||||
|
const setTabMulti = useDbViewerStore((s) => s.setTabMulti);
|
||||||
const setTabLoading = useDbViewerStore((s) => s.setTabLoading);
|
const setTabLoading = useDbViewerStore((s) => s.setTabLoading);
|
||||||
const databases = useDbViewerStore((s) => s.databases);
|
const databases = useDbViewerStore((s) => s.databases);
|
||||||
const currentDatabase = useDbViewerStore((s) => s.currentDatabase);
|
const currentDatabase = useDbViewerStore((s) => s.currentDatabase);
|
||||||
@@ -311,8 +313,24 @@ export function DbViewerScreen({
|
|||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
setTabLoading(tabId, true);
|
setTabLoading(tabId, true);
|
||||||
try {
|
try {
|
||||||
const result = await executeQuery(connectionId, sql, tab.page, tab.pageSize);
|
const multi = await executeQueryMulti(connectionId, sql, tab.page, tab.pageSize);
|
||||||
setTabData(tabId, result);
|
const first = multi.result_sets[0] ?? null;
|
||||||
|
// Stamp the run's execution time onto the first set so the toolbar
|
||||||
|
// (TableControls reads activeTab.data.execution_time_ms) still shows it.
|
||||||
|
if (first) {
|
||||||
|
setTabData(tabId, { ...first, execution_time_ms: multi.execution_time_ms });
|
||||||
|
} else {
|
||||||
|
// Lone DML/DDL run with no result set — keep the empty-state behavior.
|
||||||
|
setTabData(tabId, {
|
||||||
|
columns: [],
|
||||||
|
rows: [],
|
||||||
|
total_rows: 0,
|
||||||
|
page: tab.page,
|
||||||
|
page_size: tab.pageSize,
|
||||||
|
execution_time_ms: multi.execution_time_ms,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTabMulti(tabId, multi);
|
||||||
useQueryStore.getState().invalidateHistory(connectionId);
|
useQueryStore.getState().invalidateHistory(connectionId);
|
||||||
if (isSchemaModifyingQuery(sql)) {
|
if (isSchemaModifyingQuery(sql)) {
|
||||||
const st = useDbViewerStore.getState();
|
const st = useDbViewerStore.getState();
|
||||||
@@ -1017,6 +1035,48 @@ const onQueriesPanelResizeStart = useCallback(
|
|||||||
)
|
)
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
// Shared VirtualDataGrid props for the query-tab results region. The
|
||||||
|
// multi-result panels reuse these (overriding rows/columns per set);
|
||||||
|
// single-set runs render the grid directly with today's props.
|
||||||
|
const gridPropsForQueryTab = {
|
||||||
|
connectionId,
|
||||||
|
schema: activeSchema,
|
||||||
|
table: activeTable,
|
||||||
|
rows: displayRows,
|
||||||
|
columns,
|
||||||
|
hiddenColumns,
|
||||||
|
selectedRows,
|
||||||
|
dbType: currentConnection?.db_type ?? "postgresql",
|
||||||
|
tabType: activeTab?.tabType ?? "table",
|
||||||
|
getLocator,
|
||||||
|
onStageEdit: readOnlyTable ? undefined : handleStageEdit,
|
||||||
|
onStageInsertCell: readOnlyTable ? undefined : handleStageInsertCell,
|
||||||
|
onOpenRowDetail: handleOpenRowDetail,
|
||||||
|
readOnly: readOnlyTable,
|
||||||
|
enumValues: editorOptions?.enums,
|
||||||
|
fkOptions: editorOptions?.fks,
|
||||||
|
fkPlaceholders: editorOptions?.fkPlaceholders,
|
||||||
|
stagedValues,
|
||||||
|
pendingKeys,
|
||||||
|
pendingInsertChangeIds,
|
||||||
|
onToggleRow: (rowIndex: number) => {
|
||||||
|
setSelectedRows((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(rowIndex)) next.delete(rowIndex);
|
||||||
|
else next.add(rowIndex);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onToggleAll: () => {
|
||||||
|
setSelectedRows((prev) => {
|
||||||
|
if (prev.size === processedRows.length && processedRows.length > 0) {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
return new Set(processedRows.map((_, i) => i));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 w-0 flex flex-col min-w-0 overflow-hidden">
|
<div className="flex-1 w-0 flex flex-col min-w-0 overflow-hidden">
|
||||||
<TabBar onCommitted={handleCommitted} />
|
<TabBar onCommitted={handleCommitted} />
|
||||||
@@ -1212,71 +1272,16 @@ const onQueriesPanelResizeStart = useCallback(
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
<VirtualDataGrid
|
{activeTab?.tabType === "query" && activeTab.multi && (activeTab.multi.result_sets.length > 1 || activeTab.multi.notices.length > 0) ? (
|
||||||
connectionId={connectionId}
|
<ResultSetPanels
|
||||||
schema={activeSchema}
|
multi={activeTab.multi}
|
||||||
table={activeTable}
|
renderGrid={(set) => (
|
||||||
rows={displayRows}
|
<VirtualDataGrid {...gridPropsForQueryTab} rows={set.rows} columns={set.columns} />
|
||||||
columns={columns}
|
)}
|
||||||
hiddenColumns={hiddenColumns}
|
|
||||||
selectedRows={selectedRows}
|
|
||||||
dbType={currentConnection?.db_type ?? "postgresql"}
|
|
||||||
tabType={activeTab?.tabType ?? "table"}
|
|
||||||
getLocator={getLocator}
|
|
||||||
onStageEdit={readOnlyTable ? undefined : handleStageEdit}
|
|
||||||
onStageInsertCell={readOnlyTable ? undefined : handleStageInsertCell}
|
|
||||||
onOpenRowDetail={handleOpenRowDetail}
|
|
||||||
readOnly={readOnlyTable}
|
|
||||||
enumValues={editorOptions?.enums}
|
|
||||||
fkOptions={editorOptions?.fks}
|
|
||||||
fkPlaceholders={editorOptions?.fkPlaceholders}
|
|
||||||
stagedValues={stagedValues}
|
|
||||||
pendingKeys={pendingKeys}
|
|
||||||
pendingInsertChangeIds={pendingInsertChangeIds}
|
|
||||||
onToggleRow={(rowIndex) => {
|
|
||||||
setSelectedRows(
|
|
||||||
(prev) => {
|
|
||||||
const next =
|
|
||||||
new Set(
|
|
||||||
prev,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
next.has(
|
|
||||||
rowIndex,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
next.delete(
|
|
||||||
rowIndex,
|
|
||||||
);
|
|
||||||
else
|
|
||||||
next.add(
|
|
||||||
rowIndex,
|
|
||||||
);
|
|
||||||
return next;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onToggleAll={() => {
|
|
||||||
setSelectedRows(
|
|
||||||
(prev) => {
|
|
||||||
if (
|
|
||||||
prev.size ===
|
|
||||||
processedRows.length &&
|
|
||||||
processedRows.length >
|
|
||||||
0
|
|
||||||
) {
|
|
||||||
return new Set();
|
|
||||||
}
|
|
||||||
return new Set(
|
|
||||||
processedRows.map(
|
|
||||||
(_, i) =>
|
|
||||||
i,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<VirtualDataGrid {...gridPropsForQueryTab} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { ResultSetPanels } from "./ResultSetPanels";
|
||||||
|
import type { MultiQueryResult } from "../../lib/types";
|
||||||
|
|
||||||
|
const multi: MultiQueryResult = {
|
||||||
|
result_sets: [
|
||||||
|
{ columns: [{ name: "a", data_type: "int", is_nullable: true, is_pk: false, is_fk: false, fk_ref: null, default_value: null, editable: true, is_generated: false }], rows: [[1], [2]], total_rows: 2, page: 1, page_size: 50 },
|
||||||
|
{ columns: [{ name: "b", data_type: "text", is_nullable: true, is_pk: false, is_fk: false, fk_ref: null, default_value: null, editable: true, is_generated: false }], rows: [], total_rows: 0, page: 1, page_size: 50 },
|
||||||
|
],
|
||||||
|
notices: [
|
||||||
|
{ statement_index: 1, kind: "dml", text: "UPDATE 3", affected: 3 },
|
||||||
|
{ statement_index: 3, kind: "error", text: "relation \"nope\" does not exist", affected: null },
|
||||||
|
],
|
||||||
|
execution_time_ms: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("ResultSetPanels", () => {
|
||||||
|
it("renders one panel header per result set with row counts", () => {
|
||||||
|
render(<ResultSetPanels multi={multi} renderGrid={vi.fn()} />);
|
||||||
|
expect(screen.getByText(/Result 1/)).toBeTruthy();
|
||||||
|
expect(screen.getByText(/Result 2/)).toBeTruthy();
|
||||||
|
expect(screen.getByText(/2 rows/)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders notice cards for DML and errors", () => {
|
||||||
|
render(<ResultSetPanels multi={multi} renderGrid={vi.fn()} />);
|
||||||
|
expect(screen.getByText(/UPDATE 3/)).toBeTruthy();
|
||||||
|
expect(screen.getByText(/3 rows affected/)).toBeTruthy();
|
||||||
|
expect(screen.getByText(/relation "nope" does not exist/)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates each set to the grid renderer with its index", () => {
|
||||||
|
const renderGrid = vi.fn(() => <div />);
|
||||||
|
render(<ResultSetPanels multi={multi} renderGrid={renderGrid} />);
|
||||||
|
expect(renderGrid).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { AlertCircle, Info } from "lucide-react";
|
||||||
|
import type { MultiQueryResult, QueryResult } from "../../lib/types";
|
||||||
|
|
||||||
|
export function ResultSetPanels({
|
||||||
|
multi,
|
||||||
|
renderGrid,
|
||||||
|
}: {
|
||||||
|
multi: MultiQueryResult;
|
||||||
|
renderGrid: (set: QueryResult, index: number) => React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto flex flex-col">
|
||||||
|
{multi.result_sets.map((set, i) => (
|
||||||
|
<section key={i} className="border-b border-border flex flex-col min-h-0">
|
||||||
|
<header className="px-3 py-1.5 text-xs text-text-muted bg-surface-raised border-b border-border flex items-center gap-2 shrink-0">
|
||||||
|
<span className="font-medium text-text">Result {i + 1}</span>
|
||||||
|
<span>— {set.total_rows} row{set.total_rows === 1 ? "" : "s"}</span>
|
||||||
|
</header>
|
||||||
|
<div className="min-h-0" style={{ height: Math.min(Math.max(set.rows.length * 28 + 32, 160), 480) }}>
|
||||||
|
{renderGrid(set, i)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
{multi.notices.map((n, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
role={n.kind === "error" ? "alert" : "status"}
|
||||||
|
className={`flex items-center gap-2 px-3 py-1.5 text-xs border-b border-border ${
|
||||||
|
n.kind === "error" ? "text-red-400" : "text-text-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{n.kind === "error" ? <AlertCircle size={13} /> : <Info size={13} />}
|
||||||
|
<span className="font-medium">Statement {n.statement_index + 1}</span>
|
||||||
|
<span className="truncate">{n.text}</span>
|
||||||
|
{n.affected !== null && n.affected !== undefined && (
|
||||||
|
<span>({n.affected} rows affected)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import type { Connection, ConnectionInput, ConnectionTestResult, Folder, FolderInput, Tag, TagInput, Settings, ImportResult, TableInfo, QueryResult, BackupOptions, RestoreOptions, SyncOptions, PgToolStatus, MySqlToolStatus, MySqlBackupOptions, MySqlRestoreOptions, SqliteBackupOptions, SqliteRestoreOptions, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, SchemaGraph, IndexInfo, ConstraintInfo, RecentConnection, ObjectSearchHit, DependencyInfo, RoleInfo, PrivilegeEntry, RebuildReadiness, MaintenanceResult, TablespaceInfo, ColumnInfo, HypertableListResponse } from "./types";
|
import type { Connection, ConnectionInput, ConnectionTestResult, Folder, FolderInput, Tag, TagInput, Settings, ImportResult, TableInfo, QueryResult, BackupOptions, RestoreOptions, SyncOptions, PgToolStatus, MySqlToolStatus, MySqlBackupOptions, MySqlRestoreOptions, SqliteBackupOptions, SqliteRestoreOptions, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, SchemaGraph, IndexInfo, ConstraintInfo, RecentConnection, ObjectSearchHit, DependencyInfo, RoleInfo, PrivilegeEntry, RebuildReadiness, MaintenanceResult, TablespaceInfo, ColumnInfo, HypertableListResponse, MultiQueryResult } from "./types";
|
||||||
import type { FilterRule, SortRule } from "../stores/dbViewerStore";
|
import type { FilterRule, SortRule } from "../stores/dbViewerStore";
|
||||||
import type { ChangePayload } from "./changePayload";
|
import type { ChangePayload } from "./changePayload";
|
||||||
import { buildObjectDdl as buildObjectDdlImpl, type ObjectKind, type DdlParams } from "./objectCrud";
|
import { buildObjectDdl as buildObjectDdlImpl, type ObjectKind, type DdlParams } from "./objectCrud";
|
||||||
@@ -286,6 +286,15 @@ export async function executeQuery(
|
|||||||
return invoke<QueryResult>("execute_query", { connectionId, query, page, pageSize });
|
return invoke<QueryResult>("execute_query", { connectionId, query, page, pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function executeQueryMulti(
|
||||||
|
connectionId: string,
|
||||||
|
query: string,
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
): Promise<MultiQueryResult> {
|
||||||
|
return invoke<MultiQueryResult>("execute_query_multi", { connectionId, query, page, pageSize });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getQueryHistory(
|
export async function getQueryHistory(
|
||||||
connectionId: string | null,
|
connectionId: string | null,
|
||||||
limit: number,
|
limit: number,
|
||||||
|
|||||||
@@ -189,6 +189,19 @@ export interface QueryResult {
|
|||||||
error?: string | null;
|
error?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StatementNotice {
|
||||||
|
statement_index: number;
|
||||||
|
kind: "dml" | "ddl" | "error" | string;
|
||||||
|
text: string;
|
||||||
|
affected: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultiQueryResult {
|
||||||
|
result_sets: QueryResult[];
|
||||||
|
notices: StatementNotice[];
|
||||||
|
execution_time_ms: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type ChangeStatus = "pending" | "applied" | "error";
|
export type ChangeStatus = "pending" | "applied" | "error";
|
||||||
|
|
||||||
export type ChangeItemType =
|
export type ChangeItemType =
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import type { QueryResult, TableInfo, ChangeItemType, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, HypertableInfo, IndexInfo, ConstraintInfo, ObjectType, RoleInfo } from "../lib/types";
|
import type { QueryResult, TableInfo, ChangeItemType, FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, HypertableInfo, IndexInfo, ConstraintInfo, ObjectType, RoleInfo, MultiQueryResult } from "../lib/types";
|
||||||
import { getDatabases, getSchemas, getTables } from "../lib/commands";
|
import { getDatabases, getSchemas, getTables } from "../lib/commands";
|
||||||
import type { ObjectKind, DdlParams } from "../lib/objectCrud";
|
import type { ObjectKind, DdlParams } from "../lib/objectCrud";
|
||||||
|
|
||||||
@@ -58,6 +58,7 @@ export interface ViewerTab {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
data: QueryResult | null;
|
data: QueryResult | null;
|
||||||
|
multi?: MultiQueryResult | null;
|
||||||
columnFilter?: { column: string; value: string };
|
columnFilter?: { column: string; value: string };
|
||||||
filterRules: FilterRule[];
|
filterRules: FilterRule[];
|
||||||
sortRules: SortRule[];
|
sortRules: SortRule[];
|
||||||
@@ -146,6 +147,7 @@ interface DbViewerState {
|
|||||||
setPage: (tabId: string, page: number) => void;
|
setPage: (tabId: string, page: number) => void;
|
||||||
setPageSize: (tabId: string, pageSize: number) => void;
|
setPageSize: (tabId: string, pageSize: number) => void;
|
||||||
setTabData: (tabId: string, data: QueryResult) => void;
|
setTabData: (tabId: string, data: QueryResult) => void;
|
||||||
|
setTabMulti: (tabId: string, multi: MultiQueryResult | null) => void;
|
||||||
setTabLoading: (tabId: string, loading: boolean) => void;
|
setTabLoading: (tabId: string, loading: boolean) => void;
|
||||||
setTabError: (tabId: string, error: string) => void;
|
setTabError: (tabId: string, error: string) => void;
|
||||||
setColumnFilter: (tabId: string, column: string, value: string) => void;
|
setColumnFilter: (tabId: string, column: string, value: string) => void;
|
||||||
@@ -449,10 +451,15 @@ export const useDbViewerStore = create<DbViewerState>((set, get) => ({
|
|||||||
setTabData: (tabId, data) =>
|
setTabData: (tabId, data) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
tabs: state.tabs.map((t) =>
|
tabs: state.tabs.map((t) =>
|
||||||
t.id === tabId ? { ...t, data, loading: false, error: null } : t,
|
t.id === tabId ? { ...t, data, multi: null, loading: false, error: null } : t,
|
||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
setTabMulti: (tabId, multi) =>
|
||||||
|
set((s) => ({
|
||||||
|
tabs: s.tabs.map((t) => (t.id === tabId ? { ...t, multi } : t)),
|
||||||
|
})),
|
||||||
|
|
||||||
setTabLoading: (tabId, loading) =>
|
setTabLoading: (tabId, loading) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
tabs: state.tabs.map((t) =>
|
tabs: state.tabs.map((t) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user