How to get the structure of json element? - sql

does a function exist (or how to create such a function) that take a json element and return the structure.
For instance, I would like a function f that in this case :
SELECT f(json_Array (json_object ('a' VALUE 1,json_Array (b valuejson_object ('a' VALUE 1)))= FROM DUAL;
returns [a integer, b [ a integer]] or somethings equivalent

From Oracle 12.2, You can use JSON_DATAGUIDE:
WITH table_name (value) AS (
SELECT json_Array(
json_object (
'a' VALUE 1,
'b' VALUE json_Array(
json_object ('a' VALUE 1),
'abcd',
123
)
)
)
FROM DUAL
)
SELECT j.path,
j.type
FROM table_name t
CROSS JOIN LATERAL(
SELECT JSON_DATAGUIDE(t.value) AS data
FROM DUAL
) d
CROSS JOIN LATERAL(
SELECT *
FROM JSON_TABLE(
d.data,
'$[*]'
COLUMNS(
path VARCHAR2(200) PATH '$."o:path"',
type VARCHAR2(200) PATH '$.type',
len INTEGER PATH '$."o:length"'
)
)
) j;
Which outputs:
PATH
TYPE
$
array
$.a
number
$.b
array
$.b[*]
string
$.b.a
number
If you want something more detailed then you are probably going to have to write your own JSON parser in PL/SQL (or Java and compile it in the database).
db<>fiddle here

Related

Oracle SQL XMLAGG &quote; Issue

I am trying to aggregate A into a list of dictionaries. Oracle's LISTAGG() works but does run into the 4k max char limit. I tried with XMLAGG but now I'm getting "and quot;" instead of "". Please suggest the best way of resolving this. XMLCAST ? Will the final outputs of listagg() and the work around be identical ?
A
N
{"1":"09","2":"11","3":"2010","4":"XYZ","5":""}
1
{"1":"09","2":"11","3":"2010","4":"XYZ","6":""}
2
{"1":"09","2":"11","3":"2010","4":"XYZ","7":""}
3
select
-- '[' ||
-- LISTAGG(cte.A, ' , ') WITHIN GROUP(
-- ORDER BY
-- cte.N
-- )
-- || ']'
'[' ||
rtrim(xmlagg(xmlelement(e,cte.A,', ').extract('//text()') order by cte.N).getclobval(),', ')
|| ']' aggr_lsts
from cte;
Bad Output:
[{"1":"09","2":"11","3":"2010","4":"XYZ","5":""}, {"1":"09","2":"11","3":"2010","4":"XYZ","6":""}, {"1":"09","2":"11","3":"2010","4":"XYZ","7":""}]
Good Output:
[{"1":"09","2":"11","3":"2010","4":"XYZ","5":""} , {"1":"09","2":"11","3":"2010","4":"XYZ","6":""} , {"1":"09","2":"11","3":"2010","4":"XYZ","7":""}]
Thank you.
You may use xmlcast function to correctly deserialize XML data as a clob.
Please note, that XMLType methods are deprecated since Oracle 11.2, so use XMLQUERY and XMLCAST instead.
with sample(col, rn) as (
select column_value, rownum
from sys.odcivarchar2list(
'&',
'>',
'<',
'"',
'correctly serializable text'
)
)
select
rtrim(xmlcast(xmlquery(
'//text()'
passing xmlagg(
xmlelement(e,col,', ')
order by rn
)
returning content
) as clob), ', ') as res
from sample
RES
&, >, <, ", correctly serializable text
But for 12.2 and above you may use JSON functions directly to produce correct JSON array: JSON_ARRAYAGG (with a restriction on 32k JSON keys per object, if I remember correctly).
with sample(col, rn) as (
select column_value, rownum
from sys.odcivarchar2list(
'{"a": 1, "b": "2"}',
'{"a": 2, "b": "qwe"}',
'{"a": 3, "c": "test"}'
)
)
select
json_arrayagg(
col order by rn
) as res
from sample
RES
["{"a": 1, "b": "2"}","{"a": 2, "b": "qwe"}","{"a": 3, "c": "test"}"]
fiddle
Don't use XML functions with JSON data. From Oracle 12.2, use JSONAGG to aggregate the rows:
SELECT JSON_ARRAYAGG(a FORMAT JSON ORDER BY n RETURNING CLOB) As output
FROM table_name
Note: you need FORMAT JSON to treat your input as valid JSON, otherwise it will be treated as a string and quoted. You also need RETURNING CLOB otherwise the default return type is a VARCHAR2 which is limited to 4000 characters and will give you the same issues as LISTAGG.
Which, for the sample data:
CREATE TABLE table_name (A, N) AS
SELECT '{"1":"09","2":"11","3":"2010","4":"XYZ","5":""}', 1 FROM DUAL UNION ALL
SELECT '{"1":"09","2":"11","3":"2010","4":"XYZ","6":""}', 2 FROM DUAL UNION ALL
SELECT '{"1":"09","2":"11","3":"2010","4":"XYZ","7":""}', 3 FROM DUAL;
Outputs:
OUTPUT
[{"1":"09","2":"11","3":"2010","4":"XYZ","5":""},{"1":"09","2":"11","3":"2010","4":"XYZ","6":""},{"1":"09","2":"11","3":"2010","4":"XYZ","7":""}]
fiddle

How do I append/prepend/concatenate a string to all field names in a SELECT * statement in BigQuery

I would like to SELECT fields from a table as part of a bigger query and I would like all of the field names for a particular table to be prepended or appended with a string without having to manually rename each field individually.
Example:
WITH a AS (
SELECT 'value_in_table_a' AS val1, 'another' AS val2
),
b AS (
SELECT 'value_in_table_b' AS val
)
SELECT
a.*,
b.*
FROM a
CROSS JOIN b
I would like the result to be something like:
a_val1
a_val2
b_val
value_in_table_a
another
value_in_table_b
But, I want to avoid having to manually rename each field -- which can be annoyingly tedious when I have 50 field names -- like:
SELECT
a.val1 AS a_val1,
a.val2 AS a_val2,
b.val AS b_val
FROM a
CROSS JOIN b
I know that I can do:
SELECT
a,
b
FROM a
CROSS JOIN b
And then the result is a STRUCT type which prepends the field names with the table name, but is there something more general?
Something like:
SELECT
a.* AS 'a_'*,
b.* AS *'_b'
FROM a
CROSS JOIN b
To get:
a_val1
a_val2
val_b
value_in_table_a
another
value_in_table_b
Consider below solution
create temp function extract_keys(input string) returns array<string> language js as """
return Object.keys(JSON.parse(input));
""";
create temp function extract_values(input string) returns array<string> language js as """
return Object.values(JSON.parse(input));
""";
create temp function flatten_json(input string) returns string language js as '''
function flattenObj(obj, parent = '', res = {}){
for(let key in obj){
let propName = parent ? parent + '.' + key : key;
if(typeof obj[key] == 'object'){
flattenObj(obj[key], propName, res);
} else {
res[propName] = obj[key];
}
}
return JSON.stringify(res);
}
return flattenObj(JSON.parse(input));
''';
create temp table temp_table as (
select offset, key, value, format('%t', t) row_id
from (select a, b from a cross join b) t,
unnest([struct(to_json_string(t) as json)]),
unnest([struct(flatten_json(json) as leaves)]),
unnest(extract_keys(leaves)) key with offset
join unnest(extract_values(leaves)) value with offset
using(offset)
);
execute immediate (
select '''
select * except(row_id) from (select * except(offset) from temp_table)
pivot (any_value(value) for replace(key, '.', '_') in (''' || keys || "))"
from (
select string_agg('"' || replace(key, '.', '_') || '"', ',' order by offset) keys
from (select key, min(offset) as offset from temp_table group by key)
)
);
if applied to sample data in your question - output is

TRIM in bigquery

I want to apply TRIM function for my columns. But TRIM after Format function is not working. It's not trimming the spaces.
If I do it before format as below then it gives me error for datatype because the columns have other datatypes than string and byte as well.
Please tell me a solution for this.
Meantime, you can apply some extra processing on top of original query to get desired result - as in below example
select *,
trim(replace(regexp_replace(format('%t', t), r' *, *| *\)|\( *', '/'), '/NULL/', '/_/'), '/') HashColumn
from your_table t
if applied to sample data
with your_table as (
select ' 1' A, '2 ' B, null C, 4 D union all
select ' 12 ', null, '4', 5
)
output is
Consider below approach
create temp function json_extract_values(input string) returns array<string> language js as """
return Object.values(JSON.parse(input));""";
select *,
( select string_agg(trim(value), '/')
from unnest(json_extract_values(replace(to_json_string(t), ':null', ':"_"'))) value
) as HashColumn
from your_table t
if applied to dummy data as below
with your_table as (
select ' 1' A, '2 ' B, null C, 4 D union all
select ' 12 ', null, '4', 5
)
output is
which, I hope, is exactly what you are looking for

XML Table- Parametrized Path

I have a problem with getting positions of the specific node.
Problem is that positions doesn't have reference to the parent node (no attribute), but they are under it in xml hierarchy.
When I try to parametrize it in this way:
SELECT something....
FROM ktr_xml x,
XMLTABLE (
'/Invoices/Invoice[#ID="' || p_invoice_number || '"]' || '/InvoiceLine'
PASSING x.xml
COLUMNS line_number VARCHAR2 (100) PATH 'ID',
Product_quantity VARCHAR2 (100)
PATH 'InvoicedQuantity',
etc.... etc....
WHERE x.id = p_id;
It gives me error, that string is expected, it means I cant build dynamic path based on field of invoice (ID).
If I run this query:
FROM ktr_xml x,
XMLTABLE (
'/Invoices/Invoice/InvoiceLine'
PASSING x.xml
Its getting all the items, not only from the specific invoice.
Any ideas how I could solve it?
You could split the query into several correlated XMLTABLE statements:
Oracle Setup:
CREATE TABLE ktr_xml( id, xml ) AS
SELECT 1, XMLTYPE(
'<Invoices>
<Invoice ID="2">
<InvoiceLine>
<ID>Invoice1</ID>
<InvoicedQuantity>42</InvoicedQuantity>
</InvoiceLine>
</Invoice>
</Invoices>'
) FROM DUAL;
Query:
SELECT x1.*
FROM ktr_xml x
INNER JOIN
XMLTABLE (
'/Invoices/Invoice'
PASSING x.xml
COLUMNS
id NUMBER PATH './#ID',
xml XMLTYPE PATH '.'
) t
ON ( t.id = 2 /*p_invoice_number*/ )
CROSS JOIN
XMLTABLE (
'Invoice/InvoiceLine'
PASSING t.xml
COLUMNS
line_number VARCHAR2 (100) PATH 'ID',
Product_quantity VARCHAR2 (100) PATH 'InvoicedQuantity'
) x1
WHERE x.id = 1 /*p_id*/;
Output:
LINE_NUMBER | PRODUCT_QUANTITY
:---------- | :---------------
Invoice1 | 42
db<>fiddle here

remove extra + from text SQL

this refers to a question asked by someone else previously
previous question
my question is how do I adapt this solution so that before any function/script is ran the name and value fields are stripped of any additional + and updated so no additional + remain.
For e.g.
Name Value
A+B+C+ 1+2+3+
A++B 1++2
this should be updated to
Name Value
A+B+C 1+2+3
A+B 1+2
once this update has taken place, I can run the solution provided in the previous question.
Thanks
You need to replace ++ with + and to remove the + at the end of the string.
/* sample data */
with input(Name, Value) as (
select 'A+B+C+' ,'1+2+3+' from dual union all
select 'A++B' ,'1++2' from dual
)
/* query */
select trim('+' from regexp_replace(name, '\+{2,}', '+') ) as name,
trim('+' from regexp_replace(value, '\+{2,}', '+') ) as value
from input
If you need to update a table, you may need:
update yourTable
set name = trim('+' from regexp_replace(name, '\+{2,}', '+') ),
value= trim('+' from regexp_replace(value, '\+{2,}', '+') )
In a more compact way, without the external trim ( assuming you have no leading +):
/* sample data */
with input(Name, Value) as (
select 'A+B+C+' ,'1+2+3+' from dual union all
select 'A++B+++C+' ,'1++2+++3+' from dual union all
select 'A+B' ,'1+2' from dual
)
/* query */
select regexp_replace(name, '(\+)+(\+|$)', '\2') as name,
regexp_replace(value, '(\+)+(\+|$)', '\2') as value
from input
You could use something on the lines of:
Select substr('1+2+3+', 0, length('1+2+3+')-1) from dual ;
Select replace('1++2', '++', '+') from dual;
I'm assuming you have the output already present in a variable you can play with.
EDIT:
Here's a function that can solve the problem (You can call this function in your select clauses thereby solving the problem):
CREATE OR REPLACE Function ReplaceChars
( name_in IN varchar2 )
RETURN varchar2
IS
changed_string varchar2(20) ;
BEGIN
changed_string:=replace(name_in, '++', '+') ;
CASE WHEN substr(changed_string, -1) in ('+')
then
changed_string:=substr(changed_string,0, length(changed_string) - 1) ;
else changed_string:=changed_string ;
end CASE ;
RETURN changed_string;
END;
You can use the below:
LTRIM(RTRIM (REGEXP_REPLACE (column_name, '\+{2,}', '+'), '+'),'+')
Eg:
SELECT LTRIM(RTRIM (REGEXP_REPLACE ('+A+++B+C+++D++', '\+{2,}', '+'), '+'),'+') VALUE
FROM DUAL;
returns output: A+B+C+D
if youre working with ssms, GIVE IT A GO:::
UPDATE tablename
SET colname=
CASE colname WHEN LIKE '%++%' THEN
WHILE colname LIKE '%++%'
(REPLACE(colname,++,+))
END LOOP
WHEN LIKE '%+' THEN
SUBSTR(colname, 1, LENGTH(colname)-1)
WHEN LIKE '+%' THEN
SUBSTR(colname, 2, LENGTH(colname))
ELSE
colname
END