Skip to content
Merged
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
13 changes: 13 additions & 0 deletions src/Weasel.Core/Migrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,17 @@ await WriteTemplatedFile(dropFile, (r, w) =>
/// </summary>
/// <param name="name"></param>
public abstract void AssertValidIdentifier(string name);

/// <summary>
/// Ensures that the database referenced by the supplied connection's connection string exists,
/// creating it if necessary. This is a lightweight operation that only creates the database --
/// it does not run any schema migrations or DDL.
/// </summary>
/// <param name="connection">A connection whose ConnectionString identifies the target database</param>
/// <param name="ct">Cancellation token</param>
public virtual Task EnsureDatabaseExistsAsync(DbConnection connection, CancellationToken ct = default)
{
throw new NotSupportedException(
$"EnsureDatabaseExistsAsync is not supported by {GetType().Name}. Override this method in a provider-specific migrator.");
}
}
50 changes: 50 additions & 0 deletions src/Weasel.MySql.Tests/MySqlMigratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,54 @@ public void create_table_returns_mysql_table()
table.ShouldBeOfType<Table>();
table.Identifier.ShouldBe(identifier);
}

[Fact]
public async Task can_ensure_database_that_does_not_exist()
{
var migrator = new MySqlMigrator();
var databaseName = $"weasel_ensure_{Guid.NewGuid():N}";

// Use root credentials for CREATE DATABASE privileges
var rootBuilder = new MySqlConnectionStringBuilder(ConnectionSource.ConnectionString)
{
UserID = "root",
Password = "P@55w0rd",
Database = databaseName
};

try
{
await using var targetConn = new MySqlConnection(rootBuilder.ConnectionString);
await migrator.EnsureDatabaseExistsAsync(targetConn);

// Verify the database was created by opening a connection to it
await using var verifyConn = new MySqlConnection(rootBuilder.ConnectionString);
await verifyConn.OpenAsync();
}
finally
{
var adminBuilder = new MySqlConnectionStringBuilder(ConnectionSource.ConnectionString)
{
UserID = "root",
Password = "P@55w0rd",
Database = ""
};
await using var adminConn = new MySqlConnection(adminBuilder.ConnectionString);
await adminConn.OpenAsync();

var cmd = adminConn.CreateCommand();
cmd.CommandText = $"DROP DATABASE IF EXISTS `{databaseName}`";
await cmd.ExecuteNonQueryAsync();
}
}

[Fact]
public async Task ensure_database_is_idempotent()
{
var migrator = new MySqlMigrator();

// Use the existing test database - should not throw
await using var connection = new MySqlConnection(ConnectionSource.ConnectionString);
await migrator.EnsureDatabaseExistsAsync(connection);
}
}
19 changes: 19 additions & 0 deletions src/Weasel.MySql/MySqlMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,25 @@ public static string CreateDatabaseStatementFor(string databaseName)
return $"CREATE DATABASE IF NOT EXISTS `{databaseName}`;";
}

public override async Task EnsureDatabaseExistsAsync(DbConnection connection, CancellationToken ct = default)
{
var builder = new MySqlConnectionStringBuilder(connection.ConnectionString);
var databaseName = builder.Database;

if (string.IsNullOrEmpty(databaseName))
{
throw new ArgumentException("The connection string does not specify a database name.");
}

builder.Database = "";
await using var adminConn = new MySqlConnection(builder.ConnectionString);
await adminConn.OpenAsync(ct).ConfigureAwait(false);

var cmd = adminConn.CreateCommand();
cmd.CommandText = $"CREATE DATABASE IF NOT EXISTS `{databaseName}`";
await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
}

public override ITable CreateTable(DbObjectName identifier)
{
return new Tables.Table(identifier);
Expand Down
10 changes: 10 additions & 0 deletions src/Weasel.Oracle.Tests/OracleMigratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,14 @@ public void create_table_returns_oracle_table()
table.ShouldBeOfType<Table>();
table.Identifier.ShouldBe(identifier);
}

[Fact]
public async Task ensure_database_is_idempotent()
{
var migrator = new OracleMigrator();

// Use the existing test connection/schema - should not throw
await using var connection = new OracleConnection(ConnectionSource.ConnectionString);
await migrator.EnsureDatabaseExistsAsync(connection);
}
}
31 changes: 31 additions & 0 deletions src/Weasel.Oracle/OracleMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,37 @@ public static string CreateSchemaStatementFor(string schemaName)
END;";
}

public override async Task EnsureDatabaseExistsAsync(DbConnection connection, CancellationToken ct = default)
{
var builder = new OracleConnectionStringBuilder(connection.ConnectionString);
var schemaName = builder.UserID;

if (string.IsNullOrEmpty(schemaName))
{
throw new ArgumentException("The connection string does not specify a User ID (schema name).");
}

var wasOpen = connection.State == System.Data.ConnectionState.Open;
if (!wasOpen)
{
await connection.OpenAsync(ct).ConfigureAwait(false);
}

try
{
var cmd = connection.CreateCommand();
cmd.CommandText = CreateSchemaStatementFor(schemaName);
await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
}
finally
{
if (!wasOpen)
{
await connection.CloseAsync().ConfigureAwait(false);
}
}
}

public override ITable CreateTable(DbObjectName identifier)
{
return new Tables.Table(identifier);
Expand Down
48 changes: 48 additions & 0 deletions src/Weasel.Postgresql.Tests/PostgresqlMigratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,52 @@ public void assert_identifier_length_exceeding_maximum()
options.AssertValidIdentifier(text);
});
}

[Fact]
public async Task can_ensure_database_that_does_not_exist()
{
var migrator = new PostgresqlMigrator();
var databaseName = $"weasel_ensure_test_{Guid.NewGuid():N}";

var builder = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString)
{
Database = databaseName
};

try
{
await using var targetConn = new NpgsqlConnection(builder.ConnectionString);
await migrator.EnsureDatabaseExistsAsync(targetConn);

// Verify the database was created
var adminBuilder = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString)
{
Database = "postgres"
};
await using var adminConn = new NpgsqlConnection(adminBuilder.ConnectionString);
await adminConn.OpenAsync();
(await adminConn.DatabaseExists(databaseName)).ShouldBeTrue();
}
finally
{
var adminBuilder = new NpgsqlConnectionStringBuilder(ConnectionSource.ConnectionString)
{
Database = "postgres"
};
await using var adminConn = new NpgsqlConnection(adminBuilder.ConnectionString);
await adminConn.OpenAsync();
await adminConn.KillIdleSessions(databaseName);
await adminConn.DropDatabase(databaseName);
}
}

[Fact]
public async Task ensure_database_is_idempotent()
{
var migrator = new PostgresqlMigrator();

// Use the existing test database - should not throw
await using var connection = new NpgsqlConnection(ConnectionSource.ConnectionString);
await migrator.EnsureDatabaseExistsAsync(connection);
}
}
21 changes: 21 additions & 0 deletions src/Weasel.Postgresql/PostgresqlMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Npgsql;
using Weasel.Core;
using Weasel.Core.Migrations;
using Weasel.Postgresql.Migrations;
using Weasel.Postgresql.Tables;

namespace Weasel.Postgresql;
Expand Down Expand Up @@ -171,6 +172,26 @@ public override void AssertValidIdentifier(string name)
throw new PostgresqlIdentifierTooLongException(NameDataLength, name);
}

public override async Task EnsureDatabaseExistsAsync(DbConnection connection, CancellationToken ct = default)
{
var builder = new NpgsqlConnectionStringBuilder(connection.ConnectionString);
var databaseName = builder.Database;

if (string.IsNullOrEmpty(databaseName))
{
throw new ArgumentException("The connection string does not specify a database name.");
}

builder.Database = "postgres";
await using var adminConn = new NpgsqlConnection(builder.ConnectionString);
await adminConn.OpenAsync(ct).ConfigureAwait(false);

if (!await adminConn.DatabaseExists(databaseName, ct).ConfigureAwait(false))
{
await new DatabaseSpecification().BuildDatabase(adminConn, databaseName, ct).ConfigureAwait(false);
}
}

public override ITable CreateTable(DbObjectName identifier)
{
return new Tables.Table(identifier);
Expand Down
50 changes: 50 additions & 0 deletions src/Weasel.SqlServer.Tests/SqlServerMigratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,54 @@ public void create_table_returns_sql_server_table()
table.ShouldBeOfType<Table>();
table.Identifier.ShouldBe(identifier);
}

[Fact]
public async Task can_ensure_database_that_does_not_exist()
{
var migrator = new SqlServerMigrator();
var databaseName = $"weasel_ensure_{Guid.NewGuid():N}";

var builder = new SqlConnectionStringBuilder(ConnectionSource.ConnectionString)
{
InitialCatalog = databaseName
};

try
{
await using var targetConn = new SqlConnection(builder.ConnectionString);
await migrator.EnsureDatabaseExistsAsync(targetConn);

// Verify the database was created by opening a connection to it
await using var verifyConn = new SqlConnection(builder.ConnectionString);
await verifyConn.OpenAsync();
}
finally
{
var adminBuilder = new SqlConnectionStringBuilder(ConnectionSource.ConnectionString)
{
InitialCatalog = "master"
};
await using var adminConn = new SqlConnection(adminBuilder.ConnectionString);
await adminConn.OpenAsync();

var cmd = adminConn.CreateCommand();
cmd.CommandText = $@"
IF DB_ID('{databaseName}') IS NOT NULL
BEGIN
ALTER DATABASE [{databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE [{databaseName}];
END";
await cmd.ExecuteNonQueryAsync();
}
}

[Fact]
public async Task ensure_database_is_idempotent()
{
var migrator = new SqlServerMigrator();

// Use the existing test database - should not throw
await using var connection = new SqlConnection(ConnectionSource.ConnectionString);
await migrator.EnsureDatabaseExistsAsync(connection);
}
}
31 changes: 31 additions & 0 deletions src/Weasel.SqlServer/SqlServerMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,37 @@ FROM sys.schemas
";
}

public override async Task EnsureDatabaseExistsAsync(DbConnection connection, CancellationToken ct = default)
{
var builder = new SqlConnectionStringBuilder(connection.ConnectionString);
var databaseName = builder.InitialCatalog;

if (string.IsNullOrEmpty(databaseName))
{
throw new ArgumentException("The connection string does not specify a database name (Initial Catalog).");
}

builder.InitialCatalog = "master";
await using var adminConn = new SqlConnection(builder.ConnectionString);
await adminConn.OpenAsync(ct).ConfigureAwait(false);

var cmd = adminConn.CreateCommand();
cmd.CommandText = "SELECT DB_ID(@name)";
var param = cmd.CreateParameter();
param.ParameterName = "@name";
param.Value = databaseName;
cmd.Parameters.Add(param);

var result = await cmd.ExecuteScalarAsync(ct).ConfigureAwait(false);

if (result is null or DBNull)
{
var createCmd = adminConn.CreateCommand();
createCmd.CommandText = $"CREATE DATABASE [{databaseName}]";
await createCmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
}
}

public override ITable CreateTable(DbObjectName identifier)
{
return new Tables.Table(identifier);
Expand Down
33 changes: 33 additions & 0 deletions src/Weasel.Sqlite.Tests/SqliteMigratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,37 @@ public void assert_valid_identifier_rejects_too_long()

Should.Throw<InvalidOperationException>(() => migrator.AssertValidIdentifier(longName));
}

[Fact]
public async Task ensure_database_exists_is_noop_for_memory()
{
var migrator = new SqliteMigrator();
await using var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();

// Should not throw - SQLite databases are auto-created
await migrator.EnsureDatabaseExistsAsync(connection);
}

[Fact]
public async Task ensure_database_exists_is_noop_for_file()
{
var migrator = new SqliteMigrator();
var tempFile = Path.Combine(Path.GetTempPath(), $"weasel_test_{Guid.NewGuid():N}.db");

try
{
await using var connection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={tempFile}");

// Should not throw - SQLite databases are auto-created
await migrator.EnsureDatabaseExistsAsync(connection);
}
finally
{
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
}
}
8 changes: 8 additions & 0 deletions src/Weasel.Sqlite/SqliteMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ private static async Task executeCommand(DbConnection conn, IMigrationLogger log
}
}

/// <summary>
/// No-op for SQLite. SQLite databases are automatically created when the connection is opened.
/// </summary>
public override Task EnsureDatabaseExistsAsync(DbConnection connection, CancellationToken ct = default)
{
return Task.CompletedTask;
}

public override IDatabaseWithTables CreateDatabase(DbConnection connection, string? identifier = null)
{
if (connection is not Microsoft.Data.Sqlite.SqliteConnection)
Expand Down
Loading