71881163df
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
130 lines
5.8 KiB
C#
130 lines
5.8 KiB
C#
using System.Data.Common;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests;
|
|
|
|
/// <summary>
|
|
/// A <b>test-only</b> dialect that spells parameters the ODP.NET way — <c>:name</c> in the SQL text and a
|
|
/// <b>bare</b> <c>name</c> as the <see cref="DbParameter.ParameterName"/> — over the SQLite fixture. It is
|
|
/// the falsifiability control for dialect-owned parameter naming: every marker in this repo was hardcoded
|
|
/// <c>"@"</c> before, and a planner or loader that still hardcodes it emits <c>@k0</c> against this
|
|
/// dialect and fails loudly.
|
|
/// <para><b>Why the two halves must differ.</b> A dialect whose marker and bind name are the same string
|
|
/// (SQL Server, Npgsql, SQLite) cannot tell a call site that <em>copies</em> the plan's bind name apart
|
|
/// from one that reuses the marker. Oracle is the provider where they genuinely diverge, so this fake
|
|
/// reproduces that divergence rather than merely changing the prefix.</para>
|
|
/// <para><b>It really executes.</b> Everything but the parameter naming (and the catalog SQL's markers)
|
|
/// delegates to <see cref="SqliteDialect"/>, and Microsoft.Data.Sqlite accepts both a <c>:</c>-prefixed
|
|
/// marker and a prefix-less parameter name — so a poll or a catalog load driven through this dialect runs
|
|
/// against the real fixture database rather than being asserted on as text.</para>
|
|
/// <para>A decorator rather than a subclass: <see cref="SqliteDialect"/> is sealed, and
|
|
/// <c>new</c>-hiding a member would leave interface dispatch calling the original — the substitution
|
|
/// would silently not happen and the control would pass for the wrong reason.</para>
|
|
/// </summary>
|
|
public sealed class ColonMarkerDialect : ISqlDialect
|
|
{
|
|
private static readonly SqliteDialect Inner = new();
|
|
|
|
/// <summary>Every command handed to <see cref="ConfigureCommand"/>, kept so its binds can be read back.</summary>
|
|
private readonly List<DbCommand> _commands = [];
|
|
|
|
/// <summary>How many times <see cref="ConfigureCommand"/> has been called on this instance.</summary>
|
|
/// <remarks>
|
|
/// Oracle's <c>BindByName</c> is set here, and a command created without it mis-binds by position
|
|
/// rather than failing — so "was every command configured" has to be observable to be testable.
|
|
/// </remarks>
|
|
public int ConfiguredCommands
|
|
{
|
|
get { lock (_commands) return _commands.Count; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The <see cref="DbParameter.ParameterName"/> of every parameter bound on every configured command,
|
|
/// read after the fact.
|
|
/// <para><b>Why this and not the query's own success.</b> Microsoft.Data.Sqlite resolves a parameter
|
|
/// whether it is named <c>:k0</c> or bare <c>k0</c>, so a poll that returns the right values proves
|
|
/// the SQL parsed — not that the caller bound the dialect's bind name. Only the name actually put on
|
|
/// the parameter distinguishes the two, and on ODP.NET the wrong one is a hard bind failure.</para>
|
|
/// </summary>
|
|
public IReadOnlyList<string> BoundParameterNames
|
|
{
|
|
get
|
|
{
|
|
lock (_commands)
|
|
return _commands
|
|
.SelectMany(c => c.Parameters.Cast<DbParameter>())
|
|
.Select(p => p.ParameterName)
|
|
.ToList();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public SqlProvider Provider => Inner.Provider;
|
|
|
|
/// <inheritdoc/>
|
|
public DbProviderFactory Factory => Inner.Factory;
|
|
|
|
/// <inheritdoc/>
|
|
public int MaxBindParameters => Inner.MaxBindParameters;
|
|
|
|
/// <inheritdoc/>
|
|
public string LivenessSql => Inner.LivenessSql;
|
|
|
|
/// <inheritdoc/>
|
|
public string SingleRowLimitPrefix => Inner.SingleRowLimitPrefix;
|
|
|
|
/// <inheritdoc/>
|
|
public string SingleRowLimitSuffix => Inner.SingleRowLimitSuffix;
|
|
|
|
/// <inheritdoc/>
|
|
public string ListSchemasSql => Colonize(Inner.ListSchemasSql);
|
|
|
|
/// <inheritdoc/>
|
|
public string DefaultSchemaSql => Colonize(Inner.DefaultSchemaSql);
|
|
|
|
/// <inheritdoc/>
|
|
public string ListTablesSql => Colonize(Inner.ListTablesSql);
|
|
|
|
/// <inheritdoc/>
|
|
public string ListColumnsSql => Colonize(Inner.ListColumnsSql);
|
|
|
|
/// <inheritdoc/>
|
|
public string ParameterMarker(string baseName) => ":" + baseName;
|
|
|
|
/// <summary>The bare name, with no prefix — ODP.NET's convention, and deliberately NOT the marker.</summary>
|
|
/// <param name="baseName">The planner's/loader's base name.</param>
|
|
/// <returns>The bind name.</returns>
|
|
public string ParameterBindName(string baseName) => baseName;
|
|
|
|
/// <inheritdoc/>
|
|
public void ConfigureCommand(DbCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
lock (_commands) _commands.Add(command);
|
|
}
|
|
|
|
/// <summary>Every value handed to <see cref="ConfigureParameter"/>, in call order.</summary>
|
|
public List<object?> ConfiguredParameterValues { get; } = [];
|
|
|
|
/// <inheritdoc/>
|
|
public void ConfigureParameter(DbParameter parameter, object? value)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(parameter);
|
|
lock (ConfiguredParameterValues) ConfiguredParameterValues.Add(value);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public string QuoteIdentifier(string ident) => Inner.QuoteIdentifier(ident);
|
|
|
|
/// <inheritdoc/>
|
|
public DriverDataType MapColumnType(string sqlDataType) => Inner.MapColumnType(sqlDataType);
|
|
|
|
/// <summary>
|
|
/// Rewrites the SQLite dialect's <c>@</c> markers as <c>:</c> markers. The catalog SQL is dialect-owned
|
|
/// text, so a colon-marker dialect must carry colon markers in it — that is precisely the coupling the
|
|
/// catalog loader's bind names have to stay in step with.
|
|
/// </summary>
|
|
private static string Colonize(string sql) => sql.Replace('@', ':');
|
|
}
|