続・PostgreSQL に Santa Clause（サンタ句）を実装する - good_behavior パラメータの追加 -

本記事は「[SRA Advent Calendar 2020](https://qiita.com/advent-calendar/2020/sra)」の24日目です。

# はじめに

この記事は「[PostgreSQL に Santa Clause（サンタ句）を実装する](https://qiita.com/yugo-n/private/38c6a268be3032a32930)」の続編です[^advent]。

前の記事は「``SELECT`` 文の構文を拡張して、PostgreSQL に新しい機能 ``SANTA`` 句を追加してみる」という内容で、PostgreSQLの内部の処理の流れに沿って具体的な実装の流れを説明しました。

この記事では、前の記事で追加した``SANTA``句の振る舞いに影響を与える ``good_behavior`` という設定パラメータを追加してみます。

# 前提

[前の記事](https://qiita.com/yugo-n/private/38c6a268be3032a32930)の内容を踏まえていますので、先にそちらに目を通していただかないと意味がわからないと思います。

### おさらい

一応、前の記事で作成した「``SANTA``句（SANTA clause）」とはどんな機能だったのかをおさらいしておきます。

これは、今日がクリスマス・イブということで、それにあやかって作成した機能です。``SELECT``文の中で``SANTA``句を使うと、テーブルから取得された各行の数を増やすことができます。実際に何倍になるかは``SANTA``句に指定した数字を上限にランダムに決まります。

例えば以下のようなテーブルがある場合：

```sql
santa=# SELECT * FROM box ;
  present  
-----------
 chocolate
 book
 game
(3 rows)
```

``SELECT``文の最後に``SANTA 5``を付けると、サンタさんからのプレゼントで最大で行数が５倍に増えます。

```sql
santa=# SELECT * FROM box SANTA 5;
INFO:  Merry Christmas!  Santa will give you 2 times rows as a present!!
  present  
-----------
 chocolate
 chocolate
 book
 book
 game
 game
(6 rows)

santa=# SELECT * FROM box SANTA 5;
INFO:  Merry Christmas!  Santa will give you 5 times rows as a present!!
  present  
-----------
 chocolate
 chocolate
 chocolate
 chocolate
 chocolate
 book
 book
 book
 book
 book
 game
 game
 game
 game
 game
(15 rows)
```

ただし、``SANTA``句を指定しても行数が増えない場合もあります。今年は悪い子だったからサンタさんが来なかったのかな？

```sql
santa=# SELECT * FROM box SANTA 5;
INFO:  Oh, were you a bad boy this year? Santa will not bring you any present on Christmas...
  present  
-----------
 chocolate
 book
 game
(3 rows)
```

# どんな設定を追加するか

さて、すでに実装済の``SANTA``句には１つ問題があります。もらえるプレゼントの数（増やされる行数）が完全にランダムなのです。サンタさんも多忙ですからプレゼントの配分にムラがあるのは致し方がないとはいえ、やはり今年良い子だった人にはその分豪華なプレゼントを贈りたくなるものです。

サンタさんからプレゼントがもらえなかった理由も、あなたが悪い子だったからではなく単に「運の問題」です。それではあんまりなので、今年積んだ善行（good behavior）でバイアスをかけて、良い子は少しでもプレゼントをもらえる確率を増やせるようにしましょう。

ついでに、今年悪い子だった人からはすべての行を取り上げることにしようと思います。

ということで、今回追加する設定項目 ``good_behavior`` には「今年どれだけよい振る舞いをしたか」を整数値で指定できるようにします[^guc]。この値が大きい場合には、``SANTA``句によって貰えるプレゼントの数（増やされる行数）が多くなるようにします。また、この値がマイナスの場合（かつ ``SANTA`` 句がある場合）には、テーブルから一切の行を出力できないようにします。

# パラメータの追加 

PostgreSQLには[様々な設定パラメータ](https://www.postgresql.jp/document/12/html/runtime-config.html)がありますが、内部ではこれらは GUC（Grand Unified Configuration）と呼ばれています。

実質的には GUC はグローバル変数です。なので、新しいグローバル変数 ``good_behavior`` を追加しましょう。テーブルからの行の出力に影響をあたえるパラメータなので、追加するファイルはテーブルアクセスメソッド（Table Access Method）のコードが書かれている src/backend/access/table/tableam.c にしました。 

```diff:src/backend/access/table/tableam.c
 /* GUC variables */
 char      *default_table_access_method = DEFAULT_TABLE_ACCESS_METHOD;
 bool       synchronize_seqscans = true;
+int        good_behavior = 0;
```

GUCの詳細は src/backend/utils/misc/guc.c の中で定義されています。``good_behavior`` は整数値のパラメータなので以下のエントリを ``ConfigureNamesInt`` 配列にエントリを追加しましょう。

詳細の説明は省きますが、今回は設定ファイル postgresql.conf の中で記述する他に、``SET`` コマンドでも値を変更できるように ``PGC_USERSET`` コンテキストを使用します。また、初期値は0、最小値は-1、最大値は10としました。

```diff:src/backend/utils/misc/guc.c
static struct config_int ConfigureNamesInt[] =
{
（中略）
    {
+       {"good_behavior", PGC_USERSET, DEVELOPER_OPTIONS,
+           gettext_noop("How good your behavior was this year"),
+           NULL
+       },
+       &good_behavior,
+       0, -1, 10,
+       NULL, NULL, NULL
+   },    
+
    /* End-of-list marker */
    {
        {NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL
    }
};

```

# 構文解析処理の修正

次に構文解析時の処理を修正します。構文解析については[前の記事](https://qiita.com/yugo-n/private/38c6a268be3032a32930#%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90)で説明しています。

``good_behavior`` が負値の場合は、``INFO`` メッセージを出力して、``SANTA`` 句の解析結果をダミー値の 1 としています。このダミー値は「``SANTA``句が使用されていること」をエクゼキュータノードに伝える役割があります。

``good_behavior`` が0以上の場合は、これと ``SANTA`` 句に指定された値の合計を最大とする乱数値を計算します。テーブルの各行は、その数だけ増やされて出力されることになります。

```diff:src/backend/parser/gram.y
/*
 * Santa Definitions
 */
santa_clause:
            SANTA Iconst
                {
+                   if (good_behavior < 0)
+                   {
+                       ereport(INFO, errmsg("You were a bad boy, so you will lose all your rows.  What a pity!"));
+                       $$ = 1; /* dummy */
+                   }
-                   if ($2 > 1)
+                   else if ($2 > 1)
                    {
-                       int santa = random() % $2 + 1;
+                       int santa = random() % ($2 + good_behavior) + 1;

                        if (santa > 1)
                            ereport(INFO, errmsg("Merry Christmas!  Santa will give you %d times rows as a present!!", santa));
                        else
                            ereport(INFO, errmsg("Oh, were you a bad boy this year? Santa will not bring you any present on Christmas..."));

                        $$ = santa;
                    }
                    else
                        $$ = 0;
                }
            | /*EMPTY*/         { $$ = 0; }
        ;
```

# エクゼキュータの修正

最後にエクゼキュータのコードを修正します。[前回の記事](https://qiita.com/yugo-n/private/38c6a268be3032a32930#%E3%82%A8%E3%82%AF%E3%82%BC%E3%82%AD%E3%83%A5%E3%83%BC%E3%82%BF)と同様、ここではシーケンシャルスキャン（SeqScan）の場合のみを見ていきます。

SeqScan で「次の行を取得」する処理を行う ``SeqNext`` 関数の先頭に以下のコードを追加します。``NULL`` を返すと「次の行は存在しない = 全ての行を出力済」という意味になるので、``SANTA``句が使われていて、かつ ``good_behavior`` が負の場合には、この関数は何も行を返さずに終了することになります。

```diff:src/backend/executor/nodeSeqscan.c
static TupleTableSlot *
SeqNext(SeqScanState *node)
{
    TableScanDesc scandesc;
    EState     *estate;
    ScanDirection direction;
    TupleTableSlot *slot;

+   /* you were a bad boy! */
+   if (node->ss.ss_santa > 0 && good_behavior < 0)
+       return NULL;

```

他のスキャン（IndexScan、IndexOnlyScan、BitmapHeapScan、SampleScan、TiScan）のエクゼキュータのコードについても同様の修正を行います。

テーブル行を増やすコードは前回のまま手を加える必要はないので、これで ``good_behavior`` パラメータの追加は完了です。

前回の実装からの差分全体は以下の通りとなりました。

<details><summary>前回からの差分（156行）</summary><div>

```diff
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index 6438c45716..fbd2ba481a 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -47,6 +47,7 @@
 /* GUC variables */
 char	   *default_table_access_method = DEFAULT_TABLE_ACCESS_METHOD;
 bool		synchronize_seqscans = true;
+int 		good_behavior = 0;
 
 
 /* ----------------------------------------------------------------------------
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index 6c023177e7..192be5eb7c 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -81,6 +81,10 @@ BitmapHeapNext(BitmapHeapScanState *node)
 	ParallelBitmapHeapState *pstate = node->pstate;
 	dsa_area   *dsa = node->ss.ps.state->es_query_dsa;
 
+	/* you were a bad boy! */
+	if (node->ss.ss_santa > 0 && good_behavior < 0)
+		return NULL;
+
 	/*
 	 * extract necessary information from index scan node
 	 */
diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c
index b4a4f5392a..feb8d56d45 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -66,6 +66,10 @@ IndexOnlyNext(IndexOnlyScanState *node)
 	TupleTableSlot *slot;
 	ItemPointer tid;
 
+	/* you were a bad boy! */
+	if (node->ss.ss_santa > 0 && good_behavior < 0)
+		return NULL;
+
 	/*
 	 * extract necessary information from index scan node
 	 */
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index 6dd12248ab..1cd55122b3 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -86,6 +86,10 @@ IndexNext(IndexScanState *node)
 	IndexScanDesc scandesc;
 	TupleTableSlot *slot;
 
+	/* you were a bad boy! */
+	if (node->ss.ss_santa > 0 && good_behavior < 0)
+		return NULL;
+
 	/*
 	 * extract necessary information from index scan node
 	 */
diff --git a/src/backend/executor/nodeSamplescan.c b/src/backend/executor/nodeSamplescan.c
index 83c073f07e..b8b4942473 100644
--- a/src/backend/executor/nodeSamplescan.c
+++ b/src/backend/executor/nodeSamplescan.c
@@ -339,6 +339,10 @@ tablesample_getnext(SampleScanState *scanstate)
 	TableScanDesc scan = scanstate->ss.ss_currentScanDesc;
 	TupleTableSlot *slot = scanstate->ss.ss_ScanTupleSlot;
 
+	/* you were a bad boy! */
+	if (scanstate->ss.ss_santa > 0 && good_behavior < 0)
+		return NULL;
+
 	/* If SANTA clause exists, repeate each tuple "santa" times. */
 	if (scanstate->donetuples > 0 && scanstate->ss.ss_santa > 1)
 	{
diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c
index 69803b1982..904eb9efd6 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -54,6 +54,10 @@ SeqNext(SeqScanState *node)
 	ScanDirection direction;
 	TupleTableSlot *slot;
 
+	/* you were a bad boy! */
+	if (node->ss.ss_santa > 0 && good_behavior < 0)
+		return NULL;
+
 	/*
 	 * get information from the estate and scan state
 	 */
diff --git a/src/backend/executor/nodeTidscan.c b/src/backend/executor/nodeTidscan.c
index 66f3fdc35b..1fef6de2bc 100644
--- a/src/backend/executor/nodeTidscan.c
+++ b/src/backend/executor/nodeTidscan.c
@@ -319,6 +319,10 @@ TidNext(TidScanState *node)
 	int			numTids;
 	bool		bBackward;
 
+	/* you were a bad boy! */
+	if (node->ss.ss_santa > 0 && good_behavior < 0)
+		return NULL;
+
 	/*
 	 * extract necessary information from tid scan node
 	 */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9303dc607f..5a69a66e2d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -14141,9 +14141,14 @@ window_specification: '(' opt_existing_window_name opt_partition_clause
 santa_clause:
 			SANTA Iconst
 				{
-					if ($2 > 1)
+					if (good_behavior < 0)
 					{
-						int santa = random() % $2 + 1;
+						ereport(INFO, errmsg("You were a bad boy, so you will lose all your rows.  What a pity!"));
+						$$ = 1; /* dummy */
+					}
+					else if ($2 > 1)
+					{
+						int santa = random() % ($2 + good_behavior) + 1;
 
 						if (santa > 1)
 							ereport(INFO, errmsg("Merry Christmas!  Santa will give you %d times rows as a present!!", santa));
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index dabcbb0736..27fd7e5632 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -3389,6 +3389,16 @@ static struct config_int ConfigureNamesInt[] =
 		check_huge_page_size, NULL, NULL
 	},
 
+	{
+		{"good_behavior", PGC_USERSET, DEVELOPER_OPTIONS,
+			gettext_noop("How good your behavior was this year"),
+			NULL
+		},
+		&good_behavior,
+		0, -1, 10,
+		NULL, NULL, NULL
+	},
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 387eb34a61..8a616e151b 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -30,6 +30,7 @@
 /* GUCs */
 extern char *default_table_access_method;
 extern bool synchronize_seqscans;
+extern int good_behavior;
 
 
 struct BulkInsertStateData;


```
</div></details>

# 動作結果

追加した ``good_behavior`` は ``pg_settings`` システムビューで確認することができます。

```sql
santa=# SELECT * FROM pg_settings WHERE name = 'good_behavior';
-[ RECORD 1 ]---+-------------------------------------
name            | good_behavior
setting         | 0
unit            | 
category        | Developer Options
short_desc      | How good your behavior was this year
extra_desc      | 
context         | user
vartype         | integer
source          | default
min_val         | -1
max_val         | 10
enumvals        | 
boot_val        | 0
reset_val       | 0
sourcefile      | 
sourceline      | 
pending_restart | f
```

この値を1以上の値に設定しておくと、``SANTA`` 句で指定した以上の数の行を貰えることがあります。普段の行いが良いと、良いことがありますね！

```sql
santa=# SET good_behavior TO 3;
SET

santa=# SELECT * FROM box SANTA 5;
INFO:  Merry Christmas!  Santa will give you 7 times rows as a present!!
  present  
-----------
 chocolate
 chocolate
 chocolate
 chocolate
 chocolate
 chocolate
 chocolate
 book
 book
 book
 book
 book
 book
 book
 game
 game
 game
 game
 game
 game
 game
(21 rows)
```

ただし、普段の行いが悪いとテーブルから何も得られません。悲しい。。。

```sql
santa=# SET good_behavior TO -1;
SET

santa=# SELECT * FROM box SANTA 5;
INFO:  You were a bad boy, so you will lose all your rows.  What a pity!
 present 
---------
(0 rows)
```

こういう時はサンタに頼らず、素直に自分で行を取得すれば大丈夫です。来年は良い子でがんばりましょう。

```sql
santa=# SELECT * FROM box;
  present  
-----------
 chocolate
 book
 game
(3 rows)
```

# おわりに

PostgreSQL の ``SELECT`` 文を拡張して新しい句を追加した前回に続いて、今回は新しい設定パラメータを追加してみました。

パラメータの追加自体は思ったほど難しくないと思われたのではないでしょうか。PostgreSQLの内部状態を出力したり振る舞いをちょっと変えてみたいといった奇特な方がいましたら、ちょろっと自分でパラメータを追加して遊んでみるのはいかがでしょうか？

[^advent]: 本当はクロスポストで[前の記事](https://qiita.com/yugo-n/private/38c6a268be3032a32930)を SRA Advent Calender の24日目記事として流用しようと思ったのですが、Qiita さんに「指定された記事は既に他のカレンダーに紐付けられています。」と怒られたので、急遽別のネタを拵えました。
[^guc]: まぁ、結局の所、自己申告なわけですが。