CDS Scalar Functionsを使って独自の関数を定義する

## はじめに
以下の記事で、CDS Viewの日付項目と実行日の差を計算する方法を紹介しました。このような計算はほかのViewでも使う可能性があるので、再利用する方法について考えます。

https://qiita.com/tami/items/30de35917d4c7c029395

一つ目の方法は、以下の記事で紹介したCDS Aspectsです。

https://qiita.com/tami/items/1ab57bad2f70837c9d50

この記事では、CDS Scaler Functionを使って同じことを実現してみます。

## CDS Scaler Function
CDS Scaler Functionは、CDS Viewの中で使うファンクションを独自に定義するものです。SAP BTP ABAP environment 2208およびSAP S/4HANA Cloud 2208から利用できるようになりました。
[Version 3.28 | What's New](https://help.sap.com/docs/abap-cloud/abap-cds-tools-user-guide/version-3-28)

ファンクションは以下のようにして呼び出すことができます。

```:ファンクションの呼び出し方法
function_name(param1 => <設定値>, param2 => <設定値>) as Field1,
```
CDS Scaler Functionについて、以下の動画で紹介されていました。
[Data Modeling with the latest features of ABAP Core Data Services | Youtube](https://www.youtube.com/watch?v=s8i44J4t7J0)

## CDS Scalar Functionsで日付項目と実行日の差を計算する関数を作る
Aspectを使用する前のViewの定義は以下のようになっています。

```
@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: '###GENERATED Core Data Service Entity'
define root view entity ZR_TODO
  as select from ztodo as Todo
{
  key id as Id,
  title as Title,
  detail as Detail,
  due_date as DueDate,
  
  //計算項目
  tstmp_current_utctimestamp() as TimeStampToday,
  tstmp_to_dats($projection.TimeStampToday, 'JAPAN', $session.client, 'NULL') as DateToday,
  dats_days_between($projection.DateToday, $projection.DueDate) as DateDifference,
  
  case
    when $projection.DateDifference < 0 then 1 //negative: overdue
    when $projection.DateDifference < 3 then 2 //critical
    else 3 end as Criticality, 
  //
  
  @Semantics.user.createdBy: true
  created_by as CreatedBy,
  @Semantics.systemDateTime.createdAt: true
  created_at as CreatedAt,
  @Semantics.user.localInstanceLastChangedBy: true
  last_changed_by as LastChangedBy,
  @Semantics.systemDateTime.localInstanceLastChangedAt: true
  local_last_changed_at as LocalLastChangedAt,
  @Semantics.systemDateTime.lastChangedAt: true
  last_changed_at as LastChangedAt
}
```
以下のステップでCDS Scalar Functionを作成し、それを元のCDS Viewに組み込みます。
1. Scaler Functionの定義
1. AMDPクラスの定義
1. Scaler Function Implementation Referenceの定義
1. AMDPクラスの実装
1. CDS ViewでScalar Functionを利用

:::note
BTP ABAP Environment（トライアル）で検証を行っています。
:::

### 1. Scaler Functionの定義
Core Data Services > Scaler Function Definition を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/95121dba-32f6-caee-ad59-f2deea9d31f5.png)

名前は最大26桁で、2番目と3番目の位置にアンダースコアがあってはいけないという制約があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d0338c09-c6c2-9b72-4632-19df75b35c33.png)

初期状態は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/65b9eea7-e45b-81e1-1433-7c719556238c.png)

以下のようにScaler Functionを定義しました。
```
define scalar function ZDATE_DIFF_FROM_TODAY
  with parameters
    timezone_in: zde_timezone_hana,
    target_date: abap.dats
  returns abap.int2
```
`zde_timezone_hana`というデータエレメントにはHANAで定義されているタイムゾーンを指定するため、CHAR 64桁でCase-sensitiveなドメインを定義して使用しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/3075494a-976a-9f34-b38a-d1d0f4f423e5.png)



### 2. AMDPクラスの定義
CDS Scaler FunctionのロジックはAMDPで実装します。Interface: `IF_AMDP_MARKER_HDB`を実装したクラスを定義します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9c401230-ee9f-3a67-1d36-c778fa0e0816.png)

以下の状態で有効化します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5b152590-8a77-d1cc-1d61-7737a2aabd61.png)

### 3. Scaler Function Implementation Referenceの定義
Scaler Functionを右クリックし、"New Scaler Function Implementation Reference"を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/aca26ce9-c6af-b1ce-f667-49fe1fe0f686.png)

CDS Viewから呼び出す場合、Engineに"SQL Engine"を選択します。分析クエリから呼び出す場合は"Analytical Engine"を選択します。選択したエンジンに応じて、名前の末尾に"_SQL"または、"_ANA"をつける必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ef538792-5d39-d4b6-af79-301b224ad857.png)

AMDP Referenceに2.で定義したクラスと、メソッド（未実装）を指定します。ワーニングが出ますが、有効化できます。（メソッド実装後に再有効化が必要）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a0a8078d-faff-3176-20a2-047739dfcc32.png)

### 4. AMDPクラスの実装
Implementation Referenceで指定した`execute`というメソッドを定義します（メソッド名はImplementation Referenceと一致していればなんでもよい）。

:::note warn
Implementation Referenceを先に定義しておかないと、`zdate_diff_from_today`というScaler Functionが見つからないというエラーになります。
:::

```
CLASS zcl_date_diff_from_today_amdp DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    INTERFACES if_amdp_marker_hdb .

    CLASS-METHODS execute
        FOR SCALAR FUNCTION zdate_diff_from_today.

  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.
```
メソッドの実装は以下のようになります。
```
CLASS zcl_date_diff_from_today_amdp IMPLEMENTATION.
  METHOD execute BY DATABASE FUNCTION FOR HDB LANGUAGE SQLSCRIPT.
    declare utc_timestamp timestamp;
    declare local_timestamp timestamp;

    -- today
    SELECT current_utctimestamp INTO utc_timestamp FROM dummy;
    SELECT utctolocal(:utc_timestamp, :timezone_in) AS local_timestamp INTO local_timestamp FROM dummy;

    -- date difference
    SELECT days_between(to_date(:local_timestamp), to_date(:target_date)) AS days_between INTO result FROM dummy;
  ENDMETHOD.

ENDCLASS.
```
このメソッドでは以下の計算を行っています。

#### 4.1. 現在の日付を取得する
タイムゾーンを考慮した日付を取得するため、はじめに現在のタイムスタンプを取得し、それを日付型に変換します。

①現在のタイムスタンプを取得する
```
    SELECT current_utctimestamp INTO utc_timestamp FROM dummy;
```
[CURRENT_UTCTIMESTAMP](https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-sql-reference-guide/current-utctimestamp-function-datetime)というファンクションで、現在のUTCのタイムスタンプを取得します。
出力例：`2024-10-05 20:55:33.965000000`

②タイムスタンプを指定したタイムゾーンに変換する
```
    SELECT utctolocal(:utc_timestamp, :timezone_in) AS local_timestamp INTO local_timestamp FROM dummy;
```
[UTCTOLOCAL](https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-sql-reference-guide/utctolocal-function-datetime)というファンクションで、UTCのタイムスタンプを第二引数で指定したタイムゾーンでのタイムスタンプに変換します。タイムゾーンには、Scaler Functionのインプットである"timezone_in"を指定しています。
出力例：`2024-10-06 05:55:33.965000000`

指定可能なタイムゾーンはシステムビュー：`TIMEZONES`で確認できます。ABAP側で利用するタイムゾーンの値とは異なることに注意が必要です（ABAPの場合、日本は大文字の'JAPAN'）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4935ed7c-a451-d981-51bc-75e92837c847.png)

#### 4.2. 指定された日付との差を計算する
```
    SELECT days_between(to_date(:local_timestamp), to_date(:target_date)) AS days_between INTO result FROM dummy;
```
[DAYS_BETWEEN(date1, date2)](https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-sql-reference-guide/days-between-function-datetime)というファンクションで、date2からdate1を引いた日数を計算します。[TO_DATE()](https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-sql-reference-guide/to-date-function-data-type-conversion)というファンクションは、指定された日付のストリングをDAYDATE型(YYYY-MM-DD)に変換します。
最後に計算結果を格納している`result`という項目は、Scaler Functionの返り値です。


以下はDatabase Explorerで動作確認した結果です。AMDPはCDS Viewに組み込むまで正しく動くかがわからないので、事前に確認するためにはDatabase Explorerが便利でした。S/4HANAに固有なテーブルはここでは使えませんが、ファンクションなどの動きを確認するのには役立ちます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8eed8fd5-37fb-9380-eb8b-922413a9822d.png)

### 5. CDS ViewでScalar Functionを利用
:::note warn
 CDS Viewで利用可能にするために、AMDPメソッドを実装したあとでScaler Function Implementation Referenceを再有効化する必要があります。
:::

CDS Viewにもともとあった計算項目をコメントアウトし、Scaler Functionを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2f8dbd1d-352a-20ae-b3b2-c329feabca5b.png)

Scaler Functionが"zde_timezone_hana"という型でタイムゾーンを受け取るため、リテラルをその型に変換しています。
```
  cast('Japan' as zde_timezone_hana) as TimeZoneHana,
```
以下でScaler Functionを呼び出しています。
```
    ZDATE_DIFF_FROM_TODAY( timezone_in => $projection.TimeZoneHana, 
                        target_date => $projection.DueDate ) as DateDifference,
```

プレビューで見ると、"DateDifference"が正しく計算されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2b6df8fe-52b8-0779-8aea-25832b0a98f9.png)

## Scaler Functionと比較して
[CDS Scaler Functions](https://qiita.com/tami/items/1ab57bad2f70837c9d50)の場合、最終結果に必要ない項目も計算のために持たなければなりませんが、CDS Scaler Functionsは結果のみ返すので、今回の目的にはより合っています。
ただし、ファンクションの実装をAMDP（SQL Script）で書く必要があるので、CDS Scaler Functionsに比べ実装は難しかったです。

## 参考
- [Creating a CDS Scalar Function | Help Portal](https://help.sap.com/docs/abap-cloud/abap-cds-tools-user-guide/creating-cds-scalar-function)
- [ABAP CDS - SQL-Based Scalar Functions | ABAP Keyword Documentation](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abencds_sql_scalar_function.htm)