using System.Data.Common; using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Sql; /// /// One compiled poll query and the tags it feeds (design §3.6, "group execution shape"). Every tag that /// shares a reads from the same result set, so a poll pass executes one plan /// per group rather than one query per tag. /// Injection boundary. is built from dialect-quoted identifiers only; /// every authored value lives in and is bound as a /// named by the positionally-aligned . There is /// no path by which a tag's keyValue or whereValue reaches the command text. /// Markers and bind names are carried separately because they are not the same string on /// every provider: ODP.NET's text says :p_k0 while its is the /// bare p_k0. Both lists are positionally aligned with , so a binding site /// copies the pairing the planner minted rather than re-deriving a provider convention it does not own. /// Produced only by ; consumed by the poll reader, which executes /// once and slices the rows back to per-member snapshots. /// /// /// The tag→value mapping model every member shares. Determines how the reader slices the result set: /// indexes rows by and matches each member's /// ; takes the single row and /// reads each member's . /// /// /// 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. /// Not unique across plans. A key-value group holding more distinct keys than the dialect's /// is emitted as several plans carrying this same key /// — the chunking is an execution detail of one group, not a regrouping. /// /// The single parameterized statement to execute for this group. /// /// The parameter markers appearing in , in the order they appear, aligned index-for-index /// with . Carried explicitly so the reader never has to re-derive the naming /// convention. /// /// /// The to bind each value under, from /// , aligned index-for-index with and /// with . Equal to the marker on every provider but ODP.NET, which wants the /// bare name — which is exactly why it is a carried list and not a transformation of the marker. /// /// /// The values to bind, aligned with and . /// Captured verbatim from the authored tags — a hostile value is inert here because it is data, not text. /// /// /// 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. /// /// /// The bare (unquoted) column names in 's SELECT list, in order and distinct — the /// reader's map from result-set ordinal to source column. /// public sealed record SqlQueryPlan( SqlTagModel Model, object GroupKey, string SqlText, IReadOnlyList ParameterNames, IReadOnlyList ParameterBindNames, IReadOnlyList Parameters, IReadOnlyList Members, IReadOnlyList SelectedColumns) { // A plan is documented as safe to cache and reuse across polls, so it must not stay aliased to the // planner's mutable working lists — an IReadOnlyList holding a List is downcastable, and a // mutation would silently corrupt every later poll that reuses the plan. Snapshot at the boundary. /// The parameter markers in , aligned with . public IReadOnlyList ParameterNames { get; } = [.. ParameterNames]; /// The parameter names to bind under, aligned with . public IReadOnlyList ParameterBindNames { get; } = [.. ParameterBindNames]; /// The values to bind, aligned with . public IReadOnlyList Parameters { get; } = [.. Parameters]; /// The tags this plan feeds, in input order; duplicates preserved. public IReadOnlyList Members { get; } = [.. Members]; /// The bare SELECT-list column names, in result-set ordinal order. public IReadOnlyList SelectedColumns { get; } = [.. SelectedColumns]; /// /// The key column all members are matched on — for any model but /// . Uniform across by the group-key invariant, /// so Members[0] is authoritative. /// public string? KeyColumn => Model == SqlTagModel.KeyValue ? Members[0].KeyColumn : null; /// /// The column every member's value is read from — for any model but /// . Uniform across by the group-key invariant. /// public string? ValueColumn => Model == SqlTagModel.KeyValue ? Members[0].ValueColumn : null; /// /// The shared source-timestamp column, or when the group has none (the reader /// then stamps the poll time). only — it is part of that model's /// group key, so it is uniform. A plan deliberately allows members to /// carry different timestamp columns (all of them are selected), so the reader must read /// per member there. /// public string? TimestampColumn => Model == SqlTagModel.KeyValue ? Members[0].TimestampColumn : null; }