JavaScript UDFs 简介

可以使用 JavaScript 编写用户定义函数 (UDF) 的处理程序。本部分中的主题描述如何设计和编写 JavaScript 处理程序。

有关 UDFs 的介绍,包括可用于编写 UDF 处理程序的语言列表,请参阅 用户定义的函数概述。

有了处理程序后,就可以用 SQL 创建 UDF。有关使用 SQL 创建或调用 UDF 的信息,请参阅 创建用户定义的函数 或 执行 UDF。

您可以在处理程序代码执行时获取日志和跟踪数据。有关更多信息,请参阅 日志记录、跟踪和指标。

备注

要了解与 JavaScript UDF 处理程序相关的限制,请参阅 JavaScript UDF 限制。

JavaScript 处理程序的工作原理

当用户调用 UDF 时,用户将 UDF 的名称和实参传递给 Snowflake。Snowflake 调用相关处理程序代码(如有实参则使用实参)来执行 UDF 的逻辑。然后,处理程序函数将输出返回给 Snowflake,后者将其传递回客户端。

对于传递给 UDF 的每行, UDF 返回标量(即单个)值,或者如果定义为表函数,则返回一组行。

示例

下面示例中的代码创建了一个名为 my_array_reverse 的 UDF,其处理程序代码接受输入 ARRAY,并按相反顺序返回包含元素的 ARRAY。根据 SQL-JavaScript 数据类型映射 中描述的映射,JavaScript 实参和返回类型通过 Snowflake 与 SQL 相互转换。

请注意,即使在 SQL 代码中输入的参数名称不是大写字母,JavaScript 代码也必须将这些名称全部大写。

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

JavaScript 数据类型

SQL 和 JavaScript UDFs 基于其原生数据类型支持,提供相似但不同的数据类型。Snowflake 和 JavaScript 中的对象使用以下映射进行传输。

整数和双精度

JavaScript 没有整型类型;所有数字都表示为双精度。除非通过类型转换,否则 JavaScript UDFs 不接受或返回整数值(即,您可以将向接受双精度的 JavaScript UDF 传递整数)。

Snowflake SQL 和 JavaScript 都支持双精度值。这些值按原样传输。

字符串

Snowflake SQL 和 JavaScript 都支持字符串值。这些值按原样传输。

二进制值

所有二进制值都转换为 JavaScript Uint8Array 对象。这些类型化数组的访问方式与常规 JavaScript 数组相同,但效率更高,并且支持更多方法。

如果 JavaScript UDF 返回 Uint8Array 对象,则将其转换为 Snowflake SQL 二进制值。

日期

所有时间戳和日期类型都将转换为 JavaScript Date() 对象。JavaScript 日期类型等效于 Snowflake SQL 中的 TIMESTAMP_LTZ(3)。

对于接受日期或时间的 JavaScript UDFs,请参考以下说明:

  • 精度超过毫秒的所有数据都会丢失。

  • 从 SQL TIMESTAMP_NTZ 生成的 JavaScript Date 不再作为“挂钟”时间;它会受到夏令时的影响。这与将 TIMESTAMP_NTZ 转换为 TIMESTAMP_LTZ 时的行为类似。

  • 从 SQL TIMESTAMP_TZ 生成的 JavaScript Date 会丢失时区信息,但表示的时间与输入时间相同(类似于将 TIMESTAMP_TZ 转换为 TIMESTAMP_LTZ)。

  • SQL DATE 会转换为 JavaScript Date,代表当地时区当前日期的午夜。

此外,对于返回 DATE 和 TIMESTAMP 类型的 JavaScript UDFs,请考虑以下注意事项:

  • JavaScript Date 对象会转换为 UDF 的结果数据类型,其转换语义与从 TIMESTAMP_LTZ(3) 到返回数据类型的转换语义相同。

  • 嵌套在 VARIANT 对象中的 JavaScript Date 对象始终是 TIMESTAMP_LTZ(3) 类型。

变体、对象和数组

JavaScript UDFs 允许轻松、直观地操作变体和 JSON 数据。传递给 UDF 的变体对象会转换为原生 JavaScript 类型和值。之前列出的任何值都会转换为相应的 JavaScript 类型。变体对象和数组会转换为 JavaScript 对象和数组。同样,UDF 返回的所有值都将转换为适当的变体值。请注意,UDF 返回的对象和数组受大小和深度的限制。

-- 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        |
-----------+

JavaScript 实参和返回值

在 JavaScript 中,实参可以通过名称直接引用。请注意,在引用未加引号的标识符时,必须使用大写的变量名。由于实参和 UDF 是在 JavaScript 中引用的,因此它们必须是合法的 JavaScript 标识符。具体来说,UDF 和实参名称必须以字母或 $ 开头,后面的字符可以是字母数字、$ 或 _。此外,名称不能是 JavaScript 保留字。

以下三个示例说明了使用按名称引用实参的 UDFs:

-- 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);

NULL 和未定义的值

使用 JavaScript UDFs 时,请密切注意可能包含 NULL 值的行和变量。具体而言,Snowflake 包含两个不同的 NULL 值(SQL NULL 和变体的 JSON null),而 JavaScript 除 null 外还包含 undefined 值。

JavaScript UDF 的 SQL NULL 实参将转换为 JavaScript undefined 值。同样,返回的 JavaScript undefined 值也会转换回 SQL NULL。这适用于所有数据类型,包括变体。对于非变体类型,返回的 JavaScript null 值也会转换为 SQL NULL 值。

变体类型的实参和返回值区分 JavaScript 的 undefined 和 null 值。SQL NULL 继续转换为 JavaScript undefined`(JavaScript :code:`undefined 返回 SQL NULL);变体 JSON null 转换为 JavaScript null`(JavaScript :code:`null 返回变体 JSON null)。嵌入到 JavaScript 对象(作为值)或数组中的 undefined 值将导致元素被省略。

创建一个包含一个字符串和一个 NULL 值的表:

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

创建一个函数,将字符串转换为 NULL,将 NULL 转换为字符串:

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

调用函数:

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

创建一个函数,显示传递 SQL NULL 和传递变体 JSON null 之间的区别:

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;
      }
      ';
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            |
+------+--------------------------------------+-----------------------------------+

创建一个函数,显示返回 undefined、null 与包含 undefined 和 null 的变体之间的区别(注意,undefined 值会从返回的变体中移除):

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;
      }
      $$;
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                                                            |
|                    |               | }                                                                         |
+--------------------+---------------+---------------------------------------------------------------------------+

JavaScript 中的类型转换

JavaScript 会在许多不同类型之间隐式转换值。返回任何值时,该值首先转换为请求的返回类型,然后再转换为 SQL 值。例如,如果返回一个数字,但 UDF 声明返回的是一个字符串,则此数字将在 JavaScript 中转换为字符串。请注意,JavaScript 编程错误(如返回错误的类型)可能会被这种行为所掩盖。此外,如果在转换值的类型时出现错误,也会导致出错。

JavaScript 数字范围

精度完好无损的数字的范围是

-(2^53 -1)

到

(2^53 -1)

Snowflake NUMBER(p, s)和 DOUBLE 数据类型中的有效值范围更大。从 Snowflake 检索值并将其存储到 JavaScript 数值变量中可能会导致精度损失。例如:

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

前两列应匹配,第三列应包含 0.0。

该问题适用于 JavaScript 用户定义的函数 (UDFs) 和存储过程。

如果您在存储过程中使用 getColumnValue() 时遇到问题,则可以通过以字符串形式检索值来避免该问题,例如:

getColumnValueAsString()

然后,您可以返回存储过程中的字符串,并在 SQL 中将该字符串转换为数字数据类型。

JavaScript 错误

用户在执行 JavaScript 时遇到的任何错误都会显示为 SQL 错误。这包括解析错误、运行时错误和 UDF 中抛出的未捕获错误。如果错误包含堆栈跟踪,则会与错误消息一起打印出来。为了结束查询并产生 SQL 错误,抛出错误而不捕获是可以接受的。

调试时,您可能会发现在打印错误消息的同时打印实参值非常有用,这样实参值就会显示在 SQL 错误消息文本中。对于确定性 UDFs,这为在本地 JavaScript 引擎中重现错误提供了必要的数据。一种常见的模式是将整个 JavaScript UDF 主体放在 try-catch 块中,将实参值追加到捕获的错误消息中,然后抛出带有扩展消息的错误。在将 UDFs 部署到生产环境之前,应考虑移除此类机制;在错误消息中记录值可能会无意中泄露敏感数据。

函数可以抛出并捕获预定义异常或自定义异常。点击 此处 可查看抛出自定义异常的简单示例。

另请参阅 JavaScript UDFs 故障排除。

JavaScript UDF 安全

JavaScript UDFs 通过提供多层查询和数据隔离,旨在确保安全可靠:

  • 执行 JavaScript UDF 的虚拟仓库内的计算资源只能从您的账户内访问(即仓库不会与其他 Snowflake 账户共享资源)。

  • 表数据在虚拟仓库中加密,以防止未经授权的访问。

  • JavaScript 代码是在受限的引擎内执行的,因此无法从 JavaScript 上下文的系统调用系统资源(例如,无法访问网络和磁盘),并限制了引擎可用的系统资源,特别是内存。

因此,JavaScript UDFs 只能访问执行定义功能所需的数据,除了消耗合理的内存和处理器时间外,不会影响底层系统的状态。