Showing posts with label PL/SQL. Show all posts
Showing posts with label PL/SQL. Show all posts

Wednesday, January 1, 2014

Up, Periscope!

Last year I "inherited" the maintenance of some our shop's old PL/SQL code.  It was a key piece of infrastructure code; it was also over 15 years old, and many hands had worked on the code before me.  There was some interesting functionality with which I was not familiar, so before I made changes to the code, I needed to take a deeper dive into the code. 

After 15 years and many hands working on the code, I found the scope of the variables to be very confusing.  Variables were global; variables were public; variables were local to a function or a procedure or an anonymous block.  Some variables were scalars, others were part of a structure.  Some variables belonged to the same domain; they were really duplicates of each other.  There were local and global variables with the same name, and some of the code just assigned values back and forth.

So, for year, I maintained the code as it was, doing what I needed to do to add functionality as required, trying to make as few changes as possible to not break something. 

Now I have a new mandate:  retire the old code and write a new, simpler version.  There's a lot of good work in the old code, so I'm not going to throw the baby out with the bath water.  But I am taking a deeper dive into the scope of the variables, and that leads to this post's topic, taking a deep dive into variable scope and variable lifetime.  We'll take a look at private versus public variables, local versus global variables, and why we might choose one over the other.  Our examples are in Oracle's PL/SQL programming language, but it's important to understand variable scope and variable lifetime in any programming language.

Let's start with a package specification:
create or replace package demo_scope as

global_public_string varchar2(32);

procedure test (
   p_arg1 in varchar2 default null,
   p_arg2 in out number   default null);

function test_func (
    p_arg3 in number )
  return varchar2;

end demo_scope;
Our package has one procedure, one function, and one varchar2 string visible to any user with execute permission on the package. Based on the package specification, a valid anonymous block could look like the following:
declare
   my_func_results varchar2;
   my_number number := 1776 ;
begin
   demo_scope.global_public_string := 'my value';

   demo_scope.test(p_arg1 => 'argument to test', 
                   p_arg2 => my_number);

   my_func_results := demo_scope.test_func(2001);
end; 
Our package body implements the public functions and procedures. In addition, the package body defines more variables, functions, and procedures that are not visible to package users. Here's our package body:
create or replace package body  demo_scope as

global_private_string varchar2(32);

procedure test (
   p_arg1 in varchar2 default null,
   p_arg2 in out number   default null) is
    
   local_private_string varchar2(32);

begin
     
    -- An anonymous block within a named procedure
    declare
    
       local_private_in_block_string varchar2(32);

    begin
    
       global_public_string := p_arg1;
       global_private_string := 'some other value';

    end;
    
    p_arg2 := 1976;

end test;
 
function test_func ( p_arg3 in number )
    return varchar2 is

begin

  return null;

end test_func;

begin  --Package initialzation

  global_public_string := null;
  global_private_string := null;  

end demo_scope;
Let's take a look at the variables and see what we can do with them. We'll start from the package specification and work down through the package body.
  1. Variable global_public_string -- Any user of the package and any block within the package can directly reference this variable.  Declaring  variables (functions and procedures, too) in the package spec instead of the package body makes them public.
  2. Procedure test -- Procedure test is public, and any user of the package can call this procedure.
  3. Parameters p_arg1 and p_arg2 -- Any user calling procedure test can supply arguments for these parameters..  Parameter p_arg1 is passed by value to the procedure. Because p_arg2 is specified as an "out" parameter, procedure test can change p_arg2 and the new value is returned to the caller.  If we try to assign a value to parameter p_arg1 within procedure test, we will get an error message when we try to compile the procedure.
  4. Function test_func -- Any user of the package can call this function.
  5. Parameter p_arg3 -- Any user calling function test_func can supply an argument for this parameter.
  6. Variable global_private_string -- Only functions, procedures, and blocks defined within the package body can use this variable.  Declaring variables outside of functions and procedures in a package body makes the variable scope global.  Declaring variables in the package body rather than the package spec makes them private; only functions and procedures defined in the package can reference these variables.
  7. Variable local_private_string -- Only the procedure test and any blocks within procedure test can use this variable. When procedure test ends, the value of this variable is lost.
  8. Variable local_private_in_block -- Only the anonymous block within procedure test can use this variable. When this block ends, the value is lost. This variable can not be referenced outside of the block.
To review: locally-defined variables do not retain their value when the block (procedure, function, or anonymous block) ends.   Locally-defined variables can only be referenced within the block that declares them.

Parameters passed to functions and procedures can not be changed by the function or procedure unless the parameter is an "out" parameter.  The PL/SQL compiler flags as an error any attempt to assign a value to an "in" parameter.

Global variables declared in the package body can be referenced by any block defined in the package, but they can not be referenced outside the package body.  Global variables declared in the package spec are public and can be referenced anywhere in the package body, and they can be referenced by any user of the package.

Global variables retain their values when a block ends.   In fact, a package's  global variables retain their values as long as the session is active.  Control can pass from the package back to its caller;  global variables still retain their value.

This property makes them useful for caching results that may be expensive to compute.  Repeatedly selecting data from a lookup table can often be done more inexpensively by defining an associative array as a global variable,  loading data into the array during package initialization, and then searching the array by a key.  This can be a big performance gain when lookups are performed in a tight loop.  And as long as the database session is active, the array retains its values, and there is no need to reload it.

Good programming practice dictates that we define variables in the scope that we need them.  It may be tempting to define variables globally, so that "we'll have them when we need them".  Over-use of globally-defined variables makes a program difficult to understand.  But used carefully, global variables can be a useful tool for improving program performance. 


One last thought:  Have a Happy New Year, and may all your code be bug-free!

Saturday, December 14, 2013

Regular Expressions: Analyzing PL/SQL Source

We're going to examine a few themes that we've touched on in earlier posts.  In Get Regular (Expressions) we took a first look at regular expressions, and in Recursive SQL we used the Oracle DBA_DEPENDENCIES view for some sample data.  We'll take another look at a couple of views in the Oracle 11g database, and we'll use regular expressions to dig a little deeper into these views.

We have an old package that's been running for over 15 years.  It's gotten rather long in the tooth, and we need to replace it.   First, we need to find all the other objects that use this package. Oracle's DBA_DEPENDENCIES view will show every object that directly references a package. 

describe dba_dependencies
Name                 Null     Type          
-------------------- -------- ------------- 
OWNER                NOT NULL VARCHAR2(30)  
NAME                 NOT NULL VARCHAR2(30)  
TYPE                          VARCHAR2(18)  
REFERENCED_OWNER              VARCHAR2(30)  
REFERENCED_NAME               VARCHAR2(64)  
REFERENCED_TYPE               VARCHAR2(18)  
REFERENCED_LINK_NAME          VARCHAR2(128) 
DEPENDENCY_TYPE               VARCHAR2(4)   
We plug our schema name and package name in as the referenced_owner and referenced_name, run the query, and the database displays the information we're looking for.
select owner, name, type  
from dba_dependencies
where referenced_owner = 'MY_SCHEMA'
  and referenced_name = 'MY_PACKAGE';
And we see that three other objects use our package:
OWNER       NAME      TYPE             
-------     ------    ------------    
SCHEMA1     PCK1      PACKAGE BODY           
SCHEMA4     PROC1     PROCEDURE        
SCHEMA5     FUNC1     FUNCTION       
But we need to dig a little deeper. Our package has many procedures and functions defined within it. Some of the package functions are obsolete, and some of the package functions need to be rewritten in a new package.  Not only do we want to know what other objects are dependent on our package, we want to know which of the package's functions and procedures are called. The DBA_SOURCE view has all the PL/SQL code compiled into the database.
describe dba_source
Name  Null Type           
----- ---- -------------- 
OWNER      VARCHAR2(30)   
NAME       VARCHAR2(30)   
TYPE       VARCHAR2(12)   
LINE       NUMBER         
TEXT       VARCHAR2(4000) 
We will use two regular expression functions to examine the source.   We will use REGEXP_LIKE to identify the source text that has one of the functions. REGEXP_LIKE returns true if the pattern is found in TEXT and false otherwise.  The lower-case "i" is the match parameter indicating that we want a case-insensitive search.   PL/SQL source is not case sensitive, so the case-insensitive search allows us to find text that is upper, lower, or camel-case.

We will use REGEXP_SUBSTR to identify the function or procedure.  We'll start searching the first position of the TEXT, and we'll return the first occurrence we find.  Again, we will use a case-insensitive search.  Using these two functions, we can perform the same sort of analysis that we we could do by grepping through a code tree on disk. In this example, we're looking to see if specific functions or procedures are used. Here's our query to perform this analysis against the code compiled into the database:
select distinct owner, name, type, 
       regexp_substr(text,
                    'my_package\.(my_func1|my_func2|my_proc1|my_proc2)',1,1,'i') calls
from dba_source
where regexp_like(text,
                  'my_package\.(my_func1|my_func2|my_proc1|my_proc2)','i') 
 ;
The results of the query are every package, procedure, or function that uses the package, and the query identifies what functions and procedures the calling object uses:
OWNER       NAME      TYPE             CALLS
-------     ------    ------------     -------------------
SCHEMA1     PCK1      PACKAGE BODY     my_package.my_func1
SCHEMA1     PCK1      PACKAGE BODY     MY_PACKAGE.MY_FUNC1
SCHEMA4     PROC1     PROCEDURE        my_package.my_proc2
SCHEMA5     FUNC1     FUNCTION         MY_PACKAGE.MY_FUNC2
This query is a useful summary of what's calling my_package and which procedures in my_package are used. We can get more detail from dba_source if we need it. For example, dba_source includes the line number, so the query
select distinct owner, name, type, line, 
       regexp_substr(text,
                    'my_package\.(my_func1|my_func2|my_proc1|my_proc2)',1,1,'i') calls
from dba_source
where regexp_like(text,
                  'my_package\.(my_func1|my_func2|my_proc1|my_proc2)','i') 
 ;
would return
OWNER       NAME      TYPE             LINE      CALLS
-------     ------    ------------     ----      -------------------
SCHEMA1     PCK1      PACKAGE BODY     330       my_package.my_func1
SCHEMA1     PCK1      PACKAGE BODY     621       my_package.my_func1
SCHEMA1     PCK1      PACKAGE BODY     721       MY_PACKAGE.MY_FUNC1
SCHEMA4     PROC1     PROCEDURE        45        my_package.my_proc2
SCHEMA5     FUNC1     FUNCTION         87        MY_PACKAGE.MY_FUNC2
This query shows the line number where the calling object uses our package. This is useful if we need some context to see why the calling object uses our package and what the calling package is doing with the results.

The above examples show us where specific procedures and functions are used in other objects. This is useful if we want a narrow search, but often we want to see every reference to a function or procedure or even a publicly-defined variable in our package. The regular expression for this search is even simpler: PL/SQL  names consistent of letters, numbers, and a few special characters, so when the regular expression can't match any more characters in the expression  [a-z0-9_$#]+, then we have the whole name. Here's the query:
select distinct owner, name, type, 
       regexp_substr(text,
                    'my_package\.[a-z0-9_$#]+',1,1,'i') calls
from dba_source
where regexp_like(text,
                  'my_package\.[a-z0-9_$#]+','i') 
 ;
DBA_DEPENDENCIES and DBA_SOURCE are good repositories to help us understand what objects are using our code and how the objects are using our code.  And, the regular expression functions make searching though these tables much easier. Anytime we can get the database to do the work for us, "that's a good thing".

Wednesday, December 4, 2013

A Simple Web Client

Cloud  computing and software as a service providers have lots of capacity and attractive rates, so many organizations are taking their applications to the cloud.  After an application becomes cloud-based, many users need to pull some of the data to feed to other systems.  In this post, we'll look at a simple PL/SQL web client  that connects to a web service and downloads data. 

For our data,  we'll use the Data Download Program at the Federal Reserve Bank.  There's a lot of statistical data on the Federal Reserve site, and we will use that data as the source for our simple web client.   The data files are small and free to download, so the Bank gives us a good source of data to use.

From the Data Download Program we click on the Foreign Exchange Rates (G.5 / H.10) link, then we get a page that lets us choose which package we would like.  The H10 radio button is selected by default, so clicking the Download button takes us to the download page.  We could download a CSV into our browser and save the result as an OpenOffice or Excel spreadsheet.  But for production purposes, following these links and downloading data through a web browser is a tedious chore. Notice the Direct Download for Automated Systems link?   We'll copy that link and use it for our demonstration. 

create or replace procedure simple_web_client as
    
  timeout number := 60;  -- timeout in seconds

  /* Below is the URL from the Direct Download for Automated Systems.  Just copy the link
     and paste the text.
  */
  v_url varchar2(1024) :=
  'http://www.federalreserve.gov/datadownload/Output.aspx?rel=H10&series=122e3bcb627e8e53f1bf72a1a09cfb81&lastObs=10&from=&to=&filetype=csv&label=include&layout=seriescolumn&type=package';
   
  v_line_count number := 0;
  v_line_in varchar2(1024);
  v_header varchar2(1024);

  /* UTL_HTTP.REQ is set by the BEGIN_REQUEST function.  REQ includes 
     the URL, method, and http_version.
  */
  req   UTL_HTTP.REQ;

  /* UTL_HTTP.RESP is set by the BEGIN_RESPONSE function.  RESP includes   
     the status, a brief message, and the http version.
  */
  resp  UTL_HTTP.RESP;
    
begin
    
  utl_http.set_transfer_timeout(timeout);         -- set the timeout before opening the request
  req := utl_http.begin_request(v_url);           -- Pass our URL, get a req object back
  
  utl_http.set_header(req, 'User-Agent', 'Mozilla/4.0');

  resp := utl_http.get_response(req);             -- Pass our req object, get a resp object back
  utl_http.read_line(resp, v_header, true);       -- Read line of resp into v_header

  dbms_output.put_line('CSV Header = ' || v_header);
  v_line_count := v_line_count + 1;

  loop

    utl_http.read_line(resp, v_line_in, true);    -- csv data
    v_line_count := v_line_count + 1;
    dbms_output.put_line('CSV Data = ' || v_line_in);

  end loop;

exception
  when utl_http.end_of_body then                  -- end of the data
      dbms_output.put_line('Lines read, including header = ' || v_line_count );
      util_http.end_response(resp);               -- close the response object

  when others then
    dbms_output.put_line('Unhandled exception, lines = ' || v_line_count  );
    dbms_output.put_line(sqlerrm);
    dbms_output.put_line(dbms_utility.format_error_backtrace);
 
    -- Do not leave anything open.  Close everything and ignore errors.
    begin
       utl_http.end_response(resp);
       utl_http.end_request(req);
    exception
       when others then null;
    end;
    
end simple_web_client;

Now we execute our client using SQLPLUS or SQL/Developer. If we connect properly, our output will look something like this:
anonymous block completed
CSV Header = "Series Description","Nominal Broad Dollar Index ","Nominal Major Currencies Dollar Index ","Nominal Other Important Trading Partners Dollar Index "
CSV Data = "Unit:","Index:_1973_Mar_100","Index:_1973_Mar_100","Index:_1997_Jan_100"
CSV Data = "Multiplier:","1","1","1"
CSV Data = "Currency:","NA","NA","NA"
CSV Data = "Unique Identifier: ","H10/H10/JRXWTFB_N.B","H10/H10/JRXWTFN_N.B","H10/H10/JRXWTFO_N.B"
CSV Data = "Time Period","JRXWTFB_N.B","JRXWTFN_N.B","JRXWTFO_N.B"
CSV Data = 2013-10-14,ND,ND,ND
CSV Data = 2013-10-15,101.1463,75.7635,128.2686
CSV Data = 2013-10-16,101.0694,75.7839,128.0691
CSV Data = 2013-10-17,100.4167,74.9717,127.6647
CSV Data = 2013-10-18,100.3842,74.8882,127.7015
CSV Data = 2013-10-21,100.6396,75.0142,128.1114
CSV Data = 2013-10-22,100.3595,74.7093,127.8816
CSV Data = 2013-10-23,100.4550,74.8150,127.9575
CSV Data = 2013-10-24,100.5485,74.8527,128.1189
CSV Data = 2013-10-25,100.5134,74.9733,127.8807
Lines read, including header = 16

The Oracle utl_http packages makes reading data from a webserver as simple as reading data from a file. With a simple web client, we can read any data that's available from a web server. Our web service example used CSV-formatted data, but we could download XML data as easily. And for a real hands-free operation, schedule your client using DBMS_SCHEDULE or DBMS_JOB.

There's just one caveat when using the utl_http functions: depending on your DBAs and your installation, you may not have access to use the network functions. If you get an error like
ORA-24247: network access denied by access control list (ACL) 
then it's time to visit the DBAs and request that they add your schema or your user account to the network ACL.

Tuesday, November 12, 2013

Quirks Mode

"The good thing about standards is that there are so many to choose from."  I had a good laugh when I read that years ago. Andrew Tanenbaum, in his book Computer Networks, is quite correct.  It was true of computer networks twenty years ago (remember SNA, DECNet, Arpanet, and BITNET, just to name a few), and it's true of web standards and web browsers today.

After laying out a page with <div> tags and testing the page with Internet Explorer, Chrome, Safari, and Firefox, one tester reported the page was not displaying properly.  A bit of investigation revealed that the tester was using Internet Explorer 9.  As of this writing, IE9 is only one release behind the current IE10, so we were puzzled why  IE9 did not render the page as expected.   Further investigation revealed that IE9 behaves differently when viewing intra-net pages and inter-net pages.  For pages served within our corporate firewall, IE 9 runs in "quirks mode", and behaves like IE 5!  "The good thing about standards..."  For pages served from the external network, IE9 behaves like a modern browser.  Google "IE quirks mode", and you will find lots of discussion regarding IE in quirks mode and standards mode. 

We are using APEX to deliver the problem page, so we'll examine one solution to handling this problem in APEX.  Our page just displays information with some links,  and we are not collecting any information nor interacting with the end-user.  This makes things much simpler. Our solution will consist of three parts:
  • we will detect the browser and save the browser in an Apex item
  • we will use two APEX regions, one for newer browsers and a second for older browsers
  • Each region will have a Condition, and we will display the region (or not) depending on the browser
 I discussed browser detection in an earlier post, so I grabbed some code from the demo page. After adding a hidden item in the first region named "P1_BROWSER",  add this PL/SQL anonymous block to a "Before Header" process on the Apex page:
declare

   v_browser_string varchar2(512) := null;

begin

   v_browser_string := upper(owa_util.get_cgi_env('HTTP_USER_AGENT') ) ;

   if instr(v_browser_string,'MOZILLA/4.0') > 0       -- older browser
         or instr(v_browser_string,'MSIE 9') > 0 then -- IE9
      :p1_browser := 'OLD';
   else
      :p1_browser := 'NEW';
   end if;
    

end;

Unfortunately, when we have an IE9 browser, we can't tell what document mode the browser is in.  We could examine the client's IP address, too, but now we're introducing network variables into our code. And the end-user can switch document modes, too.   So, we'll just treat IE9 as an old browser.

Our modern browser region was laid out in three columns with <div> tags.  For older browsers, we can lay out the page with <table> tags:
<table width=100%>
  <tbody>
   <tr>
      <td width=20%>
         <!-- Column 1 content -->
      </td>
      <td width=40%>
         <!-- Column 2 content -->
      </td>
      <td width=40%>
         <!-- Column 2 content -->
      </td>
   </tr>
   </tbody>
</table>      
Next, we need to add a Condition to the modern browser region and the old browser region. The condition should be "PL/SQL Function Body Returning a Boolean", and the code should look like the first line for the modern browser region or the second line for the old browser region.
return :p1_browser = 'NEW';

return :p1_browser <> 'NEW';
Now when a browser requests our page, we detect the browser and display the region that is appropriate for that browser. Modern browsers get the <div> layout, older browers get the <table> layout, and our end-users see the same layout.

Finally, a rant and a plug. When I read Tanenbaum's Computer Networks, I was really impressed by how well written the book is.  Computer Networks was a pleasure to read; I can not say that about many technical books.  Why shouldn't a technical book read as well as a good novel?  Sure, we're all geeks, and we love to learn; but I could read Tanenbaum's book just for the fun of reading it.

Tuesday, November 5, 2013

Retirement

Cloud computing is the rage these days. As we migrate old systems to the cloud, we need to retire the old system that we left behind. This is usually a several step process: we start with the easiest step to implement and undo, and we finish with the most difficult step to undo.

Assuming that the retired application has its own schema, the easiest first step is to simply revoke all the privileges on the underlying database schema. We leave the data and the programs in place, and we simply deny all users other than the owner access to the database objects. If we're retiring a large system, there will be hundreds of grants to revoke. We would like an easy way to find all the grants and revoke them. Also, we would like an easy way to undo these changes!

I work in an Oracle shop, so we'll use the Oracle system catalog as an example. Using two two views in the Oracle catalog, we can find all the information we need to revoke every grant on the retired schema's objects.  The USER_TAB_PRIVS view shows all the objects where the schema is object owner, grantor, or grantee.  We want to revoke every grant where the schema is the grantor and owner of the object.   From USER_TAB_PRIVS we get the object name and the privilege.  The  USER_OBJECTS view shows us all the objects that belong to the schema.  From USER_OBJECTS we get the object type, and we use the object type to skip PACKAGE BODYs and TYPE BODYs, because the grants belong to the PACKAGE or TYPE, not the PACKAGE BODY nor TYPE BODY.

Here's our script.  We open a cursor from the join of the two views, we concatenate a string to revoke the privilege, and then we execute the string.   The SPOOL command writes the results in an output file. SQL/Developer does not support the spool command, so login to the retired schema and run this script using sqlplus.   If you're a bit nervous about running the script under the wrong user or schema, change the USER function to the quoted name of the schema instead.
set serveroutput on
spool 'Revoked_Grants.txt' 
begin

    for c in (

        select 'REVOKE ' || a.privilege ||
                  ' ON ' || a.table_name ||
                  ' FROM ' || a.grantee as revoke_command
        from user_tab_privs a
        inner join user_objects b
          on a.table_name = b.object_name
        where b.object_type not in ('PACKAGE BODY', 'TYPE BODY')
          and a.owner  =  user
          and a.grantor = user) loop
    
        dbms_output.put_line(c.revoke_command);

        begin
           execute immediate c.revoke_command;
        exception
           when others then dbms_output.put_line(' *** ' || sqlerrm);
        end;
   
    end loop;
   
end;
/
spool off
When we run this script, the spool command writes a file listing every grant that we revoked. In addition to logging the results for audit purposes, we can use this file to reverse the changes. Simply edit the file, change the REVOKEs to GRANTs, change the FROMs to TOs, save the file, and process it through  sqlplus. Now we have an easy way to revoke all the privileges and an easy way to restore them if necessary.


Monday, August 26, 2013

Translate and Replace and Replace and Replace and ...

One task many programmers face is examining a string of characters and replacing specific characters with different ones.  Every COBOL programmer is familiar with the INSPECT verb, and if they've really been programming COBOL for a while, they're familiar with the now-obsolete EXAMINE verb, too. We're not going to step that far back; instead we'll look at a couple ways of handling this problem in Oracle's PL/SQL programming language.

Oracle's PL/SQL function library includes several functions that replace single characters or multiple characters with a new characters or string of characters:
Regexp_replace and its siblings regexp_like and regexp_substr are worthy of their own post, so we'll focus on the translate and replace functions.

Translate is the simplest function. Translate performs single characters substitution.  There are three arguments:  the input string we want to examine, a string of characters that we want to replace, and a new string of characters that we want to use as replacements.  Here's a simple example:

select 'abcdefgh' input,
       translate('abcdefgh','bdfh','BDFH') output
from dual;

INPUT       OUTPUT
--------    --------
abcdefgh    aBcDeFgH


We see that every occurrence of b, d, f, or h was replaced by the upper case equivalent.  Every character in the second argument is replaced by its equivalent character in the third argument.

This is a really easy way to do one-to-one character translations.  Sometimes, we want to replace a single character by a string of characters, or perhaps replace a string of characters by a single character.  The replace function handles this nicely.   Here's an example; anyone needing to translate multi-byte characters to a single-byte character is familiar with this problem:

select '10 ≥ 5' input,
       replace('10 ≥ 5','≥ ','>=') output
from dual;

INPUT       OUTPUT
--------    --------
10  ≥  5     10  >= 5

This is pretty handy.  We translated the single character greater than or equal to symbol into a two-character string that we can represent in a 7-bit ASCII codeset.

The replace function can convert strings into single characters, too.  Here's another example:

select '(c)2013' input, 
       replace('(c)2013','(c)','©') output 
from dual;

INPUT       OUTPUT
--------    --------
(c)2013     ©2013 

This is pretty handy, too.  We can take a string encoded in 7-bit ASCII and replace character strings like (c) and >= with their single-character equivalents in a multi-byte character set.  

 There's just one thing missing:  Translate can change many different characters for us in one call, but it does not handle character string replacements.  Replace can change characters strings, but only one string at a time.  If we want to change both the © and the  ≥, we need to do something like this:

select '© ≥' input,
       replace(replace('© ≥','©','(c)),'≥','>=') output
from dual;

INPUT    OUTPUT
-----    ------
© ≥      (C) >=

This works fine for a couple of translations, but for more than a few translations, this is pretty cumbersome. Why not let the computer do it for us? We'll define a new function, called MREPLACE, that will handle multiple replace strings. The first argument will be the string we want to examine. The following arguments are pairs of strings: first the old string, then the new string. Here's an example of MREPLACE in use:

select '© ≥' input,
       mreplace('© ≥',
                '©','(c)',
                '≥','>=') output
from dual;

INPUT OUTPUT                                                                     
----- ------
© ≥  (c) >= 

Perfect! Now, with just one function call, we have an easy way to replace multi-character or single-character strings with multi-character or single-character strings in one function call. Here's the source:

create or replace
function mreplace (
    p_string in varchar2, 
    p_from1 in varchar2 default null, p_to1 in varchar2 default null,
    p_from2 in varchar2 default null, p_to2 in varchar2 default null,
    p_from3 in varchar2 default null, p_to3 in varchar2 default null,
    p_from4 in varchar2 default null, p_to4 in varchar2 default null,
    p_from5 in varchar2 default null, p_to5 in varchar2 default null,
    p_from6 in varchar2 default null, p_to6 in varchar2 default null,
    p_from7 in varchar2 default null, p_to7 in varchar2 default null,
    p_from8 in varchar2 default null, p_to8 in varchar2 default null 
                  ) return varchar2 as

begin 

  if ( p_from1 is null ) then return p_string;
  else
     return mreplace(replace(p_string,p_from1, p_to1),
                     p_from2 , p_to2 ,
                     p_from3 , p_to3 ,
                     p_from4 , p_to4 ,
                     p_from5 , p_to5 ,
                     p_from6 , p_to6 ,
                     p_from7 , p_to7 ,
                     p_from8 , p_to8 );
  end if;           

end mreplace;

How does it work? MREPLACE uses recursion, repeatedly calling itself. MREPLACE keeps calling itself until the p_from1 argument is null. The test to stop the recursion is important if you don't want your DBAs and sysadmins darkening your office door! Also, notice that MREPLACE accepts one string to examine and eight from/to pairs. When we invoke it recursively, we call it with one string to examine that includes our string and the first from/to pair:

          replace(p_string, p_from1, p_to1)

but only seven from/to pairs:

                     p_from2 , p_to2 ,
                     p_from3 , p_to3 ,
                     p_from4 , p_to4 ,
                     p_from5 , p_to5 ,
                     p_from6 , p_to6 ,
                     p_from7 , p_to7 ,
                     p_from8 , p_to8 )

So, each time MREPLACE calls itself, it shifts all the arguments to the left. The eigth from/to pair are not specified and defaults to null. When p_from1 is null, then all the arguments are used, all the substitutions are completed, and MREPLACE returns the answer.

For the purposes of illustration, MREPLACE accepts up to 8 from and to pairs, but in practice we would set it to accept more, maybe up to 32 from/to pairs, or 64 from/to pairs or even more if required.  Remember, MREPLACE stops the recursion when it finds the first null p_from1 argument, so you should define it to accept more from/to substitution pairs than you expect to use.

Happy computing!