Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 type { 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