Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
9 changes: 9 additions & 0 deletions client/src/components/LibraryNavigator/LibraryDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class LibraryDataProvider
registerAPI("getColumns", model.fetchColumns.bind(model));
registerAPI("getTables", model.getTables.bind(model));
registerAPI("getLibraries", model.getLibraries.bind(model));
registerAPI("getTableInfo", model.getTableInfo.bind(model));
}

public getSubscriptions(): Disposable[] {
Expand Down Expand Up @@ -178,6 +179,14 @@ class LibraryDataProvider
this.model.useAdapter(libraryAdapter);
this._onDidChangeTreeData.fire(undefined);
}

public async getTableInfo(item: LibraryItem) {
return await this.model.getTableInfo(item);
}

public async fetchColumns(item: LibraryItem) {
return await this.model.fetchColumns(item);
}
}

export default LibraryDataProvider;
8 changes: 8 additions & 0 deletions client/src/components/LibraryNavigator/LibraryModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ class LibraryModel {
}
}

public async getTableInfo(item: LibraryItem) {
await this.libraryAdapter.setup();
if (this.libraryAdapter.getTableInfo) {
return await this.libraryAdapter.getTableInfo(item);
}
throw new Error("Table properties not supported for this connection type");
}

public async getChildren(item?: LibraryItem): Promise<LibraryItem[]> {
if (!this.libraryAdapter) {
return [];
Expand Down
25 changes: 25 additions & 0 deletions client/src/components/LibraryNavigator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as path from "path";
import { profileConfig } from "../../commands/profile";
import { Column } from "../../connection/rest/api/compute";
import DataViewer from "../../panels/DataViewer";
import TablePropertiesViewer from "../../panels/TablePropertiesViewer";
import { WebViewManager } from "../../panels/WebviewManager";
import { SubscriptionProvider } from "../SubscriptionProvider";
import LibraryAdapterFactory from "./LibraryAdapterFactory";
Expand Down Expand Up @@ -101,6 +102,30 @@ class LibraryNavigator implements SubscriptionProvider {
);
},
),
commands.registerCommand(
"SAS.showTableProperties",
async (item: LibraryItem) => {
try {
const tableInfo = await this.libraryDataProvider.getTableInfo(item);
const columns = await this.libraryDataProvider.fetchColumns(item);

this.webviewManager.render(
new TablePropertiesViewer(
this.extensionUri,
item.uid,
tableInfo,
columns,
false, // Show properties tab
),
`properties-${item.uid}`,
);
} catch (error) {
window.showErrorMessage(
`Failed to load table properties: ${error.message}`,
);
}
},
),
commands.registerCommand("SAS.collapseAllLibraries", () => {
commands.executeCommand(
"workbench.actions.treeView.librarydataprovider.collapseAll",
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/LibraryNavigator/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { ColumnCollection } from "../../connection/rest/api/compute";
import { ColumnCollection, TableInfo } from "../../connection/rest/api/compute";

export const LibraryType = "library";
export const TableType = "table";
Expand Down Expand Up @@ -56,5 +56,6 @@ export interface LibraryAdapter {
items: LibraryItem[];
count: number;
}>;
getTableInfo?(item: LibraryItem): Promise<TableInfo>;
setup(): Promise<void>;
}
57 changes: 54 additions & 3 deletions client/src/connection/itc/ItcLibraryAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
TableData,
TableRow,
} from "../../components/LibraryNavigator/types";
import { ColumnCollection } from "../rest/api/compute";
import { ColumnCollection, TableInfo } from "../rest/api/compute";
import { getColumnIconType } from "../util";
import { executeRawCode, runCode } from "./CodeRunner";
import { Config } from "./types";
Expand Down Expand Up @@ -50,7 +50,8 @@ class ItcLibraryAdapter implements LibraryAdapter {
$runner.GetColumns("${item.library}", "${item.name}")
`;
const output = await executeRawCode(code);
const columns = JSON.parse(output).map((column) => ({
const rawColumns = JSON.parse(output);
const columns = rawColumns.map((column) => ({
...column,
type: getColumnIconType(column),
}));
Expand Down Expand Up @@ -127,7 +128,6 @@ class ItcLibraryAdapter implements LibraryAdapter {
const { rows } = await this.getRows(item, start, limit);

rows.unshift(columns);

// Fetching csv doesn't rely on count. Instead, we get the count
// upfront via getTableRowCount
return { rows, count: -1 };
Expand Down Expand Up @@ -196,6 +196,57 @@ class ItcLibraryAdapter implements LibraryAdapter {
}
}

public async getTableInfo(item: LibraryItem): Promise<TableInfo> {
try {
// Use the PowerShell GetTableInfo function which queries sashelp.vtable
const code = `
$runner.GetTableInfo("${item.library}", "${item.name}")
`;
const output = await executeRawCode(code);
const tableInfo = JSON.parse(output);

return {
name: tableInfo.name || item.name,
libref: tableInfo.libref || item.library,
type: tableInfo.type || "DATA",
label: tableInfo.label || "",
engine: "", // Not available in sashelp.vtable for SAS 9.4
extendedType: tableInfo.extendedType || "",
rowCount: tableInfo.rowCount || 0,
columnCount: tableInfo.columnCount || 0,
logicalRecordCount: tableInfo.rowCount || 0,
physicalRecordCount: tableInfo.rowCount || 0,
recordLength: 0, // Not available in vtable
bookmarkLength: 0, // Not available in vtable
compressionRoutine: tableInfo.compressionRoutine || "",
encoding: "", // Not available in vtable
creationTimeStamp: tableInfo.creationTimeStamp || "",
modifiedTimeStamp: tableInfo.modifiedTimeStamp || "",
};
} catch (error) {
console.warn("Failed to get table info:", error);
// If anything fails, return basic info
return {
name: item.name,
libref: item.library,
type: "DATA",
label: "",
engine: "",
extendedType: "",
rowCount: 0,
columnCount: 0,
logicalRecordCount: 0,
physicalRecordCount: 0,
recordLength: 0,
bookmarkLength: 0,
compressionRoutine: "",
encoding: "",
creationTimeStamp: "",
modifiedTimeStamp: "",
};
}
}

protected async executionHandler(
callback: () => Promise<string>,
): Promise<string> {
Expand Down
45 changes: 43 additions & 2 deletions client/src/connection/itc/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,10 @@ class SASRunner{
$objRecordSet = New-Object -comobject ADODB.Recordset
$objRecordSet.ActiveConnection = $this.dataConnection
$query = @"
select name, type, format
select name, type, format, label, length, varnum
from sashelp.vcolumn
where libname='$libname' and memname='$memname';
where libname='$libname' and memname='$memname'
order by varnum;
"@
$objRecordSet.Open(
$query,
Expand All @@ -318,6 +319,9 @@ class SASRunner{
name = $rows[0, $i]
type = $rows[1, $i]
format = $rows[2, $i]
label = $rows[3, $i]
length = $rows[4, $i]
varnum = $rows[5, $i]
}
$parsedRows += $parsedRow
}
Expand Down Expand Up @@ -580,6 +584,43 @@ class SASRunner{
Write-Host $(ConvertTo-Json -Depth 10 -InputObject $result -Compress)
}

[void]GetTableInfo([string]$libname, [string]$memname) {
$objRecordSet = New-Object -comobject ADODB.Recordset
$objRecordSet.ActiveConnection = $this.dataConnection
$query = @"
select memname, memtype, crdate, modate, nobs, nvar, compress,
memlabel, typemem, filesize, delobs
from sashelp.vtable
where libname='$libname' and memname='$memname';
"@
$objRecordSet.Open(
$query,
[System.Reflection.Missing]::Value, # Use the active connection
2, # adOpenDynamic
1, # adLockReadOnly
1 # adCmdText
)

$result = New-Object psobject
if (-not $objRecordSet.EOF) {
$result | Add-Member -MemberType NoteProperty -Name "name" -Value $objRecordSet.Fields.Item(0).Value
$result | Add-Member -MemberType NoteProperty -Name "type" -Value $objRecordSet.Fields.Item(1).Value
$result | Add-Member -MemberType NoteProperty -Name "creationTimeStamp" -Value $objRecordSet.Fields.Item(2).Value
$result | Add-Member -MemberType NoteProperty -Name "modifiedTimeStamp" -Value $objRecordSet.Fields.Item(3).Value
$result | Add-Member -MemberType NoteProperty -Name "rowCount" -Value $objRecordSet.Fields.Item(4).Value
$result | Add-Member -MemberType NoteProperty -Name "columnCount" -Value $objRecordSet.Fields.Item(5).Value
$result | Add-Member -MemberType NoteProperty -Name "compressionRoutine" -Value $objRecordSet.Fields.Item(6).Value
$result | Add-Member -MemberType NoteProperty -Name "label" -Value $objRecordSet.Fields.Item(7).Value
$result | Add-Member -MemberType NoteProperty -Name "extendedType" -Value $objRecordSet.Fields.Item(8).Value
$result | Add-Member -MemberType NoteProperty -Name "fileSize" -Value $objRecordSet.Fields.Item(9).Value
$result | Add-Member -MemberType NoteProperty -Name "deletedObs" -Value $objRecordSet.Fields.Item(10).Value
$result | Add-Member -MemberType NoteProperty -Name "libref" -Value $libname
}
$objRecordSet.Close()

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

[void]GetTables([string]$libname) {
$objRecordSet = New-Object -comobject ADODB.Recordset
$objRecordSet.ActiveConnection = $this.dataConnection
Expand Down
24 changes: 23 additions & 1 deletion client/src/connection/rest/RestLibraryAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
TableData,
} from "../../components/LibraryNavigator/types";
import { appendSessionLogFn } from "../../components/logViewer";
import { ColumnCollection, DataAccessApi, RowCollection } from "./api/compute";
import {
ColumnCollection,
DataAccessApi,
RowCollection,
TableInfo,
} from "./api/compute";
import { getApiConfig } from "./common";

const requestOptions = {
Expand Down Expand Up @@ -137,6 +142,23 @@ class RestLibraryAdapter implements LibraryAdapter {
return { rowCount: response.data.rowCount, maxNumberOfRowsToRead: 1000 };
}

public async getTableInfo(item: LibraryItem): Promise<TableInfo> {
await this.setup();
const response = await this.retryOnFail(
async () =>
await this.dataAccessApi.getTable(
{
sessionId: this.sessionId,
libref: item.library || "",
tableName: item.name,
},
{ headers: { Accept: "application/json" } },
),
);

return response.data;
}

private async retryOnFail<T>(
callbackFn: () => Promise<AxiosResponse<T>>,
): Promise<AxiosResponse<T>> {
Expand Down
Loading
Loading