Almacene una fórmula en una tabla y use la fórmula en una función

10

Tengo una base de datos PostgreSQL 9.1 donde parte de ella maneja las comisiones de los agentes. Cada agente tiene su propia fórmula de cálculo de la cantidad de comisión que obtienen. Tengo una función para generar la cantidad de comisión que cada agente debería obtener, pero se está volviendo imposible de usar a medida que aumenta el número de agentes. Me veo obligado a hacer algunas declaraciones de caso extremadamente largas y código repetido, lo que ha hecho que mi función sea muy grande.

Todas las fórmulas tienen variables constantes:

d .. días trabajados ese mes
r .. nuevos nodos adquiridos
l .. puntaje de lealtad
s .. comisión de subagentes
b .. tasa base
i .. ingresos obtenidos

La fórmula puede ser algo como:

d*b+(l*4+r)+(i/d)+s

Cada agente negocia la fórmula de pago con el departamento de recursos humanos. Entonces, ¿puedo almacenar la fórmula en la tabla de agentes y luego tener una pequeña función que solo obtiene la fórmula de la tabla y la traduce con valores y calcula la cantidad?

indago
fuente

Respuestas:

6

Preparar

Sus fórmulas se ven así:

d*b+(l*4+r)+(i/d)+s

Reemplazaría las variables con $nnotación para que puedan reemplazarse con valores directamente en plpgsql EXECUTE(ver a continuación):

$1*$5+($3*4+$2)+($6/$1)+$4

Puede almacenar sus fórmulas originales adicionalmente (para el ojo humano) o generar este formulario dinámicamente con una expresión como:

SELECT regexp_replace(regexp_replace(regexp_replace(
       regexp_replace(regexp_replace(regexp_replace(
      'd*b+(l*4+r)+(i/d)+s'
      , '\md\M', '$1', 'g')
      , '\mr\M', '$2', 'g')
      , '\ml\M', '$3', 'g')
      , '\ms\M', '$4', 'g')
      , '\mb\M', '$5', 'g')
      , '\mi\M', '$6', 'g');

Solo asegúrate de que tu traducción sea sólida. Alguna explicación para las expresiones regexp :

\ m .. coincide solo al comienzo de una palabra
\ M .. coincide solo al final de una palabra

4to parámetro 'g'... reemplazar globalmente

Función básica

CREATE OR REPLACE FUNCTION f_calc(
    d int         --  days worked that month
   ,r int         --  new nodes accuired
   ,l int         --  loyalty score
   ,s numeric     --  subagent commission
   ,b numeric     --  base rate
   ,i numeric     --  revenue gained
   ,formula text
   ,OUT result numeric
)  RETURNS numeric AS
$func$
BEGIN    
   EXECUTE 'SELECT '|| formula
   INTO   result
   USING  $1, $2, $3, $4, $5, $6;                                          
END
$func$ LANGUAGE plpgsql SECURITY DEFINER IMMUTABLE; 

Llamada:

SELECT f_calc(1, 2, 3, 4.1, 5.2, 6.3, '$1*$5+($3*4+$2)+($6/$1)+$4');

Devoluciones:

29.6000000000000000

Puntos principales

  • La función toma 6 parámetros de valor y formula textcomo séptimo. Puse la fórmula al final, para que podamos usar en $1 .. $6lugar de $2 .. $7. Solo por el bien de la legibilidad.
    Asigné tipos de datos para los valores como mejor me parecía. Asigne los tipos adecuados (para implementar controles básicos de cordura) o simplemente haga que todos numeric:

  • Pase valores para la ejecución dinámica con la USINGcláusula Esto evita el vaciado de un lado a otro y hace que todo sea más simple, seguro y rápido.

  • Utilizo un OUTparámetro porque es más elegante y hace una sintaxis más clara y corta. RETURNNo se necesita un final , el valor de los parámetros OUT se devuelve automáticamente.

  • Considere la conferencia sobre seguridad de @Chris y el capítulo "Cómo escribir las funciones de DEFINER DE SEGURIDAD de forma segura" en el manual. En mi diseño, el único punto de inyección es la fórmula misma.

  • Puede usar valores predeterminados para algunos parámetros para simplificar aún más la llamada.

Erwin Brandstetter
fuente
5

Lea esto detenidamente con respecto a las consideraciones de seguridad. Esencialmente, está tratando de inyectar SQL arbitrario en sus funciones. En consecuencia, debe ejecutar esto bajo un usuario con permisos altamente restringidos.

  1. Cree un usuario y revoque todos los permisos de él. No conceda permisos al público en la misma base de datos mientras lo hace.

  2. Cree una función para evaluar la expresión, hágala security definery modifique el propietario a ese usuario restringido.

  3. Preprocese la expresión y luego páselo a la función eval () que creó anteriormente. Puede hacer esto en otra función si lo necesita,

Tenga en cuenta nuevamente, esto tiene serias implicaciones de seguridad.

Editar: breve código de muestra (no probado, pero debería llevarlo allí si sigue los documentos):

CREATE OR REPLACE FUNCTION eval_numeric(text) returns numeric language plpgsql security definer immutable as
$$
declare retval numeric;
begin

execute $e$ SELECT ($1)::numeric$e$ into retval;
return retval;
end;
$$;

ALTER FUNCTION eval_numeric OWNER TO jailed_user;

CREATE OR REPLACE FUNCTION foo(expression text, a numeric, b numeric) returns numeric language sql immutable as $$
select eval(regexp_replace(regexp_replace($1, 'a', $2, 'g'), 'b', '$3', 'g'));
$$; -- can be security invoker, but eval needs to be jailed.
Chris Travers
fuente
"Definir la seguridad" es realmente confuso, ¿puedes explicarlo?
jcolebrand
1
PostgreSQL tiene dos modos de seguridad con los que una función puede ejecutarse. INVOCADOR DE SEGURIDAD es el valor predeterminado. DEFINIDOR DE SEGURIDAD significa "ejecutar con el contexto de seguridad del propietario de la función" como el bit SETUID en * nix. Para definir la seguridad de una función, puede especificar esto en la declaración de función ( CREATE FUNCTION foo(text) returns text IMMUTABLE LANGUAGE SQL SECURITY DEFINER AS $$...) o puedeALTER FUNCTION foo(text) SECURITY DEFINER
Chris Travers,
Oh, eso es específico PG lino. Gotcha Shoulda usó backticks en la respuesta ;-)
jcolebrand
@ChrisTravers esperaba un código de muestra para evaluar una fórmula, a+bes decir, se almacena en una columna de tipo de texto en una tabla, entonces tengo una función foo(a int, b int,formula text)si obtiene la fórmula es a + b, ¿cómo puedo hacer que la función realmente haga a + b en lugar de ¿Tengo que tener una declaración de caso muy larga para todas las fórmulas posibles y repetir el código en todos los segmentos?
indago
1
@indago, creo que quieres dividir esto en dos capas por motivos de seguridad. El primero es una capa de interpolación. Puede usar expresiones regulares en PostgreSQL para hacer esto. En el nivel inferior, básicamente está ejecutando esto en una función SQL encarcelada. Sin embargo, debe prestar mucha atención a la seguridad si va a hacer esto, y también debe prestar mucha atención a los valores de retorno. Sin saber mucho más, es difícil hacer mucho con el código de samople, pero modificará la respuesta.
Chris Travers
2

Una alternativa a simplemente almacenar la fórmula y luego ejecutarla (que, como Chris mencionó, tiene problemas de seguridad ) sería tener una tabla separada llamada formula_stepsque básicamente contendría las variables y operadores y la secuencia en la que se ejecutan. Esto sería un poco más de trabajo, pero sería más seguro. La tabla podría verse así:

pasos_formula
-------------
  formula_step_id
  formula_id (FK, referenciado por la tabla de agentes)
  input_1
  input_2
  operador (también podría ser una ID para una tabla de operadores permitidos, si no desea almacenar símbolos de operador directamente)
  secuencia

Otra opción sería utilizar alguna biblioteca / herramienta de terceros para evaluar expresiones matemáticas. Esto haría que su base de datos sea menos vulnerable a la inyección de SQL, pero ahora acaba de cambiar los posibles problemas de seguridad a su herramienta externa (que aún podría ser bastante segura).


La opción final sería escribir (o descargar) un procedimiento que evalúe expresiones matemáticas. Existen algoritmos conocidos para este problema, por lo que no debería ser difícil encontrar información en línea.

FrustratedWithFormsDesigner
fuente
1
+1 para la tercera opción. Si se conocen todas las entradas potenciales, codifique una selección de cada una de las entradas y sustitúyalas (si es necesario) en la fórmula almacenada como texto, luego use una rutina de biblioteca para evaluar la aritmética. Riesgo de inyección SQL eliminado.
Joel Brown