/// <inheritdoc /> public void Run( string workspace, string targetVersion = null, bool?isAutoCreateDatabase = false, List <KeyValuePair <string, string> > tokens = null, bool?isVerifyOnly = false, string bulkSeparator = null, string metaSchemaName = null, string metaTableName = null, int?commandTimeout = null, int?bulkBatchSize = null, string appliedByTool = null, string appliedByToolVersion = null, string environment = null, bool?isContinueAfterFailure = null, string transactionMode = null, bool isRequiredClearedDraft = false ) { //print run configuration information _traceService.Info($"Run configuration: {Environment.NewLine}{_configurationService.PrintAsJson()}"); //check the workspace structure if required directories are present _workspaceService.Validate(workspace); //when uncomitted run is not supported, fail migration, throw exceptions and return error exit code if (isVerifyOnly.HasValue && isVerifyOnly == true && !_dataService.IsTransactionalDdlSupported) { throw new NotSupportedException("Yuniql.Verify is not supported in the target platform. " + "The feature requires support for atomic DDL operations. " + "An atomic DDL operations ensures creation of tables, views and other objects and data are rolledback in case of error. " + "For more information see https://yuniql.io/docs/."); } //when no target version specified, we use the latest local version available if (string.IsNullOrEmpty(targetVersion)) { targetVersion = _workspaceService.GetLatestVersion(workspace); _traceService.Info($"No explicit target version requested. We'll use latest available locally {targetVersion} on {workspace}."); } var connectionInfo = _dataService.GetConnectionInfo(); var targetDatabaseName = connectionInfo.Database; var targetDatabaseServer = connectionInfo.DataSource; //we try to auto-create the database, we need this to be outside of the transaction scope //in an event of failure, users have to manually drop the auto-created database! //we only check if the db exists when --auto-create-db is true if (isAutoCreateDatabase.HasValue && isAutoCreateDatabase == true) { var targetDatabaseExists = _metadataService.IsDatabaseExists(); if (!targetDatabaseExists) { _traceService.Info($"Target database does not exist. Creating database {targetDatabaseName} on {targetDatabaseServer}."); _metadataService.CreateDatabase(); _traceService.Info($"Created database {targetDatabaseName} on {targetDatabaseServer}."); } } //check if database has been pre-configured to support migration and setup when its not var targetDatabaseConfigured = _metadataService.IsDatabaseConfigured(metaSchemaName, metaTableName); if (!targetDatabaseConfigured) { //create custom schema when user supplied and only if platform supports it if (_dataService.IsSchemaSupported && null != metaSchemaName && !_dataService.SchemaName.Equals(metaSchemaName)) { _traceService.Info($"Target schema does not exist. Creating schema {metaSchemaName} on {targetDatabaseName} on {targetDatabaseServer}."); _metadataService.CreateSchema(metaSchemaName); _traceService.Info($"Created schema {metaSchemaName} on {targetDatabaseName} on {targetDatabaseServer}."); } //create empty versions tracking table _traceService.Info($"Target database {targetDatabaseName} on {targetDatabaseServer} not yet configured for migration."); _metadataService.ConfigureDatabase(metaSchemaName, metaTableName); _traceService.Info($"Configured database migration support for {targetDatabaseName} on {targetDatabaseServer}."); } //we may have to upgrade the version tracking table for yuniql to work in this release var targetDatabaseUpdated = _metadataService.UpdateDatabaseConfiguration(metaSchemaName, metaTableName); var databaseUpgradedMessage = targetDatabaseUpdated ? $"The schema version tracking table has been upgraded for {targetDatabaseName} on {targetDatabaseServer}." : $"The schema version tracking table is up to date for {targetDatabaseName} on {targetDatabaseServer}."; _traceService.Info(databaseUpgradedMessage); TransactionContext transactionContext = null; //check for presence of failed no-transactional versions from previous runs var allVersions = _metadataService.GetAllVersions(metaSchemaName, metaTableName); var failedVersion = allVersions.Where(x => x.Status == Status.Failed).FirstOrDefault(); if (failedVersion != null) { //check if user had issue resolving option such as continue on failure if (isContinueAfterFailure == null) { //program should exit with non zero exit code var message = @$ "Previous migration of " "{failedVersion.Version}" " version was not running in transaction and has failed when executing of script " "{failedVersion.FailedScriptPath}" " with following error: {failedVersion.FailedScriptError}. {MESSAGES.ManualResolvingAfterFailureMessage}"; _traceService.Error(message); throw new InvalidOperationException(message); } _traceService.Warn($@"The non-transactional failure resolving option ""{isContinueAfterFailure}"" was used. Version scripts already applied by previous migration run will be skipped."); transactionContext = new TransactionContext(failedVersion, isContinueAfterFailure.Value); } else { //check if the non-txn option is passed even if there was no previous failed runs if (isContinueAfterFailure != null && isContinueAfterFailure.Value == true) { //program should exit with non zero exit code _traceService.Warn(@$ "The transaction handling parameter --continue-after-failure received but no previous failed migrations recorded."); } } var appliedVersions = _metadataService.GetAllAppliedVersions(metaSchemaName, metaTableName) .Select(dv => dv.Version) .OrderBy(v => v) .ToList(); //check if target database already runs the latest version and skips work if it already is var targeDatabaseLatest = IsTargetDatabaseLatest(targetVersion, metaSchemaName, metaTableName); if (!targeDatabaseLatest) { //enclose all executions in a single transaction, in the event of failure we roll back everything using (var connection = _dataService.CreateConnection()) { connection.Open(); using (var transaction = (!string.IsNullOrEmpty(transactionMode) && transactionMode.Equals(TRANSACTION_MODE.SESSION)) ? connection.BeginTransaction() : null) { try { if (null != transaction) { _traceService.Info("Transaction created for current session. This migration run will be executed in a shared connection and transaction context."); } //run all migrations present in all directories RunAllInternal(connection, transaction, isRequiredClearedDraft); //when true, the execution is an uncommitted transaction //and only for purpose of testing if all can go well when it run to the target environment if (isVerifyOnly.HasValue && isVerifyOnly == true) { transaction?.Rollback(); } else { if (transaction?.Connection == null) { _traceService.Warn("Transaction has been committed before the end of the session. " + "Please verify if all schema migrations has been successfully applied. " + "If there was fault in the process, the database changes during migration process will be rolled back."); } else { transaction?.Commit(); } } } catch (Exception) { transaction?.Rollback(); throw; } } } } else { //when target database already runs the latest version, we at least execute scripts in draft folder //enclose all executions in a single transaction using (var connection = _dataService.CreateConnection()) { connection.Open(); using (var transaction = (!string.IsNullOrEmpty(transactionMode) && transactionMode.Equals(TRANSACTION_MODE.SESSION)) ? connection.BeginTransaction() : null) { try { //run all scripts present in the _pre, _draft and _post directories if (null != transaction) { _traceService.Info("Transaction created for current session. This migration run will be executed in a shared connection and transaction context."); } RunPreDraftPostInternal(connection, transaction, isRequiredClearedDraft); //when true, the execution is an uncommitted transaction //and only for purpose of testing if all can go well when it run to the target environment if (isVerifyOnly.HasValue && isVerifyOnly == true) { transaction?.Rollback(); } else { transaction?.Commit(); } } catch (Exception) { transaction?.Rollback(); throw; } } } _traceService.Info($"Target database runs the latest version already. Scripts in {RESERVED_DIRECTORY_NAME.PRE}, {RESERVED_DIRECTORY_NAME.DRAFT} and {RESERVED_DIRECTORY_NAME.POST} are executed."); } //local method void RunAllInternal(IDbConnection connection, IDbTransaction transaction, bool isRequiredClearedDraft) { //check if database has been pre-configured and execute init scripts if (!targetDatabaseConfigured) { //runs all scripts in the _init folder RunNonVersionDirectories(connection, transaction, workspace, Path.Combine(workspace, RESERVED_DIRECTORY_NAME.INIT), tokens: tokens, bulkBatchSize: bulkBatchSize, bulkSeparator: bulkSeparator, commandTimeout: commandTimeout, environment: environment, transactionMode: transactionMode); _traceService.Info($"Executed script files on {Path.Combine(workspace, RESERVED_DIRECTORY_NAME.INIT)}"); } //checks if target database already runs the latest version and skips work if it already is //runs all scripts in the _pre folder and subfolders RunNonVersionDirectories(connection, transaction, workspace, Path.Combine(workspace, RESERVED_DIRECTORY_NAME.PRE), tokens: tokens, bulkBatchSize: bulkBatchSize, bulkSeparator: bulkSeparator, commandTimeout: commandTimeout, environment: environment, transactionMode: transactionMode); _traceService.Info($"Executed script files on {Path.Combine(workspace, RESERVED_DIRECTORY_NAME.PRE)}"); //runs all scripts int the vxx.xx folders and subfolders RunVersionDirectories(connection, transaction, appliedVersions, workspace, targetVersion, transactionContext, tokens, bulkSeparator: bulkSeparator, metaSchemaName: metaSchemaName, metaTableName: metaTableName, commandTimeout: commandTimeout, bulkBatchSize: bulkBatchSize, appliedByTool: appliedByTool, appliedByToolVersion: appliedByToolVersion, environment: environment, transactionMode: transactionMode); //runs all scripts in the _draft folder and subfolders RunNonVersionDirectories(connection, transaction, workspace, Path.Combine(workspace, RESERVED_DIRECTORY_NAME.DRAFT), tokens: tokens, bulkBatchSize: bulkBatchSize, bulkSeparator: bulkSeparator, commandTimeout: commandTimeout, environment: environment, transactionMode: transactionMode, isRequiredClearedDraft: isRequiredClearedDraft); _traceService.Info($"Executed script files on {Path.Combine(workspace, RESERVED_DIRECTORY_NAME.DRAFT)}"); //runs all scripts in the _post folder and subfolders RunNonVersionDirectories(connection, transaction, workspace, Path.Combine(workspace, RESERVED_DIRECTORY_NAME.POST), tokens: tokens, bulkBatchSize: bulkBatchSize, bulkSeparator: bulkSeparator, commandTimeout: commandTimeout, environment: environment, transactionMode: transactionMode); _traceService.Info($"Executed script files on {Path.Combine(workspace, RESERVED_DIRECTORY_NAME.POST)}"); } //local method void RunPreDraftPostInternal(IDbConnection connection, IDbTransaction transaction, bool requiredClearedDraft) { //runs all scripts in the _pre folder and subfolders RunNonVersionDirectories(connection, transaction, workspace, Path.Combine(workspace, RESERVED_DIRECTORY_NAME.PRE), tokens: tokens, bulkBatchSize: bulkBatchSize, bulkSeparator: bulkSeparator, commandTimeout: commandTimeout, environment: environment, transactionMode: transactionMode); _traceService.Info($"Executed script files on {Path.Combine(workspace, RESERVED_DIRECTORY_NAME.PRE)}"); //runs all scripts in the _draft folder and subfolders RunNonVersionDirectories(connection, transaction, workspace, Path.Combine(workspace, RESERVED_DIRECTORY_NAME.DRAFT), tokens: tokens, bulkBatchSize: bulkBatchSize, bulkSeparator: bulkSeparator, commandTimeout: commandTimeout, environment: environment, transactionMode: transactionMode, isRequiredClearedDraft: requiredClearedDraft); _traceService.Info($"Executed script files on {Path.Combine(workspace, RESERVED_DIRECTORY_NAME.DRAFT)}"); //runs all scripts in the _post folder and subfolders RunNonVersionDirectories(connection, transaction, workspace, Path.Combine(workspace, RESERVED_DIRECTORY_NAME.POST), tokens: tokens, bulkBatchSize: bulkBatchSize, bulkSeparator: bulkSeparator, commandTimeout: commandTimeout, environment: environment, transactionMode: transactionMode); _traceService.Info($"Executed script files on {Path.Combine(workspace, RESERVED_DIRECTORY_NAME.POST)}"); } }