refactor(sql): dialect-owned parameter markers, bind names and command configuration
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.
This commit is contained in:
@@ -371,4 +371,31 @@ public sealed class SqlBrowseSessionTests
|
||||
await session.AttributesAsync(columnNodeId, CancellationToken.None);
|
||||
session.LastUsedUtc.ShouldBeGreaterThan(afterExpand);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The browse path binds the <em>dialect's</em> parameter names, not a hardcoded <c>"@schema"</c>.
|
||||
/// Driven through <see cref="ColonMarkerDialect"/> (markers <c>:schema</c>/<c>:table</c>, bare bind
|
||||
/// names) — ODP.NET's shape, and the only place the two spellings differ.
|
||||
/// <para>Discriminating in two independent ways: a session still binding <c>"@schema"</c> leaves the
|
||||
/// colon marker unbound (the expand yields nothing, or the provider refuses the command), and the
|
||||
/// recorded names would carry the prefix.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TheCatalogParameters_areBoundUnderTheDialectsBindNames()
|
||||
{
|
||||
await using var db = await SqliteBrowseFixture.CreateAsync();
|
||||
var dialect = new ColonMarkerDialect();
|
||||
await using var session = new SqlBrowseSession(db.Connection, dialect);
|
||||
|
||||
var tableNode = (await session.ExpandAsync(SchemaNodeId, CancellationToken.None))
|
||||
.First(n => n.DisplayName == SqliteBrowseFixture.KeyValueTable);
|
||||
var columns = await session.ExpandAsync(tableNode.NodeId, CancellationToken.None);
|
||||
|
||||
columns.ShouldContain(c => c.DisplayName == SqliteBrowseFixture.RealColumn);
|
||||
dialect.BoundParameterNames.ShouldContain("schema");
|
||||
dialect.BoundParameterNames.ShouldContain("table");
|
||||
dialect.BoundParameterNames.ShouldNotContain(n => n.StartsWith(':'));
|
||||
// Every catalog command went through the provider hook (Oracle's BindByName lives there).
|
||||
dialect.ConfiguredCommands.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -45,6 +45,13 @@
|
||||
— see its class docs.
|
||||
-->
|
||||
<Compile Include="..\ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests\SqliteDialect.cs" Link="SqliteDialect.cs"/>
|
||||
<!--
|
||||
Linked for the same reason and on the same terms: ColonMarkerDialect is the control for
|
||||
dialect-owned parameter naming (":schema" in the text, bare "schema" on the parameter — ODP.NET's
|
||||
shape), and the browse session binds those names. It decorates SqliteDialect, which is already
|
||||
linked above, so the pair travels together.
|
||||
-->
|
||||
<Compile Include="..\ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests\ColonMarkerDialect.cs" Link="ColonMarkerDialect.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
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('@', ':');
|
||||
}
|
||||
@@ -227,6 +227,50 @@ public sealed class SqlCatalogGateDriverTests
|
||||
driver.GetHealth().State.ShouldBe(DriverState.Faulted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The catalog path against a dialect whose markers are <c>:schema</c>/<c>:table</c> and whose bind
|
||||
/// names are the bare <c>schema</c>/<c>table</c> — ODP.NET's shape. The gate still resolves a real
|
||||
/// table and still refuses an unknown column, so the loader bound names the dialect's SQL actually
|
||||
/// carries.
|
||||
/// <para>Behaviourally discriminating, not decorative: a loader still binding the literal
|
||||
/// <c>"@schema"</c> leaves <c>:schema</c> unbound, the catalog query matches nothing, and
|
||||
/// <em>every</em> tag — including the honest one — falls to <c>BadNodeIdUnknown</c>.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task The_catalog_load_binds_the_dialects_bind_names_rather_than_a_hardcoded_marker()
|
||||
{
|
||||
using var fixture = new SqlitePollFixture();
|
||||
var dialect = new ColonMarkerDialect();
|
||||
await using var driver = new SqlDriver(
|
||||
new SqlDriverOptions
|
||||
{
|
||||
RawTags =
|
||||
[
|
||||
KvEntry("Speed", SqlitePollFixture.PresentKey),
|
||||
KvEntry("Bogus", SqlitePollFixture.PresentKey, valueColumn: "no_such_column"),
|
||||
],
|
||||
OperationTimeout = TimeSpan.FromSeconds(15),
|
||||
CommandTimeout = TimeSpan.FromSeconds(10),
|
||||
},
|
||||
DriverInstanceId,
|
||||
dialect,
|
||||
fixture.ConnectionString,
|
||||
factory: fixture.Factory,
|
||||
logger: NullLogger<SqlDriver>.Instance);
|
||||
|
||||
await driver.InitializeAsync(ConfigJson, CancellationToken.None);
|
||||
|
||||
var snapshots = await driver.ReadAsync(["Speed", "Bogus"], CancellationToken.None);
|
||||
SqlStatusCodes.IsGood(snapshots[0].StatusCode).ShouldBeTrue();
|
||||
snapshots[1].StatusCode.ShouldBe(SqlStatusCodes.BadNodeIdUnknown);
|
||||
|
||||
// The catalog queries bound the bare names, and every catalog command went through the hook.
|
||||
dialect.BoundParameterNames.ShouldContain("schema");
|
||||
dialect.BoundParameterNames.ShouldContain("table");
|
||||
dialect.BoundParameterNames.ShouldNotContain(n => n.StartsWith(':'));
|
||||
dialect.ConfiguredCommands.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
/// <summary>
|
||||
@@ -260,6 +304,12 @@ public sealed class SqlCatalogGateDriverTests
|
||||
|
||||
public string ListColumnsSql => Inner.ListColumnsSql;
|
||||
|
||||
public string ParameterMarker(string baseName) => Inner.ParameterMarker(baseName);
|
||||
|
||||
public string ParameterBindName(string baseName) => Inner.ParameterBindName(baseName);
|
||||
|
||||
public void ConfigureCommand(System.Data.Common.DbCommand command) => Inner.ConfigureCommand(command);
|
||||
|
||||
public string QuoteIdentifier(string ident) => Inner.QuoteIdentifier(ident);
|
||||
|
||||
public DriverDataType MapColumnType(string sqlDataType) => Inner.MapColumnType(sqlDataType);
|
||||
|
||||
@@ -185,6 +185,84 @@ public class SqlGroupPlannerTests
|
||||
plan.Parameters.ShouldBe(new object[] { "k0", "k1", "k2" });
|
||||
}
|
||||
|
||||
// ---- dialect-owned parameter markers ----
|
||||
|
||||
/// <summary>
|
||||
/// The falsifiability control for dialect-owned markers: against a <c>:</c>-marker dialect the emitted
|
||||
/// statement must carry <c>:k0</c>, and no <c>@</c> at all. A planner holding the T-SQL prefix in a
|
||||
/// const passes every SQL Server test in this class and fails only here.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void KeyValueMarkers_comeFromTheDialect_ratherThanAHardcodedAtSign()
|
||||
{
|
||||
var plans = SqlGroupPlanner.Plan(
|
||||
[
|
||||
KvTag("A", "TagValues", "tag_name", "Line1.Speed", "num_value", null),
|
||||
KvTag("B", "TagValues", "tag_name", "Line1.Temp", "num_value", null),
|
||||
], new ColonMarkerDialect()).ToList();
|
||||
|
||||
plans[0].SqlText.ShouldEndWith("IN (:k0, :k1)");
|
||||
plans[0].SqlText.ShouldNotContain("@");
|
||||
plans[0].ParameterNames.ShouldBe(new[] { ":k0", ":k1" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheWideRowSelectorMarker_comesFromTheDialect_ratherThanAHardcodedAtSign()
|
||||
{
|
||||
var plans = SqlGroupPlanner.Plan(
|
||||
[WideTag("A", "LatestStatus", "oven_temp", selectorColumn: "station_id", selectorValue: "7")],
|
||||
new ColonMarkerDialect()).ToList();
|
||||
|
||||
plans[0].SqlText.ShouldEndWith("= :w");
|
||||
plans[0].SqlText.ShouldNotContain("@");
|
||||
plans[0].ParameterNames.ShouldBe(new[] { ":w" });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The bind names ride the plan <em>separately</em> from the markers, because on ODP.NET they are a
|
||||
/// different string. A plan that carried only markers would leave the reader deriving the bind name —
|
||||
/// the convention it is not entitled to know.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParameterBindNames_areTheDialects_andNeedNotEqualTheMarkers()
|
||||
{
|
||||
var plans = SqlGroupPlanner.Plan(
|
||||
[
|
||||
KvTag("A", "TagValues", "tag_name", "Line1.Speed", "num_value", null),
|
||||
KvTag("B", "TagValues", "tag_name", "Line1.Temp", "num_value", null),
|
||||
], new ColonMarkerDialect()).ToList();
|
||||
|
||||
plans[0].ParameterNames.ShouldBe(new[] { ":k0", ":k1" }); // in the SQL text
|
||||
plans[0].ParameterBindNames.ShouldBe(new[] { "k0", "k1" }); // on the DbParameter
|
||||
plans[0].ParameterBindNames.Count.ShouldBe(plans[0].Parameters.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheWideRowSelectorBindName_isTheDialects_andNeedNotEqualTheMarker()
|
||||
{
|
||||
var plans = SqlGroupPlanner.Plan(
|
||||
[WideTag("A", "LatestStatus", "oven_temp", selectorColumn: "station_id", selectorValue: "7")],
|
||||
new ColonMarkerDialect()).ToList();
|
||||
|
||||
plans[0].ParameterNames.ShouldBe(new[] { ":w" });
|
||||
plans[0].ParameterBindNames.ShouldBe(new[] { "w" });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On a SqlClient-style dialect the two lists coincide — which is exactly why the colon control above
|
||||
/// exists, and why this one cannot stand in for it.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OnSqlServer_theBindNamesAreTheMarkers()
|
||||
{
|
||||
var plans = SqlGroupPlanner.Plan(
|
||||
[KvTag("A", "dbo.TagValues", "tag_name", "Line1.Speed", "num_value", null)],
|
||||
new SqlServerDialect()).ToList();
|
||||
|
||||
plans[0].ParameterBindNames.ShouldBe(plans[0].ParameterNames);
|
||||
plans[0].ParameterBindNames.ShouldBe(new[] { "@k0" });
|
||||
}
|
||||
|
||||
// ---- wide row ----
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -413,6 +413,47 @@ public sealed class SqlPollReaderTests
|
||||
async () => await reader.ReadAsync(["Speed"], CancellationToken.None));
|
||||
}
|
||||
|
||||
// ---- dialect-owned parameter naming ----
|
||||
|
||||
/// <summary>
|
||||
/// Runs a real poll through a dialect whose SQL markers (<c>:k0</c>) and bind names (bare <c>k0</c>)
|
||||
/// <b>differ</b> — ODP.NET's shape — against the SQLite fixture, and asserts <em>both</em> that the
|
||||
/// values arrive and that the names actually put on the parameters are the dialect's bind names.
|
||||
/// <para><b>The name assertion is the load-bearing half.</b> Microsoft.Data.Sqlite happily resolves
|
||||
/// <c>:k0</c> and bare <c>k0</c> alike, so a reader that bound its markers would still return the
|
||||
/// right values here — and pass a values-only test — while failing outright on ODP.NET.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_bindsTheDialectsBindNames_whichNeedNotBeTheMarkersInTheSql()
|
||||
{
|
||||
using var fixture = new SqlitePollFixture();
|
||||
var dialect = new ColonMarkerDialect();
|
||||
var reader = new SqlPollReader(
|
||||
fixture.Factory, fixture.ConnectionString, dialect,
|
||||
commandTimeout: CommandTimeout, operationTimeout: OperationTimeout,
|
||||
maxConcurrentGroups: 4, nullIsBad: false,
|
||||
resolve: Resolver(
|
||||
KvTag("Speed", SqlitePollFixture.PresentKey),
|
||||
KvTag("Pressure", SqlitePollFixture.SecondPresentKey),
|
||||
WideTag("Oven", "oven_temp", selectorValue: SqlitePollFixture.PresentStation)));
|
||||
|
||||
var snapshots = await reader.ReadAsync(
|
||||
["Speed", "Pressure", "Oven"], CancellationToken.None);
|
||||
|
||||
// The IN list bound both of its distinct values, and the wide-row selector bound its one.
|
||||
snapshots[0].Value.ShouldBe(SqlitePollFixture.PresentValue);
|
||||
snapshots[1].Value.ShouldBe(SqlitePollFixture.SecondPresentValue);
|
||||
snapshots[2].Value.ShouldBe(SqlitePollFixture.PresentStationOvenTemp);
|
||||
snapshots.All(s => SqlStatusCodes.IsGood(s.StatusCode)).ShouldBeTrue();
|
||||
|
||||
// The bind names are the dialect's bare ones, NOT the ":"-prefixed markers in the SQL text.
|
||||
dialect.BoundParameterNames.ShouldBe(new[] { "k0", "k1", "w" }, ignoreOrder: true);
|
||||
|
||||
// Two groups, two commands — and the provider hook ran on each. On Oracle this is BindByName, whose
|
||||
// absence mis-binds an IN list by position instead of failing.
|
||||
dialect.ConfiguredCommands.ShouldBe(2);
|
||||
}
|
||||
|
||||
// ---- connection lifecycle ----
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -12,6 +12,7 @@ public class SqlQueryPlanImmutabilityTests
|
||||
public void Plan_doesNotAliasTheCallersLists()
|
||||
{
|
||||
var names = new List<string> { "@k0" };
|
||||
var bindNames = new List<string> { "@k0" };
|
||||
var values = new List<object?> { "v" };
|
||||
var members = new List<SqlTagDefinition>
|
||||
{
|
||||
@@ -19,14 +20,17 @@ public class SqlQueryPlanImmutabilityTests
|
||||
};
|
||||
var selected = new List<string> { "k", "c" };
|
||||
|
||||
var plan = new SqlQueryPlan(SqlTagModel.KeyValue, "gk", "SELECT 1", names, values, members, selected);
|
||||
var plan = new SqlQueryPlan(
|
||||
SqlTagModel.KeyValue, "gk", "SELECT 1", names, bindNames, values, members, selected);
|
||||
|
||||
names.Add("@k1");
|
||||
bindNames.Add("@k1");
|
||||
values.Add("other");
|
||||
members.Add(members[0]);
|
||||
selected.Add("extra");
|
||||
|
||||
plan.ParameterNames.Count.ShouldBe(1);
|
||||
plan.ParameterBindNames.Count.ShouldBe(1);
|
||||
plan.Parameters.Count.ShouldBe(1);
|
||||
plan.Members.Count.ShouldBe(1);
|
||||
plan.SelectedColumns.Count.ShouldBe(2);
|
||||
|
||||
@@ -42,6 +42,28 @@ public sealed class SqliteDialect : ISqlDialect
|
||||
/// <inheritdoc/>
|
||||
public DbProviderFactory Factory => SqliteFactory.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// <c>@</c>-prefixed, matching SQL Server — SQLite accepts <c>@</c>, <c>:</c> and <c>$</c> equally, so
|
||||
/// this dialect deliberately keeps the <em>same</em> spelling as the production one. Divergent
|
||||
/// parameter naming is the job of <c>ColonMarkerDialect</c>, which is the control for it; if this
|
||||
/// dialect diverged too, a SQL-Server-shaped assumption would have nothing left holding it in place.
|
||||
/// </summary>
|
||||
/// <param name="baseName">The bare base name.</param>
|
||||
/// <returns>The <c>@</c>-prefixed marker.</returns>
|
||||
public string ParameterMarker(string baseName) => "@" + baseName;
|
||||
|
||||
/// <summary>Identical to the marker — <c>Microsoft.Data.Sqlite</c> matches on the prefixed name.</summary>
|
||||
/// <param name="baseName">The bare base name.</param>
|
||||
/// <returns>The <c>@</c>-prefixed bind name.</returns>
|
||||
public string ParameterBindName(string baseName) => "@" + baseName;
|
||||
|
||||
/// <summary>No-op: <c>SqliteCommand</c> binds by name and needs no per-command setup.</summary>
|
||||
/// <param name="command">The command to configure; untouched here.</param>
|
||||
public void ConfigureCommand(DbCommand command)
|
||||
{
|
||||
// Intentionally empty: see the summary.
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string LivenessSql => "SELECT 1";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user