private async doExecution()

in gui/frontend/src/modules/shell/ShellTab.tsx [351:710]


    private async doExecution(command: string, context: ExecutionContext, index: number,
        _params?: Array<[string, string]>): Promise<void> {
        const { savedState } = this.props;

        savedState.lastCommand = command;

        try {
            const { id = "" } = this.props;

            let columns: IColumnInfo[] = [];
            let resultId = uuid();

            const finalResult = await savedState.backend.execute(command, undefined, (data, requestId) => {
                if (!data.result) {
                    return;
                }

                const result = data.result;

                // Shell response results can have many different fields. There's no other way but to test
                // for each possible field to see what a response is about.
                if (this.isShellShellDocumentData(result)) {
                    // Document data must be handled first, as that includes an info field,
                    // like the simple result.
                    if (result.hasData) {
                        const documentString = result.documents.length === 1 ? "document" : "documents";
                        const status = {
                            type: MessageType.Info,
                            text: `${result.documents.length} ${documentString} in set ` +
                                `(${result.executionTime})`,
                        };

                        if (result.warningsCount > 0) {
                            status.type = MessageType.Warning;
                            status.text += `, ${result.warningsCount} ` +
                                `${result.warningsCount === 1 ? "warning" : "warnings"}`;
                        }

                        const text: ITextResultEntry[] = [{
                            type: MessageType.Info,
                            index,
                            content: JSON.stringify(result.documents, undefined, "\t"),
                            language: "json",
                        }];

                        result.warnings.forEach((warning) => {
                            text.push({
                                type: MessageType.Warning,
                                index,
                                content: `\n${warning.message}`,
                            });
                        });

                        this.addTimedResult(context, {
                            type: "text",
                            text,
                            executionInfo: status,
                        }, resultId);
                    } else {
                        // No data was returned. Use the info field for the status message then.
                        this.addTimedResult(context, {
                            type: "text",
                            text: [{
                                type: MessageType.Info,
                                index,
                                content: result.info,
                                language: "ansi",
                            }],
                            executionInfo: { text: "" },
                        }, resultId);
                    }
                } else if (this.isShellShellColumnsMetaData(result)) {
                    const rawColumns = Object.values(result).map((value) => {
                        return {
                            name: unquote(value.Name),
                            type: value.Type,
                            length: value.Length,
                        };
                    });
                    columns.push(...generateColumnInfo(
                        context.language === "mysql" ? DBType.MySQL : DBType.Sqlite, rawColumns, true));
                } else if (this.isShellShellRowData(result)) {
                    // If we have column info at this point then we got SQL mode results (show the result grid).
                    // Otherwise display the result as JSON text.
                    if (columns.length === 0) {
                        const resultText = result.warningsCount > 0 ?
                            `finished with warnings (${result.warningsCount})` : "OK";
                        let info = `Query ${resultText}, ${result.affectedItemsCount || result.rows.length} ` +
                            `rows affected`;
                        if (result.executionTime) {
                            info += ` (${result.executionTime})`;
                        }

                        // If we have data show it as result otherwise only the execution info.
                        if (result.rows.length > 0) {
                            const content = JSON.stringify(result.rows, undefined, "\t") + "\n";

                            this.addTimedResult(context, {
                                type: "text",
                                text: [{
                                    type: MessageType.Info,
                                    index,
                                    content,
                                    language: "json",
                                }],
                                executionInfo: { type: MessageType.Response, text: info },
                            }, resultId);
                        } else {
                            this.addTimedResult(context, {
                                type: "text",
                                executionInfo: { type: MessageType.Info, text: info },
                            }, resultId);
                        }
                    } else {
                        const rowString = result.rows.length === 1 ? "row" : "rows";
                        const status = {
                            type: MessageType.Info,
                            text: `${result.rows.length} ${rowString} in set (${result.executionTime})`,
                        };

                        if (result.warningsCount > 0) {
                            status.type = MessageType.Warning;
                            status.text += `, ${result.warningsCount} ` +
                                `${result.warningsCount === 1 ? "warning" : "warnings"}`;
                        }

                        // Flatten nested objects + arrays.
                        result.rows.forEach((value) => {
                            flattenObject(value as IDictionary);
                        });

                        const rows = convertRows(columns, result.rows);

                        void ApplicationDB.db.add("shellModuleResultData", {
                            tabId: id,
                            resultId,
                            rows,
                            columns,
                            executionInfo: status,
                            index,
                        });

                        this.addTimedResult(context, {
                            type: "resultSetRows",
                            rows,
                            columns,
                            currentPage: 0,
                            executionInfo: status,
                        }, resultId);
                    }

                    // We got the final data. Start a new result.
                    resultId = uuid();
                    columns = [];
                } else if (this.isShellShellData(result)) {
                    // Unspecified shell data (no documents, no rows). Just print the info as status, for now.
                    this.addTimedResult(context, {
                        type: "text",
                        text: [{
                            type: MessageType.Info,
                            index,
                            content: result.info,
                            language: "ansi",
                        }],
                        executionInfo: { text: "" },
                    }, resultId);
                } else if (this.isShellObjectListResult(result)) {
                    let text = "[\n";
                    result.forEach((value) => {
                        text += "\t<" + value.class;
                        if (value.name) {
                            text += ":" + value.name;
                        }
                        text += ">\n";
                    });
                    text += "]";
                    this.addTimedResult(context, {
                        type: "text",
                        text: [{
                            type: MessageType.Info,
                            index,
                            content: text,
                            language: "xml",
                        }],
                    }, resultId);
                } else if (this.isShellSimpleResult(result)) {
                    if (result.error) {
                        // Errors can be a string or an object with a string.
                        const text = typeof result.error === "string" ? result.error : result.error.message;
                        this.addTimedResult(context, {
                            type: "text",
                            text: [{
                                type: MessageType.Error,
                                index,
                                content: text,
                                language: "ansi",
                            }],
                            executionInfo: { type: MessageType.Error, text: "" },
                        }, resultId);
                    } else if (result.warning) {
                        // Errors can be a string or an object with a string.
                        this.addTimedResult(context, {
                            type: "text",
                            text: [{
                                type: MessageType.Info,
                                index,
                                content: result.warning,
                                language: "ansi",
                            }],
                            executionInfo: { type: MessageType.Warning, text: "" },
                        }, resultId);
                    } else {
                        // See if a new session is going to be established. That's our sign to
                        // remove a previously stored password.
                        // If the scheme is explicit, the message will include the type of session that is being
                        // created ("Classic" or "X"). We can be open-ended and look for any kind of session, even
                        // those types that do not yet exist.
                        const newSessionPattern = /^Creating (a|an)([\s\w]+)? session to/;
                        if (result.info && newSessionPattern.test(result.info)) {
                            this.closeDbSession(result.info);
                        }

                        const content = (result.info ?? result.note ?? result.status)!;
                        this.addTimedResult(context, {
                            type: "text",
                            text: [{
                                type: MessageType.Info,
                                index,
                                content,
                                language: "ansi",
                            }],
                            executionInfo: { type: MessageType.Info, text: "" },
                        }, resultId);
                    }

                    // We got a full result. Start a new one.
                    resultId = uuid();
                    columns = [];
                } else if (this.isShellValueResult(result)) {
                    this.addTimedResult(context, {
                        type: "text",
                        text: [{
                            type: MessageType.Info,
                            index,
                            content: String(result.value),
                            language: "ansi",
                        }],
                    }, resultId);
                } else {
                    // XXX: The password requisition is no longer available. Find a different way to capture
                    //      the password.
                    //requisitions.register("acceptPassword", this.acceptPassword);

                    if (!ShellPromptHandler.handleShellPrompt(result, requestId, savedState.backend)) {
                        if (this.isShellObjectResult(result)) {
                            let text = "<" + result.class;
                            if (result.name) {
                                text += ":" + result.name;
                            }
                            text += ">";
                            this.addTimedResult(context, {
                                type: "text",
                                text: [{
                                    type: MessageType.Info,
                                    index,
                                    content: text,
                                    language: "xml",
                                }],
                            }, resultId);
                        } else {
                            // If no specialized result then print as is.
                            const executionInfo: IStatusInfo = {
                                text: result ? "" : JSON.stringify(result, undefined, "\t"),
                            };

                            const text = !result ? [] : [{
                                type: MessageType.Info,
                                index,
                                content: JSON.stringify(result, undefined, "\t"),
                                language: "json" as ResultTextLanguage,
                            }];

                            this.addTimedResult(context, {
                                type: "text",
                                text,
                                executionInfo,
                            }, resultId);
                        }
                    }
                }
            });

            // Handling the final response here.
            if (finalResult && this.hasPromptDescriptor(finalResult)) {
                // A descriptor change occurs when any of the following change types occur:
                // 1) Changes involving an active session update:
                //    1.1) A new active shell session was established
                //    1.2) The active shell session got disconnected
                // 2) Changes not involving changes on the active session:
                //   2.1) The active schema changed
                //   2.2) The Shell Console mode changed
                const hadOpenSession = savedState.dbSession !== undefined;
                const needsOpenSession = (finalResult.promptDescriptor?.host !== undefined)
                    || (finalResult.promptDescriptor?.socket !== undefined);

                const existing = savedState.promptDescriptor;
                let newSessionRequired = (!hadOpenSession && needsOpenSession);
                if (!newSessionRequired) {
                    newSessionRequired = (existing?.user !== finalResult.promptDescriptor?.user)
                        || (existing?.host !== finalResult.promptDescriptor?.host)
                        || (existing?.port !== finalResult.promptDescriptor?.port)
                        || (existing?.socket !== finalResult.promptDescriptor?.socket)
                        || (existing?.ssl !== finalResult.promptDescriptor?.ssl)
                        || (existing?.session !== finalResult.promptDescriptor?.session);
                }

                // Prompt needs to be updated the first time a descriptor comes in.
                let refreshRequired = !savedState.promptDescriptor
                    || newSessionRequired
                    || (hadOpenSession !== needsOpenSession);

                if (!refreshRequired) {
                    refreshRequired = (existing?.isProduction !== finalResult.promptDescriptor?.isProduction)
                        || (existing?.mode !== finalResult.promptDescriptor?.mode)
                        || (existing?.schema !== finalResult.promptDescriptor?.schema);
                }

                savedState.promptDescriptor = finalResult.promptDescriptor;

                if (hadOpenSession && !needsOpenSession) {
                    // 1.2) There was an active session and got closed
                    this.closeDbSession("");
                    void requisitions.execute("updateShellPrompt", finalResult);
                } else if (newSessionRequired) {
                    // A new session will be created, however if password is not available, it attempts to
                    // get it from the last executed command (common case when password is given in the
                    // connection string)
                    if (savedState.lastPassword === undefined) {
                        savedState.lastPassword = this.getPasswordFromLastCommand();
                    }

                    // Only trigger the creation of the Session if the password was defined (even if empty)
                    // otherwise the start session will be requiring shell prompt handling
                    if (savedState.lastPassword !== undefined) {
                        // 1.1) The active session got created or updated
                        await this.openDbSession();
                    }
                    await requisitions.execute("updateShellPrompt", finalResult);
                } else if (refreshRequired) {
                    // 2) Changes not related to a session update were detected
                    await requisitions.execute("updateShellPrompt", finalResult);
                }
            }
        } catch (reason) {
            const message = convertErrorToString(reason);
            void ui.showErrorMessage(`Shell Execution Error: ${message}`, {});

            throw reason;
        }
    }