SQL Select of Audit Trail CLOB

This is just another attempt to unpack Siebel Audit Trail CLOB value through PL/SQL. This time it is with a "classic" select statement.

It is definitely not the most performant solution but useful if you want to quickly look through a handful of S_AUDIT_ITEM records without a need to open Siebel views or install SQL packages.

Might give you an idea of how to leverage a recursive SQL for parsing purposes.

with user_select as (
    -- free form S_AUDIT_ITEM select should include at least ROW_ID, AUDIT_LOG, BUSCOMP_NAME, TBL_NAME columns
    select *
    from siebel.S_AUDIT_ITEM
    where AUDIT_LOG is not null and rownum <= 5
    order by row_id desc
),
-- split audit_log value into tokens
tokens (ROW_ID, LVL, AUDIT_LOG, BUSCOMP_NAME, TBL_NAME, REMAIN, TOKEN_LEN, TOKEN, NEW_POS) as (
    -- maybe it is worth fetching first token here?
    select ROW_ID, 0 LVL, AUDIT_LOG, BUSCOMP_NAME, TBL_NAME,
        AUDIT_LOG REMAIN,
        0 TOKEN_LEN,
        null TOKEN,
        1 NEW_POS
    from user_select
    union all
    select i.ROW_ID, p.LVL + 1 LVL, i.AUDIT_LOG, i.BUSCOMP_NAME, i.TBL_NAME,
        SUBSTR(p.AUDIT_LOG, p.NEW_POS) REMAIN,  -- remaining AUDIT_LOG value after cutting processed tokens (for debugging)
        TO_NUMBER(REGEXP_SUBSTR(i.AUDIT_LOG, '(\d*)\*', p.NEW_POS, 1, '', 1)) TOKEN_LEN, -- length of the following token (for debugging)
         -- token lenght comes before * and then a comes a token of that lenght
        REGEXP_SUBSTR(p.AUDIT_LOG, '\*(.{' || REGEXP_SUBSTR(i.AUDIT_LOG, '(\d*)\*', p.NEW_POS, 1, '', 1) || '})', p.NEW_POS, 1, '', 1) TOKEN,
        -- position at which starts next token length (current position + token lenght + lenght of token lenght + 1(* sign))
        p.NEW_POS + TO_NUMBER(REGEXP_SUBSTR(i.AUDIT_LOG, '(\d*)\*', p.NEW_POS, 1, '', 1)) + length(REGEXP_SUBSTR(i.AUDIT_LOG, '(\d*)\*', p.NEW_POS, 1, '', 1)) + 1 NEW_POS
    from user_select i
        join tokens p on p.ROW_ID = i.ROW_ID and p.NEW_POS < length(i.AUDIT_LOG) -- recursive join until we reach the end of AUDIT_LOG value
),
-- group tokens into C,N,O,J,L,K arrays and number rows
blocks (ROW_ID, LVL, AUDIT_LOG, BUSCOMP_NAME, TBL_NAME, TOKEN, TYPE, ROWCNT, NUM) as (
    select ROW_ID, LVL, AUDIT_LOG, BUSCOMP_NAME, TBL_NAME, TOKEN, 
        TO_CHAR(SUBSTR(TOKEN, 1, 1)) TYPE, -- first token indicates the meaning of following tokens (C, N, O, J, L, K)
        TO_NUMBER(SUBSTR(TOKEN, 2)) ROWCNT, -- and a count of following tokens
        0 NUM -- mark first(technical) token to filter it out later
    from tokens
    where lvl = 1
    union all
    select t.ROW_ID, t.LVL, t.AUDIT_LOG, t.BUSCOMP_NAME, t.TBL_NAME, t.TOKEN, 
        CASE WHEN p.ROWCNT = p.NUM  -- fetch next technical token after reached the end of token array
            THEN TO_CHAR(SUBSTR(t.TOKEN, 1, 1))
            ELSE p.TYPE
        END TYPE,
        CASE WHEN p.ROWCNT = p.NUM
            THEN TO_NUMBER(SUBSTR(t.TOKEN, 2))
            ELSE p.ROWCNT
        END ROWCNT,
        CASE WHEN p.ROWCNT = p.NUM -- also reset a row number
            THEN 0
            ELSE p.NUM + 1
        END NUM
    from tokens t
        join blocks p on t.row_id = p.row_id and t.lvl = p.lvl + 1
),
-- combine arrays into records and map columns into fields
recs as (
    -- J(oin) entries are recorded with field names
    select j.*, to_char(j.token) FIELD_NAME, o.token OLD_VAL, n.token NEW_VAL
    from blocks j
        join blocks n on n.row_id = j.row_id and n.num = j.num and n.type = 'L'
        join blocks o on o.row_id = j.row_id and o.num = j.num and o.type = 'K'
    where j.num > 0 and j.type = 'J'
    union all
    -- C(olumn) entries need to be mapped to field names through [Administration - Audit Trail]
    select c.*, NVL(a.field_name, to_char(c.token)) FIELD_NAME, o.token OLD_VAL, n.token NEW_VAL
    from blocks c
        join blocks n on n.row_id = c.row_id and n.num = c.num and n.type = 'N'
        join blocks o on o.row_id = c.row_id and o.num = c.num and o.type = 'O'
        left join (
            select b.buscomp_name, f.tbl_name, f.col_name, f.field_name,
                ROW_NUMBER() OVER (PARTITION BY b.buscomp_name, f.tbl_name, f.col_name ORDER BY f.created desc) row_num
            from siebel.S_AUDIT_BUSCOMP b
                join siebel.S_AUDIT_FIELD f on f.audit_bc_id = b.row_id
        ) a on a.col_name = to_char(c.token) and a.buscomp_name = c.buscomp_name and a.tbl_name = c.tbl_name and row_num = 1 -- fetching only last created field entry
    where c.num > 0 and c.type = 'C'
)
select ROW_ID, NUM, FIELD_NAME, NEW_VAL, OLD_VAL 
from recs 
    -- here you can join original S_AUDIT_LOG records by ROW_ID if you need more info
order by ROW_ID desc, TYPE desc, NUM asc;

And it is also on GitHub.