hierarchyid GetDescendant: Insert Between Siblings

hierarchyid GetDescendant calculates a child location relative to sibling bounds. I keep that value calculation separate from inserting a record or coordinating competing writers.

Gouache painting: from a wooden beam hang two brass bells with a wide gap between them
A small boat of potted plants between two stone-lined garden banks.

A generated location is a scalar result

GetDescendant returns a hierarchyid child of its invoking parent. Its two arguments supply lower and upper sibling bounds. NULL leaves a bound open. The method calculates a value without inserting any row.

That distinction matters when the method appears in an allocation workflow. A value calculation alone doesn’t enforce uniqueness. It also doesn’t reserve the value for one caller. Two callers supplying the same inputs can calculate the same deterministic output.

I’d review transaction and uniqueness design separately before using the method during a write. This article performs only read-only calculations over literal paths. It makes no concurrency claim. Its small examples are not a complete insertion protocol.

Keep the parent and bounds visible

All three cases use parent /3/1/. The first supplies two NULL bounds. The second supplies the first child as its lower bound and leaves the upper open. The third supplies two existing sibling bounds.

The expected generated paths are /3/1/1/, /3/1/2/ and /3/1/1.1/. The dotted path falls between its supplied siblings. It stays at the same child depth.

The output retains the original parent and both bounds. It also returns the generated path, level and subtree membership. Those columns expose whether the value remains beneath the stated parent. Text length isn’t used as a substitute for hierarchy relationships.

WITH Inputs AS
(
    SELECT Id, CAST(ParentPath AS nvarchar(40)) AS ParentPath,
           CAST(LowerPath AS nvarchar(40)) AS LowerPath,
           CAST(UpperPath AS nvarchar(40)) AS UpperPath
    FROM (VALUES (1, N'/3/1/', NULL, NULL),
                 (2, N'/3/1/', N'/3/1/1/', NULL),
                 (3, N'/3/1/', N'/3/1/1/', N'/3/1/2/'))
        AS v(Id, ParentPath, LowerPath, UpperPath)
)
SELECT i.Id, i.ParentPath, i.LowerPath, i.UpperPath,
       CAST(g.ChildNode.ToString() AS nvarchar(40)) AS GeneratedPath,
       g.ChildNode.GetLevel() AS ChildLevel,
       g.ChildNode.IsDescendantOf(n.ParentNode) AS InParentSubtree
FROM Inputs AS i
CROSS APPLY (VALUES (hierarchyid::Parse(i.ParentPath),
    hierarchyid::Parse(i.LowerPath), hierarchyid::Parse(i.UpperPath)))
    AS n(ParentNode, LowerNode, UpperNode)
CROSS APPLY (VALUES (n.ParentNode.GetDescendant(n.LowerNode, n.UpperNode)))
    AS g(ChildNode)
ORDER BY i.Id;
Native SSMS results show all three generated child paths, including the path inserted between siblings. Each child stays in the parent subtree at level 3.
Native SSMS results show all three generated child paths, including the path inserted between siblings. Each child stays in the parent subtree at level 3. Open the results at full size.
Three calls, three child paths

Give valid sibling bounds

Each non-NULL bound must be a child of the invoking parent. A lower bound must also precede the upper bound. Invalid relationships can raise an exception. The script supplies only valid pairs and doesn’t execute deliberately failing calls.

I can justify leaving both bounds NULL when choosing an initial child location. A subsequent call should use the intended neighboring bounds. Repeating the empty-bound call doesn’t mean next available child. It repeats the same deterministic value calculation.

The generated identity depends on the supplied relationship. A dotted component isn’t a newly added hierarchy level. It describes a position between siblings. GetLevel in the result keeps that distinction visible beside the text path.

Separate logical order from stored identity

An application can use a separate surrogate key for record identity while hierarchyid describes placement. Moving or placing a node is then a different concern from naming the record. This article doesn’t dictate a particular schema. It simply avoids conflating value generation with record creation.

The full script parses three literal parent-and-bound tuples. It calls the method in a SELECT and creates no objects or settings. ORDER BY fixes the case sequence. No generated value is installed into a table.

Compare all three complete tuples with their SQL types. Keep parent, bounds, generated path and depth together. Read the dotted sibling beside the interval that produced it. A generated-looking string alone doesn’t prove it falls within the required sibling interval.

Calculate the value first, then design the insert around it.

A generated child value is not an allocated record, it is a location calculated from the supplied bounds.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

SQL Datatype, SQL Function, SQL Scripts, SQL Server
Previous Post
SQL SERVER – Fix : Error : The request failed or the service did not respond in a timely fashion
Next Post
SQL SERVER – ‘Denali’ – A Simple Example of Contained Databases

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.