My example only returns BMW 2010.
How do I get it to return AUDI 2000 and BMW 2010?
declare
sample_json varchar2 (32767)
:= '
[{"NAME":"AUDI","YEAR":"2000"},{"NAME":"BMW","YEAR":"2010"}]
';
begin
apex_json.parse (sample_json);
dbms_output.put_line (apex_json.get_varchar2 ('NAME'));
dbms_output.put_line (apex_json.get_varchar2 ('YEAR'));
end;
TL;DR - You cannot as you have duplicate keys in an object.
From JSON Standard - RFC 7159
Objects
An object structure is represented as a pair of curly brackets
surrounding zero or more name/value pairs (or members). A name is a
string. A single colon comes after each name, separating the name
from the value. A single comma separates a value from a following
name. The names within an object SHOULD be unique.
(Added emphasis)
{"NAME":"AUDI","YEAR":"2000","NAME":"BMW","YEAR":"2010"}
While technically it is syntactically correct JSON, it does not make sense as you are duplicating keys so most (every) JSON parsers following RFC 7159 will overwrite the first instance of a key with later occurrences so your JSON is effectively:
{"NAME":"BMW","YEAR":"2010"}
And you cannot get AUDI/2000 from the output (unless you parse the JSON by hand).
If you want to send multiple values then you should use an array:
[{"NAME":"AUDI","YEAR":"2000"},{"NAME":"BMW","YEAR":"2010"}]
Update
You can try:
declare
sample_json varchar2(32767) := '{"data":[{"NAME":"AUDI","YEAR":"2000"},{"NAME":"BMW","YEAR":"2010"}]}';
begin
apex_json.parse (sample_json);
dbms_output.put_line (apex_json.get_varchar2 ('data[1].NAME'));
dbms_output.put_line (apex_json.get_varchar2 ('data[1].YEAR'));
dbms_output.put_line (apex_json.get_varchar2 ('data[2].NAME'));
dbms_output.put_line (apex_json.get_varchar2 ('data[2].YEAR'));
end;
or (if apex will accept an array as its outer object):
declare
sample_json varchar2(32767) := '[{"NAME":"AUDI","YEAR":"2000"},{"NAME":"BMW","YEAR":"2010"}]';
begin
apex_json.parse (sample_json);
FOR i IN 1 .. 2 LOOP
dbms_output.put_line (apex_json.get_varchar2(p_path=>'[%d].NAME',p0=>i));
dbms_output.put_line (apex_json.get_varchar2(p_path=>'[%d].YEAR',p0=>i));
END LOOP;
end;
Since you're running Oracle 12c, you don't need Apex_json. You may use standard Oracle's JSON functions.
set serveroutput on
declare
sample_json varchar2 (32767)
:= '[{"NAME":"AUDI","YEAR":"2000"},{"NAME":"BMW","YEAR":"2010"}]';
BEGIN
for rec IN (
select j.name,j.year
from json_table(sample_json,'$[*]' COLUMNS
name varchar2(20) PATH '$.NAME',
year NUMBER PATH '$.YEAR'
) j )
LOOP
dbms_output.put_line (rec.name||','||rec.year);
END LOOP;
END;
/
AUDI,2000
BMW,2010
PL/SQL procedure successfully completed.
Related
I am trying to sort a data from runtime query and no way I can do that without use of a collection. Essentially, I have 2 columns in Service table - SERVICE_ID, SERVICE_NAME. I have created an Associative array in a package so that I can use it in my procedure.
TYPE g_vc_arr IS TABLE OF VARCHAR2(4000) INDEX BY PLS_INTEGER;
In my pl/sql block I am creating a variable like below:
service_list my_pkg.g_vc_arr;
I am assigning the SERVICE_ID and SERVICE_NAME to this variable like below:
LOOP
service_list(services.SERVICE_ID) := services.SERVICE_NAME;
END LOOP;
Now, I am using this to sort the name column like below snippet. I am able to print the name but in this process, I am losing the ID.
for query_result_row in (SELECT * from table(service_list) order by 1) loop
dbms_output.put_line(query_result_row.COLUMN_VALUE);
end loop;
I need both ID and NAME to process further. How can I get both?
As others have mentioned, you don't need to sort it as it is already held in the order you defined it:
create or replace package demo
as
type currency_tt is table of varchar2(3) index by pls_integer;
currencies demo.currency_tt :=
demo.currency_tt
( 1 => 'USD'
, 2 => 'GBP'
, 3 => 'EUR' );
end demo;
begin
for r in (
select * from table(demo.currencies)
)
loop
dbms_output.put_line(r.column_value);
end loop;
end;
Output:
USD
GBP
EUR
But you don't need SQL, as you can iterate over an associative array procedurally:
declare
i pls_integer := demo.currencies.first;
begin
while i is not null loop
dbms_output.put_line(i||' '||demo.currencies(i));
i := demo.currencies.next(i);
end loop;
end;
Or from 21c you can loop more conveniently like this:
begin
for id, ccy in pairs of demo.currencies loop
dbms_output.put_line(id || ' '|| ccy);
end loop;
end;
1 USD
2 GBP
3 EUR
I changed my approach a bit to solve this issue. I created a package with 2 column TYPE as record and an associative array of the record type:
create or replace package my_pkg is
TYPE g_rec_type IS RECORD (ID NUMBER, name VARCHAR2(4000));
TYPE g_rec_arr IS TABLE OF g_rec_type INDEX BY PLS_INTEGER;
end my_pkg;
In my actual PL/sqL block I created couple of variables:
i NUMBER := 0;
service_list MY_PKG.g_rec_arr;
Then I created a loop to assign the values to array:
LOOP
service_list(i).ID := services.SERVICE_ID;
service_list(i).NAME := services.SUMMARY;
i := i+1;
END LOOP;
Further to sort the value I used following:
FOR query_result_row in (SELECT ID, NAME from table(service_list) order by NAME) LOOP
dbms_output.put_line('ID is =>' || query_result_row.ID|| ' Name is =>' || query_result_row.NAME);
END LOOP;
And it worked as expected.
I made a package that compiles fine but when I try to test it it gives me "invalid data type".
I've tried two different ways, first one like this
select pkg_contabilidad.f_totalizar_Detalle(100) FROM DUAL;
It gives me the ORA-00902 'invalid data type'
Also I've tried this
DECLARE
TYPE r_registro IS RECORD
(rubro_contable CN_RUBROS_CONTABLES.COD_RUBRO%TYPE,
tipo VARCHAR2(1),
monto NUMBER(16));
resultao r_registro;
numero NUMBER :=100;
BEGIN
resultao := pkg_contabilidad.f_totalizar_detalle(numero);
END;
It gives me another error PLS-00382 'expression is of wrong type'
I don't know what am I doing wrong, cause my function receives just one parameter and is of type NUMBER, so I dont know where's my mistake. I'll leave the code of my package just in case
CREATE OR REPLACE PACKAGE pkg_contabilidad AS
TYPE r_registro IS RECORD
(rubro_contable CN_RUBROS_CONTABLES.COD_RUBRO%TYPE,
tipo VARCHAR2(1),
monto NUMBER(16));
TYPE t_detalle IS TABLE OF
r_registro INDEX BY BINARY_INTEGER;
FUNCTION f_totalizar_detalle(p_clave NUMBER)RETURN t_detalle;
END pkg_contabilidad;
/
CREATE OR REPLACE PACKAGE BODY pkg_contabilidad AS
B_detalle t_detalle;
i integer :=1;
FUNCTION f_totalizar_detalle(p_clave NUMBER) RETURN t_detalle IS
v_detalle t_detalle;
CURSOR c_facturado IS
SELECT c.cod_rubro, 'H', CASE WHEN SUM(d.gravada)=0 THEN SUM(d.iva) ELSE SUM(d.gravada) END
FROM fn_documentos_det d JOIN fn_conceptos c ON d.cod_concepto = c.cod_concepto
WHERE d.clave_doc=p_clave
GROUP BY c.cod_rubro;
CURSOR c_datos IS
SELECT SUM(d.total_doc), 'D',r.cod_rubro
FROM fn_documentos d JOIN fn_cajas_ctas r ON d.num_caja_cta = r.num_caja_cta
WHERE d.clave_doc = p_clave
GROUP BY r.cod_rubro;
BEGIN
open c_datos;
LOOP
FETCH c_datos INTO v_detalle(1);
END LOOP;
CLOSE c_datos;
FOR fila IN c_facturado LOOP
i := i + 1;
v_detalle(i) := fila;
END LOOP;
END;
END PKG_CONTABILIDAD;
The function returns a pkg_contabilidad.t_detalle, so the test needs to be:
declare
resultao pkg_contabilidad.t_detalle;
numero number := 100;
begin
resultao := pkg_contabilidad.f_totalizar_detalle(numero);
end;
It doesn't work in SQL because pkg_contabilidad.t_detalle is a PL/SQL type, not a SQL type (create or replace type). The database can perform some automatic conversions, but there are still limitations.
By the way, this loop will never complete because it lacks an exit condition:
open c_datos;
loop
fetch c_datos into v_detalle(1);
end loop;
close c_datos;
Your function returns a PL/SQL table type, with a table of a PL/SQL record type, which is defined in your package, which plain SQL doesn't know about and can't display - hence your invalid datatype error. If you need to call the function and access the data from SQL you can create schema-level object and collection types instead.
In your anonymous block you are a declaring a new record type. That looks the same to you because the structure is the same, but Oracle expects the exact type the function returns. That makes your test code shorter and simpler though. But you are also trying to return the whole collection into a single record.
DECLARE
l_detalle pkg_contabilidad.t_detalle;
l_registro pkg_contabilidad.r_registro;
l_idx pls_integer;
numero NUMBER :=100;
BEGIN
l_detalle := pkg_contabilidad.f_totalizar_detalle(numero);
l_idx := l_detalle.FIRST;
WHILE l_idx is not null LOOP
l_registro := l_detalle(l_idx);
-- do something with this record
dbms_output.put_line(l_registro.tipo);
l_idx := l_detalle.NEXT(l_idx);
END LOOP;
END;
db<>fiddle with dummy cursors.
Your function is a bit strange and probably isn't doing quite what you want; but also has two fatal problems: it isn't returning anything, and it has an infinite loop. I've fixed those for the fiddle but not anything else, as this seems to be an exercise.
I've been asked to do input validation in order to prevent sql injection. I've been using dbms assert package functions to do the sanitization. However, when I try to sanitize a number(I'm getting it in varchar2(12 byte)) error is thrown. It's the same case with alphanumeric characters starting with number.
I tried various functions of dbms assert. Nothing seems to work except noop. But, noop is of no use since it does not do any validation.
create or replace procedure employee
(
v_emp_id IN varchar2(12 byte)
)
AS
lv_query CLOB;
BEGIN
if v_emp_id is NOT NULL THEN
lv_query := 'select * from employee where emp_id=''' || dbms_assert.enquote_name(v_emp_id) || '''';
--I also tried below:
-- lv_query := 'select * from employee where emp_id=''' || dbms_assert.simple_sql_name(v_emp_id) || '''';
end if;
END
No source gives more detailed input on dbms_assert package. Please help me in
Whether dbms_assert package can be used to sanitize numeric values(stored in VARCHAR2 variables). If yes, how?
Other ways of sanitizing input. (other than using bind variables)
Thanks.
Oracle 12.2 and higher
If you are on Oracle 12.2 or higher, you can use the VALIDATE_CONVERSION function which would be the simplest solution. Your code could potentially look something like this:
CREATE OR REPLACE PROCEDURE employee (v_emp_id IN VARCHAR2)
AS
lv_query CLOB;
BEGIN
IF v_emp_id IS NOT NULL AND validate_conversion (v_emp_id AS NUMBER) = 1
THEN
lv_query := 'select * from employee where emp_id = ' || v_emp_id;
ELSE
--do something here with an invalid number
null;
END IF;
END;
/
Earlier than Oracle 12.2
If you are not on Oracle 12.2 or higher, you can write your own small function to validate that the value is a number. Using a method similar to what Belayer suggested, just attempt to convert the value to a number using the TO_NUMBER function and if it fails, then you know it's not a number. In my example, I have it as a small anonymous block within the code but you can also make it a standalone function if you wish.
CREATE OR REPLACE PROCEDURE employee (v_emp_id IN VARCHAR2)
AS
lv_query CLOB;
l_is_number BOOLEAN;
BEGIN
--Verify that the parameter is a number
DECLARE
l_test_num NUMBER;
BEGIN
l_test_num := TO_NUMBER (v_emp_id);
l_is_number := TRUE;
EXCEPTION
WHEN VALUE_ERROR
THEN
l_is_number := FALSE;
END;
--Finished verifying if the parameter is a number
IF v_emp_id IS NOT NULL AND l_is_number
THEN
lv_query := 'select * from employee where emp_id = ' || v_emp_id;
ELSE
--do something here with an invalid number
null;
END IF;
END;
/
Well if you cannot change the procedure it means you have no test as that procedure will not compile, so it cannot be executed. However that may be a moot point. You need to define exactly what you mean by "sanitize numeric values". Do you mean validate a string contains a numeric value. If so DBMS_ASSERT will not do that. (Note: The function chooses ENQUOTE_NAME will uppercase the string and put double quotes (") around it thus making it a valid object name.) Further your particular validation may require you define a valid numeric value, is it: an integer, a floating point, is scientific nation permitted, is there a required precision and scale that must be satisfied, etc. As a brute force validation you can simulate the assertion by just convert to number. The following will do that. Like dbms_assert if the assertion is successful it returns the input string. Unlike dbms_assert, however, when the assertion fails it just returns null instead of raising an exception. See fiddle.
create or replace
function assert_is_numeric(value_in varchar2)
return varchar2
is
not_numeric exception;
pragma exception_init (not_numeric,-06502);
l_numeric number;
begin
l_numeric := to_number(value_in);
return value_in;
exception
when not_numeric then
return null;
end assert_is_numeric;
I am concatenating string using cursor (to form query to execute later). Here, the query that will be formed is going to be way bigger that what VARCHAR2(32767) can handle. There fore, I am getting error on proc execution - ORA-06502: PL/SQL: numeric or value error: character string buffer too small.
I used CLOB data type as well bu got error ORA-06502: PL/SQL: numeric or value error.
My code is here below:
CREATE OR REPLACE PROCEDURE sp_Market
IS
Names VARCHAR2(32767);
BEGIN
DECLARE CURSOR cur IS ('Select ID, Order_of, field_name
FROM pld_medicare_config');
BEGIN
FOR i IN cur
LOOP
Names := Names || i.sqql;
END LOOP;
dbms_output.put_line(Names);
END;
END sp_Market;
How can I handle my string of queries and what data type is there to accomplish the task?
CLOB is OK (as far as I can tell); I doubt queries you store in there are that big.
Remove dbms_output.put_line call from the procedure; I suspect it is the one that raises the error.
I'm not sure how you got any runtime error, as your procedure won't compile.
The valid PL/SQL version would look something like this:
create or replace procedure sp_market is
names varchar2(32767);
begin
for r in (
select id, order_of, field_name
from pld_medicare_config
)
loop
names := names || ' ' || r.field_name;
end loop;
names := ltrim(names);
dbms_output.put_line(names);
end sp_market;
If names needs to be longer, change the datatype to clob.
Use the CLOB datatype and append data using the dbms_lob.writeappend procedure. This is the reference (Oracle 18c).
The error probably origins with the dbms_output.put_line call. The procedure is defined for varchar2 arguments only which means that an implicit conversion takes place during the call. It will fail for clob contents longer than 32767 chars/bytes.
Alternatively you may declare a collection over varchar2(4000) and fill the collection elements sequentially:
CREATE OR REPLACE PROCEDURE sp_Market
IS
TYPE tLongString IS TABLE OF VARCHAR2(4000) INDEX BY BINARY_INTEGER;
cNames tLongString;
BEGIN
DECLARE CURSOR cur IS Select ID, Order_of, field_name, sqql FROM pld_medicare_config;
BEGIN
FOR i IN cur
LOOP
cNames(cNames.COUNT+1) := i.sqql;
END LOOP;
END;
END sp_Market;
Note
Rectified code, will compile now.
I'm struggling to create a dynamic sql parametrized query. It involves using 'IS NULL' or 'IS NOT NULL'
Here's a simple pl/sql query:
CREATE OR REPLACE PROCEDURE GET_ALL_INFORMATION
(
"PARAM_START_DATE" IN DATE,
"PARAM_END_DATE" IN DATE,
"PARAM_IS_SUBMITTED" IN NUMBER,
"EXTRACT_SUBMITTED_CONTACTS" OUT sys_refcursor
) IS
sql_stmt VARCHAR2(3000);
PARAM_CONDITION VARCHAR2(20);
BEGIN
IF PARAM_IS_SUBMITTED = 1 THEN
PARAM_CONDITION := 'NOT NULL';
ELSE
PARAM_CONDITION := 'NULL';
END IF;
sql_stmt := ' SELECT
REGISTRATION_NUMBER,
NAME PROVIDER_TYPE,
ORGANIZATION
FROM TABLE_A
WHERE
P.DATE_FINALIZED IS :A;
OPEN EXTRACT_SUBMITTED_CONTACTS FOR sql_stmt USING PARAM_CONDITION;
Whereas the parameter (:A) in (USING PARAM_CONDITION) should have 'NULL' or 'NOT NULL'. It does not seem to work the way I envisioned.
Am I missing something?
As explained by GriffeyDog in a comment above, bind parameters could only be used as place holder for values. Not to replace keywords or identifiers.
However, this is not really an issue here, as you are using dynamic SQL. The key idea ifs that you build your query as a string -- and it will be parsed at run-time by the PL/SQL engine when you invoke EXECUTE or OPEN .. FOR.
Simply said, you need a concatenation -- not a bound parameter:
...
sql_stmt := ' SELECT
REGISTRATION_NUMBER,
NAME PROVIDER_TYPE,
ORGANIZATION
FROM TABLE_A
WHERE
P.DATE_FINALIZED IS ' || PARAM_CONDITION;
-- ^^
OPEN EXTRACT_SUBMITTED_CONTACTS FOR sql_stmt;