Skip to content

Commit ca90585

Browse files
authored
Merge pull request #420 from codefori/feature/binding_parameters
Support for SQL parameters and binding
2 parents 5b7ccfd + 96b4954 commit ca90585

File tree

15 files changed

+323
-66
lines changed

15 files changed

+323
-66
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Storage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import vscode from 'vscode';
22

33
const QUERIES_KEY = `queries`;
4-
const SERVERCOMPONENT_KEY = `serverVersion`
4+
const SERVERCOMPONENT_KEY = `serverVersion`;
55

66
export interface QueryHistoryItem {
77
query: string;
88
unix: number;
9+
substatements?: string[];
910
starred?: boolean;
1011
}
1112

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { CodeAction, CodeActionKind, languages, TextDocument, Uri, WorkspaceEdit } from "vscode";
2+
import { remoteAssistIsEnabled } from "./logic/available";
3+
import { getSqlDocument } from "./logic/parse";
4+
5+
class SqlCodeAction extends CodeAction {
6+
constructor(title: string, kind: CodeActionKind, public file: {document: TextDocument, statementOffset: number, names: string[]}) {
7+
super(title, kind);
8+
}
9+
}
10+
11+
const invalidBindingLabels = [`bind`, `cl`];
12+
13+
export const actionProvider = languages.registerCodeActionsProvider({ language: `sql` }, {
14+
provideCodeActions(document, range, context, token) {
15+
if (range.isEmpty) {
16+
const offset = document.offsetAt(range.start);
17+
18+
const enabled = remoteAssistIsEnabled();
19+
if (!enabled) return;
20+
21+
const sqlDoc = getSqlDocument(document);
22+
if (!sqlDoc) return;
23+
24+
const currentStatement = sqlDoc.getStatementByOffset(offset);
25+
const label = currentStatement.getLabel()?.toLowerCase() || ``;
26+
27+
if (currentStatement && !invalidBindingLabels.includes(label)) {
28+
const markers = currentStatement.getEmbeddedStatementAreas().filter(a => a.type === `marker`);
29+
const codeActions: SqlCodeAction[] = [];
30+
31+
if (markers.length > 0) {
32+
const action = new SqlCodeAction(`Generate bind statement`, CodeActionKind.QuickFix, {
33+
document,
34+
statementOffset: currentStatement.range.end,
35+
names: markers.map(marker => marker.named || `?`)
36+
});
37+
38+
codeActions.push(action);
39+
40+
return codeActions;
41+
}
42+
}
43+
44+
return [];
45+
}
46+
},
47+
resolveCodeAction(codeAction: SqlCodeAction, token) {
48+
if (!(codeAction instanceof SqlCodeAction)) return codeAction;
49+
codeAction.edit = new WorkspaceEdit();
50+
const document = codeAction.file.document;
51+
52+
const endOfStatementPos = document.positionAt(codeAction.file.statementOffset);
53+
const lineOfStatement = document.lineAt(endOfStatementPos.line);
54+
55+
let statement = `bind: ${codeAction.file.names.join(`, `)}`;
56+
57+
codeAction.edit.insert(document.uri, lineOfStatement.range.end, `\n${statement};`);
58+
return codeAction;
59+
}
60+
});

src/language/providers/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { actionProvider } from "./actionProvider";
12
import { completionProvider } from "./completionProvider";
23
import { formatProvider } from "./formatProvider";
34
import { hoverProvider, openProvider } from "./hoverProvider";
@@ -20,7 +21,8 @@ export function languageInit() {
2021
// peekProvider,
2122
...problemProvider,
2223
checkDocumentDefintion,
23-
sqlLanguageStatus
24+
sqlLanguageStatus,
25+
actionProvider
2426
);
2527

2628
return functionality;

src/language/providers/logic/parse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function getSqlDocument(document: TextDocument): Document|undefined {
1818
}
1919
}
2020

21-
const newAsp = new Document(document.getText());
21+
const newAsp = new Document(document.getText(), false);
2222
cached.set(uri, { ast: newAsp, version: document.version });
2323

2424
return newAsp;

src/language/providers/problemProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ function getStatementRangeFromGroup(currentGroup: StatementGroup, groupId: numbe
287287

288288
const label = firstStatement.getLabel();
289289
if (label) {
290-
if (label.toUpperCase() === `CL`) {
290+
if ([`CL`, `BIND`].includes(label.toUpperCase())) {
291291
statementRange.validate = false;
292292
} else {
293293
statementRange.start = firstStatement.tokens[2].range.start;

src/language/sql/document.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export default class Document {
223223
})
224224
}
225225

226-
removeEmbeddedAreas(statement: Statement, snippetString?: boolean): ParsedEmbeddedStatement {
226+
removeEmbeddedAreas(statement: Statement, options: {replacement: `snippet`|`?`|`values`, values?: any[]} = {replacement: `?`}): ParsedEmbeddedStatement {
227227
const areas = statement.getEmbeddedStatementAreas();
228228

229229
const totalParameters = areas.filter(a => a.type === `marker`).length;
@@ -242,20 +242,44 @@ export default class Document {
242242
case `marker`:
243243
const markerContent = newContent.substring(start, end);
244244

245-
newContent = newContent.substring(0, start) + (snippetString ? `\${${totalParameters-parameterCount}:${markerContent}}` : `?`) + newContent.substring(end) + (snippetString ? `$0` : ``);
245+
switch (options.replacement) {
246+
case `snippet`:
247+
newContent = newContent.substring(0, start) + `\${${totalParameters-parameterCount}:${markerContent}}` + newContent.substring(end) + `$0`;
248+
break;
249+
case `?`:
250+
newContent = newContent.substring(0, start) + `?` + newContent.substring(end);
251+
break;
252+
case `values`:
253+
let valueIndex = totalParameters - parameterCount - 1;
254+
if (options.values && options.values.length > valueIndex) {
255+
let value = options.values[valueIndex];
256+
257+
if (typeof value === `string`) {
258+
value = `'${value.replace(/'/g, `''`)}'`; // Escape single quotes in strings
259+
}
260+
261+
newContent = newContent.substring(0, start) + value + newContent.substring(end);
262+
} else {
263+
newContent = newContent.substring(0, start) + `?` + newContent.substring(end);
264+
}
265+
break;
266+
}
246267

247268
parameterCount++;
248269
break;
249270

250271
case `remove`:
251-
newContent = newContent.substring(0, start) + newContent.substring(end+1);
272+
newContent = newContent.substring(0, start) + newContent.substring(end);
273+
if (newContent[start-1] === ` ` && newContent[start] === ` `) {
274+
newContent = newContent.substring(0, start-1) + newContent.substring(start);
275+
}
252276
break;
253277
}
254278
}
255279

256280
return {
257281
changed: areas.length > 0,
258-
content: newContent,
282+
content: newContent.trim(),
259283
parameterCount
260284
};
261285
}

src/language/sql/statement.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import SQLTokeniser, { NameTypes } from "./tokens";
22
import { CTEReference, CallableReference, ClauseType, ClauseTypeWord, IRange, ObjectRef, QualifiedObject, StatementType, StatementTypeWord, Token } from "./types";
33

4-
const tokenIs = (token: Token|undefined, type: string, value?: string) => {
4+
export const tokenIs = (token: Token|undefined, type: string, value?: string) => {
55
return (token && token.type === type && (value ? token.value?.toUpperCase() === value : true));
66
}
77

@@ -30,7 +30,7 @@ export default class Statement {
3030
first = this.tokens[2];
3131
}
3232

33-
const wordValue = first.value?.toUpperCase();
33+
const wordValue = first?.value?.toUpperCase();
3434

3535
this.type = StatementTypeWord[wordValue] || StatementType.Unknown;
3636

@@ -659,14 +659,19 @@ export default class Statement {
659659
// Only these statements support the INTO clause in embedded SQL really
660660
const validIntoStatements: StatementType[] = [StatementType.Unknown, StatementType.With, StatementType.Select];
661661

662-
let ranges: {type: "remove"|"marker", range: IRange}[] = [];
662+
let ranges: {type: "remove"|"marker", range: IRange, named?: string}[] = [];
663663
let intoClause: Token|undefined;
664664
let declareStmt: Token|undefined;
665+
let lastTokenWasMarker = false;
665666

666667
for (let i = 0; i < this.tokens.length; i++) {
667668
const prevToken = this.tokens[i-1];
668669
const currentToken = this.tokens[i];
669670

671+
if (!tokenIs(currentToken, `colon`)) {
672+
lastTokenWasMarker = false;
673+
}
674+
670675
switch (currentToken.type) {
671676
case `statementType`:
672677
const currentValue = currentToken.value.toLowerCase();
@@ -752,14 +757,16 @@ export default class Statement {
752757

753758
if (endToken) {
754759
ranges.push({
755-
type: `marker`,
760+
type: lastTokenWasMarker ? `remove` : `marker`,
756761
range: {
757762
start: currentToken.range.start,
758-
end: endToken.range.end
759-
}
763+
end: (endToken.range.end)
764+
},
765+
named: endToken.value
760766
});
761767

762-
i = followingTokenI;
768+
lastTokenWasMarker = true;
769+
i = (followingTokenI-1);
763770
}
764771

765772
break;

src/language/sql/tests/statements.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,7 +1386,7 @@ parserScenarios(`PL body tests`, ({newDoc}) => {
13861386

13871387
const parameterTokens = medianResultSetProc.getBlockAt(46);
13881388
expect(parameterTokens.length).toBeGreaterThan(0);
1389-
expect(parameterTokens.map(t => t.type).join()).toBe([`parmType`, `word`, `word`, `openbracket`, `word`, `comma`, `word`, `closebracket`].join());
1389+
expect(parameterTokens.map(t => t.type).join()).toBe([`parmType`, `word`, `word`, `openbracket`, `number`, `comma`, `number`, `closebracket`].join());
13901390

13911391
const numRecordsDeclare = statements[1];
13921392
expect(numRecordsDeclare.type).toBe(StatementType.Declare);
@@ -1886,7 +1886,7 @@ describe(`Parameter statement tests`, () => {
18861886
const result = document.removeEmbeddedAreas(statement);
18871887
expect(result.parameterCount).toBe(1);
18881888
expect(result.content).toBe([
1889-
` SELECT EMPNO, FIRSTNME, LASTNAME, JOB`,
1889+
`SELECT EMPNO, FIRSTNME, LASTNAME, JOB`,
18901890
` FROM EMPLOYEE`,
18911891
` WHERE WORKDEPT = ?`
18921892
].join(`\n`));
@@ -1960,6 +1960,32 @@ describe(`Parameter statement tests`, () => {
19601960
expect(result.changed).toBe(false);
19611961
});
19621962

1963+
test('Remove indicator variables', () => {
1964+
const content = [
1965+
`UPDATE CORPDATA.EMPLOYEE`,
1966+
`SET PHONENO = :NEWPHONE:PHONEIND`,
1967+
`WHERE EMPNO = :EMPID;`,
1968+
``,
1969+
`bind: '3535', '000110';`,
1970+
].join(`\n`);
1971+
1972+
const expectedContent = [
1973+
`UPDATE CORPDATA.EMPLOYEE`,
1974+
`SET PHONENO = ?`,
1975+
`WHERE EMPNO = ?`,
1976+
].join(`\n`);
1977+
1978+
const document = new Document(content);
1979+
const statements = document.statements;
1980+
expect(statements.length).toBe(2);
1981+
1982+
const statement = statements[0];
1983+
const result = document.removeEmbeddedAreas(statement);
1984+
console.log(result.content);
1985+
expect(result.parameterCount).toBe(2);
1986+
expect(result.content).toBe(expectedContent);
1987+
});
1988+
19631989
test(`Callable blocks`, () => {
19641990
const lines = [
19651991
`call qsys2.create_abcd();`,

src/language/sql/tokens.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ interface TokenState {
2626

2727
export default class SQLTokeniser {
2828
static matchers: Matcher[] = [
29+
{
30+
name: `IS_NUMBER`,
31+
match: [
32+
{ type: `word`, match: (value: string) => {return !isNaN(Number(value)) && !isNaN(parseFloat(value)); }},
33+
],
34+
becomes: `number`,
35+
},
2936
{
3037
name: `PROCEDURE_PARM_TYPE`,
3138
match: [{ type: `word`, match: (value: string) => {return [`IN`, `OUT`, `INOUT`].includes(value.toUpperCase())}}],

0 commit comments

Comments
 (0)