UDFs Java tabulaires (UDTFs)

Ce document explique comment écrire une UDTF (fonction de table définie par l’utilisateur) en Java.

Dans ce chapitre :

Introduction

Votre classe de gestionnaire d’UDTF Java traite les lignes reçues dans l’appel de l’UDTF et renvoie un résultat sous forme de tableau. Les lignes reçues sont partitionnées, soit implicitement par Snowflake, soit explicitement dans la syntaxe de l’appel de fonction. Vous pouvez utiliser les méthodes que vous implémentez dans la classe pour traiter des lignes individuelles ainsi que les partitions dans lesquelles elles sont regroupées.

Votre classe de gestionnaire peut traiter les partitions et les lignes de la manière suivante :

  • Un constructeur à zéro argument comme initialisateur. Vous pouvez l’utiliser pour configurer l’état de la partition.

  • Une méthode process pour traiter chaque ligne.

  • Une méthode endPartition à zéro argument comme finalisateur pour terminer le traitement de la partition, y compris le retour d’une valeur délimitée à la partition.

Pour plus de détails, voir Classes Java pour UDTFs (dans cette rubrique).

Chaque UDTF Java nécessite également une classe de ligne de sortie, qui spécifie les types de données Java des colonnes de la ou des lignes de sortie générées par la classe de gestion. Les détails sont inclus dans La classe de ligne de sortie (dans cette rubrique).

Notes sur l’utilisation du partitionnement

  • Lorsqu’il reçoit des lignes qui sont implicitement partitionnées par Snowflake, votre code de gestionnaire ne peut pas faire d’hypothèses sur les partitions. L’exécution sans partitionnement implicite est particulièrement utile lorsque l’UDTF n’a que besoin d’examiner les lignes en isolation pour produire sa sortie, et aucun état n’est agrégé d’une ligne à l’autre. Dans ce cas, le code n’a probablement pas besoin d’un constructeur ou d’une méthode endPartition.

  • Pour améliorer les performances, Snowflake exécute généralement plusieurs instances du code de gestion de l’UDTF en parallèle. Chaque partition de lignes est transmise à une seule instance de l’UDTF.

  • Bien que chaque partition ne soit traitée que par une seule instance d’UDTF, l’inverse n’est pas nécessairement vrai – une seule instance d’UDTF peut traiter plusieurs partitions séquentiellement. Il est donc important d’utiliser l’initialiseur et le finaliseur pour initialiser et nettoyer chaque partition afin d’éviter de reporter les valeurs accumulées du traitement d’une partition au traitement d’une autre partition.

Note

Les fonctions tabulaires (UDTFs) ont une limite de 500 arguments d’entrée et 500 colonnes de sortie.

Classes Java pour UDTFs

Les principaux composants de l’UDTF sont la classe de gestion et la classe de ligne de sortie.

La classe de gestion

Snowflake interagit avec l’UDTF principalement en appelant les méthodes suivantes de la classe de gestion :

  • L’initialiseur (le constructeur).

  • La méthode par ligne (process).

  • La méthode finaliseur (endPartition).

La classe de gestion peut contenir des méthodes supplémentaires nécessaires à la prise en charge de ces trois méthodes.

La classe de gestionnaire contient également une méthode getOutputClass, qui est décrite plus loin.

Le lancement d’une exception à partir de n’importe quelle méthode de la classe de gestion (ou de la ligne de sortie) entraîne l’arrêt du traitement. La requête qui a appelé l’UDTF échoue avec un message d’erreur.

Le constructeur

Une classe de gestion peut avoir un constructeur, qui doit prendre zéro argument.

Le constructeur est appelé une fois pour chaque partition avant tout appel de process.

Le constructeur ne peut pas produire de lignes de sortie.

Utilisez le constructeur pour initialiser l’état de la partition ; cet état peut être utilisé par les méthodes process et endPartition. Le constructeur est également l’endroit approprié pour placer toute initialisation de longue durée qui doit être effectuée une seule fois par partition plutôt qu’une fois par ligne.

Le constructeur est facultatif.

La méthode process

Cette méthode process est appelée une fois pour chaque ligne de la partition d’entrée.

Les arguments transmis à l’UDTF sont transmis à process. Les valeurs des arguments sont converties de types de données SQL en types de données Java. (Pour des informations sur le mappage de type de données SQL et Java, voir Mappages de type de données SQL-Java).

Les noms des paramètres de la méthode process peuvent être n’importe quels identificateurs Java valides ; les noms ne doivent pas nécessairement correspondre aux noms spécifiés dans l’instruction CREATE FUNCTION.

Chaque fois que process est appelée, elle peut renvoyer zéro, une ou plusieurs lignes.

Le type de données renvoyé par la méthode process doit être Stream<OutputRow>, où le flux (Stream) est défini dans java.util.stream.Stream et OutputRow est le nom de la classe de ligne de sortie. L’exemple ci-dessous montre une méthode process simple qui renvoie simplement son entrée via un flux :

import java.util.stream.Stream;

...

public Stream<OutputRow> process(String v) {
  return Stream.of(new OutputRow(v));
}

...
Copy

Si la méthode process ne conserve ou n’utilise aucun état dans l’objet (par exemple, si la méthode est conçue pour exclure de la sortie les lignes d’entrée sélectionnées), vous pouvez déclarer la méthode static. Si la méthode process est static et que la classe du gestionnaire ne possède pas de constructeur ou de méthode endPartition non statique, Snowflake transmet chaque ligne directement à la méthode process statique sans construire une instance de la classe du gestionnaire.

Si vous devez ignorer une ligne d’entrée et traiter la ligne suivante (par exemple, si vous validez les lignes d’entrée), renvoyez un objet Stream vide. Par exemple, la méthode process ci-dessous ne renvoie que les lignes pour lesquelles number est un nombre entier positif. Si number n’est pas positif, la méthode renvoie un objet Stream vide pour ignorer la ligne actuelle et poursuivre le traitement de la ligne suivante.

public Stream<OutputRow> process(int number) {
  if (inputNumber < 1) {
    return Stream.empty();
  }
  return Stream.of(new OutputRow(number));
}
Copy

Si process renvoie un flux nul, le traitement s’arrête. (La méthode endPartition est toujours appelée même si un flux nul est renvoyé).

Cette méthode est requise.

La méthode endPartition

Cette méthode facultative peut être utilisée pour générer des lignes de sortie qui sont basées sur n’importe quelle information d’état agrégée dans process. Cette méthode est appelée une fois pour chaque partition, après que toutes les lignes de cette partition ont été transmises à process.

Si vous incluez cette méthode, elle est appelée sur chaque partition, que les données aient été partitionnées explicitement ou implicitement. Si les données ne sont pas partitionnées de manière significative, la sortie du finaliseur peut ne pas être significative.

Note

Si l’utilisateur ne partitionne pas les données explicitement, alors Snowflake partitionne les données implicitement. Pour plus de détails, voir : partitions.

Cette méthode peut produire zéro, une ou plusieurs lignes.

Note

Bien que Snowflake prenne en charge les grandes partitions avec des délais d’expiration définis pour les traiter avec succès, les partitions particulièrement grandes peuvent entraîner des expirations (par exemple lorsque endPartition prend trop de temps à se terminer). Veuillez contacter le support Snowflake si vous avez besoin d’ajuster le seuil d’expiration pour des scénarios d’utilisation spécifiques.

La méthode getOutputClass

Cette méthode renvoie des informations sur la classe de ligne de sortie. La classe de ligne de sortie contient des informations sur les types de données de la ligne renvoyée.

La classe de ligne de sortie

Snowflake utilise la classe de ligne de sortie pour aider à spécifier les conversions entre les types de données Java et les types de données SQL.

Lorsqu’une UDTF Java renvoie une ligne, la valeur de chaque colonne de la ligne doit être convertie du type de données Java au type de données SQL correspondant. Les types de données SQL sont spécifiés dans la clause RETURNS de l’instruction CREATE FUNCTION. Toutefois, le mappage entre les types de données Java et SQL n’est pas de 1 pour 1, de sorte que Snowflake doit connaître le type de données Java pour chaque colonne renvoyée. (Pour plus d’informations sur le mappage de types de données SQL et Java, voir Mappages de type de données SQL-Java).

Une UDTF Java spécifie les types de données Java des colonnes de sortie en définissant une classe de ligne de sortie. Chaque ligne renvoyée par l’UDTF est renvoyée comme une instance de la classe de ligne de sortie. Chaque instance de la classe de ligne de sortie contient un champ public pour chaque colonne de sortie. Snowflake lit les valeurs des champs publics de chaque instance de la classe de ligne de sortie, convertit les valeurs Java en valeurs SQL et construit une ligne de sortie SQL contenant ces valeurs.

Les valeurs de chaque instance de la classe de ligne de sortie sont définies en appelant le constructeur de la classe de ligne de sortie. Le constructeur accepte les paramètres qui correspondent aux colonnes de sortie, puis définit les champs publics en fonction de ces paramètres.

Le code ci-dessous définit un exemple de classe de ligne de sortie :

class OutputRow {

  public String name;
  public int id;

  public OutputRow(String pName, int pId) {
    this.name = pName;
    this.id = pId
  }

}
Copy

Les variables publiques spécifiées par cette classe doivent correspondre aux colonnes spécifiées dans la clause RETURNS TABLE (...) de l’instruction CREATE FUNCTION. Par exemple, la classe OutputRow ci-dessus correspond à la clause RETURNS ci-dessous :

CREATE FUNCTION F(...)
    RETURNS TABLE(NAME VARCHAR, ID INTEGER)
    ...
Copy

Important

La correspondance entre les noms des colonnes SQL et les noms des champs publics Java dans la classe de ligne de sortie n’est pas sensible à la casse. Par exemple, dans le code Java et SQL présenté ci-dessus, le champ Java nommé id correspond à la colonne SQL nommée ID.

La classe de lignes de sortie est utilisée comme suit :

  • La classe de gestion utilise la classe de ligne de sortie pour spécifier le type de retour de la méthode process et de la méthode endPartition. La classe de gestion utilise également la classe de ligne de sortie pour construire les valeurs retournées. Par exemple :

    public Stream<OutputRow> process(String v) {
      ...
      return Stream.of(new OutputRow(...));
    }
    
    public Stream<OutputRow> endPartition() {
      ...
      return Stream.of(new OutputRow(...));
    }
    
    Copy
  • La classe de ligne de sortie est également utilisée dans la méthode getOutputClass de la classe de gestion, qui est une méthode statique que Snowflake appelle afin d’apprendre les types de données Java des sorties :

    public static Class getOutputClass() {
      return OutputRow.class;
    }
    
    Copy

Le lancement d’une exception à partir de n’importe quelle méthode de la classe de ligne de sortie (ou de la classe de gestionnaire) entraîne l’arrêt du traitement. La requête qui a appelé l’UDTF échoue avec un message d’erreur.

Résumé des exigences

Le code Java de l’UDTF doit répondre aux exigences suivantes :

  • Le code doit définir une classe de ligne de sortie.

  • La classe de gestion de l’UDTF doit inclure une méthode publique nommée process qui renvoie un flux <output_row_class>, où le flux (Stream) est défini dans java.util.stream.Stream.

  • La classe de gestion de l’UDTF doit définir une méthode statique publique nommée getOutputClass, qui doit renvoyer <output_row_class>.class.

Si le code Java ne répond pas à ces exigences, la création ou l’exécution de l’UDTF échoue :

  • Si la session a un entrepôt actif au moment où l’instruction CREATE FUNCTION s’exécute, alors Snowflake détecte des violations lors de la création de la fonction.

  • Si la session n’a pas d’entrepôt actif au moment où l’instruction CREATE FUNCTION est exécutée, Snowflake détecte des violations lorsque la fonction est appelée.

Exemples d’appels d’UDTFs Java dans des requêtes

Pour des informations générales sur l’appel d’UDFs et d’UDTFs, voir Appel d’une UDF.

Appel sans partitionnement explicite

Cet exemple montre comment créer une UDTF. Cet exemple renvoie deux copies de chaque entrée et renvoie une ligne supplémentaire pour chaque partition.

create function return_two_copies(v varchar)
returns table(output_value varchar)
language java
handler='TestFunction'
target_path='@~/TestFunction.jar'
as
$$

  import java.util.stream.Stream;

  class OutputRow {

    public String output_value;

    public OutputRow(String outputValue) {
      this.output_value = outputValue;
    }

  }


  class TestFunction {

    String myString;

    public TestFunction()  {
      myString = "Created in constructor and output from endPartition()";
    }

    public static Class getOutputClass() {
      return OutputRow.class;
    }

    public Stream<OutputRow> process(String inputValue) {
      // Return two rows with the same value.
      return Stream.of(new OutputRow(inputValue), new OutputRow(inputValue));
    }

    public Stream<OutputRow> endPartition() {
      // Returns the value we initialized in the constructor.
      return Stream.of(new OutputRow(myString));
    }

  }

$$;
Copy

Cet exemple montre comment appeler une UDTF. Pour que cet exemple reste simple, l’instruction transmet une valeur littérale plutôt qu’une colonne et omet la clause OVER().

SELECT output_value
   FROM TABLE(return_two_copies('Input string'));
+-------------------------------------------------------+
| OUTPUT_VALUE                                          |
|-------------------------------------------------------|
| Input string                                          |
| Input string                                          |
| Created in constructor and output from endPartition() |
+-------------------------------------------------------+
Copy

Cet exemple appelle l’UDTF avec des valeurs lues à partir d’une autre table. Chaque fois que la méthode process est appelée, on lui transmet une valeur de la colonne city_name de la ligne actuelle de la table cities_of_interest. Comme ci-dessus, l’UDTF est appelée sans clause OVER() explicite.

Créez une table simple à utiliser comme source de données :

CREATE TABLE cities_of_interest (city_name VARCHAR);
INSERT INTO cities_of_interest (city_name) VALUES
    ('Toronto'),
    ('Warsaw'),
    ('Kyoto');
Copy

Appelez l’UDTF Java :

SELECT city_name, output_value
   FROM cities_of_interest,
       TABLE(return_two_copies(city_name))
   ORDER BY city_name, output_value;
+-----------+-------------------------------------------------------+
| CITY_NAME | OUTPUT_VALUE                                          |
|-----------+-------------------------------------------------------|
| Kyoto     | Kyoto                                                 |
| Kyoto     | Kyoto                                                 |
| Toronto   | Toronto                                               |
| Toronto   | Toronto                                               |
| Warsaw    | Warsaw                                                |
| Warsaw    | Warsaw                                                |
| NULL      | Created in constructor and output from endPartition() |
+-----------+-------------------------------------------------------+
Copy

Attention

Dans cet exemple, la syntaxe utilisée dans la clause FROM est identique à la syntaxe d’une jointure interne (c’est-à-dire FROM t1, t2) ; cependant, l’opération effectuée n’est pas une jointure interne vraie. Le comportement réel est que la fonction est appelée avec les valeurs de chaque ligne de la table. En d’autres termes, étant donné la clause FROM suivante :

from cities_of_interest, table(f(city_name))
Copy

le comportement serait équivalent au pseudo-code suivant :

for city_name in cities_of_interest:
    output_row = f(city_name)
Copy

La section d’exemples de la documentation des UDTFs JavaScript contient des exemples plus complexes de requêtes qui appellent des UDTFs avec des valeurs provenant de tables.

Si l’instruction ne spécifie pas explicitement le partitionnement, le moteur d’exécution Snowflake utilise le partitionnement implicite.

S’il n’y a qu’une seule partition, la méthode endPartition n’est appelée qu’une seule fois et la sortie de la requête ne comprend qu’une seule ligne contenant la valeur Created in constructor and output from endPartition(). Si les données sont regroupées en un nombre différent de partitions lors des différentes exécutions de l’instruction, la méthode endPartition est appelée un nombre différent de fois, et la sortie contient un nombre différent de copies de cette ligne.

Pour plus d’informations, voir partitionnement implicite.

Appel avec partitionnement explicite

Les UDTFs Java peuvent également être appelées en utilisant un partitionnement explicite.

Partitions multiples

L’exemple suivant utilise la même UDTF et la même table créées précédemment. L’exemple partitionne les données par nom de ville.

SELECT city_name, output_value
   FROM cities_of_interest,
       TABLE(return_two_copies(city_name) OVER (PARTITION BY city_name))
   ORDER BY city_name, output_value;
+-----------+-------------------------------------------------------+
| CITY_NAME | OUTPUT_VALUE                                          |
|-----------+-------------------------------------------------------|
| Kyoto     | Created in constructor and output from endPartition() |
| Kyoto     | Kyoto                                                 |
| Kyoto     | Kyoto                                                 |
| Toronto   | Created in constructor and output from endPartition() |
| Toronto   | Toronto                                               |
| Toronto   | Toronto                                               |
| Warsaw    | Created in constructor and output from endPartition() |
| Warsaw    | Warsaw                                                |
| Warsaw    | Warsaw                                                |
+-----------+-------------------------------------------------------+
Copy

Partition unique

L’exemple suivant utilise la même UDTF et la même table créées précédemment et partitionne les données par une constante, ce qui oblige Snowflake à n’utiliser qu’une seule partition :

SELECT city_name, output_value
   FROM cities_of_interest,
       TABLE(return_two_copies(city_name) OVER (PARTITION BY 1))
   ORDER BY city_name, output_value;
+-----------+-------------------------------------------------------+
| CITY_NAME | OUTPUT_VALUE                                          |
|-----------+-------------------------------------------------------|
| Kyoto     | Kyoto                                                 |
| Kyoto     | Kyoto                                                 |
| Toronto   | Toronto                                               |
| Toronto   | Toronto                                               |
| Warsaw    | Warsaw                                                |
| Warsaw    | Warsaw                                                |
| NULL      | Created in constructor and output from endPartition() |
+-----------+-------------------------------------------------------+
Copy

Notez que seule une copie du message Created in constructor and output from endPartition() a été incluse dans la sortie, ce qui indique que endPartition n’a été appelé qu’une seule fois.

Traitement d’entrées très volumineuses (par exemple, des fichiers volumineux)

Dans certains cas, une UDTF nécessite une très grande quantité de mémoire pour traiter chaque ligne d’entrée. Par exemple, une UDTF peut lire et traiter un fichier trop volumineux pour tenir dans la mémoire.

Pour traiter des fichiers volumineux dans une UDF ou une UDTF, utilisez la classe SnowflakeFile ou InputStream. Pour plus d’informations, voir Traitement des données non structurées avec des gestionnaires d’UDF et de procédures.