Function SQL Server 2008 - sql-server-2008

I created the following function yesterday, and now I am facing an error:
Invalid length parameter passed to the left or substring function
Could you guys have a look at my function?I really appreciate it.
Create function nowFunctionNewadd
(#fladd varchar(255))
returns #tbl table(addr varchar(100), city varchar(100),
state varchar(100), Zip varchar(5))
as
begin
declare #str varchar(100)
,#i int
,#j int
,#str2 varchar(100)
,#address varchar(100)
,#city varchar(100)
,#lastcomma int
,#lastPart varchar(100)
,#zipstart int
,#zip varchar(5) = ''
select #str=rtrim(ltrim(#fladd))
set #i = charindex(',', #str)
set #str2=rtrim(ltrim(substring(#str, #i+1, 999)))
set #j=CHARINDEX(',',#str2)
set #lastcomma = len(#str) - charindex(',', reverse(#str)+',')
set #lastPart = substring(#str, #lastcomma+2, 100)
set #address = REPLACE(rtrim(ltrim(substring(#str,1,#i-1))),',','')
set #zipstart = patindex('%[0-9]%', #lastpart)
set #city=LTRIM(RTRIM(substring(#str, #i+1, #j-1)))
If #zipstart > 0
select #zip = substring(#lastpart, #zipstart, 5),
#lastPart = rtrim(substring(#lastpart, 1, #zipstart-1))
insert into #tbl(addr, city, state, Zip)
values(#address, #city, #lastpart, #zip)
return
end

The problem that I can see with your function starts with this line:
set #j=CHARINDEX(',',#str2)
And then I am guessing that the error is being thrown by this line:
set #city=LTRIM(RTRIM(substring(#str, #i+1, #j-1)))
Your function is working under the assumption that you will have more than one comma present in the string value that you are passing. But if you don't have more than one comma the value for #j will be zero and then you are trying to use a -1 as the length of the city and this will fail throwing the error you are getting.
I created a SQL Fiddle with a demo to work with. Using the address '1234 S.Alameda way,LA,CA12345' your function will work.
But if you change the value to '1234 S.Alameda way,LACA12345' it will fail
See SQL Fiddle Demo
Do you know what the format is going to be for all of the values that you need to pass into the function? If this format is going to change from 1 to 2 or even 3 commas I think you need to rethink how this function is written because it will not work as expected.

Related

Get String between two strings - SQL Server

I got a function that returns string between two strings:
CREATE FUNCTION dbo.udf_GetStringBetween2Chars (#String VARCHAR(50), #FirstSpecialChar VARCHAR(50), #LastSpecialChar VARCHAR(50))
RETURNS VARCHAR(50)
AS
BEGIN
DECLARE #FirstIndexOfChar INT,
#LastIndexOfChar INT,
#LengthOfStringBetweenChars INT
SET #FirstIndexOfChar = CHARINDEX(#FirstSpecialChar,#String,0)
SET #LastIndexOfChar = CHARINDEX(#LastSpecialChar,#String,#FirstIndexOfChar+1)
SET #LengthOfStringBetweenChars = #LastIndexOfChar - #FirstIndexOfChar -1
SET #String = SUBSTRING(#String,#FirstIndexOfChar+1,#LengthOfStringBetweenChars)
RETURN #String
END
However, when I try to get string between POINTDESCRIPTION and DATAPOINT, I get error:
SET ANSI_WARNINGS OFF
declare #table table
(string varchar(50))
insert into #table
select 'EVENT: ID Order Reassignment (Individual Job Specific Update)|||LOCATION: Reassign.reassign()|||DATEPOINTDESCRIPTION: Only specific ID orders were reassigned from user fatis to user blake.|||DATAPOINT: blake' union all
select 'EVENT: ID Order Reassignment (Individual Job Specific Update)|||LOCATION: Reassign.reassign()|||DATAPOINTDESCRIPTION: Only specific ID orders were reassigned from user ilevic to user manic2.|||DATAPOINT: manic2' union all
select 'EVENT: ID Order Reassignment (Individual Job Specific Update)|||LOCATION: Reassign.reassign()|||DATAPOINTDESCRIPTION: Only specific ID orders were reassigned from user links to user sepir.|||DATAPOINT: sepir'
select dbo.udf_GetStringBetween2Chars (Tab.string,'POINTDESCRIPTION: ','|||DATAPOINT')
FROM #table Tab
Msg 537, Level 16, State 2, Line 10
Invalid length parameter passed to the LEFT or SUBSTRING function.
Does anyone see why this would happen?
If anyone finds it useful, here is final function to return string between 2 strings:
CREATE FUNCTION dbo.udf_GetStringBetween2Chars (#String VARCHAR(500), #FirstSpecialChar VARCHAR(500), #LastSpecialChar VARCHAR(500))
RETURNS VARCHAR(500)
AS
BEGIN
DECLARE #FirstIndexOfChar INT,
#LastIndexOfChar INT,
#LengthOfStringBetweenChars INT
SET #FirstIndexOfChar = CHARINDEX(#FirstSpecialChar,#String,0)
SET #LastIndexOfChar = CHARINDEX(#LastSpecialChar,#String,#FirstIndexOfChar+1)
SET #LengthOfStringBetweenChars = #LastIndexOfChar - #FirstIndexOfChar -1
SET #String = SUBSTRING(#String,#FirstIndexOfChar+LEN(#FirstSpecialChar),#LengthOfStringBetweenChars)
RETURN #String
END
GO
And to call it:
select dbo.udf_GetStringBetween2Chars (tab.someString,'POINTDESCRIPTION: ','|||DATAPOINT')
FROM yourTable tab

Conversion failed when converting the varchar value 'LAST1, FIRST1' to data type int

I have a TSQL query that is doing 3-4 different things (my 1st attempt at this type of query). I encounter this error on the first record:
Error: "Conversion failed when converting the varchar value 'LAST1, FIRST1' to data type int."
The value referenced in the error is a concatenation of first and last name from a view.
From reading other posts I have deduced that the comma is the issue but how do I get around it? Do I use substring to strip the comma out from the view before I use it or do I re-write the view or is there a CAST or parse function I can use?
Here's overview of what the query should do:
Select and loop through records in a view
If column, "daysUntil" (int) = 30, INSERT new record into another database via stored procedure
Return scope_identity to output parameter #PK_ID to use in email body
Send email using new #PK_ID in body of email
BTW I already have a C# console app that can handle all of the above but I want to move this functionality to SQL Server.
Here's the SQL:
DECLARE #MailTo varchar (1024)
DECLARE #MailBody varchar (1024)
DECLARE #MailSubject varchar (1024)
DECLARE #MailFlag int
DECLARE #Bcc varchar (256)
SET #Bcc = 'user1#companydomain.org;user2#companydomain.org
DECLARE #MailFrom varchar (256)
SET #MailFrom = 'recordsystem#companydomain.org'
DECLARE #Notification30Body varchar (512)
DECLARE #Notification30Subject varchar (512)
DECLARE #NotificationAllBody varchar (512)
SET #NotificationAllBody = 'REQUIREMENTS: requirements here'
--Declare variables to hold the data which we get after looping each record
DECLARE
#iEmpName VARCHAR(102),
#iEmpID INT,
#iEmail1 VARCHAR(24),
#iRvwDate smalldatetime,
#idaysUntil INT
DECLARE Empid_cur CURSOR FOR
select EmpName, EmpID, RvwDate, Email1, daysUntil from ABC.dbo.vwEmpDataTEST
--Flag the start of loop/curser
SET #MailFlag = 0
OPEN Empid_cur
FETCH NEXT FROM Empid_cur
INTO #iEmpID, #iEmpID, #RvwDate, #iEmail1, #idaysUntil
WHILE ##FETCH_STATUS = 0
BEGIN
IF (#idaysUntil = 30)
BEGIN
DECLARE #PK_ID INT;
EXEC XYZ.dbo.InsertEmpDataTEST #iEmpID, #PK_ID OUTPUT
SELECT #PK_ID
--send 30 day email here
SET #MailTo = #iEmail1
SET #MailBody = #Notification30Body
SET #MailSubject = #Notification30Subject
SET #Notification30Body = 'This Record is for ' + #iEmpName + ' is due in 30 days. Please open the fake URL to the new Record for ' + #iEmpName + ':' + CHAR(10) + ' http://server/Record/template.aspx?ID=' + #PK_ID
SET #Notification30Subject = 'Record for ' + #iEmpName
SET #MailFlag = 1 --email ready
END
IF (#MailFlag = 1)
BEGIN
--send email
EXEC msdb..sp_send_dbmail
#profile_name ='MailUser',
#recipients = #MailTo,
#blind_copy_recipients = #Bcc,
#subject = #MailSubject,
#body = #MailBody
END
FETCH NEXT FROM Empid_cur
INTO #iEmpID, #iEmpID, #RvwDate, #iEmail1, #idaysUntil
END
CLOSE Empid_cur
DEALLOCATE Empid_cur
You have INTO #iEmpID, #iEmpID, ... with the int variable repeated twice, change to INTO #iEmpName, #iEmpID,... so the columns and variables (and their types) are all aligned.

Getting an error creating SQL Function (cannot convert to int) when no ints are expected

I am needing to grab data from one table and use a relationship to place it into another table. Long story short, I need to get back an array of IDs so I created a function to return them. I am only retrieving data which is IDs so all int values. The problem is when I invoke my function I get the error:
Conversion failed when converting the varchar value '/' to data type int.
But everything is cast as varchar so I have NO idea why this is happening. I know its dirty code but it is a high priority project with a tight deadline so I just need it to work. I am using SQL Server 2012. Any thoughts?
Function:
CREATE FUNCTION FactPersonList (#FactID varchar(100),
#delimiter char(1))
RETURNS varchar(8000)
AS
BEGIN
DECLARE #CharIDList varchar(8000)
DECLARE #id int
DECLARE #FinalTable TABLE (
factid int,
charid int
)
SET #CharIDList = CHAR(08) + #delimiter
INSERT INTO #FinalTable
SELECT DISTINCT
#FactID,
CharacterId
FROM KeyFactsCharacters
WHERE KeyFactID = #FactID
WHILE ((SELECT
COUNT(*)
FROM #FinalTable)
> 0)
BEGIN
SET #Id = (SELECT TOP 1
charid
FROM #FinalTable)
SET #CharIDList = CAST(#CharIDList + #Id + #delimiter AS varchar(8000))
END
SET #CharIDList = CAST(#CharIDList + CHAR(08) + '~' AS varchar(8000))
RETURN CAST(#CharIDList AS varchar(8000))
END
I invoke it using this:
SELECT dbo.FactPersonList(KeyFact.KeyFactId, '/') FROM KeyFactsCharacters, KeyFact
I'm not familiar with MS SQL (experience almost exclusively in MySQL), but I would guess that #CharIDList + #Id + #delimiter is causing the error; since #Id is defined as an int, MS SQL may be trying to coerce the other operands to int values as well (for addition rather than concatenation).

T-SQL function with dynamic SELECT (not possible) - solved with procedure instead

I've managed to use EXEC sp_executesql in a one off statement to do a dynamic lookup, but am unable to adjust the code to create a function since EXEC is not allowed in functions. It works in procedures and I've managed to get output via PRINT for a single lookup by using a temporary table, but really that was just me struggling to find a workaround. Ideally I'd like to be able to create a scalar-value function.
The reason that I need a dynamic lookup is because the column name is stored in another table.
Here's a quick breakdown of the tables:
Questions:
Columns: Q_Group, Q_Nbr, Question_Desc, Data_Field
Sample data: 'R3', 5, 'Do you have any allergies?', 'TXT_04'
Responses:
Columns: Order_Nbr, Q_Group, TXT_01, TXT_02, TXT_03, TXT_04, etc.
Data: 999, 'R3', 'blah', 'blah', 'blah', 'NO'
Orders will be assigned a particular set of questions 'Q_Group' and often a particular question will be the same across various different sets of questions. The problem is that when the set/groups of questions were set up, the questions may not have been added in the same order, and thus the responses go into different columns.
So here's where I'm at...
I can get 'TXT_04' from the Data_Field column in Questions and use EXEC sp_executesql to do a lookup for a single order, but am struggling to find a way to accomplish this as a function of some sort.
DECLARE #col_name VARCHAR(6)
DECLARE #sql VARCHAR(100)
SET #col_name = SELECT Data_Field FROM QUESTIONS WHERE Q_Group = 'R3'
AND Question_Desc = 'Do you have any allergies?'
SET #sql = 'SELECT ' + #col_name + ' FROM RESPONSES WHERE Order_Nbr = 999'
EXEC sp_executesql #sql
I'm just at a loss as to how this could be incorporated into a function so that I could get responses for several orders in a result set. Any workarounds possible? Maybe I'm totally off base using EXEC sp_executesql?
Thanks.
Edit...
Okay, I've changed the title to reflect that I'm going to consider this solved with a procedure instead of a function, as it ended up getting the output that I wanted. Which was a table with all of the corresponding responses.
Here's the code that I settled on. I decided to use LIKE to match the Question_Desc instead of equals, and then included the Question_Desc in the results, so that it could be used a bit more broadly. Thankfully it's pretty quick to run currently. Although that could always change as the database grows!
CREATE PROCEDURE get_all_responses (#question_txt VARCHAR(255))
AS
DECLARE #response_col VARCHAR(35)
DECLARE #t TABLE (order_nbr int, question_txt VARCHAR(255), response_col VARCHAR(35), response VARCHAR(255))
DECLARE #i TABLE (id INT PRIMARY KEY IDENTITY(1,1), response_col VARCHAR(35))
DECLARE #u TABLE (order_nbr int, response VARCHAR(255))
DECLARE #sql VARCHAR(200)
INSERT #t
SELECT Order_Nbr, Question_Desc, Data_Field, NULL
FROM Responses
JOIN (
SELECT Q_Group, Question_Desc, Data_Field
FROM Questions
WHERE Question_Desc LIKE #question_txt
) #Q ON Q_Group = #Q.Q_Group
WHERE Q_Group <> '0'
ORDER BY Data_Field, Order_Nbr
-- Stop if no results found and return empty result set
IF (SELECT COUNT(*) FROM #t) = 0
BEGIN
SELECT order_nbr, question_txt, response FROM #t
RETURN
END
INSERT #i SELECT response_col FROM #t GROUP BY response_col
DECLARE #row_nbr int
DECLARE #last_row int
SET #row_nbr = 1
SET #last_row = (SELECT COUNT(*) FROM #i)
-- Iterate through each Data_Field found
WHILE #row_nbr <= #last_row
BEGIN
SET #response_col = (SELECT response_col FROM #i WHERE id = #row_nbr)
SET #sql = 'SELECT Order_Nbr, ' + #response_col + ' FROM Responses WHERE NullIf(' + #response_col + ','''') IS NOT NULL'
INSERT INTO #u
EXEC (#sql)
UPDATE #t
SET response = y.response
FROM #t AS x
INNER JOIN #u AS y ON x.order_nbr = y.order_nbr
SET #row_nbr = #row_nbr + 1
END
-- Remove results with no responses
DELETE FROM #t WHERE response IS NULL
SELECT order_nbr, question_txt, response FROM #t
RETURN
You will not be able to execute dynamic SQL from within a function but you could do this with a stored procedure and capture the output.
DECLARE #col_name VARCHAR(6), #param NVARCHAR(50), #myReturnValue VARCHAR(50)
SET #param = N'#result VARCHAR(50) OUTPUT'
DECLARE #sql VARCHAR(100)
SET #col_name = SELECT Data_Field FROM QUESTIONS WHERE Q_Group = 'R3'
AND Question_Desc = 'Do you have any allergies?'
SET #sql = 'SELECT #result = ' + #col_name + ' FROM RESPONSES WHERE Order_Nbr = 999'
EXEC sp_executesql #sql, #param, #result = #myReturnValue output
--manipulate value here
print #myReturnValue
You could also create a temp table and do an insert into from exec sp_executesql.

Porting MySQL Stored Procedure to Oracle

I'm trying to port a stored procedure from MySQL to Oracle, and I'm having a lot of trouble. I've gone through Oracle documentation, and I'm having trouble doing very basic things like declaring variables properly. I was hoping someone could show me how to properly declare and set variables.
My stored procedure is used to add values to two different tables and ensure that it's being mapped properly and the foreign keys aren't being violated.
Here is my MySQL Code:
CREATE DEFINER=root#% PROCEDURE proc_add_entry(IN theName vARCHAR(50), IN theKey VARCHAR(50), IN theOtherData VARCHAR(50), IN theOtherData2 INT, IN theStartDate DATE, IN theEndDate DaTE, IN theReferenceDate DaTE)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
BEGIN
declare theNameID int ;
declare theKeyID int ;
declare theOtherDataID int default null;
declare error bool default false;
declare continue handler for SQLEXCEPTION
set error = true;
set theKeyID = (select KeyID from map_alias ma where ma.alias = trim(theKey));
set theOtherDataID = (select theOtherDataID from map_otherdata mc where mc.otherdata = trim(theOtherData));
set theNameID = (select max(nameID) from inserttable);
set theNameID = theNameID + 1;
insert into inserttable values (theNameID , theKeyID , theOtherDataID , theOtherData2, theStartDate ,
theEndDate , theReferenceDate);
if error = true then
insert into errors_inserttable values (theNameID , theKeyID , theOtherDataID , theOtherData2, theStartDate ,
theEndDate , theReferenceDate);
end if;
set error = false;
insert into map_inserttable (theNameID , datasourceid, theName) values (theNameID , 1, theName);
if error = true then
insert into errors_map_inserttable (theNameID , datasourceid, theName) values (theNameID , 1, theName);
end if;
END
In Oracle, my last statement are being ignored (ORA-00922: Missing or invalid option). It should be a local variable, so I'm not sure why i'm getting that particular error.
I'm struggling to declare the continue handler as well. I'm getting the error:
Error(16,27): PLS-00103: Encountered the symbol "FOR" when expecting one of the following: := . ( # % ; not null range default character.
Here is my oracle code so far:
CREATE OR REPLACE PROCEDURE PROC_ADD_ENTRY
(
THENAME IN VARCHAR2
, THEKEY IN VARCHAR2
, THEOTHERDATA IN VARCHAR2
, THEOTHERDATA2 IN NUMBER
, THEFIRSTDATE IN DATE
, THELASTDATE IN DATE
, THEREFERENCEDATE IN DATE
) AS
THENAMEID INT;
THEKEYID INT;
THEOTHERDATAID int;
ERROR bool default false;
BEGIN
declare continue HANDLER FOR SQLEXCEPTION set error = true;
set THEKEYID = (select KEYID from map_INSERTTABLE mc where mc.Key = trim(THEKEY));
END PROC_ADD_ENTRY;
I'm sure this is stupidly simple for someone that uses oracle, but I'm reading the documentation and I'm seeing conflicting information on where and how to declare variables, continue handlers, and assign values to variables. (is it := or = to assign values? Do i use the word declare after the begin statement to declare variables, or do I do it the way I show below?)
If someone could show me:
a) where to declare a local variable
b) how to assign a value to it (i.e. 1 to an int)
c) how to assign a value from the DB to a variable (set var = select number from table_number tn where tn.number = 1)
d) how to declare a continue handler properly
I would really appreciate it.
You've go the basic structure fine.
create or replace procedure <name> (<param list>) as
<local variables>
begin
<body>
end <name>;
To address your specific questions:
a) where to declare a local variable
I've marked this section up above.
b) how to assign a value to it (i.e. 1 to an int)
You would use := for assignment.
eg. thenameid := 1;
The data type you want will typically match the sql data types (eg. NUMBER for the above) though there are PL/SQL-specific data types such as PLS_INTEGER. See the PL/SQL data types documentation for more details.
c) how to assign a value from the DB to a variable (set var = select number from table_number tn where tn.number = 1)
You would use the into keyword with a locally defined variable to store the value in. eg.
l_num_rows number;
select count(*) into l_num_rows from user_objects;
d) how to declare a continue handler properly
If I'm reading understanding your code correctly, you want set error = true to be executed every time there is a problem with an sql statement and then you want the stored procedure to carry on.
Exception handling is what you are after. You would wrap any or SQL PL/SQL statements that you think may have errors in an exception block like this, with as many exception cases as needed (eg. NO_DATA_FOUND):
begin
<statements that may fail>
exception when <exception name> then
<action>
...
exception when others then
<action>
end;
"other" is the catchall. You can have just this case handled but as with any error handling it is better practise to catch specific cases first.
For completion, here's roughly what your example procedure would look like. I've removed the error code flag as it is not needed and also changed the ints to numbers:
create or replace procedure proc_add_entry (
in thename varchar(50),
in thekey varchar(50),
in theotherdata varchar(50),
in theotherdata2 number,
in thestartdate date,
in theenddate date,
in thereferencedate date
) as
thenameid number;
thekeyid number;
theotherdataid number default null;
begin
begin
select keyid into thekeyid from map_alias ma where ma.alias = trim(thekey);
select theotherdataid into theotherdataid from map_otherdata mc where mc.otherdata = trim(theotherdata);
select max(nameid) into thenameid from inserttable;
thenameid := thenameid + 1;
insert into inserttable values (thenameid, thekeyid, theotherdataid, theotherdata2, thestartdate, theenddate, thereferencedate);
exception when others then
insert into errors_inserttable values (thenameid, thekeyid, theotherdataid, theotherdata2, thestartdate, theenddate, thereferencedate);
end;
begin
insert into map_inserttable (thenameid, datasourceid, thename) values (thenameid, 1, thename);
exception when others then
insert into errors_map_inserttable (thenameid, datasourceid, thename) values (thenameid, 1, thename);
end;
end proc_add_entry;