MacLochlainns Weblog

Michael McLaughlin's Technical Blog

Site Admin

Archive for the ‘Oracle 19c’ Category

Comparative Techniques

without comments

This post is designed to illustrate PL/SQL code with techniques employed before Oracle Database 12c with manual sequence references rather than auto generated IDs. The trick is small but should be made with older code. It impacts creation statement, conditional drop statements, and modification to stored procedures that manage transactions across two or more tables.

The key to the difference is in the table creation. Prior to Oracle Database 12c, there were no automatic sequences that you could assign to table definitions. Tables were created without sequences, and sequences were created independently. Likewise, there was no “IF EXISTS” clause for a DROP statement, which meant conditional drops were required as anonymous blocks.

Old tables and sequences from the Oracle Database 11g would use the following definitions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* Conditionally drop grandma table and grandma_s sequence. */
BEGIN
  FOR i IN (SELECT   object_name
            ,        object_type
            FROM     user_objects
            WHERE    object_name IN ('TWEETIE_BIRD','TWEETIE_BIRD_SEQ','GRANDMA','GRANDMA_SEQ')
            ORDER BY 1 DESC, 2) LOOP
    IF i.object_type = 'TABLE' THEN
      /* Use the cascade constraints to drop the dependent constraint. */
      EXECUTE IMMEDIATE 'DROP TABLE '||i.object_name||' CASCADE CONSTRAINTS';
    ELSE
      EXECUTE IMMEDIATE 'DROP SEQUENCE '||i.object_name;
    END IF;
  END LOOP;
END;
/
 
/* Create the table. */
CREATE TABLE GRANDMA
( grandma_id     NUMBER       CONSTRAINT grandma_nn1 NOT NULL
, grandma_house  VARCHAR2(30) CONSTRAINT grandma_nn2 NOT NULL
, CONSTRAINT grandma_pk       PRIMARY KEY (grandma_id)
);
 
/* Create the sequence. */
CREATE SEQUENCE grandma_seq;
 
/* Create the table with primary and foreign key out-of-line constraints. */
CREATE TABLE TWEETIE_BIRD
( tweetie_bird_id     NUMBER        CONSTRAINT tweetie_bird_nn1 NOT NULL
, tweetie_bird_house  VARCHAR2(30)  CONSTRAINT tweetie_bird_nn2 NOT NULL
, grandma_id          NUMBER        CONSTRAINT tweetie_bird_nn3 NOT NULL
, CONSTRAINT tweetie_bird_pk        PRIMARY KEY (tweetie_bird_id)
, CONSTRAINT tweetie_bird_fk        FOREIGN KEY (grandma_id)
  REFERENCES GRANDMA (GRANDMA_ID)
);
 
/* Create sequence. */
CREATE SEQUENCE tweetie_bird_seq;

The key to the differences in Oracle Database 12c are in the table creation, and then in the INSERT statement because you can exclude the surrogate key column. A surrogate key column is also known as an ID column or sequence identified column.

New tables and sequences from the Oracle Database 12c forward use the following definitions to manage sequences:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Conditionally drop grandma table and grandma_s sequence. */
DROP TABLE IF EXISTS tweetie_bird;
DROP TABLE IF EXISTS grandma;
 
/* Create the table. */
CREATE TABLE GRANDMA
( grandma_id     NUMBER        DEFAULT grandma_seq.NEXTVAL
, grandma_house  VARCHAR2(30)  CONSTRAINT grandma_nn2 NOT NULL
, CONSTRAINT grandma_pk        PRIMARY KEY (grandma_id)
);
 
/* Create the table with primary and foreign key out-of-line constraints. */
CREATE TABLE TWEETIE_BIRD
( tweetie_bird_id     NUMBER        DEFAULT tweetie_bird_seq.NEXTVAL
, tweetie_bird_house  VARCHAR2(30)  CONSTRAINT tweetie_bird_nn1 NOT NULL
, grandma_id          NUMBER        DEFAULT grandma_seq.CURRVAL
, CONSTRAINT tweetie_bird_pk        PRIMARY KEY (tweetie_bird_id)
, CONSTRAINT tweetie_bird_fk        FOREIGN KEY (grandma_id)
  REFERENCES GRANDMA (GRANDMA_ID)
);

This is the pattern that I setup to explain the new default surrogate primary and foreign keys when I wrote the Oracle Database 12c PL/SQL Programming book (pp. 20-22). Unfortunately, it was just a simple example and fits this use case:

  • You write one create_something procedure or function, also known interchangeably as a method, to insert initial records; and
  • You write one add_something method add a subsequent dependent record; and

This is a common practice by many but it creates unnecessary code that you need to maintain, which translates to accumulated technical debt throughout a design. The technical debt grows by a factor of two when you want to overload behaviors for security features or batch processing.

A better solution is to write one method that accommodates both an creates a new record and adds dependent records. The warner_brother procedure will do that later in this post. To prepare for the warner_brother procedure the dependent tweetie_bird table needs to change.

The default of the .currval of a “parent” table’s sequence value is no longer guaranteed in an independent action. The create and add methods now share the same method, and inspection of an existing independent record is necessary for both the create and add methods.

The new tweetie_bird table looks like:

/* Create the table with primary and foreign key out-of-line constraints. */
CREATE TABLE TWEETIE_BIRD
( tweetie_bird_id     NUMBER        DEFAULT tweetie_bird_seq.NEXTVAL
, tweetie_bird_house  VARCHAR2(30)  CONSTRAINT tweetie_bird_nn1 NOT NULL
, grandma_id          NUMBER        CONSTRAINT tweetie_bird_nn2 NOT NULL
, CONSTRAINT tweetie_bird_pk        PRIMARY KEY (tweetie_bird_id)
, CONSTRAINT tweetie_bird_fk        FOREIGN KEY (grandma_id)
  REFERENCES GRANDMA (GRANDMA_ID)
);

The next part of the post discusses how to write the create and add procedure in pre-12c and 12c forward implementations. Clearly, the automatic generated sequence values are the best approach when you use user-defined names for them as shown earlier.

The warner_brother procedure models the idea that any grandma may have one or more Tweetie birds, which means you must check if there’s a row in the grandma table. If there’s a row in the grandma table, you shouldn’t insert a new row in the grandma table. The internal get_grandma_id function returns:

  • A zero if there is no existing row in the grandma table. After which, the nested if-block will insert a new row into the grandma table; and then assign the grandma_seq.CURRVAL value to a local variable.
  • An existing grandma_id value, which can be used as a foreign key value in the subsequent insert statement to the tweety_bird table.

The warner_brother procedure for Oracle Database 11g is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/* Create or replace procedure warner_brother. */
CREATE OR REPLACE PROCEDURE warner_brother
  ( pv_grandma_house       VARCHAR2
  , pv_tweetie_bird_house  VARCHAR2 ) IS
 
  /* Declare a local variable for an existing grandma_id. */
  lv_grandma_id   NUMBER;
 
  FUNCTION get_grandma_id
  ( pv_grandma_house  VARCHAR2 ) RETURN NUMBER IS
 
    /* Initialized local return variable. */
    lv_retval  NUMBER := 0;  -- Default value is 0.
 
    /* A cursor that lookups up a grandma's ID by their name. */
    CURSOR find_grandma_id
    ( cv_grandma_house  VARCHAR2 ) IS
      SELECT grandma_id
      FROM   grandma
      WHERE  grandma_house = cv_grandma_house;
 
  BEGIN   
    /* Assign a grandma_id as the return value when a row exists. */
    FOR i IN find_grandma_id(pv_grandma_house) LOOP
      lv_retval := i.grandma_id;
    END LOOP;
 
    /* Return 0 when no row found and the grandma_id when a row is found. */
    RETURN lv_retval;
  END get_grandma_id;
 
BEGIN
 
  /* Set the savepoint. */
  SAVEPOINT starting;
 
  /*
   *  Identify whether a member account exists and assign it's value
   *  to a local variable.
   */
  lv_grandma_id := get_grandma_id(pv_grandma_house);
 
  /*
   *  Conditionally insert a new member account into the member table
   *  only when a member account does not exist.
   */
  IF lv_grandma_id = 0 THEN
 
    /* Insert grandma. */
    INSERT INTO grandma
    ( grandma_id
    , grandma_house )
    VALUES
    ( grandma_seq.NEXTVAL
    , pv_grandma_house );
    /* Assign grandma_seq.currval to local variable. */
    lv_grandma_id := grandma_seq.CURRVAL;
 
  END IF;
 
  /* Insert tweetie bird. */
  INSERT INTO tweetie_bird
  ( tweetie_bird_id
  , tweetie_bird_house 
  , grandma_id )
  VALUES
  ( tweetie_bird_seq.NEXTVAL
  , pv_tweetie_bird_house
  , lv_grandma_id );
 
  /* If the program gets here, both insert statements work. Commit it. */
  COMMIT;
 
EXCEPTION
  /* When anything is broken do this. */
  WHEN OTHERS THEN
    /* Until any partial results. */
    ROLLBACK TO starting;
END;
/

The warner_brother procedure for Oracle Database 12c forward simplifies somethings and complicates others. The DEFAULT for the grandma_id foreign key column in the tweetie_bird table must be overwritten with a copy of the correct primary key value. More or less, it requires the correct grandma_id column value by finding the existing row in the grandma table. The internal get_grandma_id function grabs that value while returning a value that precludes writing to the grandma table.

The warner_brother procedure for Oracle Database 12c forward shown below shows that the grandma_id column default value is overridden by a local lv_grandma_id value returned by the internal get_grandma_id function on lines 60 thru 65 below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/* Create or replace procedure warner_brother. */
CREATE OR REPLACE PROCEDURE warner_brother
  ( pv_grandma_house       VARCHAR2
  , pv_tweetie_bird_house  VARCHAR2 ) IS
 
  /* Declare a local variable for an existing grandma_id. */
  lv_grandma_id   NUMBER;
 
  FUNCTION get_grandma_id
  ( pv_grandma_house  VARCHAR2 ) RETURN NUMBER IS
 
    /* Initialized local return variable. */
    lv_retval  NUMBER := 0;  -- Default value is 0.
 
    /* A cursor that lookups up a grandma's ID by their name. */
    CURSOR find_grandma_id
    ( cv_grandma_house  VARCHAR2 ) IS
      SELECT grandma_id
      FROM   grandma
      WHERE  grandma_house = cv_grandma_house;
 
  BEGIN   
    /* Assign a grandma_id as the return value when a row exists. */
    FOR i IN find_grandma_id(pv_grandma_house) LOOP
      lv_retval := i.grandma_id;
    END LOOP;
 
    /* Return 0 when no row found and the grandma_id when a row is found. */
    RETURN lv_retval;
  END get_grandma_id;
 
BEGIN
 
  /* Set the savepoint. */
  SAVEPOINT starting;
 
  /*
   *  Identify whether a member account exists and assign it's value
   *  to a local variable.
   */
  lv_grandma_id := get_grandma_id(pv_grandma_house);
 
  /*
   *  Conditionally insert a new member account into the member table
   *  only when a member account does not exist.
   */
  IF lv_grandma_id = 0 THEN
 
    /* Insert grandma. */
    INSERT INTO grandma
    ( grandma_house )
    VALUES
    ( pv_grandma_house );
    /* Assign grandma_seq.currval to local variable. */
    lv_grandma_id := grandma_seq.CURRVAL;
 
  END IF;
 
  /* Insert tweetie bird. */
  INSERT INTO tweetie_bird
  ( tweetie_bird_house 
  , grandma_id )
  VALUES
  ( pv_tweetie_bird_house
  , lv_grandma_id );
 
  /* If the program gets here, both insert statements work. Commit it. */
  COMMIT;
 
EXCEPTION
  /* When anything is broken do this. */
  WHEN OTHERS THEN
    /* Until any partial results. */
    ROLLBACK TO starting;
END;
/

You can test either version with the following anonymous PL/SQL block:

/* Test the warner_brother procedure. */
BEGIN
  warner_brother( pv_grandma_house      => 'Yellow House'
                , pv_tweetie_bird_house => 'Cage');
  warner_brother( pv_grandma_house      => 'Yellow House'
                , pv_tweetie_bird_house => 'Tree House');
END;
/

Ultimately, we’ll use a natural join to test the integrity of primary and foreign key relationship:

/* Query results from warner_brother procedure. */
COL grandma_id          FORMAT 9999999  HEADING "Grandma|ID #"
COL grandma_house       FORMAT A20      HEADING "Grandma House"
COL tweetie_bird_id     FORMAT 9999999  HEADING "Tweetie|Bird ID"
COL tweetie_bird_house  FORMAT A20      HEADING "Tweetie Bird House"
SELECT *
FROM   grandma NATURAL JOIN tweetie_bird;

As always, I hope this helps those trying to leverage existing PL/SQL code into more modern code.

Written by maclochlainn

March 30th, 2025 at 6:58 pm

PL/SQL Table Function

without comments

Eleven years ago I had an interesting conversation about table functions in Oracle’s PL/SQL; and the fact that they’re not available in MySQL. When I explained they were available in Microsoft T-SQL User-Defined Functions (UDFs) with Microsoft’s Common Language Infrastructure (CLI). Naturally, I wrote an example for them in T-SQL. Now a reader wants an equivalent PL/SQL example.

I figured that borrowing that sample data was appropriate. This creates the conquistador table:

CREATE TABLE conquistador
( conquistador_id    NUMBER GENERATED ALWAYS AS IDENTITY
, conquistador       VARCHAR2(30)
, actual_name        VARCHAR2(30)
, nationality        VARCHAR2(30)
, lang               VARCHAR2(2));

Then, you can insert the following data:

INSERT INTO conquistador
( conquistador, actual_name, nationality, lang )
VALUES
 ('Juan de Fuca','Ioánnis Fokás','Greek','el')
,('Nicolás de Federmán','Nikolaus Federmann','German','de')
,('Sebastián Caboto','Sebastiano Caboto','Venetian','it')
,('Jorge de la Espira','Georg von Speyer','German','de')
,('Eusebio Francisco Kino','Eusebius Franz Kühn','Italian','it')
,('Wenceslao Linck','Wenceslaus Linck','Bohemian','cs')
,('Fernando Consag','Ferdinand Konšcak','Croatian','sr')
,('Américo Vespucio','Amerigo Vespucci','Italian','it')
,('Alejo García','Aleixo Garcia','Portuguese','pt');

Next, create a UDT struct(ure) and table of that structure:

/* Create type of object structure. */
CREATE OR REPLACE
  TYPE conquistador_struct IS OBJECT
  ( conquistador      VARCHAR(30)
  , actual_name       VARCHAR(30)
  , nationality       VARCHAR(30));
/
 
/* Create table of object structure. */
CREATE OR REPLACE
  TYPE conquistador_table IS TABLE OF conquistador_struct;
/

Now, we can create a table function that filters on nationality:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CREATE OR REPLACE
  FUNCTION getConquistador (pv_lang IN VARCHAR) RETURN conquistador_table IS
 
  /* Declare a return variable. */
  lv_retval  CONQUISTADOR_TABLE := conquistador_table();
 
  /* Declare a dynamic cursor. */
  CURSOR get_conquistador
  ( cv_lang  VARCHAR2 ) IS
    SELECT c.conquistador
    ,      c.actual_name
    ,      c.nationality
    FROM   conquistador c
    WHERE  c.lang = cv_lang;
 
  /* Local procedure to add to the song. */
  PROCEDURE ADD
  ( pv_input  CONQUISTADOR_STRUCT ) IS
  BEGIN
    lv_retval.EXTEND;
    lv_retval(lv_retval.COUNT) := pv_input;
  END ADD;
 
BEGIN
  /* Read through the cursor and assign to the UDT table. */
  FOR i IN get_conquistador(pv_lang) LOOP
    ADD(conquistador_struct( conquistador => i.conquistador
                           , actual_name  => i.actual_name
			   , nationality  => i.nationality ));
  END LOOP;
 
  /* Return collection. */
  RETURN lv_retval;
END;
/

You can select only the rows with German nationality, qualified by the ISO standard DE with the following query:

COL conquistador  FORMAT A21
COL actual_name   FORMAT A21
COL nationality   FORMAT A12
SELECT * FROM TABLE(getConquistador('de'));

It should return:

CONQUISTADOR	      ACTUAL_NAME	    NATIONALITY
--------------------- --------------------- ------------
Nicolás de Federmán   Nikolaus Federmann    German
Jorge de la Espira    Georg von Speyer	    German

I retested this on Oracle Database 23ai Free, and found the NLS character set fails to support the characters for Nicolás de Federmán. It returns:

CONQUISTADOR	      ACTUAL_NAME	    NATIONALITY
--------------------- --------------------- ------------
Nicol??s de Federm??n Nikolaus Federmann    German
Jorge de la Espira    Georg von Speyer	    German

This is interesting because it brought to my attention that Oracle does try to account for unreadable ASCII values based on character sets. In Google’s GO language these would be considered “Runes”.

As always, I hope these coding samples help those solving new problems.

Written by maclochlainn

March 29th, 2025 at 6:04 pm

PL/SQL Coupled Loops

without comments

A standard approach using fall through in languages that support it, like C, C++, and Java leverages a case structure. Unfortunately, in language that follow the Pascal pattern case statements act like if-else-if-else statements. In the Pascal model, you need to use coupled loops.

PL/SQL follows that Pascal, via Ada, structure. So, you must couple the performance of the inner descending loop to the maximum value of the outer ascending loop. Here’s an example of how you would code that in PL/SQL with the 12 Days of Christmas song, which has a chorus that grows with each verse.

It requires the definition of two ADTs (Attribute Data Types), a UDT (User-Defined Data Type) like a struct(ure), and a UDT table of the UDT struct. They’re all done in this code snippet.

/* Create a list of strings. */
CREATE OR REPLACE
  TYPE days IS TABLE OF VARCHAR2(8);
/
 
/* Create a list of strings. */
CREATE OR REPLACE
  TYPE song IS TABLE OF VARCHAR2(36);
/
 
/* Create a record structure. */
CREATE OR REPLACE
  TYPE lyric IS OBJECT
  ( DAY  VARCHAR2(8)
  , gift VARCHAR2(24));
/
 
/* Create a list of the record structure. */
CREATE OR REPLACE
  TYPE lyrics IS TABLE OF LYRIC;
/

The twelve_days function that will display the lyrics of the 12-Days of Christmas. It reads forward through the 12 days of Christmas and backwards through the chorus in the inner loop beginning with the current day of the outer loop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* Create a function. */
CREATE OR REPLACE
  FUNCTION twelve_days
  ( pv_days DAYS
  , pv_gifts LYRICS ) RETURN song IS
 
    /* Initialize the collection of lyrics. */
    lv_retval SONG := song();
 
    /* Local procedure to add to the song. */
    PROCEDURE ADD
    ( pv_input VARCHAR2 ) IS
    BEGIN
      lv_retval.EXTEND;
      lv_retval(lv_retval.COUNT) := pv_input;
    END ADD;
 
  BEGIN
    /* Read forward through the days. */
    FOR i IN 1..pv_days.COUNT LOOP
      ADD('On the ' || pv_days(i) || ' day of Christmas');
      ADD('my true love sent to me:');
 
      /* Read backward through the lyrics based on ascending value of the day. */
      FOR j IN REVERSE 1..i LOOP
        IF i = 1 THEN
          ADD('- '||'A'||' '||pv_gifts(j).gift);
        ELSE
          ADD('- '||pv_gifts(j).DAY||' '||pv_gifts(j).gift);
        END IF;
      END LOOP;
 
      /* A line break by verse. */
      ADD(CHR(13));
    END LOOP;
 
    /* Return the song's lyrics. */
    RETURN lv_retval;
  END;
/

The typical test case for the function in PL/SQL is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* Test the function in PL/SQL. */
SET SERVEROUTPUT ON SIZE UNLIMITED
DECLARE
  /*
  * Declare an lv_days array of an 8 character variable length string
  * and initialize it with values.
  */
  lv_days days := days('first','second','third','fourth'
                      ,'fifth','sixth','seventh','eighth'
                      ,'nineth','tenth','eleventh','twelfth');
 
  /*
  * Declare an lv_gifts array of the user-defined LYRIC data type and
  * initialize it with values.
  */
  lv_gifts lyrics := lyrics(lyric(DAY => 'and a', gift => 'Partridge in a pear tree')
                           ,lyric(DAY => 'Two', gift => 'Turtle doves')
                           ,lyric(DAY => 'Three', gift => 'French hens')
                           ,lyric(DAY => 'Four', gift => 'Calling birds')
                           ,lyric(DAY => 'Five', gift => 'Golden rings' )
                           ,lyric(DAY => 'Six', gift => 'Geese a laying')
                           ,lyric(DAY => 'Seven', gift => 'Swans a swimming')
                           ,lyric(DAY => 'Eight', gift => 'Maids a milking')
                           ,lyric(DAY => 'Nine', gift => 'Ladies dancing')
                           ,lyric(DAY => 'Ten', gift => 'Lords a leaping')
                           ,lyric(DAY => 'Eleven',gift => 'Pipers piping')
                           ,lyric(DAY => 'Twelve',gift => 'Drummers drumming'));
 
  /*
  * Declare an lv_days array of an 36 character variable length string
  * and initialize it with values.
  */
  lv_song song := song();
 
BEGIN
  /* Call the twelve_days function and assign the results to the local
  * lv_song variable.
  */
  lv_song := twelve_days(lv_days,lv_gifts);
 
  /*
  * Read the lines from the local lv_song variable and print them.
  */
  FOR i IN 1..lv_song.LAST LOOP
    dbms_output.put_line(lv_song(i));
  END LOOP;
END;
/

It displays:

On the first day of Christmas
my true love sent to me:
- A Partridge in a pear tree
 
On the second day of Christmas
my true love sent to me:
- Two Turtle doves
- and a Partridge in a pear tree
 
On the third day of Christmas
my true love sent to me:
- Three French hens
- Two Turtle doves
- and a Partridge in a pear tree
 
...
 
On the twelfth day of Christmas
my true love sent to me:
- Twelve Drummers drumming
- Eleven Pipers piping
- Ten Lords a leaping
- Nine Ladies dancing
- Eight Maids a milking
- Seven Swans a swimming
- Six Geese a laying
- Five Golden rings
- Four Calling birds
- Three French hens
- Two Turtle doves
- and a Partridge in a pear tree

As always, I hope this helps those learning how to write PL/SQL and/or code.

Written by maclochlainn

March 28th, 2025 at 7:42 pm

PL/SQL List to Struct

without comments

This post shows you how to take a list of strings and convert them into a struct(ure) of a date, number, and string. Oracle implements IDL (Interface Description Language), which means the solution requires creating:

  • An attribute data type (ADT), or collection of a standard date type, which in this case is a varchar2 (variable length string).
  • A user defined type (UDT), which is an object type with or without methods. In this case, it implements a UDT without methods.
  • A collection of the UDT object type (known in Oracle’s parlance as a table).

Here’s the data definition language (DDL) for the three required structures:

  • The ADT of strings:

    CREATE OR REPLACE
      TYPE tre IS TABLE OF VARCHAR2(100);
    /

  • The UDT struct(ure) object type:

    CREATE OR REPLACE
      TYPE struct IS OBJECT
      ( xdate    DATE
      , xnumber  NUMBER
      , xstring  VARCHAR2(20));
    /

  • The UDT structs collection or Oracle table:

    CREATE OR REPLACE
      TYPE structs IS TABLE OF struct;
    /

Next, we create a function that accepts a collection of strings and returns a record UDT, which is the struct(ure) object type. As PL/SQL code gets larger, using conditional compilation can help discover problems.

Enable conditional compilation with the following statement:

ALTER SESSION SET PLSQL_CCFLAGS = 'debug:1';

The cast_strings function is defined as:

CREATE OR REPLACE
  FUNCTION cast_strings
  ( pv_list  TRE ) RETURN struct IS
 
    /* Declare a UDT and initialize an empty struct variable. */
    lv_retval  STRUCT := struct( xdate => NULL
                               , xnumber => NULL
                               , xstring => NULL); 
 
    /* A debugger function. */	
    FUNCTION debugger
    ( pv_string  VARCHAR2 ) RETURN VARCHAR2 IS
      /* Declare return value. */
      lv_retval  VARCHAR2(60);
    BEGIN
      /* Conditional compilation evaluation. */
      $IF $$DEBUG = 1 $THEN
        lv_retval := 'Evaluating ['||pv_string||']';
      $END
 
      /* Return debug value. */
      RETURN lv_retval;
    END debugger;
 
  BEGIN  
    /* Loop through list of values to find only the numbers. */
    FOR i IN 1..pv_list.LAST LOOP
 
      /* Print debugger remark. */
      dbms_output.put_line(debugger(pv_list(i)));
 
      /* Ensure that a sparsely populated list can't fail. */
      IF pv_list.EXISTS(i) THEN
        /* Order if number evaluation before string evaluation. */
        CASE
            /* Implement WHEN clause that checks that the xnumber member is null and that
               the pv_list element contains only digits; and assign the pv_list element to
               the lv_retval's xnumber member. */
          WHEN REGEXP_LIKE(pv_list(i),'^[[:digit:]]*$') THEN
			lv_retval.xnumber := pv_list(i);
 
            /* Implement WHEN clause that checks that the xdate member is null and that
               the pv_list element is a valid date; and assign the pv_list element to
               the lv_retval's xdate member. */
          WHEN verify_date(pv_list(i)) THEN
            lv_retval.xdate := pv_list(i);
 
            /* Implement WHEN clause that checks that the xstring member is null and that
               the pv_list element contains only alphanumeric values; and assign the pv_list
               element to the lv_retval's xstring member. */
          WHEN REGEXP_LIKE(pv_list(i),'^([[:alnum:]]|[[:punct:]]|[[:space:]])*$') THEN
            lv_retval.xstring := pv_list(i);
          ELSE
            NULL;
        END CASE;
      END IF;
    END LOOP;
 
    /* Print the results. */
    RETURN lv_retval;
  END;
/

The following tests the cast_strings function:

DECLARE
  /* Define a list. */
  lv_list  TRE := tre('16-APR-2018','Day after ...','1040');
 
  /* Declare a structure. */
  lv_struct  STRUCT := struct( xdate => NULL
                             , xnumber => NULL
                             , xstring => NULL); 
BEGIN
  /* Assign a parsed value set to get a value structure. */
  lv_struct := cast_strings(lv_list);
 
  /* Print the values of the compound struct variable. */
  dbms_output.put_line('xstring ['||lv_struct.xstring||']');
  dbms_output.put_line('xdate   ['||TO_CHAR(lv_struct.xdate,'DD-MON-YYYY')||']');
  dbms_output.put_line('xnumber ['||lv_struct.xnumber||']');
END;
/

If the PLSQL_CCFLAGS is enabled for the session, the function will display these three rows:

Evaluating [16-APR-2018]
Evaluating [DAY after ...]
Evaluating [1040]

Whether the PLSQL_CCFLAGS is set or not, the test case returns the following:

xstring [DAY after ...]
xdate	[16-APR-2018]
xnumber [1040]

As always, I hope this helps those looking for new approaches.

Written by maclochlainn

March 27th, 2025 at 10:10 pm

Hidden thought SQL question

without comments

I’m always amazed at how people ask questions about skill sets. A few weeks ago, I ran into a neat Oracle SQL question that hinges on whether the individual truly understands One Phase Commit (1PC) and Two Phase Commit (2PC) operations.

It’s a simple question that tests two levels of understanding. The question is:

  • If you create table A and insert a row of data and subsequently you create table B and insert a row of data before issuing a ROLLBACK; statement, how many rows of data will you find in table A and table B?

Level 1 Understanding

Here’s the test script:

CREATE TABLE a1
( text  VARCHAR2(12) );
INSERT INTO a1 ( text ) VALUES ('Hello World!');
CREATE TABLE b1
( text  VARCHAR2(12) );
INSERT INTO b1 ( text ) VALUES ('Hello World!');
ROLLBACK;

The answer is 1 row in table A1 and no row in table B1 because the second CREATE statement issues an implicit COMMIT. However, the INSERT statement to table B1 is a 2PC and the ROLLBACK statement undoes the first phase of the INSERT statement and removes the data. If you were using a sequence value in the INSERT statement, the sequence value would be consumed because it’s not replaced by a ROLLBACK statement.

Level 2 Understanding

Here’s the test script:

CREATE TABLE a2 AS (SELECT 'Hello World!' AS text);
CREATE TABLE b2 AS (SELECT 'Hello World!' AS text);
ROLLBACK;

The answer is 1 row in table A2 and 1 row in table B2 because a CREATE statement using the AS clause subquery issues an implicit COMMIT on the data inserted from the subquery because its a 1PC transaction.

Most likely and interviewer would be looking for level one understanding but you can demonstrate mastery by sharing level two understanding. As always, I hope this helps those reading it.

Written by maclochlainn

March 21st, 2025 at 10:38 pm

Parallels + Ubuntu

without comments

Installing Parallels on my iStudio (M2) was straightforward because I let it install Windows 11. Then, when I wanted to install Ubuntu it wasn’t quite that easy. You just need to understand that you click the Parallels’ Window menu option and Control Center option.

The Control Center option provides the following dialog.

Click the + symbol to create a new virtualization and you get the following dialog; and choose the Download Ubuntu with x86_64 emulation if you want to install a Docker image that’s not ported to ARM, like the Oracle Database 19c.

It’ll then explain in this dialog that is uses Apple’s Rosetta 2 technology, which means you should be able to install an Intel architecture Docker image with Oracle Database 23c.

As always, I hope this helps those trying to sort out how to leverage a new stack.

Written by maclochlainn

October 19th, 2023 at 3:41 pm