Quantcast
Channel: Philipp Salvisberg's Blog
Viewing all articles
Browse latest Browse all 118

Limitations of PL/Scope and How to Deal with Them

$
0
0

My first car was a Renault R5 TX. The motor cooling of this car was really bad. On a hot summer day it was simply not possible to drive slowly in high traffic without overheating the engine. To cool the engine you could either stop the car, open the front lid and hope for the best or turn on the heating. I decided for the latter. It was impressive how well the heating system worked on hot days. Not very pleasant to drive uphill behind a slow Dutch caravan in the Alps, but a funny experience in retrospect. My R5 was a reliable companion for years and I loved it.

When you are aware of the limitations of PL/Scope and know how to deal with them, you will find PL/Scope a very useful tool. This post is supposed to enable you to use PL/Scope more effectively. I’m fond of PL/Scope, because it may provide very reliable insights of your static PL/SQL code. I hope your are going to use PL/Scope, even if it is required sometimes “to turn on the heating”.

PL/Scope gathers at compile time metadata about your PL/SQL code and stores it in dedicated tables. These metadata are accessible for analysis via the views dba/all/user_identfiers (since 11.1) and in dba/all/user_statements (since 12.2). If you are not familiar with PL/Scope I recommend to read Steven Feuerstein’s article Powerful Impact Analysis or have a look a the introduction chapters of the documentation. If you are fluent in German, I can recommend also Sabine Heimsath’s article Schöner Coden – PL/SQL analysieren mit PL/Scope.

The limitations covered in this post are based on Oracle Database version 12.2.0.1.170814. Most of the limitations are bugs. You may track the progress on MOS with the provided bug numbers.

1. Missing Results After NULL Statement

In the following example we analyse the procedure p2. Look at the result of the user_identifiers query on line 27-29. All three calls to procedure p1 have been detected. That’s good and correct.

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

CREATE OR REPLACE PROCEDURE p1 (in_p IN INTEGER) IS
BEGIN
   sys.dbms_output.put_line(sqrt(in_p));
END;
/

CREATE OR REPLACE PROCEDURE p2 IS
BEGIN
   p1(4);
   p1(9);
   p1(16);
END;
/

SELECT usage, type, name, line, col
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P2'
 ORDER BY line, col;

USAGE       TYPE               NAME             LINE        COL
----------- ------------------ ---------- ---------- ----------
DEFINITION  PROCEDURE          P2                  1         11
DECLARATION PROCEDURE          P2                  1         11
CALL        PROCEDURE          P1                  3          4
CALL        PROCEDURE          P1                  4          4
CALL        PROCEDURE          P1                  5          4

But when we add a NULL statement before the first call of p1, the calls after the NULL statement are not reported anymore.

CREATE OR REPLACE PROCEDURE p2 IS
BEGIN
   NULL;
   p1(4);
   p1(9);
   p1(16);
END;
/

SELECT usage, type, name, line, col
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P2'
 ORDER BY line, col;

USAGE       TYPE               NAME             LINE        COL
----------- ------------------ ---------- ---------- ----------
DECLARATION PROCEDURE          P2                  1         11
DEFINITION  PROCEDURE          P2                  1         11

This is a bug. See bug 24916492 on MOS.

I am not aware of a simple workaround. This means you have to change the code. In this case it is easy, just remove the NULL statement.

The good news is, that it affects just the basic block of the NULL statement. Other basic blocks are not affected. Here’s an example of a complete result, even if a NULL statement is used. The term “basic block” has been introduced with PL/SQL Basic Block Coverage in version 12.2. However the definition is valid for all versions of PL/SQL. I like Chris Saxon’s definition: “It’s a piece of code that either runs completely or not at all”.

CREATE OR REPLACE PROCEDURE p2 IS
BEGIN
   IF FALSE THEN
      NULL;
   END IF;
   p1(4);
   p1(9);
   p1(16);
END;
/

SELECT usage, type, name, line, col
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P2'
 ORDER BY line, col, usage_id;

USAGE       TYPE               NAME             LINE        COL
----------- ------------------ ---------- ---------- ----------
DEFINITION  PROCEDURE          P2                  1         11
DECLARATION PROCEDURE          P2                  1         11
CALL        PROCEDURE          P1                  6          4
CALL        PROCEDURE          P1                  7          4
CALL        PROCEDURE          P1                  8          4

In cases where PL/SQL requires at least one statement and the NULL statement is the only one, you should not have a problem. Just unnecessary usages of  NULL statements might cause problems. So, make sure that you do not use unnecessary NULL statements. They are noise and may lead to incomplete code analysis results.

2. Broken Usage Hierarchy

Let’s look at the usage hierarchy of the procedure p2 in example 1c. The hierarchy level is represented in the result column usage. Three leading spaces for each sub-level. The usage_id identifies a row for an object, p2 in this case. The usage_id starts with 1 and ends with 5. There are no gaps. The column usage_context_id is part of the foreign key to the parent usage_id. Oracle decided to start the hierarchy with the non-existing usage_id 0 (zero). That’s what we use in the start_with clause. The recursive query produces the same number of result rows as the non-recursive query in example 1c. That’s important, and that’s how it should be. Always.

WITH ids AS (
   SELECT usage, type, name, line, col, usage_id, usage_context_id
     FROM user_identifiers ids
    WHERE object_type = 'PROCEDURE'
      AND object_name = 'P2'
)
 SELECT lpad(' ', 3 * (level - 1)) || ids.usage AS usage,
        ids.type,
        ids.name,
        ids.line,
        ids.col,
        ids.usage_id,
        ids.usage_context_id
   FROM ids
  START WITH ids.usage_context_id = 0
CONNECT BY PRIOR ids.usage_id = ids.usage_context_id
  ORDER BY ids.line, ids.col, usage_id;

USAGE          TYPE       NAME  LINE  COL USAGE_ID USAGE_CONTEXT_ID
-------------- ---------- ----- ---- ---- -------- ----------------
DECLARATION    PROCEDURE  P2       1   11        1                0
   DEFINITION  PROCEDURE  P2       1   11        2                1
      CALL     PROCEDURE  P1       6    4        3                2
      CALL     PROCEDURE  P1       7    4        4                2
      CALL     PROCEDURE  P1       8    4        5                2

There are two reasons for broken usage hierarchies.

  1. Static SQL statements (expected behaviour)
  2. References to uncompiled synonyms (bug)

2.1. Static SQL Statements

Since version 12.2 PL/Scope covers static SQL statements in the user_statements view. Static SQL statements are missing in the user_identifiers view, probably for compatibility reasons. To get the full usage hierarchy you have to combine the views using UNION ALL like in line 49 of the next example 2.1a. Please note that the usages on line 25-29 are referring to the parent on line 41. A recursive query on user_identifiers only, would return just the lines 69-70 – a quite incomplete result set. Therefore you should think twice before applying your “old” PL/Scope version 11.1 scripts against an Oracle 12.2 database.

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

CREATE OR REPLACE FUNCTION f1 (in_val NUMBER) RETURN NUMBER IS
BEGIN
   RETURN SQRT(in_val);
END;
/

CREATE OR REPLACE PROCEDURE p3 IS
BEGIN
   UPDATE emp SET sal = sal + f1(comm);
END;
/

SELECT usage, type, name, line, col, usage_id, usage_context_id
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P3'
 ORDER BY line, col, usage_id;

USAGE          TYPE       NAME  LINE  COL USAGE_ID USAGE_CONTEXT_ID
-------------- ---------- ----- ---- ---- -------- ----------------
DECLARATION    PROCEDURE  P3       1   11        1                0
DEFINITION     PROCEDURE  P3       1   11        2                1
REFERENCE      TABLE      EMP      3   11        4                3
REFERENCE      COLUMN     SAL      3   19        6                3
REFERENCE      COLUMN     SAL      3   25        5                3
CALL           FUNCTION   F1       3   31        7                3
REFERENCE      COLUMN     COMM     3   34        8                7

7 rows selected.

SELECT text, type, sql_id, line, col, usage_id, usage_context_id
  FROM user_statements
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P3'
 ORDER BY line, col;

TEXT                                TYPE   SQL_ID        LINE  COL USAGE_ID USAGE_CONTEXT_ID
----------------------------------- ------ ------------- ---- ---- -------- ----------------
UPDATE EMP SET SAL = SAL + F1(COMM) UPDATE 8kyysdc8m75ag    3    4        3                2

WITH
   ids AS (
      SELECT usage, type, name, line, col, usage_id, usage_context_id
        FROM user_identifiers
       WHERE object_type = 'PROCEDURE'
         AND object_name = 'P3'
    UNION ALL
      SELECT 'EXECUTE' AS usage, type, sql_id AS name, line, col, usage_id, usage_context_id
       FROM user_statements
       WHERE object_type = 'PROCEDURE'
         AND object_name = 'P3'
   )
 SELECT lpad(' ', 3 * (level - 1)) || usage AS usage,
        type,
        name,
        line,
        col,
        usage_id,
        usage_context_id
   FROM ids
  START WITH usage_context_id = 0
CONNECT BY PRIOR usage_id = usage_context_id
  ORDER BY line, col, usage_id;

USAGE                 TYPE       NAME           LINE  COL USAGE_ID USAGE_CONTEXT_ID
--------------------- ---------- -------------- ---- ---- -------- ----------------
DECLARATION           PROCEDURE  P3                1   11        1                0
   DEFINITION         PROCEDURE  P3                1   11        2                1
      EXECUTE         UPDATE     8kyysdc8m75ag     3    4        3                2
         REFERENCE    TABLE      EMP               3   11        4                3
         REFERENCE    COLUMN     SAL               3   19        6                3
         REFERENCE    COLUMN     SAL               3   25        5                3
         CALL         FUNCTION   F1                3   31        7                3
            REFERENCE COLUMN     COMM              3   34        8                7

2.2 References to Uncompiled Synonyms

In the example 2.2a the procedure p4 calls procedure p1, but p1 is not compiled with PL/Scope. The usage hierarchy is intact. Please note that the result set contains also all usages of the parameter in_p.

ALTER SESSION SET plscope_settings='identifiers:none, statements:none';

CREATE OR REPLACE PROCEDURE p1 (in_p IN INTEGER) IS
BEGIN
   sys.dbms_output.put_line(sqrt(in_p));
END;
/

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

CREATE OR REPLACE PROCEDURE p4 (in_p IN INTEGER) IS
BEGIN
   p1(in_p => in_p);
END;
/

SELECT usage, type, name, line, col, usage_id, usage_context_id
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P4'
 ORDER BY line, col, usage_id;

USAGE                 TYPE       NAME           LINE  COL USAGE_ID USAGE_CONTEXT_ID
--------------------- ---------- -------------- ---- ---- -------- ----------------
DECLARATION           PROCEDURE  P4                1   11        1                0
DEFINITION            PROCEDURE  P4                1   11        2                1
DECLARATION           FORMAL IN  IN_P              1   15        3                2
REFERENCE             SUBTYPE    INTEGER           1   23        4                3
REFERENCE             FORMAL IN  IN_P              3   15        5                2

But the usage hierarchy becomes broken when we call procedure p1via an uncompiled synonym as in the example 2.2b. The highlighted result row is referencing a non-existing usage_id. In a recursive query this result row will get lost.

This is a bug. See bug 26363026 on MOS.

ALTER SESSION SET plscope_settings='identifiers:none, statements:none';

CREATE OR REPLACE SYNONYM s1 FOR p1;

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

CREATE OR REPLACE PROCEDURE p4 (in_p IN INTEGER) IS
BEGIN
   s1(in_p => in_p);
END;
/

SELECT usage, type, name, line, col, usage_id, usage_context_id
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P4'
 ORDER BY line, col, usage_id;

USAGE                 TYPE       NAME           LINE  COL USAGE_ID USAGE_CONTEXT_ID
--------------------- ---------- -------------- ---- ---- -------- ----------------
DECLARATION           PROCEDURE  P4                1   11        1                0
DEFINITION            PROCEDURE  P4                1   11        2                1
DECLARATION           FORMAL IN  IN_P              1   15        3                2
REFERENCE             SUBTYPE    INTEGER           1   23        4                3
REFERENCE             FORMAL IN  IN_P              3   15        6                5

You can fix such a broken usage hierarchy on the fly. Here’s simplified version of  the query based on the plscope_identifiers view of the plscope-utils project. The analytic function in line 27-31 fixes invalid foreign keys. The highest preceding usage_id might not always be the best choice, but it is usually not that bad either. In this case the non-existing ‘5’ was replaced with a ‘4’ as you can see on line 53.

WITH
   filtered_ids AS (
      SELECT usage, type, name, line, col, usage_id, usage_context_id
        FROM user_identifiers ids
       WHERE object_type = 'PROCEDURE'
         AND object_name = 'P4'
   ),
   sanitized_ids AS (
      SELECT fids.usage, fids.type, fids.name, fids.line, fids.col,
             fids.usage_id, fids.usage_context_id,
             CASE
                WHEN fk.usage_id IS NOT NULL OR fids.usage_context_id = 0 THEN
                   'YES'
                ELSE
                   'NO'
             END AS sane_fk
        FROM filtered_ids fids
        LEFT JOIN filtered_ids fk
          ON fk.usage_id = fids.usage_context_id
   ),
   ids AS (
      SELECT usage, type, name, line, col, usage_id,
             CASE
                WHEN sane_fk = 'YES' THEN
                   usage_context_id
                ELSE
                   last_value(CASE WHEN sane_fk = 'YES' THEN usage_id END)
                      IGNORE NULLS OVER (
                         ORDER BY line, col
                         ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
                      )
             END AS usage_context_id -- fix broken hierarchies
        FROM sanitized_ids
   )
 SELECT lpad(' ', 3 * (level - 1)) || usage AS usage,
        type,
        name,
        line,
        col,
        usage_id,
        usage_context_id
   FROM ids
  START WITH usage_context_id = 0
CONNECT BY PRIOR usage_id = usage_context_id
  ORDER BY line, col, usage_id;

USAGE                 TYPE       NAME           LINE  COL USAGE_ID USAGE_CONTEXT_ID
--------------------- ---------- -------------- ---- ---- -------- ----------------
DECLARATION           PROCEDURE  P4                1   11        1                0
   DEFINITION         PROCEDURE  P4                1   11        2                1
      DECLARATION     FORMAL IN  IN_P              1   15        3                2
         REFERENCE    SUBTYPE    INTEGER           1   23        4                3
            REFERENCE FORMAL IN  IN_P              3   15        6                4

3 Missing Usages of Objects in CDB$ROOT

PL/Scope stores by default the metadata for the following SYS packages:

  • DBMS_STANDARD
  • STANDARD

If you want to analyse the usage of other supplied PL/SQL packages, you need to compile these package with PL/Scope settings first. The next example shows how to do that. On line 29 and 30 the reference to the package dbms_output and its procedure put_line are properly reported. So far so good.

CONNECT sys/oracle@odb AS SYSDBA

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

ALTER PACKAGE dbms_output COMPILE;

CONNECT scott/tiger@odb

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

CREATE OR REPLACE PROCEDURE p5 (in_p IN VARCHAR2) IS
BEGIN
   sys.dbms_output.put_line(in_p);
END;
/

SELECT usage, type, name, line, col, usage_id, usage_context_id
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P5'
 ORDER BY line, col, usage_id;

USAGE       TYPE               NAME        LINE  COL USAGE_ID USAGE_CONTEXT_ID
----------- ------------------ ----------- ---- ---- -------- ----------------
DECLARATION PROCEDURE          P5             1   11        1                0
DEFINITION  PROCEDURE          P5             1   11        2                1
DECLARATION FORMAL IN          IN_P           1   15        3                2
REFERENCE   CHARACTER DATATYPE VARCHAR2       1   23        4                3
REFERENCE   PACKAGE            DBMS_OUTPUT    3    8        5                2
CALL        PROCEDURE          PUT_LINE       3   20        6                5
REFERENCE   FORMAL IN          IN_P           3   29        7                6

We know that the non-CDB architecture has been deprecated with with Oracle 12.2. So let’s try the same with the recommended CDB architecture. In the next example we compile the dbms_output package within the CDB$ROOT container, which owns this package. Compiling it in a PDB is not possible (it does not throw an error, but it simply has no effect). On line 19 to 23 you see the container identifiers and their names. On line line 37 to 40 you see that the PL/Scope identifiers for the procedure put_lineis available in every container, except PDB$SEED. So far everything still looks good.

CONNECT sys/oracle@ocdb AS SYSDBA

SHOW con_name

CON_NAME
------------------------------
CDB$ROOT

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

ALTER PACKAGE dbms_output COMPILE;

SELECT con_id, name
  FROM v$containers
 ORDER BY con_id;

CON_ID NAME
------ -----------
     1 CDB$ROOT
     2 PDB$SEED
     3 OPDB1
     4 OPDB2
     5 OPDB3

SELECT usage, type, name, line, col, origin_con_id, con_id
  FROM cdb_identifiers
 WHERE OWNER = 'SYS'
   AND object_type = 'PACKAGE'
   AND object_name = 'DBMS_OUTPUT'
   AND TYPE = 'PROCEDURE'
   AND USAGE = 'DECLARATION'
   AND NAME = 'PUT_LINE'
 ORDER BY con_id;

USAGE       TYPE               NAME        LINE  COL ORIGIN_CON_ID CON_ID
----------- ------------------ ----------- ---- ---- ------------- ------
DECLARATION PROCEDURE          PUT_LINE      83   13             1      1
DECLARATION PROCEDURE          PUT_LINE      83   13             1      3
DECLARATION PROCEDURE          PUT_LINE      83   13             1      4
DECLARATION PROCEDURE          PUT_LINE      83   13             1      5

Now we are ready to create our procedure p5 which is using dbms_output in user SCOTT. However, the PL/Scope result is incomplete. Only 5 instead of 7 rows are reported. The two usages of dbms_output are missing.

This is a bug. See bug 26169004 on MOS.

CONNECT scott/tiger@opdb1

SHOW con_name

CON_NAME
------------------------------
OPDB1

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

CREATE OR REPLACE PROCEDURE p5 (in_p IN VARCHAR2) IS
BEGIN
   sys.dbms_output.put_line(in_p);
END;
/

SELECT usage, type, name, line, col, usage_id, usage_context_id
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P5'
 ORDER BY line, col, usage_id;

USAGE       TYPE               NAME        LINE  COL USAGE_ID USAGE_CONTEXT_ID
----------- ------------------ ----------- ---- ---- -------- ----------------
DECLARATION PROCEDURE          P5             1   11        1                0
DEFINITION  PROCEDURE          P5             1   11        2                1
DECLARATION FORMAL IN          IN_P           1   15        3                2
REFERENCE   CHARACTER DATATYPE VARCHAR2       1   23        4                3
REFERENCE   FORMAL IN          IN_P           3   29        5                2

I see the following workarounds:

  • Use a non-CDB database for PL/Scope analysis
  • Do the analysis in the CDB$ROOT container

4. Missing Identifiers

If you are analysing the usages of identifiers, e.g. to check if a declared identifier is used, then you will report false positives if PL/Scope does not report all identifier usages. See line 6 in the next example. The identifier l_stmt is referenced in the execute immediate statement, but the usage is not reported.

This is a bug. See bug 26351814 on MOS.

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

CREATE OR REPLACE PROCEDURE p6 IS
   l_stmt VARCHAR2(100) := 'BEGIN NULL; END;';
BEGIN
   EXECUTE IMMEDIATE l_stmt;
END;
/

SELECT usage, type, name, line, col, usage_id, usage_context_id
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P6'
 ORDER BY line, col, usage_id;

USAGE       TYPE               NAME        LINE  COL USAGE_ID USAGE_CONTEXT_ID
----------- ------------------ ----------- ---- ---- -------- ----------------
DECLARATION PROCEDURE          P6             1   11        1                0
DEFINITION  PROCEDURE          P6             1   11        2                1
DECLARATION VARIABLE           L_STMT         2    4        3                2
ASSIGNMENT  VARIABLE           L_STMT         2    4        5                3
REFERENCE   CHARACTER DATATYPE VARCHAR2       2   11        4                3

There is no feasible workaround. Ok, you could use a third party parser to verify the result, but that’s an extreme measure and a lot of work. I really hope Oracle is going to fix this bug soon.

5. Wrong Usages

In the next example the usage of the parameter in_pin the if statement is reported as DEFINITION instead of REFERENCE on line 23.

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

CREATE OR REPLACE PROCEDURE p7 (in_p IN BOOLEAN) IS
BEGIN
   IF in_p THEN
      NULL;
   END IF;
END;
/

SELECT usage, type, name, line, col, usage_id, usage_context_id
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P7'
 ORDER BY line, col, usage_id;

USAGE       TYPE               NAME        LINE  COL USAGE_ID USAGE_CONTEXT_ID
----------- ------------------ ----------- ---- ---- -------- ----------------
DECLARATION PROCEDURE          P7             1   11        1                0
DEFINITION  PROCEDURE          P7             1   11        2                1
DECLARATION FORMAL IN          IN_P           1   15        3                2
REFERENCE   BOOLEAN DATATYPE   BOOLEAN        1   23        4                3
DEFINITION  FORMAL IN          IN_P           3    7        5                2

This is a bug. See bug 20056796 on MOS.

Since a DEFINITION for a FORMAL IN type does not make sense, you can just replace all occurrences as follows:

SELECT CASE
          WHEN type = 'FORMAL IN' AND usage = 'DEFINITION' THEN
             'REFERENCE'
          ELSE
             usage
       END AS usage, type, name, line, col, usage_id, usage_context_id
  FROM user_identifiers
 WHERE object_type = 'PROCEDURE'
   AND object_name = 'P7'
 ORDER BY line, col, usage_id;

USAGE       TYPE               NAME        LINE  COL USAGE_ID USAGE_CONTEXT_ID
----------- ------------------ ----------- ---- ---- -------- ----------------
DECLARATION PROCEDURE          P7             1   11        1                0
DEFINITION  PROCEDURE          P7             1   11        2                1
DECLARATION FORMAL IN          IN_P           1   15        3                2
REFERENCE   BOOLEAN DATATYPE   BOOLEAN        1   23        4                3
REFERENCE   FORMAL IN          IN_P           3    7        5                2

However,  when you look at the bug description, you will also find examples for the following additional wrong usage reports:

  • an ASSIGNMENT instead of a REFERENCE
  • a REFERENCE instead of an ASSIGNMENT

For these wrong usages it might not be so simple to work around it. You have to find a solution on a on case basis until Oracle provides a bug fix.

6. Missing Usages and Structures in Static SQL Statements

In example 6a the procedure p8 creates a new row in the dept table in a rather awkward manner. In line 31 the analysis query adds the table name to the column name. Something like that is necessary if column names are not unique across tables.

However, the result has some flaws. Let’s look at the line 59-70. They are all direct descendants of the insert statement.

ALTER SESSION SET plscope_settings='identifiers:all, statements:all';

CREATE OR REPLACE FUNCTION f2 (in_name IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
   RETURN in_name || ' CITY';
END;
/

CREATE OR REPLACE PROCEDURE p8 IS
BEGIN
   INSERT INTO dept (deptno, dname, loc)
   WITH
      silly_data AS (
         SELECT job || substr(ename, 1, 0) AS loc,
                ename AS dname,
                deptno + 20 AS deptno
           FROM emp
          WHERE comm IS NOT NULL
            AND ROWNUM = 1
      )
   SELECT deptno, dname, f2(loc)
     FROM silly_data;
END;
/

WITH
   ids AS (
      SELECT i.usage, i.type,
             CASE
                WHEN i.type = 'COLUMN' THEN
                   p.object_name || '.' || i.name
                ELSE
                   i.name
             END AS name, i.line, i.col, i.usage_id, i.usage_context_id
        FROM user_identifiers i
        JOIN user_identifiers p
          ON p.signature = i.signature
             AND p.usage = 'DECLARATION'
       WHERE i.object_type = 'PROCEDURE'
         AND i.object_name = 'P8'
    UNION ALL
      SELECT 'EXECUTE' AS usage, type, sql_id AS name, line, col, usage_id, usage_context_id
       FROM user_statements
       WHERE object_type = 'PROCEDURE'
         AND object_name = 'P8'
   )
 SELECT lpad(' ', 3 * (level - 1)) || usage AS usage,
        type, name, line, col, usage_id, usage_context_id
   FROM ids
  START WITH usage_context_id = 0
CONNECT BY PRIOR usage_id = usage_context_id
  ORDER BY usage_id;

USAGE                 TYPE               NAME          LINE  COL USAGE_ID USAGE_CONTEXT_ID
--------------------- ------------------ ------------- ---- ---- -------- ----------------
DECLARATION           PROCEDURE          P8               1   11        1                0
   DEFINITION         PROCEDURE          P8               1   11        2                1
      EXECUTE         INSERT             7dkawtszvu0a8    3    4        3                2
         REFERENCE    TABLE              EMP              9   17        4                3
         REFERENCE    COLUMN             EMP.COMM        10   17        5                3
         REFERENCE    COLUMN             EMP.DEPTNO       8   17        6                3
         REFERENCE    COLUMN             EMP.ENAME        7   17        7                3
         REFERENCE    COLUMN             EMP.ENAME        6   31        8                3
         REFERENCE    COLUMN             EMP.JOB          6   17        9                3
         REFERENCE    COLUMN             EMP.ENAME       13   19       10                3
         REFERENCE    TABLE              DEPT             3   16       11                3
         REFERENCE    COLUMN             DEPT.LOC         3   37       12                3
         REFERENCE    COLUMN             DEPT.DNAME       3   30       13                3
         REFERENCE    COLUMN             DEPT.DEPTNO      3   22       14                3
         CALL         FUNCTION           F2              13   26       16                3

How do we identify the target table of the insert statement? Is it emp or is it dept? Ok, when we order the result by line and column instead of usage_id, we could know based on the SQL grammar that dept must be a target table. But what about emp? Could that be the target of a mutitable insert statement? Probably not since there is no other table to query data from, right? Probably right, but if we’d query data from a table function which is not compiled with PL/Scope emp could still be a target of a multitable insert statement. A different usage might help, but unfortunately all table and column usages within static SQL statements are reported as REFERENCE.  This is not a bug. If we want to change that we have to file an enhancement request.

Identifying target columns, source columns or columns used to filter data is impossible with PL/Scope alone. You need a SQL parser or tools using such a parser for deeper static code analysis.

Even if you are interested in the column usages only, you have to be aware that column-less access to tables are possible, e.g. when omitting the column list in the insert_into_clause. In such cases all visible columns of the target table are used. If synonyms and views are used, the analysis is becoming a bit harder.

Nonetheless, the metadata provided through user_statementscompletes the missing pure PL/SQL analysis reporting capabilities for PL/SQL identifiers. Now all usages of PL/SQL identifiers are reported with a static SQL statement context – if they have one. E.g the use of the function f2 within the insert statement on line 70. And that alone is very useful.

7. Summary

Most of the limitations mentioned in this post are based on bugs. Hence I recommend to check from time to time the status of the following bugs on MOS and open a SR when you are unable to produce a correct analysis result due to PL/Scope bugs.

  • Bug 24916492: PLSCOPE_SETTINGS DOESN’T PARSE IDENTIFIER INFORMATION AFTER NULL STATEMENT
  • Bug 26363026: WRONG RESULT IN PL/SCOPE HIERARCHICAL QUERY USING SYNONYM
  • Bug 26351814: EXECUTE IMMEDIATE STATEMENT IDENTIFIER REFERENCE NOT COLLECTED BY PL/SCOPE
  • Bug 26169004: PL/SCOPE DOES NOT DETECT USAGES OF CDB OBJECTS SUCH AS SYS.DBMS_SQL
  • Bug 20056796 : PLSCOPE SHOWS WRONG USAGE OF IDENTIFIERS

Some limitations of PL/Scope are by design. In the end PL/Scope provides just information about identifiers, a subset of data produced during parse time and not a complete parse tree, which would be desirable for complex static code analysis. However, if you just want to analyse the use of identifiers in your PL/SQL code, you should consider using PL/Scope. PL/Scope is storing the results after the semantic analysis, therefore each identifier comes with a context such as the schema, nested program unit, etc. Even if you need a third party tool for static code analysis, PL/Scope might be helpful to verify or to complete the result.

Before you start developing your own PL/Scope queries from scratch, have a look at plscope-utils. There are predefined views which address some of the mentioned limitations out of the box. There’s also a SQL Developer plugin which works against any database version with PL/Scope.


Viewing all articles
Browse latest Browse all 118

Trending Articles