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:
@@ -50,6 +50,58 @@ public interface ISqlDialect
|
||||
/// <exception cref="ArgumentException">The value cannot be a valid catalog identifier.</exception>
|
||||
string QuoteIdentifier(string ident);
|
||||
|
||||
/// <summary>
|
||||
/// The token that stands for a bound parameter <b>in the command text</b> — <c>"@k0"</c> for
|
||||
/// SqlClient/Npgsql, <c>":k0"</c> for ODP.NET — given the caller's bare base name (<c>"k0"</c>,
|
||||
/// <c>"w"</c>, <c>"schema"</c>).
|
||||
/// <para><b>Why the marker and the bind name are two members.</b> They are the same string on every
|
||||
/// provider but Oracle, where the SQL text carries <c>:schema</c> while
|
||||
/// <see cref="DbParameter.ParameterName"/> is the bare <c>schema</c>. Modelling one and deriving the
|
||||
/// other means a call site guessing at a convention it does not own — the exact defect this seam
|
||||
/// exists to prevent, and one that surfaces as a runtime bind failure (or, worse under
|
||||
/// bind-by-position, as a <em>silently mis-bound value</em>) rather than a compile error.</para>
|
||||
/// <para>The base name is caller-owned and never operator input: the poll planner mints
|
||||
/// <c>k0…kN</c>/<c>w</c>, the catalog path uses <c>schema</c>/<c>table</c>. Authored <em>values</em>
|
||||
/// ride the parameter; authored <em>identifiers</em> go through <see cref="QuoteIdentifier"/>. Nothing
|
||||
/// else reaches a command text.</para>
|
||||
/// </summary>
|
||||
/// <param name="baseName">The bare base name, without any provider prefix.</param>
|
||||
/// <returns>The marker to embed in the SQL text.</returns>
|
||||
string ParameterMarker(string baseName);
|
||||
|
||||
/// <summary>
|
||||
/// The value to assign to <see cref="DbParameter.ParameterName"/> for the same
|
||||
/// <paramref name="baseName"/> — <c>"@k0"</c> for SqlClient-style providers (which expect the marker
|
||||
/// verbatim), the bare <c>"k0"</c> for ODP.NET-style ones (whose documented convention is a
|
||||
/// prefix-less bind name).
|
||||
/// <para>Always paired with <see cref="ParameterMarker"/> over the <em>same</em> base name: a plan
|
||||
/// carries both lists and every binding site <b>copies</b> them index-for-index rather than
|
||||
/// re-deriving either from the other.</para>
|
||||
/// </summary>
|
||||
/// <param name="baseName">The bare base name, without any provider prefix.</param>
|
||||
/// <returns>The parameter name to bind under.</returns>
|
||||
string ParameterBindName(string baseName);
|
||||
|
||||
/// <summary>
|
||||
/// Applies provider-specific setup to a freshly created <see cref="DbCommand"/>, <b>before</b> its
|
||||
/// parameters are added or it is executed. A no-op for providers that need none.
|
||||
/// <para><b>Oracle uses this to set <c>OracleCommand.BindByName = true</c></b>, and that call is the
|
||||
/// sharpest correctness edge on this seam: ODP.NET binds <em>by position</em> by default, so a
|
||||
/// multi-parameter statement (the planner's <c>IN (:k0, :k1, …)</c> list, the catalog path's
|
||||
/// <c>schema</c> + <c>table</c> pair) would bind values to the wrong markers whenever the parameter
|
||||
/// order and the marker order differ — publishing one tag's value onto another tag's node with no
|
||||
/// error anywhere. Every command that carries parameters — the poll path, the catalog path, the
|
||||
/// browse path — therefore calls this immediately after <c>CreateCommand()</c> and <b>before</b> the
|
||||
/// first parameter is added. (Bind mode cannot affect a parameterless statement, so the liveness
|
||||
/// ones are indifferent: <c>SqlDriverProbe</c> calls it anyway for uniformity,
|
||||
/// <c>SqlDriver.PingAsync</c> does not.)</para>
|
||||
/// <para>Implementations must tolerate <em>any</em> <see cref="DbCommand"/> they are handed: the
|
||||
/// driver can be pointed at an instrumented or substituted factory, so a dialect that casts to its own
|
||||
/// provider type must do so defensively rather than throwing.</para>
|
||||
/// </summary>
|
||||
/// <param name="command">The command to configure.</param>
|
||||
void ConfigureCommand(DbCommand command);
|
||||
|
||||
/// <summary>Cheapest statement that proves the connection is alive (<c>SELECT 1</c>; Oracle needs <c>FROM DUAL</c>).</summary>
|
||||
string LivenessSql { get; }
|
||||
|
||||
@@ -105,10 +157,19 @@ public interface ISqlDialect
|
||||
/// </remarks>
|
||||
string DefaultSchemaSql { get; }
|
||||
|
||||
/// <summary>Catalog query listing tables + views in one schema — the browser's schema-expand level. Bind <c>@schema</c>.</summary>
|
||||
/// <summary>
|
||||
/// Catalog query listing tables + views in one schema — the browser's schema-expand level. Carries one
|
||||
/// parameter under the base name <c>schema</c>, spelled with this dialect's own
|
||||
/// <see cref="ParameterMarker"/> (<c>@schema</c> on SqlClient, <c>:schema</c> on ODP.NET); callers bind
|
||||
/// it under <see cref="ParameterBindName"/> of the same base name.
|
||||
/// </summary>
|
||||
string ListTablesSql { get; }
|
||||
|
||||
/// <summary>Catalog query listing columns of one table — the browser's table-expand / attributes level. Bind <c>@schema</c> and <c>@table</c>.</summary>
|
||||
/// <summary>
|
||||
/// Catalog query listing columns of one table — the browser's table-expand / attributes level. Carries
|
||||
/// parameters under the base names <c>schema</c> and <c>table</c>, spelled and bound exactly as
|
||||
/// <see cref="ListTablesSql"/> describes.
|
||||
/// </summary>
|
||||
string ListColumnsSql { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -27,6 +27,34 @@ public sealed class SqlServerDialect : ISqlDialect
|
||||
/// <inheritdoc/>
|
||||
public DbProviderFactory Factory => SqlClientFactory.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// T-SQL markers are <c>@</c>-prefixed — <c>ParameterMarker("k0")</c> is <c>"@k0"</c>.
|
||||
/// </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 <see cref="ParameterMarker"/>: <c>Microsoft.Data.SqlClient</c> expects
|
||||
/// <see cref="DbParameter.ParameterName"/> to carry the same <c>@</c> the statement does. (It also
|
||||
/// accepts a bare name and prepends the <c>@</c> itself, but spelling it out keeps the parameter
|
||||
/// collection readable in a profiler trace and matches what the SQL text says.)
|
||||
/// </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>SqlCommand</c> binds by name out of the box, so there is nothing SQL Server needs set on a
|
||||
/// fresh command — the member exists for Oracle's <c>BindByName</c> (see
|
||||
/// <see cref="ISqlDialect.ConfigureCommand"/>).
|
||||
/// </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