Related
I have this scenario:
CREATE TABLE tbl(templateId INT, id INT, name NVARCHAR(50), value NVARCHAR(50), row INT);
INSERT INTO tbl(templateId, id, name, value, row)
VALUES
(1, 12, 'question1', '5 To 10', 1),
(2, 12, 'question2', 'washing machine', 1),
(3, 12, 'question3', 'yes', 1),
(4, 12, 'question2', 'tv', 2),
(5, 12, 'question1', '11 To 15', 2),
(6, 12, 'question1', '16 To 20', 2),
(7, 12, 'question4', 'employed' 2);
The data must be grouped by id and row
and what I would need would be another column with data like this:
-If we have different questions on the same row (grouped by id = 12 and row = 1):
(question1: (5 To 10) [AND] question2: (washing machine) [AND] question3: (yes))
-If we have different questions on the same row and one of them has many answers it should look like this (id = 12 and row = 2):
(question2: (tv) [AND] question1: (11 To 15, 16 To 20) [AND] question4: (employed))
I managed to create the first case, but I’m having problems with the second. For the second I created something like
(question2: (tv) [AND] question1: (11 To 15) OR question1:(16 To 20) OR question4:(employed))
but it's not good, the answers for question1 have to be separated by comma and the name shouldn't be displayed everytime. Moreover, it puts [AND] only between the first two names, it should be between question1 [AND] question4 as well, I just don't know how to replace that OR...
I’ve created a function like this :
declare #result varchar(1000), #name1 varchar(250), #name2 varchar(250),
#duplicates int;
set #result = '';
set #duplicates = 0;
set #name1 = '';
set #name2 = '';
SELECT #result = #result + ' [AND] ' + t.name + ': (' + t.value + ')',
#duplicates = (len(#result) - len(replace(#result,t.name,''))) /
LEN(t.name)
FROM tbl t
WHERE t.id = #table_id and t.row = #row
if(len(#result)>0)
if (#duplicates > 1)
begin
SET #result =replace(substring(#result, 7, LEN(#result) - 4), '
[AND] ', ' OR ');
SET #name1 = LEFT(#result,CHARINDEX(': ',#result)-1);
SET #name2 = SUBSTRING(SUBSTRING(#result,CHARINDEX('OR ', #result)
+ 2,LEN(#result)), 0,CHARINDEX(':', #result) + 0)
if (#name1 <> #name2)
begin
SET #result=STUFF(#result, CHARINDEX('OR', #result), LEN('OR'),
'[AND]')
end
end
else
begin
SET #result=substring(#result, 7, LEN(#result) - 4);
end
return #result;
I hope I managed to make clear what I want to accomplish. Every suggestion will be highly appreciated. Thanks !
Give this a shot
Example
;with cte as (
Select top 1 with ties
[id]
,[row]
,[name]
,TempValue = Stuff((Select ', ' + value From tbl Where [Row]=A.[Row] and [Name]=A.[Name] For XML Path ('')),1,2,'')
,RN = Row_Number() over (Partition By [id],[row] Order by templateId)
From tbl A
Order by Row_Number() over (Partition By ID,[name],row order by templateid)
)
Select [Row]
,NewValue = '('+Stuff((Select ' [AND] ' +concat(Name,': (',TempValue,')') From cte Where [Row]=A.[Row] Order by RN For XML Path ('')),1,7,'')+')'
From cte A
Group By [Row]
Returns
Row NewValue
1 (question1: (5 To 10) [AND] question2: (washing machine) [AND] question3: (yes))
2 (question2: (tv) [AND] question1: (11 To 15, 16 To 20) [AND] question4: (employed))
This is my string: 11.0000.0101.000.000.0101.000.000
I need to replace ONLY the first "0101" with "101." I can't use replace, as it replaces the 2nd instance of 0101 as well.
I tried
stuff(string, 9, 3, '101')
but since the replacement string is shorter than the existing string, I end up with
11.0000.1011.000.000.0101.000.000
What can I use besides REPLACE or STUFF? Thanks!
declare #t table(s varchar(100))
insert into #t values
( '11.0000.0101.000.000.0101.000.000'), ('abc');
select case charindex('0101', s)
when 0 then s
else stuff(s, charindex('0101', s), 4, '101')
end as new_s
from #t;
Your expression was almost right.
Just tell it to replace 4 characters of the original string instead of 3 :
stuff(string, 9, 4, '101')
But this will only work if your string has always the same positions.
you can do something like:
replace (stuff(string,9,3,'#101'),'#','')
If you need replace all 0101 to 101 go with below code
DECLARE #TempData TABLE(Data VARCHAR(1000));
INSERT INTO #TempData VALUES
('11.0000.0101.000.000.0101.000.000');
;WITH Cte
AS (
SELECT CASE
WHEN DATA = '0101'
THEN '101'
ELSE CAST(DATA AS VARCHAR(10))
END AS DATA
FROM (
SELECT Split.a.value('.', 'VARCHAR(1000)') AS Data
FROM (
SELECT CAST('<S>' + REPLACE(Data, '.', '</S><S>') + '</S>' AS XML) AS Data
FROM #TempData
) AS A
CROSS APPLY Data.nodes('/S') AS Split(a)
) Dt
)
SELECT STUFF((
SELECT '.' + DATA
FROM cte
FOR XML PATH('')
), 1, 1, '') AS ExpectedResult
OutPut
ExpectedResult
11.0000.101.000.000.101.000.000
Yet another option if you don't know the 1st observation
Declare #S varchar(50) = '11.0000.0101.000.000.0101.000.000'
Declare #Find varchar(25) = '0101'
Declare #Repl varchar(25) = '101'
Select isnull(stuff(#S, charindex(#Find,#S), len(#Find), #Repl),#S)
Returns
11.0000.101.000.000.0101.000.000
Does someone perhaps know how to get a comma separated list from SQL that doesn't duplicate - it's a little hard to explain. Let me give an example.
I have a list of invoices + the shipment it belongs to in a table like below:
InvoiceNumber ShipmentNumber
0180376000 1stShipment
0180376005 1stShipment
0180376003 1stShipment
0180375997 1stShipment
0180375993 1stShipment
This list needs to be divided up into main InvoiceNumbers followed by the right 2 digits of the remaining invoice numbers. Result should look similar to the below.
01803760, 00, 05, 03, 01803759, 97, 93
At this point I can get the comma separated list fairly easily but cannot figure out how to position the 2 digit after each respective invoice that it belongs to.
Any suggestion of how to do this would be great!!
Try it like this
DECLARE #tbl TABLE(InvoiceNumber VARCHAR(100));
INSERT INTO #tbl VALUES
('0180376000')
,('0180376005')
,('0180376003')
,('0180375997')
,('0180375993');
WITH CutInTwo AS
(
SELECT LEFT(InvoiceNumber,8) AS Number
,RIGHT(InvoiceNumber,2) AS SubNumber
FROM #tbl
)
,OnlyMainNumbers AS
(
SELECT DISTINCT Number
FROM CutInTwo
)
SELECT y.Number + ' ' + (STUFF((SELECT ', ' + x.SubNumber FROM CutInTwo AS x WHERE x.Number=y.Number FOR XML PATH('')),1,2,''))
FROM OnlyMainNumbers AS y
If you really need this in one single string, you might wrap the final select like this:
SELECT STUFF(
(
SELECT ', ' + y.Number + ', ' + (STUFF((SELECT ', ' + x.SubNumber FROM CutInTwo AS x WHERE x.Number=y.Number FOR XML PATH('')),1,2,''))
FROM OnlyMainNumbers AS y
FOR XML PATH('')
),1,2,'')
Try this
DECLARE #tbl TABLE (InvoceNumber NVARCHAR(50))
INSERT INTO #tbl VALUES ('0180376000')
INSERT INTO #tbl VALUES ('0180376005')
INSERT INTO #tbl VALUES ('0180376003')
INSERT INTO #tbl VALUES ('0180375997')
INSERT INTO #tbl VALUES ('0180375993')
SELECT
(
SELECT
A.InvoceNumber + ', '
FROM
(
SELECT DISTINCT LEFT(InvoceNumber, LEN(InvoceNumber) - 2) InvoceNumber FROM #tbl
UNION ALL
SELECT RIGHT(InvoceNumber, 2) InvoceNumber FROM #tbl
) A
FOR XML PATH ('')
) Invoce
Output:
01803759, 01803760, 00, 05, 03, 97, 93,
If Order is important.
SELECT
(
SELECT
Result.InvoceNumber + ', ' + Result.Invo
FROM
(
SELECT
A.InvoceNumber,
(
SELECT
IA.Invo + ', ' AS [text()]
FROM
(
SELECT DISTINCT LEFT(InvoceNumber, LEN(InvoceNumber) - 2) InvoceNumber, RIGHT(InvoceNumber, 2) AS Invo FROM #tbl
) IA
WHERE
IA.InvoceNumber = A.InvoceNumber
FOR XML PATH ('')
) Invo
FROM
(
SELECT DISTINCT LEFT(InvoceNumber, LEN(InvoceNumber) - 2) AS InvoceNumber FROM #tbl
) A
GROUP BY
A.InvoceNumber
) Result
FOR XML PATH ('')
) S
Output:
01803759, 93, 97, 01803760, 00, 03, 05,
Currently, I have 12 rows with column Named 'Value'. The sample like this (just sample data, real data will be more):
Value
1
2
3
4
6
7
8
9
10
11
12
14
What I want is select them to get result like this:
Result Result_Miss
1-4, 6-12, 14 5, 13
I want to avoid using a cursor to work row-by-row.
Dynamic, set-based approach using CTEs to hunt down the missing values, and write out the ranges available based on those missing values.
--(I can't seem to get SqlFiddle to work with CTE's or I'd post one up here)--
Reworked to be more dynamic for number of records:
This works provided you always have '1' in your set of value
CREATE TABLE #OneTen
(
Value INT NOT NULL
);
INSERT INTO #OneTen
VALUES (1), (2), (3), (4), (6), (8), (9), (10), (11), (12), (14);
WITH ExpectedActual AS
(
SELECT ot.Value AS Actual, ROW_NUMBER() OVER (ORDER BY Value) AS Expected
FROM #OneTen AS ot
)
, DegreesOff AS
(
SELECT ea.Expected, ea.Actual, (ea.Actual - ea.Expected) AS Change
FROM ExpectedActual AS ea
)
, Missing AS
(
SELECT CASE
WHEN MIN(do.Expected) = 1 THEN 0
ELSE MIN(do.Expected) + do.Change - 1
END AS Missing
, ROW_NUMBER() OVER (ORDER BY MIN(do.Expected)) AS RowNumber
FROM DegreesOff AS do
GROUP BY do.Change
UNION ALL
SELECT MAX(do.Actual + 1), MAX(do.Change + 2) --Adding Last Value 1 higher than Actual so the code below that takes mNext.Missing - 1 brings it down to the proper value:
--Change + 2 to account for 0 plus being 1 higher
FROM DegreesOff AS do
)
SELECT STUFF((
SELECT ', ' + CASE
WHEN m.Missing + 1 = mNext.Missing - 1 THEN CAST(m.Missing + 1 AS NVARCHAR(4))
ELSE CAST(m.Missing + 1 AS NVARCHAR(4)) + '-' + CAST(mNext.Missing - 1 AS NVARCHAR(4))
END
FROM Missing AS m
LEFT JOIN Missing AS mNext ON m.RowNumber = mNext.RowNumber - 1
FOR XML PATH('')), 1, 2, '') AS Result
, STUFF((
SELECT ', ' + CAST(MIN(do.Expected + do.Change - 1) AS NVARCHAR(4))
FROM DegreesOff AS do
WHERE do.Change > 0
GROUP BY do.Change
FOR XML PATH('')), 1, 2, '') AS Result_Miss
Try the following script:
DDL
CREATE TABLE Numbers
(
Value INT NOT NULL
);
INSERT INTO Numbers
VALUES (1), (2), (3), (4), (6), (7), (8), (9), (10), (12),(13);
Script
DECLARE #MinValue INT
DECLARE #MaxValue INT
DECLARE #Temp TABLE(MissingValues INT)
DECLARE #MissingValues VARCHAR(50)
SELECT #MinValue = MIN(Value),
#MaxValue = MAX(Value)
FROM Numbers
;WITH CTE AS
(
SELECT #MinValue Value
UNION ALL
SELECT Value + 1
FROM CTE
WHERE Value + 1 <= #MaxValue
)
INSERT INTO #Temp
SELECT CTE.Value
FROM CTE
LEFT JOIN Numbers N
ON CTE.Value = N.Value
WHERE N.Value IS NULL
OPTION (MAXRECURSION 1000)
SELECT #MissingValues =
STUFF(( SELECT ',' + CAST(MissingValues AS VARCHAR)
FROM #Temp
FOR XML PATH('')),1,1,'')
INSERT INTO #Temp
SELECT #MinValue - 1
UNION ALL
SELECT #MaxValue + 1
;WITH CTE AS
(
SELECT MissingValues,
ROW_NUMBER() OVER(ORDER BY MissingValues ASC) RN
FROM #Temp
)
,Ranges AS
(
SELECT CAST(T1.MissingValues + 1 AS VARCHAR) + '-' +
CAST(T2.MissingValues - 1 AS VARCHAR) Ranges
FROM CTE AS T1
INNER JOIN CTE AS T2
ON T1.RN = T2.RN - 1
)
SELECT STUFF(( SELECT ',' + R.Ranges
FROM Ranges R
FOR XML PATH('')),1,1,'') Result,
#MissingValues AS Result_Miss
I am trying to use a recursive CTE in SQL Server to build up a predicate formula from a table containing the underlying tree structure.
For example, my table looks like:
Id | Operator/Val | ParentId
--------------------------
1 | 'OR' | NULL
2 | 'AND' | 1
3 | 'AND' | 1
4 | '>' | 2
5 | 'a' | 4
6 | 'alpha' | 4
...
...which represents ((a > alpha) AND (b > beta)) OR ((c > gamma) AND (a < delta)).
ParentId is a reference to the Id in the same table of the parent node.
I want to write a query which will build up this string from the table. Is it possible?
Thanks
For a production environment, you may want to go with a recursive function for simplicity if performance and recursion depth limits (32 levels) is not a problem.
However, here's a quite clean and pretty efficient solution with CTEs (note that it will accept any number of "trees" and return one result for each item which has no parent):
DECLARE #tbl TABLE
(
id int PRIMARY KEY
NOT NULL,
op nvarchar(max) NOT NULL,
parent int
) ;
INSERT INTO #tbl
SELECT 1, 'OR', NULL UNION ALL
SELECT 2, 'AND', 1 UNION ALL
SELECT 3, 'AND', 1 UNION ALL
SELECT 4, '>', 2 UNION ALL
SELECT 5, 'a', 4 UNION ALL
SELECT 6, 'alpha', 4 UNION ALL
SELECT 7, '>', 2 UNION ALL
SELECT 8, 'b', 7 UNION ALL
SELECT 9, 'beta', 7 UNION ALL
SELECT 10, '>', 3 UNION ALL
SELECT 11, 'c', 10 UNION ALL
SELECT 12, 'gamma', 10 UNION ALL
SELECT 13, '>', 3 UNION ALL
SELECT 14, 'd', 13 UNION ALL
SELECT 15, 'delta', 13 ;
WITH nodes -- A CTE which sets a flag to 1 for non-leaf nodes
AS (
SELECT t.*, CASE WHEN p.parent IS NULL THEN 0
ELSE 1
END node
FROM #tbl t
LEFT JOIN (
SELECT DISTINCT parent
FROM #tbl
) p ON p.parent = T.id
),
rec -- the main recursive run to determine the sort order and add meta information
AS (
SELECT id rootId, node lvl, CAST(0 AS float) sort, CAST(0.5 AS float) offset, *
FROM nodes
WHERE parent IS NULL
UNION ALL
SELECT r.rootId, r.lvl+t.node, r.sort+r.offset*CAST((ROW_NUMBER() OVER (ORDER BY t.id)-1)*2-1 AS float),
r.offset/2, t.*
FROM rec r
JOIN
nodes t ON r.id = t.parent
),
ranked -- ranking of the result to sort and find the last item
AS (
SELECT rootId, ROW_NUMBER() OVER (PARTITION BY rootId ORDER BY sort) ix,
COUNT(1) OVER (PARTITION BY rootId) cnt, lvl, op
FROM rec
),
concatenated -- concatenate the string, adding ( and ) as needed
AS (
SELECT rootId, ix, cnt, lvl, CAST(REPLICATE('(', lvl)+op AS nvarchar(max)) txt
FROM ranked
WHERE ix = 1
UNION ALL
SELECT r.rootId, r.ix, r.cnt, r.lvl,
c.txt+COALESCE(REPLICATE(')', c.lvl-r.lvl), '')+' '+COALESCE(REPLICATE('(', r.lvl-c.lvl), '')+r.op
+CASE WHEN r.ix = r.cnt THEN REPLICATE(')', r.lvl)
ELSE ''
END
FROM ranked r
JOIN
concatenated c ON (r.rootId = c.rootId)
AND (r.ix = c.ix+1)
)
SELECT rootId id, txt
FROM concatenated
WHERE ix = cnt
OPTION (MAXRECURSION 0);
I found something, but it looks pretty nasty. You would be able to do this a lot easier using a recursive fundtion...
DECLARE #Table TABLE(
ID INT,
Op VARCHAR(20),
ParentID INT
)
INSERT INTO #Table SELECT 1,'OR',NULL
INSERT INTO #Table SELECT 2,'AND',1
INSERT INTO #Table SELECT 3,'AND',1
INSERT INTO #Table SELECT 4,'>',2
INSERT INTO #Table SELECT 5,'a',4
INSERT INTO #Table SELECT 6,'alpha',4
INSERT INTO #Table SELECT 7,'>',2
INSERT INTO #Table SELECT 8,'b',7
INSERT INTO #Table SELECT 9,'beta',7
INSERT INTO #Table SELECT 10,'>',3
INSERT INTO #Table SELECT 11,'c',10
INSERT INTO #Table SELECT 12,'gamma',10
INSERT INTO #Table SELECT 13,'<',3
INSERT INTO #Table SELECT 14,'a',13
INSERT INTO #Table SELECT 15,'delta',13
;WITH Vals AS (
SELECT t.*,
1 Depth
FROM #Table t LEFT JOIN
#Table parent ON t.ID = parent.ParentID
WHERE parent.ParentID IS NULL
UNION ALL
SELECT t.*,
v.Depth + 1
FROM #Table t INNER JOIN
Vals v ON v.ParentID = t.ID
),
ValLR AS(
SELECT DISTINCT
vLeft.ID LeftID,
vLeft.Op LeftOp,
vRight.ID RightID,
vRight.Op RightOp,
vLeft.ParentID OperationID,
vLeft.Depth
FROM Vals vLeft INNER JOIN
Vals vRight ON vLeft.ParentID = vRight.ParentID
AND vLeft.ID < vRight.ID
WHERE (vRight.ID IS NOT NULL)
),
ConcatVals AS(
SELECT CAST('(' + LeftOp + ' ' + Op + ' ' + RightOp + ')' AS VARCHAR(500)) ConcatOp,
t.ID OpID,
v.Depth,
1 CurrentDepth
FROM ValLR v INNER JOIN
#Table t ON v.OperationID = t.ID
WHERE v.Depth = 1
UNION ALL
SELECT CAST('(' + cL.ConcatOp + ' ' + t.Op + ' {' + CAST(v.RightID AS VARCHAR(10)) + '})' AS VARCHAR(500)) ConcatOp,
t.ID OpID,
v.Depth,
cL.CurrentDepth + 1
FROM ValLR v INNER JOIN
#Table t ON v.OperationID = t.ID INNER JOIN
ConcatVals cL ON v.LeftID = cL.OpID
WHERE v.Depth = cL.CurrentDepth + 1
),
Replaces AS(
SELECT REPLACE(
c.ConcatOp,
SUBSTRING(c.ConcatOp,PATINDEX('%{%', c.ConcatOp), PATINDEX('%}%', c.ConcatOp) - PATINDEX('%{%', c.ConcatOp) + 1),
(SELECT ConcatOp FROM ConcatVals WHERE OpID = CAST(SUBSTRING(c.ConcatOp,PATINDEX('%{%', c.ConcatOp) + 1, PATINDEX('%}%', c.ConcatOp) - PATINDEX('%{%', c.ConcatOp) - 1) AS INT))
) ConcatOp,
1 Num
FROM ConcatVals c
WHERE Depth = (SELECT MAX(Depth) FROM ConcatVals)
UNION ALL
SELECT REPLACE(
r.ConcatOp,
SUBSTRING(r.ConcatOp,PATINDEX('%{%', r.ConcatOp), PATINDEX('%}%', r.ConcatOp) - PATINDEX('%{%', r.ConcatOp) + 1),
(SELECT ConcatOp FROM ConcatVals WHERE OpID = CAST(SUBSTRING(r.ConcatOp,PATINDEX('%{%', r.ConcatOp) + 1, PATINDEX('%}%', r.ConcatOp) - PATINDEX('%{%', r.ConcatOp) - 1) AS INT))
) ConcatOp,
Num + 1
FROM Replaces r
WHERE PATINDEX('%{%', r.ConcatOp) > 0
)
SELECT TOP 1
*
FROM Replaces
ORDER BY Num DESC
OUTPUT
ConcatOp
----------------------------------------------------------------
(((a > alpha) AND (b > beta)) OR ((c > gamma) AND (a < delta)))
If you would rather want to look at a recursive function, give me a shout and we can have a look.
EDIT: Recursive Function
Have a look at how much easier this is
CREATE TABLE TableValues (
ID INT,
Op VARCHAR(20),
ParentID INT
)
INSERT INTO TableValues SELECT 1,'OR',NULL
INSERT INTO TableValues SELECT 2,'AND',1
INSERT INTO TableValues SELECT 3,'AND',1
INSERT INTO TableValues SELECT 4,'>',2
INSERT INTO TableValues SELECT 5,'a',4
INSERT INTO TableValues SELECT 6,'alpha',4
INSERT INTO TableValues SELECT 7,'>',2
INSERT INTO TableValues SELECT 8,'b',7
INSERT INTO TableValues SELECT 9,'beta',7
INSERT INTO TableValues SELECT 10,'>',3
INSERT INTO TableValues SELECT 11,'c',10
INSERT INTO TableValues SELECT 12,'gamma',10
INSERT INTO TableValues SELECT 13,'<',3
INSERT INTO TableValues SELECT 14,'a',13
INSERT INTO TableValues SELECT 15,'delta',13
GO
CREATE FUNCTION ReturnMathVals (#ParentID INT, #Side VARCHAR(1))
RETURNS VARCHAR(500)
AS
BEGIN
DECLARE #RetVal VARCHAR(500)
IF (#ParentID IS NULL)
BEGIN
SELECT #RetVal = ' (' + dbo.ReturnMathVals(ID,'L') + Op + dbo.ReturnMathVals(ID,'R') + ') '
FROM TableValues
WHERE ParentID IS NULL
END
ELSE
BEGIN
SELECT TOP 1 #RetVal = ' (' + dbo.ReturnMathVals(ID,'L') + Op + dbo.ReturnMathVals(ID,'R') + ') '
FROM TableValues
WHERE ParentID = #ParentID
ORDER BY CASE WHEN #Side = 'L' THEN ID ELSE -ID END
SET #RetVal = ISNULL(#RetVal, (SELECT TOP 1 Op FROM TableValues WHERE ParentID = #ParentID ORDER BY CASE WHEN #Side = 'L' THEN ID ELSE -ID END))
END
RETURN #RetVal
END
GO
SELECT dbo.ReturnMathVals(NULL, NULL)
GO
DROP FUNCTION ReturnMathVals
DROP TABLE TableValues
Yes it is possible to do it but the problem is not the CTE, check it with PIVOT
read more about it from this link
http://msdn.microsoft.com/en-us/library/ms177410.aspx
some examples in this documentation is similar with your problem
I couldn't figure out how to do the double-recursion, but hopefully one of the intermediate CTEs in this will set you on the right track:
SET NOCOUNT ON
DECLARE #tree AS TABLE
(
Id int NOT NULL
,Operator varchar(10) NOT NULL
,ParentId int
)
INSERT INTO #tree
VALUES (1, 'OR', NULL)
INSERT INTO #tree
VALUES (2, 'AND', 1)
INSERT INTO #tree
VALUES (3, 'AND', 1)
INSERT INTO #tree
VALUES (4, '>', 2)
INSERT INTO #tree
VALUES (5, 'a', 4)
INSERT INTO #tree
VALUES (6, 'alpha', 4)
INSERT INTO #tree
VALUES (7, '>', 2)
INSERT INTO #tree
VALUES (8, 'b', 7)
INSERT INTO #tree
VALUES (9, 'beta', 7)
INSERT INTO #tree
VALUES (10, '>', 3)
INSERT INTO #tree
VALUES (11, 'c', 10)
INSERT INTO #tree
VALUES (12, 'gamma', 10)
INSERT INTO #tree
VALUES (13, '>', 3)
INSERT INTO #tree
VALUES (14, 'd', 13)
INSERT INTO #tree
VALUES (15, 'delta', 13) ;
WITH lhs_selector
AS (
SELECT ParentId
,MIN(Id) AS Id
FROM #tree
GROUP BY ParentId
),
rhs_selector
AS (
SELECT ParentId
,MAX(Id) AS Id
FROM #tree
GROUP BY ParentId
),
leaf_selector
AS (
SELECT Id
FROM #tree AS leaf
WHERE NOT EXISTS ( SELECT *
FROM #tree
WHERE ParentId = leaf.Id )
),
recurse
AS (
SELECT operator.Id
,CASE WHEN lhs_is_leaf.Id IS NOT NULL THEN NULL
ELSE lhs.Id
END AS LhsId
,CASE WHEN rhs_is_leaf.Id IS NOT NULL THEN NULL
ELSE rhs.Id
END AS RhsId
,CASE WHEN COALESCE(lhs_is_leaf.Id, rhs_is_leaf.Id) IS NULL
THEN '({' + CAST(lhs.Id AS varchar) + '} ' + operator.Operator + ' {'
+ CAST(rhs.Id AS varchar) + '})'
ELSE '(' + lhs.Operator + ' ' + operator.Operator + ' ' + rhs.Operator + ')'
END AS expression
FROM #tree AS operator
INNER JOIN lhs_selector
ON lhs_selector.ParentID = operator.Id
INNER JOIN rhs_selector
ON rhs_selector.ParentID = operator.Id
INNER JOIN #tree AS lhs
ON lhs.Id = lhs_selector.Id
INNER JOIN #tree AS rhs
ON rhs.Id = rhs_selector.Id
LEFT JOIN leaf_selector AS lhs_is_leaf
ON lhs_is_leaf.Id = lhs.Id
LEFT JOIN leaf_selector AS rhs_is_leaf
ON rhs_is_leaf.Id = rhs.Id
)
SELECT *
,REPLACE(REPLACE(op.expression, '{' + CAST(op.LhsId AS varchar) + '}', lhs.expression),
'{' + CAST(op.RhsId AS varchar) + '}', rhs.expression) AS final_expression
FROM recurse AS op
LEFT JOIN recurse AS lhs
ON lhs.Id = op.LhsId
LEFT JOIN recurse AS rhs
ON rhs.Id = op.RhsId