diff --git a/src/Weasel.MySql.Tests/CommandBuilderNeutralContractTests.cs b/src/Weasel.MySql.Tests/CommandBuilderNeutralContractTests.cs
new file mode 100644
index 0000000..1ed0da8
--- /dev/null
+++ b/src/Weasel.MySql.Tests/CommandBuilderNeutralContractTests.cs
@@ -0,0 +1,69 @@
+using Shouldly;
+using Xunit;
+
+namespace Weasel.MySql.Tests;
+
+///
+/// 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.
+///
+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(() => 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");
+ }
+}
diff --git a/src/Weasel.MySql/CommandBuilder.cs b/src/Weasel.MySql/CommandBuilder.cs
index b12913d..969712a 100644
--- a/src/Weasel.MySql/CommandBuilder.cs
+++ b/src/Weasel.MySql/CommandBuilder.cs
@@ -4,7 +4,7 @@
namespace Weasel.MySql;
-public class CommandBuilder: CommandBuilderBase
+public class CommandBuilder: CommandBuilderBase, ICommandBuilder
{
public CommandBuilder(): this(new MySqlCommand())
{
@@ -13,6 +13,47 @@ public CommandBuilder(): this(new MySqlCommand())
public CommandBuilder(MySqlCommand command): base(MySqlProvider.Instance, '@', command)
{
}
+
+ ///
+ /// It became so common, that it's turned out to be convenient to place
+ /// this here
+ ///
+ public string TenantId { get; set; } = string.Empty;
+
+ ///
+ /// Append a single parameter through the dialect-neutral value path, returning the newly created
+ /// parameter upcast to .
+ ///
+ /// Explicitly implemented, as in Weasel.Oracle: the base class already exposes void-returning
+ /// AppendParameter overloads, so a public member here would hide them and silently change
+ /// which one existing call sites bind to.
+ ///
+ ///
+ 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
diff --git a/src/Weasel.Sqlite.Tests/CommandBuilderTests.cs b/src/Weasel.Sqlite.Tests/CommandBuilderTests.cs
index 8e6d103..6f394b7 100644
--- a/src/Weasel.Sqlite.Tests/CommandBuilderTests.cs
+++ b/src/Weasel.Sqlite.Tests/CommandBuilderTests.cs
@@ -116,3 +116,67 @@ public void append_multiple_times()
builder.ToString().ShouldBe("SELECT * FROM users");
}
}
+
+///
+/// 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.
+///
+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(() => 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");
+ }
+}
diff --git a/src/Weasel.Sqlite/CommandBuilder.cs b/src/Weasel.Sqlite/CommandBuilder.cs
index 90f9153..e389f55 100644
--- a/src/Weasel.Sqlite/CommandBuilder.cs
+++ b/src/Weasel.Sqlite/CommandBuilder.cs
@@ -4,7 +4,7 @@
namespace Weasel.Sqlite;
-public class CommandBuilder: CommandBuilderBase
+public class CommandBuilder: CommandBuilderBase, ICommandBuilder
{
public CommandBuilder(): this(new SqliteCommand())
{
@@ -13,6 +13,51 @@ public CommandBuilder(): this(new SqliteCommand())
public CommandBuilder(SqliteCommand command): base(SqliteProvider.Instance, '@', command)
{
}
+
+ ///
+ /// It became so common, that it's turned out to be convenient to place
+ /// this here
+ ///
+ public string TenantId { get; set; } = string.Empty;
+
+ ///
+ /// Append a single parameter through the dialect-neutral value path, returning the newly created
+ /// parameter upcast to .
+ ///
+ /// Explicitly implemented, as in Weasel.Oracle: the base class already exposes void-returning
+ /// AppendParameter overloads, so a public member here would hide them and silently change
+ /// which one existing call sites bind to.
+ ///
+ ///
+ 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