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; /// /// A test-only dialect that spells parameters the ODP.NET way — :name in the SQL text and a /// bare name as the — over the SQLite fixture. It is /// the falsifiability control for dialect-owned parameter naming: every marker in this repo was hardcoded /// "@" before, and a planner or loader that still hardcodes it emits @k0 against this /// dialect and fails loudly. /// Why the two halves must differ. A dialect whose marker and bind name are the same string /// (SQL Server, Npgsql, SQLite) cannot tell a call site that copies 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. /// It really executes. Everything but the parameter naming (and the catalog SQL's markers) /// delegates to , and Microsoft.Data.Sqlite accepts both a :-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. /// A decorator rather than a subclass: is sealed, and /// new-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. /// public sealed class ColonMarkerDialect : ISqlDialect { private static readonly SqliteDialect Inner = new(); /// Every command handed to , kept so its binds can be read back. private readonly List _commands = []; /// How many times has been called on this instance. /// /// Oracle's BindByName 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. /// public int ConfiguredCommands { get { lock (_commands) return _commands.Count; } } /// /// The of every parameter bound on every configured command, /// read after the fact. /// Why this and not the query's own success. Microsoft.Data.Sqlite resolves a parameter /// whether it is named :k0 or bare k0, 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. /// public IReadOnlyList BoundParameterNames { get { lock (_commands) return _commands .SelectMany(c => c.Parameters.Cast()) .Select(p => p.ParameterName) .ToList(); } } /// public SqlProvider Provider => Inner.Provider; /// public DbProviderFactory Factory => Inner.Factory; /// public int MaxBindParameters => Inner.MaxBindParameters; /// public string LivenessSql => Inner.LivenessSql; /// public string SingleRowLimitPrefix => Inner.SingleRowLimitPrefix; /// public string SingleRowLimitSuffix => Inner.SingleRowLimitSuffix; /// public string ListSchemasSql => Colonize(Inner.ListSchemasSql); /// public string DefaultSchemaSql => Colonize(Inner.DefaultSchemaSql); /// public string ListTablesSql => Colonize(Inner.ListTablesSql); /// public string ListColumnsSql => Colonize(Inner.ListColumnsSql); /// public string ParameterMarker(string baseName) => ":" + baseName; /// The bare name, with no prefix — ODP.NET's convention, and deliberately NOT the marker. /// The planner's/loader's base name. /// The bind name. public string ParameterBindName(string baseName) => baseName; /// public void ConfigureCommand(DbCommand command) { ArgumentNullException.ThrowIfNull(command); lock (_commands) _commands.Add(command); } /// Every value handed to , in call order. public List ConfiguredParameterValues { get; } = []; /// public void ConfigureParameter(DbParameter parameter, object? value) { ArgumentNullException.ThrowIfNull(parameter); lock (ConfiguredParameterValues) ConfiguredParameterValues.Add(value); } /// public string QuoteIdentifier(string ident) => Inner.QuoteIdentifier(ident); /// public DriverDataType MapColumnType(string sqlDataType) => Inner.MapColumnType(sqlDataType); /// /// Rewrites the SQLite dialect's @ markers as : 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. /// private static string Colonize(string sql) => sql.Replace('@', ':'); }