Lab 6 – Advanced PL/SQL Programming SOLVING MORE COMPLEX PROBLEMS In the last lecture you were introduced to PL/SQL and the unnamed or anonymous procedure blocks. These procedure blocks provide the database developer with the capability to build executable program units that can take some action on the actual data stored in your database. However, we were limited in what we could do. We were unable to for example: 1. 2. 3. 4. solve problems that required a decision or repeated steps, handle errors in a user friendly manner, operate separately on a single row returned from a SELECT execution, or execute a procedure with out entering the syntax into the PL/SQL editor. This time we will explore techniques to solve these problems. By creating reusable modules and by implementing additional control structures, we will create program units that are more robust and flexible. This will allow us to better solve business problems. First, the only procedural construct that we have explored is Sequential Structures. So far, all we do is simply processed one instruction after another. This construct can only be used to solve simple problems. In order to solve more complex problems, however, we need the ability to make decisions as well as repeat steps. To do this we will use Decision Control Structures to make decisions and the Iterative Control Structures to repeat steps. Second, in order to handle errors in a more flexible and “user friendly” manner, we will implement Exception Handling. In addition to displaying more descriptive messages, the database designer can provide error handling routines. Programs can provide a way for the user to correct the problem instead of simply exiting the program. Third, we will discover how to manipulate a single row of data contained in a data set. When executing a SELECT statement in PL/SQL, Oracle establishes a work area that contains the set of data. This work area containing the set of data is referred to as a cursor. A cursor can be thought of as a pointer to a table in the database buffer cache. By declaring a cursor, we can process the returned rows individually. Therefore, using this technique, we can examine and manipulate one row at a time. Finally, the only was we could execute the anonymous procedural program unit was to enter the instructions (PL/SQL commands) directly into one of the PL/SQL editors or reference a file from within SQL*Plus. To keep the procedure, we stored the commands in a text (.txt) or SQL (.sql) file outside the database. This method, however, is not very practical and prone to error. We will discover how to store the program units in the database by creating named stored procedures. Learner Outcomes Management of Solution Development By completely this lab, you will achieve a deep level of knowledge and comprehension of the disciplines used in the development of information system solutions. You will develop the ability to apply these disciplines to the solution of organizational and business problems. Specifically, after completing this lab, you will be able to: 1. Design, construct, and maintain a database and various database objects using procedural language constructs. 2. Design and implement a complete problem solution using current database technology (Oracle 11g Database) To accomplish this you will: Implement decision control and iterative control structures Create and exception handling routines Implement record level processing in Oracle 11g using cursors Create and implement callable (stored) procedures in Oracle 11g So, let’s get started!!! 1. Control Structures We will extend our knowledge of procedures by using some of the more popular procedural constructs known as decision control structures and iterative control structures. These two families of constructs allow you to change the control flow of a program unit from purely sequential to one where you are in command of how the logic within the procedure flows. Decision Control The PL/SQL commands used to make decisions, that is, alter the order in with commands are executed are the IF/THEN, IF/THEN/ELSE and the IF/THEN/ELSIF. IF/THEN format Notice there are semi-colons on the commands after the THEN and on the END IF; IF condition THEN Commands that you want to execute if the condition is true; END IF; The IF portion of the command tests the condition. If the condition is true, control is transferred to the command that immediately follows the THEN. If the condition is false, control is transferred to the command following the END IF. Here is an example of the IF/THEN command: DECLARE DISTANCEVAR NUMERIC (4) CONSTANT := 500; BEGIN IF policyType = ‘HO’ AND Notice that you can use a compound condition using logical operators fireHydrantLoc > DISTANCEVAR THEN DBMS_OUTPUT.PUT_LINE(‘Insured property must be within ’|| DISTANCEVAR ||’feet’); END IF; END; IF/THEN/ELSE format IF condition THEN Commands that you want to execute if the condition is true; ELSE Commands that you want to execute if the condition is false; END IF; The IF part of the command tests the condition. If the condition is true, control is transferred to the command that immediately following the THEN. If the condition is false, control is transferred to the command following the ELSE. Here is an example of the IF/THEN/ELSE command: DECLARE DISTANCEVAR Numeric (4) CONSTANT := 500; BEGIN IF policyType = ‘HO’ AND fireHydrantLoc > DISTANCEVAR THEN DBMS_OUTPUT.PUT_LINE(‘Insured property must be within ’|| DISTANCEVAR || ‘ feet of a fire hydrant’); ELSE DBMS_OUTPUT.PUT_LINE(‘Insured property is within the acceptable distance to a fire hydrant); END IF; END; Notice that when the condition is false the ELSE path is taken. You can also nest your IF statements by including them after the ELSE. IF/THEN/ELSIF format This decision control structure allows you to test for multiple conditions in the control string. IF condition1 THEN Note odd spelling Commands that you want to execute if the condition1 is true; ELSIF condition2 THEN Commands that you want to execute if the new condition2 is true; ELSE Commands that you want to execute if the new condition2 is false; END IF; The IF portion of the command tests condition1. If condition1 is true, control is transferred to the command that immediately following the THEN. If the condition1 is false, control is transferred to the ELSIF command where condition2 is tested. If condition2 is true control is transferred to the command following the second THEN. If condition2 is false control is transferred to the command following the ELSE. Here is an example of the IF/THEN/ELSIF command: DECLARE DISTANCEVAR Numeric (4) CONSTANT := 500; BEGIN IF policyType = ‘HO’ AND fireHydrantLoc > DISTANCEVAR THEN DBMS_OUTPUT.PUT_LINE(‘Insured property must be within ’|| DISTANCEVAR ||‘ feet of a fire hydrant’); ELSIF policyType = ‘FO’ THEN DBMS_OUTPUT.PUT_LINE You can have as many of these ELSIF commands as practical. (‘Insured property does not require access to a fire hydrant); ELSE DBMS_OUTPUT.PUT_LINE (‘Insured property has required access to a fire hydrant); END IF; END; An alternative to the IF/THEN/ELSIF command would be to use the CASE structure which for the previous example would look like this: DECLARE DISTANCEVAR NUMERIC (4) CONSTANT:= 500; BEGIN CASE WHEN policyType = 'HO' AND fireHydrantLoc > DISTANCEVAR THEN DBMS_OUTPUT.PUT_LINE ('Property must be within '|| DISTANCEVAR || ' feet of a fire hydrant'); WHEN policyType = 'FO' THEN DBMS_OUTPUT.PUT_LINE You can have as many of these WHEN/THEN blocks as practical. ('Insured property does not require access to a fire hydrant'); ELSE DBMS_OUTPUT.PUT_LINE ('Insured property has required access to a fire hydrant'); END CASE; END; Hands-On Examples Vote for Pedro. Let’s write a program which asks for the number of votes for Pedro. When Pedro has more than 200 votes, he wins! Do This: Connect to SQL Developer as IST469 and open a new sql file. Type the following code into the query window as save it as vote-for-pedro.sql : Run this program a couple of times entering different values each time. See if you can make Pedro win and lose the election with different values for vote_countvote A better Vote for Pedro. Next, let’s write a program which is a little more descriptive with the narrative when Pedro wins or loses. Do This: Connect to SQL Developer as IST469 and open a new sql file. Type the following code into the query window as save it as a-better-vote-for-pedro.sql : Iterative Control Sometimes you need to execute a certain logic pattern or a series of commands many times in a row before doing something else. We call this process of managing iterative control, looping. There are two types of looping: pretest and posttest. You would use pretest looping when you want to evaluate an “exit condition” (that is determining when to stop the looping process) before your commands are executed. You would use posttest looping when you need to execute the commands in your loop at least once. There are five different looping control structures: LOOP/EXIT, LOOP/EXIT/WHEN, WHILE/LOOP, FOR/LOOP and CURSOR/FOR/LOOPS. For this lab we will discuss the first four. Cursor/For/Loops will be discussed later in the semester. LOOP/EXIT format. This can be either a pretest or a posttest loop depending on how you structure your statements. LOOP Commands you want to execute IF exit condition THEN EXIT; END IF; END LOOP; The LOOP keyword signals the beginning of the loop process followed any commands you want executed. An IF/THEN control structure tests the loop’s exit condition. If the exit condition is true, control is transferred to the EXIT which signals the end of the looping process. Here is an example of the LOOP/EXIT command: DECLARE deptTotalSalaryVar Numeric (7) := 0; empSalaryVar Numeric (7) := 1000; BEGIN LOOP deptTotalSalaryVar := deptTotalSalaryVar + empSalaryVar; IF deptTotalSalaryVar > 1000000 THEN EXIT; END IF; END LOOP; END; Notice that you need to end the loop with an END LOOP; command. LOOP/EXIT/WHEN format. This can be either a pretest or a posttest loop depending on how you structure your statements. LOOP Commands you want to execute EXIT WHEN condition; END LOOP; The LOOP keyword signals the beginning of the loop process followed any commands you want executed. WHEN control structure tests the loop’s exit condition. If the exit condition is true, control is transferred to the EXIT which signals the end of the looping process. Here is an example of the LOOP/EXIT/WHEN command: DECLARE deptTotalSalaryVar Numeric (7) := 0; empSalaryVar Numeric (7) := 1000; BEGIN LOOP deptTotalSalaryVar := deptTotalSalaryVar + empSalaryVar ; EXIT WHEN deptTotalSalaryVar >1000000; END LOOP; DBMS_OUTPUT.PUT_LINE ('Dept Salary: ' || deptTotalSalaryVar); END; WHILE/LOOP format. This is a pretest loop that evaluates the exit condition before any commands are executed. WHILE exit condition LOOP Commands you want to execute END LOOP; The WHILE evaluates the exit condition before the LOOP keyword signals the beginning of the loop process. If the exit condition is true control is transferred to the commands following the LOOP keyword. If the exit condition is false control is transferred to the END LOOP; which signals an exit from the looping process. Here is an example of the WHILE/LOOP command: DECLARE vDeptTotalSalary Numeric (7) := 0; vEmpSalary Numeric (7) := 1000; BEGIN WHILE vDeptTotalSalary <1000000 LOOP vDeptTotalSalary := vDeptTotalSalary + vEmpSalary; END LOOP; DBMS_OUTPUT.PUT_LINE ('Dept Salary: ' || vDeptTotalSalary); END; FOR LOOP format. This is a posttest loop that evaluates the exit condition after the commands are executed. FOR counter_variable IN start_value .. end_value LOOP Commands you want to execute The counter_variable does not have to be defined in the DECLARE section. Start and end values must be integers END LOOP; The FOR evaluates the exit condition before the LOOP keyword signals the beginning of the loop process. Control is transferred to the commands following the LOOP keyword. The loop increments the variable counter by one until it equals the end value. When the end value is reached, i.e. the exit condition, false control is transferred to the END LOOP; which signals an exit from the looping process. Here is an example of the FOR LOOP command: DECLARE deptTotalSalaryVar Numeric (7) := 0; The vLoopCount counter variable can not be referenced outside of the FOR LOOP unless you explicitly declare it in the DECLARE section. BEGIN FOR vLoopCount IN 1 .. 5 LOOP No semi-colon at the end of the FOR deptTotalSalaryVar := deptTotalSalaryVar + &empSalarySV; END LOOP; DBMS_OUTPUT.PUT_LINE ('Dept Salary: END; ' || deptTotalSalaryVar); Hands-On Examples Multiples of. Let’s write a program that uses a loop. This program will ask you for two numbers and generate multiples. Do This: Connect to SQL Developer as IST469 and open a new sql file. Type the following code into the query window as save it as multiples-of.sql : Run this code a few times to better understand what it is doing. It should be fairly straightforward. 2. Exception Handling So, now we can really solve complex business problems. We can execute one instruction after another (Sequential Control), make decisions (Decision Control), and even repeat steps (Iterative Control) if necessary. This is great as long as everything is perfect. That is, when executing a select query for a specific record with a Where clause, the record exists and any data requested from the user is entered in a valid format. These are only a few examples of potential errors. There exist various potential problems that could cause program units to not execute properly. For example, what would happen in the previous example if the data in the multiple field was entered as an alphabetic such as ‘abc’ and not a valid numeric value? It just so happens that Oracle will display an error message similar to the one below and terminate the execution of the code. There is no way to test for every possible combination of data that might be entered, the Oracle error message is not user friendly, and there is no way to recover. To solve this problem, Oracle has implemented a technique to except and handle errors (situations that should not occur) using an architecture referred to as exception handling. The format of the exception handling architecture is: BEGIN EXCEPTION Execution Statements Exception Statements END; Control transfers to the EXCEPTION section when a defined exception occurs. So, now let’s look at the previous example with an exception section added to trap a VALUE error. Do This: Connect to SQL Developer as IST469 and open a new sql file. Type the following code into the query window as save it as multiples-of-with-exception-handling.sql : Try to execute this code and at one of the prompts, enter something which is not a number, for example: And you should see the following error message at execution: This is a much nicer way to handle execution errors and there are a variety of predefined exceptions available to use such as NO_DATA_FOUND, TOO_MANY_ROWS, and ZERO_DIVIDE. You can find a table of predefined exception in Oracle’s “PL/SQL User’s Guide and Reference”. A list of common predefined exceptions can also be located in the “Guide to Oracle 91” text. Undefined Exceptions There are errors that are not as common that could occur and they have not be assigned a name. There are exceptions that are generated by Oracle but they do not have “user friendly” name that can easily be tested for. When these errors occur, the error message displayed may make sense to someone who knows the database; but, the message would not make sense to an end user. You can handle these exception by defining them in the DECLARE section of your procedure. To do this you would: 1. Declare an exception 2. Associate the exception with the Oracle Error Number DECLARE Declare an exception variable exceptional_ex EXCEPTION; using the datatype EXCEPTION PRAGMA EXCEPTION_INIT (exceptional_ex, -#####); BEGIN Code goes here; Associate that exception with a specific error Oracle error number (do not forget the - ) EXCEPTION WHEN exceptional_ex THEN do something here; END; Do this when the exception occurs Let’s look at an example. Suppose, we had a customer table that contained an attribute for the State two character abbreviation. We also have a state table and the State abbreviation code is a foreign key that references the State table. Now, suppose we have an unnamed stored procedure that inserts values into the table based on substitution variables. If we enter data and use a state abbreviation that does not have an entry in the State table, we would get a foreign key constraint violation. This is an Oracle – 02291 and the error message that would be displayed would look something like the following: ORA-02291: integrity constraint (FUDGEMART.STATE_FK) violated - parent key not found To avoid this problem we could create our own exception and let the complier know which Oracle error number to associate with this newly defined exception using the above format. The following PL/SQL unnamed procedure creates a NoStateFoundEX and will now throw an error that has a more meaningful message; /* PROGRAM: AUTHOR: DATE: PURPOSE: Customer Insert Susan Dischiave 2/28/2006 To insert a record into the customer table */ DECLARE cIDVar cNameVar cStateVar CUSTOMER_T.Customer_ID%TYPE := CUSTOMER_T.Customer_Name%TYPE := CUSTOMER_T.Customer_State%TYPE := &cIDSV; &cNameSV; &cStateSV; -- Declare an exception for No State Found NoStateFoundEx EXCEPTION; -- Associate the Oracle exception with the NOStateFound -use the Oracle error code for foreign key constraint error PRAGMA EXCEPTION_INIT (NoStateFoundEx, -02291); Process the NoStateFoundEx when Oracle BEGIN 02291 occurs --Insert values into the customer table -using the substitution variables -INSERT INTO Customer_T (Customer_ID, Customer_Name, Customer_State) VALUES (cIDVar, cNameVar, cStateVar); EXCEPTION WHEN NoStateFoundEx THEN DBMS_OUTPUT.PUT_LINE('There is no entry for state ' || cStateVar); END; Now the error message that is displayed is There is no entry for state xx The Oracle developers could not possibly have thought of all possible exceptions that you might want to capture. For example, a grade entry procedure for Syracuse University might want to raise an exception if a grade value of Z was entered. An employer would probably not want an employee’s birth date to be greater than the current date. There are numerous custom exceptions that would cause inaccurate data in the database if they were not caught and handled properly. You can avoid this problem that could potentially result in anomalies by creating your own exceptions. User-Defined Exceptions Oracle has provided a simple method for users to implement their own custom, user-defined, exceptions. The steps involved are to: 1. 2. 3. 4. 5. declare an exception variable using the EXCEPTION datatype, check for the error condition using a decision control structure, raise an exception condition using the RAISE command, transfer control to the exception handling section, and handle the exception. The format is: DECLARE MY_ERROR_EX EXCEPTION; Declare an exception variable using the EXCEPTION datatype BEGIN IF some condition Check for the error condition THEN RAISE MY_ERROR_EX Raise the error (transfer control to the exception handler) END IF; Some other program commands; EXCEPTION WHEN MY_ERROR_EX THEN END; some program commands; Handle the error in an appropriate manner Suppose in the previous example, we wanted to check the salary to make sure that the hourly wage was at or above the NYS minimum wage. The procedure might look like this: DECLARE deptTotalSalaryVar MIN_WAGE_CON CONSTANT empSalaryVar INVALID_SALARY_EX NUMERIC (7) NUMERIC (7) NUMERIC(7) EXCEPTION; := := := -- 0; 6; &empSalarySV; declare the exception variable BEGIN -- check that the salary is above minimum wage IF empSalaryVar < MIN_WAGE_CON THEN RAISE INVALID_SALARY_EX; END IF; FOR vLoopCount IN 1 .. 5 LOOP deptTotalSalaryVar := deptTotalSalaryVar + (40*empSalaryVar); END LOOP; DBMS_OUTPUT.PUT_LINE ('Dept Salary: ' || TO_CHAR(deptTotalSalaryVar)); EXCEPTION WHEN VALUE_ERROR THEN DBMS_OUTPUT.PUT_LINE('An invalid value was entered for the employee salary'); WHEN INVALID_SALARY_EX THEN DBMS_OUTPUT.PUT_LINE('The salary IS BELOW minimum wage '); END; 3. Cursors Now that we can write program units (procedures) that can solve complex problems, we need to access the data. The data required for the procedures is stored in the database tables. And, we already know the mechanism supplied by SQL that allows us to retrieve the desired rows. You are already familiar with the SQL SELECT command. This command allows you to specify the table, attributes, and even specify criteria for rows that you wish to retrieve. Upon execution, the data values are returned into a work area (database buffer cache). In addition, Oracle creates a pointer to the area called a cursor. There are implicit and explicit cursors. An implicit cursor is automatically established by Oracle every time an SQL statement is executed. The user is unaware of this cursor and has no way to control the process or the data using this cursor. If, however, you would like to have control and access each row individually, you can create an explicit cursor. Think of an explicit cursor as a pointer to the database buffer cache that has a name you know. Since you know the name that contains the address, you can now use the name to go to the address. So, let’s take a closer look at how to use an explicit cursor. The process includes: 1. 2. 3. 4. declare the cursor in the DECLARE section, in the code section (BEGIN) open the cursor, fetch a row and continue until there are no more rows left in the cursor, finally, close the cursor. DECLARE CURSOR testCur IS SELECT * FROM TestTable; testTableRec TestTable%ROWTYPE; BEGIN OPEN testCur; Declare a cursors which includes the SELECT required to retrieve the data Declare a variable to hold the individual row Fetched Open the cursor Fetch (read) a single row from the cursor LOOP FETCH testCur INTO testTableRec; EXIT WHEN testCur%NOTFOUND; EXIT when there are no more rows execute some commands; END LOOP; CLOSE testCUR; Close the cursor when processing is completed END; Let’s take a look at a specific example. The FUDGEMART schema contains a PRODUCTS table. Suppose we want discount all products in the ‘Electronics’ department based on the following discount rule structure. Retail Price Over $500 Over $50 50 or under Discount 25% 15% 5% It would be difficult, if not impossible to write a SELECT query to do this – sometimes the only way to solve a problem like this is to use a cursor to cycle and process individual rows. Do This: Connect to SQL Developer as IST469 and open a new sql file. Type the following code into the query window as save it as fudgemart-eletronics-discounts.sql : When you execute the anonymous procedure, you should see the following output: A more useful means of writing this cursor would be to store the discounts somewhere so that we do not have to recalculate them each time someone considers placing an order. We’ll alter the PRODUCTS table to accommodate discounts and then re-write the procedure. Do This: First let’s alter the FUDGEMART.PRODUCTS table and add some columns to handle discounts. Newly added columns Make sure your new columns are there and doing their duties: Now that things are going to plan, we need to we-write our cursor-using anonymous procedure to apply discounts to the table (rather that writing them to the screen). Do This: Connect to SQL Developer as IST469 and open a new sql file. Type the following code into the query window as save it as process-fudgemart-eletronics-discounts.sql : After you get this script to execute without error, it might not seem to do anything but if your check the products table, you should see your new price structure! Remember that because we changed data, we‘re going to need to use COMMIT to save those changes, or ROLLBACK when we don’t want to keep them. This is very useful when debugging your procedures as you can always rollback any pending changes to your data. If you don’t commit this update it will not persist through your database session, so it is important to COMMIT or ROLLBACK ASAP. It’s never a good idea to leave open transactions kicking around so it’s best to write this procedure to be transaction safe by including logic to commit or rollback inside the procedure itself: At the end of the procedure, I commit. If at any point, the you-know-what hits the you-know-where I rollback and pass the exception along. 4. Stored Procedures This is all really great. We have the ability to solve complex problems, handle errors effectively, and process a single row at a time. But, we’re still entering the commands in an editor separate from the database and storing the anonymous procedures in a separate location. We also have no effective way to share our program units with other users. By creating stored procedures, however, we can store the program unit in a convenient location, the database, and allow access to any other user that we want. In addition, stored procedures allow for input parameters as well as output parameters. That is, you can pass a value to the procedure to use when it executes and/or you can return a value to the calling procedure. We could have certainly used this in our last example! So, let’s take a look at the basic format required to create or replace a stored procedure. The format of a stored procedure is: CREATE OR REPLACE PROCEDURE MyProcedureName Procedure Name ( variable IN/OUT dataType defaultValue ) IS Define all IN/OUT parameters Declare all variables here BEGIN Place PL/SQL here END; The command to execute a stored procedure from SQL*Plus or other editor: EXEC MyProcedureName (parameter list); We can now take one of the previous procedures (FOR LOOP demo) and create a stored procedure. Instead of using substitution variables, we can pass the input as parameters. Do This: Connect to SQL Developer as IST469 and open a new sql file. Type the following code into the query window as save it as create-listmultiples.sql : One thing you’ll notice about a stored procedure is that when you execute the code to define the procedure it does not actually execute the procedure logic. It simply compiles the procedure and stores it in the IST469 schema. Observe from SQL Developer: That’s our stored procedure here. Now that we’ve stored our procedure, let’s execute it. Do This: Connect to SQL Developer as IST469 and open a new sql file. Type the following code into the query window as save it as run-listmultiples.sql : I hope you can see the obvious advantages of using stored procedures. The convenience of storing the PL/SQL in the catalog allows gives you that “write-once, run many” advantage. Also since the procedure itself is abstracted and encapsulated you can secure it using SQL like you can with tables and views. Not only can these procedures be easily accessed by other database users, but They can also be accessed by other procedures. Stored procedures can execute other stored procedures passing and returning parameters. This technique allows us to create small reusable modules which ultimately facilitate quicker development and easier maintenance. In this final example, let’s write a transaction safe procedure to add a new Fudgemart vendor, and then demonstrate how to execute the procedure. Do This: Connect to SQL Developer as IST469 and open a new sql file. Type the following code into the query window as save it as create-addvendor.sql : Next, execute the procedure adding this new vendor: And you should see this output: Now we can make the database more modular allowing us to take advantage of reuse and reliability. Creating stored procedures allow us to create small reusable program units. We also now can process individual rows of data (not just sets of records) using cursors; and, we can also make our units of code display messages that have meaning for the users! Lab Assignment – On Your Own Use the FUDGEMART schema to complete each of the following problems. For each question you must provide a screenshot of your code and a screen shot which proves your code executed properly. 1. Write an anonymous procedure which, given an order ID will calculate and display the total amount of the order. HINT: the total amount should be the sum of the order quantity * the product retail price. Execute your stored procedure to verify it works. As a self-check, Order 19’s total should be $24.49, order 693 is $222.39 and order 118 should be $19.95 2. Alter your fudgemart.orders table twice. The first time add a new column called order_tax of type decimal(10,2) which does not allow nulls and uses a default value of zero. The second time add a new column called order_total of type decimal(10,2) which does not allow nulls and uses a default value of zero. 3. Write stored procedure called TotalOrders which will calculate the total of each order (like in question 1) and then store the order amount in the order_total column of the fudgemart.orders table. HINT: Use a cursor to solve this problem similar to what we did in the lab. 4. Since Fudgemart now has a warehouse in California, orders shipped in CA must pay 11% sales tax. Write an anonymous stored procedure which updates the order_tax with the column for any orders for customers in CA. HINT: As a self-check, order 693 (a CA order) should have a sales tax of 24.4629. 5. Fudgemart just opened an East-coast warehouse in New York, and as a result must now collect sales tax at a rate of 8% on orders shipped to NY, in addition to the 11% sales tax for CA shipped orders. Since this might happen for another state in the future, write a named stored procedure called LevySalesTax which, when given a two character state code such as ‘NY’ and a tax rate such as 0.08 will update all orders to customers in that state to include the appropriately calculated sales tax for that order. Make the procedure transaction-safe execute the procedure to update sales tax for NY.