|
| 1 | +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; |
| 2 | +using static SqlServerSimulator.TestHelpers; |
| 3 | + |
| 4 | +namespace SqlServerSimulator; |
| 5 | + |
| 6 | +/// <summary> |
| 7 | +/// Behavioral tests for <c>hierarchyid</c>: tree-path storage, the static |
| 8 | +/// factories <c>hierarchyid::Parse</c> / <c>hierarchyid::GetRoot</c>, the |
| 9 | +/// instance methods <c>.GetLevel</c> / <c>.GetAncestor</c> / |
| 10 | +/// <c>.GetDescendant</c> / <c>.IsDescendantOf</c> / <c>.ToString</c>, |
| 11 | +/// path comparison via <c>ORDER BY</c>, and Msg 6522 verbatim wording on |
| 12 | +/// invalid input. |
| 13 | +/// </summary> |
| 14 | +/// <remarks> |
| 15 | +/// The CAST-to-varbinary byte form is simulator-native rather than SQL Server's |
| 16 | +/// documented variable-bit ordinal encoding (deferred to the BACPAC loader |
| 17 | +/// bundle). All other surfaces probe-match real SQL Server 2025 on 2026-05-14. |
| 18 | +/// </remarks> |
| 19 | +[TestClass] |
| 20 | +public sealed class HierarchyIdTests |
| 21 | +{ |
| 22 | + [TestMethod] |
| 23 | + public void Parse_Root_ProducesEmptyPath() |
| 24 | + => AreEqual("/", ExecuteScalar("select hierarchyid::Parse('/').ToString()")); |
| 25 | + |
| 26 | + [TestMethod] |
| 27 | + public void GetRoot_ProducesSlashPath() |
| 28 | + => AreEqual("/", ExecuteScalar("select hierarchyid::GetRoot().ToString()")); |
| 29 | + |
| 30 | + [TestMethod] |
| 31 | + [DataRow("/1/", "/1/")] |
| 32 | + [DataRow("/1/2/", "/1/2/")] |
| 33 | + [DataRow("/1/2/3/", "/1/2/3/")] |
| 34 | + [DataRow("/-1/", "/-1/")] |
| 35 | + [DataRow("/0/", "/0/")] |
| 36 | + [DataRow("/1.1/", "/1.1/")] |
| 37 | + [DataRow("/1/2.5/3/", "/1/2.5/3/")] |
| 38 | + [DataRow("/100/", "/100/")] |
| 39 | + [DataRow("/-200/", "/-200/")] |
| 40 | + public void Parse_ToString_RoundTrip(string input, string expected) |
| 41 | + => AreEqual(expected, ExecuteScalar($"select hierarchyid::Parse('{input}').ToString()")); |
| 42 | + |
| 43 | + [TestMethod] |
| 44 | + [DataRow("")] |
| 45 | + [DataRow("/1")] |
| 46 | + [DataRow("1/")] |
| 47 | + [DataRow("//")] |
| 48 | + [DataRow("/1//2/")] |
| 49 | + [DataRow("/a/")] |
| 50 | + public void Parse_BadInput_RaisesMsg6522(string input) |
| 51 | + => new Simulation().AssertSqlError($"select hierarchyid::Parse('{input}').ToString()", 6522); |
| 52 | + |
| 53 | + [TestMethod] |
| 54 | + [DataRow("/", 0)] |
| 55 | + [DataRow("/1/", 1)] |
| 56 | + [DataRow("/1/2/", 2)] |
| 57 | + [DataRow("/1/2/3/", 3)] |
| 58 | + [DataRow("/1.1/", 1)] |
| 59 | + [DataRow("/1/2.5/3/", 3)] |
| 60 | + public void GetLevel_ReturnsSegmentCount(string path, int expected) |
| 61 | + => AreEqual((short)expected, ExecuteScalar($"select hierarchyid::Parse('{path}').GetLevel()")); |
| 62 | + |
| 63 | + [TestMethod] |
| 64 | + [DataRow("/1/2/3/", 0, "/1/2/3/")] |
| 65 | + [DataRow("/1/2/3/", 1, "/1/2/")] |
| 66 | + [DataRow("/1/2/3/", 2, "/1/")] |
| 67 | + [DataRow("/1/2/3/", 3, "/")] |
| 68 | + public void GetAncestor_WalksUpThePath(string path, int depth, string expected) |
| 69 | + => AreEqual(expected, ExecuteScalar($"select hierarchyid::Parse('{path}').GetAncestor({depth}).ToString()")); |
| 70 | + |
| 71 | + [TestMethod] |
| 72 | + public void GetAncestor_BeyondRoot_ReturnsNull() |
| 73 | + => AreEqual(DBNull.Value, ExecuteScalar("select hierarchyid::Parse('/1/2/3/').GetAncestor(4).ToString()")); |
| 74 | + |
| 75 | + [TestMethod] |
| 76 | + public void GetAncestor_NegativeDepth_RaisesMsg6522() |
| 77 | + => new Simulation().AssertSqlError("select hierarchyid::Parse('/1/').GetAncestor(-1).ToString()", 6522); |
| 78 | + |
| 79 | + [TestMethod] |
| 80 | + public void GetDescendant_BothNull_ProducesFirstChild() |
| 81 | + => AreEqual("/1/", ExecuteScalar("select hierarchyid::Parse('/').GetDescendant(null, null).ToString()")); |
| 82 | + |
| 83 | + [TestMethod] |
| 84 | + public void GetDescendant_AboveC1_IncrementsLastLabel() |
| 85 | + => AreEqual("/2/", ExecuteScalar("select hierarchyid::Parse('/').GetDescendant(hierarchyid::Parse('/1/'), null).ToString()")); |
| 86 | + |
| 87 | + [TestMethod] |
| 88 | + public void GetDescendant_BelowC2_DecrementsLastLabel() |
| 89 | + => AreEqual("/0/", ExecuteScalar("select hierarchyid::Parse('/').GetDescendant(null, hierarchyid::Parse('/1/')).ToString()")); |
| 90 | + |
| 91 | + [TestMethod] |
| 92 | + public void GetDescendant_GapBetweenC1AndC2_PicksMidpointInteger() |
| 93 | + => AreEqual("/2/", ExecuteScalar("select hierarchyid::Parse('/').GetDescendant(hierarchyid::Parse('/1/'), hierarchyid::Parse('/3/')).ToString()")); |
| 94 | + |
| 95 | + [TestMethod] |
| 96 | + public void GetDescendant_AdjacentSiblings_ExtendsWithSubOrdinal() |
| 97 | + => AreEqual("/1.1/", ExecuteScalar("select hierarchyid::Parse('/').GetDescendant(hierarchyid::Parse('/1/'), hierarchyid::Parse('/2/')).ToString()")); |
| 98 | + |
| 99 | + [TestMethod] |
| 100 | + public void GetDescendant_DeeperParent_PreservesSelfPath() |
| 101 | + => AreEqual("/1/3/", ExecuteScalar("select hierarchyid::Parse('/1/').GetDescendant(hierarchyid::Parse('/1/2/'), null).ToString()")); |
| 102 | + |
| 103 | + [TestMethod] |
| 104 | + public void GetDescendant_C1NotChildOfSelf_RaisesMsg6522() |
| 105 | + => new Simulation().AssertSqlError("select hierarchyid::Parse('/1/').GetDescendant(hierarchyid::Parse('/2/'), null).ToString()", 6522); |
| 106 | + |
| 107 | + [TestMethod] |
| 108 | + public void GetDescendant_C1GreaterThanC2_RaisesMsg6522() |
| 109 | + => new Simulation().AssertSqlError("select hierarchyid::Parse('/1/').GetDescendant(hierarchyid::Parse('/1/3/'), hierarchyid::Parse('/1/2/')).ToString()", 6522); |
| 110 | + |
| 111 | + [TestMethod] |
| 112 | + [DataRow("/", "/", true)] |
| 113 | + [DataRow("/1/", "/", true)] |
| 114 | + [DataRow("/1/2/", "/1/", true)] |
| 115 | + [DataRow("/1/2/", "/2/", false)] |
| 116 | + [DataRow("/", "/1/", false)] |
| 117 | + [DataRow("/1/2/3/", "/1/", true)] |
| 118 | + public void IsDescendantOf_FollowsPrefixContainment(string self, string ancestor, bool expected) |
| 119 | + => AreEqual(expected, ExecuteScalar($"select hierarchyid::Parse('{self}').IsDescendantOf(hierarchyid::Parse('{ancestor}'))")); |
| 120 | + |
| 121 | + [TestMethod] |
| 122 | + public void OrderBy_FollowsLexicographicPathOrder() |
| 123 | + { |
| 124 | + var sim = new Simulation(); |
| 125 | + _ = sim.ExecuteNonQuery(""" |
| 126 | + create table t (h hierarchyid not null); |
| 127 | + insert t values |
| 128 | + (hierarchyid::Parse('/2/')), |
| 129 | + (hierarchyid::Parse('/1/2/')), |
| 130 | + (hierarchyid::Parse('/1/')), |
| 131 | + (hierarchyid::Parse('/')), |
| 132 | + (hierarchyid::Parse('/1/1/')), |
| 133 | + (hierarchyid::Parse('/-1/')), |
| 134 | + (hierarchyid::Parse('/1/1/1/')) |
| 135 | + """); |
| 136 | + using var conn = sim.CreateOpenConnection(); |
| 137 | + using var cmd = conn.CreateCommand(); |
| 138 | + cmd.CommandText = "select h.ToString() from t order by h"; |
| 139 | + using var reader = cmd.ExecuteReader(); |
| 140 | + var actual = new List<string>(); |
| 141 | + while (reader.Read()) |
| 142 | + actual.Add(reader.GetString(0)); |
| 143 | + var expected = new[] { "/", "/-1/", "/1/", "/1/1/", "/1/1/1/", "/1/2/", "/2/" }; |
| 144 | + AreEqual(string.Join(",", expected), string.Join(",", actual)); |
| 145 | + } |
| 146 | + |
| 147 | + [TestMethod] |
| 148 | + public void Storage_RoundTripsThroughHeap() |
| 149 | + { |
| 150 | + var sim = new Simulation(); |
| 151 | + _ = sim.ExecuteNonQuery(""" |
| 152 | + create table t (id int not null primary key, h hierarchyid not null); |
| 153 | + insert t values (1, hierarchyid::Parse('/1/2/3/')), (2, hierarchyid::Parse('/-1/')) |
| 154 | + """); |
| 155 | + AreEqual("/1/2/3/", sim.ExecuteScalar("select h.ToString() from t where id = 1")); |
| 156 | + AreEqual("/-1/", sim.ExecuteScalar("select h.ToString() from t where id = 2")); |
| 157 | + } |
| 158 | + |
| 159 | + [TestMethod] |
| 160 | + public void Null_Hierarchyid_RoundTripsAsNull() |
| 161 | + { |
| 162 | + var sim = new Simulation(); |
| 163 | + _ = sim.ExecuteNonQuery(""" |
| 164 | + create table t (id int not null primary key, h hierarchyid null); |
| 165 | + insert t values (1, null) |
| 166 | + """); |
| 167 | + AreEqual(DBNull.Value, sim.ExecuteScalar("select h from t where id = 1")); |
| 168 | + } |
| 169 | + |
| 170 | + [TestMethod] |
| 171 | + public void DeclareVariable_AssignParse_ReadGetLevel() |
| 172 | + => AreEqual((short)3, ExecuteScalar(""" |
| 173 | + declare @h hierarchyid = hierarchyid::Parse('/1/2/3/'); |
| 174 | + select @h.GetLevel() |
| 175 | + """)); |
| 176 | + |
| 177 | + [TestMethod] |
| 178 | + public void Parse_NullArgument_ReturnsNullHierarchyid() |
| 179 | + => AreEqual(DBNull.Value, ExecuteScalar("select hierarchyid::Parse(cast(null as nvarchar(100))).ToString()")); |
| 180 | + |
| 181 | + [TestMethod] |
| 182 | + public void IsDescendantOf_NullArgument_ReturnsNullBit() |
| 183 | + => AreEqual(DBNull.Value, ExecuteScalar("select hierarchyid::Parse('/1/').IsDescendantOf(cast(null as hierarchyid))")); |
| 184 | + |
| 185 | + [TestMethod] |
| 186 | + public void SysTypes_ListsHierarchyId() |
| 187 | + { |
| 188 | + var sim = new Simulation(); |
| 189 | + AreEqual(240, sim.ExecuteScalar("select cast(system_type_id as int) from sys.types where name = 'hierarchyid'")); |
| 190 | + AreEqual(128, sim.ExecuteScalar("select user_type_id from sys.types where name = 'hierarchyid'")); |
| 191 | + } |
| 192 | +} |
0 commit comments