diff --git a/extensions/sql-migration/src/api/sqlUtils.ts b/extensions/sql-migration/src/api/sqlUtils.ts index aef3ee1188..ddd9068b66 100644 --- a/extensions/sql-migration/src/api/sqlUtils.ts +++ b/extensions/sql-migration/src/api/sqlUtils.ts @@ -53,7 +53,7 @@ const query_databases_with_size = ` FROM sys.master_files with (nolock) GROUP BY database_id ) - SELECT name, state_desc AS state, db_size.size + SELECT name, state_desc AS state, db_size.size, collation_name FROM sys.databases with (nolock) LEFT JOIN db_size ON sys.databases.database_id = db_size.database_id WHERE sys.databases.state = 0 `; @@ -88,6 +88,13 @@ export interface TableInfo { selectedForMigration: boolean; } +export interface SourceDatabaseInfo { + databaseName: string; + databaseCollation: string; + databaseState: number; + databaseSizeInMB: string; +} + export interface TargetDatabaseInfo { serverName: string; serverCollation: string; @@ -331,6 +338,7 @@ export async function getDatabasesList(connectionProfile: azdata.connection.Conn name: getSqlString(row[0]), state: getSqlString(row[1]), sizeInMB: getSqlString(row[2]), + collation: getSqlString(row[3]) } }; }) ?? []; diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 1ceaa81c97..17653dcd58 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -410,6 +410,23 @@ export function SQL_TARGET_CONNECTION_SOURCE_NOT_MAPPED(sourceDatabaseName: stri sourceDatabaseName); } +//`Database mapping error. Source database ({0}) collation ({1}) does not match target database ({2}) collation ({3}). Please select a target database with the same collation to the source database.` +export function SQL_TARGET_SOURCE_COLLATION_NOT_SAME( + sourceDatabaseName: string, + targetDatabaseName: string, + sourceDatabaseCollation: string | undefined, + targetDatabaseCollation: string | undefined): string { + return localize( + 'sql.migration.wizard.target.source.collation.error', + "A mapping error was found between '{0}' and '{1}' databases. The source database collation '{2}' does not match the target database collation '{3}'. Please select or re-create a target database with the same collation as the source database.", + sourceDatabaseName, + targetDatabaseName, + sourceDatabaseCollation, + targetDatabaseCollation); +} + +export const SQL_MIGRATION_TROUBLESHOOTING_LINK = localize('sql.migration.wizard.troubleshooting', 'Learn more: https://aka.ms/dms-migrations-troubleshooting.'); + // Managed Instance export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Managed Instance"); export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instances found."); diff --git a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts index 0fb0c93a57..2641106bd5 100644 --- a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts +++ b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts @@ -42,6 +42,7 @@ export class RetryMigrationDialog { // SKURecommendation databaseList: [sourceDatabaseName], + databaseInfoList: [], serverAssessment: null, skuRecommendation: null, migrationTargetType: getMigrationTargetTypeEnum(migration)!, diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 51d6e5f65e..44db4f25f4 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -14,7 +14,7 @@ import { v4 as uuidv4 } from 'uuid'; import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery'; import { hashString, deepClone } from '../api/utils'; import { SKURecommendationPage } from '../wizard/skuRecommendationPage'; -import { excludeDatabases, getConnectionProfile, LoginTableInfo, TargetDatabaseInfo } from '../api/sqlUtils'; +import { excludeDatabases, getConnectionProfile, LoginTableInfo, SourceDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils'; const localize = nls.loadMessageBundle(); export enum ValidateIrState { @@ -143,6 +143,7 @@ export interface SavedInfo { closedPage: number; databaseAssessment: string[]; databaseList: string[]; + databaseInfoList: SourceDatabaseInfo[]; migrationTargetType: MigrationTargetType | null; azureAccount: azdata.Account | null; azureTenant: azurecore.Tenant | null; @@ -218,6 +219,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public mementoString: string; public _databasesForMigration: string[] = []; + public _databaseInfosForMigration: SourceDatabaseInfo[] = []; public _didUpdateDatabasesForMigration: boolean = false; public _didDatabaseMappingChange: boolean = false; public _vmDbs: string[] = []; @@ -1268,6 +1270,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { closedPage: currentPage, databaseAssessment: [], databaseList: [], + databaseInfoList: [], migrationTargetType: null, azureAccount: null, azureTenant: null, diff --git a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts index 7849d099e2..781f8326f7 100644 --- a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts +++ b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts @@ -11,7 +11,7 @@ import * as constants from '../constants/strings'; import { debounce } from '../api/utils'; import * as styles from '../constants/styles'; import { IconPathHelper } from '../constants/iconPathHelper'; -import { getDatabasesList, excludeDatabases } from '../api/sqlUtils'; +import { getDatabasesList, excludeDatabases, SourceDatabaseInfo } from '../api/sqlUtils'; export class DatabaseSelectorPage extends MigrationWizardPage { private _view!: azdata.ModelView; @@ -236,10 +236,12 @@ export class DatabaseSelectorPage extends MigrationWizardPage { databaseList.sort((a, b) => a.options.name.localeCompare(b.options.name)); this._dbNames = []; + stateMachine._databaseInfosForMigration = []; this._databaseTableValues = databaseList.map(database => { const databaseName = database.options.name; this._dbNames.push(databaseName); + stateMachine._databaseInfosForMigration.push(this.getSourceDatabaseInfo(database)); return [ selectedDatabases?.indexOf(databaseName) > -1, { @@ -271,4 +273,13 @@ export class DatabaseSelectorPage extends MigrationWizardPage { }); this.migrationStateModel._databasesForAssessment = selectedDatabases; } + + private getSourceDatabaseInfo(database: azdata.DatabaseInfo): SourceDatabaseInfo { + return { + databaseName: database.options.name, + databaseCollation: database.options.collation, + databaseSizeInMB: database.options.sizeInMB, + databaseState: database.options.state + }; + } } diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index d2c1b51521..b66b69c347 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -1039,17 +1039,23 @@ export class TargetSelectionPage extends MigrationWizardPage { private _getSourceTargetMappingErrors(): string[] { // Validate source/target database mappings: - const errors: string[] = []; + var errors: string[] = []; + const collationErrors: string[] = []; const targetDatabaseKeys = new Map(); const migrationDatabaseCount = this._azureResourceTable.dataValues?.length ?? 0; this.migrationStateModel._targetDatabaseNames = []; + const databaseInfosForMigration = new Map(this.migrationStateModel._databaseInfosForMigration.map(o => [o.databaseName, o])); + if (migrationDatabaseCount === 0) { errors.push(constants.SQL_TARGET_MAPPING_ERROR_MISSING_TARGET); } else { for (let i = 0; i < this.migrationStateModel._databasesForMigration.length; i++) { const sourceDatabaseName = this.migrationStateModel._databasesForMigration[i]; + const sourceDatabaseInfo = databaseInfosForMigration.get(sourceDatabaseName); const targetDatabaseInfo = this.migrationStateModel._sourceTargetMapping.get(sourceDatabaseName); const targetDatabaseName = targetDatabaseInfo?.databaseName; + const sourceDatabaseCollation = sourceDatabaseInfo?.databaseCollation; + const targetDatabaseCollation = targetDatabaseInfo?.databaseCollation; if (targetDatabaseName && targetDatabaseName.length > 0) { if (!targetDatabaseKeys.has(targetDatabaseName)) { targetDatabaseKeys.set(targetDatabaseName, sourceDatabaseName); @@ -1063,12 +1069,28 @@ export class TargetSelectionPage extends MigrationWizardPage { sourceDatabaseName, mappedSourceDatabaseName)); } + + // Collation validation + if (!this._isCollationSame(sourceDatabaseCollation, targetDatabaseCollation)) { + collationErrors.push( + constants.SQL_TARGET_SOURCE_COLLATION_NOT_SAME( + sourceDatabaseName, + targetDatabaseName, + sourceDatabaseCollation, + targetDatabaseCollation)); + } } else { // source/target has mapping errors.push(constants.SQL_TARGET_CONNECTION_SOURCE_NOT_MAPPED(sourceDatabaseName)); } } } + + if (collationErrors.length > 0) { + collationErrors.push(constants.SQL_MIGRATION_TROUBLESHOOTING_LINK); + errors = errors.concat(collationErrors); + } + return errors; } @@ -1083,4 +1105,12 @@ export class TargetSelectionPage extends MigrationWizardPage { await this._targetUserNameInputBox.validate(); await this._azureResourceTable.validate(); } + + private _isCollationSame(sourceDatabaseCollation: string | undefined, targetDatabaseCollation: string | undefined): boolean { + return sourceDatabaseCollation !== undefined && + sourceDatabaseCollation.length > 0 && + targetDatabaseCollation !== undefined && + targetDatabaseCollation.length > 0 && + sourceDatabaseCollation.toLocaleLowerCase() === targetDatabaseCollation.toLocaleLowerCase(); + } }