T-SQL replace function missing some occurrences with nvarchar(max) - tsql

Given the following T-SQL code:
declare #lf char(1) set #lf = char(10);
declare #cr char(1) set #cr = char(13);
select replace(isnull(note,''), #cr+#lf,#lf) from T
are there circumstances where not every occurrence of #cr+#lf in the note column would be replaced with #lf?
I am trying to troubleshoot a situation where that is indeed happening.
The note column is defined as nvarchar(max). The documentation says:
If string_expression is not of type varchar(max) or nvarchar(max), REPLACE truncates the return value at 8,000 bytes. To return values greater than 8,000 bytes, string_expression must be explicitly cast to a large-value data type.
If I understand that correctly, there's no need to cast because note is already of the proper datatype to allow for return values greater than 8000 bytes.
I thought maybe the isnull function was not returning nvarchar(max) but the documentation says it returns the type of the value being tested:
... Return Types
Returns the same type as check_expression.
And the returned value isn't being truncated; it's just that some crlf pairs are being overlooked.
I must be overlooking something.
declare #t table( notes nvarchar(max));
insert #t(notes)
values
(
'From: kkkkkkk#aaaaaaaaa.com <kkkkkkk#aaaaaaaaa.com>
Sent: Monday, May 00, 0008 00:55 PM
To: Jan Zzzz <sZzzz#dddd.dd.com>
Subject: RE: [Secure Message] aaaaaaaaa ABC ddddddddddddd--XXXXX-X
Hi Jan,
The ddddddddddddd is valid for one year. I have attached the Zzzzzzz Rrrrrrrr which you will find behind the blank cover page and ddddddddddddd form. Please let me know if this is what you need.
Best Regards,
Yyyyyy
Kkkkkkkk Kkkkkk, ABC, DEF
ABC Mmmmmmmm
P 800.007.0000 ext 000 | F 600.000.0070
Electronic mail is not considered to be a secure form of communication for Protected Health Information. If you are concerned about privacy, please call aaaaaaaaa directly at 0-800-007-0000. If you would like to review our privacy policy, it can be found on our website: www.ddddddddddddd.com.
This email, including any attached files, may contain confidential and privileged information for the sole use of the intended recipient(s). Any review, use, distribution or disclosure by others is strictly prohibited. If you are not the addressee indicated in this message (or authorized to receive information for the recipient), please contact the sender by reply e-mail and delete all copies of this message (including any attachments).
From: Jan Zzzz <sZzzz#dddd.dd.com>
Sent: Monday, May 00, 0008 8:56 AM
To: Kkkkkkkk Kkkkkk <kkkkkkk#aaaaaaaaa.com>; Jan Zzzz <sZzzz#dddd.dd.com>
Subject: Re: [Secure Message] aaaaaaaaa ABC ddddddddddddd--XXXXX-X
Hi, this expired, I need a copy of the aaaaaaa aaaa so I can submit my aaaaaaa aaa aaaaaaaa. Thank you. SZzzz
On 0/00/0008 8:00 AM, Jan Zzzz wrote:
Thank you for the dddddddd, I am mmmmmmm mmm today.
On 0/0/0008 6:05 PM, Kkkkkkkk Kkkkkk wrote:
[Secure Message] aaaaaaaaa ABC ddddddddddddd--XXXXX-X
Kkkkkkkk Kkkkkk has sent you a secure message.
Subject: aaaaaaaaa ABC ddddddddddddd--XXXXX-X
From: Kkkkkkkk Kkkkkk <kkkkkkk#aaaaaaaaa.com>
To: Jan Zzzz <sZzzz#dddd.dd.com>
Expires: May 00, 0008
View Secure Message
Note: If you have any concerns about the authenticity of this message please contact the original sender directly.'
)
select notes from #t;
select replace(notes, char(13),'') from #t;

You would be left with #cr+#lf in the SELECT if #cr+#cr+#lf occurs in the note, unless you need #cr when it occurs on its own you are probably better off doing:
declare #cr char(1) set #cr = char(13);
select replace(isnull(note,''), #cr,'') from T

if its just a single line, this should do it, remove them all and put it back
set #note = replace(replace(isnull(#note,''),#cr,''),#lf,'')+#lf . //or whatever line endings you want
if its multi line try something like this
declare #note as nvarchar(max)
declare #lf char(1) set #lf = char(10);
declare #cr char(1) set #cr = char(13);
set #note = 'A'+char(10)+char(13)+char(10)+char(13)+char(10)+char(13)+char(10)+char(13)+'A'+char(10)+char(13)
set #note = replace(isnull(#note,''),#cr,'')
--not sure if you want to keep all the user lf's but if you want only one try this?
if (patindex(isnull(#note,''),#lf+#lf) >= 0)
begin
set #note = replace(isnull(#note,''),#lf+#lf,#lf)
end
select #note
select cast(#note as VARBINARY(100))
select len(#note)

Every circumstance will be replaced, but you might be creating some CrLfs via your replace. Please see the example below and how to mitigate it.
DECLARE #Cr CHAR(1)=CHAR(13)
DECLARE #Lf CHAR(1)=CHAR(10)
DECLARE #CrLf CHAR(2)=CHAR(13)+CHAR(10)
DECLARE #NoteTbl TABLE(Note NVARCHAR(MAX))
INSERT INTO #NoteTbl (Note) SELECT #Cr + #CrLf
--example can result in CrLF being created
SELECT [NewNote],LEN([NewNote]) FROM (SELECT replace(isnull(note,''), #CrLf,#lf) AS [NewNote] FROM #NoteTbl) AS a
--Option 1: Replace all Cr with nothing; this is effectively the same as replacing CrLf with Lf
SELECT [NewNote],LEN([NewNote]) FROM (SELECT replace(isnull(note,''), #Cr,'') AS [NewNote] FROM #NoteTbl) AS a
--Option 2: insert the notes into a table and loop until CrLf is gone, this might be useful if you need to do multiple different data scrubs
DECLARE #NotesCleaned TABLE(Note NVARCHAR(MAX))
INSERT INTO #NotesCleaned (Note) SELECT Note FROM #NoteTbl
WHILE EXISTS(
SELECT * FROM #NotesCleaned WHERE Note Like '%' + #CrLf + '%'
)
BEGIN
UPDATE #NotesCleaned SET Note=replace(isnull(note,''), #CrLf,#lf)
END
SELECT Note,LEN(Note) FROM #NotesCleaned

I believe I may have found a partial answer. In SSMS:
Tools->Options->SQL Server->Results to Grid
[ x ] Retain CR/LF on copy or save
will actually restore the CR that your call to replace() has removed.

Related

How can I return the first 3 characters after every comma in a string in TSQL

I have a string that I need to seperate into values seperated by commas. I have achieved this part with the below REPLACE statement:
declare #mc varchar(200)
declare #mc1 varchar(200)
select #mc = 'FRED&#g4;&#4g;MARY&#g4;&#4g;BILL&#g4;&#4g;TIMOTHY&#g4;&#4g;JOHNATHAN'
select #mc1 = REPLACE(#mc, '&#g4;&#4g;',', ')
The replace returns a string 'FRED, MARY, BILL, TIMOTHY, JOHNATHAN'
I then want to have another variable that will return the first 3 characters of each value before the commas, so the above string would be returned as:
'FRE, MAR, TIM, JOH'
Anyone know how I can achieve this?
Also happy for this to be done directly to the original #mc variable
ON SQL Server 2017+ you can make use of openJson to split the string into manageble segments and then string_agg to assemble the desired result:
declare #mc varchar(100)='FRED&#g4;&#4g;MARY&#g4;&#4g;BILL&#g4;&#4g;TIMOTHY&#g4;&#4g;JOHNATHAN'
select String_Agg(v, ', ')
from (select #mc)x(s)
cross apply (
select Left(j.[value],3) v, Convert(tinyint,j.[key]) Seq
from OpenJson(Concat('["',replace(s,';', '","'),'"]')) j
where Convert(tinyint,j.[key]) % 2 = 0
)j;
Demo Fiddle

How to fetch doctype eg: address or tax rule

I want to fetch the doctype. How do I do this? I want to add a separate column which will give doctype such as sales order, purchase order etc. The first line gives me error what query should be fired. Please help I am new to ERP Next.
SELECT
AD.ref_doctype AS “Doctype:Link/User:120”,
AD.name AS “Doc#:Link/Doctype:120”,
AD.owner AS “Created By:Link/User:120”,
AD.modified AS “Modified On:Date:120”
FROM tabAddress AS AD
WHERE
DATEDIFF(now(),AD.modified) BETWEEN 1 AND 30
UNION ALL
SELECT
TR.name AS “Doc#:Link/Doctype:120”,
TR.owner AS “Created By:Link/User:120”,
TR.modified AS “Modified On:Date:120”
FROM tabTax Rule AS TR
WHERE
DATEDIFF(now(),TR.modified) BETWEEN 1 AND 30
UNION ALL
SELECT
IT.name AS “Doc#:Link/Doctype:120”,
IT.owner AS “Created By:Link/User:120”,
IT.modified AS “Modified On:Date:120”
FROM tabItem AS IT
WHERE
DATEDIFF(now(),IT.modified) BETWEEN 1 AND 30
It isn't completely clear to me what you mean by docType field.
Are you wanting a result like this?
Doctype:Link/User:120|Doc#:Link/Doctype:120|Created By:Link/User:120|Modified On:Date:120|
---------------------|---------------------|------------------------|--------------------|
Email Account |Jobs |Administrator | 2019-12-04 06:07:55|
Email Account |Notifications |Administrator | 2019-12-01 05:25:53|
Email Account |Replies |Administrator | 2019-12-01 05:25:53|
Email Account |Sales |Administrator | 2019-12-04 06:07:55|
Email Account |Support |Administrator | 2019-12-04 06:07:55|
Here's the select :
set #docType = "Email Account";
SELECT
#tabDocType AS `Doctype:Link/User:120`,
AD.name AS `Doc#:Link/Doctype:120`,
AD.owner AS `Created By:Link/User:120`,
AD.modified AS `Modified On:Date:120`
FROM `tabEmail Account` AS AD
Note the backticks on the field aliases! All these have different meanings in SQL:
"
'
`
The last one, backtick, is used to refer to database entities. You were trying to use “Doctype:Link/User:120” with double quotes, which declare plain text. Using backtick converts the alias into a db entity which can be referred to from elsewhere.
MariaDb doesn't allow the use of variables as table names directly, but you can do it using prepared statements, like this:
set #docType = "Email Account";
set #tabDocType = CONCAT('tab', #docType);
SET #sql_text = concat('
SELECT
"', #docType, '" AS `Doctype:Link/User:120`
, AD.name AS `Doc#:Link/Doctype:120`
, AD.owner AS `Created By:Link/User:120`
, AD.modified AS `Modified On:Date:120`
FROM `', #tabDocType, '` as AD;
');
PREPARE stmt FROM #sql_text;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
The table name is now also specified by a variable, created from concatenation of 'tab' with the docType declared before.
You get the same result as above but -- you avoid accidentally changing the table name in one place but not in the other when editing some time in the future.
to fetch doctype name you have to give the linked doctype name, For example,
select
IT.name as "IT No:Link/IT:120"

Using date in ref cusor with pl/sql

I have a var: acc_date with type date.
It takes its value from a cursor and when I insert its value to logger table as:
insert into logger values(1,acc_date);
the out put when a select it from logger is
1 01-JAN-10
but when i use it to compare with another Date value in another cursor as
OPEN c_get_date_id
for 'SELECT Date_D.DATEKEY from Date_D where Date_D.DATEVALUE='||acc_date;
EXIT WHEN c_get_date_id%NOTFOUND;
FETCH c_get_date_id
INTO date_id;
insert into logger values (1,'Now with date_id'||date_id);
CLOSE c_get_date_id;
an error occurs:
Error report:
ORA-00904: "JAN": invalid identifier
ORA-06512: at "HW.FILLFACT", line 82
ORA-06512: at line 1
00904. 00000 - "%s: invalid identifier"
*Cause:
*Action:
strong text
You need at least add some quotes around the date:
....' where Date_D.DATEVALUE='''||acc_date||'''';
Double apostrophes within a string will be concatenated to a single apostrophe, so that the expression becomes
where Date_D.DATEVALUE='....';
In order to make the thing more foolprof, I'd also add a specific to_date:
.... ' where Date_D.DATEVALUE=to_date(''' || acc_date || ', ''dd-mon-yy'')';
At the moment your dynamic query is being interpreted as:
SELECT Date_D.DATEKEY from Date_D where Date_D.DATEVALUE=01-JAN-10
The error is because string representation of the date isn't being quoted, so it's seeing JAN as an identifier - and nothing matches that name. You could enclose the date value in quotes:
open c_get_date_id
for 'SELECT Date_D.DATEKEY from Date_D where Date_D.DATEVALUE='''||acc_date||'''';
But you're treating the date as a string, and forcing conversion of all your table values to strings to be compared, using your session's NLS_DATE_FORMAT. It would be better to compare it as a date (although this somewhat assumes all your values have the time portion set to midnight):
open c_get_date_id
for select date_d.datekey from date_d where date_d.datevalue = acc_date;
Your exit is in the wrong place though, and you aren't looping, so maybe you want:
open c_get_date_id
for select date_d.datekey from date_d where date_d.datevalue = acc_date;
loop
fetch c_get_date_id into date_id;
exit when c_get_date_id%notfound;
insert into logger values (1, 'Now with date_id'||date_id);
end loop;
close c_get_date_id;
If you only have one value in the first place though, you probably don't want a loop or cursor at all, and could do a simple select ... into instead:
select date_d.datekey into date_id from date_d
where date_d.datevalue = acc_date;
insert into logger values (1, 'Now with date_id'||date_id);
Though of course that would error if you had no matching date in your table, or more than one, and you'd need to deal with that - but then I guess you'd want to anyway.

Nested SELECT statement in a CASE expression

Greetings,
Here is my problem.
I need to get data from multiple rows and return them as a single result in a larger query.
I already posted a similar question here.
Return multiple values in one column within a main query but I suspect my lack of SQL knowledge made the question too vague because the answers did not work.
I am using Microsoft SQL 2005.
Here is what I have.
Multiple tables with CaseID as the PK, CaseID is unique.
One table (tblKIN) with CaseID and ItemNum(AutoInc) as the combined PK.
Because each person in the database will likely have more than one relative.
If I run the following, in a SQL query window, it works.
DECLARE #KINList varchar(1000)
SELECT #KINList = coalesce(#KINList + ', ','') + KINRel from tblKIN
WHERE CaseID = 'xxx' and Address = 'yyy'
ORDER BY KINRel
SELECT #KINList
This will return the relation of all people who live at the same address. the results look like this...
Father, Niece, Sister, Son
Now, the problem for me is how do I add that to my main query?
Shortened to relevant information, the main query looks like this.
SELECT DISTINCT
c.CaseID,
c.Name,
c.Address,
Relatives=CASE WHEN exists(select k.CaseID from tblKIN k where c.CaseID = k.CaseID)
THEN DECLARE #KINList varchar(1000)
SELECT #KINList = coalesce(#KINList + ', ','') + KINRel from tblKIN
WHERE CaseID = 'xxx' and Address = 'yyy'
ORDER BY KINRel
SELECT #KINList
ELSE ''
END
FROM tblCase c
ORDER BY c.CaseID
The errors I receive are.
Server: Msg 156, Level 15, State 1, Line 13
Incorrect syntax near the keyword 'DECLARE'.
Server: Msg 156, Level 15, State 1, Line 18
Incorrect syntax near the keyword 'ELSE'.
I tried nesting inside parenthesis from the DECLARE to the end of the SELECT #KINList.
I tried adding a BEGIN and END to the THEN section of the CASE statement.
Neither worked.
The source table data looks something like this. (periods added for readability)
tblCase
CaseID Name Address
10-001 Jim......100 Main St.
10-002 Tom....150 Elm St.
10-003 Abe.....200 1st St.
tblKIN
CaseID ItemNum Name Relation Address
10-001 00001 Steve...Son........100 Main St.
10-002 00002 James..Father....150 Elm St.
10-002 00003 Betty....Niece......150 Elm St.
10-002 00004 Greta...Sister.....150 Elm St.
10-002 00005 Davey..Son........150 Elm St.
10-003 00006 Edgar...Brother...200 1st St.
If I run the query for CaseID = 10-002, it needs to return the following.
CaseID Name Address.......Relatives
10-002 Tom...150 Elm St. ..Father, Niece, Sister, Son
I am sure this is probably a simple fix, but I just don't know how to do it.
Thank you for your time, and I apologize for the length of the question, but I wanted to be clear.
Thanks !!!
When I did something similar I had to create a scalar function to do the coalesce that returns the varchar result. Then just call it in the select.
CREATE FUNCTION GetRelatives
(
#CaseID varchar(10)
)
RETURNS varchar(1000)
AS
BEGIN
DECLARE #KINList varchar(1000)
SELECT #KINList = coalesce(#KINList + ', ','') + KINRel from tblKIN
WHERE CaseID = #CaseID
ORDER BY KINRel
RETURN #KINList
END
Then your select
SELECT DISTINCT
c.CaseID,
c.Name,
c.Address,
database.dbo.GetRelatives(c.CaseID) AS Relatives
FROM tblCase c
ORDER BY c.CaseID
You can create a FUNCTION which takes in the caseID as the arguement and returns true or false.
Since you are calling the nested query multiple times, its definitely a performance hit. A better solution is to execute the query and store the results in a temporary table.
Then pass this temporary table and the caseID to the FUNCTION and check for containment.

IsDate in TSQL Issue

I have a bug on our app and it points that if there is a date format like this:
SELECT IsDate('4:27:01') -- ok
SELECT IsDate('25:01') ---> not ok
SELECT IsDate('23:01') ---> ok
In our data, sometimes the format only 5 characters which means it's only for minutes and seconds.
What is best way to handle this?
Easiest way might be to add "00:" when the length is 5 characters.
e.g. :
declare #str varchar(100)
set #str = '25:01'
-- if length is 5, interpret as 00:mi:ss
set #str = (CASE WHEN LEN(#str)=5 THEN '00:' + #str ELSE #str END)
select ISDATE(#str) -- shows 1
select cast(#str as time) -- shows 00:25:01.000