Add datavirtualization extension (#21594)

* initial

* cleanup

* Add typings ref

* fix compile

* remove unused

* add missing

* another unused

* Use newer vscodetestcover

* newer dataprotocol

* format

* cleanup ignores

* fix out path

* fix entry point

* more cleanup

* Move into src folder

* Handle service client log messages

* remove unused
This commit is contained in:
Charles Gagnon
2023-01-17 09:57:21 -08:00
committed by GitHub
parent 9184c414de
commit ec838947b0
103 changed files with 12432 additions and 1 deletions

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { DataSourceWizardService } from '../../../services/contracts';
import { ImportDataModel } from './models';
import { TableFromFileWizard } from '../tableFromFileWizard';
export abstract class ImportPage {
protected constructor(
protected readonly instance: TableFromFileWizard,
protected readonly wizardPage: azdata.window.WizardPage,
protected readonly model: ImportDataModel,
protected readonly view: azdata.ModelView,
protected readonly provider: DataSourceWizardService) { }
/**
* This method constructs all the elements of the page.
* @returns {Promise<boolean>}
*/
public abstract start(): Promise<boolean>;
/**
* This method is called when the user is entering the page.
* @returns {Promise<boolean>}
*/
public abstract onPageEnter(): Promise<void>;
/**
* This method is called when the user is leaving the page.
* @returns {Promise<boolean>}
*/
public abstract onPageLeave(clickedNext: boolean): Promise<boolean>;
}

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { FileNode } from '../../../hdfsProvider';
import { ColumnDefinition, DataSourceInstance } from '../../../services/contracts';
/**
* The main data model that communicates between the pages.
*/
export interface ImportDataModel {
proseColumns: ColumnDefinition[];
proseDataPreview: string[][];
serverConn: azdata.connection.ConnectionProfile;
sessionId: string;
allDatabases: string[];
versionInfo: {
serverMajorVersion: number;
productLevel: string;
};
database: string;
existingDataSource: string;
newDataSource: DataSourceInstance;
table: string;
fileFormat: string;
existingSchema: string;
newSchema: string;
parentFile: {
isFolder: boolean;
filePath: string;
};
proseParsingFile: FileNode;
fileType: string;
columnDelimiter: string;
firstRow: number;
quoteCharacter: string;
}

View File

@@ -0,0 +1,548 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import * as path from 'path';
import * as url from 'url';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { TableFromFileWizard } from '../tableFromFileWizard';
import { DataSourceWizardService, DatabaseInfo } from '../../../services/contracts';
import { getDropdownValue, getErrorMessage, stripUrlPathSlashes } from '../../../utils';
import { ctp24Version, sql2019MajorVersion, ctp25Version, ctp3Version } from '../../../constants';
const localize = nls.loadMessageBundle();
export class FileConfigPageUiElements {
public fileTextBox: azdata.TextComponent;
public serverTextBox: azdata.TextComponent;
public databaseDropdown: azdata.DropDownComponent;
public dataSourceDropdown: azdata.DropDownComponent;
public tableNameTextBox: azdata.InputBoxComponent;
public schemaDropdown: azdata.DropDownComponent;
public databaseLoader: azdata.LoadingComponent;
public dataSourceLoader: azdata.LoadingComponent;
public schemaLoader: azdata.LoadingComponent;
public fileFormatNameTextBox: azdata.InputBoxComponent;
public refreshButton: azdata.ButtonComponent;
}
export class FileConfigPage extends ImportPage {
private ui: FileConfigPageUiElements;
public form: azdata.FormContainer;
private readonly noDataSourcesError = localize('tableFromFileImport.noDataSources', 'No valid external data sources were found in the specified database.');
private readonly noSchemasError = localize('tableFromFileImport.noSchemas', 'No user schemas were found in the specified database.');
private readonly tableExistsError = localize('tableFromFileImport.tableExists', 'The specified table name already exists under the specified schema.');
private readonly fileFormatExistsError = localize('tableFromFileImport.fileFormatExists', 'The specified external file format name already exists.');
private pageSetupComplete: boolean = false;
private existingTableSet: Set<string>;
private existingFileFormatSet: Set<string>;
private existingSchemaSet: Set<string>;
public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) {
super(instance, wizardPage, model, view, provider);
}
public setUi(ui: FileConfigPageUiElements) {
this.ui = ui;
}
async start(): Promise<boolean> {
this.ui = new FileConfigPageUiElements();
let fileNameComponent = this.createFileTextBox();
let serverNameComponent = this.createServerTextBox();
let databaseComponent = this.createDatabaseDropdown();
let dataSourceComponent = this.createDataSourceDropdown();
let tableNameComponent = this.createTableNameBox();
let schemaComponent = this.createSchemaDropdown();
let fileFormatNameComponent = this.createFileFormatNameBox();
let refreshButton = this.createRefreshButton();
this.form = this.view.modelBuilder.formContainer()
.withFormItems([
fileNameComponent,
serverNameComponent,
databaseComponent,
dataSourceComponent,
tableNameComponent,
schemaComponent,
fileFormatNameComponent,
refreshButton
]).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<void> {
if (!this.pageSetupComplete) {
this.instance.clearStatusMessage();
this.toggleInputsEnabled(false, true);
try {
this.parseFileInfo();
await this.createSession();
await this.populateDatabaseDropdown();
await this.populateDatabaseInfo();
} finally {
this.toggleInputsEnabled(true, true);
}
this.pageSetupComplete = true;
}
}
async onPageLeave(clickedNext: boolean): Promise<boolean> {
if (this.ui.schemaLoader.loading ||
this.ui.databaseLoader.loading ||
this.ui.dataSourceLoader.loading ||
!this.ui.refreshButton.enabled) {
return false;
}
if (clickedNext) {
if ((this.model.newSchema === undefined || this.model.newSchema === '') &&
(this.model.existingSchema === undefined || this.model.existingSchema === '')) {
return false;
}
if (!this.model.newDataSource &&
(this.model.existingDataSource === undefined || this.model.existingDataSource === '')) {
return false;
}
if (this.model.existingSchema && this.model.existingSchema !== '' &&
this.existingTableSet && this.existingTableSet.has(this.model.existingSchema + '.' + this.model.table)) {
this.instance.showErrorMessage(this.tableExistsError);
return false;
}
if (this.existingFileFormatSet && this.existingFileFormatSet.has(this.model.fileFormat)) {
this.instance.showErrorMessage(this.fileFormatExistsError);
return false;
}
}
return true;
}
private async createSession(): Promise<void> {
try {
this.ui.serverTextBox.value = this.model.serverConn.serverName;
if (this.model.sessionId) {
await this.provider.disposeWizardSession(this.model.sessionId);
delete this.model.sessionId;
delete this.model.allDatabases;
delete this.model.versionInfo;
}
let sessionResponse = await this.provider.createDataSourceWizardSession(this.model.serverConn);
this.model.sessionId = sessionResponse.sessionId;
this.model.allDatabases = sessionResponse.databaseList.map(db => db.name);
this.model.versionInfo = {
serverMajorVersion: sessionResponse.serverMajorVersion,
productLevel: sessionResponse.productLevel
};
} catch (err) {
this.instance.showErrorMessage(getErrorMessage(err));
}
}
private createDatabaseDropdown(): azdata.FormComponent {
this.ui.databaseDropdown = this.view.modelBuilder.dropDown().withProps({
values: [''],
value: undefined
}).component();
// Handle database changes
this.ui.databaseDropdown.onValueChanged(async (db) => {
this.model.database = getDropdownValue(this.ui.databaseDropdown.value);
this.instance.clearStatusMessage();
this.toggleInputsEnabled(false, false);
try {
await this.populateDatabaseInfo();
} finally {
this.toggleInputsEnabled(true, false);
}
});
this.ui.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.ui.databaseDropdown).component();
return {
component: this.ui.databaseLoader,
title: localize('tableFromFileImport.databaseDropdownTitle', 'Database the external table will be created in')
};
}
private async populateDatabaseDropdown(): Promise<boolean> {
let idx = -1;
let count = -1;
let dbNames = await this.model.allDatabases.map(dbName => {
count++;
if (this.model.database && dbName === this.model.database) {
idx = count;
}
return dbName;
});
if (idx >= 0) {
let tmp = dbNames[0];
dbNames[0] = dbNames[idx];
dbNames[idx] = tmp;
}
this.model.database = dbNames[0];
this.ui.databaseDropdown.updateProperties({
values: dbNames,
value: dbNames[0]
});
return true;
}
private createDataSourceDropdown(): azdata.FormComponent {
this.ui.dataSourceDropdown = this.view.modelBuilder.dropDown().withProps({
values: [''],
value: undefined
}).component();
this.ui.dataSourceDropdown.onValueChanged(async (db) => {
if (!this.model.newDataSource) {
this.model.existingDataSource = getDropdownValue(this.ui.dataSourceDropdown.value);
}
});
this.ui.dataSourceLoader = this.view.modelBuilder.loadingComponent().withItem(this.ui.dataSourceDropdown).component();
return {
component: this.ui.dataSourceLoader,
title: localize('tableFromFileImport.dataSourceDropdown', 'External data source for new external table')
};
}
private populateDataSourceDropdown(dbInfo: DatabaseInfo): boolean {
let errorCleanup = (errorMsg: string = this.noDataSourcesError) => {
this.ui.dataSourceDropdown.updateProperties({ values: [''], value: undefined });
this.instance.showErrorMessage(errorMsg);
this.model.existingDataSource = undefined;
this.model.newDataSource = undefined;
};
if (!dbInfo || !dbInfo.externalDataSources) {
errorCleanup();
return false;
}
let expectedDataSourceHost: string;
let expectedDataSourcePort: string;
let expectedDataSourcePath = '';
let majorVersion = this.model.versionInfo.serverMajorVersion;
let productLevel = this.model.versionInfo.productLevel;
if (majorVersion === sql2019MajorVersion && productLevel === ctp24Version) {
expectedDataSourceHost = 'service-master-pool';
expectedDataSourcePort = '50070';
} else if (majorVersion === sql2019MajorVersion && productLevel === ctp25Version) {
expectedDataSourceHost = 'nmnode-0-svc';
expectedDataSourcePort = '50070';
} else if (majorVersion === sql2019MajorVersion && productLevel === ctp3Version) {
expectedDataSourceHost = 'controller-svc';
expectedDataSourcePort = '8080';
expectedDataSourcePath = 'default';
} else { // Default: SQL 2019 CTP 3.1 syntax
expectedDataSourceHost = 'controller-svc';
expectedDataSourcePort = null;
expectedDataSourcePath = 'default';
}
let filteredSources = dbInfo.externalDataSources.filter(dataSource => {
if (!dataSource.location) {
return false;
}
let locationUrl = url.parse(dataSource.location);
let pathName = stripUrlPathSlashes(locationUrl.pathname);
return locationUrl.protocol === 'sqlhdfs:'
&& locationUrl.hostname === expectedDataSourceHost
&& locationUrl.port === expectedDataSourcePort
&& pathName === expectedDataSourcePath;
});
if (filteredSources.length === 0) {
let sourceName = 'SqlStoragePool';
let nameSuffix = 0;
let existingNames = new Set<string>(dbInfo.externalDataSources.map(dataSource => dataSource.name));
while (existingNames.has(sourceName)) {
sourceName = `SqlStoragePool${++nameSuffix}`;
}
let storageLocation: string;
if (expectedDataSourcePort !== null) {
storageLocation = `sqlhdfs://${expectedDataSourceHost}:${expectedDataSourcePort}/${expectedDataSourcePath}`;
} else {
storageLocation = `sqlhdfs://${expectedDataSourceHost}/${expectedDataSourcePath}`;
}
this.model.newDataSource = {
name: sourceName,
location: storageLocation,
authenticationType: undefined,
username: undefined,
credentialName: undefined
};
filteredSources.unshift(this.model.newDataSource);
} else {
this.model.newDataSource = undefined;
}
let idx = -1;
let count = -1;
let dataSourceNames = filteredSources.map(dataSource => {
let sourceName = dataSource.name;
count++;
if ((this.model.existingDataSource && sourceName === this.model.existingDataSource) ||
(this.model.newDataSource && sourceName === this.model.newDataSource.name)) {
idx = count;
}
return sourceName;
});
if (idx >= 0) {
let tmp = dataSourceNames[0];
dataSourceNames[0] = dataSourceNames[idx];
dataSourceNames[idx] = tmp;
}
if (this.model.newDataSource) {
this.model.existingDataSource = undefined;
} else {
this.model.existingDataSource = dataSourceNames[0];
}
this.ui.dataSourceDropdown.updateProperties({
values: dataSourceNames,
value: dataSourceNames[0]
});
return true;
}
private createFileTextBox(): azdata.FormComponent {
this.ui.fileTextBox = this.view.modelBuilder.text().component();
let title = this.model.parentFile.isFolder
? localize('tableFromFileImport.folderTextboxTitle', 'Source Folder')
: localize('tableFromFileImport.fileTextboxTitle', 'Source File');
return {
component: this.ui.fileTextBox,
title: title
};
}
private createServerTextBox(): azdata.FormComponent {
this.ui.serverTextBox = this.view.modelBuilder.text().component();
return {
component: this.ui.serverTextBox,
title: localize('tableFromFileImport.destConnTitle', 'Destination Server')
};
}
private parseFileInfo(): void {
let parentFilePath = this.model.parentFile.filePath;
this.ui.fileTextBox.value = parentFilePath;
let parsingFileExtension = path.extname(this.model.proseParsingFile.hdfsPath);
if (parsingFileExtension.toLowerCase() === '.json') {
this.model.fileType = 'JSON';
} else {
this.model.fileType = 'TXT';
}
let parentBaseName = path.basename(parentFilePath, parsingFileExtension);
this.ui.tableNameTextBox.value = parentBaseName;
this.model.table = this.ui.tableNameTextBox.value;
this.ui.tableNameTextBox.validate();
this.ui.fileFormatNameTextBox.value = `FileFormat_${parentBaseName}`;
this.model.fileFormat = this.ui.fileFormatNameTextBox.value;
this.ui.fileFormatNameTextBox.validate();
}
private createTableNameBox(): azdata.FormComponent {
this.ui.tableNameTextBox = this.view.modelBuilder.inputBox()
.withValidation((name) => {
let tableName = name.value;
if (!tableName || tableName.length === 0) {
return false;
}
return true;
}).withProperties({
required: true,
}).component();
this.ui.tableNameTextBox.onTextChanged((tableName) => {
this.model.table = tableName;
});
return {
component: this.ui.tableNameTextBox,
title: localize('tableFromFileImport.tableTextboxTitle', 'Name for new external table '),
};
}
private createFileFormatNameBox(): azdata.FormComponent {
this.ui.fileFormatNameTextBox = this.view.modelBuilder.inputBox()
.withValidation((name) => {
let fileFormat = name.value;
if (!fileFormat || fileFormat.length === 0) {
return false;
}
return true;
}).withProperties({
required: true,
}).component();
this.ui.fileFormatNameTextBox.onTextChanged((fileFormat) => {
this.model.fileFormat = fileFormat;
});
return {
component: this.ui.fileFormatNameTextBox,
title: localize('tableFromFileImport.fileFormatTextboxTitle', 'Name for new table\'s external file format'),
};
}
private createSchemaDropdown(): azdata.FormComponent {
this.ui.schemaDropdown = this.view.modelBuilder.dropDown().withProps({
values: [''],
value: undefined,
editable: true,
fireOnTextChange: true
}).component();
this.ui.schemaLoader = this.view.modelBuilder.loadingComponent().withItem(this.ui.schemaDropdown).component();
this.ui.schemaDropdown.onValueChanged(() => {
let schema = getDropdownValue(this.ui.schemaDropdown.value);
if (this.existingSchemaSet.has(schema)) {
this.model.newSchema = undefined;
this.model.existingSchema = schema;
} else {
this.model.newSchema = schema;
this.model.existingSchema = undefined;
}
});
return {
component: this.ui.schemaLoader,
title: localize('tableFromFileImport.schemaTextboxTitle', 'Schema for new external table'),
};
}
private populateSchemaDropdown(dbInfo: DatabaseInfo): boolean {
if (!dbInfo || !dbInfo.schemaList || dbInfo.schemaList.length === 0) {
this.ui.schemaDropdown.updateProperties({ values: [''], value: undefined });
this.instance.showErrorMessage(this.noSchemasError);
this.model.newSchema = undefined;
this.model.existingSchema = undefined;
return false;
}
this.model.newSchema = undefined;
if (!this.model.existingSchema) {
this.model.existingSchema = dbInfo.defaultSchema;
}
let idx = -1;
let count = -1;
let values = dbInfo.schemaList.map(schema => {
count++;
if (this.model.existingSchema && schema === this.model.existingSchema) {
idx = count;
}
return schema;
});
if (idx >= 0) {
let tmp = values[0];
values[0] = values[idx];
values[idx] = tmp;
} else {
// Default schema wasn't in the list, so take the first one instead
this.model.existingSchema = values[0];
}
this.ui.schemaDropdown.updateProperties({
values: values,
value: values[0]
});
return true;
}
private async refreshPage(): Promise<void> {
this.pageSetupComplete = false;
await this.onPageEnter();
}
private createRefreshButton(): azdata.FormComponent {
this.ui.refreshButton = this.view.modelBuilder.button().withProps({
label: localize('tableFromFileImport.refreshButtonTitle', 'Refresh')
}).component();
this.ui.refreshButton.onDidClick(async () => await this.refreshPage());
return {
component: this.ui.refreshButton,
title: undefined
};
}
private async populateDatabaseInfo(): Promise<boolean> {
try {
let dbInfo: DatabaseInfo = undefined;
let dbInfoResponse = await this.provider.getDatabaseInfo({ sessionId: this.model.sessionId, databaseName: this.model.database });
if (!dbInfoResponse.isSuccess) {
this.instance.showErrorMessage(dbInfoResponse.errorMessages.join('\n'));
this.existingTableSet = undefined;
this.existingFileFormatSet = undefined;
this.existingSchemaSet = undefined;
} else {
dbInfo = dbInfoResponse.databaseInfo;
this.existingTableSet = new Set<string>(dbInfo.externalTables.map(table => table.schemaName + '.' + table.tableName));
this.existingFileFormatSet = new Set<string>(dbInfo.externalFileFormats);
this.existingSchemaSet = new Set<string>(dbInfo.schemaList);
}
let r1 = this.populateDataSourceDropdown(dbInfo);
let r2 = this.populateSchemaDropdown(dbInfo);
return r1 && r2;
} catch (err) {
this.instance.showErrorMessage(getErrorMessage(err));
}
}
private toggleInputsEnabled(enable: boolean, includeDbLoader: boolean) {
if (includeDbLoader) {
this.ui.databaseLoader.loading = !enable;
}
this.ui.databaseDropdown.enabled = enable;
this.ui.refreshButton.enabled = enable;
this.ui.dataSourceDropdown.enabled = enable;
this.ui.schemaDropdown.enabled = enable;
this.ui.dataSourceLoader.loading = !enable;
this.ui.schemaLoader.loading = !enable;
}
}

View File

@@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { TableFromFileWizard } from '../tableFromFileWizard';
import { DataSourceWizardService, ColumnDefinition } from '../../../services/contracts';
const localize = nls.loadMessageBundle();
export class ModifyColumnsPageUiElements {
public table: azdata.DeclarativeTableComponent;
public loading: azdata.LoadingComponent;
public text: azdata.TextComponent;
}
export class ModifyColumnsPage extends ImportPage {
private readonly categoryValues = [
{ name: 'bigint', displayName: 'bigint' },
{ name: 'binary(50)', displayName: 'binary(50)' },
{ name: 'bit', displayName: 'bit' },
{ name: 'char(10)', displayName: 'char(10)' },
{ name: 'date', displayName: 'date' },
{ name: 'datetime', displayName: 'datetime' },
{ name: 'datetime2(7)', displayName: 'datetime2(7)' },
{ name: 'datetimeoffset(7)', displayName: 'datetimeoffset(7)' },
{ name: 'decimal(18, 10)', displayName: 'decimal(18, 10)' },
{ name: 'float', displayName: 'float' },
{ name: 'geography', displayName: 'geography' },
{ name: 'geometry', displayName: 'geometry' },
{ name: 'hierarchyid', displayName: 'hierarchyid' },
{ name: 'int', displayName: 'int' },
{ name: 'money', displayName: 'money' },
{ name: 'nchar(10)', displayName: 'nchar(10)' },
{ name: 'ntext', displayName: 'ntext' },
{ name: 'numeric(18, 0)', displayName: 'numeric(18, 0)' },
{ name: 'nvarchar(50)', displayName: 'nvarchar(50)' },
{ name: 'nvarchar(MAX)', displayName: 'nvarchar(MAX)' },
{ name: 'real', displayName: 'real' },
{ name: 'smalldatetime', displayName: 'smalldatetime' },
{ name: 'smallint', displayName: 'smallint' },
{ name: 'smallmoney', displayName: 'smallmoney' },
{ name: 'sql_variant', displayName: 'sql_variant' },
{ name: 'text', displayName: 'text' },
{ name: 'time(7)', displayName: 'time(7)' },
{ name: 'timestamp', displayName: 'timestamp' },
{ name: 'tinyint', displayName: 'tinyint' },
{ name: 'uniqueidentifier', displayName: 'uniqueidentifier' },
{ name: 'varbinary(50)', displayName: 'varbinary(50)' },
{ name: 'varbinary(MAX)', displayName: 'varbinary(MAX)' },
{ name: 'varchar(50)', displayName: 'varchar(50)' },
{ name: 'varchar(MAX)', displayName: 'varchar(MAX)' }
];
private ui: ModifyColumnsPageUiElements;
private form: azdata.FormContainer;
public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) {
super(instance, wizardPage, model, view, provider);
}
public setUi(ui: ModifyColumnsPageUiElements) {
this.ui = ui;
}
private static convertMetadata(column: ColumnDefinition): any[] {
return [column.columnName, column.dataType, column.isNullable];
}
async start(): Promise<boolean> {
this.ui = new ModifyColumnsPageUiElements();
this.ui.loading = this.view.modelBuilder.loadingComponent().component();
this.ui.table = this.view.modelBuilder.declarativeTable().component();
this.ui.text = this.view.modelBuilder.text().component();
this.ui.table.onDataChanged((e) => {
this.model.proseColumns = [];
this.ui.table.data.forEach((row) => {
this.model.proseColumns.push({
columnName: row[0],
dataType: row[1],
isNullable: row[2],
collationName: undefined
});
});
});
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
{
component: this.ui.text,
title: ''
},
{
component: this.ui.table,
title: ''
}
], {
horizontal: false,
componentWidth: '100%'
}).component();
this.ui.loading.component = this.form;
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<void> {
this.ui.loading.loading = true;
await this.populateTable();
this.ui.loading.loading = false;
}
async onPageLeave(clickedNext: boolean): Promise<boolean> {
if (this.ui.loading.loading) {
return false;
}
return true;
}
private async populateTable() {
let data: any[][] = [];
this.model.proseColumns.forEach((column) => {
data.push(ModifyColumnsPage.convertMetadata(column));
});
this.ui.table.updateProperties({
columns: [{
displayName: localize('tableFromFileImport.columnName', 'Column Name'),
valueType: azdata.DeclarativeDataType.string,
width: '150px',
isReadOnly: false
}, {
displayName: localize('tableFromFileImport.dataType', 'Data Type'),
valueType: azdata.DeclarativeDataType.editableCategory,
width: '150px',
isReadOnly: false,
categoryValues: this.categoryValues
}, {
displayName: localize('tableFromFileImport.allowNulls', 'Allow Nulls'),
valueType: azdata.DeclarativeDataType.boolean,
isReadOnly: false,
width: '100px'
}],
data: data
});
}
}

View File

@@ -0,0 +1,159 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { TableFromFileWizard } from '../tableFromFileWizard';
import { DataSourceWizardService, ColumnDefinition, ProseDiscoveryResponse } from '../../../services/contracts';
import { getErrorMessage } from '../../../utils';
import { extensionConfigSectionName, configProseParsingMaxLines, proseMaxLinesDefault } from '../../../constants';
const localize = nls.loadMessageBundle();
export class ProsePreviewPageUiElements {
public table: azdata.TableComponent;
public loading: azdata.LoadingComponent;
}
export class ProsePreviewPage extends ImportPage {
private ui: ProsePreviewPageUiElements;
private form: azdata.FormContainer;
private proseParsingComplete: Promise<ProseDiscoveryResponse>;
public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) {
super(instance, wizardPage, model, view, provider);
this.proseParsingComplete = this.doProseDiscovery();
}
public setUi(ui: ProsePreviewPageUiElements) {
this.ui = ui;
}
async start(): Promise<boolean> {
this.ui = new ProsePreviewPageUiElements();
this.ui.table = this.view.modelBuilder.table().component();
this.ui.loading = this.view.modelBuilder.loadingComponent().component();
this.form = this.view.modelBuilder.formContainer().withFormItems([
{
component: this.ui.table,
title: localize('tableFromFileImport.prosePreviewMessage', 'This operation analyzed the input file structure to generate the preview below for up to the first 50 rows.')
}
]).component();
this.ui.loading.component = this.form;
await this.view.initializeModel(this.ui.loading);
return true;
}
async onPageEnter(): Promise<void> {
if (!this.model.proseDataPreview) {
this.ui.loading.loading = true;
await this.handleProsePreview();
this.ui.loading.loading = false;
await this.populateTable(this.model.proseDataPreview, this.model.proseColumns);
}
}
async onPageLeave(clickedNext: boolean): Promise<boolean> {
if (this.ui.loading.loading) {
return false;
}
if (clickedNext) {
// Should have shown an error for these already in the loading step
return this.model.proseDataPreview !== undefined && this.model.proseColumns !== undefined;
} else {
return true;
}
}
private async doProseDiscovery(): Promise<ProseDiscoveryResponse> {
let maxLines = proseMaxLinesDefault;
let config = vscode.workspace.getConfiguration(extensionConfigSectionName);
if (config) {
let maxLinesConfig = config[configProseParsingMaxLines];
if (maxLinesConfig) {
maxLines = maxLinesConfig;
}
}
let contents = await this.model.proseParsingFile.getFileLinesAsString(maxLines);
return this.provider.sendProseDiscoveryRequest({
filePath: undefined,
tableName: this.model.table,
schemaName: this.model.newSchema ? this.model.newSchema : this.model.existingSchema,
fileType: this.model.fileType,
fileContents: contents
});
}
private async handleProsePreview() {
let result: ProseDiscoveryResponse;
try {
result = await this.proseParsingComplete;
} catch (err) {
this.instance.showErrorMessage(getErrorMessage(err));
return;
}
if (!result || !result.dataPreview) {
this.instance.showErrorMessage(localize('tableFromFileImport.noPreviewData', 'Failed to retrieve any data from the specified file.'));
return;
}
if (!result.columnInfo) {
this.instance.showErrorMessage(localize('tableFromFileImport.noProseInfo', 'Failed to generate column information for the specified file.'));
return;
}
this.model.proseDataPreview = result.dataPreview;
this.model.proseColumns = [];
result.columnInfo.forEach((column) => {
this.model.proseColumns.push({
columnName: column.name,
dataType: column.sqlType,
isNullable: column.isNullable,
collationName: undefined
});
});
let unquoteString = (value: string): string => {
return value ? value.replace(/^"(.*)"$/, '$1') : undefined;
};
this.model.columnDelimiter = unquoteString(result.columnDelimiter);
this.model.firstRow = result.firstRow;
this.model.quoteCharacter = unquoteString(result.quoteCharacter);
}
private async populateTable(tableData: string[][], columns: ColumnDefinition[]) {
let columnHeaders: string[] = columns ? columns.map(c => c.columnName) : undefined;
let rows;
const maxRows = 50;
if (tableData && tableData.length > maxRows) {
rows = tableData.slice(0, maxRows);
} else {
rows = tableData;
}
this.ui.table.updateProperties({
data: rows,
columns: columnHeaders,
height: 600,
width: 800
});
}
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { TableFromFileWizard } from '../tableFromFileWizard';
import { DataSourceWizardService } from '../../../services/contracts';
const localize = nls.loadMessageBundle();
export class SummaryPageUiElements {
public table: azdata.TableComponent;
}
export class SummaryPage extends ImportPage {
private ui: SummaryPageUiElements;
private form: azdata.FormContainer;
public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) {
super(instance, wizardPage, model, view, provider);
}
public setUi(ui: SummaryPageUiElements) {
this.ui = ui;
}
async start(): Promise<boolean> {
this.ui = new SummaryPageUiElements();
this.ui.table = this.view.modelBuilder.table().component();
this.form = this.view.modelBuilder.formContainer().withFormItems(
[{
component: this.ui.table,
title: localize('tableFromFileImport.importInformation', 'Data Virtualization information')
}]
).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<void> {
this.instance.changeDoneButtonLabel(localize('tableFromFileImport.importData', 'Virtualize Data'));
this.instance.setGenerateScriptVisibility(true);
this.populateTable();
}
async onPageLeave(clickedNext: boolean): Promise<boolean> {
this.instance.changeDoneButtonLabel(localize('tableFromFileImport.next', 'Next'));
this.instance.setGenerateScriptVisibility(false);
return true;
}
private populateTable() {
let sourceTitle = this.model.parentFile.isFolder
? localize('tableFromFileImport.summaryFolderName', 'Source Folder')
: localize('tableFromFileImport.summaryFileName', 'Source File');
this.ui.table.updateProperties({
data: [
[localize('tableFromFileImport.serverName', 'Server name'), this.model.serverConn.serverName],
[localize('tableFromFileImport.databaseName', 'Database name'), this.model.database],
[localize('tableFromFileImport.tableName', 'Table name'), this.model.table],
[localize('tableFromFileImport.tableSchema', 'Table schema'), this.model.newSchema ? this.model.newSchema : this.model.existingSchema],
[localize('tableFromFileImport.fileFormat', 'File format name'), this.model.fileFormat],
[sourceTitle, this.model.parentFile.filePath]
],
columns: ['Object type', 'Name'],
width: 600,
height: 200
});
}
}

View File

@@ -0,0 +1,329 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as azdata from 'azdata';
import * as path from 'path';
import * as url from 'url';
import * as utils from '../../utils';
import { ImportDataModel } from './api/models';
import { ImportPage } from './api/importPage';
import { FileConfigPage } from './pages/fileConfigPage';
import { ProsePreviewPage } from './pages/prosePreviewPage';
import { ModifyColumnsPage } from './pages/modifyColumnsPage';
import { SummaryPage } from './pages/summaryPage';
import { DataSourceWizardService, VirtualizeDataInput } from '../../services/contracts';
import { HdfsFileSourceNode, FileNode } from '../../hdfsProvider';
import { AppContext } from '../../appContext';
import { TreeNode } from '../../treeNodes';
import { HdfsItems, MssqlClusterItems, DataSourceType, delimitedTextFileType } from '../../constants';
const localize = nls.loadMessageBundle();
export class TableFromFileWizard {
private readonly connection: azdata.connection.ConnectionProfile;
private readonly appContext: AppContext;
private readonly provider: DataSourceWizardService;
private wizard: azdata.window.Wizard;
private model: ImportDataModel;
constructor(connection: azdata.connection.ConnectionProfile, appContext: AppContext, provider: DataSourceWizardService) {
this.connection = connection;
this.appContext = appContext;
this.provider = provider;
}
public async start(hdfsFileNode: HdfsFileSourceNode, ...args: any[]) {
if (!hdfsFileNode) {
vscode.window.showErrorMessage(localize('import.needFile', 'Please select a source file or folder before using this wizard.'));
return;
}
let noCsvError = localize('tableFromFileImport.onlyCsvSupported', 'Currently only csv files are supported for this wizard.');
let proseParsingFile: FileNode;
let parentIsFolder = false;
let isFolder = (node: TreeNode): boolean => {
let nodeType = node.getNodeInfo().nodeType;
return nodeType === HdfsItems.Folder || nodeType === MssqlClusterItems.Folder;
};
if (isFolder(hdfsFileNode)) {
let visibleFilesFilter = node => {
// Polybase excludes files that start with '.' or '_', so skip these
// files when trying to find a file to run prose discovery on
if (node.hdfsPath) {
let baseName = path.basename(node.hdfsPath);
return baseName.length > 0 && baseName[0] !== '.' && baseName[0] !== '_';
}
return false;
};
let nodeSearch = async (condition) => TreeNode.findNode(hdfsFileNode, condition, visibleFilesFilter, true);
let nonCsvFile = await nodeSearch(node => {
return !isFolder(node) && path.extname(node.hdfsPath).toLowerCase() !== '.csv';
});
if (nonCsvFile) {
vscode.window.showErrorMessage(noCsvError);
return;
}
let csvFile = await nodeSearch(node => {
return !isFolder(node) && path.extname(node.hdfsPath).toLowerCase() === '.csv';
}) as FileNode;
if (!csvFile) {
vscode.window.showErrorMessage(localize('tableFromFileImport.noCsvFileFound', 'No csv files were found in the specified folder.'));
return;
}
parentIsFolder = true;
proseParsingFile = csvFile;
} else {
if (path.extname(hdfsFileNode.hdfsPath).toLowerCase() !== '.csv') {
vscode.window.showErrorMessage(noCsvError);
return;
}
proseParsingFile = hdfsFileNode as FileNode;
}
this.model = <ImportDataModel>{
parentFile: {
isFolder: parentIsFolder,
filePath: hdfsFileNode.hdfsPath
},
proseParsingFile: proseParsingFile,
serverConn: this.connection
};
let pages: Map<number, ImportPage> = new Map<number, ImportPage>();
this.wizard = azdata.window.createWizard(localize('tableFromFileImport.wizardName', 'Virtualize Data From CSV'));
let page0 = azdata.window.createWizardPage(localize('tableFromFileImport.page0Name', 'Select the destination database for your external table'));
let page1 = azdata.window.createWizardPage(localize('tableFromFileImport.page1Name', 'Preview Data'));
let page2 = azdata.window.createWizardPage(localize('tableFromFileImport.page2Name', 'Modify Columns'));
let page3 = azdata.window.createWizardPage(localize('tableFromFileImport.page3Name', 'Summary'));
let fileConfigPage: FileConfigPage;
page0.registerContent(async (view) => {
fileConfigPage = new FileConfigPage(this, page0, this.model, view, this.provider);
pages.set(0, fileConfigPage);
await fileConfigPage.start().then(() => {
fileConfigPage.onPageEnter();
});
});
let prosePreviewPage: ProsePreviewPage;
page1.registerContent(async (view) => {
prosePreviewPage = new ProsePreviewPage(this, page1, this.model, view, this.provider);
pages.set(1, prosePreviewPage);
await prosePreviewPage.start();
});
let modifyColumnsPage: ModifyColumnsPage;
page2.registerContent(async (view) => {
modifyColumnsPage = new ModifyColumnsPage(this, page2, this.model, view, this.provider);
pages.set(2, modifyColumnsPage);
await modifyColumnsPage.start();
});
let summaryPage: SummaryPage;
page3.registerContent(async (view) => {
summaryPage = new SummaryPage(this, page3, this.model, view, this.provider);
pages.set(3, summaryPage);
await summaryPage.start();
});
this.wizard.onPageChanged(async info => {
let newPage = pages.get(info.newPage);
if (newPage) {
await newPage.onPageEnter();
}
});
this.wizard.registerNavigationValidator(async (info) => {
let lastPage = pages.get(info.lastPage);
let newPage = pages.get(info.newPage);
// Hit "next" on last page, so handle submit
let nextOnLastPage = !newPage && lastPage instanceof SummaryPage;
if (nextOnLastPage) {
let createSuccess = await this.handleVirtualizeData();
if (createSuccess) {
this.showTaskComplete();
}
return createSuccess;
}
if (lastPage) {
let clickedNext = nextOnLastPage || info.newPage > info.lastPage;
let pageValid = await lastPage.onPageLeave(clickedNext);
if (!pageValid) {
return false;
}
}
this.clearStatusMessage();
return true;
});
let cleanupSession = async () => {
try {
if (this.model.sessionId) {
await this.provider.disposeWizardSession(this.model.sessionId);
delete this.model.sessionId;
delete this.model.allDatabases;
}
} catch (error) {
this.appContext.apiWrapper.showErrorMessage(error.toString());
}
};
this.wizard.cancelButton.onClick(() => {
cleanupSession();
});
this.wizard.doneButton.onClick(() => {
cleanupSession();
});
this.wizard.generateScriptButton.hidden = true;
this.wizard.generateScriptButton.onClick(async () => {
let input = TableFromFileWizard.generateInputFromModel(this.model);
let generateScriptResponse = await this.provider.generateScript(input);
if (generateScriptResponse.isSuccess) {
let doc = await this.appContext.apiWrapper.openTextDocument({ language: 'sql', content: generateScriptResponse.script });
await this.appContext.apiWrapper.showDocument(doc);
this.showInfoMessage(
localize('tableFromFileImport.openScriptMsg',
'The script has opened in a document window. You can view it once the wizard is closed.'));
} else {
this.showErrorMessage(generateScriptResponse.errorMessages.join('\n'));
}
});
this.wizard.pages = [page0, page1, page2, page3];
this.wizard.open();
}
public setGenerateScriptVisibility(visible: boolean) {
this.wizard.generateScriptButton.hidden = !visible;
}
public registerNavigationValidator(validator: (pageChangeInfo: azdata.window.WizardPageChangeInfo) => boolean) {
this.wizard.registerNavigationValidator(validator);
}
public changeDoneButtonLabel(label: string) {
this.wizard.doneButton.label = label;
}
public showErrorMessage(errorMsg: string) {
this.showStatusMessage(errorMsg, azdata.window.MessageLevel.Error);
}
public showInfoMessage(infoMsg: string) {
this.showStatusMessage(infoMsg, azdata.window.MessageLevel.Information);
}
private async getConnectionInfo(): Promise<azdata.connection.ConnectionProfile> {
let serverConn = await azdata.connection.getCurrentConnection();
if (serverConn) {
let credentials = await azdata.connection.getCredentials(serverConn.connectionId);
if (credentials) {
Object.assign(serverConn, credentials);
}
}
return serverConn;
}
private showStatusMessage(message: string, level: azdata.window.MessageLevel) {
this.wizard.message = <azdata.window.DialogMessage>{
text: message,
level: level
};
}
public clearStatusMessage() {
this.wizard.message = undefined;
}
public static generateInputFromModel(model: ImportDataModel): VirtualizeDataInput {
if (!model) {
return undefined;
}
let result = <VirtualizeDataInput>{
sessionId: model.sessionId,
destDatabaseName: model.database,
sourceServerType: DataSourceType.SqlHDFS,
externalTableInfoList: [{
externalTableName: undefined,
columnDefinitionList: model.proseColumns,
sourceTableLocation: [model.parentFile.filePath],
fileFormat: {
formatName: model.fileFormat,
formatType: delimitedTextFileType,
fieldTerminator: model.columnDelimiter,
stringDelimiter: model.quoteCharacter,
firstRow: model.firstRow
}
}]
};
if (model.newDataSource) {
result.newDataSourceName = model.newDataSource.name;
let dataSrcUrl = url.parse(model.newDataSource.location);
result.sourceServerName = `${dataSrcUrl.host}${dataSrcUrl.pathname}`;
} else {
result.existingDataSourceName = model.existingDataSource;
}
if (model.newSchema) {
result.newSchemas = [model.newSchema];
result.externalTableInfoList[0].externalTableName = [model.newSchema, model.table];
} else {
result.externalTableInfoList[0].externalTableName = [model.existingSchema, model.table];
}
return result;
}
private async handleVirtualizeData(): Promise<boolean> {
let errorMsg: string;
try {
let dataInput = TableFromFileWizard.generateInputFromModel(this.model);
let createTableResponse = await this.provider.processVirtualizeDataInput(dataInput);
if (!createTableResponse.isSuccess) {
errorMsg = createTableResponse.errorMessages.join('\n');
}
} catch (err) {
errorMsg = utils.getErrorMessage(err);
}
if (errorMsg) {
this.showErrorMessage(errorMsg);
return false;
}
return true;
}
private showTaskComplete() {
this.wizard.registerOperation({
connection: undefined,
displayName: localize('tableFromFile.taskLabel', 'Virtualize Data'),
description: undefined,
isCancelable: false,
operation: op => {
op.updateStatus(azdata.TaskStatus.Succeeded);
}
});
}
}