Home > Back-end >  T-SQL: Single Table contains Permissions Tree
T-SQL: Single Table contains Permissions Tree

Time:05-05

I have a SQL Server Table that contains a 'Hierarchy/Tree' of User Permissions.

Each Individual Permission can have values: 1 [Allowed], Blank [Not Allowed] & 0 [specifically Cancelled].

Each Individual Permission can be in one or more 'Permission Groups' & a User can be assigned all the Individual Permissions in one or more Permission Groups.

Each of the 'Permission Groups', in turn, can be in one or more higher level permission groups ... and eventually, all Permissions Groups are under a Master Group named 'Main Menu'.

This SQL Code:

Select 
'Main Menu' Base, 
Description Level1,
ParentId,
SecurityNodesId,
ListOrder,
Category,
LastModified

From SecurityNodes 
Where ParentId = 1
Order By Description

Produces the following Output:

enter image description here

'Main Menu' has a ParentId of NULL [Not Shown in screenshot].

The 'Level1' 'Folders' contain other folders or Individual Permissions which are 'Referenced' by the Values under SecurityNodesId.

For instance, a search for SecurityNodesId 102 [Level1 - Administration] in the ParentId column returns this list of Sub Folders under 'Level2':

enter image description here

So ... I can access each of these sub folders by writing separate queries.

But what I want is to have an end result that displays every Node of this Permissions Tree in Table form like this:

Main Menu   Level1   Level2   Level3   Level4   PermissionName    PermissionValue

I have never had to do something this complex before, though I have done plenty of self-joins.

I am currently thinking that I would need to do a self join to each self join ... to get to successive Levels of the Tree ... but I believe there may be a 'recursive' approach to this that might be more efficient?

I would appreciate any help I can get with this.

Thanks in advance!

CodePudding user response:

The way to solve this is with a Recursive CTE.

These are definitely more advanced than your usual SQL, but once you have your head wrapped around them, they are pretty easy to put together and VERY useful for hierarchical data (any table that stores a parent/child relationship).

A recursive CTE has two parts, separated by a UNION ALL.

  1. The recursive seed which is ran only once and determines the starting result set for the recursion. For you, this is likely any record with a parentId of 1.

  2. The recursive term (or member) which joins the cte (itself) to the table that holds the parent/child relationship. It will run over and over and over and over again until the Join or a WHERE filter causes it to return no new records.

In your case, it will look something like below. Note that I don't know what your starting table looks like. Namely the Level1 column from your original SQL isn't clear if that is the column name or an alias you call Level1. Furthermore it's not at all clear how you derive a "Permission Group" or "Permission Value" from this data. But... at any rate this should get you in the ballpark:

WITH reccte as (
    /* 
    * To start the recursion we need a "Seed"... or a set of data
    *   that defines the starting point on which we iterate after
    *   the UNION ALL below.
    *
    * The seed here is all records with a parentid of 1
    */
    SELECT Base, 
        ParentID,
        SecurityNodesID,
        Level as Level1, 
        NULL as Level2, 
        NULL as Level3, 
        NULL as Level4,
        '?' as PermissionName,
        Category as PermissionValue,
        1 as depth, --track how deep we recurse
        Base   '>'   Level as path --keep track of where we've been and what has led us to this point in recurssion
    FROM SecurityNodes

    UNION ALL

    /*
    * This section is the part that iterates. It continues to join 
    *  all rows that have been collected up that point with the Security 
    *  Nodes table until that join fails. 
    */
    
    SELECT 
        reccte.Base,
        SecurityNodes.ParentID,
        SecurityNodes.SecurityNodesID,
        reccte.Level1,
        /*
        * Depending on how deep we are in the security hierarchy
        *  capture the level string to the appropriate column
        */
        CASE WHEN depth = 1 THEN SecurityNodes.Level ELSE reccte.Level2,
        CASE WHEN depth = 2 THEN SecurityNodes.Level ELSE reccte.Level3,
        CASE WHEN depth = 3 THEN SecurityNodes.Level ELSE reccte.Level4,
        '?' as PermissionName,
        SecurityNodes.Category as PermissionValue,
        reccte.depth   1, --increment depth
        reccte.path   '>'   SecurityNodes.Level --add to the path so we know how we got here        
    FROM reccte
        INNER JOIN SecurityNodes
            /*Join parent to child*/
            ON reccte.SecurityNodesId = SecurityNodes.parentId
    WHERE depth < 5 --Stop looking up if we go deeper than 4 levels. 
)
SELECT *
FROM reccte

While we track depth here and stop the recursion if we hit a depth of 4, you could stop the recursion with the MAXRECURSIVE option/hint. That would just go at the end of your query:

SELECT *
FROM reccte
OPTION (MAXRECURSION 4);

It's important to add either/or to your recursive CTE otherwise you risk causing an infinite loop should a security node have a child that is also one of its ancestors which would cause it to cycle endlessly.

OPTION (MAXRECURSION 2);

CodePudding user response:

I followed through on an idea I mentioned in my original post and it looks like I have achieved what I was wanting.

I don't think it is the best possible solution because I know how many total levels there currently are. If we suddenly add another level or two, the SQL will not capture everything and I'll manually have to add one or more Left Joins.

Select 
'Main Menu' Base, 
sn.Description Level1,
sn2.Description Level2,
sn3.Description Level3,
sn4.Description Level4,
sn.ParentId,
sn.SecurityNodesId,
sn.ListOrder,
sn.Category,
sn.LastModified

From 
    SecurityNodes sn
    Left Join SecurityNodes sn2 On sn2.ParentId = sn.SecurityNodesId
    Left Join SecurityNodes sn3 On sn3.ParentId = sn2.SecurityNodesId
    Left Join SecurityNodes sn4 On sn3.ParentId = sn3.SecurityNodesId

Order By sn.ParentId, sn.Description

I would still appreciate any suggestions for a more elegant/dynamic way of achieving what I need ... but for now, the above SQL is doing the job.

  • Related