using System.Data; using System.Data.Common; using System.Text; using LinqToDB; using LinqToDB.Data; using LinqToDB.DataProvider; using LinqToDB.DataProvider.MySql; using LinqToDB.SqlQuery; using MySqlConnector; using Nop.Core; using Nop.Data.Mapping; namespace Nop.Data.DataProviders; public partial class MySqlNopDataProvider : BaseDataProvider, INopDataProvider { #region Fields //it's quite fast hash (to cheaply distinguish between objects) protected const string HASH_ALGORITHM = "SHA1"; #endregion #region Utilities /// /// Creates the database connection /// protected override DataConnection CreateDataConnection() { var dataContext = CreateDataConnection(LinqToDbDataProvider); dataContext.MappingSchema.SetDataType(typeof(Guid), new SqlDataType(DataType.NChar, typeof(Guid), 36)); dataContext.MappingSchema.SetConvertExpression(strGuid => new Guid(strGuid)); return dataContext; } /// /// Gets the connection string builder /// /// The connection string builder protected static MySqlConnectionStringBuilder GetConnectionStringBuilder() { return new MySqlConnectionStringBuilder(GetCurrentConnectionString()); } /// /// Gets a connection to the database for a current data provider /// /// Connection string /// Connection to a database protected override DbConnection GetInternalDbConnection(string connectionString) { ArgumentException.ThrowIfNullOrEmpty(connectionString); return new MySqlConnection(connectionString); } #endregion #region Methods /// /// Creates the database by using the loaded connection string /// /// /// public void CreateDatabase(string collation, int triesToConnect = 10) { if (DatabaseExists()) return; var builder = GetConnectionStringBuilder(); //gets database name var databaseName = builder.Database; //now create connection string to 'master' database. It always exists. builder.Database = null; using (var connection = GetInternalDbConnection(builder.ConnectionString)) { var query = $"CREATE DATABASE IF NOT EXISTS {databaseName}"; if (!string.IsNullOrWhiteSpace(collation)) query = $"{query} COLLATE {collation}"; var command = connection.CreateCommand(); command.CommandText = query; command.Connection.Open(); command.ExecuteNonQuery(); } //try connect if (triesToConnect <= 0) return; //sometimes on slow servers (hosting) there could be situations when database requires some time to be created. //but we have already started creation of tables and sample data. //as a result there is an exception thrown and the installation process cannot continue. //that's why we are in a cycle of "triesToConnect" times trying to connect to a database with a delay of one second. for (var i = 0; i <= triesToConnect; i++) { if (i == triesToConnect) throw new Exception("Unable to connect to the new database. Please try one more time"); if (!DatabaseExists()) Thread.Sleep(1000); else break; } } /// /// Checks if the specified database exists, returns true if database exists /// /// /// A task that represents the asynchronous operation /// The task result contains the returns true if the database exists. /// public async Task DatabaseExistsAsync() { try { await using var connection = GetInternalDbConnection(GetCurrentConnectionString()); //just try to connect await connection.OpenAsync(); return true; } catch { return false; } } /// /// Checks if the specified database exists, returns true if database exists /// /// Returns true if the database exists. public bool DatabaseExists() { try { using var connection = CreateDbConnection(); //just try to connect connection.Open(); return true; } catch { return false; } } /// /// Get the current identity value /// /// Entity type /// /// A task that represents the asynchronous operation /// The task result contains the integer identity; null if cannot get the result /// public virtual async Task GetTableIdentAsync() where TEntity : BaseEntity { using var currentConnection = CreateDataConnection(); var tableName = NopMappingSchema.GetEntityDescriptor(typeof(TEntity)).EntityName; var databaseName = currentConnection.Connection.Database; //we're using the DbConnection object until linq2db solve this issue https://github.com/linq2db/linq2db/issues/1987 //with DataContext we could be used KeepConnectionAlive option await using var dbConnection = GetInternalDbConnection(GetCurrentConnectionString()); dbConnection.StateChange += (sender, e) => { try { if (e.CurrentState != ConnectionState.Open) return; var connection = (IDbConnection)sender; using var internalCommand = connection.CreateCommand(); internalCommand.Connection = connection; internalCommand.CommandText = "SET @@SESSION.information_schema_stats_expiry = 0;"; internalCommand.ExecuteNonQuery(); } //ignoring for older than 8.0 versions MySQL (#1193 Unknown system variable) catch (MySqlException ex) when (ex.Number == 1193) { //ignore } }; await using var command = dbConnection.CreateCommand(); command.Connection = dbConnection; command.CommandText = $"SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{databaseName}' AND TABLE_NAME = '{tableName}'"; await dbConnection.OpenAsync(); return Convert.ToInt32((await command.ExecuteScalarAsync()) ?? 1); } /// /// Set table identity (is supported) /// /// Entity type /// Identity value /// A task that represents the asynchronous operation public virtual async Task SetTableIdentAsync(int ident) where TEntity : BaseEntity { var currentIdent = await GetTableIdentAsync(); if (!currentIdent.HasValue || ident <= currentIdent.Value) return; using var currentConnection = CreateDataConnection(); var tableName = NopMappingSchema.GetEntityDescriptor(typeof(TEntity)).EntityName; await currentConnection.ExecuteAsync($"ALTER TABLE `{tableName}` AUTO_INCREMENT = {ident};"); } /// /// Creates a backup of the database /// /// A task that represents the asynchronous operation public virtual Task BackupDatabaseAsync(string fileName) { throw new DataException("This database provider does not support backup"); } /// /// Restores the database from a backup /// /// The name of the backup file /// A task that represents the asynchronous operation public virtual Task RestoreDatabaseAsync(string backupFileName) { throw new DataException("This database provider does not support backup"); } /// /// Re-index database tables /// /// A task that represents the asynchronous operation public virtual async Task ReIndexTablesAsync() { using var currentConnection = CreateDataConnection(); var tables = currentConnection.Query($"SHOW TABLES FROM `{currentConnection.Connection.Database}`").ToList(); if (tables.Count > 0) await currentConnection.ExecuteAsync($"OPTIMIZE TABLE `{string.Join("`, `", tables)}`"); } /// /// Build the connection string /// /// Connection string info /// Connection string public virtual string BuildConnectionString(INopConnectionStringInfo nopConnectionString) { ArgumentNullException.ThrowIfNull(nopConnectionString); if (nopConnectionString.IntegratedSecurity) throw new NopException("Data provider supports connection only with login and password"); var builder = new MySqlConnectionStringBuilder { Server = nopConnectionString.ServerName, //Cast DatabaseName to lowercase to avoid case-sensitivity problems Database = nopConnectionString.DatabaseName.ToLowerInvariant(), AllowUserVariables = true, UserID = nopConnectionString.Username, Password = nopConnectionString.Password, UseXaTransactions = false }; return builder.ConnectionString; } /// /// Gets the name of a foreign key /// /// Foreign key table /// Foreign key column name /// Primary table /// Primary key column name /// Name of a foreign key public virtual string CreateForeignKeyName(string foreignTable, string foreignColumn, string primaryTable, string primaryColumn) { //mySql support only 64 chars for constraint name //that is why we use hash function for create unique name //see details on this topic: https://dev.mysql.com/doc/refman/8.0/en/identifier-length.html return "FK_" + HashHelper.CreateHash(Encoding.UTF8.GetBytes($"{foreignTable}_{foreignColumn}_{primaryTable}_{primaryColumn}"), HASH_ALGORITHM); } /// /// Gets the name of an index /// /// Target table name /// Target column name /// Name of an index public virtual string GetIndexName(string targetTable, string targetColumn) { return "IX_" + HashHelper.CreateHash(Encoding.UTF8.GetBytes($"{targetTable}_{targetColumn}"), HASH_ALGORITHM); } #endregion #region Properties /// /// MySql data provider /// protected override IDataProvider LinqToDbDataProvider => MySqlTools.GetDataProvider(ProviderName.MySqlConnector); /// /// Gets allowed a limit input value of the data for hashing functions, returns 0 if not limited /// public int SupportedLengthOfBinaryHash { get; } = 0; /// /// Gets a value indicating whether this data provider supports backup /// public virtual bool BackupSupported => false; #endregion }