Skip to content

Commit a0457e9

Browse files
committed
feat(analytics): freeze metric-view runtime contracts (PR5 phase 0)
Freeze the three shared seams the metric-view hook/server/generator phases compile against: - S1: MetricColumnMeta + MetricViewsMetadata value types in packages/shared - S2: optional per-column metadata on the SSE result message + makeResultMessage - S3: base MetricRegistry, MetricKey, Infer* helpers, MetricFilter mirrors, and UseMetricViewOptions/UseMetricViewResult in appkit-ui hook types Types only (plus a makeResultMessage passthrough); existing /query callers are unchanged since metadata is optional. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
1 parent 4e6da4d commit a0457e9

5 files changed

Lines changed: 173 additions & 3 deletions

File tree

packages/appkit-ui/src/react/hooks/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,29 @@ export {
77
} from "../resource-status-indicator";
88
export type {
99
AnalyticsFormat,
10+
InferDimensionKeys,
11+
InferMeasureKeys,
12+
InferMetricRow,
1013
InferResultByFormat,
1114
InferRowType,
1215
InferServingChunk,
1316
InferServingRequest,
1417
InferServingResponse,
18+
InferTimeGrains,
19+
MetricFilter,
20+
MetricFilterOperatorName,
21+
MetricKey,
22+
MetricPredicate,
23+
MetricRegistry,
1524
PluginRegistry,
1625
QueryRegistry,
1726
ServingAlias,
1827
ServingEndpointRegistry,
1928
TypedArrowTable,
2029
UseAnalyticsQueryOptions,
2130
UseAnalyticsQueryResult,
31+
UseMetricViewOptions,
32+
UseMetricViewResult,
2233
WarehouseState,
2334
WarehouseStatus,
2435
} from "./types";

packages/appkit-ui/src/react/hooks/types.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Table } from "apache-arrow";
2+
import type { MetricColumnMeta } from "shared";
23

34
// ============================================================================
45
// Data Format Types
@@ -247,3 +248,128 @@ export type InferServingRequest<K> =
247248
? Req
248249
: Record<string, unknown>
249250
: Record<string, unknown>;
251+
252+
// ============================================================================
253+
// Metric View Registry
254+
// ============================================================================
255+
256+
/**
257+
* Metric view registry for type-safe metric keys, measure/dimension names,
258+
* time grains, and row shapes. Extend this interface via module augmentation
259+
* to get autocomplete for `useMetricView`:
260+
*
261+
* @example
262+
* ```typescript
263+
* // Auto-generated (generated metric-views.ts)
264+
* declare module "@databricks/appkit-ui/react" {
265+
* interface MetricRegistry {
266+
* orders: {
267+
* measureKeys: "revenue" | "order_count";
268+
* dimensionKeys: "region" | "order_date";
269+
* timeGrains: "day" | "month";
270+
* measures: { revenue: number; order_count: number };
271+
* dimensions: { region: string; order_date: string };
272+
* };
273+
* }
274+
* }
275+
* ```
276+
*/
277+
// biome-ignore lint/suspicious/noEmptyInterface: intentionally empty — populated via module augmentation (generated metric-views.ts)
278+
export interface MetricRegistry {}
279+
280+
/** Resolves to registry keys if populated, otherwise string */
281+
export type MetricKey = AugmentedRegistry<MetricRegistry> extends never
282+
? string
283+
: AugmentedRegistry<MetricRegistry>;
284+
285+
/** Infers measure key names from the registry when K is a known key */
286+
export type InferMeasureKeys<K> = K extends AugmentedRegistry<MetricRegistry>
287+
? MetricRegistry[K] extends { measureKeys: infer M }
288+
? M
289+
: string
290+
: string;
291+
292+
/** Infers dimension key names from the registry when K is a known key */
293+
export type InferDimensionKeys<K> = K extends AugmentedRegistry<MetricRegistry>
294+
? MetricRegistry[K] extends { dimensionKeys: infer D }
295+
? D
296+
: string
297+
: string;
298+
299+
/** Infers time-grain names from the registry when K is a known key */
300+
export type InferTimeGrains<K> = K extends AugmentedRegistry<MetricRegistry>
301+
? MetricRegistry[K] extends { timeGrains: infer G }
302+
? G
303+
: string
304+
: string;
305+
306+
/**
307+
* Infers the row shape (measures + dimensions) from the registry when K is a
308+
* known key, otherwise a total `Record<string, unknown>`. Never resolves to
309+
* `never` — always assignable to `Record<string, unknown>`.
310+
*/
311+
export type InferMetricRow<K> = K extends AugmentedRegistry<MetricRegistry>
312+
? MetricRegistry[K] extends {
313+
measures: infer Meas;
314+
dimensions: infer Dim;
315+
}
316+
? Meas & Dim
317+
: Record<string, unknown>
318+
: Record<string, unknown>;
319+
320+
// ────────────────────────────────────────────────────────────────────────────
321+
// Metric filter vocabulary.
322+
//
323+
// **Kept in sync with appkit `plugins/analytics/types.ts`** — appkit-ui cannot
324+
// depend on appkit, so this mirrors the twelve-operator filter grammar by hand.
325+
// ────────────────────────────────────────────────────────────────────────────
326+
327+
/** v1 filter operator vocabulary — exactly twelve names. */
328+
export type MetricFilterOperatorName =
329+
| "equals"
330+
| "notEquals"
331+
| "in"
332+
| "notIn"
333+
| "gt"
334+
| "gte"
335+
| "lt"
336+
| "lte"
337+
| "contains"
338+
| "notContains"
339+
| "set"
340+
| "notSet";
341+
342+
/** A single filter predicate — the leaf node of the recursive {@link MetricFilter} tree. */
343+
export interface MetricPredicate {
344+
member: string;
345+
operator: MetricFilterOperatorName;
346+
values?: ReadonlyArray<string | number>;
347+
}
348+
349+
/** Recursive filter expression: a leaf {@link MetricPredicate} or an `and`/`or` group. */
350+
export type MetricFilter =
351+
| MetricPredicate
352+
| { and: ReadonlyArray<MetricFilter> }
353+
| { or: ReadonlyArray<MetricFilter> };
354+
355+
/** Options for configuring a `useMetricView` query. */
356+
export interface UseMetricViewOptions<K extends MetricKey = MetricKey> {
357+
measures: ReadonlyArray<InferMeasureKeys<K>>;
358+
dimensions?: ReadonlyArray<InferDimensionKeys<K>>;
359+
filter?: MetricFilter;
360+
timeGrain?: InferTimeGrains<K>;
361+
timeDimension?: InferDimensionKeys<K>;
362+
limit?: number;
363+
autoStart?: boolean;
364+
}
365+
366+
/** Result state returned by `useMetricView`. */
367+
export interface UseMetricViewResult<T = Record<string, unknown>[]> {
368+
data: T | null;
369+
loading: boolean;
370+
error: string | null;
371+
/** Structured upstream error code, mirroring useAnalyticsQuery. */
372+
errorCode: string | null;
373+
/** Per-column display metadata for the queried columns, carried in the SSE result payload. `undefined` when the server injected no metadata (dormant / unknown key). */
374+
metadata: Record<string, MetricColumnMeta> | undefined;
375+
}

packages/shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from "./agent";
22
export * from "./cache";
33
export * from "./execute";
44
export * from "./genie";
5+
export * from "./metric-metadata";
56
export * from "./plugin";
67
export * from "./sql";
78
export * from "./sse/analytics";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/** Per-column display metadata for a UC Metric View column, sourced from the
2+
* YAML 1.1 display_name/format attributes + SQL type. Loose enough that an
3+
* `as const` generated literal assigns to it. */
4+
export interface MetricColumnMeta {
5+
type: string;
6+
display_name?: string;
7+
format?: string;
8+
description?: string;
9+
}
10+
/** Build-time-generated metadata for every registered metric view, keyed by
11+
* metric key. Injected into the analytics plugin via `analytics({ metricViewsMetadata })`. */
12+
export type MetricViewsMetadata = Record<
13+
string,
14+
{
15+
measures: Record<string, MetricColumnMeta>;
16+
dimensions: Record<string, MetricColumnMeta>;
17+
}
18+
>;

packages/shared/src/sse/analytics.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from "zod";
2+
import type { MetricColumnMeta } from "../metric-metadata";
23

34
/**
45
* Wire protocol for analytics SSE messages emitted by `/api/analytics/query`.
@@ -37,22 +38,31 @@ export const AnalyticsResultMessage = z.object({
3738
// `unknown` so we don't bake the SDK's detailed shape into the contract.
3839
status: z.unknown().optional(),
3940
statement_id: z.string().optional(),
41+
// Per-column display metadata for a metric-view result (display_name /
42+
// format / type). Kept loose (`z.record(z.string(), z.unknown())`) for the
43+
// same "keep client validation cheap" reason as `data` — the server
44+
// constructs it via the typed builder, so the per-column shape is enforced
45+
// at the source. Absent for plain `/query` results.
46+
metadata: z.record(z.string(), z.unknown()).optional(),
4047
});
4148

4249
/**
4350
* TS-level shape of a successful row-shaped result message.
4451
*
4552
* **Kept in sync by hand** with `AnalyticsResultMessage` above. The Zod
46-
* schema is intentionally loose (`z.array(z.unknown())`) to keep client
53+
* schema is intentionally loose (`z.array(z.unknown())` for `data`,
54+
* `z.record(z.string(), z.unknown())` for `metadata`) to keep client
4755
* validation cheap; this interface narrows `data` to
48-
* `Record<string, unknown>[]` so consumers don't have to cast at every
56+
* `Record<string, unknown>[]` and `metadata` to
57+
* `Record<string, MetricColumnMeta>` so consumers don't have to cast at every
4958
* call site. If you add a field to the Zod schema, add it here too.
5059
*/
5160
export interface AnalyticsResultMessage {
5261
type: "result";
5362
data?: Record<string, unknown>[];
5463
status?: unknown;
5564
statement_id?: string;
65+
metadata?: Record<string, MetricColumnMeta>;
5666
}
5767

5868
/**
@@ -72,7 +82,11 @@ export type AnalyticsSseMessage = z.infer<typeof AnalyticsSseMessage>;
7282

7383
export function makeResultMessage(
7484
data: Record<string, unknown>[] | undefined,
75-
extras: { status?: unknown; statement_id?: string } = {},
85+
extras: {
86+
status?: unknown;
87+
statement_id?: string;
88+
metadata?: Record<string, MetricColumnMeta>;
89+
} = {},
7690
): AnalyticsResultMessage {
7791
return { type: "result", data, ...extras };
7892
}

0 commit comments

Comments
 (0)