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:
@@ -12,8 +12,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser;
|
||||
/// <para><b>Every level is the dialect's catalog SQL</b> (<see cref="ISqlDialect.ListSchemasSql"/> /
|
||||
/// <see cref="ISqlDialect.ListTablesSql"/> / <see cref="ISqlDialect.ListColumnsSql"/>) — nothing here
|
||||
/// knows what <c>INFORMATION_SCHEMA</c> is, because Oracle and SQLite do not have it. The catalog SQL is
|
||||
/// read verbatim and <c>@schema</c> / <c>@table</c> are <b>bound as parameters</b>; no name from a NodeId
|
||||
/// is ever concatenated into a command text, so a hostile or merely awkward catalog name is inert here.
|
||||
/// read verbatim and its <c>schema</c> / <c>table</c> parameters are <b>bound as parameters</b> under
|
||||
/// <see cref="ISqlDialect.ParameterBindName"/> — so the bind names cannot drift from the markers the
|
||||
/// dialect's own SQL carries; no name from a NodeId is ever concatenated into a command text, so a
|
||||
/// hostile or merely awkward catalog name is inert here.
|
||||
/// <see cref="ISqlDialect.QuoteIdentifier"/> is deliberately <em>not</em> used on this path — the browse
|
||||
/// never needs an identifier in text.</para>
|
||||
/// <para><b>Connection ownership: this session owns the connection it is handed and closes it on
|
||||
@@ -47,6 +49,12 @@ internal sealed class SqlBrowseSession : IBrowseSession
|
||||
/// <summary>The <c>TABLE_TYPE</c> value marking a view rather than a base table.</summary>
|
||||
private const string ViewTableType = "VIEW";
|
||||
|
||||
/// <summary>Base name of the schema parameter every dialect's catalog SQL carries.</summary>
|
||||
private const string SchemaParameter = "schema";
|
||||
|
||||
/// <summary>Base name of the table parameter <see cref="ISqlDialect.ListColumnsSql"/> carries.</summary>
|
||||
private const string TableParameter = "table";
|
||||
|
||||
/// <summary>
|
||||
/// The security class every browsed column reports. The <c>Sql</c> driver is read-only in v1
|
||||
/// (design §4.3), so there is nothing else a picked column could be.
|
||||
@@ -195,7 +203,7 @@ internal sealed class SqlBrowseSession : IBrowseSession
|
||||
{
|
||||
var rows = await QueryAsync(
|
||||
_dialect.ListTablesSql,
|
||||
command => Bind(command, "@schema", schema),
|
||||
command => Bind(command, _dialect.ParameterBindName(SchemaParameter), schema),
|
||||
static reader => (
|
||||
Name: ReadString(reader, TableNameColumn),
|
||||
Type: ReadOptionalString(reader, TableTypeColumn)),
|
||||
@@ -248,8 +256,8 @@ internal sealed class SqlBrowseSession : IBrowseSession
|
||||
_dialect.ListColumnsSql,
|
||||
command =>
|
||||
{
|
||||
Bind(command, "@schema", schema);
|
||||
Bind(command, "@table", table);
|
||||
Bind(command, _dialect.ParameterBindName(SchemaParameter), schema);
|
||||
Bind(command, _dialect.ParameterBindName(TableParameter), table);
|
||||
},
|
||||
static reader => (
|
||||
Name: ReadString(reader, ColumnNameColumn),
|
||||
@@ -275,6 +283,9 @@ internal sealed class SqlBrowseSession : IBrowseSession
|
||||
|
||||
var results = new List<T>();
|
||||
await using var command = _connection.CreateCommand();
|
||||
// Before the binds: on ODP.NET this is what switches bind-by-position off, and the two-parameter
|
||||
// column query is exactly where positional binding swaps the schema and the table silently.
|
||||
_dialect.ConfigureCommand(command);
|
||||
command.CommandText = sql;
|
||||
bind(command);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user