I think I have captured what you need in this basic design:
db<>fiddle
ElementBase
Self-fk for the hierarchy:
CREATE TABLE dbo.ElementBase
(
id integer NOT NULL,
parent_id integer NOT NULL,
element_type char(1) NOT NULL,
-- id key
CONSTRAINT [PK dbo.ElementBase id]
PRIMARY KEY CLUSTERED (id),
-- fk target
CONSTRAINT [UQ dbo.ElementBase id, element_type]
UNIQUE NONCLUSTERED (id, element_type),
-- self fk
CONSTRAINT [FK dbo.ElementBase parent_id id]
FOREIGN KEY (parent_id)
REFERENCES dbo.ElementBase (id),
-- valid element types
CONSTRAINT [CK dbo.ElementBase element_type]
CHECK (element_type IN ('a', 'b', 'c')),
-- for maintenance
INDEX [IX dbo.ElementBase parent_id]
NONCLUSTERED (parent_id)
);
BData
Cascade delete from ElementBase:
CREATE TABLE dbo.BData
(
id integer NOT NULL,
element_type AS CONVERT(char(1), 'b') PERSISTED,
-- id key
CONSTRAINT [PK dbo.BData id]
PRIMARY KEY CLUSTERED (id),
-- fk to ElementBase
CONSTRAINT [FK Bdata ElementBase id, element_type]
FOREIGN KEY (id, element_type)
REFERENCES dbo.ElementBase (id, element_type)
ON DELETE CASCADE
);
AData
No cascading delete from ElementBase; SET NULL cascading delete from BData:
CREATE TABLE dbo.AData
(
id integer NOT NULL,
element_type AS CONVERT(char(1), 'a') PERSISTED,
b_element integer NULL,
-- id key
CONSTRAINT [PK dbo.AData id]
PRIMARY KEY CLUSTERED (id),
-- fk to ElementBase
CONSTRAINT [FK Adata ElementBase id, element_type]
FOREIGN KEY (id, element_type)
REFERENCES dbo.ElementBase (id, element_type)
ON DELETE NO ACTION,
-- fk to BData
CONSTRAINT [FK dbo.AData dbo.BData id b_element]
FOREIGN KEY (b_element)
REFERENCES dbo.BData (id)
ON DELETE SET NULL,
-- fk lookup
INDEX [IDX dbo.AData b_element]
NONCLUSTERED (b_element),
);
CData
No cascading delete from ElementBase; SET NULL cascading delete from BData:
CREATE TABLE dbo.CData
(
id integer NOT NULL,
element_type AS CONVERT(char(1), 'c') PERSISTED,
b_element integer NOT NULL,
-- id key
CONSTRAINT [PK dbo.CData id]
PRIMARY KEY CLUSTERED (id),
-- fk to ElementBase
CONSTRAINT [FK Cdata ElementBase]
FOREIGN KEY (id, element_type)
REFERENCES dbo.ElementBase (id, element_type)
ON DELETE NO ACTION,
-- fk to BData
CONSTRAINT [FK dbo.CData dbo.BData b_element id]
FOREIGN KEY (b_element)
REFERENCES dbo.BData (id)
ON DELETE CASCADE,
-- fk lookup
INDEX [IDX dbo.CData b_element]
NONCLUSTERED (b_element),
);
S
Cascading delete from BData:
CREATE TABLE dbo.S
(
s_id integer NOT NULL,
b_element integer NOT NULL,
-- id key
CONSTRAINT [PK dbo.S s_id]
PRIMARY KEY CLUSTERED (s_id),
-- fk to BData
CONSTRAINT [FK dbo.S dbo.BData b_element id]
FOREIGN KEY (b_element)
REFERENCES dbo.BData (id)
ON DELETE CASCADE,
-- fk lookup
INDEX [IDX dbo.S b_element]
NONCLUSTERED (b_element),
);
ElementBase Instead of Delete Trigger
This handles deleting related items within ElementBase, then cascading deletes to AData and CData. Cascade deletes to BData and S are handled by RI:
CREATE OR ALTER TRIGGER [dbo.ElementBase IOD Cascade]
ON dbo.ElementBase
INSTEAD OF DELETE AS
BEGIN
SET ROWCOUNT 0;
SET NOCOUNT ON;
-- Exit if no work to do
IF NOT EXISTS (SELECT * FROM Deleted) RETURN;
-- Holds ElementBase rows identified for deletion
CREATE TABLE #ToDelete
(
id integer PRIMARY KEY,
element_type char(1) NOT NULL
);
-- Find all related ElementBase records
WITH R AS
(
-- Anchor: parent ElementBase rows
SELECT D.id, D.element_type
FROM Deleted AS D
UNION ALL
-- Recursive: children
SELECT EB.id, EB.element_type
FROM R
JOIN dbo.ElementBase AS EB
ON EB.parent_id = R.id
AND EB.id <> R.id
)
INSERT #ToDelete
(id, element_type)
SELECT DISTINCT
R.id,
R.element_type
FROM R
OPTION (MAXRECURSION 0);
-- Delete related CData records (manual cascade)
DELETE CD
FROM #ToDelete AS TD
JOIN dbo.CData AS CD
ON CD.id = TD.id
WHERE
TD.element_type = 'c';
-- Delete related AData records (manual cascade)
DELETE AD
FROM #ToDelete AS TD
JOIN dbo.AData AS AD
ON AD.id = TD.id
WHERE
TD.element_type = 'a';
-- Delete ElementBase (BData, S records via cascade)
DELETE EB
FROM #ToDelete AS TD
JOIN dbo.ElementBase AS EB
ON EB.id = TD.id;
END;