Saturday, March 20, 2021

Determining actions you can take with Edge and Node tables in SQL Server

One of the interesting things about working with many-to-many relationships in SQL Server with graph tables instead of a relational table is that unlike a relational many-to-many table, by default an edge may can implement relationships from lots of different tables (nodes). You can also limit what nodes can be related using which edges.

For example, say you have 4 nodes and 2 edges, both of the edges, by default, each edge would allow relationships from each node to itself, or each node to each other node. It can all get a bit complicated to figure out if you have a lot of objects (and to be fair, you probably also want to be able to check to make sure your objects are configured as you expect.

In this blog, I will demonstrate how to determine, given a given edge or node, what operations are possible. To demonstrate, I will use the following nodes and edges:

CREATE TABLE dbo.Node1(Name varchar(20)) AS NODE;
CREATE TABLE dbo.Node2(Name varchar(20)) AS NODE;
CREATE TABLE dbo.Node3(Name varchar(20)) AS NODE;
CREATE TABLE dbo.Node4(Name varchar(20)) AS NODE;

For one edge table, I will create it to allow any nodes to connect:

CREATE TABLE dbo.Edge1 AS EDGE;

And then another edge, but this one will have 2 conditions that are defined to allow cascading deletes (so if either node is deleted, the edge is removed), and one that is requires you to remove the edge to remove the node.

CREATE TABLE dbo.Edge2
(
        CONSTRAINT EC_Edge2 CONNECTION (dbo.Node1 TO dbo.Node2,
                                        dbo.Node2 TO dbo.Node2) 
                         ON DELETE CASCADE
) AS EDGE;
CREATE TABLE dbo.Edge3
(
   CONSTRAINT EC_Edge3 CONNECTION (dbo.Node1 TO dbo.Node2,
                                   dbo.Node2 TO dbo.Node3,
                                   dbo.Node2 TO dbo.Node1) 
                      ON DELETE NO ACTION
) AS EDGE;

To find the metadata about these objects, we can use a couple of base catalog views. sys.edge_constraints and sys.edge_constraint_clauses. sys.edge_constraints is the typical extension of sys.objects, with one row per edge constraint (which is itself an object, like other constraints.) sys.edge_constraint_clauses gives you one row per the node to node relationship. (Note, the query allows for the case where you have more than one edge connection, though that is not generally something you should generally implement (Covered in this blog).

I want to have two views of the metadata, one in the context of the node (what edges exist that I can insert into?) and from the context of the edge (what nodes can be involved in a relationship with the edge?)

So, here first is the query of sys.edge_constraints, showing the constraints that exist:

SELECT object_id AS edge_object_id,
   CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(
                    edge_constraints.parent_object_id)),'.',
    QUOTENAME(OBJECT_NAME(edge_constraints.parent_object_id))) 
                                                AS ObjectName,
    QUOTENAME(name) AS EdgeConstraintName, 
    delete_referential_action_desc AS DeleteAction
FROM  sys.edge_constraints;

This returns the constraints and what happens on delete:

edge_object_id ObjectName       EdgeConstraintName    DeleteAction
-------------- ---------------- --------------------- ----------------
725577623      [dbo].[Edge2]    [EC_Edge2]            CASCADE
789577851      [dbo].[Edge3]    [EC_Edge3]            NO ACTION

Next to get the nodes that can be involved in the relationship, use:

SELECT object_id AS edge_object_id, 
           CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(from_object_id)), 
                  '.',QUOTENAME(OBJECT_NAME(from_object_id))) 
                                                  AS FromNode,
           from_object_id,
           CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(to_object_id)), 
                  '.',QUOTENAME(OBJECT_NAME(to_object_id))) 
                                                  AS ToNode,
           to_object_id
FROM   sys.edge_constraint_clauses;

This returns (less the from and to object_id values):

edge_object_id FromNode         from_object_id ToNode         to_object_id
-------------- ---------------- -------------- -------------- ------------
757577737      [dbo].[Node2]    645577338      [dbo].[Node1]  629577281
725577623      [dbo].[Node1]    629577281      [dbo].[Node2]  645577338
725577623      [dbo].[Node2]    645577338      [dbo].[Node2]  645577338
757577737      [dbo].[Node1]    629577281      [dbo].[Node2]  645577338
757577737      [dbo].[Node2]    645577338      [dbo].[Node3]  661577395

Now I am going to put these together to get the conditions and the constraints:

WITH Constraints AS (
SELECT object_id AS edge_object_id,
           CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(edge_constraints.parent_object_id)), 
                  '.',QUOTENAME(OBJECT_NAME(edge_constraints.parent_object_id))) AS ObjectName,
           QUOTENAME(name) AS EdgeConstraintName, 
           delete_referential_action_desc AS DeleteAction
FROM  sys.edge_constraints),
Clauses AS (
SELECT object_id AS edge_object_id, 
       CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(from_object_id)), '.',
              QUOTENAME(OBJECT_NAME(from_object_id))) AS FromNode,
       from_object_id,
       CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(to_object_id)), '.',
              QUOTENAME(OBJECT_NAME(to_object_id))) AS ToNode,
       to_object_id
FROM   sys.edge_constraint_clauses)
SELECT Constraints.ObjectName, Constraints.EdgeConstraintName, Constraints.DeleteAction, 
       Clauses.FromNode, Clauses.ToNode
FROM   constraints
                JOIN Clauses
                        ON Clauses.edge_object_id = Constraints.edge_object_id;

Thie returns the rough output:

ObjectName     EdgeConstraintName   DeleteAction  FromNode        ToNode
-------------- -------------------- ------------- --------------- -----------------
[dbo].[Edge2]  [EC_Edge2]           CASCADE       [dbo].[Node2]   [dbo].[Node2]
[dbo].[Edge2]  [EC_Edge2]           CASCADE       [dbo].[Node1]   [dbo].[Node2]
[dbo].[Edge3]  [EC_Edge3]           NO_ACTION     [dbo].[Node1]   [dbo].[Node2]
[dbo].[Edge3]  [EC_Edge3]           NO_ACTION     [dbo].[Node2]   [dbo].[Node3]
[dbo].[Edge3]  [EC_Edge3]           NO_ACTION     [dbo].[Node2]   [dbo].[Node1]

In this next query (which is the query I was targeting in the first place), I am going to output an edge centric view, partitioned by object, constraint, giving the delete action and the list of node to node relationships. For edges without an edge constraint, I will use Any Node -> Any Node, rather than listing out every possible permutation of nodes that exist (which would make for a very large list pretty quick.)

WITH Constraints AS (
SELECT object_id AS edge_object_id,
           CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(edge_constraints.parent_object_id)), 
                         '.',QUOTENAME(OBJECT_NAME(edge_constraints.parent_object_id))) AS ObjectName,
           QUOTENAME(name) AS EdgeConstraintName, 
           delete_referential_action_desc AS DeleteAction
FROM  sys.edge_constraints),
Clauses AS (SELECT object_id AS edge_object_id, 
           CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(from_object_id)), '.',
                 QUOTENAME(OBJECT_NAME(from_object_id))) AS FromNode,
           from_object_id,
           CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(to_object_id)), '.',
                  QUOTENAME(OBJECT_NAME(to_object_id))) AS ToNode,
           to_object_id
FROM   sys.edge_constraint_clauses)
SELECT Constraints.ObjectName, Constraints.EdgeConstraintName, 
       Constraints.DeleteAction,
           --aggregate allowable connections
           STRING_AGG(CONCAT('{',Clauses.FromNode,' -> '
                    ,Clauses.ToNode,'}'),'; ') AS AllowedConnections
FROM   constraints
                JOIN Clauses
                        ON Clauses.edge_object_id = Constraints.edge_object_id
GROUP BY Constraints.ObjectName, Constraints.EdgeConstraintName, Constraints.DeleteAction
UNION ALL 
--add in any edge that does not have a constraint, and indicate it can be used for any connection
SELECT CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(object_id)),'.',QUOTENAME(name)) AS ObjectName, 
           'N\A','N\A', '{Any Node -> Any Node}'
FROM   sys.tables
WHERE  tables.is_edge = 1
 AND   NOT EXISTS (SELECT *
                                   FROM   sys.edge_constraints
                                   WHERE  edge_constraints.parent_object_id = tables.object_id);

This returns:

ObjectName      EdgeConstraintName  DeleteAction    AllowedConnections
--------------- ------------------- --------------- --------------------------------------------------------------------------------------------------------
[dbo].[Edge3]   [EC_Edge3]          NO_ACTION       {[dbo].[Node2] -> [dbo].[Node3]}; {[dbo].[Node2] -> [dbo].[Node1]}; {[dbo].[Node1] -> [dbo].[Node2]}
[dbo].[Edge2]   [EC_Edge2]          CASCADE         {[dbo].[Node1] -> [dbo].[Node2]}; {[dbo].[Node2] -> [dbo].[Node2]}
[dbo].[Edge1]   N\A                 N\A             {Any Node -> Any Node}

Finally, this next query lists things in a node centric format:

WITH UnconstrainedEdgeMix AS (
--output unconstrained nodes as Any Node, rather than the cross product of all node types
SELECT CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(edges.object_id)), 
                         '.',QUOTENAME(OBJECT_NAME(edges.object_id))) AS EdgeName,
                         CAST(NULL AS int) AS FromNodeId, -CAST(NULL AS int)  AS ToNodeId,
                         'Orphan' AS DeleteAction
FROM   sys.tables AS edges
WHERE  edges.is_edge = 1
  AND  NOT EXISTS (SELECT *
                       FROM  sys.edge_constraints
                                   WHERE edges.object_id = edge_constraints.parent_object_id )
), BaseRows AS (
SELECT EdgeName, FromNodeId, ToNodeId, UnconstrainedEdgeMix.DeleteAction
FROM UnconstrainedEdgeMix 
UNION ALL
--add the constrained edges in, with their id and actions
SELECT CONCAT(QUOTENAME(OBJECT_SCHEMA_NAME(edge_constraints.parent_object_id)), 
                         '.',QUOTENAME(OBJECT_NAME(edge_constraints.parent_object_id))) AS EdgeName,
           from_object_id AS FromNodeId,
           to_object_id AS ToNodeId,
           edge_constraints.delete_referential_action_desc AS DeleteAction
FROM   sys.edge_constraint_clauses
                JOIN sys.edge_constraints
                        ON edge_constraints.object_id = edge_constraint_clauses.object_id
),
--And the last CTE lets you add filters to the query so you can just look for what Node1 can connect to 
--explicitly (by name) or implicitly (by looking for Any in the node and schema).
FilterFrom AS (
SELECT COALESCE(OBJECT_SCHEMA_NAME(BaseRows.FromNodeId),'Any') AS NodeSchema,
                COALESCE(OBJECT_NAME(BaseRows.FromNodeId),'Any') AS Node, EdgeName, 'From' AS Relationship, DeleteAction
FROM   BaseRows
UNION ALL
SELECT COALESCE(OBJECT_SCHEMA_NAME(BaseRows.FromNodeId),'Any') AS NodeSchema,
           COALESCE(OBJECT_NAME(BaseRows.FromNodeId),'Any') AS Node, EdgeName, 'To' AS Relationship, DeleteAction
FROM   BaseRows)
SELECT *
FROM   FilterFrom
ORDER BY FilterFrom.NodeSchema, FilterFrom.Node, FilterFrom.Relationship, FilterFrom.EdgeName;

This outputs:

NodeSchema    Node      EdgeName        Relationship DeleteAction
------------- --------- --------------- ------------ -----------------------
Any           Any       [dbo].[Edge1]   From         Orphan
Any           Any       [dbo].[Edge1]   To           Orphan
dbo           Node1     [dbo].[Edge2]   From         CASCADE
dbo           Node1     [dbo].[Edge3]   From         NO_ACTION
dbo           Node1     [dbo].[Edge2]   To           CASCADE
dbo           Node1     [dbo].[Edge3]   To           NO_ACTION
dbo           Node2     [dbo].[Edge2]   From         CASCADE
dbo           Node2     [dbo].[Edge3]   From         NO_ACTION
dbo           Node2     [dbo].[Edge3]   From         NO_ACTION
dbo           Node2     [dbo].[Edge2]   To           CASCADE
dbo           Node2     [dbo].[Edge3]   To           NO_ACTION
dbo           Node2     [dbo].[Edge3]   To           NO_ACTION

As a reminder, the action of Orphan represents what happens without a constraint (it leaves the edge in the table that references the node(s) you delete. For more details about this scenario, I cover that in the following blog entry: (https://www.red-gate.com/simple-talk/blogs/ways-to-get-and-deal-with-invalid-node-identifiers-in-sql-server-edge-references/).

As usual, you can find the primary useful queries on my github page as both metadata queries and in my SQL Prompt snippet repos.

The post Determining actions you can take with Edge and Node tables in SQL Server appeared first on Simple Talk.



from Simple Talk https://ift.tt/2NxsX9g
via

No comments:

Post a Comment