@prisma/sqlcommenter-query-insights 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @prisma/sqlcommenter-query-insights
2
+
3
+ A SQL commenter plugin for Prisma ORM that adds query shape information to SQL comments. This enables observability tools to analyze and group queries by their structural patterns rather than specific values.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @prisma/sqlcommenter-query-insights
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { prismaQueryInsights } from '@prisma/sqlcommenter-query-insights'
15
+ import { PrismaClient } from '@prisma/client'
16
+
17
+ const prisma = new PrismaClient({
18
+ adapter: myAdapter, // Driver adapter required (alternatively, Accelerate URL)
19
+ comments: [prismaQueryInsights()],
20
+ })
21
+ ```
22
+
23
+ The resulting SQL will include comments like:
24
+
25
+ ```sql
26
+ SELECT ... FROM "User" /*prismaQuery='User.findMany:eyJ3aGVyZSI6eyJhY3RpdmUiOnsiJHR5cGUiOiJQYXJhbSJ9fSwiaW5jbHVkZSI6eyJwb3N0cyI6dHJ1ZX19'*/
27
+ ```
28
+
29
+ ## What It Does
30
+
31
+ This plugin adds a `prismaQuery` comment tag to every SQL query. The tag contains:
32
+
33
+ - **Model name**: The Prisma model being queried (e.g., `User`, `Post`)
34
+ - **Action**: The Prisma operation (e.g., `findMany`, `createOne`, `updateOne`)
35
+ - **Query shape**: A parameterized representation of the query structure (base64url encoded)
36
+
37
+ ### Example Outputs
38
+
39
+ | Query Type | Output Format |
40
+ | ------------------------ | ------------------------------------------------------------------ |
41
+ | Raw query | `queryRaw` |
42
+ | Simple find (all fields) | `User.findMany:e30` |
43
+ | Find with where | `User.findUnique:eyJ3aGVyZSI6eyJpZCI6eyIkdHlwZSI6IlBhcmFtIn19fQ` |
44
+ | Find with include | `User.findMany:eyJpbmNsdWRlIjp7InBvc3RzIjp0cnVlfX0` |
45
+ | Find with select | `User.findMany:eyJzZWxlY3QiOnsibmFtZSI6dHJ1ZX19` |
46
+ | Batched queries | `User.findUnique:W3sid2hlcmUiOnsiaWQiOnsiJHR5cGUiOiJQYXJhbSJ9fX1d` |
47
+
48
+ ## Security
49
+
50
+ **User data is never included in the comments.** All values that could contain user data are replaced with placeholder markers before encoding. This includes:
51
+
52
+ - Filter values (in `where` clauses)
53
+ - Data values (in `create`/`update` operations)
54
+ - Values in all filter operators (`equals`, `contains`, `in`, etc.)
55
+ - Tagged values (DateTime, Decimal, BigInt, Bytes, Json)
56
+
57
+ Only structural information is preserved:
58
+
59
+ - Field names and relationships
60
+ - Query structure (selection, filters, ordering)
61
+ - Pagination parameters (`take`, `skip`)
62
+ - Sort directions and null handling options
63
+
64
+ ## Use Cases
65
+
66
+ ### Query Analysis
67
+
68
+ Group queries by their shape to identify:
69
+
70
+ - Most frequently executed query patterns
71
+ - Slow query patterns that need optimization
72
+ - Unusual query patterns that might indicate bugs
73
+
74
+ ### Observability Integration
75
+
76
+ The `prismaQuery` tag can be parsed by observability tools to:
77
+
78
+ - Create dashboards showing query pattern distribution
79
+ - Set up alerts for specific query patterns
80
+ - Correlate application behavior with database load
81
+
82
+ ### Debugging
83
+
84
+ Quickly identify which Prisma operation generated a specific SQL query in your database logs.
85
+
86
+ ## Combining with Other Plugins
87
+
88
+ ```typescript
89
+ import { prismaQueryInsights } from '@prisma/sqlcommenter-query-insights'
90
+ import { traceContext } from '@prisma/sqlcommenter-trace-context'
91
+ import { queryTags, withQueryTags } from '@prisma/sqlcommenter-query-tags'
92
+
93
+ const prisma = new PrismaClient({
94
+ adapter: myAdapter,
95
+ comments: [prismaQueryInsights(), traceContext(), queryTags()],
96
+ })
97
+
98
+ // All tags are merged into the SQL comment
99
+ await withQueryTags({ route: '/api/users' }, () => prisma.user.findMany())
100
+ ```
101
+
102
+ ## API
103
+
104
+ ### `prismaQueryInsights()`
105
+
106
+ Creates a SQL commenter plugin that adds query shape information.
107
+
108
+ ```typescript
109
+ function prismaQueryInsights(): SqlCommenterPlugin
110
+ ```
111
+
112
+ **Returns:** A `SqlCommenterPlugin` that adds the `prismaQuery` tag.
113
+
114
+ ## Technical Details
115
+
116
+ For integrators building observability tools that parse these comments, see the [Embedder Documentation](./docs/embedder-guide.md).
117
+
118
+ ## License
119
+
120
+ Apache-2.0
@@ -0,0 +1,15 @@
1
+ import type { SqlCommenterQueryInfo } from '@prisma/sqlcommenter';
2
+ /**
3
+ * Encodes data to base64url (URL-safe base64 without padding)
4
+ */
5
+ export declare function toBase64Url(data: string): string;
6
+ /**
7
+ * Formats query info into the compact prismaQuery format.
8
+ *
9
+ * Format: [modelName].action[:base64urlEncodedPayload]
10
+ *
11
+ * - Raw queries: just the action (e.g., "queryRaw", "executeRaw")
12
+ * - Single queries: Model.action:base64url(query)
13
+ * - Compacted batches: Model.action:base64url(queries)
14
+ */
15
+ export declare function formatQueryInsight(queryInfo: SqlCommenterQueryInfo): string;
@@ -0,0 +1,133 @@
1
+ declare type JsonQueryAction = 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'createOne' | 'createMany' | 'createManyAndReturn' | 'updateOne' | 'updateMany' | 'updateManyAndReturn' | 'deleteOne' | 'deleteMany' | 'upsertOne' | 'aggregate' | 'groupBy' | 'executeRaw' | 'queryRaw' | 'runCommandRaw' | 'findRaw' | 'aggregateRaw';
2
+
3
+ /**
4
+ * Creates a SQL commenter plugin that adds Prisma query shape information
5
+ * to SQL queries as `prismaQuery` comments.
6
+ *
7
+ * The query shapes are parameterized to remove user data, making them safe
8
+ * for logging and observability while still providing useful structural
9
+ * information about the queries being executed.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { prismaQueryInsights } from '@prisma/sqlcommenter-query-insights'
14
+ *
15
+ * const prisma = new PrismaClient({
16
+ * adapter: myAdapter,
17
+ * comments: [prismaQueryInsights()],
18
+ * })
19
+ * ```
20
+ *
21
+ * Output examples:
22
+ * - Raw query: `/*prismaQuery='queryRaw'* /`
23
+ * - Single query: `/*prismaQuery='User.findMany:eyJzZWxlY3Rpb24iOnsiJHNjYWxhcnMiOnRydWV9fX0'* /`
24
+ * - Batched queries: `/*prismaQuery='User.findUnique:W3siYXJndW1lbnRzIjp7IndoZXJlIjp7ImlkIjp7IiR0eXBlIjoiUGFyYW0ifX19fV0'* /`
25
+ */
26
+ export declare function prismaQueryInsights(): SqlCommenterPlugin;
27
+
28
+ /**
29
+ * Information about a compacted batch query (e.g. multiple independent
30
+ * `findUnique` queries automatically merged into a single `SELECT` SQL
31
+ * statement).
32
+ */
33
+ declare interface SqlCommenterCompactedQueryInfo {
34
+ /**
35
+ * The model name (e.g., "User", "Post").
36
+ */
37
+ readonly modelName: string;
38
+ /**
39
+ * The Prisma operation (e.g., "findUnique").
40
+ */
41
+ readonly action: SqlCommenterQueryAction;
42
+ /**
43
+ * The full query objects (selections, arguments, etc.).
44
+ * Specifics of the query representation are not part of the public API yet.
45
+ */
46
+ readonly queries: ReadonlyArray<unknown>;
47
+ }
48
+
49
+ /**
50
+ * Context provided to SQL commenter plugins.
51
+ */
52
+ declare interface SqlCommenterContext {
53
+ /**
54
+ * Information about the Prisma query being executed.
55
+ */
56
+ readonly query: SqlCommenterQueryInfo;
57
+ /**
58
+ * Raw SQL query generated from this Prisma query.
59
+ *
60
+ * It is always available when `PrismaClient` connects to the database and
61
+ * renders SQL queries directly.
62
+ *
63
+ * When using Prisma Accelerate, SQL rendering happens on Accelerate side and the raw
64
+ * SQL strings are not yet available when SQL commenter plugins are executed.
65
+ */
66
+ readonly sql?: string;
67
+ }
68
+
69
+ /**
70
+ * A SQL commenter plugin that returns key-value pairs to be added as comments.
71
+ * Return an empty object to add no comments. Keys with undefined values will be omitted.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const myPlugin: SqlCommenterPlugin = (context) => {
76
+ * return {
77
+ * application: 'my-app',
78
+ * model: context.query.modelName ?? 'raw',
79
+ * // Conditional key - will be omitted if ctx.sql is undefined
80
+ * sqlLength: context.sql ? String(context.sql.length) : undefined,
81
+ * }
82
+ * }
83
+ * ```
84
+ */
85
+ declare interface SqlCommenterPlugin {
86
+ (context: SqlCommenterContext): SqlCommenterTags;
87
+ }
88
+
89
+ /**
90
+ * Prisma query type corresponding to this SQL query.
91
+ */
92
+ declare type SqlCommenterQueryAction = JsonQueryAction;
93
+
94
+ /**
95
+ * Information about the query or queries being executed.
96
+ *
97
+ * - `single`: A single query is being executed
98
+ * - `compacted`: Multiple queries have been compacted into a single SQL statement
99
+ */
100
+ declare type SqlCommenterQueryInfo = ({
101
+ readonly type: 'single';
102
+ } & SqlCommenterSingleQueryInfo) | ({
103
+ readonly type: 'compacted';
104
+ } & SqlCommenterCompactedQueryInfo);
105
+
106
+ /**
107
+ * Information about a single Prisma query.
108
+ */
109
+ declare interface SqlCommenterSingleQueryInfo {
110
+ /**
111
+ * The model name (e.g., "User", "Post"). Undefined for raw queries.
112
+ */
113
+ readonly modelName?: string;
114
+ /**
115
+ * The Prisma operation (e.g., "findMany", "createOne", "queryRaw").
116
+ */
117
+ readonly action: SqlCommenterQueryAction;
118
+ /**
119
+ * The full query object (selection, arguments, etc.).
120
+ * Specifics of the query representation are not part of the public API yet.
121
+ */
122
+ readonly query: unknown;
123
+ }
124
+
125
+ /**
126
+ * Key-value pairs to add as SQL comments.
127
+ * Keys with undefined values will be omitted from the final comment.
128
+ */
129
+ declare type SqlCommenterTags = {
130
+ readonly [key: string]: string | undefined;
131
+ };
132
+
133
+ export { }
@@ -0,0 +1,133 @@
1
+ declare type JsonQueryAction = 'findUnique' | 'findUniqueOrThrow' | 'findFirst' | 'findFirstOrThrow' | 'findMany' | 'createOne' | 'createMany' | 'createManyAndReturn' | 'updateOne' | 'updateMany' | 'updateManyAndReturn' | 'deleteOne' | 'deleteMany' | 'upsertOne' | 'aggregate' | 'groupBy' | 'executeRaw' | 'queryRaw' | 'runCommandRaw' | 'findRaw' | 'aggregateRaw';
2
+
3
+ /**
4
+ * Creates a SQL commenter plugin that adds Prisma query shape information
5
+ * to SQL queries as `prismaQuery` comments.
6
+ *
7
+ * The query shapes are parameterized to remove user data, making them safe
8
+ * for logging and observability while still providing useful structural
9
+ * information about the queries being executed.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { prismaQueryInsights } from '@prisma/sqlcommenter-query-insights'
14
+ *
15
+ * const prisma = new PrismaClient({
16
+ * adapter: myAdapter,
17
+ * comments: [prismaQueryInsights()],
18
+ * })
19
+ * ```
20
+ *
21
+ * Output examples:
22
+ * - Raw query: `/*prismaQuery='queryRaw'* /`
23
+ * - Single query: `/*prismaQuery='User.findMany:eyJzZWxlY3Rpb24iOnsiJHNjYWxhcnMiOnRydWV9fX0'* /`
24
+ * - Batched queries: `/*prismaQuery='User.findUnique:W3siYXJndW1lbnRzIjp7IndoZXJlIjp7ImlkIjp7IiR0eXBlIjoiUGFyYW0ifX19fV0'* /`
25
+ */
26
+ export declare function prismaQueryInsights(): SqlCommenterPlugin;
27
+
28
+ /**
29
+ * Information about a compacted batch query (e.g. multiple independent
30
+ * `findUnique` queries automatically merged into a single `SELECT` SQL
31
+ * statement).
32
+ */
33
+ declare interface SqlCommenterCompactedQueryInfo {
34
+ /**
35
+ * The model name (e.g., "User", "Post").
36
+ */
37
+ readonly modelName: string;
38
+ /**
39
+ * The Prisma operation (e.g., "findUnique").
40
+ */
41
+ readonly action: SqlCommenterQueryAction;
42
+ /**
43
+ * The full query objects (selections, arguments, etc.).
44
+ * Specifics of the query representation are not part of the public API yet.
45
+ */
46
+ readonly queries: ReadonlyArray<unknown>;
47
+ }
48
+
49
+ /**
50
+ * Context provided to SQL commenter plugins.
51
+ */
52
+ declare interface SqlCommenterContext {
53
+ /**
54
+ * Information about the Prisma query being executed.
55
+ */
56
+ readonly query: SqlCommenterQueryInfo;
57
+ /**
58
+ * Raw SQL query generated from this Prisma query.
59
+ *
60
+ * It is always available when `PrismaClient` connects to the database and
61
+ * renders SQL queries directly.
62
+ *
63
+ * When using Prisma Accelerate, SQL rendering happens on Accelerate side and the raw
64
+ * SQL strings are not yet available when SQL commenter plugins are executed.
65
+ */
66
+ readonly sql?: string;
67
+ }
68
+
69
+ /**
70
+ * A SQL commenter plugin that returns key-value pairs to be added as comments.
71
+ * Return an empty object to add no comments. Keys with undefined values will be omitted.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const myPlugin: SqlCommenterPlugin = (context) => {
76
+ * return {
77
+ * application: 'my-app',
78
+ * model: context.query.modelName ?? 'raw',
79
+ * // Conditional key - will be omitted if ctx.sql is undefined
80
+ * sqlLength: context.sql ? String(context.sql.length) : undefined,
81
+ * }
82
+ * }
83
+ * ```
84
+ */
85
+ declare interface SqlCommenterPlugin {
86
+ (context: SqlCommenterContext): SqlCommenterTags;
87
+ }
88
+
89
+ /**
90
+ * Prisma query type corresponding to this SQL query.
91
+ */
92
+ declare type SqlCommenterQueryAction = JsonQueryAction;
93
+
94
+ /**
95
+ * Information about the query or queries being executed.
96
+ *
97
+ * - `single`: A single query is being executed
98
+ * - `compacted`: Multiple queries have been compacted into a single SQL statement
99
+ */
100
+ declare type SqlCommenterQueryInfo = ({
101
+ readonly type: 'single';
102
+ } & SqlCommenterSingleQueryInfo) | ({
103
+ readonly type: 'compacted';
104
+ } & SqlCommenterCompactedQueryInfo);
105
+
106
+ /**
107
+ * Information about a single Prisma query.
108
+ */
109
+ declare interface SqlCommenterSingleQueryInfo {
110
+ /**
111
+ * The model name (e.g., "User", "Post"). Undefined for raw queries.
112
+ */
113
+ readonly modelName?: string;
114
+ /**
115
+ * The Prisma operation (e.g., "findMany", "createOne", "queryRaw").
116
+ */
117
+ readonly action: SqlCommenterQueryAction;
118
+ /**
119
+ * The full query object (selection, arguments, etc.).
120
+ * Specifics of the query representation are not part of the public API yet.
121
+ */
122
+ readonly query: unknown;
123
+ }
124
+
125
+ /**
126
+ * Key-value pairs to add as SQL comments.
127
+ * Keys with undefined values will be omitted from the final comment.
128
+ */
129
+ declare type SqlCommenterTags = {
130
+ readonly [key: string]: string | undefined;
131
+ };
132
+
133
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,415 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ prismaQueryInsights: () => prismaQueryInsights
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/parameterize/parameterize.ts
28
+ var PARAM_PLACEHOLDER = { $type: "Param" };
29
+ function isTaggedValue(value) {
30
+ return typeof value === "object" && value !== null && "$type" in value && typeof value.$type === "string";
31
+ }
32
+ function isSelectionMarker(key) {
33
+ return key === "$scalars" || key === "$composites";
34
+ }
35
+ var STRUCTURE_KEYS = /* @__PURE__ */ new Set([
36
+ // Selection/field specifiers
37
+ "select",
38
+ "include",
39
+ "omit",
40
+ "selection",
41
+ // Relation operations
42
+ "connect",
43
+ "connectOrCreate",
44
+ "disconnect",
45
+ "set",
46
+ "create",
47
+ "createMany",
48
+ "update",
49
+ "updateMany",
50
+ "upsert",
51
+ "delete",
52
+ "deleteMany",
53
+ // Query structure
54
+ "where",
55
+ "data",
56
+ "orderBy",
57
+ "cursor",
58
+ "distinct",
59
+ "relationLoadStrategy",
60
+ // Aggregation
61
+ "_count",
62
+ "_sum",
63
+ "_avg",
64
+ "_min",
65
+ "_max"
66
+ ]);
67
+ var STRUCTURAL_VALUE_KEYS = /* @__PURE__ */ new Set([
68
+ // Sort directions
69
+ "sort",
70
+ // Null handling
71
+ "nulls",
72
+ // Relation load strategy
73
+ "relationLoadStrategy"
74
+ ]);
75
+ var NUMERIC_STRUCTURE_KEYS = /* @__PURE__ */ new Set(["take", "skip"]);
76
+ var FILTER_OPERATORS = /* @__PURE__ */ new Set([
77
+ "equals",
78
+ "not",
79
+ "in",
80
+ "notIn",
81
+ "lt",
82
+ "lte",
83
+ "gt",
84
+ "gte",
85
+ "contains",
86
+ "startsWith",
87
+ "endsWith",
88
+ "search",
89
+ "mode",
90
+ "has",
91
+ "hasEvery",
92
+ "hasSome",
93
+ "isEmpty",
94
+ // JSON operators
95
+ "path",
96
+ "string_contains",
97
+ "string_starts_with",
98
+ "string_ends_with",
99
+ "array_contains",
100
+ "array_starts_with",
101
+ "array_ends_with"
102
+ ]);
103
+ var LOGICAL_OPERATORS = /* @__PURE__ */ new Set(["AND", "OR", "NOT", "is", "isNot", "some", "every", "none"]);
104
+ var SORT_DIRECTIONS = /* @__PURE__ */ new Set(["asc", "desc"]);
105
+ function parameterizeValue(value, context) {
106
+ if (value === null || value === void 0) {
107
+ return value;
108
+ }
109
+ if (isTaggedValue(value)) {
110
+ if (value.$type === "FieldRef") {
111
+ return value;
112
+ }
113
+ if (value.$type === "Enum" && context.isStructuralEnum) {
114
+ return value;
115
+ }
116
+ return PARAM_PLACEHOLDER;
117
+ }
118
+ if (Array.isArray(value)) {
119
+ return value.map((item) => parameterizeValue(item, context));
120
+ }
121
+ if (typeof value === "object") {
122
+ return parameterizeObject(value, context);
123
+ }
124
+ if (context.isDataContext) {
125
+ return PARAM_PLACEHOLDER;
126
+ }
127
+ if (context.isSelectionContext && typeof value === "boolean") {
128
+ return value;
129
+ }
130
+ if (context.isStructuralValue) {
131
+ return value;
132
+ }
133
+ if (context.isOrderByContext && typeof value === "string" && SORT_DIRECTIONS.has(value)) {
134
+ return value;
135
+ }
136
+ return PARAM_PLACEHOLDER;
137
+ }
138
+ function parameterizeObject(obj, context) {
139
+ const result = {};
140
+ for (const [key, value] of Object.entries(obj)) {
141
+ if (isSelectionMarker(key)) {
142
+ result[key] = value;
143
+ continue;
144
+ }
145
+ const newContext = getContextForKey(key, context);
146
+ result[key] = parameterizeValue(value, newContext);
147
+ }
148
+ return result;
149
+ }
150
+ function isStructuralKey(key) {
151
+ return key === "arguments" || STRUCTURE_KEYS.has(key);
152
+ }
153
+ function getContextForKey(key, parentContext) {
154
+ if (key === "arguments") {
155
+ return {
156
+ isDataContext: false,
157
+ isStructuralValue: false,
158
+ isStructuralEnum: false,
159
+ isSelectionContext: false,
160
+ isOrderByContext: false,
161
+ parentKey: key
162
+ };
163
+ }
164
+ if (key === "selection" || parentContext.isSelectionContext && !isStructuralKey(key)) {
165
+ return {
166
+ isDataContext: false,
167
+ isStructuralValue: false,
168
+ isStructuralEnum: false,
169
+ isSelectionContext: true,
170
+ isOrderByContext: false,
171
+ parentKey: key
172
+ };
173
+ }
174
+ if (key === "data" || parentContext.isDataContext) {
175
+ if (STRUCTURE_KEYS.has(key)) {
176
+ return {
177
+ isDataContext: false,
178
+ isStructuralValue: false,
179
+ isStructuralEnum: false,
180
+ isSelectionContext: false,
181
+ isOrderByContext: false,
182
+ parentKey: key
183
+ };
184
+ }
185
+ return {
186
+ isDataContext: true,
187
+ isStructuralValue: false,
188
+ isStructuralEnum: false,
189
+ isSelectionContext: false,
190
+ isOrderByContext: false,
191
+ parentKey: key
192
+ };
193
+ }
194
+ if (STRUCTURAL_VALUE_KEYS.has(key)) {
195
+ return {
196
+ isDataContext: false,
197
+ isStructuralValue: true,
198
+ isStructuralEnum: true,
199
+ isSelectionContext: false,
200
+ isOrderByContext: false,
201
+ parentKey: key
202
+ };
203
+ }
204
+ if (NUMERIC_STRUCTURE_KEYS.has(key)) {
205
+ return {
206
+ isDataContext: false,
207
+ isStructuralValue: true,
208
+ isStructuralEnum: false,
209
+ isSelectionContext: false,
210
+ isOrderByContext: false,
211
+ parentKey: key
212
+ };
213
+ }
214
+ if (key === "orderBy" || parentContext.isOrderByContext) {
215
+ return {
216
+ isDataContext: false,
217
+ isStructuralValue: false,
218
+ isStructuralEnum: false,
219
+ isSelectionContext: false,
220
+ isOrderByContext: true,
221
+ parentKey: key
222
+ };
223
+ }
224
+ if (FILTER_OPERATORS.has(key)) {
225
+ if (key === "mode") {
226
+ return {
227
+ isDataContext: false,
228
+ isStructuralValue: true,
229
+ isStructuralEnum: true,
230
+ isSelectionContext: false,
231
+ isOrderByContext: false,
232
+ parentKey: key
233
+ };
234
+ }
235
+ return {
236
+ isDataContext: false,
237
+ isStructuralValue: false,
238
+ isStructuralEnum: false,
239
+ isSelectionContext: false,
240
+ isOrderByContext: false,
241
+ parentKey: key
242
+ };
243
+ }
244
+ if (LOGICAL_OPERATORS.has(key)) {
245
+ return {
246
+ isDataContext: false,
247
+ isStructuralValue: false,
248
+ isStructuralEnum: false,
249
+ isSelectionContext: false,
250
+ isOrderByContext: false,
251
+ parentKey: key
252
+ };
253
+ }
254
+ if (STRUCTURE_KEYS.has(key)) {
255
+ return {
256
+ isDataContext: false,
257
+ isStructuralValue: false,
258
+ isStructuralEnum: false,
259
+ isSelectionContext: key === "selection",
260
+ isOrderByContext: false,
261
+ parentKey: key
262
+ };
263
+ }
264
+ return {
265
+ isDataContext: parentContext.isDataContext,
266
+ isStructuralValue: false,
267
+ isStructuralEnum: false,
268
+ isSelectionContext: parentContext.isSelectionContext,
269
+ isOrderByContext: parentContext.isOrderByContext,
270
+ parentKey: key
271
+ };
272
+ }
273
+ function parameterizeQuery(query) {
274
+ const initialContext = {
275
+ isDataContext: false,
276
+ isStructuralValue: false,
277
+ isStructuralEnum: false,
278
+ isSelectionContext: false,
279
+ isOrderByContext: false
280
+ };
281
+ return parameterizeValue(query, initialContext);
282
+ }
283
+
284
+ // src/shape/shape.ts
285
+ function isNestedQuery(value) {
286
+ return typeof value === "object" && value !== null && ("arguments" in value || "selection" in value);
287
+ }
288
+ function isEmptyObject(value) {
289
+ return typeof value === "object" && value !== null && Object.keys(value).length === 0;
290
+ }
291
+ function canSimplifySelectionToTrue(selection) {
292
+ if (selection.$scalars !== true) {
293
+ return false;
294
+ }
295
+ for (const key of Object.keys(selection)) {
296
+ if (key !== "$scalars" && key !== "$composites") {
297
+ return false;
298
+ }
299
+ }
300
+ return true;
301
+ }
302
+ function canSimplifyNestedQueryToTrue(args, selection) {
303
+ const argsEmpty = args === void 0 || isEmptyObject(args);
304
+ if (!argsEmpty) {
305
+ return false;
306
+ }
307
+ if (!selection) {
308
+ return false;
309
+ }
310
+ return canSimplifySelectionToTrue(selection);
311
+ }
312
+ function shapeRelation(relation) {
313
+ if (relation === true) {
314
+ return true;
315
+ }
316
+ if (typeof relation !== "object" || relation === null) {
317
+ return relation;
318
+ }
319
+ const relationObj = relation;
320
+ if (isNestedQuery(relationObj)) {
321
+ const args = relationObj.arguments;
322
+ const selection = relationObj.selection;
323
+ if (canSimplifyNestedQueryToTrue(args, selection)) {
324
+ return true;
325
+ }
326
+ return shapeQuery(relationObj);
327
+ } else {
328
+ if (canSimplifySelectionToTrue(relationObj)) {
329
+ return true;
330
+ }
331
+ const shaped = shapeSelectionObject(relationObj);
332
+ return shaped || true;
333
+ }
334
+ }
335
+ function shapeSelectionObject(selection) {
336
+ const hasScalars = selection.$scalars === true;
337
+ const booleanFields = {};
338
+ const objectFields = {};
339
+ for (const [key, value] of Object.entries(selection)) {
340
+ if (key === "$scalars" || key === "$composites") {
341
+ continue;
342
+ }
343
+ if (typeof value === "boolean") {
344
+ booleanFields[key] = value;
345
+ } else if (typeof value === "object" && value !== null) {
346
+ objectFields[key] = value;
347
+ }
348
+ }
349
+ const hasBooleanFields = Object.keys(booleanFields).length > 0;
350
+ const hasObjectFields = Object.keys(objectFields).length > 0;
351
+ if (!hasBooleanFields && !hasObjectFields) {
352
+ return null;
353
+ }
354
+ const shapedObjects = {};
355
+ for (const [key, value] of Object.entries(objectFields)) {
356
+ shapedObjects[key] = shapeRelation(value);
357
+ }
358
+ if (hasScalars) {
359
+ if (!hasObjectFields) {
360
+ return null;
361
+ }
362
+ return { include: shapedObjects };
363
+ } else {
364
+ return { select: { ...booleanFields, ...shapedObjects } };
365
+ }
366
+ }
367
+ function shapeQuery(query) {
368
+ if (typeof query !== "object" || query === null) {
369
+ return query;
370
+ }
371
+ const queryObj = query;
372
+ const args = queryObj.arguments;
373
+ const selection = queryObj.selection;
374
+ const result = args ? { ...args } : {};
375
+ if (selection) {
376
+ const shaped = shapeSelectionObject(selection);
377
+ if (shaped) {
378
+ Object.assign(result, shaped);
379
+ }
380
+ }
381
+ return result;
382
+ }
383
+
384
+ // src/format.ts
385
+ function toBase64Url(data) {
386
+ return Buffer.from(data, "utf-8").toString("base64url");
387
+ }
388
+ function formatQueryInsight(queryInfo) {
389
+ if (queryInfo.type === "single") {
390
+ const { modelName: modelName2, action: action2, query } = queryInfo;
391
+ if (!modelName2) {
392
+ return action2;
393
+ }
394
+ const parameterizedQuery = parameterizeQuery(query);
395
+ const shapedQuery = shapeQuery(parameterizedQuery);
396
+ const encoded2 = toBase64Url(JSON.stringify(shapedQuery));
397
+ return `${modelName2}.${action2}:${encoded2}`;
398
+ }
399
+ const { modelName, action, queries } = queryInfo;
400
+ const shapedQueries = queries.map((q) => shapeQuery(parameterizeQuery(q)));
401
+ const encoded = toBase64Url(JSON.stringify(shapedQueries));
402
+ return `${modelName}.${action}:${encoded}`;
403
+ }
404
+
405
+ // src/index.ts
406
+ function prismaQueryInsights() {
407
+ return (context) => {
408
+ const insight = formatQueryInsight(context.query);
409
+ return { prismaQuery: insight };
410
+ };
411
+ }
412
+ // Annotate the CommonJS export names for ESM import in node:
413
+ 0 && (module.exports = {
414
+ prismaQueryInsights
415
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,388 @@
1
+ // src/parameterize/parameterize.ts
2
+ var PARAM_PLACEHOLDER = { $type: "Param" };
3
+ function isTaggedValue(value) {
4
+ return typeof value === "object" && value !== null && "$type" in value && typeof value.$type === "string";
5
+ }
6
+ function isSelectionMarker(key) {
7
+ return key === "$scalars" || key === "$composites";
8
+ }
9
+ var STRUCTURE_KEYS = /* @__PURE__ */ new Set([
10
+ // Selection/field specifiers
11
+ "select",
12
+ "include",
13
+ "omit",
14
+ "selection",
15
+ // Relation operations
16
+ "connect",
17
+ "connectOrCreate",
18
+ "disconnect",
19
+ "set",
20
+ "create",
21
+ "createMany",
22
+ "update",
23
+ "updateMany",
24
+ "upsert",
25
+ "delete",
26
+ "deleteMany",
27
+ // Query structure
28
+ "where",
29
+ "data",
30
+ "orderBy",
31
+ "cursor",
32
+ "distinct",
33
+ "relationLoadStrategy",
34
+ // Aggregation
35
+ "_count",
36
+ "_sum",
37
+ "_avg",
38
+ "_min",
39
+ "_max"
40
+ ]);
41
+ var STRUCTURAL_VALUE_KEYS = /* @__PURE__ */ new Set([
42
+ // Sort directions
43
+ "sort",
44
+ // Null handling
45
+ "nulls",
46
+ // Relation load strategy
47
+ "relationLoadStrategy"
48
+ ]);
49
+ var NUMERIC_STRUCTURE_KEYS = /* @__PURE__ */ new Set(["take", "skip"]);
50
+ var FILTER_OPERATORS = /* @__PURE__ */ new Set([
51
+ "equals",
52
+ "not",
53
+ "in",
54
+ "notIn",
55
+ "lt",
56
+ "lte",
57
+ "gt",
58
+ "gte",
59
+ "contains",
60
+ "startsWith",
61
+ "endsWith",
62
+ "search",
63
+ "mode",
64
+ "has",
65
+ "hasEvery",
66
+ "hasSome",
67
+ "isEmpty",
68
+ // JSON operators
69
+ "path",
70
+ "string_contains",
71
+ "string_starts_with",
72
+ "string_ends_with",
73
+ "array_contains",
74
+ "array_starts_with",
75
+ "array_ends_with"
76
+ ]);
77
+ var LOGICAL_OPERATORS = /* @__PURE__ */ new Set(["AND", "OR", "NOT", "is", "isNot", "some", "every", "none"]);
78
+ var SORT_DIRECTIONS = /* @__PURE__ */ new Set(["asc", "desc"]);
79
+ function parameterizeValue(value, context) {
80
+ if (value === null || value === void 0) {
81
+ return value;
82
+ }
83
+ if (isTaggedValue(value)) {
84
+ if (value.$type === "FieldRef") {
85
+ return value;
86
+ }
87
+ if (value.$type === "Enum" && context.isStructuralEnum) {
88
+ return value;
89
+ }
90
+ return PARAM_PLACEHOLDER;
91
+ }
92
+ if (Array.isArray(value)) {
93
+ return value.map((item) => parameterizeValue(item, context));
94
+ }
95
+ if (typeof value === "object") {
96
+ return parameterizeObject(value, context);
97
+ }
98
+ if (context.isDataContext) {
99
+ return PARAM_PLACEHOLDER;
100
+ }
101
+ if (context.isSelectionContext && typeof value === "boolean") {
102
+ return value;
103
+ }
104
+ if (context.isStructuralValue) {
105
+ return value;
106
+ }
107
+ if (context.isOrderByContext && typeof value === "string" && SORT_DIRECTIONS.has(value)) {
108
+ return value;
109
+ }
110
+ return PARAM_PLACEHOLDER;
111
+ }
112
+ function parameterizeObject(obj, context) {
113
+ const result = {};
114
+ for (const [key, value] of Object.entries(obj)) {
115
+ if (isSelectionMarker(key)) {
116
+ result[key] = value;
117
+ continue;
118
+ }
119
+ const newContext = getContextForKey(key, context);
120
+ result[key] = parameterizeValue(value, newContext);
121
+ }
122
+ return result;
123
+ }
124
+ function isStructuralKey(key) {
125
+ return key === "arguments" || STRUCTURE_KEYS.has(key);
126
+ }
127
+ function getContextForKey(key, parentContext) {
128
+ if (key === "arguments") {
129
+ return {
130
+ isDataContext: false,
131
+ isStructuralValue: false,
132
+ isStructuralEnum: false,
133
+ isSelectionContext: false,
134
+ isOrderByContext: false,
135
+ parentKey: key
136
+ };
137
+ }
138
+ if (key === "selection" || parentContext.isSelectionContext && !isStructuralKey(key)) {
139
+ return {
140
+ isDataContext: false,
141
+ isStructuralValue: false,
142
+ isStructuralEnum: false,
143
+ isSelectionContext: true,
144
+ isOrderByContext: false,
145
+ parentKey: key
146
+ };
147
+ }
148
+ if (key === "data" || parentContext.isDataContext) {
149
+ if (STRUCTURE_KEYS.has(key)) {
150
+ return {
151
+ isDataContext: false,
152
+ isStructuralValue: false,
153
+ isStructuralEnum: false,
154
+ isSelectionContext: false,
155
+ isOrderByContext: false,
156
+ parentKey: key
157
+ };
158
+ }
159
+ return {
160
+ isDataContext: true,
161
+ isStructuralValue: false,
162
+ isStructuralEnum: false,
163
+ isSelectionContext: false,
164
+ isOrderByContext: false,
165
+ parentKey: key
166
+ };
167
+ }
168
+ if (STRUCTURAL_VALUE_KEYS.has(key)) {
169
+ return {
170
+ isDataContext: false,
171
+ isStructuralValue: true,
172
+ isStructuralEnum: true,
173
+ isSelectionContext: false,
174
+ isOrderByContext: false,
175
+ parentKey: key
176
+ };
177
+ }
178
+ if (NUMERIC_STRUCTURE_KEYS.has(key)) {
179
+ return {
180
+ isDataContext: false,
181
+ isStructuralValue: true,
182
+ isStructuralEnum: false,
183
+ isSelectionContext: false,
184
+ isOrderByContext: false,
185
+ parentKey: key
186
+ };
187
+ }
188
+ if (key === "orderBy" || parentContext.isOrderByContext) {
189
+ return {
190
+ isDataContext: false,
191
+ isStructuralValue: false,
192
+ isStructuralEnum: false,
193
+ isSelectionContext: false,
194
+ isOrderByContext: true,
195
+ parentKey: key
196
+ };
197
+ }
198
+ if (FILTER_OPERATORS.has(key)) {
199
+ if (key === "mode") {
200
+ return {
201
+ isDataContext: false,
202
+ isStructuralValue: true,
203
+ isStructuralEnum: true,
204
+ isSelectionContext: false,
205
+ isOrderByContext: false,
206
+ parentKey: key
207
+ };
208
+ }
209
+ return {
210
+ isDataContext: false,
211
+ isStructuralValue: false,
212
+ isStructuralEnum: false,
213
+ isSelectionContext: false,
214
+ isOrderByContext: false,
215
+ parentKey: key
216
+ };
217
+ }
218
+ if (LOGICAL_OPERATORS.has(key)) {
219
+ return {
220
+ isDataContext: false,
221
+ isStructuralValue: false,
222
+ isStructuralEnum: false,
223
+ isSelectionContext: false,
224
+ isOrderByContext: false,
225
+ parentKey: key
226
+ };
227
+ }
228
+ if (STRUCTURE_KEYS.has(key)) {
229
+ return {
230
+ isDataContext: false,
231
+ isStructuralValue: false,
232
+ isStructuralEnum: false,
233
+ isSelectionContext: key === "selection",
234
+ isOrderByContext: false,
235
+ parentKey: key
236
+ };
237
+ }
238
+ return {
239
+ isDataContext: parentContext.isDataContext,
240
+ isStructuralValue: false,
241
+ isStructuralEnum: false,
242
+ isSelectionContext: parentContext.isSelectionContext,
243
+ isOrderByContext: parentContext.isOrderByContext,
244
+ parentKey: key
245
+ };
246
+ }
247
+ function parameterizeQuery(query) {
248
+ const initialContext = {
249
+ isDataContext: false,
250
+ isStructuralValue: false,
251
+ isStructuralEnum: false,
252
+ isSelectionContext: false,
253
+ isOrderByContext: false
254
+ };
255
+ return parameterizeValue(query, initialContext);
256
+ }
257
+
258
+ // src/shape/shape.ts
259
+ function isNestedQuery(value) {
260
+ return typeof value === "object" && value !== null && ("arguments" in value || "selection" in value);
261
+ }
262
+ function isEmptyObject(value) {
263
+ return typeof value === "object" && value !== null && Object.keys(value).length === 0;
264
+ }
265
+ function canSimplifySelectionToTrue(selection) {
266
+ if (selection.$scalars !== true) {
267
+ return false;
268
+ }
269
+ for (const key of Object.keys(selection)) {
270
+ if (key !== "$scalars" && key !== "$composites") {
271
+ return false;
272
+ }
273
+ }
274
+ return true;
275
+ }
276
+ function canSimplifyNestedQueryToTrue(args, selection) {
277
+ const argsEmpty = args === void 0 || isEmptyObject(args);
278
+ if (!argsEmpty) {
279
+ return false;
280
+ }
281
+ if (!selection) {
282
+ return false;
283
+ }
284
+ return canSimplifySelectionToTrue(selection);
285
+ }
286
+ function shapeRelation(relation) {
287
+ if (relation === true) {
288
+ return true;
289
+ }
290
+ if (typeof relation !== "object" || relation === null) {
291
+ return relation;
292
+ }
293
+ const relationObj = relation;
294
+ if (isNestedQuery(relationObj)) {
295
+ const args = relationObj.arguments;
296
+ const selection = relationObj.selection;
297
+ if (canSimplifyNestedQueryToTrue(args, selection)) {
298
+ return true;
299
+ }
300
+ return shapeQuery(relationObj);
301
+ } else {
302
+ if (canSimplifySelectionToTrue(relationObj)) {
303
+ return true;
304
+ }
305
+ const shaped = shapeSelectionObject(relationObj);
306
+ return shaped || true;
307
+ }
308
+ }
309
+ function shapeSelectionObject(selection) {
310
+ const hasScalars = selection.$scalars === true;
311
+ const booleanFields = {};
312
+ const objectFields = {};
313
+ for (const [key, value] of Object.entries(selection)) {
314
+ if (key === "$scalars" || key === "$composites") {
315
+ continue;
316
+ }
317
+ if (typeof value === "boolean") {
318
+ booleanFields[key] = value;
319
+ } else if (typeof value === "object" && value !== null) {
320
+ objectFields[key] = value;
321
+ }
322
+ }
323
+ const hasBooleanFields = Object.keys(booleanFields).length > 0;
324
+ const hasObjectFields = Object.keys(objectFields).length > 0;
325
+ if (!hasBooleanFields && !hasObjectFields) {
326
+ return null;
327
+ }
328
+ const shapedObjects = {};
329
+ for (const [key, value] of Object.entries(objectFields)) {
330
+ shapedObjects[key] = shapeRelation(value);
331
+ }
332
+ if (hasScalars) {
333
+ if (!hasObjectFields) {
334
+ return null;
335
+ }
336
+ return { include: shapedObjects };
337
+ } else {
338
+ return { select: { ...booleanFields, ...shapedObjects } };
339
+ }
340
+ }
341
+ function shapeQuery(query) {
342
+ if (typeof query !== "object" || query === null) {
343
+ return query;
344
+ }
345
+ const queryObj = query;
346
+ const args = queryObj.arguments;
347
+ const selection = queryObj.selection;
348
+ const result = args ? { ...args } : {};
349
+ if (selection) {
350
+ const shaped = shapeSelectionObject(selection);
351
+ if (shaped) {
352
+ Object.assign(result, shaped);
353
+ }
354
+ }
355
+ return result;
356
+ }
357
+
358
+ // src/format.ts
359
+ function toBase64Url(data) {
360
+ return Buffer.from(data, "utf-8").toString("base64url");
361
+ }
362
+ function formatQueryInsight(queryInfo) {
363
+ if (queryInfo.type === "single") {
364
+ const { modelName: modelName2, action: action2, query } = queryInfo;
365
+ if (!modelName2) {
366
+ return action2;
367
+ }
368
+ const parameterizedQuery = parameterizeQuery(query);
369
+ const shapedQuery = shapeQuery(parameterizedQuery);
370
+ const encoded2 = toBase64Url(JSON.stringify(shapedQuery));
371
+ return `${modelName2}.${action2}:${encoded2}`;
372
+ }
373
+ const { modelName, action, queries } = queryInfo;
374
+ const shapedQueries = queries.map((q) => shapeQuery(parameterizeQuery(q)));
375
+ const encoded = toBase64Url(JSON.stringify(shapedQueries));
376
+ return `${modelName}.${action}:${encoded}`;
377
+ }
378
+
379
+ // src/index.ts
380
+ function prismaQueryInsights() {
381
+ return (context) => {
382
+ const insight = formatQueryInsight(context.query);
383
+ return { prismaQuery: insight };
384
+ };
385
+ }
386
+ export {
387
+ prismaQueryInsights
388
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Query parameterization logic for Prisma query insights.
3
+ *
4
+ * This module handles the conversion of Prisma queries into parameterized shapes
5
+ * where user data values are replaced with placeholder markers.
6
+ */
7
+ /**
8
+ * Placeholder object used to replace parameterized values in query shapes.
9
+ * In the future, this may include additional fields like `value` containing
10
+ * param name and/or param type when queries are parameterized by the query compiler.
11
+ */
12
+ export declare const PARAM_PLACEHOLDER: {
13
+ $type: string;
14
+ };
15
+ /**
16
+ * Parameterizes a query object, replacing all user data values with placeholders
17
+ */
18
+ export declare function parameterizeQuery(query: unknown): unknown;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Query parameterization logic for Prisma query insights.
3
+ *
4
+ * This module handles the conversion of Prisma queries into parameterized shapes
5
+ * where user data values are replaced with placeholder markers.
6
+ */
7
+ /**
8
+ * Placeholder object used to replace parameterized values in query shapes.
9
+ * In the future, this may include additional fields like `value` containing
10
+ * param name and/or param type when queries are parameterized by the query compiler.
11
+ */
12
+ export declare const PARAM_PLACEHOLDER: {
13
+ $type: string;
14
+ };
15
+ /**
16
+ * Parameterizes a query object, replacing all user data values with placeholders
17
+ */
18
+ export declare function parameterizeQuery(query: unknown): unknown;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Query shaping logic for Prisma query insights.
3
+ *
4
+ * This module transforms JSON protocol query representations into a format
5
+ * that more closely resembles the original Prisma query API, making the
6
+ * encoded query shapes more intuitive to read and understand.
7
+ *
8
+ * Key transformations:
9
+ * - Move `arguments` contents one level up
10
+ * - Convert `selection` to `select`/`include`:
11
+ * - If `$scalars: true`, use `include` for relations
12
+ * - If no `$scalars`, use `select` for both scalars and relations
13
+ * - Simplify relation selections to `true` when possible
14
+ */
15
+ /**
16
+ * Shapes a query object from JSON protocol format to Prisma-like format.
17
+ *
18
+ * Transforms:
19
+ * - `{ arguments: {...}, selection: {...} }` → `{ ..., select/include: {...} }`
20
+ *
21
+ * @param query - The query object in JSON protocol format
22
+ * @returns The shaped query in Prisma-like format
23
+ */
24
+ export declare function shapeQuery(query: unknown): unknown;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@prisma/sqlcommenter-query-insights",
3
+ "version": "0.0.1",
4
+ "description": "Query insights plugin for Prisma SQL commenter",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "require": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "import": {
15
+ "types": "./dist/index.d.mts",
16
+ "default": "./dist/index.mjs"
17
+ }
18
+ }
19
+ },
20
+ "license": "Apache-2.0",
21
+ "homepage": "https://www.prisma.io",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/prisma/prisma.git",
25
+ "directory": "packages/sqlcommenter-query-insights"
26
+ },
27
+ "bugs": "https://github.com/prisma/prisma/issues",
28
+ "scripts": {
29
+ "dev": "DEV=true tsx helpers/build.ts",
30
+ "build": "tsx helpers/build.ts",
31
+ "prepublishOnly": "pnpm run build",
32
+ "test": "vitest run"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "sideEffects": false,
38
+ "devDependencies": {
39
+ "@prisma/sqlcommenter": "workspace:*",
40
+ "fast-check": "4.3.0"
41
+ }
42
+ }