37.11. Portage d'Oracle PL/SQL

Cette section explicite les différences entre le PL/pgSQL de PostgreSQL et le langage PL/SQL d'Oracle, afin d'aider les développeurs qui portent des applications d'Oracle vers PostgreSQL.

PL/pgSQL est similaire à PL/SQL sur de nombreux aspects. C'est un langage itératif structuré en blocs et toutes les variables doivent être déclarées. Les assignations, boucles, conditionnelles sont similaires. Les principales différences que vous devez garder à l'esprit quand vous portez de PL/SQL vers PL/pgSQL sont:

37.11.1. Exemples de Portages

Exemple 37-2 montre comment porter une simple fonction de PL/SQL vers PL/pgSQL.

Exemple 37-2. Portage d'une Fonction Simple de PL/SQL vers PL/pgSQL

Voici une fonction en PL/SQL Oracle :

CREATE OR REPLACE FUNCTION cs_fmt_browser_version(v_name IN varchar, v_version IN varchar)
RETURN varchar IS
BEGIN
    IF v_version IS NULL THEN
        RETURN v_name;
    END IF;
    RETURN v_name || '/' || v_version;
END;
/
show errors;

Parcourons cette fonction et voyons les différences avec PL/pgSQL :

  • PostgreSQL n'a pas de paramètres nommés. Vous devez les expliciter ou les aliaser dans votre fonction.

  • Oracle peut avoir des paramètres IN, OUT, et INOUT passées aux fonctions. INOUT, par exemple, signifie que le paramètre recevra une valeur et en renverra une autre. PostgreSQL n'a que les paramètres IN.

  • Le mot clé RETURN dans le prototype de la fonction (pas dans le corps de la fonction) devient RETURNS dans PostgreSQL.

  • Dans PostgreSQL, les fonctions sont créées en utilisant des guillemets simples comme délimiteurs du corps de la fonction, vous devez donc échapper les guillemets simples dans le corps de la fonction.

  • La commande /show errors n'existe pas dans PostgreSQL.

Voici de quoi aurait l'air cette fonction portée sous PostgreSQL :

CREATE OR REPLACE FUNCTION cs_fmt_browser_version(varchar, varchar)
RETURNS varchar AS '
DECLARE
    v_name ALIAS FOR $1;
    v_version ALIAS FOR $2;
BEGIN
    IF v_version IS NULL THEN
        return v_name;
    END IF;
    RETURN v_name || ''/'' || v_version;
END;
' LANGUAGE plpgsql;

L'Exemple 37-3 montre comment porter une fonction qui crée une autre fonction et comment gérer les problèmes de guillemets résultants.

Exemple 37-3. Portage d'une fonction qui crée une autre fonction de PL/SQL vers PL/pgSQL

La procédure suivante récupère des lignes d'une instruction SELECT et construit une grande fonction dont les résultats sont dans une instruction IF pour favoriser l'efficacité. Notez particulièrement les différences dans le curseur et la boucle FOR.

Voici la version Oracle :

CREATE OR REPLACE PROCEDURE cs_update_referrer_type_proc IS
    CURSOR referrer_keys IS 
        SELECT * FROM cs_referrer_keys 
        ORDER BY try_order;

    a_output VARCHAR(4000); 
BEGIN 
    a_output := 'CREATE OR REPLACE FUNCTION cs_find_referrer_type(v_host IN VARCHAR, v_domain IN VARCHAR, 
v_url IN VARCHAR) RETURN VARCHAR IS BEGIN'; 

    FOR referrer_key IN referrer_keys LOOP 
        a_output := a_output || ' IF v_' || referrer_key.kind || ' LIKE ''' || 
referrer_key.key_string || ''' THEN RETURN ''' || referrer_key.referrer_type || 
'''; END IF;'; 
    END LOOP; 

    a_output := a_output || ' RETURN NULL; END;'; 
    EXECUTE IMMEDIATE a_output; 
END; 
/ 
show errors;

Voici comment la fonction serait dans PostgreSQL :

CREATE FUNCTION cs_update_referrer_type_proc() RETURNS integer AS '
DECLARE
    referrer_keys RECORD;  -- Declare a generic record to be used in a FOR
    a_output varchar(4000);
BEGIN 
    a_output := ''CREATE FUNCTION cs_find_referrer_type(varchar, varchar, varchar) 
                  RETURNS varchar AS '''' 
                     DECLARE 
                         v_host ALIAS FOR $1; 
                         v_domain ALIAS FOR $2; 
                         v_url ALIAS FOR $3;
                     BEGIN ''; 

    -- Remarquez comment nous parcourons les résultats d'une requête dans une boucle FOR
    -- en utilisant la construction FOR <record>.

    FOR referrer_keys IN SELECT * FROM cs_referrer_keys ORDER BY try_order LOOP
        a_output := a_output || '' IF v_'' || referrer_keys.kind || '' LIKE '''''''''' 
                 || referrer_keys.key_string || '''''''''' THEN RETURN '''''' 
                 || referrer_keys.referrer_type || ''''''; END IF;''; 
    END LOOP; 
  
    a_output := a_output || '' RETURN NULL; END; '''' LANGUAGE plpgsql;''; 
 
    -- EXECUTE fonctionnera parce que nous ne substituons aucune variable.
    -- Autrement cela échouerait.  Regardez PERFORM pour d'autres façons d'exécuter une fonction.
    
    EXECUTE a_output; 
END; 
' LANGUAGE plpgsql;

Exemple 37-4 montre comment porter une fonction ayant des paramètres OUT et effectuant des manipulations de chaînes. PostgreSQL n'a pas de fonction instr, mais vous pouvez contourner cela en utilisant une combinaison d'autres fonctions. Dans Section 37.11.3 il y a une implémentation PL/pgSQL d'instr que vous pouvez utiliser pour faciliter votre portage.

Exemple 37-4. Portage d'une Procédure avec Manipulation de Chaînes et Paramètres OUT de PL/SQL vers PL/pgSQL

La procédure Oracle suivante est utilisées pour analyser une URL et renvoyer plusieurs éléments (hôte, chemin et requête). Les fonctions PL/pgSQL ne peuvent renvoyer qu'une seule valeur. Dans PostgreSQL, un moyen de contourner cela est de décomposer la procédure en trois fonctions différentes : la première pour renvoyer l'hôte, une autre pour le chemin et la denière pour la requête.

Voici la version Oracle :

CREATE OR REPLACE PROCEDURE cs_parse_url(
    v_url IN VARCHAR,
    v_host OUT VARCHAR,  -- Celle-ci sera passée en retour
    v_path OUT VARCHAR,  -- Celle-là aussi
    v_query OUT VARCHAR) -- Et celle-là
IS
    a_pos1 INTEGER;
    a_pos2 INTEGER;
BEGIN
    v_host := NULL;
    v_path := NULL;
    v_query := NULL;
    a_pos1 := instr(v_url, '//');

    IF a_pos1 = 0 THEN
        RETURN;
    END IF;
    a_pos2 := instr(v_url, '/', a_pos1 + 2);
    IF a_pos2 = 0 THEN
        v_host := substr(v_url, a_pos1 + 2);
        v_path := '/';
        RETURN;
    END IF;

    v_host := substr(v_url, a_pos1 + 2, a_pos2 - a_pos1 - 2);
    a_pos1 := instr(v_url, '?', a_pos2 + 1);

    IF a_pos1 = 0 THEN
        v_path := substr(v_url, a_pos2);
        RETURN;
    END IF;

    v_path := substr(v_url, a_pos2, a_pos1 - a_pos2);
    v_query := substr(v_url, a_pos1 + 1);
END;
/
show errors;

Voici à quoi ressemble la fonction PL/pgSQL renvoyant la partie hôte :

CREATE OR REPLACE FUNCTION cs_parse_url_host(varchar) RETURNS varchar AS '
DECLARE
    v_url ALIAS FOR $1;
    v_host varchar;
    v_path varchar;
    a_pos1 integer;
    a_pos2 integer;
    a_pos3 integer;
BEGIN 
    v_host := NULL; 
    a_pos1 := instr(v_url, ''//'');

    IF a_pos1 = 0 THEN 
        RETURN '''';  -- Renvoie un blanc
    END IF; 

    a_pos2 := instr(v_url,''/'',a_pos1 + 2);
    IF a_pos2 = 0 THEN 
        v_host := substr(v_url, a_pos1 + 2); 
        v_path := ''/''; 
        RETURN v_host; 
    END IF; 

    v_host := substr(v_url, a_pos1 + 2, a_pos2 - a_pos1 - 2 ); 
    RETURN v_host; 
END; 
' LANGUAGE plpgsql;

L'Exemple 37-5 montre comment porter une procédure qui utilise de nombreuses fonctionnalités spécifiques à Oracle.

Exemple 37-5. Portage d'une procédure de PL/SQL vers PL/pgSQL

La version Oracle :

CREATE OR REPLACE PROCEDURE cs_create_job(v_job_id IN INTEGER) IS
    a_running_job_count INTEGER;
    PRAGMA AUTONOMOUS_TRANSACTION;(1)
BEGIN
    LOCK TABLE cs_jobs IN EXCLUSIVE MODE;(2)

    SELECT count(*) INTO a_running_job_count FROM cs_jobs WHERE end_stamp IS NULL;

    IF a_running_job_count > 0 THEN
        COMMIT; -- free lock(3)
        raise_application_error(-20000, 'Unable to create a new job: a job is currently running.');
    END IF;

    DELETE FROM cs_active_job;
    INSERT INTO cs_active_job(job_id) VALUES (v_job_id);

    BEGIN
        INSERT INTO cs_jobs (job_id, start_stamp) VALUES (v_job_id, sysdate);
        EXCEPTION WHEN dup_val_on_index THEN NULL; -- ne vous inquietez pas si cela existe déjà(4)
    END;
    COMMIT;
END;
/
show errors

Les procédures comme celles-ci peuvent être aisément converties en fonctions PostgreSQL renvoyant un integer. Cette procédure en particulier est intéressante parce qu'elle peut nous apprendre diverses choses :

(1)
Il n'y a pas d'instruction PRAGMA dans PostgreSQL.
(2)
Si vous faites un LOCK TABLE dans PL/pgSQL, le verrou ne sera pas libéré jusqu'à ce que la transaction appelante soit terminée.
(3)
Vous pouvez aussi avoir des transactions dans des fonctions PL/pgSQL. La fonction entière (et d'autres fonctions appelées de l'intérieur) est exécutée en une transaction et PostgreSQL annule la transaction si quelque chose se passe mal.
(4)
L'exception when devra être remplacée par une instruction IF.

Voici comment nous pourrions porter cette procédure vers PL/pgSQL :

CREATE OR REPLACE FUNCTION cs_create_job(integer) RETURNS integer AS '
DECLARE
    v_job_id ALIAS FOR $1;
    a_running_job_count integer;
    a_num integer;
BEGIN
    LOCK TABLE cs_jobs IN EXCLUSIVE MODE;
    SELECT count(*) INTO a_running_job_count FROM cs_jobs WHERE end_stamp IS NULL;

    IF a_running_job_count > 0
    THEN
        RAISE EXCEPTION ''Unable to create a new job: a job is currently running.'';
    END IF;

    DELETE FROM cs_active_job;
    INSERT INTO cs_active_job(job_id) VALUES (v_job_id);

    SELECT count(*) INTO a_num FROM cs_jobs WHERE job_id=v_job_id;
    IF NOT FOUND THEN  -- If nothing was returned in the last query
        -- This job is not in the table so lets insert it.
        INSERT INTO cs_jobs(job_id, start_stamp) VALUES (v_job_id, current_timestamp);
        RETURN 1;
    ELSE
        RAISE NOTICE ''Job already running.'';(1)
    END IF;

    RETURN 0;
END;
' LANGUAGE plpgsql;

(1)
Notez que vous pouvez lever des notices (ou erreurs) dans PL/pgSQL.

37.11.2. Autres choses à surveiller

Cette section explique quelques autres choses à surveiller quand on effectue un portage de fonctions PL/SQL Oracle vers PostgreSQL.

37.11.2.1. EXECUTE

La version PL/pgSQL d'EXECUTE fonctionne de façon similaire à la version PL/SQL, mais vous devez vous rappeler d'utiliser quote_literal(text) et quote_string(text) comme décrit dans Section 37.6.4. Les constructions de type EXECUTE ''SELECT * FROM $1''; ne fonctionneront pas à moins d'utiliser ces fonctions.

37.11.2.2. Optimisation des Fonctions PL/pgSQL

PostgreSQL vous donne deux modificateurs de création de fonctions pour optimiser l'exécution : la volatilité (la fonction renvoie toujours le même résultat quand on lui donne les mêmes arguments) et la << rigueur >> (une fonction renvoie NULL si tous ses arguments sont NULL). Consultez la description de CREATE FUNCTION pour les détails.

Pour faire usage de ces attributs d'optimisation, votre instruction CREATE FUNCTION devrait ressembler a ceci:

CREATE FUNCTION foo(...) RETURNS integer AS '
...
' LANGUAGE plpgsql STRICT IMMUTABLE;

37.11.3. Annexe

Cette section contient le code d'une fonction instr compatible Oracle que vous pouvez utiliser pour simplifier vos efforts de portage.

--
-- fonctions instr qui reproduisent la contrepartie Oracle
-- Syntaxe: instr(string1, string2, [n], [m]) où [] signifie paramètre optionnel.
-- 
-- Cherche string1 en commençant par le  n-ième caractère pour la m-ième occurrence
-- de string2.  Si n est négatif, cherche en sens inverse. Si m n'est pas fourni
-- suppose 1 (la recherche commence au premier caractère).
--

CREATE FUNCTION instr(varchar, varchar) RETURNS integer AS '
DECLARE
    pos integer;
BEGIN
    pos:= instr($1, $2, 1);
    RETURN pos;
END;
' LANGUAGE plpgsql;


CREATE FUNCTION instr(varchar, varchar, varchar) RETURNS integer AS '
DECLARE
    string ALIAS FOR $1;
    string_to_search ALIAS FOR $2;
    beg_index ALIAS FOR $3;
    pos integer NOT NULL DEFAULT 0;
    temp_str varchar;
    beg integer;
    length integer;
    ss_length integer;
BEGIN
    IF beg_index > 0 THEN
        temp_str := substring(string FROM beg_index);
        pos := position(string_to_search IN temp_str);

        IF pos = 0 THEN
            RETURN 0;
        ELSE
            RETURN pos + beg_index - 1;
        END IF;
    ELSE
        ss_length := char_length(string_to_search);
        length := char_length(string);
        beg := length + beg_index - ss_length + 2;

        WHILE beg > 0 LOOP
            temp_str := substring(string FROM beg FOR ss_length);
            pos := position(string_to_search IN temp_str);

            IF pos > 0 THEN
                RETURN beg;
            END IF;

            beg := beg - 1;
        END LOOP;

        RETURN 0;
    END IF;
END;
' LANGUAGE plpgsql;


CREATE FUNCTION instr(varchar, varchar, integer, integer) RETURNS integer AS '
DECLARE
    string ALIAS FOR $1;
    string_to_search ALIAS FOR $2;
    beg_index ALIAS FOR $3;
    occur_index ALIAS FOR $4;
    pos integer NOT NULL DEFAULT 0;
    occur_nombre integer NOT NULL DEFAULT 0;
    occur_nombre integer NOT NULL DEFAULT 0;
    temp_str varchar;
    beg integer;
    i integer;
    length integer;
    ss_length integer;
BEGIN
    IF beg_index > 0 THEN
        beg := beg_index;
        temp_str := substring(string FROM beg_index);

        FOR i IN 1..occur_index LOOP
            pos := position(string_to_search IN temp_str);

            IF i = 1 THEN
                beg := beg + pos - 1;
            ELSE
                beg := beg + pos;
            END IF;

            temp_str := substring(string FROM beg + 1);
        END LOOP;

        IF pos = 0 THEN
            RETURN 0;
        ELSE
            RETURN beg;
        END IF;
    ELSE
        ss_length := char_length(string_to_search);
        length := char_length(string);
        beg := length + beg_index - ss_length + 2;
    occur_nombre integer NOT NULL DEFAULT 0;
    temp_str varchar;
    beg integer;
    i integer;
    length integer;
    ss_length integer;
BEGIN
    IF beg_index > 0 THEN
        beg := beg_index;
        temp_str := substring(string FROM beg_index);

        FOR i IN 1..occur_index LOOP
            pos := position(string_to_search IN temp_str);

            IF i = 1 THEN
                beg := beg + pos - 1;
            ELSE
                beg := beg + pos;
            END IF;

            temp_str := substring(string FROM beg + 1);
        END LOOP;

        IF pos = 0 THEN
            RETURN 0;
        ELSE
            RETURN beg;
        END IF;
    ELSE
        ss_length := char_length(string_to_search);
        length := char_length(string);
        beg := length + beg_index - ss_length + 2;

        WHILE beg > 0 LOOP
            temp_str := substring(string FROM beg FOR ss_length);
            pos := position(string_to_search IN temp_str);

            IF pos > 0 THEN
                occur_number := occur_number + 1;

                IF occur_number = occur_index THEN
                    RETURN beg;
                END IF;
            END IF;

            beg := beg - 1;
        END LOOP;

        RETURN 0;
    END IF;
END;
' LANGUAGE plpgsql;