Skip to content

Table Viewer optimization in ITC #1466

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

Merged
merged 29 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
176ab4b
Add changes from file explorer feature.
DanilaGrobovSEB Apr 13, 2025
c12016f
Use data connection instead of code execution.
DanilaGrobovSEB May 1, 2025
9d1aff6
Improve columns loading time.
DanilaGrobovSEB May 3, 2025
328b22d
Add column type mapping to the format.
DanilaGrobovSEB May 3, 2025
70069a6
Merge branch 'main' into table_viewer_optimization
danila-grobov May 3, 2025
179fcb7
DCO Remediation Commit for Danila Grobov (s4642g) <danila.grobov@seb.se>
DanilaGrobovSEB May 3, 2025
16eedec
Support empty arrays in json
DanilaGrobovSEB May 3, 2025
2ab7999
Fix incorrect label.
DanilaGrobovSEB May 6, 2025
e78ff54
Remove unnecessary parameters in data connection
DanilaGrobovSEB May 6, 2025
5383d38
Merge branch 'main' into table_viewer_optimization
danila-grobov May 6, 2025
ed6a9ed
Merge branch 'main' into table_viewer_optimization
danila-grobov May 8, 2025
cd0ef99
Fix tests
DanilaGrobovSEB May 11, 2025
019bd3d
Use proxyquire for mocking uuid.
danila-grobov May 12, 2025
d39ce5d
Merge remote-tracking branch 'upstream/main' into table_viewer_optimi…
danila-grobov May 12, 2025
bbcc56d
Merge branch 'main' into table_viewer_optimization
danila-grobov May 13, 2025
192c22f
Fix linting problems.
danila-grobov May 13, 2025
7ec2b55
Merge remote-tracking branch 'upstream/main' into table_viewer_optimi…
DanilaGrobovSEB May 25, 2025
7e4d8a2
Fix package-lock
danila-grobov May 25, 2025
b5785aa
Bring the file explorer changes back.
DanilaGrobovSEB May 25, 2025
207d325
DCO Remediation Commit for Danila Grobov <danila.grob@gmail.com>
danila-grobov May 25, 2025
674c87e
DCO Remediation Commit for Danila Grobov (s4642g) <danila.grobov@seb.se>
DanilaGrobovSEB May 25, 2025
a23005e
Merge branch 'main' into table_viewer_optimization
danila-grobov May 28, 2025
9cfb1f6
Fix formatting issues.
danila-grobov May 28, 2025
ce94b23
Refactor getColumnIconType.
danila-grobov May 30, 2025
3e92c0d
Merge branch 'main' into table_viewer_optimization
danila-grobov May 30, 2025
7bca4b8
chore(deps): bump the docusaurus group in /website with 4 updates (#1…
dependabot[bot] Jun 3, 2025
bdf1181
chore(deps-dev): bump the dev group across 1 directory with 4 updates…
dependabot[bot] Jun 3, 2025
ea8e546
Merge remote-tracking branch 'upstream/main' into table_viewer_optimi…
DanilaGrobovSEB Jun 9, 2025
6260bdc
Fix formatting.
danila-grobov Jun 9, 2025
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
103 changes: 30 additions & 73 deletions client/src/connection/itc/ItcLibraryAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
TableData,
TableRow,
} from "../../components/LibraryNavigator/types";
import { Column, ColumnCollection } from "../rest/api/compute";
import { runCode } from "./CodeRunner";
import { ColumnCollection } from "../rest/api/compute";
import { getColumnIconType } from "../util";
import { executeRawCode, runCode } from "./CodeRunner";
import { Config } from "./types";

class ItcLibraryAdapter implements LibraryAdapter {
Expand Down Expand Up @@ -45,31 +46,14 @@ class ItcLibraryAdapter implements LibraryAdapter {
}

public async getColumns(item: LibraryItem): Promise<ColumnCollection> {
const sql = `
%let OUTPUT;
proc sql;
select catx(',', name, type, varnum) as column into: OUTPUT separated by '~'
from sashelp.vcolumn
where libname='${item.library}' and memname='${item.name}'
order by varnum;
quit;
%put <COLOUTPUT> &OUTPUT; %put </COLOUTPUT>;
const code = `
$runner.GetColumns("${item.library}", "${item.name}")
`;

const columnLines = processQueryRows(
await this.runCode(sql, "<COLOUTPUT>", "</COLOUTPUT>"),
);

const columns = columnLines.map((lineText): Column => {
const [name, type, index] = lineText.split(",");

return {
name,
type,
index: parseInt(index, 10),
};
});

const output = await executeRawCode(code);
const columns = JSON.parse(output).map((column) => ({
...column,
type: getColumnIconType(column),
}));
return {
items: columns,
count: -1,
Expand Down Expand Up @@ -213,48 +197,16 @@ class ItcLibraryAdapter implements LibraryAdapter {
start: number,
limit: number,
): Promise<{ rows: Array<string[]>; count: number }> {
const maxTableNameLength = 32;
const tempTable = `${item.name}${hms()}${start}`.substring(
0,
maxTableNameLength,
);
const fullTableName = `${item.library}.${item.name}`;
const code = `
options nonotes nosource nodate nonumber;
%let COUNT;
proc sql;
SELECT COUNT(1) into: COUNT FROM ${item.library}.${item.name};
quit;
data work.${tempTable};
set ${item.library}.${item.name};
if ${start + 1} <= _N_ <= ${start + limit} then output;
run;

filename out temp;
proc json nokeys out=out pretty; export work.${tempTable}; run;

%put <TABLEDATA>;
%put <Count>&COUNT</Count>;
data _null_; infile out; input; put _infile_; run;
%put </TABLEDATA>;
proc datasets library=work nolist nodetails; delete ${tempTable}; run;
options notes source date number;
$runner.GetDatasetRecords("${fullTableName}", ${start}, ${limit})
`;

let output = await this.runCode(code, "<TABLEDATA>", "</TABLEDATA>");

// Extract result count
const countRegex = /<Count>(.*)<\/Count>/;
const countMatches = output.match(countRegex);
const count = parseInt(countMatches[1].replace(/\s|\n/gm, ""), 10);
output = output.replace(countRegex, "");

const rows = output.replace(/\n|\t/gm, "").slice(output.indexOf("{"));
const output = await executeRawCode(code);
try {
const tableData = JSON.parse(rows);
return { rows: tableData[`SASTableData+${tempTable}`], count };
return JSON.parse(output);
} catch (e) {
console.warn("Failed to load table data with error", e);
console.warn("Raw output", rows);
console.warn("Raw output", output);
throw new Error(
l10n.t(
"An error was encountered when loading table data. This usually happens when a table is too large or the data couldn't be processed. See console for more details.",
Expand All @@ -263,18 +215,28 @@ class ItcLibraryAdapter implements LibraryAdapter {
}
}

protected async runCode(
code: string,
startTag: string = "",
endTag: string = "",
protected async executionHandler(
callback: () => Promise<string>,
): Promise<string> {
try {
return await runCode(code, startTag, endTag);
return await callback();
} catch (e) {
onRunError(e);
return "";
}
}

protected async runCode(
code: string,
startTag: string = "",
endTag: string = "",
): Promise<string> {
return this.executionHandler(() => runCode(code, startTag, endTag));
}

protected async executeRawCode(code: string): Promise<string> {
return this.executionHandler(() => executeRawCode(code));
}
}

const processQueryRows = (response: string): string[] => {
Expand All @@ -288,9 +250,4 @@ const processQueryRows = (response: string): string[] => {
.filter((value, index, array) => array.indexOf(value) === index);
};

const hms = () => {
const date = new Date();
return `${date.getHours()}${date.getMinutes()}${date.getSeconds()}`;
};

export default ItcLibraryAdapter;
96 changes: 96 additions & 0 deletions client/src/connection/itc/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ScriptProperties = {
export const getScript = ({
interopLibraryFolderPath = "",
}: ScriptProperties) => `
using namespace System.Collections.Generic
function GetInteropDirectory {
# try to load from user specified path first
if ("${interopLibraryFolderPath}") {
Expand Down Expand Up @@ -51,6 +52,8 @@ try {

class SASRunner{
[System.__ComObject] $objSAS
[System.__ComObject] $objKeeper
[System.__ComObject] $dataConnection

[void]ResolveSystemVars(){
try {
Expand Down Expand Up @@ -89,6 +92,16 @@ class SASRunner{
$password
)

$this.objKeeper = New-Object -ComObject SASObjectManager.ObjectKeeper
$this.objKeeper.AddObject(1, "WorkspaceObject", $this.objSAS)

$this.dataConnection = New-Object -comobject ADODB.Connection
$this.dataConnection.Provider = "sas.IOMProvider"
$this.dataConnection.Properties("Data Source") = (
"iom-id://" + $this.objSAS.UniqueIdentifier
)
$this.dataConnection.Open()

Write-Host "${LineCodes.SessionCreatedCode}"
} catch {
Write-Error "${ERROR_START_TAG}Setup error: $_${ERROR_END_TAG}"
Expand Down Expand Up @@ -151,6 +164,8 @@ class SASRunner{

[void]Close(){
try{
$this.dataConnection.Close()
$this.objKeeper.RemoveObject($this.objSAS)
$this.objSAS.Close()
}catch{
Write-Error "${ERROR_START_TAG}Close error: $_${ERROR_END_TAG}"
Expand Down Expand Up @@ -228,6 +243,87 @@ class SASRunner{

Write-Host "${LineCodes.ResultsFetchedCode}"
}

[void]GetDatasetRecords([string]$tableName, [int]$start = 0, [int]$limit = 100) {
$objRecordSet = New-Object -comobject ADODB.Recordset
$objRecordSet.ActiveConnection = $this.dataConnection # This is needed to set the properties for sas formats.
$objRecordSet.Properties.Item("SAS Formats").Value = "_ALL_"

$objRecordSet.Open(
$tableName,
[System.Reflection.Missing]::Value, # Use the active connection
2, # adOpenDynamic
1, # adLockReadOnly
512 # adCmdTableDirect
)

$records = [List[List[object]]]::new()
$fields = $objRecordSet.Fields.Count

if ($objRecordSet.EOF) {
Write-Host '{"rows": [], "count": 0}'
return
}

$objRecordSet.AbsolutePosition = $start + 1

for ($j = 0; $j -lt $limit -and $objRecordSet.EOF -eq $False; $j++) {
$cell = [List[object]]::new()
for ($i = 0; $i -lt $fields; $i++) {
$cell.Add($objRecordSet.Fields.Item($i).Value)
}
$records.Add($cell)
$objRecordSet.MoveNext()
}
$objRecordSet.Close()

$objRecordSet.Open(
"SELECT COUNT(1) FROM $tableName",
$this.dataConnection, 3, 1, 1
) # adOpenStatic, adLockReadOnly, adCmdText
$count = $objRecordSet.Fields.Item(0).Value
$objRecordSet.Close()

$result = New-Object psobject
$result | Add-Member -MemberType NoteProperty -Name "rows" -Value $records
$result | Add-Member -MemberType NoteProperty -Name "count" -Value $count

Write-Host $(ConvertTo-Json -Depth 10 -InputObject $result -Compress)
}

[void]GetColumns([string]$libname, [string]$memname) {
$objRecordSet = New-Object -comobject ADODB.Recordset
$objRecordSet.ActiveConnection = $this.dataConnection
$query = @"
select name, type, format
from sashelp.vcolumn
where libname='$libname' and memname='$memname';
"@
$objRecordSet.Open(
$query,
[System.Reflection.Missing]::Value, # Use the active connection
2, # adOpenDynamic
1, # adLockReadOnly
1 # adCmdText
)

$rows = $objRecordSet.GetRows()

$objRecordSet.Close()

$parsedRows = @()
for ($i = 0; $i -lt $rows.GetLength(1); $i++) {
$parsedRow = [PSCustomObject]@{
index = $i + 1
name = $rows[0, $i]
type = $rows[1, $i]
format = $rows[2, $i]
}
$parsedRows += $parsedRow
}

Write-Host $(ConvertTo-Json -Depth 10 -InputObject $parsedRows -Compress)
}

[void]DeleteItemAtPath([string]$filePath,[bool]$recursive) {
if ($recursive) {
Expand Down
73 changes: 73 additions & 0 deletions client/src/connection/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,76 @@ export const extractTextBetweenTags = (
.replace(/\n$/, "")
: text;
};

export const getColumnIconType = ({
type,
format,
}: {
index: number;
type: string;
name: string;
format: string;
}) => {
format = format.toUpperCase();

const isDateFormat = () =>
[
"DAT",
"MM",
"DD",
"YY",
"EURDF",
"JUL",
"YEAR",
"DAY",
"MONTH",
"MON",
"DOWNAME",
].some((f) => format.includes(f)) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add 8601 formats? They are common in clinical programming.

Date: E8601DA, E8601DN, D8601DA, D8601DN: ([B|E]8601D[N|A])
Datetime: ([B|E]8601(D[T|Z|X]|LX))
Time: ([B|E]8601(T[M|Z|X]|LZ))

Copy link
Collaborator

@snlwih snlwih Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@scnwwu: Perhaps enhancing SAS format support to include the lateset available SAS-provided formats should be moved into a dedicated pull request?

@DmitryMK: Also wanted to create awarenes for the fact the the doc link mentioned in the previous comment for National Language Support formats is really old. Here are doc links that will always point to the latest production:

Tip: Use .../default/... instead of .../v_xxx/... in SAS Viya 4 doc links, so they resolve to the latest documentation.

![
"TIME",
"HH",
"SS",
"COMM",
"DATEAMPM",
"DATETIME",
"NLDATMTM",
"NLDATM",
"NLDATMAP",
"NLDATMW",
].some((f) => format.includes(f));

const isTimeFormat = () =>
["TIME", "TIMAP", "HOUR", "HH", "MM", "SS", "NLDATMTM"].some((f) =>
format.includes(f),
) && !["DATEAMPM", "DATETIME", "COMMA"].some((f) => format.includes(f));

const isDateTimeFormat = () =>
["DATEAMPM", "DATETIME", "NLDATM", "NLDATMAP", "NLDATMW"].some((f) =>
format.includes(f),
);

const isCurrencyFormat = () =>
["NLMNI", "NLMNL", "NLMNY", "YEN", "DOLLAR", "EURO"].some((f) =>
format.includes(f),
);

if (type !== "num") {
return type;
}

if (isDateFormat()) {
return "date";
}
if (isTimeFormat()) {
return "time";
}
if (isDateTimeFormat()) {
return "datetime";
}
if (isCurrencyFormat()) {
return "currency";
}

return type;
};
2 changes: 1 addition & 1 deletion client/test/connection/itc/Coderunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { runCode } from "../../../src/connection/itc/CodeRunner";
import { Session } from "../../../src/connection/session";

export class MockSession extends Session {
private _logFn;
protected _logFn;
private _runMap: Record<string, string> | undefined;
public sasSystemLine = "The Sas System";

Expand Down
Loading
Loading