-- Attempt to resolve an object name following the same resolution path as SQL or PL/SQL:
-- e.g. given the name "EMP", figure out that it's a public synonym for SCOTT.EMPLOYEES.
--
-- Note that SQL and PL/SQL may resolve objects differently due to definer/invoker rights, roles etc.
--
-- Three parts below:
-- 1. Parse multipart string e.g. "scott.somepkg.someproc" into owner, name and sub-element.
-- 2. Query the data dictionary to list out all objects matching the above.
-- 3. Call DBMS_UTILITY.NAME_RESOLVE and some increasingly contrived tests to figure out which it is.
--
-- William Robertson - www.williamrobertson.net

set term off
store set sqlplus_settings.sql replace

-- Use DBA_ views if possible:
col dict_level new_value dict_level
select 'ALL' as dict_level from dual;
select 'DBA' as dict_level from dba_users where rownum = 1;
-- Now &DICT_LEVEL contains either 'ALL' or 'DBA'
col dict_level clear

set term on
set autoprint off autotrace off
set timing off
set feed off

def object = &&1
col object_name hea "Name" format a30
col object_type hea "Type"
col granted format a8 hea "Granted?"
col synonym_name hea "Synonym"
col owner  format a25

break on object_name skip1 on object_type skip1

var object_name varchar2(100)
var object_name varchar2(30)
var owner varchar2(30)
var sub varchar2(30)
var dblink varchar2(30)
exec :object_name := upper('&object')

-- define object
-- print :object_name

declare
   nextpos      binary_integer;  -- discard
   v_name       varchar2(30);
   v_test_user  varchar2(30);
   k_original_object constant varchar2(30) := :object_name;
begin
   if :object_name like  '%#%%' escape '#'
   then
      -- Wildcard was passed: skip this part:
      return;
   end if;

   dbms_utility.name_tokenize
   ( :object_name
   , :owner
   , :object_name
   , :sub
   , :dblink
   , nextpos );

   -- Above call is not foolproof, e.g. X.Y may be parsed as owner X, object Y
   -- when a more likely explanation is object X, sub-element Y (e.g. package.procedure):
   if :owner = k_original_object and :object_name is null then
      -- NAME_RESOLVE parsed "X" as "owner = X, object = NULL" -
      -- More likely that we want "owner unspecified, object = X":
      :object_name := k_original_object;
      :owner := null;

   elsif :owner is not null
   and :dblink is null
   then
      begin
         select username into v_test_user
         from   &dict_level._users
         where  upper(username) like :owner
         and rownum = 1;

         -- DBMS_OUTPUT.PUT_LINE('Found user "' || :owner || '" in &DICT_LEVEL._USERS.');

         if :object_name is null
         and :owner is not null
         then
            dbms_output.put_line('"' || :owner || '" is a schema name.');
         else
            dbms_output.put_line('object name is "' || :object_name || '".');
         end if;

      exception
         when no_data_found then
            -- There is no user matching this name: try shifting elements up:
            -- (e.g. "X.Y" was parsed into "owner X, object Y", when
            -- "object X, sub-element Y" e.g. "package.proc" more likely)

            :dblink := :sub;
            :sub := :object_name;
            :object_name := :owner;
            :owner := NULL;
            :object_name := LTRIM(:owner || '.' || :object_name, '.');
      end;
   end if;
end;
/

-- Report matches from data dictionary using info parsed into owner/object/subobject etc above:
ttitle on left "Objects matching '&1':"

select --+ all_rows
       distinct
       da.object_name
     , da.object_type
     , da.owner
     , case
          when da.object_type not in ('TABLE','VIEW','SEQUENCE','OPERATOR','PROCEDURE','FUNCTION','PACKAGE','MATERIALIZED VIEW','TYPE') then null
          when aa.object_name is not null then 'Yes'
          else 'No'
       end as granted
     , case
          -- when da.object_type not like '% PARTITION' and da.object_type != 'DIMENSION' then s.synonym_name
          when da.object_type in ('TABLE','VIEW','SEQUENCE','OPERATOR','PROCEDURE','FUNCTION','PACKAGE','MATERIALIZED VIEW','TYPE') then s.synonym_name
       end as synonym_name
from   all_objects aa
     , &dict_level._objects da
     , &dict_level._synonyms s
where  da.object_name like :object_name
and    (    da.owner like :owner
        or  :owner is null )
and    aa.object_name (+)= da.object_name
and    aa.object_type (+)= da.object_type
and    aa.owner (+)= da.owner
and    s.table_name (+)= da.object_name
and    s.table_owner (+)= da.owner
order by  da.object_name, da.object_type, da.owner
/

ttitle off
set serverout on size 1000000

def table = &1

declare
   v_objectname   varchar2(70) default upper(:object_name);  -- Might be a synonym to a differently-named table
   v_objecttype   varchar2(70);
   v_tablename    varchar2(70) := v_objectname;
   v_db_link      varchar2(30);
   v_owner        varchar2(30) := upper(:owner);
   v_remote_owner varchar2(30);
   v_found_yn     varchar2(1) := 'n';

   v_name_resolve_part1 varchar2(35);
   v_name_resolve_part2 varchar2(35);
   v_name_resolve_part1_type varchar2(35);
   v_object_id    number;
   name_resolved  boolean := false;

   v_oracle_version integer;

   invalid_type_for_subobjects exception;
   pragma exception_init(invalid_type_for_subobjects, -06563);

   no_such_object exception;
   pragma exception_init(no_such_object, -06564);

   incompatible_flag exception;
   pragma exception_init(incompatible_flag, -04047);

   cursor c_resolved_owner(cp_objectname varchar2) is
      select /*+ all_rows */
             nvl(s.table_owner, o.owner) owner
           , nvl(s.table_name, o.object_name) name
           , o.object_type
           , s.db_link
           , s.table_owner
      from   ( select object_name
                    , object_type
                    , owner
                    , decode
                      ( owner || ' ' || object_type
                      , user || ' TABLE',   1
                      , user || ' VIEW',    1
                      , user || ' SYNONYM', 1
                                          , 'PUBLIC SYNONYM', 2) ORDER_KEY
               from   &dict_lEVEL._objects
               where  object_name = cp_objectname
               and    object_type in ('TABLE','VIEW','SYNONYM')
               and    owner in (user, 'PUBLIC') ) o
           , &dict_level._synonyms s
      where  s.synonym_name (+)= o.object_name
      and    s.owner (+)= o.owner
      order by order_key;

   cursor c_tables
      ( cp_owner varchar2
      , cp_objectname varchar2 )
   is
      select 'Y'
      from   &dict_level._all_tables
      where  table_name = cp_objectname
      and    owner = cp_owner
      union
      select 'Y'
      from   &dict_level._views
      where  view_name = cp_objectname
      and    owner = cp_owner;
begin
   select substr(version,1,instr(version,'.')) into v_oracle_version from v$instance;

   -- DBMS_OUTPUT.PUT_LINE('1: owner = "' || v_owner || '", object_name = "' || v_objectname || '"');

   if v_objectname like '%#%%' escape '#'
   or v_objectname is null
   then
      -- Wildcard was passed, or no object exists (e.g. the object was actually a schema name): skip whole NAME_RESOLVE process.
      return;
   end if;

   -- NB as of 9.2(?) DBMS_UTILITY.NAME_RESOLVE *DOES* find types which are not stored PL/SQL. In particular this is useful for
   -- resolving synonyms, e.g. "CREATE SYNONYM BANANA FOR SYS.DUAL;" - BANANA is not resolved in 8i, OK in 9i.
   -- Documentation for earlier versions recommended context = 1, while claiming it was unused (see below).

   -- First try with DBMS_UTILITY.NAME_RESOLVE.
   -- If unsuccessful, continue query of data dictionary:
   begin
      -- Exact meaning of 'context' param appears to be undocumented, but:
      -- ORU-10034: context argument must be 1 or 2 or 3 or 4 or 5 or 6 or 7
      -- (even though the only comment in dbmsutil.sql is "Must be an integer between 0 and 8".)
      -- From experience it seems:
      -- 1   Package, procedure or function
      -- 2   Table (any type), view, materialized view or sequence
      -- 7   Type (object or collection)

      -- Not catered for:
      -- Indexes
      -- Users
      -- Roles
      -- Directories
      -- Contexts
      -- Policies
      -- Queues

      for i_context in 1..7 loop
         exit when name_resolved = true;

         begin
            dbms_utility.name_resolve
            ( v_objectname
            , i_context -- required but undocumented (see note above): loop through values 1-7.
            , v_owner
            , v_name_resolve_part1
            , v_name_resolve_part2
            , v_db_link
            , v_name_resolve_part1_type
            , v_object_id );

            name_resolved := TRUE;
            -- DBMS_OUTPUT.PUT_LINE ( 'Success using context ' || i_context );
         exception
            when incompatible_flag or no_such_object then null;

            when invalid_type_for_subobjects then
               -- e.g. my_object_type.method, when my_object_type has no type body
               -- DBMS_OUTPUT.PUT_LINE('Discarding subobject');
               :sub := null;
         end;
      end loop;

      -- Examine what DBMS_UTILITY.NAME_RESOLVE captured, if anything:
      -- (may need to take context into account - needs testing)
      if not name_resolved then
         raise no_such_object;
      elsif v_name_resolve_part1_type = 2 then
         v_objecttype := 'TABLE';
      elsif v_name_resolve_part1_type = 4 then
         v_objecttype := 'VIEW';
      elsif v_name_resolve_part1_type = 5 then
         v_objecttype := 'SYNONYM';
      elsif v_name_resolve_part1_type = 6 then
         v_objecttype := 'SEQUENCE';
      elsif v_name_resolve_part1_type = 7 then
         v_objecttype := 'PROCEDURE';
      elsif v_name_resolve_part1_type = 8 then
         v_objecttype := 'FUNCTION';
      elsif v_name_resolve_part1_type = 9 then
         if v_name_resolve_part2 is null then
            v_objecttype := 'PACKAGE';
         else
            v_objecttype := 'PACKAGE ELEMENT';
            -- DBMS_OUTPUT.PUT_LINE('v_name_resolve_part2 = ' || v_name_resolve_part2);
         end if;
      elsif v_name_resolve_part1_type = 12 then
         v_objecttype := 'TRIGGER';  -- context = 3
      elsif v_name_resolve_part1_type = 13 then
         v_objecttype := 'TYPE';
      else
         -- DBMS_OUTPUT.PUT_LINE('v_name_resolve_part1_type = ' || v_name_resolve_part1_type);
         -- DBMS_OUTPUT.PUT_LINE('v_name_resolve_part2 = ' || v_name_resolve_part2);
         v_objecttype := 'UNKNOWN';
      end if;

      dbms_output.put_line
      ( rtrim
        ( v_owner || '.'
          || ltrim(v_name_resolve_part1 || '.', '.')
          || v_name_resolve_part2
        , '.' )
        || ' (' || v_objecttype || ')'
      );

      -- Successful result found - end processing:
      return;
   exception
      when no_such_object then
         -- NAME_RESOLVE failed to resolve, e.g. because object is a table, view etc,
         -- or does not exist:
         name_resolved := false;
         dbms_output.put_line('Could not resolve "' || ltrim(v_owner || '.' || v_objectname,'.') || '".');
   end;

   if v_oracle_version <= 8 then
      -- Prior to 9i, NAME_RESOLVE only works for stored PL/SQL, e.g. not for synonyms.

      -- NAME_RESOLVE does not resolve other users' private synonyms as of 9.2.0.5, e.g:
      -- SCOTT creates synonym E for EMP,
      -- then WILLIAM calls DBMS_UTILITY.NAME_RESOLVE for SCOTT.E.
      -- Possibly code below should check for this speciic case only.

      if v_owner is null then
         if v_objectname like '%.%' then
            v_owner := substr(v_objectname,1,instr(v_objectname,'.') -1);
            v_objectname := substr(v_objectname,instr(v_objectname,'.') +1);
            v_tablename := v_objectname;

            if v_tablename like '%@%' then
               v_db_link := substr(v_tablename,instr(v_tablename,'@') +1);
               v_tablename := substr(v_tablename,1,instr(v_tablename,'@') -1);
               v_remote_owner := v_owner;
            end if;
         else
            open c_resolved_owner(v_objectname);
            fetch c_resolved_owner into v_owner, v_tablename, v_objecttype, v_db_link, v_remote_owner;
            close c_resolved_owner;
         end if;
      end if;

      if v_owner is null then
         dbms_output.put_line
         ( 'Table or view ''' || v_objectname ||
         ''' does not exist, or must be qualified with a schema name.' );

      elsif v_db_link is not null then
         dbms_output.put_line(v_owner || '.' || v_tablename || '@' || v_db_link);

      else
         -- Double-check in case just found invalid synonym above:
         -- c_tables looks for a  table named v_tablename owned by v_owner
         open c_tables(v_owner, v_tablename);
         fetch c_tables into v_found_yn;
         dbms_output.put_line('Found tables matching ' || v_owner || ',' || v_tablename || '? ' || v_found_yn);
         close c_tables;

         if v_found_yn = 'Y' then
            dbms_output.put(v_owner || '.' || v_tablename);

            if v_objecttype != 'TABLE' then
               dbms_output.put(' (' || v_objecttype || ')');
            end if;

            dbms_output.new_line;
         else
            if v_objecttype = 'SYNONYM' then
               dbms_output.put_line
               ( 'Synonym ''' || v_objectname
               || ''' (' || v_owner || '.' || v_tablename || ') is no longer valid' );
            else
               dbms_output.put_line
               ( nvl(initcap(v_objecttype),'Object')
               || ' ' || v_owner || '.' || v_tablename
               || ' is not a table, view or synonym.' );
            end if;
         end if;
      end if;
   end if;
end;
/

set feed 1
prompt

@sqlplus_settings.sql
set term on

-- vim: ft=plsql
