fix(sql): bind string keys untyped on Postgres so integer key columns compare (#602 STAB-24)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -107,6 +107,21 @@ public interface ISqlDialect
|
||||
/// <param name="command">The command to configure.</param>
|
||||
void ConfigureCommand(DbCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Gives the dialect a chance to adjust a poll-path parameter after its name and value are set and
|
||||
/// before it is added to the command (STAB-24). Authored key / row-selector values are CLR strings,
|
||||
/// because a tag's <c>keyValue</c> is configuration text — which a strictly-typed provider will not
|
||||
/// compare against a numeric key column (Npgsql sends <c>text</c>, and
|
||||
/// <c>integer = text</c> has no operator). A dialect that needs the server to infer the type from
|
||||
/// context overrides this. The default does nothing, which is correct for providers that coerce
|
||||
/// implicitly (SQL Server, ODP.NET).
|
||||
/// <para>Implementations must tolerate any <see cref="DbParameter"/> they are handed, for the same
|
||||
/// substituted-factory reason as <see cref="ConfigureCommand"/>.</para>
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter, with <see cref="DbParameter.ParameterName"/> and value already set.</param>
|
||||
/// <param name="value">The authored value being bound (never <see cref="DBNull"/>; may be <c>null</c>).</param>
|
||||
void ConfigureParameter(DbParameter parameter, object? value) { }
|
||||
|
||||
/// <summary>
|
||||
/// The most bound parameters this backend accepts in <b>one</b> command — 2098 usable of SQL Server's
|
||||
/// 2100, 1000 entries in an Oracle <c>IN</c> expression list, 65535 on the Postgres wire protocol.
|
||||
|
||||
@@ -64,6 +64,21 @@ public sealed class PostgresDialect : ISqlDialect
|
||||
// Intentionally empty: see the summary.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// STAB-24: a string-valued bind is sent with <see cref="NpgsqlTypes.NpgsqlDbType.Unknown"/> so the
|
||||
/// server infers its type from the compared column. Bound as the default <c>text</c>, a tag whose
|
||||
/// <c>keyValue</c> is <c>"3"</c> against an <c>integer</c> key column fails with
|
||||
/// <c>operator does not exist: integer = text</c>. Unknown still carries the value as data — it is
|
||||
/// never interpolated — so the injection boundary is unchanged.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to adjust.</param>
|
||||
/// <param name="value">The authored value.</param>
|
||||
public void ConfigureParameter(DbParameter parameter, object? value)
|
||||
{
|
||||
if (parameter is NpgsqlParameter npgsql && value is string)
|
||||
npgsql.NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Unknown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 65535 — the extended-query protocol carries the parameter count as a signed/unsigned 16-bit field
|
||||
/// in <c>Bind</c>, so that is a protocol ceiling rather than a server setting, and Npgsql raises it
|
||||
|
||||
@@ -405,6 +405,7 @@ public sealed class SqlPollReader
|
||||
// and never from ParameterNames: the marker and the bind name differ on ODP.NET.
|
||||
parameter.ParameterName = plan.ParameterBindNames[i];
|
||||
parameter.Value = plan.Parameters[i] ?? DBNull.Value;
|
||||
_dialect.ConfigureParameter(parameter, plan.Parameters[i]);
|
||||
command.Parameters.Add(parameter);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user