Metabase における週初めは日曜
Metabase を試していたところ、以下の点が気になりました。
- 週単位で集計すると週初めが日曜になる(日曜から土曜までの集計)
(画面例)
DB 等、一般的なシステムにおける週初めは月曜になる(ISO 8601)はずなので、Metabase が日曜へ変えているのは確実。
そこで、「SQLを見る」をクリックして SQL の内容を確認してみると、やはり日曜へ変える(週初めの月曜 - 1日)ようになっていました。(接続先 DB は PostgreSQL)
クエリビルダーで生成された SQL
SELECT (date_trunc('week', CAST((CAST("public"."stock_move"."date" AS timestamp) + INTERVAL '1 day') AS timestamp)) - INTERVAL '1 day') AS "date", sum("public"."stock_move"."product_qty") AS "sum" FROM "public"."stock_move" GROUP BY (date_trunc('week', CAST((CAST("public"."stock_move"."date" AS timestamp) + INTERVAL '1 day') AS timestamp)) - INTERVAL '1 day') ORDER BY (date_trunc('week', CAST((CAST("public"."stock_move"."date" AS timestamp) + INTERVAL '1 day') AS timestamp)) - INTERVAL '1 day') ASC
特に設定も見当たらないので(タイムゾーンや言語を設定しても無駄だった)、該当箇所の ソース を見てみると、日曜へ変える事しか考慮していない事が判明。
src/matabase/driver/postgres.clj
・・・ (defmethod sql.qp/date [:postgres :week] [_ _ expr] (hx/- (date-trunc :week (hx/+ (hx/->timestamp expr) one-day)) one-day)) ・・・
(現時点では)週初めを月曜へ変えるには Metabase のソースを書き換える事になりそうですが、日曜を前提に作られている点が懸念されます。
また、Allow organizations to determine the start of their week #1779 などを見る限り、Metabase 側の対応に期待するのも厳しそうです。
PostgreSQL 検索時の週初めを月曜へ変更
試しに、Java の Instrumentation 機能を利用し、PostgreSQL 検索時に週初めが月曜となるようにしてみます。
Clojure で実装された該当処理(上記 postgres.clj の処理内容)が Java 上でどのように処理されるのか調べたところ、以下のようになっていました。
metabase.driver.postgres__init
クラスの static initializer 実行時にmetabase.driver.sql.query-processor
名前空間に属するdate
変数の rawRoot に[:postgres :week]
をキーにして addMethod している
つまり、この処理が終わった後で [:postgres :week]
の処理を差し替えれば何とかなりそうです。
実装
ソースは http://github.com/fits/try_samples/tree/master/blog/20190525/
postgres__init クラスの初期化後に処理を実施したいので、ClassFileTransformer
を使って任意のクラスのロード時に処理を差し込めるようにしました。(クラスのロード時に transform メソッドが呼ばれる)
ここでは org/postgresql/Driver
クラスのロード時に処理するようにしましたが、postgres__init の初期化が済んでいればどのタイミングでも問題ないと思います。
差し替え後の処理 PgWeekFunc
は Clojure のコードで (date-trunc :week expr)
を実施するように実装しています。
sample/SampleAgent.java
package sample; import java.lang.instrument.*; import java.security.*; import clojure.lang.*; public class SampleAgent { public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new PgWeekTransformer()); } static class PgWeekTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // org/postgresql/Driver クラスのロード時に実施 if (className.equals("org/postgresql/Driver")) { replacePgWeek(); } return null; } // [:postgres :week] の処理を置き換えるための処理 private void replacePgWeek() { // metabase.driver.sql.query-processor 名前空間の取得 Namespace n = Namespace.find( Symbol.intern("metabase.driver.sql.query-processor") ); // date 変数の取得 Var v = n.findInternedVar(Symbol.intern("date")); MultiFn root = (MultiFn)v.getRawRoot(); // キー [:postgres :week] の作成 IPersistentVector k = Tuple.create( RT.keyword(null, "postgres"), RT.keyword(null, "week") ); // [:postgres :week] へ紐づいた処理を差し替え root.removeMethod(k); root.addMethod(k, new PgWeekFunc()); } } // 週の処理関数を定義 static class PgWeekFunc extends AFunction { public static final Var const__1 = RT.var( "metabase.driver.postgres", "date-trunc" ); public static final Keyword const__2 = RT.keyword(null, "week"); public static Object invokeStatic(Object obj1, Object obj2, Object expr) { // (date-trunc :week expr) の実施 return ((IFn)const__1.getRawRoot()).invoke(const__2, expr); } public Object invoke(Object obj1, Object obj2, Object obj3) { return invokeStatic(obj1, obj2, obj3); } } }
META-INF/MANIFEST.MF
Manifest-Version: 1.0 Premain-Class: sample.SampleAgent
上記ソースをビルドして JAR ファイル化(例. sample-agent.jar
)しておきます。
実行
Metabase の実行時に(上で作成した)sample-agent.jar を -javaagent
オプションで適用します。
Metabase 実行(Instrumentation 適用)
> java -javaagent:sample-agent.jar -jar metabase.jar
SQL を確認してみると、INTERVAL '1 day'
の減算等が無くなり、処理の差し替えが効いている事を確認できました。
クエリビルダーで生成された SQL(差し替え後)
SELECT date_trunc('week', CAST("public"."stock_move"."date" AS timestamp)) AS "date", sum("public"."stock_move"."product_qty") AS "sum" FROM "public"."stock_move" GROUP BY date_trunc('week', CAST("public"."stock_move"."date" AS timestamp)) ORDER BY date_trunc('week', CAST("public"."stock_move"."date" AS timestamp)) ASC
ついでに、サーバーからのレスポンス内容を確認してみると、日付が月曜になり集計結果が変わっている事を確認できました。
レスポンス内容(一部)
"data":{ "rows":[ ["2019-04-22T00:00:00.000+09:00",2139.0], ["2019-05-06T00:00:00.000+09:00",30.0], ["2019-05-13T00:00:00.000+09:00",13.0] ], "columns":["date","sum"], ・・・ }
ただし、Web 画面上は JavaScript が週の範囲を生成している事から、(date: Week の)表示上は日曜から土曜となってしまいます。
(画面例)
このように、週初めを月曜へ変えるには以下のような JavaScript の処理に関しても考慮が必要になりそうです。
frontend/src/metabase/lib/formatting.js
・・・ function formatWeek(m: Moment, options: FormattingOptions = {}) { // force 'en' locale for now since our weeks currently always start on Sundays m = m.locale("en"); return formatMajorMinor(m.format("wo"), m.format("gggg"), options); } ・・・