メインコンテンツに移動

Analytics プラグイン

Analytics プラグイン

Databricks SQL Warehouse に対する SQL クエリの実行を可能にします。

主な機能:

  • 型定義の自動生成に対応したファイルベースの SQL クエリ
  • 型安全な SQL ヘルパーによるパラメータ化クエリ
  • JSON 形式と Arrow 形式のサポート
  • キャッシュとリトライロジックを標準搭載
  • Server-Sent Events (SSE) ストリーミング

基本的な使い方

import { analytics, createApp, server } from "@databricks/appkit";

await createApp({
  plugins: [server(), analytics({})],
});

クエリファイル

  • .sql ファイルは config/queries/ に配置します
  • クエリキーは .sql を除いたファイル名です (例: spend_summary.sql"spend_summary")

実行コンテキスト

  • queryKey.sqlサービスプリンシパル として実行されます (共有キャッシュ)
  • queryKey.obo.sqlユーザー として実行されます (OBO = on-behalf-of、ユーザーごとのキャッシュ)

実行コンテキストは、フックの呼び出し方ではなく SQL ファイル名によって決まります。

SQL パラメータ

:paramName プレースホルダーを使用します。必要に応じて、SQL コメントでパラメータの型を指定できます:

-- @param startDate DATE
-- @param endDate DATE
-- @param limit INT
SELECT ...
WHERE usage_date BETWEEN :startDate AND :endDate
LIMIT :limit

LIMIT / OFFSET には Spark の IntegerType が必要です。BIGINT (LongType) は INVALID_LIMIT_LIKE_EXPRESSION.DATA_TYPE で拒否されます。 INT で型注釈を付けるか、呼び出し箇所で sql.number() ([-2^31, 2^31-1] の範囲の値は INT を自動推論し、それを超える値は BIGINT にフォールバック) または sql.int() を使用してください。

サポートされる -- @param の型 (大文字小文字は区別しません) :

  • STRINGBOOLEANDATETIMESTAMPBINARY
  • INTBIGINTTINYINTSMALLINTsql.int() / sql.bigint() でバインド
  • FLOATDOUBLEsql.float() / sql.double() でバインド
  • NUMERICDECIMALsql.numeric() でバインド (精度を保つには文字列を渡してください)

型生成用のサンプル値

クエリの中には、パラメータに具体的な値が入って初めて正しい形になるものがあります。最も よくあるのは IDENTIFIER() で組み立てる動的なテーブル名です。型生成時、AppKit は プレースホルダのデフォルト値で DESCRIBE QUERY を実行するため、解決されなかったパラメータは 空文字列になり、不正な SQL が生成されます (IDENTIFIER('' || '.schema.table')PARSE_SYNTAX_ERROR) 。

-- @param アノテーションに = value を付けると、型生成にサンプル値を渡せます。 この値はクエリを DESCRIBE する間にのみ使われます。実行時には実際の パラメータがバインドされるため、クエリは環境をまたいで移植可能なままです。

-- @param target_catalog STRING = main
SELECT *
FROM IDENTIFIER(:target_catalog || '.sales.nation')

型生成では main.sales.nation を DESCRIBE して結果の列を推論しますが、デプロイ後のアプリは呼び出し元が渡したカタログをそのままバインドします。文字列、DATETIMESTAMP の値は自動的にクォートされ (= main'main') 、すでにクォート済みのリテラルはそのまま保持されます (= '2024-01-01') 。数値、BOOLEANBINARY の値は厳密なリテラル形式で検証されます (= 100= true= X'00') 。これに一致しない値 (DESCRIBE ステートメントに SQL を注入し得る値) は無視され、そのパラメータは型ベースのプレースホルダーにフォールバックします。そのため、サンプル値が DESCRIBE QUERY の外へ抜け出すことはありません。

サーバーが注入するパラメータ

:workspaceIdサーバーによって注入されるため、アノテーションを付けてはいけません

WHERE workspace_id = :workspaceId

HTTP endpoint

Analytics プラグインは、次の endpoint を公開します (/api/analytics 配下にマウントされます) :

  • POST /api/analytics/query/:query_key
  • GET /api/analytics/arrow-result/:jobId
  • POST /api/analytics/metric/:key — Unity Catalog の Metric View を測定します (Metric views を参照)

フォーマットオプション

  • format: "JSON" (デフォルト) : JSON 形式の行を返します
  • format: "ARROW": SSE 経由で Arrow の "statement_id" ペイロードを返し、その後クライアントが /api/analytics/arrow-result/:jobId からバイナリ形式の Arrow を取得します

Metric views

POST /api/analytics/metric/:key は、config/metric-views/definitions.json で宣言した Unity Catalog Metric View を測定します。SQL を記述する必要はなく、呼び出し側は構造化されたリクエスト (集計するメジャー、グループ化するディメンション、任意のフィルター) を送信するだけで、plugin が対象の view に対する SELECT MEASURE(...) ... GROUP BY ALL を組み立てて実行します。

このルートは config/metric-views/definitions.json が存在しない限り休止状態 です。設定ファイルがなければ、どのメトリクスキーでも 404 が返ります。ファイルの宣言 (および型の生成) については Metric-view types を参照してください。ここでは、この設定によって有効になる runtime の endpoint について説明します。

リクエストボディ

POST /api/analytics/metric/:key Content-Type: application/json { "measures": ["arr", "revenue"], "dimensions": ["region", "order_date"], "timeGrain": "month", "timeDimension": "order_date", "filter": { "member": "region", "operator": "in", "values": ["EMEA", "APAC"] }, "orderBy": [{ "field": "revenue", "direction": "DESC" }], "limit": 100 }

:keydefinitions.json のメトリクスキーです。ボディのフィールドは次のとおりです。

フィールド必須説明
measuresstring[]はい集計するメジャー。1 個以上 50 個以下。それぞれ MEASURE(<name>) AS <name> に展開されます。
dimensionsstring[]いいえグループ化に使うディメンション (最大 20 個) 。そのまま選択され、GROUP BY ALL でグループ化されます。
filterobjectいいえパラメータ化された WHERE 句に変換される構造化された述語ツリー (フィルターを参照) 。
timeGrainstringいいえdate_trunc('<grain>', …) で時間ディメンションをバケット化します (例: daymonth) 。timeDimension の指定が必要です。
timeDimensionstringいいえtimeGrain がバケット化する単一のディメンション。dimensions のいずれかである必要があります。timeGrain を設定する場合は必須です。
orderByarrayいいえ{field, direction} 形式のソートキーの配列 (最大 20 個) 。field は選択済みのメジャーまたはディメンションである必要があります。direction"ASC" (既定値、SQL では省略) または "DESC" です。メジャーは SELECT のエイリアスで並べ替えます。
limitnumberいいえ行数の上限を表す正の整数 (最大 100000) 。
formatstringいいえJSON_ARRAY (既定値) 。JSON は非推奨のエイリアスとして受け付けられます。Arrow 形式 (ARROWARROW_STREAM) はこのルートでは拒否されます。

メジャーとディメンションは両方のリストを通じて一意である必要があります。同じ名前を重複させることも、メジャーとディメンションの両方に指定することもできません。

リクエストがSQLに変換される仕組み

catalog.schema.revenue_metrics として登録されたビューの場合、上記のリクエストは次のSQLを生成します (メジャーとディメンションは、SELECTリストを決定的にするためにソートされます) :

SELECT MEASURE(`arr`) AS `arr`, MEASURE(`revenue`) AS `revenue`,
       date_trunc('month', `order_date`) AS `order_date`, `region`
FROM `catalog`.`schema`.`revenue_metrics`
WHERE `region` IN (:f_0, :f_1)
GROUP BY ALL
ORDER BY `revenue` DESC, `order_date`, `region`
LIMIT 100

メトリックビューのFQNと、すべてのメジャー/ディメンション識別子はバッククォートで囲まれます。フィルター値はパラメータ (:f_0:f_1、…) としてバインドされ、SQL文字列に直接埋め込まれることはありません。

limit による決定的な結果

limit を設定すると、ルートはグループ化されたすべてのディメンションをタイブレーカーとして ORDER BY 句に自動的に追加します (すでに orderBy で指定されているものは除く) 。GROUP BY ALL では、ディメンションのタプル全体が行ごとに一意になるため、すべてのディメンションで並べ替えると 全順序 が得られます。つまり、run のたびに同じ行が返り、任意に抽出されたサンプルにはなりません。

これが重要なのは、ORDER BY のない LIMIT は「上位 n 件」ではなく行の サンプル にすぎないためです。Spark は先に生成された行をそのまま返すため、結果はパーティショニングや並列度、キャッシュの状態によって変わります。こうしたリクエストを基にしたカードは、エラーが出ないまま run ごとに異なる数値を表示することがあります。タイブレーカーはこのギャップを埋めるもので、データが変わらない限り、同じリクエストが同じ行を返すようになります。

メジャーによる上位 N 件 が必要な場合は、そのメジャーを明示的に並べ替えの対象に指定し、limit を指定してください:

{ "orderBy": [{ "field": "revenue", "direction": "DESC" }], "limit": 100 }

このルートは、明示的に指定したエントリの後に残りのディメンション (上記の例では order_dateregion) を追加するため、実行のたびに結果が変わることはありません。

重要: メジャーの並べ替えには SELECT のエイリアス を使用してください。Spark は ORDER BY MEASURE(\revenue`)METRIC_VIEW_INVALID_MEASURE_FUNCTION_INPUTで拒否します。生成される SQL はすべてのメジャーにエイリアスを付与する(例:MEASURE(`revenue`) AS `revenue`)ため、常にエイリアスを参照してください。この場合は単に "revenue"` です。

フィルター

filter は再帰的なツリー構造です。リーフは単一の述語になります:

{ "member": "region", "operator": "equals", "values": ["EMEA"] }

述語は and / or グループで組み合わせることができ、入れ子にすることも可能です:

{
  "and": [
    { "member": "region", "operator": "in", "values": ["EMEA", "APAC"] },
    {
      "or": [
        { "member": "segment", "operator": "equals", "values": ["Enterprise"] },
        { "member": "deal_size", "operator": "gt", "values": [50000] }
      ]
    }
  ]
}

演算子の一覧:

演算子SQL
equals=1つのみ
notEquals<>1つのみ
inIN (…)1つ以上
notInNOT IN (…)1つ以上
gt / gte / lt / lte> / >= / < / <=1つのみ
containsLIKE :param文字列1つのみ
notContainsNOT LIKE :param文字列1つのみ
setIS NOT NULLなし
notSetIS NULLなし

contains / notContains では、%…% のワイルドカードは バインドされたパラメータ値 (%value%) 側に適用され、SQL テキストには書き込まれません。つまり、他のすべての演算子と同様に、値が SQL に埋め込まれることはありません。

空のグループ ({ "or": [] }{ "and": [] }) はどちらの種類でも 400 で拒否されます。and / or グループには必ず1つ以上の述語を含めてください。フィルタを指定しない場合は、空のグループを渡すのではなく filter フィールド自体を省略します。

悪意のある入力によるサーバーリソースの枯渇を防ぐため、フィルタには上限が設けられています: ネストの深さは 8 以下、and / or グループあたりの子要素は 100 以下、述語あたりの値は 1000 以下です。上限を超えるリクエストは 400 で拒否されます。

エグゼキューター (キャッシュスコープ)

definitions.json の各エントリでは、クエリを実行するエグゼキューターを指定します。これによってキャッシュスコープも決まります。エグゼキューターは設定で固定され、リクエストごとに変更することはできません:

executor実行主体キャッシュ
app_service_principal (デフォルト)アプリのサービスプリンシパル全ユーザーで共有
userリクエスト元のユーザー (on-behalf-of)ユーザーごと

これはファイルベースのクエリにおける <key>.sql<key>.obo.sql の使い分けと同じ考え方です。

レスポンス

レスポンスは POST /api/analytics/query/:query_key と同じ SSE ストリームです。SQL warehouse がコールド状態の場合は、まず warehouse_status イベントが送出され (Warehouse readiness を参照) 、続いて行をオブジェクトとして含む単一の result イベントが送出されます。

{
  "type": "result",
  "data": [
    {
      "region": "EMEA",
      "order_date": "2025-01-01",
      "arr": 1200000,
      "revenue": 340000
    }
  ]
}

失敗した場合は、代わりに error イベントを発行します。

エラーと動作

ステータスボディ発生条件
404{ "error": "Metric not found" }:keydefinitions.json に宣言されていない場合 (ファイルが存在しない場合は、すべてのキーに対してこのレスポンスが返ります) 。
400{ "error": "Invalid metric request body (fields: …)", "code": … }リクエストボディが検証に失敗した場合。メッセージには問題のあるフィールドパスのみが示され、送信された値は一切含まれません。
503{ "error": "Metric registry not available", "code": "METRIC_REGISTRY_LOAD_FAILED" }definitions.json は存在するが、形式が不正または読み取り不能な場合。

definitions.json の編集内容は次のリクエストで反映されるため、サーバーの再起動は不要です。形式が不正だったファイルを修正した場合も、同様に次のリクエストから正しく動作します。

フロントエンドでの使用方法

useAnalyticsQuery

SSE 経由でアナリティクスクエリを購読し、最新の結果を返す React フックです。

import { useAnalyticsQuery } from "@databricks/appkit-ui/react";

const { data, loading, error } = useAnalyticsQuery(
  queryKey,
  parameters,
  options,
);

戻り値の型:

{
  data: T | null; // クエリ結果(JSON の場合は型付き配列、ARROW の場合は TypedArrowTable)
  loading: boolean; // クエリ実行中は true
  error: string | null; // エラーメッセージ。成功時は null
  warehouseStatus: WarehouseStatus | null; // 後述の「Warehouse readiness」を参照
}

オプション:

オプションデフォルト説明
format"JSON" | "ARROW""JSON"レスポンス形式
maxParametersSizenumber102400シリアライズ後のパラメータの最大サイズ (バイト)
autoStartbooleantrueマウント時にクエリを開始

Warehouse の準備状態

クエリが要求された時点で、設定された SQL warehouse が STOPPED または STARTING の場合、Analytics プラグインは次の処理を行います。

  1. warehouse を自動起動する (STOPPED の場合) 。
  2. warehouse の状態をポーリングし、RUNNING になるまで warehouse_status イベントを SSE でストリーミングする。
  3. SQL ステートメントを実行する。

これにより、コールドスタート時に UI がスピナーのまま固まることはなくなります。useAnalyticsQueryuseMetricView はどちらも、現在のリクエストにおける最新のステータスを warehouseStatus で公開します。これを描画して、ユーザーに状況を伝えましょう。

import { useAnalyticsQuery } from "@databricks/appkit-ui/react";

function SpendTable() {
  const { data, loading, error, warehouseStatus } = useAnalyticsQuery(
    "spend_summary",
    params,
  );

  if (warehouseStatus && warehouseStatus.state !== "RUNNING") {
    return <div>Warehouse is {warehouseStatus.state.toLowerCase()}…</div>;
  }
  if (loading) return <div>Loading…</div>;
  if (error) return <div>{error}</div>;
  return <table>{/* データをレンダリング */}</table>;
}

どちらのフックでも、リクエストが開始されると warehouseStatusnull にリセットされ、最初のステータスイベントが届くまでその状態が続きます。サーバーが warehouse の RUNNING を一度確認した後は、約 30 秒以内の後続リクエストでは準備状態チェックが完全にスキップされ、warehouseStatusnull のままとなるため、定常状態のホットパスで余分なラウンドトリップが発生することはありません。

warehouse が DELETED/DELETING の場合、または設定されたタイムアウト内に RUNNING に到達しなかった場合、ルートは error イベントを発行します (error フィールド経由で通知されます) 。

グローバル準備状況インジケーター

チャートが多数あるダッシュボードでは、コンポーネントごとのスピナーだけでは不十分です。とはいえ、すべてのスケルトンに同じ「warehouse のウォームアップ中」UI を組み込むのは手間がかかります。AppKit には、あらゆるプラグインからステータスを発行できる小さな汎用コンテキスト (ResourceStatusProvider) と、そのまま組み込めるインジケーター (ResourceStatusIndicator) が同梱されており、Analytics の warehouse は自動的に接続されます。

インジケーターは、保留中のステータスのうち最も深刻なものを sonner のトーストとして表示するため、sonner のアニメーション、テーマ、スタック表示をそのまま継承します。このコンポーネントは独自の <Toaster /> (デフォルトでは右上) をマウントし、その props (positionthemerichColors など) を引き渡します。

import {
  ResourceStatusIndicator,
  ResourceStatusProvider,
} from "@databricks/appkit-ui/react";

export function AppShell({ children }) {
  return (
    <ResourceStatusProvider>
      <ResourceStatusIndicator />
      {children}
    </ResourceStatusProvider>
  );
}

useAnalyticsQueryuseMetricView は最も近いプロバイダーに自身を登録するため、チャートごとの接続作業は不要です。すべてのリソースが正常な間、インジケーターは <Toaster /> のマウントポイントだけをレンダリングします。問題が発生すると、最も深刻な種別をキーとして固定表示のトーストを 1 つだけ表示し (コールドスタート時は toast.loading、回復不能な状態では toast.error) 、すべてが解消した時点で閉じます。同じプロバイダーがリソース種別 (warehouse、lakebase、Model Serving など) をまたいで共有されるため、インジケーター 1 つですべての plugin をカバーできます。

アプリ内の別用途のトースト向けにすでに独自の <Toaster /> をレンダリングしている場合は、インジケーターは使わず、代わりに useResourceStatusToaster() を呼び出して、リソース状態のトーストがその単一の Toaster を共有するようにしてください:

import { useResourceStatusToaster, Toaster } from "@databricks/appkit-ui/react";

function App() {
  useResourceStatusToaster();
  return (
    <>
      <Toaster position="top-right" />
      <Routes />
    </>
  );
}

トーストの本文を完全にカスタマイズするには、render を渡します (toast.custom 経由でレンダリングされます) :

<ResourceStatusIndicator
  render={(agg) => (
    <div className="rounded-lg border bg-background p-3 shadow">
      {agg.worst?.kind} {agg.worst?.state.toLowerCase()} ({agg.activeCount}{" "}
      waiting)
    </div>
  )}
/>

UI 全体を書き換えることなく特定の kind の文言だけを上書きするには、renderers を渡します。

<ResourceStatusIndicator
  renderers={{
    warehouse: {
      title: () => "Spinning up your data",
      description: (_s, agg) => `${agg.affectedLabels.length} chart(s) waiting`,
    },
  }}
/>

あるいは、useResourceStatus() の集約結果をもとに独自の UI を構築することもできます:

import { useResourceStatus } from "@databricks/appkit-ui/react";

// 全種類のうち最も深刻な状態
const aggregate = useResourceStatus();
// warehouse のみ
const warehouseOnly = useResourceStatus({ kind: "warehouse" });
// { worst, byKind, affectedLabels, activeCount, elapsedMs }

プロバイダーの利用は任意です。マウントしないアプリでもフックごとの warehouseStatus フィールドは利用でき、フックは従来とまったく同じように動作します。

独自のリソースステータスを公開する

plugin (または独自のコード) から、分析以外のリソースについても同じプロバイダーにフックできます。たとえば、Lakebase Postgres 接続のウォームアップや、モデルサービング endpoint のコールドスタートなどです。

import { useResourceStatusPublisher } from "@databricks/appkit-ui/react";
import { useEffect, useId } from "react";

function useLakebaseReadiness() {
  const id = useId();
  const { publish, unpublish } = useResourceStatusPublisher(id, "lakebase", {
    kindHint: "lakebase",
  });

  useEffect(() => {
    publish({
      kind: "lakebase",
      state: "STARTING",
      severity: "pending",
      startedAt: Date.now(),
    });
    return () => unpublish();
  }, [publish, unpublish]);
}

サーバー設定 (analytics({...}) 内) :

オプションデフォルト説明
warehouseStartupTimeoutMsnumber300000 (5分)warehouse が RUNNING になるまで待機する最大時間。これを超えるとリクエストは失敗します
autoStartWarehousebooleantruetrue の場合、STOPPED 状態の warehouse は最初のリクエストで自動起動されます。課金対象となる warehouse の起動をユーザーリクエストによって発生させたくないコスト管理重視のデプロイでは false に設定してください。その場合、STOPPEDConfigurationError として通知されます

ローディング・エラー・空状態の処理例:

import { useAnalyticsQuery } from "@databricks/appkit-ui/react";
import { sql } from "@databricks/appkit-ui/js";
import { Skeleton } from "@databricks/appkit-ui";

function SpendTable() {
  const params = useMemo(
    () => ({
      startDate: sql.date("2025-01-01"),
      endDate: sql.date("2025-12-31"),
    }),
    [],
  );

  const { data, loading, error } = useAnalyticsQuery("spend_summary", params);

  if (loading) return <Skeleton className="h-32 w-full" />;
  if (error) return <div className="text-destructive">{error}</div>;
  if (!data?.length)
    return <div className="text-muted-foreground">No results</div>;

  return (
    <ul>
      {data.map((row) => (
        <li key={row.id}>
          {row.name}: ${row.cost_usd}
        </li>
      ))}
    </ul>
  );
}

型安全なクエリ

QueryRegistry インターフェースを拡張すると、パラメータと結果に対する完全な型推論が得られます:

// shared/appkit-types/analytics.d.ts
declare module "@databricks/appkit-ui/react" {
  interface QueryRegistry {
    spend_summary: {
      name: "spend_summary";
      parameters: { startDate: string; endDate: string };
      result: Array<{ id: string; name: string; cost_usd: number }>;
    };
  }
}

SQLファイルからの自動生成については、型生成を参照してください。

メモ化

再フェッチのループを防ぐため、パラメータは必ず useMemo でラップしてください。フックはパラメータの参照が変わるたびに再実行されます:

// 良い例
const params = useMemo(() => ({ status: sql.string("active") }), []);
const { data } = useAnalyticsQuery("users", params);

// 悪い例 - レンダリングのたびに新しいオブジェクトが生成され、再取得が無限に繰り返される
const { data } = useAnalyticsQuery("users", { status: sql.string("active") });

useMetricView

metric view を SSE 経由で集計する React フックで、POST /api/analytics/metric/:key のクライアント側の対応物です。SQL を書く代わりに、メジャー・ディメンション・フィルターを構造化リクエストとして渡すと、型付きの列名を持つ行と列ごとの表示メタデータがストリーミングで返されます。

import { useMetricView } from "@databricks/appkit-ui/react";

const { data, loading, error, errorCode, metadata, warehouseStatus } =
  useMetricView("revenue", {
    measures: ["arr", "mrr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });

"revenue" が生成された MetricRegistry のキーである場合 (メトリックビューの型を参照) 、measure/dimension 名、許可される timeGrain の値、選択された行のキーはすべて推論されます。未知の measure を渡すと型エラーになります。JSON_ARRAY は SQL のスカラーセルを文字列として保持し、すべての列で SQL NULL を許容するため、data の型は Array<{ arr: string | null; mrr: string | null; created_at: string | null }> | null になります。意図的に値をパースする場合は metadata[col].type を使用してください。

時系列のクエリでは、上記のように選択した時間ディメンションを明示的に昇順で並べ替えてください。指定しない場合、SQL の結果順序は保証されません。チャートヘルパーが時系列データを念のため正規化することもありますが、クエリの並び順をそれに依存しないでください。

オプション:

オプション必須説明
measuresstring[]はい集計する measure。既知のキーの場合は MetricRegistry[key].measureKeys から推論されます。
dimensionsstring[]いいえグループ化する dimension。measureKeys / dimensionKeys から推論されます。
filterMetricFilterいいえ再帰的な述語ツリー (ルートと同じ文法 — フィルターを参照) 。
timeGrainstringいいえ時間ディメンションをバケット化します (daymonth など) 。timeDimension が必要です。timeGrains から推論されます。
timeDimensionstringいいえtimeGrain がバケット化する単一のディメンション。dimensions のいずれかである必要があります。
orderBy{field, direction?}[]いいえソートキー。field はこの呼び出しで選択した measure/dimension に絞り込まれるため、選択していない列で並べ替えると型エラーになります。limit で決定的な結果を得るを参照してください。
limitnumberいいえ行数の上限 (正の整数) 。
autoStartbooleanいいえメトリッククエリを自動的に開始します。既定値は true です。false に設定すると、このオプションが true になるまで実行を遅延します。

戻り値の型:

{
  data: T | null; // 選択した行のキー。値は JSON_ARRAY の string | null
  loading: boolean; // メトリッククエリの実行中は true
  error: string | null; // サニタイズ済みの可読メッセージ。成功時は null
  errorCode: string | null; // 安定した上流コード(メッセージではなくこの値で分岐する)
  metadata: Record<string, MetricViewColumnDisplay> | undefined; // 列ごとの表示メタデータ(下記参照)
  warehouseStatus: WarehouseStatus | null; // 現在のリクエストにおける最新の準備状況
}

useAnalyticsQuery と同様に、オプションオブジェクトは内部でシリアライズされる (JSON.stringify) ため、レンダリングのたびに新しいオブジェクト/配列リテラルを渡しても、シリアライズ結果の文字列が同じであれば再取得は発生しません。つまり、オプションを useMemo する必要はありません。 (判定はシリアライズ結果の一致であり、深い構造的等価性ではありません。filter 内のキーの順序を入れ替えると文字列が変わるため、再クエリが実行されます。measuresdimensions をモジュールスコープに引き上げたりメモ化したりするのも問題なく、配列の型をリテラルのタプルに絞り込んだまま維持できます。)

metadata は、クエリしたカラムのみを対象とするカラムごとの表示メタデータで、その範囲に限定されて SSE の result ペイロードに含まれて返されます。サーバー側でメタデータを解決できなかった場合 (メトリックキーが不明、または型が生成されていない場合) は undefined になるため、常にオプショナルとして扱ってください。

メタデータ

メトリックルートは、列ごとの表示メタデータ (display_nameformattypedescription) を各 result メッセージに付与します。このメタデータは metric view 型ジェネレーターがビルド時に生成するもので、手動で記述した definitions.json と同じディレクトリの config/metric-views/metadata.generated.json に書き出されます。

追加の設定は不要です。 plugin は definitions.json と同じ方法でバンドルを検出するため、analytics({}) だけで十分です。

// server/index.ts
import { analytics, createApp, server } from "@databricks/appkit";

createApp({
  plugins: [
    server(),
    analytics({}),
    // …
  ],
});

metadata.generated.json は生成された型と一緒にコミットしてください。これは同じ生成処理の runtime 側にあたるもので、ルートがリクエスト時にディスクから読み込みます。

これは純粋なレスポンスの装飾です。メタデータがキャッシュキーに含まれることはなく、SQL を変えることもありません。メトリクスの result メッセージには必ず、要求された列に対応する metadata フィールドが含まれます。バンドルが存在しない場合、メッセージは素の /query の結果とバイト単位で同一になり、フックの metadataundefined になります。バンドルが欠落している、または不正な形式の場合は、ラベルなしの列にフォールバックして警告をログに記録するだけで、クエリが失敗することはありません。メタデータはペイロードに載って届くため、クライアント側で生成ファイルをインポートしたりフォーマット文字列をハードコードしたりする必要はありません。つまりペイロード経由で運ばれ、クライアントに依存しないということです。

ファイルを完全にバイパスしたい場合 — 別の方法でメタデータを組み立てるアプリや、意図的に固定したい場合 — は analytics({ metricViewsMetadata }) を渡してください。明示的に指定した値は、検出されたバンドルよりも常に優先されます。

フォーマットユーティリティ

@databricks/appkit-ui/js には、生の値と上記のメタデータを表示用の文字列に変換する、小さく純粋でツリーシェイク可能なフォーマッタが同梱されています。フォーマット仕様 (または MetricViewColumnDisplay) を引数として受け取り、React にもチャートライブラリにも依存しないため、テーブル、ツールチップ、チャート設定のいずれでも同じように使えます。

関数目的
formatValue(value, format?)UC/スプレッドシート形式のフォーマット仕様 ("$#,##0.00""#,##0""0.0%") で生の値を整形します。仕様がない場合は妥当な既定値を使用します。
formatLabel(name, columnMeta?)列の表示ラベル。columnMeta.display_name を優先し、なければ生の名前を読みやすく整形します。
toD3Format(format?)UC フォーマットを d3-formatspecifier と通貨リテラルの prefix に分割します。

鉄則は、フォーマットは必ず metadata から取得し、手書きしないことです。metadataundefined の場合は metadata?.[col]?.formatundefined となり、formatValue は既定の挙動へ自然にフォールバックします。

import { formatLabel, formatValue } from "@databricks/appkit-ui/js";
import { useMetricView } from "@databricks/appkit-ui/react";

function RevenueTable() {
  const { data, metadata } = useMetricView("revenue", {
    measures: ["arr", "mrr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });
  const columns = ["created_at", "arr", "mrr"] as const;

  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => (
            // ヘッダーのテキストは display_name(なければ読みやすく整形した名前)を使用。
            <th key={col}>{formatLabel(col, metadata?.[col])}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data?.map((row, i) => (
          <tr key={i}>
            {columns.map((col) => (
              // フォーマット文字列は metadata から取得する。手書きは厳禁。
              <td key={col}>{formatValue(row[col], metadata?.[col]?.format)}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

フォーマットをチャートに渡す

metadata[col].format はペイロード上の単なる文字列なので、同じ定義をどのチャートライブラリでも軸目盛りやツールチップにそのまま利用できます。

AppKit チャート — 組み込みチャートに valueFormatter を渡します。第 2 引数はメジャーのフィールドなので、1 つのコールバックで系列ごとにカタログのフォーマットを切り替えられます。チャートは内部の ECharts の yAxistooltip のデフォルトを置き換えることなく、組み込みの値軸と系列ごとのツールチップにそのフォーマットを適用します:

import { formatValue } from "@databricks/appkit-ui/js";
import { LineChart, useMetricView } from "@databricks/appkit-ui/react";

function RevenueChart() {
  const { data, metadata } = useMetricView("revenue", {
    measures: ["arr", "mrr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });

  if (!data) return null;

  return (
    <LineChart
      data={data}
      xKey="created_at"
      yKey={["arr", "mrr"]}
      valueFormatter={(value, field) =>
        formatValue(value, metadata?.[field]?.format)
      }
    />
  );
}

複数の系列が1つの値軸を共有する場合、その目盛りには最初の yKey が使われ、各ツールチップには対応する系列のフィールドが使われます。

selected prop による宣言的な強調が効くのは、棒グラフ、円グラフ、ドーナツグラフのみです。折れ線、エリア、散布図、ヒートマップ、レーダーの各チャートでは、カテゴリ選択のセマンティクスが定義されていないため無視されます。

Plotly — 数値指定子は tickformat に、通貨記号そのものは tickprefix に渡します。d3 の $ マーカーはロケール依存で任意の記号を表現できないため、両者は分けて指定する必要があります:

import Plot from "react-plotly.js";
import { toD3Format } from "@databricks/appkit-ui/js";
import { useMetricView } from "@databricks/appkit-ui/react";

function RevenuePlot() {
  const { data, metadata } = useMetricView("revenue", {
    measures: ["arr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });
  const arrFormat = toD3Format(metadata?.arr?.format);
  // "€#,##0.00" → { specifier: ",.2f", prefix: "€" }

  return (
    <Plot
      data={[
        {
          type: "scatter",
          mode: "lines+markers",
          x: data?.map((r) => r.created_at) ?? [],
          y: data?.map((r) => r.arr) ?? [],
          name: metadata?.arr?.display_name ?? "arr",
        },
      ]}
      layout={{
        yaxis: {
          tickformat: arrFormat?.specifier,
          tickprefix: arrFormat?.prefix,
        },
        hoverlabel: { namelength: -1 },
      }}
    />
  );
}

EChartsaxisLabel.formatter / tooltip.formatter の中で formatValue を使ってフォーマット仕様を適用します:

import ReactECharts from "echarts-for-react";
import { formatLabel, formatValue } from "@databricks/appkit-ui/js";
import { useMetricView } from "@databricks/appkit-ui/react";

function RevenueECharts() {
  const { data, metadata } = useMetricView("revenue", {
    measures: ["arr"],
    dimensions: ["created_at"],
    timeGrain: "month",
    timeDimension: "created_at",
    orderBy: [{ field: "created_at", direction: "ASC" }],
  });
  const arrFormat = metadata?.arr?.format;

  const option = {
    xAxis: { type: "category", data: data?.map((r) => r.created_at) ?? [] },
    yAxis: {
      type: "value",
      axisLabel: { formatter: (v: number) => formatValue(v, arrFormat) },
    },
    tooltip: {
      trigger: "axis",
      valueFormatter: (v: number) => formatValue(v, arrFormat),
    },
    series: [
      {
        name: formatLabel("arr", metadata?.arr),
        type: "line",
        data: data?.map((r) => r.arr) ?? [],
      },
    ],
  };

  return <ReactECharts option={option} />;
}

いずれの場合も、フォーマット文字列はサーバーから注入された metadata に由来し、コンポーネントにハードコードされることはありません。metric view の YAML の format 属性を差し替えるだけで、クライアント側に変更を加えることなく、すべての軸・ツールチップ・テーブルセルの表示が更新されます。

Databricks Developer Hub

次のエージェント型アプリを数分でリリースする準備はできていますか?

ドキュメントを読む