UDFs tabulares (UDTFs) de Java

Este documento explica como escrever uma UDTF (função de tabela definida pelo usuário) em Java.

Neste tópico:

Introdução

Sua classe de manipulador da UDTF Java processa as linhas recebidas na chamada UDTF e retorna um resultado tabular. As linhas recebidas são divididas, seja implicitamente pelo Snowflake ou explicitamente na sintaxe da chamada de função. Você pode usar os métodos implementados na classe para processar linhas individuais, bem como as partições nas quais elas são agrupadas.

Sua classe de manipuladores pode processar partições e linhas com o seguinte:

  • Um construtor sem argumentos como inicializador. Você pode usar isto para configurar o estado de partição com escopo.

  • Um método process para processar cada linha.

  • Um método endPartition sem argumentos como finalizador para completar o processamento da partição, incluindo o retorno de um valor com escopo à partição.

Para obter mais detalhes, consulte Classes para UDTFs de Java (neste tópico).

Cada UDTF de Java também requer uma classe de linha de saída, que especifica os tipos de dados de Java das colunas da(s) linha(s) de saída que são geradas pela classe do manipulador. Você encontra detalhes em A classe da linha de saída (neste tópico).

Notas de uso para particionamento

  • Quando ele recebe linhas que são implicitamente particionadas pelo Snowflake, seu código de manipulador não pode fazer suposições sobre as partições. A execução com particionamento implícito é mais útil quando a UDTF só precisa olhar as linhas isoladamente para produzir sua saída e nenhum estado é agregado entre as linhas. Nesse caso, o código provavelmente não precisa de um construtor ou de um método endPartition.

  • Para melhorar o desempenho, o Snowflake normalmente executa várias instâncias do código do manipulador da UDTF em paralelo. Cada partição de linhas é passada para uma única instância da UDTF.

  • Embora cada partição seja processada por apenas uma instância de UDTF, o inverso não é necessariamente verdadeiro — uma única instância de UDTF pode processar várias partições sequencialmente. Portanto, é importante usar o inicializador e o finalizador para inicializar e limpar cada partição, de forma a evitar passar valores acumulados do processamento de uma partição para o processamento de outra partição.

Nota

Funções tabulares (UDTFs) têm um limite de 500 argumentos de entrada e 500 colunas de saída.

Classes para UDTFs de Java

Os componentes primários da UDTF são a classe do manipulador e a classe da linha de saída.

A classe do manipulador

O Snowflake interage com a UDTF principalmente invocando os seguintes métodos da classe do manipulador:

  • O inicializador (o construtor).

  • O método por linha (process).

  • O método finalizador (endPartition).

A classe do manipulador pode conter métodos adicionais necessários para apoiar esses três métodos.

A classe do manipulador também contém um método getOutputClass, que é descrito mais adiante.

Gerar uma exceção de qualquer método na classe do manipulador (ou na classe da linha de saída) faz com que o processamento seja interrompido. A consulta que chamou a UDTF falha com uma mensagem de erro.

O construtor

Uma classe de manipulador pode ter um construtor, que deve aceitar zero argumentos.

O construtor é invocado uma vez para cada partição antes de qualquer invocação de process.

O construtor não pode produzir linhas de saída.

Use o construtor para inicializar o estado para a partição; esse estado pode ser usado pelos métodos process e endPartition. O construtor também é o lugar adequado para colocar qualquer inicialização de longo prazo que precisa ser feita apenas uma vez por partição e não uma vez por linha.

O construtor é opcional.

O método process

O método process é invocado uma vez para cada linha na partição de entrada.

Os argumentos passados à UDTF são passados ao process. Os valores dos argumentos são convertidos de tipos de dados de SQL para tipos de dados de Java. (Para obter mais informações sobre mapeamento de tipos de dados de SQL e Java, consulte Mapeamentos de tipos de dados SQL-Java).

Os nomes dos parâmetros do método process podem ser quaisquer identificadores de Java válidos; os nomes não precisam corresponder aos nomes especificados na instrução CREATE FUNCTION.

Cada vez que process é chamado, ele pode retornar zero, uma ou várias linhas.

O tipo de dados retornado pelo método process deve ser Stream<OutputRow>, onde Stream é definido em java.util.stream.Stream, e OutputRow é o nome da classe da linha de saída. O exemplo abaixo mostra um método simples process que simplesmente retorna sua entrada através de um Stream:

import java.util.stream.Stream;

...

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

...
Copy

Se o método process não mantiver ou usar nenhum estado no objeto (por exemplo, se o método for projetado para apenas excluir linhas de entrada selecionadas da saída), você pode declarar o método static. Se o método process for static e a classe do manipulador não tiver um construtor ou método endPartition não estático, o Snowflake passará cada linha diretamente para o método process estático sem construir uma instância da classe do manipulador.

Se você precisar pular uma linha de entrada e processar a próxima linha (por exemplo, se você estiver validando as linhas de entrada), retorne um objeto Stream vazio. Por exemplo, o método process abaixo só retorna as linhas para as quais number é um número inteiro positivo. Se number não for positivo, o método retorna um objeto Stream vazio para pular a linha atual e continuar processando a próxima linha.

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

Se process retornar um Stream nulo, o processamento é interrompido. (O método endPartition ainda é chamado mesmo que um Stream nulo seja retornado).

Esse método é exigido.

O método endPartition

Este método opcional pode ser usado para gerar linhas de saída baseadas em qualquer informação de estado agregada em process. Esse método é invocado uma vez para cada partição depois que todas as linhas nessa partição tiverem sido passadas para process.

Se você incluir este método, ele será chamado em cada partição, independentemente de os dados terem sido particionados explícita ou implicitamente. Se os dados não forem particionados de forma significativa, a saída do finalizador pode não ser significativa.

Nota

Se o usuário não particionar os dados explicitamente, o Snowflake particionará os dados implicitamente. Para obter mais detalhes, consulte: partições.

Esse método pode produzir zero, uma ou várias linhas.

Nota

Enquanto o Snowflake oferece suporte a grandes partições com tempos limite ajustados para processá-las com sucesso, as grandes partições em especial podem causar um tempo limite no processamento (como quando endPartition leva muito tempo para ser concluído). Entre em contato com o suporte Snowflake se você precisar ajustar o tempo limite para cenários específicos de uso.

O método getOutputClass

Esse método retorna informações sobre a classe da linha de saída. A classe da linha de saída contém informações sobre os tipos de dados da linha retornada.

A classe da linha de saída

O Snowflake usa a classe da linha de saída para ajudar a especificar as conversões entre tipos de dados de Java e de SQL.

Quando uma UDTF de Java retorna uma linha, o valor em cada coluna da linha deve ser convertido do tipo de dados de Java para o tipo de dados de SQL correspondente. Os tipos de dados de SQL são especificados na cláusula RETURNS da instrução CREATE FUNCTION. Entretanto, o mapeamento entre os tipos de dados de Java e de SQL não tem uma correspondência exata; portanto, o Snowflake precisa saber o tipo de dados de Java para cada coluna retornada. (Para obter mais informações sobre o mapeamento de tipos de dados de SQL e Java, consulte Mapeamentos de tipos de dados SQL-Java).

Uma UDTF de Java especifica os tipos de dados de Java das colunas de saída definindo uma classe de linha de saída. Cada linha retornada da UDTF é retornada como uma instância da classe da linha de saída. Cada instância da classe da linha de saída contém um campo público para cada coluna de saída. O Snowflake lê os valores dos campos públicos de cada instância da classe da linha de saída, converte os valores de Java para valores de SQL e cria uma linha de saída de SQL contendo esses valores.

Os valores em cada instância da classe da linha de saída são definidos chamando o construtor da classe da linha de saída. O construtor aceita parâmetros que correspondem às colunas de saída e depois define os campos públicos para esses parâmetros.

O código abaixo define uma classe de linha de saída de exemplo:

class OutputRow {

  public String name;
  public int id;

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

}
Copy

As variáveis públicas especificadas por essa classe devem corresponder às colunas especificadas na cláusula RETURNS TABLE (...) da instrução CREATE FUNCTION. Por exemplo, a classe OutputRow acima corresponde à cláusula RETURNS abaixo:

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

Importante

A correspondência entre os nomes das colunas de SQL e os nomes dos campos públicos de Java na classe da linha de saída não diferencia maiúsculas e minúsculas. Por exemplo, no código de Java e de SQL mostrado acima, o campo de Java chamado id corresponde à coluna de SQL chamada ID.

A classe da linha de saída é utilizada da seguinte forma:

  • A classe do manipulador usa a classe da linha de saída para especificar o tipo de retorno do método process e do método endPartition. A classe do manipulador também usa a classe da linha de saída para construir valores retornados. Por exemplo:

    public Stream<OutputRow> process(String v) {
      ...
      return Stream.of(new OutputRow(...));
    }
    
    public Stream<OutputRow> endPartition() {
      ...
      return Stream.of(new OutputRow(...));
    }
    
    Copy
  • A classe de linha de saída também é usada no método getOutputClass da classe do manipulador, que é um método estático que o Snowflake chama para aprender os tipos de dados de Java a partir das saídas:

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

Gerar uma exceção de qualquer método na classe da linha de saída (ou classe do manipulador) faz com que o processamento seja interrompido. A consulta que chamou a UDTF falha com uma mensagem de erro.

Resumo dos requisitos

O código da UDTF de Java deve atender aos seguintes requisitos:

  • O código deve definir uma classe de linha de saída.

  • A classe do manipulador da UDTF deve incluir um método público chamado process que retorna um Stream de <output_row_class>, onde Stream é definido em java.util.stream.stream.Stream.

  • A classe do manipulador da UDTF deve definir um método estático público chamado getOutputClass, que deve retornar <output_row_class>.class.

Se o de código Java não atender a esses requisitos, a criação ou execução da UDTF falha:

  • Se a sessão tiver um warehouse ativo no momento em que a instrução CREATE FUNCTION for executada, o Snowflake detectará violações quando a função for criada.

  • Se a sessão não tiver um warehouse ativo no momento em que a instrução CREATE FUNCTION for executada, o Snowflake detectará violações quando a função for chamada.

Exemplos de como chamar UDTFs de Java em consultas

Para obter informações gerais sobre como chamar UDFs e UDTFs, consulte Como chamar uma UDF.

Chamada sem particionamento explícito

Este exemplo mostra como criar uma UDTF. Este exemplo retorna duas cópias de cada entrada e retorna uma linha adicional para cada partição.

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

Este exemplo mostra como chamar uma UDTF. Para manter este exemplo simples, a instrução passa um valor literal em vez de uma coluna e omite a cláusula 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

Este exemplo chama a UDTF com valores lidos de outra tabela. Cada vez que o método process é chamado, ele recebe um valor da coluna city_name da linha atual da tabela cities_of_interest. Como acima, a cláusula UDTF é chamada sem uma cláusula OVER() explícita.

Crie uma tabela simples para ser usada como fonte de entradas:

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

Chame a UDTF de 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

Atenção

Neste exemplo, a sintaxe utilizada na cláusula FROM é idêntica à sintaxe de uma junção interna (ou seja, FROM t1, t2); entretanto, a operação realizada não é uma junção interna verdadeira. O comportamento real é que a função é chamada com os valores de cada linha da tabela. Em outras palavras, dada a seguinte cláusula FROM:

from cities_of_interest, table(f(city_name))
Copy

o comportamento seria equivalente ao seguinte pseudocódigo:

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

A seção de exemplos na documentação para UDTFs de JavaScript contém exemplos mais complexos de consultas que chamam UDTFs com valores de tabelas.

Se a instrução não especificar explicitamente o particionamento, o mecanismo de execução do Snowflake usa particionamento implícito.

Se houver apenas uma partição, o método endPartition é chamado apenas uma vez e a saída da consulta inclui apenas uma linha que contém o valor Created in constructor and output from endPartition(). Se os dados forem agrupados em diferentes números de partições durante diferentes execuções da instrução, o método endPartition é chamado em números de vezes diferentes, e a saída contém números diferentes de cópias dessa linha.

Para obter mais informações, consulte partição implícita.

Chamada com partição explícita

UDTFs de Java também podem ser chamadas usando partição explícita.

Partições múltiplas

O exemplo a seguir utiliza a mesma UDTF e tabela criadas anteriormente. O exemplo particiona os dados por city_name.

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

Partição única

O exemplo a seguir utiliza a mesma UDTF e tabela criadas anteriormente e particiona os dados por uma constante, o que força o Snowflake a usar somente uma única partição:

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

Note que apenas uma cópia da mensagem Created in constructor and output from endPartition() foi incluída na saída, o que indica que endPartition foi chamada apenas uma vez.

Processamento de entradas muito grandes (por exemplo, arquivos grandes)

Em alguns casos, uma UDTF requer uma quantidade muito grande de memória para processar cada linha de entrada. Por exemplo, uma UDTF pode ler e processar um arquivo que é grande demais para caber na memória.

Para processar arquivos grandes em uma UDF ou UDTF, use a classe SnowflakeFile ou InputStream. Para obter mais informações, consulte Processamento de dados não estruturados com manipuladores de procedimento e UDF.