Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function useDashboardData(filters: DashboardFilters) {
data: kpisRaw,
loading: kpisLoading,
error: kpisError,
} = useAnalyticsQuery("dashboard_kpis", params) as {
} = useAnalyticsQuery("dashboard_kpis", params, { format: "JSON_ARRAY" }) as {
data: KPIRawRow[] | null;
loading: boolean;
error: string | null;
Expand All @@ -90,7 +90,9 @@ export function useDashboardData(filters: DashboardFilters) {
data: topZoneRaw,
loading: topZoneLoading,
error: topZoneError,
} = useAnalyticsQuery("dashboard_top_zone", params) as {
} = useAnalyticsQuery("dashboard_top_zone", params, {
format: "JSON_ARRAY",
}) as {
data: TopZoneData[] | null;
loading: boolean;
error: string | null;
Expand All @@ -109,7 +111,9 @@ export function useDashboardData(filters: DashboardFilters) {
data: tripsOverTime,
loading: tripsLoading,
error: tripsError,
} = useAnalyticsQuery("dashboard_trips_over_time", tripsParams) as {
} = useAnalyticsQuery("dashboard_trips_over_time", tripsParams, {
format: "JSON_ARRAY",
}) as {
data: TripOverTime[] | null;
loading: boolean;
error: string | null;
Expand All @@ -119,7 +123,9 @@ export function useDashboardData(filters: DashboardFilters) {
data: fareDistribution,
loading: fareLoading,
error: fareError,
} = useAnalyticsQuery("dashboard_fare_distribution", tripsParams) as {
} = useAnalyticsQuery("dashboard_fare_distribution", tripsParams, {
format: "JSON_ARRAY",
}) as {
data: FareBucket[] | null;
loading: boolean;
error: string | null;
Expand All @@ -129,7 +135,9 @@ export function useDashboardData(filters: DashboardFilters) {
data: heatmap,
loading: heatmapLoading,
error: heatmapError,
} = useAnalyticsQuery("dashboard_hourly_heatmap", params) as {
} = useAnalyticsQuery("dashboard_hourly_heatmap", params, {
format: "JSON_ARRAY",
}) as {
data: HeatmapCell[] | null;
loading: boolean;
error: string | null;
Expand All @@ -139,7 +147,9 @@ export function useDashboardData(filters: DashboardFilters) {
data: topZones,
loading: topZonesLoading,
error: topZonesError,
} = useAnalyticsQuery("dashboard_top_zones", params) as {
} = useAnalyticsQuery("dashboard_top_zones", params, {
format: "JSON_ARRAY",
}) as {
data: TopZoneRow[] | null;
loading: boolean;
error: string | null;
Expand All @@ -149,7 +159,9 @@ export function useDashboardData(filters: DashboardFilters) {
data: sparklines,
loading: sparklinesLoading,
error: sparklinesError,
} = useAnalyticsQuery("dashboard_kpi_sparklines", params) as {
} = useAnalyticsQuery("dashboard_kpi_sparklines", params, {
format: "JSON_ARRAY",
}) as {
data: SparklineRow[] | null;
loading: boolean;
error: string | null;
Expand Down
14 changes: 11 additions & 3 deletions apps/dev-playground/client/src/routes/analytics.route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,15 @@ function AnalyticsRoute() {
data: summaryDataRaw,
loading: summaryLoading,
error: summaryError,
} = useAnalyticsQuery("spend_summary", summaryParams);
} = useAnalyticsQuery("spend_summary", summaryParams, {
format: "JSON_ARRAY",
});

const { data: appsListData } = useAnalyticsQuery("apps_list", {});
const { data: appsListData } = useAnalyticsQuery(
"apps_list",
{},
{ format: "JSON_ARRAY" },
);

const untaggedAppsParams = useMemo(() => {
return {
Expand All @@ -69,7 +75,9 @@ function AnalyticsRoute() {
data: untaggedAppsData,
loading: untaggedAppsLoading,
error: untaggedAppsError,
} = useAnalyticsQuery("untagged_apps", untaggedAppsParams);
} = useAnalyticsQuery("untagged_apps", untaggedAppsParams, {
format: "JSON_ARRAY",
});

const metrics = useMemo(() => {
if (!summaryDataRaw || summaryDataRaw.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ function SqlHelpersRoute() {
const { data, loading, error } = useAnalyticsQuery(
"sql_helpers_test",
queryParams ?? {},
{ format: "JSON_ARRAY" },
);

// Helper to show the marker result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ describe("useChartData", () => {
);
});

test("auto-selects JSON_ARRAY by default when no heuristics match", () => {
test("auto-selects ARROW_STREAM by default when no heuristics match", () => {
mockUseAnalyticsQuery.mockReturnValue({
data: [],
loading: false,
Expand All @@ -223,11 +223,11 @@ describe("useChartData", () => {
expect(mockUseAnalyticsQuery).toHaveBeenCalledWith(
"test",
{ limit: 100 },
expect.objectContaining({ format: "JSON_ARRAY" }),
expect.objectContaining({ format: "ARROW_STREAM" }),
);
});

test("defaults to auto format (JSON_ARRAY) when format is not specified", () => {
test("defaults to auto format (ARROW_STREAM) when format is not specified", () => {
mockUseAnalyticsQuery.mockReturnValue({
data: [],
loading: false,
Expand All @@ -243,7 +243,7 @@ describe("useChartData", () => {
expect(mockUseAnalyticsQuery).toHaveBeenCalledWith(
"test",
undefined,
expect.objectContaining({ format: "JSON_ARRAY" }),
expect.objectContaining({ format: "ARROW_STREAM" }),
);
});
});
Expand Down
4 changes: 2 additions & 2 deletions packages/appkit-ui/src/react/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export interface TypedArrowTable<

/** Options for configuring an analytics SSE query */
export interface UseAnalyticsQueryOptions<
F extends AnalyticsFormat = "JSON_ARRAY",
F extends AnalyticsFormat = "ARROW_STREAM",
> {
/** Response format - "JSON_ARRAY" (default) returns typed arrays, "ARROW_STREAM" uses Arrow (inline or external links) */
/** Response format - "ARROW_STREAM" (default) returns a TypedArrowTable (compact binary wire, type-preserving). "JSON_ARRAY" returns typed row arrays. */
format?: F;

/** Maximum size of serialized parameters in bytes */
Expand Down
18 changes: 9 additions & 9 deletions packages/appkit-ui/src/react/hooks/use-analytics-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ function getArrowStreamUrl(id: string) {
* Integration hook between client and analytics plugin.
*
* The return type is automatically inferred based on the format:
* - `format: "JSON_ARRAY"` (default): Returns typed array from QueryRegistry
* - `format: "ARROW_STREAM"`: Returns TypedArrowTable with row type preserved
* - `format: "ARROW_STREAM"` (default): Returns TypedArrowTable with row type preserved — works across all warehouse variants and avoids JSON serialization cost
* - `format: "JSON_ARRAY"`: Returns typed array from QueryRegistry
*
* Note: User context execution is determined by query file naming:
* - `queryKey.obo.sql`: Executes as user (OBO = on-behalf-of / user delegation)
Expand All @@ -89,28 +89,28 @@ function getArrowStreamUrl(id: string) {
* @param options - Analytics query settings including format
* @returns Query result state with format-appropriate data type
*
* @example JSON_ARRAY format (default)
* @example ARROW_STREAM format (default)
* ```typescript
* const { data } = useAnalyticsQuery("spend_data", params);
* // data: Array<{ group_key: string; cost_usd: number; ... }> | null
* // data: TypedArrowTable<{ group_key: string; cost_usd: number; ... }> | null
* ```
*
* @example ARROW_STREAM format
* @example JSON_ARRAY format
* ```typescript
* const { data } = useAnalyticsQuery("spend_data", params, { format: "ARROW_STREAM" });
* // data: TypedArrowTable<{ group_key: string; cost_usd: number; ... }> | null
* const { data } = useAnalyticsQuery("spend_data", params, { format: "JSON_ARRAY" });
* // data: Array<{ group_key: string; cost_usd: number; ... }> | null
* ```
*/
export function useAnalyticsQuery<
T = unknown,
K extends QueryKey = QueryKey,
F extends AnalyticsFormat = "JSON_ARRAY",
F extends AnalyticsFormat = "ARROW_STREAM",
>(
queryKey: K,
parameters?: InferParams<K> | null,
options: UseAnalyticsQueryOptions<F> = {} as UseAnalyticsQueryOptions<F>,
): UseAnalyticsQueryResult<InferResultByFormat<T, K, F>> {
const format = options?.format ?? "JSON_ARRAY";
const format = options?.format ?? "ARROW_STREAM";
const maxParametersSize = options?.maxParametersSize ?? 100 * 1024;
const autoStart = options?.autoStart ?? true;

Expand Down
4 changes: 2 additions & 2 deletions packages/appkit-ui/src/react/hooks/use-chart-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ function resolveFormat(
return "ARROW_STREAM";
}

return "JSON_ARRAY";
return "ARROW_STREAM";
}

return "JSON_ARRAY";
return "ARROW_STREAM";
}

// ============================================================================
Expand Down
13 changes: 9 additions & 4 deletions packages/appkit-ui/src/react/table/table-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,15 @@ export function TableWrapper<TRaw = any, TProcessed = any>(
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

const { data, loading, error } = useAnalyticsQuery<TRaw[]>(
queryKey,
parameters,
);
// Pinned to JSON_ARRAY: the table walks `data.length` / `data[i]` for
// tabular rendering. A migration to Arrow's columnar API is a separate
// optimization — keeping it on the JSON shape preserves behavior across
// the default-switch to ARROW_STREAM.
const { data, loading, error } = useAnalyticsQuery<
TRaw[],
typeof queryKey,
"JSON_ARRAY"
>(queryKey, parameters, { format: "JSON_ARRAY" });

useEffect(() => {
if (onRowSelectionChange && enableRowSelection) {
Expand Down
2 changes: 1 addition & 1 deletion packages/appkit/src/connectors/sql-warehouse/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface ExecuteStatementDefaults {
export const executeStatementDefaults: ExecuteStatementDefaults = {
wait_timeout: "30s",
disposition: "INLINE",
format: "JSON_ARRAY",
format: "ARROW_STREAM",
on_wait_timeout: "CONTINUE",
timeout: 60000,
};
2 changes: 1 addition & 1 deletion packages/appkit/src/plugins/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export class AnalyticsPlugin extends Plugin implements ToolProvider {
res: express.Response,
): Promise<void> {
const { query_key } = req.params;
const { parameters, format: rawFormat = "JSON_ARRAY" } =
const { parameters, format: rawFormat = "ARROW_STREAM" } =
req.body as IAnalyticsQueryRequest;

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,20 @@ describe("Analytics Plugin Integration", () => {
createSuccessfulSQLResponse([["cached_value"]], [{ name: "value" }]),
);

// Caching is JSON_ARRAY-only — the ARROW_STREAM default bypasses
// cache because inline-stash ids drain on first /arrow-result fetch,
// so a cache hit would replay a dead id. Explicitly request the
// cacheable shape.
const cacheableBody = JSON.stringify({
parameters: {},
format: "JSON_ARRAY",
});
const response1 = await fetch(
`${baseUrl}/api/analytics/query/cache_test`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parameters: {} }),
body: cacheableBody,
},
);
const data1 = await parseSSEResponse(response1);
Expand All @@ -259,7 +267,7 @@ describe("Analytics Plugin Integration", () => {
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parameters: {} }),
body: cacheableBody,
},
);
const data2 = await parseSSEResponse(response2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ describe("Analytics Plugin", () => {
);
});

test("/query/:query_key should use INLINE + JSON_ARRAY by default when no format specified", async () => {
test("/query/:query_key should use INLINE + ARROW_STREAM by default when no format specified", async () => {
const plugin = new AnalyticsPlugin(config);
const { router, getHandler } = createMockRouter();

Expand All @@ -708,8 +708,11 @@ describe("Analytics Plugin", () => {
isAsUser: false,
});

// ARROW_STREAM + INLINE returns an attachment (one row's worth of
// Arrow IPC bytes is fine for shape-checking the request, the
// contents don't need to be decoded for this test).
const executeMock = vi.fn().mockResolvedValue({
result: { data: [{ id: 1 }] },
result: { attachment: Buffer.from("AQID").toString("base64") },
});
(plugin as any).SQLClient.executeStatement = executeMock;

Expand All @@ -728,7 +731,7 @@ describe("Analytics Plugin", () => {
expect.anything(),
expect.objectContaining({
disposition: "INLINE",
format: "JSON_ARRAY",
format: "ARROW_STREAM",
}),
expect.any(AbortSignal),
);
Expand Down
6 changes: 3 additions & 3 deletions template/client/src/pages/analytics/AnalyticsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ export function AnalyticsPage() {
</div>
)}
{error && <div className="text-destructive bg-destructive/10 p-3 rounded-md">Error: {error}</div>}
{data && data.length > 0 && (
{data && data.numRows > 0 && (
<div className="space-y-2">
<div className="text-sm text-muted-foreground">Query: SELECT :message AS value</div>
<div className="text-2xl font-bold text-primary">{data[0].value}</div>
<div className="text-2xl font-bold text-primary">{data.getChild('value')?.get(0)}</div>
</div>
)}
{data && data.length === 0 && <div className="text-muted-foreground">No results</div>}
{data && data.numRows === 0 && <div className="text-muted-foreground">No results</div>}
</CardContent>
</Card>

Expand Down