Dynamically generate sql statement based on metadata about the data being queried - sql

I would like to dynamically query data that is staged as a long string by defining how to read the string and how to split it up.
So I can define the data with the following elements
FIELD_NAME VARCHAR2(30) NOT NULL,
DATA_TYPE VARCHAR2(20) NOT NULL,
COLUMN_ID NUMBER NOT NULL,
FIELD_START_POS NUMBER,
FIELD_END_POS NUMBER,
FIELD_LEN NUMBER,
ROW_TYPE VARCHAR2(10),
DATE_MASK VARCHAR2(12)
sample data in this table
can I take that info to create a select that would look something like
SELECT CASE cd.data_type
WHEN 'DATE'
THEN
TO_DATE (SUBSTR (sd.source_text, cd.field_start_pos, cd.field_len), cd.date_mask)
WHEN 'NUMBER'
THEN
TO_NUMBER (SUBSTR (sd.source_text, cd.field_start_pos, cd.field_len))
ELSE
TRIM (SUBSTR (sd.source_text, cd.field_start_pos, cd.field_len))
END
AS cd.field_name
FROM staged_data sd, column_definitions cd
I am having difficulties trying to tie the 2 together.
I know I could pivot the column names in the definition out like so:
SELECT *
FROM column_definitions
PIVOT (max(field_name) FOR column_id IN (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20))
but this still results in many rows
My goal is to generate this statement so that is can be run via the EXECUTE IMMEDIATE so it could work for many different files just by defining how to read the string.
I also have the need to read different row types hence the row_type column which will be defined for the same file but had their own column order and columns.
So I have been able to generate a string that is the select I am looking for based on the metadata about the staged file like this:
DECLARE
select_items VARCHAR2 (4000);
BEGIN
FOR c IN ( SELECT *
FROM column_definitions
WHERE file_pk = 1 AND row_type = 1
ORDER BY column_id)
LOOP
IF c.data_type = 'NUMBER'
THEN
select_items :=
select_items
|| 'CASE WHEN is_number(SUBSTR(row_data,'
|| c.field_start_pos
|| ','
|| c.field_len
|| ')) = ''TRUE'' THEN TO_NUMBER(SUBSTR(row_data,'
|| c.field_start_pos
|| ','
|| c.field_len
|| ')) ELSE NULL END AS '
|| c.field_name
|| ',';
ELSIF c.data_type = 'DATE'
THEN
select_items :=
select_items
|| 'CASE WHEN ISDATE(SUBSTR(row_data,'
|| c.field_start_pos
|| ','
|| c.field_len
|| '))=''true'' THEN TO_DATE(SUBSTR(row_data,'
|| c.field_start_pos
|| ','
|| c.field_len
|| '),'''
|| c.date_mask
|| ''') ELSE NULL END AS '
|| c.field_name
|| ',';
ELSE
select_items :=
select_items
|| 'TRIM(SUBSTR(row_data,'
|| c.field_start_pos
|| ','
|| c.field_len
|| ')) AS '
|| c.field_name
|| ',';
END IF;
END LOOP;
select_items := SUBSTR (select_items, 1, LENGTH (select_items) - 1);
select_items :=
'SELECT '
|| select_items
|| ' FROM STAGED_FILE where row_type=1 AND rownum <= 1000;';
DBMS_OUTPUT.PUT_LINE (select_items);
END;
this spits out something like this:
SELECT CASE
WHEN is_number (SUBSTR (row_data, 1, 1)) = 'TRUE'
THEN
TO_NUMBER (SUBSTR (row_data, 1, 1))
ELSE
NULL
END
AS REC_TYPE_IND,
SUBSTR (row_data, 11, 4) AS SRVC_LOC,
CASE
WHEN ISDATE (SUBSTR (row_data, 15, 8)) = 'true'
THEN
TO_DATE (SUBSTR (row_data, 15, 8), 'YYYYMMDD')
ELSE
NULL
END
AS BEGIN_DT,
CASE
WHEN ISDATE (SUBSTR (row_data, 23, 8)) = 'true'
THEN
TO_DATE (SUBSTR (row_data, 23, 8), 'YYYYMMDD')
ELSE
NULL
END
AS END_DT,
SUBSTR (row_data, 31, 50) AS ID,
SUBSTR (row_data, 101, 2) AS COUNTY_CD,
SUBSTR (row_data, 103, 30) AS ADDR_LN_1,
SUBSTR (row_data, 133, 30) AS ADDR_LN_2,
SUBSTR (row_data, 163, 18) AS CITY,
SUBSTR (row_data, 181, 2) AS STATE_CD,
CASE
WHEN is_number (SUBSTR (row_data, 183, 5)) = 'TRUE'
THEN
TO_NUMBER (SUBSTR (row_data, 183, 5))
ELSE
NULL
END
AS ZIP_CD,
CASE
WHEN is_number (SUBSTR (row_data, 188, 4)) = 'TRUE'
THEN
TO_NUMBER (SUBSTR (row_data, 188, 4))
ELSE
NULL
END
AS ZIP_CD4,
CASE
WHEN is_number (SUBSTR (row_data, 192, 10)) = 'TRUE'
THEN
TO_NUMBER (SUBSTR (row_data, 192, 10))
ELSE
NULL
END
AS PHONE_NUM
FROM staged_FILE
WHERE row_type = 1 AND ROWNUM <= 1000;
Now off to solve how to dynamically create an associative array to stuff the data into or another way to work with the data.

In your example, you use a CASE statement. Your first expression has a DATE datatype, the second has NUMBER and the third is a VARCHAR2. From the documentation:
For a simple CASE expression, the expr and all comparison_expr values
must either have the same datatype or must all have a
numeric datatype.
Basically, you can't do this because there's no way to know at compile time what the datatype of the field_name column is.
This is not a straightforward problem to solve, since you don't know what your datatype is going to be until runtime. Even once you get a dynamic SQL statement, what sort of variable are you going to select the data into?
I think you're basically going to have to:
Using column_definitions, construct a string that contains a SQL statement appropriate for the data type in question.
Create a TYPE that contains members of all the possible resulting data types.
Use either EXECUTE IMMEDIATE or DBMS_SQL to parse and execute that string, then fetch the result into an instance of that type.
You may actually be best off not doing this via SQL at all. Instead, I would probably do the following:
Get the data type of interest from column_definitions.
Use SUBSTR to extract the region of interest from the string in staged_data.
Do something like:
.
l_token := SUBSTR (sd.source_text, cd.field_start_pos, cd.field_len);
IF l_datatype = 'DATE' THEN
l_date := TO_DATE( l_token, 'yyyy-mm-dd' );
ELSIF l_datatype = 'NUMBER' THEN
l_number := TO_NUMBER( l_token);
....
END IF;
I would not expect high performance from this sort of approach.

Related

when I run the query I get this error: ORA-06550

i have this error when i run this query.
PLS-00428: an INTO clause is expected in this SELECT statement
Query:
DECLARE
v_flag_tipo_factu varchar2(20);
BEGIN
SELECT valor_param INTO v_flag_tipo_factu FROM t_param WHERE cod_param =
'TIPOFAC';
if v_flag_tipo_factu = 'FN' THEN
SELECT
substr('00' || to_char(cta.cod_correo), -2) ||
substr('000000000' || to_char(cta.num_cta_cte), -9) ||
substr('000000000' || to_char(max(distinct fac.num_fac)),- 9)
FROM
t_acc_const cta,
t_fac fac
WHERE
cta.num_cta_cte = fac.num_cta_cte AND
fac.num_factura < 900000000
GROUP BY cta.cod_correo, cta.num_cta_cte;
end if;
END;
I expect 240,000 results like this:
13000291956000774037
06000167340000223372
13000089241000246480
13000057283000105163
06001632092003471840
13000093581000257191
12000252313001947873
06000120216000066999
06000309294001948770
13000192054000285006
If you don't like the PL/SQL rules, where the queries must be stored in variables for each step, you could use a query like this which should return the same
with v as (
SELECT valor_param FROM t_param WHERE cod_param = 'TIPOFAC'
)
SELECT
substr('00' || to_char(cta.cod_correo), -2) ||
substr('000000000' || to_char(cta.num_cta_cte), -9) ||
substr('000000000' || to_char(max(distinct fac.num_fac)),- 9)
FROM
t_acc_const cta,
t_fac fac
WHERE
cta.num_cta_cte = fac.num_cta_cte AND
fac.num_factura < 900000000 AND
EXISTS (select 1 from v where valor_param = 'FN')
GROUP BY cta.cod_correo, cta.num_cta_cte;
As you were told, SELECT (the one inside the IF) lacks in then INTO clause. You'll have to declare one more local variable and use it:
DECLARE
v_flag_tipo_factu varchar2(20);
--
v_result varchar2(50); --> this
BEGIN
SELECT valor_param INTO v_flag_tipo_factu FROM t_param WHERE cod_param =
'TIPOFAC';
if v_flag_tipo_factu = 'FN' THEN
SELECT
substr('00' || to_char(cta.cod_correo), -2) ||
substr('000000000' || to_char(cta.num_cta_cte), -9) ||
substr('000000000' || to_char(max(distinct fac.num_fac)),- 9)
INTO v_result --> this
FROM
...
It'll work if that select returns a single value; otherwise, query will raise
no_data_found if there are no rows that satisfy conditions
too_many_rows. How to handle it? It depends on what you want to do ...

Requesting help converting DB2 sql to Oracle

Looking for a little help converting a query from DB2 to Oracle. I'm using an existing SQL query to add to a new report, but that report query has been written in DB2 SQL. I am trying to convert it to Oracle, and having issues with how the dates are setup.
trunc_timestamp(CASE ......
ELSE ((
CASE
WHEN
TRFRDATE IS NULL OR
TRFRTIME IS NULL
THEN
NULL
ELSE timestamp(substr(char(TRFRDATE), 1, 4) || '-' ||
substr(char(TRFRDATE), 5, 2) || '-' || substr(char(TRFRDATE), 7, 2)
|| '-' || substr(digits(TRFRTIME), 1, 2) || '.' ||
substr(digits(TRFRTIME), 3, 2) || '.' || substr(digits(TRFRTIME), 5, 2)
|| '.000000')
END) + (-1) DAY)
END, 'dd') AS "NewOrderDate",
As you can see above, I have a case statement that will be converted to a timestamp, the difficult part is the last part, where after calculating the timestamp, I need to subtract -1 day, and that's where I keep falling into one error or the other.
Just to be clear, the ELSE part of the above query is just concatenating the date and time and converting to timestamp. Any help figuring this out would be great.
EDIT: My query now is this, and im getting the following error: hour must be between 1 and 12
SELECT CASE
WHEN
CASE
WHEN
CARRSERV IN ('1DYT','1PRT','SATN')
THEN 1
WHEN
CARRSERV IN ('2DYT','SAT2')
THEN 2
ELSE 3
END = 1 AND
TO_CHAR(
CASE
WHEN TFRDATE IS NULL OR TFRTIME IS NULL THEN NULL
ELSE TO_TIMESTAMP(substr(TFRDATE, 1, 2) || '-' || substr(TFRDATE, 4, 3) || '-' ||
substr(TFRDATE, 8, 2) || ' ' || substr(TFRTIME, 1, 2) || '.' ||
substr(TFRTIME, 3, 2) || '.' || substr(TFRTIME, 5, 2))
END,'hh24:mi:ss') > TO_CHAR('17:00:00','hh24:mi:ss')
THEN
CASE
WHEN TFRDATE IS NULL OR TFRTIME IS NULL THEN NULL
ELSE TO_TIMESTAMP(substr(TFRDATE, 1, 2) || '-' || substr(TFRDATE, 4, 3) || '-' ||
substr(TFRDATE, 8, 2) || ' ' || substr(TFRTIME, 1, 2) || '.' ||
substr(TFRTIME, 3, 2) || '.' || substr(TFRTIME, 5, 2))
END
END AS NEW_DATE from table
If I get this right you're truncating any hour/minute/second/... portion anyway so just ignore it from the beginning.
You can simply use to_date() to convert a string into a date:
...
to_date(trfrdate, 'YYYYMMDD') - 1 AS "NewOrderDate"
...
But you should really consider to use appropriate data types and strings types aren't appropriate for date/times, date/time types are. You could for example have one date column instead of trfrdate and trfrtime.
And it's a CASE expression you have there, not a statement. SQL knows no control flow statements at all.

SQL: Converting number to time

I have a database column having the following values
column (hh/mm/ss)
042336
050623
Now using sql i want to covert it to like
column
04:23:63:000
05:06:23:000
I have been trying to_date function but no success yet.
You have a string so you can use string operations to insert the additional characters:
select (substr(x, 1, 2) || ':' || substr(x, 3, 2) || ':' || substr(x, 5, 2) || ':000')
from (select '042336' as x from dual) t

SQL: parse a column delimited (shorten the script)

I created a query and it's properly working.. but I'm not yet satisfied because my code is too long, is there way that I can simplify or shorten my select statement ?
select
/*GenInfo*/
id ,name name,
replace(regexp_substr(properties, 'EntityID=[^;]*'), 'EntityID=', '') as EntityID,
replace(regexp_substr(properties, 'deployed=[^;]*'), 'deployed=', '') as deployed,
replace(regexp_substr(properties, 'type=[^;]*'), 'type=', '') as type,
replace(regexp_substr(properties, 'level=[^;]*'), 'level=', '') as "LEVEL",
replace(regexp_substr(properties, 'description=[^;]*'), 'description=', '') as description,
replace(regexp_substr(properties, 'indicator=[^;]*'), 'indicator=', '') as indicator,
replace(regexp_substr(properties, 'Agreement=[^;]*'), 'Agreement=', '') as Agreement,
replace(regexp_substr(properties, 'Activation date to charge=[^;]*'), 'Activation date to charge=', '') as Activationdatetocharge,
replace(regexp_substr(properties, 'id=[^;]*'), 'id=', '') as id,
replace(regexp_substr(properties, 'name=[^;]*'), 'name=', '') as name,
replace(regexp_substr(properties, 'currencyCode=[^;]*'), 'currencyCode=', '') as currencyCode,
replace(regexp_substr(properties, 'saleExpirationDate=[^;]*'), 'saleExpirationDate=', '') as saleExpirationDate,
replace(regexp_substr(properties, 'Product type=[^;]*'), 'Product type=', '') as Producttype,
replace(regexp_substr(properties, 'saleEffectiveDate=[^;]*'), 'saleEffectiveDate=', '') as saleEffectiveDate,
replace(regexp_substr(properties, 'Deactivation date to charge=[^;]*'), 'Deactivation date to charge=', '') as Deactivationdatetocharge
.
.
.
.
.
.
from OFFER
where name = 'PLAN 599'
;
You have to double-split and generate a query:
Split Row into "ColName=Value"-fields
Split fields into two seperate vars
Create a from-dual-query
DECLARE
inputstring VARCHAR2 (2000) := 'EntityID=1;deployed=2018-01-01;type=app';
myquery VARCHAR2 (2000) := 'SELECT'; -- result-var
tmpValue VARCHAR2 (2000);
tmpName VARCHAR2 (2000);
BEGIN
FOR i IN
(
SELECT TRIM (REGEXP_SUBSTR (inputstring, -- Split input and loop through
'[^;]+',
1,
LEVEL))
l
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT (inputstring, ';') + 1
)
LOOP
tmpName := REGEXP_SUBSTR (i.l, '[^=]+', 1, 1); -- Split column into value and name
tmpValue := REGEXP_SUBSTR (i.l, '[^=]+', 1, 2);
myquery := myquery || ' ''' || tmpValue || ''' as ' || tmpName || ','; -- build some query
END LOOP;
myQuery := SUBSTR (myQuery, 0, LENGTH (myQuery) - 1) || ' FROM DUAL'; -- complete query
DBMS_OUTPUT.put_line (myQuery); --output result
-- Result: SELECT '1' as EntityID, '2018-01-01' as deployed, 'app' as type FROM DUAL
END;
Anyway you'll get a problem with this query if you want to read out the values by code.
Perhaps you could tell us, what you want to do with the data.
I hope, you only want to convert some data from a file. If so you can add another split-loop to split rows.
Example-Function
CREATE OR REPLACE FUNCTION MakeSQL (inputstring VARCHAR2)
RETURN VARCHAR2
IS
myquery VARCHAR2 (2000); -- result-var
tmpValue VARCHAR2 (2000);
tmpName VARCHAR2 (2000);
BEGIN
FOR x IN ( SELECT TRIM (REGEXP_SUBSTR (inputstring, -- Split input and loop through
'[^' || CHR (10) || ']+',
1,
LEVEL))
tmpRow
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT (inputstring, CHR (10)) + 1)
LOOP
myquery := myquery || 'SELECT';
FOR i IN ( SELECT TRIM (REGEXP_SUBSTR (x.tmpRow, -- Split input and loop through
'[^;]+',
1,
LEVEL))
l
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT (x.tmpRow, ';') + 1)
LOOP
tmpName :=
REGEXP_SUBSTR (i.l,
'[^=]+',
1,
1); -- Split column into value and name
tmpValue :=
REGEXP_SUBSTR (i.l,
'[^=]+',
1,
2);
myquery :=
myquery || ' ''' || tmpValue || ''' as ' || tmpName || ','; -- build some query
END LOOP;
myQuery :=
SUBSTR (myQuery, 0, LENGTH (myQuery) - 1)
|| ' FROM DUAL UNION ALL'
|| CHR (10); -- complete row-select
END LOOP;
myQuery := SUBSTR (myQuery, 0, LENGTH (myQuery) - 11); -- complete query
DBMS_OUTPUT.put_line (myQuery); --output result
RETURN myQuery;
END MakeSQL;
Example call
SELECT MakeSQL('EntityID=1;deployed=2018-01-01;type=app
EntityID=2;deployed=2018-02-02;type=app') FROM DUAL;
Example-Result
SELECT '1' as EntityID, '2018-01-01' as deployed, 'app' as type FROM DUAL UNION ALL
SELECT '2' as EntityID, '2018-02-02' as deployed, 'app' as type FROM DUAL

Capitalize words in a string

I've used INITCAP to capitalize words in a string, but I've run into a small issue:
select initcap(q'[JOE'S CARD 'N' CANDY]') from dual;
It returns "Joe'S Card 'N' Candy", but I wonder if there is another way to capitalize the words so it will look like this "Joe's Card 'N' Candy" (notice the s is in lowercase)
In your place I would create a custom PL/SQL procedure of the kind:
create or replace function initcap_cust(p_input varchar2)
return varchar2
as
l_input varchar2(4000) := lower(p_input);
l_capitalize_first_letter boolean := true;
l_output varchar2(4000) := null;
l_curr_char char(1);
begin
-- here we iterate over the lowercased string characters
for i in 1..length(l_input) loop
l_curr_char := substr(l_input, i, 1);
-- if we find a space - OK, next alphabet letter should be capitalized
-- you can add here more delimiters, e.g.: l_curr_char in (' ', ',', etc)
if l_curr_char = ' ' then
l_capitalize_first_letter := true;
end if;
-- makes O'Sullivan look this way
if regexp_like(l_output, '(^| )O''$') then
l_capitalize_first_letter := true;
end if;
-- found the first letter after delimiter - OK, capitalize
if l_capitalize_first_letter and (l_curr_char between 'a' and 'z') then
l_curr_char := upper(l_curr_char);
l_capitalize_first_letter := false;
end if;
-- build the output string
l_output := l_output || l_curr_char;
end loop;
return l_output;
end;
It works in your case and similar ones. Also it can be customized depending on your needs without dealing with complex queries built using the only functions provided by Oracle out of the box.
N.B. Also there is an option to create equivalent java stored procedure, on the link provided by Edgars T. there is an example.
Adapted from this answer to use a single simpler regular expression to parse each word:
WITH names ( name ) AS (
SELECT 'FIRSTNAME O''MALLEY' FROM DUAL UNION
SELECT 'FIRST''NAME TEH''TE' FROM DUAL UNION
SELECT 'FORMAT ME BYGGER''N' FROM DUAL UNION
SELECT 'OLD MCDONALD' FROM DUAL UNION
SELECT 'EVEN OL''DER MACDONALD' FROM DUAL UNION
SELECT q'[JOE'S CARD 'N' CANDY]' FROM DUAL
)
SELECT name,
formatted_name
FROM names
MODEL
PARTITION BY (ROWNUM rn)
DIMENSION BY (0 dim)
MEASURES(name, CAST('' AS VARCHAR2(255)) word, CAST('' AS VARCHAR(255)) formatted_name)
RULES ITERATE(99) UNTIL (word[0] IS NULL)
(
word[0] = REGEXP_SUBSTR(name[0], '[^ ]+( *|$)', 1, ITERATION_NUMBER + 1),
formatted_name[0] = formatted_name[0]
-- Capitalise names starting with ', *', MC and MAC:
|| INITCAP(REGEXP_SUBSTR( word[0], '^([^'']?''|ma?c)?(.)(.*)$', 1, 1, 'i', 1 ) )
-- Capitalise the next letter of the word
|| UPPER( REGEXP_SUBSTR( word[0], '^([^'']?''|ma?c)?(.)(.*)$', 1, 1, 'i', 2 ) )
-- Lower case the rest of the word
|| LOWER( REGEXP_SUBSTR( word[0], '^([^'']?''|ma?c)?(.)(.*)$', 1, 1, 'i', 3 ) )
);
Output:
NAME FORMATTED_NAME
----------------------- ----------------------
EVEN OL'DER MACDONALD Even Ol'der MacDonald
OLD MCDONALD Old McDonald
FIRST'NAME TEH'TE First'name Teh'te
FORMAT ME BYGGER'N Format Me Bygger'n
JOE'S CARD 'N' CANDY Joe's Card 'N' Candy
FIRSTNAME O'MALLEY Firstname O'Malley