Criação de UDFs de Java

Este tópico ajuda você a criar UDFs de Java.

Neste tópico:

Como escolher seus tipos de dados

Antes de escrever seu código:

  • Escolha os tipos de dados que sua função deve aceitar como argumentos e o tipo de dados que sua função deve retornar.

  • Leve em conta questões relacionadas ao fuso horário.

  • Decida como lidar com valores NULL.

Mapeamentos de tipos de dados SQL-Java para tipos de parâmetros e retorno

A tabela abaixo mostra os mapeamentos de tipo entre SQL e Java. Esses mapeamentos geralmente se aplicam tanto aos argumentos passados à UDF de Java quanto aos valores retornados da UDF. Entretanto, há algumas exceções, que estão listadas em notas de rodapé.

Alguns tipos de dados de SQL (por exemplo, NUMBER) são compatíveis com vários tipos de dados de Java (por exemplo, int, long, etc.). Nesses casos, você pode usar qualquer tipo de dados de Java que tenha capacidade suficiente para manter os valores reais que serão passados. Se você passar um valor de SQL para um tipo de dados de Java incompatível (ou vice-versa), o Snowflake gerará um erro.

Tipo de SQL

Tipo de Java

Notas

NUMBER

short

Não pode ser nulo. Deve caber na faixa de valores do short (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/minutos do short).

NUMBER

Short

Deve caber na faixa de valores do short (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/minutos do short).

NUMBER

int

Não pode ser nulo. Deve caber na faixa do int (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/mínimos do int).

NUMBER

Inteiro

Deve caber na faixa do int (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/mínimos do int).

NUMBER

long

Não pode ser nulo. Deve caber na faixa do long (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/mínimos do long).

NUMBER

Long

Deve caber na faixa do long (nenhuma parte fracionária, e a parte inteira não pode exceder os valores máximos/mínimos do long).

NUMBER

java.math.BigDecimal

NUMBER

java.math.BigInteger

Deve caber na faixa do BigInteger (nenhuma parte fracionária).

NUMBER

Cadeia de caracteres

FLOAT

double

Não pode ser nulo.

FLOAT

Double

FLOAT

float

Não pode ser nulo. Pode resultar em perda de precisão.

FLOAT

Float

Pode resultar em perda de precisão.

FLOAT

Cadeia de caracteres

Pode resultar em perda de precisão (a conversão float -> string causa perdas).

VARCHAR

Cadeia de caracteres

BINARY

byte[]

BINARY

Cadeia de caracteres

Codifica a cadeia de caracteres binária em hexadecimal. 4

BINARY

InputStream

Expõe o valor BINARY como uma sequência de bytes.

BOOLEAN

booleano

Não pode ser nulo.

BOOLEAN

Booleano

BOOLEAN

Cadeia de caracteres

4

DATE

java.sql.Date

DATE

Cadeia de caracteres

Formata a data como YYYY-MM-DD. 4

TIME

java.sql.Time

3

TIME

Cadeia de caracteres

Formata o tempo como HH:MI:SS.SSSSSSSSS, onde a parte fracionária dos segundos depende da precisão do tempo. 3

TIMESTAMP_LTZ

java.sql.Timestamp

Deve caber na faixa do java.sql.Timestamp. 3

TIMESTAMP_LTZ

Cadeia de caracteres

O formato de saída é DY, DD MON YYYY HH24:MI:SS TZHTZM. 1 , 3 , 4

TIMESTAMP_NTZ

java.sql.Timestamp

Deve caber na faixa de java.sql.Timestamp. Trata o tempo real como uma compensação da época Unix (efetivamente impondo um fuso horário UTC). 3

TIMESTAMP_NTZ

Cadeia de caracteres

Trata o tempo real como uma compensação da época Unix (efetivamente impondo um fuso horário UTC). O formato de saída é DY, DD MON YYYY HH:MI:SS. 2 , 3 , 4

TIMESTAMP_TZ

java.sql.Timestamp

Deve caber na faixa do java.sql.Timestamp. 3

TIMESTAMP_TZ

Cadeia de caracteres

O formato de saída é DY, DD MON YYYY HH24:MI:SS TZHTZM. 1 , 3 , 4

VARIANT

Variant

O tipo de dados Variant é uma classe no pacote Snowpark. Para obter mais informações, consulte Tipos suportados do pacote Snowpark. Para obter um exemplo, consulte Como passar um valor VARIANT para uma UDF de Java inline.

OBJECT

Map<String, String>

As chaves do mapa são as chaves do objeto e os valores são formatados como cadeias de caracteres.

OBJECT

Cadeia de caracteres

Formata o objeto como uma cadeia de caracteres JSON (por exemplo, {"x": 3, "y": true}).

ARRAY

String[]

Formata os elementos da array como cadeias de caracteres.

ARRAY

Cadeia de caracteres

Formata a matriz como uma cadeia de cadeia de caracteres JSON (por exemplo, [1, "foo", null]).

GEOGRAPHY

Cadeia de caracteres

Formata a geografia como GeoJSON.

GEOGRAPHY

Geography

5

1(1,2)

O formato corresponde ao formato de carimbo de data/hora da internet (RFC) DY, DD MON YYYY HH24:MI:SS TZHTZM, conforme descrito em Formatos de carimbo de data/hora. Se uma diferença de fuso horário (o componente TZHTZM) estiver presente, normalmente são dígitos (por exemplo, -0700 indica 7 horas atrás de UTC). Se a diferença do fuso horário for Z (de “Zulu”) e não dígitos, isso é sinônimo de “+0000” (UTC).

2

O formato corresponde ao formato de carimbo de data/hora da internet (RFC) DY, DD MON YYYY HH24:MI:SS, conforme descrito em Formatos de carimbo de data/hora. Se a cadeia de cadeia de caracteres for seguida por um espaço e Z (de “Zulu”), isso indica explicitamente que a diferença é “+0000” (UTC).

3(1,2,3,4,5,6,7,8)

Embora o Snowflake possa armazenar valores de tempo com precisão de nanossegundos, a biblioteca de tempo java.sql.time mantém apenas uma precisão de milissegundos. A conversão entre os tipos de dados do Snowflake e do Java pode reduzir a precisão efetiva para milissegundos.

4(1,2,3,4,5,6)

Esse tipo de mapeamento é suportado na conversão de argumentos de SQL para Java, mas não na conversão de tipos de retorno de Java para tipos de SQL.

5

O Java não tem um tipo de dados Geography nativo. O tipo de dados Geography referido aqui é uma classe do pacote Snowpark. Para obter mais informações, consulte Tipos suportados do pacote Snowpark.

Tipos suportados do pacote Snowpark

Em uma função definida pelo usuário, é possível usar um subconjunto específico de tipos que estão incluídos no pacote do Snowflake Snowpark Java package. Embora estes tipos sejam projetados para uso em código do Snowpark, alguns também são suportados para uso em UDFs pela conveniência que podem proporcionar. (Para obter mais informações sobre o Snowpark, consulte a documentação Snowpark.)

Os tipos do Snowpark na tabela a seguir são suportados em código de UDF. Você não deve usar outros tipos do Snowpark em código de UDF; eles não são suportados lá.

Tipo do Snowpark

Versão necessária do Snowpark

Descrição

Geography

1.2.0 e superior

Representa o tipo do Snowflake GEOGRAPHY. Para um exemplo que utiliza o tipo de dados Geography, consulte Como passar um valor GEOGRAPHY para uma UDF de Java inline.

Variant

1.4.0 e superior

Representa dados de VARIANT do Snowflake. Para um exemplo que utiliza o tipo de dados Variant, consulte Como passar um valor VARIANT para uma UDF de Java inline.

Como especificar o pacote Snowpark como uma dependência de UDF

Ao desenvolver o código de uma UDF que utiliza o pacote Snowpark, você precisará configurar seu ambiente de desenvolvimento para que possa compilar e executar o código com dependências do Snowpark. Para obter mais informações, consulte Como configurar outros ambientes de desenvolvimento para o Snowpark Java.

Ao implantar uma UDF executando a instrução CREATE FUNCTION, você pode especificar o pacote Snowpark como uma dependência sem carregar o arquivo JAR para um estágio (a biblioteca já está no Snowflake). Para isso, especifique o nome e a versão do pacote na cláusula PACKAGES. Para obter um exemplo de sintaxe, consulte Como passar um valor GEOGRAPHY para uma UDF de Java inline.

Valores TIMESTAMP_LTZ e fusos horários

Uma UDF de Java fica bastante isolada do ambiente no qual é chamada. Entretanto, o fuso horário é herdado do ambiente de chamada. Se a sessão do chamador definir um fuso horário padrão antes de chamar a UDF de Java, a UDF de Java tem o mesmo fuso horário padrão. A UDF de Java utiliza os mesmos dados da base de dados de fuso horário IANA usados pelo TIMEZONE SQL do Snowflake nativo (isso é, dados do lançamento 2021a do banco de dados de fuso horário).

Valores NULL

O Snowflake oferece suporte para dois valores NULL distintos: SQL NULL e JSON de VARIANT null. (Para obter mais informações sobre o VARIANT NULL do Snowflake, consulte Valores NULL).

O Java oferece suporte para um valor null, que é apenas para tipos de dados não-primitivos.

Um argumento SQL NULL para uma UDF de Java é convertido para o valor de Java null, mas somente para tipos de dados de Java que oferecem suporte para null.

Um valor de Java null retornado é convertido de volta para um NULL de SQL.

Arrays e número variável de argumentos

UDFs de Java podem receber arrays de qualquer um dos seguintes tipos de dados de Java:

  • Cadeia de caracteres

  • booleano

  • double

  • float

  • int

  • long

  • short

O tipo de dados dos valores de SQL passados deve ser compatível com o tipo de dados de Java correspondente. Para obter mais detalhes sobre compatibilidade de tipos de dados, consulte Mapeamentos de tipos de dados SQL-Java para tipos de parâmetros e retorno.

As seguintes regras adicionais se aplicam para cada um dos tipos de dados de Java especificados:

  • boolean: a ARRAY do Snowflake deve conter somente elementos BOOLEAN, e não deve conter nenhum valor NULL.

  • int/short/long: a ARRAY do Snowflake deve conter somente elementos de ponto fixo com uma escala de 0, e não deve conter nenhum valor NULL.

  • float/double: a ARRAY do Snowflake deve conter um dos seguintes:

    A ARRAY não deve conter nenhum valor NULL.

Os métodos de Java podem receber essas arrays de uma das duas maneiras a seguir:

  • Usando o recurso de array do Java.

  • Usando o recurso varargs (número variável de argumentos) do Java.

Em ambos os casos, seu código de SQL deve passar uma ARRAY.

Passando por uma ARRAY

Declare o parâmetro de Java como uma array. Por exemplo, o terceiro parâmetro no método a seguir é uma array de cadeias de caracteres:

static int myMethod(int fixedArgument1, int fixedArgument2, String[] stringArray)

Abaixo está um exemplo completo:

Criar e carregar a tabela:

CREATE TABLE string_array_table(id INTEGER, a ARRAY);
INSERT INTO string_array_table (id, a) SELECT
        1, ARRAY_CONSTRUCT('Hello');
INSERT INTO string_array_table (id, a) SELECT
        2, ARRAY_CONSTRUCT('Hello', 'Jay');
INSERT INTO string_array_table (id, a) SELECT
        3, ARRAY_CONSTRUCT('Hello', 'Jay', 'Smith');

Crie a UDF:

create or replace function concat_varchar_2(a ARRAY)
returns varchar
language java
handler='TestFunc_2.concatVarchar2'
target_path='@~/TestFunc_2.jar'
as
$$
    class TestFunc_2 {
        public static String concatVarchar2(String[] strings) {
            return String.join(" ", strings);
        }
    }
$$;

Chame a UDF:

SELECT concat_varchar_2(a)
    FROM string_array_table
    ORDER BY id;
+---------------------+
| CONCAT_VARCHAR_2(A) |
|---------------------|
| Hello               |
| Hello Jay           |
| Hello Jay Smith     |
+---------------------+

Como passar usando o Varargs

O uso do varargs é muito semelhante ao uso de uma array.

Em seu código de Java, use o estilo de declaração do varargs de Java:

static int myMethod(int fixedArgument1, int fixedArgument2, String ... stringArray)

Abaixo está um exemplo completo. A única diferença significativa entre este exemplo e o exemplo anterior (para arrays) é a declaração dos parâmetros para o método.

Criar e carregar a tabela:

CREATE TABLE string_array_table(id INTEGER, a ARRAY);
INSERT INTO string_array_table (id, a) SELECT
        1, ARRAY_CONSTRUCT('Hello');
INSERT INTO string_array_table (id, a) SELECT
        2, ARRAY_CONSTRUCT('Hello', 'Jay');
INSERT INTO string_array_table (id, a) SELECT
        3, ARRAY_CONSTRUCT('Hello', 'Jay', 'Smith');

Crie a UDF:

create or replace function concat_varchar(a ARRAY)
returns varchar
language java
handler='TestFunc.concatVarchar'
target_path='@~/TestFunc.jar'
as
$$
    class TestFunc {
        public static String concatVarchar(String ... stringArray) {
            return String.join(" ", stringArray);
        }
    }
$$;

Chame a UDF:

SELECT concat_varchar(a)
    FROM string_array_table
    ORDER BY id;
+-------------------+
| CONCAT_VARCHAR(A) |
|-------------------|
| Hello             |
| Hello Jay         |
| Hello Jay Smith   |
+-------------------+

Criação de UDFs de Java que ficam dentro das restrições impostas pelo Snowflake

Para garantir a estabilidade dentro do ambiente do Snowflake, o Snowflake impõe as seguintes restrições a UDFs de Java. A menos que o contrário seja indicado, essas limitações são aplicadas quando a UDF é executada, não quando ela é criada.

Memória

Evite consumir memória demais.

  • Grandes valores de dados (normalmente tipo de dados BINARY, VARCHAR longos ou ARRAY ou OBJECT ou VARIANT grandes) podem consumir uma grande quantidade de memória.

  • Uma profundidade de pilha excessiva pode consumir uma grande quantidade de memória. O Snowflake testou chamadas de funções simples com aninhamento de 50 níveis de profundidade sem erros. O limite máximo prático depende de quanta informação é colocada na pilha.

UDFs retornam um erro se elas consumirem memória demais. O limite específico está sujeito a mudanças.

Hora

Evite algoritmos que demorem muito tempo por chamada.

Se uma UDF leva tempo demais para ser concluída, o Snowflake interrompe a instrução SQL e retorna um erro ao usuário. Isso limita o impacto e o custo de erros como loops infinitos.

Bibliotecas

Embora seu método de Java possa usar classes e métodos nas bibliotecas padrão de Java, as restrições de segurança do Snowflake desabilitam algumas capacidades, tais como escrever em arquivos. Para obter mais detalhes sobre restrições de bibliotecas, consulte a seção intitulada Boas práticas de segurança.

Criação da classe

Quando uma instrução de SQL chama sua UDF de Java, o Snowflake chama um método de Java que você escreveu. Seu método de Java é chamado de “método do manipulador”, ou “manipulador” abreviadamente.

Como em qualquer método de Java, seu método deve ser declarado como parte de uma classe. Seu método do manipulador pode ser um método estático ou um método de instância da classe. Se seu manipulador é um método de instância e sua classe definir um construtor sem argumentos, o Snowflake invocará seu construtor no momento da inicialização para criar uma instância de sua classe. Se seu manipulador for um método estático, sua classe não é obrigada a ter um construtor.

O manipulador é chamado uma vez para cada linha passada para a UDF de Java. (Nota: uma nova instância da classe não é criada para cada linha; o Snowflake pode chamar o mesmo método do manipulador da mesma instância mais de uma vez, ou chamar o mesmo método estático mais de uma vez).

Para otimizar a execução de seu código, o Snowflake supõe que a inicialização pode ser lenta, enquanto a execução do método do manipulador é rápida. O Snowflake define um tempo limite mais longo para executar a inicialização (incluindo o tempo para carregar sua UDF e o tempo para chamar o construtor do método do manipulador contendo a classe, se um construtor estiver definido) do que para executar o manipulador (o tempo para chamar seu manipulador com uma linha de entrada).

Informações adicionais sobre a criação da classe podem ser encontradas em Criação de UDFs Java.

Como otimizar a inicialização e controlar o estado global em UDFs escalares

A maioria das UDFs escalares deve seguir as diretrizes abaixo:

  • Se você precisar inicializar o estado compartilhado que não muda entre linhas, inicialize-o no construtor da classe da UDF.

  • Escreva seu método do manipulador de forma a ser thread-safe.

  • Evite armazenar e compartilhar o estado dinâmico em linhas diferentes.

Se sua UDF não puder seguir essas diretrizes, ou se você quiser entender mais profundamente as razões para essas diretrizes, leia as próximas subseções.

Introdução

O Snowflake espera que o UDFs escalares seja processadas de forma independente. Confiar em um estado compartilhado entre invocações pode resultar em comportamento inesperado, pois o sistema pode processar linhas em qualquer ordem e espalhar essas invocações por várias JVMs. UDFs devem evitar confiar em um estado compartilhado entre diferentes chamadas para o método do manipulador. Entretanto, há duas situações em que você pode querer que uma UDF armazene um estado compartilhado:

  • Código que contém uma lógica de inicialização cara que você não quer repetir para cada linha.

  • Código que use um estado compartilhado em linhas diferentes, como um cache.

Se você precisar compartilhar o estado em várias linhas, e se esse estado não mudar com o tempo, use um construtor para criar um estado compartilhado definindo variáveis em nível de instância. O construtor é executado apenas uma vez por instância, enquanto o manipulador é chamado uma vez por linha. Portanto, inicializar no construtor é mais barato quando um manipulador processa várias linhas. E como o construtor é chamado apenas uma vez, o construtor não precisa ser escrito de forma a ser thread-safe.

Se sua UDF armazena um estado compartilhado que muda, seu código deve estar preparado para lidar com o acesso simultâneo a esse estado. As duas próximas seções fornecem mais informações sobre paralelismo e estados compartilhados.

Entendendo a paralelização de UDF de Java

Para melhorar o desempenho, o Snowflake paraleliza tanto entre JVMs diferente quanto dentro de JVMs.

  • Entre JVMs diferentes:

    O Snowflake paralela entre os trabalhadores em um warehouse. Cada trabalhador executa uma (ou mais) JVMs. Isso significa que não existe um estado global compartilhado. No máximo, o estado só pode ser compartilhado dentro de uma única JVM.

  • Dentro de JVMs:

    • Cada JVM pode executar vários threads que podem chamar o mesmo método do manipulador da mesma instância em paralelo. Isso significa que cada método do manipulador precisa ser thread-safe.

    • Se uma UDF é IMMUTABLE e uma instrução SQL chama a UDF mais de uma vez com os mesmos argumentos para a mesma linha, a UDF retorna o mesmo valor para cada chamada para aquela linha. Por exemplo, o código a seguir retorna o mesmo valor duas vezes para cada linha se a UDF for IMMUTABLE:

      select
             my_java_udf(42),
             my_java_udf(42)
          from table1;
      

      Se você quiser que múltiplas chamadas retornem valores independentes mesmo quando passarem os mesmos argumentos não quiser declarar a função como VOLATILE, ligue diferentes chamadas a UDFs separadas ao mesmo método do manipulador. Por exemplo:

      1. Crie um arquivo JAR chamado @java_udf_stage/rand.jar com o código:

        class MyClass {
        
            private double x;
        
            // Constructor
            public MyClass()  {
                x = Math.random();
            }
        
            // Handler
            public double myHandler() {
                return x;
            }
        }
        
      2. Crie UDFs de Java da forma mostrada abaixo. Essas UDFs têm nomes diferentes, mas usam o mesmo arquivo JAR e o mesmo manipulador dentro desse arquivo JAR.

        create function my_java_udf_1()
            returns double
            language java
            imports = ('@java_udf_stage/rand.jar')
            handler = 'MyClass.myHandler';
        
        create function my_java_udf_2()
            returns double
            language java
            imports = ('@java_udf_stage/rand.jar')
            handler = 'MyClass.myHandler';
        
      3. O código a seguir chama ambas as UDFs. As UDFs apontam para o mesmo arquivo e manipulador JAR. Essas chamadas criam duas instâncias da mesma classe. Cada instância retorna um valor independente, portanto o exemplo abaixo retorna dois valores independentes em vez de retornar o mesmo valor duas vezes:

        select
                my_java_udf_1(),
                my_java_udf_2()
            from table1;
        

Como armazenar informações de estado de uma JVM

Uma razão para evitar confiar no estado dinâmico compartilhado é que as linhas não são necessariamente processadas em uma ordem previsível. Cada vez que uma instrução SQL é executada, o Snowflake pode variar o número de lotes, a ordem em que os lotes são processados e a ordem das linhas dentro de um lote. Se uma UDF escalar for projetada para que uma linha afete o valor de retorno para uma linha posterior, a UDF pode retornar resultados diferentes cada vez que a UDF for executada.

Tratamento de erros

Um método de Java usado como uma UDF pode usar as técnicas normais de tratamento de exceções de Java para detectar erros dentro do método.

Se uma exceção ocorre dentro do método e não é capturada pelo método, o Snowflake gera um erro que inclui o traço da pilha para a exceção.

Você pode gerar uma exceção explicitamente sem capturá-la para encerrar a consulta e produzir um erro de SQL. Por exemplo:

if (x < 0)  {
    throw new IllegalArgumentException("x must be non-negative.");
    }

Ao depurar, você pode incluir valores no texto da mensagem de erro do SQL. Para fazer isso, coloque todo um corpo de método de Java em um bloco try-catch; anexe valores de argumento à mensagem do erro capturado; e gere uma exceção com a mensagem estendida. Para evitar revelar dados sensíveis, remova valores de argumento antes de enviar arquivos JAR para um ambiente de produção.

Práticas recomendadas

  • Escreva código independente de plataforma.

    • Evite códigos que suponham uma arquitetura de CPU específica (por exemplo, x86).

    • Evite códigos que suponham um sistema operacional específico.

  • Se você precisar executar o código de inicialização e não quiser incluí-lo no método que você chama, você pode colocar o código de inicialização em um bloco de inicialização estático.

Consulte também:

Boas práticas de segurança

  • Seu método (e qualquer método de biblioteca que você chamar) deve agir como uma função pura, agindo somente sobre os dados que recebe e retornando um valor baseado nesses dados, sem causar efeitos colaterais. Seu código não deve tentar afetar o estado do sistema subjacente, a não ser consumir uma quantidade razoável de memória e tempo de processador.

  • UDFs de Java são executadas dentro de um mecanismo restrito. Nem seu código nem o código nos métodos de biblioteca que você utiliza devem empregar quaisquer chamadas de sistema proibidas, inclusive:

    • Controle de processo. Por exemplo, você não pode bifurcar um processo. (Entretanto, você pode usar threads múltiplos).

    • Acesso ao sistema de arquivos.

      Com as seguintes exceções, UDFs de Java não devem ler ou escrever arquivos:

      • UDFs de Java podem ler arquivos especificados na cláusula imports do comando CREATE FUNCTION. Para obter mais informações, consulte CREATE FUNCTION.

      • UDFs de Java podem escrever arquivos, tais como arquivos de log, no diretório /tmp.

        Cada consulta recebe seu próprio sistema de arquivo com memória, no qual seu próprio /tmp é armazenado, de modo que consultas diferentes não podem ter conflitos de nome de arquivo.

        Entretanto, conflitos dentro de uma consulta são possíveis se uma única consulta chamar mais de uma UDF e essas UDFs tentarem escrever no mesmo nome de arquivo.

    • Acesso à rede.

      Nota

      Como seu código não pode acessar a rede direta ou indiretamente, você não pode usar o código no driver JDBC do Snowflake para acessar o banco de dados. Sua UDF em si não pode agir como cliente do Snowflake.

  • JNI (Java Native Interface) não é suportado. O Snowflake proíbe o carregamento de bibliotecas que contenham código nativo (ao contrário de código de bytes de Java).

  • Quando usado dentro de uma região do governo, UDFs de Java oferecem suporte para algoritmos de criptografia que são validados para atender aos requisitos do padrão Federal Information Processing Standard (140-2) (FIPS 140-2). Somente podem ser usados algoritmos criptográficos que são permitidos no modo aprovado pelo FIPS da API de criptografia BouncyCastle para Java. Para obter mais informações sobre o FIPS 140-2, consulte FIPS 140-2.