PL/SQL: Creating a Function: invalid identifier - plsql

I have an oracle 19c ee database build via their docker image on Oracles github (https://github.com/oracle/docker-images/tree/master/OracleDatabase/SingleInstance). I am trying to follow their example as to how to create a function from here.
I have copied their example exactly. Setup table and data:
CREATE TABLE orders (
customer_id number(10),
order_total NUMBER(11,2)
);
INSERT INTO orders (customer_id, order_total) VALUES (1, 200.01)
The actual function:
CREATE FUNCTION get_bal(acc_no IN NUMBER)
RETURN NUMBER
IS acc_bal NUMBER(11,2);
BEGIN
SELECT order_total
INTO acc_bal
FROM orders
WHERE customer_id = acc_no;
RETURN(acc_bal);
END;
However I keep running into this error when I try and create the function
Query 2 ERROR: ORA-06550: line 5, column 27:
PL/SQL: ORA-00904: "ACC_NO": invalid identifier
ORA-06550: line 2, column 7:
PL/SQL: SQL Statement ignored
ORA-06550: line 6, column 7:
PLS-00372: In a procedure, RETURN statement cannot contain an expression
ORA-06550: line 6, column 7:
PL/SQL: Statement ignored
What am I doing wrong?

The example works for me. You must have mistyped something. Are you sure your function is exactly the same as the one in the manual?
ORA-00904: "ACC_NO": invalid identifier
suggests the declaration acc_bal NUMBER(11,2); is missing or different.
PLS-00372: In a procedure, RETURN statement cannot contain an expression
indicates that your code is a procedure, not a function.
SQL> CREATE TABLE orders (
2 customer_id number(10),
3 order_total NUMBER(11,2)
4 );
Table created
SQL> INSERT INTO orders (customer_id, order_total) VALUES (1, 200.01);
1 row inserted
SQL> CREATE FUNCTION get_bal(acc_no IN NUMBER)
2 RETURN NUMBER
3 IS acc_bal NUMBER(11,2);
4 BEGIN
5 SELECT order_total
6 INTO acc_bal
7 FROM orders
8 WHERE customer_id = acc_no;
9 RETURN(acc_bal);
10 END;
11 /
Function created
SQL> select get_bal(1) from dual;
GET_BAL(1)
----------
200.01
As an aside, while I'm a big fan of the Oracle documentation in general, and this example does neatly illustrate how to create a PL/SQL function, I think it could be improved:
For readability, it's better to give each declaration its own line, so line 3 would be better split into two with acc_bal NUMBER(11,2); on its own line.
The IS and AS keywords are interchangeable here, but surely create ... as (similar to what you might use to create a table or a view) reads better than create ... is.
Understandably, the author didn't want to complicate the example by introducing %type before it had been explained, but a more advanced version would use acc_bal orders.order_total%type; to make acc_bal inherit its datatype from the table column rather than hard-coding it. This goes for all three values used in the function.
The parameter and variable names are OK - they are at least clear - but there is a danger when using the same naming pattern for parameters and variables as for table columns. One day you will type WHERE c.customer_id = customer_id and wonder why you're getting more rows back than you expected. Again it's understandable that the author didn't want to get into that whole discussion in the first example, but it's something to think about. You might use get_bal.acc_no within the function, or use camelCase for parameters and variables, or prefix them with p_ for 'parameter' etc.
A basic rule of layout is that opening and closing keywords such as if/else and begin/end should be left-aligned. The END at line 10 is misaligned under its opening BEGIN. I suppose indenting the entire thing after the first line is a valid personal layout choice, but to me it doesn't add anything.
It's a good idea to leave blank lines around each SQL statement, to avoid a solid wall of text. Personally, I'd prefer a blank line before the RETURN at line 9.
A RETURN clause doesn't require any brackets. The compiler is ignoring the redundant brackets at line 9. I'd lose them.
It's good practice (though optional) to include the procedure/function name in the closing END, so line 10 would become END get_bal;
The COBOL-style uppercase habit is widespread in the industry, but there is no need for it. (PL/SQL's syntax is famously based on Ada, though some also point to ALGOL and PL/1 - nobody ever wrote those in uppercase.) I would improve readability by lowercasing the whole thing.
With these changes, I get this:
create or replace function get_bal
( inAccNo in orders.customer_id%type )
return orders.order_total%type
as
accBal orders.order_total%type;
begin
select order_total into accBal
from orders
where customer_id = inAccNo;
return accBal;
end;

Related

What syntax am I missing?

I am a completely new to SQL and I am follow a tutorial verbatim to try and create a new table in my first database. However I am getting the following error.
USE menu;
CREATE TABLE Burgers
(
`Burger Number` TINYINT,
Burger VARCHAR(50),
Price DECIMAL(5,2),
Description VARCHAR(300),
);
Yields SQL Error 1064:
SQL Error (1064): You have an errror in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ')' at line 8
I made sure to use commas to separate each column and used parenthesis to what I understand is the proper way to do so. I am not sure if version is relevant but I am using version 10.5.8 of MariaDB. Any insight is appreciated!
You have one comma too many.
...
Description VARCHAR(300),
);
You need commas between each column, index, or constraint within the CREATE TABLE statement, except for the last one before the closing parenthesis.
It should be like this:
...
Description VARCHAR(300)
);
General tip about syntax errors: They tell you exactly where to look for the problem, because it reports the place in your SQL statement where the syntax parser got confused.
In this case it reported:
...right syntax to use near ')' at line 8
This tells you the problem is at that point in the syntax. The ) didn't belong there, because the syntax was expecting something else. Because commas separate columns, it was expecting another column definition following a comma.

How to print unique numbers dynamically with PLSQL

I would like to display Unique numbers dynamically. I have tried below code for the same but same number is displaying all the times.
DECLARE
a NUMBER;
BEGIN
FOR i IN 1 .. 3 LOOP
DBMS_OUTPUT.PUT_LINE(&a);
END LOOP;
END;
the above code will ask me for "a" value three times, if i pass 1,2,3 as parameters then it should display 1,2,3 but this code is displaying first(1) value three time as 1,1,1.
Could you please help me to get the required output like 1,2,3
You can't really create an interactive program in just PL/SQL. When you put &a in the PL/SQL and run it in a tool like SQL Developer, it prompts you once for a value for a before it runs the code, using the value you typed instead of the substitution variable a.
You want to print i and not a. Also the ampersand in front of the a means you will be prompted to enter a value for a.

Prevent SQLite query from stripping leading zeros from numeric strings?

In my database, a table contains two columns each containing an 8 digit ASCII code, usually it's just alphanumeric. For example, a row might contain A123B45C in col1 and PQ2R4680 in col2.
I need to have a query/view that outputs a 4 character string calculated as the 2nd+3rd chars of these, concatenated. So in this example the extra column value would be 12Q2.
This is a cut-down version of the SQL I'd like to use, although it won't work as written because of zero stripping / conversion:
select
*,
(substr(col1, 2, 2) || substr(col2, 2, 2)) AS mode
from (nested SQL source query)
where (conditions)
This fails because if a row contains A00B23B4 in col1 and P32R4680 in col2, it will evaluate as 0032 and the query output will contain numeric 32 not 0032. (It's worse if col1 contains P1-2345 or "1.23456" or something like that)
Other questions on preventing zero stripping and string to integer conversion in Sqlite, all relate to data in tables where you can define a column text affinity, or static (quotable) data. In this case I can't do these things. I also can only create queries, not tables, so I can't write to a temp table.
What is the best way to ensure I get a 4 character output in all cases?
I believe you issue is not with substr stripping characters as this works as expected e.g. :-
Then running query SELECT substr(col1,2,2) || substr(col2,2,2) as mode FROM stripping
results in (as expected):-
Rather, your issue is likely how you subsequently utilise mode in which case you may need to use a CAST expression CAST expressions
For example the following does what is possibly happening :-
`SELECT substr(col1,2,2) || substr(col2,2,2) as mode, CAST(substr(col1,2,2) || substr(col2,2,2) AS INTEGER) AS oops FROM stripping`
resulting in :-

How to replace occurrence only on the start of the string in Oracle SQL?

I have a source column and I want to search for string values starting with 05, 5 971971 and 97105 to be replaced by 9715. As showin in output table.
SOURCE OUTPUT
0514377920 971514377920
544233920 971544233920
971971511233920 971511233920
9710511233920 971511233920
I tried following which works for first case.
SELECT REGEXP_REPLACE ('0544377905', '^(\05*)', '9715')FROM dual;
But following is not working, for second case:
SELECT REGEXP_REPLACE ('544377905', '^(\5*)', '9715')FROM dual;
Something is wrong with my regular expression. As I am getting: ORA-12727: invalid back reference in regular expression.
You can provide your four patterns using alternation; that is, in parentheses with a vertical bar between them:
with t(source) as (
select '0514377920' from dual
union all select '544233920' from dual
union all select '971971511233920' from dual
union all select '9710511233920' from dual
)
SELECT source, REGEXP_REPLACE (source, '^(05|5|9719715|97105)', '9715') as output
FROM t;
SOURCE OUTPUT
--------------- --------------------
0514377920 971514377920
544233920 971544233920
971971511233920 971511233920
9710511233920 971511233920
Depending on your data and any other restrictions you have, you may be able to make it as simple as replacing the first part of any string that has a 5 in it, which works for your small sample:
SELECT source, REGEXP_REPLACE (source, '^.[^5]?5', '9715') as output
FROM t;
That matches zero or more characters that are not 5, followed by a 5. That may be too simplistic for your real situation though.

SUM totals by FOR ALL ENTRIES itab keys

I want to execute a SELECT query on a database table that has 6 key fields, let's assume they are keyA, keyB, ..., keyF.
As input parameters to my ABAP function module I do receive an internal table with exactly that structure of the key fields, each entry in that internal table therefore corresponds to one tuple in the database table.
Thus I simply need to select all tuples from the database table that correspond to the entries in my internal table.
Furthermore, I want to aggregate an amount column in that database table in exactly the same query.
In pseudo SQL the query would look as follows:
SELECT SUM(amount) FROM table WHERE (keyA, keyB, keyC, keyD, keyE, keyF) IN {internal table}.
However, this representation is not possible in ABAP OpenSQL.
Only one column (such as keyA) is allowed to state, not a composite key. Furthermore I can only use 'selection tables' (those with SIGN, OPTIOn, LOW, HIGH) after they keyword IN.
Using FOR ALL ENTRIES seems feasible, however in this case I cannot use SUM since aggregation is not allowed in the same query.
Any suggestions?
For selecting records for each entry of an internal table, normally the for all entries idiom in ABAP Open SQL is your friend. In your case, you have the additional requirement to aggregate a sum. Unfortunately, the result set of a SELECT statement that works with for all entries is not allowed to use aggregate functions. In my eyes, the best way in this case is to compute the sum from the result set in the ABAP layer. The following example works in my system (note in passing: using the new ABAP language features that came with 7.40, you could considerably shorten the whole code).
report zz_ztmp_test.
start-of-selection.
perform test.
* Database table ZTMP_TEST :
* ID - key field - type CHAR10
* VALUE - no key field - type INT4
* Content: 'A' 10, 'B' 20, 'C' 30, 'D' 40, 'E' 50
types: ty_entries type standard table of ztmp_test.
* ---
form test.
data: lv_sum type i,
lt_result type ty_entries,
lt_keys type ty_entries.
perform fill_keys changing lt_keys.
if lt_keys is not initial.
select * into table lt_result
from ztmp_test
for all entries in lt_keys
where id = lt_keys-id.
endif.
perform get_sum using lt_result
changing lv_sum.
write: / lv_sum.
endform.
form fill_keys changing ct_keys type ty_entries.
append :
'A' to ct_keys,
'C' to ct_keys,
'E' to ct_keys.
endform.
form get_sum using it_entries type ty_entries
changing value(ev_sum) type i.
field-symbols: <ls_test> type ztmp_test.
clear ev_sum.
loop at it_entries assigning <ls_test>.
add <ls_test>-value to ev_sum.
endloop.
endform.
I would use FOR ALL ENTRIES to fetch all the related rows, then LOOP round the resulting table and add up the relevant field into a total. If you have ABAP 740 or later, you can use REDUCE operator to avoid having to loop round the table manually:
DATA(total) = REDUCE i( INIT sum = 0
FOR wa IN itab NEXT sum = sum + wa-field ).
One possible approach is simultaneous summarizing inside SELECT loop using statement SELECT...ENDSELECT statement.
Sample with calculating all order lines/quantities for the plant:
TYPES: BEGIN OF ls_collect,
werks TYPE t001w-werks,
menge TYPE ekpo-menge,
END OF ls_collect.
DATA: lt_collect TYPE TABLE OF ls_collect.
SELECT werks UP TO 100 ROWS
FROM t001w
INTO TABLE #DATA(lt_werks).
SELECT werks, menge
FROM ekpo
INTO #DATA(order)
FOR ALL ENTRIES IN #lt_werks
WHERE werks = #lt_werks-werks.
COLLECT order INTO lt_collect.
ENDSELECT.
The sample has no business sense and placed here just for educational purpose.
Another more robust and modern approach is CTE (Common Table Expressions) available since ABAP 751 version. This technique is specially intended among others for total/subtotal tasks:
WITH
+plants AS (
SELECT werks UP TO 100 ROWS
FROM t011w ),
+orders_by_plant AS (
SELECT SUM( menge )
FROM ekpo AS e
INNER JOIN +plants AS m
ON e~werks = m~werks
GROUP BY werks )
SELECT werks, menge
FROM +orders_by_plant
INTO TABLE #DATA(lt_sums)
ORDER BY werks.
cl_demo_output=>display( lt_sums ).
The first table expression +material is your internal table, the second +orders_by_mat quantities totals selected by the above materials and the last query is the final output query.

Resources