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:
@@ -14,8 +14,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;
|
||||
/// tables issues a handful of round-trips at Initialize and none thereafter; it never enumerates a
|
||||
/// warehouse.</para>
|
||||
/// <para><b>Every parameter is bound.</b> Authored names reach the catalog queries as
|
||||
/// <c>@schema</c>/<c>@table</c> parameters — the same discipline the schema browser follows — so loading
|
||||
/// the allow-list cannot itself be an injection vector. No identifier is quoted into text on this path.</para>
|
||||
/// <c>schema</c>/<c>table</c> parameters — the same discipline the schema browser follows — so loading
|
||||
/// the allow-list cannot itself be an injection vector. No identifier is quoted into text on this path.
|
||||
/// The bind names come from <see cref="ISqlDialect.ParameterBindName"/> over those two base names, so
|
||||
/// they can never disagree with the markers in the dialect's own catalog SQL (<c>@schema</c> on SQL
|
||||
/// Server, <c>:schema</c> on Oracle).</para>
|
||||
/// <para><b>Failures throw; they do not degrade to an empty catalog.</b> An empty catalog would reject
|
||||
/// every tag, which is indistinguishable at the OPC UA surface from a database whose objects were all
|
||||
/// dropped. A caller that cannot read the catalog has not learned that the tags are invalid — it has
|
||||
@@ -33,6 +36,12 @@ internal static class SqlCatalogLoader
|
||||
/// <summary>Column alias <see cref="ISqlDialect.ListColumnsSql"/> projects.</summary>
|
||||
private const string ColumnNameColumn = "COLUMN_NAME";
|
||||
|
||||
/// <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>
|
||||
/// Reads the catalog slice covering <paramref name="authoredTables"/>.
|
||||
/// </summary>
|
||||
@@ -53,7 +62,7 @@ internal static class SqlCatalogLoader
|
||||
var timeoutSeconds = Math.Max(1, (int)Math.Ceiling(commandTimeout.TotalSeconds));
|
||||
|
||||
var schemas = await QueryColumnAsync(
|
||||
connection, dialect.ListSchemasSql, SchemaColumn,
|
||||
connection, dialect, dialect.ListSchemasSql, SchemaColumn,
|
||||
static _ => { }, timeoutSeconds, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Zero schemas is a permissions or visibility symptom, never a real database. Rejecting every tag on
|
||||
@@ -94,8 +103,8 @@ internal static class SqlCatalogLoader
|
||||
if (!tablesBySchema.TryGetValue(schema, out var tables))
|
||||
{
|
||||
tables = await QueryColumnAsync(
|
||||
connection, dialect.ListTablesSql, TableNameColumn,
|
||||
command => Bind(command, "@schema", schema),
|
||||
connection, dialect, dialect.ListTablesSql, TableNameColumn,
|
||||
command => Bind(command, dialect.ParameterBindName(SchemaParameter), schema),
|
||||
timeoutSeconds, cancellationToken).ConfigureAwait(false);
|
||||
tablesBySchema[schema] = tables;
|
||||
}
|
||||
@@ -106,11 +115,11 @@ internal static class SqlCatalogLoader
|
||||
if (columnsByTable.ContainsKey(key)) continue;
|
||||
|
||||
columnsByTable[key] = await QueryColumnAsync(
|
||||
connection, dialect.ListColumnsSql, ColumnNameColumn,
|
||||
connection, dialect, dialect.ListColumnsSql, ColumnNameColumn,
|
||||
command =>
|
||||
{
|
||||
Bind(command, "@schema", schema);
|
||||
Bind(command, "@table", table);
|
||||
Bind(command, dialect.ParameterBindName(SchemaParameter), schema);
|
||||
Bind(command, dialect.ParameterBindName(TableParameter), table);
|
||||
},
|
||||
timeoutSeconds, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -133,6 +142,7 @@ internal static class SqlCatalogLoader
|
||||
var command = connection.CreateCommand();
|
||||
await using (command.ConfigureAwait(false))
|
||||
{
|
||||
dialect.ConfigureCommand(command);
|
||||
command.CommandText = dialect.DefaultSchemaSql;
|
||||
command.CommandTimeout = timeoutSeconds;
|
||||
var value = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -144,6 +154,7 @@ internal static class SqlCatalogLoader
|
||||
/// <summary>Runs one catalog query and projects a single string column from every row.</summary>
|
||||
private static async Task<IReadOnlyList<string>> QueryColumnAsync(
|
||||
DbConnection connection,
|
||||
ISqlDialect dialect,
|
||||
string sql,
|
||||
string columnName,
|
||||
Action<DbCommand> bind,
|
||||
@@ -155,6 +166,8 @@ internal static class SqlCatalogLoader
|
||||
var command = connection.CreateCommand();
|
||||
await using (command.ConfigureAwait(false))
|
||||
{
|
||||
// Before the binds: ODP.NET's BindByName has to be set while the parameter collection is empty.
|
||||
dialect.ConfigureCommand(command);
|
||||
command.CommandText = sql;
|
||||
command.CommandTimeout = timeoutSeconds;
|
||||
bind(command);
|
||||
|
||||
Reference in New Issue
Block a user