Armazene uma fórmula em uma tabela e use a fórmula em uma função

10

Eu tenho um banco de dados PostgreSQL 9.1 onde parte dele lida com as comissões de agentes. Cada agente tem sua própria fórmula de cálculo de quanta comissão recebe. Eu tenho uma função para gerar a quantidade de comissão que cada agente deve receber, mas está se tornando impossível de usar à medida que o número de agentes aumenta. Sou forçado a fazer algumas declarações de casos extremamente longas e a repetir códigos, o que tornou minha função muito grande.

Todas as fórmulas têm variáveis ​​constantes:

d .. dias trabalhados naquele mês
r .. novos nós necessários
l .. pontuação de lealdade
s .. comissão subagente
b .. taxa básica
i .. receita obtida

A fórmula pode ser algo como:

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

Cada agente negocia a fórmula de pagamento com o departamento de RH. Então, posso armazenar a fórmula na tabela de agentes e ter uma função pequena que apenas obtém a fórmula da tabela e a converte em valores e calcula a quantidade?

indago
fonte

Respostas:

6

Preparar

Suas fórmulas são assim:

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

Eu substituiria as variáveis ​​pela $nnotação para que elas possam ser substituídas pelos valores diretamente no plpgsql EXECUTE(veja abaixo):

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

Você pode armazenar suas fórmulas originais adicionalmente (para o olho humano) ou gerar esse formulário dinamicamente com uma expressão 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');

Apenas certifique-se de que sua tradução é sólida. Alguma explicação para as expressões regexp :

\ m .. corresponde apenas ao início de uma palavra
\ M .. corresponde apenas ao final de uma palavra

Quarto parâmetro 'g'.. substituir globalmente

Função principal

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; 

Ligar:

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

Devoluções:

29.6000000000000000

Pontos principais

  • A função assume o parâmetro 6 value e formula textcomo 7th. Coloquei a fórmula por último, para que possamos usar em $1 .. $6vez de $2 .. $7. Apenas por uma questão de legibilidade.
    Atribuí tipos de dados para os valores como achar melhor. Atribua tipos adequados (para implementar verificações básicas de sanidade) ou apenas faça todos eles numeric:

  • Passe valores para execução dinâmica com a USINGcláusula Isso evita a transmissão para frente e para trás e torna tudo mais simples, seguro e rápido.

  • Eu uso um OUTparâmetro porque é mais elegante e cria uma sintaxe mais clara e clara. Uma final RETURNnão é necessária, o valor do (s) parâmetro (s) OUT é retornado automaticamente.

  • Considere a palestra sobre segurança de @Chris e o capítulo "Como escrever com segurança as funções do DEFINER DE SEGURANÇA" no manual. Na minha concepção, o único ponto de injeção é a própria fórmula.

  • Você pode usar padrões para alguns parâmetros para simplificar ainda mais a chamada.

Erwin Brandstetter
fonte
5

Por favor, leia isso com atenção em relação às considerações de segurança. Essencialmente, você está tentando injetar SQL arbitrário em suas funções. Conseqüentemente, você precisa executar isso em um usuário com permissões altamente restritas.

  1. Crie um usuário e revogue todas as permissões dele. Não conceda permissões ao público no mesmo banco de dados que você faz isso.

  2. Crie uma função para avaliar a expressão, faça-a security definere altere o proprietário para esse usuário restrito.

  3. Pré-processe a expressão e depois passe-a para a função eval () que você criou acima. Você pode fazer isso em outra função, se precisar,

Observe novamente, isso tem sérias implicações de segurança.

Edit: Breve exemplo de código (não testado, mas você deve chegar lá se seguir os 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
fonte
"torná-lo definidor de segurança" é realmente confuso, você pode explicar?
jcolebrand
11
O PostgreSQL possui dois modos de segurança nos quais uma função pode ser executada. INVOKER DE SEGURANÇA é o padrão. DEFINER DE SEGURANÇA significa "executar com o contexto de segurança do proprietário da função", como o bit SETUID no * nix. Para tornar um definidor de segurança da função, você pode especificar isso na declaração da função ( CREATE FUNCTION foo(text) returns text IMMUTABLE LANGUAGE SQL SECURITY DEFINER AS $$...) ou podeALTER FUNCTION foo(text) SECURITY DEFINER
Chris Travers
Ah, então isso é PG lino específico. Peguei vocês. Deveria ter usado acentos graves na resposta ;-)
jcolebrand
@ChrisTravers eu estava esperando algum código de exemplo para avaliar uma fórmula, ou seja, a+bé armazenado em uma coluna de tipo de texto em uma tabela, então eu tenho uma função foo(a int, b int,formula text)se obtiver a fórmula é a + b, como posso fazer a função realmente fazer a + b em vez de eu tendo que ter uma declaração de caso muito longa para todas as fórmulas possíveis e repetir o código em todos os segmentos?
Indago
11
@ indago, acho que você deseja dividir isso em duas camadas por causa de preocupações de segurança. O primeiro é uma camada de interpolação. Você pode usar expressões regulares no PostgreSQL para fazer isso. No nível inferior, você basicamente está executando isso em uma função SQL presa. Você realmente precisa prestar muita atenção à segurança se quiser fazer isso, e também deve prestar muita atenção para retornar valores. Sem saber muito mais, é difícil fazer muito com o código das pessoas, mas a resposta será alterada.
Chris Travers
2

Uma alternativa para apenas armazenar a fórmula e executá-la (que, como Chris mencionou, tem problemas de segurança ) seria ter uma tabela separada chamada formula_stepsque basicamente conteria as variáveis ​​e operadores e a sequência na qual eles são executados. Isso seria um pouco mais trabalhoso, mas seria mais seguro. A tabela pode ficar assim:

formula_steps
-------------
  formula_step_id
  formula_id (FK, referenciado pela tabela de agentes)
  input_1
  input_2
  operador (também pode ser um ID para uma tabela de operadores permitidos, se você não quiser armazenar os símbolos do operador diretamente)
  seqüência

Outra opção seria usar alguma biblioteca / ferramenta de terceiros para avaliar expressões matemáticas. Isso tornaria seu banco de dados menos vulnerável à injeção de SQL, mas agora você acabou de mudar os possíveis problemas de segurança para sua ferramenta externa (que ainda pode ser bastante segura).


A opção final seria escrever (ou baixar) um procedimento que avalie expressões matemáticas. Existem algoritmos conhecidos para esse problema, portanto, não deve ser difícil encontrar informações on-line.

FrustratedWithFormsDesigner
fonte
11
+1 para a terceira opção. Se todas as entradas possíveis forem conhecidas, codifique com força uma seleção de cada uma delas e substitua-as (se e conforme necessário) na fórmula armazenada como texto, em seguida, use uma rotina de biblioteca para avaliar a aritmética. Eliminado o risco de injeção de SQL.
Joel Brown