Building a PL/SQL Code Parser (using PL/SQL), Part 3

From the Database-Geek.

Continuing with the parser, begun week in PL/SQL Parser Part 1 and PL/SQL Parser Part 2, today I am going to modify the code to account for keywords, operators and data. By data, I don’t mean strings. I mean anything not a keyword, not a comment and not an operator. Data may be a quoted string (which we accounted for in Part 2), but it is also non-language functions and procedures. If you call a user defined procedure, that procedure call is considered data (at least it is here, for now).

In the code presented below, I have done several things. First, I have added new key values and modified the lookup function, is_a_break, to handle those. Second I have add a new procedure to handle working_strings that are full keywords or data (identified by break keys). I also added a new print proc to replace all of my call to DBMS_OUTPUT. The print proc gives me some extra control over output. Lastly, I have added code to the main case statement to account for the new conditions that I am checking for.

New Keys

The new keys I added are to accommodate keywords and operators. I also added a single char break structure. I know that if I hit certain characters, I am starting some kind of a break. It may be a transition to an operator or to a new keyword, but it will be some kind of a transition. The new keys are:

  -- Strings identified as PL/SQL keywords
  v_keywords a_keys  := a_keys('BEGIN', 'PROCEDURE', 'FUNCTION', 'DECLARE', 
                               'SELECT', 'FROM', 'TYPE', 'IS', 'TABLE', 'OF', 
                               'VARCHAR2', 'AS', 'PACKAGE', 'BODY', 'LOOP', 
                               'RETURN', 'IN', 'FOR', '..', '%', 
                               'END', 'EXCEPTION', 'WHEN', 'IF',  
                               'THEN', '%TYPE', 'CREATE', 'PROCEDURE');

  -- Strings 3Identified as PL/SQL operators (things that take operands)
  v_operator a_keys := a_keys(':=', '=', '-', '+', '/', '*', '<>', '<', '>', 
                               '<=', '>=', '(', ')', ',', '||', '!=', '.', ';',
                               '||');

  -- chars that potentially cause a logic or command break
  v_break a_keys     := a_keys(':', '=', '-', '+', '/', '*', '<', '>', '(', ')', 
                               ' ', ',', '|', '!', '.', ';', ' ', chr(10), chr(13));

  -- Operators and keywords
  v_full_list a_keys;

Notice that v_keywords is not the full list of keywords available in PL/SQL. For the purposes of development, there is no reason to include everything. Also, in the real world, I will end up putting this in a table to enable easy modifications to the list. The same may be true for all of the character lists. In that case, I could easily change my lookup function and not have to worry about changes to the bulk of the code.

WORKING_DATA

I have also added a new proc to handle working data writes. It’s easy to make a call out such as:

        -- Deal with working_data as a whole
        handle_working_data(
          v_working_data,
          v_char,
          v_char_peek,
          l_operator );
          

Whenever I need to write out the full working data string. This is just the first step of me making this code more modular. Right now, this is some ugly code. This is usually how I develop (when I don’t sit down and write out a detailed design spec). I write what I want the code to do, and then I refactor it to clean it up and make it more modular.

  PROCEDURE handle_working_data(
    v_working_data IN OUT NOCOPY VARCHAR2,
    v_char IN OUT NOCOPY VARCHAR2,
    v_char_peek IN OUT NOCOPY VARCHAR2,
    l_operator IN OUT NOCOPY BOOLEAN )
  IS
  BEGIN
  
        CASE
        
        -- If we have a full keyword or operator and we are in the midst of 
        -- an operator
        WHEN is_a_break(v_working_data, 'FULL') AND l_operator THEN
        
          -- It will *probably* be an operator
          IF is_a_break(v_working_data, 'KEY') THEN
            print('Keyword: ', v_working_data );
          ELSE
            print('Operator: ', v_working_data  );
          END IF;
          v_working_data := NULL;
          l_operator    := FALSE;
 
        -- If we find a FULL match (operators and keywords) and the next char is 
        -- an operator, consider it data
        -- Can drop?
        WHEN is_a_break(v_working_data, 'FULL') AND is_a_break(v_char_peek, 'OPER') 
        THEN    
          print('Data: ',  v_working_data);
          --print(' Data: "' || v_working_data || '"');
          v_working_data := NULL;
          l_operator := FALSE;

        -- If we have a FULL match and the next char is a break it is probably a keyword
        WHEN is_a_break(v_working_data, 'FULL') AND is_a_break(v_char_peek, 'BREAK') THEN
          print('Keyword: ', v_working_data );
          --print('pDelimiter( ' || v_char || ' )');
          v_working_data := NULL;
          l_operator := FALSE;

        ELSE
          --print('Null:', v_working_data || ':' || v_char || ':' || v_char_peek);
          -- just fall thru and do nothing
          NULL;
        END CASE;
  END;

Notice the one when statement that, in the comments, I say “Can drop?” This is another “feature” of my development. As I code, I write things that might be unneeded or eventually superseded. This is one example. As I do further testing, I may find that I do need it and I will keep it. Otherwise, at this point, I plan to drop that statement before going “live” with this code.

Print Proc

The print proc is pretty self explanatory. While this print is strictly for debugging, it is still useful and may find some life in the final product.

  PROCEDURE print( p_type IN VARCHAR2,
                   p_string IN VARCHAR2 )
  IS
  BEGIN
  
    -- if the input string is empty, ignore it
    -- the regexp below is just removing line feeds
    -- line feeds aren't needed at this point
    IF length(ltrim(p_string)) > 0
    THEN
       DBMS_OUTPUT.PUT_LINE(REGEXP_REPLACE(
         p_type || 
          p_string,
          '[' || chr(10) || '*|' || chr(13) || '*]',
          ''));
    END IF;
  END;

New Char Handling

The new char handling code works much the same way that the string and comments code works. Test the current character and the current state (by state I mean: am I in a comment? am I in an operator, etc). I also peek into the next char to see what is coming.

      -- Begin getting keywords and "data"
      WHEN NOT l_quotes AND NOT l_comment
          AND (is_a_break(v_char, 'BREAK') OR is_a_break(v_char, 'KEY') )
  
      THEN
        CASE  
        
        -- If the break char is a full operator and the text is not
        -- already in an operator mode
        WHEN is_a_break(v_char, 'OPER') AND NOT l_operator THEN

          -- Working data will either be a keyword or data
          IF is_a_break(v_working_data, 'KEY') THEN
            print('Keyword: ', v_working_data );
          ELSE
            print('Data: ', v_working_data);
          END IF;
          
          v_working_data := NULL;
          l_operator := FALSE;

          print('Operator: ', v_char );
          
        -- If we are not working on an operator and hit a break key  
        WHEN is_a_break(v_char, 'BREAK') AND NOT l_operator
        THEN
        
          -- We are breaking so working data will either 
          -- be a keyword or data
          IF is_a_break(v_working_data, 'KEY') THEN
            print('Keyword: ', v_working_data );
          ELSE
            print('Data: ', v_working_data);
          END IF;
          
          v_working_data := NULL;
          l_operator := FALSE;

       -- This when will probably be removed when cleaning up the code
       WHEN is_a_break(v_char_peek, 'BREAK') AND NOT l_operator
        THEN
        
          IF is_a_break(v_working_data, 'KEY') THEN
            print('1Keyword: ', v_working_data );
            v_working_data := NULL;
          ELSE
            print('1Data: ', v_working_data);
            v_working_data := NULL;
          END IF;
         
          IF is_a_break(v_char, 'OPER')
          THEN
            print('1Delimiter: ', v_char);
            v_working_data := NULL;
             
          ELSE
            v_working_data := v_working_data || v_char;
            l_operator    := TRUE;
          END IF;
 
        -- If we hit a break and are in an operator  
        -- Are there any 3 character operators?
        WHEN (is_a_break(v_char_peek, 'BREAK')  AND l_operator)
        THEN
          -- If the full monty is an operator then print it
          IF is_a_break(v_working_data || v_char || v_char_peek, 'OPER')
          THEN
            l_bookem_dano := TRUE;
          ELSE
            v_working_data := v_working_data || v_char;
          
          END IF;  

        ELSE  -- Default for character handling
        
          IF is_a_break(v_working_data, 'KEY') THEN
            print('Keyword: ', v_working_data );
          ELSE
            print('Data: ', v_working_data);
          END IF;
          v_working_data := NULL;
          l_operator := FALSE;
        END CASE;
        
        -- Deal with working_data as a whole
        handle_working_data(
          v_working_data,
          v_char,
          v_char_peek,
          l_operator );

I end the char handling code with a call to the handle_working_data proc to print out any keywords or data that may be complete. I have an additional when clause that might get dropped and I have some notes to myself.

And finally, if we run the code using the same sample proc as in the last entry, here are our new results:

Input: CREATE OR REPLACE PROCEDURE YADA AS  / THIS IS A COMMENT /   BEGIN
DBMS_OUTPUT.PUT_LINE('STRING WITH A SPACE'    'STRING WITH EMB''EDD''''ED
QUOTES');   -- SECOND_FUNC('TEST'); END;
Keyword: CREATE
Data: OR
Data: REPLACE
Keyword: PROCEDURE
Data: YADA
Keyword: AS
Comment: / THIS IS A COMMENT /
Keyword: BEGIN
Data: DBMS_OUTPUT
Operator: .
Data: PUT_LINE
Operator: (
Data: 'STRING WITH A SPACE''
Data: 'STRING WITH EMB''''EDD''''''''ED QUOTES''
Operator: )
Operator: ;
Comment: -- SECOND_FUNC('TEST');
Keyword: END
Operator: ;

Now, as I have said, this is some ugly code and will be refactored. I plan to do some of that for Part 4. Rather than add a lot of new functionality, I want part 4 to modularize the code and maybe clean up some of the unneeded when statements.

Here is the full listing for Part 3:

create or replace
PACKAGE BODY lrc_plsql_parser
AS  

  TYPE a_keys IS TABLE OF VARCHAR2(32000);

  -- Strings identified as End Of Line
  v_EOL a_keys := a_keys(chr(10), chr(13));

  v_NO_BREAK a_keys := a_keys('', chr(10), chr(13));

  -- Strings identified as pl/sql quotes
  v_quotes a_keys := a_keys('''');

  -- Strings identified as multi-line comments
  v_MLC a_keys := a_keys('/*');

  -- Strings identified as end of multi-line comments
  v_EMLC a_keys := a_keys('*/');

  -- Strings identified as to-EOL comments
  v_EOLC a_keys := a_keys('--');

  -- Strings identified as PL/SQL keywords
  v_keywords a_keys  := a_keys('BEGIN', 'PROCEDURE', 'FUNCTION', 'DECLARE', 
                               'SELECT', 'FROM', 'TYPE', 'IS', 'TABLE', 'OF', 
                               'VARCHAR2', 'AS', 'PACKAGE', 'BODY', 'LOOP', 
                               'RETURN', 'IN', 'FOR', '..', '%', 
                               'END', 'EXCEPTION', 'WHEN', 'IF',  
                               'THEN', '%TYPE', 'CREATE', 'PROCEDURE');

  -- Strings 3Identified as PL/SQL operators (things that take operands)
  v_operator a_keys := a_keys(':=', '=', '-', '+', '/', '*', '<>', '<', '>', 
                               '<=', '>=', '(', ')', ',', '||', '!=', '.', ';',
                               '||');

  -- chars that potentially cause a logic or command break
  v_break a_keys     := a_keys(':', '=', '-', '+', '/', '*', '<', '>', '(', ')', 
                               ' ', ',', '|', '!', '.', ';', ' ', chr(10), chr(13));

  -- Operators and keywords
  v_full_list a_keys;

  -- All starting comments
  v_start_comment a_keys;

  -- Function to test for break conditions
  FUNCTION is_a_break
  (
    p_string     IN VARCHAR2,
    p_break_type IN VARCHAR2 )
  RETURN BOOLEAN;

  -- Display text
  PROCEDURE print( p_type IN VARCHAR2,
                   p_string IN VARCHAR2 );
                   
  -- deal with full_strings                 
  PROCEDURE handle_working_data(
    v_working_data IN OUT NOCOPY VARCHAR2,
    v_char IN OUT NOCOPY VARCHAR2,
    v_char_peek IN OUT NOCOPY VARCHAR2,
    l_operator IN OUT NOCOPY BOOLEAN );
    

  -- Main line parser
  -- This has really turned into the body parser, not the line parser
  FUNCTION parse_line
  (
    p_string IN VARCHAR2 )
  RETURN VARCHAR2
  IS
    -- Dummy return value, not currently used
    v_string VARCHAR2(32000);
    -- Working string, holds intermediate results
    v_working_data VARCHAR2(32000);
    -- Easier to work on upper case
    v_upper_p      VARCHAR2(32000) := UPPER(p_string) || chr(10);
    -- Current character in buffer
    v_char         VARCHAR2(1);
    -- Next character in buffer
    v_char_peek    VARCHAR2(1);
    -- Are we currently working on a string
    l_quotes    BOOLEAN := FALSE;
    -- Are we currently working on a string
    l_comment    BOOLEAN := FALSE;
    -- Are we currently working on a string
    l_mlc    BOOLEAN := FALSE;
    -- How many quote chars have been found
    l_quote_count INTEGER := 0;
    -- Boolean to let us know to write a comment
    l_bookem_dano BOOLEAN := FALSE;
    -- In the midst of an operator
    l_operator    BOOLEAN := FALSE;
  BEGIN

    -- Display the input
    print( 'Input: ', v_upper_p );

    -- Loop through the string line by line
    FOR i IN 1..length(v_upper_p)
    LOOP
      v_char      := SUBSTR(v_upper_p, i, 1);
      v_char_peek := SUBSTR(v_upper_p, i+1, 1);

          CASE
      -- When the current character is a COMMENT
            WHEN is_a_break(v_char || v_char_peek, 'COMMENT') AND
           NOT l_comment
                THEN
        l_comment := TRUE;
        --print( 'Starting comment: ' || v_char || v_char_peek );
        IF is_a_break(v_char || v_char_peek, 'MLCOMMENT')
        THEN
          l_mlc := TRUE;
        END IF;  
        v_working_data := v_working_data || v_char;

      -- When the current and peek characters are the 
      -- End Of Multi-line Comment characters
            WHEN is_a_break(v_char || v_char_peek, 'EMLCOMMENT') AND
           l_comment AND
           l_mlc
                THEN
        v_working_data := v_working_data || v_char;
        l_bookem_dano := TRUE;

     -- Unconditionally Print
     WHEN l_bookem_dano
     THEN
        v_working_data := v_working_data || v_char;
        print('Comment: ', v_working_data );
        l_comment := FALSE;
        l_mlc := FALSE;
        l_bookem_dano := FALSE;
        v_working_data := NULL;
        l_operator := FALSE;
     
      -- When the current character is EOL and a comment and not multi line
            WHEN is_a_break(v_char, 'EOL') AND
           l_comment AND
           NOT l_mlc
                THEN
        print('Comment: ', v_working_data );
        l_comment := FALSE;
        v_working_data := NULL;
 
      -- When the current character is a quote
            WHEN is_a_break(v_char, 'QUOTE') AND
           NOT l_comment
                THEN
         v_working_data := v_working_data || v_char;
          
        CASE
        -- If we are in the midst of a string and the next char is also a quote
              WHEN l_quotes AND
             is_a_break(v_char_peek, 'QUOTE')
        THEN
          -- Continue getting data
          v_working_data := v_working_data || v_char;
          -- Undo the nested quote count
          l_quote_count := l_quote_count - 1;

        -- If we are in the midst of a string and the next char is NOT a quote
        -- AND the number of quotes so far is not even (this one would make it even)
            WHEN l_quotes AND
           NOT is_a_break(v_char_peek, 'QUOTE')
           AND mod(l_quote_count,2) != 0
        THEN

          -- Continue getting data
          v_working_data := v_working_data || v_char;
          -- Write out a string
          print('Data: ', v_working_data );
          -- Clear the working buffer
          v_working_data := NULL;
          -- Turn off quote mode
          l_quotes := FALSE;
          -- Reset quote count
          l_quote_count := 0;

        -- If we are in the midst of a string and the next char is NOT a quote
        -- AND the number of quotes so far is even (this one would make it odd)
              WHEN l_quotes AND
           NOT is_a_break(v_char_peek, 'QUOTE')
           AND mod(l_quote_count,2) = 0
        THEN

          -- Undo the nested quote count
          l_quote_count := l_quote_count - 1;
          -- Continue getting data
          v_working_data := v_working_data || v_char;
        WHEN NOT l_quotes AND 
             NOT l_comment
        THEN

          -- Increment quote count
          l_quote_count := l_quote_count + 1;
          -- Start up quote mode
          l_quotes := TRUE;
          -- Start the working string
          v_working_data := v_char;

        ELSE

          -- Continue getting data
          v_working_data := v_working_data || v_char;
        END CASE;

      -- Begin getting keywords and "data"
      WHEN NOT l_quotes AND NOT l_comment
          AND (is_a_break(v_char, 'BREAK') OR is_a_break(v_char, 'KEY') )
  
      THEN
        CASE  
        
        -- If the break char is a full operator and the text is not
        -- already in an operator mode
        WHEN is_a_break(v_char, 'OPER') AND NOT l_operator THEN

          -- Working data will either be a keyword or data
          IF is_a_break(v_working_data, 'KEY') THEN
            print('Keyword: ', v_working_data );
          ELSE
            print('Data: ', v_working_data);
          END IF;
          
          v_working_data := NULL;
          l_operator := FALSE;

          print('Operator: ', v_char );
          
        -- If we are not working on an operator and hit a break key  
        WHEN is_a_break(v_char, 'BREAK') AND NOT l_operator
        THEN
        
          -- We are breaking so working data will either 
          -- be a keyword or data
          IF is_a_break(v_working_data, 'KEY') THEN
            print('Keyword: ', v_working_data );
          ELSE
            print('Data: ', v_working_data);
          END IF;
          
          v_working_data := NULL;
          l_operator := FALSE;

       -- This when will probably be removed when cleaning up the code
       WHEN is_a_break(v_char_peek, 'BREAK') AND NOT l_operator
        THEN
        
          IF is_a_break(v_working_data, 'KEY') THEN
            print('1Keyword: ', v_working_data );
            v_working_data := NULL;
          ELSE
            print('1Data: ', v_working_data);
            v_working_data := NULL;
          END IF;
         
          IF is_a_break(v_char, 'OPER')
          THEN
            print('1Delimiter: ', v_char);
            v_working_data := NULL;
             
          ELSE
            v_working_data := v_working_data || v_char;
            l_operator    := TRUE;
          END IF;
 
        -- If we hit a break and are in an operator  
        -- Are there any 3 character operators?
        WHEN (is_a_break(v_char_peek, 'BREAK')  AND l_operator)
        THEN
          -- If the full monty is an operator then print it
          IF is_a_break(v_working_data || v_char || v_char_peek, 'OPER')
          THEN
            l_bookem_dano := TRUE;
          ELSE
            v_working_data := v_working_data || v_char;
          
          END IF;  

        ELSE  -- Default for character handling
        
          IF is_a_break(v_working_data, 'KEY') THEN
            print('Keyword: ', v_working_data );
          ELSE
            print('Data: ', v_working_data);
          END IF;
          v_working_data := NULL;
          l_operator := FALSE;
        END CASE;
        
        -- Deal with working_data as a whole
        handle_working_data(
          v_working_data,
          v_char,
          v_char_peek,
          l_operator );
          
      ELSE  -- Default case, most visited piece of code
        -- Continue getting data
        v_working_data := v_working_data || v_char;

      END CASE;

    END LOOP;

    RETURN v_string;

  END;


  FUNCTION is_a_break
  (
    p_string     IN VARCHAR2,
    p_break_type IN VARCHAR2 )
  RETURN BOOLEAN
  AS
  BEGIN
   CASE
    WHEN p_break_type = 'COMMENT' THEN
      FOR i          IN v_start_comment.FIRST..v_start_comment.LAST
       LOOP
        IF v_start_comment(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
    WHEN p_break_type = 'MLCOMMENT' THEN
      FOR i          IN v_mlc.FIRST..v_mlc.LAST
       LOOP
        IF v_mlc(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
    WHEN p_break_type = 'EMLCOMMENT' THEN
      FOR i          IN v_emlc.FIRST..v_emlc.LAST
       LOOP
        IF v_emlc(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
    WHEN p_break_type = 'QUOTE' THEN
      FOR i          IN v_quotes.FIRST..v_quotes.LAST
       LOOP
        IF v_quotes(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
    WHEN p_break_type = 'EOL' THEN
      FOR i          IN v_EOL.FIRST..v_EOL.LAST
       LOOP
        IF v_EOL(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
   WHEN p_break_type = 'FULL' THEN
      FOR i          IN v_full_list.FIRST..v_full_list.LAST
      LOOP
        IF v_full_list(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
    WHEN p_break_type = 'OPER' THEN
      FOR i          IN v_operator.FIRST..v_operator.LAST
      LOOP
        IF v_operator(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
    WHEN p_break_type = 'KEY' THEN
      FOR i          IN v_keywords.FIRST..v_keywords.LAST
      LOOP
        IF v_keywords(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
    WHEN p_break_type = 'BREAK' THEN
      FOR i          IN v_break.FIRST..v_break.LAST
       LOOP
        IF v_break(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
    WHEN p_break_type = 'NOBREAK' THEN
      FOR i          IN v_no_break.FIRST..v_no_break.LAST
       LOOP
        IF v_no_break(i) = p_string THEN
          RETURN TRUE;
        END IF;
      END LOOP;
    ELSE
      NULL;
    END CASE;
    RETURN FALSE;
  END is_a_break;
  
  
  PROCEDURE handle_working_data(
    v_working_data IN OUT NOCOPY VARCHAR2,
    v_char IN OUT NOCOPY VARCHAR2,
    v_char_peek IN OUT NOCOPY VARCHAR2,
    l_operator IN OUT NOCOPY BOOLEAN )
  IS
  BEGIN
  
        CASE
        
        -- If we have a full keyword or operator and we are in the midst of 
        -- an oeprator
        WHEN is_a_break(v_working_data, 'FULL') AND l_operator THEN
        
          -- It will *probably* be an operator
          IF is_a_break(v_working_data, 'KEY') THEN
            print('Keyword: ', v_working_data );
          ELSE
            print('Operator: ', v_working_data  );
          END IF;
          v_working_data := NULL;
          l_operator    := FALSE;
 
        -- If we find a FULL match (operators and keywords) and the next char is 
        -- an operator, consider it data
        -- Can drop?
        WHEN is_a_break(v_working_data, 'FULL') AND is_a_break(v_char_peek, 'OPER') 
        THEN    
          print('Data: ',  v_working_data);
          --print(' Data: "' || v_working_data || '"');
          v_working_data := NULL;
          l_operator := FALSE;

        -- If we have a FULL match and the next char is a break it is probably a keyword
        WHEN is_a_break(v_working_data, 'FULL') AND is_a_break(v_char_peek, 'BREAK') THEN
          print('Keyword: ', v_working_data );
          --print('pDelimiter( ' || v_char || ' )');
          v_working_data := NULL;
          l_operator := FALSE;

        ELSE
          --print('Null:', v_working_data || ':' || v_char || ':' || v_char_peek);
          -- just fall thru and do nothing
          NULL;
        END CASE;
  END;

  PROCEDURE print( p_type IN VARCHAR2,
                   p_string IN VARCHAR2 )
  IS
  BEGIN
  
    -- if the input string is empty, ignore it
    -- the regexp below is just removing line feeds
    -- line feeds aren't needed at this point
    IF length(ltrim(p_string)) > 0
    THEN
       DBMS_OUTPUT.PUT_LINE(REGEXP_REPLACE(
         p_type || 
          p_string,
          '[' || chr(10) || '*|' || chr(13) || '*]',
          ''));
    END IF;
  END;

BEGIN

  -- make the unions
  
  v_start_comment := v_mlc 
                 MULTISET UNION DISTINCT 
                 v_eolc;
                 
  v_full_list := v_operator 
                 MULTISET UNION DISTINCT 
                 v_keywords;
                 

END lrc_plsql_parser;
/

sho err

Take care,

LewisC

Technorati : , , , ,

You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.

1 Comment »

 
  • Ted Zuschlag says:

    12 months after you wrote this article, I discover it as a mini-goldmine! (I need to scan thousands of lines of pl/sql. This is a step in building an “enterprise data-dictionary” for Oregon University system.) The need to parse plsql-code is infrequent, like once every 3-5 years, but I am very grateful that you jumped started me–LewisC– with this article. Thank you! T. Zuschlag, Oregon State U.