Skip to content

Commit

Permalink
Merge pull request #90079 from xinhaoz/backport22.1-85080-88859
Browse files Browse the repository at this point in the history
  • Loading branch information
xinhaoz authored Oct 20, 2022
2 parents c03f6c0 + f1094cb commit e7f33f2
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 0 deletions.
38 changes: 38 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/fetchData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,41 @@ export const fetchData = <P extends ProtoBuilder<P>, T extends ProtoBuilder<T>>(
})
.then(buffer => RespBuilder.decode(new Uint8Array(buffer)));
};

/**
* fetchDataJSON makes a request for /api/v2 which uses content type JSON.
* @param path relative path for requested resource.
* @param reqPayload request payload object.
*/
export function fetchDataJSON<ResponseType, RequestType>(
path: string,
reqPayload?: RequestType,
): Promise<ResponseType> {
const params: RequestInit = {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Cockroach-API-Session": "cookie",
},
credentials: "same-origin",
};

if (reqPayload) {
params.method = "POST";
params.body = JSON.stringify(reqPayload);
}

const basePath = getBasePath();

return fetch(`${basePath}${path}`, params).then(response => {
if (!response.ok) {
throw new RequestError(
response.statusText,
response.status,
response.statusText,
);
}

return response.json();
});
}
1 change: 1 addition & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from "./statementDiagnosticsApi";
export * from "./statementsApi";
export * from "./basePath";
export * from "./nodesApi";
export * from "./sqlApi";
86 changes: 86 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/sqlApi.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { SqlExecutionResponse, sqlResultsAreEmpty } from "./sqlApi";

describe("sqlApi", () => {
test("sqlResultsAreEmpty should return true when there are no rows in the response", () => {
const testCases: {
response: SqlExecutionResponse<unknown>;
expected: boolean;
}[] = [
{
response: {
num_statements: 1,
execution: {
retries: 0,
txn_results: [
{
statement: 1,
tag: "SELECT",
start: "start-date",
end: "end-date",
rows_affected: 0,
rows: [{ hello: "world" }],
},
],
},
},
expected: false,
},
{
response: {
num_statements: 1,
execution: {
retries: 0,
txn_results: [
{
statement: 1,
tag: "SELECT",
start: "start-date",
end: "end-date",
rows_affected: 0,
rows: [],
},
],
},
},
expected: true,
},
{
response: {
num_statements: 1,
execution: {
retries: 0,
txn_results: [
{
statement: 1,
tag: "SELECT",
start: "start-date",
end: "end-date",
rows_affected: 0,
columns: [],
},
],
},
},
expected: true,
},
{
response: {},
expected: true,
},
];

testCases.forEach(tc => {
expect(sqlResultsAreEmpty(tc.response)).toEqual(tc.expected);
});
});
});
121 changes: 121 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/sqlApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { fetchDataJSON } from "./fetchData";

export type SqlExecutionRequest = {
statements: SqlStatement[];
execute?: boolean;
timeout?: string; // Default 5s
application_name?: string; // Defaults to '$ api-v2-sql'
database?: string; // Defaults to defaultDb
max_result_size?: number; // Default 10kib
};

export type SqlStatement = {
sql: string;
arguments?: unknown[];
};

export type SqlExecutionResponse<T> = {
num_statements?: number;
execution?: SqlExecutionExecResult<T>;
error?: SqlExecutionErrorMessage;
request?: SqlExecutionRequest;
};

export interface SqlExecutionExecResult<T> {
retries: number;
txn_results: SqlTxnResult<T>[];
}

export type SqlTxnResult<RowType> = {
statement: number; // Statement index from input array
tag: string; // Short stmt tag
start: string; // Start timestamp, encoded as RFC3339
end: string; // End timestamp, encoded as RFC3339
rows_affected: number;
columns?: SqlResultColumn[];
rows?: RowType[];
error?: Error;
};

export type SqlResultColumn = {
name: string;
type: string;
oid: number;
};

export type SqlExecutionErrorMessage = {
message: string;
code: string;
severity: string;
source: { file: string; line: number; function: "string" };
};

export const SQL_API_PATH = "/api/v2/sql/";

/**
* executeSql executes the provided SQL statements in a single transaction
* over HTTP.
*
* @param req execution request details
*/
export function executeSql<RowType>(
req: SqlExecutionRequest,
): Promise<SqlExecutionResponse<RowType>> {
return fetchDataJSON<SqlExecutionResponse<RowType>, SqlExecutionRequest>(
SQL_API_PATH,
req,
);
}

export const INTERNAL_SQL_API_APP = "$ internal-console";
export const LONG_TIMEOUT = "300s";
export const LARGE_RESULT_SIZE = 50000; // 50 kib

/**
* executeInternalSql executes the provided SQL statements with
* the app name set to the internal sql api app name above.
* Note that technically all SQL executed over this API are
* executed as internal, but we make this distinction using the
* function name for when we want to execute user queries in the
* future, where such queries should not have an internal app name.
*
* @param req execution request details
*/
export function executeInternalSql<RowType>(
req: SqlExecutionRequest,
): Promise<SqlExecutionResponse<RowType>> {
if (!req.application_name) {
req.application_name = INTERNAL_SQL_API_APP;
} else {
req.application_name = `$ internal-${req.application_name}`;
}

return executeSql(req);
}

/**
* sqlResultsAreEmpty returns true if the provided result
* does not contain any rows.
* @param result the sql execution result returned by the server
* @returns
*/
export function sqlResultsAreEmpty(
result: SqlExecutionResponse<unknown>,
): boolean {
return (
!result.execution?.txn_results?.length ||
result.execution.txn_results.every(
txn => !txn.rows || txn.rows.length === 0,
)
);
}

0 comments on commit e7f33f2

Please sign in to comment.