merge: #552 — Sql browse-commit states a topByTimestamp row selector from column-type metadata (catalog-order tie-break); no-candidate leaves refused in words at the modal
This commit is contained in:
@@ -341,18 +341,43 @@ public static class RawBrowseCommitMapper
|
||||
/// <param name="addressFields">The structured address the browse session stated, if any.</param>
|
||||
/// <returns>The operator-facing reason, or <c>null</c> when the leaf is committable.</returns>
|
||||
/// <remarks>
|
||||
/// Only MQTT can fail this today, and only in one shape: a leaf whose attribute lookup returned
|
||||
/// nothing, so no address was stated. Committing it anyway is the defect this whole seam exists
|
||||
/// to close — a Sparkplug tuple cannot be reconstructed from the node id, so the row would deploy
|
||||
/// clean and then report <c>BadNodeIdUnknown</c> forever with nothing to point at. Every
|
||||
/// single-reference driver is unaffected: its address IS the node id.
|
||||
/// <para>
|
||||
/// Exactly two drivers can fail this, and both for the same underlying reason: their binding is a
|
||||
/// TUPLE, so a leaf can be selectable in the tree and still not carry enough to bind. Every
|
||||
/// single-reference driver is unaffected — its address IS the node id.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>MQTT</b>: a leaf whose attribute lookup returned nothing, so no address was stated. Committing
|
||||
/// it anyway is the defect this whole seam exists to close — a Sparkplug tuple cannot be
|
||||
/// reconstructed from the node id, so the row would deploy clean and then report
|
||||
/// <c>BadNodeIdUnknown</c> forever with nothing to point at.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Sql</b> (Gitea #552): a column of a table carrying no date/time column. A wide-row tag must say
|
||||
/// which ROW it reads, and with no timestamp to order by the only remaining selector is a
|
||||
/// <c>whereColumn</c>/<c>whereValue</c> pair — whose value is <em>data</em>, not schema, and therefore
|
||||
/// is not something any browse can know. Refusing here rather than letting the import's own gate
|
||||
/// refuse the blob is what makes the message name the table and the fix; the gate would only report
|
||||
/// that a row selector is missing, on every selected column at once.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static string? DescribeUncommittableLeaf(
|
||||
string driverType,
|
||||
string browseName,
|
||||
IReadOnlyDictionary<string, string>? addressFields)
|
||||
{
|
||||
if (!Is(driverType, DriverTypeNames.Mqtt)) return null;
|
||||
if (Is(driverType, DriverTypeNames.Mqtt)) return DescribeUncommittableMqttLeaf(browseName, addressFields);
|
||||
if (Is(driverType, DriverTypeNames.Sql)) return DescribeUncommittableSqlLeaf(browseName, addressFields);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Describes an MQTT leaf that states no address at all, or <c>null</c> when it states one.</summary>
|
||||
/// <param name="browseName">The leaf's browse name, for the message.</param>
|
||||
/// <param name="addressFields">The structured address the browse session stated, if any.</param>
|
||||
/// <returns>The operator-facing reason, or <c>null</c>.</returns>
|
||||
private static string? DescribeUncommittableMqttLeaf(
|
||||
string browseName, IReadOnlyDictionary<string, string>? addressFields)
|
||||
{
|
||||
if (TryGetField(addressFields, MqttTagConfigKeys.MetricName) is not null) return null;
|
||||
if (TryGetField(addressFields, MqttTagConfigKeys.Topic) is not null) return null;
|
||||
|
||||
@@ -361,6 +386,31 @@ public static class RawBrowseCommitMapper
|
||||
+ "the tag manually.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a Sql column leaf whose wide-row binding cannot be completed from the catalog, or
|
||||
/// <c>null</c> when it can (Gitea #552).
|
||||
/// </summary>
|
||||
/// <param name="browseName">The leaf's browse name, for the message.</param>
|
||||
/// <param name="addressFields">The structured address the browse session stated, if any.</param>
|
||||
/// <returns>The operator-facing reason, or <c>null</c>.</returns>
|
||||
private static string? DescribeUncommittableSqlLeaf(
|
||||
string browseName, IReadOnlyDictionary<string, string>? addressFields)
|
||||
{
|
||||
if (TryGetField(addressFields, SqlBrowseAddressFields.Table) is not { } table)
|
||||
{
|
||||
return $"'{browseName}' could not be committed: the browse session reported no Sql table for it "
|
||||
+ "(its attribute lookup returned nothing). Re-open the browser and re-select it, or author "
|
||||
+ "the tag manually.";
|
||||
}
|
||||
|
||||
if (TryGetField(addressFields, SqlBrowseAddressFields.TopByTimestamp) is not null) return null;
|
||||
|
||||
return $"'{browseName}' could not be committed: '{table}' has no date/time column, so which ROW the "
|
||||
+ "tag reads cannot be derived from the schema. Author it in the tag editor with a "
|
||||
+ "whereColumn/whereValue row selector — the value it matches on is data, not schema, so the "
|
||||
+ "browser cannot supply it.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the browse leaf's driver-native data type out of the stated address fields, falling
|
||||
/// back to <paramref name="fallback"/> — the driver model's own default — when the browser
|
||||
@@ -438,17 +488,49 @@ public static class RawBrowseCommitMapper
|
||||
/// <para>Falls back to the generic key when the fields are absent — an older browser build, or a
|
||||
/// hand-made selection — rather than emitting a half-built Sql blob.</para>
|
||||
/// </summary>
|
||||
/// <param name="address">The leaf's browse node id — the column-name fallback only.</param>
|
||||
/// <param name="addressFields">The structured address the browse session stated.</param>
|
||||
/// <returns>The serialised <c>TagConfig</c> JSON.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>A wide-row tag needs three things, not two: the table, the column, and WHICH ROW</b>
|
||||
/// (Gitea #552). This branch used to emit only the first two, and a selector-less wide-row blob is
|
||||
/// refused by <c>SqlEquipmentTagParser</c> and by <c>SqlTagConfigModel.Validate</c> alike — so every
|
||||
/// browse-committed Sql tag was unreadable, first silently (<c>BadNodeIdUnknown</c> forever) and then,
|
||||
/// once #551 put the gate on the import, loudly at commit.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The row selector is READ from the session's stated fields, never inferred here</b> — same rule
|
||||
/// as MQTT's Sparkplug tuple. The session holds the table's column metadata and names a date/time
|
||||
/// column by TYPE (see <c>SqlBrowseSession.PreferredTimestampColumn</c>); this mapper has only a
|
||||
/// column name and a table name, from which any selector it invented would be a guess about data.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>When no candidate was stated the blob stays selector-less on purpose.</b> The alternatives are
|
||||
/// worse: a fabricated selector binds the tag to a row nobody chose, and dropping to the generic
|
||||
/// <c>{"address": …}</c> key throws away the table the session did know. Kept in the wide-row shape,
|
||||
/// the blob opens in the typed editor with everything but the selector filled in — and the commit
|
||||
/// itself does not reach here, because <see cref="DescribeUncommittableLeaf"/> refuses that leaf in
|
||||
/// words first.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The blob's top-level <c>timestampColumn</c> (the SOURCE-timestamp column, which makes the driver
|
||||
/// stamp values with the row's time instead of the poll time) is deliberately left unset: ordering by
|
||||
/// a column is not a claim that it timestamps the value, and turning it on silently would change every
|
||||
/// browse-committed tag's published SourceTimestamp.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static string BuildSqlTagConfig(string address, IReadOnlyDictionary<string, string>? addressFields)
|
||||
{
|
||||
if (addressFields is null
|
||||
|| !addressFields.TryGetValue("table", out var table)
|
||||
|| !addressFields.TryGetValue(SqlBrowseAddressFields.Table, out var table)
|
||||
|| string.IsNullOrWhiteSpace(table))
|
||||
{
|
||||
return WriteSingleKey("address", address);
|
||||
}
|
||||
|
||||
addressFields.TryGetValue("schema", out var schema);
|
||||
addressFields.TryGetValue("columnName", out var columnName);
|
||||
addressFields.TryGetValue(SqlBrowseAddressFields.Schema, out var schema);
|
||||
addressFields.TryGetValue(SqlBrowseAddressFields.ColumnName, out var columnName);
|
||||
|
||||
// Qualify the table with its schema when the schema is not the default, so a tag authored against
|
||||
// "sales.Readings" cannot silently resolve to "dbo.Readings".
|
||||
@@ -461,6 +543,13 @@ public static class RawBrowseCommitMapper
|
||||
Model = SqlTagModel.WideRow,
|
||||
Table = qualified,
|
||||
ColumnName = string.IsNullOrWhiteSpace(columnName) ? address : columnName,
|
||||
|
||||
// Null when the session stated none: SqlRowSelectorModel.ToJsonObject then returns null and the
|
||||
// "rowSelector" key is omitted entirely, rather than written as an empty or blank-valued object.
|
||||
RowSelector = new SqlRowSelectorModel
|
||||
{
|
||||
TopByTimestamp = TryGetField(addressFields, SqlBrowseAddressFields.TopByTimestamp),
|
||||
},
|
||||
}.ToJson();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user