MERGE statement with "WITH FUNCTION" definition - sql

I am trying to use a function declared in WITH clause, into a MERGE statement.
Here is my code:
create table test
(c1 varchar2(10),
c2 varchar2(10),
c3 varchar2(10));
insert into test(c1, c2) values ('a', 'A');
insert into test(c1, c2) values ('b', 'A');
select * from test;
begin
with function to_upper(val varchar2) return varchar is
begin
return upper(val);
end;
merge into test a
using (select * from test) b
on (upper(a.c1) = upper(b.c2))
when matched then
update set a.c3 = to_upper(a.c1);
end;
but I am getting this error:
Error report - ORA-06550: line 2, column 15: PL/SQL: ORA-00905:
missing keyword ORA-06550: line 2, column 1: PL/SQL: SQL Statement
ignored ORA-06550: line 6, column 1: PLS-00103: Encountered the symbol
"MERGE"
06550. 00000 - "line %s, column %s:\n%s"
*Cause: Usually a PL/SQL compilation error.
*Action:
Can someone explain why it is not working, please?
Thank you,

The with clause is part of the select syntax. As the railroad diagram for that shows, the only thing that's valid after the with clause is the select keyword - you don't have that, hence the error you are seeing.
As the documentation also says says:
The plsql_declarations clause lets you declare and define PL/SQL functions and procedures. You can then reference the PL/SQL functions in the query in which you specify this clause, as well as its subqueries, if any.
and
If the query in which you specify this clause is not a top-level SELECT statement, then the following rules apply to the top-level SQL statement that contains the query:
If the top-level statement is a SELECT statement, then it must have either a WITH plsql_declarations clause or the WITH_PLSQL hint.
If the top-level statement is a DELETE, MERGE, INSERT, or UPDATE statement, then it must have the WITH_PLSQL hint.
So, you can't apply the with clause to the whole merge statement, you can only use it as part of a query within that, i.e. in the using clause:
merge /*+ WITH_PLSQL */ into test a
using (
with function to_upper(val varchar2) return varchar is
begin
return upper(val);
end;
select to_upper(c2) as c2 from test
) b
on (upper(a.c1) = b.c2)
when matched then
update set a.c3 = upper(a.c1);
Without the /*+ WITH_PLSQL */ hint this would error with "ORA-32034: unsupported use of WITH clause" (though only if you actually call the function, otherwise the complier seems to strip the unused function out and not complain about it).
But the function is still only in scope for the using clause; you can't refer to it in the on or update clauses. You would need to do any function calls within the using clause; and if you need the same function applied to anything from the target table, you would need to repeat the function and call that with the into clause, e.g.:
merge /*+ WITH_PLSQL */ into (
with function to_upper(val varchar2) return varchar is
begin
return upper(val);
end;
select to_upper(c1) as c1, c3 from test
) a
using (
with function to_upper(val varchar2) return varchar is
begin
return upper(val);
end;
select to_upper(c2) as c2 from test
) b
on (a.c1 = b.c2)
when matched then
update set a.c3 = a.c1;
db<>fiddle showing things that do and don't work.
It's quite a contrived example so it would probably be clearer with a more real-world scenario.

Related

Declaring variables and select statement in a procedure

I'm writing a SQL procedure which should use calculated date stored as a local variable in a select statement. I'm using Oracle SQL developer. My code is:
create or replace PROCEDURE
my_procedure
AS
BEGIN
DECLARE
l_max_dt DATE;
BEGIN
SELECT MAX(TRX_DT)
INTO l_max_dt
FROM TABLE
WHERE 1=1;
end;
select * from TABLE where trx_dt = l_max_dt;
end;
This code gives me an error : " Error(14,48): PL/SQL: ORA-00904: "L_MAX_DT": invalid identifier" when select statement is present.
How can I store variables to use them in statements?
This is how you write a Procedure. Syntax is incorrect. Read about syntax Here
CREATE OR REPLACE PROCEDURE my_procedure
AS
l_max_dt DATE;
v_var TABLE2%ROWTYPE;
BEGIN
SELECT MAX (TRX_DT)
INTO l_max_dt
FROM TABLE1
WHERE 1 = 1;
-- Assuming the query will retrun only 1 row.
SELECT *
INTO v_var
FROM TABLE2
WHERE trx_dt = l_max_dt;
END;
Your issue is one of scope. In your procedure, you have a nested block, in which you declare the l_max_dt variable. Once the code has exited that block, the l_max_dt variable is no longer in scope - i.e. the outer block does not know anything about it.
There is no need to have a nested block in this instance - you can do it all in the same block, like so:
create or replace PROCEDURE my_procedure
AS
l_max_dt DATE;
BEGIN
SELECT MAX(TRX_DT)
INTO l_max_dt
FROM TABLE
WHERE 1=1;
-- commented out as this isn't valid syntax; there is a missing INTO clause
-- select *
-- from TABLE where trx_dt = l_max_dt;
END my_procedure;
However, you could simply do the query in one fell swoop - e.g.:
select *
from your_table
where trx_dt = (select max(trx_dt) from your_table);
A couple of points about your procedure:
In PL/SQL, if you use an implicit cursor (i.e. when you put a select statement directly in the body of the code) you need to have something to put the results into. You could bulk collect the results into an array, or you could ensure that you will receive exactly one row (or code error handling for NO_DATA_FOUND and TOO_MANY_ROWS) into a record or corresponding scalar variables.
You shouldn't use select * in your procedure - instead, you should explicitly state the columns being returned, because someone adding a column to that table could cause your procedure to error. There are exceptions to this "rule", but explicitly stating the columns is a good habit to get into.

exists condition in a trigger

I'm quite new to PL/SQL, sorry if the question is obvious
According to the TRIGGER documentation, there is a WHEN ( condition ) for triggers. I wanted to use an exists condition, which requires a subquery, however, I have the following error :
ORA-02251
00000 - "subquery not allowed here"
*Cause: Subquery is not allowed here in the statement.
*Action: Remove the subquery from the statement.
What did I miss?
My condition is the following :
CREATE OR REPLACE TRIGGER mytrigger AFTER UPDATE OF column ON THIS_TABLE
FOR EACH ROW
WHEN (NEW.status = 'approved' AND EXISTS (
SELECT * FROM JUNCTION_TABLE WHERE THIS_TABLE_ID=NEW.this_table_id AND OTHER_TABLE_ID = 'SOMETHING'))
DECLARE
BEGIN
END;
I want to check whether the row is associated to a given value, which I can only find in a junction table.
I could surely do this in the PL/SQL part of the trigger, but :
it is related to the trigger rather than the business logic in itself
I'd like to understand what I missed in the documentation and why it is not possible.
If another condition might do this, I'm also interested.
I would write this with the conditional element within the trigger itself, something like
CREATE OR REPLACE TRIGGER mytrigger AFTER UPDATE OF column ON THIS_TABLE
FOR EACH ROW
WHEN (NEW.status = 'approved')
DECLARE
BEGIN
IF EXISTS (
SELECT * FROM JUNCTION_TABLE WHERE THIS_TABLE_ID=NEW.this_table_id AND OTHER_TABLE_ID = 'SOMETHING'))
...
END;
I don't know which Oracle documentation was used. Though the Oracle 10.2 documentation doesn't mention this, in the Oracle 11.1 documentation the limitation is mentioned:
The expression in a WHEN clause must be a SQL expression, and it cannot include a subquery. You cannot use a PL/SQL expression (including user-defined functions) in the WHEN clause.
There's no alternative I can think of at the moment to checking the condition inside the trigger code, as mentioned.

PL/SQL Error assign sysdate value to variable (date) using select into clause

I was writing a PL/SQL procedure to come up with a report.
Here is part of my scripts where I tested and had the compilation error.
I believe it's not the syntax of the select into clause, but I don't know the exact problem when assigning value to the date variables...
Did anyone encounter this error before?
DECLARE
v_bc_mth DATE;
BEGIN
SELECT TRUNC(ADD_MONTHS(SYSDATE, -1)) INTO v_bc_mth FROM dual; --what's wrong with the clause
SELECT * FROM bc WHERE v_bc_mth BETWEEN bc_start_date AND bc_end_date;
END;
PLS-00428: an INTO clause is expected in this SELECT statement
06550. 00000 - "line %s, column %s:\n%s"
*Cause: Usually a PL/SQL compilation error.
You need the INTO clause for both SELECT statements.
However, do you really need to use PL/SQL?
You could do it all in SQL, avoiding context switches with:
SELECT *
FROM bc
WHERE TRUNC(ADD_MONTHS(SYSDATE, -1)) BETWEEN bc_start_date AND bc_end_date;
Or, you could rewrite what you have so there is only one switch to SQL from within your PL/SQL block as:
DECLARE
TYPE bc_tabtype IS TABLE OF bc%ROWTYPE
INDEX BY pls_integer;
--
bc_tab bc_tabtype;
BEGIN
SELECT *
BULK COLLECT INTO bc_tab
FROM bc
WHERE TRUNC(ADD_MONTHS(SYSDATE, -1)) BETWEEN bc_start_date AND bc_end_date;
-- Do what you want with the results you now have in the Associative Array bc_tab
END;
You might need to look up associative arrays and BULK COLLECT etc. to understand them.
Tom Kyte, the Oracle VP states it succinctly when he says:
I have a pretty simple mantra when it comes to developing database
software, and I have written this many times over the years:
You should do it in a single SQL statement if at all possible.
If you cannot do it in a single SQL statement, do it in PL/SQL.
If you cannot do it in PL/SQL, try a Java stored procedure.
If you cannot do it in Java, do it in a C external procedure.
If you cannot do it in a C external procedure, you might want to seriously think about why it is you need to do it.
EDIT:
In light of your comment, try this:
DECLARE
v_bc_mth DATE := TRUNC(ADD_MONTHS(SYSDATE, -1));
--
TYPE bc_tabtype IS TABLE OF bc%ROWTYPE
INDEX BY pls_integer;
--
bc_tab bc_tabtype;
BEGIN
SELECT *
BULK COLLECT INTO bc_tab
FROM bc
WHERE v_bc_mth BETWEEN bc_start_date AND bc_end_date;
END;

Error(2,7): PLS-00428: an INTO clause is expected in this SELECT statement

I'm trying to create this trigger and getting the following compiler errors:
create or replace
TRIGGER RESTAR_PLAZAS
AFTER INSERT ON PLAN_VUELO
BEGIN
SELECT F.NRO_VUELO, M.CAPACIDAD, M.CAPACIDAD - COALESCE((
SELECT count(*) FROM PLAN_VUELO P
WHERE P.NRO_VUELO = F.NRO_VUELO
), 0) as PLAZAS_DISPONIBLES
FROM VUELO F
INNER JOIN MODELO M ON M.ID = F.CODIGO_AVION;
END RESTAR_PLAZAS;
Error(2,7): PL/SQL: SQL Statement ignored
Error(8,5): PL/SQL: ORA-00933: SQL command not properly ended
Error(8,27): PLS-00103: Encountered the symbol "end-of-file" when expecting one of the following: begin case declare end exception exit for goto if loop mod null pragma raise return select update while with <an identifier> <a double-quoted delimited-identifier> <a bind variable> << close current delete fetch lock insert open rollback savepoint set sql execute commit forall merge pipe
Error(2,1): PLS-00428: an INTO clause is expected in this SELECT statement
What's wrong with this trigger?
You won't be allowed to
SELECT count(*) FROM PLAN_VUELO
in a trigger on PLAN_VUELO
Don't use a trigger. Use a stored procedure.
Inside a PL/SQL block you have to SELECT ... INTO something. I had an example of this in an answer to one of your questions yesterday. In this case you may want to select into a local variable, and use the result to then update another table.
But it looks like you're probably going to get lots of results back because you haven't restricted to the value you're interested in; the WHERE clauses don't filter on any of the inserted row's :NEW values. That will cause an ORA-02112. You need to make sure your select will return exactly one row, or look at cursors if you actually want multiple rows.
Just add the into clause according to the result type, one example:
declare
my_result VUELO%rowtype;
begin
select v.* into my_result from VUELO v where id = '1';
end;

Using variables in PLSQL SELECT statement

I have a query that queries on ReportStartDate and ReportEndDate so I thought I would use variables in PLSQL. Not sure what I am missing here, but I get an error:
CLEAR;
DECLARE
varReportStartDate Date := to_date('05/01/2010', 'mm/dd/yyyy');
varReportEndDate Date := to_date('05/31/2010', 'mm/dd/yyyy');
BEGIN
SELECT
'Value TYPE',
1 AS CountType1,
2 AS CountType2,
3 AS CountType3
FROM DUAL;
SELECT COUNT (*)
FROM CDR.MSRS_E_INADVCH
WHERE 1=1
AND ReportStartDate = varReportStartDate
AND ReportEndDate = varReportEndDate
;
END;
/
The Error is:
Error starting at line 2 in command:
Error report:
ORA-06550: line 6, column 5:
PLS-00428: an INTO clause is expected in this SELECT statement
ORA-06550: line 8, column 5:
PLS-00428: an INTO clause is expected in this SELECT statement
06550. 00000 - "line %s, column %s:\n%s"
*Cause: Usually a PL/SQL compilation error.
*Action:
This happens in Toad as well as in SQL Developer.
What is the proper way of using the variables in my WHERE clause?
You cannot use SQL statements directly in a PL/SQL block ( unless you use EXECUTE IMMEDIATE). The columns will need to be fetched into variables ( which is what PL/SQL is telling you with PLS-00428: an INTO clause is expected in this SELECT statement error). So you'll have to rewrite your statements as below.
SELECT
'Value TYPE',
1 AS CountType1,
2 AS CountType2,
3 AS CountType3
INTO
V_VALUE_TYPE,
V_CountType1,
V_CountType2,
V_CountType3
FROM DUAL;
SELECT COUNT(*)
INTO V_COUNT
FROM CDR.MSRS_E_INADVCH
WHERE 1=1
AND ReportStartDate = varReportStartDate
AND ReportEndDate = varReportEndDate
Be sure to add Exception Handlers, since PL/SQL expects only 1 row to be returned. If the statement returns no rows, you'll hit a NO_DATA_FOUND exception - and if the statement fetches too many rows, you'll hit a TOO_MANY_ROWS exception.
The question you have to answer is what do you want to do with the data that has been selected?
Sathya gave you one approach - declare variables in your PL/SQL block and select the columns INTO those variables. Note that this requires that the SELECT statement returns exactly one row - any more or less rows will throw an error. Another way is to declare collection types using the BULK COLLECT option: http://oracletoday.blogspot.com/2005/11/bulk-collect_15.html
Yet another option is to have the procedure return a cursor. This is useful in the case where the calling code expects to be able to fetch the data that the procedure has selected:
PROCEDURE GET_MY_REPORT( varReportStartDate in date, varReportEndDate in date, cur out sys_refcursor) is
begin
OPEN cur FOR SELECT *
FROM CDR.MSRS_E_INADVCH
WHERE 1=1
AND ReportStartDate = varReportStartDate
AND ReportEndDate = varReportEndDate;
END GET_MY_REPORT;