web-dev-qa-db-fra.com

Alternative de PostgreSQL à la fonction `try_cast` de SQL Server

Microsoft SQL Server a ce que je considère comme une fonction remarquablement sensible, try_cast() qui renvoie un null si le transtypage échoue, plutôt que de déclencher une erreur.

Cela permet ensuite d'utiliser une expression CASE ou coalesce pour se replier. Par exemple:

SELECT coalesce(try_cast(data as int),0);

La question est, est-ce que PostgreSQL a quelque chose de similaire?

La question est posée pour combler certaines lacunes dans mes connaissances, mais il y a aussi le principe général selon lequel certains préfèrent une réaction moins dramatique à certaines erreurs utilisateur. Renvoyer un null est plus facilement pris dans sa foulée en SQL qu'une erreur. Par exemple SELECT * FROM data WHERE try_cast(value) IS NOT NULL;. D'après mon expérience, les erreurs des utilisateurs sont parfois mieux gérées s'il existe un plan B.

6
Manngo

Si la conversion à partir d'un type spécifique vers un autre type spécifique suffit, vous pouvez le faire avec une fonction PL/pgSQL:

create function try_cast_int(p_in text, p_default int default null)
   returns int
as
$$
begin
  begin
    return $1::int;
  exception 
    when others then
       return p_default;
  end;
end;
$$
language plpgsql;

Alors

select try_cast_int('42'), try_cast_int('foo', -1), try_cast_int('bar')

Retour

try_cast_int | try_cast_int | try_cast_int
-------------+--------------+-------------
          42 |           -1 |             

S'il s'agit uniquement de nombres, une autre approche consisterait à utiliser une expression régulière pour vérifier si la chaîne d'entrée est un nombre valide. Ce serait probablement plus rapide que de détecter des exceptions lorsque vous attendez de nombreuses valeurs incorrectes.

5

Raisonnement

Il est difficile d'envelopper quelque chose comme TRY_CAST De SQL Server dans une fonction PostgreSQL générique. L'entrée et la sortie peuvent être de n'importe quel type de données, mais SQL est strictement typé et les fonctions Postgres exigent que les types de paramètres et de retour soient déclarés au moment de la création.

Postgres a le concept de types polymorphes , mais les déclarations de fonction acceptent au plus un type polymorphe. Le manuel:

Les arguments et les résultats polymorphes sont liés les uns aux autres et sont résolus en un type de données spécifique lorsqu'une requête appelant une fonction polymorphe est analysée. Chaque position (argument ou valeur de retour) déclarée comme anyelement est autorisée à avoir n'importe quel type de données réel spécifique, mais dans tout appel donné, elles doivent toutes être les même type réel.

CAST ( expression AS type ) semblerait être une exception à cette règle, prenant n'importe quel type et retournant n'importe quel (autre) type. Mais cast() seulement ressemble à une fonction alors que c'est un élément de syntaxe SQL SQL sous le capot. Le manuel:

[...] Lorsqu'une des deux syntaxes de transtypage standard est utilisée pour effectuer une conversion au moment de l'exécution, elle invoque en interne une fonction enregistrée pour effectuer la conversion.

Il existe une fonction distincte pour chaque combinaison de type d'entrée et de sortie. (Vous pouvez créer le vôtre avec CREATE CAST ...)

Une fonction

Mon compromis est d'utiliser text comme entrée car tout type peut être converti en text. La conversion supplémentaire en text signifie un coût supplémentaire (mais pas beaucoup). Le polymorphisme ajoute également un peu de surcharge. Mais les parties modérément chères sont le SQL dynamique dont nous avons besoin, la concaténation de chaînes impliquée et, surtout, la gestion des exceptions.

Cela dit, cette petite fonction peut être utilisée pour toute combinaison de types y compris les types de tableau. (Mais les modificateurs de type comme dans varchar(20) sont perdus):

CREATE OR REPLACE FUNCTION try_cast(_in text, INOUT _out ANYELEMENT) AS
$func$
BEGIN
   EXECUTE format('SELECT %L::%s', $1, pg_typeof(_out))
   INTO  _out;
EXCEPTION WHEN others THEN
   -- do nothing: _out already carries default
END
$func$  LANGUAGE plpgsql;

Le paramètre INOUT_out A deux fonctions:

  1. déclare le type polymorphe
  2. contient également la valeur par défaut pour les cas d'erreur

Vous ne l'appelleriez pas comme dans votre exemple:

SELECT coalesce(try_cast(data as int),0);

.. où COALESCE élimine également les valeurs NULL authentiques de la source (!!), probablement pas comme prévu. Mais simplement:

SELECT try_cast(data, 0);

.. qui retourne NULL sur NULL entrée, ou 0 sur entrée invalide.

La syntaxe courte fonctionne lorsque data est un type de caractère (comme text ou varchar) et parce que 0 Est un littéral numérique qui est implicitement tapé comme integer. Dans d'autres cas, vous devrez peut-être être plus explicite:

Exemples d'appels

Les littéraux de chaîne non typés fonctionnent hors de la boîte:

SELECT try_cast('foo', NULL::varchar);
SELECT try_cast('2018-01-41', NULL::date);   -- returns NULL
SELECT try_cast('2018-01-41', CURRENT_DATE); -- returns current date

Valeurs typées qui ont un cast implicite enregistré en text travailler hors de la boîte aussi:

SELECT try_cast(name 'foobar', 'foo'::varchar);
SELECT try_cast(my_varchar_column, NULL::numeric);

Liste complète des types de données avec transtypage implicite enregistré en text:

SELECT castsource::regtype
FROM   pg_cast
WHERE  casttarget = 'text'::regtype
AND    castcontext = 'i';

Tous les autres types d'entrée nécessitent une conversion explicite en text:

SELECT try_cast((inet '192.168.100.128/20')::text, NULL::cidr);
SELECT try_cast(my_text_array_column::text, NULL::int[]));

Nous pourrions facilement faire fonctionner le corps de la fonction pour tout type, mais la résolution du type de fonction échoue. En relation:

7
Erwin Brandstetter

Voici un essai générique, probablement très lent.

CREATE OR REPLACE FUNCTION try_cast(p_in text, type regtype, out result text )
RETURNS text AS $$
  BEGIN
    EXECUTE FORMAT('SELECT %L::%s;', $1, $2)
      INTO result;
exception 
    WHEN others THEN result = null;
  END;
$$ LANGUAGE plpgsql;

 SELECT try_cast('2.2','int')::int as "2.2"
   ,try_cast('today','int')::int as "today"
   ,try_cast('222','int')::int as "222";

 SELECT try_cast('2.2','date')::date as "2.2"
   ,try_cast('today','date')::date as "today"
   ,try_cast('222','date')::date as "222";

 SELECT try_cast('2.2','float')::float as "2.2"
   ,try_cast('today','float')::float as "today"
   ,try_cast('222','float')::float as "222";

Cela n'acceptera pas les types comme varchar(20) (bien que nous puissions ajouter un autre paramètre pour accepter "typemod" comme 20.

cette fonction renvoie du texte car les fonctions postgreqsl doivent avoir un type de retour fixe. vous pouvez donc avoir besoin d'un cast explicite en dehors de la fonction pour contraindre le résultat au type souhaité.

1
Jasen