diff --git a/extensions/sql-migration/README.md b/extensions/sql-migration/README.md index d84e78ab22..3a121d9121 100644 --- a/extensions/sql-migration/README.md +++ b/extensions/sql-migration/README.md @@ -1,8 +1,8 @@ # Azure SQL Migration -The Azure SQL Migration extension in Azure Data Studio brings together a simplified assessment, recommendation, and migration experience that delivers the following capabilities: +The [Azure SQL Migration extension for Azure Data Studio](https://docs.microsoft.com/sql/azure-data-studio/extensions/azure-sql-migration-extension) brings together a simplified assessment, recommendation, and migration experience that delivers the following capabilities: - A responsive user interface that provides you with an end-to-end migration experience that starts with a migration readiness assessment, SKU recommendation (based on performance data), and finalizes with the actual migration to Azure SQL. - An enhanced assessment mechanism that can evaluate SQL Server instances, identifying databases that are ready for migration to the different Azure SQL targets. -- A SKU recommendation engine that collects performance data from the source SQL Server instance on-premises, generating right-sized SKU recommendations based on your Azure SQL target. +- A SKU recommendation engine (Preview) that collects performance data from the source SQL Server instance on-premises, generating right-sized SKU recommendations based on your Azure SQL target. - A reliable Azure service powered by Azure Database Migration Service that orchestrates data movement activities to deliver a seamless migration experience. - The ability to run online (for migrations requiring minimal downtime) or offline (for migrations where downtime persists through the migration) migration modes to suit your business requirements. - The flexibility to create and configure a self-hosted integration runtime to provide your own compute for accessing the source SQL Server and backups in your on-premises environment. @@ -18,12 +18,11 @@ Open the Azure Data Studio marketplace, select and install the latest version of - A source SQL Server database(s) running on-premises, or on SQL Server on Azure Virtual Machine or any virtual machine running in the cloud (private, public). - An Azure SQL Managed Instance, SQL Server on Azure Virtual Machine, or Azure SQL Database to migrate your database(s) to. > Azure SQL Database offline migrations are still in public preview. -- Your database backup location details, either a network file share or an Azure Blob Storage container. - +- Your database backup location details, either a network file share or an Azure Blob Storage container (not required for Azure SQL Database targets). ## Getting started Refer to [Migrate databases using the Azure SQL Migration extension for Azure Data Studio](https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio) for detailed documentation on capabilities and concepts. -## Assessment and SKU recommendation +## Assessment and SKU recommendation (Preview) The assessment and SKU recommendation feature evaluates the source SQL Server database(s) for migration readiness. It also generates right-sized SKU recommendations for your Azure target to meet the performance requirements of the source SQL Server database(s) with minimal cost. [Learn more.](https://aka.ms/ads-sql-sku-recommend) @@ -32,13 +31,12 @@ It also generates right-sized SKU recommendations for your Azure target to meet The Azure SQL Migration extension supports database migrations to the following Azure SQL targets. - [Azure SQL Managed Instance](https://docs.microsoft.com/azure/azure-sql/managed-instance/sql-managed-instance-paas-overview) - [SQL Server on Azure Virtual Machines](https://docs.microsoft.com/azure/azure-sql/virtual-machines/windows/sql-server-on-azure-vm-iaas-what-is-overview) -- [Azure SQL Database (Public preview)](https://docs.microsoft.com/azure/azure-sql/database/sql-database-paas-overview?view=azuresql) +- [Azure SQL Database (Preview)](https://docs.microsoft.com/azure/azure-sql/database/sql-database-paas-overview?view=azuresql) ## Migration modes The following migration modes are supported for the corresponding Azure SQL targets. - Online - The source SQL Server database is available for read and write activity, while the database backups (full + log) are continuously restored on the Azure SQL target. Application downtime is limited to the duration of the cutover at the end of migration. - > Online migrations to Azure SQL Database targets are not yet supported. - Offline - The source SQL Server database cannot be used for write activity, while the database backup files are restored on the Azure SQL target. Application downtime persists from the start until the completion of the migration process. > Azure SQL Database offline migrations are still in public preview. diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index f36c5a651a..0cbb05f3cf 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -94,6 +94,9 @@ export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure SQL Managed Instance"); export const SKU_RECOMMENDATION_SQLDB_CARD_TEXT = localize('sql.migration.sku.sqldb.card.title', "Azure SQL Database (PREVIEW)"); export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "SQL Server on Azure Virtual Machine"); +export const SKU_RECOMMENDATION_MI_TARGET_TEXT = localize('sql.migration.sku.mi.target.title', "Azure SQL Managed Instance"); +export const SKU_RECOMMENDATION_SQLDB_TARGET_TEXT = localize('sql.migration.sku.sqldb.target.title', "Azure SQL Database"); +export const SKU_RECOMMENDATION_VM_TARGET_TEXT = localize('sql.migration.sku.vm.target.title', "SQL Server on Azure Virtual Machine"); export const SELECT_AZURE_MI = localize('sql.migration.select.azure.mi', "Select your target Azure subscription and your target Azure SQL Managed Instance."); export const SELECT_AZURE_VM = localize('sql.migration.select.azure.vm', "Select your target Azure Subscription and your target SQL Server on Azure Virtual Machine for your target."); export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI = localize('sql.migration.sku.recommendation.view.assessment.mi', "To migrate to Azure SQL Managed Instance, view assessment results and select one or more databases."); @@ -130,7 +133,7 @@ export function SAVE_RECOMMENDATION_REPORT_SUCCESS(filePath: string): string { } // SKU -export const AZURE_RECOMMENDATION = localize('sql.migration.sku.recommendation', "Azure recommendation"); +export const AZURE_RECOMMENDATION = localize('sql.migration.sku.recommendation', "Azure recommendation (PREVIEW)"); export function RECOMMENDATIONS_TITLE(targetType: string): string { return localize('sql.migration.sku.recommendations.title', "{0} Recommendations", targetType); } @@ -519,6 +522,9 @@ export function TABLE_SELECTION_COUNT(selectedCount: number, rowCount: number): export function TABLE_SELECTED_COUNT(selectedCount: number, rowCount: number): string { return localize('sql.migration.table.selected.count', "{0} of {1} tables selected", selectedCount, rowCount); } +export function MISSING_TARGET_TABLES_COUNT(tables: number): string { + return localize('sql.migration.table.missing.count', "Missing target tables excluded from list: {0}", tables); +} export const DATABASE_MISSING_TABLES = localize('sql.migratino.database.missing.tables', "0 tables found."); export const DATABASE_LOADING_TABLES = localize('sql.migratino.database.loading.tables', "Loading tables list..."); export const TABLE_SELECTION_FILTER = localize('sql.migratino.table.selection.filter', "Filter tables"); @@ -659,7 +665,7 @@ export const DASHBOARD_REFRESH_MIGRATIONS_TITLE = localize('sql.migration.refres export const DASHBOARD_REFRESH_MIGRATIONS_LABEL = localize('sql.migration.refresh.migrations.error.label', "An error occurred while refreshing the migrations list. Please check your linked Azure connection and click refresh to try again."); export const DASHBOARD_TITLE = localize('sql.migration.dashboard.title', "Azure SQL Migration"); -export const DASHBOARD_DESCRIPTION = localize('sql.migration.dashboard.description', "Determine the migration readiness of your SQL Server instances, identify a recommended Azure SQL target, and complete the migration of your SQL Server instance to Azure SQL Managed Instance, SQL Server on Azure Virtual Machines or Azure SQL Database (PREVIEW)."); +export const DASHBOARD_DESCRIPTION = localize('sql.migration.dashboard.description', "Determine the migration readiness of your SQL Server instances, identify a recommended Azure SQL target, and complete the migration of your SQL Server instance to Azure SQL Managed Instance, SQL Server on Azure Virtual Machines or Azure SQL Database."); export const DASHBOARD_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.migrate.task.button', "Migrate to Azure SQL"); export const DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION = localize('sql.migration.dashboard.migrate.task.button.description', "Migrate a SQL Server instance to Azure SQL."); export const DATABASE_MIGRATION_STATUS = localize('sql.migration.database.migration.status', "Database migration status"); @@ -667,7 +673,7 @@ export const HELP_TITLE = localize('sql.migration.dashboard.help.title', "Help a export const PRE_REQ_TITLE = localize('sql.migration.pre.req.title', "Things you need before starting your Azure SQL migration:"); export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "An Azure account (not required for assessment or SKU recommendation functionality)"); export const PRE_REQ_2 = localize('sql.migration.pre.req.2', "A source SQL Server database(s) running on on-premises, or on SQL Server on Azure Virtual Machine or any virtual machine running in the cloud (private, public)."); -export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "An Azure SQL Managed Instance, SQL Server on Azure Virtual Machine, or Azure SQL Database (PREVIEW) to migrate your database(s) to."); +export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "An Azure SQL Managed Instance, SQL Server on Azure Virtual Machine, or Azure SQL Database to migrate your database(s) to."); export const PRE_REQ_4 = localize('sql.migration.pre.req.4', "Your database backup location details, either a network file share or an Azure Blob Storage container (not required for Azure SQL Database targets)."); export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Database migrations in progress"); export const MIGRATION_FAILED = localize('sql.migration.failed', "Database migrations failed"); diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts index 149004692e..415770bf05 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts @@ -35,13 +35,13 @@ export class SkuRecommendationResultsDialog { constructor(public model: MigrationStateModel, public _targetType: MigrationTargetType) { switch (this._targetType) { case MigrationTargetType.SQLMI: - this.targetName = constants.SKU_RECOMMENDATION_MI_CARD_TEXT; + this.targetName = constants.SKU_RECOMMENDATION_MI_TARGET_TEXT; break; case MigrationTargetType.SQLVM: - this.targetName = constants.SKU_RECOMMENDATION_VM_CARD_TEXT; + this.targetName = constants.SKU_RECOMMENDATION_VM_TARGET_TEXT; break; case MigrationTargetType.SQLDB: - this.targetName = constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT; + this.targetName = constants.SKU_RECOMMENDATION_SQLDB_TARGET_TEXT; break; } diff --git a/extensions/sql-migration/src/dialog/tableMigrationSelection/tableMigrationSelectionDialog.ts b/extensions/sql-migration/src/dialog/tableMigrationSelection/tableMigrationSelectionDialog.ts index 1decf27460..2851345164 100644 --- a/extensions/sql-migration/src/dialog/tableMigrationSelection/tableMigrationSelectionDialog.ts +++ b/extensions/sql-migration/src/dialog/tableMigrationSelection/tableMigrationSelectionDialog.ts @@ -9,12 +9,14 @@ import * as constants from '../../constants/strings'; import { AzureSqlDatabaseServer } from '../../api/azure'; import { collectSourceDatabaseTableInfo, collectTargetDatabaseTableInfo, TableInfo } from '../../api/sqlUtils'; import { MigrationStateModel } from '../../models/stateMachine'; +import { updateControlDisplay } from '../../api/utils'; const DialogName = 'TableMigrationSelection'; export class TableMigrationSelectionDialog { private _dialog: azdata.window.Dialog | undefined; private _headingText!: azdata.TextComponent; + private _missingTablesText!: azdata.TextComponent; private _filterInputBox!: azdata.InputBoxComponent; private _tableSelectionTable!: azdata.TableComponent; private _tableLoader!: azdata.LoadingComponent; @@ -25,6 +27,7 @@ export class TableMigrationSelectionDialog { private _tableSelectionMap!: Map; private _targetTableMap!: Map; private _onSaveCallback: () => Promise; + private _missingTableCount: number = 0; constructor( model: MigrationStateModel, @@ -92,30 +95,32 @@ export class TableMigrationSelectionDialog { const filterText = this._filterInputBox.value ?? ''; const selectedItems: number[] = []; let tableRow = 0; + this._missingTableCount = 0; this._tableSelectionMap.forEach(sourceTable => { if (filterText?.length === 0 || sourceTable.tableName.indexOf(filterText) > -1) { - let tableStatus = constants.TARGET_TABLE_MISSING; const targetTable = this._targetTableMap.get(sourceTable.tableName); if (targetTable) { const targetTableRowCount = targetTable?.rowCount ?? 0; - tableStatus = targetTableRowCount > 0 + const tableStatus = targetTableRowCount > 0 ? constants.TARGET_TABLE_NOT_EMPTY : '--'; - } - data.push([ - sourceTable.selectedForMigration, - sourceTable.tableName, - tableStatus]); - if (sourceTable.selectedForMigration && targetTable) { - selectedItems.push(tableRow); + data.push([ + sourceTable.selectedForMigration, + sourceTable.tableName, + tableStatus]); + + if (sourceTable.selectedForMigration) { + selectedItems.push(tableRow); + } } + this._missingTableCount += targetTable ? 0 : 1; tableRow++; } }); await this._tableSelectionTable.updateProperty('data', data); this._tableSelectionTable.selectedRows = selectedItems; - this._updateRowSelection(); + await this._updateRowSelection(); } private async _initializeDialog(dialog: azdata.window.Dialog): Promise { @@ -135,6 +140,10 @@ export class TableMigrationSelectionDialog { .withProps({ value: constants.DATABASE_LOADING_TABLES }) .component(); + this._missingTablesText = view.modelBuilder.text() + .withProps({ display: 'none' }) + .component(); + this._tableSelectionTable = await this._createSelectionTable(view); this._tableLoader = view.modelBuilder.loadingComponent() .withItem(this._tableSelectionTable) @@ -147,6 +156,7 @@ export class TableMigrationSelectionDialog { .withItems([ this._filterInputBox, this._headingText, + this._missingTablesText, this._tableLoader], { flex: '0 0 auto' }) .withProps({ CSSStyles: { 'margin': '0 0 0 15px' } }) @@ -228,62 +238,36 @@ export class TableMigrationSelectionDialog { .withValidation(() => true) .component(); - let updating: boolean = false; this._disposables.push( - table.onRowSelected(e => { - if (updating) { - return; - } - updating = true; - - // collect table list selected for migration - const selectedRows = this._tableSelectionTable.selectedRows ?? []; - const keepSelectedRows: number[] = []; - // determine if selected rows have a matching target and can be selected - selectedRows.forEach(rowIndex => { - // get selected source table name - const sourceTableName = this._tableSelectionTable.data[rowIndex][1] as string; - // get source table info - const sourceTableInfo = this._tableSelectionMap.get(sourceTableName); - if (sourceTableInfo) { - // see if source table exists on target database - const targetTableInfo = this._targetTableMap.get(sourceTableName); - // keep source table selected - sourceTableInfo.selectedForMigration = targetTableInfo !== undefined; - // update table selection map with new selectedForMigration value - this._tableSelectionMap.set(sourceTableName, sourceTableInfo); - // keep row selected - if (sourceTableInfo.selectedForMigration) { - keepSelectedRows.push(rowIndex); + table.onRowSelected( + async e => { + // collect table list selected for migration + const selectedRows = this._tableSelectionTable.selectedRows ?? []; + selectedRows.forEach(rowIndex => { + // get selected source table name + const rowData = this._tableSelectionTable.data[rowIndex]; + const sourceTableName = rowData.length > 1 + ? rowData[1] as string + : ''; + // get source table info + const sourceTableInfo = this._tableSelectionMap.get(sourceTableName); + if (sourceTableInfo) { + // see if source table exists on target database + const targetTableInfo = this._targetTableMap.get(sourceTableName); + // keep source table selected + sourceTableInfo.selectedForMigration = targetTableInfo !== undefined; + // update table selection map with new selectedForMigration value + this._tableSelectionMap.set(sourceTableName, sourceTableInfo); } - } - }); + }); - // if the selected rows are different, update the selectedRows property - if (!this._areEqual(this._tableSelectionTable.selectedRows ?? [], keepSelectedRows)) { - this._tableSelectionTable.selectedRows = keepSelectedRows; - } - - this._updateRowSelection(); - updating = false; - })); + await this._updateRowSelection(); + })); return table; } - private _areEqual(source: number[], target: number[]): boolean { - if (source.length === target.length) { - for (let i = 0; i < source.length; i++) { - if (source[i] !== target[i]) { - return false; - } - } - return true; - } - return false; - } - - private _updateRowSelection(): void { + private async _updateRowSelection(): Promise { this._headingText.value = this._tableSelectionTable.data.length > 0 ? constants.TABLE_SELECTED_COUNT( this._tableSelectionTable.selectedRows?.length ?? 0, @@ -291,6 +275,9 @@ export class TableMigrationSelectionDialog { : this._tableLoader.loading ? constants.DATABASE_LOADING_TABLES : constants.DATABASE_MISSING_TABLES; + + this._missingTablesText.value = constants.MISSING_TARGET_TABLES_COUNT(this._missingTableCount); + await updateControlDisplay(this._missingTablesText, this._missingTableCount > 0); } private async _save(): Promise { @@ -300,7 +287,10 @@ export class TableMigrationSelectionDialog { const selectedRows = this._tableSelectionTable.selectedRows ?? []; const selectedTables = new Map(); selectedRows.forEach(rowIndex => { - const tableName = this._tableSelectionTable.data[rowIndex][1] as string; + const tableRow = this._tableSelectionTable.data[rowIndex]; + const tableName = tableRow.length > 1 + ? this._tableSelectionTable.data[rowIndex][1] as string + : ''; const tableInfo = this._tableSelectionMap.get(tableName); if (tableInfo) { selectedTables.set(tableName, tableInfo); diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index d56006df2b..4ec575a2b7 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -95,6 +95,10 @@ export class TargetSelectionPage extends MigrationWizardPage { this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE; this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE; this._updateConnectionButtonState(); + if (this.migrationStateModel._didUpdateDatabasesForMigration) { + await this._resetTargetMapping(); + this.migrationStateModel._didUpdateDatabasesForMigration = false; + } break; } @@ -940,6 +944,7 @@ export class TargetSelectionPage extends MigrationWizardPage { this.migrationStateModel._sourceTargetMapping.set( sourceDatabase, targetDatabaseInfo); + this.migrationStateModel.refreshDatabaseBackupPage = true; this.migrationStateModel._didDatabaseMappingChange = true; }));