前回やったエスケープシーケンスをSQLに混ぜるの応用です。クエリ結果1行ずつの末尾に疑似的なスリープを入れて遅延表示。普通のデータには使い道がないので、簡単なランダムウォーク(前進のみ)をコンソール上に出します。久々に動画も。
追記 初出時はキャレットを消す方法が不明でしたが、その後(27日夕方)分かったので修正しました。修正前の文書はHTMLの履歴で見れます。
Contents



普通のSELECT文に遅延表示を入れる基本形

実行環境は前回と同じです。エスケープシーケンスを使える他のシェル・ターミナルでも動きそうですが、今回は未確認。

PostgreSQLで遅延表示させるには、組み込み言語それぞれのスリープ&メッセージ出力によるのが普通。例えばPL/pgSQLならpg_sleep関数とraise文のように。今回それより特殊なエスケープシーケンスを使うのは、カラー表示をはじめ色々な画面制御と共通に書けるのと、普通のSELECT文に簡単に混ぜられるから。

↓ そのテストと、比較用にPL/pgSQLの場合。どちらも現在時刻を10回、間隔を空けて表示してます。エスケープシーケンスには直接のスリープが見当らないので、文字色を元に戻す命令を繰り返し挿入して代替(表示そのものは変化なし。回数は適当に)。実行中だけキャレットを消すため、先頭に\x1b[?25l(小文字のエル)、最後に25hを加えました。だから先頭・末尾に空行が入ります。
# \t on \\ \pset format unaligned \\ \timing on 

# select text e'\x1b[?25l'
  union all
  select concat(clock_timestamp(),
    repeat(e'\x1b[m', 2.4E+4 :: int)) 
  from generate_series(1, 10)
  union all
  select e'\x1b[?25h';
  
# do $$ begin
    for i in 1..10 loop
        raise info '%', clock_timestamp();
        perform pg_sleep(1);
    end loop;
  end; $$;

↑ 全部終わった時の画像。10回の時刻を見ると、最初のSELECT文がほぼ同一なのに対しPL/pgSQLでは1秒間隔です。SELECT文はクエリ結果が出た後に遅延(コンソールがエスケープシーケンスを受け取ってそうする)。一方PL/pgSQLではクエリを返すまでの間に関数内部で遅延が起きるという、基本原理の違いによります。

SELECT文の遅延は時間指定できず、エスケープシーケンスを入れる回数と時間の関係も実行環境に依存。何回か試して概ねの回数を決めます。

↓ 実行時の動画。確かにSELECT文でも遅延表示できました。
≫ Link : cap1.webm


簡易ランダムウォークの骨組み

コンピュータによる普通のランダムウォークは上下左右いずれにもある確率で移動しますが、今回は簡単のため「左下・真下・右下」の3方向だけ。つまり下に足される行に必ず移り、前行に戻ったり真横に行ったりしません。

↓ まず遅延表示も色もない骨組みを作成。WITH RECURSIVE(再帰クエリ)を使い、後述の終了条件になるまで繰り返します。ウォーク主体は2人、横幅を3分する位置から出発。終了条件は(1)どちらかが横にはみ出る(2)2人が衝突か交錯(3)設定した最大行数に達する、のいずれか。
with recursive setting (ncol, nrow_max) as (
    select 30, 30
), line (rowid, p1, p2) as (
    select 1, ncol / 3, ncol / 3 * 2 from setting
    union all
    ( select
        rowid + 1,
        case when p1 >= p2 or p1 < 1 or ncol < p1 then null
            else direction1(p1) end,
        case when p2 < 1 or ncol < p2 then null
            else direction1(p2) end
      from line, setting
      where p1 is not null and p2 is not null and rowid <= nrow_max 
      order by rowid desc limit 1
    )
)
select * from line;


上の赤字directionが、ある位置から次への移動を確率的に決める関数。今回は ↓ こんな感じで、真下に行くのはかなり稀(0.1)というフラフラの状態です。
create or replace function direction1(int)
returns int language sql immutable as 
$$
    select case
        when rnd < brk[1] then $1 - 1
        when rnd < brk[2] then $1
        else $1 + 1 end
    from cast('{ 0.45, 0.55 }' as float[]) as brk, 
        random() as rnd;
$$;


これを実行すると例えば ↓ こんな結果になります。列p1・p22人の横位置を表し、9回目と10回目の試行の間に交錯したので終了。
+-------+----+----+
| rowid | p1 | p2 |
+-------+----+----+
|     1 | 10 | 20 |
|     2 | 11 | 21 |
|     3 | 12 | 20 |
|     4 | 13 | 19 |
|     5 | 14 | 19 |
|     6 | 13 | 20 |
|     7 | 14 | 19 |
|     8 | 15 | 18 |
|     9 | 16 | 17 |
|    10 | 17 | 16 |
|    11 |    | 17 |
+-------+----+----+ 
(11 行)


上のように途中で終わるのは意外と少なく、大半は終了条件に至らず何10行も進みます。例えば ↓ のように。左下・右下が同確率なので、ジグザグながらも総体的には真下を中心に行く感じ。


2点の色、行ごとの遅延をエスケープシーケンスで追加

2点の位置を色付きの■で表現し、間を□で埋めます。↓ がそれを関数化したもの。横にはみ出てたり交錯してたらNULLにするなど、意外と面倒でした。
create or replace function randwalk_line(
    ncol int, p1 int, p2 int,
    color0 text, color1 text, color2 text)
returns text language sql immutable as 
$$
select case
    when p1 = p2 then concat(
        repeat(m0, p1 - 1), m1, repeat(m0, ncol - p2))
    when p1 = p2 + 1 then concat(
        repeat(m0, p2 - 1), m2, m1, repeat(m0, ncol - p1))
    else concat(
        repeat(m0, p1 - 1), case when p1 between 1 and p2 then m1 end, 
        repeat(m0, p2 - p1 - 1), case when p1 < p2 then m2 end,
        repeat(m0, ncol - p2))
    end
from concat(e'\x1b[38;2;', color0, 'm□') as m0,
    concat(e'\x1b[38;2;', color1, 'm■') as m1,
    concat(e'\x1b[38;2;', color2, 'm■') as m2;
$$;


上の関数を前項のクエリに加え、各行末に遅延用のエスケープシーケンスを入れて ↓ でき上がり。これで格子とランダムウォークの軌跡が表示されます。冒頭のブロックで列数、最大行数、遅延の長さを変更可。あと前項と同様、実行中だけキャレットを消すため先頭・末尾にもエスケープシーケンスを入れました。
# \t on \\ \pset format unaligned \\ \pset pager off
# with recursive setting (ncol, nrow_max, wait) as (
    select 30, 30, 1e+4
), line (rowid, p1, p2) as (
    select 1, ncol / 3, ncol / 3 * 2 from setting
    union all
    ( select
        rowid + 1,
        case when p1 >= p2 or p1 < 1 or ncol < p1 then null
            else direction1(p1) end,
        case when p2 < 1 or ncol < p2 then null
            else direction1(p2) end
      from line, setting
      where p1 is not null and p2 is not null and rowid <= nrow_max
      order by rowid desc limit 1
    )
)
select text e'\x1b[?25l'
union all
select randwalk_line(ncol, p1, p2,
    '50;50;50', '0;255;0', '255;0;0') || repeat(e'\x1b[m', wait :: int) 
from line, setting
where p1 is not null and p2 is not null
union all
select e'\x1b[38;2;255;255;255m' || e'\x1b[?25h';
    -- reset font color and show caret


稀にConEmuの色表示がおかしくなったけど、別ウィンドウで起動すれば正常になりました。

↓ 最後にクエリ全体をストアド化。基本、WITH句の冒頭ブロックを引数にするだけ。ついでに4つ目の引数として、実行前の画面クリア有無を追加。これもエスケープシーケンスで出来ます(ターミナルによっては上手くいかない場合も)。
create or replace function randwalk1(
    int, int, float, bool default null)
returns setof text language sql immutable as
$$
with recursive setting (ncol, nrow_max, wait, init_cls) as (
    select $1, $2, $3, coalesce($4, false)
    -- select 30, 30, 1e+4, true -- ex
), line (rowid, p1, p2) as (
    select 1, ncol / 3, ncol / 3 * 2 from setting
    union all
    ( select
        rowid + 1,
        case when p1 >= p2 or p1 < 1 or ncol < p1 then null
            else direction1(p1) end,
        case when p2 < 1 or ncol < p2 then null
            else direction1(p2) end
      from line, setting
      where p1 is not null and p2 is not null and rowid <= nrow_max
      order by rowid desc limit 1
    )
)
select text e'\x1b[?25l'
union all
select concat(e'\x1b[2J', 'SELECT randwalk1(',
    ncol, ', ', nrow_max, ', ', wait, ', ', init_cls, ');')
from setting where init_cls
    -- clear screen
union all
select randwalk_line(ncol, p1, p2,
    '50;50;50', '0;255;0', '255;0;0') || repeat(e'\x1b[m', wait :: int) 
from line, setting
where p1 is not null and p2 is not null
union all
select e'\x1b[38;2;255;255;255m' || e'\x1b[?25h';
    -- reset font color and show caret
$$;



デモ動画

ストアドの実行例をキャプチャし、WEBM形式に変換して埋め込みました。ブラウザが未対応なら、動画下のリンクからダウンロードして適当なプレーヤで見て下さい。
≫ Link : cap2.webm

動画で実行しているクエリは ↓ こちら。2行目の引数はいくつか変えてます。
# \t on \\ \pset format unaligned \\ \pset pager off 
# select randwalk1(15, 50, 2e+4);

列数15でやると ↓ すぐ横にはみ出して終わる時もあれば、結構長く続く時もあり。列数30、行数50だと「完走」が大半で稀に途中終了、という感じです。