Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/language/providers/completionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function getColumnAttributes(column: TableColumn, useSystemName: boolean): strin
`Column: ${shownName}`,
`${useSystemName ? `SQL name` : `System name`}: ${alternateName}`,
`Type: ${prepareParamType(column)}`,
...(column.COLUMN_TEXT?.trim() ? [`Text: ${column.COLUMN_TEXT.trim()}`] : []),
`HAS_DEFAULT: ${column.HAS_DEFAULT}`,
`IS_IDENTITY: ${column.IS_IDENTITY}`,
`IS_NULLABLE: ${column.IS_NULLABLE}`,
Expand All @@ -81,7 +82,7 @@ function getAllColumns(name: string, schema: string, items: CompletionItem[]) {
);

allCols.sortText = 'a@allCols';
allCols.insertText = items.map(item => item.label).join(", ");
allCols.insertText = items.map(item => typeof item.label === `string` ? item.label : item.label.label).join(", ");
return allCols;
}

Expand Down Expand Up @@ -123,15 +124,21 @@ async function getObjectColumns(
return [];
}

completionItems = columns.map((i) =>
createCompletionItem(
completionItems = columns.map((i) => {
const item = createCompletionItem(
Statement.prettyName(useSystemNamesInColumn ? i.SYSTEM_COLUMN_NAME : i.COLUMN_NAME),
CompletionItemKind.Field,
getColumnAttributes(i, useSystemNamesInColumn),
`Schema: ${schema}\nTable: ${name}\n`,
`a@objectcolumn`
)
);
);

const text = i.COLUMN_TEXT?.trim();
if (text) {
item.label = { label: item.label as string, description: text };
}
return item;
});
}

const allCols = getAllColumns(name, schema, completionItems);
Expand Down
52 changes: 47 additions & 5 deletions src/views/html/dataTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,13 @@ export function registerDataTable<T>(options: DataTableOptions<T>, handlers: Dat
function rowActionContext<T>(post: (message: any) => void, options: DataTableOptions<T>, row: T): DataTableRowActionContext {
return {
removeRow: () => {
if (options.rows.includes(row)) {
const index = options.rows.indexOf(row);
if (index < 0) return;

if (options.streaming) {
options.rows.splice(index, 1);
post({ command: `removeRow`, row: index });
} else {
updateDataTableRows(post, options, options.rows.filter(candidate => candidate !== row));
}
},
Expand Down Expand Up @@ -495,6 +501,8 @@ export function renderDataTable<T>(options: DataTableOptions<T>, tableId = ``):
color: var(--dt-null-color);
}

/* Keeps an empty value tall enough to show the caret and the edit outline */
.dt-hoverable { min-height: 1lh; }
.dt-hoverable[contenteditable="true"] {
outline: 1px solid var(--dt-accent);
outline-offset: -1px;
Expand Down Expand Up @@ -1140,6 +1148,36 @@ export function renderDataTable<T>(options: DataTableOptions<T>, tableId = ``):
MODEL.rows.push(...wireRows);
}

function emptyStreamingText() {
return state.appliedQuery.trim() ? MODEL.emptyMessage : (MODEL.noRowsMessage || "No rows returned.");
}

// Rows are append-only, so a row's index is also its position on screen
function removeStreamingRow(index) {
const rowEls = grid.querySelectorAll(":scope > .dt-row");
if (!rowEls[index]) return;
rowEls[index].remove();
MODEL.rows.splice(index, 1);
state.zebraCount = MODEL.rows.length;

for (let i = index; i < MODEL.rows.length; i++) {
const rowEl = rowEls[i + 1];
MODEL.rows[i].i = i;
rowEl.dataset.i = String(i);
rowEl.className = "dt-row " + (i % 2 ? "even" : "odd");
if (rowEl.dataset.vscodeContext) {
const context = JSON.parse(rowEl.dataset.vscodeContext);
context.dtRow = i;
rowEl.dataset.vscodeContext = JSON.stringify(context);
}
}

updateStreamingStatus({});
if (MODEL.rows.length) fetchIfSentinelVisible();
else if (state.noMoreRows) showStreamingMessage(emptyStreamingText(), true);
else if (!state.isFetching) requestFetch(state.allRows);
}

function requestFetch(allRows) {
state.isFetching = true;
if (MODEL.rows.length) el("dtStatus").textContent = "Loaded " + MODEL.rows.length + " rows. Fetching more…";
Expand Down Expand Up @@ -1264,10 +1302,11 @@ export function renderDataTable<T>(options: DataTableOptions<T>, tableId = ``):
};

grid.addEventListener("click", (e) => {
const hoverable = e.target.closest(".dt-hoverable");
if (!hoverable) return;
const cellDiv = hoverable.closest(".dt-c");
// Resolve from the cell: an empty value leaves nothing of .dt-hoverable to click on
const cellDiv = e.target.closest(".dt-c");
if (!cellDiv) return;
const hoverable = cellDiv.querySelector(".dt-hoverable");
if (!hoverable) return;
const chosenColumn = cellDiv.dataset.col;
if (!chosenColumn || chosenColumn === "RRN") return;
const chosenColumnDetail = updateTable.columns.find((c) => c.name === chosenColumn);
Expand Down Expand Up @@ -1484,13 +1523,16 @@ export function renderDataTable<T>(options: DataTableOptions<T>, tableId = ``):
const affected = typeof data.updateCount === "number" && data.updateCount >= 0 ? data.updateCount : 0;
showStreamingMessage("Statement executed with no result set returned. Rows affected: " + affected, false);
} else {
showStreamingMessage(state.appliedQuery.trim() ? MODEL.emptyMessage : "No rows returned.", true);
showStreamingMessage(emptyStreamingText(), true);
}
} else {
hideStreamingMessage();
fetchIfSentinelVisible();
}
break;
case "removeRow":
removeStreamingRow(data.row);
break;
case "cellResponse":
if (data.id) handleCellResponse(data.id, data.success === true);
break;
Expand Down
61 changes: 17 additions & 44 deletions src/views/jobManager/selfCodes/sqlErrorLog.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ColumnMetaData } from "@ibm/mapepire-js";
import * as vscode from "vscode";
import { getInstance } from "../../../base";
import { JobManager } from "../../../config";
import Statement from "../../../database/statement";
import { DataTableColumn, DataTableHandlers, DataTableOptions } from "../../html/dataTable";
import { showDataTable, showDataTableError, showDataTableLoading } from "../../results";
import { formatTimestamp, listFooter, LoadStats, prettyColumnTitle } from "../../schemaBrowser/indexCreation";
import { DataTableColumn } from "../../html/dataTable";
import { showListing } from "../../results";
import { formatTimestamp, isNumericType, prettyColumnTitle } from "../../schemaBrowser/indexCreation";

/** A row from `QSYS2.SQL_ERROR_LOG` */
interface SqlErrorLogEntry {
Expand Down Expand Up @@ -61,21 +62,18 @@ const LEADING_COLUMNS = [`LOGGED_TIME`, `LOGGED_SQLCODE`, `LOGGED_SQLSTATE`, `NU

const HIDDEN_COLUMNS = new Set([`INITIAL_STACK`]);

function sqlErrorLogColumns(entries: SqlErrorLogEntry[]): DataTableColumn<SqlErrorLogEntry>[] {
const first = entries[0];
if (!first) return [];

const available = Object.keys(first).filter(column => !HIDDEN_COLUMNS.has(column));
function sqlErrorLogColumns(columnMetaData: ColumnMetaData[]): DataTableColumn<SqlErrorLogEntry>[] {
const available = columnMetaData.filter(column => !HIDDEN_COLUMNS.has(column.name));
const ordered = [
...LEADING_COLUMNS.filter(column => available.includes(column)),
...available.filter(column => !LEADING_COLUMNS.includes(column)),
...LEADING_COLUMNS.flatMap(name => available.filter(column => column.name === name)),
...available.filter(column => !LEADING_COLUMNS.includes(column.name)),
];

return ordered.map(column => ({
id: column,
title: prettyColumnTitle(column),
value: (entry: SqlErrorLogEntry) => formatColumnValue(entry, column),
align: typeof first[column] === `number` ? `right` : `left`,
id: column.name,
title: prettyColumnTitle(column.name),
value: (entry: SqlErrorLogEntry) => formatColumnValue(entry, column.name),
align: isNumericType(column.type) ? `right` : `left`,
}));
}

Expand Down Expand Up @@ -142,46 +140,21 @@ export async function viewOtherSelfLogs(): Promise<void> {
if (!scope) return;

const target = scopeTarget(scope);
const sql = sqlErrorLogStatement(scope);
const stats: LoadStats = { executionTimeMs: 0 };
const load = async () => {
const startTime = performance.now();
const entries = await JobManager.runSQL<SqlErrorLogEntry>(sql);
stats.executionTimeMs = performance.now() - startTime;
stats.jobId = JobManager.getSelection()?.job.id;
return entries;
};
let entries: SqlErrorLogEntry[];

try {
await showDataTableLoading(`Fetching the SQL error log for ${target}...`);
entries = await load();
} catch (e: any) {
showDataTableError(e.message);
return;
}

const options: DataTableOptions<SqlErrorLogEntry> = {
title: `SQL Error Log for ${target}`,
subtitle: (shown, total) => listFooter({ one: `logged error`, many: `logged errors` }, shown, total, stats),
columns: sqlErrorLogColumns(entries),
rows: entries,
await showListing<SqlErrorLogEntry>(sqlErrorLogStatement(scope), {
heading: `SQL Error Log for ${target}`,
columns: sqlErrorLogColumns,
loadingText: `Fetching the SQL error log for ${target}...`,
searchPlaceholder: `Search logged errors…`,
emptyMessage: `No logged errors match the search.`,
noRowsMessage: `No errors were logged by SELF for ${target}.`,
actions: [
{ id: SQL_ERROR_LOG_ACTIONS.openStatement, primary: true, when: entry => statementText(entry) !== undefined },
],
};

const handlers: DataTableHandlers<SqlErrorLogEntry> = {
onAction: async (actionId, entry) => {
if (actionId === SQL_ERROR_LOG_ACTIONS.openStatement) {
await openStatement(entry);
}
},
};

showDataTable(options, handlers, { sql, reload: load, columns: sqlErrorLogColumns })
.catch(e => vscode.window.showErrorMessage(`Could not show the SQL error log: ${e?.message ?? e}`));
});
}
8 changes: 7 additions & 1 deletion src/views/results/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { DoveResultsView, ExplainTreeItem } from "./explain/doveResultsView";
import { DoveTreeDecorationProvider } from "./explain/doveTreeDecorationProvider";
import { ExplainTree } from "./explain/nodes";
import { DataTableHandlers, DataTableOptions } from "../html/dataTable";
import { DataTableExtras, ResultSetPanelProvider, SqlParameter } from "./resultSetPanelProvider";
import { DataTableExtras, ResultSetListing, ResultSetPanelProvider, SqlParameter } from "./resultSetPanelProvider";

export type StatementQualifier = "statement" | "bind" | "update" | "explain" | "onlyexplain" | "json" | "csv" | "md" | "cl" | "sql" | "rpg" | "udtf";

Expand Down Expand Up @@ -89,6 +89,12 @@ export function showDataTable<T>(options: DataTableOptions<T>, handlers?: DataTa
return resultSetProvider.showDataTable(options, handlers, extras);
}

/** Run a statement in the Results view and show its rows as a listing (MTIs, advised indexes, …) */
export async function showListing<T>(sql: string, listing: ResultSetListing<T>): Promise<void> {
await resultSetProvider.ensureActivation();
await resultSetProvider.setScrolling({ basicSelect: sql, title: listing.heading, listing });
}

/** Loading placeholder in the Results view while a data table listing is fetched */
export function showDataTableLoading(text: string): Promise<void> {
return resultSetProvider.showDataTableLoading(text);
Expand Down
Loading
Loading