私が歌川です

@utgwkk が書いている

WEB+DB PRESS Vol.123『Perl Hackers Hub』に寄稿した #wdpress

WEB+DB PRESS Vol.123 (2021/6/24 (明日!) 発売) のPerl Hackers Hubに「他言語のライブラリをPerlに移植する」というテーマで寄稿しました。twitter-textをPerlに移植した際に得られた知見をもとに、他言語で実装されたライブラリをPerlに移植する際のコツについて執筆しました。

Perlに限らず他言語であっても、使いたいライブラリやSDKが使いたい言語で実装されていない、ということはしばしばあると思います。そういった際にどのようにライブラリを移植するのか、気をつけるべきことは何か、について6ページにまとめています。特にPerlに移植する際に気をつけることを重点的に書いていますが、Perl以外への移植を目標とする際にも参考にできることが書けたと思います。

見本誌をいただいたのですが、すごく読みごたえのある記事ばかりでした。ぜひお買い求めください。個人的には、HTTP/3やNext.js・GraphQLなど最新の話題が気になっていたところなので、WEB+DB PRESSでキャッチアップできるのがありがたいです。

gihyo.jp

執筆に誘っていただいた id:papix さん、原稿を重点的にレビューしていただいた @inao さん、監修の皆様、技術評論社の皆様にこの場を借りて感謝を申し上げます。

@inao さんを含めた編集部の方々に原稿をレビューしていただけたのが本当によかったです。普段たどたどしく書いている日本語がどんどん洗練されていって、すごい、意味の通った日本語になっていく……と思いました。

昔からブログにはいろいろな記事を書いていたのですが、こうやってPerl Hackers Hubに寄稿する貴重な機会をいただけたのは嬉しい限りです。Perlに関する情報を調べていて何度も参考にさせていただいた連載なので、そこに自分の名を連ねられるのは、恩返しのような心持ちがあります。

繰り返しになりますが、2021/6/24 (なんと明日!!) 発売のWEB+DB PRESS Vol.123をよろしくお願いします。

f:id:utgwkk:20210608213827p:plain

2021/8/25 追記

全文がgihyo.jp上で公開されました。どうぞご覧ください。

gihyo.jp

SQLでSQLを組み立てる

趣味で作っているアプリケーションについて、データマイグレーションを行いたくなった。ちゃんとやるなら、マイグレーション用のスクリプトを書いて、メンテナンスモードにして、スクリプトを適用する……みたいな手順を踏むと思うけど、趣味プロダクトなので高速に済ませたい。SQLでSQLを組み立てればワンタイムスクリプトを書かなくてもよいのではないか、という考えに思い至ったのでメモする。

以下はSQLiteでの事例だけど、他のDBMSでも似たようなことはできると思う。

組み立て方

文字列連結

SQLiteの文字列連結は || 演算子で行う。

sqlite> SELECT 'a' || 'b';
ab

NULLと文字列を連結するとNULLになり、ターミナル上では空行が出力される。 typeof は値の型を返す関数である。空文字列ではなくNULLが出力されていることを確認した。

sqlite> SELECT 'aaa' || NULL;

sqlite> SELECT typeof('aaa' || NULL);
null

文字列のエスケープ

文字列連結でSQLを組み立てると、当然SQLインジェクションのリスクがある。 user テーブルに name カラムがあって、たとえば ' OR 1=1 -- という文字列が格納されていると考える。以下の例ではすでにSQLインジェクションに成功している。

sqlite> SELECT "UPDATE users SET some_flag = 1 WHERE name = '" || "' OR 1=1 --" || "'";
UPDATE users SET some_flag = 1 WHERE name = '' OR 1=1 --'

SQLiteには quote という関数があり、与えられた文字列をクォートして、さらにエスケープも行ってくれる。

sqlite> SELECT "UPDATE users SET some_flag = 1 WHERE name = " || quote("' OR 1=1 --");
UPDATE users SET some_flag = 1 WHERE name = ''' OR 1=1 --'

ちなみにシングルクォートのエスケープは '' である。

sqlite> SELECT 'a''b';
a'b

ここまでの内容で、単純なUPDATE文ぐらいなら書けるようになったと思う。

WHERE IN を書きたい

ところで、こういうSQLを生成したいときはどうしたらよいのか? IN (...) の中には複数の値が入るが、果たしてSQLで書けるのか。

UPDATE users SET some_flag = 1 WHERE id IN (...)

これは group_concat という関数を使うことで実現できる。

まず、対象となるidをかき集めるSELECT文を用意する。

-- 対象となるidをかき集めるSELECT文
SELECT id FROM users WHERE some_condition;

idを group_concat(id) に置換すると、INクエリに突っ込めそうな文字列ができる。

-- 対象となるidをかき集めるSELECT文
sqlite> SELECT group_concat(id) FROM users WHERE some_condition;
2,4,6,8,10,12,14,16,18,20

あとはうまく文字列連結してやればよい。

sqlite> SELECT 'UPDATE users SET some_flag = 1 WHERE id IN (' || group_concat(id) || ')' FROM users WHERE some_condition;
UPDATE users SET some_flag = 1 WHERE id IN (2,4,6,8,10,12,14,16,18,20)

万が一、対象となるidが存在しない場合は group_concat(id) はNULLを返す。従って文字列連結した結果もNULLとなり、空行が出力される。 UPDATE users SET ... WHERE id IN () のようなクエリを実行してしまいエラー、ということにはならないようだった。そもそもSQLiteの場合はエラーにならないが……*1

WHERE IN の中でUPDATEする対象のidをSELECTするのとの違い

SQLを生成する場合は、具体的にどういう行がUPDATEされるのかをクエリやコメントに出力できる。SELECT文だけで何行UPDATEされるかはデータに対する知識がないとすぐには計算できないけど、クエリに直接idが書いてあったら、なんか多すぎておかしい、全くUPDATEされないじゃん、みたいなことに気づけそう。

UPDATE users SET some_flag = 1 WHERE id IN (...) -- ()の中を見たら、UPDATEされたusers.idが分かる

たとえばこういう感じにしたら、対象となるユーザー名も分かってお得。UPDATE文を生成するなら、UPDATEする前の値もメモしておくと後々調査したくなったときに便利かもしれない。

sqlite> SELECT 'UPDATE users SET some_flag = 1 WHERE id = ' || id || '; -- ' || name FROM users;
UPDATE users SET some_flag = 1 WHERE id = 2; -- john_doe
...

まとめ

SQLでSQLを組み立てることでデータマイグレーションを行えそう。生成したSQLをどこかにメモしておけばあとで見返すことができる。

補足 (組み立てたSQLをどう実行するのか)

SQLiteには与えられた文字列をSQLだと思って実行する eval 的な関数はない (当然標準SQLにもないと思う) ので、出力したSQLをコピペして改めて実行することになる。

標準入力から流し込んでもよいし、先頭に BEGIN; を足してからトランザクションを張りつつペーストして実行してもよい。

参考

SQLiteのドキュメントが好きで、よく読んでいる。

Pythonのキーワード引数に任意のdictを展開して渡す

Pythonでは、引数に **kwargs のように書くことで、任意のキーワード引数を受ける関数を定義することができる。 キーワード引数はdictとして使うことができる。

def print_kwargs_as_dict(**kwargs):
    print(kwargs)

キーワード引数では、識別子としてvalidな名前の引数しか渡せない。また、任意の文字列や文字列リテラルを使うことはできない。以下の式はいずれも文法エラーになる。

print_kwargs_as_dict(foo/bar='baz')
print_kwargs_as_dict('foo/bar/'='baz')

ところで、Pythonには任意のdictを展開して関数に渡す文法が定義されている。

kwargs = dict(foo='bar')
print_kwargs_as_dict(**kwargs)
# => {'foo':'bar'}

ここで、関数呼び出しの文法を注意深く眺めると、キーワード引数に任意のdictを展開して渡せることが分かる。つまり以下の式はvalidである。

print_kwargs_as_dict(**{'foo/bar':'baz'})
# => {'foo/bar':'baz'}

ペアプロ活動パターン

仕事でよくペアプロでコードを書くのですが、気をつけていることがいくつかあるので共有します。みなさまは何に気をつけていますか?

考えていることを声に出す

  • やりたいことが実現できずに詰まっていると黙りがち
    • どうやったらいいか分からず困っている・違う結果になる、みたいなのを口に出す
    • ペアの人が解決方法を知っているかもしれない
    • クマさんデバッグみたいな効果もありそう
      • ところで「クマさんデバッグ」って一般に通じる言葉なんですか?
  • 書いているコードの自己評価を行う
    • 最高のアルゴリズムになった、とか、これは一時しのぎです、みたいな
  • 認識がずれていないかの確認をする
    • 「こう思っているけどそれで合ってますか?」

こまめにcommitする

  • こまめにセーブするイメージ
  • git diffを確認したら、どこまでできているのかを見れる
  • いざとなったらgit resetで戻れる
  • ひと通りできあがってからちょっとずつcommitするよりは、こまめにcommitしたほうが、どこまでstageするのかとか考えなくて済む

エディタだけでなくブラウザの様子も見せる

  • リファレンスを参照している様子とか、やり方が分からないのでググっている様子とかも見せる
  • 知見共有みたいな側面が強い
    • こうやって調べてるのか〜みたいなのが伝わるとよさそう
    • ナビゲーターをやるときは、こういうページを参照したり検索したりしてみるといいかも、というヒントを思い付いたら伝えている
  • 映り込むとまずいタブは予め閉じておくか、ウィンドウを分けておきましょう

「クマさんデバッグ」について

ペアプロ活動パターン - 私が歌川です

くまのぬいぐるみについて <a href="https://toya.hatenablog.com/entry/2015/07/09/133609" target="_blank" rel="noopener nofollow">https://toya.hatenablog.com/entry/2015/07/09/133609</a>

2021/06/05 09:21
b.hatena.ne.jp

toya.hatenablog.com

「プログラミング作法」こんど読んでみます。

fetch APIにおけるHTTPリクエストの中断・タイムアウト

AbortControllerを使うことで実現できる。

MDNにも書いてあるけど、以下の操作でfetch APIによるHTTPリクエストを中断できる。

  1. fetch() の第2引数のオブジェクトの signal フィールドに AbortController.signal を渡す
  2. AbortController.abort() を呼ぶ

HTTPリクエストが中断されると、 fetch() が返すPromiseはrejectされる。

ユーザー操作でリクエストを中断する

MDNのサンプルコードデモを参照。

タイムアウトさせる

window.setTimeout() のコールバック関数内で AbortController.abort() を呼ぶとできる。 タイムアウトを過ぎなかったときのために window.clearTimeout() しているけど、いらないかもしれない。

const invokeAPI = async (url) => {
  const controller = new AbortController();
  const timer = window.setTimeout(() => {
    controller.abort();
  }, 1000);
  const response = await fetch(url, {
    signal: controller.signal,
  });
  window.clearTimeout(timer);
  return resp;
};

Promise.race() でタイムアウトさせる場合との違い

Promise.race()fetch() とタイムアウトさせるPromiseを並走させてもタイムアウトは実現できそうに見える。

const timeout = (msec) => {
  return new Promise((_, reject) => {
    window.setTimeout(() => reject('timeout'), msec);
  });
};

await Promise.race([fetch(...), timeout(1000)]);

たしかにタイムアウトできているように見えるけど、AbortControllerを使った場合と違ってHTTPリクエスト自体はキャンセルされない。

一方axiosでは

cancel tokenという仕組みでHTTPリクエストをキャンセルできる。また、リクエストに対してタイムアウトを設定できる

cancel tokenはcancelable PromiseというECMAScriptのproposalに基づいて実装されたらしいけど、このproposalは取り下げられた。Promiseをキャンセルしたいという欲求がありそう。