Introdução a UDFs de JavaScript

Você pode escrever o manipulador para uma função definida pelo usuário (UDF) em JavaScript. Os tópicos desta seção descrevem como criar e escrever um manipulador de JavaScript.

Para uma introdução às UDFs, incluindo uma lista de linguagens na qual você pode escrever um manipulador de UDF, consulte Visão geral das funções definidas pelo usuário.

Quando você tiver um manipulador, você criará a SQL com SQL. Para obter mais informações sobre como usar UDF para criar ou chamar uma UDF, consulte Criação de uma UDF ou Como chamar uma UDF.

É possível capturar dados de registro e rastreamento à medida que o código do manipulador é executado. Para obter mais informações, consulte Visão geral do registro e do rastreamento.

Nota

Para limitações relacionadas a JavaScriptmanipuladores de UDF, consulte Limitações de UDFs de JavaScript.

Neste tópico:

Como funciona um manipulador de JavaScript

Quando um usuário chama uma UDF, o usuário passa o nome e os argumentos da UDF para o Snowflake. Snowflake chama o código do manipulador associado (com argumentos, se houver) para executar a lógica da UDF. A função do manipulador retorna então a saída ao Snowflake, que a repassa ao cliente.

Para cada linha passada para uma UDF, a UDF retorna um valor escalar (ou seja, único) ou, se definida como uma função de tabela, um conjunto de linhas.

Exemplo

O código no exemplo a seguir cria uma UDF chamada my_array_reverse com um código do manipulador que aceita um ARRAY de entrada e retorna um ARRAY contendo os elementos na ordem inversa. O argumento JavaScript e os tipos de retorno são convertidos de e para SQL pelo Snowflake de acordo com os mapeamentos descritos em Mapeamentos de tipos de dados SQL-JavaScript.

Observe que o código de JavaScript deve se referir aos nomes dos parâmetros de entrada usando somente maiúsculas, mesmo que os nomes não estejam em maiúsculas no código de SQL.

-- Create the UDF.
CREATE OR REPLACE FUNCTION my_array_reverse(a ARRAY)
  RETURNS ARRAY
  LANGUAGE JAVASCRIPT
AS
$$
  return A.reverse();
$$
;
Copy

Tipos de dados de JavaScript

UDFs de SQL e JavaScript fornecem tipos de dados semelhantes, mas diferentes, com base em seu suporte de tipos de dados nativos. Objetos dentro do Snowflake e do JavaScript são transferidos utilizando os seguintes mapeamentos.

Integers e Doubles

O JavaScript não tem o tipo integer; todos os números são representados como doubles. UDFs de JavaScript não aceitam enm retornam valores integer, exceto através de conversão de tipo (ou seja, você pode passar um integer para uma UDF de JavaScript que aceite um double).

Tanto o SQL do Snowflake quanto o JavaScript oferecem suporte para valores double. Esses valores são transferidos como estão.

Cadeias de caracteres

Tanto o SQL do Snowflake quanto o JavaScript oferecem suporte para valores de cadeia de caracteres. Esses valores são transferidos como estão.

Valores binários

Todos os valores binários são convertidos em objetos JavaScript Uint8Array. Essas arrays tipificadas podem ser acessadas da mesma forma que arrays comuns de JavaScript, mas são mais eficientes e oferecem suporte para métodos adicionais.

Se uma UDF de JavaScript retorna um objeto Uint8Array, ele é convertido em um valor binário de SQL do Snowflake.

Datas

Todos os tipos de carimbo de data/hora e de data são convertidos em objetos Date() de JavaScript. O tipo de data do JavaScript é equivalente ao TIMESTAMP_LTZ(3) no SQL do Snowflake.

Considere as seguintes notas para UDFs de JavaScript que aceitam uma data ou hora:

  • Toda precisão além de milissegundos é perdida.

  • Um Date de JavaScript gerado a partir de um TIMESTAMP_NTZ de SQL não atua mais como “tempo real”; ele é influenciado pelo horário de verão. Isso é semelhante ao comportamento ao converter TIMESTAMP_NTZ para TIMESTAMP_LTZ.

  • Um Date de JavaScript gerado a partir de um TIMESTAMP_TZ de SQL perde informações de fuso horário, mas representa o mesmo momento no tempo que a entrada (semelhante a quando se converte TIMESTAMP_TZ para TIMESTAMP_LTZ).

  • Um DATE de SQL é convertido para um Date de JavaScript representando a meia-noite do dia atual no fuso horário local.

Além disso, considere as seguintes notas para UDFs de JavaScript que retornam tipos DATE e TIMESTAMP:

  • Objetos Date de JavaScript são convertidos para o tipo de dados do resultado da UDF, aderindo à mesma semântica de conversão que conversões de TIMESTAMP_LTZ(3) para o tipo de dados de retorno.

  • Objetos Date de JavaScript aninhados dentro de objetos VARIANT são sempre do tipo TIMESTAMP_LTZ(3).

Variantes, objetos e arrays

UDFs de JavaScript permitem uma manipulação fácil e intuitiva de dados de JSON e variantes. Objetos variantes passados a uma UDF são transformados em tipos e valores nativos de JavaScript. Qualquer um dos valores listados anteriormente é traduzido em seus tipos correspondentes de JavaScript. Objetos e arrays variantes são convertidos em objetos e arrays de JavaScript. Da mesma forma, todos os valores retornados pela UDF são transformados nos valores adequados da variante. Observe que objetos e arrays retornaos pela UDF estão sujeitos a limitações de tamanho e profundidade.

-- flatten all arrays and values of objects into a single array
-- order of objects may be lost
CREATE OR REPLACE FUNCTION flatten_complete(v variant)
  RETURNS variant
  LANGUAGE JAVASCRIPT
  AS '
  // Define a function flatten(), which always returns an array.
  function flatten(input) {
    var returnArray = [];
    if (Array.isArray(input)) {
      var arrayLength = input.length;
      for (var i = 0; i < arrayLength; i++) {
        returnArray.push.apply(returnArray, flatten(input[i]));
      }
    } else if (typeof input === "object") {
      for (var key in input) {
        if (input.hasOwnProperty(key)) {
          returnArray.push.apply(returnArray, flatten(input[key]));
        }
      }
    } else {
      returnArray.push(input);
    }
    return returnArray;
  }

  // Now call the function flatten() that we defined earlier.
  return flatten(V);
  ';

select value from table(flatten(flatten_complete(parse_json(
'[
  {"key1" : [1, 2], "key2" : ["string1", "string2"]},
  {"key3" : [{"inner key 1" : 10, "inner key 2" : 11}, 12]}
  ]'))));

-----------+
   VALUE   |
-----------+
 1         |
 2         |
 "string1" |
 "string2" |
 10        |
 11        |
 12        |
-----------+
Copy

Argumentos e valores retornados de JavaScript

Argumentos podem ser referenciados diretamente pelo nome dentro do JavaScript. Observe que um identificador não cotado deve ser referenciado com o nome da variável em maiúscula. Como os argumentos e a UDF são referenciados de dentro do JavaScript, eles devem ser identificadores legais de JavaScript. Especificamente, os nomes da UDF e dos argumentos devem começar com uma letra ou $, enquanto os caracteres seguintes podem ser alfanuméricos, $ ou _. Além disso, os nomes não podem ser palavras reservadas de JavaScript.

Os três exemplos a seguir ilustram UDFs que usam argumentos referenciados pelo nome:

-- Valid UDF.  'N' must be capitalized.
CREATE OR REPLACE FUNCTION add5(n double)
  RETURNS double
  LANGUAGE JAVASCRIPT
  AS 'return N + 5;';

select add5(0.0);

-- Valid UDF. Lowercase argument is double-quoted.
CREATE OR REPLACE FUNCTION add5_quoted("n" double)
  RETURNS double
  LANGUAGE JAVASCRIPT
  AS 'return n + 5;';

select add5_quoted(0.0);

-- Invalid UDF. Error returned at runtime because JavaScript identifier 'n' cannot be resolved.
CREATE OR REPLACE FUNCTION add5_lowercase(n double)
  RETURNS double
  LANGUAGE JAVASCRIPT
  AS 'return n + 5;';

select add5_lowercase(0.0);
Copy

Valores NULL e indefinidos

Ao utilizar UDFs de JavaScript, preste muita atenção a linhas e variáveis que possam conter valores NULL. Especificamente, o Snowflake contém dois valores distintos de NULL (NULL de SQL e null de JSON de variantes), enquanto o JavaScript contém o valor undefined além de null.

Argumentos NULL de SQL para uma UDF de JavaScript serão traduzidos ao valor undefined de JavaScript. Da mesma forma, valores undefined de JavaScript retornados serão traduzidos de volta para NULL de SQL. Isso é verdade para todos os tipos de dados, incluindo variantes. Para tipos não variantes, um null de JavaScript retornado também resultará em um valor NULL de SQL.

Argumentos e valores retornados do tipo variante distinguem entre valores undefined e null de JavaScript. Valores NULL de SQL continuam a ser traduzidos como undefined de JavaScript (e undefined de JavaScript de volta para NULL de SQL); o null de JSON variante é traduzido para null de JavaScript (e null de JavaScript de volta para null de JSON variante). Um valor undefined incluído em um objeto (como o valor) ou array de JavaScript fará com que o elemento seja omitido.

Crie uma tabela com uma cadeia de caracteres e um valor NULL:

create or replace table strings (s string);
insert into strings values (null), ('non-null string');
Copy

Crie uma função que converte uma string em um NULL e um NULL em uma cadeia de cadeia de caracteres:

CREATE OR REPLACE FUNCTION string_reverse_nulls(s string)
    RETURNS string
    LANGUAGE JAVASCRIPT
    AS '
    if (S === undefined) {
        return "string was null";
    } else
    {
        return undefined;
    }
    ';
Copy

Chame a função:

select string_reverse_nulls(s) 
    from strings
    order by 1;
+-------------------------+
| STRING_REVERSE_NULLS(S) |
|-------------------------|
| string was null         |
| NULL                    |
+-------------------------+
Copy

Crie uma função que mostre a diferença entre passar um NULL de SQL e passar um null de JSON variante:

CREATE OR REPLACE FUNCTION variant_nulls(V VARIANT)
      RETURNS VARCHAR
      LANGUAGE JAVASCRIPT
      AS '
      if (V === undefined) {
        return "input was SQL null";
      } else if (V === null) {
        return "input was variant null";
      } else {
        return V;
      }
      ';
Copy
select null, 
       variant_nulls(cast(null as variant)),
       variant_nulls(PARSE_JSON('null'))
       ;
+------+--------------------------------------+-----------------------------------+
| NULL | VARIANT_NULLS(CAST(NULL AS VARIANT)) | VARIANT_NULLS(PARSE_JSON('NULL')) |
|------+--------------------------------------+-----------------------------------|
| NULL | input was SQL null                   | input was variant null            |
+------+--------------------------------------+-----------------------------------+
Copy

Crie uma função que mostre a diferença entre retornar undefined, null e uma variante contendo undefined e null (note que o valor undefined é removido da variante retornada):

CREATE OR REPLACE FUNCTION variant_nulls(V VARIANT)
      RETURNS variant
      LANGUAGE JAVASCRIPT
      AS $$
      if (V == 'return undefined') {
        return undefined;
      } else if (V == 'return null') {
        return null;
      } else if (V == 3) {
        return {
            key1 : undefined,
            key2 : null
            };
      } else {
        return V;
      }
      $$;
Copy
select variant_nulls('return undefined'::VARIANT) AS "RETURNED UNDEFINED",
       variant_nulls('return null'::VARIANT) AS "RETURNED NULL",
       variant_nulls(3) AS "RETURNED VARIANT WITH UNDEFINED AND NULL; NOTE THAT UNDEFINED WAS REMOVED";
+--------------------+---------------+---------------------------------------------------------------------------+
| RETURNED UNDEFINED | RETURNED NULL | RETURNED VARIANT WITH UNDEFINED AND NULL; NOTE THAT UNDEFINED WAS REMOVED |
|--------------------+---------------+---------------------------------------------------------------------------|
| NULL               | null          | {                                                                         |
|                    |               |   "key2": null                                                            |
|                    |               | }                                                                         |
+--------------------+---------------+---------------------------------------------------------------------------+
Copy

Conversão de tipo dentro do JavaScript

O JavaScript converterá valores implicitamente entre muitos tipos diferentes. Quando qualquer valor é retornado, o valor é primeiro convertido para o tipo de retorno solicitado antes de ser traduzido para um valor de SQL. Por exemplo, se um número for retornado, mas a UDF for declarada de forma a retornar uma cadeia de caracteres, esse número será convertido para uma cadeia de caracteres dentro do JavaScript. Tenha em mente que erros de programação de JavaScript, tais como retornar o tipo errado, podem ser ocultados por esse comportamento. Além disso, se um erro for gerado durante a conversão do tipo do valor, isso resultará em um erro.

Intervalo de números do JavaScript

A faixa para números com precisão intacta é de

-(2^53 -1)

a

(2^53 -1)

O intervalo de valores válidos nos tipos de dados NUMBER(p, s) e DOUBLE do Snowflake é maior. Recuperar um valor do Snowflake e armazená-lo em uma variável numérica de JavaScript pode resultar em perda de precisão. Por exemplo:

CREATE OR REPLACE FUNCTION num_test(a double)
  RETURNS string
  LANGUAGE JAVASCRIPT
AS
$$
  return A;
$$
;
Copy
select hash(1) AS a, 
       num_test(hash(1)) AS b, 
       a - b;
+----------------------+----------------------+------------+
|                    A | B                    |      A - B |
|----------------------+----------------------+------------|
| -4730168494964875235 | -4730168494964875000 | -235.00000 |
+----------------------+----------------------+------------+
Copy

As duas primeiras colunas devem coincidir, e a terceira deve conter 0.0.

O problema se aplica a funções definidas pelo usuário (UDFs) e procedimentos armazenados de JavaScript.

Se você tiver esse problema em procedimentos armazenados ao usar getColumnValue(), você pode conseguir evitar o problema recuperando um valor como uma cadeia de caracteres, por exemplo com:

getColumnValueAsString()
Copy

Você pode então retornar a cadeia de caracteres do procedimento armazenado e converter a cadeia em um tipo de dados numéricos em SQL.

Erros de JavaScript

Quaisquer erros encontrados durante a execução do JavaScript aparecem para o usuário como erros de SQL. Isso inclui erros de análise, erros de runtime e erros não detectados gerados dentro da UDF. Se o erro contiver um stacktrace, ele será impresso junto com a mensagem de erro. É aceitável gerar um erro sem capturá-lo para finalizar a consulta e produzir um erro de SQL.

Ao depurar, você pode achar útil imprimir valores de argumentos junto com a mensagem de erro para que eles apareçam no texto da mensagem de erro no SQL. Para UDFs determinísticas, isso fornece os dados necessários para reproduzir os erros em um mecanismo local de JavaScript. Um padrão comum é colocar o corpo inteiro de uma UDF de JavaScript em um bloco try-catch, anexar valores de argumento à mensagem do erro capturado e gerar um erro com a mensagem estendida. Você deve considerar a remoção de tais mecanismos antes de implementar UDFs em um ambiente de produção; o registro de valores em mensagens de erro pode revelar involuntariamente dados sensíveis.

A função pode gerar e capturar exceções pré-definidas ou exceções personalizadas. Um exemplo simples de geração de uma exceção personalizada está aqui.

Consulte também Solução de problemas em UDFs de JavaScript.

Segurança de UDFs de JavaScript

UDFs de JavaScript são projetadas para serem seguras e protegidas fornecendo várias camadas de consulta e isolamento de dados:

  • Os recursos de computação dentro do warehouse virtual que executa uma UDF de JavaScript são acessíveis somente de sua conta (ou seja, warehouses não compartilham recursos com outras contas Snowflake).

  • Dados de tabelas são criptografados dentro do warehouse virtual para impedir acesso não autorizado.

  • O código de JavaScript é executado dentro de um mecanismo restrito, impedindo chamadas de sistema do contexto JavaScript (por exemplo, sem acesso à rede e ao disco) e restringindo os recursos do sistema disponíveis para o mecanismo, especificamente a memória.

Como resultado, UDFs de JavaScript podem acessar somente os dados necessários para executar a função definida e não podem afetar o estado do sistema subjacente, além de consumir uma quantidade razoável de memória e tempo do processador.