1. Enorable
  2. Base De Datos
  3. Oracle
2
0
 
vistas: 895
Joswald Oropeza
| 10/11/2019

Crear dinámicamente funciones y procedimientos en PL/SQL (Oracle)

Crear funciones y procedimientos en Oracle de manera dinámica permite adaptar ágilimente modificaciones a los procesos ya existentes, pero además son una herramienta eficaz para la resolución de problemas que se puedan presentar durante la implementación de nuevos subprocesos.

Adicionalmente este tipo de métodos le ofrece al usuario la oportunidad de implementar dichos cambios sin que esto traiga consigo largos períodos de respuesta por parte de consultores y programadores. La aplicación de éste tipo de métodos auto-administrables también ayudan a disminuir considerablemente los costos de producción en cualquier tipo de negocio.

Una de las ventajas que se busca con un sistema o aplicación es la capacidad de éste para hacer que la gran mayoría de sus procesos sean flexibles, gracias a la parametrización activa desde el puesto del usuario funcional, permitiendo a su vez una amplia adaptación en el objetivo de resolver otros inconvenientes. Por tal razón, muchos analistas y programadores se las ingenian para crear paquetes, métodos o funciones que ayuden a minimizar los costos desde el punto de vista del tiempo y la puesta en marcha de los proyectos informáticos que se emprenden.

Como la mayoría de los lenguajes de programación, Oracle cuenta con su propio lenguaje interno, conocido como: PL/SQL (Procedural Language/Structured Query Language por sus siglas en inglés). Éste permite trabajar de manera muy amplia con la información que se encuentra almacenada en la base de datos de esta plataforma a través de la inyección de sentencias DDL y DML de SQL, la programación estructurada y la programación procedimental.

En esta oportunidad vamos a mostrar un pequeño y sencillo ejemplo de cómo se puede trabajar con funciones y procedimientos en una plataforma como la de Oracle, implementando código PL/SQL que resulte en nuevos métodos generados de manera dinámica mediante la impresión de líneas de códigos.

Método para crear funciones y procedimientos de manera dinámica con PL/SQL

Para mostrar el objetivo principal de todo lo dicho anteriormente, visualizamos un procedimiento escrito en PL/SQL que simplemente llamaremos: «P_GENE_PROC», el cual se encargará de leer líneas de códigos adicionales encontradas en un pequeño banco de datos que previamente fue creado para servir la información que se podrá imprimir tanto en pantalla como en un archivo externo (extensión .sql) para su posteriormente compilación.

En el ejemplo mostramos paso a paso un método para crear principalmente funciones y procedimientos de manera dinámica en la base de datos de Oracle. Sin embargo, se podría tomar como referencia si lo que se busca es crear paquetes, disparadores (trigger) y otros objetos también de manera dinámica.

Recuerda que si esta publicación te ha servido de ayuda para solucionar una problema informático, puedes apoyar mi contribución realizando una donación (BTC) a través de tu billetera digital preferida. Por otro lado, puedes escribirme tus comentarios o inquietudes a través de nuestro sistema de contacto.

Lo primero que debemos hacer es crear dos tablas base que almacenarán la información que más adelante usará nuestro «P_GENE_PROC» para la generación de nuevos subprocesos. Dichas tablas las he nombrado «T_GENE_PROC_PARAM» y «T_GENE_PROC_VALUE».

[[:error:] corregir lista] ⇩
  • T_GENE_PROC_PARAM - Almancena la información de los parámetros que puede o no contener una función o procedimiento.
  • T_GENE_PROC_VALUE - Almacena la información del código PL/SQL principal que intentará resolver un problema informático. Se trata del núcleo de toda función o procedimiento creado dentro del bloque BEGIN, EXCEPTION, y END.

Creando la tabla T_GENE_PROC_PARAM

Esta tabla la he construído con cinco (5) campos básicos y necesarios para establecer los parámetros que serán requeridos por el subproceso. Estos campos los describimos de la siguiente manera:

  • PARAM_PROC_NAME_IN - Contiene el nombre de nuestro subproceso nuevo.
  • PARAM_ID - Identificador único del parámetro.
  • PARAM_OF_INOUT - Define si el parámetros es de Entrada o Salida («IN», «IN OUT»).
  • PARAM_DATATYPE - Establece el Tipo de Dato del parámetro.
  • PARAM_VALUE - Nombre del parámetro a definir para nuestro subproceso.


CREATE TABLE T_GENE_PROC_PARAM
(PARAM_PROC_NAME_IN VARCHAR2(1000) NOT NULL,
PARAM_ID NUMBER NOT NULL,
PARAM_OF_INOUT VARCHAR2(10) DEFAULT 'IN' NOT NULL,
PARAM_DATATYPE VARCHAR2(40) DEFAULT 'VARCHAR2' NOT NULL,
PARAM_VALUE VARCHAR2(30) DEFAULT 'p_' NOT NULL);
Table created.

Agregamos algunos datos a nuestra tabla T_GENE_PROC_PARAM

Para nuestro ejemplo fueron insertados dos registros para un solo subproceso que necesitamos crear y compilar para nuestra prueba, y el cual nombraremos «f_proceso_ab».

BEGIN
INSERT INTO T_GENE_PROC_PARAM
VALUES
('f_proceso_ab',
1,
'IN',
'VARCHAR2',
'param_1');
INSERT INTO T_GENE_PROC_PARAM
VALUES
('f_proceso_ab',
2,
'IN',
'NUMBER',
'param_2');
COMMIT;
END;
/
Statement processed.

Creando la tabla T_GENE_PROC_VALUE

En este banco de datos solo se requieren cuatro (4) campos para que almacenen la información del código PL/SQL que será compilado posteriormente. Éstos campos fueron definidos de la siguiente manera:

  • PROC_TYPE_VALUE - Es un parámetro que para nuestra prueba solo requiere de dos valores («HEAD» y «BODY»), que hacen referencia a las líneas de código que serán incrustadas tanto en el área de declaración («DECLARE») como en el cuerpo («BEGIN-END») del subproceso.
  • PROC_NAME_IN - Contiene el nombre de nuestro subproceso a crear.
  • PROC_VALUE_LINE - Este campos hace referencia al número de la línea donde estará ubicada la instrucción que construirá la sentencia o el código completo de nuestro subproceso.
  • PROC_VALUE - Almacena una instrucción o líneas de código de hasta 2000 caracteres para nuestro subproceso. Se aconseja definir múltiples líneas de códigos dentro de un mismo subproceso para emular la programación estructurada.

CREATE TABLE T_GENE_PROC_VALUE
(PROC_TYPE_VALUE VARCHAR2(15) DEFAULT 'BODY' NOT NULL,
PROC_NAME_IN VARCHAR2(1000) NOT NULL,
PROC_VALUE_LINE NUMBER NOT NULL,
PROC_VALUE VARCHAR2(2000) NULL);
Table created.

En esta tabla se han insertado siete (7) registros que le darán forma al subproceso «f_proceso_ab» y que fueron definidos de la siguiente manera:

  • El primer registro es del tipo «HEAD», para insertar una línea de código dentro de nuestro DECLARE.
  • Los siguientes seis (6) registros componen el resto de nuestra sentencia del tipo «BODY», donde fueron creadas las siguientes instrucciones:

BEGIN
INSERT INTO T_GENE_PROC_VALUE
VALUES
('HEAD',
'f_proceso_ab',
1,
'v_x VARCHAR2(100) := '''';' );
INSERT INTO T_GENE_PROC_VALUE
VALUES
('BODY',
'f_proceso_ab',
1,
' BEGIN' );
INSERT INTO T_GENE_PROC_VALUE
VALUES
('BODY',
'f_proceso_ab',
2,
' SELECT ''X'' AS VAL' );
INSERT INTO T_GENE_PROC_VALUE
VALUES
('BODY',
'f_proceso_ab',
3,
' INTO v_x' );
INSERT INTO T_GENE_PROC_VALUE
VALUES
('BODY',
'f_proceso_ab',
4,
' FROM DUAL; ' );
INSERT INTO T_GENE_PROC_VALUE
VALUES
('BODY',
'f_proceso_ab',
5,
' -- ' );
INSERT INTO T_GENE_PROC_VALUE
VALUES
('BODY',
'f_proceso_ab',
6,
' RETURN v_x; ' );
INSERT INTO T_GENE_PROC_VALUE
VALUES
('BODY',
'f_proceso_ab',
7,
' END;' );
COMMIT;
END;
/
Statement processed.

NOTA: Al final, los últimos seis registros que hemos insertado en la tabla «T_GENE_PROC_VALUE» deben resultar en una sentencia impresa por nuestro procedimiento, como la que se muestra a continuación:
BEGIN
SELECT 'X' AS VAL
INTO v_x
FROM DUAL;
--
RETURN v_x;
END;

Construyendo el procedimiento P_GENE_PROC

Como mencionamos más arriba, el procedimiento «P_GENE_PROC» es el que nos permitirá leer y organizar todos los datos previamente creados en las tablas «T_GENE_PROC_VALUE» y «T_GENE_PROC_PARAM» hasta poder construir nuestros subprocesos (funciones y procedimientos) para más tarde ser compilados en nuestra base de datos Oracle.

Al desarrollar dicho procedimiento se decidió que éste debía aceptar algunos parámetros de entrada para facilitar su implementación en esta prueba, por lo que he definido las siguientes entradas:

[[:error:] corregir lista] ⇩
  • TYPE_PROC - Hace referencia al tipo de subproceso que requerimos, bien sea una función o un procedimiento («FUNCTION», «PROCEDURE»).
  • NAME_IN - Hace referencia al nombre del nuevo subproceso.
  • NUM_OF_PARAM: Para definir finalmente la cantidad de parámetros que usará el subproceso.
  • IN_OUTPUT_FILE - Definir si se genera el subproceso en un archivo de salida.
  • EXT_FILE_OUTPUT - La extensión del archivo de salida.
  • DATATYPE - Define el Tipo de Dato del retorno, si el subproceso es una función.

CREATE OR REPLACE PROCEDURE P_GENE_PROC
(TYPE_PROC IN VARCHAR2 DEFAULT 'FUNCTION',
NAME_IN IN VARCHAR2 DEFAULT 'f_proceso_ab',
NUM_OF_PARAM IN NUMBER DEFAULT 1,
IN_OUTPUT_FILE IN BOOLEAN DEFAULT FALSE,
EXT_FILE_OUTPUT IN VARCHAR2 DEFAULT 'sql',
DATATYPE IN VARCHAR2 DEFAULT 'VARCHAR2')
IS
c_file CONSTANT VARCHAR2 (1000) := NAME_IN || '.' || EXT_FILE_OUTPUT; -- Nombre del archivo de salida
c_param CONSTANT NUMBER := NUM_OF_PARAM; -- Número de parámetros aceptados por el subproceso
c_output_screen CONSTANT BOOLEAN := NVL(NOT IN_OUTPUT_FILE, TRUE);
c_max_param CONSTANT NUMBER := 5; -- Se define internamente un máximo de hasta 5 parámetros a mostrar
TYPE lines_t IS TABLE OF VARCHAR2 (1000) -- Se declara un tipo de array asociativo
INDEX BY PLS_INTEGER;
salida lines_t; -- Se declara una variable del tipo array asociativo
-- Función para definir los parámetros
FUNCTION F_NUM_OF_PARAM (num_of_param IN NUMBER)
RETURN VARCHAR2
IS
v_param_id NUMBER;
v_param_of_inout VARCHAR2 (10) := 'IN';
v_param_datatype VARCHAR2 (40) := 'VARCHAR2';
v_param_value VARCHAR2 (30) := 'param_';
CURSOR cur_params
IS
SELECT gpp.PARAM_ID as id,
gpp.PARAM_OF_INOUT as inout,
gpp.PARAM_DATATYPE as datatype,
gpp.PARAM_VALUE as val
INTO v_param_id, v_param_of_inout, v_param_datatype, v_param_value
FROM T_GENE_PROC_PARAM gpp
WHERE gpp.PARAM_PROC_NAME_IN = NAME_IN;
v_param VARCHAR2 (100);
BEGIN
DECLARE
v_i NUMBER := 0;
BEGIN
FOR rec IN cur_params
LOOP
v_i := v_i + 1;
v_param_id := rec.id;
v_param_of_inout := rec.inout;
v_param_datatype := rec.datatype;
v_param_value := rec.val;
v_param := v_param || v_param_value || ' ' || v_param_of_inout || ' ' || v_param_datatype ||',';
EXIT WHEN v_i > NUM_OF_PARAM;
END LOOP;
IF v_param IS NULL THEN
FOR idx IN 1 .. c_param
LOOP
v_param := v_param || 'p_' || idx || ' IN VARCHAR2,';
EXIT WHEN idx >= c_max_param;
END LOOP;
END IF;
EXCEPTION
WHEN OTHERS
THEN
v_param := NULL;
END;
RETURN SUBSTR (v_param,1,LENGTH (v_param)-1);
EXCEPTION WHEN OTHERS THEN
RETURN NULL;
END;
-- Procedimiento para agregar líneas de cadenas a la variable "salida"
PROCEDURE P_ADD_LINES (str IN VARCHAR2)
IS
BEGIN
salida (salida.COUNT + 1) := str;
END;
-- Procedimiento para imprimir el proceso
PROCEDURE P_PROC_OUTPUT
IS
BEGIN
IF c_output_screen THEN
FOR idx IN salida.FIRST .. salida.LAST
LOOP
DBMS_OUTPUT.put_line (salida (idx));
END LOOP;
ELSE
/* ******************************************************************************
* Se envía la 'salida' a un archivo en la ubicación «/home/usr/scripts/»,
* pero cambie la ruta de acuerdo a la jerarquía de directorios de su preferencia
* y de su Sistema Operativo.
* Nota: Al ejecutar en Live SQL de Oracle el procedimiento no se compila,
* debido a un error con UTL_FILE.
* ******************************************************************************/
DECLARE
v_file_id UTL_FILE.file_type;
BEGIN
v_file_id := UTL_FILE.fopen ('/home/usr/scripts/', c_file, 'W'); -- Crear archivo con permisos de escritura
/* ******************************************************************************
* El usuario que corre el script debe poseer permisos sobre la ruta del archivo
* En el siguiente enlace se explica cómo conceder permisos de lectura y escritura a un usuario
********************************************************************************/
FOR idx IN salida.FIRST .. salida.LAST
LOOP
UTL_FILE.put_line (v_file_id, salida (idx));
END LOOP;
UTL_FILE.fclose (v_file_id);
DBMS_OUTPUT.put_line ('UN ARCHIVO CON PROCESO DINÁMICO HA SIDO CREADO...');
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line (
'ERROR: Falla al crear archivo ['
|| '/home/usr/scripts/'
|| c_file
|| '] ');
UTL_FILE.fclose (v_file_id);
END;
DBMS_OUTPUT.put_line ('UN ARCHIVO CON PROCESO DINÁMICO HA SIDO PROCESADO...');
END IF;
END;
BEGIN
-- Validar TYPE_PROC IN ('FUNCTION', 'PROCEDURE')
-- Agregando la primera línea de cadena "CREATE OR REPLACE FUNCTION NAME_IN (NUM_OF_PARAM)"
-- Si TYPE_PROC es 'FUNCTION', entonces RETURN DATATYPE, sino es 'PROCEDURE'.
IF TYPE_PROC IN ('FUNCTION','PROCEDURE') THEN
P_ADD_LINES ('CREATE OR REPLACE ' || TYPE_PROC || ' ' || NAME_IN ||' (' || F_NUM_OF_PARAM(c_param) || ') ');
IF TYPE_PROC = 'FUNCTION' THEN
P_ADD_LINES ('RETURN ' || DATATYPE);
END IF;
-- Agregando el resto de las líneas que componen el cuerpo del proceso
P_ADD_LINES ('IS ');
P_ADD_LINES ('/* *************************************************************************************');
P_ADD_LINES (' * Proceso: ' || NAME_IN);
P_ADD_LINES (' * Descripción: ' || INITCAP(TYPE_PROC));
P_ADD_LINES (' * Parámetros: ' || F_NUM_OF_PARAM(c_param));
P_ADD_LINES (' * Retorna: ' || INITCAP(DATATYPE));
P_ADD_LINES (' * *************************************************************************************/');
P_ADD_LINES (' ');
P_ADD_LINES (' -- Variables agregadas dinámicamente ');
-- Agregando el resto de las líneas que componen el cuerpo del proceso
BEGIN
FOR rec IN (SELECT gpb.PROC_VALUE AS head
FROM T_GENE_PROC_VALUE gpb
WHERE gpb.PROC_TYPE_VALUE = 'HEAD'
AND gpb.PROC_NAME_IN = NAME_IN
ORDER BY gpb.PROC_VALUE_LINE)
LOOP
P_ADD_LINES (rec.head);
END LOOP;
END;
P_ADD_LINES ('BEGIN ');
P_ADD_LINES (' -- Líneas agregadas dinámicamente ');
-- Agregando líneas del BODY
DECLARE
v_flag BOOLEAN := FALSE;
BEGIN
FOR rec IN (SELECT gpb.PROC_VALUE AS body
FROM T_GENE_PROC_VALUE gpb
WHERE gpb.PROC_TYPE_VALUE = 'BODY'
AND gpb.PROC_NAME_IN = NAME_IN
ORDER BY gpb.PROC_VALUE_LINE)
LOOP
P_ADD_LINES (rec.body);
v_flag := TRUE;
END LOOP;
IF NOT v_flag THEN
P_ADD_LINES (' NULL; ');
END IF;
END;
P_ADD_LINES (' -- ');
P_ADD_LINES ('END ' || NAME_IN || ';');
P_ADD_LINES ('/');
P_PROC_OUTPUT ();
ELSE
-- Un un mensaje de error se imprime si TYPE_PROC NOT IN ('FUNCTION','PROCEDURE')
DBMS_OUTPUT.put_line (
'ERROR:'
|| ' Falla al generar un proceso dinámico (' || NAME_IN || ').');
DBMS_OUTPUT.put_line (
'REQUIERE: (PARAM) TYPE_PROC ["FUNCTION","PROCEDURE"]');
END IF;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line (
'ERROR ('
|| sqlcode
|| ') Falla al generar lo siguiente:'
|| ' [PROCESO: '
|| TYPE_PROC
|| '], [NOMBRE: '
|| NAME_IN
|| '], [PARÁMETRO(S): '
|| NUM_OF_PARAM
|| '], [TIPO DE DATO: '
|| DATATYPE ||']');
END P_GENE_PROC;
/
Procedure created.

Probando la ejecución del proceso P_GENE_PROC

En este punto ya debimos haber creado y compilado nuestros objetos en la base de datos de Oracle, por lo que procedemos a probar la ejecución de «P_GENE_PROC» que resultará en la generación de los nuevos subprocesos de la siguiente manera:

BEGIN
P_GENE_PROC('FUNCTION','f_proceso_ab',2,FALSE,'sql','VARCHAR2');
P_GENE_PROC('FUNCTION','f_mi_funcion_a',2,FALSE,'sql','NUMBER');
P_GENE_PROC('PROCEDURE','p_mi_procedimiento_a',10,FALSE);
END;
/
Statement processed.

Ahora mostramos la salida que fue generada dinámicamente por nuestro procedimiento.
CREATE OR REPLACE FUNCTION f_proceso_ab (param_1 IN VARCHAR2,param_2 IN NUMBER)
RETURN VARCHAR2
IS
/* *************************************************************************************
* Proceso: f_proceso_ab
* Descripción: Function
* Parámetros: param_1 IN VARCHAR2,param_2 IN NUMBER
* Retorna: Varchar2
* *************************************************************************************/
-- Variables agregadas dinámicamente
v_x VARCHAR2(100) := '';
BEGIN
-- Líneas agregadas dinámicamente
BEGIN
SELECT 'X' AS VAL
INTO v_x
FROM DUAL;
--
RETURN v_x;
END;
--
END f_proceso_ab;
/
CREATE OR REPLACE FUNCTION f_mi_funcion_a (p_1 IN VARCHAR2,p_2 IN VARCHAR2)
RETURN NUMBER
IS
/* *************************************************************************************
* Proceso: f_mi_funcion_a
* Descripción: Function
* Parámetros: p_1 IN VARCHAR2,p_2 IN VARCHAR2
* Retorna: Number
* *************************************************************************************/
-- Variables agregadas dinámicamente
BEGIN
-- Líneas agregadas dinámicamente
NULL;
--
END f_mi_funcion_a;
/
CREATE OR REPLACE PROCEDURE p_mi_procedimiento_a (p_1 IN VARCHAR2,p_2 IN VARCHAR2,p_3 IN VARCHAR2,p_4 IN VARCHAR2,p_5 IN VARCHAR2)
IS
/* *************************************************************************************
* Proceso: p_mi_procedimiento_a
* Descripción: Procedure
* Parámetros: p_1 IN VARCHAR2,p_2 IN VARCHAR2,p_3 IN VARCHAR2,p_4 IN VARCHAR2,p_5 IN VARCHAR2
* Retorna: Varchar2
* *************************************************************************************/
-- Variables agregadas dinámicamente
BEGIN
-- Líneas agregadas dinámicamente
NULL;
--
END p_mi_procedimiento_a;
/
Nuestro procedimiento «P_GENE_PROC» permite validar el correcto funcionamiento de uno de sus parámetros (TYPE_PROC), emitiendo un mensaje de error cuando algo falla como en la siguiente demostración.
BEGIN
P_GENE_PROC('FUNCTIONN','f_proceso_ab',1,FALSE,'sql','VARCHAR2');
P_GENE_PROC('FUNCTIONA','f_mi_funcion_a',2,FALSE,'sql','NUMBER');
P_GENE_PROC('PROCEDURES','p_mi_procedimiento_a');
END;
/
Statement processed.
ERROR: Falla al generar un proceso dinámico (f_proceso_ab).
REQUIERE: (PARAM) TYPE_PROC ["FUNCTION","PROCEDURE"]
ERROR: Falla al generar un proceso dinámico (f_mi_funcion_a).
REQUIERE: (PARAM) TYPE_PROC ["FUNCTION","PROCEDURE"]
ERROR: Falla al generar un proceso dinámico (p_mi_procedimiento_a).
REQUIERE: (PARAM) TYPE_PROC ["FUNCTION","PROCEDURE"]


Probar el resultado impreso tras la corrida de P_GENE_PROC

Muy bien todo hasta aquí, pero vamos a probar el resultado que fue impreso por nuestro «P_GENE_PROC».

CREATE OR REPLACE FUNCTION f_proceso_ab (param_1 IN VARCHAR2,param_2 IN NUMBER)
RETURN VARCHAR2
IS
/* *************************************************************************************
* Proceso: f_proceso_ab
* Descripción: Function
* Parámetros: param_1 IN VARCHAR2,param_2 IN NUMBER
* Retorna: Varchar2
* *************************************************************************************/
-- Variables agregadas dinámicamente
v_x VARCHAR2(100) := '';
BEGIN
-- Líneas agregadas dinámicamente
BEGIN
SELECT 'X' AS VAL
INTO v_x
FROM DUAL;
--
RETURN v_x;
END;
--
END f_proceso_ab;
/
Function created.
CREATE OR REPLACE FUNCTION f_mi_funcion_a (p_1 IN VARCHAR2,p_2 IN VARCHAR2)
RETURN NUMBER
IS
/* *************************************************************************************
* Proceso: f_mi_funcion_a
* Descripción: Function
* Parámetros: p_1 IN VARCHAR2,p_2 IN VARCHAR2
* Retorna: Number
* *************************************************************************************/
-- Variables agregadas dinámicamente
BEGIN
-- Líneas agregadas dinámicamente
NULL;
--
END f_mi_funcion_a;
/
Function created.
CREATE OR REPLACE PROCEDURE p_mi_procedimiento_a (p_1 IN VARCHAR2,p_2 IN VARCHAR2,p_3 IN VARCHAR2,p_4 IN VARCHAR2,p_5 IN VARCHAR2)
IS
/* *************************************************************************************
* Proceso: p_mi_procedimiento_a
* Descripción: Procedure
* Parámetros: p_1 IN VARCHAR2,p_2 IN VARCHAR2,p_3 IN VARCHAR2,p_4 IN VARCHAR2,p_5 IN VARCHAR2
* Retorna: Varchar2
* *************************************************************************************/
-- Variables agregadas dinámicamente
BEGIN
-- Líneas agregadas dinámicamente
NULL;
--
END p_mi_procedimiento_a;
/
Procedure created.

Ahora probaremos el buen funcionamiento de los nuevos subprocesos que fueron creados por P_GENE_CONC.

SELECT
f_proceso_ab ('A', 1)
FROM DUAL;
F_PROCESO_AB('A',1)
X
BEGIN
p_mi_procedimiento_a ('A','B','C','D','E');
END;
/
Statement processed.
SELECT
f_mi_funcion_a ('A','B')
FROM DUAL;
ORA-06503: PL/SQL: Function returned without value

Puede comprobar los resultados arrojados tras la compilación de todas las sentencias creadas mediante Live SQL de Oracle.

Vale destacar que para esta explicación he tomado como referencia el Generador de Paquetes que compartió Steven Feuerstein de Oracle en el 2017 y que de igual manera se puede visualizar con un ejemplo en Live SQL.

Todos los titulares en un solo lugar...