feat(sql): SqlGroupPlanner folds tags into one parameterized query per group
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using System.Data.Common;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;
|
||||
|
||||
/// <summary>
|
||||
/// One compiled poll query and the tags it feeds (design §3.6, "group execution shape"). Every tag that
|
||||
/// shares a <see cref="GroupKey"/> reads from the <b>same</b> result set, so a poll pass executes one plan
|
||||
/// per group rather than one query per tag.
|
||||
/// <para><b>Injection boundary.</b> <see cref="SqlText"/> is built from dialect-quoted identifiers only;
|
||||
/// every authored <em>value</em> lives in <see cref="Parameters"/> and is bound as a
|
||||
/// <see cref="DbParameter"/> named by the positionally-aligned <see cref="ParameterNames"/>. There is no
|
||||
/// path by which a tag's <c>keyValue</c> or <c>whereValue</c> reaches the command text.</para>
|
||||
/// <para>Produced only by <see cref="SqlGroupPlanner.Plan"/>; consumed by the poll reader, which executes
|
||||
/// <see cref="SqlText"/> once and slices the rows back to per-member snapshots.</para>
|
||||
/// </summary>
|
||||
/// <param name="Model">
|
||||
/// The tag→value mapping model every member shares. Determines how the reader slices the result set:
|
||||
/// <see cref="SqlTagModel.KeyValue"/> indexes rows by <see cref="KeyColumn"/> and matches each member's
|
||||
/// <see cref="SqlTagDefinition.KeyValue"/>; <see cref="SqlTagModel.WideRow"/> takes the single row and
|
||||
/// reads each member's <see cref="SqlTagDefinition.ColumnName"/>.
|
||||
/// </param>
|
||||
/// <param name="GroupKey">
|
||||
/// The opaque equality key that decided this grouping — a value tuple, so two plans compare equal exactly
|
||||
/// when they would have folded together. Exposed for diagnostics and plan caching, never for parsing.
|
||||
/// </param>
|
||||
/// <param name="SqlText">The single parameterized statement to execute for this group.</param>
|
||||
/// <param name="ParameterNames">
|
||||
/// The parameter markers appearing in <see cref="SqlText"/>, in the order they appear, aligned index-for-index
|
||||
/// with <see cref="Parameters"/>. Carried explicitly so the reader never has to re-derive the naming
|
||||
/// convention.
|
||||
/// </param>
|
||||
/// <param name="Parameters">
|
||||
/// The values to bind, aligned with <see cref="ParameterNames"/>. Captured verbatim from the authored tags
|
||||
/// — a hostile value is inert here because it is data, not text.
|
||||
/// </param>
|
||||
/// <param name="Members">
|
||||
/// The tags this plan feeds, in the planner's input order. Never empty. Duplicates are preserved: two tags
|
||||
/// may legitimately read the same cell, and both must receive a value.
|
||||
/// </param>
|
||||
/// <param name="SelectedColumns">
|
||||
/// The bare (unquoted) column names in <see cref="SqlText"/>'s SELECT list, in order and distinct — the
|
||||
/// reader's map from result-set ordinal to source column.
|
||||
/// </param>
|
||||
public sealed record SqlQueryPlan(
|
||||
SqlTagModel Model,
|
||||
object GroupKey,
|
||||
string SqlText,
|
||||
IReadOnlyList<string> ParameterNames,
|
||||
IReadOnlyList<object?> Parameters,
|
||||
IReadOnlyList<SqlTagDefinition> Members,
|
||||
IReadOnlyList<string> SelectedColumns)
|
||||
{
|
||||
/// <summary>
|
||||
/// The key column all members are matched on — <see langword="null"/> for any model but
|
||||
/// <see cref="SqlTagModel.KeyValue"/>. Uniform across <see cref="Members"/> by the group-key invariant,
|
||||
/// so <c>Members[0]</c> is authoritative.
|
||||
/// </summary>
|
||||
public string? KeyColumn => Model == SqlTagModel.KeyValue ? Members[0].KeyColumn : null;
|
||||
|
||||
/// <summary>
|
||||
/// The column every member's value is read from — <see langword="null"/> for any model but
|
||||
/// <see cref="SqlTagModel.KeyValue"/>. Uniform across <see cref="Members"/> by the group-key invariant.
|
||||
/// </summary>
|
||||
public string? ValueColumn => Model == SqlTagModel.KeyValue ? Members[0].ValueColumn : null;
|
||||
|
||||
/// <summary>
|
||||
/// The shared source-timestamp column, or <see langword="null"/> when the group has none (the reader
|
||||
/// then stamps the poll time). <see cref="SqlTagModel.KeyValue"/> only — it is part of that model's
|
||||
/// group key, so it is uniform. A <see cref="SqlTagModel.WideRow"/> plan deliberately allows members to
|
||||
/// carry <em>different</em> timestamp columns (all of them are selected), so the reader must read
|
||||
/// <see cref="SqlTagDefinition.TimestampColumn"/> per member there.
|
||||
/// </summary>
|
||||
public string? TimestampColumn => Model == SqlTagModel.KeyValue ? Members[0].TimestampColumn : null;
|
||||
}
|
||||
Reference in New Issue
Block a user