Stored Procedure With Function giving me errors in Oracle - function

I have stored procedure and function and I am calling the function in the stored procedure in ORACLE.The function CalculateIncomeTax is what is giving me errors.In MSSQL,this type of update is possible because I have done it before.I called the function in the stored procedure.When I read around the answer I get is to use a package before I cannot use a function to update a table from another table.Please if you have any idea,tell me.The error I get is
table string.string is mutating, trigger/function may not see it
Cause: A trigger (or a user defined plsql function that is referenced in this statement) attempted to look at (or modify) a table that was in the middle of being modified by the statement which fired it.
Action: Rewrite the trigger (or function) so it does not read that table.
This is function
CREATE OR REPLACE function CalculateIncomeTax(periodId NVARCHAR2,
employeeId NVARCHAR2, taxableIncome NUMBER)return NUMBER
AS
IncomeTax NUMBER (18,4);Taxable NUMBER(18,4);
BEGIN
SELECT SUM(CASE WHEN (taxableIncome > T.TAX_CUMMULATIVE_AMOUNT)
THEN (taxableIncome - T.TAX_CUMMULATIVE_AMOUNT)* T.TAX_PERCENTAGE/ 100
ELSE 0.00 END ) INTO IncomeTax
FROM TAX_LAW T JOIN PAY_GROUP P ON P.PAY_FORMULA_ID =T.TAX_FORMULA_ID
JOIN PAYROLL_MASTER PP ON P.PAY_CODE =PP.PAY_PAY_GROUP_CODE
WHERE PP.PAY_EMPLOYEE_ID = employeeId AND PP.PAY_PERIOD_CODE = periodId;
if IncomeTax IS NULL THEN IncomeTax :=0;
end if;
return IncomeTax;
end;/
This is the stored procedure
CREATE OR REPLACE PROCEDURE PROCESSPAYROLLMASTER (periodcode
VARCHAR2) AS BEGIN
INSERT INTO PAYROLL_MASTER
(
PAY_PAYROLL_ID,PAY_EMPLOYEE_ID ,PAY_EMPLOYEE_NAME,PAY_SALARY_GRADE_CODE
,PAY_SALARY_NOTCH_CODE,PAY_BASIC_SALARY,PAY_TOTAL_ALLOWANCE
,PAY_TOTAL_CASH_BENEFIT,PAY_MEDICAL_BENEFIT,PAY_TOTAL_BENEFIT
,PAY_TOTAL_DEDUCTION,PAY_GROSS_SALARY,PAY_TOTAL_TAXABLE,PAY_INCOME_TAX
,PAY_TAXABLE,PAY_PERIOD_CODE,PAY_BANK_CODE,PAY_BANK_NAME,PAY_BANK_ACCOUNT_NO
,PAY_PAY_GROUP_CODE )
SELECT
1,
E.EMP_ID AS PAY_EMPLOYEE_ID ,
E.EMP_FIRST_NAME || ' ' || E.EMP_LAST_NAME AS PAY_EMPLOYEE_NAME,
E.EMP_RANK_CODE,
'CODE',
(SC.SAL_MINIMUM_AMOUNT+( SN.SAL_SALARY_PERCENTAGE *
SC.SAL_MINIMUM_AMOUNT)/100) AS PAY_BASIC_SALARY,
0,
0,
0,
0,
0,
0,
0,
0,
0,
periodcode,
'BANKCODE',
'BANKNAME',
'BANKNUMBER',
'GENERAL'
FROM EMPLOYEE E
LEFT JOIN SALARY_SCALE SC ON SC.SAL_RANK_CODE = E.EMP_RANK_CODE
LEFT JOIN SALARY_NOTCH SN ON SC.SAL_ID = SN.SAL_SALARYSCALE_ID
WHERE E.EMP_RANK_CODE = SC.SAL_RANK_CODE AND E.EMP_STATUS=2;
CALCULATEALLOWANCE(v_payrollId,periodcode);
CALCULATECASHBENEFITS(v_payrollId,periodcode);
CALCULATEDEDUCTIONS(v_payrollId,periodcode);
-- UPDATE PAYROLL PAY_INCOME_TAX
UPDATE PAYROLL_MASTER PM SET PM.PAY_INCOME_TAX = CalculateIncomeTax(PM.PAY_PERIOD_CODE,PM.PAY_EMPLOYEE_ID,PM.PAY_TOTAL_TAXABLE) WHERE PM.PAY_PAYROLL_ID = v_payrollId;
UPDATE PAYROLL_PROCESS set PAY_CANCELLED = 1 WHERE PAY_PAY_GROUP_CODE='GENERAL' AND PAY_PERIOD_CODE=periodcode
AND PAY_ID<>v_payrollId;
COMMIT;
END ;
/

The function is querying the same table you are updating, which is what the error is reporting. As it happens you are not changing the value of the column you're querying, but Oracle doesn't check to that level - not least because there could be, for instance, a trigger that has less obvious side-effects.
The best solution really would be to not have to update at all, and to calculate and set all the value as part of the original insert, by joining to all the relevant tables. But you are already calling other procedures which are, presumably, updating some of the values you're inserting as zeros, including pay_total_taxable.
Unless you're able to reevaluate those as well, you may be stuck with doing a further update. In which case, you could remove the reference to the payroll_master table from the function and instead pass in the relevant data.
I think this is equivalent, though with out the table structures, sample data and what the other procedures are doing it's hard to be sure (so this is untested, obviously):
create or replace function calculateincometax (
p_periodid nvarchar2,
p_employeeid nvarchar2,
p_paypaygroupcode payroll_master.pay_pay_group_code%type,
p_taxableincome number
) return number as
l_incometax number(18, 4);
begin
select coalesce(sum(case when p_taxableincome > t.tax_cummulative_amount
then (taxableincome - t.tax_cummulative_amount) * t.tax_percentage / 100
else 0 end), 0)
into l_incometax
from tax_law t
join pay_group p
on p.pay_formula_id = t.tax_formula_id
where p.pay_code = p_paypaygroupcode;
return l_incometax;
end;
/
and then include the extra argument in your call:
update payroll_master pm
set pm.pay_income_tax = calculateincometax(pm.pay_period_code, pm.pay_employee_id,
pm.pay_pay_group_code, pm.pay_total_taxable)
where pm.pay_payroll_id = v_payrollid;
Although v_payrollid isn't defined in what you've shown, so even that isn't entirely clear.
I've also modified the function argument and local variable names with prefixes to remove potential ambiguity (which you seem to do by removing underscores from the names), removed the unused variable, and added a coalesce() call in place of the separate null check. Those things aren't directly relevant to the approach though.

Related

I am building my first function in PLSQL, it needs to return a single summed value of spending by a user based off parameters username and fiscal year

The function compiles, but when I run this block:
select sumyUserSpending('ALCraft', 15) from infor.credit_card_transaction
It throws error ORA-06575: Package or function sumyuserspending is in an invalid state.
When I run this block:
declare
answer number;
begin
answer := sumyUserSpending('ALCraft', 15);
dbms_output.put_line(answer);
end;
It throws errors ORA-65550 and PLS-00905: object sumyuserspending is invalid.
Here is my function. When I only run the function, it throws no errors, so I am lost as to what it could need to run smoothly. When the query is taken out of the function and I run the query alone, it returns the value I want based on the placeholder values I put in for the parameters. I am working in oracle 12c.
create or replace function sumyUserSpending (userN in varchar2, fiscYear in number)
return number
is
total number;
cursor search1 is
select sum(infor.credit_card_transaction.due_cc_co_amount) into total from infor.credit_card_transaction
inner join infor.credit_card using (credit_card_id)
inner join infor.ext_user using (user_id)
where infor.credit_card_transaction.transaction_date > concat('01-JUL-', (fiscYear - 1))
and infor.credit_card_transaction.transaction_date < concat('30-JUNE-', fiscYear)
and NVL(SUBSTR(infor.ext_user.email_address, 0, INSTR(infor.ext_user.email_address, '#')-1), infor.ext_user.email_address) = userN;
begin
open search1
fetch search1 into total;
if search1%notfound then
total := 0;
end if;
close search1;
return total;
end;
You need to refer to the tables by their just table name or an alias outside the FROM clause and not by schema_name.table_name.
Do not rely on implicit conversion of strings to dates; explicitly convert them using TO_DATE.
If you are using a cursor then a INTO clause is not syntactically valid.
However, you do not need a cursor.
Fixing all that gives you:
create or replace function sumyUserSpending (
userN in ext_user.email_address%TYPE,
fiscYear in number
) RETURN number
IS
total number;
BEGIN
select COALESCE(sum(cct.due_cc_co_amount), 0)
into total
from infor.credit_card_transaction cct
inner join infor.credit_card cc using (credit_card_id)
inner join infor.ext_user eu using (user_id)
where cct.transaction_date >= TO_DATE((fiscyear - 1) || '07-01', 'RR-MM-DD')
and cct.transaction_date < TO_DATE(fiscyear || '07-01', 'RR-MM-DD')
and NVL(SUBSTR(eu.email_address, 0, INSTR(eu.email_address, '#')-1), eu.email_address)
= userN;
return total;
end;
/
db<>fiddle here

Insert multiple value parameters into table

I have a client who wants to drop a list of values into a parameter in Reporting Services, compare it to a table in her database and return the full list of values with a yes or no in the next field indicating whether the value is found in the database table. For example if her list is Duck, Horse, Chicken and only Duck exists in the table she wants the result to look like this:
Duck Yes
Horse No
Chicken No
She doesn't want to return only those values that match so a simple WHERE Animal IN (#ReportParameter1) isn't going to do it.
I can make this work for a single value parameter like this:
DECLARE #FarmAnimals AS TABLE (Animal varchar(50))
INSERT INTO #FarmAnimals VALUES (#ReportParameter1)
SELECT Animal
,'In Barnyard’ = CASE WHEN EXISTS
(SELECT *
FROM tblBarnyard
WHERE BarnyardAnimal = Animal)
THEN 'Yes'
ELSE 'No'
END
FROM #FarmAnimals
But is it possible to loop through a multiple value parameter of unspecified length and create an INSERT INTO statement for each individual value? If this isn't possible I'm happy to tell her it can't be done but I can't think of a time I've found that something was impossible in SQL.
There's no need to do it in SQL, you can just write a custom code function to iterate through the selected parameter values to get the result that you want.
On the Report menu, access Report Properties... and select the Code tab. Insert the following code:
Function IsFieldSelected (fieldValue as String, ByVal parameter As Parameter) As String
Dim Result As String
Result = "No"
If parameter.IsMultiValue Then
For i As integer = 0 To parameter.Count-1
If (fieldValue = parameter.Value(i)) Then
Result = "Yes"
End If
Next
Else
If (fieldValue = parameter.Value) Then
Result = "Yes"
End If
End If
Return Result
End Function
then use this expression in the cell that you want the "Yes/No" to access the result:
=Code.IsFieldSelected(Fields!MyField.Value, Parameters!MyParameter)
Note that you are passing the parameter object here, not the Value property (so don't put .Value on the end). We access the Value property of the parameter object in the custom code function and compare it to the field value passed.
This will work for both single- and multi-value parameters.
You can do this using the STRING_SPLIT function in SQL Server.
--Placeholder table
DECLARE #ExistingValuesInTable TABLE (Val VARCHAR(255)) ;
INSERT INTO #ExistingValuesInTable (Val) VALUES ('Duck') ;
--
DECLARE #UserInput VARCHAR(255) = 'Duck, Horse, Chicken' ;
SELECT ss.value,
CASE WHEN evit.Val IS NULL THEN 'No' ELSE 'Yes' END AS AlreadyExists
FROM STRING_SPLIT(#UserInput, ',') AS ss
LEFT OUTER JOIN #ExistingValuesInTable AS evit ON ss.value = evit.Val ;

Mysql function not returning the expected result

As I have mentioned in my question title below Mysql function returns null always :
CREATE DEFINER=`root`#`localhost` FUNCTION `nextCode`(tbl_name VARCHAR(30), prv_code VARCHAR(30)) RETURNS varchar(30) CHARSET utf8
READS SQL DATA
BEGIN
DECLARE nxtCode VARCHAR(30);
SELECT ds.prefix, ds.suffix, ds.is_used, ds.next_number, CHAR_LENGTH(ds.pattern)
INTO #prefix, #suffix, #isUsed, #nxtNum, #pLength
FROM ths_inventory.doc_sequnce ds WHERE ds.`table_name` = tbl_name;
SET nxtCode = CONCAT(#prefix, LPAD((CASE WHEN #isUsed
THEN
(ExtractNumber(prv_code) + 1)
ELSE
(#nxtNum)
END
), #pLength,'0'), #suffix);
RETURN nxtCode;
END
But once I change the below line :
CONCAT(#prefix, LPAD((CASE WHEN #isUsed
THEN
(ExtractNumber(prv_code) + 1)
ELSE
(#nxtNum)
END
), #pLength,'0'), #suffix)
To some static values like below :
CONCAT('PR', LPAD((CASE WHEN true
THEN
(ExtractNumber(prv_code) + 1)
ELSE
(5)
END
), 6,'0'), '')
function start returning values accordingly.
Here is how I call my function :
nextCode('item','PR000002');
UPDATE:
I defined this function to get the next possible code for Item table :
According to my requirement the next possible code should be PR000000005.
But instead of getting it, I always get empty result .
SELECT nextCode('item',(SELECT `code` FROM item ORDER BY id DESC LIMIT 1)) AS next_code;
Any help would be appreciable.
Run a query that uses the function, and then...
SELECT #prefix, #suffix, #isUsed, #nxtNum, #pLength;
...to inspect the values. The # prefix means these are user-defined variables, so they have session scope, not program scope, and will still hold their values after the funcfion executes.
This should help pinpoint your problem.
But, you have two other problems you will need to solve after that.
SELECT ... INTO does not set the target variables when no row matches the query, so once you fix your issue, you will get very wrong results if you pass in arguments that don't match anything.
To resolve this, the function needs to set all these variables to null before the SELECT ... INTO query.
SET #prefix = NULL, #suffix = NULL, #isUsed = NULL, #nxtNum = NULL, #pLength = NULL;
See https://dba.stackexchange.com/a/35207/11651.
Also, your function does not handle concurrency, so two threads trying to find the "next" value for the same table, concurrently, will produce the same answer, so you will need to insure that your code handles this correctly with unique constraints and transactions or other appropriate locks.

Add 2 SUM CASE statements as a column update in MySQL

I think I have this almost figured out but after 50+ Google searches, I ask this: How can I add a column to a db that is essentially a sumif function? I've seen many related questions as simple Select statements for just looking at the table in a mini table but I was hoping to actually add a column that would show these totals. I'm taking this and then pulling the data into R for further analysis.
In Excel it works like so with [ ] denoting columns of a table. It is split into 2 areas via the Serial #. The first 6 digits of the serial indicate the "parent" and the later half indicate the "child". One parent can have multiple children, as seen with BSA101 below. What I'm trying to do is sum all the costs that went into making the child (parent + child costs). So the total parent costs, get allocated to both children below.
"Packing" is the last step so this is where I'd want the totals to end up so there are no duplicates.
Example
=IF(LEN([serial])>6,IF([process]="Packing",SUMIF([serial],[#serial],[process_cost])+SUMIF([serial],LEFT([#serial],6),[process_cost]),""),"")
serial process process_cost total_child_cost
BSA101A33 Packing 10 160
BSA101A34 Packing 10 195
BSA101 Cast 50 ""
BSA101 Mold 30 ""
BSA101 Mold 30 ""
BSA101A33 Finish 15 ""
BSA101A34 Finish 25 ""
BSA101A33 Polish 25 ""
BSA101A34 Polish 50 ""
^desired table result above
MySQL attempt:This post helped me Adding Case Statements
SQL Fiddle: http://sqlfiddle.com/#!9/b0e58
Here I've added a column in data called total_cost. Right now I'm getting an "Invalid use of group function" error which after researching, talks about a HAVING clause but not sure where to place it.
UPDATE data
SET total__child_cost =
(CASE WHEN length(serial) > 6
AND process = 'Packing'
THEN
IF(serial = serial, sum(process_cost),0) END)
+
(CASE WHEN left(serial,6) = serial
THEN sum(process_cost)
END)
This ended up being the solution.
DELIMITER //
CREATE FUNCTION `getParent1`(inSerialn Varchar(20)) RETURNS int(11)
BEGIN
Declare parent varchar(20);
Declare result int;
set parent = left(inSerialn, 6);
set result = (Select sum(process_cost) From mfng.data where serialn = parent);
return result;
END //
DELIMITER //
CREATE FUNCTION `getChild1`(inSerialn Varchar(20)) RETURNS int(11)
BEGIN
Declare result int;
set result = (Select sum(process_cost) FROM mfng.data where serialn = inSerialn);
return result;
END//
UPDATE mfng.data set total_child_cost =
(case when length(serialn) > 6 AND pdn_process = 'Packing'
THEN
getChild1(serialn) + getParent1(serialn)
ELSE
0 END);
//

Is it possible to have mulitiple checks in a single mySQL IF case?

I want to write a query that will verify 8 parameters of a row are the same as the parameters passed to the procedure. If any one of them is different then modify a status, otherwise do nothing.
Is there a way I can check all those parameters in a single IF case? For example:
IF (v_duty <> duty) OR (v_current <> current) OR (v_frequency <> frequency) THEN
* UPDATE ......;
END IF
Or do I have to use an ELSE IF for each comparison I want to make?
The above doesn't work, with or without brackets between each test.
You could probably manage this with a simple UPDATE and WHERE condition:
UPDATE table_name
SET status_column = 'new_status'
WHERE identifying_column = :identifier
AND (
v_duty != :v_duty
OR v_current != :current
OR v_frequency != :frequency
)