Skip to content

Commit d404afe

Browse files
authored
NoSQL execute query (#2217)
* Saved work * Simple working query editor * Test getQueryPlan * Better code lens * Show query result/plan side by side prototype * Support query with metrics * Add connect code lens * Add package json declaration * Rename new query title
1 parent 04c76ba commit d404afe

8 files changed

+263
-10
lines changed

extension.bundle.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,23 @@ export * from './src/docdb/registerDocDBCommands';
2727
export { activateInternal, cosmosDBCopyConnectionString, createServer, deactivateInternal, deleteAccount } from './src/extension';
2828
export { ext } from './src/extensionVariables';
2929
export * from './src/graph/registerGraphCommands';
30-
export { connectToMongoClient, isCosmosEmulatorConnectionString } from './src/mongo/connectToMongoClient';
3130
export { MongoCommand } from './src/mongo/MongoCommand';
32-
export { addDatabaseToAccountConnectionString, encodeMongoConnectionString, getDatabaseNameFromConnectionString } from './src/mongo/mongoConnectionStrings';
3331
export { findCommandAtPosition, getAllCommandsFromText } from './src/mongo/MongoScrapbook';
3432
export { MongoShell } from './src/mongo/MongoShell';
33+
export { connectToMongoClient, isCosmosEmulatorConnectionString } from './src/mongo/connectToMongoClient';
34+
export { addDatabaseToAccountConnectionString, encodeMongoConnectionString, getDatabaseNameFromConnectionString } from './src/mongo/mongoConnectionStrings';
3535
export * from './src/mongo/registerMongoCommands';
3636
export { IDatabaseInfo } from './src/mongo/tree/MongoAccountTreeItem';
3737
export { addDatabaseToConnectionString } from './src/postgres/postgresConnectionStrings';
3838
export { AttachedAccountsTreeItem, MONGO_CONNECTION_EXPECTED } from './src/tree/AttachedAccountsTreeItem';
3939
export { AzureAccountTreeItemWithAttached } from './src/tree/AzureAccountTreeItemWithAttached';
4040
export * from './src/utils/azureClients';
41+
export { getPublicIpv4, isIpInRanges } from './src/utils/getIp';
4142
export { improveError } from './src/utils/improveError';
4243
export { randomUtils } from './src/utils/randomUtils';
4344
export { getGlobalSetting, updateGlobalSetting } from './src/utils/settingUtils';
4445
export { rejectOnTimeout, valueOnTimeout } from './src/utils/timeout';
45-
export { getDocumentTreeItemLabel, IDisposable } from './src/utils/vscodeUtils';
46+
export { IDisposable, getDocumentTreeItemLabel } from './src/utils/vscodeUtils';
4647
export { wrapError } from './src/utils/wrapError';
47-
export { isIpInRanges, getPublicIpv4 } from './src/utils/getIp'
4848

4949
// NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen

package.json

+38
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@
151151
"extensions": [
152152
".psql"
153153
]
154+
},
155+
{
156+
"id": "nosql",
157+
"aliases": [
158+
"Cosmos NoSQL",
159+
"nosql"
160+
],
161+
"extensions": [
162+
".nosql"
163+
]
154164
}
155165
],
156166
"grammars": [
@@ -214,6 +224,12 @@
214224
"command": "cosmosDB.connectMongoDB",
215225
"title": "Connect to Database..."
216226
},
227+
{
228+
"category": "Cosmos DB",
229+
"command": "cosmosDB.connectNoSqlContainer",
230+
"title": "Connect to NoSQL container",
231+
"when": "false"
232+
},
217233
{
218234
"category": "Cosmos DB",
219235
"command": "cosmosDB.copyConnectionString",
@@ -324,6 +340,18 @@
324340
"command": "cosmosDB.executeMongoCommand",
325341
"title": "Execute MongoDB Command"
326342
},
343+
{
344+
"category": "Cosmos DB",
345+
"command": "cosmosDB.executeNoSqlQuery",
346+
"title": "Execute NoSQL Query",
347+
"when": "false"
348+
},
349+
{
350+
"category": "Cosmos DB",
351+
"command": "cosmosDB.getNoSqlQueryPlan",
352+
"title": "Get NoSQL Query Plan",
353+
"when": "false"
354+
},
327355
{
328356
"category": "Cosmos DB",
329357
"command": "cosmosDB.importDocument",
@@ -360,6 +388,11 @@
360388
"command": "cosmosDB.openStoredProcedure",
361389
"title": "Open Stored Procedure"
362390
},
391+
{
392+
"category": "Core (SQL)",
393+
"command": "cosmosDB.writeNoSqlQuery",
394+
"title": "Create New NoSQL Query"
395+
},
363396
{
364397
"category": "PostgreSQL",
365398
"command": "postgreSQL.configureFirewall",
@@ -575,6 +608,11 @@
575608
"when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentsGroup",
576609
"group": "1@1"
577610
},
611+
{
612+
"command": "cosmosDB.writeNoSqlQuery",
613+
"when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBDocumentCollection",
614+
"group": "1@2"
615+
},
578616
{
579617
"command": "cosmosDB.createDocDBStoredProcedure",
580618
"when": "view =~ /azure(ResourceGroups|Workspace|FocusView)/ && viewItem == cosmosDBStoredProceduresGroup",

src/KeyValueStore.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
/**
7+
* A global key value store that associates tree nodes with open editors.
8+
*/
9+
export class KeyValueStore {
10+
private _items: Map<string, object>;
11+
12+
private static _instance: KeyValueStore | undefined;
13+
14+
public static get instance(): KeyValueStore {
15+
if (!this._instance) {
16+
this._instance = new KeyValueStore();
17+
}
18+
return this._instance;
19+
}
20+
21+
/**
22+
* Prevent external instantiation.
23+
*/
24+
private constructor() {
25+
this._items = new Map<string, object>();
26+
}
27+
28+
public get(key: string): object | undefined {
29+
return this._items.get(key);
30+
}
31+
32+
public set(key: string, value: object): void {
33+
this._items.set(key, value);
34+
}
35+
}

src/docdb/NoSqlCodeLensProvider.ts

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { IActionContext, callWithTelemetryAndErrorHandling } from "@microsoft/vscode-azext-utils";
7+
import {
8+
CancellationToken,
9+
CodeLens,
10+
CodeLensProvider,
11+
Event,
12+
EventEmitter,
13+
Position,
14+
ProviderResult,
15+
Range,
16+
TextDocument
17+
} from "vscode";
18+
import { KeyValueStore } from "../KeyValueStore";
19+
20+
export type NoSqlQueryConnection = {
21+
databaseId: string;
22+
containerId: string;
23+
endpoint: string;
24+
masterKey: string;
25+
isEmulator: boolean;
26+
};
27+
28+
export const noSqlQueryConnectionKey = "NO_SQL_QUERY_CONNECTION_KEY.v1";
29+
30+
export class NoSqlCodeLensProvider implements CodeLensProvider {
31+
private _onDidChangeEmitter: EventEmitter<void> = new EventEmitter<void>();
32+
33+
public get onDidChangeCodeLenses(): Event<void> {
34+
return this._onDidChangeEmitter.event;
35+
}
36+
37+
public updateCodeLens(): void {
38+
this._onDidChangeEmitter.fire();
39+
}
40+
41+
public provideCodeLenses(document: TextDocument, _token: CancellationToken): ProviderResult<CodeLens[]> {
42+
return callWithTelemetryAndErrorHandling("nosql.provideCodeLenses", (context: IActionContext) => {
43+
context.telemetry.suppressIfSuccessful = true;
44+
const text = document.getText();
45+
const queryText = text;
46+
47+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
48+
const connectedCollection: NoSqlQueryConnection | undefined = KeyValueStore.instance.get(noSqlQueryConnectionKey) as any;
49+
let connectCodeLens: CodeLens;
50+
if (!connectedCollection) {
51+
connectCodeLens = new CodeLens(
52+
new Range(new Position(0, 0), new Position(0, 0)),
53+
{
54+
title: "Not connected",
55+
command: "cosmosDB.connectNoSqlContainer",
56+
arguments: []
57+
}
58+
);
59+
} else {
60+
connectCodeLens = new CodeLens(
61+
new Range(new Position(0, 0), new Position(0, 0)),
62+
{
63+
title: `Connected to ${connectedCollection.databaseId}.${connectedCollection.containerId}`,
64+
command: "cosmosDB.connectNoSqlContainer",
65+
arguments: []
66+
}
67+
);
68+
}
69+
const lenses: CodeLens[] = [
70+
connectCodeLens,
71+
new CodeLens(
72+
new Range(new Position(0, 0), new Position(0, 0)),
73+
{
74+
title: "Execute",
75+
command: "cosmosDB.executeNoSqlQuery",
76+
arguments: [{ queryText }]
77+
}
78+
),
79+
new CodeLens(
80+
new Range(new Position(0, 0), new Position(0, 0)),
81+
{
82+
title: "Execute with Query Metrics",
83+
command: "cosmosDB.executeNoSqlQuery",
84+
arguments: [{ queryText, populateQueryMetrics: true }]
85+
}
86+
),
87+
new CodeLens(
88+
new Range(new Position(0, 0), new Position(0, 0)),
89+
{
90+
title: "Get Query Plan",
91+
command: "cosmosDB.getNoSqlQueryPlan",
92+
arguments: [{ queryText }]
93+
}
94+
)
95+
];
96+
97+
return lenses;
98+
});
99+
}
100+
}

src/docdb/getCosmosClient.ts

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import * as vscode from 'vscode';
1010
import { ext } from "../extensionVariables";
1111

1212
export function getCosmosClient(endpoint: string, key: string, isEmulator: boolean | undefined): CosmosClient {
13-
1413
const vscodeStrictSSL: boolean | undefined = vscode.workspace.getConfiguration().get<boolean>(ext.settingsKeys.vsCode.proxyStrictSSL);
1514
const enableEndpointDiscovery: boolean | undefined = vscode.workspace.getConfiguration().get<boolean>(ext.settingsKeys.enableEndpointDiscovery);
1615
const connectionPolicy = { enableEndpointDiscovery: (enableEndpointDiscovery === undefined) ? true : enableEndpointDiscovery };

src/docdb/registerDocDBCommands.ts

+83-4
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,33 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { AzExtTreeItem, IActionContext, ITreeItemPickerContext, registerCommandWithTreeNodeUnwrapping } from "@microsoft/vscode-azext-utils";
7-
import { commands } from "vscode";
6+
import { AzExtTreeItem, IActionContext, ITreeItemPickerContext, registerCommand, registerCommandWithTreeNodeUnwrapping } from "@microsoft/vscode-azext-utils";
7+
import { ViewColumn, commands, languages } from "vscode";
8+
import { KeyValueStore } from "../KeyValueStore";
89
import { doubleClickDebounceDelay, sqlFilter } from "../constants";
910
import { ext } from "../extensionVariables";
11+
import * as vscodeUtil from "../utils/vscodeUtils";
12+
import { NoSqlCodeLensProvider, NoSqlQueryConnection, noSqlQueryConnectionKey } from "./NoSqlCodeLensProvider";
13+
import { getCosmosClient } from "./getCosmosClient";
1014
import { DocDBAccountTreeItem } from "./tree/DocDBAccountTreeItem";
1115
import { DocDBCollectionTreeItem } from "./tree/DocDBCollectionTreeItem";
1216
import { DocDBDatabaseTreeItem } from "./tree/DocDBDatabaseTreeItem";
13-
import { DocDBDocumentsTreeItem } from "./tree/DocDBDocumentsTreeItem";
1417
import { DocDBDocumentTreeItem } from "./tree/DocDBDocumentTreeItem";
15-
import { DocDBStoredProceduresTreeItem } from "./tree/DocDBStoredProceduresTreeItem";
18+
import { DocDBDocumentsTreeItem } from "./tree/DocDBDocumentsTreeItem";
1619
import { DocDBStoredProcedureTreeItem } from "./tree/DocDBStoredProcedureTreeItem";
20+
import { DocDBStoredProceduresTreeItem } from "./tree/DocDBStoredProceduresTreeItem";
21+
22+
const nosqlLanguageId = "nosql";
1723

1824
export function registerDocDBCommands(): void {
25+
ext.noSqlCodeLensProvider = new NoSqlCodeLensProvider();
26+
ext.context.subscriptions.push(languages.registerCodeLensProvider(nosqlLanguageId, ext.noSqlCodeLensProvider));
27+
28+
registerCommand("cosmosDB.connectNoSqlContainer", connectNoSqlContainer);
29+
registerCommand("cosmosDB.executeNoSqlQuery", executeNoSqlQuery);
30+
registerCommand("cosmosDB.getNoSqlQueryPlan", getNoSqlQueryPlan);
1931
registerCommandWithTreeNodeUnwrapping('cosmosDB.createDocDBDatabase', createDocDBDatabase);
32+
registerCommandWithTreeNodeUnwrapping('cosmosDB.writeNoSqlQuery', writeNoSqlQuery);
2033
registerCommandWithTreeNodeUnwrapping('cosmosDB.createDocDBCollection', createDocDBCollection);
2134
registerCommandWithTreeNodeUnwrapping('cosmosDB.createDocDBDocument', async (context: IActionContext, node?: DocDBDocumentsTreeItem) => {
2235
if (!node) {
@@ -61,6 +74,72 @@ export function registerDocDBCommands(): void {
6174
});
6275
}
6376

77+
function setConnectedNoSqlContainer(node: DocDBCollectionTreeItem): void {
78+
const noSqlQueryConnection: NoSqlQueryConnection = {
79+
databaseId: node.parent.id,
80+
containerId: node.id,
81+
endpoint: node.root.endpoint,
82+
masterKey: node.root.masterKey,
83+
isEmulator: !!node.root.isEmulator
84+
};
85+
KeyValueStore.instance.set(noSqlQueryConnectionKey, noSqlQueryConnection);
86+
ext.noSqlCodeLensProvider.updateCodeLens();
87+
}
88+
89+
async function writeNoSqlQuery(_context: IActionContext, node: DocDBCollectionTreeItem): Promise<void> {
90+
setConnectedNoSqlContainer(node);
91+
const sampleQuery = `SELECT * FROM ${node.id}`;
92+
await vscodeUtil.showNewFile(sampleQuery, `query for ${node.label}`, ".nosql");
93+
}
94+
95+
async function connectNoSqlContainer(context: IActionContext): Promise<void> {
96+
const node = await pickDocDBAccount<DocDBCollectionTreeItem>(context, DocDBCollectionTreeItem.contextValue);
97+
setConnectedNoSqlContainer(node);
98+
}
99+
100+
async function executeNoSqlQuery(_context: IActionContext, args: { queryText: string, populateQueryMetrics?: boolean }): Promise<void> {
101+
if (!args) {
102+
throw new Error("Unable to execute query due to missing args. Please connect to a Cosmos DB collection.");
103+
}
104+
const { queryText, populateQueryMetrics } = args;
105+
const connectedCollection = KeyValueStore.instance.get(noSqlQueryConnectionKey);
106+
if (!connectedCollection) {
107+
throw new Error("Unable to execute query due to missing node data. Please connect to a Cosmos DB collection node.");
108+
} else {
109+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
110+
const { databaseId, containerId, endpoint, masterKey, isEmulator } = connectedCollection as NoSqlQueryConnection;
111+
const client = getCosmosClient(endpoint, masterKey, isEmulator);
112+
const options = { populateQueryMetrics };
113+
const response = await client.database(databaseId).container(containerId).items.query(queryText, options).fetchAll();
114+
const resultDocumentTitle = `query results for ${containerId}`;
115+
if (populateQueryMetrics === true) {
116+
await vscodeUtil.showNewFile(JSON.stringify({
117+
result: response.resources,
118+
queryMetrics: response.queryMetrics
119+
}, undefined, 2), resultDocumentTitle, ".json", ViewColumn.Beside);
120+
} else {
121+
await vscodeUtil.showNewFile(JSON.stringify(response.resources, undefined, 2), resultDocumentTitle, ".json", ViewColumn.Beside);
122+
}
123+
}
124+
}
125+
126+
async function getNoSqlQueryPlan(_context: IActionContext, args: { queryText: string } | undefined): Promise<void> {
127+
if (!args) {
128+
throw new Error("Unable to get query plan due to missing args. Please connect to a Cosmos DB collection node.");
129+
}
130+
const queryText = args.queryText;
131+
const connectedCollection = KeyValueStore.instance.get(noSqlQueryConnectionKey);
132+
if (!connectedCollection) {
133+
throw new Error("Unable to get query plan due to missing node data. Please connect to a Cosmos DB collection.");
134+
} else {
135+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
136+
const { databaseId, containerId, endpoint, masterKey, isEmulator } = connectedCollection as NoSqlQueryConnection;
137+
const client = getCosmosClient(endpoint, masterKey, isEmulator);
138+
const response = await client.database(databaseId).container(containerId).getQueryPlan(queryText);
139+
await vscodeUtil.showNewFile(JSON.stringify(response.result, undefined, 2), `query results for ${containerId}`, ".json", ViewColumn.Beside);
140+
}
141+
}
142+
64143
export async function createDocDBDatabase(context: IActionContext, node?: DocDBAccountTreeItem): Promise<void> {
65144
if (!node) {
66145
node = await pickDocDBAccount<DocDBAccountTreeItem>(context);

src/docdb/tree/DocDBDocumentTreeItem.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export class DocDBDocumentTreeItem extends AzExtTreeItem implements IEditableTre
106106
}
107107
}
108108

109-
private getPartitionKeyValue(): string | undefined | Object {
109+
private getPartitionKeyValue(): string | undefined {
110110
const partitionKey = this.parent.parent.partitionKey;
111111
if (!partitionKey) { //Fixed collections -> no partitionKeyValue
112112
return undefined;

src/extensionVariables.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AzExtTreeDataProvider, AzExtTreeItem, IAzExtOutputChannel } from "@micr
77
import { AzureHostExtensionApi } from "@microsoft/vscode-azext-utils/hostapi";
88
import { ExtensionContext, SecretStorage, TreeView } from "vscode";
99
import { DatabasesFileSystem } from "./DatabasesFileSystem";
10+
import { NoSqlCodeLensProvider } from "./docdb/NoSqlCodeLensProvider";
1011
import { MongoDBLanguageClient } from "./mongo/languageClient";
1112
import { MongoCodeLensProvider } from "./mongo/services/MongoCodeLensProvider";
1213
import { MongoDatabaseTreeItem } from "./mongo/tree/MongoDatabaseTreeItem";
@@ -33,6 +34,7 @@ export namespace ext {
3334
export const prefix: string = 'azureDatabases';
3435
export let fileSystem: DatabasesFileSystem;
3536
export let mongoCodeLensProvider: MongoCodeLensProvider;
37+
export let noSqlCodeLensProvider: NoSqlCodeLensProvider;
3638
export let mongoLanguageClient: MongoDBLanguageClient;
3739
export let rgApi: AzureHostExtensionApi;
3840

0 commit comments

Comments
 (0)