【AWS Step Functions】JSONataの紹介とシングルクォートを文字列として扱う方法

## AWS Step FUnctionsでJSONataが使えるようになった！

Step FunctionsでJSONataが使えるようになりました！

https://aws.amazon.com/jp/blogs/news/simplifying-developer-experience-with-variables-and-jsonata-in-aws-step-functions/

といっても私はこれまでJSONanaのことを存じ上げず、正直これで何が嬉しいのか？とかよくわかっておりませんでした。良くなった点として一例が記事には

>JSONPath が無効になり、InputPath、Parameters、ResultPath、ResultSelector、OutputPath の使用が制限されます。代わりに JSONata では Arguments と Output を使用します。

とあります。確かにInputPath、Parameters、ResultPath、ResultSelector、OutputPathのあたりはそれぞれの役割が分かりづらかったのですが、Argumentsで引数の設定をし、Outputで出力の設定をしてあげるというところでかなりシンプルになった印象です。

## JSONata
といってもJSONataがなんなのかよくわかってなかったのですが、以下がドキュメントで

https://jsonata.org/

ざっくり言うと基本JSONなのですが、ちょっとしたクエリや処理がかけるといったもののようです。

これをStep Functionsで書くときは
```
JSONata 構文： "{% <JSONata expression> %}"
```
といった形式となり、具体的には

```json
# JSONataでの書き方
{
  "QueryLanguage": "JSONata", // Set explicitly; could be set and inherited from top-level
  "Type": "Task",
  ...
  "Arguments": { // JSONata states do not have Parameters
    "static": "Hello",
    "title": "{% $states.input.title %}", 
    "name": "{% $customerName %}",   // With $customerName declared as a variable
    "not-evaluated": "$customerName"
  }
}
```

```json
# 元々の書き方
{
  "QueryLanguage": "JSONPath", // Set explicitly; could be set and inherited from top-level
  "Type": "Task",
  ...
  "Parameters": {
    "static": "Hello",
    "title.$": "$.title",
    "name.$": "$customerName",  // With $customerName declared as a variable
    "not-evaluated": "$customerName"
  }
}
```
QiitaのコードブロックにJSONataが用意されていない点からもあまり普及していないというのがわかります。

のような形です個人的にわかりやすかったのはinputの値にアクセスしやすくなった点で、`$states.input.value_name`の形式で、アクセスできる点です。

$statesは予約変数です。以下を参照してみてください。

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/transforming-data.html#transforming-reserved-variable-states

## RedshiftのUnloadとCopyを実行したい
今回Step Functionsであるクラスターから別のクラスターにテーブルデータをコピーしたい要件があり、その時作成したステートマシンを共有します。
**長いので折りたたみます。**
<details>

```json
{
  "Comment": "A description of my state machine",
  "StartAt": "S3パス生成",
  "States": {
    "S3パス生成": {
      "Type": "Pass",
      "Next": "Unload",
      "Output": {
        "table_name": "{% $states.input.table_name %}",
        "s3_path": "{% \"s3://bucket-name/path/to/output/\" & $now() & \"/\" & $states.input.table_name & \"/result.parquet\" %}",
        "where_condition": "{% $exists($states.input.where_condition) ? ' WHERE ' & $states.input.where_condition & ' ' : '' %}"
      }
    },
    "Unload": {
      "Type": "Task",
      "Resource": "arn:aws:states:::states:startExecution.sync:2",
      "Arguments": {
        "StateMachineArn": "arn:aws:states:ap-northeast-1:012345678901:stateMachine:ExcecuteRedShiftQuery",
        "Input": {
          "ClusterIdentifier": "original-cluster-name",
          "Database": "db_name",
          "DbUser": "user_name",
          "Sql": "{% \"UNLOAD ('SELECT * FROM \" & $states.input.table_name & $states.input.where_condition & \"') \nTO '\" & $states.input.s3_path & \"'\nIAM_ROLE 'arn:aws:iam::012345678901:role/role_name' \nREGION 'ap-northeast-1' \nformat as parquet;\" %}"
        }
      },
      "Next": "Copy先Truncate",
      "Output": "{% $states.input %}"
    },
    "Copy先Truncate": {
      "Type": "Task",
      "Resource": "arn:aws:states:::states:startExecution.sync:2",
      "Arguments": {
        "StateMachineArn": "arn:aws:states:ap-northeast-1:012345678901:stateMachine:ExcecuteRedShiftQuery",
        "Input": {
          "ClusterIdentifier": "import-cluster-name",
          "Database": "db_name",
          "DbUser": "user_name",
          "Sql": "{% \"TRUNCATE TABLE \" & $states.input.table_name %}"
        }
      },
      "Next": "Copy実行",
      "Output": "{% $states.input %}"
    },
    "Copy実行": {
      "Type": "Task",
      "Resource": "arn:aws:states:::states:startExecution.sync:2",
      "Arguments": {
        "StateMachineArn": "arn:aws:states:ap-northeast-1:012345678901:stateMachine:ExcecuteRedShiftQuery",
        "Input": {
          "ClusterIdentifier": "import-cluster-name",
          "Database": "db_name",
          "DbUser": "user_name",
          "Sql": "{% \"COPY \" & $states.input.table_name & \" FROM '\" & $states.input.s3_path &\"' \nIAM_ROLE 'arn:aws:iam::012345678901:role/role_name' \nformat as parquet;\" %}"
        }
      },
      "End": true,
      "Output": "{% $states.input %}"
    }
  },
  "QueryLanguage": "JSONata"
}
```

</details>

ここで苦戦したのはシングルクォートの扱いです。
ドキュメントには以下のような書き方をされており文中にシングルクォートをどのようにかけるのか記載が見つかりませんでした。
```
"lastName": "{% 'Last=>' & $states.input.customer.lastName %}",
```
バックスラッシュなどエスケープできそうなやり方も試したのがだめで、AWSサポートに問い合わせたところ以下の回答がありました。

>シングルクォートを含める場合は、文字列をシングルクォートで囲う代わりにダブルクォートをご利用ください。
その場合文字列を囲うダブルクォートをエスケープいただく必要がございます。

つまりダブルクォートで囲い、もしダブルクォートが必要ならエスケープしてくださいとのこと。

ステートマシンでは以下のようになっていますね
```
"Sql": "{% \"UNLOAD ('SELECT * FROM \" & $states.input.table_name & $states.input.where_condition & \"') \nTO '\" & $states.input.s3_path & \"'\nIAM_ROLE 'arn:aws:iam::012345678901:role/role_name' \nREGION 'ap-northeast-1' \nformat as parquet;\" %}"
```

ここでいくつか補足しておくと、改行は`\n`で文字列の結合は`&`でやっています。
このあたり生成AIに聞いても最近実装された機能なので、正しい答えを得られませんでした。
またおそらくドキュメント上にも記載がないので生成AIに検索かけさせてもだめでした。
なので本記事を執筆することとなりました。