SQL Server: String insertion by position - sql-server

I want to insert a set of values into a given string at specified positions. I couldn't find an existing solution for this so I created the following query which does so. I am wondering if there is a way to simplify it, especially the portion which queries the cte and creates the NewString. Also, is there a way to construct the cte recursion without involving the ID column? The ID (and the rest) seems to work fine, just wondering if it could be tweaked to make it more organic/elegant or if there is a better solution overall. The different groups are really only for testing purposes; each group corresponds to a possible insertion position scenario. Thus, there will not be groups involved when I use it.
declare
#String varchar(200),
#StringLen int,
#GroupID int,
#PositionMax int
declare #Chars table (
ID int identity,
GroupID int,
Position int,
Value varchar(20)
)
select
#String = 'abcde',
#StringLen = len(#String),
#GroupID = 1
--Affix
--[P]refix
--[I]nfix
--[S]uffix
insert #Chars
select
GroupID,
Position,
Value
from (
values
(1, 0, 'X'), --P
(2, 2, 'Y'), --I
(3, 5, 'Z'), --S
(4, 0, 'X'), --P
(4, 2, 'Y'), --I
(5, 2, 'Y'), --I
(5, 5, 'Z'), --S
(6, 0, 'X'), --P
(6, 5, 'Z'), --S
(7, 0, 'X'), --P
(7, 2, 'Y'), --I
(7, 5, 'Z'), --S
(8, 2, 'Y1'), --I
(8, 4, 'Y2'), --I
(9, 0, 'X'), --P
(9, 2, 'Y1'), --I
(9, 4, 'Y2'), --I
(10, 2, 'Y1'), --I
(10, 4, 'Y2'), --I
(10, 5, 'Z'), --S
(11, 0, 'X'), --P
(11, 2, 'Y1'), --I
(11, 4, 'Y2'), --I
(11, 5, 'Z') --S
) as T(GroupID, Position, Value)
order by GroupID, Position
;with cte (
ID,
GroupID,
LeftString,
Value,
RightString,
Position
) as (
select
ID,
GroupID,
LeftString,
Value,
RightString,
Position
from (
select
row_number() over (partition by GroupID order by ID) as RowNumber,
ID,
GroupID,
cast(left(#String, Position) as varchar(200)) as LeftString,
Value,
cast(right(#String, #StringLen - Position) as varchar(200)) as RightString,
Position
from #Chars
) as C
where RowNumber = 1
union all
select
vc.ID,
vc.GroupID,
cast(left(RightString, vc.Position - cte.Position) as varchar(200)) as LeftString,
vc.Value,
cast(right(RightString, #StringLen - vc.Position) as varchar(200)) as RightString,
vc.Position
from #Chars vc
join cte cte
on cte.GroupID = vc.GroupID
and cte.ID + 1 = vc.ID
)
select
GroupID,
case
when LSLenSumMax < #StringLen
then NewString + RightString
else NewString
end as NewString
from (
select
GroupID,
max(LSLenSum) as LSLenSumMax,
RightString,
stuff((
select
LeftString + Value
from cte cte
where cte.GroupID = cteLR.GroupID
for xml path(''), type
).value('(./text())[1]', 'varchar(max)'), 1, 0, '') as NewString
from (
select
GroupID,
(select sum(len(LeftString)) from cte cteL where cteL.groupID = cte.groupID) as LSLenSum,
(select top 1 RightString from cte cteR where cteR.groupID = cte.groupID order by cteR.ID desc) as RightString
from cte cte
) as cteLR
group by
GroupID,
RightString
) as C
order by GroupID

You can implement a custom
aggregate function... Or try this: A recursive scalar function:
CREATE FUNCTION dbo.GetPrefixTo (#GroupID INT, #Position INT, #String CHAR(200))
RETURNS Char(200)
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #str CHAR(200) = NULL
DECLARE #beforePosition INT = 0
IF (SELECT COUNT(*) FROM Chars WHERE GroupID = #GroupID AND Position < #Position) > 0
BEGIN
SELECT #beforePosition = MAX(Position) FROM chars WHERE GroupID = #GroupID AND Position < #Position
select #str = RTRIM(dbo.GetPrefixTo(#GroupID, #beforePosition, substring(#String, 1, #Position))) +
+ RTrim(Value) + substring(#String, #Position + 1, len(#string) + 1)
FROM Chars WHERE GroupID = #GroupID AND Position = #Position
END
ELSE
SELECT #str = substring(#String, 1, #Position) + RTrim(Value) + substring(#String, #Position + 1, len(#string) + 1) FROM Chars
WHERE GroupID = #GroupID AND Position = #Position
RETURN #str
END
And group by GroupID and aggregate max(position):
SELECT groupID, max(Position)
, dbo.GetPrefixTo(groupID, max(Position), 'abcde')
FROM Chars
GROUP BY GroupID
This is the result:
1 0 Xabcde
2 2 abYcde
3 5 abcdeZ
4 2 XabYcde
5 5 abYcdeZ
6 5 XabcdeZ
7 5 XabYcdeZ
8 4 abY1cdY2e
9 4 XabY1cdY2e
10 5 abY1cdY2eZ
11 5 XabY1cdY2eZ
========================================================================

Related

t-sql - Apply function to a column without sqlFunction

I've to transform a numeric code in an alphabetic code like this:
1234 -> ABCD
where 0 = 0, 1 = A, 2 = b, etc..
This is my function and how to use:
Create function dbo.DecodeNumToChar
(
#change varchar(10),
#foo varchar(10)
) returns varchar(10)
as
begin
DECLARE #II int = 1,
#result varchar(10)
;WITH x AS
(
SELECT #II as ii, STUFF(#foo,#II,1,SUBSTRING(#change,CAST(SUBSTRING(#foo,#II,1) AS INT)+1,1)) AS AA
UNION ALL
--SELECT #II+1
SELECT ii+1, STUFF(AA,ii+1,1,SUBSTRING(#change,CAST(SUBSTRING(#foo,ii+1,1) AS INT)+1,1)) AS AA
FROM x
where ii+1 <= LEN(#foo)
)
select top 1 #result = AA from x order by ii desc
return #result
end
--------------------------------------------
select brand_code, dbo.DecodeNumToChar('0ABCDEFGHI', brand_code)
from (VALUES('1234'),('5834'),('9905'),('0250')) as t(brand_code)
This function working well, but in production DB we do not have permission to create functions.
I've tryed to transform this function into CTE like this
declare #change varchar(9) = 'ABCDEFGHI'
DECLARE #II int = 0
;WITH x AS
(
SELECT TOP (10) n = ROW_NUMBER() OVER (ORDER BY Number)
FROM master.dbo.spt_values ORDER BY Number
),
innerCTE as
(
SELECT x.n, SUBSTRING(t.brand_code, x.n, 1) chnum,
case SUBSTRING(t.brand_code, x.n, 1)
when '0' then '0'
else char(65-1+SUBSTRING(t.brand_code, x.n, 1))
end chalfa, t.brand_code
FROM x INNER JOIN (VALUES('1234'),('5834'),('9905'),('0250')) as t(brand_code)
ON x.n <= LEN(t.brand_code)
),
CTE as
(
select n, chnum, chalfa, brand_code, stuff(brand_code, n, 1, chalfa) as code
from innerCTE
union all
select n+1, chnum, chalfa, brand_code, STUFF(code, n+1, 1, chalfa) as code
from cte
where n+1 <= LEN(cte.brand_code)
)
--select * from innerCTE
select * from CTE;
or using CROSS APPLY like this example: Example_1
or using CROSS APPLY with PIVOT like this example: Example_2
but my experience on SQL is low and I have not been able to get the correct result.
I'd like to have this:
brand_code decoded_code
1234 ABCD
5834 EHCD
9905 II0E
0250 0BE0
thanks
If using SQL 2017+
DECLARE #integerValues TABLE ([I] INT);
INSERT INTO #integerValues ([I])
VALUES
(1234),
(6485834),
(99084705),
(1124601);
SELECT
T.[I],
STRING_AGG(
CASE SUBSTRING(T.[S], V.[number] + 1, 1)
WHEN '9' THEN 'I'
WHEN '8' THEN 'H'
WHEN '7' THEN 'G'
WHEN '6' THEN 'F'
WHEN '5' THEN 'E'
WHEN '4' THEN 'D'
WHEN '3' THEN 'C'
WHEN '2' THEN 'B'
WHEN '1' THEN 'A'
ELSE '0'
END,
'') WITHIN GROUP (ORDER BY T.[I]) [S]
FROM
(SELECT [I], CAST([I] AS VARCHAR(10)) [S] FROM #integerValues) T
JOIN
[master]..[spt_values] V ON V.[number] < LEN(T.[S])
WHERE
V.[type] = 'P';
or, if 2016 or earlier
DECLARE #integerValues TABLE ([I] INT);
INSERT INTO #integerValues ([I])
VALUES
(1234),
(6485834),
(99084705),
(1124601);
SELECT
[I],
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
REPLACE(
CAST([I] AS VARCHAR(10)),
'1',
'A'),
'2',
'B'),
'3',
'C'),
'4',
'D'),
'5',
'E'),
'6',
'F'),
'7',
'G'),
'8',
'H'),
'9',
'I') [S]
FROM
#integerValues;
The following solution uses a FOR XML subquery and some mod (%) operations against the integer value to split each digit, then a mapping table to relate each digit with a character. The FOR XML returns the digits back in order.
DECLARE #IntegerValues TABLE (Integer INT)
INSERT INTO #IntegerValues (Integer)
VALUES
(1234),
(6485834),
(99084705),
(1124601)
SELECT
T.Integer,
Conversion = (
SELECT
'' + M.Character -- Must have no declared alias (for xml)
FROM
(VALUES
(1, T.Integer % 10),
(2, T.Integer / 10 % 10),
(3, T.Integer / 100 % 10),
(4, T.Integer / 1000 % 10),
(5, T.Integer / 10000 % 10),
(6, T.Integer / 100000 % 10),
(7, T.Integer / 1000000 % 10),
(8, T.Integer / 10000000 % 10),
(9, T.Integer / 100000000 % 10),
(10, T.Integer / 1000000000 % 10),
(11, T.Integer / 10000000000 % 10)
) AS X(OrdinalPosition, SplitDigit)
INNER JOIN (VALUES
(0, '0'),
(1, 'A'),
(2, 'B'),
(3, 'C'),
(4, 'D'),
(5, 'E'),
(6, 'F'),
(7, 'G'),
(8, 'H'),
(9, 'I')
) AS M(Digit, Character) ON X.SplitDigit = M.Digit
WHERE
X.OrdinalPosition <= FLOOR(LOG10(T.Integer)) + 1 -- This expression returns the number of digits
ORDER BY
X.OrdinalPosition DESC
FOR XML
PATH('')
)
FROM
#IntegerValues AS T
Result:
Integer Conversion
1234 ABCD
6485834 FDHEHCD
99084705 II0HDG0E
1124601 AABDF0A
There is probably a cleaner way to write the mappings and the mod operations, but this should give an idea.
I've find a solution using recursive CTE.
declare #change varchar(10) = '0ABCDEFGHI'
DECLARE #II int = 1;
with BRANDS as
(
select * from (VALUES('1234'),('5834'),('9905'),('0250')) as t(brand_code)
),
CTE as
(
select #II as ii, BRAND_CODE,
STUFF(brand_code,#II,1,SUBSTRING(#change,CAST(SUBSTRING(brand_code,#II,1) AS INT)+1,1)) AS AA
from BRANDS
union all
select c.ii+1, b.BRAND_CODE,
STUFF(AA,c.ii+1,1,SUBSTRING(#change,CAST(SUBSTRING(AA,c.ii+1,1) AS INT)+1,1)) AS AA
from BRANDS b
inner join CTE c on b.BRAND_CODE = c.BRAND_CODE
where c.ii < LEN(b.BRAND_CODE)
)
select BRAND_CODE, AA as NewCode from CTE where ii = len(brand_code) order by BRAND_CODE, ii
This is the code for the suggestion from #Martin Smith to use 10 nested replace.
DECLARE #change char(10) = '0ABCDEFGHI';
--------------------------------------------
select brand_code,
REPLACE( REPLACE( REPLACE( REPLACE( REPLACE(
REPLACE( REPLACE( REPLACE( REPLACE( REPLACE( brand_code
, '0', SUBSTRING( #change, 1, 1)) , '1', SUBSTRING( #change, 2, 1))
, '2', SUBSTRING( #change, 3, 1)) , '3', SUBSTRING( #change, 4, 1))
, '4', SUBSTRING( #change, 5, 1)) , '5', SUBSTRING( #change, 6, 1))
, '6', SUBSTRING( #change, 7, 1)) , '7', SUBSTRING( #change, 8, 1))
, '8', SUBSTRING( #change, 9, 1)) , '9', SUBSTRING( #change, 10, 1))
from (VALUES('1234'),('5834'),('9905'),('0250')) as t(brand_code);

I want to convert some data to pivot in SQL Server with join and dynamic

The table, at last, is my target.
This is my demo database
create database pvtestDb;
go
use pvtestDb;
go
create table custTransaction
(
id int,
custNum int,
value nvarchar(50)
)
go
create table customers
(
id int,
custName nvarchar(50)
)
insert into Customers(id, custName)
values (1, 'aaa'), (2, 'bbb'), (3, 'ccc'), (4, 'ddd'),
(5, 'eee'), (6, 'fff'), (7, 'ggg'), (8, 'hhh'), (9, 'iii')
insert into custTransaction (id, custNum, value)
values (1, 3, 'a'), (1, 4, 'b'), (1, 5, 'c'),
(2, 3, 'd'), (2, 4, 'e'), (2, 6, 'f'),
(3, 3, 'g'), (3, 8, 'h'), (3, 9, 'i')
select * from customers
select * from custTransaction
select custName, custNum, value
from customers
join custTransaction on custTransaction.id = customers.id
I tried code like this but not worked at all
SELECT
custNum, [a], [b], [c], [d]
FROM
customers
JOIN
custTransaction ON custTransaction.id = customers.id
PIVOT
(COUNT([custName])
FOR [custName] IN ([a], [b], [c], [d])) AS p
I need to join between the two tables in first.
Any hints would be appreciated as I am stuck with this situation
Here's approach with dynamic SQL
declare #customers varchar(8000)
declare #sql varchar(8000)
select #customers = stuff((
select ',' + quotename(custName)
from customers
for xml path('')
), 1, 1, '')
set #sql = 'select
id, ' + #customers + '
from (
select
ct.id, c.custName, ct.value
from
customers c
join custTransaction ct on ct.custNum = c.id
) t
pivot (
max(value) for custName in (' + #customers + ')
) p'
exec (#sql)
Output
id aaa bbb ccc ddd eee fff ggg hhh iii
----------------------------------------------------------------
1 NULL NULL a b c NULL NULL NULL NULL
2 NULL NULL d e NULL f NULL NULL NULL
3 NULL NULL g NULL NULL NULL NULL h i

Alternative for this Cursor SQL

How can I do this without the cursor:
SET NOCOUNT ON;
DECLARE #VAR_A BIGINT, #VAR_B TINYINT;
DECLARE _CURSOR CURSOR FOR
SELECT A, B FROM MY_TABLE
OPEN _CURSOR
FETCH NEXT FROM _CURSOR
INTO #VAR_A, #VAR_B
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT TOP 2 A, B, C, ROW_NUMBER() OVER (ORDER BY A DESC) AS ROW_NUM
INTO #TMP FROM MY_TABLE_2
WHERE A = #VAR_A AND X = 0 ORDER BY A DESC
IF ((SELECT COUNT(*) FROM #TMP) = 1) BEGIN
UPDATE MY_TABLE
SET Y = (SELECT B FROM #TMP WHERE ROW_NUM = 1)
WHERE A = #VAR_A
END ELSE IF (#VAR_B = 7) BEGIN
UPDATE MY_TABLE
SET Y = (SELECT B FROM #TMP WHERE ROW_NUM = 2),
Z = (SELECT C FROM #TMP WHERE ROW_NUM = 2)
WHERE A = #VAR_A
END ELSE BEGIN
UPDATE MY_TABLE
SET Y = (SELECT B FROM #TMP WHERE ROW_NUM = 2)
WHERE A = #VAR_A
END
DROP TABLE #TMP
FETCH NEXT FROM _CURSOR
INTO #VAR_A, #VAR_B
END
CLOSE _CURSOR;
DEALLOCATE _CURSOR;
I have a PRODUCT table, this table has a status column, the last status of the product. I have another table, the PRODUCT_HISTORY, that have all information changed on product, including the older status. For each product, I need to get the last status and insert it on a new field on product table. Like this:
This should do the trick...
IF OBJECT_ID('tempdb..#Product', 'U') IS NOT NULL
DROP TABLE #Product;
CREATE TABLE #Product (
Id INT NOT NULL PRIMARY KEY CLUSTERED,
[Description] VARCHAR(20) NOT NULL,
[Status] INT NOT NULL,
OlderStatus INT NULL
);
INSERT #Product (Id, [Description], [Status]) VALUES
(1, 'Product A', 5),
(2, 'Product B', 7),
(3, 'Product C', 4),
(4, 'Product D', 3),
(5, 'Product E', 0);
IF OBJECT_ID('tempdb..#ProductHistory', 'U') IS NOT NULL
DROP TABLE #ProductHistory;
CREATE TABLE #ProductHistory (
Id INT NOT NULL PRIMARY KEY CLUSTERED,
[Date] DATE NOT NULL,
Status INT NOT NULL,
ProductId INT NOT NULL
);
INSERT #ProductHistory (Id, [Date], [Status], ProductId) VALUES
(1, '2017-01-01', 0, 1),
(2, '2017-01-02', 1, 1),
(3, '2017-01-03', 5, 1),
(4, '2017-01-04', 0, 2),
(5, '2017-01-05', 1, 2),
(6, '2017-01-06', 5, 2),
(7, '2017-01-07', 7, 2),
(8, '2017-01-08', 0, 3),
(9, '2017-01-09', 4, 3),
(10, '2017-01-10', 0, 4),
(11, '2017-01-11', 3, 4),
(12, '2017-01-12', 0, 5);
--===================================================
-- the actual solution...
WITH
cte_PH AS (
SELECT
ph.Id, ph.Date, ph.Status, ph.ProductId,
RN = ROW_NUMBER() OVER (PARTITION BY ph.ProductId ORDER BY ph.Date DESC)
FROM
#ProductHistory ph
)
UPDATE p SET
p.OlderStatus = ISNULL(ph.Status, 0)
FROM
#Product p
LEFT JOIN cte_PH ph
ON p.id = ph.ProductId
AND ph.RN = 2;
----------------------------------------
SELECT * FROM #Product p;
Results...
Id Description Status OlderStatus
----------- -------------------- ----------- -----------
1 Product A 5 1
2 Product B 7 5
3 Product C 4 0
4 Product D 3 0
5 Product E 0 0

Get parents and children given a segment in a recursion SQL

Given an ID at the beginning or middle or end, I want to look up for all the rows that are linked between them.
With this I found the child elements recursively
declare #T table(
Id int primary key,
Name nvarchar(255) not null,
ParentId int)
insert into #T values
(1, 'A', NULL),
(2, 'B', 1),
(3, 'C', 2),
(4, 'D', NULL),
(5, 'E', 1)
declare #Id int = 2
;with cte as
(
select T.*
from #T as T
where T.Id = #Id
union all
select T.*
from #T as T
inner join cte as C
on T.ParentId = C.Id
)
select *
from cte
If #Id is equals 2, I will get 2 and 3. But I want to retrieve the parent too, in this case 1.
I want to get this:
If id = 1
1 A NULL
2 B 1
3 C 2
5 E 1
If id = 2
1 A NULL
2 B 1
3 C 2
5 E 1
If id = 3
1 A NULL
2 B 1
3 C 2
5 E 1
If id = 4
4 D NULL
IF id = 5
1 A NULL
2 B 1
3 C 2
5 E 1
How can I fix this?
Ok! I've got you:
I created a separate family for testing.
The first thing you need to do is create a query to find the oldest ancestor of your chosen ID. That is the first CTE below. Then you designate that, I called it #Eve and find Eve, all children of Eve.
Here is the example you can play around with
create table UserType (
Id int,
name nvarchar(255),
parentid int)
insert into UserType values
(1, 'A', NULL),
(2, 'B', 1),
(3, 'C', 2),
(4, 'D', NULL),
(5, 'E', 1),
(6, 'F', NULL),
(7, 'G', 6),
(8, 'H', 7);
DECLARE #eve BIGINT; --ancestor
DECLARE #id BIGINT;
SET #id = 8; --This is where you choose
WITH tblParent AS --CTE for oldest ancestor
(
SELECT *
FROM UserType WHERE Id = #id
UNION ALL
SELECT UserType.*
FROM UserType JOIN tblParent ON UserType.Id = tblParent.ParentId
)
select
#eve = (select top 1 id from tblParent order by id);
WITH tblChild AS --CTE for all #eve and all children
(
SELECT *
FROM UserType WHERE id = #eve
UNION ALL
SELECT UserType.* FROM UserType JOIN tblChild ON UserType.ParentId = tblChild.Id
)
SELECT * FROM tblChild
order by id
OPTION(MAXRECURSION 32767)
Props to CodeProject which was quite helpful.
Try something like this.
declare #tbl table( --test table var
Id int,
name nvarchar(255),
parentid int)
insert into #tbl values -- test data
(1, 'A', NULL),
(2, 'B', 1),
(3, 'C', 2),
(4, 'D', NULL),
(5, 'E', 1),
(6, 'F', NULL),
(7, 'G', 6),
(8, 'H', 7);
declare #id int = 7
;with parents as (
select id,name,parentid, 0 lvl
from #tbl
where id=#id
union all
select t.id,t.name,t.parentid, lvl-1
from #tbl t
inner join parents p on t.Id=p.parentid --up the tree
)
,children as (
select id,name,parentid, 0 lvl
from #tbl
where id=#id --select single record
union all
select t.id,t.name,t.parentid, lvl+1
from #tbl t
inner join children c on c.Id=t.parentid -- down the tree
)
select * from parents
union --combine results
select * from children
order by lvl

Recursive CTE Problem

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

Resources