Example #1
0
        /// <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)}");
            }
        }