using System.Data; using System.Data.Common; using LinqToDB; using LinqToDB.Common; using LinqToDB.Data; using LinqToDB.DataProvider; using LinqToDB.SqlQuery; using Nop.Core; using Nop.Data.DataProviders.LinqToDB; using Nop.Data.Mapping; using Npgsql; namespace Nop.Data.DataProviders; public partial class PostgreSqlDataProvider : BaseDataProvider, INopDataProvider { #region Fields protected static readonly Lazy _dataProvider = new(() => new LinqToDBPostgreSQLDataProvider(), true); #endregion #region Utilities /// /// Creates the database connection by the current data configuration /// protected override DataConnection CreateDataConnection() { var dataContext = CreateDataConnection(LinqToDbDataProvider); dataContext.MappingSchema.SetDataType( typeof(string), new SqlDataType(new DbDataType(typeof(string), "citext"))); return dataContext; } /// /// Gets the connection string builder /// /// The connection string builder protected static NpgsqlConnectionStringBuilder GetConnectionStringBuilder() { return new NpgsqlConnectionStringBuilder(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 NpgsqlConnection(connectionString); } /// /// Get the name of the sequence associated with a identity column /// /// A database connection object /// Entity type /// Returns the name of the sequence, or NULL if no sequence is associated with the column protected virtual string GetSequenceName(DataConnection dataConnection) where TEntity : BaseEntity { ArgumentNullException.ThrowIfNull(dataConnection); var descriptor = NopMappingSchema.GetEntityDescriptor(typeof(TEntity)) ?? throw new NopException($"Mapped entity descriptor is not found: {typeof(TEntity).Name}"); var tableName = descriptor.EntityName; var columnName = descriptor.Fields.FirstOrDefault(x => x.IsIdentity && x.IsPrimaryKey)?.Name; if (string.IsNullOrEmpty(columnName)) throw new NopException("A table's primary key does not have an identity constraint"); return dataConnection.Query($"SELECT pg_get_serial_sequence('\"{tableName}\"', '{columnName}');") .FirstOrDefault(); } #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 'postgres' - default administrative connection database. builder.Database = "postgres"; using (var connection = GetInternalDbConnection(builder.ConnectionString)) { var query = $"CREATE DATABASE \"{databaseName}\" WITH OWNER = '{builder.Username}'"; if (!string.IsNullOrWhiteSpace(collation)) query = $"{query} LC_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 { builder.Database = databaseName; using var connection = GetInternalDbConnection(builder.ConnectionString) as NpgsqlConnection; var command = connection.CreateCommand(); command.CommandText = "CREATE EXTENSION IF NOT EXISTS citext; CREATE EXTENSION IF NOT EXISTS pgcrypto;"; command.Connection.Open(); command.ExecuteNonQuery(); connection.ReloadTypes(); break; } } } /// /// Checks if the specified database exists, returns true if database exists /// /// Returns true if the database exists. public bool DatabaseExists() { try { using var connection = GetInternalDbConnection(GetCurrentConnectionString()); //just try to connect connection.Open(); return true; } catch { return false; } } /// /// 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; } } /// /// 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 Task GetTableIdentAsync() where TEntity : BaseEntity { using var currentConnection = CreateDataConnection(); var seqName = GetSequenceName(currentConnection); var result = currentConnection.Query($"SELECT COALESCE(last_value + CASE WHEN is_called THEN 1 ELSE 0 END, 1) as Value FROM {seqName};") .FirstOrDefault(); return Task.FromResult(result); } /// /// 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 seqName = GetSequenceName(currentConnection); await currentConnection.ExecuteAsync($"select setval('{seqName}', {ident}, false);"); } /// /// 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"); } /// /// Inserts record into table. Returns inserted entity with identity /// /// /// /// Inserted entity public override TEntity InsertEntity(TEntity entity) { using var dataContext = CreateDataConnection(); try { entity.Id = dataContext.InsertWithInt32Identity(entity); } // Ignore when we try insert foreign entity via InsertWithInt32IdentityAsync method catch (SqlException ex) when (ex.Message.StartsWith("Identity field must be defined for")) { dataContext.Insert(entity); } return entity; } /// /// Inserts record into table. Returns inserted entity with identity /// /// /// /// /// A task that represents the asynchronous operation /// The task result contains the inserted entity /// public override async Task InsertEntityAsync(TEntity entity) { using var dataContext = CreateDataConnection(); try { entity.Id = await dataContext.InsertWithInt32IdentityAsync(entity); } // Ignore when we try insert foreign entity via InsertWithInt32IdentityAsync method catch (SqlException ex) when (ex.Message.StartsWith("Identity field must be defined for")) { await dataContext.InsertAsync(entity); } return entity; } /// /// 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(); await currentConnection.ExecuteAsync($"REINDEX DATABASE \"{currentConnection.Connection.Database}\";"); } /// /// 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 NpgsqlConnectionStringBuilder { Host = nopConnectionString.ServerName, //Cast DatabaseName to lowercase to avoid case-sensitivity problems Database = nopConnectionString.DatabaseName.ToLowerInvariant(), Username = nopConnectionString.Username, Password = nopConnectionString.Password, }; 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) { return $"FK_{foreignTable}_{foreignColumn}_{primaryTable}_{primaryColumn}"; } /// /// 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_{targetTable}_{targetColumn}"; } #endregion #region Properties protected override IDataProvider LinqToDbDataProvider => _dataProvider.Value; public int SupportedLengthOfBinaryHash => 0; public bool BackupSupported => false; #endregion }