using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy; /// /// SQL access to the Galaxy ZB repository — port of v1 GalaxyRepositoryService. /// The two SQL bodies (Hierarchy + Attributes) are byte-for-byte identical to v1 so the /// queries surface the same row set at parity time. Extended-attributes and scope-filter /// queries from v1 are intentionally not ported yet — they're refinements that aren't on /// the Phase 2 critical path. /// public sealed class GalaxyRepository(GalaxyRepositoryOptions options) { public async Task TestConnectionAsync(CancellationToken ct = default) { try { using var conn = new SqlConnection(options.ConnectionString); await conn.OpenAsync(ct).ConfigureAwait(false); using var cmd = new SqlCommand("SELECT 1", conn) { CommandTimeout = options.CommandTimeoutSeconds }; var result = await cmd.ExecuteScalarAsync(ct).ConfigureAwait(false); return result is int i && i == 1; } catch (SqlException) { return false; } catch (InvalidOperationException) { return false; } } public async Task GetLastDeployTimeAsync(CancellationToken ct = default) { using var conn = new SqlConnection(options.ConnectionString); await conn.OpenAsync(ct).ConfigureAwait(false); using var cmd = new SqlCommand("SELECT time_of_last_deploy FROM galaxy", conn) { CommandTimeout = options.CommandTimeoutSeconds }; var result = await cmd.ExecuteScalarAsync(ct).ConfigureAwait(false); return result is DateTime dt ? dt : null; } public async Task> GetHierarchyAsync(CancellationToken ct = default) { var rows = new List(); using var conn = new SqlConnection(options.ConnectionString); await conn.OpenAsync(ct).ConfigureAwait(false); using var cmd = new SqlCommand(HierarchySql, conn) { CommandTimeout = options.CommandTimeoutSeconds }; using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); while (await reader.ReadAsync(ct).ConfigureAwait(false)) { var templateChainRaw = reader.IsDBNull(8) ? string.Empty : reader.GetString(8); var templateChain = templateChainRaw.Length == 0 ? Array.Empty() : templateChainRaw.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .Where(s => s.Length > 0) .ToArray(); rows.Add(new GalaxyHierarchyRow { GobjectId = Convert.ToInt32(reader.GetValue(0)), TagName = reader.GetString(1), ContainedName = reader.IsDBNull(2) ? string.Empty : reader.GetString(2), BrowseName = reader.GetString(3), ParentGobjectId = Convert.ToInt32(reader.GetValue(4)), IsArea = Convert.ToInt32(reader.GetValue(5)) == 1, CategoryId = Convert.ToInt32(reader.GetValue(6)), HostedByGobjectId = Convert.ToInt32(reader.GetValue(7)), TemplateChain = templateChain, }); } return rows; } public async Task> GetAttributesAsync(CancellationToken ct = default) { var rows = new List(); using var conn = new SqlConnection(options.ConnectionString); await conn.OpenAsync(ct).ConfigureAwait(false); using var cmd = new SqlCommand(AttributesSql, conn) { CommandTimeout = options.CommandTimeoutSeconds }; using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false); while (await reader.ReadAsync(ct).ConfigureAwait(false)) { rows.Add(new GalaxyAttributeRow { 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) ? null : reader.GetString(5), IsArray = Convert.ToInt32(reader.GetValue(6)) == 1, ArrayDimension = reader.IsDBNull(7) ? (int?)null : Convert.ToInt32(reader.GetValue(7)), MxAttributeCategory = Convert.ToInt32(reader.GetValue(8)), SecurityClassification = Convert.ToInt32(reader.GetValue(9)), IsHistorized = Convert.ToInt32(reader.GetValue(10)) == 1, IsAlarm = Convert.ToInt32(reader.GetValue(11)) == 1, }); } return rows; } private const string HierarchySql = @" ;WITH template_chain AS ( SELECT g.gobject_id AS instance_gobject_id, t.gobject_id AS template_gobject_id, t.tag_name AS template_tag_name, t.derived_from_gobject_id, 0 AS depth FROM gobject g INNER JOIN gobject t ON t.gobject_id = g.derived_from_gobject_id WHERE g.is_template = 0 AND g.deployed_package_id <> 0 AND g.derived_from_gobject_id <> 0 UNION ALL SELECT tc.instance_gobject_id, t.gobject_id, t.tag_name, 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, 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, td.category_id AS category_id, g.hosted_by_gobject_id AS hosted_by_gobject_id, ISNULL( STUFF(( SELECT '|' + tc.template_tag_name FROM template_chain tc WHERE tc.instance_gobject_id = g.gobject_id ORDER BY tc.depth FOR XML PATH('') ), 1, 1, ''), '' ) AS template_chain 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 deployed_package_chain AS ( SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth FROM gobject g INNER JOIN package p ON p.package_id = g.deployed_package_id WHERE g.is_template = 0 AND g.deployed_package_id <> 0 UNION ALL SELECT dpc.gobject_id, p.package_id, p.derived_from_package_id, dpc.depth + 1 FROM deployed_package_chain dpc INNER JOIN package p ON p.package_id = dpc.derived_from_package_id WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10 ) SELECT gobject_id, tag_name, attribute_name, full_tag_reference, mx_data_type, data_type_name, is_array, array_dimension, mx_attribute_category, security_classification, is_historized, is_alarm FROM ( SELECT dpc.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, CASE WHEN EXISTS ( SELECT 1 FROM deployed_package_chain dpc2 INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = da.attribute_name INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'HistoryExtension' WHERE dpc2.gobject_id = dpc.gobject_id ) THEN 1 ELSE 0 END AS is_historized, CASE WHEN EXISTS ( SELECT 1 FROM deployed_package_chain dpc2 INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = da.attribute_name INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'AlarmExtension' WHERE dpc2.gobject_id = dpc.gobject_id ) THEN 1 ELSE 0 END AS is_alarm, ROW_NUMBER() OVER ( PARTITION BY dpc.gobject_id, da.attribute_name ORDER BY dpc.depth ) AS rn FROM deployed_package_chain dpc INNER JOIN dynamic_attribute da ON da.package_id = dpc.package_id INNER JOIN gobject g ON g.gobject_id = dpc.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 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) ) ranked WHERE rn = 1 ORDER BY tag_name, attribute_name"; }