From a490821ec651265bd5c9bd055e9e9e8281361c0b Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Thu, 9 Oct 2025 19:30:46 -0400 Subject: [PATCH 1/3] Add UDTF SQL prefix Signed-off-by: Sanjula Ganepola --- src/views/results/codegen.ts | 50 ++++++++++++++++++++++++++++++++++++ src/views/results/index.ts | 28 +++++++++++++++++--- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/views/results/codegen.ts b/src/views/results/codegen.ts index b90ceb75..be4fdcea 100644 --- a/src/views/results/codegen.ts +++ b/src/views/results/codegen.ts @@ -59,3 +59,53 @@ export function columnToRpgDefinition(column: ColumnMetaData) : string { return `// type:${column.type} precision:${column.precision} scale:${column.scale}`; } } + +export function queryResultToUdtf(result: QueryResult, sqlStatement: string) : string { + let columnDefinitions = ''; + for (let i = 0; i < result.metadata.column_count; i++) { + const column = result.metadata.columns[i]; + columnDefinitions += ` ${column.name} ${columnToSqlDefinition(column)}`; + if (i < result.metadata.column_count - 1) { + columnDefinitions += ',\n'; + } else { + columnDefinitions += '\n'; + } + } + + return `create or replace function MyFunction()\n` + + `returns table (\n` + + columnDefinitions + + `)\n` + + `begin\n` + + ` return ${sqlStatement};\n` + + `end;`; +} + +export function columnToSqlDefinition(column: ColumnMetaData) : string { + switch (column.type) { + case 'NUMERIC': + return `numeric(${column.precision},${column.scale})`; + case 'DECIMAL': + return `decimal(${column.precision},${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 `integer`; + case 'INTEGER': + return `integer`; + case 'BIGINT': + return `bigint`; + case 'BOOLEAN': + return `boolean`; + default: + return `-- type:${column.type} precision:${column.precision} scale:${column.scale} */`; + } +} \ No newline at end of file diff --git a/src/views/results/index.ts b/src/views/results/index.ts index ee1b17f6..6677a9f5 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -13,7 +13,7 @@ import Statement from "../../language/sql/statement"; import { ObjectRef, ParsedEmbeddedStatement, StatementGroup, StatementType } from "../../language/sql/types"; import { updateStatusBar } from "../jobManager/statusBar"; import { getLiteralsFromStatement, getPriorBindableStatement } from "./binding"; -import { queryResultToRpgDs } from "./codegen"; +import { queryResultToRpgDs, queryResultToUdtf } from "./codegen"; import { registerRunStatement } from "./editorUi"; import { generateSqlForAdvisedIndexes } from "./explain/advice"; import { DoveNodeView, PropertyNode } from "./explain/doveNodeView"; @@ -22,7 +22,7 @@ import { DoveTreeDecorationProvider } from "./explain/doveTreeDecorationProvider import { ExplainTree } from "./explain/nodes"; import { ResultSetPanelProvider, SqlParameter } from "./resultSetPanelProvider"; -export type StatementQualifier = "statement" | "bind" | "update" | "explain" | "onlyexplain" | "json" | "csv" | "cl" | "sql" | "rpg"; +export type StatementQualifier = "statement" | "bind" | "update" | "explain" | "onlyexplain" | "json" | "csv" | "cl" | "sql" | "rpg" | "udtf"; export interface StatementInfo { content: string, @@ -440,6 +440,26 @@ async function runHandler(options?: StatementInfo) { await vscode.window.showTextDocument(textDoc); chosenView.setLoadingText(`RPG data structure generated.`, false); } + + } else if (statementDetail.qualifier === `udtf`) { + if (statementDetail.statement.type !== StatementType.Select) { + vscode.window.showErrorMessage('UDTF 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 = `-- statement:\n` + + `-- ${statementDetail.content.replace(/(\r\n|\r|\n)/g, '\n-- ') }\n\n` + + `-- User-defined table function\n` + + queryResultToUdtf(result, statementDetail.content); + + const textDoc = await vscode.workspace.openTextDocument({ language: 'sql', content }); + await vscode.window.showTextDocument(textDoc); + chosenView.setLoadingText(`User-defined table function generated.`, false); + } } else { // Otherwise... it's a bit complicated. @@ -528,7 +548,7 @@ async function runHandler(options?: StatementInfo) { errorText = e.message || `Error running SQL statement.`; } - if ([`statement`, `explain`, `onlyexplain`, `cl`].includes(statementDetail.qualifier) && statementDetail.history !== false) { + if ([`statement`, `explain`, `onlyexplain`, `cl`, `udtf`].includes(statementDetail.qualifier) && statementDetail.history !== false) { chosenView.setError(errorText); } else { vscode.window.showErrorMessage(errorText); @@ -592,7 +612,7 @@ export function parseStatement(editor?: vscode.TextEditor, existingInfo?: Statem } if (statementInfo.content) { - [`cl`, `json`, `csv`, `sql`, `explain`, `update`, `rpg`, `bind`].forEach(mode => { + [`cl`, `json`, `csv`, `sql`, `explain`, `update`, `rpg`, `bind`, `udtf`].forEach(mode => { if (statementInfo.content.trim().toLowerCase().startsWith(mode + `:`)) { statementInfo.content = statementInfo.content.substring(mode.length + 1).trim(); From c04254fe284b3d3de5bfc9700b896212bb8ac1be Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Thu, 9 Oct 2025 20:15:29 -0400 Subject: [PATCH 2/3] Fix prefix check Signed-off-by: Sanjula Ganepola --- src/views/results/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/results/index.ts b/src/views/results/index.ts index 6677a9f5..90456c77 100644 --- a/src/views/results/index.ts +++ b/src/views/results/index.ts @@ -548,7 +548,7 @@ async function runHandler(options?: StatementInfo) { errorText = e.message || `Error running SQL statement.`; } - if ([`statement`, `explain`, `onlyexplain`, `cl`, `udtf`].includes(statementDetail.qualifier) && statementDetail.history !== false) { + if ([`statement`, `explain`, `onlyexplain`, `cl`].includes(statementDetail.qualifier) && statementDetail.history !== false) { chosenView.setError(errorText); } else { vscode.window.showErrorMessage(errorText); @@ -612,7 +612,7 @@ export function parseStatement(editor?: vscode.TextEditor, existingInfo?: Statem } if (statementInfo.content) { - [`cl`, `json`, `csv`, `sql`, `explain`, `update`, `rpg`, `bind`, `udtf`].forEach(mode => { + [`cl`, `json`, `csv`, `sql`, `explain`, `update`, `rpg`, `udtf`, `bind`].forEach(mode => { if (statementInfo.content.trim().toLowerCase().startsWith(mode + `:`)) { statementInfo.content = statementInfo.content.substring(mode.length + 1).trim(); From 967065062970a55a2b84a376ecf7c84d1b44101d Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Fri, 10 Oct 2025 10:51:17 -0400 Subject: [PATCH 3/3] Add UDTF keywords and fix spacing/casing Signed-off-by: Sanjula Ganepola --- src/views/results/codegen.ts | 56 ++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/views/results/codegen.ts b/src/views/results/codegen.ts index be4fdcea..8b865c31 100644 --- a/src/views/results/codegen.ts +++ b/src/views/results/codegen.ts @@ -1,6 +1,6 @@ import { ColumnMetaData, QueryResult } from "@ibm/mapepire-js"; -export function queryResultToRpgDs(result: QueryResult, source: string = 'Name') : string { +export function queryResultToRpgDs(result: QueryResult, source: string = 'Name'): string { let content = `dcl-ds row_t qualified template;\n`; for (let i = 0; i < result.metadata.column_count; i++) { const name = columnToRpgFieldName(result.metadata.columns[i], source); @@ -10,7 +10,7 @@ export function queryResultToRpgDs(result: QueryResult, source: string = 'N return content; } -export function columnToRpgFieldName(column: ColumnMetaData, source: string = 'Name') : string { +export function columnToRpgFieldName(column: ColumnMetaData, source: string = 'Name'): string { let name = source === 'Label' ? column.label.toLowerCase().trim() : column.name.toLowerCase().trim(); name = name.replace(/\u00fc/g, "u") // ü -> u .replace(/\u00e4/g, "a") // ä -> a @@ -24,14 +24,14 @@ export function columnToRpgFieldName(column: ColumnMetaData, source: string = 'N .replace(/\s+/g, "_") // remaining whitespaces to underscore .replace(/[^a-zA-Z0-9_]/g, "") // remove non-alphanumeric chars .replace(/\_+/i, "_") // replace multiple underscores with single underscore - .trim(); + .trim(); if (!isNaN(+name.charAt(0))) { name = `col` + name; } return name; } -export function columnToRpgDefinition(column: ColumnMetaData) : string { +export function columnToRpgDefinition(column: ColumnMetaData): string { switch (column.type) { case `NUMERIC`: return `zoned(${column.precision}${column.scale > 0 ? ' : ' + column.scale : ''})`; @@ -60,51 +60,57 @@ export function columnToRpgDefinition(column: ColumnMetaData) : string { } } -export function queryResultToUdtf(result: QueryResult, sqlStatement: string) : string { +export function queryResultToUdtf(result: QueryResult, sqlStatement: string): string { let columnDefinitions = ''; for (let i = 0; i < result.metadata.column_count; i++) { const column = result.metadata.columns[i]; - columnDefinitions += ` ${column.name} ${columnToSqlDefinition(column)}`; + columnDefinitions += ` ${column.name} ${columnToSqlDefinition(column)}`; if (i < result.metadata.column_count - 1) { columnDefinitions += ',\n'; } else { columnDefinitions += '\n'; } } - - return `create or replace function MyFunction()\n` - + `returns table (\n` + + return `CREATE OR REPLACE FUNCTION MyFunction()\n` + + ` RETURNS TABLE (\n` + columnDefinitions - + `)\n` - + `begin\n` - + ` return ${sqlStatement};\n` - + `end;`; + + ` )\n` + + ` NOT DETERMINISTIC\n` + + ` NO EXTERNAL ACTION\n` + + ` READS SQL DATA\n` + + ` SET OPTION COMMIT = *NONE,\n` + + ` DYNUSRPRF = *USER,\n` + + ` USRPRF = *USER\n` + + ` BEGIN\n` + + ` RETURN ${sqlStatement};\n` + + ` END;`; } -export function columnToSqlDefinition(column: ColumnMetaData) : string { +export function columnToSqlDefinition(column: ColumnMetaData): string { switch (column.type) { case 'NUMERIC': - return `numeric(${column.precision},${column.scale})`; + return `NUMERIC(${column.precision},${column.scale})`; case 'DECIMAL': - return `decimal(${column.precision},${column.scale})`; + return `DECIMAL(${column.precision},${column.scale})`; case 'CHAR': - return `char(${column.precision})`; + return `CHAR(${column.precision})`; case 'VARCHAR': - return `varchar(${column.precision})`; + return `VARCHAR(${column.precision})`; case 'DATE': - return `date`; + return `DATE`; case 'TIME': - return `time`; + return `TIME`; case 'TIMESTAMP': - return `timestamp`; + return `TIMESTAMP`; case 'SMALLINT': - return `integer`; + return `INTEGER`; case 'INTEGER': - return `integer`; + return `INTEGER`; case 'BIGINT': - return `bigint`; + return `BIGINT`; case 'BOOLEAN': - return `boolean`; + return `BOOLEAN`; default: return `-- type:${column.type} precision:${column.precision} scale:${column.scale} */`; }