❄️

Snowflake: ENCODING='SHIFTJIS' 完全解説

2024/05/30に公開

Intro

Snowflake では、すべての文字列を UTF-8 で扱っていますが、データロード時のみ、ファイルフォーマットの ENCONDING オプションで文字エンコーディングを指定し、UTF-8 に変換しながらロードすることができます。

https://docs.snowflake.com/ja/sql-reference/sql/create-file-format

ENCODING = 'string'
使用 データのロードおよび外部テーブル
定義 データをテーブルにロードする際にソースデータの文字セットを指定する文字列(定数)。

日本語の文字エンコーディングとしては、EUCJP, ISO2022JP, SHIFTJIS の 3 つがサポートされています。

この記事では、ENCODING = 'SHIFTJIS' を指定したときに、実際に使われる文字エンコーディングは CP932 なのか、CP943C なのか、もしくは別の何かなのか、という話をします。

Snowflake の文字エンコーディングの扱い

前述の通り、Snowflake では、すべての文字列を UTF-8 で扱います。

https://docs.snowflake.com/ja/sql-reference/data-types-text#varchar

VARCHAR は、 UTF-8文字を保持します。

ところで、Snowflake は照合順序をサポートしていますが、この照合順序は ICU (International Component for Unicode) の仕様をサポートし、また UCA (Unicode Collation Algorithm) に従って処理されます。

Snowflakeは、次の 照合仕様 をサポートしています。

  • ICU (Unicode用の国際化コンポーネント)。
  • Snowflake固有の照合仕様(例: upper および lower)。
    ICU で定義された大文字と小文字を区別しない比較操作の場合、Snowflakeは Unicode照合アルゴリズム(UCA) に従い、Unicode文字の3次の重みではなく、1次および2次の重みのみを考慮します。3次の重みのみが異なる文字は、同一として扱われます。たとえば、 en-ci 照合仕様を使用すると、スペースと改行なしスペースは同一であると見なされます。

ということで、ENCODING オプションも ICU に従うんじゃないかと仮定して考えていきます。

ICU における Shift_JIS の扱い

ICU では、文字エンコーディングごとに Converter が指定されており、各 Converter は .ucm ファイル (UniCode Mapping) に記述されているマッピングを使用して、Unicode/文字エンコーディング間の変換を行います。

さて、ICU における Shift_JIS に対応する Converter ですが、これは ibm-943_P15A-2003 になります。

https://github.com/unicode-org/icu/blob/main/icu4c/source/data/mappings/convrtrs.txt#L584-L607

convrtrs.txt を見る限り、Shift_JIS も CP932 も CP943C も Windows-31J も MS_Kanji も、すべてこの Converter にマッピングされています。

ということで、ibm-943_P15A-2003 Converter の .ucm ファイルを、いくつかの扱いが特殊な文字について確認していってみます。

https://github.com/unicode-org/icu/blob/main/icu4c/source/data/mappings/ibm-943_P15A-2003.ucm

Case 1: コードポイントが重複している文字

これは NEC 特殊文字にも IBM 拡張文字にも含まれている文字で、例えばローマ数字や が該当します。

例えば は CP932 でも CP943C でも、0x878A (NEC 特殊文字由来) と 0xFA58 (IBM 拡張文字由来) の両方のコードポイントが存在し、文字エンコーディングおよび処理系によってどちらかに変換されます。

$ echo -e "\x87\x8a"|iconv -f cp932 -t utf8
㈱
$ echo -e "\xfa\x58"|iconv -f cp932 -t utf8
㈱

この 2 つのコードポイントについて、ibm-943_P15A-2003.ucm を確認すると、どちらも U+3231 にマッピングされており、3 列目が |0|3 になっています。
https://github.com/unicode-org/icu/blob/main/icu4c/source/data/mappings/ibm-943_P15A-2003.ucm#L9410-L9411

https://unicode-org.github.io/icu/userguide/conversion/data.html

The precision indicator either must be present in all mappings or in none of them. The indicator is a pipe symbol | followed by a 0, 1, 2, 3, or 4 that has the following meaning:

|0 - A “normal”, roundtrip mapping from a Unicode code point and back.
|1 - A “fallback” mapping only from Unicode to the codepage, but not back.
|2 - A subchar1 mapping. The code point is unmappable, and if a substitution is performed, then the subchar1 should be used rather than the subchar. Otherwise, such mappings are ignored.
|3 - A “reverse fallback” mapping only from the codepage to Unicode, but not back to the codepage.
|4 - A “good one-way” mapping only from Unicode to the codepage, but not back.

...

“Reverse fallbacks” are technically similar, but the same Unicode character can be encoded twice in the codepage. ICU always uses reverse fallbacks at runtime.

なので、ここでは、

  • 0x878a は U+3231 と相互変換可能
  • 0xfa58 は 0xfa58 → U+3231 の変換のみ可能 (フォールバック)

という扱いとなります。

したがって、U+3231 を ibm-943_P15A-2003 で変換すると、常に 0x878a に変換され、0xfa58 に変換されることはありませんが、0xfa58 を U+3231 に変換することはできる、ということになります。

そこで、実際にこれらのコードポイントを Snowflake に ENCODING = 'SHIFTJIS' 付きでロードしてみると、上記の動作の通りになることがわかります。

$ echo -e "\x87\x8a" > x87x8a.csv
$ echo -e "\xfa\x58" > xfax58.csv
$ snowsql
...
>create or replace stage sjistest file_format = (type = csv, encoding = 'SHIFTJIS');
...
>put 'file://~/x87x8a.csv' @sjistest;
...
>put 'file://~/xfax58.csv' @sjistest;
...
>select $1, hex_encode($1) from @sjistest/x87x8a.csv.gz;
+----+----------------+
| $1 | HEX_ENCODE($1) |
|----+----------------|
|| E388B1         |
+----+----------------+
1 Row(s) produced. Time Elapsed: 0.439s
>select $1, hex_encode($1) from @sjistest/xfax58.csv.gz;
+----+----------------+
| $1 | HEX_ENCODE($1) |
|----+----------------|
|| E388B1         |
+----+----------------+
1 Row(s) produced. Time Elapsed: 0.371s

$ echo -ne "\xe3\x88\xb1"|iconv -f utf8 -t utf16|xxd
00000000: feff 3231                                ..21

Case 2: CP932/CP943C で変換後の Unicode コードポイントが異なる同一文字

これは などが該当し、CP932 では U+FF5E (FULLWIDTH TILDE) に、CP943C では U+301C (WAVE DASH) に変換されます。

$ echo -ne "\x81\x60"|iconv -f cp932 -t utf16|xxd
00000000: feff ff5e                                ...^
$ echo -ne "\x81\x60"|iconv -f cp943c -t utf16|xxd
00000000: feff 301c                                ..0.

https://github.com/unicode-org/icu/blob/main/icu4c/source/data/mappings/ibm-943_P15A-2003.ucm#L9407
https://github.com/unicode-org/icu/blob/main/icu4c/source/data/mappings/ibm-943_P15A-2003.ucm#L9867

今回は U+FF5E が |0、U+301C が |1 になっているため、

  • 0x8160 と U+FF5E は相互変換可能
  • 0x8160 と U+301C は、U+301C → 0x8160 の変換のみ、かつ Fallback フラグが立っている場合のみ可能

という形となります。

したがって、0x8160 から Unicode への変換は、常に U+FF5E になることが想定されます。

こちらも、実際に Snowflake に ENCODING = 'SHIFTJIS' 付きでロードしてみると、上記の動作の通りになることがわかります。

$ echo -e "\x81\x60" > x81x60.csv
$ snowsql
...
>put 'file://~/x81x60.csv' @sjistest;
...
>select $1, hex_encode($1) from @sjistest/x81x60.csv.gz;
+----+----------------+
| $1 | HEX_ENCODE($1) |
|----+----------------|
|| EFBD9E         |
+----+----------------+
1 Row(s) produced. Time Elapsed: 0.467s

$ echo -ne "\xef\xbd\x9e"|iconv -f utf8 -t utf16|xxd
00000000: feff ff5e

まとめ

Snowflake の ENCODING = 'SHIFTJIS' は、観測した限りでは ICU の ibm-943_P15A-2003 Converter の定義に沿った動作を取ります。

ibm-943_P15A-2003 は、CP932/CP943C にできる限り両対応できるように設計されており、また Snowflake における文字エンコーディング変換が「UTF-8 への変換」の一方向のみになるため、元データがどちらの文字エンコーディングになっていても、変換に失敗するなどの問題は起きなそうです。

が、例えばアプリケーションが 0x8160 のデータを Snowflake に取り込んだときに U+301C (WAVE DASH) に変換されることを想定していたりすると問題になる可能性はあるので、できる限り CP932 に寄せるのがよさそうです。

ちなみに Shift_JIS-2004 は非互換です。

$ echo -ne|iconv -f utf8 -t cp932|xxd
iconv: iconv(): Illegal byte sequence
$ echo -ne|iconv -f utf8 -t cp943c|xxd
iconv: iconv(): Illegal byte sequence
$ echo -ne|iconv -f utf8 -t shift_jis-2004|xxd
00000000: 875e

Discussion