diff --git a/package.json b/package.json index 8588552b..94a7e57e 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,19 @@ "name": "Results", "when": "code-for-ibmi:connected == true", "contextualTitle": "IBM i" + }, + { + "type": "tree", + "id": "vscode-db2i.dove.nodes", + "name": "Visual Explain", + "when": "vscode-db2i:explaining == true", + "contextualTitle": "IBM i" + }, + { + "type": "tree", + "id": "vscode-db2i.dove.node", + "name": "Node Detail", + "when": "vscode-db2i:explainingNode == true" } ], "db2-explorer": [ @@ -263,10 +276,16 @@ }, { "command": "vscode-db2i.runEditorStatement", - "title": "Run selected statement", + "title": "Run statement", "category": "Db2 for i", "icon": "$(notebook-execute)" }, + { + "command": "vscode-db2i.explainEditorStatement", + "title": "Explain statement", + "category": "Db2 for i", + "icon": "$(debug-alt)" + }, { "command": "vscode-db2i.queryHistory.remove", "title": "Remove query from history", @@ -365,6 +384,24 @@ "title": "Delete configuration", "category": "Db2 for i", "icon": "$(trash)" + }, + { + "command": "vscode-db2i.dove.close", + "title": "Close detail", + "category": "Db2 for i", + "icon": "$(close-all)" + }, + { + "command": "vscode-db2i.dove.nodeDetail", + "title": "See detail", + "category": "Db2 for i", + "icon": "$(info)" + }, + { + "command": "vscode-db2i.dove.export", + "title": "Export current VE data", + "category": "Db2 for i", + "icon": "$(file)" } ], "menus": { @@ -420,6 +457,14 @@ { "command": "vscode-db2i.jobManager.deleteConfig", "when": "never" + }, + { + "command": "vscode-db2i.dove.nodeDetail", + "when": "never" + }, + { + "command": "vscode-db2i.dove.export", + "when": "vscode-db2i:explaining == true" } ], "editor/context": [ @@ -433,12 +478,12 @@ { "command": "vscode-db2i.runEditorStatement", "when": "editorLangId == sql", - "group": "navigation@2" + "group": "navigation@sql" }, { - "command": "code-for-ibmi.changeCurrentLibrary", + "command": "vscode-db2i.explainEditorStatement", "when": "editorLangId == sql", - "group": "navigation@3" + "group": "navigation@sql" } ], "view/title": [ @@ -481,6 +526,16 @@ "command": "vscode-db2i.jobManager.endAll", "group": "navigation", "when": "view == jobManager" + }, + { + "command": "vscode-db2i.dove.export", + "group": "navigation", + "when": "view == vscode-db2i.dove.nodes" + }, + { + "command": "vscode-db2i.dove.close", + "group": "navigation", + "when": "view == vscode-db2i.dove.nodes" } ], "view/item/context": [ @@ -608,9 +663,106 @@ "command": "vscode-db2i.jobManager.deleteConfig", "when": "view == jobManager && viewItem == savedConfig", "group": "inline" + }, + { + "command": "vscode-db2i.dove.nodeDetail", + "when": "view == vscode-db2i.dove.nodes && viewItem == explainTreeItem", + "group": "inline" } ] - } + }, + "colors": [ + { + "id": "db2i.dove.resultsView.HighlightIndexAdvised", + "description": "Highlight color for index advised", + "defaults": { + "dark": "#acace0", + "light": "#8c8cbd", + "highContrast": "#acace0", + "highContrastLight": "#8c8cbd" + } + }, + { + "id": "db2i.dove.resultsView.HighlightActualExpensiveRows", + "description": "Highlight color for actual expensive rows", + "defaults": { + "dark": "#cc9933", + "light": "#cc9933", + "highContrast": "#cc9933", + "highContrastLight": "#cc9933" + } + }, + { + "id": "db2i.dove.resultsView.HighlightEstimatedExpensiveRows", + "description": "Highlight color for estimated expensive rows", + "defaults": { + "dark": "#ffffbf", + "light": "#ffffbf", + "highContrast": "#ffffbf", + "highContrastLight": "#ffffbf" + } + }, + { + "id": "db2i.dove.resultsView.HighlightActualExpensiveTime", + "description": "Highlight color for actual expensive time", + "defaults": { + "dark": "#bc0f0f", + "light": "#bc0f0f", + "highContrast": "#bc0f0f", + "highContrastLight": "#bc0f0f" + } + }, + { + "id": "db2i.dove.resultsView.HighlightEstimatedExpensiveTime", + "description": "Highlight color for estimated expensive time", + "defaults": { + "dark": "#f2bdbd", + "light": "#f2bdbd", + "highContrast": "#f2bdbd", + "highContrastLight": "#f2bdbd" + } + }, + { + "id": "db2i.dove.resultsView.HighlightLookaheadPredicateGeneration", + "description": "Highlight color for Lookahead Predicate Generation", + "defaults": { + "dark": "#00ff00", + "light": "#00ff00", + "highContrast": "#00ff00", + "highContrastLight": "#00ff00" + } + }, + { + "id": "db2i.dove.resultsView.HighlightMaterializedQueryTable", + "description": "Highlight color for Materialized Query Table", + "defaults": { + "dark": "#ff8400", + "light": "#ff8400", + "highContrast": "#ff8400", + "highContrastLight": "#ff8400" + } + }, + { + "id": "db2i.dove.resultsView.HighlightRefreshedNode", + "description": "Highlight color for refreshed node", + "defaults": { + "dark": "#00ffff", + "light": "#00ffff", + "highContrast": "#00ffff", + "highContrastLight": "#00ffff" + } + }, + { + "id": "db2i.dove.nodeView.AttributeSectionHeading", + "description": "Color for attributes section heading", + "defaults": { + "dark": "#bd8c8c", + "light": "#5976df", + "highContrast": "#bd8c8c", + "highContrastLight": "#5976df" + } + } + ] }, "scripts": { "lint": "eslint .", diff --git a/src/connection/sqlJob.ts b/src/connection/sqlJob.ts index f5629b53..8c44c390 100644 --- a/src/connection/sqlJob.ts +++ b/src/connection/sqlJob.ts @@ -1,7 +1,7 @@ import { CommandResult } from "@halcyontech/vscode-ibmi-types"; import { getInstance } from "../base"; import { ServerComponent } from "./serverComponent"; -import { JDBCOptions, ConnectionResult, Rows, QueryResult, JobLogEntry, CLCommandResult, VersionCheckResult, GetTraceDataResult, ServerTraceDest, ServerTraceLevel, SetConfigResult, QueryOptions } from "./types"; +import { JDBCOptions, ConnectionResult, Rows, QueryResult, JobLogEntry, CLCommandResult, VersionCheckResult, GetTraceDataResult, ServerTraceDest, ServerTraceLevel, SetConfigResult, QueryOptions, ExplainResults } from "./types"; import { Query } from "./query"; import { EventEmitter } from "stream"; @@ -12,6 +12,11 @@ export enum JobStatus { Ended = "ended" } +export enum ExplainType { + Run, + DoNotRun +} + export enum TransactionEndType { COMMIT, ROLLBACK @@ -122,6 +127,7 @@ export class SQLJob { async send(content: string): Promise { if (this.isTracingChannelData) ServerComponent.writeOutput(content); + let req: ReqRespFmt = JSON.parse(content); this.channel.stdin.write(content + `\n`); this.status = JobStatus.Active; @@ -138,6 +144,8 @@ export class SQLJob { } async connect(): Promise { + this.isTracingChannelData = true; + this.channel = await this.getChannel(); this.channel.on(`error`, (err) => { @@ -164,7 +172,8 @@ export class SQLJob { const connectionObject = { id: SQLJob.getNewUniqueId(), type: `connect`, - technique: (getInstance().getConnection().qccsid === 65535 || this.options["database name"]) ? `tcp` : `cli`, //TODO: investigate why QCCSID 65535 breaks CLI and if there is any workaround + //technique: (getInstance().getConnection().qccsid === 65535 || this.options["database name"]) ? `tcp` : `cli`, //TODO: investigate why QCCSID 65535 breaks CLI and if there is any workaround + technique: `tcp`, // TODO: DOVE does not work in cli mode application: `vscode-db2i ${DB2I_VERSION}`, props: props.length > 0 ? props : undefined } @@ -186,8 +195,11 @@ export class SQLJob { this.id = connectResult.job; this.status = JobStatus.Ready; + this.isTracingChannelData = false; + return connectResult; } + query(sql: string, opts?: QueryOptions): Query { return new Query(this, sql, opts); } @@ -209,6 +221,35 @@ export class SQLJob { return version; } + async explain(statement: string, type: ExplainType = ExplainType.Run): Promise> { + if (type !== ExplainType.Run) { + throw new Error("TODO: support more types of explains"); + } + + // if (this.options["full open"] !== true) { + // throw new Error("Job option 'full open' must be true."); + // } + + const explainRequest = { + id: SQLJob.getNewUniqueId(), + type: `dove`, + sql: statement, + run: type === ExplainType.Run + } + + const result = await this.send(JSON.stringify(explainRequest)); + + const explainResult: ExplainResults = JSON.parse(result); + + if (explainResult.success !== true) { + this.dispose(); + this.status = JobStatus.NotStarted; + throw new Error(explainResult.error || `Failed to explain.`); + } + + return explainResult; + } + getTraceFilePath(): string|undefined { return this.traceFile; } diff --git a/src/connection/types.ts b/src/connection/types.ts index 4146da23..f4bd25f5 100644 --- a/src/connection/types.ts +++ b/src/connection/types.ts @@ -14,6 +14,11 @@ export interface VersionCheckResult extends ServerResponse { version: string; } +export interface ExplainResults extends QueryResult { + vemetadata: QueryMetaData, + vedata: any; +} + export interface GetTraceDataResult extends ServerResponse { tracedata: string } diff --git a/src/testing/jobs.ts b/src/testing/jobs.ts index e4c4d0c6..cf939433 100644 --- a/src/testing/jobs.ts +++ b/src/testing/jobs.ts @@ -5,6 +5,7 @@ import { Query } from "../connection/query"; import { ServerComponent } from "../connection/serverComponent"; import { JobStatus, SQLJob } from "../connection/sqlJob"; import { ServerTraceDest, ServerTraceLevel } from "../connection/types"; +import { ExplainTree } from "../views/results/explain/nodes"; export const JobsSuite: TestSuite = { name: `Connection tests`, @@ -418,5 +419,20 @@ export const JobsSuite: TestSuite = { console.log(`Old query method took ${oe - os} milliseconds.`); assert.equal((ne - ns) < (oe - os), true); }}, + + {name: `Explain API`, test: async () => { + const newJob = new SQLJob({"full open": true}); + await newJob.connect(); + + const query = `select * from qiws.qcustcdt`; + + const result = await newJob.explain(query); + + const tree = new ExplainTree(result.data); + + const topLevel = tree.get(); + + assert.notStrictEqual(topLevel, undefined); + }} ] } \ No newline at end of file diff --git a/src/views/results/explain/doveNodeView.ts b/src/views/results/explain/doveNodeView.ts new file mode 100644 index 00000000..88515493 --- /dev/null +++ b/src/views/results/explain/doveNodeView.ts @@ -0,0 +1,53 @@ +import { CancellationToken, Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, ThemeIcon, commands } from "vscode"; +import { ExplainNode, ExplainProperty, Highlighting, RecordType, NodeHighlights } from "./nodes"; +import { toDoveTreeDecorationProviderUri } from "./doveTreeDecorationProvider"; + +type EventType = PropertyNode | undefined | null | void; + +export class DoveNodeView implements TreeDataProvider { + private _onDidChangeTreeData: EventEmitter = new EventEmitter(); + readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; + + private propertyNodes: PropertyNode[]; + + setNode(currentNode: ExplainNode) { + this.propertyNodes = currentNode.props.map(p => new PropertyNode(p)); + this._onDidChangeTreeData.fire(); + + // Show tree in the view + commands.executeCommand(`setContext`, `vscode-db2i:explainingNode`, true); + } + + close() { + commands.executeCommand(`setContext`, `vscode-db2i:explainingNode`, false); + } + + getTreeItem(element: PropertyNode): PropertyNode | Thenable { + return element; + } + + getChildren(element?: PropertyNode): ProviderResult { + return this.propertyNodes; + } + + getParent?(element: any) { + throw new Error("Method not implemented."); + } + resolveTreeItem?(item: TreeItem, element: any, token: CancellationToken): ProviderResult { + throw new Error("Method not implemented."); + } +} + +export class PropertyNode extends TreeItem { + constructor(property: ExplainProperty) { + super(property.title); + this.description = String(property.value); + // Set an empty tooltip, otherwise 'Loading...' is displayed + this.tooltip = ``; + // Differentiate section headings from the rest of the attributes via node highlighting + if (property.type === RecordType.HEADING) { + this.resourceUri = toDoveTreeDecorationProviderUri(new NodeHighlights().set(Highlighting.ATTRIBUTE_SECTION_HEADING)); + this.iconPath = new ThemeIcon("list-tree", Highlighting.Colors[Highlighting.ATTRIBUTE_SECTION_HEADING]); + } + } +} \ No newline at end of file diff --git a/src/views/results/explain/doveResultsView.ts b/src/views/results/explain/doveResultsView.ts new file mode 100644 index 00000000..666c8fcf --- /dev/null +++ b/src/views/results/explain/doveResultsView.ts @@ -0,0 +1,125 @@ +import { CancellationToken, Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, TreeItemCollapsibleState, commands, ThemeIcon } from "vscode"; +import { ExplainNode } from "./nodes"; +import { toDoveTreeDecorationProviderUri } from "./doveTreeDecorationProvider"; + +/** + * Icon labels as defined by the API, along with the name of the icon to display. + * Not surprisingly, the reference link does not provide a complete list of icons. + * TODO: Add missing icons + * @see https://www.ibm.com/docs/en/i/7.5?topic=ssw_ibm_i_75/apis/qqqvexpl.html#icon_labels + * @see https://code.visualstudio.com/api/references/icons-in-labels + */ +const icons = { + "Bitmap Merge": `merge`, + "Cache": ``, + "Cache Probe": ``, + "Delete": `trash`, + "Distinct": `list-flat`, + "Dynamic Bitmap": `symbol-misc`, + "Encoded Vector Index": `symbol-reference`, + "Encoded Vector Index, Parallel": `symbol-reference`, + "Final Select": `selection`, + "Hash Grouping": `group-by-ref-type`, + "Hash Join": `add`, + "Hash Scan": `search`, + "Index Grouping": `group-by-ref-type`, + "Index Scan - Key Positioning": `key`, + "Index Scan - Key Positioning, Parallel": `key`, + "Index Scan - Key Selection": `key`, + "Index Scan - Key Selection, Parallel": `key`, + "Insert": `insert`, + "Nested Loop Join": `add`, + "Select": `selection`, + "Skip Sequential Table Scan": `list-unordered`, + "Skip Sequential Table Scan, Parallel": `list-unordered`, + "Sort": `sort-precedence`, + "Sorted List Scan": `list-ordered`, + "Subquery Merge": `merge`, + "Table Probe": `list-selection`, + "Table Scan": `search`, + "Table Scan, Parallel": `search`, + "Temporary Distinct Hash Table": `new-file`, + "Temporary Hash Table": `new-file`, + "Temporary Index": `new-file`, + "Temporary Sorted List": `list-ordered`, + "Temporary Table": `new-file`, + "Union Merge": `merge`, + "User Defined Table Function": `symbol-function`, + "Unknown": `question`, + "Update": `replace`, + "VALUES LIST": `list-flat`, +} + +type ChangeTreeDataEventType = ExplainTreeItem | undefined | null | void; + +export class DoveResultsView implements TreeDataProvider { + private _onDidChangeTreeData: EventEmitter = new EventEmitter(); + readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; + + private topNode: ExplainTreeItem; + + setRootNode(topNode: ExplainNode): ExplainTreeItem { + this.topNode = new ExplainTreeItem(topNode); + this._onDidChangeTreeData.fire(); + + // Show tree in the view + commands.executeCommand(`setContext`, `vscode-db2i:explaining`, true); + return this.topNode; + } + + getRootExplainNode() { + return this.topNode.explainNode; + } + + close() { + commands.executeCommand(`setContext`, `vscode-db2i:explaining`, false); + } + + getTreeItem(element: ExplainTreeItem): ExplainTreeItem | Thenable { + return element; + } + + getChildren(element?: ExplainTreeItem): ProviderResult { + if (element) { + return element.getChildren(); + } else if (this.topNode) { + return [this.topNode]; + } else { + return []; + } + } + + getParent?(element: any) { + throw new Error("Method not implemented."); + } + resolveTreeItem?(item: TreeItem, element: any, token: CancellationToken): ProviderResult { + throw new Error("Method not implemented."); + } +} + +export class ExplainTreeItem extends TreeItem { + children: ExplainTreeItem[]; + explainNode: ExplainNode; + + constructor(node: ExplainNode) { + super(node.title, node.childrenNodes > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); + + this.explainNode = node; + this.contextValue = `explainTreeItem`; + + // If the node is associated with a DB object, display the qualified object name in the description + if (node.objectSchema && node.objectName) { + this.description = node.objectSchema + `.` + node.objectName; + } + + // TODO: ideally the tooltip would be built using a MarkdownString, but regardless of everything tried, 'Loading...' is always displayed + this.tooltip = [node.title, node.tooltipProps.map(prop => prop.title + `: ` + prop.value).join(`\n`), ``].join(`\n`); + this.resourceUri = toDoveTreeDecorationProviderUri(node.highlights); + this.iconPath = new ThemeIcon(icons[node.title] || `server-process`, node.highlights.getPriorityColor()); // `circle-outline` + } + + getChildren() { + return this.explainNode.children.map(c => new ExplainTreeItem(c)); + } +} + diff --git a/src/views/results/explain/doveTreeDecorationProvider.ts b/src/views/results/explain/doveTreeDecorationProvider.ts new file mode 100644 index 00000000..f7977a3f --- /dev/null +++ b/src/views/results/explain/doveTreeDecorationProvider.ts @@ -0,0 +1,69 @@ +import { window, Event, EventEmitter, TreeItem, ThemeColor, FileDecorationProvider, FileDecoration, Uri, Disposable } from "vscode"; +import { NodeHighlights, Highlighting } from "./nodes"; + +/** + * The Uri scheme for VE node highlights + */ +const doveUriScheme = "db2i.dove"; + +/** + * Generates a {@link DoveTreeDecorationProvider} compatible Uri. The Uri scheme is set to {@link doveUriScheme}. + */ +export function toDoveTreeDecorationProviderUri(highlights: NodeHighlights): Uri { + return highlights.formatValue != 0 ? Uri.parse(doveUriScheme + ":" + highlights.formatValue, false) : null; +} + +/** + * Provides tree node decorations specific to Db2 for i Visual Explain. + */ +export class DoveTreeDecorationProvider implements FileDecorationProvider { + private disposables: Array = []; + + readonly _onDidChangeFileDecorations: EventEmitter = new EventEmitter(); + readonly onDidChangeFileDecorations: Event = this._onDidChangeFileDecorations.event; + + constructor() { + this.disposables = []; + this.disposables.push(window.registerFileDecorationProvider(this)); + } + + async updateTreeItems(treeItem: TreeItem): Promise { + this._onDidChangeFileDecorations.fire(treeItem.resourceUri); + } + + /** + * @inheritdoc + * Provides tree node decorations specific to Db2 for i Visual Explain. + */ + async provideFileDecoration(uri: Uri): Promise { + // Only decorate tree items tagged with the VE scheme + if (uri?.scheme === doveUriScheme) { + // The Uri path should simply be a number that represents the highlight attributes + const value: number = Number(uri.fsPath); + if (!isNaN(value) && value > 0) { + const nodeHighlights = new NodeHighlights(value); + let color: ThemeColor; + let badge: string; + let tooltip: string; + // For attribute section headings, only the color needs to be applied, which is not controlled by the highlight preferences + if (nodeHighlights.isSet(Highlighting.ATTRIBUTE_SECTION_HEADING)) { + color = Highlighting.Colors[Highlighting.ATTRIBUTE_SECTION_HEADING]; + } else { + color = nodeHighlights.getPriorityColor(); + badge = String(nodeHighlights.getCount()); // The number of highlights set for the node + tooltip = "\n" + nodeHighlights.getNames().map(h => "🔥 " + Highlighting.Descriptions[Highlighting[h]]).join("\n"); + } + return { + color: color, + badge: badge, + tooltip: tooltip, + } + } + } + return null; + } + + dispose() { + this.disposables.forEach((d) => d.dispose()); + } +} \ No newline at end of file diff --git a/src/views/results/explain/nodes.ts b/src/views/results/explain/nodes.ts new file mode 100644 index 00000000..ba2d45ae --- /dev/null +++ b/src/views/results/explain/nodes.ts @@ -0,0 +1,385 @@ +import Statement from "../../../database/statement"; +import { ThemeColor } from "vscode"; +import Configuration from "../../../configuration"; + +export interface ExplainNode { + id: number; + title: string; + objectSchema: string; + objectName: string; + childrenNodes: number; + children: ExplainNode[]; + props: ExplainProperty[]; + tooltipProps: ExplainProperty[]; + highlights: NodeHighlights; +} + +export interface ExplainProperty { + type: number; + title: string; + value: string|number; +} + +export class ExplainTree { + private flatNodes: {[nodeId: number]: any[]} = {}; + private order: number[] = []; + private nextNodeIndex = 0; + + private topNode: ExplainNode|undefined; + + constructor(veData) { + for (let node of veData) { + const nodeId = node.IFA_ICON; + + if (!this.order.includes(nodeId)) { + this.order.push(nodeId) + } + + if (!this.flatNodes[nodeId]) { + this.flatNodes[nodeId] = []; + } + + this.flatNodes[nodeId].push(node); + } + + this.topNode = this.handleNode(this.nextNodeIndex); + } + + public get() { + return this.topNode; + } + + private handleNode(index: number) { + const nodeId = this.order[index]; + /** @type {any[]} */ + const nodeData = this.flatNodes[nodeId]; + + let currentNode: ExplainNode = { + id: nodeId, + title: ``, + objectSchema: ``, + objectName: ``, + childrenNodes: 0, + children: [], + props: [], + tooltipProps: [], + highlights: new NodeHighlights() + }; + + // The IFA_CHROUT column is VARCHAR(128) and the IFA_DBLBYT column is VARGRAPHIC(64). When longer data needs to be returned in those columns, + // the IFA_TYPOUT field will contain values 'X' and 'Y' respectively, indicating that the value continues in subsequent result rows. Row + // continuation is terminated in the 'X' case when any other value is found. Double-byte string continuation, represented by 'Y' is terminated + // when a 'D' is found. + let longString = ``; + let longDoubleByteString = ``; + let processingLongString = false; + let processingLongDoubleByteString = false; + // Delta attributes were originally designed for a refresh in Explain While Running, but SQE has decided to return these on the initial paint. + let processingDeltaAttributesForNode = false; + + for (const data of nodeData) { + // TODO: list of column types + const nodeTitle = data.IFA_COLHDG; + // Ignore rows with no title + if (!nodeTitle) { + continue; + } + + // When a DELTA_ATTRIBUTES_INDICATOR row is encountered, the rows following it provide new values for previously processed attributes, + // so we need to look back at currentNode.props to find the right entry to update with a new value. + if (DELTA_ATTRIBUTES_INDICATOR === nodeTitle) { + processingDeltaAttributesForNode = true; + continue; + } + + let nodeValue: any; + switch (data.IFA_TYPOUT) { + case ValueType.LONG_STRING: + // Type indicates a long string so the value is continued in additional rows + processingLongString = true; + longString += data.IFA_CHROUT; + continue; + case ValueType.DOUBLE_BYTE_STRING: + // Type indicates a long double-byte string so the value is continued in additional rows + processingLongDoubleByteString = true; + longDoubleByteString += data.IFA_DBLBYT; + continue; + case ValueType.DOUBLE_BYTE_STRING_END: + processingLongDoubleByteString = false; + nodeValue = longDoubleByteString + data.IFA_DBLBYT; + longDoubleByteString = ``; + break; + case ValueType.NUMERIC: + nodeValue = data.IFA_NUMOUT; + break; + case ValueType.INVISIBLE: + // TODO: invisible attributes are not meant for display, but are associated with context actions defined for the node. + // Until we implement context action support, ignore these attributes. + continue; + case ValueType.CHARACTER: + default: + // If we were processing a long string, we've reached the end of the data, so wrap it up, clear the long string mode + if (processingLongString) { + nodeValue = longString + data.IFA_CHROUT; + longString = ``; + processingLongString = false; + break; + } else if (processingLongDoubleByteString) { + nodeValue = longDoubleByteString + data.IFA_DBLBYT; + longString = ``; + break; + } + nodeValue = data.IFA_CHROUT; + break; + } + + const nodeDataType = data.IFA_COLTYP; + switch (nodeDataType) { + case RecordType.NEW_ICON: + currentNode.title = nodeValue; + break; + case RecordType.CHILD_COUNT: + currentNode.childrenNodes = nodeValue; + break; + case RecordType.CHILD_ICON: + // TODO: what do we do with this? + break; + case RecordType.HEADING: + // If not the first node, add a blank node before the heading to space things out a bit + if (currentNode.props.length > 0) { + currentNode.props.push({ + type: 0, + title: ``, + value: `` + }); + } + currentNode.props.push({ + type: nodeDataType, + title: nodeTitle, + value: `` + }); + break; + default: + if (nodeValue || nodeValue === 0) { + // If processing delta attributes, update the existing property entry, otherwise push a new one + if (processingDeltaAttributesForNode) { + currentNode.props.find(prop => prop.title === nodeTitle).value = nodeValue; + } else { + const newProperty: ExplainProperty = { + type: nodeDataType, + title: nodeTitle, + value: nodeValue + }; + currentNode.props.push(newProperty); + if (data.IFA_FLYORD > 0) { + currentNode.tooltipProps.push(newProperty); + } + } + // If this property is tagged as an object attribute, set the value + switch (data.IFA_IFLAG) { + case `1`: currentNode.objectSchema = Statement.prettyName(nodeValue); break; + case `2`: currentNode.objectName = Statement.prettyName(nodeValue); break; + default: break; + } + } + } + // Update the highlight settings for this node, bitwise excluding 16 ( binary 10000 ) which corresponds to Highlighting.ATTRIBUTE_SECTION_HEADING ( bit index 4, zero-based ), since it doesn't apply to the main VE tree + currentNode.highlights.update(data.IFA_FMTVAL & ~16); + } + + for (let subIndex = 0; subIndex < currentNode.childrenNodes; subIndex++) { + this.nextNodeIndex += 1; + currentNode.children.push(this.handleNode(this.nextNodeIndex)); + } + + if (currentNode.childrenNodes !== currentNode.children.length) { + throw new Error(`This makes no sense.`); + } + + return currentNode; + } + + private dumpNode(node: ExplainNode) { + console.log(node.title); + node.highlights.dump(); + } +} + +const DELTA_ATTRIBUTES_INDICATOR = `BEGIN DELTA ATTRIBUTES`; + +/** + * Record type indicators from the type column ( IFA_COLTYP ) + * @see https://www.ibm.com/docs/en/i/7.5?topic=ssw_ibm_i_75/apis/qqqvexpl.html#record_types + */ +export enum RecordType { + NEW_ICON = 10, + CHILD_COUNT = 11, + CHILD_ICON = 12, + HEADING = 111 +} + +/** + * Value type indicators from the data type column ( IFA_TYPOUT ) + */ +export enum ValueType { + CHARACTER = `C`, + NUMERIC = `N`, + LONG_STRING = `X`, + DOUBLE_BYTE_STRING = `Y`, + DOUBLE_BYTE_STRING_END = `D`, + INVISIBLE = `I` +} + +/** + * Bit indexes for the format value column ( IFA_FMTVAL ) + */ +export enum Highlighting { + ESTIMATED_ROW_EXPENSIVE = 1, + ESTIMATED_TIME_EXPENSIVE = 2, + INDEX_ADVISED = 3, + ATTRIBUTE_SECTION_HEADING = 4, + LOOKAHEAD_PREDICATE_GENERATION = 5, + MATERIALIZED_QUERY_TABLE = 6, + ACTUAL_ROWS_EXPENSIVE = 7, + ACTUAL_TIME_EXPENSIVE = 8, +} + +export namespace Highlighting { + export const SettingsKey = "doveHighlighting"; + export function saveSettings(highlights: NodeHighlights) { + if (highlights) { + Configuration.set(SettingsKey, String(highlights.formatValue)); + } + } + export function getFromSettings(): NodeHighlights { + const saved: number = Number(Configuration.get(SettingsKey)); + // Default to highlighting everything + return isNaN(saved) ? new NodeHighlights().setAll() : new NodeHighlights(saved); + } + + /** + * Returns the Highlighting entries in order of precedence + */ + export function priorityOrder(): Highlighting[] { + return [ + Highlighting.INDEX_ADVISED, + Highlighting.ACTUAL_ROWS_EXPENSIVE, + Highlighting.ACTUAL_TIME_EXPENSIVE, + Highlighting.ESTIMATED_ROW_EXPENSIVE, + Highlighting.ESTIMATED_TIME_EXPENSIVE, + Highlighting.LOOKAHEAD_PREDICATE_GENERATION, + Highlighting.MATERIALIZED_QUERY_TABLE, + Highlighting.ATTRIBUTE_SECTION_HEADING + ]; + } + + export const Descriptions: { [element in Highlighting]: string } = { + 1: /* ESTIMATED_ROW_EXPENSIVE */ "Estimated Number of Rows", + 2: /* ESTIMATED_TIME_EXPENSIVE */ "Estimated Processing Time", + 3: /* INDEX_ADVISED */ "Index Advised", + 4: /* ATTRIBUTE_SECTION_HEADING */ "Attribute Section Heading", + 5: /* LOOKAHEAD_PREDICATE_GENERATION */ "Lookahead Predicate Generation (LPG)", + 6: /* MATERIALIZED_QUERY_TABLE */ "Materialized Query Table (MQT)", + 7: /* ACTUAL_ROWS_EXPENSIVE */ "Actual Number of Rows", + 8: /* ACTUAL_TIME_EXPENSIVE */ "Actual Processing Time", + } + + export const Colors: { [element in Highlighting]: ThemeColor } = { + 1: /* ESTIMATED_ROW_EXPENSIVE */ new ThemeColor("db2i.dove.resultsView.HighlightEstimatedExpensiveRows"), + 2: /* ESTIMATED_TIME_EXPENSIVE */ new ThemeColor("db2i.dove.resultsView.HighlightEstimatedExpensiveTime"), + 3: /* INDEX_ADVISED */ new ThemeColor("db2i.dove.resultsView.HighlightIndexAdvised"), + 4: /* ATTRIBUTE_SECTION_HEADING */ new ThemeColor("db2i.dove.nodeView.AttributeSectionHeading"), + 5: /* LOOKAHEAD_PREDICATE_GENERATION */ new ThemeColor("db2i.dove.resultsView.HighlightLookaheadPredicateGeneration"), + 6: /* MATERIALIZED_QUERY_TABLE */ new ThemeColor("db2i.dove.resultsView.HighlightMaterializedQueryTable"), + 7: /* ACTUAL_ROWS_EXPENSIVE */ new ThemeColor("db2i.dove.resultsView.HighlightActualExpensiveRows"), + 8: /* ACTUAL_TIME_EXPENSIVE */ new ThemeColor("db2i.dove.resultsView.HighlightActualExpensiveTime"), + } +} + +/** + * Simple class for tracking node highlights + * {@link Highlighting} + */ +export class NodeHighlights { + /** The aggregate of format values from all the node attributes */ + formatValue: number = 0; + + constructor(value?: number) { + this.formatValue = value || 0; + } + + /** + * Sets all the highlight bits + */ + setAll(): this { + Object.keys(Highlighting).filter(h => !isNaN(Number(h))).map(h => Number(h)).forEach(h => this.set(h)); + return this; + } + + /** + * Update the highlight bits + */ + update(value: number): this { + this.formatValue |= value; + return this; + } + + /** + * Set the specified highlight bit + */ + set(highlight: Highlighting): this { + this.update(1 << highlight); + return this; + } + + /** + * Returns whether or not the specified highlight bit is set + */ + isSet(highlight: Highlighting): boolean { + return ((this.formatValue >> highlight) % 2) == 1; + } + + /** + * Returns the count of highlight bits that are set + */ + getCount(): number { + return Object.keys(Highlighting).filter(h => isNaN(Number(h))).filter(h => this.isSet(Highlighting[h])).map(h => Highlighting[h]).length; + } + + /** + * Returns the names of the highlight bits that are set, in priority order + */ + getNames(): string[] { + let names: string[] = []; + for (let highlight of Highlighting.priorityOrder()) { + if (this.isSet(highlight)) { + names.push(Highlighting[highlight]); + } + } + return names; + } + + /** + * Returns the highest priority highlight color that matches the user's highlight preferences + */ + getPriorityColor(): ThemeColor { + // From the user's highlight preferences, find the highest priority highlight set for this node + for (let name of Highlighting.getFromSettings().getNames()) { + const highlight: Highlighting = Highlighting[name]; + if (this.isSet(highlight)) { + return Highlighting.Colors[highlight]; + } + } + return null; + } + + dump(): void { + if (this.formatValue > 0) { + console.log([ + " " + this.constructor['name'] + ": " + this.formatValue, + ...Object.keys(Highlighting).filter(h => isNaN(Number(h)) && this.isSet(Highlighting[h])).map(h => " - " + h) + ].join("\n") + ); + } + } +} \ No newline at end of file diff --git a/src/views/results/index.ts b/src/views/results/index.ts index 16356c83..5c663689 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -2,128 +2,19 @@ import vscode, { SnippetString, ViewColumn } from "vscode" import * as csv from "csv/sync"; -import Configuration from "../../configuration" -import * as html from "./html"; -import { getInstance } from "../../base"; + import { JobManager } from "../../config"; -import { Query, QueryState } from "../../connection/query"; -import { updateStatusBar } from "../jobManager/statusBar"; import Document from "../../language/sql/document"; import { changedCache } from "../../language/providers/completionItemCache"; -import { IRange, ObjectRef, ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types"; +import { ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types"; import Statement from "../../language/sql/statement"; +import { ExplainTree } from "./explain/nodes"; +import { DoveResultsView, ExplainTreeItem } from "./explain/doveResultsView"; +import { DoveNodeView } from "./explain/doveNodeView"; +import { DoveTreeDecorationProvider } from "./explain/doveTreeDecorationProvider"; +import { ResultSetPanelProvider } from "./resultSetPanelProvider"; -function delay(t: number, v?: number) { - return new Promise(resolve => setTimeout(resolve, t, v)); -} - -class ResultSetPanelProvider { - _view: vscode.WebviewView; - loadingState: boolean; - constructor() { - this._view = undefined; - this.loadingState = false; - } - - resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken) { - this._view = webviewView; - - webviewView.webview.options = { - // Allow scripts in the webview - enableScripts: true, - }; - - webviewView.webview.html = html.getLoadingHTML(); - this._view.webview.onDidReceiveMessage(async (message) => { - if (message.query) { - let data = []; - - let queryObject = Query.byId(message.queryId); - try { - if (queryObject === undefined) { - // We will need to revisit this if we ever allow multiple result tabs like ACS does - Query.cleanup(); - - let query = await JobManager.getPagingStatement(message.query, { isClCommand: message.isCL, autoClose: true, isTerseResults: true }); - queryObject = query; - } - - let queryResults = queryObject.getState() == QueryState.RUN_MORE_DATA_AVAILABLE ? await queryObject.fetchMore() : await queryObject.run(); - data = queryResults.data; - this._view.webview.postMessage({ - command: `rows`, - rows: queryResults.data, - columnList: queryResults.metadata ? queryResults.metadata.columns.map(x=>x.name) : undefined, // Query.fetchMore() doesn't return the metadata - queryId: queryObject.getId(), - update_count: queryResults.update_count, - isDone: queryResults.is_done - }); - - } catch (e) { - this.setError(e.message); - this._view.webview.postMessage({ - command: `rows`, - rows: [], - queryId: ``, - isDone: true - }); - } - - updateStatusBar(); - } - }); - } - - async ensureActivation() { - let currentLoop = 0; - while (!this._view && currentLoop < 15) { - await this.focus(); - await delay(100); - currentLoop += 1; - } - } - - async focus() { - if (!this._view) { - // Weird one. Kind of a hack. _view.show doesn't work yet because it's not initialized. - // But, we can call a VS Code API to focus on the tab, which then - // 1. calls resolveWebviewView - // 2. sets this._view - await vscode.commands.executeCommand(`vscode-db2i.resultset.focus`); - } else { - this._view.show(true); - } - } - - async setLoadingText(content: string) { - await this.focus(); - - if (!this.loadingState) { - this._view.webview.html = html.getLoadingHTML(); - this.loadingState = true; - } - - html.setLoadingText(this._view.webview, content); - } - - async setScrolling(basicSelect, isCL = false) { - await this.focus(); - - this._view.webview.html = html.generateScroller(basicSelect, isCL); - - this._view.webview.postMessage({ - command: `fetch`, - queryId: `` - }); - } - - setError(error) { - // TODO: pretty error - this._view.webview.html = `

${error}

`; - } -} - -export type StatementQualifier = "statement"|"json"|"csv"|"cl"|"sql"; +export type StatementQualifier = "statement" | "explain" | "json" | "csv" | "cl" | "sql"; export interface StatementInfo { content: string, @@ -140,9 +31,12 @@ export interface ParsedStatementInfo extends StatementInfo { embeddedInfo: ParsedEmbeddedStatement; } -export function initialise(context: vscode.ExtensionContext) { - let resultSetProvider = new ResultSetPanelProvider(); +let resultSetProvider = new ResultSetPanelProvider(); +let doveResultsView = new DoveResultsView(); +let doveNodeView = new DoveNodeView(); +let doveTreeDecorationProvider = new DoveTreeDecorationProvider(); // Self-registers as a tree decoration providor +export function initialise(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.window.registerWebviewViewProvider(`vscode-db2i.resultset`, resultSetProvider, { webviewOptions: { retainContextWhenHidden: true }, @@ -152,71 +46,122 @@ export function initialise(context: vscode.ExtensionContext) { resultSetProvider.loadingState = false; resultSetProvider.setLoadingText(`View will be active when a statement is executed.`); }), + + vscode.window.registerTreeDataProvider(`vscode-db2i.dove.nodes`, doveResultsView), + vscode.window.registerTreeDataProvider(`vscode-db2i.dove.node`, doveNodeView), - vscode.commands.registerCommand(`vscode-db2i.runEditorStatement`, - async (options?: StatementInfo) => { - // Options here can be a vscode.Uri when called from editor context. - // But that isn't valid here. - const optionsIsValid = (options && options.content !== undefined); - let editor = vscode.window.activeTextEditor; + vscode.commands.registerCommand(`vscode-db2i.dove.close`, () => { + doveResultsView.close(); + doveNodeView.close(); + }), + vscode.commands.registerCommand(`vscode-db2i.dove.nodeDetail`, (explainTreeItem: ExplainTreeItem) => { + doveNodeView.setNode(explainTreeItem.explainNode); + }), + vscode.commands.registerCommand(`vscode-db2i.dove.export`, () => { + vscode.workspace.openTextDocument({ + language: `json`, + content: JSON.stringify(doveResultsView.getRootExplainNode(), null, 2) + }).then(doc => { + vscode.window.showTextDocument(doc); + }); + }), - if (optionsIsValid || (editor && editor.document.languageId === `sql`)) { - await resultSetProvider.ensureActivation(); + vscode.commands.registerCommand(`vscode-db2i.explainEditorStatement`, (options?: StatementInfo) => { runHandler({ qualifier: `explain`, ...options }) }), + vscode.commands.registerCommand(`vscode-db2i.runEditorStatement`, (options?: StatementInfo) => { runHandler(options) }) + ) +} - const statementDetail = parseStatement(editor, optionsIsValid ? options : undefined); +async function runHandler(options?: StatementInfo) { + // Options here can be a vscode.Uri when called from editor context. + // But that isn't valid here. + const optionsIsValid = (options && options.content !== undefined); + let editor = vscode.window.activeTextEditor; - if (statementDetail.open) { - const textDoc = await vscode.workspace.openTextDocument({ language: `sql`, content: statementDetail.content }); - editor = await vscode.window.showTextDocument(textDoc, statementDetail.viewColumn, statementDetail.viewFocus); - } + doveResultsView.close(); + doveNodeView.close(); - if (editor) { - const group = statementDetail.group; - editor.selection = new vscode.Selection(editor.document.positionAt(group.range.start), editor.document.positionAt(group.range.end)); + if (optionsIsValid || (editor && editor.document.languageId === `sql`)) { + await resultSetProvider.ensureActivation(); - if (group.statements.length === 1 && statementDetail.embeddedInfo && statementDetail.embeddedInfo.changed) { - editor.insertSnippet( - new SnippetString(statementDetail.embeddedInfo.content) - ) - return; - } - } + const statementDetail = parseStatement(editor, optionsIsValid ? options : undefined); - const statement = statementDetail.statement; - - if (statement.type === StatementType.Create || statement.type === StatementType.Alter) { - const refs = statement.getObjectReferences(); - if (refs.length > 0) { - const ref = refs[0]; - const databaseObj = - statement.type === StatementType.Create && ref.createType.toUpperCase() === `schema` - ? ref.object.schema || `` - : ref.object.schema + ref.object.name; - changedCache.add((databaseObj || ``).toUpperCase()); - } - } + if (options && options.qualifier) { + statementDetail.qualifier = options.qualifier + } + + if (statementDetail.open) { + const textDoc = await vscode.workspace.openTextDocument({ language: `sql`, content: statementDetail.content }); + editor = await vscode.window.showTextDocument(textDoc, statementDetail.viewColumn, statementDetail.viewFocus); + } - if (statementDetail.content.trim().length > 0) { - try { - if (statementDetail.qualifier === `cl`) { - resultSetProvider.setScrolling(statementDetail.content, true); + if (editor) { + const group = statementDetail.group; + editor.selection = new vscode.Selection(editor.document.positionAt(group.range.start), editor.document.positionAt(group.range.end)); + + if (group.statements.length === 1 && statementDetail.embeddedInfo.changed) { + editor.insertSnippet( + new SnippetString(statementDetail.embeddedInfo.content) + ) + return; + } + } + + const statement = statementDetail.statement; + + if (statement.type === StatementType.Create || statement.type === StatementType.Alter) { + const refs = statement.getObjectReferences(); + const ref = refs[0]; + const databaseObj = + statement.type === StatementType.Create && ref.createType.toUpperCase() === `schema` + ? ref.object.schema || `` + : ref.object.schema + ref.object.name; + changedCache.add((databaseObj || ``).toUpperCase()); + } + + if (statementDetail.content.trim().length > 0) { + try { + if (statementDetail.qualifier === `cl`) { + resultSetProvider.setScrolling(statementDetail.content, true); + } else { + if (statementDetail.qualifier === `statement`) { + // If it's a basic statement, we can let it scroll! + resultSetProvider.setScrolling(statementDetail.content); + + } else { + if (statementDetail.qualifier === `explain`) { + const selectedJob = JobManager.getSelection(); + if (selectedJob) { + try { + resultSetProvider.setLoadingText(`Explaining...`); + + const explained = await selectedJob.job.explain(statementDetail.content); + const tree = new ExplainTree(explained.vedata); + const topLevel = tree.get(); + + doveTreeDecorationProvider.updateTreeItems(doveResultsView.setRootNode(topLevel)); + + // TODO: handle when explain without running + resultSetProvider.setScrolling(statementDetail.content, false, explained.id); + + } catch (e) { + resultSetProvider.setError(e.message); + } } else { - if (statementDetail.qualifier === `statement`) { - // If it's a basic statement, we can let it scroll! - resultSetProvider.setScrolling(statementDetail.content); + vscode.window.showInformationMessage(`No job currently selected.`); + } - } else { - // Otherwise... it's a bit complicated. - const data = await JobManager.runSQL(statementDetail.content, undefined); + } else { + // Otherwise... it's a bit complicated. + const data = await JobManager.runSQL(statementDetail.content, undefined); - if (data.length > 0) { - switch (statementDetail.qualifier) { + if (data.length > 0) { + switch (statementDetail.qualifier) { - case `csv`: - case `json`: - case `sql`: - let content = ``; - switch (statementDetail.qualifier) { + case `csv`: + case `json`: + case `sql`: + let content = ``; + switch (statementDetail.qualifier) { case `csv`: content = csv.stringify(data, { header: true, quoted_string: true, @@ -240,41 +185,40 @@ export function initialise(context: vscode.ExtensionContext) { ]; content = insertStatement.join(`\n`); break; - } - - const textDoc = await vscode.workspace.openTextDocument({ language: statementDetail.qualifier, content }); - await vscode.window.showTextDocument(textDoc); - break; } - } else { - vscode.window.showInformationMessage(`Query executed with no data returned.`); - } + const textDoc = await vscode.workspace.openTextDocument({ language: statementDetail.qualifier, content }); + await vscode.window.showTextDocument(textDoc); + break; } - } - - if (statementDetail.qualifier === `statement` && statementDetail.history !== false) { - vscode.commands.executeCommand(`vscode-db2i.queryHistory.prepend`, statementDetail.content); - } - } catch (e) { - let errorText; - if (typeof e === `string`) { - errorText = e.length > 0 ? e : `An error occurred when executing the statement.`; } else { - errorText = e.message || `Error running SQL statement.`; - } - - if (statementDetail.qualifier === `statement` && statementDetail.history !== false) { - resultSetProvider.setError(errorText); - } else { - vscode.window.showErrorMessage(errorText); + vscode.window.showInformationMessage(`Query executed with no data returned.`); } } } } - }), - ) + + if (statementDetail.qualifier === `statement` && statementDetail.history !== false) { + vscode.commands.executeCommand(`vscode-db2i.queryHistory.prepend`, statementDetail.content); + } + + } catch (e) { + let errorText; + if (typeof e === `string`) { + errorText = e.length > 0 ? e : `An error occurred when executing the statement.`; + } else { + errorText = e.message || `Error running SQL statement.`; + } + + if (statementDetail.qualifier === `statement` && statementDetail.history !== false) { + resultSetProvider.setError(errorText); + } else { + vscode.window.showErrorMessage(errorText); + } + } + } + } } export function parseStatement(editor?: vscode.TextEditor, existingInfo?: StatementInfo): ParsedStatementInfo { @@ -287,7 +231,7 @@ export function parseStatement(editor?: vscode.TextEditor, existingInfo?: Statem }; let sqlDocument: Document; - + if (existingInfo) { statementInfo = { ...existingInfo, @@ -316,10 +260,10 @@ export function parseStatement(editor?: vscode.TextEditor, existingInfo?: Statem } if (statementInfo.content) { - [`cl`, `json`, `csv`, `sql`].forEach(mode => { + [`cl`, `json`, `csv`, `sql`, `explain`].forEach(mode => { if (statementInfo.content.trim().toLowerCase().startsWith(mode + `:`)) { statementInfo.content = statementInfo.content.substring(mode.length + 1).trim(); - + //@ts-ignore We know the type. statementInfo.qualifier = mode; } diff --git a/src/views/results/resultSetPanelProvider.ts b/src/views/results/resultSetPanelProvider.ts new file mode 100644 index 00000000..ac7df8d3 --- /dev/null +++ b/src/views/results/resultSetPanelProvider.ts @@ -0,0 +1,116 @@ +import { WebviewView, WebviewViewResolveContext, CancellationToken, commands } from "vscode"; + +import { Query, QueryState } from "../../connection/query"; +import * as html from "./html"; +import { updateStatusBar } from "../jobManager/statusBar"; +import { JobManager } from "../../config"; + +export class ResultSetPanelProvider { + _view: WebviewView; + loadingState: boolean; + constructor() { + this._view = undefined; + this.loadingState = false; + } + + resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, _token: CancellationToken) { + this._view = webviewView; + + webviewView.webview.options = { + // Allow scripts in the webview + enableScripts: true, + }; + + webviewView.webview.html = html.getLoadingHTML(); + this._view.webview.onDidReceiveMessage(async (message) => { + if (message.query) { + let data = []; + + let queryObject = Query.byId(message.queryId); + try { + if (queryObject === undefined) { + // We will need to revisit this if we ever allow multiple result tabs like ACS does + Query.cleanup(); + + let query = await JobManager.getPagingStatement(message.query, { isClCommand: message.isCL, autoClose: true, isTerseResults: true }); + queryObject = query; + } + + let queryResults = queryObject.getState() == QueryState.RUN_MORE_DATA_AVAILABLE ? await queryObject.fetchMore() : await queryObject.run(); + data = queryResults.data; + this._view.webview.postMessage({ + command: `rows`, + rows: queryResults.data, + columnList: queryResults.metadata ? queryResults.metadata.columns.map(x => x.name) : undefined, // Query.fetchMore() doesn't return the metadata + queryId: queryObject.getId(), + update_count: queryResults.update_count, + isDone: queryResults.is_done + }); + + } catch (e) { + this.setError(e.message); + this._view.webview.postMessage({ + command: `rows`, + rows: [], + queryId: ``, + isDone: true + }); + } + + updateStatusBar(); + } + }); + } + + async ensureActivation() { + let currentLoop = 0; + while (!this._view && currentLoop < 15) { + await this.focus(); + await delay(100); + currentLoop += 1; + } + } + + async focus() { + if (!this._view) { + // Weird one. Kind of a hack. _view.show doesn't work yet because it's not initialized. + // But, we can call a VS Code API to focus on the tab, which then + // 1. calls resolveWebviewView + // 2. sets this._view + await commands.executeCommand(`vscode-db2i.resultset.focus`); + } else { + this._view.show(true); + } + } + + async setLoadingText(content) { + await this.focus(); + + if (!this.loadingState) { + this._view.webview.html = html.getLoadingHTML(); + this.loadingState = true; + } + + html.setLoadingText(this._view.webview, content); + } + + async setScrolling(basicSelect, isCL = false, queryId: string = ``) { + await this.focus(); + + this._view.webview.html = html.generateScroller(basicSelect, isCL); + + this._view.webview.postMessage({ + command: `fetch`, + queryId + }); + } + + setError(error) { + // TODO: pretty error + this._view.webview.html = `

${error}

`; + } +} + +function delay(t: number, v?: number) { + return new Promise(resolve => setTimeout(resolve, t, v)); +}