diff --git a/package.json b/package.json index 99e82f13..6184c65c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ ], "main": "./dist/extension.js", "scripts": { - "language:test": "vitest", + "test": "vitest", "dsc": "npx tsx src/dsc", "package": "vsce package", "vscode:prepublish": "rm -rf dist && npm run webpack && npm run dsc", diff --git a/src/views/results/codegen.test.ts b/src/views/results/codegen.test.ts new file mode 100644 index 00000000..375c8ac6 --- /dev/null +++ b/src/views/results/codegen.test.ts @@ -0,0 +1,95 @@ +import { assert, expect, test } from 'vitest' +import { columnToRpgDefinition, queryResultToRpgDs } from './codegen'; +import { QueryResult } from '@ibm/mapepire-js'; + +test('Column to RPG definition', () => { + let rpgdef; + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'NUMERIC', precision: 11, scale: 0}); + expect(rpgdef).toBe('zoned(11)'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'DECIMAL', precision: 13, scale: 2}); + expect(rpgdef).toBe('packed(13 : 2)'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'VARCHAR', precision: 60, scale: 0}); + expect(rpgdef).toBe('varchar(60)'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'CHAR', precision: 10, scale: 0}); + expect(rpgdef).toBe('char(10)'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'DATE', precision: 0, scale: 0}); + expect(rpgdef).toBe('date'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'TIME', precision: 0, scale: 0}); + expect(rpgdef).toBe('time'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'TIMESTAMP', precision: 0, scale: 0}); + expect(rpgdef).toBe('timestamp'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'SMALLINT', precision: 0, scale: 0}); + expect(rpgdef).toBe('int(5)'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'INTEGER', precision: 0, scale: 0}); + expect(rpgdef).toBe('int(10)'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'BIGINT', precision: 0, scale: 0}); + expect(rpgdef).toBe('int(20)'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'BOOLEAN', precision: 0, scale: 0}); + expect(rpgdef).toBe('ind'); + + rpgdef = columnToRpgDefinition({display_size: 0, label: '', name: '', type: 'SOME_UNKNOWN_TYPE', precision: 0, scale: 0}); + expect(rpgdef).toBe('// type:SOME_UNKNOWN_TYPE precision:0 scale:0'); +}); + +test('QueryResult to RPG data structure', () => { + const queryResult: QueryResult<any> = { + metadata: { + column_count: 3, + columns: [ + { + display_size: 0, + label: 'id', + name: 'id', + type: 'INTEGER', + precision: 0, + scale: 0 + }, + { + display_size: 0, + label: 'name', + name: 'name', + type: 'VARCHAR', + precision: 80, + scale: 0 + }, + { + display_size: 0, + label: 'salary', + name: 'salary', + type: 'DECIMAL', + precision: 13, + scale: 2 + }, + ] + }, + is_done: true, + has_results: true, + update_count: 0, + data: [], + id: '', + success: true, + sql_rc: 0, + sql_state: '', + execution_time: 0 + }; + const ds = queryResultToRpgDs(queryResult); + const lines = ds.split('\n').filter(l => l !== ''); + expect(lines.length).toBe(5); + expect(lines.at(0)).toBe('dcl-ds row_t qualified template;'); + expect(lines.at(1).trim()).toBe('id int(10);'); + expect(lines.at(2).trim()).toBe('name varchar(80);'); + expect(lines.at(3).trim()).toBe('salary packed(13 : 2);'); + expect(lines.at(4)).toBe('end-ds;'); +}); + diff --git a/src/views/results/codegen.ts b/src/views/results/codegen.ts new file mode 100644 index 00000000..9b488059 --- /dev/null +++ b/src/views/results/codegen.ts @@ -0,0 +1,40 @@ +import { ColumnMetaData, QueryResult } from "@ibm/mapepire-js"; + +export function queryResultToRpgDs(result: QueryResult<any>) : string { + let content = `dcl-ds row_t qualified template;\n`; + for (let i = 0; i < result.metadata.column_count; i++) { + const name = `${isNaN(+result.metadata.columns[i].label.charAt(0)) ? '' : 'col'}${result.metadata.columns[i].label.toLowerCase()}` + content += ` ${name} ${columnToRpgDefinition(result.metadata.columns[i])};\n`; + } + content += `end-ds;\n`; + return content; +} + +export function columnToRpgDefinition(column: ColumnMetaData) : string { + switch (column.type) { + case `NUMERIC`: + return `zoned(${column.precision}${column.scale > 0 ? ' : ' + column.scale : ''})`; + case `DECIMAL`: + return `packed(${column.precision}${column.scale > 0 ? ' : ' + column.scale : ''})`; + case `CHAR`: + return `char(${column.precision})`; + case `VARCHAR`: + return `varchar(${column.precision})`; + case `DATE`: + return `date`; + case `TIME`: + return `time`; + case `TIMESTAMP`: + return `timestamp`; + case `SMALLINT`: + return `int(5)`; + case `INTEGER`: + return `int(10)`; + case `BIGINT`: + return `int(20)`; + case `BOOLEAN`: + return `ind`; + default: + return `// type:${column.type} precision:${column.precision} scale:${column.scale}`; + } +} diff --git a/src/views/results/index.ts b/src/views/results/index.ts index f4fc7a46..82837aa1 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -17,8 +17,9 @@ import { generateSqlForAdvisedIndexes } from "./explain/advice"; import { updateStatusBar } from "../jobManager/statusBar"; import { DbCache } from "../../language/providers/logic/cache"; import { ExplainType } from "../../connection/types"; +import { queryResultToRpgDs } from "./codegen"; -export type StatementQualifier = "statement" | "update" | "explain" | "onlyexplain" | "json" | "csv" | "cl" | "sql"; +export type StatementQualifier = "statement" | "update" | "explain" | "onlyexplain" | "json" | "csv" | "cl" | "sql" | "rpg"; export interface StatementInfo { content: string, @@ -375,7 +376,26 @@ async function runHandler(options?: StatementInfo) { } else { vscode.window.showInformationMessage(`No job currently selected.`); } - + + } else if (statementDetail.qualifier === `rpg`) { + if (statementDetail.statement.type !== StatementType.Select) { + vscode.window.showErrorMessage('RPG qualifier only supported for select statements'); + } else { + chosenView.setLoadingText(`Executing SQL statement...`, false); + setCancelButtonVisibility(true); + updateStatusBar({executing: true}); + const result = await JobManager.runSQLVerbose(statementDetail.content, undefined, 1); + setCancelButtonVisibility(false); + updateStatusBar({executing: false}); + let content = `**free\n\n` + + `// statement: ${statementDetail.content}\n\n` + + `// Row data structure\n` + + queryResultToRpgDs(result); + const textDoc = await vscode.workspace.openTextDocument({ language: 'rpgle', content }); + await vscode.window.showTextDocument(textDoc); + chosenView.setLoadingText(`RPG data structure generated.`, false); + } + } else { // Otherwise... it's a bit complicated. chosenView.setLoadingText(`Executing SQL statement...`, false); @@ -524,7 +544,7 @@ export function parseStatement(editor?: vscode.TextEditor, existingInfo?: Statem } if (statementInfo.content) { - [`cl`, `json`, `csv`, `sql`, `explain`, `update`].forEach(mode => { + [`cl`, `json`, `csv`, `sql`, `explain`, `update`, `rpg`].forEach(mode => { if (statementInfo.content.trim().toLowerCase().startsWith(mode + `:`)) { statementInfo.content = statementInfo.content.substring(mode.length + 1).trim();