(Image by Andrew Martin from Pixabay – modifications by Sam)

When you work with SQL, you usually want to have data that is nicely arranged into columns and rows. That’s where SQL shines and where it can feel like Magic.

However, sometimes we have data that is not split into columns and rows, for example when we deal with some legacy code input or call dbms_metadata.get_ddl on a table.

(Yes, we can select most of the information dbms_metadata provides by querying the data dictionary, but there are some edge cases where we can’t, for example whether an identity column is GENERATED ALWAYS or GENERATED BY DEFAULT)

Thanks to recursive queries, we can solve nearly every programming problem in pure SQL, so let’s implement a split into rows by delimiter query.

Our input is the result of dbms_metadata.get_ddl for a table that holds information about Wookies:

create table wookies
(
  id int generated always as identity,
  name varchar2(200),
  height number(5,2)
);

select dbms_metadata.get_ddl('TABLE', 'WOOKIES')
from dual;

/* Output:

  CREATE TABLE "SITHDB"."WOOKIES" 
   (	"ID" NUMBER(*,0) GENERATED ALWAYS AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER  NOCYCLE  NOT NULL ENABLE, 
	"NAME" VARCHAR2(200), 
	"HEIGHT" NUMBER(5,2)
   ) SEGMENT CREATION DEFERRED 
  PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 
 NOCOMPRESS NOLOGGING
  TABLESPACE "USERS" 
*/

Wouldn’t it be great if we could get that DDL output in separate lines, so we can easily query for different columns?

with
  /* Let's have a config-subquery so we can
     easily change the delimiter if we want */
  config as (
    select
      chr(10) as delimiter
    from dual
  ),
  /* This is our input, the DDL of the WOOKIES table
     Of course, this could be any other input */
  ddl as (
    select
      dbms_metadata.get_ddl('TABLE', 'WOOKIES') as text
    from dual
  ),
  /* Here is where the magic happens: a recursive query
   */
  lines(start_pos, end_pos, line, line_no, text) as (
    /* We start with the first line, which also
       is our starting point and anchor for the recursion
     */
    select
      -- Start position inside the text is always 1
      1 as start_pos,
      -- End position: first occurence of delimiter
      instr(text, config.delimiter) as end_pos,
      -- Line: all text between start and end position
      substr(text, 1, instr(text, config.delimiter)-1) as line,
      -- Line-Number: obvious
      1 as line_no,
      -- We pass the text along so we don't need to
      -- select it every time from the DDL query
      text
    from ddl, config
    /* Now the recursion starts, defining all
       fruther lines. What we do here is pretty much
       the same as before, it's just more text, because
       we need to give some additional boundaries, like
       starting point for INSTR search
     */
    union all
    select
        prev.end_pos+1 as start_pos,
        instr(prev.text, config.delimiter, prev.end_pos+1) as end_pos,
        substr(prev.text, prev.end_pos+1, instr(prev.text, config.delimiter, prev.end_pos+1)-1) as line,
        prev.line_no+1,
        prev.text
    from lines prev, config
    -- We add lines as long as there are more delimiters
    where instr(prev.text, config.delimiter, prev.end_pos+1) > 0
  )
select line_no, line from lines;

Of course, you can do this with any text and any delimiter (if you have multi-char delimiters, you will need to replace the +1/-1 with +length(delimiter) etc.)

You can try it out on Oracle LiveSQL and find the whole example as always on my repository.


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.