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
69 changes: 69 additions & 0 deletions src/Weasel.MySql.Tests/CommandBuilderNeutralContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Shouldly;
using Xunit;

namespace Weasel.MySql.Tests;

/// <summary>
/// weasel#423: Weasel.MySql.CommandBuilder shipped without the non-generic
/// Weasel.Core.ICommandBuilder, which is the surface every Weasel.Storage closed-shape operation
/// configures itself against. SQLite was the provider where this actually blocked a consumer, but
/// MySql had the identical gap and would have hit it the moment a Weasel.Storage consumer targeted
/// it. These guard the members the base class cannot supply on its own.
/// </summary>
public class CommandBuilderNeutralContractTests
{
[Fact]
public void implements_the_dialect_neutral_command_builder()
{
typeof(Weasel.Core.ICommandBuilder).IsAssignableFrom(typeof(CommandBuilder)).ShouldBeTrue();
}

[Fact]
public void append_parameter_returns_the_created_parameter()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder();

builder.Append("select * from foo where bar = ");
var parameter = builder.AppendParameter("baz");

parameter.ShouldNotBeNull();
parameter.Value.ShouldBe("baz");
builder.ToString().ShouldBe("select * from foo where bar = @p0");
}

[Fact]
public void append_parameters_writes_each_value_comma_separated()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder();

builder.Append("select * from foo where bar in (");
builder.AppendParameters("one", "two", "three");
builder.Append(")");

builder.ToString().ShouldBe("select * from foo where bar in (@p0, @p1, @p2)");
}

[Fact]
public void append_parameters_rejects_an_empty_set()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder();

Should.Throw<ArgumentOutOfRangeException>(() => builder.AppendParameters());
}

[Fact]
public void creates_a_grouped_parameter_builder()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder();

builder.CreateGroupedParameterBuilder().ShouldNotBeNull();
}

[Fact]
public void carries_a_tenant_id()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder { TenantId = "tenant-a" };

builder.TenantId.ShouldBe("tenant-a");
}
}
43 changes: 42 additions & 1 deletion src/Weasel.MySql/CommandBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Weasel.MySql;

public class CommandBuilder: CommandBuilderBase<MySqlCommand, MySqlParameter, MySqlDbType>
public class CommandBuilder: CommandBuilderBase<MySqlCommand, MySqlParameter, MySqlDbType>, ICommandBuilder
{
public CommandBuilder(): this(new MySqlCommand())
{
Expand All @@ -13,6 +13,47 @@ public CommandBuilder(): this(new MySqlCommand())
public CommandBuilder(MySqlCommand command): base(MySqlProvider.Instance, '@', command)
{
}

/// <summary>
/// It became so common, that it's turned out to be convenient to place
/// this here
/// </summary>
public string TenantId { get; set; } = string.Empty;

/// <summary>
/// Append a single parameter through the dialect-neutral value path, returning the newly created
/// parameter upcast to <see cref="DbParameter" />.
/// <para>
/// Explicitly implemented, as in Weasel.Oracle: the base class already exposes void-returning
/// <c>AppendParameter</c> overloads, so a public member here would hide them and silently change
/// which one existing call sites bind to.
/// </para>
/// </summary>
DbParameter ICommandBuilder.AppendParameter(object value)
{
base.AppendParameter(value);
return _command.Parameters[^1];
}

void ICommandBuilder.AppendParameters(params object[] parameters)
{
if (parameters.Length == 0)
throw new ArgumentOutOfRangeException(nameof(parameters),
"Must be at least one parameter value, but got " + parameters.Length);

AppendParameter(parameters[0]);

for (var i = 1; i < parameters.Length; i++)
{
Append(", ");
AppendParameter(parameters[i]);
}
}

public IGroupedParameterBuilder CreateGroupedParameterBuilder(char? seperator = null)
{
return new GroupedParameterBuilder(this, seperator);
}
}

public static class CommandBuilderExtensions
Expand Down
64 changes: 64 additions & 0 deletions src/Weasel.Sqlite.Tests/CommandBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,67 @@ public void append_multiple_times()
builder.ToString().ShouldBe("SELECT * FROM users");
}
}

/// <summary>
/// weasel#423: Weasel.Sqlite.CommandBuilder shipped without the non-generic
/// Weasel.Core.ICommandBuilder, which is the surface every Weasel.Storage closed-shape operation
/// configures itself against — so no document or event operation could execute against SQLite at
/// all. These guard the members the base class cannot supply on its own.
/// </summary>
public class CommandBuilderNeutralContractTests
{
[Fact]
public void implements_the_dialect_neutral_command_builder()
{
typeof(Weasel.Core.ICommandBuilder).IsAssignableFrom(typeof(CommandBuilder)).ShouldBeTrue();
}

[Fact]
public void append_parameter_returns_the_created_parameter()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder();

builder.Append("select * from foo where bar = ");
var parameter = builder.AppendParameter("baz");

parameter.ShouldNotBeNull();
parameter.Value.ShouldBe("baz");
builder.ToString().ShouldBe("select * from foo where bar = @p0");
}

[Fact]
public void append_parameters_writes_each_value_comma_separated()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder();

builder.Append("select * from foo where bar in (");
builder.AppendParameters("one", "two", "three");
builder.Append(")");

builder.ToString().ShouldBe("select * from foo where bar in (@p0, @p1, @p2)");
}

[Fact]
public void append_parameters_rejects_an_empty_set()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder();

Should.Throw<ArgumentOutOfRangeException>(() => builder.AppendParameters());
}

[Fact]
public void creates_a_grouped_parameter_builder()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder();

builder.CreateGroupedParameterBuilder().ShouldNotBeNull();
}

[Fact]
public void carries_a_tenant_id()
{
Weasel.Core.ICommandBuilder builder = new CommandBuilder { TenantId = "tenant-a" };

builder.TenantId.ShouldBe("tenant-a");
}
}
47 changes: 46 additions & 1 deletion src/Weasel.Sqlite/CommandBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Weasel.Sqlite;

public class CommandBuilder: CommandBuilderBase<SqliteCommand, SqliteParameter, SqliteType>
public class CommandBuilder: CommandBuilderBase<SqliteCommand, SqliteParameter, SqliteType>, ICommandBuilder
{
public CommandBuilder(): this(new SqliteCommand())
{
Expand All @@ -13,6 +13,51 @@ public CommandBuilder(): this(new SqliteCommand())
public CommandBuilder(SqliteCommand command): base(SqliteProvider.Instance, '@', command)
{
}

/// <summary>
/// It became so common, that it's turned out to be convenient to place
/// this here
/// </summary>
public string TenantId { get; set; } = string.Empty;

/// <summary>
/// Append a single parameter through the dialect-neutral value path, returning the newly created
/// parameter upcast to <see cref="DbParameter" />.
/// <para>
/// Explicitly implemented, as in Weasel.Oracle: the base class already exposes void-returning
/// <c>AppendParameter</c> overloads, so a public member here would hide them and silently change
/// which one existing call sites bind to.
/// </para>
/// </summary>
DbParameter ICommandBuilder.AppendParameter(object value)
{
base.AppendParameter(value);
return _command.Parameters[^1];
}

void ICommandBuilder.AppendParameters(params object[] parameters)
{
if (parameters.Length == 0)
throw new ArgumentOutOfRangeException(nameof(parameters),
"Must be at least one parameter value, but got " + parameters.Length);

AppendParameter(parameters[0]);

for (var i = 1; i < parameters.Length; i++)
{
Append(", ");
AppendParameter(parameters[i]);
}
}

public IGroupedParameterBuilder CreateGroupedParameterBuilder(char? seperator = null)
{
return new GroupedParameterBuilder(this, seperator);
}

// StartNewCommand is deliberately not overridden: the base is already a no-op, which is correct
// here because Microsoft.Data.Sqlite executes several semicolon-separated statements from one
// command.
}

public static class CommandBuilderExtensions
Expand Down
Loading