Skip to content

Commit c419f5d

Browse files
authored
triggering a param add pop-up if running a cypher with missing params (#452)
1 parent 88e4930 commit c419f5d

File tree

8 files changed

+117
-33
lines changed

8 files changed

+117
-33
lines changed

package-lock.json

+14-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/language-support/src/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ export { _internalFeatureFlags } from './featureFlags';
77
export { formatQuery } from './formatting/formatting';
88
export { antlrUtils } from './helpers';
99
export { CypherTokenType, lexerSymbols } from './lexerSymbols';
10-
export { parse, parserWrapper, parseStatementsStrs } from './parserWrapper';
10+
export {
11+
parse,
12+
parseParameters,
13+
parserWrapper,
14+
parseStatementsStrs,
15+
} from './parserWrapper';
1116
export { signatureHelp, toSignatureInformation } from './signatureHelp';
1217
export {
1318
applySyntaxColouring,

packages/language-support/src/parserWrapper.ts

+18
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,24 @@ export function createParsingResult(
236236
return parsingResult;
237237
}
238238

239+
const getClearParamName = (name: string): string => {
240+
if (name.startsWith('`') && name.endsWith('`')) {
241+
return name.slice(1, -1);
242+
}
243+
return name;
244+
};
245+
246+
export function parseParameters(
247+
query: string,
248+
consoleCommandsEnabled: boolean,
249+
): string[] {
250+
const parsingResult = createParsingResult(query, consoleCommandsEnabled);
251+
const parameters = parsingResult.statementsParsing.flatMap((statement) =>
252+
statement.collectedParameters.map((param) => getClearParamName(param.name)),
253+
);
254+
return [...new Set(parameters)];
255+
}
256+
239257
// This listener collects all labels and relationship types
240258
class LabelAndRelTypesCollector extends ParseTreeListener {
241259
labelOrRelTypes: LabelOrRelType[] = [];

packages/language-support/src/syntaxValidation/syntaxValidation.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,7 @@ function errorOnUndeclaredParameters(
392392
if (parameterName.startsWith('`') && parameterName.endsWith('`')) {
393393
parameterName = parameterName.substring(1, parameterName.length - 1);
394394
}
395-
const paramExists = !!dbSchema.parameters?.[parameterName];
396-
if (!paramExists) {
395+
if (dbSchema.parameters?.[parameterName] === undefined) {
397396
errors.push(
398397
generateSyntaxDiagnostic(
399398
parameter.rawText,

packages/vscode-extension/src/commandHandlers/params.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function validateParamInput(
4444
return undefined;
4545
}
4646

47-
export async function addParameter(): Promise<void> {
47+
export async function addParameter(defaultParamName?: string): Promise<void> {
4848
const connected = await isConnected();
4949

5050
if (!connected) {
@@ -54,12 +54,14 @@ export async function addParameter(): Promise<void> {
5454
return;
5555
}
5656

57-
const paramName = await window.showInputBox({
58-
prompt: 'Parameter name',
59-
placeHolder:
60-
'The name you want to store your parameter with, for example: param, p, `my parameter`',
61-
ignoreFocusOut: true,
62-
});
57+
const paramName =
58+
defaultParamName ??
59+
(await window.showInputBox({
60+
prompt: 'Parameter name',
61+
placeHolder:
62+
'The name you want to store your parameter with, for example: param, p, `my parameter`',
63+
ignoreFocusOut: true,
64+
}));
6365
if (!paramName) {
6466
await window.showErrorMessage('Parameter name cannot be empty.');
6567
return;
@@ -68,7 +70,9 @@ export async function addParameter(): Promise<void> {
6870
const dbSchema = schemaPoller.metadata.dbSchema;
6971
let timeout: NodeJS.Timeout;
7072
const paramValue = await window.showInputBox({
71-
prompt: 'Parameter value',
73+
prompt: defaultParamName
74+
? `Parameter value for the parameter ${defaultParamName}`
75+
: 'Parameter value',
7276
placeHolder:
7377
'The value for your parameter (anything you could evaluate in a RETURN), for example: 1234, "some string", datetime(), {a: 1, b: "some string"}',
7478
ignoreFocusOut: true,
@@ -84,7 +88,7 @@ export async function addParameter(): Promise<void> {
8488
});
8589

8690
if (!paramValue) {
87-
await window.showErrorMessage('Parameter value cannot be empty.');
91+
void window.showErrorMessage('Parameter value cannot be empty.');
8892
return;
8993
}
9094

@@ -150,7 +154,7 @@ export async function evaluateParam(
150154
const db = getCurrentDatabase();
151155

152156
if (db.type === 'system') {
153-
await window.showErrorMessage(
157+
void window.showErrorMessage(
154158
'Parameters cannot be evaluated against a system database. Please connect to a user database.',
155159
);
156160
return;
@@ -186,7 +190,7 @@ export async function evaluateParam(
186190
if (e instanceof Neo4jError) {
187191
//If we can get past linting-check with invalid query but still have failing query
188192
//when executing, we catch here as a backup
189-
await window.showErrorMessage(
193+
void window.showErrorMessage(
190194
'Failed to evaluate parameter: ' + e.message,
191195
);
192196
} else {

packages/vscode-extension/src/cypherRunner.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { parseStatementsStrs } from '@neo4j-cypher/language-support';
1+
import {
2+
parseParameters,
3+
parseStatementsStrs,
4+
} from '@neo4j-cypher/language-support';
25
import { Uri } from 'vscode';
6+
import { addParameter } from './commandHandlers/params';
37
import { Connection } from './connectionService';
8+
import { getDeserializedParams } from './parameterService';
49
import ResultWindow from './webviews/resultWindow';
510

611
export default class CypherRunner {
@@ -10,7 +15,15 @@ export default class CypherRunner {
1015

1116
async run(connection: Connection, uri: Uri, input: string) {
1217
const statements = parseStatementsStrs(input);
18+
const statementParams = parseParameters(input, false);
1319
const filePath = uri.toString();
20+
const parameters = getDeserializedParams();
21+
22+
for (const param of statementParams) {
23+
if (parameters[param] === undefined) {
24+
await addParameter(param);
25+
}
26+
}
1427

1528
if (this.results.has(filePath)) {
1629
const resultWindow = this.results.get(filePath);
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
RETURN $a, $b, $`some param`, $`some-param`
1+
RETURN $a, $b, $`some param`, $`some-param`, $a + $b;

packages/vscode-extension/tests/specs/webviews/params.spec.ts

+48-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { browser } from '@wdio/globals';
22
import { before } from 'mocha';
33
import { Workbench } from 'wdio-vscode-service';
4+
import { Key } from 'webdriverio';
45
import {
56
checkResultsContent,
67
executeFile,
@@ -14,6 +15,14 @@ suite('Params panel testing', () => {
1415
workbench = await browser.getWorkbench();
1516
});
1617

18+
async function escapeModal(count: number) {
19+
for (let i = 0; i < count; i++) {
20+
await browser.pause(500);
21+
await browser.keys([Key.Escape]);
22+
await waitUntilNotification(browser, 'Parameter value cannot be empty.');
23+
}
24+
}
25+
1726
async function addParamWithInputBox() {
1827
await browser.executeWorkbench(async (vscode) => {
1928
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
@@ -100,10 +109,13 @@ suite('Params panel testing', () => {
100109
await clearParams();
101110

102111
await executeFile(workbench, 'params.cypher');
112+
113+
await escapeModal(4);
114+
103115
await checkResultsContent(workbench, async () => {
104116
const text = await (await $('#query-error')).getText();
105117
await expect(text).toContain(
106-
'Error executing query RETURN $a, $b, $`some param`, $`some-param`:\nExpected parameter(s): a, b, some param, some-param',
118+
'Error executing query RETURN $a, $b, $`some param`, $`some-param`, $a + $b;:\nExpected parameter(s): a, b, some param, some-param',
107119
);
108120
});
109121
});
@@ -155,10 +167,13 @@ suite('Params panel testing', () => {
155167
await forceDeleteParam('b');
156168

157169
await executeFile(workbench, 'params.cypher');
170+
171+
await escapeModal(2);
172+
158173
await checkResultsContent(workbench, async () => {
159174
const text = await (await $('#query-error')).getText();
160175
await expect(text).toContain(
161-
'Error executing query RETURN $a, $b, $`some param`, $`some-param`:\nExpected parameter(s): a, b',
176+
'Error executing query RETURN $a, $b, $`some param`, $`some-param`, $a + $b;:\nExpected parameter(s): a, b',
162177
);
163178
});
164179
});
@@ -187,11 +202,41 @@ suite('Params panel testing', () => {
187202
// to execute the file we need to be on the user database
188203
await forceSwitchDatabase('neo4j');
189204
await executeFile(workbench, 'params.cypher');
205+
206+
await escapeModal(4);
207+
190208
await checkResultsContent(workbench, async () => {
191209
const text = await (await $('#query-error')).getText();
192210
await expect(text).toContain(
193-
'Error executing query RETURN $a, $b, $`some param`, $`some-param`:\nExpected parameter(s): a, b, some param, some-param',
211+
'Error executing query RETURN $a, $b, $`some param`, $`some-param`, $a + $b;:\nExpected parameter(s): a, b',
194212
);
195213
});
196214
});
215+
216+
test('Should trigger parameter add pop-up when running a query with an unknown parameter', async () => {
217+
await clearParams();
218+
await forceAddParam('a', '1998');
219+
await executeFile(workbench, 'params.cypher');
220+
221+
// initial pop-up for param b
222+
await browser.pause(1000);
223+
await browser.keys(['1', '2', Key.Enter]);
224+
225+
// initial pop-up for param `some param`
226+
await browser.pause(1000);
227+
await browser.keys(['f', 'a', 'l', 's', 'e', Key.Enter]);
228+
229+
// initial pop-up for param `some-param`
230+
await browser.pause(1000);
231+
await browser.keys(['5', Key.Enter]);
232+
233+
await checkResultsContent(workbench, async () => {
234+
const queryResult = await (await $('#query-result')).getText();
235+
await expect(queryResult).toContain('1998');
236+
await expect(queryResult).toContain('12');
237+
await expect(queryResult).toContain('false');
238+
await expect(queryResult).toContain('5');
239+
await expect(queryResult).toContain('2010');
240+
});
241+
});
197242
});

0 commit comments

Comments
 (0)