My stored proc is defined as
create or replace procedure TEST(
name IN table1.col_name%type,
price IN table1.col_price%type
)
is
begin
update table1 t set t.name =name where t.price = price;
commit;
end TEST;
I am trying to execute it as
exec TEST(name => 'John', price => 1000);
However, it gives invalid SQL error. What am i missing here?
Your input parameter %type statements claim the column names are col_name and col_price. But that is not how you refer to them in your stored procedure (name and price).
Bad things can happen when you name variables after column names. AskTom recommends a limited convention of variable naming conventions:
local variables start with L_
parameters start with P_
global package variables start with G_
That link has a good general discussion on PL/SQL naming conventions. I personally just use V_ for most variables (aside from indexes and other obvious things), but that's just me.
Lastly, the col_ in the column names seem redundant; simply use name and price as column names.
So, that said, I think this does what you want:
create table table1 (
name varchar2(30),
price number
);
create or replace procedure TEST(
p_name IN table1.name%type,
p_price IN table1.price%type
)
is
begin
update table1
set name = p_name
where price = p_price;
commit;
end TEST;
/
insert into table1 values ('John', 500);
commit;
select * from table1;
exec TEST(p_name => 'Bob', p_price => 500);
select * from table1;
-- Clean up test artifacts
drop procedure test;
drop table table1;
Giving the output:
table TABLE1 created.
PROCEDURE TEST compiled
1 rows inserted.
committed.
NAME PRICE
------------------------------ ----------
John 500
anonymous block completed
NAME PRICE
------------------------------ ----------
Bob 500
procedure TEST dropped.
table TABLE1 dropped.
I really don't understand the variable prefixing approach. Oracle don't do it with their own API's, and it would be extraordinarily irritating if they did. It always seems like a workaround, rather than a fix.
For me the fix is to namespace the variables with the procedure name. It keeps the argument names "clean" and makes your code 100% proof against capture:
create or replace procedure TEST(
name IN table1.col_name%type,
price IN table1.col_price%type)
is
begin
update table1 t
set name = test.name
where t.price = price;
commit;
end TEST;
Lots more info on capture here.
Related
When using a stored procedure to dynamically generate a table based on configuration and return a result set (SELECT) with the records in that table, the second call to that procedure to generate a different table structure returns no records and it reports a missing column from a previous temporary table of the same name.
I tried this with MariaDB 10.3 and 10.1.21 and received the same result. I have minimized my code here to the minimum to demonstrate the error after trying several variations of single and multiple sub-procedures.
I also tried using some transaction control with COMMITS after executing the process, before trying to start the process with a different parameter, but got the same results.
DROP PROCEDURE IF EXISTS CreateATable;
DELIMITER $$
CREATE PROCEDURE CreateATable( _TableType tinyint )
BEGIN
DROP TEMPORARY TABLE IF EXISTS aTable;
IF _TableType = 1 THEN
SET #SQL_Statement :=
CONCAT(
'CREATE TEMPORARY TABLE aTable (',
'the_id bigint, ',
'the_column varchar(100) ',
') engine=INNODB',
';');
ELSE
SET #SQL_Statement :=
CONCAT(
'CREATE TEMPORARY TABLE aTable (',
'the_id bigint, ',
'the_other_column varchar(100) ',
') engine=INNODB',
';');
END IF;
PREPARE stmtCreateTable FROM #SQL_Statement;
EXECUTE stmtCreateTable;
DEALLOCATE PREPARE stmtCreateTable;
SET #SQL_Statement := NULL;
END$$
DELIMITER ;
DROP PROCEDURE IF EXISTS GetATable;
DELIMITER $$
CREATE PROCEDURE GetATable()
BEGIN
CALL CreateATable( 1 );
SELECT * FROM aTable;
CALL CreateATable( 2 );
SELECT * FROM aTable;
END$$
DELIMITER ;
DROP PROCEDURE IF EXISTS GetATable2;
DELIMITER $$
CREATE PROCEDURE GetATable2(_TableType tinyint)
BEGIN
CALL CreateATable( _TableType );
SELECT * FROM aTable;
END$$
DELIMITER ;
/*
Test execution script starts here
*/
-- Just CALL Create for one and Select
CALL CreateATable( 1 );
DESCRIBE aTable;
SELECT * FROM aTable;
CALL CreateATable( 2 );
DESCRIBE aTable;
SELECT * FROM aTable;
-- -> no errors
-- now CALL procedure to Create and Select from two different temp tables
CALL GetATable();
-- -> no errors
-- now CALL procedure to CREATE AND SELECT from ONE temp table definition using a parameter to select
CALL GetATable2(1);
CALL GetATable2(2);
-- Error Code: 1054. Unknown column 'mySchema.aTable.the_column' in 'field list'
I would expect that I can pass a parameter to a stored procedure to generate a temporary table, and return the records of that temporary table. Even if I call that same procedure multiple times with different parameters on the same session.
The actual results are that when the stored procedure is called to generate the temporary table with a different table structure, it returns this error complaining about the column missing from the temporary table created in the previous invocation of that same stored procedure.
Error Code: 1054. Unknown column 'mySchema.aTable.the_column' in 'field list'
The only way I have found to prevent this error is
a. ending the jdbc connection and ending the server session
b. recompiling one of the stored procedures in the call stack
Recompiling is not viable. And ending the session seems unreasonable.
This seems like a defect. But would be interested to find if there is some way to get this to work.
This seems like a bug and you can report it directly to the MariaDB team at MariaDB bugs database.
A temporary solution is to use a prepared statement in the stored procedure GetATable2 (my test on MariaDB 10.3.16 to use EXECUTE IMMEDIATE):
...
CREATE PROCEDURE `GetATable2`(`_TableType` TINYINT)
BEGIN
CALL CreateATable(`_TableType`);
-- SELECT * FROM `aTable`;
EXECUTE IMMEDIATE 'SELECT * FROM `aTable`';
END$$
...
See dbfiddle.
I'm new to PL/SQL. I'm using oracle 11g XE along with sql developer. I'm trying to create to create an after insert trigger as follows
create or replace trigger tr1
after
insert ON
employee
for each row
begin
print :new.emp_id;
end;
The employee table is as follows
create table employee
( emp_id varchar2(5) primary key,
emp_name varchar2(10),
salary number,
company varchar2(10) foreign key references companies(comp_name)
);
When I run the statement I got a 'enter binds' window for the bind variable :new. But I was confused that why do I need to enter the value for :new since it is pseudorecord. Then I entered 'employee' as the values for :new. Now I'm getting the following error.
Error(2,8): PLS-00103: Encountered the symbol "" when expecting one of the following: := . ( # % ; The symbol ":=" was substituted for "" to continue.
Your problem is not in the :new pseudorecord. The error is coming from the usage of print, which is used to print the bind variable used in successful PL/SQL block or used in an EXECUTE command. For example, you can use it this way:
VARIABLE n NUMBER
BEGIN
:n := 1;
END;
/
Then
PRINT n;
But if you want to test the value being inserted, you can use DBMS_OUTPUT.PUT_LINE like this:
create or replace trigger tr1
after
insert ON
employee
for each row
BEGIN
dbms_output.put_line(:new.emp_id);
END;
/
Enable DBMS_OUTPUT window in your SQL Developer, then run
insert into employee values(1, 'empName', 1000, 'ABC');
You'll see 1 printed out.
However, you can always test the value from the table. Because the value should be already inserted into table. You can just query.
I'm very new to tSQLt and am having some difficulty with what should really be a very simple test.
I have added a column to the SELECT statement executed within a stored procedure.
How do I test in a tSQLt test that the column is included in the resultset from that stored procedure?
Generally, when adding a column to the output of a stored procedure, you will want to test that the column both exists and is populated with the correct data. Since we're going to make sure that the column is populated with the same data, we can design a test that does exactly that:
CREATE PROCEDURE MyTests.[test stored procedure values MyNewColumn correctly]
AS
BEGIN
-- Create Actual and Expected table to hold the actual results of MyProcedure
-- and the results that I expect
CREATE TABLE MyTests.Actual (FirstColumn INT, MyNewColumn INT);
CREATE TABLE MyTests.Expected (FirstColumn INT, MyNewColumn INT);
-- Capture the results of MyProcedure into the Actual table
INSERT INTO MyTests.Actual
EXEC MySchema.MyProcedure;
-- Create the expected output
INSERT INTO MyTests.Expected (FirstColumn, MyNewColumn)
VALUES (7, 12);
INSERT INTO MyTests.Expected (FirstColumn, MyNewColumn)
VALUES (25, 99);
-- Check that Expected and Actual tables contain the same results
EXEC tSQLt.AssertEqualsTable 'MyTests.Expected', 'MyTests.Actual';
END;
Generally, the stored procedure you are testing relies on other tables or other stored procedures. Therefore, you should become familiar with FakeTable and SpyProcedure as well: http://tsqlt.org/user-guide/isolating-dependencies/
Another option if you are just interested in the structure of the output and not the content (and you are running on SQL2012 or greater) would be to make use of sys.dm_exec_describe_first_result_set_for_object in your test.
This dmo (dynamic management object) returns a variety of information about the first result set returned for a given object.
In my example below, I have only used a few of the columns returned by this dmo but others are available if, for example, your output includes decimal data types.
In this test, I populate a temporary table (#expected) with information about how I expect each column to be returned - such as name, datatype and nullability.
I then select the equivalent columns from the dmo into another temporary table (#actual).
Finally I use tSQLt.AssertEqualsTable to compare the contents of the two tables.
Having said all that, whilst I frequently write tests to validate the structure of views or tables (using tSQLt.AssertResultSetsHaveSameMetaData), I have never found the need to just test the result set contract for procedures. Dennis is correct, you would typically be interested in asserting that the various columns in your result set are populated with the correct values and by the time you've covered that functionality you should have covered every column anyway.
if object_id('dbo.myTable') is not null drop table dbo.myTable;
go
if object_id('dbo.myTable') is null
begin
create table dbo.myTable
(
Id int not null primary key
, ColumnA varchar(32) not null
, ColumnB varchar(64) null
)
end
go
if object_id('dbo.myProcedure') is not null drop procedure dbo.myProcedure;
go
create procedure dbo.myProcedure
as
begin
select Id, ColumnA, ColumnB from dbo.myTable;
end
go
exec tSQLt.NewTestClass #ClassName = 'myTests';
if object_id('[myTests].[test result set on SQL2012+]') is not null drop procedure [myTests].[test result set on SQL2012+];
go
create procedure [myTests].[test result set on SQL2012+]
as
begin
; with expectedCte (name, column_ordinal, system_type_name, is_nullable)
as
(
-- The first row sets up the data types for the #expected but is excluded from the expected results
select cast('' as nvarchar(200)), cast(0 as int), cast('' as nvarchar(200)), cast(0 as bit)
-- This is the result we are expecting to see
union all select 'Id', 1, 'int', 0
union all select 'ColumnA', 2, 'varchar(32)', 0
union all select 'ColumnB', 3, 'varchar(64)', 1
)
select * into #expected from expectedCte where column_ordinal > 0;
--! Act
select
name
, column_ordinal
, system_type_name
, is_nullable
into
#actual
from
sys.dm_exec_describe_first_result_set_for_object(object_id('dbo.myProcedure'), 0);
--! Assert
exec tSQLt.AssertEqualsTable '#expected', '#actual';
end
go
exec tSQLt.Run '[myTests].[test result set on SQL2012+]'
Facing the following error:
[Error] PLS-00049 (12: 11): PLS-00049: bad bind variable 'NEW.OPTION_D'
I am trying to execute the following:
create or replace trigger check_option_for_stage
before insert on lot_option
for each row
when (new.stage_id > 0 and new.option_id > 0)
declare
not_existing_option exception;
num_count number;
begin
select count(*) into num_count
from option_cost os
where :new.option_id = os.option_id and :new.stage_id = os.stage_id;
if num_count = 1 then
DBMS_OUTPUT.PUT_LINE('The option can be applied to the lot at the current stage');
ELSE
raise not_existing_option;
end if;
exception
when not_existing_option then
DBMS_OUTPUT.PUT_LINE('The option is not available on this stage, therefore rejected');
when others then
DBMS_OUTPUT.PUT_LINE('Oops!, something went wrong, it needs your attention!');
end;
/
Why am I facing this? Why is it a bad bind variable? I know that I should be able to access the new values by typing :new.whateverthecolumnname
I am using Oracle 11g.
The definition of the table I am playing around
SQL> desc option_cost
Name Null? Type
----------------------------------------- -------- ----------------------------
COST NOT NULL NUMBER
OPTION_ID NOT NULL NUMBER(38)
STAGE_ID NOT NULL NUMBER(38)
SQL> desc lot_option;
Name Null? Type
----------------------------------------- -------- ----------------------------
LOT_ID NOT NULL NUMBER(38)
OPTION_ID NOT NULL NUMBER(38)
COST NOT NULL NUMBER
DATE_CREATED DATE
STAGE_ID NOT NULL NUMBER(38)
Is the column option_id (with an id at the end) or just option_d (with no i)? option_id would seem to make more sense. Assuming option_id is correct, you have a typo in your SELECT statement where you are missing the i in id. You want something like
select count(*)
into count
from option_cost oc
where :new.option_id = oc.option_id
and :new.stage_id = oc.stage_id;
Of course, since count is a reserved word, it is not a good idea to declare a local variable named count. It would make much more sense to name that variable, say, l_count or use some other naming convention both to identify a local variable and to avoid using a reserved word.
I have create the following tables...
CREATE TABLE Actor
(Actor_ID CHAR(5),
lastName CHAR(24),
firstName CHAR(24),
/
CREATE TABLE Movie
(movieID CHAR(3) ,
title CHAR(36),
year NUMBER,
/
CREATE TABLE Role
(roleID CHAR(5),
roleName CHAR(36),
actorID CHAR(5),
movieID CHAR(3))
/
CREATE TABLE Quote
(quoteID CHAR(4),
quoteCHAR CHAR(255))
/
CREATE TABLE RoleQuote
(roleID CHAR(5),
quoteID CHAR(4))
/
Then i created this schemas....
CREATE TYPE ACTOR_QUOTE_TYPE AS OBJECT (
Movie_Title CHAR(36),
Year NUMBER,
Role CHAR(36),
Quote CHAR(255)
)
/
CREATE TYPE AQ_NT AS TABLE OF ACTOR_QUOTE_TYPE
/
CREATE TABLE ACTOR_QUOTES (
ACTORID CHAR(5),
QUOTES AQ_NT
) NESTED TABLE QUOTES STORE AS ACTOR_QUOTES_NT
/
I need to create a procedure with a single parameter(ACTORID is procedure parameter) and insert all the quotes in all the movies for any ACTORID, into the row(s) (an actor may have many movies and many quotes, some may have no quotes!) of the QUOTES nested table in the ACTOR_QUOTES table for any ACTORID.
How do i do it ?
Thanks
So far i tried this, i am not sure it is correct or not.
CREATE OR REPLACE PROCEDURE Populate_Movies_Quote
AS
CURSOR Quote_cursor (ActorID in CHAR) IS
SELECT ActorID, Quote, Movie_Title from Actor_Quotes, AQ_NT where Quotes.ActorID=ActorID;
BEGIN
FOR row IN Quote_cursor
LOOP
INSERT INTO ACTOR_QUOTES (ActorID, quotes) values (row.ActorID, AQ_NT(Actor_Quote_Type));
END LOOP;
END Populate_Movies_Quote ;
/
Show erros
LINE/COL ERROR
-------- -----------------------------------------------------------------
4/1 PL/SQL: SQL Statement ignored
4/55 PL/SQL: ORA-04044: procedure, function, package, or type is not
allowed here
6/1 PL/SQL: Statement ignored
6/10 PLS-00306: wrong number or types of arguments in call to
'QUOTE_CURSOR'
This certainly looks familiar. I take it someone else is stuck with Paul Judges Assignment too.
Without doing this for you here's the basic strategy I took.
First just write a select query that returns Movie Title, Movie Year, Role Name, and the Quote for a given Actor ID in that order. Forget about the procedure for now just get this select statement working. It means joining all the tables in the where clause.
If you achieve that then you basically have all the data that needs to be inserted into the nested table.
You can access the nested table for insertion by using the table function. So something like:
INSERT INTO TABLE(SELECT QUOTES FROM Actor_Quotes WHERE ActorID = Actor_ID)
Where "Actor_ID" is the name of your procedures parameter. PL/SQL actually lets you insert into a table values directly from a select statement. You just have to ensure the values returned by the select statement match the order and type that your insert statement is expecting. This is pretty handy as it means there is no need for a cursor loop. So essentially all you have to do is place the select statement I said to write earlier directly below the above insert statement and you should be sorted. Make sure you use the same Actor_ID Parameter in your select query though.