Skip to content

Commit c31cd86

Browse files
committed
Log system queries to query insights stream
1 parent 211b5f8 commit c31cd86

9 files changed

Lines changed: 62 additions & 10 deletions

File tree

Architecture/query-insights.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Selecting a row auto-pauses table updates. Closing a sheet that caused auto-paus
7373

7474
Studio's `Query` contract includes `meta.visibility`.
7575

76-
Postgres adapter-generated introspection, table reads, mutations, and fallback lint helper queries MUST be marked `studio-system`. Raw SQL editor executions remain user-visible. BFF hosts should append `-- prisma:studio` to system queries before forwarding them to the database so backend Query Insights implementations can filter them consistently with `-- prisma:console`.
76+
Postgres adapter-generated introspection, table reads, mutations, and fallback lint helper queries MUST be marked `studio-system`. Raw SQL editor executions remain user-visible. BFF hosts should append `-- prisma:studio` to system queries before forwarding them to the database so backend Query Insights implementations can classify them consistently with `-- prisma:console`. System classification MUST NOT prevent successful query executions from being appended to `prisma-log`.
7777

7878
## ppg-dev Demo
7979

@@ -82,7 +82,7 @@ The local `ppg-dev` demo hosts the Query Insights backend itself:
8282
- `/api/config` advertises the Query Insights transport URLs
8383
- ppg-dev ensures the `prisma-log` stream exists on the configured Prisma
8484
Streams server at startup
85-
- `/api/query` appends the Studio system suffix to system queries, executes the database request, and appends each successful user-visible SQL execution to `prisma-log`
85+
- `/api/query` appends the Studio system suffix to system queries, executes the database request, and appends each successful SQL execution to `prisma-log` with query visibility metadata
8686
- the Query Insights UI reads from `/api/streams/v1/stream/prisma-log`, so it
8787
uses the same same-origin Streams proxy as the Stream view
8888
- `/api/query-insights/analyze` returns deterministic demo analysis

FEATURES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ In that mode the shell hides schema selection, table navigation, and database-on
2626
Embedders can provide a Query Insights transport that adds a native Studio view at `view=query-insights`.
2727
The view reads query events from a `prisma-log` Prisma Streams stream while active, buckets event timestamps into latency and queries-per-second charts, groups Prisma operations when metadata is available, and keeps the query table sortable, filterable by table, and bounded in memory.
2828
Selecting a row opens a Studio sheet with SQL, runtime stats, Prisma operation context, and structured recommendations after workspace-scoped AI consent.
29-
The `ppg-dev` demo appends user-originated SQL from `/api/query` into `prisma-log` continuously through the same Streams server used by the demo, while hiding Studio system queries.
29+
The `ppg-dev` demo appends every successful SQL execution from `/api/query` into `prisma-log` continuously through the same Streams server used by the demo, with Studio system queries annotated for downstream classification.
3030

3131
## Local Streams Development Override
3232

demo/ppg-dev/query-insights.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
analyzeDemoQueryInsight,
55
appendStudioSystemQuerySuffix,
66
createQueryInsightsLogEvent,
7+
getQueryInsightsQueryVisibility,
78
isStudioSystemQuery,
89
parseSqlTableNames,
910
QUERY_INSIGHTS_LOG_STREAM_NAME,
@@ -22,6 +23,7 @@ describe("ppg-dev Query Insights helpers", () => {
2223
`select * from pg_catalog.pg_class ${STUDIO_SYSTEM_QUERY_SUFFIX}`,
2324
);
2425
expect(isStudioSystemQuery(tagged)).toBe(true);
26+
expect(getQueryInsightsQueryVisibility(tagged)).toBe("studio-system");
2527
expect(
2628
isStudioSystemQuery({
2729
meta: { visibility: "user" },
@@ -43,7 +45,7 @@ describe("ppg-dev Query Insights helpers", () => {
4345
expect(parseSqlTableNames("select * from pg_catalog.pg_class")).toEqual([]);
4446
});
4547

46-
it("builds prisma-log query events and skips Studio system queries", () => {
48+
it("builds prisma-log query events for user and Studio system queries", () => {
4749
const event = createQueryInsightsLogEvent({
4850
durationMs: 12.5,
4951
query: {
@@ -63,14 +65,30 @@ describe("ppg-dev Query Insights helpers", () => {
6365
tables: ["organizations"],
6466
ts: 1_700_000_000_000,
6567
type: "query",
68+
visibility: "user",
69+
});
70+
const systemEvent = createQueryInsightsLogEvent({
71+
durationMs: 1,
72+
query: appendStudioSystemQuerySuffix({
73+
meta: { visibility: "studio-system" },
74+
parameters: [],
75+
sql: "select * from pg_catalog.pg_class",
76+
}),
77+
rows: [],
78+
ts: 1_700_000_000_001,
79+
});
80+
81+
expect(systemEvent).toMatchObject({
82+
sql: "select * from pg_catalog.pg_class",
83+
type: "query",
84+
visibility: "studio-system",
6685
});
6786
expect(
6887
createQueryInsightsLogEvent({
6988
durationMs: 1,
7089
query: {
71-
meta: { visibility: "studio-system" },
7290
parameters: [],
73-
sql: "select * from pg_catalog.pg_class",
91+
sql: " ",
7492
},
7593
rows: [],
7694
}),

demo/ppg-dev/query-insights.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Query } from "../../data/query";
22
import type {
33
QueryInsightsAnalysisResult,
44
QueryInsightsAnalyzeInput,
5+
QueryInsightsQueryVisibility,
56
QueryInsightsStreamQuery,
67
} from "../../ui/studio/views/query-insights/types";
78

@@ -11,6 +12,7 @@ const CONSOLE_SYSTEM_QUERY_SUFFIX = "-- prisma:console";
1112

1213
export interface QueryInsightsLogEvent extends QueryInsightsStreamQuery {
1314
type: "query";
15+
visibility: QueryInsightsQueryVisibility;
1416
}
1517

1618
function normalizeSql(sql: string): string {
@@ -32,6 +34,12 @@ export function isStudioSystemQuery(query: Query<unknown>): boolean {
3234
);
3335
}
3436

37+
export function getQueryInsightsQueryVisibility(
38+
query: Query<unknown>,
39+
): QueryInsightsQueryVisibility {
40+
return isStudioSystemQuery(query) ? "studio-system" : "user";
41+
}
42+
3543
export function appendStudioSystemQuerySuffix<T>(query: Query<T>): Query<T> {
3644
if (query.meta?.visibility !== "studio-system") {
3745
return query;
@@ -76,10 +84,6 @@ export function createQueryInsightsLogEvent(args: {
7684
rows: unknown;
7785
ts?: number;
7886
}): QueryInsightsLogEvent | null {
79-
if (isStudioSystemQuery(args.query)) {
80-
return null;
81-
}
82-
8387
const cleanedSql = stripKnownSystemSuffixes(args.query.sql);
8488
const normalizedSql = normalizeSql(cleanedSql);
8589

@@ -103,6 +107,7 @@ export function createQueryInsightsLogEvent(args: {
103107
tables: parseSqlTableNames(cleanedSql),
104108
ts: args.ts ?? Date.now(),
105109
type: "query",
110+
visibility: getQueryInsightsQueryVisibility(args.query),
106111
};
107112
}
108113

ui/studio/views/query-insights/codecs.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe("Query Insights stream codecs", () => {
2929
sql: "select id from users",
3030
tables: ["users"],
3131
ts: 1_700_000_000_000,
32+
visibility: "user",
3233
},
3334
]),
3435
);
@@ -48,6 +49,7 @@ describe("Query Insights stream codecs", () => {
4849
model: "User",
4950
},
5051
queryId: "query-1",
52+
visibility: "user",
5153
});
5254
expect(chartTick).toEqual({
5355
data: {
@@ -85,6 +87,7 @@ describe("Query Insights stream codecs", () => {
8587
tables: ["organizations"],
8688
ts: 1_700_000_000_100,
8789
type: "query",
90+
visibility: "studio-system",
8891
},
8992
{
9093
count: 2,
@@ -124,6 +127,7 @@ describe("Query Insights stream codecs", () => {
124127

125128
expect(decoded.success).toBe(true);
126129
expect(decoded.data).toHaveLength(3);
130+
expect(decoded.data?.[0]?.visibility).toBe("studio-system");
127131
expect(createChartTicksFromQueries(decoded.data ?? [])).toEqual([
128132
{
129133
avgDurationMs: 40 / 3,

ui/studio/views/query-insights/codecs.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
QUERY_INSIGHTS_CHART_BUCKET_MS,
33
type QueryInsightsChartPoint,
44
type QueryInsightsPrismaInfo,
5+
type QueryInsightsQueryVisibility,
56
type QueryInsightsStreamQuery,
67
} from "./types";
78

@@ -101,6 +102,22 @@ function decodeOptionalString(value: unknown): string | null | undefined {
101102
return assertString(value, "optional string field");
102103
}
103104

105+
function decodeOptionalVisibility(
106+
value: unknown,
107+
): QueryInsightsQueryVisibility | undefined {
108+
if (value === undefined) {
109+
return undefined;
110+
}
111+
112+
if (value === "studio-system" || value === "user") {
113+
return value;
114+
}
115+
116+
throw new Error(
117+
"Invalid Query Insights event: visibility must be studio-system or user.",
118+
);
119+
}
120+
104121
function decodeStreamQuery(value: unknown): QueryInsightsStreamQuery {
105122
if (!isRecord(value)) {
106123
throw new Error(
@@ -138,6 +155,7 @@ function decodeStreamQuery(value: unknown): QueryInsightsStreamQuery {
138155
sql,
139156
tables: assertStringArray(value.tables, "tables"),
140157
ts,
158+
visibility: decodeOptionalVisibility(value.visibility),
141159
};
142160
}
143161

ui/studio/views/query-insights/rows.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe("Query Insights row helpers", () => {
6464
duration: 84.2,
6565
reads: 12,
6666
rowsReturned: 2,
67+
visibility: undefined,
6768
});
6869
});
6970

ui/studio/views/query-insights/rows.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function toQueryInsight(
3434
reads: query.reads,
3535
rowsReturned: query.rowsReturned,
3636
tables: query.tables,
37+
visibility: query.visibility,
3738
};
3839
}
3940

@@ -73,6 +74,7 @@ function mergeQueryInsight(
7374
reads: existing.reads + incoming.reads,
7475
rowsReturned: existing.rowsReturned + incoming.rowsReturned,
7576
tables: Array.from(tables).sort(),
77+
visibility: incoming.visibility ?? existing.visibility,
7678
};
7779
}
7880

ui/studio/views/query-insights/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export interface QueryInsightsPrismaInfo {
1010
payload?: Record<string, unknown> | Array<Record<string, unknown>>;
1111
}
1212

13+
export type QueryInsightsQueryVisibility = "studio-system" | "user";
14+
1315
export interface QueryInsightsQuery {
1416
count: number;
1517
duration: number;
@@ -24,6 +26,7 @@ export interface QueryInsightsQuery {
2426
reads: number;
2527
rowsReturned: number;
2628
tables: string[];
29+
visibility?: QueryInsightsQueryVisibility;
2730
}
2831

2932
export interface QueryInsightsStreamQuery {
@@ -39,6 +42,7 @@ export interface QueryInsightsStreamQuery {
3942
sql: string;
4043
tables: string[];
4144
ts: number;
45+
visibility?: QueryInsightsQueryVisibility;
4246
}
4347

4448
export interface QueryInsightsChartPoint {

0 commit comments

Comments
 (0)