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
59 changes: 57 additions & 2 deletions src/Weasel.Core/CommandBuilderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@
? null
: _command.Parameters[^1].ParameterName;

/// <summary>
/// The bind marker this dialect uses in command text — <c>@</c> for SQL Server, MySQL and
/// SQLite, <c>:</c> for PostgreSQL and Oracle. Callers that hand-write a marker for a named
/// parameter (rather than going through <see cref="AppendParameter(object, TParameterType?)" />,
/// which writes it for them) should read it from here rather than hard-coding one.
/// </summary>
public char ParameterPrefix => _parameterPrefix;

/// <summary>
/// Add text to the batched command SQL string
/// </summary>
Expand Down Expand Up @@ -94,14 +102,61 @@
return _command;
}

/// <summary>
/// Marks the end of a logical statement within the batch. On providers whose ADO.NET
/// driver can execute several statements from a single command — which is every Weasel
/// provider except Oracle — this is a no-op, because the statements are simply
/// concatenated into one <see cref="DbCommand" />.
/// <para>
/// A provider whose driver cannot do that overrides this to close the current statement
/// and start a new one, so that <see cref="CompileCommands" /> hands back one command per
/// boundary. Callers that build a batch should call this between logical operations
/// regardless of provider; on the multi-statement providers it costs nothing.
/// </para>
/// </summary>
public virtual void StartNewCommand()
{
// Nothing by default -- multi-statement providers just keep appending
}

/// <summary>
/// The number of executable commands accumulated so far. Providers that support
/// multi-statement commands always report 1, no matter how many times
/// <see cref="StartNewCommand" /> has been called.
/// </summary>
public virtual int CommandCount => 1;

/// <summary>
/// Build out the batch as one or more executable ADO.NET commands, in order. Providers
/// that support multi-statement commands return a single command holding every statement;
/// providers that do not return one command per <see cref="StartNewCommand" /> boundary.
/// </summary>
/// <returns></returns>
public virtual IReadOnlyList<DbCommand> CompileCommands()
{
return [Compile()];
}

/// <summary>
/// Take the SQL accumulated since the last call and reset the buffer. Intended for derived
/// builders that implement real statement splitting in <see cref="StartNewCommand" />.
/// </summary>
/// <returns></returns>
protected string TakeSql()
{
var sql = _sql.ToString();
_sql.Clear();
return sql;
}

/// <summary>
/// Adds a parameter to the underlying command, but does NOT add the
/// parameter usage to the command text
/// </summary>
/// <param name="value"></param>
/// <param name="dbType"></param>
/// <returns></returns>
public TParameter AddParameter(object? value, TParameterType? dbType = null)
public virtual TParameter AddParameter(object? value, TParameterType? dbType = null)
{
var name = "p" + _command.Parameters.Count;

Expand All @@ -127,7 +182,7 @@
/// <param name="value"></param>
/// <param name="dbType"></param>
/// <returns></returns>
public TParameter AddNamedParameter(string name, object value, TParameterType? dbType = null)
public virtual TParameter AddNamedParameter(string name, object value, TParameterType? dbType = null)
{
var existing = _command.Parameters.OfType<TParameter>().FirstOrDefault(x => x.ParameterName == name);
if (existing != null)
Expand Down Expand Up @@ -563,7 +618,7 @@
/// <param name="ct"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static async Task<IReadOnlyList<T>> FetchListAsync<T, TCommand>(

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / EF Core net10.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MySql mysql:8.0 net9.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MySql mysql:8.0 net8.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / SQLite ubuntu-latest net8.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2022-latest net8.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / EF Core net9.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MySql mysql:8.0 net10.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2022-latest net10.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Oracle net10.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / SQLite ubuntu-latest net9.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine net8.0 Case Sensitive false

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine net10.0 Case Sensitive true

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / SQLite ubuntu-latest net10.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2019-latest net8.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine net9.0 Case Sensitive false

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2019-latest net9.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2022-latest net9.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Oracle net8.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / MSSQL mcr.microsoft.com/mssql/server:2019-latest net10.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine net9.0 Case Sensitive true

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Oracle net9.0

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine net10.0 Case Sensitive false

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)

Check warning on line 621 in src/Weasel.Core/CommandBuilderBase.cs

View workflow job for this annotation

GitHub Actions / Postgres postgres:15.3-alpine net8.0 Case Sensitive true

Type parameter 'TCommand' has no matching typeparam tag in the XML comment on 'CommandBuilderExtensions.FetchListAsync<T, TCommand>(DbConnection, ICommandBuilder<TCommand>, Func<DbDataReader, CancellationToken, Task<T>>, DbTransaction?, CancellationToken)' (but other type parameters do)
DbConnection connection,
ICommandBuilder<TCommand> commandBuilder,
Func<DbDataReader, CancellationToken, Task<T>> transform,
Expand Down
12 changes: 12 additions & 0 deletions src/Weasel.Core/DbCommandBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ public DbCommandBuilder(DbCommand command): base(DbDatabaseProvider.Instance, '@
public DbCommandBuilder(DbConnection connection): base(DbDatabaseProvider.Instance, '@', connection.CreateCommand())
{
}

/// <summary>
/// Build against a dialect whose bind marker is not <c>@</c> — Oracle's is <c>:</c>, for
/// example. Lets a database-agnostic consumer keep using <see cref="DbCommandBuilder" />
/// against such a provider instead of emitting SQL the driver will reject.
/// </summary>
/// <param name="command"></param>
/// <param name="parameterPrefix"></param>
protected DbCommandBuilder(DbCommand command, char parameterPrefix)
: base(DbDatabaseProvider.Instance, parameterPrefix, command)
{
}
}

public static class DbCommandBuilderExtensions
Expand Down
144 changes: 144 additions & 0 deletions src/Weasel.Oracle.Tests/CommandBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System.Data.Common;
using Oracle.ManagedDataAccess.Client;
using Shouldly;
using Xunit;

namespace Weasel.Oracle.Tests;

public class CommandBuilderTests
{
[Fact]
public void uses_the_oracle_bind_marker()
{
var builder = new CommandBuilder();

builder.Append("select data from messages where ");
builder.AppendWithParameters("foo = ?").Length.ShouldBe(1);

builder.ToString().ShouldBe("select data from messages where foo = :p0");
}

[Fact]
public void binds_by_name()
{
var command = new OracleCommand();
_ = new CommandBuilder(command);

command.BindByName.ShouldBeTrue();
}

/// <summary>
/// The Guid conversion used to be a `new` member, so every one of the base class's typed
/// AppendParameter overloads routed straight past it through AddParameter and handed a raw
/// Guid to OracleParameter.Value. Regression guard for that.
/// </summary>
[Fact]
public void typed_guid_overload_converts_to_raw()
{
var id = Guid.NewGuid();
var builder = new CommandBuilder();

builder.Append("select 1 from dual where id = ");
builder.AppendParameter(id);

var parameter = builder.Compile().Parameters[0];
parameter.OracleDbType.ShouldBe(OracleDbType.Raw);
parameter.Value.ShouldBe(id.ToByteArray());
}

[Fact]
public void boxed_guid_converts_to_raw()
{
var id = Guid.NewGuid();
var builder = new CommandBuilder();

builder.Append("select 1 from dual where id = ");
builder.AppendParameter((object)id);

var parameter = builder.Compile().Parameters[0];
parameter.OracleDbType.ShouldBe(OracleDbType.Raw);
parameter.Value.ShouldBe(id.ToByteArray());
}

[Fact]
public void named_boolean_parameter_converts_to_an_oracle_number()
{
var builder = new CommandBuilder();

builder.Append("update dead_letters set replayable = ");
builder.AddNamedParameter("replayable", true);

var parameter = builder.Compile().Parameters["replayable"];
parameter.OracleDbType.ShouldBe(OracleDbType.Int16);
parameter.Value.ShouldBe(1);
}

[Fact]
public void named_guid_parameter_converts_to_raw()
{
var id = Guid.NewGuid();
var builder = new CommandBuilder();

builder.AddNamedParameter("id", id);

var parameter = builder.Compile().Parameters["id"];
parameter.OracleDbType.ShouldBe(OracleDbType.Raw);
parameter.Value.ShouldBe(id.ToByteArray());
}

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

builder.Append("select data from messages where foo = ");
var parameter = builder.AppendParameter(5);

parameter.ShouldBeOfType<OracleParameter>();
builder.ToString().ShouldBe("select data from messages where foo = :p0");
}

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

builder.Append("select data from messages where ");
DbParameter[] parameters = builder.AppendWithDbParameters("foo = ? and bar = ?");

parameters.Length.ShouldBe(2);
builder.ToString().ShouldBe("select data from messages where foo = :p0 and bar = :p1");
}

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

builder.Append("select data from messages where id in (");
var grouped = builder.CreateGroupedParameterBuilder(',');
grouped.AppendParameter(1);
grouped.AppendParameter(2);
builder.Append(")");

builder.ToString().ShouldBe("select data from messages where id in (:p0,:p1)");
}

/// <summary>
/// Oracle is the one provider that splits a batch. Everything else concatenates, so
/// StartNewCommand has to stay free for them.
/// </summary>
[Fact]
public void start_new_command_is_a_no_op_on_the_plain_command_builder()
{
var builder = new CommandBuilder();

builder.Append("delete from incoming");
builder.StartNewCommand();
builder.Append(";delete from outgoing");

builder.CommandCount.ShouldBe(1);
builder.CompileCommands().Count.ShouldBe(1);
builder.CompileCommands()[0].CommandText.ShouldBe("delete from incoming;delete from outgoing");
}
}
Loading
Loading