Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/dbup-postgresql/DataSourceConnectionFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Data;
using DbUp.Engine.Output;
using DbUp.Engine.Transactions;
using Npgsql;

namespace DbUp.Postgresql;

/// <summary>
/// A connection factory that uses Npgsql's data source pattern to create PostgreSQL database connections.
/// This factory provides better performance and resource management compared to traditional connection strings
/// by reusing configured data sources and connection pooling.
/// </summary>
internal class DataSourceConnectionFactory : IConnectionFactory
{

private readonly NpgsqlDataSource dataSource;

/// <summary>
/// Initializes a new instance of the <see cref="DataSourceConnectionFactory"/> class.
/// </summary>
/// <param name="connectionString">The PostgreSQL connection string used to configure the data source.</param>
/// <param name="connectionOptions">Additional connection options including SSL certificate configuration.</param>
/// <exception cref="System.ArgumentNullException">Thrown when <paramref name="connectionString"/> or <paramref name="connectionOptions"/> is null.</exception>
/// <exception cref="System.ArgumentException">Thrown when <paramref name="connectionString"/> is empty or invalid.</exception>
public DataSourceConnectionFactory(string connectionString, PostgresqlConnectionOptions connectionOptions)
{
if (connectionString == null)
{
throw new ArgumentNullException(nameof(connectionString));
}
if (string.IsNullOrEmpty(connectionString))
{
throw new ArgumentException("Connection string cannot be empty.", nameof(connectionString));
}
if (connectionOptions == null)
{
throw new ArgumentNullException(nameof(connectionOptions));
}
var builder = new NpgsqlDataSourceBuilder(connectionString);

#if NET8_0_OR_GREATER
// Use the new SSL authentication callback API for .NET 8.0 with Npgsql 9
if (connectionOptions.ClientCertificate != null || connectionOptions.UserCertificateValidationCallback != null)
{
builder.UseSslClientAuthenticationOptionsCallback(options =>
{
if (connectionOptions.ClientCertificate != null)
{
options.ClientCertificates = new System.Security.Cryptography.X509Certificates.X509CertificateCollection
{
connectionOptions.ClientCertificate
};
}
if (connectionOptions.UserCertificateValidationCallback != null)
{
options.RemoteCertificateValidationCallback = connectionOptions.UserCertificateValidationCallback;
}
});
}
#else
// Use legacy API for netstandard2.0 with Npgsql 8
if (connectionOptions.ClientCertificate != null)
{
builder.UseClientCertificate(connectionOptions.ClientCertificate);
}
if (connectionOptions.UserCertificateValidationCallback != null)
{
builder.UseUserCertificateValidationCallback(connectionOptions.UserCertificateValidationCallback);
}
#endif
dataSource = builder.Build();
}

/// <summary>
/// Creates a new database connection using the configured data source.
/// </summary>
/// <param name="upgradeLog">The upgrade log for recording connection-related messages. This parameter is not used in this implementation.</param>
/// <param name="databaseConnectionManager">The database connection manager. This parameter is not used in this implementation.</param>
/// <returns>A new <see cref="IDbConnection"/> instance ready for use.</returns>
/// <remarks>
/// The returned connection is not automatically opened. The caller is responsible for opening and properly disposing of the connection.
/// The connection benefits from the data source's connection pooling and configuration reuse.
/// </remarks>
public IDbConnection CreateConnection(IUpgradeLog upgradeLog, DatabaseConnectionManager databaseConnectionManager) => dataSource.CreateConnection();

/// <summary>
/// Creates a new database connection using the configured data source.
/// Simpler implementation of <see cref="CreateConnection(IUpgradeLog, DatabaseConnectionManager)"/> for internal use.
/// </summary>
/// <returns>A new <see cref="IDbConnection"/> instance ready for use.</returns>
internal NpgsqlConnection CreateConnection() => dataSource.CreateConnection();
}
16 changes: 5 additions & 11 deletions src/dbup-postgresql/PostgresqlConnectionManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using DbUp.Engine.Transactions;
Expand All @@ -15,7 +16,7 @@ public class PostgresqlConnectionManager : DatabaseConnectionManager
/// Disallow single quotes to be escaped with a backslash (\')
/// </summary>
public bool StandardConformingStrings { get; set; } = true;

/// <summary>
/// Creates a new PostgreSQL database connection.
/// </summary>
Expand Down Expand Up @@ -44,14 +45,7 @@ public PostgresqlConnectionManager(string connectionString, X509Certificate2 cer
/// <param name="connectionString">The PostgreSQL connection string.</param>
/// <param name="connectionOptions">Custom options to apply on the created connection</param>
public PostgresqlConnectionManager(string connectionString, PostgresqlConnectionOptions connectionOptions)
: base(new DelegateConnectionFactory(l =>
{
NpgsqlConnection databaseConnection = new NpgsqlConnection(connectionString);
databaseConnection.ApplyConnectionOptions(connectionOptions);

return databaseConnection;
}
))
: base(new DataSourceConnectionFactory(connectionString, connectionOptions))
{
}

Expand All @@ -78,4 +72,4 @@ public override IEnumerable<string> SplitScriptIntoCommands(string scriptContent

return scriptStatements;
}
}
}
68 changes: 27 additions & 41 deletions src/dbup-postgresql/PostgresqlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,42 +194,40 @@ PostgresqlConnectionOptions connectionOptions

logger.LogDebug("Master ConnectionString => {0}", logMasterConnectionStringBuilder.ConnectionString);

using (var connection = new NpgsqlConnection(masterConnectionStringBuilder.ConnectionString))
var factory = new DataSourceConnectionFactory(masterConnectionStringBuilder.ConnectionString, connectionOptions);
using var connection = factory.CreateConnection();
connection.Open();

var sqlCommandText =
$"SELECT case WHEN oid IS NOT NULL THEN 1 ELSE 0 end FROM pg_database WHERE datname = '{databaseName}' limit 1;";

// check to see if the database already exists..
using (var command = new NpgsqlCommand(sqlCommandText, connection)
{
CommandType = CommandType.Text
})
{
connection.ApplyConnectionOptions(connectionOptions);
connection.Open();
var results = Convert.ToInt32(command.ExecuteScalar());

var sqlCommandText =
$@"SELECT case WHEN oid IS NOT NULL THEN 1 ELSE 0 end FROM pg_database WHERE datname = '{databaseName}' limit 1;";

// check to see if the database already exists..
using (var command = new NpgsqlCommand(sqlCommandText, connection)
{
CommandType = CommandType.Text
})
// if the database exists, we're done here...
if (results == 1)
{
var results = Convert.ToInt32(command.ExecuteScalar());

// if the database exists, we're done here...
if (results == 1)
{
return;
}
return;
}
}

sqlCommandText = $"create database \"{databaseName}\";";

// Create the database...
using (var command = new NpgsqlCommand(sqlCommandText, connection)
{
CommandType = CommandType.Text
})
{
command.ExecuteNonQuery();
}
sqlCommandText = $"create database \"{databaseName}\";";

logger.LogInformation(@"Created database {0}", databaseName);
// Create the database...
using (var command = new NpgsqlCommand(sqlCommandText, connection)
{
CommandType = CommandType.Text
})
{
command.ExecuteNonQuery();
}

logger.LogInformation(@"Created database {0}", databaseName);
}

/// <summary>
Expand All @@ -244,16 +242,4 @@ public static UpgradeEngineBuilder JournalToPostgresqlTable(this UpgradeEngineBu
builder.Configure(c => c.Journal = new PostgresqlTableJournal(() => c.ConnectionManager, () => c.Log, schema, table));
return builder;
}

internal static void ApplyConnectionOptions(this NpgsqlConnection connection, PostgresqlConnectionOptions connectionOptions)
{
connection.SslClientAuthenticationOptionsCallback = options =>
{
if (connectionOptions?.ClientCertificate != null)
options.ClientCertificates = new X509Certificate2Collection(connectionOptions.ClientCertificate);

if (connectionOptions?.UserCertificateValidationCallback != null)
options.RemoteCertificateValidationCallback = connectionOptions.UserCertificateValidationCallback;
};
}
}
11 changes: 9 additions & 2 deletions src/dbup-postgresql/dbup-postgresql.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Company>DbUp Contributors</Company>
<Product>DbUp</Product>
<Copyright>Copyright © DbUp Contributors 2015</Copyright>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<AssemblyName>dbup-postgresql</AssemblyName>
<RootNamespace>DbUp.Postgresql</RootNamespace>
<PackageId>dbup-postgresql</PackageId>
Expand All @@ -24,7 +24,14 @@

<ItemGroup>
<PackageReference Include="dbup-core" Version="6.0.4" />
<PackageReference Include="Npgsql" Version="9.0.2" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Npgsql" Version="8.0.7" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Npgsql" Version="9.0.3" />
</ItemGroup>

<ItemGroup>
Expand Down