JavaScript UDFs の紹介

このトピックでは、一般的な JavaScript UDF (ユーザー定義関数)要件と使用方法の詳細、および JavaScript UDFs に固有の制限について説明します。

このトピックの内容:

使用例

次の例は、 JavaScript UDF を使用して配列をソートする方法を示しています。この例では、 JavaScript に配列用の組み込みSort()メソッドがあるという事実を利用しています。

SQL コードで名前が大文字でなくても、 JavaScript コードは入力パラメーター名をすべて大文字として参照する必要があります。

-- Create the UDF.
CREATE OR REPLACE FUNCTION array_sort(a array)
  RETURNS array
  LANGUAGE JAVASCRIPT
AS
$$
  return A.sort();
$$
;

-- Call the UDF with a small array.
SELECT ARRAY_SORT(PARSE_JSON('[2,4,5,3,1]'));

出力は次のようになります。

[1, 2, 3, 4, 5]

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 は、ローカルタイムゾーンの当日の午前0時を表す JavaScript Date に変換されます。

さらに、 DATE および TIMESTAMP 型を返す JavaScript UDFs に関する次の注意事項も考慮してください。

  • JavaScript Date オブジェクトは UDF の結果データ型に変換され、 TIMESTAMP_LTZ(3) から戻りデータ型へのキャストと同じ変換セマンティクスに従います。

  • JavaScript Date VARIANT オブジェクト内にネストされたオブジェクトは、常に 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 予約語にすることはできません。

次の3つの例は、名前で参照される引数を使用する 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には2つの異なる NULL 値(SQL NULL とバリアントの JSON null)が含まれ、 JavaScript には null に加えて undefined 値が含まれています。

JavaScript UDF への SQL NULL 引数は、 JavaScript undefined 値に変換されます。同様に、返された JavaScript undefined 値は SQL NULL に変換されます。これは、バリアントを含むすべてのデータ型に当てはまります。非バリアント型の場合は、返された JavaScript null も SQL NULL 値になります。

バリアント型の引数と戻り値は、 JavaScript の undefinednull 値を区別します。SQL NULL は、引き続きJavaScript undefined に変換され(また JavaScript undefined は SQL NULL に戻され)ます。バリアント JSON null は、 JavaScript null に変換され(また JavaScript null はバリアント JSON null に戻され)ます。JavaScript オブジェクト(値として)または配列に埋め込まれた undefined 値により、要素が省略されます。

1つの文字列と1つの 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            |
+------+--------------------------------------+-----------------------------------+

undefinednull、および undefinednull を含むバリアントを返すことの違いを示す関数を作成します(返されるバリアントから 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 |
+----------------------+----------------------+------------+

最初の2つの列は一致し、3番目の列は0.0を含む必要があります。

この問題は、 JavaScript ユーザー定義関数(UDFs)およびストアドプロシージャに適用されます。

getColumnValue() を使用しているときにストアドプロシージャで問題が発生した場合、次のように値を文字列として取得することで問題を回避できる可能性があります。

getColumnValueAsString()

その後、ストアドプロシージャから文字列を返し、文字列を SQL の数値データ型にキャストできます。

JavaScript エラー

JavaScript の実行中に発生したエラーは、 SQL エラーとしてユーザーに表示されます。これには、 UDF内でスローされた解析エラー、ランタイムエラー、キャッチされなかったエラーが含まれます。エラーにスタックトレースが含まれている場合、エラーメッセージとともに出力されます。クエリを終了して SQL エラーを生成するために、エラーをキャッチせずにスローすることは許容されます。

デバッグの際、引数値をエラーメッセージとともに出力して SQL エラーメッセージテキストに表示すると便利な場合があります。決定的 UDFs の場合、これはローカル JavaScript エンジンでエラーを再現するために必要なデータを提供します。一般的なパターンの1つは、 JavaScript UDF 本文全体をtry-catchブロックに配置し、キャッチしたエラーのメッセージに引数値を追加し、拡張メッセージでエラーをスローすることです。 UDFs を運用環境に展開する前に、このようなメカニズムを削除することを検討する必要があります。エラーメッセージに値を記録すると、意図せずに機密データが表示される可能性があります。

この関数は、事前定義された例外またはカスタム例外をスローおよびキャッチできます。カスタム例外をスローする簡単な例は、 こちら です。

JavaScript UDFs のトラブルシューティング もご参照ください。

JavaScript UDF の制限

Snowflake環境内の安定性を確保するために、Snowflakeは JavaScript UDFsに次の制限を設けています。これらの制限は、 UDF の作成時に呼び出されるのではなく、実行時に UDF が呼び出されるときに呼び出されます。

メモリ

メモリを消費しすぎると、JavaScript UDFs は失敗します。特定の制限は変更される場合があります。メモリを使いすぎると、エラーが返されます。

時間

完了するのに時間がかかりすぎるJavaScript UDFs は削除され、ユーザーにエラーが返されます。さらに、無限ループに入る JavaScript UDFs はエラーになります。

スタック深度

再帰による過度のスタック深度はエラーになります。

グローバルステート

Snowflakeは通常、 UDFの反復間で JavaScript グローバル状態を保持します。ただし、関数呼び出し間で利用可能なグローバル状態に対する以前の変更に依存しないでください。さらに、すべての行が同じ JavaScript 環境内で実行されると想定しないでください。

実際には、グローバル状態は次に関連しています。

  • 複雑/高価な初期化ロジック。デフォルトでは、提供された UDF コードは処理されたすべての行に対して評価されます。そのコードに複雑なロジックが含まれている場合、これは非効率的です。

  • べき等でないコードを含む関数。典型的なパターンは次のとおりです。

    Date.prototype._originalToString = Date.prototype.toString;
    Date.prototype.toString = function() {
      /* ... SOME CUSTOM CODE ... */
      this._originalToString()
      }
    

    このコードが初めて実行されると、 toString_originalToString の状態が変更されます。これらの変更は、グローバル状態で保持され、このコードが2回目に実行されると、値が再帰を作成する方法で再び変更されます。2回目に toString が呼び出されると、コードは無限に再帰します(スタック領域がなくなるまで)。

これらの状況で推奨されるパターンは、 JavaScript のグローバル変数セマンティクスを使用して、関連するコードが1回だけ評価されることを保証することです。例:

var setup = function() {
/* SETUP LOGIC */
};

if (typeof(setup_done) === "undefined") {
  setup();
  setup_done = true;  // setting global variable to true
}

このメカニズムは、コード評価の効果をキャッシュする場合にのみ安全です。初期化後、すべての行でグローバルコンテキストが保持されることは保証されず、それに依存するビジネスロジックはありません。

ライブラリ

JavaScript UDFs は、標準 JavaScript ライブラリへのアクセスをサポートします。これにより、ブラウザが通常提供する多くのオブジェクトとメソッドが除外されます。追加のライブラリをインポート、含める、または呼び出すメカニズムはありません。必要なコードはすべて UDF 内に埋め込む必要があります。

また、組み込みの JavaScript eval() 機能は無効になっています。

返されるバリアントのサイズと深度

返されるバリアントオブジェクトには、サイズと入れ子の深度の制限があります。

サイズ

現在、数メガバイトに制限されていますが、変更される可能性があります。

深度

現在、ネストの深度は1000に制限されていますが、変更される可能性があります。

オブジェクトが大きすぎるか深すぎる場合、 UDF が呼び出されたときにエラーが返されます。

JavaScript UDF セキュリティ

JavaScript UDFs は、いくつかの層のクエリとデータ分離を提供することにより、安全性を確保するように設計されています。

  • JavaScript UDF を実行する仮想ウェアハウス内のコンピューティングリソースには、アカウント内からのみアクセスできます(つまり、ウェアハウスは他のSnowflakeアカウントとリソースを共有しない)。

  • テーブルデータは、不正アクセスを防ぐために仮想ウェアハウス内で暗号化されます。

  • JavaScript コードは制限されたエンジン内で実行され、 JavaScript コンテキストからのシステムコール(例: ネットワークおよびディスクアクセスなし)を防ぎ、エンジンで使用できるシステムリソース、特にメモリを制限します。

その結果、 JavaScript UDFs は、定義された機能を実行するために必要なデータのみにアクセスでき、適切な量のメモリとプロセッサ時間を消費する以外は、基になるシステムの状態に影響を与えることはできません。