Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component API #1985

Merged
merged 48 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f0eb283
Initial work on component API
worksofliam Apr 11, 2024
a4156c9
Test API
worksofliam Apr 11, 2024
e65c8bd
Better support for version check
worksofliam Apr 11, 2024
ca510d9
Check components before showing UI
worksofliam Apr 12, 2024
f3d87ea
SQL to CSV component
worksofliam Apr 12, 2024
952c801
Merge branch 'master' into cleanup/component_api
worksofliam Apr 12, 2024
b43188e
Change how components are defined
worksofliam Apr 12, 2024
4eac951
Untested: move content definition, move CSV logic to IBMi
worksofliam Apr 12, 2024
8117681
Add missing assignment for new SQL statement
worksofliam Apr 12, 2024
aae7485
Untested: Start of IFS_WRITE component
worksofliam Apr 12, 2024
b38d876
Use IFS_WRITE in SQL_TO_CSV
worksofliam Apr 15, 2024
b521070
Improvements to statement handler
worksofliam Apr 15, 2024
81115fa
Replace for bit data.
worksofliam Apr 15, 2024
511f29c
Fix component test
worksofliam Apr 15, 2024
607c6d7
Add encoding tests
worksofliam Apr 15, 2024
8c0987c
Added return type to withTempDir
sebjulliand Apr 16, 2024
9d484ba
Use withTempDir for deploying components
sebjulliand Apr 16, 2024
db2fb63
Use non deprecated writeStreamfileRaw
sebjulliand Apr 16, 2024
eac4e82
Tiny cleanup
sebjulliand Apr 16, 2024
105f905
Fallback to CPYTOIMPF in some cases
worksofliam Apr 17, 2024
23f0844
Remove SQL functionality from getTable
worksofliam Apr 17, 2024
091c2af
Fallback to copy to import
worksofliam Apr 18, 2024
b3c73c0
QCCSID and job CCSID have their own properties
worksofliam Apr 18, 2024
25db90c
Fix null error in SQL_TO_CSV
worksofliam Apr 18, 2024
bd93547
Correctly use version numbers
worksofliam Apr 18, 2024
5e5cb1c
Merge branch 'master' into cleanup/component_api
worksofliam Apr 18, 2024
8538273
Migrate getMemberInfo into a component
worksofliam Apr 18, 2024
04176f6
Remove null condition handler
worksofliam Apr 18, 2024
62b777f
Fix to copy to import wrap command
worksofliam Apr 18, 2024
8313b5d
Fix to tab escape issue
worksofliam Apr 19, 2024
b11059f
Move getMemberInfo test into component suite
worksofliam Apr 19, 2024
af00133
Set public authority for all SQL components
chrjorgensen Apr 23, 2024
d0b91a4
Merge branch 'master' into cleanup/component_api
worksofliam May 2, 2024
193a442
Merge branch 'master' into cleanup/component_api
worksofliam May 2, 2024
e2ce391
Merge branch 'master' into cleanup/component_api
worksofliam May 9, 2024
6cc1cf6
Remove TODO
worksofliam May 9, 2024
1c9853d
Support for CLOB in IFS_WRITE
worksofliam May 10, 2024
2b3091d
Improved component state checking
worksofliam May 10, 2024
2a8fa11
Provide input SQL when error happens
worksofliam May 10, 2024
3264a72
Disable SQL_TO_CSV
worksofliam May 10, 2024
26a9c24
Fix error message inconsistancy
worksofliam May 10, 2024
ff68822
Disable some components
worksofliam May 10, 2024
98e3557
Fix up for encoding tests (specifically Arabic)
worksofliam May 10, 2024
51de002
Merge branch 'master' into cleanup/component_api
worksofliam May 10, 2024
b1b85c8
Merge branch 'master' into cleanup/component_api
worksofliam May 10, 2024
e10ecb6
Remove SQL_TO_CSV and IFS_WRITE
worksofliam May 13, 2024
3ecfe0d
Format
sebjulliand May 14, 2024
53901fd
Fixed getMemberInfo test
sebjulliand May 14, 2024
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
134 changes: 98 additions & 36 deletions src/api/IBMi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import * as node_ssh from "node-ssh";
import * as vscode from "vscode";
import { ConnectionConfiguration } from "./Configuration";

import { parse } from 'csv-parse/sync';
import { existsSync } from "fs";
import os from "os";
import path from 'path';
import { ComponentId, ComponentManager } from "../components/component";
import { CopyToImport } from "../components/copyToImport";
import { instance } from "../instantiate";
import { CcsidOrigin, CommandData, CommandResult, ConnectionData, IBMiMember, RemoteCommand, SpecialAuthorities } from "../typings";
import { CommandData, CommandResult, ConnectionData, IBMiMember, RemoteCommand, SpecialAuthorities, WrapResult } from "../typings";
import { CompileTools } from "./CompileTools";
import IBMiContent from "./IBMiContent";
import { CachedServerSettings, GlobalStorage } from './Storage';
import { Tools } from './Tools';
import * as configVars from './configVars';
Expand Down Expand Up @@ -46,12 +50,13 @@ const remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures
];

export default class IBMi {
private runtimeCcsidOrigin = CcsidOrigin.User;
/** Runtime CCSID is either job CCSID or QCCSID */
private runtimeCcsid: number = CCSID_SYSVAL;
private qccsid: number = 65535;
private jobCcsid: number = CCSID_SYSVAL;
/** User default CCSID is job default CCSID */
private userDefaultCCSID: number = 0;

private components: ComponentManager = new ComponentManager();

client: node_ssh.NodeSSH;
currentHost: string = ``;
currentPort: number = 22;
Expand All @@ -76,6 +81,7 @@ export default class IBMi {
* */
lastErrors: object[] = [];
config?: ConnectionConfiguration.Parameters;
content = new IBMiContent(this);
shell?: string;

commandsExecuted: number = 0;
Expand Down Expand Up @@ -613,8 +619,8 @@ export default class IBMi {
}

// Fetch conversion values?
if (quickConnect === true && cachedServerSettings?.runtimeCcsid !== null && cachedServerSettings?.variantChars && cachedServerSettings?.userDefaultCCSID) {
this.runtimeCcsid = cachedServerSettings.runtimeCcsid;
if (quickConnect === true && cachedServerSettings?.jobCcsid !== null && cachedServerSettings?.variantChars && cachedServerSettings?.userDefaultCCSID && cachedServerSettings?.qccsid) {
this.jobCcsid = cachedServerSettings.jobCcsid;
this.variantChars = cachedServerSettings.variantChars;
this.userDefaultCCSID = cachedServerSettings.userDefaultCCSID;
} else {
Expand All @@ -625,20 +631,22 @@ export default class IBMi {
// Next, we're going to see if we can get the CCSID from the user or the system.
// Some things don't work without it!!!
try {
// First we grab the users default CCSID

// we need to grab the system CCSID (QCCSID)
const [systemCCSID] = await runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`);
if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') {
this.qccsid = systemCCSID.CURRENT_NUMERIC_VALUE;
}

// we grab the users default CCSID
const [userInfo] = await runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${this.currentUser}') ) )`);
if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') {
this.runtimeCcsid = userInfo.CHARACTER_CODE_SET_ID;
this.runtimeCcsidOrigin = CcsidOrigin.User;
this.jobCcsid = userInfo.CHARACTER_CODE_SET_ID;
}

// But if that CCSID is *SYSVAL, then we need to grab the system CCSID (QCCSID)
if (!this.runtimeCcsid || this.runtimeCcsid === CCSID_SYSVAL) {
const [systemCCSID] = await runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`);
if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') {
this.runtimeCcsid = systemCCSID.CURRENT_NUMERIC_VALUE;
this.runtimeCcsidOrigin = CcsidOrigin.System;
}
// if the job ccsid is *SYSVAL, then assign it to sysval
if (this.jobCcsid === CCSID_SYSVAL) {
this.jobCcsid = this.qccsid;
}

// Let's also get the user's default CCSID
Expand Down Expand Up @@ -689,10 +697,10 @@ export default class IBMi {
if (!this.enableSQL) {
const encoding = this.getEncoding();
// Show a message if the system CCSID is bad
const ccsidMessage = this.runtimeCcsidOrigin === CcsidOrigin.System && this.runtimeCcsid === 65535 ? `The system QCCSID is not set correctly. We recommend changing the CCSID on your user profile.` : undefined;
const ccsidMessage = this.qccsid === 65535 ? `The system QCCSID is not set correctly. We recommend changing the CCSID on your user profile first, and then changing your system QCCSID.` : undefined;

// Show a message if the runtime CCSID is bad (which means both runtime and default CCSID are bad) - in theory should never happen
const encodingMessage = encoding.invalid ? `Runtime CCSID detected as ${encoding.ccsid} and is invalid. Please change the CCSID in your user profile.` : undefined;
const encodingMessage = encoding.invalid ? `Runtime CCSID detected as ${encoding.ccsid} and is invalid. Please change the CCSID or default CCSID in your user profile.` : undefined;

vscode.window.showErrorMessage([
ccsidMessage,
Expand Down Expand Up @@ -765,7 +773,7 @@ export default class IBMi {
}
else {
try {
const content = instance.getContent();
const content = this.content;
if (content) {
const bashrcContent = (await content.downloadStreamfile(bashrcFile)).split("\n");
let replaced = false;
Expand Down Expand Up @@ -886,6 +894,9 @@ export default class IBMi {
}
}

progress.report({ message: `Checking Code for IBM i components.` });
await this.components.startup(this);

if (!reconnecting) {
vscode.workspace.getConfiguration().update(`workbench.editor.enablePreview`, false, true);
await vscode.commands.executeCommand(`setContext`, `code-for-ibmi:connected`, true);
Expand All @@ -895,7 +906,8 @@ export default class IBMi {

GlobalStorage.get().setServerSettingsCache(this.currentConnectionName, {
aspInfo: this.aspInfo,
runtimeCcsid: this.runtimeCcsid,
qccsid: this.qccsid,
jobCcsid: this.jobCcsid,
remoteFeatures: this.remoteFeatures,
remoteFeaturesKeys: Object.keys(this.remoteFeatures).sort().toString(),
variantChars: {
Expand Down Expand Up @@ -1240,12 +1252,12 @@ export default class IBMi {
* The directory is guaranteed to be empty when created and deleted after the `process` is done.
* @param process the process that will run on the empty directory
*/
async withTempDirectory(process: (directory: string) => Promise<void>) {
async withTempDirectory<T>(process: (directory: string) => Promise<T>) {
const tempDirectory = `${this.config?.tempDir || '/tmp'}/code4itemp${Tools.makeid(20)}`;
const prepareDirectory = await this.sendCommand({ command: `rm -rf ${tempDirectory} && mkdir -p ${tempDirectory}` });
if (prepareDirectory.code === 0) {
try {
await process(tempDirectory);
return await process(tempDirectory);
}
finally {
await this.sendCommand({ command: `rm -rf ${tempDirectory}` });
Expand Down Expand Up @@ -1279,6 +1291,9 @@ export default class IBMi {
}
}

getComponent<T>(id: ComponentId) {
return this.components.get<T>(id);
}

/**
* Run SQL statements.
Expand All @@ -1288,44 +1303,91 @@ export default class IBMi {
* @param statements
* @returns a Result set
*/
async runSQL(statements: string, opts: { userCcsid?: number } = {}): Promise<Tools.DB2Row[]> {
async runSQL(statements: string): Promise<Tools.DB2Row[]> {
const { 'QZDFMDB2.PGM': QZDFMDB2 } = this.remoteFeatures;

if (QZDFMDB2) {
const ccsidDetail = this.getEncoding();
const useCcsid = opts.userCcsid || (ccsidDetail.fallback && !ccsidDetail.invalid ? ccsidDetail.ccsid : undefined);
const useCcsid = ccsidDetail.fallback && !ccsidDetail.invalid ? ccsidDetail.ccsid : undefined;
const possibleChangeCommand = (useCcsid ? `@CHGJOB CCSID(${useCcsid});\n` : '');

let input = Tools.fixSQL(`${possibleChangeCommand}${statements}`, true);

let returningAsCsv: WrapResult | undefined;

if (this.qccsid === 65535) {
let list = input.split(`\n`).join(` `).split(`;`).filter(x => x.trim().length > 0);
const lastStmt = list.pop()?.trim();
const asUpper = lastStmt?.toUpperCase();

if (lastStmt) {
if ((asUpper?.startsWith(`SELECT`) || asUpper?.startsWith(`WITH`))) {
const copyToImport = this.getComponent<CopyToImport>(`CopyToImport`);
if (copyToImport) {
returningAsCsv = copyToImport.wrap(lastStmt);
list.push(...returningAsCsv.newStatements);
input = list.join(`;\n`);
}
}

if (!returningAsCsv) {
list.push(lastStmt);
}
}
}

const output = await this.sendCommand({
command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')"`,
stdin: Tools.fixSQL(`${possibleChangeCommand}${statements}`)
stdin: input
})

if (output.stdout) {
return Tools.db2Parse(output.stdout);
} else {
throw new Error(`There was an error running the SQL statement.`);
}
Tools.db2Parse(output.stdout, input);

if (returningAsCsv) {
// Will throw an error if stdout contains an error

const csvContent = await this.content.downloadStreamfile(returningAsCsv.outStmf);
if (csvContent) {
this.sendCommand({ command: `rm -rf "${returningAsCsv.outStmf}"` });

return parse(csvContent, {
columns: true,
skip_empty_lines: true,
cast: true,
onRecord(record) {
for (const key of Object.keys(record)) {
record[key] = record[key] === ` ` ? `` : record[key];
}
return record;
}
}) as Tools.DB2Row[];
}

} else {
throw new Error(`There is no way to run SQL on this system.`);
throw new Error(`There was an error fetching the SQL result set.`)
} else {
return Tools.db2Parse(output.stdout);
}
}
}

throw new Error(`There is no way to run SQL on this system.`);
}

getEncoding() {
const fallback = ((this.runtimeCcsid < 1 || this.runtimeCcsid === 65535) && this.userDefaultCCSID > 0);
const ccsid = fallback ? (this.userDefaultCCSID) : this.runtimeCcsid;
const fallbackToDefault = ((this.jobCcsid < 1 || this.jobCcsid === 65535) && this.userDefaultCCSID > 0);
const ccsid = fallbackToDefault ? this.userDefaultCCSID : this.jobCcsid;
return {
fallback,
fallback: fallbackToDefault,
ccsid,
invalid: (ccsid < 1 || ccsid === 65535)
};
}

getCcsids() {
return {
origin: this.runtimeCcsidOrigin,
runtimeCcsid: this.runtimeCcsid,
qccsid: this.qccsid,
runtimeCcsid: this.jobCcsid,
userDefaultCCSID: this.userDefaultCCSID,
};
}
Expand Down
101 changes: 28 additions & 73 deletions src/api/IBMiContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,38 +298,7 @@ export default class IBMiContent {
}

/**
* @param ileCommand Command that would change the library list, like CHGLIBL
*/
async getLibraryListFromCommand(ileCommand: string): Promise<{ currentLibrary: string; libraryList: string[]; } | undefined> {
if (this.ibmi.remoteFeatures[`GETNEWLIBL.PGM`]) {
const tempLib = this.config.tempLibrary;
const resultSet = await this.ibmi.runSQL(`CALL ${tempLib}.GETNEWLIBL('${ileCommand.replace(new RegExp(`'`, 'g'), `''`)}')`);

let result = {
currentLibrary: `QGPL`,
libraryList: [] as string[]
};

resultSet.forEach(row => {
const libraryName = String(row.SYSTEM_SCHEMA_NAME);
switch (row.PORTION) {
case `CURRENT`:
result.currentLibrary = libraryName;
break;
case `USER`:
result.libraryList.push(libraryName);
break;
}
})

return result;
}

return undefined;
}

/**
* Download the contents of a table.
* Download the contents of a member from a table.
* @param library
* @param file
* @param member Will default to file provided
Expand All @@ -338,52 +307,38 @@ export default class IBMiContent {
async getTable(library: string, file: string, member?: string, deleteTable?: boolean): Promise<Tools.DB2Row[]> {
if (!member) member = file; //Incase mbr is the same file

if (file === member && this.ibmi.enableSQL) {
const data = await this.ibmi.runSQL(`SELECT * FROM ${library}.${file}`);

if (deleteTable && this.config.autoClearTempData) {
await this.ibmi.runCommand({
command: `DLTOBJ OBJ(${library}/${file}) OBJTYPE(*FILE)`,
noLibList: true
});
}

return data;

} else {
const tempRmt = this.getTempRemote(Tools.qualifyPath(library, file, member));
const copyResult = await this.ibmi.runCommand({
command: `QSYS/CPYTOIMPF FROMFILE(${library}/${file} ${member}) ` +
`TOSTMF('${tempRmt}') ` +
`MBROPT(*REPLACE) STMFCCSID(1208) RCDDLM(*CRLF) DTAFMT(*DLM) RMVBLANK(*TRAILING) ADDCOLNAM(*SQL) FLDDLM(',') DECPNT(*PERIOD)`,
noLibList: true
});
const tempRmt = this.getTempRemote(Tools.qualifyPath(library, file, member));
const copyResult = await this.ibmi.runCommand({
command: `QSYS/CPYTOIMPF FROMFILE(${library}/${file} ${member}) ` +
`TOSTMF('${tempRmt}') ` +
`MBROPT(*REPLACE) STMFCCSID(1208) RCDDLM(*CRLF) DTAFMT(*DLM) RMVBLANK(*TRAILING) ADDCOLNAM(*SQL) FLDDLM(',') DECPNT(*PERIOD)`,
noLibList: true
});

if (copyResult.code === 0) {
let result = await this.downloadStreamfile(tempRmt);
if (copyResult.code === 0) {
let result = await this.downloadStreamfile(tempRmt);

if (this.config.autoClearTempData) {
Promise.allSettled([
this.ibmi.sendCommand({ command: `rm -rf ${tempRmt}`, directory: `.` }),
deleteTable ? this.ibmi.runCommand({ command: `DLTOBJ OBJ(${library}/${file}) OBJTYPE(*FILE)`, noLibList: true }) : Promise.resolve()
]);
}
if (this.config.autoClearTempData) {
Promise.allSettled([
this.ibmi.sendCommand({ command: `rm -rf ${tempRmt}`, directory: `.` }),
deleteTable ? this.ibmi.runCommand({ command: `DLTOBJ OBJ(${library}/${file}) OBJTYPE(*FILE)`, noLibList: true }) : Promise.resolve()
]);
}

return parse(result, {
columns: true,
skip_empty_lines: true,
cast: true,
onRecord(record) {
for (const key of Object.keys(record)) {
record[key] = record[key] === ` ` ? `` : record[key];
}
return record;
return parse(result, {
columns: true,
skip_empty_lines: true,
cast: true,
onRecord(record) {
for (const key of Object.keys(record)) {
record[key] = record[key] === ` ` ? `` : record[key];
}
});
return record;
}
});

} else {
throw new Error(`Failed fetching table: ${copyResult.stderr}`);
}
} else {
throw new Error(`Failed fetching table: ${copyResult.stderr}`);
}

}
Expand Down
Loading