waldrumpus
waldrumpus

Reputation: 2590

Azure Data Studio extension unable to edit data

I am writing an extension for Azure Data Studio (ADS) that will enable users to work with Oracle Database. The current task is writing an anzdata.QueryProvider implementation that makes it possible to query and edit the contents of tables.

Table editing can be initialised by using the Object Explorer's context menu of a table node and choosing "Edit Data". My Query Provider implementation successfully executes the query to select the table contents, which is then displayed by ADS as a grid.

What isn't working is the actual editing. The grid and its cells appear to be read-only.

Editing is disabled for my custom query provider

Unlike the built-in Microsoft SQL Server result grid, which allows altering data and committing changes.

Editing is enabled using a built-in query provider

Since extensions communicate with ADS by registering providers and sending messages, I assume that my extension is neglecting to send the appropriate messages at the right time in the right order. Alternatively, the data structures that represent the editable table rows and cells might be missing something or causing an unseen internal error, causing ADS to treat them as read-only.

Below is the entire implementation of the QueryProvider; as you can see many methods have not yet been implemented, but I am fairly certain they are never being called during the current flow of interaction, since the errors they throw are never being logged or displayed, and breakpoints set on those lines are also never hit.

Any help with fixing this issue, or pointing to sample code for similar ADS extensions that allow editing data (are there any?) would be much appreciated.

import * as azdata from "azdata";
import { ConnectionProvider } from "./connectionProvider";
import * as vscode from "vscode";
import OracleDB = require("oracledb");

export class QueryProvider implements azdata.QueryProvider {
  public static readonly providerId = "oracle";
  public readonly providerId = QueryProvider.providerId;

  private queryComplete?: (
    result: azdata.QueryExecuteCompleteNotificationResult
  ) => any;
  private onBatchStart?: (
    batchInfo: azdata.QueryExecuteBatchNotificationParams
  ) => any;
  private onBatchComplete?: (
    batchInfo: azdata.QueryExecuteBatchNotificationParams
  ) => any;
  private resultSetAvailable?: (
    resultSetInfo: azdata.QueryExecuteResultSetNotificationParams
  ) => any;
  private onEditSessionReady?: (
    ownerUri: string,
    success: boolean,
    message: string
  ) => any;
  private onMessage?: (message: azdata.QueryExecuteMessageParams) => any;
  private resultSetUpdated?: (
    resultSetInfo: azdata.QueryExecuteResultSetNotificationParams
  ) => any;

  private readonly results: {
    [key: string]: { [column: string]: string | number | null }[];
  } = {};

  constructor(private readonly connections: ConnectionProvider) {}

  cancelQuery(ownerUri: string): Thenable<azdata.QueryCancelResult> {
    throw new Error("Method not implemented.");
  }

  async runQuery(
    ownerUri: string,
    selection: azdata.ISelectionData,
    runOptions?: azdata.ExecutionPlanOptions | undefined
  ): Promise<void> {
    if (this.connections.connections[ownerUri]) {
      // Encode the URI before parsing in case it contains reserved characters such as %20 so that the parse doesn't
      // parse them into their values accidently (such as if a path had %20 in the actual path name - that shouldn't be
      // resolved to a space)
      const content = (
        await vscode.workspace.openTextDocument(
          vscode.Uri.parse(encodeURI(ownerUri))
        )
      ).getText();
      const result = await this.connections.connections[ownerUri].execute(
        content
      );
      const rows: { [column: string]: string | number | null }[] =
        result.rows!.map((row) =>
          (row as any[]).reduce(
            (acc, cur, i) => ({ ...acc, [result.metaData![i].name]: cur }),
            {}
          )
        ) ?? [];

      this.results[ownerUri] = rows;
      const resultSet = {
        id: 0,
        complete: true,
        rowCount: rows.length,
        batchId: 0,
        columnInfo: Object.keys(rows[0]).map((v) => ({ columnName: v })),
      };
      const batchSet = { id: 0, resultSetSummaries: [resultSet] };
      this.onBatchStart!({ batchSummary: { id: 0 } as any, ownerUri });
      this.resultSetAvailable!({
        ownerUri,
        resultSetSummary: resultSet as any,
      });
      this.onBatchComplete!({ batchSummary: batchSet as any, ownerUri });
      this.queryComplete!({ ownerUri, batchSummaries: [batchSet] as any });
    } else {
      throw new Error("Connection not found");
    }
  }

  async runOracleQuery(
    ownerUri: string,
    query: string
  ): Promise<OracleDB.Result<unknown>> {
    return await this.connections.connections[ownerUri].execute(query);
  }

  runQueryStatement(
    ownerUri: string,
    line: number,
    column: number
  ): Thenable<void> {
    throw new Error("Method not implemented.");
  }

  runQueryString(ownerUri: string, queryString: string): Thenable<void> {
    throw new Error("Method not implemented.");
  }

  runQueryAndReturn(
    ownerUri: string,
    queryString: string
  ): Thenable<azdata.SimpleExecuteResult> {
    throw new Error("Method not implemented.");
  }

  parseSyntax(
    ownerUri: string,
    query: string
  ): Thenable<azdata.SyntaxParseResult> {
    throw new Error("Method not implemented.");
  }

  async getQueryRows(
    rowData: azdata.QueryExecuteSubsetParams
  ): Promise<azdata.QueryExecuteSubsetResult> {
    const rows = this.results[rowData.ownerUri];
    if (rows) {
      const rowSubset = rows.slice(
        rowData.rowsStartIndex,
        rowData.rowsStartIndex + rowData.rowsCount
      );
      const subset: azdata.QueryExecuteSubsetResult = {
        message: '',
        resultSubset: {
          rowCount: rows.length,
          rows: rowSubset.map((row) =>
            Object.keys(row).map((key) => {
              return {
                displayValue: `${row[key]}`,
                invariantCultureDisplayValue: `${row[key]}`,
                isNull: false,
              };
            })
          ),
        },
      };
      return subset;
    }
    throw new Error("Rows not found");
  }

  disposeQuery(ownerUri: string): Thenable<void> {
    throw new Error("Method not implemented.");
  }

  saveResults(
    requestParams: azdata.SaveResultsRequestParams
  ): Thenable<azdata.SaveResultRequestResult> {
    throw new Error("Method not implemented.");
  }

  setQueryExecutionOptions(
    ownerUri: string,
    options: azdata.QueryExecutionOptions
  ): Thenable<void> {
    throw new Error("Method not implemented.");
  }

  async commitEdit(ownerUri: string): Promise<void> {
    console.log("commitEdit", ownerUri);
  }

  createRow(ownerUri: string): Thenable<azdata.EditCreateRowResult> {
    throw new Error("Method not implemented.");
  }

  deleteRow(ownerUri: string, rowId: number): Thenable<void> {
    throw new Error("Method not implemented.");
  }

  disposeEdit(ownerUri: string): Thenable<void> {
    throw new Error("Method not implemented.");
  }

  async initializeEdit(
    ownerUri: string,
    schemaName: string,
    objectName: string,
    objectType: string,
    rowLimit: number,
    queryString: string
  ): Promise<void> {
    console.log(
      "initializeEdit",
      ownerUri,
      schemaName,
      objectName,
      objectType,
      rowLimit,
      queryString
    );
    this.onBatchStart!({ batchSummary: { id: 0 } as any, ownerUri });
    const result = await this.runOracleQuery(
      ownerUri,
      `SELECT * FROM ${objectName}`
    );
    const rows: { [column: string]: string | number | null }[] =
      result.rows!.map((row) =>
        (row as any[]).reduce(
          (acc, cur, i) => ({ ...acc, [result.metaData![i].name]: cur }),
          {}
        )
      ) ?? [];
    const resultSet = {
      id: 0,
      complete: true,
      rowCount: rows.length,
      batchId: 0,
      columnInfo: Object.keys(rows[0]).map((v) => ({ columnName: v })),
    };
    const batchSet = { id: 0, resultSetSummaries: [resultSet] };
    this.results[ownerUri] = rows;
    this.resultSetAvailable!({ ownerUri, resultSetSummary: resultSet as any });
    this.onBatchComplete!({ batchSummary: batchSet as any, ownerUri });
    this.queryComplete!({ ownerUri, batchSummaries: [batchSet] as any });
    this.onEditSessionReady!(ownerUri, true, '');
  }

  revertCell(
    ownerUri: string,
    rowId: number,
    columnId: number
  ): Thenable<azdata.EditRevertCellResult> {
    throw new Error("Method not implemented.");
  }

  revertRow(ownerUri: string, rowId: number): Thenable<void> {
    throw new Error("Method not implemented.");
  }

  updateCell(
    ownerUri: string,
    rowId: number,
    columnId: number,
    newValue: string
  ): Thenable<azdata.EditUpdateCellResult> {
    throw new Error("Method not implemented.");
  }

  async getEditRows(
    rowData: azdata.EditSubsetParams
  ): Promise<azdata.EditSubsetResult> {
    const rows = this.results[rowData.ownerUri];
    if (rows) {
      const rowSubset = rows.slice(
        rowData.rowStartIndex,
        rowData.rowStartIndex + rowData.rowCount
      );
      let id = 0;
      const subset: azdata.EditRow[] = rowSubset.map((row) => {
        return {
          id: id++,
          isDirty: false,
          state: azdata.EditRowState.clean,
          cells: Object.keys(row).map((key, i) => {
            return {
              displayValue: `${row[key]}`,
              invariantCultureDisplayValue: `${row[key]}`,
              isNull: row[key] === null,
            };
          }),
        };
      });
      return {
        rowCount: rows.length,
        subset: subset,
      };
    }
    throw new Error("Rows not found");
  }

  /****************
   * Event Handlers
   */

  registerOnQueryComplete(
    handler: (result: azdata.QueryExecuteCompleteNotificationResult) => any
  ): vscode.Disposable {
    this.queryComplete = handler;
    return { dispose: () => (this.queryComplete = undefined) };
  }

  registerOnBatchStart(
    handler: (batchInfo: azdata.QueryExecuteBatchNotificationParams) => any
  ): vscode.Disposable {
    this.onBatchStart = handler;
    return { dispose: () => (this.onBatchStart = undefined) };
  }

  registerOnBatchComplete(
    handler: (batchInfo: azdata.QueryExecuteBatchNotificationParams) => any
  ): vscode.Disposable {
    this.onBatchComplete = handler;
    return { dispose: () => (this.onBatchComplete = undefined) };
  }

  registerOnResultSetAvailable(
    handler: (
      resultSetInfo: azdata.QueryExecuteResultSetNotificationParams
    ) => any
  ): vscode.Disposable {
    this.resultSetAvailable = handler;
    return { dispose: () => (this.resultSetAvailable = undefined) };
  }

  registerOnResultSetUpdated(
    handler: (
      resultSetInfo: azdata.QueryExecuteResultSetNotificationParams
    ) => any
  ): vscode.Disposable {
    this.resultSetUpdated = handler;
    return {
      dispose: () => {
        this.resultSetUpdated = undefined;
      },
    };
  }

  registerOnMessage(
    handler: (message: azdata.QueryExecuteMessageParams) => any
  ): vscode.Disposable {
    this.onMessage = handler;
    return {
      dispose: () => {
        this.onMessage = undefined;
      },
    };
  }

  registerOnEditSessionReady(
    handler: (ownerUri: string, success: boolean, message: string) => any
  ): vscode.Disposable {
    this.onEditSessionReady = handler;
    return {
      dispose: () => {
        this.onEditSessionReady = undefined;
      },
    };
  }
}

Upvotes: 0

Views: 116

Answers (0)

Related Questions