using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Threading; using System.Threading.Tasks; using Serilog; using ZB.MOM.WW.LmxOpcUa.Host.Configuration; using ZB.MOM.WW.LmxOpcUa.Host.Domain; namespace ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository { /// /// Implements IGalaxyRepository using SQL queries against the Galaxy ZB database. (GR-001 through GR-007) /// public class GalaxyRepositoryService : IGalaxyRepository { private static readonly ILogger Log = Serilog.Log.ForContext(); private readonly GalaxyRepositoryConfiguration _config; public event Action? OnGalaxyChanged; #region SQL Queries (GR-006: const string, no dynamic SQL) private const string HierarchySql = @" SELECT DISTINCT g.gobject_id, g.tag_name, g.contained_name, CASE WHEN g.contained_name IS NULL OR g.contained_name = '' THEN g.tag_name ELSE g.contained_name END AS browse_name, CASE WHEN g.contained_by_gobject_id = 0 THEN g.area_gobject_id ELSE g.contained_by_gobject_id END AS parent_gobject_id, CASE WHEN td.category_id = 13 THEN 1 ELSE 0 END AS is_area FROM gobject g INNER JOIN template_definition td ON g.template_definition_id = td.template_definition_id WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) AND g.is_template = 0 AND g.deployed_package_id <> 0 ORDER BY parent_gobject_id, g.tag_name"; private const string AttributesSql = @" ;WITH template_chain AS ( SELECT g.gobject_id, g.derived_from_gobject_id, 0 AS depth FROM gobject g WHERE g.is_template = 0 UNION ALL SELECT tc.gobject_id, t.derived_from_gobject_id, tc.depth + 1 FROM template_chain tc INNER JOIN gobject t ON t.gobject_id = tc.derived_from_gobject_id WHERE tc.derived_from_gobject_id <> 0 AND tc.depth < 10 ) SELECT DISTINCT g.gobject_id, g.tag_name, da.attribute_name, g.tag_name + '.' + da.attribute_name + CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END AS full_tag_reference, da.mx_data_type, dt.description AS data_type_name, da.is_array, CASE WHEN da.is_array = 1 THEN CONVERT(int, CONVERT(varbinary(2), SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2)) ELSE NULL END AS array_dimension, da.mx_attribute_category, da.security_classification FROM template_chain tc INNER JOIN dynamic_attribute da ON da.gobject_id = tc.derived_from_gobject_id INNER JOIN gobject g ON g.gobject_id = tc.gobject_id INNER JOIN template_definition td ON td.template_definition_id = g.template_definition_id LEFT JOIN data_type dt ON dt.mx_data_type = da.mx_data_type WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) AND g.is_template = 0 AND g.deployed_package_id <> 0 AND da.attribute_name NOT LIKE '[_]%' AND da.attribute_name NOT LIKE '%.Description' AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24) ORDER BY g.tag_name, da.attribute_name"; private const string ExtendedAttributesSql = @" ;WITH template_chain AS ( SELECT g.gobject_id, g.derived_from_gobject_id, 0 AS depth FROM gobject g WHERE g.is_template = 0 UNION ALL SELECT tc.gobject_id, t.derived_from_gobject_id, tc.depth + 1 FROM template_chain tc INNER JOIN gobject t ON t.gobject_id = tc.derived_from_gobject_id WHERE tc.derived_from_gobject_id <> 0 AND tc.depth < 10 ) SELECT DISTINCT gobject_id, tag_name, primitive_name, attribute_name, full_tag_reference, mx_data_type, data_type_name, is_array, array_dimension, mx_attribute_category, security_classification, attribute_source FROM ( SELECT g.gobject_id, g.tag_name, pi.primitive_name, ad.attribute_name, CASE WHEN pi.primitive_name = '' THEN g.tag_name + '.' + ad.attribute_name ELSE g.tag_name + '.' + pi.primitive_name + '.' + ad.attribute_name END + CASE WHEN ad.is_array = 1 THEN '[]' ELSE '' END AS full_tag_reference, ad.mx_data_type, dt.description AS data_type_name, ad.is_array, CASE WHEN ad.is_array = 1 THEN CONVERT(int, CONVERT(varbinary(2), SUBSTRING(ad.mx_value, 15, 2) + SUBSTRING(ad.mx_value, 13, 2), 2)) ELSE NULL END AS array_dimension, ad.mx_attribute_category, ad.security_classification, 'primitive' AS attribute_source FROM gobject g INNER JOIN instance i ON i.gobject_id = g.gobject_id INNER JOIN template_definition td ON td.template_definition_id = g.template_definition_id AND td.runtime_clsid <> '{00000000-0000-0000-0000-000000000000}' INNER JOIN package p ON p.package_id = g.checked_in_package_id INNER JOIN primitive_instance pi ON pi.package_id = p.package_id AND pi.property_bitmask & 0x10 <> 0x10 INNER JOIN attribute_definition ad ON ad.primitive_definition_id = pi.primitive_definition_id AND ad.attribute_name NOT LIKE '[_]%' AND ad.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24) LEFT JOIN data_type dt ON dt.mx_data_type = ad.mx_data_type WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) AND g.is_template = 0 AND g.deployed_package_id <> 0 UNION ALL SELECT g.gobject_id, g.tag_name, '' AS primitive_name, da.attribute_name, g.tag_name + '.' + da.attribute_name + CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END AS full_tag_reference, da.mx_data_type, dt.description AS data_type_name, da.is_array, CASE WHEN da.is_array = 1 THEN CONVERT(int, CONVERT(varbinary(2), SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2)) ELSE NULL END AS array_dimension, da.mx_attribute_category, da.security_classification, 'dynamic' AS attribute_source FROM template_chain tc INNER JOIN dynamic_attribute da ON da.gobject_id = tc.derived_from_gobject_id INNER JOIN gobject g ON g.gobject_id = tc.gobject_id INNER JOIN template_definition td ON td.template_definition_id = g.template_definition_id LEFT JOIN data_type dt ON dt.mx_data_type = da.mx_data_type WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) AND g.is_template = 0 AND g.deployed_package_id <> 0 AND da.attribute_name NOT LIKE '[_]%' AND da.attribute_name NOT LIKE '%.Description' AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24) ) all_attributes ORDER BY tag_name, primitive_name, attribute_name"; private const string ChangeDetectionSql = "SELECT time_of_last_deploy FROM galaxy"; private const string TestConnectionSql = "SELECT 1"; #endregion public GalaxyRepositoryService(GalaxyRepositoryConfiguration config) { _config = config; } public async Task> GetHierarchyAsync(CancellationToken ct = default) { var results = new List(); using var conn = new SqlConnection(_config.ConnectionString); await conn.OpenAsync(ct); using var cmd = new SqlCommand(HierarchySql, conn) { CommandTimeout = _config.CommandTimeoutSeconds }; using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { results.Add(new GalaxyObjectInfo { GobjectId = Convert.ToInt32(reader.GetValue(0)), TagName = reader.GetString(1), ContainedName = reader.IsDBNull(2) ? "" : reader.GetString(2), BrowseName = reader.GetString(3), ParentGobjectId = Convert.ToInt32(reader.GetValue(4)), IsArea = Convert.ToInt32(reader.GetValue(5)) == 1 }); } if (results.Count == 0) Log.Warning("GetHierarchyAsync returned zero rows"); else Log.Information("GetHierarchyAsync returned {Count} objects", results.Count); return results; } public async Task> GetAttributesAsync(CancellationToken ct = default) { var results = new List(); var extended = _config.ExtendedAttributes; var sql = extended ? ExtendedAttributesSql : AttributesSql; using var conn = new SqlConnection(_config.ConnectionString); await conn.OpenAsync(ct); using var cmd = new SqlCommand(sql, conn) { CommandTimeout = _config.CommandTimeoutSeconds }; using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { results.Add(extended ? ReadExtendedAttribute(reader) : ReadStandardAttribute(reader)); } Log.Information("GetAttributesAsync returned {Count} attributes (extended={Extended})", results.Count, extended); return results; } /// /// Reads a row from the standard attributes query (10 columns). /// Columns: gobject_id, tag_name, attribute_name, full_tag_reference, mx_data_type, /// data_type_name, is_array, array_dimension, mx_attribute_category, security_classification /// private static GalaxyAttributeInfo ReadStandardAttribute(SqlDataReader reader) { return new GalaxyAttributeInfo { GobjectId = Convert.ToInt32(reader.GetValue(0)), TagName = reader.GetString(1), AttributeName = reader.GetString(2), FullTagReference = reader.GetString(3), MxDataType = Convert.ToInt32(reader.GetValue(4)), DataTypeName = reader.IsDBNull(5) ? "" : reader.GetString(5), IsArray = Convert.ToBoolean(reader.GetValue(6)), ArrayDimension = reader.IsDBNull(7) ? null : (int?)Convert.ToInt32(reader.GetValue(7)) }; } /// /// Reads a row from the extended attributes query (12 columns). /// Columns: gobject_id, tag_name, primitive_name, attribute_name, full_tag_reference, /// mx_data_type, data_type_name, is_array, array_dimension, /// mx_attribute_category, security_classification, attribute_source /// private static GalaxyAttributeInfo ReadExtendedAttribute(SqlDataReader reader) { return new GalaxyAttributeInfo { GobjectId = Convert.ToInt32(reader.GetValue(0)), TagName = reader.GetString(1), PrimitiveName = reader.IsDBNull(2) ? "" : reader.GetString(2), AttributeName = reader.GetString(3), FullTagReference = reader.GetString(4), MxDataType = Convert.ToInt32(reader.GetValue(5)), DataTypeName = reader.IsDBNull(6) ? "" : reader.GetString(6), IsArray = Convert.ToBoolean(reader.GetValue(7)), ArrayDimension = reader.IsDBNull(8) ? null : (int?)Convert.ToInt32(reader.GetValue(8)), AttributeSource = reader.IsDBNull(11) ? "" : reader.GetString(11) }; } public async Task GetLastDeployTimeAsync(CancellationToken ct = default) { using var conn = new SqlConnection(_config.ConnectionString); await conn.OpenAsync(ct); using var cmd = new SqlCommand(ChangeDetectionSql, conn) { CommandTimeout = _config.CommandTimeoutSeconds }; var result = await cmd.ExecuteScalarAsync(ct); return result is DateTime dt ? dt : null; } public async Task TestConnectionAsync(CancellationToken ct = default) { try { using var conn = new SqlConnection(_config.ConnectionString); await conn.OpenAsync(ct); using var cmd = new SqlCommand(TestConnectionSql, conn) { CommandTimeout = _config.CommandTimeoutSeconds }; await cmd.ExecuteScalarAsync(ct); Log.Information("Galaxy repository database connection successful"); return true; } catch (Exception ex) { Log.Warning(ex, "Galaxy repository database connection failed"); return false; } } public void RaiseGalaxyChanged() => OnGalaxyChanged?.Invoke(); } }