Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@
"type": "boolean",
"description": "Wrap the SQL list in parentheses when using 'Copy as SQL List' (instant copy)",
"default": false
},
"vscode-db2i.parameterBinding": {
Comment thread
buzzia2001 marked this conversation as resolved.
"type": "string",
"description": "How values are provided for host variables (:var) and parameter markers (?) when running a statement. Statements followed by a 'bind:' statement always take their values from it.",
Comment thread
buzzia2001 marked this conversation as resolved.
Outdated
"default": "prompt",
"enum": [
"prompt",
"snippet"
Comment thread
buzzia2001 marked this conversation as resolved.
Outdated
],
"enumDescriptions": [
"Open a form to enter the values, then run the statement with bound parameters",
"Rewrite the statement as a snippet so each parameter can be edited in place (legacy behavior)"
Comment thread
buzzia2001 marked this conversation as resolved.
Outdated
]
}
}
},
Expand Down
11 changes: 11 additions & 0 deletions src/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import vscode from 'vscode';

const QUERIES_KEY = `queries`;
const START_UP_CONFIGS_KEY = `startUpConfigs`;
const BIND_VALUES_KEY = `bindValues`;

export interface QueryHistoryItem {
query: string;
Expand All @@ -19,6 +20,8 @@ export interface StartUpConfig {

export type StartUpConfigList = StartUpConfig[];

export type BindValues = { [name: string]: string | null };

abstract class Storage {
protected readonly globalState;

Expand Down Expand Up @@ -75,4 +78,12 @@ export class ConnectionStorage extends Storage {
async setStartUpConfigList(startUpConfigList: StartUpConfigList) {
await this.set(START_UP_CONFIGS_KEY, startUpConfigList);
}

getBindValues() {
return this.get<BindValues>(BIND_VALUES_KEY) || {};
}

async setBindValues(bindValues: BindValues) {
await this.set(BIND_VALUES_KEY, bindValues);
}
}
7 changes: 5 additions & 2 deletions src/language/sql/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export default class Document {
const totalParameters = areas.filter(a => a.type === `marker`).length;
let newContent = this.content.substring(statement.range.start, statement.range.end);
let parameterCount = 0;
let parameterNames: (string | undefined)[] = [];
Comment thread
buzzia2001 marked this conversation as resolved.
Outdated

const startRange = statement.range.start;

Expand Down Expand Up @@ -268,7 +269,8 @@ export default class Document {
}
break;
}


parameterNames.unshift(markerContent.startsWith(`:`) ? markerContent.substring(1).replace(/\s+/g, ``) : undefined);
parameterCount++;
break;

Expand All @@ -284,7 +286,8 @@ export default class Document {
return {
changed: areas.length > 0,
content: newContent.trim(),
parameterCount
parameterCount,
parameterNames
};
}
}
Expand Down
1 change: 1 addition & 0 deletions src/language/sql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export interface ParsedEmbeddedStatement {
changed: boolean;
content: string;
parameterCount: number;
parameterNames: (string | undefined)[];
}

export interface CallableReference {
Expand Down
26 changes: 26 additions & 0 deletions src/tests/suites/statements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1883,6 +1883,32 @@ describe(`Parameter statement tests`, () => {

const result = document.removeEmbeddedAreas(statement);
expect(result.content).toBe(`select * from sample where x = ? or y=?`);
expect(result.parameterNames).toStrictEqual([`struct.value`, `struct.val`]);
});

test(`Parameter names in order of appearance`, () => {
const document = new Document(`select * from sample where x = :Value and y = ? and z = :other or w = :value`);
const statements = document.statements;
expect(statements.length).toBe(1);

const statement = statements[0];

const result = document.removeEmbeddedAreas(statement);
expect(result.content).toBe(`select * from sample where x = ? and y = ? and z = ? or w = ?`);
expect(result.parameterCount).toBe(4);
expect(result.parameterNames).toStrictEqual([`Value`, undefined, `other`, `value`]);
});

test(`Parameter names exclude INTO and indicator variables`, () => {
const document = new Document(`select x into :myds.x :ind from sample where x = :struct.value :nullind`);
const statements = document.statements;
expect(statements.length).toBe(1);

const statement = statements[0];

const result = document.removeEmbeddedAreas(statement);
expect(result.content).toBe(`select x from sample where x = ?`);
expect(result.parameterNames).toStrictEqual([`struct.value`]);
});

test(`Single INTO clause content test`, () => {
Expand Down
86 changes: 84 additions & 2 deletions src/views/results/binding.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { TextEditor } from "vscode";
import { getBase } from "../../base";
import { Config } from "../../config";
import { getSqlDocument } from "../../language/providers/logic/parse";
import Document from "../../language/sql/document";
import { tokenIs } from "../../language/sql/statement";
import { StatementGroup } from "../../language/sql/types";
import { ParsedEmbeddedStatement, StatementGroup } from "../../language/sql/types";
import { SqlParameter } from "./resultSetPanelProvider";

const MAX_REMEMBERED_BIND_VALUES = 100;

export function getPriorBindableStatement(editor: TextEditor, offset: number): { statement: string, parameters: number } | undefined {
const sqlDocument = getSqlDocument(editor.document);

Expand Down Expand Up @@ -50,4 +55,81 @@ export function getLiteralsFromStatement(group: StatementGroup): SqlParameter[]
}

return literals;
}
}

export function hasParameters(embeddedInfo?: ParsedEmbeddedStatement) {
return Boolean(embeddedInfo?.parameterCount);
}

export function isFollowedByBind(document: Document, group: StatementGroup) {
const nextGroup = document.getStatementGroups().find(g => g.range.start >= group.range.end);
return nextGroup?.statements[0]?.getLabel()?.toLowerCase() === `bind`;
}

function escapeHtml(value: string) {
return value.replace(/&/g, `&amp;`).replace(/"/g, `&quot;`).replace(/</g, `&lt;`).replace(/>/g, `&gt;`);
Comment thread
buzzia2001 marked this conversation as resolved.
Outdated
}

export async function promptForParameterValues(statementMarkers: (string | undefined)[][]): Promise<SqlParameter[][] | undefined> {
Comment thread
buzzia2001 marked this conversation as resolved.
const remembered = Config.ready ? { ...Config.getBindValues() } : {};
const hasNamedMarkers = statementMarkers.some(markers => markers.some(name => name !== undefined));

const ui = getBase().customUI()
Comment thread
buzzia2001 marked this conversation as resolved.
Outdated
.addParagraph(`Enter a value for each parameter, or check NULL to bind a null value.` + (hasNamedMarkers ? ` Host variable values are remembered for the next run.` : ``));

const namedFields = new Map<string, string>();
let fieldCount = 0;
let positionalCount = 0;

const statementFields = statementMarkers.map(markers => markers.map(name => {
const key = name?.toUpperCase();
if (key && namedFields.has(key)) {
return namedFields.get(key)!;
}

const id = `parm${fieldCount++}`;
if (key) {
namedFields.set(key, id);
ui.addInput(id, escapeHtml(`:${name}`), undefined, { default: escapeHtml(remembered[key] || ``) });
} else {
ui.addInput(id, `Parameter marker ${++positionalCount}`);
}
ui.addCheckbox(`${id}_null`, `NULL`, undefined, key ? remembered[key] === null : false);

return id;
}));

ui.addButtons(
{ id: `run`, label: `Run` },
{ id: `cancel`, label: `Cancel` }
);

const page = await ui.loadPage<{ [id: string]: string | boolean }>(`Bind parameters`);
Comment thread
buzzia2001 marked this conversation as resolved.
Outdated
if (!page || !page.data) {
return;
}

const data = page.data;
page.panel.dispose();

// mapepire accepts null
const getValue = (id: string) => (data[`${id}_null`] === true ? null : String(data[id] || ``)) as SqlParameter;

if (data.buttons === `cancel`) {
return;
}

if (Config.ready && namedFields.size > 0) {
for (const [key, id] of namedFields) {
delete remembered[key];
remembered[key] = getValue(id) as string | null;
}

const keys = Object.keys(remembered);
keys.slice(0, keys.length - MAX_REMEMBERED_BIND_VALUES).forEach(key => delete remembered[key]);

await Config.setBindValues(remembered);
}

return statementFields.map(ids => ids.map(getValue));
}
66 changes: 56 additions & 10 deletions src/views/results/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Statement from "../../language/sql/statement";
import { ObjectRef, ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types";
import { VisualExplainData } from "../../types";
import { updateStatusBar } from "../jobManager/statusBar";
import { getLiteralsFromStatement, getPriorBindableStatement } from "./binding";
import { getLiteralsFromStatement, getPriorBindableStatement, hasParameters, isFollowedByBind, promptForParameterValues } from "./binding";
import { queryResultToRpgDs, queryResultToUdtf } from "./codegen";
import { registerRunStatement } from "./editorUi";
import { generateSqlForAdvisedIndexes } from "./explain/advice";
Expand All @@ -33,13 +33,15 @@ export interface StatementInfo {
open?: boolean,
viewColumn?: ViewColumn,
viewFocus?: boolean,
history?: boolean
history?: boolean,
parameters?: SqlParameter[]
}

export interface ParsedStatementInfo extends StatementInfo {
statement?: Statement;
group?: StatementGroup;
embeddedInfo?: ParsedEmbeddedStatement;
bindInfo?: ParsedEmbeddedStatement;
}

const DelimValue = new Map(Object.entries({
Expand Down Expand Up @@ -263,6 +265,20 @@ async function runMultipleHandler(mode: `all` | `selected` | `from`) {
// Last statement should have UI
statementInfos[statementInfos.length - 1].noUi = false;

const bindInfos = statementInfos.map(info => parseStatement(editor, info).bindInfo);
if (bindInfos.some(bindInfo => hasParameters(bindInfo))) {
const values = await promptForParameterValues(bindInfos.map(bindInfo => hasParameters(bindInfo) ? bindInfo!.parameterNames : []));
if (!values) {
return;
}

statementInfos.forEach((info, i) => {
if (hasParameters(bindInfos[i])) {
info.parameters = values[i];
}
});
}

for (let statementInfo of statementInfos) {
try {
await runHandler(statementInfo);
Expand Down Expand Up @@ -314,7 +330,7 @@ async function runHandler(options?: StatementInfo) {
editor.selection = new vscode.Selection(editor.document.positionAt(group.range.start), editor.document.positionAt(group.range.end));
editor.revealRange(editor.selection);

if (group.statements.length === 1 && statementDetail.embeddedInfo && statementDetail.embeddedInfo.changed) {
if (group.statements.length === 1 && statementDetail.embeddedInfo && statementDetail.embeddedInfo.changed && !hasParameters(statementDetail.bindInfo)) {
editor.insertSnippet(new SnippetString(statementDetail.embeddedInfo.content));
return;
}
Expand All @@ -341,6 +357,18 @@ async function runHandler(options?: StatementInfo) {
}

if (statementDetail.content.trim().length > 0) {
let parameters: SqlParameter[] = [];
let runContent = statementDetail.content;
if (statementDetail.bindInfo && hasParameters(statementDetail.bindInfo)) {
const values = statementDetail.parameters || (await promptForParameterValues([statementDetail.bindInfo.parameterNames]))?.[0];
if (!values) {
return;
}

parameters = values;
runContent = statementDetail.bindInfo.content;
}

try {
const inWindow = Boolean(options && options.viewColumn);

Expand All @@ -367,7 +395,6 @@ async function runHandler(options?: StatementInfo) {
}

} else if ([`statement`, `update`, `bind`].includes(statementDetail.qualifier)) {
let parameters: SqlParameter[] = [];
if (editor && statementDetail.qualifier === `bind`) {
const position = editor.selection.active;
const runStatement = getPriorBindableStatement(editor, editor.document.offsetAt(position));
Expand All @@ -384,14 +411,15 @@ async function runHandler(options?: StatementInfo) {

// Overwrite to run the prior statement
statementDetail.content = runStatement.statement;
runContent = runStatement.statement;
}
}

// If it's a basic statement, we can let it scroll!
if (statementDetail.noUi) {
setCancelButtonVisibility(true);
chosenView.setLoadingText(`Running SQL statement... (${possibleTitle})`, false);
await JobManager.runSQL(statementDetail.content, { parameters }, 1);
await JobManager.runSQL(runContent, { parameters }, 1);

} else {
if (inWindow) {
Expand All @@ -405,7 +433,7 @@ async function runHandler(options?: StatementInfo) {

const uiId = registerRunStatement(statementDetail);
const eol = editor?.document.eol === vscode.EndOfLine.CRLF ? `\r\n` : `\n`;
const basicSelect = statementDetail.content.split(eol).filter(line => !line.trimStart().startsWith(`--`)).join(eol);
const basicSelect = runContent.split(eol).filter(line => !line.trimStart().startsWith(`--`)).join(eol);

chosenView.setScrolling({ // Never errors
basicSelect: basicSelect,
Expand All @@ -426,14 +454,16 @@ async function runHandler(options?: StatementInfo) {
const explainType: ExplainType = onlyExplain ? ExplainType.DO_NOT_RUN : ExplainType.RUN;

setCancelButtonVisibility(true);
const explained = await selectedJob.job.explain<VisualExplainData[]>(statementDetail.content, explainType); // Can throw
// Explain does not support parameters
Comment thread
SanjulaGanepola marked this conversation as resolved.
Outdated
const explainContent = parameters.length ? getContentWithValues(statementDetail.content, parameters) : statementDetail.content;
const explained = await selectedJob.job.explain<VisualExplainData[]>(explainContent, explainType); // Can throw
setCancelButtonVisibility(false);

if (onlyExplain) {
chosenView.setLoadingText(`Explained.`, false);
} else {
chosenView.setScrolling({ // Never errors
basicSelect: statementDetail.content,
basicSelect: explainContent,
queryId: explained.id,
})
}
Expand All @@ -454,7 +484,7 @@ async function runHandler(options?: StatementInfo) {
chosenView.setLoadingText(`Executing SQL statement...`, false);
setCancelButtonVisibility(true);
updateStatusBar({ executing: true });
const result = await JobManager.runSQLVerbose(statementDetail.content, undefined, 1);
const result = await JobManager.runSQLVerbose(runContent, parameters.length ? { parameters } : undefined, 1);
setCancelButtonVisibility(false);
updateStatusBar({ executing: false });

Expand Down Expand Up @@ -489,7 +519,7 @@ async function runHandler(options?: StatementInfo) {

setCancelButtonVisibility(true);
updateStatusBar({ executing: true });
const result = await JobManager.runSQLVerbose<any>(statementDetail.content);
const result = await JobManager.runSQLVerbose<any>(runContent, parameters.length ? { parameters } : undefined);
const data = result.data;
setCancelButtonVisibility(false);

Expand Down Expand Up @@ -656,6 +686,11 @@ async function runHandler(options?: StatementInfo) {
}
}

function getContentWithValues(content: string, values: SqlParameter[]) {
Comment thread
buzzia2001 marked this conversation as resolved.
Outdated
const contentDocument = new Document(content);
return contentDocument.removeEmbeddedAreas(contentDocument.statements[0], { replacement: `values`, values }).content;
}

export function parseStatement(editor?: vscode.TextEditor, existingInfo?: StatementInfo): ParsedStatementInfo {
let statementInfo: ParsedStatementInfo = {
content: ``,
Expand Down Expand Up @@ -714,5 +749,16 @@ export function parseStatement(editor?: vscode.TextEditor, existingInfo?: Statem
statementInfo.embeddedInfo = sqlDocument.removeEmbeddedAreas(statementInfo.statement, { replacement: `snippet` });
}

if (statementInfo.content && ![`cl`, `bind`].includes(statementInfo.qualifier) && Configuration.get<string>(`parameterBinding`) !== `snippet`) {
// When a bind statement follows, the values come from it instead
const groupDocument = sqlDocument || (editor ? getSqlDocument(editor.document) : undefined);
const followedByBind = Boolean(groupDocument && statementInfo.group && isFollowedByBind(groupDocument, statementInfo.group));

const contentDocument = new Document(statementInfo.content);
if (!followedByBind && contentDocument.statements.length === 1) {
statementInfo.bindInfo = contentDocument.removeEmbeddedAreas(contentDocument.statements[0]);
}
}

Comment thread
buzzia2001 marked this conversation as resolved.
Outdated
return statementInfo;
}
Loading