- SQL
LATERALを使ったクエリ
SQLのLATERALというのを知りました。個人的な理解としては効率的なCROSS JOIN、N+1クエリのSQL版といった感じです(N+1に関してはJOINしてればだいたいそうだが)。LATERALはサブクエリの中で外側のクエリの値を参照できるという特徴があります。
具体例として下記のようなテーブルを考えてみます。アンケートと、その回答を格納するテーブルです。以降PostgreSQLを前提としています。
CREATE TABLE surveys ( survey_id bigint primary key, first_served_campaign bool ); CREATE TABLE answers ( answer_id bigint primary key, survey_id bigint, user_id bigint, answered_at timestamptz );
ここで、アンケートに回答したユーザーの中で、先着3名までにプレゼントをする企画があったとします。
first_served_campaign
カラムがtrueの場合、そのアンケートが対象となります。このとき、先着3名のユーザーを取得するクエリを考えます。パッと思いつくのがウィンドウ関数を用いる関数です。
WITH filtered_surveys AS ( SELECT survey_id FROM surveys WHERE first_served_campaign = true ), ranked_answers AS ( SELECT fs.survey_id, a.user_id, a.answered_at, ROW_NUMBER() OVER (PARTITION BY fs.survey_id ORDER BY a.answered_at) AS rank FROM filtered_surveys fs JOIN answers a ON fs.survey_id = a.survey_id ) SELECT survey_id, user_id FROM ranked_answers WHERE rank <= 3 ORDER BY survey_id;
ただ、このクエリ、ちょっと遅い。
ranked_answers
で全ての回答を取得してから、その中から先着3名を取得しているためです。ここでLATERALを使うと、サブクエリの中で外側のクエリの値を参照できるため、効率的に取得できます。SELECT s.survey_id, a.user_id FROM surveys s, LATERAL ( SELECT user_id FROM answers WHERE answers.survey_id = s.survey_id ORDER BY answered_at LIMIT 3 ) a WHERE s.first_served_campaign = true ORDER BY s.survey_id;
first_served_campaign
がtrueなアンケートが5,000件、回答がそれぞれアンケート毎に10,000件つまり合わせて回答が合わせて50,000,000件ある場合、前者のクエリは50,000,000件の回答を取得してから先着3名を取得しますが、後者のクエリはアンケート毎に3名の回答を取得するため、つまり15,000件の回答を取得するだけで済みます。実際、実行計画を見ると、前者のクエリはGather Merge
が50,000,000行に対して、後者のクエリはNested Loop
が15,000行に対して行わるような計画になっています。結果コストが圧倒的に変わります。 - PostgreSQL
Docker上でPostgreSQLの拡張機能(pg_cron)を有効化する
先日pg_cronを知りました。で、こいつを開発環境のPostgreSQLに入れようと考えました。正直こいつを開発環境にいれるのは不要だと思いますが、他に開発環境でも使いたい拡張が現れたときに対応できるよう、試しに開発環境で有効化する方法を調べました。まぁあんまり開発環境のDocker上で拡張を有効化している人いませんでしたが。
pg_cronを追加すると仮定すると、下記のようなDockerfileで対応可能です。ちなみに15を使っている理由は開発開始時にAlloyDBが16をサポートしていなかったからです。今は16もサポートしています。
FROM postgres:15 RUN apt-get update && apt-get -y install postgresql-15-cron COPY ./entrypoint-pg-cron.sql /docker-entrypoint-initdb.d CMD ["postgres", "-c", "shared_preload_libraries=pg_cron", "-c", "cron.database_name=development"]
同一ディレクトリに下記のファイルを追加します。
entrypoint-pg-cron.sqlCREATE EXTENSION IF NOT EXISTS pg_cron;
docker-comose
を利用する場合も、通常のイメージを置き換える形で問題ないです。compose.ymlservices: db: # image: postgres:15 build: context: . environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: development ports: - 5432:5432 volumes: - db_volume:/var/lib/postgresql/data
Dockerfileについての解説です。
postgresql-15-cron
のインストールはpg_cron
のインストール方法に従っているだけです。ちなみにPostgreSQLのイメージはdebian:bookworm-slim
がベースになっているので、Debianベースのインストールです。/docker-entrypoint-initdb.d
はPostgreSQLのDockerにてデータベースを立ち上げてから実行されるSQLまたはシェルスクリプトを配置するディレクトリです。ここにcreate extension
をするSQLを配置しています。
注意点として、こちらのスクリプトはすでにデータベースが構築されていると動作しません。永続化しているvolumeなどですでにデータベースが構築されている場合はdocker compose down -v
とかdocker volume rm
とかで該当のボリュームを削除してください。最後に
CMD
を拡張しています。本来のCMD
はpostgres
1つだけなので、オプションをくっつけたい場合はただ増やせばいいだけです。
confファイルを更新する方法も合ったんですが、ファイルを用意したりする必要があるので見送りました。オプションの内容はセッティングに記載されています。timezoneを指定する場合はここに追加します。調査の際に参考にしたのは下記の記事です。
- PostgreSQL
pg_cronで定期的にデータを削除する
pg_cronはその名の通り、PostgreSQL上でcronベースのジョブスケージュールを行う拡張です。ちなみにMySQLではちゃんと調べていないですがイベントスケジューラというものが標準で搭載されているらしいです。
コレを使えば定期的にデータを削除できます。例えば、ログテーブルの古いデータを削除するとか。
今回はGoogle CloudのAlloy DBを使う前提です。使用可能拡張のページがあり、そこをみると問題なく利用できます。設定が必要で、
alloydb.enable_pg_cron
を有効にする必要があります。設定変更のために20分ぐらいのダウンタイムが生じます。またこれはpg_cron側の設定ですが、cron.database_name
でデータベースを、cron.timezone
でタイムゾーンを変更できます。あとはpg_cronの公式ドキュメント通りに対応すればよいです。
-- 00:00(JST) SELECT cron.schedule('auth-delete-logs', '0 15 * * *', $$DELETE FROM logs WHERE created_at < now() - interval '90 days'$$); -- 削除するときは下記 -- SELECT cron.unschedule('auth-delete-logs');
ログはできればCloud Logging使ったほうがいいと思う。
- Electron
Playwrightで動かすElectron上でipc通信を発火する
PlaywrightではElectronの自動化を(実験的ながら)サポートしています。Playwright、Electron単体でもとっつきづらいのに、その2つが組み合わされば特級呪物になりそうな気がします。Smokeテスト程度であればよさそうかもしれません。
Electronは大雑把に言うとmainプロセスとrendererプロセスがあります。rendererプロセスは通常のブラウザです。例えばファイルの読み取り・更新をしたいときなどはnode.jsであるmainプロセスにIPC通信することで実現します。逆も同様で、mainプロセスで何らかのイベント、特定のファイルが更新された場合などにrendererプロセスにIPC通信することでファイルの変更をブラウザに反映させることができます。
テストを実施するときに、例えばどうしてもmain → rendererプロセスの通信の発火条件を満たせないケースがあったとします。ここでは特定のファイルが更新された場合、という条件とします(それぐらいなら頑張って発火できると思いますが)。このときにrendererプロセスに対してIPC通信でデータを送信する方法です。
下記が基本形です。
const { _electron: electron } = require('playwright'); (async () => { const electronApp = await electron.launch({ args: ['main.js'] }); const electronPage = await electronApp.firstWindow(); })();
ここで重要なのは
electronApp
がmainプロセス、electronPage
がrendererプロセスに紐づいていることです。今回はmain → rendererの通信ですので、electronPage
でデータをsendします。
具体的にはevaluate
メソッドを利用します。await electronApp.evaluate(async (app) => { // NOTE: ウィンドウIDは起動時からインクリメントされるため、最初に開いたウィンドウである1を指定している const focusedWindow = app.BrowserWindow.fromId(1); focusedWindow?.webContents.send("changeConfigFile", { config: { hoge: "fuga", } }); });
上記のようにすることでデータを送信できます。が、残念ながらwindowのIDを取得できないため、そこは固定値にするしか現状は無いと思われます。幸いなことにElectronのwindowのIDは1からインクリメンタルされる数字なので特定は容易いです。
- Google Cloud
permissions.cloudがCloudのIAMの権限を見るのに便利
permissions.cloudはクラウドの権限周りを確認できるサイトです。
Cloud Runにおける権限変更に合わせて、既存のIAMが問題ないかを確認するために利用しました。
現在のCloud Runのデプロイするサービスアカウントは
roles/artifactregistry.writer
を持っていましたが、このroleで問題ないか、roles/artifactregistry.reader
を付与する必要があるかを確認したかったです。が、Google Cloudのドキュメント上にはroleの持つ権限が記載がなかったですが、上記サイトにて記載されていました。
roles/artifactregistry.writerの権限を確認し、roles/artifactregistry.readerの権限をすべて持っていることを確認しました。ドキュメントに記載されていない権限が出てくるの良いですね。データセットの元はiam-datasetというリポジトリであり、中身を見るとクローラーで収集しているっぽいです。その元データはIAM permissions referenceというデータでした。こんなページあったんですね。超重いので、permissions.cloudを活用していきたいと思います。
- 心理学
特殊的好奇心と好奇心の尺度
特殊的好奇心という言葉を知った。
これは心理学者のBerlyneの「Conflict, arousal, and curiosity」によって提唱された概念である。本を読んだわけだわけではないが、調べてみると 知的好奇心尺度の作成 [1]という論文を見つけ、様々な知らない世界があったのでメモ。
Berlyne氏は好奇心には2つあり、知的好奇心と知覚好奇心とした。知覚好奇心は聴覚・触覚・味覚・嗅覚・視覚の身体的感覚の新規性を求めることである。知的好奇心はさらに2つあり、拡散的好奇心と特殊的好奇心があるとした。 知的好奇心尺度の作成 では拡散的好奇心は新奇の情報を探し求めることを動機付け、特殊的好奇心はズレや矛盾などの認知的な不一致を解消するために特定の情報を探し求め続けることを動機付ける、としている。論文内の例として、拡散的好奇心はたまたま推理小説を手に取り読むこと、特殊的好奇心は推理小説の内容を理解するまで何度も読むことが挙げられている。
論文は尺度を作るための2つの研究からなっていて、1つは12個の設問により拡散的好奇心と特殊的好奇心の尺度を作成し、もう1つはいくつかの他の尺度と比較して妥当性を検証している。比較対象の尺度も面白かったので紹介してみる。
- Big Five尺度:利用しているのはBig Five尺度短縮版[2]である。人のパーソナリティを5つの要素に分類する Five Factor Model では、外向性、協調性、誠実性、神経症的傾向、開放性がある。これらを形容詞を利用して評価するのがBig Five尺度であり、オリジナルより少ない設問数で評価できる短縮版もある。外向性は名前の通り外交的な人間かどうか。協調性・誠実性も同様に文字通り。神経的症傾向は緊張やストレスへの耐性。開放性が好奇心に強く結ばれており、この尺度が高いと新しいアイデアや既存にとらわれない考えができるとされている。
- BIS/BAS尺度:人の動機付けを評価する尺度であり、 BIS(Behavioral Inhibition System)は罰を避ける動機付け、BAS(Behavioral Activation System)は報酬を求める動機付けを評価する。BASにはさらに3つの要素があり、それぞれ駆動・刺激探求・報酬反応性とされている。駆動は報酬のために行動する・刺激探求は報酬がありそうならやってみる・報酬反応性は報酬があるなら興奮するといった感じの理解をしている。
- 話はそれるがBIS/BAS が意思決定フレーミング効果に及ぼす影響という論文も面白かった。
- 認知的完結欲求尺度:問題に対する回答に曖昧さを嫌い完結さを欲求する尺度である。心理テストとかで時折出てくる「決断するのに苦労する・時間をかける」(Yesだと低い)や「重要な決断を素早くできる」などに代表される決断性、「規則正しい生活が合っている」などの秩序(完璧に私は秩序がない人間だ)、「何がわからない状況だとワクワクする」(Yesだと低い、私は100%Yesである)や「何やるかわからない人とは行動したくない」などの「予測可能性」の3つの尺度がある。
- 認知的欲求尺度:簡単に言えば「頭を使うのが好き」とか「必要以上に課題に対して考えてしまう」などによって評価する。私は100%Yesである。
- 曖昧さへの態度尺度:曖昧さにたいしてYes/Noという単純な尺度ではなく、さらに享受・受容・統制・排除・不安という5つの要素に分離した尺度。享受は曖昧であると全部試したい/想像する。受容は曖昧であることを良しとする。統制は曖昧な情報の確度を高める。排除は曖昧な状態が嫌いである。不安は曖昧な状態から遠ざかりたい。似ている感じもするがこの5つは独立している尺度らしい。
知的好奇心尺度の作成では3つの仮説を立てている。
- 拡散的好奇心・特殊的好奇心には知的好奇心としての共通性がある。具体的には「開放性尺度」、「BAS駆動尺度」、「認知的欲求尺度」との関連が高い
- 特殊的好奇心は拡散的好奇心に比べ「認知的完結欲求尺度」「曖昧さへの統制尺度」「曖昧さへの排除尺度」「曖昧さへの不安尺度」との関連が高い
- 拡散的好奇心は特殊的好奇心に比べ「BAS刺激探求尺度」「曖昧さへの享受」「曖昧さへの受容」との関連が高い
全てが一致しているわけではない(特に知的好奇心がある人間は曖昧さへの否定的態度を取るため)が、概ね仮説通りという感じの結果だった。
今回知った尺度を個人に投影すると面白い。私としては知的好奇心はある方だと思っているが、今回の指標全体的に拡散的好奇心・特殊的好奇心両方の側面もありそうでこういった尺度に裏付けされることがわかる。しかし「認知的完結欲求尺度」はいずれも当てはまっていない。これはこの世界には未知しか存在しないからこそ未知に出会える環境にワクワクしているというある意味で好奇心のようなものかなと。
西川一二, 雨宮俊彦. 知的好奇心尺度の作成 - 拡散的好奇心と特殊的好奇心. 教育心理学研究. 2015. 63巻. p412-425. https://www.jstage.jst.go.jp/article/jjep/63/4/63_412/_pdf ↩︎
並川努, 谷伊織, 脇田貴文, 熊谷龍一, 中根愛, 野口裕之. Big Five尺度短縮版の開発と信頼性と妥当性の検討. 心理学研究. 2012. 83巻2号. p91-99. https://www.jstage.jst.go.jp/article/jjpsy/83/2/83_91/_pdf ↩︎
- Electron
XvfbによるElectronのヘッドレス化(Playwright × ElectronをDocker上で動かす)
Electronにはヘッドレスモードはない。そのため、GUIの無いシステムではElectronを使うこともできないわけで。たいてい問題はないが、つまりDocker上で動かすことができないということになる。
XvfbはX Virtual FrameBufferのことで、X Window Systemの仮想ディスプレイのサーバーである。これを使うことでElectronをヘッドレスモードで動かすことができる。下記のドキュメントを参照。
https://www.electronjs.org/ja/docs/latest/tutorial/testing-on-headless-ci
具体的にPlaywright × ElectronをDocker上で動かせるようにしたいときに使える。下記はDockerfileの例(
yarn e2e
でPlaywrightのテストを実行することを想定している)。FROM mcr.microsoft.com/playwright:latest@ WORKDIR /app RUN apt-get install -y xvfb RUN npm uninstall -g yarn \ && rm -rf /usr/local/bin/yarn /usr/local/bin/yarnpkg COPY ./package.json ./package.json RUN corepack enable \ && corepack install RUN yarn install --immutable RUN yarn playwright install chromium COPY . . CMD ["xvfb-run", "yarn", "e2e"]
注意するべきはこれだと動かない場合がある。というのもDockerの実行プロセスがPID 1で動いておりシグナルハンドルが出来ないから…だと思う。–initオプションを付与すると動くようになるので、多分そうだと思う。
後もう1つ、大抵の場合はWindowsで動かす場合が多いので、お手持ちのCIのWindows環境で動かすほうがいいと思う。Github Actionsであればwindows環境がある。
- javascript
package.jsonのdependenciesのバージョンをpinするシェルスクリプト
Renovateでは依存関係をpinすることを推奨していますし、yarnではdefaultSemverRangePrefixによりデフォルトでバージョンを固定化出来ます。
しかしこういった設定をする前にあれこれインストールしてしまって、後からpinするの、ちょっと面倒ですよね。renovateが勝手にやってくれるはずなんですが、なんかやってくれないし。
下記はそれをやってくれるスクリプトです。#!/bin/bash while read -r package current_version; do latest_version=$(yarn info "$package" version --json | jq -r '.children.Version') jq --arg package "$package" --arg latest_version "$latest_version" '.dependencies[$package] = $latest_version' package.json > tmp.json && mv tmp.json package.json done < <(jq -r '.dependencies | to_entries[] | "\(.key) \(.value)"' package.json)
注意点として下記があります。
jq
が必要ですdependencies
を対象としています- 2行目・4行目の
dependencies
をdevDependencies
に書き換えればdevDependencies
を対象に出来ます
- 2行目・4行目の
yarn
を利用していますpnpm
の場合はlistが使えると思います- その場合2行目の
jq
のコマンドもおそらく変える必要があります
- Next.js
Next.jsでビルド中かどうかを判定する(Next.jsのビルド時だけエラーになる処理に対する対応)
NEXT_PHASE
という環境変数を利用することで判定できます。またはNEXT_IS_EXPORT_WORKER
でも判定可能です。背景
下記のような、
環境変数がなかったらエラーになる
処理を書こうとします。config.tsconst fetchEnv = (key: string): string => { const value = process.env[key]; if (!value) { throw new Error(`環境変数 ${key} が設定されていません`); } return value; } export const OIDC_ISSUER = fetchEnv('OIDC_ISSUER');
この処理を、例えば
app/api/auth/login.ts
等で下記のように記載するとします。app/api/auth/login.tsimport { OIDC_ISSUER } from "@/config"; const issuer = new Issuer({ issuer: OIDC_ISSUER, // ... }); export function GET() { // ... } export const dynamic = "force-dynamic";
動的なページであれば、実行環境の環境変数が利用されるのでビルド時には本来不要です。そのためDockerなどでビルドするときも環境変数は不要なはずです。
一方で上記の状態でビルドを行うと、Next.jsのビルド時に実際のコードが読み込まれ、fetchEnv("OIDC_ISSUER")
が実行されます。理由
理由としてはNext.jsのビルド、特にApp Routerを有効化している際には
export
という処理が走るからです。export
は簡単に言えば静的なページを作成するための処理です。App Routerで常に実行されるのは、App Routerのデフォルトの挙動が静的なページを生成するためのものだからと想像しています。静的なページを作成するためには当然対象のページのコードを読み込む必要があります。その際に上記のコードであれば
fetchEnv
関数の実行まで始まってしまうため、環境変数がない場合はエラーになってしまいます。対応方法としてはいくつか考えられそうです。
fetchEnv
をビルド時に実行しないようにする例えば環境変数を定数ではなく関数にするとか。
config.tsconst fetchEnv = (key: string): string => { const value = process.env[key]; if (!value) { throw new Error(`環境変数 ${key} が設定されていません`); } return value; } export const getOidcIssuer = () => fetchEnv('OIDC_ISSUER');
Dynamic Importにするとか。
app/api/auth/login.tsexport async function GET() { const { OIDC_ISSUER } = await import('@/config'); const issuer = new Issuer({ issuer: OIDC_ISSUER, // ... }); } export const dynamic = "force-dynamic";
やりたいことに対して対応が大きすぎる気もする。
const lazy
が欲しいですね。ビルド時のみエラーにならないようにする
Next.jsでビルド中かどうかを判定するために環境変数が存在します。それが
NEXT_PHASE
です。NEXT_PHASEについて示唆されているDiscussionが一番詳細に記載されています。
NEXT_PHASEの値の定義として下記の5つの値が存在します。上記で
export
時に実行されると記載されていますが、NEXT_PHASE
はphase-export
ではなくphase-production-build
となっていることに注意です。export const PHASE_EXPORT = 'phase-export' export const PHASE_PRODUCTION_BUILD = 'phase-production-build' export const PHASE_PRODUCTION_SERVER = 'phase-production-server' export const PHASE_DEVELOPMENT_SERVER = 'phase-development-server' export const PHASE_TEST = 'phase-test'
また、それだとビルド中なのかExport中なのか判断がつかないため、
NEXT_IS_EXPORT_WORKER
という環境変数も存在します。trueという値が入ります。下記のように環境変数を利用してビルド時のみエラーにならないようにできます。
config.tsimport { PHASE_PRODUCTION_BUILD } from "next/dist/shared/lib/constants"; const fetchEnv = (key: string): string => { const value = process.env[key]; if (!value) { if (process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD) { return ""; } throw new Error(`Missing environment variable: ${key}`); } return value; }; export const OIDC_ISSUER = fetchEnv('OIDC_ISSUER');
- docker
DockerでCOPYを用いずにpackage.jsonを使ってyarn installする(RUN実行時に一時的にマウントする)
Dockerについて半日ぐらい調べたときの成果2。
RUN --mount
について。COPYコマンドは最終成果物に含まる。例えば、
COPY . .
で全てのファイルをコピーすると、最終成果物には全てのファイルが含まる。これは不要なファイルが含まれるため、最終成果物のサイズが大きくなる。マルチビルドステージにより解決できるが、それ以外にもRUN --mount
を使うことで解決可能だ。
基本的には公式ドキュメント通りだが、下記のようにすると、pakage.json
を成果物に含めずにyarn install
が可能だ。FROM node:22 AS build RUN yarn install
ただし
--mount
とCOPY
で大きく違うところが1つあり、既存のディレクトリをtargetとしたときの挙動である。
COPYはリファレンス内にて下記のように記載されている。If it contains subdirectories, these are also copied, and merged with any existing directories at the destination. Any conflicts are resolved in favor of the content being added, on a file-by-file basis,
サブディレクトリが含まれている場合、それらもコピーされ、コピー先の既存のディレクトリとマージされます。競合がある場合は、ファイルごとにコンテンツが追加されるように解決されます。
一方でRUN --mount、正確にはbind mountについてはリファレンスにて下記のように記載されている。
If you bind-mount a directory into a non-empty directory on the container, the directory’s existing contents are obscured by the bind mount.
コンテナ内の空でないディレクトリにディレクトリをバインドマウントすると、ディレクトリの既存の内容がバインドマウントによって隠されます。
つまり、COPYはディレクトリをマージするが、–mountはディレクトリを上書きするような挙動になる。
- docker
Dockerのマルチステージビルド
Dockerについて半日ぐらい調べたときの成果1。マルチステージビルドについて。
公式ドキュメント以上や、その他既にたくさんある記事以上の情報はないが、少なくとも令和のDockerfileでは必須な知識だと思う。
下記のように、FROMが2個以上出てくるのが特徴だ。例えばWebpackによりバンドルしたファイルをnodeで実行するときなんかにとても効力を発揮する。
FROM node:22 AS build WORKDIR /app COPY . . RUN npm install && npm run build FROM node:22 AS runtime WORKDIR /app COPY /app/dist /app/dist CMD ["node", "dist/index.js"]
ステージというのは簡単に言えばFROMで定義したイメージのことで、上記ではビルドと実行の2つのステージがある。Dockerの最終的な成果物は最後のステージのイメージになる。この場合は
runtime
ステージだ。
COPY --from
により異なるステージの中で生成されたファイルをコピーできる。上記では最終的に実行に必要なバンドル後のapp/dist
ディレクトリをruntime
ステージにコピーしている。これの最大の利点は最終的な成果物のサイズが小さくなることだ。1つのステージしかなければ(明示的に削除しない限り)
node_modules
やsrc
など不要なファイルがふくまる。とくにnode_modules
なんかはデカくなりがちであるため、これを削除することでイメージのサイズを小さくできる。 - docker
Dockerでモノレポのpackage.jsonをいい感じにコピーする(ディレクトリ構造を維持しつつコピーする)
Dockerについて半日ぐらい調べたときの成果3。
COPY --parents
について。モノレポ環境でDockerのビルドをしたい時に、
yarn install
をしたい場合、2つほど問題が発生する。- 他パッケージに依存しているときはモノレポのルートでCOPYをする必要がある
yarn install --immutable
をする場合は、すべてのモノレポのpackage.json
が必要
上記を解決するためには、モノレポのルートをビルドコンテキストとして指定する必要がある。そこで各パッケージの
package.json
をコピーしたいが、COPY ./packages/*/package.json .
のようなコピーの方法は使えない。というのもこれだと./packages/backend/package.json
を./package.json
にコピーした後./packages/frontend/package.json
を./package.json
にコピーする、という感じの動作になる。つまり、最後にコピーしたpackage.json
が残る。これを解決するのにはCOPY ./packages/backend/package.json ./packages/backend/package.json
のように、コピー元とコピー先を指定する必要がある。これだとパッケージが増える度に指定する必要がある。そこで**COPY --parentsを利用する。これはコピー元の親ディレクトリを保持したままコピーしてくれる**、上記の課題を解決してくれるすぐものだ。
COPY --parents ./packages/*/package.json ./
とやると、
./packages/backend/package.json
を./packages/backend/package.json
にコピーし、./packages/frontend/package.json
を./packages/frontend/package.json
にコピーする、という動作になる。ドキュメントに記載通りだが、上記はstableな機能ではないため、
# syntax=docker/dockerfile:1.7-labs
を先頭に扶養する必要がある。 - vscode
もうAuto Rename Tagは不要だった
Auto Rename TagはVSCodeの著名な拡張の1つです。
上記拡張のNoteにも記載があるのですが、実はこの機能がVSCodeのビルトインとして統合されています。
デフォルトで有効化されていませんが
editor.linkedEditing
という設定をtrueにすることで有効になります。 - sql
SQLのUPDATE RETURNINGで更新対象外のテーブルをJoinする
更新処理を実行した後、対象以外のテーブルをジョインしてRETURNINGして欲しくなった。
まぁ、これに関しては、実装的に1つのクエリでやりたいことが2つ出てしまうのであまり良くないと思ったので、結局は更新処理と取得処理を分離した。通信に関してあまり詳しくはないが、仮にトランザクション中であれば接続貼りっぱなしだと思うので通信のRTTも気にならんだろうし。
ただ、JOINしてWHEREしたいケースにも利用できる(というか、どちらかというとそちらがメイン)。FROM句を利用する。UPDATEにFROM句が使えるんだな。下記はPostgreSQLのリファレンスより引用し、RETURNINGを追加している。
UPDATE employees SET sales_count = sales_count + 1 FROM accounts WHERE accounts.name = 'Acme Corporation' AND employees.id = accounts.sales_person RETURNING employees *, accounts.*;
- sql
PostgreSQLのSKIP LOCKED
PostgreSQLを利用して、キューのようなものを作成したくなったところ、SKIP LOCKEDというものを知りました。
名前の通り、ロックされている行ををスキップしてくれるものです。例えば下記のようなテーブルを作成してみます。
CREATE TABLE tasks ( id SERIAL PRIMARY KEY, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); INSERT INTO tasks (id) VALUES (1), (2), (3), (4), (5);
現状こんな感じ。
$ select * from tasks; id | status | created_at ----+---------+------------------------------- 1 | pending | 2024-06-29 12:34:50.000000+00 2 | pending | 2024-06-29 12:34:51.000000+00 3 | pending | 2024-06-29 12:34:52.000000+00 4 | pending | 2024-06-29 12:34:53.000000+00 5 | pending | 2024-06-29 12:34:54.000000+00
ここで、下記のようなトランザクションを実行します。まだコミットしていないので対象の行はロックされたままです。
$ BEGIN; $ SELECT id FROM tasks ORDER BY created_at LIMIT 1 FOR UPDATE; id ---- 1
この間にトランザクション外から下記のコマンドを実行してみます。
$ SELECT id FROM tasks FOR UPDATE SKIP LOCKED; id ---- 2 3 4 5
この様に、ロックされたid 1の行をスキップしました。
PostgreSQLのリファレンスにはSELECT - ロック処理句に記載があります。
こちらの機能はSQL標準ではないようですが、MySQLにも存在しています。おそらく8.0から。 - npm
npm scriptsで引数をいい感じに分割して複数コマンドに分配したい
タイトルからだとかなり読みづらいが、下記のようなことをしたい。こういった欲求は度々起こるが、その度調べては難しそうだったので諦めていた。簡単に言えばnpm scriptsの内部で引数を分離したいのである。
lint・prettierを一発でやるコマンド{ "scripts": { "lint:fix": "eslint --fix", "prettier:write": "prettier --write", "format": "???" }, }
上記のスクリプトがある前提で、下記を期待する。
$ npm format __tests__/index.test.ts > npm lint:fix __tests__/index.test.ts && npm prettier:write __tests__/index.test.ts
例えばシンプルに下記のようにする。
lint・prettierを一発でやるコマンド{ "scripts": { "lint:fix": "eslint --fix", "prettier:write": "prettier --write", "format": "npm lint:fix && npm prettier:write" }, }
そうすると最後に引数がくっつくだけである。
$ npm format __tests__/index.test.ts > npm lint:fix && npm prettier:write __tests__/index.test.ts
npmだけではそういうことはできなさそう だが、ここでnpm-run-allの存在を思い出した。もう6年前から更新されてない。
結論としてはnpm-run-allで今回の欲求は満たせた。Argument placeholdersという機能を利用すれば良い。lint・prettierを一発でやるコマンド{ "scripts": { "lint:fix": "eslint --fix", "prettier:write": "prettier --write", "format": "run-s 'lint:fix {1}' 'npm prettier:write {1}' --" }, }
$ npm format __tests__/index.test.ts > npm lint:fix __tests__/index.test.ts && npm prettier:write __tests__/index.test.ts
最後の
--
はscripts側にないと想定通りに動作にはならない。 - vscode
ESLintがCLIだと動くのにVSCodeだと動かない
下記の条件を満たしていると、VSCodeのバージョンが1.90.0(2024-06-12時点での最新)以下で動作しない。
- typescript-eslintを利用している
- monorepoを利用している
- ESMである・
eslint.config.mjs
である languageOptions.parserOptions
のproject
およびtsconfigRootDir
を設定しているimport.meta.dirname
を利用している ← これが原因
原因としては、VSCode1.90.0の内部で利用されているElectron 29のNode.jsのバージョンが20.9.0であるため、
import.meta.dirname
が利用できない。
import.meta.url
は利用できるため、下記のような回避策で解決できる。eslint.config.jsonimport { fileURLToPath } from "url"; const filename = fileURLToPath(import.meta.url); const dirname = import.meta.dirname ?? path.dirname(filename); export default tseslint.config( ..., { languageOptions: { parserOptions: { project: ["./tsconfig.eslint.json", "./packages/*/tsconfig.json"], tsconfigRootDir: dirname, }, }, }, ..., )
Node.js 20.11.0で追加されている ので、Electron v30(Node.js 20.11.1)では
import.meta.dirname
が利用可能になるはず。 - vscode
VSCodeでyamlを開いた際、候補を表示しない
yamlを編集していると候補が邪魔をしてきて鬱陶しかったので設定を入れた。
下記を記載すると、入力するだけで候補の表示がされなくなる。Macであればctrl-space
(入力ソースの変換と被っているが)で候補は表示される。settings.json"[yaml]": { "editor.quickSuggestions": { "comments": "off", "strings": "off", "other": "off" } }
P.S.
1回上記の設定を入れた後に無効化しても全然候補が表示されなくなった。今回参考にしたのは下記の記事。
- gcloud, solve
brew upgrade後gcloudコマンドが動かなくなった
brew upgrade後、
gcloud
コマンドを叩くと下記のようなエラーが発生するようになった。ERROR: gcloud failed to load. This usually indicates corruption in your gcloud installation or problems with your Python interpreter. Please verify that the following is the path to a working Python 3.8-3.12 executable: /Users/sa2taka/.pyenv/versions/3.11.1/bin/python If it is not, please set the CLOUDSDK_PYTHON environment variable to point to a working Python executable. If you are still experiencing problems, please reinstall the Google Cloud CLI using the instructions here: https://cloud.google.com/sdk/docs/install Traceback (most recent call last): File "/Users/sa2taka/libs/google-cloud-sdk/lib/gcloud.py", line 106, in gcloud_exception_handler yield File "/Users/sa2taka/libs/google-cloud-sdk/lib/gcloud.py", line 183, in main gcloud_main = _import_gcloud_main() ^^^^^^^^^^^^^^^^^^^^^ ...(中略)... ImportError: dlopen(/Users/sa2taka/.pyenv/versions/3.11.1/lib/python3.11/lib-dynload/_ssl.cpython-311-darwin.so, 0x0002): Library not loaded: /usr/local/opt/openssl@1.1/lib/libssl.1.1.dylib Referenced from: <...> /Users/sa2taka/.pyenv/versions/3.11.1/lib/python3.11/lib-dynload/_ssl.cpython-311-darwin.so Reason: tried: '/usr/local/opt/openssl@1.1/lib/libssl.1.1.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/usr/local/opt/openssl@1.1/lib/libssl.1.1.dylib' (no such file), '/usr/local/opt/openssl@1.1/lib/libssl.1.1.dylib' (no such file), '/usr/local/lib/libssl.1.1.dylib' (no such file), '/usr/lib/libssl.1.1.dylib' (no such file, not in dyld cache)
エラーの上部に解決策が色々出てるが、原因は下部のほうにある
openssl@1.1
がないことだ。どうやらbrew upgrade
を叩いた際に消えたようだ。
とりあえずopenssl@1.1
をダウンロードすることで解決できる。$ brew install openssl@1.1
- notion
Notionの引用のショートカット
一般的なマークダウンでは
>
が引用であるが、Notionではトグルに割り当てられている。/quote
とすることで引用にできるが、それ以外に"
または|
がショートカットとして割り当てられている。確かに|
は一般的な引用のスタイルだし、わかりやすいかもしれない。 - typescript,eslint
TypeScript ESLintがモノレポ環境でOOMになるのを解決
TypeScript ESLintはTypeScriptの型情報を利用して、よくあるミスを洗い出してくれるいい子ですが、そのかわり1回TypeScriptのパーサーを挟むので重い・遅い・メモリを食うという状況になります。
重すぎるので今までCI以外では切っていたんですが、Flat Config化するにあたり、せっかくなら普段のVSCode上でエラーも出したいしということでTypeScript ESLintを全体有効化したところ、設定が誤っていて今までモノレポ環境のLintでTypeScript関連のルールが動いていなかったことが判明。エラーが2つぐらいでるようになったのは良いのですが、何やったってCIがメモリエラーで死ぬ。何なら32GBある私のPCもメモリエラーが吐かれる。
下記のようなエラーですね。FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
調べたところ、ちょうど今日解決策がissueに投稿されていました。
EXPERIMENTAL_useProjectService
というプロパティを設定すればよいようです。flat configであれば下記のように
parserOptions
を設定しましょう。eslint.config.jsexport default [ { languageOptions: { parserOptions: { ..., EXPERIMENTAL_useProjectService: true, }, }, } ]
EXPERIMENTAL_useProjectServiceは名前の通りまだexpreimentalな機能です。
TypeScript ESLintが重い理由はts.CreateProgram
を利用しているからです(多分)。
で、上記を有効にすると、こんな感じでASTを取得します。useProgramFromProjectService.tsconst program = service .getDefaultProjectForFile(scriptInfo!.fileName, true)! .getLanguageService(/*ensureSynchronized*/ true) .getProgram();
これがなぜ早くなるのか、正直全くわかりませんが実際にOOMが解決されるのでなにか意味があるのでしょう。
Copilot3.5では無理でしたが、Claudeで聞くといい感じに答えてくれます。全然情報が無いですね…。私のJS Test Outlineもts.Program利用しているので、ProjectServiceの利用を検討してみようかしら。
- git
Gitで最後に触っていたブランチに戻る
最近よく「一瞬だけこのブランチに移動して、その後もとに戻る」という操作をする。しかし、びっくりするぐらい短期力に難がある私にとって3分前に切り替えたブランチ名は忘れるのである。
調べると最後に触っているブランチへ戻る方法があった。$ git switch - # 上は下記の糖衣構文。-1の部分を2とか3とかにすればもっと遡れる。HEADみたいなもんすね $ git switch -@{-1}
またreflogにもログが残っている。
$ git reflog | grep switch 12345abce HEAD@{0}: checkout: moving from master to release
参考:
https://qiita.com/ginpei/items/2e0cd22df0670b3a1c3f
https://ginpen.com/2022/12/09/git-back-to-last-branch/
https://zenn.dev/yajamon/articles/422ecab49804f9 - raw
指標はハックされる〜グッドハートの法則
私は、何らかの人間に対する計測時にハックされる可能性を必ず考慮する。
例えば給料の査定(私自身にそんな権限はないが)だったりもそうだし、生産性を上げるときの計測対象なんかは最近専ら議論している。こういった話をした時にチームメンバーから教えていただいたのが、グッドハートの法則。
グッドハートの法則
グッドハートの法則は「指標が目標となると、それは良い指標ではなくなる」と説明されている。これだと、巷で噂のKPIは良い指標ではないのかと突っ込みたくはなるが、言いたいことはそうではないと思う。裏まで読むと「指標にすべきではない指標が目標となると、それは良い指標ではなくなる」、言ってしまえば目標とすべき指標はちゃんと選べよということかもしれない。
例えば、生産性の目標としてPR数を指標とした。PR数が上がれば生産性が上がったよねとする。そうなると、俺は通常より細かい単位でPRを作る。そうすれば生産性が上がるから。本当の目標は「生産性を上げる」なのに「PR数を作る」という、俗に言うと手段と目的が入れ替わっている状態となる。
これじゃ駄目だよねという話。
複数の指標を測る・加えて一部の指標を定性的な目標にする、等少なくともハックできないようなものにする。または、目的 = 指標(目的 ∈ 指標)となるような指標を選択することが回避に大切だと思う。
例えば生産性を測るためにFour Keysを利用する。この場合目的 = 指標とは言いづらいが、統計的に有為であるため問題はないといえる。
具体例としては コブラ効果 などもその例だろう。寓話っぽくて好き。
また、キャンベルの法則という似たような法則もある。
- unicode
Unicode正規化
言語処理の文脈で、度々Unicode正規化という言葉を聞く。言葉や処理自体は知っていたが、いくつか種類があるようなので改めて調べてみた。
Unicode正規化(ユニコードせいきか、英語: Unicode normalization)とは、等価な文字や文字の並びを統一的な内部表現に変換することでテキストの比較を容易にする、テキスト正規化処理の一種である。
具体的に、正規化には4種類ある。
- NFD
- NFC
- NFKD
- NFKC
末尾のD・CはそれぞれDecomposition(分割)・Composition(結合)である。Dの場合は正規化した上で分解したまま、Cの場合は分解した後結合する。
Kの有無だが、これは正準等価(K無し)・互換等価(K有り)の違いらしい。具体的には調べていないが、正準等価に比べ互換等価の方が緩く分解されるらしい。JavaScriptにおいては
String
にnormalize
メソッドが生えているため、それを利用することでノーマライズが可能である。$ node > const char = "ギ" undefined > char.normalize("NFD") 'ギ' > char.normalize("NFC") 'ギ' > char.normalize("NFKD") 'ギ' > char.normalize("NFKD").length 2 > char.normalize("NFKC") 'ギ' > char.normalize("NFKC").length 1
- エンジニアリング
SPACEという生産性のためのフレームワーク
SPACEという生産性のためのフレームワークを知った。
生産性のフレームワークは有名所だとFour Keysがあるが、SPACEは下記の5つの指標で構成されている。
- Satisfaction and well being
- 開発者が自分の仕事、チーム、ツール、文化にどれだけ満足しているか
- ストレスチェックや、開発者が必要なツール・リソースを持っているか、
- Performance
- 開発者が書いたコードは、想定されたことを確実に実行したか?
- 品質や信頼性、定量的に見るならFour Keysにおける変更障害率あたり
- Activity
- 業務を遂行する過程で完了した行動やアウトプットの数
- 論文内では設計ドキュメント・仕様、PR、コミット、レビュー数となっている。Four Keysのデプロイ数が当てはまりそう
- Communication and collaboration
- メンバーやチームがどのようにコミュニケーションをとり、協力し合うか
- 論文内では、ドキュメントのみつけやすさや、レビューの質、コミュニケーションのネットワークメトリクスやオンボーディング時間が書かれている
- Efficiency and flow
- チームやシステム全体で中断や遅延を最小限に抑え、作業を完成、進行できているか
- Four Keysにおけるサービス復元時間・変更のリードタイムあたりが当てはまりそう
Four Keysと比べると、人の満足度、コミュニケーション的な部分が加わっている印象。
システムの健全さを表す指標がFour Keysなら、チームの健全さを表す指標がSPACEなのかな、と感じた。ちなみにForu KeysやこのSAPCEをまとめて DPE(Developer Productivity Engineering) と呼ぶこともあるらしい。
- Satisfaction and well being
- javascript
JavaScriptにおける剰余の正負
JavaScriptにおける剰余の正負の結果を毎回忘れる。 常に被除数と同一 と覚えればいいと学んだ。
被除数はすなわち「割られる数」でありa % b
においてa
である。$ node Welcome to Node.js v20.5.0. Type ".help" for more information. > 17 % 7 3 > 17 % - 7 3 > -17 % 7 -3 > -17 % -7 -3
Wikipediaに言語ごとにまとまっていて非常に良かった。
- typescript
VSCodeにおいてTypeScriptの自動インポート対象から省きたい場合
autoImportFileExcludePatterns
というフラグがある。これを設定することで、自動インポートの対象から省かれDX(Developer eXperience)が向上する。例えば、
User
というよくあるクラス名は既存のライブラリからもExportされていることが多い(Sentry
やDataDog
など)。
これらを指定すればそれらのライブラリからの自動インポートを省いてくれる。弱点としてはVSCodeにしか対応していない(正確に言えばTS Serverのオプションなので設定さえすればVimとかでも問題ないとは思う)のと、ライブラリごと省かれてしまうこと。
どうしても回避したいなら独自のTS Language Server Pluginを書けば良いと思うが…。ちなみにTypeScript4.8以降の機能。
https://devblogs.microsoft.com/typescript/announcing-typescript-4-8-rc/#exclude-specific-files-from-auto-imports - データ構造/アルゴリズム
Trieというデータ構造
文字列検索の文脈でTrie(トライ)というデータ構造を知った。
retrieval
から取られたようだ。
検索文字列の辞書がある場合、その辞書の長さに関わらず一定のオーダーで検索できるのが強みらしい。 - Next.js
Next.jsでMDXを使う
画像周りが正直面倒くさ過ぎたため、MDXでなんとかできれば嬉しい。
内部的にはremark
を使っているらしい。ちなみに昔僕が使おうとしていたのはmarked
で、現在使っているのがmarkdown-it
。https://nextjs.org/docs/pages/building-your-application/configuring/mdx
- ライフハック
VSCode上でGithubのPermalinkを取得する
選択範囲に対して、行番号を右クリックすることで、Permalinkが取得できる。
- ライフハック
TIL
TILはToday I Learnedの略で、今日学んだことを振り返る意味がある。
GithubにTILリポジトリを作り、Markdownか何かを作るフレームワーク的なものが広がったらしい。Slackのtimesチャンネルによく「こんなのあるんだ」的なのを呟いていたが、振り返りづらすぎるため、僕のBlogサービスにTIL的な機能を載っけてみる。
ほぼブログみたいなもんだが、もうちょい腰を据えずに自分向けの記事を書いていくつもり。