7498d39e5b
`RawBrowseCommitMapper.BuildSqlTagConfig` emitted a WideRow blob carrying only table + columnName. A wide-row tag must also say WHICH row it reads, so both `SqlEquipmentTagParser` (the runtime authority) and `SqlTagConfigModel.Validate` (the authoring gate) refused it — a browse-committed Sql tag was never readable, first silently as BadNodeIdUnknown and, since #551 put the gate on the import, loudly at commit. `SqlBrowseSession` now states a fourth address field: the table's preferred date/time-typed column, computed from the columns its attributes call ALREADY read (no extra catalog round-trip). Candidacy is decided by `ISqlDialect.MapColumnType` reporting `DriverDataType.DateTime` — never by column name, which would order a wide-row read by a string column that merely looks like a timestamp. Multiple candidates tie-break on FIRST IN CATALOG ORDER (every dialect's column SQL orders by ordinal position), so the pick is stable across calls and servers; it is a visible, editable default the typed editor round-trips. No candidate (a table with no date/time column) is an operator-actionable refusal, not an invented selector: the only remaining selector is a whereColumn/whereValue pair whose value is data, not schema, so `DescribeUncommittableLeaf` refuses that leaf by name, names the table, and points at the tag editor. The session and the mapper now agree via `SqlBrowseAddressFields` constants rather than two independently spelled string literals — a disagreement there is silent, and is exactly the shape this defect had. `RawTagConfigGateTests`' Sql leg flips from "records the gap" to "browse-commit output passes the gate", with the no-candidate case pinned as its own test.
38 lines
2.3 KiB
C#
38 lines
2.3 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
|
|
|
|
/// <summary>
|
|
/// The keys <c>SqlBrowseSession</c> states in a browsed column's <c>AttributeInfo.AddressFields</c>, and
|
|
/// that <c>RawBrowseCommitMapper</c> reads back when it builds the committed tag's <c>TagConfig</c>.
|
|
/// <para><b>Why a shared constant rather than a literal at each end.</b> A Sql tag's binding is a tuple
|
|
/// (table + column + which row), so — exactly as for MQTT's Sparkplug tuple and
|
|
/// <c>MqttTagConfigKeys</c> — the producing session states the parts and the mapper must never
|
|
/// re-infer them. Two independently spelled string literals agree by coincidence, and a disagreement is
|
|
/// silent: the mapper simply sees no field, and emits a config missing whatever the session was trying
|
|
/// to say. That is precisely how Gitea #552 shipped — a wide-row blob with no <c>rowSelector</c>, which
|
|
/// both <see cref="SqlEquipmentTagParser"/> and the authoring gate refuse.</para>
|
|
/// <para>These are the <em>browse channel's</em> names, not the tag blob's schema. Three of them happen
|
|
/// to match a <c>TagConfig</c> key because the mapper writes them straight through;
|
|
/// <see cref="TopByTimestamp"/> matches the key <em>nested inside</em> <c>rowSelector</c> — deliberately
|
|
/// not spelled <c>timestampColumn</c>, which is the blob's unrelated top-level source-timestamp
|
|
/// field.</para>
|
|
/// </summary>
|
|
public static class SqlBrowseAddressFields
|
|
{
|
|
/// <summary>The catalog schema owning the browsed column's table (e.g. <c>dbo</c>, <c>sales</c>).</summary>
|
|
public const string Schema = "schema";
|
|
|
|
/// <summary>The unqualified table (or view) name the browsed column belongs to.</summary>
|
|
public const string Table = "table";
|
|
|
|
/// <summary>The browsed column's own name — the wide-row tag's value column.</summary>
|
|
public const string ColumnName = "columnName";
|
|
|
|
/// <summary>
|
|
/// The table's preferred date/time-typed column, when it has one: the commit writes it as the
|
|
/// wide-row <c>rowSelector.topByTimestamp</c>, i.e. "read the newest row". Absent when the table
|
|
/// carries no date/time column at all, which is not a shape a browse can bind — see
|
|
/// <c>RawBrowseCommitMapper.DescribeUncommittableLeaf</c>.
|
|
/// </summary>
|
|
public const string TopByTimestamp = "topByTimestamp";
|
|
}
|