Skip to content

Weasel.Sqlite.CommandBuilder does not implement the non-generic Weasel.Core.ICommandBuilder, blocking all Weasel.Storage operations on SQLite #423

Description

@jeremydmiller

Weasel.Sqlite.CommandBuilder derives from CommandBuilderBase<SqliteCommand, SqliteParameter, SqliteType> but does not declare the non-generic Weasel.Core.ICommandBuilder, so nothing that targets the neutral contract can be handed a SQLite command builder.

That contract is what every Weasel.Storage closed-shape operation configures itself against:

public interface IStorageOperation : Core.IStorageOperation
{
    void ConfigureCommand(ICommandBuilder builder, IStorageSession session);
}

The practical effect is that no Weasel.Storage document or event operation can be executed against SQLite at all — there is no way to construct the builder argument. Found while building Fisher (the SQLite event store) on Weasel.Storage:

error CS1503: Argument 1: cannot convert from 'Weasel.Sqlite.CommandBuilder'
to 'Weasel.Core.ICommandBuilder'

Confirmed against the published 9.23.1 assembly

Reflecting over Weasel.Sqlite 9.23.1+905a1f780bba4f07acabab560057a345ae1cf55b:

Sqlite CommandBuilder implements ICommandBuilder: False
interfaces: Weasel.Core.ICommandBuilder`1[Microsoft.Data.Sqlite.SqliteCommand]

Missing relative to the non-generic interface:

Member Status on Weasel.Sqlite.CommandBuilder
string TenantId { get; set; } absent
void AppendParameters(params object[]) absent
DbParameter AppendParameter(object) absent — the four inherited AppendParameter overloads all return void, so none satisfies the interface
IGroupedParameterBuilder CreateGroupedParameterBuilder(char?) absent

Everything else the interface needs (Append, AppendWithDbParameters, AddParameters, StartNewCommand, LastParameterName) is already inherited from CommandBuilderBase and needs no new code.

Scope

Weasel.Sqlite and Weasel.MySql are the two outliers; the rest picked this up, presumably in #327:

Weasel.Postgresql: ... , ICommandBuilder
Weasel.SqlServer:  ... , Weasel.Core.ICommandBuilder
Weasel.Oracle:     ... , ICommandBuilder
Weasel.MySql:      ... (none)
Weasel.Sqlite:     ... (none)

MySql presumably has the same blocker for any future Weasel.Storage consumer, though only SQLite is blocking anything today.

Suggested fix

Port Weasel.SqlServer.CommandBuilder's four members verbatim — they are dialect-neutral. StartNewCommand can stay the inherited no-op behaviour for the same reason as SqlClient: Microsoft.Data.Sqlite executes several semicolon-separated statements from one command.

public class CommandBuilder
    : CommandBuilderBase<SqliteCommand, SqliteParameter, SqliteType>, Weasel.Core.ICommandBuilder
{
    public string TenantId { get; set; } = StorageConstants.DefaultTenantId;

    public void 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 new DbParameter AppendParameter(object value)
    {
        base.AppendParameter(value);
        return _command.Parameters[^1];
    }

    public Weasel.Core.IGroupedParameterBuilder CreateGroupedParameterBuilder(char? separator = null)
        => new Weasel.Core.GroupedParameterBuilder(this, separator);

    public override void StartNewCommand()
    {
        // Microsoft.Data.Sqlite happily executes several statements from one command.
    }
}

Fisher carries this today as a local FisherCommandBuilder shim and will delete it once a Weasel.Sqlite release includes the change.

A regression test asserting typeof(Weasel.Core.ICommandBuilder).IsAssignableFrom(typeof(CommandBuilder)) for each provider would keep the set from drifting again.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions