1da770253e
ISqlDialect gains ParameterMarker (the token in the SQL text), ParameterBindName
(the DbParameter.ParameterName) and ConfigureCommand (per-command provider setup,
where Oracle will set BindByName). SQL Server and the test SQLite dialect answer
"@"+name for both spellings and no-op the hook, so SQL Server behaviour is
byte-identical — every existing expected-SQL assertion passes untouched.
SqlGroupPlanner drops its hardcoded @k/@w consts and mints base names (k0..kN, w)
that the dialect spells; SqlQueryPlan carries the emitted markers AND the matching
bind names, so SqlPollReader keeps copying the plan's pairing rather than
re-deriving a provider convention (the two diverge on ODP.NET: ":k0" in the text,
bare "k0" on the parameter). SqlCatalogLoader and SqlBrowseSession bind
schema/table through the dialect, so their bind names cannot drift from the
markers in the dialect-owned catalog SQL. Every parameter-carrying command site
calls ConfigureCommand before its first parameter.
Controlled by a new test-only ColonMarkerDialect (":name" markers, bare bind
names) that really executes against the SQLite fixture. Each leg was falsified
against the pre-change behaviour: the planner emitted "@k0", the reader bound
":k0", and a hardcoded "@schema" in the loader failed catalog load outright.
117 lines
5.3 KiB
C#
117 lines
5.3 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 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);
|
|
}
|
|
|
|
/// <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('@', ':');
|
|
}
|