// The `concatcp!` macro below uses fixed-size byte indexing in a `const fn` // where lengths are statically known. `.get(n)?` is not available in `const` // contexts in stable Rust 1.85, so the indexing is the only path. The // resulting `&'static str` constants are evaluated at compile time, so // any out-of-bounds would surface as a compile error rather than a runtime // panic. #![allow(clippy::indexing_slicing)] //! SQL strings used by the future tiberius-backed resolver. //! //! Direct port of the two `private const string` blocks at //! `src/MxNativeClient/GalaxyRepositoryTagResolver.cs:208-432`. Kept as //! `pub const &str` so any future SQL backend (the planned //! `tiberius`-gated implementation, an alternative dapper-style backend, //! or a snapshot-replay test harness) can grab the canonical query without //! re-typing it. //! //! Both queries assume a Galaxy DB that exposes the tables verified in //! `wwtools/grdb/`: //! //! - `dbo.gobject` / `dbo.instance` — object instances + their MX ids. //! - `dbo.package` (recursive `derived_from_package_id` for inheritance). //! - `dbo.dynamic_attribute` — dynamic attributes attached to a package. //! - `dbo.attribute_definition` / `dbo.primitive_instance` — primitive- //! bound attributes. //! //! ## Resolver input contract //! //! Both queries take `tag_name`-form input only (e.g. `DelmiaReceiver_001`), //! NOT `contained_name`-form (`TestMachine_001.DelmiaReceiver`). See //! `wwtools/grdb/README.md` for the schema asymmetry. The Rust resolver //! enforces this at the parser layer ([`crate::parser::ParsedTagReference`]) //! before dispatching to SQL. //! //! ## Result columns (in order) //! //! Both queries return the same 13-column shape — keep this list aligned //! with [`crate::metadata::GalaxyTagMetadata`] field order: //! //! 0. `object_tag_name` `nvarchar` //! 1. `attribute_name` `nvarchar` //! 2. `primitive_name` `nvarchar` or `NULL` //! 3. `mx_platform_id` `smallint` → `u16` //! 4. `mx_engine_id` `smallint` → `u16` //! 5. `mx_object_id` `smallint` → `u16` //! 6. `mx_primitive_id` `smallint` → `i16` //! 7. `mx_attribute_id` `smallint` → `i16` //! 8. `property_id` `int` → `i16` (checked-cast) //! 9. `mx_data_type` `smallint` → `i16` //! 10. `is_array` `bit` → `bool` //! 11. `security_classification` `smallint` → `i16` //! 12. `attribute_source` `nvarchar` ("dynamic" or "primitive") //! //! ## Recursive CTE depth //! //! Both queries cap package-derivation depth at 10 (`AND dpc.depth < 10`). //! Galaxy package inheritance chains are typically short (3-5 levels); //! 10 is a defensive cap against malformed package_id loops. If a real //! deployment legitimately exceeds this, the cap should be raised here //! and tracked in `design/70-risks-and-open-questions.md`. /// Single-row resolver query — `Resolve(tag_reference)`. /// /// Parameters (in order): /// - `@objectTagName` (`nvarchar`) — the leading `Object` segment. /// - `@attributeName` (`nvarchar`) — the trailing `Attribute` segment, or /// `Primitive.Attribute` for the dotted-attribute candidate. /// - `@primitiveName` (`nvarchar` or `NULL`) — the middle segment when /// the input was `Object.Primitive.Attribute`; `NULL` for dynamic-only /// candidates. /// /// Direct port of `GalaxyRepositoryTagResolver.cs:208-314` — the `;WITH` /// `deployed_package_chain`, `ranked_dynamic`, `primitive_attributes` /// blocks plus the final UNION + `ORDER BY` that prefers `dynamic` rows /// over `primitive` rows when both match. pub const RESOLVE_SQL: &str = r#";WITH deployed_package_chain AS ( SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth FROM dbo.gobject g INNER JOIN dbo.package p ON p.package_id = g.deployed_package_id WHERE g.is_template = 0 AND g.deployed_package_id <> 0 AND g.tag_name = @objectTagName UNION ALL SELECT dpc.gobject_id, p.package_id, p.derived_from_package_id, dpc.depth + 1 FROM deployed_package_chain dpc INNER JOIN dbo.package p ON p.package_id = dpc.derived_from_package_id WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10 ), ranked_dynamic AS ( SELECT g.tag_name AS object_tag_name, da.attribute_name, CAST(NULL AS nvarchar(329)) AS primitive_name, i.mx_platform_id, i.mx_engine_id, i.mx_object_id, da.mx_primitive_id, da.mx_attribute_id, CAST(10 AS int) AS property_id, da.mx_data_type, da.is_array, da.security_classification, CAST(N'dynamic' AS nvarchar(16)) AS attribute_source, ROW_NUMBER() OVER ( PARTITION BY dpc.gobject_id, da.attribute_name ORDER BY dpc.depth ) AS rn FROM deployed_package_chain dpc INNER JOIN dbo.dynamic_attribute da ON da.package_id = dpc.package_id INNER JOIN dbo.gobject g ON g.gobject_id = dpc.gobject_id INNER JOIN dbo.instance i ON i.gobject_id = g.gobject_id WHERE da.attribute_name = @attributeName AND @primitiveName IS NULL ), primitive_attributes AS ( SELECT g.tag_name AS object_tag_name, ad.attribute_name, NULLIF(pi.primitive_name, N'') AS primitive_name, i.mx_platform_id, i.mx_engine_id, i.mx_object_id, pi.mx_primitive_id, ad.mx_attribute_id, CAST(10 AS int) AS property_id, ad.mx_data_type, ad.is_array, ad.security_classification, CAST(N'primitive' AS nvarchar(16)) AS attribute_source, 1 AS rn FROM dbo.gobject g INNER JOIN dbo.instance i ON i.gobject_id = g.gobject_id INNER JOIN dbo.primitive_instance pi ON pi.gobject_id = g.gobject_id AND pi.package_id = g.deployed_package_id AND pi.property_bitmask & 0x10 <> 0x10 INNER JOIN dbo.attribute_definition ad ON ad.primitive_definition_id = pi.primitive_definition_id WHERE g.tag_name = @objectTagName AND ad.attribute_name = @attributeName AND ( (@primitiveName IS NULL AND pi.primitive_name = N'') OR (@primitiveName IS NOT NULL AND pi.primitive_name = @primitiveName) ) ) SELECT TOP (1) object_tag_name, attribute_name, primitive_name, mx_platform_id, mx_engine_id, mx_object_id, mx_primitive_id, mx_attribute_id, property_id, mx_data_type, is_array, security_classification, attribute_source FROM ( SELECT * FROM ranked_dynamic WHERE rn = 1 UNION ALL SELECT * FROM primitive_attributes ) resolved ORDER BY CASE attribute_source WHEN N'dynamic' THEN 0 ELSE 1 END "#; // --------------------------------------------------------------------------- // User resolver SQL — port of GalaxyRepositoryUserResolver.cs:135-148. // --------------------------------------------------------------------------- /// Common `SELECT TOP (1) ...` user-profile projection — `cs:135-144`. /// Used as the base for both [`USER_BY_GUID_SQL`] and [`USER_BY_NAME_SQL`]. /// /// Result columns (in order): /// /// 0. `user_profile_id` `int` /// 1. `user_profile_name` `nvarchar` /// 2. `user_guid` `uniqueidentifier` /// 3. `default_security_group` `nvarchar` /// 4. `intouch_access_level` `int` (NULL-able) /// 5. `roles_text` `nvarchar(max)` — `CONVERT(...)` of the /// `roles` `varbinary` column. Decode through /// [`crate::role_blob::parse_role_blob`]. pub const USER_SELECT_SQL: &str = r#"SELECT TOP (1) user_profile_id, user_profile_name, user_guid, default_security_group, intouch_access_level, CONVERT(nvarchar(max), roles) AS roles_text FROM dbo.user_profile"#; /// `Resolve user_profile by user_guid` — port of `cs:146`. /// /// Parameter: `@userGuid` (`uniqueidentifier`). pub const USER_BY_GUID_SQL: &str = concatcp!( USER_SELECT_SQL, "\nWHERE user_guid = @userGuid\nORDER BY user_profile_id" ); /// `Resolve user_profile by user_profile_name` — port of `cs:148`. /// /// Parameter: `@userName` (`nvarchar`). pub const USER_BY_NAME_SQL: &str = concatcp!( USER_SELECT_SQL, "\nWHERE user_profile_name = @userName\nORDER BY user_profile_id" ); /// Tiny `concat!`-equivalent for `&'static str` constants, since `concat!` /// only works with literals. Two-arg specialisation; keeps `USER_BY_GUID_SQL` /// and `USER_BY_NAME_SQL` evaluable at compile time without dragging in /// `const_format` as a dep. macro_rules! concatcp { ($a:expr, $b:expr) => {{ const A: &str = $a; const B: &str = $b; const N: usize = A.len() + B.len(); const fn build() -> [u8; N] { let mut out = [0u8; N]; let a = A.as_bytes(); let b = B.as_bytes(); let mut i = 0; while i < a.len() { out[i] = a[i]; i += 1; } let mut j = 0; while j < b.len() { out[a.len() + j] = b[j]; j += 1; } out } // SAFETY: A and B are valid UTF-8 (both are `&'static str`); the // concatenation of two valid UTF-8 byte sequences is valid UTF-8. const COMBINED: &[u8; N] = &build(); match core::str::from_utf8(COMBINED) { Ok(s) => s, Err(_) => "", } }}; } pub(crate) use concatcp; /// Multi-row browse query — `Browse(object_tag_like, attribute_like, max_rows)`. /// /// Parameters: /// - `@objectTagLike` (`nvarchar`) — `LIKE` pattern for `g.tag_name`. /// - `@attributeLike` (`nvarchar`) — `LIKE` pattern for `attribute_name`. /// - `@maxRows` (`int`) — `TOP (...)` cap. The .NET reference clamps to /// 1000 (`cs:137`); the Rust resolver should do the same before binding /// the parameter. /// /// Direct port of `GalaxyRepositoryTagResolver.cs:316-432`. Same column /// ordering as [`RESOLVE_SQL`]. pub const BROWSE_SQL: &str = r#";WITH deployed_objects AS ( SELECT g.gobject_id, g.tag_name, g.deployed_package_id, i.mx_platform_id, i.mx_engine_id, i.mx_object_id FROM dbo.gobject g INNER JOIN dbo.instance i ON i.gobject_id = g.gobject_id WHERE g.is_template = 0 AND g.deployed_package_id <> 0 AND g.tag_name LIKE @objectTagLike ), deployed_package_chain AS ( SELECT d.gobject_id, d.tag_name, d.mx_platform_id, d.mx_engine_id, d.mx_object_id, p.package_id, p.derived_from_package_id, 0 AS depth FROM deployed_objects d INNER JOIN dbo.package p ON p.package_id = d.deployed_package_id UNION ALL SELECT dpc.gobject_id, dpc.tag_name, dpc.mx_platform_id, dpc.mx_engine_id, dpc.mx_object_id, p.package_id, p.derived_from_package_id, dpc.depth + 1 FROM deployed_package_chain dpc INNER JOIN dbo.package p ON p.package_id = dpc.derived_from_package_id WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10 ), ranked_dynamic AS ( SELECT dpc.tag_name AS object_tag_name, da.attribute_name, CAST(NULL AS nvarchar(329)) AS primitive_name, dpc.mx_platform_id, dpc.mx_engine_id, dpc.mx_object_id, da.mx_primitive_id, da.mx_attribute_id, CAST(10 AS int) AS property_id, da.mx_data_type, da.is_array, da.security_classification, CAST(N'dynamic' AS nvarchar(16)) AS attribute_source, ROW_NUMBER() OVER ( PARTITION BY dpc.gobject_id, da.attribute_name ORDER BY dpc.depth ) AS rn FROM deployed_package_chain dpc INNER JOIN dbo.dynamic_attribute da ON da.package_id = dpc.package_id WHERE da.attribute_name LIKE @attributeLike ), primitive_attributes AS ( SELECT d.tag_name AS object_tag_name, ad.attribute_name, NULLIF(pi.primitive_name, N'') AS primitive_name, d.mx_platform_id, d.mx_engine_id, d.mx_object_id, pi.mx_primitive_id, ad.mx_attribute_id, CAST(10 AS int) AS property_id, ad.mx_data_type, ad.is_array, ad.security_classification, CAST(N'primitive' AS nvarchar(16)) AS attribute_source, 1 AS rn FROM deployed_objects d INNER JOIN dbo.gobject g ON g.gobject_id = d.gobject_id INNER JOIN dbo.primitive_instance pi ON pi.gobject_id = g.gobject_id AND pi.package_id = g.deployed_package_id AND pi.property_bitmask & 0x10 <> 0x10 INNER JOIN dbo.attribute_definition ad ON ad.primitive_definition_id = pi.primitive_definition_id WHERE ad.attribute_name LIKE @attributeLike ) SELECT TOP (@maxRows) object_tag_name, attribute_name, primitive_name, mx_platform_id, mx_engine_id, mx_object_id, mx_primitive_id, mx_attribute_id, property_id, mx_data_type, is_array, security_classification, attribute_source FROM ( SELECT * FROM ranked_dynamic WHERE rn = 1 UNION ALL SELECT * FROM primitive_attributes ) resolved ORDER BY object_tag_name, primitive_name, attribute_name "#; #[cfg(test)] #[allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] mod tests { use super::*; #[test] fn resolve_sql_references_three_named_parameters() { // Smoke check: the three @-parameters the .NET command binds at // cs:100-102 must appear by name in the query body. assert!(RESOLVE_SQL.contains("@objectTagName")); assert!(RESOLVE_SQL.contains("@attributeName")); assert!(RESOLVE_SQL.contains("@primitiveName")); } #[test] fn browse_sql_references_three_named_parameters() { assert!(BROWSE_SQL.contains("@objectTagLike")); assert!(BROWSE_SQL.contains("@attributeLike")); assert!(BROWSE_SQL.contains("@maxRows")); } #[test] fn resolve_sql_caps_recursion_at_depth_10() { // Defensive cap — see module doc. assert!(RESOLVE_SQL.contains("dpc.depth < 10")); } #[test] fn browse_sql_caps_recursion_at_depth_10() { assert!(BROWSE_SQL.contains("dpc.depth < 10")); } #[test] fn resolve_sql_orders_dynamic_before_primitive() { // Per cs:313: ORDER BY CASE attribute_source WHEN N'dynamic' THEN 0 ELSE 1 END. assert!(RESOLVE_SQL.contains("WHEN N'dynamic' THEN 0 ELSE 1 END")); } #[test] fn both_queries_select_thirteen_columns_in_documented_order() { // Spot-check: the SELECT list ends with attribute_source — the // last (13th) column. assert!(RESOLVE_SQL.contains("attribute_source\nFROM (")); assert!(BROWSE_SQL.contains("attribute_source\nFROM (")); } }