Stocker une formule dans un tableau et utiliser la formule dans une fonction

10

J'ai une base de données PostgreSQL 9.1 dont une partie gère les commissions des agents. Chaque agent a sa propre formule de calcul du montant de la commission qu'il reçoit. J'ai une fonction pour générer le montant de la commission que chaque agent devrait recevoir, mais il devient impossible de l'utiliser à mesure que le nombre d'agents augmente. Je suis obligé de faire des déclarations de cas extrêmement longues et de répéter du code, ce qui a rendu ma fonction très grande.

Toutes les formules ont des variables constantes:

d .. jours travaillés ce mois-ci
r .. nouveaux nœuds accumulés
l .. score de fidélité
s .. commission de sous-agent
b .. taux de base
i .. revenus gagnés

La formule peut être quelque chose comme:

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

Chaque agent négocie la formule de paiement avec le service RH. Alors, puis-je stocker la formule dans la table des agents, puis avoir comme une petite fonction qui obtient simplement la formule de la table et la traduit avec des valeurs et calcule le montant?

indago
la source

Réponses:

6

Préparer

Vos formules ressemblent à ceci:

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

Je remplacerais les variables par une $nnotation afin qu'elles puissent être remplacées par des valeurs directement dans plpgsql EXECUTE(voir ci-dessous):

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

Vous pouvez également stocker vos formules originales (pour l'œil humain) ou générer ce formulaire dynamiquement avec une expression comme:

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');

Assurez-vous simplement que votre traduction est bonne. Quelques explications pour les expressions regexp :

\ m .. ne correspond qu'au début d'un mot
\ M .. ne correspond qu'à la fin d'un mot

4ème paramètre 'g'.. remplacer globalement

Fonction principale

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; 

Appel:

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

Retour:

29.6000000000000000

Points majeurs

  • La fonction prend 6 paramètres de valeur et formula text7e. J'ai mis la formule en dernier, afin que nous puissions utiliser à la $1 .. $6place de $2 .. $7. Juste pour des raisons de lisibilité.
    J'ai attribué des types de données pour les valeurs comme bon me semblait. Attribuez les types appropriés (pour implémenter les contrôles de base de santé mentale) ou faites-les tous numeric:

  • Passez des valeurs pour l'exécution dynamique avec la USINGclause. Cela évite de lancer dans les deux sens et rend tout plus simple, plus sûr et plus rapide.

  • J'utilise un OUTparamètre parce qu'il est plus élégant et rend la syntaxe plus claire et plus courte. Une finale RETURNn'est pas nécessaire, la valeur du ou des paramètres OUT est retournée automatiquement.

  • Considérez la conférence sur la sécurité par @Chris et le chapitre "Écrire des fonctions de DEFINER DE SECURITE en toute sécurité" dans le manuel. Dans ma conception, le point d'injection unique est la formule elle-même.

  • Vous pouvez utiliser des valeurs par défaut pour certains paramètres pour simplifier davantage l'appel.

Erwin Brandstetter
la source
5

Veuillez lire attentivement ce qui concerne les considérations de sécurité. Essentiellement, vous essayez d'injecter du SQL arbitraire dans vos fonctions. Par conséquent, vous devez l'exécuter sous un utilisateur avec des autorisations très limitées.

  1. Créez un utilisateur et révoquez-en toutes les autorisations. N'accordez pas d'autorisations au public dans la même base de données que vous le faites.

  2. Créez une fonction pour évaluer l'expression, créez-la security defineret modifiez le propriétaire de cet utilisateur restreint.

  3. Prétraitez l'expression, puis passez-la à la fonction eval () que vous avez créée ci-dessus. Vous pouvez le faire dans une autre fonction si vous en avez besoin,

Notez à nouveau que cela a de sérieuses implications pour la sécurité.

Edit: Bref exemple de code (non testé mais devrait vous y rendre si vous suivez les documents):

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
la source
«rendre la sécurité plus précise» est vraiment déroutant, pouvez-vous expliquer?
jcolebrand
1
PostgreSQL dispose de deux modes de sécurité sous lesquels une fonction peut s'exécuter. SECURITY INVOKER est la valeur par défaut. SECURITY DEFINER signifie "exécuté avec le contexte de sécurité du propriétaire de la fonction", un peu comme le bit SETUID sur * nix. Pour faire un définisseur de sécurité de fonction, vous pouvez le spécifier dans la déclaration de fonction ( CREATE FUNCTION foo(text) returns text IMMUTABLE LANGUAGE SQL SECURITY DEFINER AS $$...) ou vous pouvezALTER FUNCTION foo(text) SECURITY DEFINER
Chris Travers
Oh, c'est donc PG lino spécifique. Je t'ai eu. Shoulda a utilisé des backticks dans la réponse ;-)
jcolebrand
@ChrisTravers, je m'attendais à ce qu'un exemple de code évalue une formule, c'est-à a+b- dire qu'il soit stocké dans une colonne de type texte dans un tableau, alors j'ai une fonction foo(a int, b int,formula text)si elle obtient la formule est a + b comment puis-je faire en sorte que la fonction fasse réellement un + b au lieu de -je devoir avoir un énoncé de cas très long pour toutes les formules possibles et répéter le code dans tous les segments?
indago
1
@indago, je pense que vous voulez diviser cela en deux couches pour des raisons de sécurité. Le premier est une couche d'interpolation. Vous pouvez utiliser des expressions rationnelles dans PostgreSQL pour ce faire. Au niveau inférieur, vous exécutez essentiellement cela dans une fonction SQL emprisonnée. Vous devez vraiment faire très attention à la sécurité si vous voulez le faire, et vous devez également faire très attention aux valeurs de retour. Sans en savoir beaucoup plus, il est difficile de faire grand-chose avec le code samople mais modifiera la réponse.
Chris Travers
2

Une alternative au simple stockage de la formule puis à son exécution (qui, comme Chris l'a mentionné, a des problèmes de sécurité ) serait d'avoir une table distincte appelée formula_stepsqui contiendrait essentiellement les variables et les opérateurs et la séquence dans laquelle ils sont exécutés. Ce serait un peu plus de travail, mais ce serait plus sûr. Le tableau pourrait ressembler à ceci:

formule_étapes
-------------
  formula_step_id
  formula_id (FK, référencé par la table des agents)
  input_1
  input_2
  opérateur (peut également être un ID à une table d'opérateurs autorisés, si vous ne souhaitez pas stocker directement les symboles d'opérateur)
  séquence

Une autre option serait d'utiliser une bibliothèque / un outil tiers pour évaluer les expressions mathématiques. Cela rendrait votre base de données moins vulnérable à l'injection SQL, mais maintenant vous venez de déplacer les problèmes de sécurité possibles vers votre outil externe (qui pourrait encore être assez sûr).


La dernière option serait d'écrire (ou de télécharger) une procédure qui évalue les expressions mathématiques. Il existe des algorithmes connus pour ce problème, il ne devrait donc pas être difficile de trouver des informations en ligne.

FrustratedWithFormsDesigner
la source
1
+1 pour la troisième option. Si toutes les entrées potentielles sont connues, codez en dur une sélection de chacune des entrées et remplacez-les (si et si nécessaire) dans la formule stockée sous forme de texte, puis utilisez une routine de bibliothèque pour évaluer l'arithmétique. Risque d'injection SQL éliminé.
Joel Brown